all repos — flounder @ b78509c99c42f8e034d677322eb7ffa727ee7de1

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	"mime"
 17	"net/http"
 18	"os"
 19	"path"
 20	"path/filepath"
 21	"strings"
 22	"time"
 23)
 24
 25var t *template.Template
 26var DB *sql.DB
 27var SessionStore *sessions.CookieStore
 28
 29func renderDefaultError(w http.ResponseWriter, statusCode int) {
 30	errorMsg := http.StatusText(statusCode)
 31	renderError(w, errorMsg, statusCode)
 32}
 33
 34func renderError(w http.ResponseWriter, errorMsg string, statusCode int) {
 35	data := struct {
 36		PageTitle  string
 37		StatusCode int
 38		ErrorMsg   string
 39	}{"Error!", statusCode, errorMsg}
 40	err := t.ExecuteTemplate(w, "error.html", data)
 41	if err != nil { // Shouldn't happen probably
 42		http.Error(w, errorMsg, statusCode)
 43	}
 44}
 45
 46func rootHandler(w http.ResponseWriter, r *http.Request) {
 47	// serve everything inside static directory
 48	if r.URL.Path != "/" {
 49		fileName := path.Join(c.TemplatesDirectory, "static", filepath.Clean(r.URL.Path))
 50		_, err := os.Stat(fileName)
 51		if err != nil {
 52			renderDefaultError(w, http.StatusNotFound)
 53			return
 54		}
 55		http.ServeFile(w, r, fileName) // TODO better error handling
 56		return
 57	}
 58
 59	user := newGetAuthUser(r)
 60	indexFiles, err := getIndexFiles(user.IsAdmin)
 61	if err != nil {
 62		panic(err)
 63	}
 64	allUsers, err := getActiveUserNames()
 65	if err != nil {
 66		panic(err)
 67	}
 68	data := struct {
 69		Host      string
 70		PageTitle string
 71		Files     []*File
 72		Users     []string
 73		AuthUser  AuthUser
 74	}{c.Host, c.SiteTitle, indexFiles, allUsers, user}
 75	err = t.ExecuteTemplate(w, "index.html", data)
 76	if err != nil {
 77		panic(err)
 78	}
 79}
 80
 81func feedHandler(w http.ResponseWriter, r *http.Request) {
 82	user := newGetAuthUser(r)
 83	feedEntries, err := getAllGemfeedEntries()
 84	if err != nil {
 85		panic(err)
 86	}
 87	data := struct {
 88		Host        string
 89		PageTitle   string
 90		FeedEntries []*FeedEntry
 91		AuthUser    AuthUser
 92	}{c.Host, c.SiteTitle, feedEntries, 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	isText := strings.HasPrefix(mime.TypeByExtension(path.Ext(fileName)), "text")
107	filePath := path.Join(c.FilesDirectory, user.Username, fileName)
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	data := struct {
255		Host      string
256		PageTitle string
257		Files     []*File
258		AuthUser  AuthUser
259	}{c.Host, c.SiteTitle, files, user}
260	_ = t.ExecuteTemplate(w, "my_site.html", data)
261}
262
263func myAccountHandler(w http.ResponseWriter, r *http.Request) {
264	user := newGetAuthUser(r)
265	authUser := user.Username
266	if !user.LoggedIn {
267		renderDefaultError(w, http.StatusForbidden)
268		return
269	}
270	me, _ := getUserByName(user.Username)
271	type pageData struct {
272		PageTitle string
273		AuthUser  AuthUser
274		Email     string
275		Errors    []string
276	}
277	data := pageData{"My Account", user, me.Email, nil}
278
279	if r.Method == "GET" {
280		err := t.ExecuteTemplate(w, "me.html", data)
281		if err != nil {
282			panic(err)
283		}
284	} else if r.Method == "POST" {
285		r.ParseForm()
286		newUsername := r.Form.Get("username")
287		errors := []string{}
288		newEmail := r.Form.Get("email")
289		newUsername = strings.ToLower(newUsername)
290		var err error
291		if newEmail != me.Email {
292			_, err = DB.Exec("update user set email = ? where username = ?", newEmail, me.Email)
293			if err != nil {
294				// TODO better error not sql
295				errors = append(errors, err.Error())
296			} else {
297				log.Printf("Changed email for %s from %s to %s", authUser, me.Email, newEmail)
298			}
299		}
300		if newUsername != authUser {
301			// Rename User
302			err = renameUser(authUser, newUsername)
303			if err != nil {
304				log.Println(err)
305				errors = append(errors, "Could not rename user")
306			} else {
307				session, _ := SessionStore.Get(r, "cookie-session")
308				session.Values["auth_user"] = newUsername
309				session.Save(r, w)
310			}
311		}
312		// reset auth
313		user = newGetAuthUser(r)
314		data.Errors = errors
315		data.AuthUser = user
316		data.Email = newEmail
317		_ = t.ExecuteTemplate(w, "me.html", data)
318	}
319}
320
321func archiveHandler(w http.ResponseWriter, r *http.Request) {
322	authUser := newGetAuthUser(r)
323	if !authUser.LoggedIn {
324		renderDefaultError(w, http.StatusForbidden)
325		return
326	}
327	if r.Method == "GET" {
328		userFolder := getUserDirectory(authUser.Username)
329		err := zipit(userFolder, w)
330		if err != nil {
331			panic(err)
332		}
333
334	}
335}
336func loginHandler(w http.ResponseWriter, r *http.Request) {
337	if r.Method == "GET" {
338		// show page
339		data := struct {
340			Error     string
341			PageTitle string
342		}{"", "Login"}
343		err := t.ExecuteTemplate(w, "login.html", data)
344		if err != nil {
345			panic(err)
346		}
347	} else if r.Method == "POST" {
348		r.ParseForm()
349		name := r.Form.Get("username")
350		password := r.Form.Get("password")
351		row := DB.QueryRow("SELECT username, password_hash, active, admin FROM user where username = $1 OR email = $1", name)
352		var db_password []byte
353		var username string
354		var active bool
355		var isAdmin bool
356		err := row.Scan(&username, &db_password, &active, &isAdmin)
357		if err != nil {
358			panic(err)
359		}
360		if db_password != nil && !active {
361			data := struct {
362				Error     string
363				PageTitle string
364			}{"Your account is not active yet. Pending admin approval", c.SiteTitle}
365			t.ExecuteTemplate(w, "login.html", data)
366			return
367		}
368		if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil {
369			log.Println("logged in")
370			session, _ := SessionStore.Get(r, "cookie-session")
371			session.Values["auth_user"] = username
372			session.Values["admin"] = isAdmin
373			session.Save(r, w)
374			http.Redirect(w, r, "/my_site", http.StatusSeeOther)
375		} else {
376			data := struct {
377				Error     string
378				PageTitle string
379			}{"Invalid login or password", c.SiteTitle}
380			err := t.ExecuteTemplate(w, "login.html", data)
381			if err != nil {
382				panic(err)
383			}
384		}
385	}
386}
387
388func logoutHandler(w http.ResponseWriter, r *http.Request) {
389	session, _ := SessionStore.Get(r, "cookie-session")
390	impers, ok := session.Values["impersonating_user"].(string)
391	if ok {
392		session.Values["auth_user"] = impers
393		session.Values["impersonating_user"] = nil // TODO expire this automatically
394		// session.Values["admin"] = nil // TODO fix admin
395	} else {
396		session.Options.MaxAge = -1
397	}
398	session.Save(r, w)
399	http.Redirect(w, r, "/", http.StatusSeeOther)
400}
401
402const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
403
404func isOkUsername(s string) error {
405	if len(s) < 1 {
406		return fmt.Errorf("Username is too short")
407	}
408	if len(s) > 32 {
409		return fmt.Errorf("Username is too long. 32 char max.")
410	}
411	for _, char := range s {
412		if !strings.Contains(ok, strings.ToLower(string(char))) {
413			return fmt.Errorf("Username contains invalid characters. Valid characters include lowercase letters, numbers, and hyphens.")
414		}
415	}
416	return nil
417}
418func registerHandler(w http.ResponseWriter, r *http.Request) {
419	if r.Method == "GET" {
420		data := struct {
421			Host      string
422			Errors    []string
423			PageTitle string
424		}{c.Host, nil, "Register"}
425		err := t.ExecuteTemplate(w, "register.html", data)
426		if err != nil {
427			panic(err)
428		}
429	} else if r.Method == "POST" {
430		r.ParseForm()
431		email := r.Form.Get("email")
432		password := r.Form.Get("password")
433		errors := []string{}
434		if r.Form.Get("password") != r.Form.Get("password2") {
435			errors = append(errors, "Passwords don't match")
436		}
437		if len(password) < 6 {
438			errors = append(errors, "Password is too short")
439		}
440		username := strings.ToLower(r.Form.Get("username"))
441		err := isOkUsername(username)
442		if err != nil {
443			errors = append(errors, err.Error())
444		}
445		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error
446		if err != nil {
447			panic(err)
448		}
449		reference := r.Form.Get("reference")
450		if len(errors) == 0 {
451			_, err = DB.Exec("insert into user (username, email, password_hash, reference) values ($1, $2, $3, $4)", username, email, string(hashedPassword), reference)
452			if err != nil {
453				errors = append(errors, "Username or email is already used")
454			}
455		}
456		if len(errors) > 0 {
457			data := struct {
458				Host      string
459				Errors    []string
460				PageTitle string
461			}{c.Host, errors, "Register"}
462			t.ExecuteTemplate(w, "register.html", data)
463		} else {
464			data := struct {
465				Host      string
466				Message   string
467				PageTitle string
468			}{c.Host, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
469			t.ExecuteTemplate(w, "message.html", data)
470		}
471	}
472}
473
474func deleteFileHandler(w http.ResponseWriter, r *http.Request) {
475	user := newGetAuthUser(r)
476	if !user.LoggedIn {
477		renderDefaultError(w, http.StatusForbidden)
478		return
479	}
480	filePath := safeGetFilePath(user.Username, r.URL.Path[len("/delete/"):])
481	if r.Method == "POST" {
482		os.Remove(filePath) // TODO handle error
483	}
484	http.Redirect(w, r, "/my_site", http.StatusSeeOther)
485}
486
487func adminHandler(w http.ResponseWriter, r *http.Request) {
488	user := newGetAuthUser(r)
489	if !user.IsAdmin {
490		renderDefaultError(w, http.StatusForbidden)
491		return
492	}
493	allUsers, err := getUsers()
494	if err != nil {
495		log.Println(err)
496		renderDefaultError(w, http.StatusInternalServerError)
497		return
498	}
499	data := struct {
500		Users     []User
501		AuthUser  AuthUser
502		PageTitle string
503		Host      string
504	}{allUsers, user, "Admin", c.Host}
505	err = t.ExecuteTemplate(w, "admin.html", data)
506	if err != nil {
507		panic(err)
508	}
509}
510
511func getFavicon(user string) string {
512	faviconPath := path.Join(c.FilesDirectory, filepath.Clean(user), "favicon.txt")
513	content, err := ioutil.ReadFile(faviconPath)
514	if err != nil {
515		return ""
516	}
517	strcontent := []rune(string(content))
518	if len(strcontent) > 0 {
519		return string(strcontent[0])
520	}
521	return ""
522}
523
524// Server a user's file
525func userFile(w http.ResponseWriter, r *http.Request) {
526	userName := filepath.Clean(strings.Split(r.Host, ".")[0]) // Clean probably unnecessary
527	p := filepath.Clean(r.URL.Path)
528	var isDir bool
529	fileName := path.Join(c.FilesDirectory, userName, p)
530	stat, err := os.Stat(fileName)
531	if stat != nil {
532		isDir = stat.IsDir()
533	}
534	if p == "/" || isDir {
535		fileName = path.Join(fileName, "index.gmi")
536	}
537
538	if strings.HasPrefix(p, "/.hidden") {
539		renderDefaultError(w, http.StatusForbidden)
540		return
541	}
542	if r.URL.Path == "/style.css" {
543		http.ServeFile(w, r, path.Join(c.TemplatesDirectory, "static/style.css"))
544		return
545	}
546
547	_, err = os.Stat(fileName)
548	if os.IsNotExist(err) {
549		renderDefaultError(w, http.StatusNotFound)
550		return
551	}
552
553	// Dumb content negotiation
554	_, raw := r.URL.Query()["raw"]
555	acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini")
556	if !raw && !acceptsGemini && isGemini(fileName) {
557		file, _ := os.Open(fileName)
558		htmlString := textToHTML(gmi.ParseText(file))
559		favicon := getFavicon(userName)
560		hostname := strings.Split(r.Host, ":")[0]
561		URI := hostname + r.URL.String()
562		data := struct {
563			SiteBody  template.HTML
564			Favicon   string
565			PageTitle string
566			URI       string
567		}{template.HTML(htmlString), favicon, userName + p, URI}
568		t.ExecuteTemplate(w, "user_page.html", data)
569	} else {
570		http.ServeFile(w, r, fileName)
571	}
572}
573
574func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
575	user := newGetAuthUser(r)
576	if r.Method == "POST" {
577		r.ParseForm()
578		validate := r.Form.Get("validate-delete")
579		if validate == user.Username {
580			err := deleteUser(user.Username)
581			if err != nil {
582				log.Println(err)
583				renderDefaultError(w, http.StatusInternalServerError)
584				return
585			}
586			logoutHandler(w, r)
587		} else {
588			http.Redirect(w, r, "/me", http.StatusSeeOther)
589		}
590	}
591}
592
593func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
594	user := newGetAuthUser(r)
595	data := struct {
596		PageTitle string
597		AuthUser  AuthUser
598		Error     string
599	}{"Reset Password", user, ""}
600	if r.Method == "GET" {
601		err := t.ExecuteTemplate(w, "reset_pass.html", data)
602		if err != nil {
603			panic(err)
604		}
605	} else if r.Method == "POST" {
606		r.ParseForm()
607		enteredCurrPass := r.Form.Get("password")
608		password1 := r.Form.Get("new_password1")
609		password2 := r.Form.Get("new_password2")
610		if password1 != password2 {
611			data.Error = "New passwords do not match"
612		} else if len(password1) < 6 {
613			data.Error = "Password is too short"
614		} else {
615			err := checkAuth(user.Username, enteredCurrPass)
616			if err == nil {
617				hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password1), 8)
618				if err != nil {
619					panic(err)
620				}
621				_, err = DB.Exec("update user set password_hash = ? where username = ?", hashedPassword, user.Username)
622				if err != nil {
623					panic(err)
624				}
625				log.Printf("User %s reset password", user.Username)
626				http.Redirect(w, r, "/me", http.StatusSeeOther)
627				return
628			} else {
629				data.Error = "That's not your current password"
630			}
631		}
632		err := t.ExecuteTemplate(w, "reset_pass.html", data)
633		if err != nil {
634			panic(err)
635		}
636	}
637}
638
639func adminUserHandler(w http.ResponseWriter, r *http.Request) {
640	user := newGetAuthUser(r)
641	if r.Method == "POST" {
642		if !user.IsAdmin {
643			renderDefaultError(w, http.StatusForbidden)
644			return
645		}
646		components := strings.Split(r.URL.Path, "/")
647		if len(components) < 5 {
648			renderError(w, "Invalid action", http.StatusBadRequest)
649			return
650		}
651		userName := components[3]
652		action := components[4]
653		var err error
654		if action == "activate" {
655			err = activateUser(userName)
656		} else if action == "impersonate" {
657			if user.ImpersonatingUser != "" {
658				// Don't allow nested impersonation
659				renderError(w, "Cannot nest impersonation, log out from impersonated user first.", 400)
660				return
661			}
662			session, _ := SessionStore.Get(r, "cookie-session")
663			session.Values["auth_user"] = userName
664			session.Values["impersonating_user"] = user.Username
665			session.Save(r, w)
666			log.Printf("User %s impersonated %s", user.Username, userName)
667			http.Redirect(w, r, "/", http.StatusSeeOther)
668			return
669		}
670		if err != nil {
671			log.Println(err)
672			renderDefaultError(w, http.StatusInternalServerError)
673			return
674		}
675		http.Redirect(w, r, "/admin", http.StatusSeeOther)
676	}
677}
678
679func runHTTPServer() {
680	log.Printf("Running http server with hostname %s on port %d. TLS enabled: %t", c.Host, c.HttpPort, c.HttpsEnabled)
681	var err error
682	t, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
683	if err != nil {
684		log.Fatal(err)
685	}
686	serveMux := http.NewServeMux()
687
688	s := strings.SplitN(c.Host, ":", 2)
689	hostname := s[0]
690	port := c.HttpPort
691
692	serveMux.HandleFunc(hostname+"/", rootHandler)
693	serveMux.HandleFunc(hostname+"/feed", feedHandler)
694	serveMux.HandleFunc(hostname+"/my_site", mySiteHandler)
695	serveMux.HandleFunc(hostname+"/me", myAccountHandler)
696	serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler)
697	serveMux.HandleFunc(hostname+"/admin", adminHandler)
698	serveMux.HandleFunc(hostname+"/edit/", editFileHandler)
699	serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler)
700	serveMux.Handle(hostname+"/login", limit(http.HandlerFunc(loginHandler)))
701	serveMux.Handle(hostname+"/register", limit(http.HandlerFunc(registerHandler)))
702	serveMux.HandleFunc(hostname+"/logout", logoutHandler)
703	serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler)
704	serveMux.HandleFunc(hostname+"/delete-account", deleteAccountHandler)
705	serveMux.HandleFunc(hostname+"/reset-password", resetPasswordHandler)
706
707	// admin commands
708	serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler)
709	// TODO authentication
710	serveMux.HandleFunc(hostname+"/webdav/", webdavHandler)
711
712	wrapped := (handlers.LoggingHandler(log.Writer(), handlers.RecoveryHandler()(serveMux)))
713
714	// handle user files based on subdomain
715	serveMux.HandleFunc("/", userFile)
716	// login+register functions
717	srv := &http.Server{
718		ReadTimeout:  5 * time.Second,
719		WriteTimeout: 10 * time.Second,
720		IdleTimeout:  120 * time.Second,
721		Addr:         fmt.Sprintf(":%d", port),
722		// TLSConfig:    tlsConfig,
723		Handler: wrapped,
724	}
725	if c.HttpsEnabled {
726		log.Fatal(srv.ListenAndServeTLS(c.TLSCertFile, c.TLSKeyFile))
727	} else {
728		log.Fatal(srv.ListenAndServe())
729	}
730}