all repos — flounder @ 646872f120abe310670252db919aaca54c7d17aa

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			htmlString = textToHTML(nil, gmi.ParseText(file))
607			defer file.Close()
608		} else {
609			htmlString = textToHTML(nil, gmi.ParseText(strings.NewReader(geminiContent)))
610		}
611		favicon := getFavicon(userName)
612		hostname := strings.Split(r.Host, ":")[0]
613		uri := url.URL{
614			Scheme: "gemini",
615			Host:   hostname,
616			Path:   p,
617		}
618		data := struct {
619			SiteBody  template.HTML
620			Favicon   string
621			PageTitle string
622			URI       *url.URL
623			Config    Config
624		}{template.HTML(htmlString), favicon, userName + p, &uri, c}
625		err = t.ExecuteTemplate(w, "user_page.html", data)
626		if err != nil {
627			panic(err)
628		}
629	} else {
630		http.ServeFile(w, r, fullPath)
631	}
632}
633
634func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
635	user := newGetAuthUser(r)
636	if r.Method == "POST" {
637		r.ParseForm()
638		validate := r.Form.Get("validate-delete")
639		if validate == user.Username {
640			err := deleteUser(user.Username)
641			if err != nil {
642				log.Println(err)
643				renderDefaultError(w, http.StatusInternalServerError)
644				return
645			}
646			logoutHandler(w, r)
647		} else {
648			http.Redirect(w, r, "/me", http.StatusSeeOther)
649		}
650	}
651}
652
653func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
654	user := newGetAuthUser(r)
655	data := struct {
656		Config   Config
657		AuthUser AuthUser
658		Error    string
659	}{c, user, ""}
660	if r.Method == "GET" {
661		err := t.ExecuteTemplate(w, "reset_pass.html", data)
662		if err != nil {
663			panic(err)
664		}
665	} else if r.Method == "POST" {
666		r.ParseForm()
667		enteredCurrPass := r.Form.Get("password")
668		password1 := r.Form.Get("new_password1")
669		password2 := r.Form.Get("new_password2")
670		if password1 != password2 {
671			data.Error = "New passwords do not match"
672		} else if len(password1) < 6 {
673			data.Error = "Password is too short"
674		} else {
675			err := checkAuth(user.Username, enteredCurrPass)
676			if err == nil {
677				hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password1), 8)
678				if err != nil {
679					panic(err)
680				}
681				_, err = DB.Exec("update user set password_hash = ? where username = ?", hashedPassword, user.Username)
682				if err != nil {
683					panic(err)
684				}
685				log.Printf("User %s reset password", user.Username)
686				http.Redirect(w, r, "/me", http.StatusSeeOther)
687				return
688			} else {
689				data.Error = "That's not your current password"
690			}
691		}
692		err := t.ExecuteTemplate(w, "reset_pass.html", data)
693		if err != nil {
694			panic(err)
695		}
696	}
697}
698
699func adminUserHandler(w http.ResponseWriter, r *http.Request) {
700	user := newGetAuthUser(r)
701	if r.Method == "POST" {
702		if !user.IsAdmin {
703			renderDefaultError(w, http.StatusForbidden)
704			return
705		}
706		components := strings.Split(r.URL.Path, "/")
707		if len(components) < 5 {
708			renderError(w, "Invalid action", http.StatusBadRequest)
709			return
710		}
711		userName := components[3]
712		action := components[4]
713		var err error
714		if action == "activate" {
715			err = activateUser(userName)
716		} else if action == "impersonate" {
717			if user.ImpersonatingUser != "" {
718				// Don't allow nested impersonation
719				renderError(w, "Cannot nest impersonation, log out from impersonated user first.", 400)
720				return
721			}
722			session, _ := SessionStore.Get(r, "cookie-session")
723			session.Values["auth_user"] = userName
724			session.Values["impersonating_user"] = user.Username
725			session.Save(r, w)
726			log.Printf("User %s impersonated %s", user.Username, userName)
727			http.Redirect(w, r, "/", http.StatusSeeOther)
728			return
729		}
730		if err != nil {
731			log.Println(err)
732			renderDefaultError(w, http.StatusInternalServerError)
733			return
734		}
735		http.Redirect(w, r, "/admin", http.StatusSeeOther)
736	}
737}
738
739func convertTextHandler(w http.ResponseWriter, r *http.Request) {
740	text := ""
741	var err error
742	if r.Method == "GET" {
743		text = ""
744	} else if r.Method == "POST" {
745		r.ParseForm()
746		fmt.Println(r.Form.Get("file_text"))
747		ctx := html2gemini.NewTraverseContext(html2gemini.Options{})
748		text, err = html2gemini.FromString(r.Form.Get("file_text"), *ctx)
749		fmt.Println(text)
750		if err != nil {
751			panic(err)
752		}
753		// TODO handle error?
754	}
755	data := struct {
756		Config Config
757		Text   string
758	}{c, text}
759	t.ExecuteTemplate(w, "convert.html", data)
760}
761
762func checkDomainHandler(w http.ResponseWriter, r *http.Request) {
763	domain := r.URL.Query().Get("domain")
764	if domain != "" && domains[domain] != "" {
765		w.Write([]byte(domain))
766		return
767	}
768	http.Error(w, "Not Found", 404)
769}
770func runHTTPServer() {
771	log.Printf("Running http server with hostname %s on port %d.", c.Host, c.HttpPort)
772	var err error
773	t = template.New("main").Funcs(template.FuncMap{"parent": path.Dir, "hasSuffix": strings.HasSuffix})
774	t, err = t.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
775	if err != nil {
776		log.Fatal(err)
777	}
778	serveMux := http.NewServeMux()
779
780	s := strings.SplitN(c.Host, ":", 2)
781	hostname := s[0]
782	port := c.HttpPort
783
784	serveMux.HandleFunc(hostname+"/", rootHandler)
785	serveMux.HandleFunc(hostname+"/feed", feedHandler)
786	serveMux.HandleFunc(hostname+"/my_site", mySiteHandler)
787	serveMux.HandleFunc(hostname+"/me", myAccountHandler)
788	serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler)
789	serveMux.HandleFunc(hostname+"/admin", adminHandler)
790	serveMux.HandleFunc(hostname+"/edit/", editFileHandler)
791	serveMux.HandleFunc(hostname+"/convert", convertTextHandler)
792	serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler)
793	serveMux.Handle(hostname+"/login", limit(http.HandlerFunc(loginHandler)))
794	serveMux.Handle(hostname+"/register", limit(http.HandlerFunc(registerHandler)))
795	serveMux.HandleFunc(hostname+"/logout", logoutHandler)
796	serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler)
797	serveMux.HandleFunc(hostname+"/delete-account", deleteAccountHandler)
798	serveMux.HandleFunc(hostname+"/reset-password", resetPasswordHandler)
799
800	// Check domain -- used by caddy
801	serveMux.HandleFunc(hostname+"/check-domain", checkDomainHandler)
802
803	// admin commands
804	serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler)
805
806	serveMux.HandleFunc(hostname+"/webdav/", webdavHandler)
807
808	wrapped := handlers.CustomLoggingHandler(log.Writer(), handlers.RecoveryHandler()(serveMux), logFormatter)
809
810	// handle user files based on subdomain
811	// also routes to proxy
812	serveMux.HandleFunc("proxy."+hostname+"/", proxyGemini) // eg. proxy.flounder.online
813	serveMux.HandleFunc("/", userFile)
814	// login+register functions
815	srv := &http.Server{
816		ReadTimeout:  5 * time.Second,
817		WriteTimeout: 10 * time.Second,
818		IdleTimeout:  120 * time.Second,
819		Addr:         fmt.Sprintf(":%d", port),
820		// TLSConfig:    tlsConfig,
821		Handler: wrapped,
822	}
823	log.Fatal(srv.ListenAndServe())
824}