| // 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 doesn’t 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'); | |
| } |