github.com/lologarithm/mattermost-server@v5.3.2-0.20181002060438-c82a84ed765b+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 "crypto/subtle" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/mattermost/mattermost-server/app" 17 "github.com/mattermost/mattermost-server/model" 18 "github.com/mattermost/mattermost-server/utils" 19 ) 20 21 const ( 22 FILE_TEAM_ID = "noteam" 23 24 PREVIEW_IMAGE_TYPE = "image/jpeg" 25 THUMBNAIL_IMAGE_TYPE = "image/jpeg" 26 ) 27 28 var UNSAFE_CONTENT_TYPES = [...]string{ 29 "application/javascript", 30 "application/ecmascript", 31 "text/javascript", 32 "text/ecmascript", 33 "application/x-javascript", 34 "text/html", 35 } 36 37 var MEDIA_CONTENT_TYPES = [...]string{ 38 "image/jpeg", 39 "image/png", 40 "image/bmp", 41 "image/gif", 42 "video/avi", 43 "video/mpeg", 44 "video/mp4", 45 "audio/mpeg", 46 "audio/wav", 47 } 48 49 func (api *API) InitFile() { 50 api.BaseRoutes.Files.Handle("", api.ApiSessionRequired(uploadFile)).Methods("POST") 51 api.BaseRoutes.File.Handle("", api.ApiSessionRequiredTrustRequester(getFile)).Methods("GET") 52 api.BaseRoutes.File.Handle("/thumbnail", api.ApiSessionRequiredTrustRequester(getFileThumbnail)).Methods("GET") 53 api.BaseRoutes.File.Handle("/link", api.ApiSessionRequired(getFileLink)).Methods("GET") 54 api.BaseRoutes.File.Handle("/preview", api.ApiSessionRequiredTrustRequester(getFilePreview)).Methods("GET") 55 api.BaseRoutes.File.Handle("/info", api.ApiSessionRequired(getFileInfo)).Methods("GET") 56 57 api.BaseRoutes.PublicFile.Handle("", api.ApiHandler(getPublicFile)).Methods("GET") 58 59 } 60 61 func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { 62 defer io.Copy(ioutil.Discard, r.Body) 63 64 if !*c.App.Config().FileSettings.EnableFileAttachments { 65 c.Err = model.NewAppError("uploadFile", "api.file.attachments.disabled.app_error", nil, "", http.StatusNotImplemented) 66 return 67 } 68 69 if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize { 70 c.Err = model.NewAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge) 71 return 72 } 73 74 now := time.Now() 75 var resStruct *model.FileUploadResponse 76 var appErr *model.AppError 77 78 if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil && err != http.ErrNotMultipart { 79 http.Error(w, err.Error(), http.StatusInternalServerError) 80 return 81 } else if err == http.ErrNotMultipart { 82 defer r.Body.Close() 83 84 c.RequireChannelId() 85 c.RequireFilename() 86 87 if c.Err != nil { 88 return 89 } 90 91 channelId := c.Params.ChannelId 92 filename := c.Params.Filename 93 94 if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) { 95 c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) 96 return 97 } 98 99 resStruct, appErr = c.App.UploadFiles( 100 FILE_TEAM_ID, 101 channelId, 102 c.Session.UserId, 103 []io.ReadCloser{r.Body}, 104 []string{filename}, 105 []string{}, 106 now, 107 ) 108 } else { 109 m := r.MultipartForm 110 111 props := m.Value 112 if len(props["channel_id"]) == 0 { 113 c.SetInvalidParam("channel_id") 114 return 115 } 116 channelId := props["channel_id"][0] 117 if len(channelId) == 0 { 118 c.SetInvalidParam("channel_id") 119 return 120 } 121 122 if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) { 123 c.SetPermissionError(model.PERMISSION_UPLOAD_FILE) 124 return 125 } 126 127 resStruct, appErr = c.App.UploadMultipartFiles( 128 FILE_TEAM_ID, 129 channelId, 130 c.Session.UserId, 131 m.File["files"], 132 m.Value["client_ids"], 133 now, 134 ) 135 } 136 137 if appErr != nil { 138 c.Err = appErr 139 return 140 } 141 142 w.WriteHeader(http.StatusCreated) 143 w.Write([]byte(resStruct.ToJson())) 144 } 145 146 func getFile(c *Context, w http.ResponseWriter, r *http.Request) { 147 c.RequireFileId() 148 if c.Err != nil { 149 return 150 } 151 152 forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download")) 153 if convErr != nil { 154 forceDownload = false 155 } 156 157 info, err := c.App.GetFileInfo(c.Params.FileId) 158 if err != nil { 159 c.Err = err 160 return 161 } 162 163 if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 164 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 165 return 166 } 167 168 fileReader, err := c.App.FileReader(info.Path) 169 if err != nil { 170 c.Err = err 171 c.Err.StatusCode = http.StatusNotFound 172 return 173 } 174 defer fileReader.Close() 175 176 err = writeFileResponse(info.Name, info.MimeType, info.Size, fileReader, forceDownload, w, r) 177 if err != nil { 178 c.Err = err 179 return 180 } 181 } 182 183 func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) { 184 c.RequireFileId() 185 if c.Err != nil { 186 return 187 } 188 189 forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download")) 190 if convErr != nil { 191 forceDownload = false 192 } 193 194 info, err := c.App.GetFileInfo(c.Params.FileId) 195 if err != nil { 196 c.Err = err 197 return 198 } 199 200 if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 201 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 202 return 203 } 204 205 if info.ThumbnailPath == "" { 206 c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) 207 return 208 } 209 210 fileReader, err := c.App.FileReader(info.ThumbnailPath) 211 if err != nil { 212 c.Err = err 213 c.Err.StatusCode = http.StatusNotFound 214 return 215 } 216 defer fileReader.Close() 217 218 err = writeFileResponse(info.Name, THUMBNAIL_IMAGE_TYPE, 0, fileReader, forceDownload, w, r) 219 if err != nil { 220 c.Err = err 221 return 222 } 223 } 224 225 func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) { 226 c.RequireFileId() 227 if c.Err != nil { 228 return 229 } 230 231 if !c.App.Config().FileSettings.EnablePublicLink { 232 c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusNotImplemented) 233 return 234 } 235 236 info, err := c.App.GetFileInfo(c.Params.FileId) 237 if err != nil { 238 c.Err = err 239 return 240 } 241 242 if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 243 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 244 return 245 } 246 247 if len(info.PostId) == 0 { 248 c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) 249 return 250 } 251 252 resp := make(map[string]string) 253 resp["link"] = c.App.GeneratePublicLink(c.GetSiteURLHeader(), info) 254 255 w.Write([]byte(model.MapToJson(resp))) 256 } 257 258 func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) { 259 c.RequireFileId() 260 if c.Err != nil { 261 return 262 } 263 264 forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download")) 265 if convErr != nil { 266 forceDownload = false 267 } 268 269 info, err := c.App.GetFileInfo(c.Params.FileId) 270 if err != nil { 271 c.Err = err 272 return 273 } 274 275 if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 276 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 277 return 278 } 279 280 if info.PreviewPath == "" { 281 c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest) 282 return 283 } 284 285 fileReader, err := c.App.FileReader(info.PreviewPath) 286 if err != nil { 287 c.Err = err 288 c.Err.StatusCode = http.StatusNotFound 289 return 290 } 291 defer fileReader.Close() 292 293 err = writeFileResponse(info.Name, PREVIEW_IMAGE_TYPE, 0, fileReader, forceDownload, w, r) 294 if err != nil { 295 c.Err = err 296 return 297 } 298 } 299 300 func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { 301 c.RequireFileId() 302 if c.Err != nil { 303 return 304 } 305 306 info, err := c.App.GetFileInfo(c.Params.FileId) 307 if err != nil { 308 c.Err = err 309 return 310 } 311 312 if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) { 313 c.SetPermissionError(model.PERMISSION_READ_CHANNEL) 314 return 315 } 316 317 w.Header().Set("Cache-Control", "max-age=2592000, public") 318 w.Write([]byte(info.ToJson())) 319 } 320 321 func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { 322 c.RequireFileId() 323 if c.Err != nil { 324 return 325 } 326 327 if !c.App.Config().FileSettings.EnablePublicLink { 328 c.Err = model.NewAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusNotImplemented) 329 return 330 } 331 332 info, err := c.App.GetFileInfo(c.Params.FileId) 333 if err != nil { 334 c.Err = err 335 return 336 } 337 338 hash := r.URL.Query().Get("h") 339 340 if len(hash) == 0 { 341 c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) 342 utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) 343 return 344 } 345 346 if subtle.ConstantTimeCompare([]byte(hash), []byte(app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt))) != 1 { 347 c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) 348 utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) 349 return 350 } 351 352 fileReader, err := c.App.FileReader(info.Path) 353 if err != nil { 354 c.Err = err 355 c.Err.StatusCode = http.StatusNotFound 356 } 357 defer fileReader.Close() 358 359 err = writeFileResponse(info.Name, info.MimeType, info.Size, fileReader, false, w, r) 360 if err != nil { 361 c.Err = err 362 return 363 } 364 } 365 366 func writeFileResponse(filename string, contentType string, contentSize int64, fileReader io.Reader, forceDownload bool, w http.ResponseWriter, r *http.Request) *model.AppError { 367 w.Header().Set("Cache-Control", "max-age=2592000, private") 368 w.Header().Set("X-Content-Type-Options", "nosniff") 369 370 if contentSize > 0 { 371 w.Header().Set("Content-Length", strconv.Itoa(int(contentSize))) 372 } 373 374 if contentType == "" { 375 contentType = "application/octet-stream" 376 } else { 377 for _, unsafeContentType := range UNSAFE_CONTENT_TYPES { 378 if strings.HasPrefix(contentType, unsafeContentType) { 379 contentType = "text/plain" 380 break 381 } 382 } 383 } 384 385 w.Header().Set("Content-Type", contentType) 386 387 var toDownload bool 388 if forceDownload { 389 toDownload = true 390 } else { 391 isMediaType := false 392 393 for _, mediaContentType := range MEDIA_CONTENT_TYPES { 394 if strings.HasPrefix(contentType, mediaContentType) { 395 isMediaType = true 396 break 397 } 398 } 399 400 toDownload = !isMediaType 401 } 402 403 filename = url.PathEscape(filename) 404 405 if toDownload { 406 w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+filename) 407 } else { 408 w.Header().Set("Content-Disposition", "inline;filename=\""+filename+"\"; filename*=UTF-8''"+filename) 409 } 410 411 // prevent file links from being embedded in iframes 412 w.Header().Set("X-Frame-Options", "DENY") 413 w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'") 414 415 io.Copy(w, fileReader) 416 417 return nil 418 }