all repos — flounder @ 5c25dd5f433771a06e2d3f88173e23bfcdecaf48

A small site builder for the Gemini protocol

http.go (view raw)

  1package main
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	gmi "git.sr.ht/~adnano/go-gemini"
  7	"github.com/gorilla/handlers"
  8	"github.com/gorilla/sessions"
  9	_ "github.com/mattn/go-sqlite3"
 10	"golang.org/x/crypto/bcrypt"
 11	"html/template"
 12	"io"
 13	"io/ioutil"
 14	"log"
 15	"net/http"
 16	"net/url"
 17	"os"
 18	"path"
 19	"path/filepath"
 20	"strings"
 21	"time"
 22)
 23
 24var t *template.Template
 25var SessionStore *sessions.CookieStore
 26
 27func renderDefaultError(w http.ResponseWriter, statusCode int) {
 28	errorMsg := http.StatusText(statusCode)
 29	renderError(w, errorMsg, statusCode)
 30}
 31
 32func renderError(w http.ResponseWriter, errorMsg string, statusCode int) {
 33	data := struct {
 34		StatusCode int
 35		ErrorMsg   string
 36		Config     Config
 37	}{statusCode, errorMsg, c}
 38	err := t.ExecuteTemplate(w, "error.html", data)
 39	if err != nil { // Shouldn't happen probably
 40		http.Error(w, errorMsg, statusCode)
 41	}
 42}
 43
 44func rootHandler(w http.ResponseWriter, r *http.Request) {
 45	// serve everything inside static directory
 46	if r.URL.Path != "/" {
 47		fileName := path.Join(c.TemplatesDirectory, "static", filepath.Clean(r.URL.Path))
 48		_, err := os.Stat(fileName)
 49		if err != nil {
 50			renderDefaultError(w, http.StatusNotFound)
 51			return
 52		}
 53		http.ServeFile(w, r, fileName) // TODO better error handling
 54		return
 55	}
 56
 57	user := getAuthUser(r)
 58	indexFiles, err := getIndexFiles(user.IsAdmin)
 59	if err != nil {
 60		panic(err)
 61	}
 62	allUsers, err := getActiveUserNames()
 63	if err != nil {
 64		panic(err)
 65	}
 66	data := struct {
 67		Config   Config
 68		AuthUser AuthUser
 69		Files    []*File
 70		Users    []string
 71	}{c, user, indexFiles, allUsers}
 72	err = t.ExecuteTemplate(w, "index.html", data)
 73	if err != nil {
 74		panic(err)
 75	}
 76}
 77
 78func editFileHandler(w http.ResponseWriter, r *http.Request) {
 79	user := getAuthUser(r)
 80	if !user.LoggedIn {
 81		renderDefaultError(w, http.StatusForbidden)
 82		return
 83	}
 84	fileName := filepath.Clean(r.URL.Path[len("/edit/"):])
 85	filePath := path.Join(c.FilesDirectory, user.Username, fileName)
 86	isText := isTextFile(filePath)
 87	alert := ""
 88	var warnings []string
 89	if r.Method == "POST" {
 90		// get post body
 91		alert = "saved"
 92		r.ParseForm()
 93		fileText := r.Form.Get("file_text")
 94		// Web form by default gives us CR LF newlines.
 95		// Unix files use just LF
 96		fileText = strings.ReplaceAll(fileText, "\r\n", "\n")
 97		fileBytes := []byte(fileText)
 98		err := checkIfValidFile(user.Username, filePath, fileBytes)
 99		if err != nil {
100			log.Println(err)
101			renderError(w, err.Error(), http.StatusBadRequest)
102			return
103		}
104		sfl := getSchemedFlounderLinkLines(strings.NewReader(fileText))
105		if len(sfl) > 0 {
106			warnings = append(warnings, "Warning! Some of your links to pages use schemas. This means that they may break when viewed in Gemini or over HTTPS. Plase remove gemini: or https: from the start of these links:\n")
107			for _, l := range sfl {
108				warnings = append(warnings, l)
109			}
110		}
111		// create directories if dne
112		os.MkdirAll(path.Dir(filePath), os.ModePerm)
113		newName := filepath.Clean(r.Form.Get("rename"))
114		err = checkIfValidFile(user.Username, newName, fileBytes)
115		if err != nil {
116			log.Println(err)
117			renderError(w, err.Error(), http.StatusBadRequest)
118			return
119		}
120		if isText { // Cant edit binary files here
121			err = ioutil.WriteFile(filePath, fileBytes, 0644)
122			if err != nil {
123				log.Println(err)
124				renderError(w, err.Error(), http.StatusBadRequest)
125			}
126		}
127		if newName != fileName {
128			newPath := path.Join(c.FilesDirectory, user.Username, newName)
129			os.MkdirAll(path.Dir(newPath), os.ModePerm)
130			os.Rename(filePath, newPath)
131			fileName = newName
132			filePath = newPath
133			alert += " and renamed"
134		}
135	}
136
137	err := checkIfValidFile(user.Username, filePath, nil)
138	if err != nil {
139		log.Println(err)
140		renderError(w, err.Error(), http.StatusBadRequest)
141		return
142	}
143	// Create directories if dne
144	f, err := os.OpenFile(filePath, os.O_RDONLY, 0644)
145	var fileBytes []byte
146	if os.IsNotExist(err) || !isText {
147		fileBytes = []byte{}
148		err = nil
149	} else {
150		defer f.Close()
151		fileBytes, err = ioutil.ReadAll(f)
152	}
153	if err != nil {
154		panic(err)
155	}
156	data := struct {
157		FileName string
158		FileText string
159		Config   Config
160		AuthUser AuthUser
161		Host     string
162		IsText   bool
163		IsGemini bool
164		IsGemlog bool
165		Alert    string
166		Warnings []string
167	}{fileName, string(fileBytes), c, user, c.Host, isText, isGemini(fileName), strings.HasPrefix(fileName, "gemlog"), alert, warnings}
168	err = t.ExecuteTemplate(w, "edit_file.html", data)
169	if err != nil {
170		panic(err)
171	}
172}
173
174func uploadFilesHandler(w http.ResponseWriter, r *http.Request) {
175	if r.Method == "POST" {
176		user := getAuthUser(r)
177		if !user.LoggedIn {
178			renderDefaultError(w, http.StatusForbidden)
179			return
180		}
181		r.ParseMultipartForm(10 << 6) // why does this not work
182		file, fileHeader, err := r.FormFile("file")
183		if err != nil {
184			log.Println(err)
185			renderError(w, "No file selected. Please go back and select a file.", http.StatusBadRequest)
186			return
187		}
188		fileName := filepath.Clean(fileHeader.Filename)
189		defer file.Close()
190		dest, _ := ioutil.ReadAll(file)
191		err = checkIfValidFile(user.Username, fileName, dest)
192		if err != nil {
193			log.Println(err)
194			renderError(w, err.Error(), http.StatusBadRequest)
195			return
196		}
197		destPath := path.Join(c.FilesDirectory, user.Username, fileName)
198
199		f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, 0644)
200		if err != nil {
201			panic(err)
202		}
203		defer f.Close()
204		io.Copy(f, bytes.NewReader(dest))
205	}
206	http.Redirect(w, r, "/my_site", http.StatusSeeOther)
207}
208
209type AuthUser struct {
210	LoggedIn          bool
211	Username          string
212	IsAdmin           bool
213	ImpersonatingUser string // used if impersonating
214}
215
216func getAuthUser(r *http.Request) AuthUser {
217	session, _ := SessionStore.Get(r, "cookie-session")
218	user, ok := session.Values["auth_user"].(string)
219	impers, _ := session.Values["impersonating_user"].(string)
220	isAdmin, _ := session.Values["admin"].(bool)
221	return AuthUser{
222		LoggedIn:          ok,
223		Username:          user,
224		IsAdmin:           isAdmin,
225		ImpersonatingUser: impers,
226	}
227}
228
229func mySiteHandler(w http.ResponseWriter, r *http.Request) {
230	user := getAuthUser(r)
231	if !user.LoggedIn {
232		renderDefaultError(w, http.StatusForbidden)
233		return
234	}
235	// check auth
236	userFolder := getUserDirectory(user.Username)
237	files, _ := getMyFilesRecursive(userFolder, user.Username)
238	currentDate := time.Now().Format("2006-01-02")
239	data := struct {
240		Config      Config
241		Files       []File
242		AuthUser    AuthUser
243		CurrentDate string
244	}{c, files, user, currentDate}
245	_ = t.ExecuteTemplate(w, "my_site.html", data)
246}
247
248func myAccountHandler(w http.ResponseWriter, r *http.Request) {
249	user := getAuthUser(r)
250	authUser := user.Username
251	if !user.LoggedIn {
252		renderDefaultError(w, http.StatusForbidden)
253		return
254	}
255	me, _ := getUserByName(user.Username)
256	type pageData struct {
257		Config   Config
258		AuthUser AuthUser
259		MyUser   *User
260		Errors   []string
261	}
262	data := pageData{c, user, me, nil}
263
264	if r.Method == "GET" {
265		err := t.ExecuteTemplate(w, "me.html", data)
266		if err != nil {
267			panic(err)
268		}
269	} else if r.Method == "POST" {
270		r.ParseForm()
271		newUsername := r.Form.Get("username")
272		errors := []string{}
273		newEmail := r.Form.Get("email")
274		newDomain := r.Form.Get("domain")
275		newUsername = strings.ToLower(newUsername)
276		var err error
277		_, exists := domains[newDomain]
278		if newDomain != me.Domain && !exists {
279			_, err = DB.Exec("update user set domain = ? where username = ?", newDomain, me.Username) // TODO use transaction
280			if err != nil {
281				errors = append(errors, err.Error())
282			} else {
283				refreshDomainMap()
284				log.Printf("Changed domain for %s from %s to %s", authUser, me.Domain, newDomain)
285			}
286		}
287		if newEmail != me.Email {
288			_, err = DB.Exec("update user set email = ? where username = ?", newEmail, me.Username)
289			if err != nil {
290				// TODO better error not sql
291				errors = append(errors, err.Error())
292			} else {
293				log.Printf("Changed email for %s from %s to %s", authUser, me.Email, newEmail)
294			}
295		}
296		if newUsername != authUser {
297			// Rename User
298			err = renameUser(authUser, newUsername)
299			if err != nil {
300				log.Println(err)
301				errors = append(errors, "Could not rename user")
302			} else {
303				session, _ := SessionStore.Get(r, "cookie-session")
304				session.Values["auth_user"] = newUsername
305				session.Save(r, w)
306			}
307		}
308		// reset auth
309		user = getAuthUser(r)
310		data.Errors = errors
311		data.AuthUser = user
312		data.MyUser.Email = newEmail
313		data.MyUser.Domain = newDomain
314		_ = t.ExecuteTemplate(w, "me.html", data)
315	}
316}
317
318func archiveHandler(w http.ResponseWriter, r *http.Request) {
319	authUser := getAuthUser(r)
320	if !authUser.LoggedIn {
321		renderDefaultError(w, http.StatusForbidden)
322		return
323	}
324	if r.Method == "GET" {
325		userFolder := getUserDirectory(authUser.Username)
326		err := zipit(userFolder, w)
327		if err != nil {
328			panic(err)
329		}
330
331	}
332}
333func loginHandler(w http.ResponseWriter, r *http.Request) {
334	if r.Method == "GET" {
335		// show page
336		data := struct {
337			Error  string
338			Config Config
339		}{"", c}
340		err := t.ExecuteTemplate(w, "login.html", data)
341		if err != nil {
342			panic(err)
343		}
344	} else if r.Method == "POST" {
345		r.ParseForm()
346		name := strings.ToLower(r.Form.Get("username"))
347		password := r.Form.Get("password")
348		row := DB.QueryRow("SELECT username, password_hash, active, admin FROM user where username = $1 OR email = $1", name)
349		var db_password []byte
350		var username string
351		var active bool
352		var isAdmin bool
353		err := row.Scan(&username, &db_password, &active, &isAdmin)
354		if err != nil {
355			if strings.Contains(err.Error(), "no rows") {
356				data := struct {
357					Error  string
358					Config Config
359				}{"Username or email '" + name + "' does not exist", c}
360				t.ExecuteTemplate(w, "login.html", data)
361				return
362			} else {
363				panic(err)
364			}
365		}
366		if db_password != nil && !active {
367			data := struct {
368				Error  string
369				Config Config
370			}{"Your account is not active yet. Pending admin approval", c}
371			t.ExecuteTemplate(w, "login.html", data)
372			return
373		}
374		if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil {
375			log.Println("logged in")
376			session, _ := SessionStore.Get(r, "cookie-session")
377			session.Values["auth_user"] = username
378			session.Values["admin"] = isAdmin
379			session.Save(r, w)
380			http.Redirect(w, r, "/my_site", http.StatusSeeOther)
381		} else {
382			data := struct {
383				Error  string
384				Config Config
385			}{"Invalid login or password", c}
386			err := t.ExecuteTemplate(w, "login.html", data)
387			if err != nil {
388				panic(err)
389			}
390		}
391	}
392}
393
394func logoutHandler(w http.ResponseWriter, r *http.Request) {
395	session, _ := SessionStore.Get(r, "cookie-session")
396	impers, ok := session.Values["impersonating_user"].(string)
397	if ok {
398		session.Values["auth_user"] = impers
399		session.Values["impersonating_user"] = nil // TODO expire this automatically
400		// session.Values["admin"] = nil // TODO fix admin
401	} else {
402		session.Options.MaxAge = -1
403	}
404	session.Save(r, w)
405	http.Redirect(w, r, "/", http.StatusSeeOther)
406}
407
408const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
409
410var bannedUsernames = []string{"www", "proxy", "grafana"}
411
412func registerHandler(w http.ResponseWriter, r *http.Request) {
413	if r.Method == "GET" {
414		data := struct {
415			Errors []string
416			Config Config
417		}{nil, c}
418		err := t.ExecuteTemplate(w, "register.html", data)
419		if err != nil {
420			panic(err)
421		}
422	} else if r.Method == "POST" {
423		r.ParseForm()
424		email := strings.ToLower(r.Form.Get("email"))
425		password := r.Form.Get("password")
426		errors := []string{}
427		if r.Form.Get("password") != r.Form.Get("password2") {
428			errors = append(errors, "Passwords don't match")
429		}
430		if len(password) < 6 {
431			errors = append(errors, "Password is too short")
432		}
433		username := strings.ToLower(r.Form.Get("username"))
434		err := isOkUsername(username)
435		if err != nil {
436			errors = append(errors, err.Error())
437		}
438		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error
439		if err != nil {
440			panic(err)
441		}
442		reference := r.Form.Get("reference")
443		if len(errors) == 0 {
444			_, err = DB.Exec("insert into user (username, email, password_hash, reference) values ($1, $2, $3, $4)", username, email, string(hashedPassword), reference)
445			if err != nil {
446				errors = append(errors, "Username or email is already used")
447			}
448		}
449		if len(errors) > 0 {
450			data := struct {
451				Config Config
452				Errors []string
453			}{c, errors}
454			t.ExecuteTemplate(w, "register.html", data)
455		} else {
456			data := struct {
457				Config  Config
458				Message string
459				Title   string
460			}{c, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
461			t.ExecuteTemplate(w, "message.html", data)
462		}
463	}
464}
465
466func deleteFileHandler(w http.ResponseWriter, r *http.Request) {
467	user := getAuthUser(r)
468	if !user.LoggedIn {
469		renderDefaultError(w, http.StatusForbidden)
470		return
471	}
472	filePath := safeGetFilePath(user.Username, r.URL.Path[len("/delete/"):])
473	if r.Method == "POST" {
474		os.Remove(filePath) // TODO handle error
475	}
476	http.Redirect(w, r, "/my_site", http.StatusSeeOther)
477}
478
479func adminHandler(w http.ResponseWriter, r *http.Request) {
480	user := getAuthUser(r)
481	if !user.IsAdmin {
482		renderDefaultError(w, http.StatusForbidden)
483		return
484	}
485	allUsers, err := getUsers()
486	if err != nil {
487		log.Println(err)
488		renderDefaultError(w, http.StatusInternalServerError)
489		return
490	}
491	data := struct {
492		Users    []User
493		AuthUser AuthUser
494		Config   Config
495	}{allUsers, user, c}
496	err = t.ExecuteTemplate(w, "admin.html", data)
497	if err != nil {
498		panic(err)
499	}
500}
501
502func getFavicon(user string) string {
503	faviconPath := path.Join(c.FilesDirectory, filepath.Clean(user), "favicon.txt")
504	content, err := ioutil.ReadFile(faviconPath)
505	if err != nil {
506		return ""
507	}
508	strcontent := []rune(string(content))
509	if len(strcontent) > 0 {
510		return string(strcontent[0])
511	}
512	return ""
513}
514
515// Server a user's file
516// TODO replace with gemini proxy
517// Here be dragons
518func userFile(w http.ResponseWriter, r *http.Request) {
519	var userName string
520	custom := domains[r.Host]
521	if custom != "" {
522		userName = custom
523	} else {
524		userName = filepath.Clean(strings.Split(r.Host, ".")[0]) // Clean probably unnecessary
525	}
526	p := filepath.Clean(r.URL.Path)
527	var isDir bool
528	fullPath := path.Join(c.FilesDirectory, userName, p) // TODO rename filepath
529	stat, err := os.Stat(fullPath)
530	if stat != nil {
531		isDir = stat.IsDir()
532	}
533	if strings.HasSuffix(p, "index.gmi") {
534		http.Redirect(w, r, path.Dir(p), http.StatusMovedPermanently)
535	}
536
537	if strings.HasPrefix(p, "/"+HiddenFolder) {
538		renderDefaultError(w, http.StatusForbidden)
539		return
540	}
541	if r.URL.Path == "/gemlog/atom.xml" && os.IsNotExist(err) {
542		w.Header().Set("Content-Type", "application/atom+xml")
543		// TODO set always somehow
544		feed := generateFeedFromUser(userName)
545		atomString := feed.toAtomFeed()
546		io.Copy(w, strings.NewReader(atomString))
547		return
548	}
549
550	var geminiContent string
551	_, err = os.Stat(path.Join(fullPath, "index.gmi"))
552	if isDir {
553		// redirect slash
554		if !strings.HasSuffix(r.URL.Path, "/") {
555			http.Redirect(w, r, p+"/", http.StatusSeeOther)
556		}
557		if os.IsNotExist(err) {
558			if p == "/gemlog" {
559				geminiContent = generateGemfeedPage(userName)
560			} else {
561				geminiContent = generateFolderPage(fullPath)
562			}
563		} else {
564			fullPath = path.Join(fullPath, "index.gmi")
565		}
566	}
567	if geminiContent == "" && os.IsNotExist(err) {
568		renderDefaultError(w, http.StatusNotFound)
569		return
570	}
571	// Dumb content negotiation
572	_, raw := r.URL.Query()["raw"]
573	acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini")
574	if !raw && !acceptsGemini && (isGemini(fullPath) || geminiContent != "") {
575		var htmlDoc ConvertedGmiDoc
576		if geminiContent == "" {
577			file, _ := os.Open(fullPath)
578			parse, _ := gmi.ParseText(file)
579			htmlDoc = textToHTML(nil, parse)
580			defer file.Close()
581		} else {
582			parse, _ := gmi.ParseText(strings.NewReader(geminiContent))
583			htmlDoc = textToHTML(nil, parse)
584		}
585		favicon := getFavicon(userName)
586		hostname := strings.Split(r.Host, ":")[0]
587		uri := url.URL{
588			Scheme: "gemini",
589			Host:   hostname,
590			Path:   p,
591		}
592		if htmlDoc.Title == "" {
593			htmlDoc.Title = userName + p
594		}
595		data := struct {
596			SiteBody  template.HTML
597			Favicon   string
598			PageTitle string
599			URI       *url.URL
600			GeminiURI *url.URL
601			Config    Config
602		}{template.HTML(htmlDoc.Content), favicon, htmlDoc.Title, &uri, &uri, c}
603		err = t.ExecuteTemplate(w, "user_page.html", data)
604		if err != nil {
605			panic(err)
606		}
607	} else {
608		http.ServeFile(w, r, fullPath)
609	}
610}
611
612func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
613	user := getAuthUser(r)
614	if r.Method == "POST" {
615		r.ParseForm()
616		validate := r.Form.Get("validate-delete")
617		if validate == user.Username {
618			err := deleteUser(user.Username)
619			if err != nil {
620				log.Println(err)
621				renderDefaultError(w, http.StatusInternalServerError)
622				return
623			}
624			logoutHandler(w, r)
625		} else {
626			http.Redirect(w, r, "/me", http.StatusSeeOther)
627		}
628	}
629}
630
631func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
632	user := getAuthUser(r)
633	data := struct {
634		Config   Config
635		AuthUser AuthUser
636		Error    string
637	}{c, user, ""}
638	if r.Method == "GET" {
639		err := t.ExecuteTemplate(w, "reset_pass.html", data)
640		if err != nil {
641			panic(err)
642		}
643	} else if r.Method == "POST" {
644		r.ParseForm()
645		enteredCurrPass := r.Form.Get("password")
646		password1 := r.Form.Get("new_password1")
647		password2 := r.Form.Get("new_password2")
648		if password1 != password2 {
649			data.Error = "New passwords do not match"
650		} else if len(password1) < 6 {
651			data.Error = "Password is too short"
652		} else {
653			err := checkAuth(user.Username, enteredCurrPass)
654			if err == nil {
655				hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password1), 8)
656				if err != nil {
657					panic(err)
658				}
659				_, err = DB.Exec("update user set password_hash = ? where username = ?", hashedPassword, user.Username)
660				if err != nil {
661					panic(err)
662				}
663				log.Printf("User %s reset password", user.Username)
664				http.Redirect(w, r, "/me", http.StatusSeeOther)
665				return
666			} else {
667				data.Error = "That's not your current password"
668			}
669		}
670		err := t.ExecuteTemplate(w, "reset_pass.html", data)
671		if err != nil {
672			panic(err)
673		}
674	}
675}
676
677func adminUserHandler(w http.ResponseWriter, r *http.Request) {
678	user := getAuthUser(r)
679	if r.Method == "POST" {
680		if !user.IsAdmin {
681			renderDefaultError(w, http.StatusForbidden)
682			return
683		}
684		components := strings.Split(r.URL.Path, "/")
685		if len(components) < 5 {
686			renderError(w, "Invalid action", http.StatusBadRequest)
687			return
688		}
689		userName := components[3]
690		action := components[4]
691		var err error
692		if action == "activate" {
693			err = activateUser(userName)
694		} else if action == "impersonate" {
695			if user.ImpersonatingUser != "" {
696				// Don't allow nested impersonation
697				renderError(w, "Cannot nest impersonation, log out from impersonated user first.", 400)
698				return
699			}
700			session, _ := SessionStore.Get(r, "cookie-session")
701			session.Values["auth_user"] = userName
702			session.Values["impersonating_user"] = user.Username
703			session.Save(r, w)
704			log.Printf("User %s impersonated %s", user.Username, userName)
705			http.Redirect(w, r, "/", http.StatusSeeOther)
706			return
707		}
708		if err != nil {
709			log.Println(err)
710			renderDefaultError(w, http.StatusInternalServerError)
711			return
712		}
713		http.Redirect(w, r, "/admin", http.StatusSeeOther)
714	}
715}
716
717func checkDomainHandler(w http.ResponseWriter, r *http.Request) {
718	domain := r.URL.Query().Get("domain")
719	if domain != "" && domains[domain] != "" {
720		w.Write([]byte(domain))
721		return
722	}
723	if domain == c.Host || strings.HasSuffix(domain, "."+c.Host) {
724		w.Write([]byte(domain))
725	}
726	http.Error(w, "Not Found", 404)
727}
728
729func runHTTPServer() {
730	log.Printf("Running http server with hostname %s on port %d.", c.Host, c.HttpPort)
731	var err error
732	t = template.New("main").Funcs(template.FuncMap{
733		"unixTime": time.Unix,
734		"parent":   path.Dir, "hasSuffix": strings.HasSuffix,
735		"safeGeminiURL": func(u string) template.URL {
736			if strings.HasPrefix(u, "gemini://") {
737				return template.URL(u)
738			}
739			return ""
740		}})
741	t, err = t.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
742	if err != nil {
743		log.Fatal(err)
744	}
745	serveMux := http.NewServeMux()
746
747	s := strings.SplitN(c.Host, ":", 2)
748	hostname := s[0]
749
750	serveMux.HandleFunc(hostname+"/", rootHandler)
751	serveMux.HandleFunc(hostname+"/my_site", mySiteHandler)
752	serveMux.HandleFunc(hostname+"/me", myAccountHandler)
753	serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler)
754	serveMux.HandleFunc(hostname+"/admin", adminHandler)
755	serveMux.HandleFunc(hostname+"/edit/", editFileHandler)
756	serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler)
757	serveMux.Handle(hostname+"/login", limit(http.HandlerFunc(loginHandler)))
758	serveMux.Handle(hostname+"/register", limit(http.HandlerFunc(registerHandler)))
759	serveMux.HandleFunc(hostname+"/logout", logoutHandler)
760	serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler)
761	serveMux.HandleFunc(hostname+"/delete-account", deleteAccountHandler)
762	serveMux.HandleFunc(hostname+"/reset-password", resetPasswordHandler)
763
764	// Used by Caddy
765	serveMux.HandleFunc(hostname+"/check-domain", checkDomainHandler)
766
767	// admin commands
768	serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler)
769
770	wrapped := handlers.CustomLoggingHandler(log.Writer(), handlers.RecoveryHandler()(serveMux), logFormatter)
771
772	// handle user files based on subdomain or custom domains
773	// also routes to proxy
774	serveMux.HandleFunc("proxy."+hostname+"/", proxyGemini)
775	serveMux.HandleFunc("/", userFile)
776	srv := &http.Server{
777		ReadTimeout:  5 * time.Second,
778		WriteTimeout: 10 * time.Second,
779		IdleTimeout:  120 * time.Second,
780		Addr:         fmt.Sprintf(":%d", c.HttpPort),
781		Handler:      wrapped,
782	}
783	log.Fatal(srv.ListenAndServe())
784}