chrome-devtools-frontend
Version:
Chrome DevTools UI
292 lines (267 loc) • 12.6 kB
text/typescript
// 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');
}