all repos — flounder @ ce6666f9f02374b70bf44526674173ddd8bb5b46

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