all repos — flounder @ 7579aa3a3803c65dffbe9d8481d49128359bdbf0

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