all repos — flounder @ 733e1efda10eabb94a821981d75c1acaf47ca1da

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