github.com/kongr45gpen/mattermost-server@v5.11.1+incompatible/api4/file.go (about) 1 // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package api4 5 6 import ( 7 "bytes" 8 "crypto/subtle" 9 "io" 10 "io/ioutil" 11 "mime" 12 "mime/multipart" 13 "net/http" 14 "net/url" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/mattermost/mattermost-server/app" 20 "github.com/mattermost/mattermost-server/model" 21 "github.com/mattermost/mattermost-server/utils" 22 ) 23 24 const ( 25 FILE_TEAM_ID = "noteam" 26 27 PREVIEW_IMAGE_TYPE = "image/jpeg" 28 THUMBNAIL_IMAGE_TYPE = "image/jpeg" 29 ) 30 31 var UNSAFE_CONTENT_TYPES = [...]string{ 32 "application/javascript", 33 "application/ecmascript", 34 "text/javascript", 35 "text/ecmascript", 36 "application/x-javascript", 37 "text/html", 38 } 39 40 var MEDIA_CONTENT_TYPES = [...]string{ 41 "image/jpeg", 42 "image/png", 43 "image/bmp", 44 "image/gif", 45 "image/tiff", 46 "video/avi", 47 "video/mpeg", 48 "video/mp4", 49 "audio/mpeg", 50 "audio/wav", 51 } 52 53 const maxUploadDrainBytes = (10 * 1024 * 1024) // 10Mb 54 const maxMultipartFormDataBytes = 10 * 1024 // 10Kb 55 56 func (api *API) InitFile() { 57 api.BaseRoutes.Files.Handle("", api.ApiSessionRequired(uploadFileStream)).Methods("POST") 58 api.BaseRoutes.File.Handle("", api.ApiSessionRequiredTrustRequester(getFile)).Methods("GET") 59 api.BaseRoutes.File.Handle("/thumbnail", api.ApiSessionRequiredTrustRequester(getFileThumbnail)).Methods("GET") 60 api.BaseRoutes.File.Handle("/link", api.ApiSessionRequired(getFileLink)).Methods("GET") 61 api.BaseRoutes.File.Handle("/preview", api.ApiSessionRequiredTrustRequester(getFilePreview)).Methods("GET") 62 api.BaseRoutes.File.Handle("/info", api.ApiSessionRequired(getFileInfo)).Methods("GET") 63 64 api.BaseRoutes.PublicFile.Handle("", api.ApiHandler(getPublicFile)).Methods("GET") 65 66 } 67 68 func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { 69 defer io.Copy(ioutil.Discard, r.Body) 70 71 if !*c.App.Config().FileSettings.EnableFileAttachments { 72 c.Err = model.NewAppError("uploadFile", "api.file.attachments.disabled.app_error", nil, "", http.StatusNotImplemented) 73 return 74 } 75 76 if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize { 77 c.Err = model.NewAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge) 78 return 79 } 80 81 now := time.Now() 82 var resStruct *model.FileUploadResponse 83 var appErr *model.AppError 84 85 if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil && err != http.ErrNotMultipart { 86 http.Error(w, err.Error(), http.StatusInternalServerError) 87 return 88 } else if err == http.ErrNotMultipart { 89 defer r.Body.Close() 90 91 c.RequireChannelId() 92 c.RequireFilename() 93 94 if c.Err != nil { 95 return 96 } 97 98 channelId := c.Params.ChannelId 99 filename := c.Params.Filename 100 101 if !c.App.SessionHasPermissionToChannel(c.App.Session, channelId, model.PERMISSION_UPLOAD_FILE) { 102 c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) 103 return 104 } 105 106 resStruct, appErr = c.App.UploadFiles( 107 FILE_TEAM_ID, 108 channelId, 109 c.App.Session.UserId, 110 []io.ReadCloser{r.Body}, 111 []string{filename}, 112 []string{}, 113 now, 114 ) 115 } else { 116 m := r.MultipartForm 117 118 props := m.Value 119 if len(props["channel_id"]) == 0 { 120 c.SetInvalidParam("channel_id") 121 return 122 } 123 channelId := props["channel_id"][0] 124 c.Params.ChannelId = channelId 125 c.RequireChannelId() 126 if c.Err != nil { 127 return 128 } 129 130 if !c.App.SessionHasPermissionToChannel(c.App.Session, channelId, model.PERMISSION_UPLOAD_FILE) { 131 c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) 132 return 133 } 134 135 resStruct, appErr = c.App.UploadMultipartFiles( 136 FILE_TEAM_ID, 137 channelId, 138 c.App.Session.UserId, 139 m.File["files"], 140 m.Value["client_ids"], 141 now, 142 ) 143 } 144 145 if appErr != nil { 146 c.Err = appErr 147 return 148 } 149 150 w.WriteHeader(http.StatusCreated) 151 w.Write([]byte(resStruct.ToJson())) 152 } 153 154 func parseMultipartRequestHeader(req *http.Request) (boundary string, err error) { 155 v := req.Header.Get("Content-Type") 156 if v == "" { 157 return "", http.ErrNotMultipart 158 } 159 d, params, err := mime.ParseMediaType(v) 160 if err != nil || d != "multipart/form-data" { 161 return "", http.ErrNotMultipart 162 } 163 boundary, ok := params["boundary"] 164 if !ok { 165 return "", http.ErrMissingBoundary 166 } 167 168 return boundary, nil 169 } 170 171 func multipartReader(req *http.Request, stream io.Reader) (*multipart.Reader, error) { 172 boundary, err := parseMultipartRequestHeader(req) 173 if err != nil { 174 return nil, err 175 } 176 177 if stream != nil { 178 return multipart.NewReader(stream, boundary), nil 179 } else { 180 return multipart.NewReader(req.Body, boundary), nil 181 } 182 } 183 184 func uploadFileStream(c *Context, w http.ResponseWriter, r *http.Request) { 185 // Drain any remaining bytes in the request body, up to a limit 186 defer io.CopyN(ioutil.Discard, r.Body, maxUploadDrainBytes) 187 188 if !*c.App.Config().FileSettings.EnableFileAttachments { 189 c.Err = model.NewAppError("uploadFileStream", 190 "api.file.attachments.disabled.app_error", 191 nil, "", http.StatusNotImplemented) 192 return 193 } 194 195 // Parse the post as a regular form (in practice, use the URL values 196 // since we never expect a real application/x-www-form-urlencoded 197 // form). 198 if r.Form == nil { 199 err := r.ParseForm() 200 if err != nil { 201 c.Err = model.NewAppError("uploadFileStream", 202 "api.file.upload_file.read_request.app_error", 203 nil, err.Error(), http.StatusBadRequest) 204 return 205 } 206 } 207 208 timestamp := time.Now() 209 var fileUploadResponse *model.FileUploadResponse 210 211 _, err := parseMultipartRequestHeader(r) 212 switch err { 213 case nil: 214 fileUploadResponse = uploadFileMultipart(c, r, nil, timestamp) 215 216 case http.ErrNotMultipart: 217 fileUploadResponse = uploadFileSimple(c, r, timestamp) 218 219 default: 220 c.Err = model.NewAppError("uploadFileStream", 221 "api.file.upload_file.read_request.app_error", 222 nil, err.Error(), http.StatusBadRequest) 223 } 224 if c.Err != nil { 225 return 226 } 227 228 // Write the response values to the output upon return 229 w.WriteHeader(http.StatusCreated) 230 w.Write([]byte(fileUploadResponse.ToJson())) 231 } 232 233 // uploadFileSimple uploads a file from a simple POST with the file in the request body 234 func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.FileUploadResponse { 235 // Simple POST with the file in the body and all metadata in the args. 236 c.RequireChannelId() 237 c.RequireFilename() 238 if c.Err != nil { 239 return nil 240 } 241 242 if !c.App.SessionHasPermissionToChannel(c.App.Session, c.Params.ChannelId, model.PERMISSION_UPLOAD_FILE) { 243 c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) 244 return nil 245 } 246 247 clientId := r.Form.Get("client_id") 248 info, appErr := c.App.UploadFileX(c.Params.ChannelId, c.Params.Filename, r.Body, 249 app.UploadFileSetTeamId(FILE_TEAM_ID), 250 app.UploadFileSetUserId(c.App.Session.UserId), 251 app.UploadFileSetTimestamp(timestamp), 252 app.UploadFileSetContentLength(r.ContentLength), 253 app.UploadFileSetClientId(clientId)) 254 if appErr != nil { 255 c.Err = appErr 256 return nil 257 } 258 259 fileUploadResponse := &model.FileUploadResponse{ 260 FileInfos: []*model.FileInfo{info}, 261 } 262 if clientId != "" { 263 fileUploadResponse.ClientIds = []string{clientId} 264 } 265 return fileUploadResponse 266 } 267 268 // uploadFileMultipart parses and uploads file(s) from a mime/multipart 269 // request. It pre-buffers up to the first part which is either the (a) 270 // `channel_id` value, or (b) a file. Then in case of (a) it re-processes the 271 // entire message recursively calling itself in stream mode. In case of (b) it 272 // calls to uploadFileMultipartLegacy for legacy support 273 func uploadFileMultipart(c *Context, r *http.Request, asStream io.Reader, timestamp time.Time) *model.FileUploadResponse { 274 275 expectClientIds := true 276 var clientIds []string 277 resp := model.FileUploadResponse{ 278 FileInfos: []*model.FileInfo{}, 279 ClientIds: []string{}, 280 } 281 282 var buf *bytes.Buffer 283 var mr *multipart.Reader 284 var err error 285 if asStream == nil { 286 // We need to buffer until we get the channel_id, or the first file. 287 buf = &bytes.Buffer{} 288 mr, err = multipartReader(r, io.TeeReader(r.Body, buf)) 289 } else { 290 mr, err = multipartReader(r, asStream) 291 } 292 if err != nil { 293 c.Err = model.NewAppError("uploadFileMultipart", 294 "api.file.upload_file.read_request.app_error", 295 nil, err.Error(), http.StatusBadRequest) 296 return nil 297 } 298 299 nFiles := 0 300 NEXT_PART: 301 for { 302 part, err := mr.NextPart() 303 if err == io.EOF { 304 break 305 } 306 if err != nil { 307 c.Err = model.NewAppError("uploadFileMultipart", 308 "api.file.upload_file.read_request.app_error", 309 nil, err.Error(), http.StatusBadRequest) 310 return nil 311 } 312 313 // Parse any form fields in the multipart. 314 formname := part.FormName() 315 if formname == "" { 316 continue 317 } 318 filename := part.FileName() 319 if filename == "" { 320 var b bytes.Buffer 321 _, err = io.CopyN(&b, part, maxMultipartFormDataBytes) 322 if err != nil && err != io.EOF { 323 c.Err = model.NewAppError("uploadFileMultipart", 324 "api.file.upload_file.read_form_value.app_error", 325 map[string]interface{}{"Formname": formname}, 326 err.Error(), http.StatusBadRequest) 327 return nil 328 } 329 v := b.String() 330 331 switch formname { 332 case "channel_id": 333 if c.Params.ChannelId != "" && c.Params.ChannelId != v { 334 c.Err = model.NewAppError("uploadFileMultipart", 335 "api.file.upload_file.multiple_channel_ids.app_error", 336 nil, "", http.StatusBadRequest) 337 return nil 338 } 339 if v != "" { 340 c.Params.ChannelId = v 341 } 342 343 // Got channel_id, re-process the entire post 344 // in the streaming mode. 345 if asStream == nil { 346 return uploadFileMultipart(c, r, io.MultiReader(buf, r.Body), timestamp) 347 } 348 349 case "client_ids": 350 if !expectClientIds { 351 c.SetInvalidParam("client_ids") 352 return nil 353 } 354 clientIds = append(clientIds, v) 355 356 default: 357 c.SetInvalidParam(formname) 358 return nil 359 } 360 361 continue NEXT_PART 362 } 363 364 // A file part. 365 366 if c.Params.ChannelId == "" && asStream == nil { 367 // Got file before channel_id, fall back to legacy buffered mode 368 mr, err = multipartReader(r, io.MultiReader(buf, r.Body)) 369 if err != nil { 370 c.Err = model.NewAppError("uploadFileMultipart", 371 "api.file.upload_file.read_request.app_error", 372 nil, err.Error(), http.StatusBadRequest) 373 return nil 374 } 375 376 return uploadFileMultipartLegacy(c, mr, timestamp) 377 } 378 379 c.RequireChannelId() 380 if c.Err != nil { 381 return nil 382 } 383 if !c.App.SessionHasPermissionToChannel(c.App.Session, c.Params.ChannelId, model.PERMISSION_UPLOAD_FILE) { 384 c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) 385 return nil 386 } 387 388 // If there's no clientIds when the first file comes, expect 389 // none later. 390 if nFiles == 0 && len(clientIds) == 0 { 391 expectClientIds = false 392 } 393 394 // Must have a exactly one client ID for each file. 395 clientId := "" 396 if expectClientIds { 397 if nFiles >= len(clientIds) { 398 c.SetInvalidParam("client_ids") 399 return nil 400 } 401 402 clientId = clientIds[nFiles] 403 } 404 405 info, appErr := c.App.UploadFileX(c.Params.ChannelId, filename, part, 406 app.UploadFileSetTeamId(FILE_TEAM_ID), 407 app.UploadFileSetUserId(c.App.Session.UserId), 408 app.UploadFileSetTimestamp(timestamp), 409 app.UploadFileSetContentLength(-1), 410 app.UploadFileSetClientId(clientId)) 411 if appErr != nil { 412 c.Err = appErr 413 return nil 414 } 415 416 // add to the response 417 resp.FileInfos = append(resp.FileInfos, info) 418 if expectClientIds { 419 resp.ClientIds = append(resp.ClientIds, clientId) 420 } 421 422 nFiles++ 423 } 424 425 // Verify that the number of ClientIds matched the number of files. 426 if expectClientIds && len(clientIds) != nFiles { 427 c.Err = model.NewAppError("uploadFileMultipart", 428 "api.file.upload_file.incorrect_number_of_client_ids.app_error", 429 map[string]interface{}{"NumClientIds": len(clientIds), "NumFiles": nFiles}, 430 "", http.StatusBadRequest) 431 return nil 432 } 433 434 return &resp 435 } 436 437 // uploadFileMultipartLegacy reads, buffers, and then uploads the message, 438 // borrowing from http.ParseMultipartForm. If successful it returns a 439 // *model.FileUploadResponse filled in with the individual model.FileInfo's. 440 func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader, 441 timestamp time.Time) *model.FileUploadResponse { 442 443 // Parse the entire form. 444 form, err := mr.ReadForm(*c.App.Config().FileSettings.MaxFileSize) 445 if err != nil { 446 c.Err = model.NewAppError("uploadFileMultipartLegacy", 447 "api.file.upload_file.read_request.app_error", 448 nil, err.Error(), http.StatusInternalServerError) 449 return nil 450 } 451 452 // get and validate the channel Id, permission to upload there. 453 if len(form.Value["channel_id"]) == 0 { 454 c.SetInvalidParam("channel_id") 455 return nil 456 } 457 channelId := form.Value["channel_id"][0] 458 c.Params.ChannelId = channelId 459 c.RequireChannelId() 460 if c.Err != nil { 461 return nil 462 } 463 if !c.App.SessionHasPermissionToChannel(c.App.Session, channelId, model.PERMISSION_UPLOAD_FILE) { 464 c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) 465 return nil 466 } 467 468 // Check that we have either no client IDs, or one per file. 469 clientIds := form.Value["client_ids"] 470 fileHeaders := form.File["files"] 471 if len(clientIds) != 0 && len(clientIds) != len(fileHeaders) { 472 c.Err = model.NewAppError("uploadFilesMultipartBuffered", 473 "api.file.upload_file.incorrect_number_of_client_ids.app_error", 474 map[string]interface{}{"NumClientIds": len(clientIds), "NumFiles": len(fileHeaders)}, 475 "", http.StatusBadRequest) 476 return nil 477 } 478 479 resp := model.FileUploadResponse{ 480 FileInfos: []*model.FileInfo{}, 481 ClientIds: []string{}, 482 } 483 484 for i, fileHeader := range fileHeaders { 485 f, err := fileHeader.Open() 486 if err != nil { 487 c.Err = model.NewAppError("uploadFileMultipartLegacy", 488 "api.file.upload_file.read_request.app_error", 489 nil, err.Error(), http.StatusBadRequest) 490 return nil 491 } 492 493 clientId := "" 494 if len(clientIds) > 0 { 495 clientId = clientIds[i] 496 } 497 498 info, appErr := c.App.UploadFileX(c.Params.ChannelId, fileHeader.Filename, f, 499 app.UploadFileSetTeamId(FILE_TEAM_ID), 500 app.UploadFileSetUserId(c.App.Session.UserId), 501 app.UploadFileSetTimestamp(timestamp), 502 app.UploadFileSetContentLength(-1), 503 app.UploadFileSetClientId(clientId)) 504 f.Close() 505 if appErr != nil { 506 c.Err = appErr 507 return nil 508 } 509 510 resp.FileInfos = append(resp.FileInfos, info) 511 if clientId != "" { 512 resp.ClientIds = append(resp.ClientIds, clientId) 513 } 514 } 515 516 return &resp 517 } 518 519 func getFile(c *Context, w http.ResponseWriter, r *http.Request) { 520 c.RequireFileId() 521 if c.Err != nil { 522 return 523 } 524 525 forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download")) 526 if convErr != nil { 527 forceDownload = false 528 } 529 530 info, err := c.App.GetFileInfo(c.Params.FileId) 531 if err != nil { 532 c.Err = err 533 return 534 } 535 536 if info.CreatorId != c.App.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.App.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 537 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 538 return 539 } 540 541 fileReader, err := c.App.FileReader(info.Path) 542 if err != nil { 543 c.Err = err 544 c.Err.StatusCode = http.StatusNotFound 545 return 546 } 547 defer fileReader.Close() 548 549 err = writeFileResponse(info.Name, info.MimeType, info.Size, fileReader, forceDownload, w, r) 550 if err != nil { 551 c.Err = err 552 return 553 } 554 } 555 556 func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) { 557 c.RequireFileId() 558 if c.Err != nil { 559 return 560 } 561 562 forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download")) 563 if convErr != nil { 564 forceDownload = false 565 } 566 567 info, err := c.App.GetFileInfo(c.Params.FileId) 568 if err != nil { 569 c.Err = err 570 return 571 } 572 573 if info.CreatorId != c.App.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.App.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 574 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 575 return 576 } 577 578 if info.ThumbnailPath == "" { 579 c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) 580 return 581 } 582 583 fileReader, err := c.App.FileReader(info.ThumbnailPath) 584 if err != nil { 585 c.Err = err 586 c.Err.StatusCode = http.StatusNotFound 587 return 588 } 589 defer fileReader.Close() 590 591 err = writeFileResponse(info.Name, THUMBNAIL_IMAGE_TYPE, 0, fileReader, forceDownload, w, r) 592 if err != nil { 593 c.Err = err 594 return 595 } 596 } 597 598 func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) { 599 c.RequireFileId() 600 if c.Err != nil { 601 return 602 } 603 604 if !*c.App.Config().FileSettings.EnablePublicLink { 605 c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusNotImplemented) 606 return 607 } 608 609 info, err := c.App.GetFileInfo(c.Params.FileId) 610 if err != nil { 611 c.Err = err 612 return 613 } 614 615 if info.CreatorId != c.App.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.App.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 616 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 617 return 618 } 619 620 if len(info.PostId) == 0 { 621 c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) 622 return 623 } 624 625 resp := make(map[string]string) 626 resp["link"] = c.App.GeneratePublicLink(c.GetSiteURLHeader(), info) 627 628 w.Write([]byte(model.MapToJson(resp))) 629 } 630 631 func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) { 632 c.RequireFileId() 633 if c.Err != nil { 634 return 635 } 636 637 forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download")) 638 if convErr != nil { 639 forceDownload = false 640 } 641 642 info, err := c.App.GetFileInfo(c.Params.FileId) 643 if err != nil { 644 c.Err = err 645 return 646 } 647 648 if info.CreatorId != c.App.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.App.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 649 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 650 return 651 } 652 653 if info.PreviewPath == "" { 654 c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) 655 return 656 } 657 658 fileReader, err := c.App.FileReader(info.PreviewPath) 659 if err != nil { 660 c.Err = err 661 c.Err.StatusCode = http.StatusNotFound 662 return 663 } 664 defer fileReader.Close() 665 666 err = writeFileResponse(info.Name, PREVIEW_IMAGE_TYPE, 0, fileReader, forceDownload, w, r) 667 if err != nil { 668 c.Err = err 669 return 670 } 671 } 672 673 func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { 674 c.RequireFileId() 675 if c.Err != nil { 676 return 677 } 678 679 info, err := c.App.GetFileInfo(c.Params.FileId) 680 if err != nil { 681 c.Err = err 682 return 683 } 684 685 if info.CreatorId != c.App.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.App.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 686 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 687 return 688 } 689 690 w.Header().Set("Cache-Control", "max-age=2592000, public") 691 w.Write([]byte(info.ToJson())) 692 } 693 694 func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { 695 c.RequireFileId() 696 if c.Err != nil { 697 return 698 } 699 700 if !*c.App.Config().FileSettings.EnablePublicLink { 701 c.Err = model.NewAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusNotImplemented) 702 return 703 } 704 705 info, err := c.App.GetFileInfo(c.Params.FileId) 706 if err != nil { 707 c.Err = err 708 return 709 } 710 711 hash := r.URL.Query().Get("h") 712 713 if len(hash) == 0 { 714 c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) 715 utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) 716 return 717 } 718 719 if subtle.ConstantTimeCompare([]byte(hash), []byte(app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt))) != 1 { 720 c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) 721 utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) 722 return 723 } 724 725 fileReader, err := c.App.FileReader(info.Path) 726 if err != nil { 727 c.Err = err 728 c.Err.StatusCode = http.StatusNotFound 729 } 730 defer fileReader.Close() 731 732 err = writeFileResponse(info.Name, info.MimeType, info.Size, fileReader, false, w, r) 733 if err != nil { 734 c.Err = err 735 return 736 } 737 } 738 739 func writeFileResponse(filename string, contentType string, contentSize int64, fileReader io.Reader, forceDownload bool, w http.ResponseWriter, r *http.Request) *model.AppError { 740 w.Header().Set("Cache-Control", "max-age=2592000, private") 741 w.Header().Set("X-Content-Type-Options", "nosniff") 742 743 if contentSize > 0 { 744 w.Header().Set("Content-Length", strconv.Itoa(int(contentSize))) 745 } 746 747 if contentType == "" { 748 contentType = "application/octet-stream" 749 } else { 750 for _, unsafeContentType := range UNSAFE_CONTENT_TYPES { 751 if strings.HasPrefix(contentType, unsafeContentType) { 752 contentType = "text/plain" 753 break 754 } 755 } 756 } 757 758 w.Header().Set("Content-Type", contentType) 759 760 var toDownload bool 761 if forceDownload { 762 toDownload = true 763 } else { 764 isMediaType := false 765 766 for _, mediaContentType := range MEDIA_CONTENT_TYPES { 767 if strings.HasPrefix(contentType, mediaContentType) { 768 isMediaType = true 769 break 770 } 771 } 772 773 toDownload = !isMediaType 774 } 775 776 filename = url.PathEscape(filename) 777 778 if toDownload { 779 w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+filename) 780 } else { 781 w.Header().Set("Content-Disposition", "inline;filename=\""+filename+"\"; filename*=UTF-8''"+filename) 782 } 783 784 // prevent file links from being embedded in iframes 785 w.Header().Set("X-Frame-Options", "DENY") 786 w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'") 787 788 io.Copy(w, fileReader) 789 790 return nil 791 }