UNPKG

chrome-devtools-frontend

Version:
292 lines (267 loc) 12.6 kB
// Copyright 2026 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as i18n from '../../core/i18n/i18n.js'; const UIStrings = { /** * @description Comment in a generated fetch command explaining why sec-* request headers are commented out. */ secHeadersSetByBrowser: 'All sec-* headers are set by the browser', /** * @description Comment in a generated fetch command explaining why proxy-* request headers are commented out. */ proxyHeadersSetByBrowser: 'All proxy-* headers are set by the browser', /** * @description Comment in a generated fetch command explaining why the Accept-Charset header is commented out. */ deprecatedBrowserDoesNotSend: 'Deprecated; browser does not send this', /** * @description Comment in a generated fetch command explaining why the Accept-Encoding header is commented out. */ browserNegotiatesCompression: 'Browser negotiates compression', /** * @description Comment in a generated fetch command explaining why an Access-Control-Request header is commented out. */ browserSetsDuringCorsPreflight: 'Browser sets during CORS preflight', /** * @description Comment in a generated fetch command explaining why a connection-related header is commented out. */ browserManagesConnections: 'Browser manages connections', /** * @description Comment in a generated fetch command explaining why the Content-Length header is commented out. */ browserCalculatesFromBody: 'Browser calculates from body', /** * @description Comment in a generated fetch command explaining why the Cookie header is commented out. */ browserManagesCookieJar: 'Browser manages this from the cookie jar', /** * @description Comment in a generated fetch command explaining why the Cookie2 header is commented out. */ deprecatedCookieHeader: 'Deprecated cookie header; browser blocks this', /** * @description Comment in a generated fetch command explaining why the Date header is commented out. */ browserControlsRequestDate: 'Browser controls the request date', /** * @description Comment in a generated fetch command explaining why the DNT header is commented out. */ browserSetsPrivacyPreferences: 'Browser sets from user privacy preferences', /** * @description Comment in a generated fetch command explaining why the Expect header is commented out. */ browserManagesRequestExpectations: 'Browser manages request expectations', /** * @description Comment in a generated fetch command explaining why the Host header is commented out. */ browserDerivesFromUrl: 'Browser will derive from URL', /** * @description Comment in a generated fetch command explaining why the Origin header is commented out. */ browserSetsRequestContext: 'Browser will set based on request context', /** * @description Comment in a generated fetch command explaining why the Referer header is commented out. */ browserSetsReferrer: 'Browser will set this from referrer option + policy', /** * @description Comment in a generated fetch command explaining why the Set-Cookie request header is commented out. */ responseHeaderBlockedOnRequests: 'Response header; browser blocks it on requests', /** * @description Comment in a generated fetch command explaining why the TE header is commented out. */ browserManagesTransferCodings: 'Browser manages transfer codings', /** * @description Comment in a generated fetch command explaining why the Trailer header is commented out. */ browserManagesRequestTrailers: 'Browser manages request trailers', /** * @description Comment in a generated fetch command explaining why the Transfer-Encoding header is commented out. */ browserManagesTransferEncoding: 'Browser manages transfer encoding', /** * @description Comment in a generated fetch command explaining why the Upgrade header is commented out. */ browserManagesProtocolUpgrades: 'Browser manages protocol upgrades', /** * @description Comment in a generated fetch command explaining why the Via header is commented out. */ browserAndProxiesManageMetadata: 'Browser and proxies manage forwarding metadata', /** * @description Comment in a generated fetch command explaining why a method override header is commented out. */ browserBlocksForbiddenMethods: 'Browser blocks overrides to forbidden methods', } as const; const str_ = i18n.i18n.registerUIStrings('panels/network/FetchHeaderCommenting.ts', UIStrings); const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_); /** * Defines a pattern for commenting out forbidden headers in a fetch command string. * * @property pattern - Regex to match against the header field name. * @property comment - Lazily computed explanatory comment text. * @property style - 'append' puts the comment on the same line; 'prefix' adds a * standalone comment line before the first match in a consecutive group. * @property isForbidden - Optional predicate for rules whose status depends on * the header value. */ export interface ForbiddenHeaderRule { pattern: RegExp; comment: () => string; style: 'append'|'prefix'; isForbidden?: (value: string) => boolean; } const FORBIDDEN_METHOD_PATTERN = /^(?:CONNECT|TRACE|TRACK)$/i; function containsForbiddenMethod(value: string): boolean { return value.split(',').some(method => FORBIDDEN_METHOD_PATTERN.test(method.trim())); } /** * Rules for request headers that the browser will ignore or override. * https://fetch.spec.whatwg.org/#forbidden-request-header */ export const FORBIDDEN_HEADER_RULES: ForbiddenHeaderRule[] = [ {pattern: /^sec-/i, comment: i18nLazyString(UIStrings.secHeadersSetByBrowser), style: 'prefix'}, {pattern: /^proxy-/i, comment: i18nLazyString(UIStrings.proxyHeadersSetByBrowser), style: 'prefix'}, {pattern: /^accept-charset$/i, comment: i18nLazyString(UIStrings.deprecatedBrowserDoesNotSend), style: 'append'}, {pattern: /^accept-encoding$/i, comment: i18nLazyString(UIStrings.browserNegotiatesCompression), style: 'append'}, { pattern: /^access-control-request-headers$/i, comment: i18nLazyString(UIStrings.browserSetsDuringCorsPreflight), style: 'append', }, { pattern: /^access-control-request-method$/i, comment: i18nLazyString(UIStrings.browserSetsDuringCorsPreflight), style: 'append', }, {pattern: /^connection$/i, comment: i18nLazyString(UIStrings.browserManagesConnections), style: 'append'}, {pattern: /^content-length$/i, comment: i18nLazyString(UIStrings.browserCalculatesFromBody), style: 'append'}, {pattern: /^cookie$/i, comment: i18nLazyString(UIStrings.browserManagesCookieJar), style: 'append'}, {pattern: /^cookie2$/i, comment: i18nLazyString(UIStrings.deprecatedCookieHeader), style: 'append'}, {pattern: /^date$/i, comment: i18nLazyString(UIStrings.browserControlsRequestDate), style: 'append'}, {pattern: /^dnt$/i, comment: i18nLazyString(UIStrings.browserSetsPrivacyPreferences), style: 'append'}, {pattern: /^expect$/i, comment: i18nLazyString(UIStrings.browserManagesRequestExpectations), style: 'append'}, {pattern: /^host$/i, comment: i18nLazyString(UIStrings.browserDerivesFromUrl), style: 'append'}, {pattern: /^keep-alive$/i, comment: i18nLazyString(UIStrings.browserManagesConnections), style: 'append'}, {pattern: /^origin$/i, comment: i18nLazyString(UIStrings.browserSetsRequestContext), style: 'append'}, {pattern: /^referer$/i, comment: i18nLazyString(UIStrings.browserSetsReferrer), style: 'append'}, {pattern: /^set-cookie$/i, comment: i18nLazyString(UIStrings.responseHeaderBlockedOnRequests), style: 'append'}, {pattern: /^te$/i, comment: i18nLazyString(UIStrings.browserManagesTransferCodings), style: 'append'}, {pattern: /^trailer$/i, comment: i18nLazyString(UIStrings.browserManagesRequestTrailers), style: 'append'}, { pattern: /^transfer-encoding$/i, comment: i18nLazyString(UIStrings.browserManagesTransferEncoding), style: 'append', }, {pattern: /^upgrade$/i, comment: i18nLazyString(UIStrings.browserManagesProtocolUpgrades), style: 'append'}, {pattern: /^via$/i, comment: i18nLazyString(UIStrings.browserAndProxiesManageMetadata), style: 'append'}, { pattern: /^x-(?:http-method(?:-override)?|method-override)$/i, comment: i18nLazyString(UIStrings.browserBlocksForbiddenMethods), style: 'append', isForbidden: containsForbiddenMethod, }, ]; function findForbiddenHeaderRule(name: string, value: string, rules: ForbiddenHeaderRule[]): ForbiddenHeaderRule| undefined { return rules.find(rule => rule.pattern.test(name) && (rule.isForbidden?.(value) ?? true)); } export function isForbiddenHeader(name: string, value: string, rules: ForbiddenHeaderRule[] = FORBIDDEN_HEADER_RULES): boolean { return Boolean(findForbiddenHeaderRule(name, value, rules)); } // Matches a typical header line: leading whitespace, then `"key": value` // Captures the indentation, key name, and JSON-encoded value. const HEADER_LINE_RE = /^(\s*)"([^"]+)"\s*:\s*("(?:\\.|[^"\\])*")(?:,)?\s*$/; // Detects the start of the headers block. const HEADERS_START_RE = /^\s*"headers"\s*:\s*\{\s*$/; // Detects a line that closes a block (just whitespace + } with optional comma). const BLOCK_CLOSE_RE = /^\s*\},?\s*$/; /** * Given serialized fetch options, comments out header lines that match any of * the forbidden header rules. * * The format of the serializedOptions is well constrained. * HTTP headers are, by spec, single-line. Multi-value headers will be * joined by commas into one string. Then, JSON.stringify will always render * each field on its own line. * * The function operates line-by-line with a simple 3-mode state machine: * Mode 1: Before the headers block * Mode 2: Inside the headers block * Mode 3: After the headers block (or after bailing on anomaly) * * If an anomalous line is encountered inside the headers block (one that doesn't * look like a simple `"key": value,` entry), processing stops immediately and * remaining lines pass through unchanged. */ export function commentForbiddenHeaders(serializedOptions: string, rules: ForbiddenHeaderRule[] = FORBIDDEN_HEADER_RULES): string { const lines = serializedOptions.split('\n'); const result: string[] = []; const enum Mode { BEFORE_HEADERS = 1, INSIDE_HEADERS = 2, AFTER_HEADERS = 3, } let mode: Mode = Mode.BEFORE_HEADERS; // For grouping consecutive matches with the same 'prefix' rule. let pendingPrefixRule: ForbiddenHeaderRule|null = null; function resetPrefixState(): void { pendingPrefixRule = null; } for (const line of lines) { switch (mode) { case Mode.BEFORE_HEADERS: { result.push(line); if (HEADERS_START_RE.test(line)) { mode = Mode.INSIDE_HEADERS; } break; } case Mode.INSIDE_HEADERS: { // Check for end of headers block. if (BLOCK_CLOSE_RE.test(line)) { resetPrefixState(); result.push(line); mode = Mode.AFTER_HEADERS; break; } // Try to parse as a header line. const match = HEADER_LINE_RE.exec(line); if (!match) { // Anomalous line — bail out. resetPrefixState(); result.push(line); mode = Mode.AFTER_HEADERS; break; } const indent = match[1]; const headerName = match[2]; const headerValue = JSON.parse(match[3]) as string; // Check if this header matches any forbidden rule. const matchedRule = findForbiddenHeaderRule(headerName, headerValue, rules); if (!matchedRule) { // Not forbidden — emit as-is. resetPrefixState(); result.push(line); } else if (matchedRule.style === 'append') { resetPrefixState(); result.push(`${indent}// ${line.trimStart()} // ${matchedRule.comment()}`); } else { // 'prefix' style: emit a heading comment before the first match in a group. if (pendingPrefixRule !== matchedRule) { pendingPrefixRule = matchedRule; result.push(`${indent}// ${matchedRule.comment()}`); } result.push(`${indent}// ${line.trimStart()}`); } break; } case Mode.AFTER_HEADERS: { result.push(line); break; } } } return result.join('\n'); }