all repos — flounder @ bae10cb7517fdbe207275c9c03e5a4245ee588de

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