From 14d9aa9b970b3cad7297f82e3a2816e932abc01e Mon Sep 17 00:00:00 2001 From: Ignacio Aldama Vicente Date: Thu, 25 Jan 2024 12:26:49 +0100 Subject: [PATCH] feat: add url component for URL specific functions --- .bitmap | 14 +++ network/agent/agent.spec.ts | 25 +++++- network/agent/agent.ts | 16 +++- network/url/index.ts | 2 + network/url/url.docs.mdx | 10 +++ network/url/url.spec.ts | 173 ++++++++++++++++++++++++++++++++++++ network/url/url.ts | 71 +++++++++++++++ 7 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 network/url/index.ts create mode 100644 network/url/url.docs.mdx create mode 100644 network/url/url.spec.ts create mode 100644 network/url/url.ts diff --git a/.bitmap b/.bitmap index bc81831..5f71f05 100644 --- a/.bitmap +++ b/.bitmap @@ -161,5 +161,19 @@ } } }, + "url": { + "name": "url", + "scope": "", + "version": "", + "defaultScope": "pnpm.network", + "mainFile": "index.ts", + "rootDir": "network/url", + "config": { + "pnpm.env/envs/pnpm-env": {}, + "teambit.envs/envs": { + "env": "pnpm.env/envs/pnpm-env" + } + } + }, "$schema-version": "17.0.0" } \ No newline at end of file diff --git a/network/agent/agent.spec.ts b/network/agent/agent.spec.ts index 4ab2e8a..f26f6ee 100644 --- a/network/agent/agent.spec.ts +++ b/network/agent/agent.spec.ts @@ -184,7 +184,30 @@ test('select correct client certificates when host has a port', () => { test('select correct client certificates when host has a path', () => { const agent = getAgent('https://foo.com/bar/baz', { clientCertificates: { - '//foo.com/bar/': { + '//foo.com/': { + ca: 'ca', + cert: 'cert', + key: 'key', + }, + }, + }) + + expect(agent).toEqual({ + ca: 'ca', + cert: 'cert', + key: 'key', + localAddress: undefined, + maxSockets: 50, + rejectUnauthorized: undefined, + timeout: 0, + __type: 'https', + }) +}) + +test('select correct client certificates when host has a path and the cert contains a path', () => { + const agent = getAgent('https://foo.com/bar/baz', { + clientCertificates: { + '//foo.com/bar': { ca: 'ca', cert: 'cert', key: 'key', diff --git a/network/agent/agent.ts b/network/agent/agent.ts index f1d60a0..4d3996b 100644 --- a/network/agent/agent.ts +++ b/network/agent/agent.ts @@ -1,8 +1,8 @@ import { URL } from 'url' import HttpAgent from 'agentkeepalive' import LRU from 'lru-cache' -import nerfDart from 'nerf-dart' import { getProxyAgent, ProxyAgentOptions } from '@pnpm/network.proxy-agent' +import { parseUri } from '@pnpm/network.url'; const HttpsAgent = HttpAgent.HttpsAgent @@ -22,14 +22,24 @@ export function getAgent (uri: string, opts: AgentOptions) { return getNonProxyAgent(uri, opts) } +function getClientCertificates(uri: string, opts: AgentOptions) { + const { host, hostOnlyDomain, pathname } = parseUri(uri) + + if (host.endsWith(pathname)) { + return opts.clientCertificates?.[host]; + } + + const fullPath = `${host}${pathname !== '/' ? pathname : ''}`; + return opts.clientCertificates?.[fullPath] ?? opts.clientCertificates?.[host] ?? opts.clientCertificates?.[hostOnlyDomain]; +} + function getNonProxyAgent (uri: string, opts: AgentOptions) { const parsedUri = new URL(uri) - const host = nerfDart(uri) const isHttps = parsedUri.protocol === 'https:' const { ca, cert, key: certKey } = { ...opts, - ...opts.clientCertificates?.[host], + ...getClientCertificates(uri, opts), } /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ diff --git a/network/url/index.ts b/network/url/index.ts new file mode 100644 index 0000000..a8ea9e0 --- /dev/null +++ b/network/url/index.ts @@ -0,0 +1,2 @@ +export { parseUri } from './url'; +export type { ParsedUri } from './url'; diff --git a/network/url/url.docs.mdx b/network/url/url.docs.mdx new file mode 100644 index 0000000..cc25a35 --- /dev/null +++ b/network/url/url.docs.mdx @@ -0,0 +1,10 @@ +--- +labels: ['Url', 'module'] +description: 'A Url module.' +--- + +A url module. + +```ts +url(); +``` diff --git a/network/url/url.spec.ts b/network/url/url.spec.ts new file mode 100644 index 0000000..5123fcb --- /dev/null +++ b/network/url/url.spec.ts @@ -0,0 +1,173 @@ +import { parseUri } from './url'; + +describe('parseUri', () => { + it('should parse a simple URL', () => { + const uri = 'https://example.com'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com/', + host: 'example.com', + hostOnlyDomain: '//example.com/', + port: '', + pathname: '/', + search: '', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with a port', () => { + const uri = 'https://example.com:8080'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com:8080/', + hostOnlyDomain: '//example.com:8080/', + host: 'example.com', + port: '8080', + pathname: '/', + search: '', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with a search', () => { + const uri = 'https://example.com?foo=bar'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com/', + host: 'example.com', + hostOnlyDomain: '//example.com/', + port: '', + pathname: '/', + search: '?foo=bar', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with a hash', () => { + const uri = 'https://example.com#foo'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com/', + host: 'example.com', + hostOnlyDomain: '//example.com/', + port: '', + pathname: '/', + search: '', + hash: '#foo', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with a path', () => { + const uri = 'https://example.com/path/to/file'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com/path/to/', + host: 'example.com', + hostOnlyDomain: '//example.com/', + port: '', + pathname: '/path/to/file', + search: '', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with a query string', () => { + const uri = 'https://example.com?foo=bar&baz=qux'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com/', + host: 'example.com', + hostOnlyDomain: '//example.com/', + port: '', + pathname: '/', + search: '?foo=bar&baz=qux', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with a fragment identifier', () => { + const uri = 'https://example.com#foo'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com/', + host: 'example.com', + hostOnlyDomain: '//example.com/', + port: '', + pathname: '/', + search: '', + hash: '#foo', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with a username and password', () => { + const uri = 'https://username:password@example.com'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com/', + host: 'example.com', + hostOnlyDomain: '//example.com/', + port: '', + pathname: '/', + search: '', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL that its an IP with port', () => { + const uri = 'https://192.168.1.1:8080'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//192.168.1.1:8080/', + hostOnlyDomain: '//192.168.1.1:8080/', + host: '192.168.1.1', + port: '8080', + pathname: '/', + search: '', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); + + it('should parse a URL with port and subpaths ', () => { + const uri = 'https://example.com:8080/path/to/file'; + const expected = { + raw: uri, + protocol: 'https:', + nerf: '//example.com:8080/path/to/', + host: 'example.com', + hostOnlyDomain: '//example.com:8080/', + port: '8080', + pathname: '/path/to/file', + search: '', + hash: '', + }; + const actual = parseUri(uri); + expect(actual).toEqual(expected); + }); +}); diff --git a/network/url/url.ts b/network/url/url.ts new file mode 100644 index 0000000..94e2b2c --- /dev/null +++ b/network/url/url.ts @@ -0,0 +1,71 @@ +import nerfDart from 'nerf-dart'; + +export interface ParsedUri { + /** + * The url as string + */ + raw: string; + /** + * The protocol of the url + */ + protocol: string; + /** + * The nerf dart of the url + * @example https://example.com -> //example.com/ + * @example https://example.com:8080/path/to/file -> //example.com:8080/path/to/ + */ + nerf: string; + /** + * The host of the url + * @example https://example.com -> example.com + */ + host: string; + /** + * The host of the url with port + * @example https://example.com:8080 -> //example.com:8080/ + */ + hostOnlyDomain: string; + /** + * The port of the url + * @example https://example.com:8080 -> 8080 + */ + port: string; + /** + * The pathname of the url + * @example https://example.com/path/to/file -> /path/to/file + */ + pathname: string; + /** + * The search of the url + * @example https://example.com/path/to/file?search=query -> ?search=query + */ + search: string; + /** + * The hash of the url + * @example https://example.com/path/to/file#hash -> #hash + */ + hash: string; +} + +export function parseUri(uri: string): ParsedUri { + const parsed = new URL(uri); + return { + raw: uri, + protocol: parsed.protocol, + nerf: nerfDart(uri), + host: parsed.hostname, + hostOnlyDomain: convertToDomain(parsed), + port: parsed.port, + pathname: parsed.pathname, + search: parsed.search, + hash: parsed.hash, + }; +} + +function convertToDomain(url: URL): string { + let result = `//${url.hostname}`; + if (url.port) { + result += `:${url.port}`; + } + return `${result}/`; +}