tusd/pkg/handler/unrouted_handler.go

1263 lines
40 KiB
Go
Raw Normal View History

package handler
import (
"context"
"encoding/base64"
"io"
"log"
"math"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
const UploadLengthDeferred = "1"
var (
reExtractFileID = regexp.MustCompile(`([^/]+)\/?$`)
reForwardedHost = regexp.MustCompile(`host=([^;]+)`)
reForwardedProto = regexp.MustCompile(`proto=(https?)`)
reMimeType = regexp.MustCompile(`^[a-z]+\/[a-z0-9\-\+\.]+$`)
)
// TODO: Move in own file
// ErrorWithResponse represents an error with an additional HTTP response
// attached, which can hold a status code, body and headers.
type Error struct {
ErrorCode string
Message string
HTTPResponse HTTPResponse
}
func (e Error) Error() string {
return e.ErrorCode + ": " + e.Message
}
// TODO: Rename comment
// NewError adds the given status code to the provided error and returns
// the new error instance. The status code may be used in corresponding HTTP
// responses. See the net/http package for standardized status codes.
func NewError(errCode string, message string, statusCode int) Error {
return Error{
ErrorCode: errCode,
Message: message,
HTTPResponse: HTTPResponse{
StatusCode: statusCode,
Body: []byte(errCode + ": " + message),
},
}
}
var (
ErrUnsupportedVersion = NewError("ERR_UNSUPPORTED_VERSION", "missing, invalid or unsupported Tus-Resumable header", http.StatusPreconditionFailed)
ErrMaxSizeExceeded = NewError("ERR_MAX_SIZE_EXCEEDED", "maximum size exceeded", http.StatusRequestEntityTooLarge)
ErrInvalidContentType = NewError("ERR_INVALID_CONTENT_TYPE", "missing or invalid Content-Type header", http.StatusBadRequest)
ErrInvalidUploadLength = NewError("ERR_INVALID_UPLOAD_LENGTH", "missing or invalid Upload-Length header", http.StatusBadRequest)
ErrInvalidOffset = NewError("ERR_INVALID_OFFSET", "missing or invalid Upload-Offset header", http.StatusBadRequest)
ErrNotFound = NewError("ERR_UPLOAD_NOT_FOUND", "upload not found", http.StatusNotFound)
ErrFileLocked = NewError("ERR_UPLOAD_LOCKED", "file currently locked", http.StatusLocked)
ErrMismatchOffset = NewError("ERR_MISMATCHED_OFFSET", "mismatched offset", http.StatusConflict)
ErrSizeExceeded = NewError("ERR_UPLOAD_SIZE_EXCEEDED", "upload's size exceeded", http.StatusRequestEntityTooLarge)
ErrNotImplemented = NewError("ERR_NOT_IMPLEMENTED", "feature not implemented", http.StatusNotImplemented)
ErrUploadNotFinished = NewError("ERR_UPLOAD_NOT_FINISHED", "one of the partial uploads is not finished", http.StatusBadRequest)
ErrInvalidConcat = NewError("ERR_INVALID_CONCAT", "invalid Upload-Concat header", http.StatusBadRequest)
ErrModifyFinal = NewError("ERR_MODIFY_FINAL", "modifying a final upload is not allowed", http.StatusForbidden)
ErrUploadLengthAndUploadDeferLength = NewError("ERR_AMBIGUOUS_UPLOAD_LENGTH", "provided both Upload-Length and Upload-Defer-Length", http.StatusBadRequest)
ErrInvalidUploadDeferLength = NewError("ERR_INVALID_UPLOAD_LENGTH_DEFER", "invalid Upload-Defer-Length header", http.StatusBadRequest)
ErrUploadStoppedByServer = NewError("ERR_UPLOAD_STOPPED", "upload has been stopped by server", http.StatusBadRequest)
ErrUploadRejectedByServer = NewError("ERR_UPLOAD_REJECTED", "upload creation has been rejected by server", http.StatusBadRequest)
2021-10-25 09:54:02 +00:00
// TODO: These two responses are 500 for backwards compatability. We should discuss
// whether it is better to more them to 4XX status codes.
ErrReadTimeout = NewError("ERR_READ_TIMEOUT", "timeout while reading request body", http.StatusInternalServerError)
ErrConnectionReset = NewError("ERR_CONNECTION_RESET", "TCP connection reset by peer", http.StatusInternalServerError)
)
// TODO: Move HTTP structs into own file
// HTTPRequest contains basic details of an incoming HTTP request.
type HTTPRequest struct {
// Method is the HTTP method, e.g. POST or PATCH
Method string
// URI is the full HTTP request URI, e.g. /files/fooo
URI string
// RemoteAddr contains the network address that sent the request
RemoteAddr string
// Header contains all HTTP headers as present in the HTTP request.
Header http.Header
}
type HTTPResponse struct {
// HTTPStatus, HTTPHeaders and HTTPBody control these details of the corresponding
// HTTP response.
// TODO: Currently only works for error responses
StatusCode int
Headers http.Header
Body []byte
}
// HookEvent represents an event from tusd which can be handled by the application.
type HookEvent struct {
// Upload contains information about the upload that caused this hook
// to be fired.
Upload FileInfo
// HTTPRequest contains details about the HTTP request that reached
// tusd.
HTTPRequest HTTPRequest
}
func newHookEvent(info FileInfo, r *http.Request) HookEvent {
return HookEvent{
Upload: info,
HTTPRequest: HTTPRequest{
Method: r.Method,
URI: r.RequestURI,
RemoteAddr: r.RemoteAddr,
Header: r.Header,
},
}
}
2015-12-07 20:09:47 +00:00
// UnroutedHandler exposes methods to handle requests as part of the tus protocol,
// such as PostFile, HeadFile, PatchFile and DelFile. In addition the GetFile method
// is provided which is, however, not part of the specification.
type UnroutedHandler struct {
config Config
2016-02-21 22:25:35 +00:00
composer *StoreComposer
isBasePathAbs bool
basePath string
logger *log.Logger
extensions string
// CompleteUploads is used to send notifications whenever an upload is
// completed by a user. The HookEvent will contain information about this
// upload after it is completed. Sending to this channel will only
// happen if the NotifyCompleteUploads field is set to true in the Config
// structure. Notifications will also be sent for completions using the
// Concatenation extension.
CompleteUploads chan HookEvent
2016-03-12 21:24:57 +00:00
// TerminatedUploads is used to send notifications whenever an upload is
// terminated by a user. The HookEvent will contain information about this
2016-03-12 21:24:57 +00:00
// upload gathered before the termination. Sending to this channel will only
// happen if the NotifyTerminatedUploads field is set to true in the Config
// structure.
TerminatedUploads chan HookEvent
// UploadProgress is used to send notifications about the progress of the
// currently running uploads. For each open PATCH request, every second
// a HookEvent instance will be send over this channel with the Offset field
// being set to the number of bytes which have been transfered to the server.
// Please be aware that this number may be higher than the number of bytes
// which have been stored by the data store! Sending to this channel will only
// happen if the NotifyUploadProgress field is set to true in the Config
// structure.
UploadProgress chan HookEvent
// CreatedUploads is used to send notifications about the uploads having been
// created. It triggers post creation and therefore has all the HookEvent incl.
// the ID available already. It facilitates the post-create hook. Sending to
// this channel will only happen if the NotifyCreatedUploads field is set to
// true in the Config structure.
CreatedUploads chan HookEvent
// Metrics provides numbers of the usage for this handler.
Metrics Metrics
}
// NewUnroutedHandler creates a new handler without routing using the given
// configuration. It exposes the http handlers which need to be combined with
// a router (aka mux) of your choice. If you are looking for preconfigured
// handler see NewHandler.
func NewUnroutedHandler(config Config) (*UnroutedHandler, error) {
2016-02-21 22:25:35 +00:00
if err := config.validate(); err != nil {
return nil, err
}
// Only promote extesions using the Tus-Extension header which are implemented
extensions := "creation,creation-with-upload"
2016-02-21 22:25:35 +00:00
if config.StoreComposer.UsesTerminater {
extensions += ",termination"
}
2016-02-21 22:25:35 +00:00
if config.StoreComposer.UsesConcater {
extensions += ",concatenation"
}
if config.StoreComposer.UsesLengthDeferrer {
extensions += ",creation-defer-length"
}
handler := &UnroutedHandler{
2016-03-12 21:24:57 +00:00
config: config,
composer: config.StoreComposer,
basePath: config.BasePath,
isBasePathAbs: config.isAbs,
CompleteUploads: make(chan HookEvent),
TerminatedUploads: make(chan HookEvent),
UploadProgress: make(chan HookEvent),
CreatedUploads: make(chan HookEvent),
2016-03-12 21:24:57 +00:00
logger: config.Logger,
extensions: extensions,
Metrics: newMetrics(),
}
return handler, nil
}
// SupportedExtensions returns a comma-separated list of the supported tus extensions.
// The availability of an extension usually depends on whether the provided data store
// implements some additional interfaces.
func (handler *UnroutedHandler) SupportedExtensions() string {
return handler.extensions
}
// Middleware checks various aspects of the request and ensures that it
// conforms with the spec. Also handles method overriding for clients which
// cannot make PATCH AND DELETE requests. If you are using the tusd handlers
// directly you will need to wrap at least the POST and PATCH endpoints in
// this middleware.
func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow overriding the HTTP method. The reason for this is
// that some libraries/environments to not support PATCH and
// DELETE requests, e.g. Flash in a browser and parts of Java
if newMethod := r.Header.Get("X-HTTP-Method-Override"); newMethod != "" {
r.Method = newMethod
}
2020-04-06 10:20:57 +00:00
handler.log("RequestIncoming", "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r))
2016-09-23 19:21:38 +00:00
handler.Metrics.incRequestsTotal(r.Method)
header := w.Header()
if origin := r.Header.Get("Origin"); origin != "" {
header.Set("Access-Control-Allow-Origin", origin)
if r.Method == "OPTIONS" {
// Preflight request
header.Add("Access-Control-Allow-Methods", "POST, GET, HEAD, PATCH, DELETE, OPTIONS")
header.Add("Access-Control-Allow-Headers", "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat")
header.Set("Access-Control-Max-Age", "86400")
} else {
// Actual request
header.Add("Access-Control-Expose-Headers", "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat")
}
}
// Set current version used by the server
header.Set("Tus-Resumable", "1.0.0")
// Add nosniff to all responses https://golang.org/src/net/http/server.go#L1429
header.Set("X-Content-Type-Options", "nosniff")
// Set appropriated headers in case of OPTIONS method allowing protocol
// discovery and end with an 204 No Content
if r.Method == "OPTIONS" {
if handler.config.MaxSize > 0 {
header.Set("Tus-Max-Size", strconv.FormatInt(handler.config.MaxSize, 10))
}
header.Set("Tus-Version", "1.0.0")
header.Set("Tus-Extension", handler.extensions)
// Although the 204 No Content status code is a better fit in this case,
// since we do not have a response body included, we cannot use it here
// as some browsers only accept 200 OK as successful response to a
// preflight request. If we send them the 204 No Content the response
// will be ignored or interpreted as a rejection.
// For example, the Presto engine, which is used in older versions of
// Opera, Opera Mobile and Opera Mini, handles CORS this way.
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusOK)
return
}
// Test if the version sent by the client is supported
// GET and HEAD methods are not checked since a browser may visit this URL and does
// not include this header. GET requests are not part of the specification.
if r.Method != "GET" && r.Method != "HEAD" && r.Header.Get("Tus-Resumable") != "1.0.0" {
handler.sendError(w, r, ErrUnsupportedVersion)
return
}
// Proceed with routing the request
h.ServeHTTP(w, r)
})
}
// PostFile creates a new file upload using the datastore after validating the
// length and parsing the metadata.
func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// Check for presence of application/offset+octet-stream. If another content
// type is defined, it will be ignored and treated as none was set because
// some HTTP clients may enforce a default value for this header.
containsChunk := r.Header.Get("Content-Type") == "application/offset+octet-stream"
// Only use the proper Upload-Concat header if the concatenation extension
// is even supported by the data store.
var concatHeader string
2016-02-21 22:25:35 +00:00
if handler.composer.UsesConcater {
concatHeader = r.Header.Get("Upload-Concat")
}
// Parse Upload-Concat header
isPartial, isFinal, partialUploadIDs, err := parseConcat(concatHeader)
if err != nil {
handler.sendError(w, r, err)
return
}
// If the upload is a final upload created by concatenation multiple partial
// uploads the size is sum of all sizes of these files (no need for
// Upload-Length header)
var size int64
var sizeIsDeferred bool
var partialUploads []Upload
if isFinal {
// A final upload must not contain a chunk within the creation request
if containsChunk {
handler.sendError(w, r, ErrModifyFinal)
return
}
partialUploads, size, err = handler.sizeOfUploads(ctx, partialUploadIDs)
if err != nil {
handler.sendError(w, r, err)
return
}
} else {
uploadLengthHeader := r.Header.Get("Upload-Length")
uploadDeferLengthHeader := r.Header.Get("Upload-Defer-Length")
size, sizeIsDeferred, err = handler.validateNewUploadLengthHeaders(uploadLengthHeader, uploadDeferLengthHeader)
if err != nil {
handler.sendError(w, r, err)
return
}
}
// Test whether the size is still allowed
if handler.config.MaxSize > 0 && size > handler.config.MaxSize {
handler.sendError(w, r, ErrMaxSizeExceeded)
return
}
// Parse metadata
meta := ParseMetadataHeader(r.Header.Get("Upload-Metadata"))
info := FileInfo{
Size: size,
SizeIsDeferred: sizeIsDeferred,
MetaData: meta,
IsPartial: isPartial,
IsFinal: isFinal,
PartialUploads: partialUploadIDs,
}
if handler.config.PreUploadCreateCallback != nil {
if _, err := handler.config.PreUploadCreateCallback(newHookEvent(info, r)); err != nil {
handler.sendError(w, r, err)
return
}
}
upload, err := handler.composer.Core.NewUpload(ctx, info)
if err != nil {
handler.sendError(w, r, err)
return
}
info, err = upload.GetInfo(ctx)
2019-08-24 13:14:51 +00:00
if err != nil {
handler.sendError(w, r, err)
return
}
id := info.ID
// Add the Location header directly after creating the new resource to even
// include it in cases of failure when an error is returned
url := handler.absFileURL(r, id)
w.Header().Set("Location", url)
handler.Metrics.incUploadsCreated()
2016-09-23 19:21:38 +00:00
handler.log("UploadCreated", "id", id, "size", i64toa(size), "url", url)
if handler.config.NotifyCreatedUploads {
handler.CreatedUploads <- newHookEvent(info, r)
}
if isFinal {
concatableUpload := handler.composer.Concater.AsConcatableUpload(upload)
if err := concatableUpload.ConcatUploads(ctx, partialUploads); err != nil {
handler.sendError(w, r, err)
return
}
info.Offset = size
if handler.config.NotifyCompleteUploads {
handler.CompleteUploads <- newHookEvent(info, r)
}
}
if containsChunk {
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(id)
if err != nil {
handler.sendError(w, r, err)
return
}
defer lock.Unlock()
}
if err := handler.writeChunk(ctx, upload, info, w, r); err != nil {
handler.sendError(w, r, err)
return
}
} else if !sizeIsDeferred && size == 0 {
// Directly finish the upload if the upload is empty (i.e. has a size of 0).
// This statement is in an else-if block to avoid causing duplicate calls
// to finishUploadIfComplete if an upload is empty and contains a chunk.
if err := handler.finishUploadIfComplete(ctx, upload, info, r); err != nil {
handler.sendError(w, r, err)
return
}
}
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusCreated)
}
// HeadFile returns the length and offset for the HEAD request
func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
2015-12-07 20:37:34 +00:00
id, err := extractIDFromPath(r.URL.Path)
if err != nil {
handler.sendError(w, r, err)
return
}
2016-02-21 22:25:35 +00:00
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(id)
if err != nil {
handler.sendError(w, r, err)
return
}
defer lock.Unlock()
}
upload, err := handler.composer.Core.GetUpload(ctx, id)
2019-08-24 13:14:51 +00:00
if err != nil {
handler.sendError(w, r, err)
return
}
info, err := upload.GetInfo(ctx)
if err != nil {
handler.sendError(w, r, err)
return
}
// Add Upload-Concat header if possible
if info.IsPartial {
w.Header().Set("Upload-Concat", "partial")
}
if info.IsFinal {
v := "final;"
for _, uploadID := range info.PartialUploads {
v += handler.absFileURL(r, uploadID) + " "
}
// Remove trailing space
v = v[:len(v)-1]
w.Header().Set("Upload-Concat", v)
}
if len(info.MetaData) != 0 {
w.Header().Set("Upload-Metadata", SerializeMetadataHeader(info.MetaData))
}
if info.SizeIsDeferred {
w.Header().Set("Upload-Defer-Length", UploadLengthDeferred)
} else {
w.Header().Set("Upload-Length", strconv.FormatInt(info.Size, 10))
w.Header().Set("Content-Length", strconv.FormatInt(info.Size, 10))
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Upload-Offset", strconv.FormatInt(info.Offset, 10))
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusOK)
}
// PatchFile adds a chunk to an upload. This operation is only allowed
// if enough space in the upload is left.
func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// Check for presence of application/offset+octet-stream
if r.Header.Get("Content-Type") != "application/offset+octet-stream" {
handler.sendError(w, r, ErrInvalidContentType)
return
}
// Check for presence of a valid Upload-Offset Header
offset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64)
if err != nil || offset < 0 {
handler.sendError(w, r, ErrInvalidOffset)
return
}
2015-12-07 20:37:34 +00:00
id, err := extractIDFromPath(r.URL.Path)
if err != nil {
handler.sendError(w, r, err)
return
}
2016-02-21 22:25:35 +00:00
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(id)
if err != nil {
handler.sendError(w, r, err)
return
}
defer lock.Unlock()
}
upload, err := handler.composer.Core.GetUpload(ctx, id)
2019-08-24 13:14:51 +00:00
if err != nil {
handler.sendError(w, r, err)
return
}
info, err := upload.GetInfo(ctx)
if err != nil {
handler.sendError(w, r, err)
return
}
// Modifying a final upload is not allowed
if info.IsFinal {
handler.sendError(w, r, ErrModifyFinal)
return
}
if offset != info.Offset {
handler.sendError(w, r, ErrMismatchOffset)
return
}
// Do not proxy the call to the data store if the upload is already completed
if !info.SizeIsDeferred && info.Offset == info.Size {
w.Header().Set("Upload-Offset", strconv.FormatInt(offset, 10))
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusNoContent)
return
}
if r.Header.Get("Upload-Length") != "" {
if !handler.composer.UsesLengthDeferrer {
handler.sendError(w, r, ErrNotImplemented)
return
}
if !info.SizeIsDeferred {
handler.sendError(w, r, ErrInvalidUploadLength)
return
}
uploadLength, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64)
if err != nil || uploadLength < 0 || uploadLength < info.Offset || (handler.config.MaxSize > 0 && uploadLength > handler.config.MaxSize) {
handler.sendError(w, r, ErrInvalidUploadLength)
return
}
2019-08-24 13:14:51 +00:00
lengthDeclarableUpload := handler.composer.LengthDeferrer.AsLengthDeclarableUpload(upload)
if err := lengthDeclarableUpload.DeclareLength(ctx, uploadLength); err != nil {
handler.sendError(w, r, err)
return
}
info.Size = uploadLength
info.SizeIsDeferred = false
}
if err := handler.writeChunk(ctx, upload, info, w, r); err != nil {
handler.sendError(w, r, err)
return
}
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusNoContent)
}
// writeChunk reads the body from the requests r and appends it to the upload
// with the corresponding id. Afterwards, it will set the necessary response
// headers but will not send the response.
func (handler *UnroutedHandler) writeChunk(ctx context.Context, upload Upload, info FileInfo, w http.ResponseWriter, r *http.Request) error {
// Get Content-Length if possible
length := r.ContentLength
offset := info.Offset
2019-08-24 13:14:51 +00:00
id := info.ID
// Test if this upload fits into the file's size
if !info.SizeIsDeferred && offset+length > info.Size {
return ErrSizeExceeded
}
maxSize := info.Size - offset
// If the upload's length is deferred and the PATCH request does not contain the Content-Length
// header (which is allowed if 'Transfer-Encoding: chunked' is used), we still need to set limits for
// the body size.
if info.SizeIsDeferred {
if handler.config.MaxSize > 0 {
// Ensure that the upload does not exceed the maximum upload size
maxSize = handler.config.MaxSize - offset
} else {
// If no upload limit is given, we allow arbitrary sizes
maxSize = math.MaxInt64
}
}
if length > 0 {
maxSize = length
}
2016-09-23 19:21:38 +00:00
handler.log("ChunkWriteStart", "id", id, "maxSize", i64toa(maxSize), "offset", i64toa(offset))
var bytesWritten int64
var err error
// Prevent a nil pointer dereference when accessing the body which may not be
// available in the case of a malicious request.
if r.Body != nil {
// Limit the data read from the request's body to the allowed maximum
reader := newBodyReader(io.LimitReader(r.Body, maxSize))
// We use a context object to allow the hook system to cancel an upload
uploadCtx, stopUpload := context.WithCancel(context.Background())
info.stopUpload = stopUpload
// terminateUpload specifies whether the upload should be deleted after
// the write has finished
terminateUpload := false
// Cancel the context when the function exits to ensure that the goroutine
// is properly cleaned up
defer stopUpload()
go func() {
// Interrupt the Read() call from the request body
<-uploadCtx.Done()
terminateUpload = true
r.Body.Close()
}()
if handler.config.NotifyUploadProgress {
stopProgressEvents := handler.sendProgressMessages(newHookEvent(info, r), reader)
defer close(stopProgressEvents)
}
bytesWritten, err = upload.WriteChunk(ctx, offset, reader)
if terminateUpload && handler.composer.UsesTerminater {
if terminateErr := handler.terminateUpload(ctx, upload, info, r); terminateErr != nil {
// We only log this error and not show it to the user since this
// termination error is not relevant to the uploading client
handler.log("UploadStopTerminateError", "id", id, "error", terminateErr.Error())
}
}
// If we encountered an error while reading the body from the HTTP request, log it, but only include
// it in the response, if the store did not also return an error.
if bodyErr := reader.hasError(); bodyErr != nil {
handler.log("BodyReadError", "id", id, "error", bodyErr.Error())
if err == nil {
err = bodyErr
}
}
// If the upload was stopped by the server, send an error response indicating this.
// TODO: Include a custom reason for the end user why the upload was stopped.
if terminateUpload {
err = ErrUploadStoppedByServer
}
}
2016-09-23 19:21:38 +00:00
handler.log("ChunkWriteComplete", "id", id, "bytesWritten", i64toa(bytesWritten))
if err != nil {
return err
}
// Send new offset to client
newOffset := offset + bytesWritten
w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10))
handler.Metrics.incBytesReceived(uint64(bytesWritten))
info.Offset = newOffset
return handler.finishUploadIfComplete(ctx, upload, info, r)
}
// finishUploadIfComplete checks whether an upload is completed (i.e. upload offset
// matches upload size) and if so, it will call the data store's FinishUpload
// function and send the necessary message on the CompleteUpload channel.
func (handler *UnroutedHandler) finishUploadIfComplete(ctx context.Context, upload Upload, info FileInfo, r *http.Request) error {
2015-12-08 21:08:54 +00:00
// If the upload is completed, ...
if !info.SizeIsDeferred && info.Offset == info.Size {
2015-12-08 21:08:54 +00:00
// ... allow custom mechanism to finish and cleanup the upload
if err := upload.FinishUpload(ctx); err != nil {
2019-08-24 13:14:51 +00:00
return err
2015-12-08 21:08:54 +00:00
}
// ... send the info out to the channel
if handler.config.NotifyCompleteUploads {
handler.CompleteUploads <- newHookEvent(info, r)
2015-12-08 21:08:54 +00:00
}
handler.Metrics.incUploadsFinished()
if handler.config.PreFinishResponseCallback != nil {
if _, err := handler.config.PreFinishResponseCallback(newHookEvent(info, r)); err != nil {
return err
}
}
}
return nil
}
// GetFile handles requests to download a file using a GET request. This is not
// part of the specification.
func (handler *UnroutedHandler) GetFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
2015-12-07 20:37:34 +00:00
id, err := extractIDFromPath(r.URL.Path)
if err != nil {
handler.sendError(w, r, err)
return
}
2016-02-21 22:25:35 +00:00
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(id)
if err != nil {
handler.sendError(w, r, err)
return
}
defer lock.Unlock()
}
upload, err := handler.composer.Core.GetUpload(ctx, id)
2019-08-24 13:14:51 +00:00
if err != nil {
handler.sendError(w, r, err)
return
}
info, err := upload.GetInfo(ctx)
if err != nil {
handler.sendError(w, r, err)
return
}
2016-10-13 16:38:43 +00:00
// Set headers before sending responses
w.Header().Set("Content-Length", strconv.FormatInt(info.Offset, 10))
contentType, contentDisposition := filterContentType(info)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", contentDisposition)
2016-10-13 16:38:43 +00:00
// If no data has been uploaded yet, respond with an empty "204 No Content" status.
if info.Offset == 0 {
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusNoContent)
return
}
src, err := upload.GetReader(ctx)
if err != nil {
handler.sendError(w, r, err)
return
}
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusOK)
io.Copy(w, src)
// Try to close the reader if the io.Closer interface is implemented
if closer, ok := src.(io.Closer); ok {
closer.Close()
}
}
// mimeInlineBrowserWhitelist is a map containing MIME types which should be
// allowed to be rendered by browser inline, instead of being forced to be
// downloadd. For example, HTML or SVG files are not allowed, since they may
// contain malicious JavaScript. In a similiar fashion PDF is not on this list
// as their parsers commonly contain vulnerabilities which can be exploited.
// The values of this map does not convei any meaning and are therefore just
// empty structs.
var mimeInlineBrowserWhitelist = map[string]struct{}{
"text/plain": struct{}{},
"image/png": struct{}{},
"image/jpeg": struct{}{},
"image/gif": struct{}{},
"image/bmp": struct{}{},
"image/webp": struct{}{},
"audio/wave": struct{}{},
"audio/wav": struct{}{},
"audio/x-wav": struct{}{},
"audio/x-pn-wav": struct{}{},
"audio/webm": struct{}{},
"video/webm": struct{}{},
"audio/ogg": struct{}{},
"video/ogg ": struct{}{},
"application/ogg": struct{}{},
}
// filterContentType returns the values for the Content-Type and
// Content-Disposition headers for a given upload. These values should be used
// in responses for GET requests to ensure that only non-malicious file types
// are shown directly in the browser. It will extract the file name and type
// from the "fileame" and "filetype".
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
func filterContentType(info FileInfo) (contentType string, contentDisposition string) {
filetype := info.MetaData["filetype"]
if reMimeType.MatchString(filetype) {
// If the filetype from metadata is well formed, we forward use this
// for the Content-Type header. However, only whitelisted mime types
// will be allowed to be shown inline in the browser
contentType = filetype
if _, isWhitelisted := mimeInlineBrowserWhitelist[filetype]; isWhitelisted {
contentDisposition = "inline"
} else {
contentDisposition = "attachment"
}
} else {
// If the filetype from the metadata is not well formed, we use a
// default type and force the browser to download the content.
contentType = "application/octet-stream"
contentDisposition = "attachment"
}
// Add a filename to Content-Disposition if one is available in the metadata
if filename, ok := info.MetaData["filename"]; ok {
contentDisposition += ";filename=" + strconv.Quote(filename)
}
return contentType, contentDisposition
}
// DelFile terminates an upload permanently.
func (handler *UnroutedHandler) DelFile(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// Abort the request handling if the required interface is not implemented
2016-02-21 22:25:35 +00:00
if !handler.composer.UsesTerminater {
handler.sendError(w, r, ErrNotImplemented)
2015-12-07 20:37:34 +00:00
return
}
2015-12-07 20:37:34 +00:00
id, err := extractIDFromPath(r.URL.Path)
if err != nil {
handler.sendError(w, r, err)
return
}
2016-02-21 22:25:35 +00:00
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(id)
if err != nil {
handler.sendError(w, r, err)
return
}
defer lock.Unlock()
}
upload, err := handler.composer.Core.GetUpload(ctx, id)
2019-08-24 13:14:51 +00:00
if err != nil {
handler.sendError(w, r, err)
return
}
2016-03-12 21:24:57 +00:00
var info FileInfo
if handler.config.NotifyTerminatedUploads {
info, err = upload.GetInfo(ctx)
2016-03-12 21:24:57 +00:00
if err != nil {
handler.sendError(w, r, err)
return
}
}
err = handler.terminateUpload(ctx, upload, info, r)
if err != nil {
handler.sendError(w, r, err)
return
}
2016-09-23 19:21:38 +00:00
handler.sendResp(w, r, http.StatusNoContent)
}
// terminateUpload passes a given upload to the DataStore's Terminater,
// send the corresponding upload info on the TerminatedUploads channnel
// and updates the statistics.
// Note the the info argument is only needed if the terminated uploads
// notifications are enabled.
func (handler *UnroutedHandler) terminateUpload(ctx context.Context, upload Upload, info FileInfo, r *http.Request) error {
2019-08-24 13:14:51 +00:00
terminatableUpload := handler.composer.Terminater.AsTerminatableUpload(upload)
err := terminatableUpload.Terminate(ctx)
if err != nil {
return err
}
2016-03-12 21:24:57 +00:00
if handler.config.NotifyTerminatedUploads {
handler.TerminatedUploads <- newHookEvent(info, r)
2016-03-12 21:24:57 +00:00
}
handler.Metrics.incUploadsTerminated()
return nil
}
// Send the error in the response body. The status code will be looked up in
// ErrStatusCodes. If none is found 500 Internal Error will be used.
func (handler *UnroutedHandler) sendError(w http.ResponseWriter, r *http.Request, err error) {
// Errors for read timeouts contain too much information which is not
// necessary for us and makes grouping for the metrics harder. The error
// message looks like: read tcp 127.0.0.1:1080->127.0.0.1:53673: i/o timeout
// Therefore, we use a common error message for all of them.
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
2021-10-25 09:54:02 +00:00
err = ErrReadTimeout
}
// Errors for connnection resets also contain TCP details, we don't need, e.g:
// read tcp 127.0.0.1:1080->127.0.0.1:10023: read: connection reset by peer
// Therefore, we also trim those down.
if strings.HasSuffix(err.Error(), "read: connection reset by peer") {
2021-10-25 09:54:02 +00:00
err = ErrConnectionReset
}
// TODO: Decide if we should handle this in here, in body_reader or not at all.
// If the HTTP PATCH request gets interrupted in the middle (e.g. because
// the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF.
// However, for the handler it's not important whether the stream has ended
// on purpose or accidentally.
//if err == io.ErrUnexpectedEOF {
// err = nil
//}
// TODO: Decide if we want to ignore connection reset errors all together.
// In some cases, the HTTP connection gets reset by the other peer. This is not
// necessarily the tus client but can also be a proxy in front of tusd, e.g. HAProxy 2
// is known to reset the connection to tusd, when the tus client closes the connection.
// To avoid erroring out in this case and loosing the uploaded data, we can ignore
// the error here without causing harm.
//if strings.Contains(err.Error(), "read: connection reset by peer") {
// err = nil
//}
detailedErr, ok := err.(Error)
Squashed commit of the following: commit d214ad5c92073cb754c70ab73e6cf229cba01c72 Author: Marius <maerious@gmail.com> Date: Tue Feb 28 20:38:47 2017 +0100 Minor code and comment cleanups commit 2826227404296d98a2d83519efaa754cc07a47b1 Merge: db47c89 1a58d6e Author: Marius <maerious@gmail.com> Date: Tue Feb 28 20:13:15 2017 +0100 Merge branch 'metrics_race_condition' of https://github.com/oliverpool/tusd into oliverpool-metrics_race_condition commit 1a58d6e084631f5c039dd64a303b524abce6d4f2 Author: oliverpool <oliverpool@hotmail.fr> Date: Tue Feb 28 19:14:21 2017 +0100 prometheus metrics typo commit db47c8976489917a8afc53878335ab66d73fe60d Merge: 168942b 12054be Author: Marius <maerious@gmail.com> Date: Tue Feb 28 17:36:47 2017 +0100 Merge branch 'metrics_race_condition' of https://github.com/oliverpool/tusd into oliverpool-metrics_race_condition commit 12054be4e76814f286f4630e7fedae36a9cdba77 Author: oliverpool <oliverpool@hotmail.fr> Date: Tue Feb 28 12:07:43 2017 +0100 add go vet on travis commit 16e95d2d91d318493c3090eeadf8eeb74d8088ad Author: oliverpool <oliverpool@hotmail.fr> Date: Tue Feb 28 11:58:58 2017 +0100 add one go vet on appveyor commit 13bc64517ba84741ad26ade44c1e8aa3ed451eba Author: oliverpool <oliverpool@hotmail.fr> Date: Tue Feb 28 11:57:56 2017 +0100 use even less pointers commit db7130f18a9c69f060cf65e6909f753a4b3cc238 Author: oliverpool <oliverpool@hotmail.fr> Date: Mon Feb 27 22:12:45 2017 +0100 use pointer only when needed commit 647f7390c6599aa7e4a8747d62ebdfb4ddc5b980 Author: oliverpool <oliverpool@hotmail.fr> Date: Thu Feb 23 16:29:01 2017 +0100 Prevent lock malfunction literal copies lock value from metrics commit 1ce196ed35492684666d4512f82fcd5f6c3e24b0 Author: oliverpool <oliverpool@hotmail.fr> Date: Wed Feb 22 12:22:19 2017 +0100 handle read timeout simplification in metrics commit 338017c9f4b24e5733787a6b0fd5fd8ea1bd8991 Merge: c378bc9 45a9278 Author: oliverpool <oliverpool@hotmail.fr> Date: Wed Feb 22 12:21:42 2017 +0100 Merge branch 'master' into metrics_race_condition commit c378bc97988363707d0ec3d185cc7ca830d7a5e0 Author: oliverpool <oliverpool@hotmail.fr> Date: Wed Feb 22 11:51:56 2017 +0100 consistent naming commit e2b0050b8d00f5df9bf7941cca986457b2d91df1 Author: oliverpool <oliverpool@hotmail.fr> Date: Wed Feb 22 11:44:05 2017 +0100 Use a simplfied error structure for error counting commit 3da4095c082fca8be187fb58a25c8b1fa6b316a6 Author: oliverpool <oliverpool@hotmail.fr> Date: Mon Feb 20 16:38:27 2017 +0100 split ptr retrieval and incrementation commit ebd6873cbbf669590ef2946ee98c55ed194cb150 Author: oliverpool <oliverpool@hotmail.fr> Date: Sun Feb 5 22:01:34 2017 +0100 Useless initialization commit 691224cdbf6b16027c598102d561c16239d959b7 Author: oliverpool <oliverpool@hotmail.fr> Date: Thu Feb 2 08:25:55 2017 +0100 increment the pointer commit 3d9395e1df1df279e7f54de9e00dd4c37c882042 Author: oliverpool <oliverpool@hotmail.fr> Date: Thu Feb 2 08:25:42 2017 +0100 Simplify lock logic commit d7a46190802a5b8312d44839228ffe46947e69d2 Author: oliverpool <oliverpool@hotmail.fr> Date: Wed Feb 1 08:37:46 2017 +0100 Prevent race condition on errorstotal increment The HTTPError interface is used to have detailed metrics (by code as well)
2017-02-28 19:39:25 +00:00
if !ok {
2021-10-25 09:54:02 +00:00
handler.log("InternalServerError", "message", err.Error(), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r))
detailedErr = NewError("ERR_INTERNAL_SERVER_ERROR", err.Error(), http.StatusInternalServerError)
}
reason := append(detailedErr.HTTPResponse.Body, '\n')
if r.Method == "HEAD" {
reason = nil
}
2021-10-25 09:54:02 +00:00
// TODO: Allow JSON response
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(reason)))
w.WriteHeader(detailedErr.HTTPResponse.StatusCode)
w.Write(reason)
handler.log("ResponseOutgoing", "status", strconv.Itoa(detailedErr.HTTPResponse.StatusCode), "method", r.Method, "path", r.URL.Path, "error", detailedErr.ErrorCode, "requestId", getRequestId(r))
2016-09-23 19:21:38 +00:00
handler.Metrics.incErrorsTotal(detailedErr)
}
2016-09-23 19:21:38 +00:00
// sendResp writes the header to w with the specified status code.
func (handler *UnroutedHandler) sendResp(w http.ResponseWriter, r *http.Request, status int) {
w.WriteHeader(status)
2020-04-06 10:20:57 +00:00
handler.log("ResponseOutgoing", "status", strconv.Itoa(status), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r))
2016-09-23 19:21:38 +00:00
}
// Make an absolute URLs to the given upload id. If the base path is absolute
// it will be prepended else the host and protocol from the request is used.
func (handler *UnroutedHandler) absFileURL(r *http.Request, id string) string {
if handler.isBasePathAbs {
return handler.basePath + id
}
// Read origin and protocol from request
host, proto := getHostAndProtocol(r, handler.config.RespectForwardedHeaders)
url := proto + "://" + host + handler.basePath + id
return url
}
// sendProgressMessage will send a notification over the UploadProgress channel
// every second, indicating how much data has been transfered to the server.
// It will stop sending these instances once the returned channel has been
// closed.
func (handler *UnroutedHandler) sendProgressMessages(hook HookEvent, reader *bodyReader) chan<- struct{} {
previousOffset := int64(0)
originalOffset := hook.Upload.Offset
stop := make(chan struct{}, 1)
go func() {
for {
select {
case <-stop:
hook.Upload.Offset = originalOffset + reader.bytesRead()
if hook.Upload.Offset != previousOffset {
handler.UploadProgress <- hook
previousOffset = hook.Upload.Offset
}
return
case <-time.After(1 * time.Second):
hook.Upload.Offset = originalOffset + reader.bytesRead()
if hook.Upload.Offset != previousOffset {
handler.UploadProgress <- hook
previousOffset = hook.Upload.Offset
}
}
}
}()
return stop
}
// getHostAndProtocol extracts the host and used protocol (either HTTP or HTTPS)
// from the given request. If `allowForwarded` is set, the X-Forwarded-Host,
// X-Forwarded-Proto and Forwarded headers will also be checked to
// support proxies.
func getHostAndProtocol(r *http.Request, allowForwarded bool) (host, proto string) {
if r.TLS != nil {
proto = "https"
} else {
proto = "http"
}
host = r.Host
if !allowForwarded {
return
}
if h := r.Header.Get("X-Forwarded-Host"); h != "" {
host = h
}
if h := r.Header.Get("X-Forwarded-Proto"); h == "http" || h == "https" {
proto = h
}
if h := r.Header.Get("Forwarded"); h != "" {
if r := reForwardedHost.FindStringSubmatch(h); len(r) == 2 {
host = r[1]
}
if r := reForwardedProto.FindStringSubmatch(h); len(r) == 2 {
proto = r[1]
}
}
return
}
// The get sum of all sizes for a list of upload ids while checking whether
// all of these uploads are finished yet. This is used to calculate the size
// of a final resource.
func (handler *UnroutedHandler) sizeOfUploads(ctx context.Context, ids []string) (partialUploads []Upload, size int64, err error) {
partialUploads = make([]Upload, len(ids))
for i, id := range ids {
upload, err := handler.composer.Core.GetUpload(ctx, id)
2019-08-24 13:14:51 +00:00
if err != nil {
return nil, 0, err
2019-08-24 13:14:51 +00:00
}
info, err := upload.GetInfo(ctx)
if err != nil {
return nil, 0, err
}
if info.SizeIsDeferred || info.Offset != info.Size {
err = ErrUploadNotFinished
return nil, 0, err
}
size += info.Size
partialUploads[i] = upload
}
return
}
// Verify that the Upload-Length and Upload-Defer-Length headers are acceptable for creating a
// new upload
func (handler *UnroutedHandler) validateNewUploadLengthHeaders(uploadLengthHeader string, uploadDeferLengthHeader string) (uploadLength int64, uploadLengthDeferred bool, err error) {
haveBothLengthHeaders := uploadLengthHeader != "" && uploadDeferLengthHeader != ""
haveInvalidDeferHeader := uploadDeferLengthHeader != "" && uploadDeferLengthHeader != UploadLengthDeferred
lengthIsDeferred := uploadDeferLengthHeader == UploadLengthDeferred
if lengthIsDeferred && !handler.composer.UsesLengthDeferrer {
err = ErrNotImplemented
} else if haveBothLengthHeaders {
err = ErrUploadLengthAndUploadDeferLength
} else if haveInvalidDeferHeader {
err = ErrInvalidUploadDeferLength
} else if lengthIsDeferred {
uploadLengthDeferred = true
} else {
uploadLength, err = strconv.ParseInt(uploadLengthHeader, 10, 64)
if err != nil || uploadLength < 0 {
err = ErrInvalidUploadLength
}
}
return
}
// lockUpload creates a new lock for the given upload ID and attempts to lock it.
// The created lock is returned if it was aquired successfully.
func (handler *UnroutedHandler) lockUpload(id string) (Lock, error) {
lock, err := handler.composer.Locker.NewLock(id)
if err != nil {
return nil, err
}
if err := lock.Lock(); err != nil {
return nil, err
}
return lock, nil
}
// ParseMetadataHeader parses the Upload-Metadata header as defined in the
// File Creation extension.
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
func ParseMetadataHeader(header string) map[string]string {
meta := make(map[string]string)
for _, element := range strings.Split(header, ",") {
element := strings.TrimSpace(element)
parts := strings.Split(element, " ")
2020-06-20 08:32:06 +00:00
if len(parts) > 2 {
continue
}
key := parts[0]
2020-06-20 08:32:06 +00:00
if key == "" {
continue
}
2020-06-20 08:32:06 +00:00
value := ""
if len(parts) == 2 {
// Ignore current element if the value is no valid base64
dec, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
continue
}
value = string(dec)
}
meta[key] = value
}
return meta
}
// SerializeMetadataHeader serializes a map of strings into the Upload-Metadata
// header format used in the response for HEAD requests.
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
func SerializeMetadataHeader(meta map[string]string) string {
header := ""
for key, value := range meta {
valueBase64 := base64.StdEncoding.EncodeToString([]byte(value))
header += key + " " + valueBase64 + ","
}
// Remove trailing comma
if len(header) > 0 {
header = header[:len(header)-1]
}
return header
}
// Parse the Upload-Concat header, e.g.
// Upload-Concat: partial
// Upload-Concat: final;http://tus.io/files/a /files/b/
func parseConcat(header string) (isPartial bool, isFinal bool, partialUploads []string, err error) {
if len(header) == 0 {
return
}
if header == "partial" {
isPartial = true
return
}
l := len("final;")
if strings.HasPrefix(header, "final;") && len(header) > l {
isFinal = true
list := strings.Split(header[l:], " ")
for _, value := range list {
value := strings.TrimSpace(value)
if value == "" {
continue
}
2015-12-07 20:37:34 +00:00
id, extractErr := extractIDFromPath(value)
if extractErr != nil {
err = extractErr
return
}
partialUploads = append(partialUploads, id)
}
}
// If no valid partial upload ids are extracted this is not a final upload.
if len(partialUploads) == 0 {
isFinal = false
err = ErrInvalidConcat
}
return
}
// extractIDFromPath pulls the last segment from the url provided
2015-12-07 20:37:34 +00:00
func extractIDFromPath(url string) (string, error) {
result := reExtractFileID.FindStringSubmatch(url)
if len(result) != 2 {
2015-12-07 20:37:34 +00:00
return "", ErrNotFound
}
2015-12-07 20:37:34 +00:00
return result[1], nil
}
2016-09-23 19:21:38 +00:00
func i64toa(num int64) string {
return strconv.FormatInt(num, 10)
}
2020-04-06 10:20:57 +00:00
// getRequestId returns the value of the X-Request-ID header, if available,
// and also takes care of truncating the input.
func getRequestId(r *http.Request) string {
reqId := r.Header.Get("X-Request-ID")
if reqId == "" {
return ""
}
// Limit the length of the request ID to 36 characters, which is enough
// to fit a UUID.
if len(reqId) > 36 {
reqId = reqId[:36]
}
return reqId
}