Documentation
¶
Overview ¶
Package crawler implements gocrawl's concurrent crawl engine. It fetches pages within a configurable scope, captures redirect chains and robots.txt data, and produces a Result that analyzers consume. The engine has no knowledge of specific SEO/SEA checks.
Index ¶
- func SanitizeSeed(raw string) (seed, user, pass string)
- type AdaptiveFetcher
- type AdaptiveLimiter
- type CertInfo
- type Cookie
- type Coverage
- type Engine
- type Fetcher
- type HTTPFetcher
- type Link
- type Options
- type Page
- type Redirect
- type RenderResult
- type Result
- type RobotsData
- type RotationStrategy
- type TLSInfo
- type UAPool
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SanitizeSeed ¶ added in v0.4.0
SanitizeSeed strips any userinfo (user:pass@) embedded in a seed URL and returns it separately, so callers can route it through Options.BasicAuthUser/BasicAuthPass instead of letting it ride along in the URL string — where it would propagate into Result.Seed, every report format, and `gocrawl history`, and (via Go's URL-resolution semantics) into every link resolved against the seed once crawled. raw is expected to already have a scheme (callers prefix a bare host with "https://" before calling this).
Types ¶
type AdaptiveFetcher ¶ added in v0.4.0
type AdaptiveFetcher struct {
Fetcher Fetcher
Limiter *AdaptiveLimiter
}
AdaptiveFetcher wraps a Fetcher so every call waits on Limiter beforehand and lets it observe the response afterward. This is how tools that fetch a bounded list of URLs directly (rather than crawling through Engine) still back off politely on 429/503.
type AdaptiveLimiter ¶ added in v0.4.0
type AdaptiveLimiter struct {
// contains filtered or unexported fields
}
AdaptiveLimiter is a requests-per-second limiter that halves its rate (down to a floor) whenever a fetch reports HTTP 429 or 503, honoring any Retry-After header, and debounces bursts of concurrent triggers into a single adjustment. It is shared by anything that fetches a live site directly — the crawl Engine and tools like check-redirects — so a run that starts too aggressively for the target backs off instead of hammering it into a bot-mitigation block.
func NewAdaptiveLimiter ¶ added in v0.4.0
func NewAdaptiveLimiter(ratePerSecond float64, enabled bool) *AdaptiveLimiter
NewAdaptiveLimiter creates a limiter starting at ratePerSecond (0 = unlimited). Wait always enforces the current rate; when enabled is false, OnResponse never backs it off.
func (*AdaptiveLimiter) CurrentRate ¶ added in v0.4.0
func (a *AdaptiveLimiter) CurrentRate() float64
CurrentRate reports the rate in effect after any backoff (0 if still unrestricted).
func (*AdaptiveLimiter) OnResponse ¶ added in v0.4.0
func (a *AdaptiveLimiter) OnResponse(statusCode int, header http.Header) (adjusted bool, newRate float64)
OnResponse inspects a fetch outcome and, if it signals overload (429/503), halves the current rate down to backoffMinRate, never faster than any Retry-After header demands. It reports whether this call triggered an adjustment (false if disabled, the status wasn't 429/503, or a trigger already landed within backoffDebounce) and the resulting rate.
func (*AdaptiveLimiter) ThrottleCount ¶ added in v0.4.0
func (a *AdaptiveLimiter) ThrottleCount() int
ThrottleCount reports how many times OnResponse has backed off the rate.
type CertInfo ¶ added in v0.5.0
type CertInfo struct {
Subject string
Issuer string
NotBefore time.Time
NotAfter time.Time
SignatureAlgorithm x509.SignatureAlgorithm
// KeyType and KeyBits describe the public key ("RSA"/2048, "ECDSA"/256, "Ed25519"/256).
// KeyBits is 0 for a key type we don't measure.
KeyType string
KeyBits int
IsCA bool
// SelfSigned is true when issuer and subject are the same distinguished name — either a
// root CA at the top of the chain, or an untrusted self-signed server certificate.
SelfSigned bool
}
CertInfo is the subset of an X.509 certificate the security audit reads. The full certificate is not retained: the audit only needs identity, validity window, and the strength of the signature and key.
type Cookie ¶ added in v0.5.0
type Cookie struct {
Name string
Domain string
Path string
// Session is true for a cookie with no expiry, which the browser drops when it closes.
Session bool
// Expires is the cookie's expiry, zero for a session cookie.
Expires time.Time
Secure bool
HTTPOnly bool
SameSite string
}
Cookie is one entry of the browser's cookie jar, captured during a headless render. It carries what a compliance check needs — identity, scope, and lifetime — and never the value, which is frequently a personal identifier and has no place in a report.
type Coverage ¶ added in v0.2.1
type Coverage struct {
// Complete is true when no in-scope, robots-allowed URL was left un-fetched because of a
// depth or page-count limit.
Complete bool `json:"complete"`
// DiscoveredNotCrawled is the number of distinct in-scope URLs that were discovered but
// never fetched because a limit was reached.
DiscoveredNotCrawled int `json:"discovered_not_crawled,omitempty"`
// PageLimitReached / DepthLimitReached record which configured limit cut the crawl short.
PageLimitReached bool `json:"page_limit_reached,omitempty"`
DepthLimitReached bool `json:"depth_limit_reached,omitempty"`
// Interrupted is true when the crawl's context was canceled before it finished on its own
// (e.g. an operator's Ctrl-C) rather than a configured limit being reached. The site may
// be far less covered than DiscoveredNotCrawled reflects, since much of it may never have
// been discovered at all.
Interrupted bool `json:"interrupted,omitempty"`
// DurationLimitReached is true when the crawl stopped because it hit its --max-duration
// wall-clock budget rather than being interrupted externally. Also implies Interrupted,
// since the same context-cancellation path stops the crawl either way.
DurationLimitReached bool `json:"duration_limit_reached,omitempty"`
// MaxPages / MaxDepth echo the limits in effect (0 = unlimited), for the report message.
MaxPages int `json:"max_pages,omitempty"`
MaxDepth int `json:"max_depth,omitempty"`
}
Coverage summarizes how much of the in-scope site the crawl actually fetched. It exists so the report can warn that "0 broken links" might mean "the broken ones weren't reached" rather than "the site is clean".
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine crawls a website concurrently within the configured scope.
type Fetcher ¶
Fetcher retrieves a single URL and returns a populated Page. Implementations include the raw HTTPFetcher and the (stubbed) headless renderer.
type HTTPFetcher ¶
type HTTPFetcher struct {
// contains filtered or unexported fields
}
HTTPFetcher fetches pages over HTTP(S). It follows redirects manually so the full redirect chain is recorded on the resulting Page.
func NewHTTPFetcher ¶
func NewHTTPFetcher(opts Options) *HTTPFetcher
NewHTTPFetcher builds a fetcher from the given options. When opts.Proxies is non-empty the client routes each request through a proxy chosen by opts.ProxyRotation; otherwise Go's default proxy behavior (environment variables) applies. The User-Agent header is chosen per request from opts.UserAgents / opts.UserAgent via opts.UserAgentRotation.
func (*HTTPFetcher) Fetch ¶
Fetch retrieves rawURL, following redirects manually up to maxRedirects.
func (*HTTPFetcher) RestrictBasicAuthToHost ¶ added in v0.6.0
func (f *HTTPFetcher) RestrictBasicAuthToHost(seedHost string, allowSubdomains bool)
RestrictBasicAuthToHost limits this fetcher's Basic Auth to requests whose host is seedHost, or one of its subdomains when allowSubdomains is set — the same scope rule the crawl itself uses (see Engine.authHostAllowed). Exported so packages outside crawler that build their own HTTPFetcher for crawl-scoped work (currently runner.Run, for the sitemap/geo/wordpress analyzers) can apply the same restriction Engine.New wires onto its own fetchers.
type Link ¶
type Link struct {
URL string `json:"url"` // absolute, normalized for dedup (trailing slash stripped)
Resolved string `json:"resolved,omitempty"` // absolute, normalized but trailing slash preserved
Anchor string `json:"anchor"` // visible text
Rel string `json:"rel"`
Nofollow bool `json:"nofollow"`
External bool `json:"external"` // points outside the seed host
}
Link is an outbound link discovered on a page.
type Options ¶
type Options struct {
MaxDepth int
MaxPages int
Concurrency int
RatePerSecond float64
UserAgent string
// UserAgents is an optional pool of User-Agent strings to rotate across. When non-empty it
// supersedes UserAgent; UserAgentRotation picks one per request.
UserAgents []string
UserAgentRotation RotationStrategy
// Proxies is an optional pool of proxy URLs (http, https, or socks5) to route requests
// through. When non-empty, ProxyRotation picks one per request. Empty leaves Go's default
// proxy behavior (honoring HTTP_PROXY/HTTPS_PROXY/NO_PROXY) in place.
Proxies []*url.URL
ProxyRotation RotationStrategy
// BasicAuthUser and BasicAuthPass, when BasicAuthUser is non-empty, are sent as an HTTP
// Basic Authorization header on every request. This targets server-level Basic Auth (the
// realm challenge common on staging/acceptance environments), sent preemptively rather
// than in response to a 401 challenge.
BasicAuthUser string
BasicAuthPass string
Include []*regexp.Regexp
Exclude []*regexp.Regexp
RespectRobots bool
AllowSubdomains bool
FollowExternal bool
FollowNofollow bool
// StripQuery drops the query string when normalizing URLs, so URLs that differ only by
// their query collapse to one and are crawled once.
StripQuery bool
Timeout time.Duration
MaxBodyBytes int64
MaxRedirects int
// Verbose logs each fetch and every rate-limit change to stderr while crawling.
Verbose bool
// AdaptiveDelay automatically slows the crawl (halving requests-per-second, honoring any
// Retry-After header) when the server responds with HTTP 429 or 503.
AdaptiveDelay bool
// MaxDuration bounds the crawl's total wall-clock time. Zero means unlimited. When it
// elapses, the crawl stops early and still returns everything fetched so far as a
// partial result (see Coverage.DurationLimitReached), the same way a canceled context
// (e.g. an operator's Ctrl-C) does.
MaxDuration time.Duration
}
Options controls crawl scope and politeness.
func DefaultOptions ¶
func DefaultOptions() Options
DefaultOptions returns conservative, polite defaults. By default the crawl is bounded by the total page budget (MaxPages), not by link depth (MaxDepth = 0 = unlimited): a shallow depth cap silently hides whole sections of a site — and the broken links in them — whereas a page budget walks the site breadth-first and stops at a predictable, reported size.
type Page ¶
type Page struct {
RequestedURL string `json:"requested_url"`
FinalURL string `json:"final_url"`
StatusCode int `json:"status_code"`
Header http.Header `json:"-"`
// TLS records the handshake behind the final response: protocol version, cipher suite, and
// the certificate chain the server presented. Nil for plain HTTP, and in headless render
// mode, where the response never passes through Go's TLS stack.
TLS *TLSInfo `json:"-"`
ContentType string `json:"content_type"`
Body []byte `json:"-"`
RawBody []byte `json:"-"` // pre-JS HTML captured during headless render; nil in raw mode
Doc *goquery.Document `json:"-"` // nil if not HTML or parse failed
Redirects []Redirect `json:"redirects,omitempty"`
Links []Link `json:"-"`
Depth int `json:"depth"`
Referrer string `json:"referrer,omitempty"`
Duration time.Duration `json:"duration_ms"`
FetchedAt time.Time `json:"fetched_at"`
Err string `json:"error,omitempty"`
Render *RenderResult `json:"render,omitempty"`
// Truncated reports whether Body was cut short of the real response — either because it
// hit the fetcher's body-size cap or because the connection failed partway through the
// read. A truncated body may be missing elements (e.g. </head>, the closing tag of a
// sitemap) that a downstream analyzer would otherwise expect to find, so treat findings
// like "missing title" or "invalid sitemap" on a truncated page with that in mind.
Truncated bool `json:"truncated,omitempty"`
}
Page is the unit passed from the engine to analyzers.
type RenderResult ¶
type RenderResult struct {
Implemented bool `json:"implemented"`
Note string `json:"note,omitempty"`
// RawFallback is set when the rendered DOM came back substantially thinner than the raw
// HTML — a sign the page had not finished rendering when it was snapshotted. The analyzed
// Body/Doc are then taken from the raw fetch (so structural checks like the H1 and meta
// tags aren't false-negatives), while the Core Web Vitals below stay from the render but
// should be treated as unreliable for this page. RenderedBytes/RawBytes record the sizes
// that triggered it.
RawFallback bool `json:"raw_fallback,omitempty"`
RenderedBytes int `json:"rendered_bytes,omitempty"`
RawBytes int `json:"raw_bytes,omitempty"`
LCP float64 `json:"lcp_ms,omitempty"`
FCP float64 `json:"fcp_ms,omitempty"`
CLS float64 `json:"cls,omitempty"`
TBT float64 `json:"tbt_ms,omitempty"`
TTFB float64 `json:"ttfb_ms,omitempty"`
// DataLayerPresent reports whether window.dataLayer was an array after the page rendered.
DataLayerPresent bool `json:"data_layer_present,omitempty"`
// DataLayer is the post-render snapshot of window.dataLayer, each element the raw JSON of
// one entry (a GTM event push or a gtag() arguments object). It is deliberately not
// serialized into reports: it can carry PII, so the datalayer analyzer emits sanitized
// findings from it instead. Nil in raw mode or when the page has no dataLayer.
DataLayer []json.RawMessage `json:"-"`
// Requests holds outbound request URLs observed during render, bounded to avoid unbounded
// growth. The datalayer analyzer uses it to confirm analytics/marketing tags actually
// fired a network beacon. Not serialized.
Requests []string `json:"-"`
// Cookies is the browser's cookie jar after the page settled — every cookie actually
// stored, first- and third-party, however it was set (Set-Cookie header, document.cookie,
// or a third-party script). Response headers alone cannot see the last two, which is why
// this is captured separately.
//
// gocrawl never interacts with a consent banner, so this jar is the site's *pre-consent*
// state: the consent analyzer reads it to find tracking cookies dropped before a visitor
// has agreed to anything. The jar belongs to the browser, not the tab, so it accumulates
// across the pages of one crawl — treat it as a site-level observation rather than proof
// that a particular page set a particular cookie. Nil in raw mode.
Cookies []Cookie `json:"-"`
}
RenderResult holds headless-rendering output, including lab-mode Core Web Vitals. Implemented is true when chromedp produced real measurements; false when rendering fell back to a raw fetch (Note carries the reason). Metric times are milliseconds; CLS is the unitless layout-shift score. Zero means not collected.
INP is a field-only metric and is not collected in lab mode. TBT (Total Blocking Time) is reported as a lab-mode proxy for responsiveness, matching Lighthouse.
type Result ¶
type Result struct {
Seed string `json:"seed"`
Pages []*Page `json:"pages"`
Robots map[string]*RobotsData `json:"robots,omitempty"`
StartedAt time.Time `json:"started_at"`
Finished time.Time `json:"finished_at"`
Opts Options `json:"-"`
// ThrottleEvents counts how many times adaptive delay reduced the crawl rate after HTTP
// 429/503 responses. FinalRate is the requests-per-second in effect when the crawl ended;
// it is meaningful only when ThrottleEvents > 0.
ThrottleEvents int `json:"throttle_events,omitempty"`
FinalRate float64 `json:"final_rate,omitempty"`
// Coverage reports whether the crawl visited every in-scope URL it discovered, or stopped
// at a configured limit. When not Complete, findings that depend on fetching a page —
// broken links above all — may be incomplete.
Coverage Coverage `json:"coverage"`
// contains filtered or unexported fields
}
Result is the complete output of a crawl, consumed by analyzers.
func (*Result) LinkPointsToRedirect ¶ added in v0.2.2
LinkPointsToRedirect reports whether an internal link whose resolved (slash-preserving) target is `resolved` points at `target`, a crawled page that redirects to a genuinely different URL. It returns false when the page's only redirect is the trailing slash the crawl index strips before fetching, so a link authored with the site's canonical trailing slash (e.g. WordPress permalinks) is not reported as pointing at a redirect.
func (*Result) Page ¶
Page looks up a crawled page by URL (matched against requested and final URLs after normalization). The second return value reports whether it was found.
func (*Result) Reindex ¶
func (r *Result) Reindex()
Reindex rebuilds the URL lookup index from r.Pages. The engine populates the index incrementally during a crawl, so production code never needs this; it lets callers that construct a Result by hand (notably tests) make Page lookups resolve.
func (*Result) ReleaseBodies ¶ added in v0.4.0
func (r *Result) ReleaseBodies()
ReleaseBodies drops every page's Body, RawBody, and parsed Doc, freeing the memory they hold. Call this once analysis and report-building have both finished reading pages — nothing in this codebase reads a page's body or DOM afterward, and a large crawl of heavy pages can otherwise hold multiple GB of raw bytes and parsed DOM trees in memory for the rest of the process's life. That matters most for a long-lived caller handling many crawls in one process (e.g. the MCP server), where the memory would otherwise accumulate across calls until the next GC cycle catches up.
func (*Result) ResolveHref ¶ added in v0.4.0
ResolveHref resolves href relative to from's own URL (mirroring how the crawl engine resolves an <a href> against a page's base — including an in-document <base href>, if any — via extractLinks) and looks up the resulting page in the crawl result. It exists for analyzers that read a raw href straight from the DOM (e.g. <link rel="amphtml"|"next"|"prev">) rather than from the engine's own extracted Links, so a relative href is resolved the same way an anchor link would be instead of failing the lookup outright. resolved is the slash-preserving absolute form, suitable for LinkPointsToRedirect. ok is false for an unusable href (empty, fragment-only, non-http(s) after resolution) or one with no match in the crawl.
type RobotsData ¶
type RobotsData struct {
Host string `json:"host"`
Found bool `json:"found"`
Status int `json:"status"`
Sitemaps []string `json:"sitemaps,omitempty"`
// contains filtered or unexported fields
}
RobotsData is the parsed robots.txt for a single host.
func (*RobotsData) TestAgent ¶
func (r *RobotsData) TestAgent(path, userAgent string) bool
TestAgent reports whether the given path is allowed for userAgent. Per RFC 9309, a 4xx robots.txt response means no rules apply (allow all); an unreachable robots.txt (network error or 5xx) means we can't know the site's intent, so crawling is disallowed until it's reachable. A 200 response we simply couldn't parse degrades to allow-all.
type RotationStrategy ¶
type RotationStrategy int
RotationStrategy selects how a pool (of proxies or User-Agent strings) picks an entry for a given request. It is shared by the proxy pool and the User-Agent pool so both rotate with identical semantics.
const ( // RotateRoundRobin cycles through the pool in order, one entry per request. It is the // default once a pool has more than one entry. RotateRoundRobin RotationStrategy = iota // RotateRandom picks a uniformly random entry per request. RotateRandom // RotateStickyHost maps each target host to a fixed entry (by hash), so every request to a // given host uses the same proxy/User-Agent. For proxies this avoids a server seeing a // single session arrive from several IPs. RotateStickyHost // RotateOff disables rotation: the first entry is always used. A pool with a single entry // behaves this way regardless of strategy. RotateOff )
func ParseRotation ¶
func ParseRotation(s string) (RotationStrategy, error)
ParseRotation converts a config string into a RotationStrategy. An empty string defaults to round-robin, so simply supplying a multi-entry pool rotates without extra configuration.
type TLSInfo ¶ added in v0.5.0
type TLSInfo struct {
Version uint16
CipherSuite uint16
// ALPN is the application protocol negotiated during the handshake ("h2", "http/1.1"), or
// empty when the server offered none.
ALPN string
// Chain is the certificate chain the server presented, leaf first. It is what the server
// actually sent, not the verified chain, so a server that omits its intermediates shows up
// here as a one-element chain even though the fetch succeeded.
Chain []CertInfo
}
TLSInfo is a snapshot of the TLS handshake that carried a page's final response: the negotiated protocol version and cipher suite, the ALPN protocol, and the certificate chain the server presented (leaf first). The engine captures it and knows nothing more about it; interpreting it is the security analyzer's opt-in audit mode's job.
It is deliberately not serialized into reports: every page of a crawl shares one host's handshake, so repeating an identical chain per page would bloat the JSON for no benefit. Findings derived from it travel as analyze.Issue values instead.
func (*TLSInfo) CipherName ¶ added in v0.5.0
CipherName renders the negotiated cipher suite ("TLS_AES_128_GCM_SHA256").
func (*TLSInfo) Leaf ¶ added in v0.5.0
Leaf returns the server (end-entity) certificate, or nil when the chain is empty.
func (*TLSInfo) VersionName ¶ added in v0.5.0
VersionName renders the negotiated protocol version ("TLS 1.3").
type UAPool ¶
type UAPool struct {
// contains filtered or unexported fields
}
UAPool selects a User-Agent string per request. It is built from Options and shared by the raw HTTP fetcher and the headless renderer so both rotate identically. A nil pool, or one with no agents, yields the empty string (callers then leave the header unset).
func NewUAPool ¶
NewUAPool builds a User-Agent pool from opts. When UserAgents is set it is the pool; otherwise the single UserAgent (if any) is the only entry.
func (*UAPool) Default ¶
Default returns the pool's first agent without advancing rotation state. The headless renderer uses it as the browser-level User-Agent; per-navigation rotation is layered on top.