file.go 10.5 KB
Newer Older
1
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
2
3
4
5
6
// See License.txt for license information.

package api4

import (
7
	"crypto/subtle"
8
	"io"
9
	"io/ioutil"
10
11
12
	"net/http"
	"net/url"
	"strconv"
13
	"strings"
14
	"time"
15

Christopher Speller's avatar
Christopher Speller committed
16
17
18
	"github.com/mattermost/mattermost-server/app"
	"github.com/mattermost/mattermost-server/model"
	"github.com/mattermost/mattermost-server/utils"
19
20
21
22
)

const (
	FILE_TEAM_ID = "noteam"
23
24
25

	PREVIEW_IMAGE_TYPE   = "image/jpeg"
	THUMBNAIL_IMAGE_TYPE = "image/jpeg"
26
27
)

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var UNSAFE_CONTENT_TYPES = [...]string{
	"application/javascript",
	"application/ecmascript",
	"text/javascript",
	"text/ecmascript",
	"application/x-javascript",
	"text/html",
}

var MEDIA_CONTENT_TYPES = [...]string{
	"image/jpeg",
	"image/png",
	"image/bmp",
	"image/gif",
	"video/avi",
	"video/mpeg",
	"video/mp4",
	"audio/mpeg",
	"audio/wav",
}

49
50
51
52
53
54
55
func (api *API) InitFile() {
	api.BaseRoutes.Files.Handle("", api.ApiSessionRequired(uploadFile)).Methods("POST")
	api.BaseRoutes.File.Handle("", api.ApiSessionRequiredTrustRequester(getFile)).Methods("GET")
	api.BaseRoutes.File.Handle("/thumbnail", api.ApiSessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
	api.BaseRoutes.File.Handle("/link", api.ApiSessionRequired(getFileLink)).Methods("GET")
	api.BaseRoutes.File.Handle("/preview", api.ApiSessionRequiredTrustRequester(getFilePreview)).Methods("GET")
	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
63
	defer io.Copy(ioutil.Discard, r.Body)

Chris's avatar
Chris committed
64
	if !*c.App.Config().FileSettings.EnableFileAttachments {
65
66
67
68
		c.Err = model.NewAppError("uploadFile", "api.file.attachments.disabled.app_error", nil, "", http.StatusNotImplemented)
		return
	}

Chris's avatar
Chris committed
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
72
73
		return
	}

74
	now := time.Now()
75
76
	var resStruct *model.FileUploadResponse
	var appErr *model.AppError
77

78
79
	if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil && err != http.ErrNotMultipart {
		http.Error(w, err.Error(), http.StatusInternalServerError)
80
		return
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
	} else if err == http.ErrNotMultipart {
		defer r.Body.Close()

		c.RequireChannelId()
		c.RequireFilename()

		if c.Err != nil {
			return
		}

		channelId := c.Params.ChannelId
		filename := c.Params.Filename

		if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) {
			c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
			return
		}

		resStruct, appErr = c.App.UploadFiles(
			FILE_TEAM_ID,
			channelId,
			c.Session.UserId,
			[]io.ReadCloser{r.Body},
			[]string{filename},
			[]string{},
106
			now,
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
		)
	} else {
		m := r.MultipartForm

		props := m.Value
		if len(props["channel_id"]) == 0 {
			c.SetInvalidParam("channel_id")
			return
		}
		channelId := props["channel_id"][0]
		if len(channelId) == 0 {
			c.SetInvalidParam("channel_id")
			return
		}

		if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) {
			c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
			return
		}

127
128
129
130
131
132
133
134
		resStruct, appErr = c.App.UploadMultipartFiles(
			FILE_TEAM_ID,
			channelId,
			c.Session.UserId,
			m.File["files"],
			m.Value["client_ids"],
			now,
		)
135
136
	}

137
138
	if appErr != nil {
		c.Err = appErr
139
140
141
142
143
144
145
146
147
148
149
150
151
		return
	}

	w.WriteHeader(http.StatusCreated)
	w.Write([]byte(resStruct.ToJson()))
}

func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
	c.RequireFileId()
	if c.Err != nil {
		return
	}

152
153
154
	forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download"))
	if convErr != nil {
		forceDownload = false
155
156
	}

Chris's avatar
Chris committed
157
	info, err := c.App.GetFileInfo(c.Params.FileId)
158
159
160
161
162
	if err != nil {
		c.Err = err
		return
	}

Chris's avatar
Chris committed
163
	if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) {
164
165
166
167
		c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
		return
	}

168
	fileReader, err := c.App.FileReader(info.Path)
169
	if err != nil {
170
171
		c.Err = err
		c.Err.StatusCode = http.StatusNotFound
172
173
		return
	}
174
	defer fileReader.Close()
175

176
	err = writeFileResponse(info.Name, info.MimeType, info.Size, fileReader, forceDownload, w, r)
177
	if err != nil {
178
179
180
181
182
		c.Err = err
		return
	}
}

183
184
185
186
187
188
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
	c.RequireFileId()
	if c.Err != nil {
		return
	}

189
190
191
	forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download"))
	if convErr != nil {
		forceDownload = false
192
193
	}

Chris's avatar
Chris committed
194
	info, err := c.App.GetFileInfo(c.Params.FileId)
195
196
197
198
199
	if err != nil {
		c.Err = err
		return
	}

Chris's avatar
Chris committed
200
	if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) {
201
202
203
204
205
		c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
		return
	}

	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
208
209
		return
	}

210
211
	fileReader, err := c.App.FileReader(info.ThumbnailPath)
	if err != nil {
212
213
		c.Err = err
		c.Err.StatusCode = http.StatusNotFound
214
215
216
217
218
219
		return
	}
	defer fileReader.Close()

	err = writeFileResponse(info.Name, THUMBNAIL_IMAGE_TYPE, 0, fileReader, forceDownload, w, r)
	if err != nil {
220
221
222
223
224
		c.Err = err
		return
	}
}

225
226
227
228
229
230
func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
	c.RequireFileId()
	if c.Err != nil {
		return
	}

Chris's avatar
Chris committed
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
234
235
		return
	}

Chris's avatar
Chris committed
236
	info, err := c.App.GetFileInfo(c.Params.FileId)
237
238
239
240
241
	if err != nil {
		c.Err = err
		return
	}

Chris's avatar
Chris committed
242
	if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) {
243
244
245
246
247
		c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
		return
	}

	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
250
251
252
		return
	}

	resp := make(map[string]string)
Chris's avatar
Chris committed
253
	resp["link"] = c.App.GeneratePublicLink(c.GetSiteURLHeader(), info)
254
255
256
257

	w.Write([]byte(model.MapToJson(resp)))
}

258
259
260
261
262
263
func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
	c.RequireFileId()
	if c.Err != nil {
		return
	}

264
265
266
	forceDownload, convErr := strconv.ParseBool(r.URL.Query().Get("download"))
	if convErr != nil {
		forceDownload = false
267
268
	}

Chris's avatar
Chris committed
269
	info, err := c.App.GetFileInfo(c.Params.FileId)
270
271
272
273
274
	if err != nil {
		c.Err = err
		return
	}

Chris's avatar
Chris committed
275
	if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) {
276
277
278
279
280
		c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
		return
	}

	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
283
284
		return
	}

285
286
	fileReader, err := c.App.FileReader(info.PreviewPath)
	if err != nil {
287
288
		c.Err = err
		c.Err.StatusCode = http.StatusNotFound
289
		return
290
291
292
293
294
	}
	defer fileReader.Close()

	err = writeFileResponse(info.Name, PREVIEW_IMAGE_TYPE, 0, fileReader, forceDownload, w, r)
	if err != nil {
295
296
297
298
299
		c.Err = err
		return
	}
}

300
301
302
303
304
305
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
	c.RequireFileId()
	if c.Err != nil {
		return
	}

Chris's avatar
Chris committed
306
	info, err := c.App.GetFileInfo(c.Params.FileId)
307
308
309
310
311
	if err != nil {
		c.Err = err
		return
	}

Chris's avatar
Chris committed
312
	if info.CreatorId != c.Session.UserId && !c.App.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) {
313
314
315
316
317
318
319
320
		c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
		return
	}

	w.Header().Set("Cache-Control", "max-age=2592000, public")
	w.Write([]byte(info.ToJson()))
}

321
322
323
324
325
326
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
	c.RequireFileId()
	if c.Err != nil {
		return
	}

Chris's avatar
Chris committed
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
330
331
		return
	}

Chris's avatar
Chris committed
332
	info, err := c.App.GetFileInfo(c.Params.FileId)
333
334
335
336
337
338
339
340
	if err != nil {
		c.Err = err
		return
	}

	hash := r.URL.Query().Get("h")

	if len(hash) == 0 {
341
		c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
Jesse Hallam's avatar
Jesse Hallam committed
342
		utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
343
344
345
		return
	}

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)
Jesse Hallam's avatar
Jesse Hallam committed
348
		utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
349
350
351
		return
	}

352
353
	fileReader, err := c.App.FileReader(info.Path)
	if err != nil {
354
355
		c.Err = err
		c.Err.StatusCode = http.StatusNotFound
356
357
358
	}
	defer fileReader.Close()

359
	err = writeFileResponse(info.Name, info.MimeType, info.Size, fileReader, false, w, r)
360
	if err != nil {
361
362
363
364
365
		c.Err = err
		return
	}
}

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
371
372
373
	if contentSize > 0 {
		w.Header().Set("Content-Length", strconv.Itoa(int(contentSize)))
	}

374
375
	if contentType == "" {
		contentType = "application/octet-stream"
376
	} else {
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
		for _, unsafeContentType := range UNSAFE_CONTENT_TYPES {
			if strings.HasPrefix(contentType, unsafeContentType) {
				contentType = "text/plain"
				break
			}
		}
	}

	w.Header().Set("Content-Type", contentType)

	var toDownload bool
	if forceDownload {
		toDownload = true
	} else {
		isMediaType := false

		for _, mediaContentType := range MEDIA_CONTENT_TYPES {
			if strings.HasPrefix(contentType, mediaContentType) {
				isMediaType = true
				break
			}
		}

		toDownload = !isMediaType
401
402
	}

403
404
	filename = url.PathEscape(filename)

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
412
413
414

	// prevent file links from being embedded in iframes
	w.Header().Set("X-Frame-Options", "DENY")
	w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'")

415
	io.Copy(w, fileReader)
416
417
418

	return nil
}