all repos — flounder @ 2772a6a12025ad55c52e87a9be1ee129d0e6750b

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	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}