all repos — flounder @ 8930d5d065a0a16cbd358acd761fc2367dc71f46

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