Documentation
¶
Overview ¶
Package link owns link and tag business logic.
Index ¶
- Constants
- func HostOf(rawURL string) string
- func ValidateDestination(raw string, p DestinationPolicy) (string, error)
- type Config
- type CreateInput
- type DestinationPolicy
- type DomainSettings
- type Invalidator
- type RootInvalidator
- type Service
- func (s *Service) Archive(ctx context.Context, actor *auth.Identity, id uuid.UUID) (*domain.Link, error)
- func (s *Service) Create(ctx context.Context, actor *auth.Identity, in CreateInput) (*domain.Link, error)
- func (s *Service) Delete(ctx context.Context, actor *auth.Identity, id uuid.UUID) error
- func (s *Service) DeleteTag(ctx context.Context, actor *auth.Identity, id uuid.UUID) error
- func (s *Service) DomainSettings(ctx context.Context, actor *auth.Identity) (*DomainSettings, error)
- func (s *Service) Get(ctx context.Context, actor *auth.Identity, id uuid.UUID) (*domain.Link, error)
- func (s *Service) List(ctx context.Context, actor *auth.Identity, f domain.LinkFilter) (*domain.Page[domain.Link], error)
- func (s *Service) ListTags(ctx context.Context, actor *auth.Identity) ([]domain.Tag, error)
- func (s *Service) LoadRootRedirect(ctx context.Context) (string, error)
- func (s *Service) Restore(ctx context.Context, actor *auth.Identity, id uuid.UUID) (*domain.Link, error)
- func (s *Service) SetRootRedirect(ctx context.Context, actor *auth.Identity, rawURL string) (*DomainSettings, error)
- func (s *Service) Update(ctx context.Context, actor *auth.Identity, id uuid.UUID, in UpdateInput) (*domain.Link, error)
- type UpdateInput
Constants ¶
const ( PermRead = "links.read" PermCreate = "links.create" PermUpdate = "links.update" PermDelete = "links.delete" PermTagsRead = "tags.read" PermTagsWrite = "tags.write" // PermDomainsWrite guards settings that apply to the hostname rather than to // a workspace. One host serves every workspace on the instance, so this is a // wider grant than links.update despite touching fewer rows. PermDomainsWrite = "domains.write" )
Permissions this service enforces. Named constants so a typo is a compile error rather than a silently-always-false check.
const TrashRetentionDays = 30
TrashRetentionDays is how long a soft-deleted link stays restorable.
Variables ¶
This section is empty.
Functions ¶
func HostOf ¶
HostOf extracts the lowercase host, stored alongside the URL so the hot path and reporting never have to re-parse.
func ValidateDestination ¶
func ValidateDestination(raw string, p DestinationPolicy) (string, error)
ValidateDestination checks a destination URL and returns its normalized form.
This is an allowlist, not a blocklist, and that is the whole design. A blocklist of dangerous schemes is a game you lose: javascript:, data:, vbscript:, file:, intent:, and whatever the next browser ships. Permitting only http and https means a new scheme is refused by default.
Known limitation, deliberately not papered over: blocking private literals does not defend against DNS rebinding, where a hostname resolves to a public address at creation and a private one when a visitor follows the link. Defending against that requires resolving at redirect time on the hot path, which cannot be afforded, or an egress policy outside this process. Recorded in docs/build-notes/SECURITY.md rather than pretended away.
Types ¶
type Config ¶
type Config struct {
Policy DestinationPolicy
// Aliases carries the operator's reserved-word additions and the profanity
// switch. The zero value is the safe default: built-in list, filter on.
Aliases alias.Policy
BaseURL string
Cache Invalidator
// SplitHosts mirrors config.SplitHosts. The root-redirect setting is refused
// when false, because there the root is the dashboard.
SplitHosts bool
RootCache RootInvalidator
}
type CreateInput ¶
type CreateInput struct {
URL string
Alias string // optional; generated when empty
Title string
Description string
Tags []string
ExpiresAt *time.Time
// ForwardQuery merges the visitor's query string into the destination.
// Off by default; the destination's own parameters always win on conflict.
ForwardQuery bool
// Phase 2 fields. Accepted by the parser so the API can reject them with a
// specific message rather than ignoring them silently, which would look
// like the feature works.
Password string
MaxClicks *int64
OneTime bool
}
CreateInput describes a new link.
type DestinationPolicy ¶
type DestinationPolicy struct {
// Schemes is the allowlist. Anything outside it is refused.
Schemes []string
MaxLength int
// BlockPrivateIPs refuses literal addresses in private, loopback,
// link-local and unique-local ranges.
BlockPrivateIPs bool
// BlockedHostSuffixes refuses matching hosts, matched on label boundaries.
BlockedHostSuffixes []string
}
DestinationPolicy governs which URLs a link may point at.
func DefaultDestinationPolicy ¶
func DefaultDestinationPolicy() DestinationPolicy
type DomainSettings ¶
type DomainSettings struct {
Hostname string `json:"hostname"`
// RootRedirectURL is where https://<link host>/ sends a visitor. Empty means
// the root answers 404, which is the default and reveals nothing.
RootRedirectURL string `json:"root_redirect_url,omitempty"`
// SplitHosts reports whether the setting is in effect at all. On a
// single-host deployment the root belongs to the dashboard.
SplitHosts bool `json:"split_hosts"`
}
DomainSettings is what an operator can configure about the hostname short links are served on.
type Invalidator ¶
type Invalidator interface {
InvalidateAlias(ctx context.Context, domainID uuid.UUID, alias string)
}
Invalidator clears cached snapshots when a link changes. The redirect cache implements it in M7; a nil Invalidator is valid and means "no cache".
type RootInvalidator ¶
type RootInvalidator interface {
InvalidateRoot()
}
RootInvalidator drops the cached root redirect when it changes. Nil is valid and means the redirect tree is not running in this process.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func (*Service) Create ¶
func (s *Service) Create(ctx context.Context, actor *auth.Identity, in CreateInput) (*domain.Link, error)
Create makes a link, generating an alias when none is supplied.
func (*Service) DomainSettings ¶
func (s *Service) DomainSettings(ctx context.Context, actor *auth.Identity) (*DomainSettings, error)
DomainSettings reads the link domain's settings.
Readable by anyone who can read links: it is one URL an operator chose, and every visitor to the bare domain sees where it points anyway.
func (*Service) List ¶
func (s *Service) List(ctx context.Context, actor *auth.Identity, f domain.LinkFilter) (*domain.Page[domain.Link], error)
List returns a keyset-paginated page of links.
func (*Service) LoadRootRedirect ¶
LoadRootRedirect reads the current value for the redirect path. Unexported callers only: the hot path uses it through a cache.
func (*Service) SetRootRedirect ¶
func (s *Service) SetRootRedirect(ctx context.Context, actor *auth.Identity, rawURL string) (*DomainSettings, error)
SetRootRedirect points the link domain's root somewhere, or clears it.
Three refusals, each of which would otherwise be discovered late.
It needs domains.write rather than links.update: this is not one link, it is where every visitor who trims a short link back to its domain ends up.
It is refused outright on a single-host deployment. There "/" is the dashboard, and honouring this would take the dashboard away from the person who just set it — a failure that reads as the product breaking rather than as a setting doing what it says.
The destination goes through exactly the same validation as a link's, which matters more here than anywhere: a root redirect that skipped the private, loopback and metadata refusals would be a cleaner SSRF than the one the validator exists to prevent, because reaching it needs no link and no alias.