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  }