package delivery import ( "errors" "net/url" ) // urlPathElision stands in for a URL's elided path. const urlPathElision = "/..." // MaskURL renders a URL as scheme plus host with everything // that can carry a secret removed. A delivery target URL is // itself a credential — a Slack incoming webhook URL is a // bearer token — so the path, query and userinfo are never // reproduced, in a page, a log line or a stored error. A URL // that does not parse into a scheme and host yields the // neutral placeholder, never the raw string. func MaskURL(raw string) string { parsed, err := url.Parse(raw) if err != nil || parsed.Scheme == "" || parsed.Host == "" { return configUnavailable } masked := parsed.Scheme + "://" + parsed.Host if parsed.Path != "" && parsed.Path != "/" { masked += urlPathElision } return masked } // maskURLError strips the credential from an error raised // against a request URL. The net/http and net/url packages // embed the full request URL in every *url.Error they return, // so an unmodified transport error persisted into // DeliveryResult.Error writes the credential to disk. // // The masked error keeps the operation and the wrapped cause, // so a DNS failure still reads differently from a refused // connection, a TLS handshake failure or a timeout, and Is, // As, Timeout and Temporary keep working on it. Only the // path, query and userinfo of the URL are dropped. Errors // that carry no URL are returned unchanged. // // Call it where the error is raised, before any wrapping: it // replaces the *url.Error itself, so any context wrapped // around it first would be discarded. func maskURLError(err error) error { var urlErr *url.Error if !errors.As(err, &urlErr) { return err } return &url.Error{ Op: urlErr.Op, URL: MaskURL(urlErr.URL), Err: urlErr.Err, } }