Change:
Issue Type: <bBug</bWrite http://192.168.1.7:3000/ into a text file. Command-click it to try to open the link. You will get an alert asking whether you want to open the external website. Click “Configure Trusted Domains”. <img width="1025" alt="link and dialog asking to confirm opening external website" src="https://user-"Look at the second option in the menu. It says “Trust 1.7:3000 and all its subdomains”. But 1.7:3000 is not a valid address. <img width="1024" alt="link and menu with buggy entry “Trust 1.7:3000 and all its subdomains”" src="https://user-"I can only guess that VS Code thinks that “7” is a TLD and “1.7” is a full domain name. What I actually want to do is whitelist opening all links to IP addresses like . In other words, white-list the subnet . At least, I think that makes sense. But I am not a network engineer, so if it makes more sense to recommend to the user whitelisting some other subnet such as , or whitelisting only the address but on any port, that would be fine too. VS Code version: Code 1.40.1 (, 2019-11-13T16:47:44.719Z) OS version: Darwin x64 18.7.0 <!-- generated by issue reporter --

Discussion:
} }; <del> type ConfigureTrustedDomainChoice = 'trustDomain' | 'trustSubdomain' | 'trustAll' | 'manage'; interface ConfigureTrustedDomainsQuickPickItem extends IQuickPickItem { id: ConfigureTrustedDomainChoice; } </del> <ins> type ConfigureTrustedDomainsQuickPickItem = IQuickPickItem & ({ id: 'manage'; } | { id: 'trust'; toTrust: string }); </ins> type ConfigureTrustedDomainsChoiceClassification = { choice: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; };

const toplevelDomainSegements = parsedDomainToConfigure.authority.split('.'); const domainEnd = toplevelDomainSegements.slice(toplevelDomainSegements.length - 2).join('.'); const topLevelDomain = '*.' + domainEnd; <ins> const options: ConfigureTrustedDomainsQuickPickItem[] = []; </ins> <del> const trustDomainAndOpenLinkItem: ConfigureTrustedDomainsQuickPickItem = { </del> <ins> options.push({ </ins> type: 'item', label: localize('trustedDomain.trustDomain', 'Trust {0}', domainToConfigure), <del> id: 'trustDomain', </del> <ins> id: 'trust', toTrust: domainToConfigure, </ins> picked: true <del> }; const trustSubDomainAndOpenLinkItem: ConfigureTrustedDomainsQuickPickItem = { type: 'item', label: localize('trustedDomain.trustSubDomain', 'Trust {0} and all its subdomains', domainEnd), id: 'trustSubdomain' }; const openAllLinksItem: ConfigureTrustedDomainsQuickPickItem = { </del> <ins> }); 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({ </ins> type: 'item', label: localize('trustedDomain.trustAllDomains', 'Trust all domains (disables link protection)'), <del> id: 'trustAll' }; const manageTrustedDomainItem: ConfigureTrustedDomainsQuickPickItem = { </del> <ins> toTrust: '*', id: 'trust' }); options.push({ </ins> type: 'item', label: localize('trustedDomain.manageTrustedDomains', 'Manage Trusted Domains'), id: 'manage' <del> }; </del> <ins> }); </ins> const pickedResult = await quickInputService.pick<ConfigureTrustedDomainsQuickPickItem>( <del> [trustDomainAndOpenLinkItem, trustSubDomainAndOpenLinkItem, openAllLinksItem, manageTrustedDomainItem], { activeItem: trustDomainAndOpenLinkItem } </del> <ins> options, { activeItem: options[0] } </ins> ); if (pickedResult && pickedResult.id) {

notificationService.prompt(Severity.Info, localize('configuringURL', "Configuring trust for: {0}", resource.toString()), [{ label: 'Copy', run: () => clipboardService.writeText(resource.toString()) }]); return trustedDomains; <del> case 'trustDomain': case 'trustSubdomain': case 'trustAll': const itemToTrust = pickedResult.id === 'trustDomain' ? domainToConfigure : pickedResult.id === 'trustSubdomain' ? topLevelDomain : '*'; </del> <ins> case 'trust': const itemToTrust = pickedResult.toTrust; </ins> if (trustedDomains.indexOf(itemToTrust) === -1) { storageService.remove(TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, StorageScope.GLOBAL); storageService.store(

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 <ins> // - "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 </ins> // - "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 <ins> // - "http://192.168.0.1: Matches this specific IP using http // - "http://192.168.0.*: Matches all IP's with this prefix using http </ins> // - "*": Match all domains using either http or https // `;

return true; } <del> const domain = `${url.scheme}://${url.authority}`; </del> for (let i = 0; i < trustedDomains.length; i++) { if (trustedDomains[i] === '*') { return true; } <del> if (trustedDomains[i] === domain) { </del> <ins> if (isTrusted(url.toString(), trustedDomains[i])) { </ins> return true; } <ins> } </ins> <del> 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]); } </del> <ins> return false; } </ins> <del> if (url.authority === parsedTrustedDomain.authority) { if (pathMatches(url.path, parsedTrustedDomain.path)) { return true; } else { continue; } } </del> <ins> export const isTrusted = (url: string, trustedURL: string): boolean => { const normalize = (url: string) => url.replace(//+$/, ''); trustedURL = normalize(trustedURL); url = normalize(url); </ins> <del> if (trustedDomains[i].indexOf('*') !== -1) { </del> <ins> const memo = Array.from({ length: url.length + 1 }).map(() => Array.from({ length: trustedURL.length + 1 }).map(() => undefined), ); </ins> <del> let reversedAuthoritySegments = url.authority.split('.').reverse(); const reversedTrustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.').reverse(); </del> <ins> if (/^[^./:]*:///.test(trustedURL)) { return doURLMatch(memo, url, trustedURL, 0, 0); } </ins> <del> if ( reversedTrustedDomainAuthoritySegments.length < reversedAuthoritySegments.length && reversedTrustedDomainAuthoritySegments[reversedTrustedDomainAuthoritySegments.length - 1] === '*' ) { reversedAuthoritySegments = reversedAuthoritySegments.slice(0, reversedTrustedDomainAuthoritySegments.length); } </del> <ins> const scheme = /^(https?):///.exec(url)?.[1]; if (scheme) { return doURLMatch(memo, url, `${scheme}://${trustedURL}`, 0, 0); } </ins> <del> const authorityMatches = reversedAuthoritySegments.every((val, i) => { return reversedTrustedDomainAuthoritySegments[i] === '*' || val === reversedTrustedDomainAuthoritySegments[i]; }); </del> <ins> return false; }; </ins> <del> if (authorityMatches && pathMatches(url.path, parsedTrustedDomain.path)) { return true; } } </del> <ins> const doURLMatch = ( memo: (boolean | undefined)[][], url: string, trustedURL: string, urlOffset: number, trustedURLOffset: number, ): boolean => { if (memo[urlOffset]?.[trustedURLOffset] !== undefined) { return memo[urlOffset][trustedURLOffset]!; </ins> } <del> return false; } </del> <ins> const options = []; </ins> <del> function pathMatches(open: string, rule: string) { if (rule === '/') { return true; </del> <ins> // Endgame. // Fully exact match if (urlOffset === url.length) { return trustedURLOffset === trustedURL.length; </ins> } <del> if (rule[rule.length - 1] === '/') { rule = rule.slice(0, -1); </del> <ins> // Some path remaining in url if (trustedURLOffset === trustedURL.length) { const remaining = url.slice(urlOffset); return remaining[0] === '/'; </ins> } <del> const openSegments = open.split('/'); const ruleSegments = rule.split('/'); for (let i = 0; i < ruleSegments.length; i++) { if (ruleSegments[i] !== openSegments[i]) { return false; </del> <ins> 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)); </ins> } <ins> options.push(doURLMatch(memo, url, trustedURL, urlOffset, trustedURLOffset + 2)); </ins> } <del> return true; } </del> <ins> 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)); }; </ins>

import { extractGitHubRemotesFromGitConfig } from 'vs/workbench/contrib/url/browser/trustedDomains'; function linkAllowedByRules(link: string, rules: string[]) { <del> assert.ok(isURLDomainTrusted(URI.parse(link), rules), `Linkn${link}n should be protected by rulesn${JSON.stringify(rules)}`); </del> <ins> assert.ok(isURLDomainTrusted(URI.parse(link), rules), `Linkn${link}n should be allowed by rulesn${JSON.stringify(rules)}`); </ins> } function linkNotAllowedByRules(link: string, rules: string[]) { <del> assert.ok(!isURLDomainTrusted(URI.parse(link), rules), `Linkn${link}n should NOT be protected by rulesn${JSON.stringify(rules)}`); </del> <ins> assert.ok(!isURLDomainTrusted(URI.parse(link), rules), `Linkn${link}n should NOT be allowed by rulesn${JSON.stringify(rules)}`); </ins> } suite('GitHub remote extraction', () => {

test('* star', () => { linkAllowedByRules('https://a.x.org', ['https://*.x.org']); linkAllowedByRules('https://a.b.x.org', ['https://*.x.org']); <del> 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']); </del> }); test('no scheme', () => {

linkAllowedByRules('https://github.com', ['https://github.com/foo/bar', 'https://github.com']); }); <ins> 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:*']); }); </ins> test('case normalization', () => { // https://github.com/microsoft/vscode/issues/99294 linkAllowedByRules('https://github.com/microsoft/vscode/issues/new', ['https://github.com/microsoft']);