Documentation
¶
Overview ¶
Package share manages public download tokens with optional PIN protection, expiry, and download caps.
Index ¶
- Constants
- Variables
- func CheckPINLength(pin string) error
- func ClampExpiry(requested *time.Time, maxDays int, now time.Time) (*time.Time, bool)
- func CookieName(token string) string
- func GalleryEligible(entries []FolderEntry) bool
- func IsPINLength(err error) bool
- func MimeForName(name string) string
- func NewToken() (string, error)
- func TokenHash(token string) string
- func UnlockTTL() time.Duration
- type CreateOpts
- type FolderEntry
- type FolderEntryKind
- type Service
- func (s *Service) AttachSecret(key string)
- func (s *Service) CheckPIN(ctx context.Context, sh *model.Share, pin string) error
- func (s *Service) Cleanup(ctx context.Context) error
- func (s *Service) CountOverMaxTTL(ctx context.Context, now time.Time) (int, error)
- func (s *Service) Create(ctx context.Context, opts CreateOpts) (*model.Share, error)
- func (s *Service) Delete(ctx context.Context, id int64) error
- func (s *Service) IncrementDownload(ctx context.Context, id int64) error
- func (s *Service) ListByNode(ctx context.Context, nodeID int64) ([]*model.Share, error)
- func (s *Service) MaxTTLDays(ctx context.Context) int
- func (s *Service) MintUnlock(token string) string
- func (s *Service) ReleaseDownload(ctx context.Context, id int64) error
- func (s *Service) ReleaseUpload(ctx context.Context, id int64, n int) error
- func (s *Service) ReserveDownload(ctx context.Context, id int64) (bool, error)
- func (s *Service) ReserveUpload(ctx context.Context, id int64, n int) (bool, error)
- func (s *Service) Resolve(ctx context.Context, token, pin string) (*model.Share, error)
- func (s *Service) RevealPIN(sh *model.Share) (string, error)
- func (s *Service) VerifyUnlock(token, value string) bool
- type Viewer
Constants ¶
const ( // PINMinLen is the shortest PIN a link may carry, in characters. PINMinLen = 4 // PINMaxLen is the longest PIN a link may carry, in characters. The // visitor's PIN box is told it as `pin_max`. PINMaxLen = 12 )
const AuditActionPinReveal = "share.pin_revealed"
AuditActionPinReveal is the audit row every PIN read writes — success or refusal-after-authorization alike, from the "My shares" endpoint and from an app plugin's share_pin alike. One spelling, so an operator grepping the audit table finds every reveal on the instance.
const DefaultMaxTTLDays = 7
DefaultMaxTTLDays applies when the setting has never been written. Seven days: a link that is meant to be opened this week, and a folder archive that does not have to be kept warm for a month.
const MaxTTLDaysLimit = 3650
MaxTTLDaysLimit bounds the setting itself.
const SettingKeyMaxTTLDays = "share.max_ttl_days"
SettingKeyMaxTTLDays is the settings row holding the longest life a NEW share link may be given, in days. Absent = DefaultMaxTTLDays; "0" = no ceiling. Written by the admin Protection page (PATCH /api/admin/protection {share_max_ttl_days}) and seeded once from FILEX_SHARE_MAX_TTL.
Variables ¶
var ( // ErrPinRequired means the link has a PIN and none was supplied. ErrPinRequired = errors.New("share: pin required") // ErrLocked means too many wrong PINs; the gate is shut for a while. ErrLocked = errors.New("share: too many wrong pins") )
PIN gate errors.
var ( // ErrNoPIN means the link has no PIN at all — there is nothing to show. ErrNoPIN = errors.New("share: link has no pin") // ErrPinNotRecoverable means THIS link's PIN was never sealed: it was // minted before 00049, or while the instance had no key, or it was sealed // under a key that has since been rotated away. The link still works; its // PIN is simply gone, exactly as it was for every link before this. ErrPinNotRecoverable = errors.New("share: pin was not stored recoverably") // ErrNoSecretKey means this INSTANCE has no FILEX_SECRET_KEY, so no PIN it // mints can ever be shown again. An operator can fix this one; the two // above they cannot, and telling them apart is the whole point. ErrNoSecretKey = errors.New("share: no secret key configured") )
Reasons a PIN cannot be shown. They are separate errors because the sentence a person has to read differs for each, and one "not available" for all three would send an operator looking for the wrong fault.
var ErrBadPIN = errors.New("share: bad pin")
ErrBadPIN is returned when an incorrect PIN is supplied.
var ErrExpired = errors.New("share: expired")
ErrExpired is returned by Resolve when a share is past its TTL or download cap.
var ErrPINLength = fmt.Errorf("share: pin must be %d-%d characters", PINMinLen, PINMaxLen)
ErrPINLength refuses a PIN outside PINMinLen..PINMaxLen characters.
Functions ¶
func CheckPINLength ¶ added in v0.54.0
CheckPINLength reports whether pin may be a link's PIN: ErrPINLength when it is shorter than PINMinLen or longer than PINMaxLen characters (not bytes - a PIN typed in another script is as long as it looks), nil otherwise. An empty PIN is "no PIN" and is not this rule's business.
func ClampExpiry ¶
ClampExpiry applies the max-TTL ceiling to a requested expiry: nil (never) becomes now+max, a later date is pulled back to now+max, anything within the ceiling is returned as is. The bool reports whether the request was changed, so the caller can tell the user the link's real life. With no ceiling (maxDays == 0) the request is returned untouched.
func CookieName ¶
CookieName is the per-link cookie name, so unlocking one link never unlocks another and a browser can hold several at once.
func GalleryEligible ¶
func GalleryEligible(entries []FolderEntry) bool
GalleryEligible reports whether the browse page should use the gallery layout: at least one visual file AND ≥60% of the FILES (dirs excluded) are images/videos.
func IsPINLength ¶ added in v0.54.0
IsPINLength reports whether err is the PIN-length refusal.
func MimeForName ¶
MimeForName resolves a Content-Type for the public sub-file endpoint by extension, defaulting to application/octet-stream.
func NewToken ¶
NewToken mints a share token in the shape every link carries: 32 lowercase hex characters from crypto/rand.
Exported because a caller sometimes has to KNOW a link's token before the row that carries it can exist — see CreateOpts.Token. Nothing else should need it: Create mints its own.
Types ¶
type CreateOpts ¶
type CreateOpts struct {
NodeID int64
PIN string // optional, hashed before persist
ExpiresAt *time.Time // optional
MaxDownloads *int // optional
CreatedBy *int64 // user ID
CreatedVia string // token username of the creating API call (display-only)
// Drop-link options (Kind == model.ShareKindDrop). Ignored/zero for a
// normal download share.
Kind string // "" defaults to download
MaxUploads *int // cap on total files a drop link may receive
DropSettings *string // JSON limits blob
// App-plugin page options (migration 00046). Set only by the
// `share_create` host function: an app's public page IS a share, so it is
// minted here and inherits the expiry ceiling, the PIN gate, the visit
// counter and the administrator's revoke like every other link. Zero here
// means an ordinary share.
PluginID int64
PageID string
Subject string
StateJSON string // the plugin's own durable record (≤ 64 KiB)
FilesJSON string // [{ref,name,file,size,mime}] — the exposed copies
// PurposeJSON is what the app says this link IS (wire.PagePurpose,
// migration 00052) — how a list of links names it.
PurposeJSON string
// AppRoot is the `root:` of the token whose job opens the link
// (migration 00082, model.Share.AppRoot); "" for none.
AppRoot string
// Token is the link's token when the caller ALREADY MINTED one and has
// handed it out. Empty — every other caller — mints a fresh one here.
//
// ⚠ It exists for one case: a plugin may promise a share of a file its
// own job is still writing (wasmplugin/public.go). Such a row cannot be
// written until the bytes are committed and the node exists, but the
// plugin needs the link NOW, to put in the mail it is composing. So the
// token is minted with NewToken at the promise and spent here when the
// output lands. Refused unless it has the shape every token has, so a
// caller cannot choose a guessable one.
Token string
}
CreateOpts is the set of fields a caller may supply when minting a share.
type FolderEntry ¶
type FolderEntry struct {
Name string
RelPath string // slash-relative path under the shared root
Kind FolderEntryKind
Size int64
Mtime time.Time
}
FolderEntry is one classified row of a shared folder listing.
type FolderEntryKind ¶
type FolderEntryKind string
FolderEntryKind classifies one listed entry of a shared folder.
const ( EntryDir FolderEntryKind = "dir" EntryImage FolderEntryKind = "image" EntryVideo FolderEntryKind = "video" EntryFile FolderEntryKind = "file" )
Folder entry kinds.
func ClassifyEntry ¶
func ClassifyEntry(name string, isDir bool) FolderEntryKind
ClassifyEntry buckets a name into dir / image / video / other-file by extension (public pages have no DB mime handy — driver listings often don't populate mime either, mirroring the thumb pipeline's fallback).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service provides high-level share operations.
func NewService ¶
NewService constructs a share Service.
The PIN box starts KEYLESS. secretbox.New("") returns a Box that refuses to Seal (ErrNoKey) rather than one that quietly stores plaintext, so a Service nobody called AttachSecret on mints links whose PIN can never be shown — which is exactly the truth about such an instance, and is said in those words by the endpoint rather than guessed at by the screen.
func (*Service) AttachSecret ¶
AttachSecret gives the unlock cookie its signing key — the instance secret (FILEX_SECRET_KEY). Without it a per-process random key is used, which is correct but logs every visitor out when the server restarts and cannot work across two instances behind one address.
⚠ Since 00049 the SAME key also seals the PIN itself (Create → pin_enc, RevealPIN below). Deliberately the same key and the same box as the rest of filex: internal/secretbox is the instance's one recoverable-secret scheme, and a second key here would be a second thing to lose.
func (*Service) CheckPIN ¶
CheckPIN verifies a PIN against a link, counting the misses.
ErrLocked while the gate is shut, ErrBadPIN on a miss (counted), nil on a hit (counter reset). A link with no PIN always passes.
⚠ The lock is checked BEFORE the hash comparison and the RIGHT PIN during a lock is refused too: a lock the correct answer lifts is no lock at all against somebody working through the space — they would simply find it.
⚠ The counters are written through UpdateSharePinLock, which touches only those two columns. A whole-row save here would let a wrong guess carry a stale expiry, cap or exposed-file list back into the database.
func (*Service) CountOverMaxTTL ¶
CountOverMaxTTL counts the EXISTING live links that outlive the current ceiling — no expiry at all, or an expiry later than now+max. They are reported, never modified: the ceiling is a rule for new links, and a customer's link that was minted under the old rule keeps working. The number is what an operator needs to decide whether to revoke any by hand. 0 when there is no ceiling.
func (*Service) Create ¶
Create issues a fresh share token.
The link's expiry is clamped to the max-TTL setting (see ClampExpiry): a request with no expiry gets one, a request past the ceiling is shortened. The returned Share carries the expiry that was actually stored. Links that already exist are never touched by this — only new ones.
func (*Service) IncrementDownload ¶
IncrementDownload bumps the counter — caller decides whether to call before or after streaming the file.
⚠ Not the cap's enforcement point. Serving first and counting afterwards leaves a window in which every concurrent request reads the same pre-download count and is waved through: measured on fm.example.com, a link capped at ONE download handed three complete files to three overlapping clients. Anything that is about to hand bytes to a visitor must call ReserveDownload instead.
func (*Service) ListByNode ¶
ListByNode returns all shares pointing at a given node.
func (*Service) MaxTTLDays ¶
MaxTTLDays returns the configured ceiling on a new link's life in days (0 = none). A missing or unparsable setting is the default, never "no ceiling": a typo in the settings table must not quietly mint permanent links.
func (*Service) MintUnlock ¶
MintUnlock returns the cookie value proving this browser answered the PIN.
func (*Service) ReleaseDownload ¶
ReleaseDownload returns a reserved slot. Call it ONLY when the serve failed before any byte reached the client — a partial download is still a download.
func (*Service) ReleaseUpload ¶ added in v0.60.0
ReleaseUpload hands back claimed files that were never written. A file that landed is a file received, and stays counted.
func (*Service) ReserveDownload ¶
ReserveDownload claims one download against the link's cap BEFORE the bytes are served, and reports whether it got one. False means the cap is spent and the caller must serve nothing.
The context is detached from the request on purpose: the claim is the record that a file was handed out, and a client that hangs up mid-request must not be able to make that record disappear.
func (*Service) ReserveUpload ¶ added in v0.60.0
ReserveUpload claims n files against a drop link's total cap (MaxUploads) BEFORE they are written, and reports whether it got them. False means fewer than n are left and the caller must write nothing. Detached from the request for the reason ReserveDownload is.
func (*Service) Resolve ¶
Resolve looks up a share by token and applies expiry/PIN checks.
pin may be empty when the share has no PIN configured. Returns ErrExpired or ErrBadPIN as appropriate.
func (*Service) RevealPIN ¶
RevealPIN returns a link's PIN in plain, or says why it cannot.
⚠ This function performs NO authorization. It is called from the one endpoint that has already decided the caller is the creator or an admin and that writes the audit row whatever the answer is. Keeping the two apart is deliberate: a permission check buried inside a decrypt helper is one the next caller of that helper silently inherits — or silently skips.
func (*Service) VerifyUnlock ¶
VerifyUnlock checks a cookie value against a link.
type Viewer ¶
type Viewer struct {
Service *Service
Store db.Store
// StorageResolver returns a constructed storage.Driver for a given
// storage ID. Wired by the server.
StorageResolver func(int64) (storage.Driver, error)
}
Viewer renders the public file at /s/{token} (or the metadata at /api/files/share/{token}).
On GET with PIN-protected shares, the controller expects ?pin= in the querystring (or the X-Filex-Pin header).
func (*Viewer) HandleDownload ¶
func (v *Viewer) HandleDownload(w http.ResponseWriter, r *http.Request)
HandleDownload streams the shared file (or returns inline preview).
func (*Viewer) HandleMetadata ¶
func (v *Viewer) HandleMetadata(w http.ResponseWriter, r *http.Request)
HandleMetadata returns share metadata as JSON — used by the embed.js viewer to decide whether to show the PIN input or jump straight to download.