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 editFileHandler(w http.ResponseWriter, r *http.Request) {
82 ok, authUser, _ := getAuthUser(r)
83 if !ok {
84 renderDefaultError(w, http.StatusForbidden)
85 return
86 }
87 fileName := filepath.Clean(r.URL.Path[len("/edit/"):])
88 isText := strings.HasPrefix(mime.TypeByExtension(path.Ext(fileName)), "text")
89 filePath := path.Join(c.FilesDirectory, authUser, fileName)
90
91 if r.Method == "GET" {
92 err := checkIfValidFile(filePath, nil)
93 if err != nil {
94 log.Println(err)
95 renderError(w, err.Error(), http.StatusBadRequest)
96 return
97 }
98 // Create directories if dne
99 f, err := os.OpenFile(filePath, os.O_RDONLY, 0644)
100 var fileBytes []byte
101 if os.IsNotExist(err) || !isText {
102 fileBytes = []byte{}
103 err = nil
104 } else {
105 defer f.Close()
106 fileBytes, err = ioutil.ReadAll(f)
107 }
108 if err != nil {
109 panic(err)
110 }
111 data := struct {
112 FileName string
113 FileText string
114 PageTitle string
115 AuthUser string
116 Host string
117 IsText bool
118 }{fileName, string(fileBytes), c.SiteTitle, authUser, c.Host, isText}
119 err = t.ExecuteTemplate(w, "edit_file.html", data)
120 if err != nil {
121 panic(err)
122 }
123 } else if r.Method == "POST" {
124 // get post body
125 r.ParseForm()
126 fileText := r.Form.Get("file_text")
127 // Web form by default gives us CR LF newlines.
128 // Unix files use just LF
129 fileText = strings.ReplaceAll(fileText, "\r\n", "\n")
130 fileBytes := []byte(fileText)
131 err := checkIfValidFile(filePath, fileBytes)
132 if err != nil {
133 log.Println(err)
134 renderError(w, err.Error(), http.StatusBadRequest)
135 return
136 }
137 // create directories if dne
138 os.MkdirAll(path.Dir(filePath), os.ModePerm)
139 if userHasSpace(authUser, len(fileBytes)) {
140 if isText { // Cant edit binary files here
141 err = ioutil.WriteFile(filePath, fileBytes, 0644)
142 }
143 } else {
144 renderError(w, fmt.Sprintf("Bad Request: Out of file space. Max space: %d.", c.MaxUserBytes), http.StatusBadRequest)
145 return
146 }
147 if err != nil {
148 panic(err)
149 }
150 newName := filepath.Clean(r.Form.Get("rename"))
151 err = checkIfValidFile(newName, fileBytes)
152 if err != nil {
153 log.Println(err)
154 renderError(w, err.Error(), http.StatusBadRequest)
155 return
156 }
157 if newName != fileName {
158 newPath := path.Join(c.FilesDirectory, authUser, newName)
159 os.MkdirAll(path.Dir(newPath), os.ModePerm)
160 os.Rename(filePath, newPath)
161 fileName = newName
162 }
163 http.Redirect(w, r, path.Join("/edit", fileName), http.StatusSeeOther)
164 }
165}
166
167func uploadFilesHandler(w http.ResponseWriter, r *http.Request) {
168 if r.Method == "POST" {
169 user := newGetAuthUser(r)
170 if !user.LoggedIn {
171 renderDefaultError(w, http.StatusForbidden)
172 return
173 }
174 r.ParseMultipartForm(10 << 6) // why does this not work
175 file, fileHeader, err := r.FormFile("file")
176 fileName := filepath.Clean(fileHeader.Filename)
177 defer file.Close()
178 if err != nil {
179 log.Println(err)
180 renderError(w, err.Error(), http.StatusBadRequest)
181 return
182 }
183 dest, _ := ioutil.ReadAll(file)
184 err = checkIfValidFile(fileName, dest)
185 if err != nil {
186 log.Println(err)
187 renderError(w, err.Error(), http.StatusBadRequest)
188 return
189 }
190 destPath := path.Join(c.FilesDirectory, user.Username, fileName)
191
192 f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, 0644)
193 if err != nil {
194 panic(err)
195 }
196 defer f.Close()
197 if userHasSpace(user.Username, c.MaxFileBytes) { // Not quite right
198 io.Copy(f, bytes.NewReader(dest))
199 } else {
200 renderError(w, fmt.Sprintf("Bad Request: Out of file space. Max space: %d.", c.MaxUserBytes), http.StatusBadRequest)
201 return
202 }
203 }
204 http.Redirect(w, r, "/my_site", http.StatusSeeOther)
205}
206
207// TODO use this
208type AuthUser struct {
209 LoggedIn bool
210 Username string
211 IsAdmin bool
212 ImpersonatingUser string // used if impersonating
213}
214
215func newGetAuthUser(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
228//TODO deprecate
229func getAuthUser(r *http.Request) (bool, string, bool) {
230 session, _ := SessionStore.Get(r, "cookie-session")
231 user, ok := session.Values["auth_user"].(string)
232 isAdmin, _ := session.Values["admin"].(bool)
233 return ok, user, isAdmin
234}
235func deleteFileHandler(w http.ResponseWriter, r *http.Request) {
236 user := newGetAuthUser(r)
237 if !user.LoggedIn {
238 renderDefaultError(w, http.StatusForbidden)
239 return
240 }
241 filePath := safeGetFilePath(user.Username, r.URL.Path[len("/delete/"):])
242 if r.Method == "POST" {
243 os.Remove(filePath) // TODO handle error
244 }
245 http.Redirect(w, r, "/my_site", http.StatusSeeOther)
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 data := struct {
258 Host string
259 PageTitle string
260 Files []*File
261 AuthUser AuthUser
262 }{c.Host, c.SiteTitle, files, user}
263 _ = t.ExecuteTemplate(w, "my_site.html", data)
264}
265
266func myAccountHandler(w http.ResponseWriter, r *http.Request) {
267 user := newGetAuthUser(r)
268 authUser := user.Username
269 if !user.LoggedIn {
270 renderDefaultError(w, http.StatusForbidden)
271 return
272 }
273 me, _ := getUserByName(user.Username)
274 type pageData struct {
275 PageTitle string
276 AuthUser AuthUser
277 Email string
278 Errors []string
279 }
280 data := pageData{"My Account", user, me.Email, nil}
281
282 if r.Method == "GET" {
283 err := t.ExecuteTemplate(w, "me.html", data)
284 if err != nil {
285 panic(err)
286 }
287 } else if r.Method == "POST" {
288 r.ParseForm()
289 newUsername := r.Form.Get("username")
290 errors := []string{}
291 newEmail := r.Form.Get("email")
292 newUsername = strings.ToLower(newUsername)
293 var err error
294 if newEmail != me.Email {
295 _, err = DB.Exec("update user set email = ? where username = ?", newEmail, me.Email)
296 if err != nil {
297 // TODO better error not sql
298 errors = append(errors, err.Error())
299 } else {
300 log.Printf("Changed email for %s from %s to %s", authUser, me.Email, newEmail)
301 }
302 }
303 if newUsername != authUser {
304 // Rename User
305 err = renameUser(authUser, newUsername)
306 if err != nil {
307 errors = append(errors, err.Error())
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 authd, authUser, _ := getAuthUser(r)
325 if !authd {
326 renderDefaultError(w, http.StatusForbidden)
327 return
328 }
329 if r.Method == "GET" {
330 userFolder := filepath.Join(c.FilesDirectory, filepath.Clean(authUser))
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 _ = row.Scan(&username, &db_password, &active, &isAdmin)
359 if db_password != nil && !active {
360 data := struct {
361 Error string
362 PageTitle string
363 }{"Your account is not active yet. Pending admin approval", c.SiteTitle}
364 t.ExecuteTemplate(w, "login.html", data)
365 return
366 }
367 if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil {
368 log.Println("logged in")
369 session, _ := SessionStore.Get(r, "cookie-session")
370 session.Values["auth_user"] = username
371 session.Values["admin"] = isAdmin
372 session.Save(r, w)
373 http.Redirect(w, r, "/my_site", http.StatusSeeOther)
374 } else {
375 data := struct {
376 Error string
377 PageTitle string
378 }{"Invalid login or password", c.SiteTitle}
379 err := t.ExecuteTemplate(w, "login.html", data)
380 if err != nil {
381 panic(err)
382 }
383 }
384 }
385}
386
387func logoutHandler(w http.ResponseWriter, r *http.Request) {
388 session, _ := SessionStore.Get(r, "cookie-session")
389 session.Options.MaxAge = -1
390 session.Save(r, w)
391 http.Redirect(w, r, "/", http.StatusSeeOther)
392}
393
394const ok = "-0123456789abcdefghijklmnopqrstuvwxyz"
395
396func isOkUsername(s string) error {
397 if len(s) < 1 {
398 return fmt.Errorf("Username is too short")
399 }
400 if len(s) > 32 {
401 return fmt.Errorf("Username is too long. 32 char max.")
402 }
403 for _, char := range s {
404 if !strings.Contains(ok, strings.ToLower(string(char))) {
405 return fmt.Errorf("Username contains invalid characters. Valid characters include lowercase letters, numbers, and hyphens.")
406 }
407 }
408 return nil
409}
410func registerHandler(w http.ResponseWriter, r *http.Request) {
411 if r.Method == "GET" {
412 data := struct {
413 Host string
414 Errors []string
415 PageTitle string
416 }{c.Host, nil, "Register"}
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 := 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 reference := r.Form.Get("reference")
439 if len(errors) == 0 {
440 _, err = DB.Exec("insert into user (username, email, password_hash, reference) values ($1, $2, $3, $4)", username, email, string(hashedPassword), reference)
441 if err != nil {
442 errors = append(errors, "Username or email is already used")
443 }
444 }
445 if len(errors) > 0 {
446 data := struct {
447 Host string
448 Errors []string
449 PageTitle string
450 }{c.Host, errors, "Register"}
451 t.ExecuteTemplate(w, "register.html", data)
452 } else {
453 data := struct {
454 Host string
455 Message string
456 PageTitle string
457 }{c.Host, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}
458 t.ExecuteTemplate(w, "message.html", data)
459 }
460 }
461}
462
463func adminHandler(w http.ResponseWriter, r *http.Request) {
464 _, _, isAdmin := getAuthUser(r)
465 if !isAdmin {
466 renderDefaultError(w, http.StatusForbidden)
467 return
468 }
469 allUsers, err := getUsers()
470 if err != nil {
471 log.Println(err)
472 renderDefaultError(w, http.StatusInternalServerError)
473 return
474 }
475 data := struct {
476 Users []User
477 LoggedIn bool
478 IsAdmin bool
479 PageTitle string
480 Host string
481 }{allUsers, true, true, "Admin", c.Host}
482 err = t.ExecuteTemplate(w, "admin.html", data)
483 if err != nil {
484 panic(err)
485 }
486}
487
488func getFavicon(user string) string {
489 faviconPath := path.Join(c.FilesDirectory, filepath.Clean(user), "favicon.txt")
490 content, err := ioutil.ReadFile(faviconPath)
491 if err != nil {
492 return ""
493 }
494 strcontent := []rune(string(content))
495 if len(strcontent) > 0 {
496 return string(strcontent[0])
497 }
498 return ""
499}
500
501// Server a user's file
502func userFile(w http.ResponseWriter, r *http.Request) {
503 userName := filepath.Clean(strings.Split(r.Host, ".")[0]) // Clean probably unnecessary
504 p := filepath.Clean(r.URL.Path)
505 var isDir bool
506 fileName := path.Join(c.FilesDirectory, userName, p)
507 stat, err := os.Stat(fileName)
508 if stat != nil {
509 isDir = stat.IsDir()
510 }
511 if p == "/" || isDir {
512 fileName = path.Join(fileName, "index.gmi")
513 }
514
515 if strings.HasPrefix(p, "/.hidden") {
516 renderDefaultError(w, http.StatusForbidden)
517 return
518 }
519 if r.URL.Path == "/style.css" {
520 http.ServeFile(w, r, path.Join(c.TemplatesDirectory, "static/style.css"))
521 return
522 }
523
524 _, err = os.Stat(fileName)
525 if os.IsNotExist(err) {
526 renderDefaultError(w, http.StatusNotFound)
527 return
528 }
529
530 // Dumb content negotiation
531 extension := path.Ext(fileName)
532 _, raw := r.URL.Query()["raw"]
533 acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini")
534 if !raw && !acceptsGemini && (extension == ".gmi" || extension == ".gemini") {
535 file, _ := os.Open(fileName)
536 htmlString := textToHTML(gmi.ParseText(file))
537 favicon := getFavicon(userName)
538 data := struct {
539 SiteBody template.HTML
540 Favicon string
541 PageTitle string
542 }{template.HTML(htmlString), favicon, userName + p}
543 t.ExecuteTemplate(w, "user_page.html", data)
544 } else {
545 http.ServeFile(w, r, fileName)
546 }
547}
548
549func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
550 _, authUser, _ := getAuthUser(r)
551 if r.Method == "POST" {
552 err := deleteUser(authUser)
553 if err != nil {
554 log.Println(err)
555 renderDefaultError(w, http.StatusInternalServerError)
556 return
557 }
558 logoutHandler(w, r)
559 }
560}
561
562func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
563 getAuthUser(r)
564}
565
566func adminUserHandler(w http.ResponseWriter, r *http.Request) {
567 _, _, isAdmin := getAuthUser(r)
568 if r.Method == "POST" {
569 if !isAdmin {
570 renderDefaultError(w, http.StatusForbidden)
571 return
572 }
573 components := strings.Split(r.URL.Path, "/")
574 if len(components) < 5 {
575 renderError(w, "Invalid action", http.StatusBadRequest)
576 return
577 }
578 userName := components[3]
579 action := components[4]
580 var err error
581 if action == "activate" {
582 err = activateUser(userName)
583 } else if action == "delete" {
584 err = deleteUser(userName)
585 }
586 if err != nil {
587 log.Println(err)
588 renderDefaultError(w, http.StatusInternalServerError)
589 return
590 }
591 http.Redirect(w, r, "/admin", http.StatusSeeOther)
592 }
593}
594
595func runHTTPServer() {
596 log.Printf("Running http server with hostname %s on port %d. TLS enabled: %t", c.Host, c.HttpPort, c.HttpsEnabled)
597 var err error
598 t, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.html"))
599 if err != nil {
600 log.Fatal(err)
601 }
602 serveMux := http.NewServeMux()
603
604 s := strings.SplitN(c.Host, ":", 2)
605 hostname := s[0]
606 port := c.HttpPort
607
608 serveMux.HandleFunc(hostname+"/", rootHandler)
609 serveMux.HandleFunc(hostname+"/my_site", mySiteHandler)
610 serveMux.HandleFunc(hostname+"/me", myAccountHandler)
611 serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler)
612 serveMux.HandleFunc(hostname+"/admin", adminHandler)
613 serveMux.HandleFunc(hostname+"/edit/", editFileHandler)
614 serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler)
615 serveMux.HandleFunc(hostname+"/login", loginHandler)
616 serveMux.HandleFunc(hostname+"/logout", logoutHandler)
617 serveMux.HandleFunc(hostname+"/register", registerHandler)
618 serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler)
619 serveMux.HandleFunc(hostname+"/delete-account", deleteAccountHandler)
620 serveMux.HandleFunc(hostname+"/reset-password", resetPasswordHandler)
621
622 // admin commands
623 serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler)
624
625 // TODO rate limit login https://github.com/ulule/limiter
626
627 wrapped := (handlers.LoggingHandler(log.Writer(), handlers.RecoveryHandler()(serveMux)))
628
629 // handle user files based on subdomain
630 serveMux.HandleFunc("/", userFile)
631 // login+register functions
632 srv := &http.Server{
633 ReadTimeout: 5 * time.Second,
634 WriteTimeout: 10 * time.Second,
635 IdleTimeout: 120 * time.Second,
636 Addr: fmt.Sprintf(":%d", port),
637 // TLSConfig: tlsConfig,
638 Handler: wrapped,
639 }
640 if c.HttpsEnabled {
641 log.Fatal(srv.ListenAndServeTLS(c.TLSCertFile, c.TLSKeyFile))
642 } else {
643 log.Fatal(srv.ListenAndServe())
644 }
645}