all repos — flounder @ 0bab54ffcc88918939cd367192be20dcd76c8f5e

A small site builder for the Gemini protocol

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