// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api import ( "net/http" "net/url" "strconv" "strings" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) const ( PREVIEW_IMAGE_TYPE = "image/jpeg" THUMBNAIL_IMAGE_TYPE = "image/jpeg" ) var UNSAFE_CONTENT_TYPES = [...]string{ "application/javascript", "application/ecmascript", "text/javascript", "text/ecmascript", "application/x-javascript", "text/html", } func InitFile() { l4g.Debug(utils.T("api.file.init.debug")) BaseRoutes.TeamFiles.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST") BaseRoutes.NeedFile.Handle("/get", ApiUserRequiredTrustRequester(getFile)).Methods("GET") BaseRoutes.NeedFile.Handle("/get_thumbnail", ApiUserRequiredTrustRequester(getFileThumbnail)).Methods("GET") BaseRoutes.NeedFile.Handle("/get_preview", ApiUserRequiredTrustRequester(getFilePreview)).Methods("GET") BaseRoutes.NeedFile.Handle("/get_info", ApiUserRequired(getFileInfo)).Methods("GET") BaseRoutes.NeedFile.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("GET") BaseRoutes.Public.Handle("/files/{file_id:[A-Za-z0-9]+}/get", ApiAppHandlerTrustRequesterIndependent(getPublicFile)).Methods("GET") BaseRoutes.Public.Handle("/files/get/{team_id:[A-Za-z0-9]+}/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:(?:[A-Za-z0-9]+/)?.+(?:\\.[A-Za-z0-9]{3,})?}", ApiAppHandlerTrustRequesterIndependent(getPublicFileOld)).Methods("GET") } func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { if !*utils.Cfg.FileSettings.EnableFileAttachments { c.Err = model.NewAppError("uploadFile", "api.file.attachments.disabled.app_error", nil, "", http.StatusNotImplemented) return } if r.ContentLength > *utils.Cfg.FileSettings.MaxFileSize { c.Err = model.NewAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge) return } if err := r.ParseMultipartForm(*utils.Cfg.FileSettings.MaxFileSize); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } m := r.MultipartForm props := m.Value if len(props["channel_id"]) == 0 { c.SetInvalidParam("uploadFile", "channel_id") return } channelId := props["channel_id"][0] if len(channelId) == 0 { c.SetInvalidParam("uploadFile", "channel_id") return } if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) { c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) return } resStruct, err := c.App.UploadFiles(c.TeamId, channelId, c.Session.UserId, m.File["files"], m.Value["client_ids"]) if err != nil { c.Err = err return } w.Write([]byte(resStruct.ToJson())) } func getFile(c *Context, w http.ResponseWriter, r *http.Request) { info, err := getFileInfoForRequest(c, r, true) if err != nil { c.Err = err return } if data, err := utils.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 getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) { info, err := getFileInfoForRequest(c, r, true) if err != nil { c.Err = err return } if info.ThumbnailPath == "" { c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) return } if data, err := utils.ReadFile(info.ThumbnailPath); err != nil { c.Err = err c.Err.StatusCode = http.StatusNotFound } else if err := writeFileResponse(info.Name, THUMBNAIL_IMAGE_TYPE, data, w, r); err != nil { c.Err = err return } } func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) { info, err := getFileInfoForRequest(c, r, true) if err != nil { c.Err = err return } if info.PreviewPath == "" { c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) return } if data, err := utils.ReadFile(info.PreviewPath); err != nil { c.Err = err c.Err.StatusCode = http.StatusNotFound } else if err := writeFileResponse(info.Name, PREVIEW_IMAGE_TYPE, data, w, r); err != nil { c.Err = err return } } func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { info, err := getFileInfoForRequest(c, r, true) if err != nil { c.Err = err return } w.Header().Set("Cache-Control", "max-age=2592000, public") w.Write([]byte(info.ToJson())) } func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.FileSettings.EnablePublicLink { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_disabled.app_error", nil, "", http.StatusNotImplemented) return } info, err := getFileInfoForRequest(c, r, false) if err != nil { c.Err = err return } hash := r.URL.Query().Get("h") if len(hash) > 0 { correctHash := app.GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) if hash != correctHash { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) return } } else { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) return } if data, err := utils.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 getFileInfoForRequest(c *Context, r *http.Request, requireFileVisible bool) (*model.FileInfo, *model.AppError) { if len(*utils.Cfg.FileSettings.DriverName) == 0 { return nil, model.NewAppError("getFileInfoForRequest", "api.file.get_info_for_request.storage.app_error", nil, "", http.StatusNotImplemented) } params := mux.Vars(r) fileId := params["file_id"] if len(fileId) != 26 { return nil, NewInvalidParamError("getFileInfoForRequest", "file_id") } info, err := c.App.GetFileInfo(fileId) if err != nil { return nil, err } // only let users access files visible in a channel, unless they're the one who uploaded the file if info.CreatorId != c.Session.UserId { if len(info.PostId) == 0 { err := model.NewAppError("getFileInfoForRequest", "api.file.get_file_info_for_request.no_post.app_error", nil, "file_id="+fileId, http.StatusBadRequest) return nil, err } if requireFileVisible { if !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { c.SetPermissionError(model.PERMISSION_READ_CHANNEL) return nil, c.Err } } } return info, nil } func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) { if len(*utils.Cfg.FileSettings.DriverName) == 0 { c.Err = model.NewAppError("getPublicFile", "api.file.get_public_file_old.storage.app_error", nil, "", http.StatusNotImplemented) return } else if !utils.Cfg.FileSettings.EnablePublicLink { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_disabled.app_error", nil, "", http.StatusNotImplemented) return } params := mux.Vars(r) teamId := params["team_id"] channelId := params["channel_id"] userId := params["user_id"] filename := params["filename"] hash := r.URL.Query().Get("h") if len(hash) > 0 { correctHash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt) if hash != correctHash { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) return } } else { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) return } path := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + filename var info *model.FileInfo if result := <-c.App.Srv.Store.FileInfo().GetByPath(path); result.Err != nil { c.Err = result.Err return } else { info = result.Data.(*model.FileInfo) } if len(info.PostId) == 0 { c.Err = model.NewAppError("getPublicFileOld", "api.file.get_public_file_old.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) return } if data, err := utils.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, private") w.Header().Set("Content-Length", strconv.Itoa(len(bytes))) w.Header().Set("X-Content-Type-Options", "nosniff") if contentType == "" { contentType = "application/octet-stream" } else { for _, unsafeContentType := range UNSAFE_CONTENT_TYPES { if strings.HasPrefix(contentType, unsafeContentType) { contentType = "text/plain" break } } } w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+url.QueryEscape(filename)) // prevent file links from being embedded in iframes w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'") w.Write(bytes) return nil } func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.FileSettings.EnablePublicLink { c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusNotImplemented) return } info, err := getFileInfoForRequest(c, r, true) if err != nil { c.Err = err return } if len(info.PostId) == 0 { c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) return } w.Write([]byte(model.StringToJson(app.GeneratePublicLinkV3(c.GetSiteURLHeader(), info)))) }