Documentation
¶
Overview ¶
Package upload is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.
Index ¶
- Constants
- Variables
- func Handler(cfg Config) http.HandlerFunc
- func SanitizeFilename(name string) string
- func ServeHandler(storage Storage) http.HandlerFunc
- func ValidateExt(filename string, allowed []string) error
- func ValidateMIME(file io.ReadSeeker, allowed []string) error
- func ValidateSize(size int64, max int64) error
- type Config
- type LocalStorage
- func (s *LocalStorage) Delete(_ context.Context, key string) error
- func (s *LocalStorage) Exists(_ context.Context, key string) (bool, error)
- func (s *LocalStorage) Get(_ context.Context, key string) (io.ReadCloser, error)
- func (s *LocalStorage) GetRange(_ context.Context, key string) (io.ReadSeekCloser, error)
- func (s *LocalStorage) Save(_ context.Context, key string, r io.Reader) error
- type Metadata
- type RangeGetter
- type Storage
Constants ¶
const MaxFilenameBytes = 255
MaxFilenameBytes caps the sanitised filename length. A user-supplied filename has no legitimate reason to exceed a few hundred bytes; the cap protects log lines, filesystem APIs, and database columns from pathological inputs.
const SanitizeFilenameInputBound = 4 * MaxFilenameBytes
SanitizeFilenameInputBound caps the *input* length before any of the O(n) sanitisation passes (control-byte strip, replacer, interior-ext split-and-join) run. A multipart Content-Disposition filename is a MIME-header value and is NOT counted against ParseMultipartForm's maxMemory, so without this guard an attacker can ship a multi-MiB filename (bounded only by the stdlib's 10 MiB per-header cap) that amplifies into tens of MB of transient allocation and >100ms of CPU per request — strings.Split on a ~9 MiB all-dots name produces a ~9.4M-element slice. The bound is a generous multiple of MaxFilenameBytes so legitimate names with escaped / multibyte sequences survive untouched; the final MaxFilenameBytes cap still applies after sanitisation.
Variables ¶
var ErrInvalidKey = errors.New("upload: invalid key")
ErrInvalidKey is wrapped when a storage key is rejected by sanitization (path traversal, empty key, or a path that escapes the base directory). The detection lives in [sanitizeKey] and the backend's escape check — callers (e.g. ServeHandler) classify the typed error rather than re-implement path validation.
var ErrNotFound = errNotFound{}
ErrNotFound is wrapped by Get when the requested key doesn't exist. Callers can match on this or on errors.Is(err, os.ErrNotExist) — the returned error wraps both so existing code continues to work.
Functions ¶
func Handler ¶
func Handler(cfg Config) http.HandlerFunc
Handler returns an http.HandlerFunc that processes multipart file uploads. It expects a single file in the "file" form field. On success it responds with 200 and JSON Metadata.
func SanitizeFilename ¶
SanitizeFilename removes path separators, null bytes, and other dangerous characters from a filename to prevent path traversal attacks. It also neutralises double-extension smuggling — e.g. `shell.php.jpg` becomes `shell_php.jpg` — so a misconfigured web server can't be tricked into executing a hidden interior extension.
Control bytes (CR, LF, TAB, anything < 0x20) are dropped so a logged filename can't escape its log line via injected newlines or terminal control sequences. The final result is truncated to MaxFilenameBytes (preserving the extension) so an attacker can't ship a 10 MB filename.
func ServeHandler ¶ added in v0.18.0
func ServeHandler(storage Storage) http.HandlerFunc
ServeHandler returns an http.HandlerFunc that streams a file stored under storage back to the client. Mount it on the framework router with a catch-all key, e.g.
r.Get("/uploads/{key...}", upload.ServeHandler(storage))
Semantics:
- GET and HEAD only; any other method yields 405 with an Allow header.
- The key is resolved from r.PathValue("key") (populated by the router's {key...} wildcard), falling back to the URL path with a leading slash stripped when no path value is set.
- The content type is sniffed from the first 512 bytes of the body via http.DetectContentType — never from the client or the key.
- X-Content-Type-Options: nosniff is always set.
- Scriptable content (HTML, XHTML, SVG) is forced to application/octet-stream with Content-Disposition: attachment so a browser downloads it instead of rendering it (stored-XSS guard).
- Path-traversal defense is delegated entirely to the Storage backend (LocalStorage's sanitizeKey is the single enforcement point); the handler performs no path manipulation of its own and echoes no filesystem path on any error.
- When the backend implements RangeGetter, `Range:` requests are answered with 206 and Accept-Ranges is advertised, so an interrupted download of a large object resumes instead of restarting. Backends that decline the capability serve whole bodies exactly as before.
func ValidateExt ¶
ValidateExt checks that the file extension is in the allowed list. Extensions are compared case-insensitively without the leading dot. If allowed is empty, all extensions are permitted.
func ValidateMIME ¶
func ValidateMIME(file io.ReadSeeker, allowed []string) error
ValidateMIME reads the first 512 bytes from file to detect MIME type, checks it against the allowed list, then resets the reader. If allowed is empty, all MIME types are permitted.
func ValidateSize ¶
ValidateSize checks that the file size does not exceed max. A max of 0 means no limit.
Types ¶
type Config ¶
type Config struct {
MaxSize int64 // Maximum file size in bytes (0 = no limit)
AllowedTypes []string // MIME type whitelist (empty = allow all)
AllowedExts []string // Extension whitelist (empty = allow all)
Storage Storage // Storage backend implementation
}
Config holds configuration for the upload handler.
type LocalStorage ¶
type LocalStorage struct {
// contains filtered or unexported fields
}
LocalStorage implements Storage using the local filesystem.
func NewLocalStorage ¶
func NewLocalStorage(baseDir string) *LocalStorage
NewLocalStorage creates a LocalStorage that saves files under baseDir.
func (*LocalStorage) Delete ¶
func (s *LocalStorage) Delete(_ context.Context, key string) error
Delete removes the file at key from the local filesystem.
func (*LocalStorage) Get ¶
func (s *LocalStorage) Get(_ context.Context, key string) (io.ReadCloser, error)
Get opens the file at key from the local filesystem for reading.
Returns ErrNotFound (wrapping os.ErrNotExist) when the key is missing — callers can match on os.ErrNotExist or upload.ErrNotFound without parsing the message. Other errors are returned with the absolute filesystem path stripped, so a 500 propagated to an end user doesn't disclose where the data lives.
func (*LocalStorage) GetRange ¶ added in v0.46.0
func (s *LocalStorage) GetRange(_ context.Context, key string) (io.ReadSeekCloser, error)
GetRange implements RangeGetter. The local backend already opens an *os.File, so seekability costs nothing here — Get simply discarded it through the io.ReadCloser return type. Key validation is the same code path, not a parallel one.
type Metadata ¶
type Metadata struct {
OriginalName string `json:"originalName"`
Size int64 `json:"size"`
MimeType string `json:"mimeType"`
UploadedAt time.Time `json:"uploadedAt"`
Key string `json:"key"`
}
Metadata holds information about an uploaded file.
type RangeGetter ¶ added in v0.46.0
RangeGetter is an optional capability a Storage backend may implement to expose seekable reads.
Storage.Get returns an io.ReadCloser, which erases seekability, and http.ServeContent needs an io.ReadSeeker to answer a `Range:` request. Without this, a client that loses its connection 1.8 GB into a 2 GB download restarts from zero, and browsers/CDNs that probe with a range request get a 200 with the whole body instead of a 206.
It is a capability interface rather than a widening of Storage on purpose: a network-backed store would have to buffer the whole object to satisfy Seek, which is worse than declining. Callers type-assert and fall back — ServeHandler is the reference consumer.
An implementation MUST apply the same key validation as its Get: a capability that skipped the traversal check would be a path-traversal hole with a performance justification.
type Storage ¶
type Storage interface {
Save(ctx context.Context, key string, r io.Reader) error
Delete(ctx context.Context, key string) error
Get(ctx context.Context, key string) (io.ReadCloser, error)
Exists(ctx context.Context, key string) (bool, error)
}
Storage defines the interface for file storage backends.