all repos — flounder @ 5cdcf08e2ddf5f8fc488a52de2349ddbe133139e

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