diff options
Diffstat (limited to 'cli/js/web/url.ts')
-rw-r--r-- | cli/js/web/url.ts | 627 |
1 files changed, 0 insertions, 627 deletions
diff --git a/cli/js/web/url.ts b/cli/js/web/url.ts deleted file mode 100644 index fabef3329..000000000 --- a/cli/js/web/url.ts +++ /dev/null @@ -1,627 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { build } from "../build.ts"; -import { getRandomValues } from "../ops/get_random_values.ts"; -import { domainToAscii } from "../ops/idna.ts"; -import { customInspect } from "./console.ts"; -import { TextEncoder } from "./text_encoding.ts"; -import { urls } from "./url_search_params.ts"; - -interface URLParts { - protocol: string; - slashes: string; - username: string; - password: string; - hostname: string; - port: string; - path: string; - query: string; - hash: string; -} - -const searchParamsMethods: Array<keyof URLSearchParams> = [ - "append", - "delete", - "set", -]; - -const specialSchemes = ["ftp", "file", "http", "https", "ws", "wss"]; - -// https://url.spec.whatwg.org/#special-scheme -const schemePorts: Record<string, string> = { - ftp: "21", - file: "", - http: "80", - https: "443", - ws: "80", - wss: "443", -}; -const MAX_PORT = 2 ** 16 - 1; - -// Remove the part of the string that matches the pattern and return the -// remainder (RHS) as well as the first captured group of the matched substring -// (LHS). e.g. -// takePattern("https://deno.land:80", /^([a-z]+):[/]{2}/) -// = ["http", "deno.land:80"] -// takePattern("deno.land:80", /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/) -// = ["deno.land", "80"] -function takePattern(string: string, pattern: RegExp): [string, string] { - let capture = ""; - const rest = string.replace(pattern, (_, capture_) => { - capture = capture_; - return ""; - }); - return [capture, rest]; -} - -function parse(url: string, isBase = true): URLParts | undefined { - const parts: Partial<URLParts> = {}; - let restUrl; - [parts.protocol, restUrl] = takePattern(url.trim(), /^([a-z]+):/); - if (isBase && parts.protocol == "") { - return undefined; - } - const isSpecial = specialSchemes.includes(parts.protocol); - if (parts.protocol == "file") { - parts.slashes = "//"; - parts.username = ""; - parts.password = ""; - [parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/); - parts.port = ""; - if (build.os == "windows" && parts.hostname == "") { - // UNC paths. e.g. "\\\\localhost\\foo\\bar" on Windows should be - // representable as `new URL("file:////localhost/foo/bar")` which is - // equivalent to: `new URL("file://localhost/foo/bar")`. - [parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2,}([^/\\?#]*)/); - } - } else { - let restAuthority; - if (isSpecial) { - parts.slashes = "//"; - [restAuthority, restUrl] = takePattern(restUrl, /^[/\\]{2,}([^/\\?#]*)/); - } else { - parts.slashes = restUrl.match(/^[/\\]{2}/) ? "//" : ""; - [restAuthority, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/); - } - let restAuthentication; - [restAuthentication, restAuthority] = takePattern(restAuthority, /^(.*)@/); - [parts.username, restAuthentication] = takePattern( - restAuthentication, - /^([^:]*)/, - ); - parts.username = encodeUserinfo(parts.username); - [parts.password] = takePattern(restAuthentication, /^:(.*)/); - parts.password = encodeUserinfo(parts.password); - [parts.hostname, restAuthority] = takePattern( - restAuthority, - /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/, - ); - [parts.port] = takePattern(restAuthority, /^:(.*)/); - if (!isValidPort(parts.port)) { - return undefined; - } - if (parts.hostname == "" && isSpecial && isBase) { - return undefined; - } - } - try { - parts.hostname = encodeHostname(parts.hostname, isSpecial); - } catch { - return undefined; - } - [parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/); - parts.path = encodePathname(parts.path.replace(/\\/g, "/")); - [parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/); - parts.query = encodeSearch(parts.query); - [parts.hash] = takePattern(restUrl, /^(#.*)/); - parts.hash = encodeHash(parts.hash); - return parts as URLParts; -} - -// Based on https://github.com/kelektiv/node-uuid -// TODO(kevinkassimo): Use deno_std version once possible. -function generateUUID(): string { - return "00000000-0000-4000-8000-000000000000".replace(/[0]/g, (): string => - // random integer from 0 to 15 as a hex digit. - (getRandomValues(new Uint8Array(1))[0] % 16).toString(16)); -} - -// Keep it outside of URL to avoid any attempts of access. -export const blobURLMap = new Map<string, Blob>(); - -function isAbsolutePath(path: string): boolean { - return path.startsWith("/"); -} - -// Resolves `.`s and `..`s where possible. -// Preserves repeating and trailing `/`s by design. -// On Windows, drive letter paths will be given a leading slash, and also a -// trailing slash if there are no other components e.g. "C:" -> "/C:/". -function normalizePath(path: string, isFilePath = false): string { - if (build.os == "windows" && isFilePath) { - path = path.replace(/^\/*([A-Za-z]:)(\/|$)/, "/$1/"); - } - const isAbsolute = isAbsolutePath(path); - path = path.replace(/^\//, ""); - const pathSegments = path.split("/"); - - const newPathSegments: string[] = []; - for (let i = 0; i < pathSegments.length; i++) { - const previous = newPathSegments[newPathSegments.length - 1]; - if ( - pathSegments[i] == ".." && - previous != ".." && - (previous != undefined || isAbsolute) - ) { - newPathSegments.pop(); - } else if (pathSegments[i] != ".") { - newPathSegments.push(pathSegments[i]); - } - } - - let newPath = newPathSegments.join("/"); - if (!isAbsolute) { - if (newPathSegments.length == 0) { - newPath = "."; - } - } else { - newPath = `/${newPath}`; - } - return newPath; -} - -// Standard URL basing logic, applied to paths. -function resolvePathFromBase( - path: string, - basePath: string, - isFilePath = false, -): string { - let normalizedPath = normalizePath(path, isFilePath); - let normalizedBasePath = normalizePath(basePath, isFilePath); - - let driveLetterPrefix = ""; - if (build.os == "windows" && isFilePath) { - let driveLetter: string; - let baseDriveLetter: string; - [driveLetter, normalizedPath] = takePattern( - normalizedPath, - /^(\/[A-Za-z]:)(?=\/)/, - ); - [baseDriveLetter, normalizedBasePath] = takePattern( - normalizedBasePath, - /^(\/[A-Za-z]:)(?=\/)/, - ); - driveLetterPrefix = driveLetter || baseDriveLetter; - } - - if (isAbsolutePath(normalizedPath)) { - return `${driveLetterPrefix}${normalizedPath}`; - } - if (!isAbsolutePath(normalizedBasePath)) { - throw new TypeError("Base path must be absolute."); - } - - // Special case. - if (path == "") { - return `${driveLetterPrefix}${normalizedBasePath}`; - } - - // Remove everything after the last `/` in `normalizedBasePath`. - const prefix = normalizedBasePath.replace(/[^\/]*$/, ""); - // If `normalizedPath` ends with `.` or `..`, add a trailing slash. - const suffix = normalizedPath.replace(/(?<=(^|\/)(\.|\.\.))$/, "/"); - - return `${driveLetterPrefix}${normalizePath(prefix + suffix)}`; -} - -function isValidPort(value: string): boolean { - // https://url.spec.whatwg.org/#port-state - if (value === "") return true; - - const port = Number(value); - return Number.isInteger(port) && port >= 0 && port <= MAX_PORT; -} - -/** @internal */ -export const parts = new WeakMap<URL, URLParts>(); - -export class URLImpl implements URL { - #searchParams!: URLSearchParams; - - [customInspect](): string { - const keys = [ - "href", - "origin", - "protocol", - "username", - "password", - "host", - "hostname", - "port", - "pathname", - "hash", - "search", - ]; - const objectString = keys - .map((key: string) => `${key}: "${this[key as keyof this] || ""}"`) - .join(", "); - return `URL { ${objectString} }`; - } - - #updateSearchParams = (): void => { - const searchParams = new URLSearchParams(this.search); - - for (const methodName of searchParamsMethods) { - /* eslint-disable @typescript-eslint/no-explicit-any */ - const method: (...args: any[]) => any = searchParams[methodName]; - searchParams[methodName] = (...args: unknown[]): any => { - method.apply(searchParams, args); - this.search = searchParams.toString(); - }; - /* eslint-enable */ - } - this.#searchParams = searchParams; - - urls.set(searchParams, this); - }; - - get hash(): string { - return parts.get(this)!.hash; - } - - set hash(value: string) { - value = unescape(String(value)); - if (!value) { - parts.get(this)!.hash = ""; - } else { - if (value.charAt(0) !== "#") { - value = `#${value}`; - } - // hashes can contain % and # unescaped - parts.get(this)!.hash = encodeHash(value); - } - } - - get host(): string { - return `${this.hostname}${this.port ? `:${this.port}` : ""}`; - } - - set host(value: string) { - value = String(value); - const url = new URL(`http://${value}`); - parts.get(this)!.hostname = url.hostname; - parts.get(this)!.port = url.port; - } - - get hostname(): string { - return parts.get(this)!.hostname; - } - - set hostname(value: string) { - value = String(value); - try { - const isSpecial = specialSchemes.includes(parts.get(this)!.protocol); - parts.get(this)!.hostname = encodeHostname(value, isSpecial); - } catch {} - } - - get href(): string { - const authentication = this.username || this.password - ? `${this.username}${this.password ? ":" + this.password : ""}@` - : ""; - const host = this.host; - const slashes = host ? "//" : parts.get(this)!.slashes; - let pathname = this.pathname; - if (pathname.charAt(0) != "/" && pathname != "" && host != "") { - pathname = `/${pathname}`; - } - return `${this.protocol}${slashes}${authentication}${host}${pathname}${this.search}${this.hash}`; - } - - set href(value: string) { - value = String(value); - if (value !== this.href) { - const url = new URL(value); - parts.set(this, { ...parts.get(url)! }); - this.#updateSearchParams(); - } - } - - get origin(): string { - if (this.host) { - return `${this.protocol}//${this.host}`; - } - return "null"; - } - - get password(): string { - return parts.get(this)!.password; - } - - set password(value: string) { - value = String(value); - parts.get(this)!.password = encodeUserinfo(value); - } - - get pathname(): string { - let path = parts.get(this)!.path; - if (specialSchemes.includes(parts.get(this)!.protocol)) { - if (path.charAt(0) != "/") { - path = `/${path}`; - } - } - return path; - } - - set pathname(value: string) { - parts.get(this)!.path = encodePathname(String(value)); - } - - get port(): string { - const port = parts.get(this)!.port; - if (schemePorts[parts.get(this)!.protocol] === port) { - return ""; - } - - return port; - } - - set port(value: string) { - if (!isValidPort(value)) { - return; - } - parts.get(this)!.port = value.toString(); - } - - get protocol(): string { - return `${parts.get(this)!.protocol}:`; - } - - set protocol(value: string) { - value = String(value); - if (value) { - if (value.charAt(value.length - 1) === ":") { - value = value.slice(0, -1); - } - parts.get(this)!.protocol = encodeURIComponent(value); - } - } - - get search(): string { - return parts.get(this)!.query; - } - - set search(value: string) { - value = String(value); - const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`; - parts.get(this)!.query = encodeSearch(query); - this.#updateSearchParams(); - } - - get username(): string { - return parts.get(this)!.username; - } - - set username(value: string) { - value = String(value); - parts.get(this)!.username = encodeUserinfo(value); - } - - get searchParams(): URLSearchParams { - return this.#searchParams; - } - - constructor(url: string | URL, base?: string | URL) { - let baseParts: URLParts | undefined; - if (base) { - baseParts = typeof base === "string" ? parse(base) : parts.get(base); - if (baseParts === undefined) { - throw new TypeError("Invalid base URL."); - } - } - - const urlParts = typeof url === "string" - ? parse(url, !baseParts) - : parts.get(url); - if (urlParts == undefined) { - throw new TypeError("Invalid URL."); - } - - if (urlParts.protocol) { - urlParts.path = normalizePath(urlParts.path, urlParts.protocol == "file"); - parts.set(this, urlParts); - } else if (baseParts) { - parts.set(this, { - protocol: baseParts.protocol, - slashes: baseParts.slashes, - username: baseParts.username, - password: baseParts.password, - hostname: baseParts.hostname, - port: baseParts.port, - path: resolvePathFromBase( - urlParts.path, - baseParts.path || "/", - baseParts.protocol == "file", - ), - query: urlParts.query, - hash: urlParts.hash, - }); - } else { - throw new TypeError("Invalid URL."); - } - - this.#updateSearchParams(); - } - - toString(): string { - return this.href; - } - - toJSON(): string { - return this.href; - } - - // TODO(kevinkassimo): implement MediaSource version in the future. - static createObjectURL(b: Blob): string { - const origin = "http://deno-opaque-origin"; - const key = `blob:${origin}/${generateUUID()}`; - blobURLMap.set(key, b); - return key; - } - - static revokeObjectURL(url: string): void { - let urlObject; - try { - urlObject = new URL(url); - } catch { - throw new TypeError("Provided URL string is not valid"); - } - if (urlObject.protocol !== "blob:") { - return; - } - // Origin match check seems irrelevant for now, unless we implement - // persisten storage for per globalThis.location.origin at some point. - blobURLMap.delete(url); - } -} - -function parseIpv4Number(s: string): number { - if (s.match(/^(0[Xx])[0-9A-Za-z]+$/)) { - return Number(s); - } - if (s.match(/^[0-9]+$/)) { - return Number(s.startsWith("0") ? `0o${s}` : s); - } - return NaN; -} - -function parseIpv4(s: string): string { - const parts = s.split("."); - if (parts[parts.length - 1] == "" && parts.length > 1) { - parts.pop(); - } - if (parts.includes("") || parts.length > 4) { - return s; - } - const numbers = parts.map(parseIpv4Number); - if (numbers.includes(NaN)) { - return s; - } - const last = numbers.pop()!; - if (last >= 256 ** (4 - numbers.length) || numbers.find((n) => n >= 256)) { - throw new TypeError("Invalid hostname."); - } - const ipv4 = numbers.reduce((sum, n, i) => sum + n * 256 ** (3 - i), last); - const ipv4Hex = ipv4.toString(16).padStart(8, "0"); - const ipv4HexParts = ipv4Hex.match(/(..)(..)(..)(..)$/)!.slice(1); - return ipv4HexParts.map((s) => String(Number(`0x${s}`))).join("."); -} - -function charInC0ControlSet(c: string): boolean { - return (c >= "\u0000" && c <= "\u001F") || c > "\u007E"; -} - -function charInSearchSet(c: string): boolean { - // deno-fmt-ignore - return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u0027", "\u003C", "\u003E"].includes(c) || c > "\u007E"; -} - -function charInFragmentSet(c: string): boolean { - // deno-fmt-ignore - return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u003C", "\u003E", "\u0060"].includes(c); -} - -function charInPathSet(c: string): boolean { - // deno-fmt-ignore - return charInFragmentSet(c) || ["\u0023", "\u003F", "\u007B", "\u007D"].includes(c); -} - -function charInUserinfoSet(c: string): boolean { - // "\u0027" ("'") seemingly isn't in the spec, but matches Chrome and Firefox. - // deno-fmt-ignore - return charInPathSet(c) || ["\u0027", "\u002F", "\u003A", "\u003B", "\u003D", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E", "\u007C"].includes(c); -} - -function charIsForbiddenInHost(c: string): boolean { - // deno-fmt-ignore - return ["\u0000", "\u0009", "\u000A", "\u000D", "\u0020", "\u0023", "\u0025", "\u002F", "\u003A", "\u003C", "\u003E", "\u003F", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E"].includes(c); -} - -const encoder = new TextEncoder(); - -function encodeChar(c: string): string { - return [...encoder.encode(c)] - .map((n) => `%${n.toString(16)}`) - .join("") - .toUpperCase(); -} - -function encodeUserinfo(s: string): string { - return [...s].map((c) => (charInUserinfoSet(c) ? encodeChar(c) : c)).join(""); -} - -function encodeHostname(s: string, isSpecial = true): string { - // IPv6 parsing. - if (s.startsWith("[") && s.endsWith("]")) { - if (!s.match(/^\[[0-9A-Fa-f.:]{2,}\]$/)) { - throw new TypeError("Invalid hostname."); - } - // IPv6 address compress - return s.toLowerCase().replace(/\b:?(?:0+:?){2,}/, "::"); - } - - let result = s; - - if (!isSpecial) { - // Check against forbidden host code points except for "%". - for (const c of result) { - if (charIsForbiddenInHost(c) && c != "\u0025") { - throw new TypeError("Invalid hostname."); - } - } - - // Percent-encode C0 control set. - result = [...result] - .map((c) => (charInC0ControlSet(c) ? encodeChar(c) : c)) - .join(""); - - return result; - } - - // Percent-decode. - if (result.match(/%(?![0-9A-Fa-f]{2})/) != null) { - throw new TypeError("Invalid hostname."); - } - result = result.replace( - /%(.{2})/g, - (_, hex) => String.fromCodePoint(Number(`0x${hex}`)), - ); - - // IDNA domain to ASCII. - result = domainToAscii(result); - - // Check against forbidden host code points. - for (const c of result) { - if (charIsForbiddenInHost(c)) { - throw new TypeError("Invalid hostname."); - } - } - - // IPv4 parsing. - if (isSpecial) { - result = parseIpv4(result); - } - - return result; -} - -function encodePathname(s: string): string { - return [...s].map((c) => (charInPathSet(c) ? encodeChar(c) : c)).join(""); -} - -function encodeSearch(s: string): string { - return [...s].map((c) => (charInSearchSet(c) ? encodeChar(c) : c)).join(""); -} - -function encodeHash(s: string): string { - return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join(""); -} |