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