From aafc63933a7e213261e28ac7f528cea50b70e1af Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Tue, 14 Mar 2017 06:13:48 +0900 Subject: APIv4: GET /files/{file_id}/public (#5665) --- api4/api.go | 3 +++ api4/file.go | 43 ++++++++++++++++++++++++++++++++ api4/file_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/api4/api.go b/api4/api.go index d5a44731f..29986f551 100644 --- a/api4/api.go +++ b/api4/api.go @@ -49,6 +49,8 @@ type Routes struct { Files *mux.Router // 'api/v4/files' File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}' + PublicFile *mux.Router // 'files/{file_id:[A-Za-z0-9]+}/public' + Commands *mux.Router // 'api/v4/commands' Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}' CommandsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/commands' @@ -120,6 +122,7 @@ func InitApi(full bool) { BaseRoutes.Files = BaseRoutes.ApiRoot.PathPrefix("/files").Subrouter() BaseRoutes.File = BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.PublicFile = BaseRoutes.Root.PathPrefix("/files/{file_id:[A-Za-z0-9]+}/public").Subrouter() BaseRoutes.Commands = BaseRoutes.ApiRoot.PathPrefix("/commands").Subrouter() BaseRoutes.Command = BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter() diff --git a/api4/file.go b/api4/file.go index d3c7f7a7f..6b649918f 100644 --- a/api4/file.go +++ b/api4/file.go @@ -28,6 +28,8 @@ func InitFile() { BaseRoutes.File.Handle("/preview", ApiSessionRequired(getFilePreview)).Methods("GET") BaseRoutes.File.Handle("/info", ApiSessionRequired(getFileInfo)).Methods("GET") + BaseRoutes.PublicFile.Handle("", ApiHandler(getPublicFile)).Methods("GET") + } func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { @@ -216,6 +218,47 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(info.ToJson())) } +func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireFileId() + if c.Err != nil { + return + } + + if !utils.Cfg.FileSettings.EnablePublicLink { + c.Err = model.NewLocAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + info, err := app.GetFileInfo(c.Params.FileId) + if err != nil { + c.Err = err + return + } + + hash := r.URL.Query().Get("h") + + if len(hash) == 0 { + c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if hash != app.GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) { + c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if data, err := app.ReadFile(info.Path); err != nil { + c.Err = err + c.Err.StatusCode = http.StatusNotFound + } else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil { + c.Err = err + return + } +} + func writeFileResponse(filename string, contentType string, bytes []byte, w http.ResponseWriter, r *http.Request) *model.AppError { w.Header().Set("Cache-Control", "max-age=2592000, public") w.Header().Set("Content-Length", strconv.Itoa(len(bytes))) diff --git a/api4/file_test.go b/api4/file_test.go index f257ec074..5e0824d45 100644 --- a/api4/file_test.go +++ b/api4/file_test.go @@ -5,6 +5,8 @@ package api4 import ( "fmt" + "net/http" + "strings" "testing" "time" @@ -396,3 +398,75 @@ func TestGetFileInfo(t *testing.T) { _, resp = th.SystemAdminClient.GetFileInfo(fileId) CheckNoError(t, resp) } + +func TestGetPublicFile(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + channel := th.BasicChannel + + if utils.Cfg.FileSettings.DriverName == "" { + t.Skip("skipping because no file driver is enabled") + } + + enablePublicLink := utils.Cfg.FileSettings.EnablePublicLink + publicLinkSalt := *utils.Cfg.FileSettings.PublicLinkSalt + defer func() { + utils.Cfg.FileSettings.EnablePublicLink = enablePublicLink + *utils.Cfg.FileSettings.PublicLinkSalt = publicLinkSalt + }() + utils.Cfg.FileSettings.EnablePublicLink = true + *utils.Cfg.FileSettings.PublicLinkSalt = GenerateTestId() + + fileId := "" + if data, err := readTestFile("test.png"); err != nil { + t.Fatal(err) + } else { + fileResp, resp := Client.UploadFile(data, channel.Id, "test.png") + CheckNoError(t, resp) + + fileId = fileResp.FileInfos[0].Id + } + + // Hacky way to assign file to a post (usually would be done by CreatePost call) + store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id)) + + result := <-app.Srv.Store.FileInfo().Get(fileId) + info := result.Data.(*model.FileInfo) + link := app.GeneratePublicLink(Client.Url, info) + + // Wait a bit for files to ready + time.Sleep(2 * time.Second) + + if resp, err := http.Get(link); err != nil || resp.StatusCode != http.StatusOK { + t.Log(link) + t.Fatal("failed to get image with public link", err) + } + + if resp, err := http.Get(link[:strings.LastIndex(link, "?")]); err == nil && resp.StatusCode != http.StatusBadRequest { + t.Fatal("should've failed to get image with public link without hash", resp.Status) + } + + utils.Cfg.FileSettings.EnablePublicLink = false + if resp, err := http.Get(link); err == nil && resp.StatusCode != http.StatusNotImplemented { + t.Fatal("should've failed to get image with disabled public link") + } + + // test after the salt has changed + utils.Cfg.FileSettings.EnablePublicLink = true + *utils.Cfg.FileSettings.PublicLinkSalt = GenerateTestId() + + if resp, err := http.Get(link); err == nil && resp.StatusCode != http.StatusBadRequest { + t.Fatal("should've failed to get image with public link after salt changed") + } + + if resp, err := http.Get(link); err == nil && resp.StatusCode != http.StatusBadRequest { + t.Fatal("should've failed to get image with public link after salt changed") + } + + if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { + t.Fatal(err) + } + + cleanupTestFile(info) +} -- cgit v1.2.3-1-g7c22