diff --git a/api4/upload.go b/api4/upload.go index cc822ef3e0..236276009a 100644 --- a/api4/upload.go +++ b/api4/upload.go @@ -4,6 +4,9 @@ package api4 import ( + "errors" + "io" + "mime/multipart" "net/http" "github.com/mattermost/mattermost-server/v5/audit" @@ -95,18 +98,38 @@ func uploadData(c *Context, w http.ResponseWriter, r *http.Request) { return } - if r.ContentLength > (us.FileSize - us.FileOffset) { - c.Err = model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length", - nil, "", http.StatusBadRequest) - return - } - if us.UserId != c.App.Session().UserId || !c.App.SessionHasPermissionToChannel(*c.App.Session(), us.ChannelId, model.PERMISSION_UPLOAD_FILE) { c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) return } - info, err := c.App.UploadData(us, r.Body) + boundary, parseErr := parseMultipartRequestHeader(r) + if parseErr != nil && !errors.Is(parseErr, http.ErrNotMultipart) { + c.Err = model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_type", + nil, parseErr.Error(), http.StatusBadRequest) + return + } + + var rd io.Reader + if boundary != "" { + mr := multipart.NewReader(r.Body, boundary) + p, partErr := mr.NextPart() + if partErr != nil { + c.Err = model.NewAppError("uploadData", "api.upload.upload_data.multipart_error", + nil, partErr.Error(), http.StatusBadRequest) + return + } + rd = p + } else { + if r.ContentLength > (us.FileSize - us.FileOffset) { + c.Err = model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length", + nil, "", http.StatusBadRequest) + return + } + rd = r.Body + } + + info, err := c.App.UploadData(us, rd) if err != nil { c.Err = err return diff --git a/api4/upload_test.go b/api4/upload_test.go index f5a1047ae5..92b0c8704a 100644 --- a/api4/upload_test.go +++ b/api4/upload_test.go @@ -6,6 +6,7 @@ package api4 import ( "bytes" "io" + "mime/multipart" "net/http" "testing" @@ -193,7 +194,12 @@ func TestUploadData(t *testing.T) { }) t.Run("bad content-length", func(t *testing.T) { - info, resp := th.Client.UploadData(us.Id, bytes.NewReader(append(data, 0x00))) + u, resp := th.Client.CreateUpload(us) + require.Nil(t, resp.Error) + require.NotEmpty(t, u) + require.Equal(t, http.StatusCreated, resp.StatusCode) + + info, resp := th.Client.UploadData(u.Id, bytes.NewReader(append(data, 0x00))) require.Nil(t, info) require.Error(t, resp.Error) require.Equal(t, "api.upload.upload_data.invalid_content_length", resp.Error.Id) @@ -240,3 +246,101 @@ func TestUploadData(t *testing.T) { require.Equal(t, file, data) }) } + +func TestUploadDataMultipart(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + if *th.App.Config().FileSettings.DriverName == "" { + t.Skip("skipping because no file driver is enabled") + } + + us := &model.UploadSession{ + Id: model.NewId(), + Type: model.UploadTypeAttachment, + CreateAt: model.GetMillis(), + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + Filename: "upload", + FileSize: 8 * 1024 * 1024, + } + us, resp := th.Client.CreateUpload(us) + require.Nil(t, resp.Error) + require.NotNil(t, us) + require.NotEmpty(t, us) + + data := randomBytes(t, int(us.FileSize)) + + genMultipartData := func(t *testing.T, data []byte) (io.Reader, string) { + mpData := &bytes.Buffer{} + mpWriter := multipart.NewWriter(mpData) + part, err := mpWriter.CreateFormFile("data", us.Filename) + require.Nil(t, err) + n, err := part.Write(data) + require.Nil(t, err) + require.Equal(t, len(data), n) + err = mpWriter.Close() + require.Nil(t, err) + return mpData, mpWriter.FormDataContentType() + } + + t.Run("bad content-type", func(t *testing.T) { + info, resp := th.Client.DoUploadFile("/uploads/"+us.Id, data, "multipart/form-data;") + require.Nil(t, info) + require.Error(t, resp.Error) + require.Equal(t, "api.upload.upload_data.invalid_content_type", resp.Error.Id) + }) + + t.Run("success", func(t *testing.T) { + mpData, contentType := genMultipartData(t, data) + + req, err := http.NewRequest("POST", th.Client.ApiUrl+"/uploads/"+us.Id, mpData) + require.Nil(t, err) + req.Header.Set("Content-Type", contentType) + req.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken) + res, err := th.Client.HttpClient.Do(req) + require.Nil(t, err) + info := model.FileInfoFromJson(res.Body) + res.Body.Close() + require.NotEmpty(t, info) + require.Equal(t, us.Filename, info.Name) + + file, resp := th.Client.GetFile(info.Id) + require.Nil(t, resp.Error) + require.Equal(t, file, data) + }) + + t.Run("resume success", func(t *testing.T) { + mpData, contentType := genMultipartData(t, data[:5*1024*1024]) + + u, resp := th.Client.CreateUpload(us) + require.Nil(t, resp.Error) + require.NotNil(t, u) + require.NotEmpty(t, u) + + req, err := http.NewRequest("POST", th.Client.ApiUrl+"/uploads/"+u.Id, mpData) + require.Nil(t, err) + req.Header.Set("Content-Type", contentType) + req.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken) + res, err := th.Client.HttpClient.Do(req) + require.Nil(t, err) + require.Equal(t, http.StatusNoContent, res.StatusCode) + require.Equal(t, int64(0), res.ContentLength) + + mpData, contentType = genMultipartData(t, data[5*1024*1024:]) + + req, err = http.NewRequest("POST", th.Client.ApiUrl+"/uploads/"+u.Id, mpData) + require.Nil(t, err) + req.Header.Set("Content-Type", contentType) + req.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken) + res, err = th.Client.HttpClient.Do(req) + require.Nil(t, err) + info := model.FileInfoFromJson(res.Body) + res.Body.Close() + require.NotEmpty(t, info) + require.Equal(t, u.Filename, info.Name) + + file, resp := th.Client.GetFile(info.Id) + require.Nil(t, resp.Error) + require.Equal(t, file, data) + }) +} diff --git a/i18n/en.json b/i18n/en.json index 221021d41a..bb329f30eb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2734,6 +2734,14 @@ "id": "api.upload.upload_data.invalid_content_length", "translation": "Invalid Content-Length." }, + { + "id": "api.upload.upload_data.invalid_content_type", + "translation": "Invalid Content-Type for multipart upload." + }, + { + "id": "api.upload.upload_data.multipart_error", + "translation": "Failed to process the multipart data." + }, { "id": "api.user.activate_mfa.email_and_ldap_only.app_error", "translation": "MFA is not available for this account type."