http.go (24246B)
1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "html/template" 7 "io" 8 "io/ioutil" 9 "log" 10 "net" 11 "net/http" 12 "net/url" 13 "os" 14 "path" 15 "strings" 16 "time" 17 18 gmi "git.sr.ht/~adnano/go-gemini" 19 feeds "git.sr.ht/~aw/gorilla-feeds" 20 "github.com/gorilla/handlers" 21 "github.com/gorilla/sessions" 22 _ "github.com/mattn/go-sqlite3" 23 "golang.org/x/crypto/bcrypt" 24 ) 25 26 var t *template.Template 27 var SessionStore *sessions.CookieStore 28 29 func renderDefaultError(w http.ResponseWriter, statusCode int) { 30 errorMsg := http.StatusText(statusCode) 31 renderError(w, errorMsg, statusCode) 32 } 33 34 func serverError(w http.ResponseWriter, err error) { 35 log.Print(err) 36 renderDefaultError(w, 500) 37 } 38 39 func renderError(w http.ResponseWriter, errorMsg string, statusCode int) { 40 data := struct { 41 StatusCode int 42 ErrorMsg string 43 Config Config 44 }{statusCode, errorMsg, c} 45 w.WriteHeader(statusCode) 46 err := t.ExecuteTemplate(w, "error.html", data) 47 if err != nil { // Shouldn't happen probably 48 http.Error(w, errorMsg, statusCode) 49 } 50 } 51 52 func rootHandler(w http.ResponseWriter, r *http.Request) { 53 // serve everything inside static directory 54 if r.URL.Path != "/" { 55 fileName := path.Join(c.TemplatesDirectory, "static", cleanPath(r.URL.Path)) 56 _, err := os.Stat(fileName) 57 if err != nil { 58 renderDefaultError(w, http.StatusNotFound) 59 return 60 } 61 http.ServeFile(w, r, fileName) // TODO better error handling 62 return 63 } 64 65 user := getAuthUser(r) 66 indexFiles, err := getUpdatedFiles(user.IsAdmin, "") 67 if err != nil { 68 serverError(w, err) 69 return 70 } 71 allUsers, err := getActiveUserNames() 72 if err != nil { 73 serverError(w, err) 74 return 75 } 76 data := struct { 77 Config Config 78 AuthUser AuthUser 79 Files []*File 80 Users []string 81 }{c, user, indexFiles, allUsers} 82 err = t.ExecuteTemplate(w, "index.html", data) 83 if err != nil { 84 serverError(w, err) 85 return 86 } 87 } 88 89 func updatesHandler(w http.ResponseWriter, r *http.Request) { 90 // user 91 authUser := getAuthUser(r) 92 var username string 93 if strings.HasSuffix(r.URL.Path, "atom.xml") { 94 username = cleanPath(r.URL.Path[len("/updates/") : len(r.URL.Path)-len("atom.xml")])[1:] 95 w.Header().Set("Content-Type", "application/atom+xml") 96 // build atom feed 97 files, err := getUpdatedFiles(authUser.IsAdmin, username) 98 if err != nil { 99 serverError(w, err) 100 return 101 } 102 baseURL := "//" + username + "." + c.Host 103 feed := feeds.Feed{ 104 Title: username + "'s updated files", 105 Author: &feeds.Author{Name: username}, 106 Link: &feeds.Link{Href: baseURL}, 107 } 108 feed.Items = []*feeds.Item{} 109 for _, file := range files { 110 feed.Items = append(feed.Items, &feeds.Item{ 111 Title: file.Name, 112 Link: &feeds.Link{Href: baseURL + "/" + file.Name}, 113 Created: file.UpdatedTime, // actually updated. kinda funky 114 }) 115 } 116 res, err := feed.ToAtom() 117 if err != nil { 118 serverError(w, err) 119 return 120 } 121 io.Copy(w, strings.NewReader(res)) 122 return 123 } else { 124 username = cleanPath(r.URL.Path[len("/updates/"):])[1:] 125 } 126 files, err := getUpdatedFiles(authUser.IsAdmin, username) 127 if err != nil { 128 serverError(w, err) 129 return 130 } 131 data := struct { 132 Config Config 133 AuthUser AuthUser 134 User string 135 Files []*File 136 }{c, authUser, username, files} 137 err = t.ExecuteTemplate(w, "updates.html", data) 138 if err != nil { 139 serverError(w, err) 140 return 141 } 142 } 143 144 func editFileHandler(w http.ResponseWriter, r *http.Request) { 145 user := getAuthUser(r) 146 if !user.LoggedIn { 147 renderDefaultError(w, http.StatusForbidden) 148 return 149 } 150 fileName := cleanPath(r.URL.Path[len("/edit/"):]) 151 filePath := path.Join(c.FilesDirectory, user.Username, fileName) 152 isText := isTextFile(filePath) 153 alert := "" 154 var warnings []string 155 if r.Method == "POST" { 156 // get post body 157 alert = "saved" 158 r.ParseForm() 159 fileText := r.Form.Get("file_text") 160 // Web form by default gives us CR LF newlines. 161 // Unix files use just LF 162 fileText = strings.ReplaceAll(fileText, "\r\n", "\n") 163 fileBytes := []byte(fileText) 164 fileBytes = bytes.Trim(fileBytes, "\xef\xbb\xbf") // Remove BOM 165 err := checkIfValidFile(user.Username, filePath, fileBytes) 166 if err != nil { 167 log.Println(err) 168 renderError(w, err.Error(), http.StatusBadRequest) 169 return 170 } 171 sfl := getSchemedFlounderLinkLines(strings.NewReader(fileText)) 172 if len(sfl) > 0 { 173 warnings = append(warnings, "Warning! Some of your links to pages use schemas. This means that they may break when viewed in Gemini or over HTTPS. Plase remove gemini: or https: from the start of these links:\n") 174 for _, l := range sfl { 175 warnings = append(warnings, l) 176 } 177 } 178 // create directories if dne 179 os.MkdirAll(path.Dir(filePath), os.ModePerm) 180 newName := cleanPath(r.Form.Get("rename")) 181 err = checkIfValidFile(user.Username, newName, fileBytes) 182 if err != nil { 183 log.Println(err) 184 renderError(w, err.Error(), http.StatusBadRequest) 185 return 186 } 187 if isText { // Cant edit binary files here 188 err = ioutil.WriteFile(filePath, fileBytes, 0644) 189 if err != nil { 190 log.Println(err) 191 renderError(w, err.Error(), http.StatusBadRequest) 192 } 193 } 194 if newName != fileName { 195 newPath := path.Join(c.FilesDirectory, user.Username, newName) 196 os.MkdirAll(path.Dir(newPath), os.ModePerm) 197 os.Rename(filePath, newPath) 198 fileName = newName 199 filePath = newPath 200 alert += " and renamed" 201 } 202 } 203 204 err := checkIfValidFile(user.Username, filePath, nil) 205 if err != nil { 206 log.Println(err) 207 renderError(w, err.Error(), http.StatusBadRequest) 208 return 209 } 210 // Create directories if dne 211 f, err := os.OpenFile(filePath, os.O_RDONLY, 0644) 212 var fileBytes []byte 213 if os.IsNotExist(err) || !isText { 214 fileBytes = []byte{} 215 err = nil 216 } else { 217 defer f.Close() 218 fileBytes, err = ioutil.ReadAll(f) 219 } 220 if err != nil { 221 serverError(w, err) 222 return 223 } 224 data := struct { 225 FileName string 226 FileText string 227 Config Config 228 AuthUser AuthUser 229 Host string 230 IsText bool 231 IsGemini bool 232 IsGemlog bool 233 Alert string 234 Warnings []string 235 }{fileName[1:], // remove starting slash 236 string(fileBytes), c, user, c.Host, isText, isGemini(fileName), strings.HasPrefix(fileName, "gemlog"), alert, warnings} 237 err = t.ExecuteTemplate(w, "edit_file.html", data) 238 if err != nil { 239 serverError(w, err) 240 return 241 } 242 } 243 244 func uploadFilesHandler(w http.ResponseWriter, r *http.Request) { 245 if r.Method == "POST" { 246 user := getAuthUser(r) 247 if !user.LoggedIn { 248 renderDefaultError(w, http.StatusForbidden) 249 return 250 } 251 r.ParseMultipartForm(10 << 6) // why does this not work 252 file, fileHeader, err := r.FormFile("file") 253 if err != nil { 254 log.Println(err) 255 renderError(w, "No file selected. Please go back and select a file.", http.StatusBadRequest) 256 return 257 } 258 fileName := cleanPath(fileHeader.Filename) 259 defer file.Close() 260 dest, _ := ioutil.ReadAll(file) 261 err = checkIfValidFile(user.Username, fileName, dest) 262 if err != nil { 263 log.Println(err) 264 renderError(w, err.Error(), http.StatusBadRequest) 265 return 266 } 267 destPath := path.Join(c.FilesDirectory, user.Username, fileName) 268 269 f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, 0644) 270 if err != nil { 271 serverError(w, err) 272 return 273 } 274 defer f.Close() 275 io.Copy(f, bytes.NewReader(dest)) 276 } 277 http.Redirect(w, r, "/my_site", http.StatusSeeOther) 278 } 279 280 type AuthUser struct { 281 LoggedIn bool 282 Username string 283 IsAdmin bool 284 ImpersonatingUser string // used if impersonating 285 } 286 287 func getAuthUser(r *http.Request) AuthUser { 288 session, _ := SessionStore.Get(r, "cookie-session") 289 user, ok := session.Values["auth_user"].(string) 290 impers, _ := session.Values["impersonating_user"].(string) 291 isAdmin, _ := session.Values["admin"].(bool) 292 return AuthUser{ 293 LoggedIn: ok, 294 Username: user, 295 IsAdmin: isAdmin, 296 ImpersonatingUser: impers, 297 } 298 } 299 300 func mySiteHandler(w http.ResponseWriter, r *http.Request) { 301 user := getAuthUser(r) 302 if !user.LoggedIn { 303 renderDefaultError(w, http.StatusForbidden) 304 return 305 } 306 // check auth 307 userFolder := getUserDirectory(user.Username) 308 files, _ := getMyFilesRecursive(userFolder, user.Username) 309 currentDate := time.Now().Format("2006-01-02") 310 data := struct { 311 Config Config 312 Files []File 313 AuthUser AuthUser 314 CurrentDate string 315 }{c, files, user, currentDate} 316 _ = t.ExecuteTemplate(w, "my_site.html", data) 317 } 318 319 func myAccountHandler(w http.ResponseWriter, r *http.Request) { 320 user := getAuthUser(r) 321 authUser := user.Username 322 if !user.LoggedIn { 323 renderDefaultError(w, http.StatusForbidden) 324 return 325 } 326 me, _ := getUserByName(user.Username) 327 type pageData struct { 328 Config Config 329 AuthUser AuthUser 330 MyUser *User 331 Errors []string 332 } 333 data := pageData{c, user, me, nil} 334 335 if r.Method == "GET" { 336 err := t.ExecuteTemplate(w, "me.html", data) 337 if err != nil { 338 serverError(w, err) 339 return 340 } 341 } else if r.Method == "POST" { 342 r.ParseForm() 343 newUsername := r.Form.Get("username") 344 errors := []string{} 345 newEmail := r.Form.Get("email") 346 newDomain := r.Form.Get("domain") 347 newUsername = strings.ToLower(newUsername) 348 var err error 349 _, exists := domains[newDomain] 350 if newDomain != me.Domain && !exists { 351 _, err = DB.Exec("update user set domain = ? where username = ?", newDomain, me.Username) // TODO use transaction 352 if err != nil { 353 errors = append(errors, err.Error()) 354 } else { 355 certificates.Register(newDomain) 356 refreshDomainMap() 357 log.Printf("Changed domain for %s from %s to %s", authUser, me.Domain, newDomain) 358 } 359 } 360 if newEmail != me.Email { 361 _, err = DB.Exec("update user set email = ? where username = ?", newEmail, me.Username) 362 if err != nil { 363 // TODO better error not sql 364 errors = append(errors, err.Error()) 365 } else { 366 log.Printf("Changed email for %s from %s to %s", authUser, me.Email, newEmail) 367 } 368 } 369 if newUsername != authUser { 370 // Rename User 371 err = renameUser(authUser, newUsername) 372 if err != nil { 373 log.Println(err) 374 errors = append(errors, "Could not rename user") 375 } else { 376 session, _ := SessionStore.Get(r, "cookie-session") 377 session.Values["auth_user"] = newUsername 378 session.Save(r, w) 379 } 380 } 381 // reset auth 382 user = getAuthUser(r) 383 data.Errors = errors 384 data.AuthUser = user 385 data.MyUser.Email = newEmail 386 data.MyUser.Domain = newDomain 387 _ = t.ExecuteTemplate(w, "me.html", data) 388 } 389 } 390 391 func archiveHandler(w http.ResponseWriter, r *http.Request) { 392 authUser := getAuthUser(r) 393 if !authUser.LoggedIn { 394 renderDefaultError(w, http.StatusForbidden) 395 return 396 } 397 if r.Method == "GET" { 398 userFolder := getUserDirectory(authUser.Username) 399 err := zipit(userFolder, w) 400 if err != nil { 401 serverError(w, err) 402 return 403 } 404 405 } 406 } 407 func loginHandler(w http.ResponseWriter, r *http.Request) { 408 if r.Method == "GET" { 409 // show page 410 data := struct { 411 Error string 412 Config Config 413 }{"", c} 414 err := t.ExecuteTemplate(w, "login.html", data) 415 if err != nil { 416 serverError(w, err) 417 return 418 } 419 } else if r.Method == "POST" { 420 r.ParseForm() 421 name := strings.ToLower(r.Form.Get("username")) 422 password := r.Form.Get("password") 423 username, isAdmin, err := checkLogin(name, password) 424 if err == nil { 425 log.Println("logged in") 426 session, _ := SessionStore.Get(r, "cookie-session") 427 session.Values["auth_user"] = username 428 session.Values["admin"] = isAdmin 429 session.Save(r, w) 430 http.Redirect(w, r, "/my_site", http.StatusSeeOther) 431 return 432 } else { 433 data := struct { 434 Error string 435 Config Config 436 }{err.Error(), c} 437 w.WriteHeader(401) 438 err := t.ExecuteTemplate(w, "login.html", data) 439 if err != nil { 440 serverError(w, err) 441 return 442 } 443 } 444 } 445 } 446 447 func logoutHandler(w http.ResponseWriter, r *http.Request) { 448 session, _ := SessionStore.Get(r, "cookie-session") 449 impers, ok := session.Values["impersonating_user"].(string) 450 if ok { 451 session.Values["auth_user"] = impers 452 session.Values["impersonating_user"] = nil // TODO expire this automatically 453 // session.Values["admin"] = nil // TODO fix admin 454 } else { 455 session.Options.MaxAge = -1 456 } 457 session.Save(r, w) 458 http.Redirect(w, r, "/", http.StatusSeeOther) 459 } 460 461 const ok = "-0123456789abcdefghijklmnopqrstuvwxyz" 462 463 // TODO improve this 464 var bannedUsernames = []string{"www", "proxy", "mtg", "lists", "grafana"} 465 466 func registerHandler(w http.ResponseWriter, r *http.Request) { 467 if r.Method == "GET" { 468 data := struct { 469 Errors []string 470 Config Config 471 }{nil, c} 472 err := t.ExecuteTemplate(w, "register.html", data) 473 if err != nil { 474 serverError(w, err) 475 return 476 } 477 } else if r.Method == "POST" { 478 r.ParseForm() 479 email := strings.ToLower(r.Form.Get("email")) 480 password := r.Form.Get("password") 481 errors := []string{} 482 if r.Form.Get("password") != r.Form.Get("password2") { 483 errors = append(errors, "Passwords don't match") 484 } 485 if len(password) < 6 { 486 errors = append(errors, "Password is too short") 487 } 488 username := strings.ToLower(r.Form.Get("username")) 489 err := isOkUsername(username) 490 if err != nil { 491 errors = append(errors, err.Error()) 492 } 493 _, err = os.Stat(getUserDirectory(username)) 494 if !os.IsNotExist(err) { 495 // Don't allow user to create account if folder dne 496 errors = append(errors, "Invalid username") 497 } 498 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) // TODO handle error 499 if err != nil { 500 serverError(w, err) 501 return 502 } 503 reference := r.Form.Get("reference") 504 if len(errors) == 0 { 505 _, err = DB.Exec("insert into user (username, email, password_hash, reference) values ($1, $2, $3, $4)", username, email, string(hashedPassword), reference) 506 if err != nil { 507 errors = append(errors, "Username or email is already used") 508 } 509 } 510 if len(errors) > 0 { 511 data := struct { 512 Config Config 513 Errors []string 514 }{c, errors} 515 w.WriteHeader(400) 516 t.ExecuteTemplate(w, "register.html", data) 517 } else { 518 data := struct { 519 Config Config 520 Message string 521 Title string 522 }{c, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"} 523 t.ExecuteTemplate(w, "message.html", data) 524 } 525 } 526 } 527 528 func deleteFileHandler(w http.ResponseWriter, r *http.Request) { 529 user := getAuthUser(r) 530 if !user.LoggedIn { 531 renderDefaultError(w, http.StatusForbidden) 532 return 533 } 534 filePath := safeGetFilePath(user.Username, r.URL.Path[len("/delete/"):]) 535 if r.Method == "POST" { 536 os.Remove(filePath) // TODO handle error 537 } 538 http.Redirect(w, r, "/my_site", http.StatusSeeOther) 539 } 540 541 func adminHandler(w http.ResponseWriter, r *http.Request) { 542 user := getAuthUser(r) 543 if !user.IsAdmin { 544 renderDefaultError(w, http.StatusForbidden) 545 return 546 } 547 allUsers, err := getUsers() 548 if err != nil { 549 log.Println(err) 550 renderDefaultError(w, http.StatusInternalServerError) 551 return 552 } 553 data := struct { 554 Users []User 555 AuthUser AuthUser 556 Config Config 557 }{allUsers, user, c} 558 err = t.ExecuteTemplate(w, "admin.html", data) 559 if err != nil { 560 serverError(w, err) 561 return 562 } 563 } 564 565 // Server a user's file 566 // TODO replace with gemini proxy 567 // Here be dragons 568 func userFile(w http.ResponseWriter, r *http.Request) { 569 var userName string 570 custom := domains[r.Host] 571 if custom != "" { 572 userName = custom 573 } else { 574 userName = cleanPath(strings.Split(r.Host, ".")[0])[1:] // Clean probably unnecessary 575 } 576 unescaped, err := url.QueryUnescape(r.URL.Path) 577 if err != nil { 578 serverError(w, err) 579 return 580 } 581 p := cleanPath(unescaped) 582 fmt.Println(p) 583 var isDir bool 584 fullPath := path.Join(c.FilesDirectory, userName, p) // TODO rename filepath 585 stat, err := os.Stat(fullPath) 586 if stat != nil { 587 isDir = stat.IsDir() 588 } 589 if strings.HasSuffix(p, "index.gmi") { 590 http.Redirect(w, r, path.Dir(p), http.StatusMovedPermanently) 591 return 592 } 593 594 if strings.HasPrefix(p, "/"+HiddenFolder) { 595 renderDefaultError(w, http.StatusForbidden) 596 return 597 } 598 if r.URL.Path == "/gemlog/atom.xml" && os.IsNotExist(err) { 599 w.Header().Set("Content-Type", "application/atom+xml") 600 // TODO set always somehow 601 feed := generateFeedFromUser(userName) 602 atomString := feed.toAtomFeed() 603 io.Copy(w, strings.NewReader(atomString)) 604 return 605 } 606 607 var geminiContent string 608 fullStat, err := os.Stat(path.Join(fullPath, "index.gmi")) 609 if isDir { 610 // redirect slash 611 if !strings.HasSuffix(r.URL.Path, "/") { 612 http.Redirect(w, r, p+"/", http.StatusSeeOther) 613 } 614 if os.IsNotExist(err) { 615 if p == "/gemlog" { 616 geminiContent = generateGemfeedPage(userName) 617 } else { 618 geminiContent = generateFolderPage(fullPath) 619 } 620 } else { 621 fullPath = path.Join(fullPath, "index.gmi") 622 } 623 if fullStat != nil { 624 stat = fullStat // wonky 625 } 626 } 627 if geminiContent == "" && os.IsNotExist(err) { 628 renderDefaultError(w, http.StatusNotFound) 629 return 630 } 631 // Dumb content negotiation 632 _, raw := r.URL.Query()["raw"] 633 acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini") 634 if !raw && !acceptsGemini && (isGemini(fullPath) || geminiContent != "") { 635 var htmlDoc ConvertedGmiDoc 636 if geminiContent == "" { 637 file, _ := os.Open(fullPath) 638 parse, _ := gmi.ParseText(file) 639 htmlDoc = textToHTML(nil, parse) 640 defer file.Close() 641 } else { 642 parse, _ := gmi.ParseText(strings.NewReader(geminiContent)) 643 htmlDoc = textToHTML(nil, parse) 644 } 645 hostname := strings.Split(r.Host, ":")[0] 646 uri := url.URL{ 647 Scheme: "gemini", 648 Host: hostname, 649 Path: p, 650 } 651 if htmlDoc.Title == "" { 652 htmlDoc.Title = userName + p 653 } 654 data := struct { 655 SiteBody template.HTML 656 PageTitle string 657 URI *url.URL 658 GeminiURI *url.URL 659 Config Config 660 }{template.HTML(htmlDoc.Content), htmlDoc.Title, &uri, &uri, c} 661 buff := bytes.NewBuffer([]byte{}) 662 err = t.ExecuteTemplate(buff, "user_page.html", data) 663 if err != nil { 664 serverError(w, err) 665 return 666 } 667 breader := bytes.NewReader(buff.Bytes()) 668 http.ServeContent(w, r, "", stat.ModTime(), breader) 669 } else { 670 http.ServeFile(w, r, fullPath) 671 } 672 } 673 674 func deleteAccountHandler(w http.ResponseWriter, r *http.Request) { 675 user := getAuthUser(r) 676 if r.Method == "POST" { 677 r.ParseForm() 678 validate := r.Form.Get("validate-delete") 679 if validate == user.Username { 680 err := deleteUser(user.Username) 681 if err != nil { 682 log.Println(err) 683 renderDefaultError(w, http.StatusInternalServerError) 684 return 685 } 686 logoutHandler(w, r) 687 } else { 688 http.Redirect(w, r, "/me", http.StatusSeeOther) 689 } 690 } 691 } 692 693 func resetPasswordHandler(w http.ResponseWriter, r *http.Request) { 694 user := getAuthUser(r) 695 data := struct { 696 Config Config 697 AuthUser AuthUser 698 Error string 699 }{c, user, ""} 700 if r.Method == "GET" { 701 err := t.ExecuteTemplate(w, "reset_pass.html", data) 702 if err != nil { 703 serverError(w, err) 704 return 705 } 706 } else if r.Method == "POST" { 707 r.ParseForm() 708 enteredCurrPass := r.Form.Get("password") 709 password1 := r.Form.Get("new_password1") 710 password2 := r.Form.Get("new_password2") 711 if password1 != password2 { 712 data.Error = "New passwords do not match" 713 } else if len(password1) < 6 { 714 data.Error = "Password is too short" 715 } else { 716 err := checkAuth(user.Username, enteredCurrPass) 717 if err == nil { 718 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password1), 8) 719 if err != nil { 720 serverError(w, err) 721 return 722 } 723 _, err = DB.Exec("update user set password_hash = ? where username = ?", hashedPassword, user.Username) 724 if err != nil { 725 serverError(w, err) 726 return 727 } 728 log.Printf("User %s reset password", user.Username) 729 http.Redirect(w, r, "/me", http.StatusSeeOther) 730 return 731 } else { 732 data.Error = "That's not your current password" 733 } 734 } 735 err := t.ExecuteTemplate(w, "reset_pass.html", data) 736 if err != nil { 737 serverError(w, err) 738 return 739 } 740 } 741 } 742 743 // unused atm 744 func proxyFinger(w http.ResponseWriter, r *http.Request) { 745 // Create connection 746 var output string 747 var errStr string 748 if r.Method == "POST" { 749 r.ParseForm() 750 query := r.Form.Get("query") 751 args := strings.Split(query, "@") 752 if len(args) != 2 { 753 errStr = "Not enough args" 754 goto render 755 } 756 // get data 757 conn, err := net.DialTimeout("tcp", args[1]+":79", time.Second*60) 758 if err != nil { 759 errStr = "Error calling service" 760 goto render 761 } 762 data := make([]byte, 16000) // 16kb 763 defer conn.Close() 764 conn.Write([]byte(args[0] + "\r\n")) 765 _, err = conn.Read(data) 766 if err != nil { 767 errStr = "Error calling service" 768 goto render 769 } 770 data = bytes.Trim(data, "\x00") 771 output = string(data) 772 } 773 render: 774 if errStr != "" { 775 w.WriteHeader(500) // lazy 776 } 777 d := struct { 778 Output string 779 Error string 780 }{output, errStr} 781 t.ExecuteTemplate(w, "fingerproxy.html", d) 782 } 783 784 func adminUserHandler(w http.ResponseWriter, r *http.Request) { 785 user := getAuthUser(r) 786 if r.Method == "POST" { 787 if !user.IsAdmin { 788 renderDefaultError(w, http.StatusForbidden) 789 return 790 } 791 components := strings.Split(r.URL.Path, "/") 792 if len(components) < 5 { 793 renderError(w, "Invalid action", http.StatusBadRequest) 794 return 795 } 796 userName := components[3] 797 action := components[4] 798 var err error 799 if action == "activate" { 800 err = activateUser(userName) 801 } else if action == "impersonate" { 802 if user.ImpersonatingUser != "" { 803 // Don't allow nested impersonation 804 renderError(w, "Cannot nest impersonation, log out from impersonated user first.", 400) 805 return 806 } 807 session, _ := SessionStore.Get(r, "cookie-session") 808 session.Values["auth_user"] = userName 809 session.Values["impersonating_user"] = user.Username 810 session.Save(r, w) 811 log.Printf("User %s impersonated %s", user.Username, userName) 812 http.Redirect(w, r, "/", http.StatusSeeOther) 813 return 814 } 815 if err != nil { 816 log.Println(err) 817 renderDefaultError(w, http.StatusInternalServerError) 818 return 819 } 820 http.Redirect(w, r, "/admin", http.StatusSeeOther) 821 } 822 } 823 824 func checkDomainHandler(w http.ResponseWriter, r *http.Request) { 825 domain := r.URL.Query().Get("domain") 826 if domain != "" && domains[domain] != "" { 827 w.Write([]byte(domain)) 828 return 829 } 830 if domain == c.Host || strings.HasSuffix(domain, "."+c.Host) { 831 w.Write([]byte(domain)) 832 } 833 http.Error(w, "Not Found", 404) 834 } 835 836 func proxyGemini(w http.ResponseWriter, r *http.Request) { 837 errorMsg := "proxy.flounder.online has been deprecated. Consider using another gemini proxy" 838 renderError(w, errorMsg, 410) 839 } 840 841 func runHTTPServer() { 842 log.Printf("Running http server with hostname %s on port %d.", c.Host, c.HttpPort) 843 var err error 844 t = template.New("main").Funcs(template.FuncMap{ 845 "unixTime": time.Unix, 846 "parent": path.Dir, "hasSuffix": strings.HasSuffix, 847 "safeGeminiURL": func(u string) template.URL { 848 if strings.HasPrefix(u, "gemini://") { 849 return template.URL(u) 850 } 851 return "" 852 }}) 853 t, err = t.ParseGlob(path.Join(c.TemplatesDirectory, "*.html")) 854 if err != nil { 855 log.Fatal(err) 856 } 857 serveMux := http.NewServeMux() 858 859 s := strings.SplitN(c.Host, ":", 2) 860 hostname := s[0] 861 862 serveMux.HandleFunc(hostname+"/", rootHandler) 863 serveMux.HandleFunc(hostname+"/my_site", mySiteHandler) 864 serveMux.HandleFunc(hostname+"/me", myAccountHandler) 865 serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler) 866 serveMux.HandleFunc(hostname+"/admin", adminHandler) 867 serveMux.HandleFunc(hostname+"/edit/", editFileHandler) 868 serveMux.HandleFunc(hostname+"/updates/", updatesHandler) 869 serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler) 870 serveMux.Handle(hostname+"/login", limit(http.HandlerFunc(loginHandler))) 871 serveMux.Handle(hostname+"/register", limit(http.HandlerFunc(registerHandler))) 872 serveMux.HandleFunc(hostname+"/logout", logoutHandler) 873 serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler) 874 serveMux.HandleFunc(hostname+"/delete-account", deleteAccountHandler) 875 serveMux.HandleFunc(hostname+"/reset-password", resetPasswordHandler) 876 877 // Used by Caddy 878 serveMux.HandleFunc(hostname+"/check-domain", checkDomainHandler) 879 880 // admin commands 881 serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler) 882 883 wrapped := handlers.CustomLoggingHandler(log.Writer(), serveMux, logFormatter) 884 885 // Deprecated 886 serveMux.HandleFunc("proxy."+hostname+"/", proxyGemini) 887 serveMux.HandleFunc("/", userFile) 888 srv := &http.Server{ 889 ReadTimeout: 5 * time.Second, 890 WriteTimeout: 10 * time.Second, 891 IdleTimeout: 120 * time.Second, 892 Addr: fmt.Sprintf(":%d", c.HttpPort), 893 Handler: wrapped, 894 } 895 log.Fatal(srv.ListenAndServe()) 896 }