favicon

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package favicon resolves and normalizes the favicon for a domain.

It is built to fetch URLs derived from untrusted, caller-supplied domains, so the network boundary is treated as hostile: every outbound request goes through an SSRF-guarded HTTP client (see fetch.go) that validates the resolved IP at dial time, caps redirects, response size, and decoded pixel count, and rejects non-raster content. The resolver, image pipeline, and cache build on top of that boundary.

Nothing in this package holds credentials or reaches internal services; its only capability is egress to the public internet on ports 80 and 443.

Index

Constants

View Source
const (
	// DefaultPositiveTTL keeps a resolved icon for a long time — a site's favicon
	// is effectively immutable, and the CDN in front does most of the work.
	DefaultPositiveTTL = 30 * 24 * time.Hour
	// DefaultNegativeTTL expires "no icon" results sooner, so a site that later
	// adds a favicon recovers within hours rather than a month.
	DefaultNegativeTTL = 6 * time.Hour

	// DefaultMaxEntries / DefaultMaxBytes bound the cache; whichever limit is hit
	// first drives eviction. Negative entries carry no PNG, so the entry count
	// bounds them while bytes bound the positive set.
	DefaultMaxEntries = 4096
	DefaultMaxBytes   = 64 << 20 // 64 MiB of encoded PNGs
)

Variables

View Source
var (
	// ErrDisallowedScheme is returned for an initial URL, or a redirect target,
	// whose scheme is not http or https.
	ErrDisallowedScheme = errors.New("favicon: disallowed URL scheme")
	// ErrMissingHost is returned when a URL has no host component.
	ErrMissingHost = errors.New("favicon: URL has no host")
	// ErrTooManyRedirects is returned when a fetch exceeds maxRedirects hops.
	ErrTooManyRedirects = errors.New("favicon: too many redirects")
	// ErrBodyTooLarge is returned when the response body exceeds maxBodyBytes.
	ErrBodyTooLarge = errors.New("favicon: response body exceeds size cap")
	// ErrEmptyResponse is returned when the response body is empty.
	ErrEmptyResponse = errors.New("favicon: empty response body")
	// ErrUnsupportedType is returned when the sniffed content type is not an
	// accepted raster image type (this is where SVG/HTML/XML are rejected).
	ErrUnsupportedType = errors.New("favicon: unsupported content type")
	// ErrImageTooLarge is returned when a decoded image's declared dimensions
	// exceed the pixel bound.
	ErrImageTooLarge = errors.New("favicon: image dimensions exceed cap")
	// ErrUpstreamStatus is returned when the upstream responds with a non-200
	// status after redirects are resolved.
	ErrUpstreamStatus = errors.New("favicon: non-200 upstream status")
)

Fetch-boundary errors. They are sentinels so callers (and the test suite) can assert the exact reason a candidate was rejected with errors.Is.

View Source
var ErrInvalidDomain = errors.New("favicon: invalid domain")

ErrInvalidDomain is returned when the domain is not a bare hostname. The HTTP service maps it to a 400.

View Source
var ErrNoIcon = errors.New("favicon: no icon found")

ErrNoIcon is returned when no favicon can be resolved for a domain. The caller (the HTTP service) maps it to a 404, and the result is negatively cached.

Functions

This section is empty.

Types

type Cache

type Cache interface {
	Get(key string) (Entry, bool)
	Put(key string, e Entry)
}

Cache stores resolver results keyed by domain+size. Storing negative results is essential: it stops repeated lookups of icon-less domains from hammering the fetch path.

type Entry

type Entry struct {
	PNG      []byte
	Negative bool
}

Entry is a cached resolution: PNG holds the encoded icon for a positive result; Negative marks a domain currently known to have no resolvable icon.

type FetchResult

type FetchResult struct {
	Body        []byte
	ContentType string
}

FetchResult is a validated favicon candidate. Body holds the original, still-encoded image bytes (decoding and re-encoding happen in the image pipeline); ContentType is the type decided by sniffing, not the header.

type Fetcher

type Fetcher struct {
	// contains filtered or unexported fields
}

Fetcher performs SSRF-guarded HTTP GETs for favicon candidates. A Fetcher is safe for concurrent use and should be reused across requests.

func NewFetcher

func NewFetcher(guardOpts ...ssrf.Option) *Fetcher

NewFetcher builds a Fetcher whose transport will only ever connect to public unicast addresses on ports 80/443 (the default policy of code.dny.dev/ssrf).

guardOpts are applied to the underlying dial guard. Production passes NONE — the safe defaults are the whole point. The options exist so tests can allow loopback (so an httptest server is reachable) without weakening the production configuration. Never pass loosening options outside tests.

func (*Fetcher) Fetch

func (f *Fetcher) Fetch(ctx context.Context, rawURL string) (*FetchResult, error)

Fetch retrieves an icon candidate at rawURL and returns the validated bytes. rawURL must be an absolute http/https URL. Every property of the response — resolved IP, redirect chain, size, content type, and declared image dimensions — is treated as hostile and bounded. The returned bytes are the original encoding; decoding/re-encoding happen in the image pipeline.

func (*Fetcher) FetchHTML

func (f *Fetcher) FetchHTML(ctx context.Context, rawURL string) ([]byte, error)

FetchHTML retrieves an HTML document (a site homepage) for icon discovery. It uses the same SSRF-guarded client as Fetch but applies no image allowlist; the body is capped tighter (only the <head> is needed) and the sniffed type must be text/HTML/XML so a hostile server can't make us buffer a large binary under an HTML content-type header.

type MemoryCache

type MemoryCache struct {
	// contains filtered or unexported fields
}

MemoryCache is a byte- and count-bounded in-process LRU with separate TTLs for positive and negative entries. It is safe for concurrent use.

It is deliberately dependency-free (container/list). An off-the-shelf LRU is count-bounded with at most one TTL; the byte bound and the split positive / negative TTLs don't map onto that cleanly, so a small purpose-built cache is the more robust fit than wrapping one.

func NewMemoryCache

func NewMemoryCache() *MemoryCache

NewMemoryCache returns a cache with the default bounds and TTLs.

func (*MemoryCache) Get

func (c *MemoryCache) Get(key string) (Entry, bool)

Get returns the entry for key if present and unexpired, refreshing its recency. An expired entry is evicted and reported as a miss.

func (*MemoryCache) Put

func (c *MemoryCache) Put(key string, e Entry)

Put stores e under key with the TTL appropriate to its sign, then evicts from the least-recently-used end until both bounds are satisfied.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver turns a domain + size into a normalized PNG favicon, backed by a cache. It owns no network capability beyond the guarded Fetcher it is given.

func NewResolver

func NewResolver(fetcher *Fetcher, cache Cache) *Resolver

NewResolver wires a Fetcher and a Cache into a Resolver.

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, domain string, sz int) ([]byte, error)

Resolve returns a PNG for domain at size sz. It consults the cache first, and on a miss runs the discovery pipeline and caches the result — positive or negative. It returns ErrNoIcon when nothing resolvable is found.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL