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