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