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