all repos — flounder @ 2db17c0358b3c441a1bf395e3fba9c8a4880e7d2

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		MyUser   *User
287		Errors   []string
288	}
289	data := pageData{c, user, me, 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		newDomain := r.Form.Get("domain")
302		newUsername = strings.ToLower(newUsername)
303		var err error
304		_, exists := domains[newDomain]
305		if newDomain != me.Domain && !exists {
306			_, err = DB.Exec("update user set domain = ? where username = ?", newDomain, me.Username) // TODO use transaction
307			if err != nil {
308				errors = append(errors, err.Error())
309			} else {
310				refreshDomainMap()
311				log.Printf("Changed domain for %s from %s to %s", authUser, me.Domain, newDomain)
312			}
313		}
314		if newEmail != me.Email {
315			_, err = DB.Exec("update user set email = ? where username = ?", newEmail, me.Username)
316			if err != nil {
317				// TODO better error not sql
318				errors = append(errors, err.Error())
319			} else {
320				log.Printf("Changed email for %s from %s to %s", authUser, me.Email, newEmail)
321			}
322		}
323		if newUsername != authUser {
324			// Rename User
325			err = renameUser(authUser, newUsername)
326			if err != nil {
327				log.Println(err)
328				errors = append(errors, "Could not rename user")
329			} else {
330				session, _ := SessionStore.Get(r, "cookie-session")
331				session.Values["auth_user"] = newUsername
332				session.Save(r, w)
333			}
334		}
335		// reset auth
336		user = newGetAuthUser(r)
337		data.Errors = errors
338		data.AuthUser = user
339		data.MyUser.Email = newEmail
340		data.MyUser.Domain = newDomain
341		_ = t.ExecuteTemplate(w, "me.html", data)
342	}
343}
344
345func archiveHandler(w http.ResponseWriter, r *http.Request) {
346	authUser := newGetAuthUser(r)
347	if !authUser.LoggedIn {
348		renderDefaultError(w, http.StatusForbidden)
349		return
350	}
351	if r.Method == "GET" {
352		userFolder := getUserDirectory(authUser.Username)
353		err := zipit(userFolder, w)
354		if err != nil {
355			panic(err)
356		}
357
358	}
359}
360func loginHandler(w http.ResponseWriter, r *http.Request) {
361	if r.Method == "GET" {
362		// show page
363		data := struct {
364			Error  string
365			Config Config
366		}{"", c}
367		err := t.ExecuteTemplate(w, "login.html", data)
368		if err != nil {
369			panic(err)
370		}
371	} else if r.Method == "POST" {
372		r.ParseForm()
373		name := strings.ToLower(r.Form.Get("username"))
374		password := r.Form.Get("password")
375		row := DB.QueryRow("SELECT username, password_hash, active, admin FROM user where username = $1 OR email = $1", name)
376		var db_password []byte
377		var username string
378		var active bool
379		var isAdmin bool
380		err := row.Scan(&username, &db_password, &active, &isAdmin)
381		if err != nil {
382			if strings.Contains(err.Error(), "no rows") {
383				data := struct {
384					Error  string
385					Config Config
386				}{"Username or email '" + name + "' does not exist", c}
387				t.ExecuteTemplate(w, "login.html", data)
388				return
389			} else {
390				panic(err)
391			}
392		}
393		if db_password != nil && !active {
394			data := struct {
395				Error  string
396				Config Config
397			}{"Your account is not active yet. Pending admin approval", c}
398			t.ExecuteTemplate(w, "login.html", data)
399			return
400		}
401		if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil {
402			log.Println("logged in")
403			session, _ := SessionStore.Get(r, "cookie-session")
404			session.Values["auth_user"] = username
405			session.Values["admin"] = isAdmin
406			session.Save(r, w)
407			http.Redirect(w, r, "/my_site", http.StatusSeeOther)
408		} else {
409			data := struct {
410				Error  string
411				Config Config
412			}{"Invalid login or password", c}
413			err := t.ExecuteTemplate(w, "login.html", data)
414			if err != nil {
415				panic(err)
416			}
417		}
418	}
419}
420
421func logoutHandler(w http.ResponseWriter, r *http.Request) {
422	session, _ := SessionStore.Get(r, "cookie-session")
423	impers, ok := session.Values["impersonating_user"].(string)
424	if ok {
425		session.Values["auth_user"] = impers
426		session.Values["impersonating_user"] = nil // TODO expire this automatically
427		// session.Values["admin"] = nil // TODO fix admin
428	} else {
429		session.Options.MaxAge = -1
430	}
431	session.Save(r, w)
432	http.Redirect(w, r, "/", http.StatusSeeOther)
433}
434
435const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
436
437var bannedUsernames = []string{"www", "proxy", "grafana"}
438
439func isOkUsername(s string) error {
440	if len(s) < 1 {
441		return fmt.Errorf("Username is too short")
442	}
443	if len(s) > 32 {
444		return fmt.Errorf("Username is too long. 32 char max.")
445	}
446	for _, char := range s {
447		if !strings.Contains(ok, strings.ToLower(string(char))) {
448			return fmt.Errorf("Username contains invalid characters. Valid characters include lowercase letters, numbers, and hyphens.")
449		}
450	}
451	for _, username := range bannedUsernames {
452		if username == s {
453			return fmt.Errorf("Username is not allowed.")
454		}
455	}
456	return nil
457}
458
459func registerHandler(w http.ResponseWriter, r *http.Request) {
460	if r.Method == "GET" {
461		data := struct {
462			Errors []string
463			Config Config
464		}{nil, c}
465		err := t.ExecuteTemplate(w, "register.html", data)
466		if err != nil {
467			panic(err)
468		}
469	} else if r.Method == "POST" {
470		r.ParseForm()
471		email := strings.ToLower(r.Form.Get("email"))
472		password := r.Form.Get("password")
473		errors := []string{}
474		if r.Form.Get("password") != r.Form.Get("password2") {
475			errors = append(errors, "Passwords don't match")
476		}
477		if len(password) < 6 {
478			errors = append(errors, "Password is too short")
479		}
480		username := strings.ToLower(r.Form.Get("username"))
481		err := isOkUsername(username)
482		if err != nil {
483			errors = append(errors, err.Error())
484		}
485		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error
486		if err != nil {
487			panic(err)
488		}
489		reference := r.Form.Get("reference")
490		if len(errors) == 0 {
491			_, err = DB.Exec("insert into user (username, email, password_hash, reference) values ($1, $2, $3, $4)", username, email, string(hashedPassword), reference)
492			if err != nil {
493				errors = append(errors, "Username or email is already used")
494			}
495		}
496		if len(errors) > 0 {
497			data := struct {
498				Config Config
499				Errors []string
500			}{c, errors}
501			t.ExecuteTemplate(w, "register.html", data)
502		} else {
503			data := struct {
504				Config  Config
505				Message string
506				Title   string
507			}{c, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
508			t.ExecuteTemplate(w, "message.html", data)
509		}
510	}
511}
512
513func deleteFileHandler(w http.ResponseWriter, r *http.Request) {
514	user := newGetAuthUser(r)
515	if !user.LoggedIn {
516		renderDefaultError(w, http.StatusForbidden)
517		return
518	}
519	filePath := safeGetFilePath(user.Username, r.URL.Path[len("/delete/"):])
520	if r.Method == "POST" {
521		os.Remove(filePath) // TODO handle error
522	}
523	http.Redirect(w, r, "/my_site", http.StatusSeeOther)
524}
525
526func adminHandler(w http.ResponseWriter, r *http.Request) {
527	user := newGetAuthUser(r)
528	if !user.IsAdmin {
529		renderDefaultError(w, http.StatusForbidden)
530		return
531	}
532	allUsers, err := getUsers()
533	if err != nil {
534		log.Println(err)
535		renderDefaultError(w, http.StatusInternalServerError)
536		return
537	}
538	data := struct {
539		Users    []User
540		AuthUser AuthUser
541		Config   Config
542	}{allUsers, user, c}
543	err = t.ExecuteTemplate(w, "admin.html", data)
544	if err != nil {
545		panic(err)
546	}
547}
548
549func getFavicon(user string) string {
550	faviconPath := path.Join(c.FilesDirectory, filepath.Clean(user), "favicon.txt")
551	content, err := ioutil.ReadFile(faviconPath)
552	if err != nil {
553		return ""
554	}
555	strcontent := []rune(string(content))
556	if len(strcontent) > 0 {
557		return string(strcontent[0])
558	}
559	return ""
560}
561
562// Server a user's file
563// TODO replace with gemini proxy
564// Here be dragons
565func userFile(w http.ResponseWriter, r *http.Request) {
566	var userName string
567	custom := domains[r.Host]
568	if custom != "" {
569		userName = custom
570	} else {
571		userName = filepath.Clean(strings.Split(r.Host, ".")[0]) // Clean probably unnecessary
572	}
573	p := filepath.Clean(r.URL.Path)
574	var isDir bool
575	fullPath := path.Join(c.FilesDirectory, userName, p) // TODO rename filepath
576	stat, err := os.Stat(fullPath)
577	if stat != nil {
578		isDir = stat.IsDir()
579	}
580	if strings.HasSuffix(p, "index.gmi") {
581		http.Redirect(w, r, path.Dir(p), http.StatusMovedPermanently)
582	}
583
584	if strings.HasPrefix(p, "/"+HiddenFolder) {
585		renderDefaultError(w, http.StatusForbidden)
586		return
587	}
588	if r.URL.Path == "/gemlog/atom.xml" && os.IsNotExist(err) {
589		w.Header().Set("Content-Type", "application/atom+xml")
590		// TODO set always somehow
591		feed := generateFeedFromUser(userName)
592		atomString := feed.toAtomFeed()
593		io.Copy(w, strings.NewReader(atomString))
594		return
595	}
596
597	var geminiContent string
598	_, err = os.Stat(path.Join(fullPath, "index.gmi"))
599	if isDir {
600		// redirect slash
601		if !strings.HasSuffix(r.URL.Path, "/") {
602			http.Redirect(w, r, p+"/", http.StatusSeeOther)
603		}
604		if os.IsNotExist(err) {
605			if p == "/gemlog" {
606				geminiContent = generateGemfeedPage(userName)
607			} else {
608				geminiContent = generateFolderPage(fullPath)
609			}
610		} else {
611			fullPath = path.Join(fullPath, "index.gmi")
612		}
613	}
614	if geminiContent == "" && os.IsNotExist(err) {
615		renderDefaultError(w, http.StatusNotFound)
616		return
617	}
618	// Dumb content negotiation
619	_, raw := r.URL.Query()["raw"]
620	acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini")
621	if !raw && !acceptsGemini && (isGemini(fullPath) || geminiContent != "") {
622		var htmlString string
623		if geminiContent == "" {
624			file, _ := os.Open(fullPath)
625			htmlString = textToHTML(nil, gmi.ParseText(file))
626			defer file.Close()
627		} else {
628			htmlString = textToHTML(nil, gmi.ParseText(strings.NewReader(geminiContent)))
629		}
630		favicon := getFavicon(userName)
631		hostname := strings.Split(r.Host, ":")[0]
632		uri := url.URL{
633			Scheme: "gemini",
634			Host:   hostname,
635			Path:   p,
636		}
637		data := struct {
638			SiteBody  template.HTML
639			Favicon   string
640			PageTitle string
641			URI       *url.URL
642		}{template.HTML(htmlString), favicon, userName + p, &uri}
643		err = t.ExecuteTemplate(w, "user_page.html", data)
644		if err != nil {
645			panic(err)
646		}
647	} else {
648		http.ServeFile(w, r, fullPath)
649	}
650}
651
652func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
653	user := newGetAuthUser(r)
654	if r.Method == "POST" {
655		r.ParseForm()
656		validate := r.Form.Get("validate-delete")
657		if validate == user.Username {
658			err := deleteUser(user.Username)
659			if err != nil {
660				log.Println(err)
661				renderDefaultError(w, http.StatusInternalServerError)
662				return
663			}
664			logoutHandler(w, r)
665		} else {
666			http.Redirect(w, r, "/me", http.StatusSeeOther)
667		}
668	}
669}
670
671func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
672	user := newGetAuthUser(r)
673	data := struct {
674		Config   Config
675		AuthUser AuthUser
676		Error    string
677	}{c, user, ""}
678	if r.Method == "GET" {
679		err := t.ExecuteTemplate(w, "reset_pass.html", data)
680		if err != nil {
681			panic(err)
682		}
683	} else if r.Method == "POST" {
684		r.ParseForm()
685		enteredCurrPass := r.Form.Get("password")
686		password1 := r.Form.Get("new_password1")
687		password2 := r.Form.Get("new_password2")
688		if password1 != password2 {
689			data.Error = "New passwords do not match"
690		} else if len(password1) < 6 {
691			data.Error = "Password is too short"
692		} else {
693			err := checkAuth(user.Username, enteredCurrPass)
694			if err == nil {
695				hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password1), 8)
696				if err != nil {
697					panic(err)
698				}
699				_, err = DB.Exec("update user set password_hash = ? where username = ?", hashedPassword, user.Username)
700				if err != nil {
701					panic(err)
702				}
703				log.Printf("User %s reset password", user.Username)
704				http.Redirect(w, r, "/me", http.StatusSeeOther)
705				return
706			} else {
707				data.Error = "That's not your current password"
708			}
709		}
710		err := t.ExecuteTemplate(w, "reset_pass.html", data)
711		if err != nil {
712			panic(err)
713		}
714	}
715}
716
717func adminUserHandler(w http.ResponseWriter, r *http.Request) {
718	user := newGetAuthUser(r)
719	if r.Method == "POST" {
720		if !user.IsAdmin {
721			renderDefaultError(w, http.StatusForbidden)
722			return
723		}
724		components := strings.Split(r.URL.Path, "/")
725		if len(components) < 5 {
726			renderError(w, "Invalid action", http.StatusBadRequest)
727			return
728		}
729		userName := components[3]
730		action := components[4]
731		var err error
732		if action == "activate" {
733			err = activateUser(userName)
734		} else if action == "impersonate" {
735			if user.ImpersonatingUser != "" {
736				// Don't allow nested impersonation
737				renderError(w, "Cannot nest impersonation, log out from impersonated user first.", 400)
738				return
739			}
740			session, _ := SessionStore.Get(r, "cookie-session")
741			session.Values["auth_user"] = userName
742			session.Values["impersonating_user"] = user.Username
743			session.Save(r, w)
744			log.Printf("User %s impersonated %s", user.Username, userName)
745			http.Redirect(w, r, "/", http.StatusSeeOther)
746			return
747		}
748		if err != nil {
749			log.Println(err)
750			renderDefaultError(w, http.StatusInternalServerError)
751			return
752		}
753		http.Redirect(w, r, "/admin", http.StatusSeeOther)
754	}
755}
756
757func checkDomainHandler(w http.ResponseWriter, r *http.Request) {
758	domain := r.URL.Query().Get("domain")
759	if domain != "" && domains[domain] != "" {
760		w.Write([]byte(domain))
761		return
762	}
763	http.Error(w, "Not Found", 404)
764}
765func runHTTPServer() {
766	log.Printf("Running http server with hostname %s on port %d.", c.Host, c.HttpPort)
767	var err error
768	t = template.New("main").Funcs(template.FuncMap{"parent": path.Dir})
769	t, err = t.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
770	if err != nil {
771		log.Fatal(err)
772	}
773	serveMux := http.NewServeMux()
774
775	s := strings.SplitN(c.Host, ":", 2)
776	hostname := s[0]
777	port := c.HttpPort
778
779	serveMux.HandleFunc(hostname+"/", rootHandler)
780	serveMux.HandleFunc(hostname+"/feed", feedHandler)
781	serveMux.HandleFunc(hostname+"/my_site", mySiteHandler)
782	serveMux.HandleFunc(hostname+"/me", myAccountHandler)
783	serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler)
784	serveMux.HandleFunc(hostname+"/admin", adminHandler)
785	serveMux.HandleFunc(hostname+"/edit/", editFileHandler)
786	serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler)
787	serveMux.Handle(hostname+"/login", limit(http.HandlerFunc(loginHandler)))
788	serveMux.Handle(hostname+"/register", limit(http.HandlerFunc(registerHandler)))
789	serveMux.HandleFunc(hostname+"/logout", logoutHandler)
790	serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler)
791	serveMux.HandleFunc(hostname+"/delete-account", deleteAccountHandler)
792	serveMux.HandleFunc(hostname+"/reset-password", resetPasswordHandler)
793
794	// Check domain -- used by caddy
795	serveMux.HandleFunc(hostname+"/check-domain", checkDomainHandler)
796
797	// admin commands
798	serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler)
799
800	serveMux.HandleFunc(hostname+"/webdav/", webdavHandler)
801
802	wrapped := handlers.CustomLoggingHandler(log.Writer(), handlers.RecoveryHandler()(serveMux), logFormatter)
803
804	// handle user files based on subdomain
805	// also routes to proxy
806	serveMux.HandleFunc("proxy."+hostname+"/", proxyGemini) // eg. proxy.flounder.online
807	serveMux.HandleFunc("/", userFile)
808	// login+register functions
809	srv := &http.Server{
810		ReadTimeout:  5 * time.Second,
811		WriteTimeout: 10 * time.Second,
812		IdleTimeout:  120 * time.Second,
813		Addr:         fmt.Sprintf(":%d", port),
814		// TLSConfig:    tlsConfig,
815		Handler: wrapped,
816	}
817	log.Fatal(srv.ListenAndServe())
818}