all repos — flounder @ dd35f98744376c783879f01907e1da6e4172b6fa

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