all repos — flounder @ c7d2aca283929b5d18a22074b0542ca7cf4f622a

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