1package httpd 2 3import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "html/template" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "path" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/go-chi/render" 20 "github.com/rs/xid" 21 22 "github.com/drakkan/sftpgo/v2/common" 23 "github.com/drakkan/sftpgo/v2/dataprovider" 24 "github.com/drakkan/sftpgo/v2/mfa" 25 "github.com/drakkan/sftpgo/v2/sdk" 26 "github.com/drakkan/sftpgo/v2/smtp" 27 "github.com/drakkan/sftpgo/v2/util" 28 "github.com/drakkan/sftpgo/v2/version" 29 "github.com/drakkan/sftpgo/v2/vfs" 30) 31 32const ( 33 templateClientDir = "webclient" 34 templateClientBase = "base.html" 35 templateClientBaseLogin = "baselogin.html" 36 templateClientLogin = "login.html" 37 templateClientFiles = "files.html" 38 templateClientMessage = "message.html" 39 templateClientProfile = "profile.html" 40 templateClientChangePwd = "changepassword.html" 41 templateClientTwoFactor = "twofactor.html" 42 templateClientTwoFactorRecovery = "twofactor-recovery.html" 43 templateClientMFA = "mfa.html" 44 templateClientEditFile = "editfile.html" 45 templateClientShare = "share.html" 46 templateClientShares = "shares.html" 47 templateClientViewPDF = "viewpdf.html" 48 pageClientFilesTitle = "My Files" 49 pageClientSharesTitle = "Shares" 50 pageClientProfileTitle = "My Profile" 51 pageClientChangePwdTitle = "Change password" 52 pageClient2FATitle = "Two-factor auth" 53 pageClientEditFileTitle = "Edit file" 54 pageClientForgotPwdTitle = "SFTPGo WebClient - Forgot password" 55 pageClientResetPwdTitle = "SFTPGo WebClient - Reset password" 56) 57 58// condResult is the result of an HTTP request precondition check. 59// See https://tools.ietf.org/html/rfc7232 section 3. 60type condResult int 61 62const ( 63 condNone condResult = iota 64 condTrue 65 condFalse 66) 67 68var ( 69 clientTemplates = make(map[string]*template.Template) 70 unixEpochTime = time.Unix(0, 0) 71) 72 73// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). 74func isZeroTime(t time.Time) bool { 75 return t.IsZero() || t.Equal(unixEpochTime) 76} 77 78type baseClientPage struct { 79 Title string 80 CurrentURL string 81 FilesURL string 82 SharesURL string 83 ShareURL string 84 ProfileURL string 85 ChangePwdURL string 86 StaticURL string 87 LogoutURL string 88 MFAURL string 89 MFATitle string 90 FilesTitle string 91 SharesTitle string 92 ProfileTitle string 93 Version string 94 CSRFToken string 95 LoggedUser *dataprovider.User 96} 97 98type dirMapping struct { 99 DirName string 100 Href string 101} 102 103type viewPDFPage struct { 104 Title string 105 URL string 106 StaticURL string 107} 108 109type editFilePage struct { 110 baseClientPage 111 CurrentDir string 112 Path string 113 Name string 114 ReadOnly bool 115 Data string 116} 117 118type filesPage struct { 119 baseClientPage 120 CurrentDir string 121 DirsURL string 122 DownloadURL string 123 ViewPDFURL string 124 CanAddFiles bool 125 CanCreateDirs bool 126 CanRename bool 127 CanDelete bool 128 CanDownload bool 129 CanShare bool 130 Error string 131 Paths []dirMapping 132} 133 134type clientMessagePage struct { 135 baseClientPage 136 Error string 137 Success string 138} 139 140type clientProfilePage struct { 141 baseClientPage 142 PublicKeys []string 143 CanSubmit bool 144 AllowAPIKeyAuth bool 145 Email string 146 Description string 147 Error string 148} 149 150type changeClientPasswordPage struct { 151 baseClientPage 152 Error string 153} 154 155type clientMFAPage struct { 156 baseClientPage 157 TOTPConfigs []string 158 TOTPConfig sdk.TOTPConfig 159 GenerateTOTPURL string 160 ValidateTOTPURL string 161 SaveTOTPURL string 162 RecCodesURL string 163 Protocols []string 164} 165 166type clientSharesPage struct { 167 baseClientPage 168 Shares []dataprovider.Share 169 BasePublicSharesURL string 170} 171 172type clientSharePage struct { 173 baseClientPage 174 Share *dataprovider.Share 175 Error string 176 IsAdd bool 177} 178 179func getFileObjectURL(baseDir, name string) string { 180 return fmt.Sprintf("%v?path=%v&_=%v", webClientFilesPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix()) 181} 182 183func getFileObjectModTime(t time.Time) string { 184 if isZeroTime(t) { 185 return "" 186 } 187 return t.Format("2006-01-02 15:04") 188} 189 190func loadClientTemplates(templatesPath string) { 191 filesPaths := []string{ 192 filepath.Join(templatesPath, templateClientDir, templateClientBase), 193 filepath.Join(templatesPath, templateClientDir, templateClientFiles), 194 } 195 editFilePath := []string{ 196 filepath.Join(templatesPath, templateClientDir, templateClientBase), 197 filepath.Join(templatesPath, templateClientDir, templateClientEditFile), 198 } 199 sharesPaths := []string{ 200 filepath.Join(templatesPath, templateClientDir, templateClientBase), 201 filepath.Join(templatesPath, templateClientDir, templateClientShares), 202 } 203 sharePaths := []string{ 204 filepath.Join(templatesPath, templateClientDir, templateClientBase), 205 filepath.Join(templatesPath, templateClientDir, templateClientShare), 206 } 207 profilePaths := []string{ 208 filepath.Join(templatesPath, templateClientDir, templateClientBase), 209 filepath.Join(templatesPath, templateClientDir, templateClientProfile), 210 } 211 changePwdPaths := []string{ 212 filepath.Join(templatesPath, templateClientDir, templateClientBase), 213 filepath.Join(templatesPath, templateClientDir, templateClientChangePwd), 214 } 215 loginPath := []string{ 216 filepath.Join(templatesPath, templateClientDir, templateClientBaseLogin), 217 filepath.Join(templatesPath, templateClientDir, templateClientLogin), 218 } 219 messagePath := []string{ 220 filepath.Join(templatesPath, templateClientDir, templateClientBase), 221 filepath.Join(templatesPath, templateClientDir, templateClientMessage), 222 } 223 mfaPath := []string{ 224 filepath.Join(templatesPath, templateClientDir, templateClientBase), 225 filepath.Join(templatesPath, templateClientDir, templateClientMFA), 226 } 227 twoFactorPath := []string{ 228 filepath.Join(templatesPath, templateClientDir, templateClientBaseLogin), 229 filepath.Join(templatesPath, templateClientDir, templateClientTwoFactor), 230 } 231 twoFactorRecoveryPath := []string{ 232 filepath.Join(templatesPath, templateClientDir, templateClientBaseLogin), 233 filepath.Join(templatesPath, templateClientDir, templateClientTwoFactorRecovery), 234 } 235 forgotPwdPaths := []string{ 236 filepath.Join(templatesPath, templateCommonDir, templateForgotPassword), 237 } 238 resetPwdPaths := []string{ 239 filepath.Join(templatesPath, templateCommonDir, templateResetPassword), 240 } 241 viewPDFPaths := []string{ 242 filepath.Join(templatesPath, templateClientDir, templateClientViewPDF), 243 } 244 245 filesTmpl := util.LoadTemplate(nil, filesPaths...) 246 profileTmpl := util.LoadTemplate(nil, profilePaths...) 247 changePwdTmpl := util.LoadTemplate(nil, changePwdPaths...) 248 loginTmpl := util.LoadTemplate(nil, loginPath...) 249 messageTmpl := util.LoadTemplate(nil, messagePath...) 250 mfaTmpl := util.LoadTemplate(nil, mfaPath...) 251 twoFactorTmpl := util.LoadTemplate(nil, twoFactorPath...) 252 twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPath...) 253 editFileTmpl := util.LoadTemplate(nil, editFilePath...) 254 sharesTmpl := util.LoadTemplate(nil, sharesPaths...) 255 shareTmpl := util.LoadTemplate(nil, sharePaths...) 256 forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...) 257 resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...) 258 viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...) 259 260 clientTemplates[templateClientFiles] = filesTmpl 261 clientTemplates[templateClientProfile] = profileTmpl 262 clientTemplates[templateClientChangePwd] = changePwdTmpl 263 clientTemplates[templateClientLogin] = loginTmpl 264 clientTemplates[templateClientMessage] = messageTmpl 265 clientTemplates[templateClientMFA] = mfaTmpl 266 clientTemplates[templateClientTwoFactor] = twoFactorTmpl 267 clientTemplates[templateClientTwoFactorRecovery] = twoFactorRecoveryTmpl 268 clientTemplates[templateClientEditFile] = editFileTmpl 269 clientTemplates[templateClientShares] = sharesTmpl 270 clientTemplates[templateClientShare] = shareTmpl 271 clientTemplates[templateForgotPassword] = forgotPwdTmpl 272 clientTemplates[templateResetPassword] = resetPwdTmpl 273 clientTemplates[templateClientViewPDF] = viewPDFTmpl 274} 275 276func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage { 277 var csrfToken string 278 if currentURL != "" { 279 csrfToken = createCSRFToken() 280 } 281 v := version.Get() 282 283 return baseClientPage{ 284 Title: title, 285 CurrentURL: currentURL, 286 FilesURL: webClientFilesPath, 287 SharesURL: webClientSharesPath, 288 ShareURL: webClientSharePath, 289 ProfileURL: webClientProfilePath, 290 ChangePwdURL: webChangeClientPwdPath, 291 StaticURL: webStaticFilesPath, 292 LogoutURL: webClientLogoutPath, 293 MFAURL: webClientMFAPath, 294 MFATitle: pageClient2FATitle, 295 FilesTitle: pageClientFilesTitle, 296 SharesTitle: pageClientSharesTitle, 297 ProfileTitle: pageClientProfileTitle, 298 Version: fmt.Sprintf("%v-%v", v.Version, v.CommitHash), 299 CSRFToken: csrfToken, 300 LoggedUser: getUserFromToken(r), 301 } 302} 303 304func renderClientForgotPwdPage(w http.ResponseWriter, error string) { 305 data := forgotPwdPage{ 306 CurrentURL: webClientForgotPwdPath, 307 Error: error, 308 CSRFToken: createCSRFToken(), 309 StaticURL: webStaticFilesPath, 310 Title: pageClientForgotPwdTitle, 311 } 312 renderClientTemplate(w, templateForgotPassword, data) 313} 314 315func renderClientResetPwdPage(w http.ResponseWriter, error string) { 316 data := resetPwdPage{ 317 CurrentURL: webClientResetPwdPath, 318 Error: error, 319 CSRFToken: createCSRFToken(), 320 StaticURL: webStaticFilesPath, 321 Title: pageClientResetPwdTitle, 322 } 323 renderClientTemplate(w, templateResetPassword, data) 324} 325 326func renderClientTemplate(w http.ResponseWriter, tmplName string, data interface{}) { 327 err := clientTemplates[tmplName].ExecuteTemplate(w, tmplName, data) 328 if err != nil { 329 http.Error(w, err.Error(), http.StatusInternalServerError) 330 } 331} 332 333func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) { 334 var errorString string 335 if body != "" { 336 errorString = body + " " 337 } 338 if err != nil { 339 errorString += err.Error() 340 } 341 data := clientMessagePage{ 342 baseClientPage: getBaseClientPageData(title, "", r), 343 Error: errorString, 344 Success: message, 345 } 346 w.WriteHeader(statusCode) 347 renderClientTemplate(w, templateClientMessage, data) 348} 349 350func renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) { 351 renderClientMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "") 352} 353 354func renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) { 355 renderClientMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "") 356} 357 358func renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, body string) { 359 renderClientMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body) 360} 361 362func renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) { 363 renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "") 364} 365 366func renderClientTwoFactorPage(w http.ResponseWriter, error string) { 367 data := twoFactorPage{ 368 CurrentURL: webClientTwoFactorPath, 369 Version: version.Get().Version, 370 Error: error, 371 CSRFToken: createCSRFToken(), 372 StaticURL: webStaticFilesPath, 373 RecoveryURL: webClientTwoFactorRecoveryPath, 374 } 375 renderClientTemplate(w, templateTwoFactor, data) 376} 377 378func renderClientTwoFactorRecoveryPage(w http.ResponseWriter, error string) { 379 data := twoFactorPage{ 380 CurrentURL: webClientTwoFactorRecoveryPath, 381 Version: version.Get().Version, 382 Error: error, 383 CSRFToken: createCSRFToken(), 384 StaticURL: webStaticFilesPath, 385 } 386 renderClientTemplate(w, templateTwoFactorRecovery, data) 387} 388 389func renderClientMFAPage(w http.ResponseWriter, r *http.Request) { 390 data := clientMFAPage{ 391 baseClientPage: getBaseClientPageData(pageMFATitle, webClientMFAPath, r), 392 TOTPConfigs: mfa.GetAvailableTOTPConfigNames(), 393 GenerateTOTPURL: webClientTOTPGeneratePath, 394 ValidateTOTPURL: webClientTOTPValidatePath, 395 SaveTOTPURL: webClientTOTPSavePath, 396 RecCodesURL: webClientRecoveryCodesPath, 397 Protocols: dataprovider.MFAProtocols, 398 } 399 user, err := dataprovider.UserExists(data.LoggedUser.Username) 400 if err != nil { 401 renderInternalServerErrorPage(w, r, err) 402 return 403 } 404 data.TOTPConfig = user.Filters.TOTPConfig 405 renderClientTemplate(w, templateClientMFA, data) 406} 407 408func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) { 409 data := editFilePage{ 410 baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r), 411 Path: fileName, 412 Name: path.Base(fileName), 413 CurrentDir: path.Dir(fileName), 414 ReadOnly: readOnly, 415 Data: fileData, 416 } 417 418 renderClientTemplate(w, templateClientEditFile, data) 419} 420 421func renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share, 422 error string, isAdd bool) { 423 currentURL := webClientSharePath 424 title := "Add a new share" 425 if !isAdd { 426 currentURL = fmt.Sprintf("%v/%v", webClientSharePath, url.PathEscape(share.ShareID)) 427 title = "Update share" 428 } 429 data := clientSharePage{ 430 baseClientPage: getBaseClientPageData(title, currentURL, r), 431 Share: share, 432 Error: error, 433 IsAdd: isAdd, 434 } 435 436 renderClientTemplate(w, templateClientShare, data) 437} 438 439func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User) { 440 data := filesPage{ 441 baseClientPage: getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r), 442 Error: error, 443 CurrentDir: url.QueryEscape(dirName), 444 DownloadURL: webClientDownloadZipPath, 445 ViewPDFURL: webClientViewPDFPath, 446 DirsURL: webClientDirsPath, 447 CanAddFiles: user.CanAddFilesFromWeb(dirName), 448 CanCreateDirs: user.CanAddDirsFromWeb(dirName), 449 CanRename: user.CanRenameFromWeb(dirName, dirName), 450 CanDelete: user.CanDeleteFromWeb(dirName), 451 CanDownload: user.HasPerm(dataprovider.PermDownload, dirName), 452 CanShare: user.CanManageShares(), 453 } 454 paths := []dirMapping{} 455 if dirName != "/" { 456 paths = append(paths, dirMapping{ 457 DirName: path.Base(dirName), 458 Href: "", 459 }) 460 for { 461 dirName = path.Dir(dirName) 462 if dirName == "/" || dirName == "." { 463 break 464 } 465 paths = append([]dirMapping{{ 466 DirName: path.Base(dirName), 467 Href: getFileObjectURL("/", dirName)}, 468 }, paths...) 469 } 470 } 471 data.Paths = paths 472 renderClientTemplate(w, templateClientFiles, data) 473} 474 475func renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) { 476 data := clientProfilePage{ 477 baseClientPage: getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r), 478 Error: error, 479 } 480 user, err := dataprovider.UserExists(data.LoggedUser.Username) 481 if err != nil { 482 renderClientInternalServerErrorPage(w, r, err) 483 return 484 } 485 data.PublicKeys = user.PublicKeys 486 data.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth 487 data.Email = user.Email 488 data.Description = user.Description 489 data.CanSubmit = user.CanChangeAPIKeyAuth() || user.CanManagePublicKeys() || user.CanChangeInfo() 490 renderClientTemplate(w, templateClientProfile, data) 491} 492 493func renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) { 494 data := changeClientPasswordPage{ 495 baseClientPage: getBaseClientPageData(pageClientChangePwdTitle, webChangeClientPwdPath, r), 496 Error: error, 497 } 498 499 renderClientTemplate(w, templateClientChangePwd, data) 500} 501 502func handleWebClientLogout(w http.ResponseWriter, r *http.Request) { 503 r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) 504 c := jwtTokenClaims{} 505 c.removeCookie(w, r, webBaseClientPath) 506 507 http.Redirect(w, r, webClientLoginPath, http.StatusFound) 508} 509 510func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) { 511 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 512 claims, err := getTokenClaims(r) 513 if err != nil || claims.Username == "" { 514 renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "") 515 return 516 } 517 518 user, err := dataprovider.UserExists(claims.Username) 519 if err != nil { 520 renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") 521 return 522 } 523 524 connID := xid.New().String() 525 connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID) 526 if err := checkHTTPClientUser(&user, r, connectionID); err != nil { 527 renderClientForbiddenPage(w, r, err.Error()) 528 return 529 } 530 connection := &Connection{ 531 BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r), 532 r.RemoteAddr, user), 533 request: r, 534 } 535 common.Connections.Add(connection) 536 defer common.Connections.Remove(connection.GetID()) 537 538 name := "/" 539 if _, ok := r.URL.Query()["path"]; ok { 540 name = util.CleanPath(r.URL.Query().Get("path")) 541 } 542 543 files := r.URL.Query().Get("files") 544 var filesList []string 545 err = json.Unmarshal([]byte(files), &filesList) 546 if err != nil { 547 renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "") 548 return 549 } 550 551 w.Header().Set("Content-Disposition", "attachment; filename=\"sftpgo-download.zip\"") 552 renderCompressedFiles(w, connection, name, filesList, nil) 553} 554 555func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) { 556 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 557 claims, err := getTokenClaims(r) 558 if err != nil || claims.Username == "" { 559 sendAPIResponse(w, r, nil, "invalid token claims", http.StatusForbidden) 560 return 561 } 562 563 user, err := dataprovider.UserExists(claims.Username) 564 if err != nil { 565 sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err)) 566 return 567 } 568 569 connID := xid.New().String() 570 connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID) 571 if err := checkHTTPClientUser(&user, r, connectionID); err != nil { 572 sendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden) 573 return 574 } 575 connection := &Connection{ 576 BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r), 577 r.RemoteAddr, user), 578 request: r, 579 } 580 common.Connections.Add(connection) 581 defer common.Connections.Remove(connection.GetID()) 582 583 name := "/" 584 if _, ok := r.URL.Query()["path"]; ok { 585 name = util.CleanPath(r.URL.Query().Get("path")) 586 } 587 588 contents, err := connection.ReadDir(name) 589 if err != nil { 590 sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err)) 591 return 592 } 593 594 results := make([]map[string]string, 0, len(contents)) 595 for _, info := range contents { 596 res := make(map[string]string) 597 res["url"] = getFileObjectURL(name, info.Name()) 598 editURL := "" 599 if info.IsDir() { 600 res["type"] = "1" 601 res["size"] = "" 602 } else { 603 res["type"] = "2" 604 if info.Mode()&os.ModeSymlink != 0 { 605 res["size"] = "" 606 } else { 607 res["size"] = util.ByteCountIEC(info.Size()) 608 if info.Size() < httpdMaxEditFileSize { 609 editURL = strings.Replace(res["url"], webClientFilesPath, webClientEditFilePath, 1) 610 } 611 } 612 } 613 res["meta"] = fmt.Sprintf("%v_%v", res["type"], info.Name()) 614 res["name"] = info.Name() 615 res["last_modified"] = getFileObjectModTime(info.ModTime()) 616 res["edit_url"] = editURL 617 results = append(results, res) 618 } 619 620 render.JSON(w, r, results) 621} 622 623func handleClientGetFiles(w http.ResponseWriter, r *http.Request) { 624 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 625 claims, err := getTokenClaims(r) 626 if err != nil || claims.Username == "" { 627 renderClientForbiddenPage(w, r, "Invalid token claims") 628 return 629 } 630 631 user, err := dataprovider.UserExists(claims.Username) 632 if err != nil { 633 renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") 634 return 635 } 636 637 connID := xid.New().String() 638 connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID) 639 if err := checkHTTPClientUser(&user, r, connectionID); err != nil { 640 renderClientForbiddenPage(w, r, err.Error()) 641 return 642 } 643 connection := &Connection{ 644 BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r), 645 r.RemoteAddr, user), 646 request: r, 647 } 648 common.Connections.Add(connection) 649 defer common.Connections.Remove(connection.GetID()) 650 651 name := "/" 652 if _, ok := r.URL.Query()["path"]; ok { 653 name = util.CleanPath(r.URL.Query().Get("path")) 654 } 655 var info os.FileInfo 656 if name == "/" { 657 info = vfs.NewFileInfo(name, true, 0, time.Now(), false) 658 } else { 659 info, err = connection.Stat(name, 0) 660 } 661 if err != nil { 662 renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err), user) 663 return 664 } 665 if info.IsDir() { 666 renderFilesPage(w, r, name, "", user) 667 return 668 } 669 inline := r.URL.Query().Get("inline") != "" 670 if status, err := downloadFile(w, r, connection, name, info, inline); err != nil && status != 0 { 671 if status > 0 { 672 if status == http.StatusRequestedRangeNotSatisfiable { 673 renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "") 674 return 675 } 676 renderFilesPage(w, r, path.Dir(name), err.Error(), user) 677 } 678 } 679} 680 681func handleClientEditFile(w http.ResponseWriter, r *http.Request) { 682 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 683 claims, err := getTokenClaims(r) 684 if err != nil || claims.Username == "" { 685 renderClientForbiddenPage(w, r, "Invalid token claims") 686 return 687 } 688 689 user, err := dataprovider.UserExists(claims.Username) 690 if err != nil { 691 renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") 692 return 693 } 694 695 connID := xid.New().String() 696 connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID) 697 if err := checkHTTPClientUser(&user, r, connectionID); err != nil { 698 renderClientForbiddenPage(w, r, err.Error()) 699 return 700 } 701 connection := &Connection{ 702 BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r), 703 r.RemoteAddr, user), 704 request: r, 705 } 706 common.Connections.Add(connection) 707 defer common.Connections.Remove(connection.GetID()) 708 709 name := util.CleanPath(r.URL.Query().Get("path")) 710 info, err := connection.Stat(name, 0) 711 if err != nil { 712 renderClientMessagePage(w, r, fmt.Sprintf("Unable to stat file %#v", name), "", 713 getRespStatus(err), nil, "") 714 return 715 } 716 if info.IsDir() { 717 renderClientMessagePage(w, r, fmt.Sprintf("The path %#v does not point to a file", name), "", 718 http.StatusBadRequest, nil, "") 719 return 720 } 721 if info.Size() > httpdMaxEditFileSize { 722 renderClientMessagePage(w, r, fmt.Sprintf("The file size %v for %#v exceeds the maximum allowed size", 723 util.ByteCountIEC(info.Size()), name), "", http.StatusBadRequest, nil, "") 724 return 725 } 726 727 reader, err := connection.getFileReader(name, 0, r.Method) 728 if err != nil { 729 renderClientMessagePage(w, r, fmt.Sprintf("Unable to get a reader for the file %#v", name), "", 730 getRespStatus(err), nil, "") 731 return 732 } 733 defer reader.Close() 734 735 var b bytes.Buffer 736 _, err = io.Copy(&b, reader) 737 if err != nil { 738 renderClientMessagePage(w, r, fmt.Sprintf("Unable to read the file %#v", name), "", http.StatusInternalServerError, 739 nil, "") 740 return 741 } 742 743 renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient)) 744} 745 746func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) { 747 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 748 share := &dataprovider.Share{Scope: dataprovider.ShareScopeRead} 749 dirName := "/" 750 if _, ok := r.URL.Query()["path"]; ok { 751 dirName = util.CleanPath(r.URL.Query().Get("path")) 752 } 753 754 if _, ok := r.URL.Query()["files"]; ok { 755 files := r.URL.Query().Get("files") 756 var filesList []string 757 err := json.Unmarshal([]byte(files), &filesList) 758 if err != nil { 759 renderClientMessagePage(w, r, "Invalid share list", "", http.StatusBadRequest, err, "") 760 return 761 } 762 for _, f := range filesList { 763 if f != "" { 764 share.Paths = append(share.Paths, path.Join(dirName, f)) 765 } 766 } 767 } 768 769 renderAddUpdateSharePage(w, r, share, "", true) 770} 771 772func handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) { 773 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 774 claims, err := getTokenClaims(r) 775 if err != nil || claims.Username == "" { 776 renderClientForbiddenPage(w, r, "Invalid token claims") 777 return 778 } 779 shareID := getURLParam(r, "id") 780 share, err := dataprovider.ShareExists(shareID, claims.Username) 781 if err == nil { 782 share.HideConfidentialData() 783 renderAddUpdateSharePage(w, r, &share, "", false) 784 } else if _, ok := err.(*util.RecordNotFoundError); ok { 785 renderClientNotFoundPage(w, r, err) 786 } else { 787 renderClientInternalServerErrorPage(w, r, err) 788 } 789} 790 791func handleClientAddSharePost(w http.ResponseWriter, r *http.Request) { 792 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 793 claims, err := getTokenClaims(r) 794 if err != nil || claims.Username == "" { 795 renderClientForbiddenPage(w, r, "Invalid token claims") 796 return 797 } 798 share, err := getShareFromPostFields(r) 799 if err != nil { 800 renderAddUpdateSharePage(w, r, share, err.Error(), true) 801 return 802 } 803 if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil { 804 renderClientForbiddenPage(w, r, err.Error()) 805 return 806 } 807 share.ID = 0 808 share.ShareID = util.GenerateUniqueID() 809 share.LastUseAt = 0 810 share.Username = claims.Username 811 err = dataprovider.AddShare(share, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)) 812 if err == nil { 813 http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther) 814 } else { 815 renderAddUpdateSharePage(w, r, share, err.Error(), true) 816 } 817} 818 819func handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) { 820 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 821 claims, err := getTokenClaims(r) 822 if err != nil || claims.Username == "" { 823 renderClientForbiddenPage(w, r, "Invalid token claims") 824 return 825 } 826 shareID := getURLParam(r, "id") 827 share, err := dataprovider.ShareExists(shareID, claims.Username) 828 if _, ok := err.(*util.RecordNotFoundError); ok { 829 renderClientNotFoundPage(w, r, err) 830 return 831 } else if err != nil { 832 renderClientInternalServerErrorPage(w, r, err) 833 return 834 } 835 updatedShare, err := getShareFromPostFields(r) 836 if err != nil { 837 renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false) 838 return 839 } 840 if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil { 841 renderClientForbiddenPage(w, r, err.Error()) 842 return 843 } 844 updatedShare.ShareID = shareID 845 updatedShare.Username = claims.Username 846 if updatedShare.Password == redactedSecret { 847 updatedShare.Password = share.Password 848 } 849 err = dataprovider.UpdateShare(updatedShare, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)) 850 if err == nil { 851 http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther) 852 } else { 853 renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false) 854 } 855} 856 857func handleClientGetShares(w http.ResponseWriter, r *http.Request) { 858 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 859 claims, err := getTokenClaims(r) 860 if err != nil || claims.Username == "" { 861 renderClientForbiddenPage(w, r, "Invalid token claims") 862 return 863 } 864 limit := defaultQueryLimit 865 if _, ok := r.URL.Query()["qlimit"]; ok { 866 var err error 867 limit, err = strconv.Atoi(r.URL.Query().Get("qlimit")) 868 if err != nil { 869 limit = defaultQueryLimit 870 } 871 } 872 shares := make([]dataprovider.Share, 0, limit) 873 for { 874 s, err := dataprovider.GetShares(limit, len(shares), dataprovider.OrderASC, claims.Username) 875 if err != nil { 876 renderInternalServerErrorPage(w, r, err) 877 return 878 } 879 shares = append(shares, s...) 880 if len(s) < limit { 881 break 882 } 883 } 884 data := clientSharesPage{ 885 baseClientPage: getBaseClientPageData(pageClientSharesTitle, webClientSharesPath, r), 886 Shares: shares, 887 BasePublicSharesURL: webClientPubSharesPath, 888 } 889 renderClientTemplate(w, templateClientShares, data) 890} 891 892func handleClientGetProfile(w http.ResponseWriter, r *http.Request) { 893 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 894 renderClientProfilePage(w, r, "") 895} 896 897func handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) { 898 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 899 renderClientChangePasswordPage(w, r, "") 900} 901 902func handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) { 903 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 904 err := r.ParseForm() 905 if err != nil { 906 renderClientChangePasswordPage(w, r, err.Error()) 907 return 908 } 909 if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil { 910 renderClientForbiddenPage(w, r, err.Error()) 911 return 912 } 913 err = doChangeUserPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"), 914 r.Form.Get("new_password2")) 915 if err != nil { 916 renderClientChangePasswordPage(w, r, err.Error()) 917 return 918 } 919 handleWebClientLogout(w, r) 920} 921 922func handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) { 923 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 924 err := r.ParseForm() 925 if err != nil { 926 renderClientProfilePage(w, r, err.Error()) 927 return 928 } 929 if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil { 930 renderClientForbiddenPage(w, r, err.Error()) 931 return 932 } 933 claims, err := getTokenClaims(r) 934 if err != nil || claims.Username == "" { 935 renderClientForbiddenPage(w, r, "Invalid token claims") 936 return 937 } 938 user, err := dataprovider.UserExists(claims.Username) 939 if err != nil { 940 renderClientProfilePage(w, r, err.Error()) 941 return 942 } 943 if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() { 944 renderClientForbiddenPage(w, r, "You are not allowed to change anything") 945 return 946 } 947 if user.CanManagePublicKeys() { 948 user.PublicKeys = r.Form["public_keys"] 949 } 950 if user.CanChangeAPIKeyAuth() { 951 user.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0 952 } 953 if user.CanChangeInfo() { 954 user.Email = r.Form.Get("email") 955 user.Description = r.Form.Get("description") 956 } 957 err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr)) 958 if err != nil { 959 renderClientProfilePage(w, r, err.Error()) 960 return 961 } 962 renderClientMessagePage(w, r, "Profile updated", "", http.StatusOK, nil, 963 "Your profile has been successfully updated") 964} 965 966func handleWebClientMFA(w http.ResponseWriter, r *http.Request) { 967 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 968 renderClientMFAPage(w, r) 969} 970 971func handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) { 972 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 973 renderClientTwoFactorPage(w, "") 974} 975 976func handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) { 977 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 978 renderClientTwoFactorRecoveryPage(w, "") 979} 980 981func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) { 982 share := &dataprovider.Share{} 983 if err := r.ParseForm(); err != nil { 984 return share, err 985 } 986 share.Name = r.Form.Get("name") 987 share.Description = r.Form.Get("description") 988 share.Paths = r.Form["paths"] 989 share.Password = r.Form.Get("password") 990 share.AllowFrom = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") 991 scope, err := strconv.Atoi(r.Form.Get("scope")) 992 if err != nil { 993 return share, err 994 } 995 share.Scope = dataprovider.ShareScope(scope) 996 maxTokens, err := strconv.Atoi(r.Form.Get("max_tokens")) 997 if err != nil { 998 return share, err 999 } 1000 share.MaxTokens = maxTokens 1001 expirationDateMillis := int64(0) 1002 expirationDateString := r.Form.Get("expiration_date") 1003 if strings.TrimSpace(expirationDateString) != "" { 1004 expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString) 1005 if err != nil { 1006 return share, err 1007 } 1008 expirationDateMillis = util.GetTimeAsMsSinceEpoch(expirationDate) 1009 } 1010 share.ExpiresAt = expirationDateMillis 1011 return share, nil 1012} 1013 1014func handleWebClientForgotPwd(w http.ResponseWriter, r *http.Request) { 1015 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 1016 if !smtp.IsEnabled() { 1017 renderClientNotFoundPage(w, r, errors.New("this page does not exist")) 1018 return 1019 } 1020 renderClientForgotPwdPage(w, "") 1021} 1022 1023func handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) { 1024 r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 1025 err := r.ParseForm() 1026 if err != nil { 1027 renderClientForgotPwdPage(w, err.Error()) 1028 return 1029 } 1030 if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil { 1031 renderClientForbiddenPage(w, r, err.Error()) 1032 return 1033 } 1034 username := r.Form.Get("username") 1035 err = handleForgotPassword(r, username, false) 1036 if err != nil { 1037 if e, ok := err.(*util.ValidationError); ok { 1038 renderClientForgotPwdPage(w, e.GetErrorString()) 1039 return 1040 } 1041 renderClientForgotPwdPage(w, err.Error()) 1042 return 1043 } 1044 http.Redirect(w, r, webClientResetPwdPath, http.StatusFound) 1045} 1046 1047func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) { 1048 r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) 1049 if !smtp.IsEnabled() { 1050 renderClientNotFoundPage(w, r, errors.New("this page does not exist")) 1051 return 1052 } 1053 renderClientResetPwdPage(w, "") 1054} 1055 1056func handleClientViewPDF(w http.ResponseWriter, r *http.Request) { 1057 r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) 1058 name := r.URL.Query().Get("path") 1059 if name == "" { 1060 renderClientBadRequestPage(w, r, errors.New("no file specified")) 1061 return 1062 } 1063 name = util.CleanPath(name) 1064 data := viewPDFPage{ 1065 Title: path.Base(name), 1066 URL: fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)), 1067 StaticURL: webStaticFilesPath, 1068 } 1069 renderClientTemplate(w, templateClientViewPDF, data) 1070} 1071