未验证 提交 2ed16d0c 编写于 作者: J Jackson Kearl 提交者: GitHub

Update trusted domains to work with IPs. (#108634)

* Update trusted domains to work with IPs.

* Remove `only`s

* Add new matching rules to help text
上级 adef870d
......@@ -36,10 +36,8 @@ export const manageTrustedDomainSettingsCommand = {
}
};
type ConfigureTrustedDomainChoice = 'trustDomain' | 'trustSubdomain' | 'trustAll' | 'manage';
interface ConfigureTrustedDomainsQuickPickItem extends IQuickPickItem {
id: ConfigureTrustedDomainChoice;
}
type ConfigureTrustedDomainsQuickPickItem = IQuickPickItem & ({ id: 'manage'; } | { id: 'trust'; toTrust: string });
type ConfigureTrustedDomainsChoiceClassification = {
choice: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
......@@ -59,34 +57,54 @@ export async function configureOpenerTrustedDomainsHandler(
const toplevelDomainSegements = parsedDomainToConfigure.authority.split('.');
const domainEnd = toplevelDomainSegements.slice(toplevelDomainSegements.length - 2).join('.');
const topLevelDomain = '*.' + domainEnd;
const options: ConfigureTrustedDomainsQuickPickItem[] = [];
const trustDomainAndOpenLinkItem: ConfigureTrustedDomainsQuickPickItem = {
options.push({
type: 'item',
label: localize('trustedDomain.trustDomain', 'Trust {0}', domainToConfigure),
id: 'trustDomain',
id: 'trust',
toTrust: domainToConfigure,
picked: true
};
const trustSubDomainAndOpenLinkItem: ConfigureTrustedDomainsQuickPickItem = {
type: 'item',
label: localize('trustedDomain.trustSubDomain', 'Trust {0} and all its subdomains', domainEnd),
id: 'trustSubdomain'
};
const openAllLinksItem: ConfigureTrustedDomainsQuickPickItem = {
});
const isIP =
toplevelDomainSegements.length === 4 &&
toplevelDomainSegements.every(segment =>
Number.isInteger(+segment) || Number.isInteger(+segment.split(':')[0]));
if (isIP) {
if (parsedDomainToConfigure.authority.includes(':')) {
const base = parsedDomainToConfigure.authority.split(':')[0];
options.push({
type: 'item',
label: localize('trustedDomain.trustAllPorts', 'Trust {0} on all ports', base),
toTrust: base + ':*',
id: 'trust'
});
}
} else {
options.push({
type: 'item',
label: localize('trustedDomain.trustSubDomain', 'Trust {0} and all its subdomains', domainEnd),
toTrust: topLevelDomain,
id: 'trust'
});
}
options.push({
type: 'item',
label: localize('trustedDomain.trustAllDomains', 'Trust all domains (disables link protection)'),
id: 'trustAll'
};
const manageTrustedDomainItem: ConfigureTrustedDomainsQuickPickItem = {
toTrust: '*',
id: 'trust'
});
options.push({
type: 'item',
label: localize('trustedDomain.manageTrustedDomains', 'Manage Trusted Domains'),
id: 'manage'
};
});
const pickedResult = await quickInputService.pick<ConfigureTrustedDomainsQuickPickItem>(
[trustDomainAndOpenLinkItem, trustSubDomainAndOpenLinkItem, openAllLinksItem, manageTrustedDomainItem],
{
activeItem: trustDomainAndOpenLinkItem
}
options, { activeItem: options[0] }
);
if (pickedResult && pickedResult.id) {
......@@ -104,13 +122,8 @@ export async function configureOpenerTrustedDomainsHandler(
notificationService.prompt(Severity.Info, localize('configuringURL', "Configuring trust for: {0}", resource.toString()),
[{ label: 'Copy', run: () => clipboardService.writeText(resource.toString()) }]);
return trustedDomains;
case 'trustDomain':
case 'trustSubdomain':
case 'trustAll':
const itemToTrust = pickedResult.id === 'trustDomain'
? domainToConfigure
: pickedResult.id === 'trustSubdomain' ? topLevelDomain : '*';
case 'trust':
const itemToTrust = pickedResult.toTrust;
if (trustedDomains.indexOf(itemToTrust) === -1) {
storageService.remove(TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, StorageScope.GLOBAL);
storageService.store(
......
......@@ -28,11 +28,15 @@ const TRUSTED_DOMAINS_STAT: IStat = {
const CONFIG_HELP_TEXT_PRE = `// Links matching one or more entries in the list below can be opened without link protection.
// The following examples show what entries can look like:
// - "https://microsoft.com": Matches this specific domain using https
// - "https://microsoft.com:8080": Matches this specific domain on this port using https
// - "https://microsoft.com:*": Matches this specific domain on any port using https
// - "https://microsoft.com/foo": Matches https://microsoft.com/foo and https://microsoft.com/foo/bar,
// but not https://microsoft.com/foobar or https://microsoft.com/bar
// - "https://*.microsoft.com": Match all domains ending in "microsoft.com" using https
// - "microsoft.com": Match this specific domain using either http or https
// - "*.microsoft.com": Match all domains ending in "microsoft.com" using either http or https
// - "http://192.168.0.1: Matches this specific IP using http
// - "http://192.168.0.*: Matches all IP's with this prefix using http
// - "*": Match all domains using either http or https
//
`;
......
......@@ -188,76 +188,97 @@ export function isURLDomainTrusted(url: URI, trustedDomains: string[]) {
return true;
}
const domain = `${url.scheme}://${url.authority}`;
for (let i = 0; i < trustedDomains.length; i++) {
if (trustedDomains[i] === '*') {
return true;
}
if (trustedDomains[i] === domain) {
if (isTrusted(url.toString(), trustedDomains[i])) {
return true;
}
}
let parsedTrustedDomain;
if (/^https?:\/\//.test(trustedDomains[i])) {
parsedTrustedDomain = URI.parse(trustedDomains[i]);
if (url.scheme !== parsedTrustedDomain.scheme) {
continue;
}
} else {
parsedTrustedDomain = URI.parse('https://' + trustedDomains[i]);
}
return false;
}
if (url.authority === parsedTrustedDomain.authority) {
if (pathMatches(url.path, parsedTrustedDomain.path)) {
return true;
} else {
continue;
}
}
export const isTrusted = (url: string, trustedURL: string): boolean => {
const normalize = (url: string) => url.replace(/\/+$/, '');
trustedURL = normalize(trustedURL);
url = normalize(url);
if (trustedDomains[i].indexOf('*') !== -1) {
const memo = Array.from({ length: url.length + 1 }).map(() =>
Array.from({ length: trustedURL.length + 1 }).map(() => undefined),
);
let reversedAuthoritySegments = url.authority.split('.').reverse();
const reversedTrustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.').reverse();
if (/^[^./:]*:\/\//.test(trustedURL)) {
return doURLMatch(memo, url, trustedURL, 0, 0);
}
if (
reversedTrustedDomainAuthoritySegments.length < reversedAuthoritySegments.length &&
reversedTrustedDomainAuthoritySegments[reversedTrustedDomainAuthoritySegments.length - 1] === '*'
) {
reversedAuthoritySegments = reversedAuthoritySegments.slice(0, reversedTrustedDomainAuthoritySegments.length);
}
const scheme = /^(https?):\/\//.exec(url)?.[1];
if (scheme) {
return doURLMatch(memo, url, `${scheme}://${trustedURL}`, 0, 0);
}
const authorityMatches = reversedAuthoritySegments.every((val, i) => {
return reversedTrustedDomainAuthoritySegments[i] === '*' || val === reversedTrustedDomainAuthoritySegments[i];
});
return false;
};
if (authorityMatches && pathMatches(url.path, parsedTrustedDomain.path)) {
return true;
}
}
const doURLMatch = (
memo: (boolean | undefined)[][],
url: string,
trustedURL: string,
urlOffset: number,
trustedURLOffset: number,
): boolean => {
if (memo[urlOffset]?.[trustedURLOffset] !== undefined) {
return memo[urlOffset][trustedURLOffset]!;
}
return false;
}
const options = [];
function pathMatches(open: string, rule: string) {
if (rule === '/') {
return true;
// Endgame.
// Fully exact match
if (urlOffset === url.length) {
return trustedURLOffset === trustedURL.length;
}
if (rule[rule.length - 1] === '/') {
rule = rule.slice(0, -1);
// Some path remaining in url
if (trustedURLOffset === trustedURL.length) {
const remaining = url.slice(urlOffset);
return remaining[0] === '/';
}
const openSegments = open.split('/');
const ruleSegments = rule.split('/');
for (let i = 0; i < ruleSegments.length; i++) {
if (ruleSegments[i] !== openSegments[i]) {
return false;
if (url[urlOffset] === trustedURL[trustedURLOffset]) {
// Exact match.
options.push(doURLMatch(memo, url, trustedURL, urlOffset + 1, trustedURLOffset + 1));
}
if (trustedURL[trustedURLOffset] + trustedURL[trustedURLOffset + 1] === '*.') {
// Any subdomain match. Either consume one thing that's not a / or : and don't advance base or consume nothing and do.
if (!['/', ':'].includes(url[urlOffset])) {
options.push(doURLMatch(memo, url, trustedURL, urlOffset + 1, trustedURLOffset));
}
options.push(doURLMatch(memo, url, trustedURL, urlOffset, trustedURLOffset + 2));
}
return true;
}
if (trustedURL[trustedURLOffset] + trustedURL[trustedURLOffset + 1] === '.*' && url[urlOffset] === '.') {
// IP mode. Consume one segment of numbers or nothing.
let endBlockIndex = urlOffset + 1;
do { endBlockIndex++; } while (/[0-9]/.test(url[endBlockIndex]));
if (['.', ':', '/', undefined].includes(url[endBlockIndex])) {
options.push(doURLMatch(memo, url, trustedURL, endBlockIndex, trustedURLOffset + 2));
}
}
if (trustedURL[trustedURLOffset] + trustedURL[trustedURLOffset + 1] === ':*') {
// any port match. Consume a port if it exists otherwise nothing. Always comsume the base.
if (url[urlOffset] === ':') {
let endPortIndex = urlOffset + 1;
do { endPortIndex++; } while (/[0-9]/.test(url[endPortIndex]));
options.push(doURLMatch(memo, url, trustedURL, endPortIndex, trustedURLOffset + 2));
} else {
options.push(doURLMatch(memo, url, trustedURL, urlOffset, trustedURLOffset + 2));
}
}
return (memo[urlOffset][trustedURLOffset] = options.some(a => a === true));
};
......@@ -10,10 +10,10 @@ import { URI } from 'vs/base/common/uri';
import { extractGitHubRemotesFromGitConfig } from 'vs/workbench/contrib/url/browser/trustedDomains';
function linkAllowedByRules(link: string, rules: string[]) {
assert.ok(isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should be protected by rules\n${JSON.stringify(rules)}`);
assert.ok(isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should be allowed by rules\n${JSON.stringify(rules)}`);
}
function linkNotAllowedByRules(link: string, rules: string[]) {
assert.ok(!isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should NOT be protected by rules\n${JSON.stringify(rules)}`);
assert.ok(!isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should NOT be allowed by rules\n${JSON.stringify(rules)}`);
}
suite('GitHub remote extraction', () => {
......@@ -63,11 +63,6 @@ suite('Link protection domain matching', () => {
test('* star', () => {
linkAllowedByRules('https://a.x.org', ['https://*.x.org']);
linkAllowedByRules('https://a.b.x.org', ['https://*.x.org']);
linkAllowedByRules('https://a.x.org', ['https://a.x.*']);
linkAllowedByRules('https://a.x.org', ['https://a.*.org']);
linkAllowedByRules('https://a.x.org', ['https://*.*.org']);
linkAllowedByRules('https://a.b.x.org', ['https://*.b.*.org']);
linkAllowedByRules('https://a.a.b.x.org', ['https://*.b.*.org']);
});
test('no scheme', () => {
......@@ -102,6 +97,25 @@ suite('Link protection domain matching', () => {
linkAllowedByRules('https://github.com', ['https://github.com/foo/bar', 'https://github.com']);
});
test('ports', () => {
linkNotAllowedByRules('https://x.org:8080/foo/bar', ['https://x.org:8081/foo']);
linkAllowedByRules('https://x.org:8080/foo/bar', ['https://x.org:*/foo']);
linkAllowedByRules('https://x.org/foo/bar', ['https://x.org:*/foo']);
linkAllowedByRules('https://x.org:8080/foo/bar', ['https://x.org:8080/foo']);
});
test('ip addresses', () => {
linkAllowedByRules('http://192.168.1.7/', ['http://192.168.1.7/']);
linkAllowedByRules('http://192.168.1.7/', ['http://192.168.1.7']);
linkAllowedByRules('http://192.168.1.7/', ['http://192.168.1.*']);
linkNotAllowedByRules('http://192.168.1.7:3000/', ['http://192.168.*.6:*']);
linkAllowedByRules('http://192.168.1.7:3000/', ['http://192.168.1.7:3000/']);
linkAllowedByRules('http://192.168.1.7:3000/', ['http://192.168.1.7:*']);
linkAllowedByRules('http://192.168.1.7:3000/', ['http://192.168.1.*:*']);
linkNotAllowedByRules('http://192.168.1.7:3000/', ['http://192.168.*.6:*']);
});
test('case normalization', () => {
// https://github.com/microsoft/vscode/issues/99294
linkAllowedByRules('https://github.com/microsoft/vscode/issues/new', ['https://github.com/microsoft']);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册