├── .travis.yml ├── lib ├── h16.js ├── query.js ├── host-header.js ├── ipv4address.js ├── ls32.js ├── absolute-path.js ├── subdomain.js ├── port.js ├── authority.js ├── request-target.js ├── segment.js ├── host.js └── ipv6address.js ├── test ├── 14.idna.js ├── util.js ├── 12.authority-form.js ├── 13.asterisk-form.js ├── 11.absolute-form.js └── 10.origin-form.js ├── benchmark ├── index.js └── fake.js ├── package.json ├── LICENSE ├── .gitignore ├── index.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '9' 5 | - '10' 6 | - 'node' 7 | -------------------------------------------------------------------------------- /lib/h16.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://tools.ietf.org/html/rfc3986#appendix-A 3 | module.exports = '(?:[0-9a-f]{1,4})'; 4 | -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://tools.ietf.org/html/rfc3986#section-3.4 3 | module.exports = '(?:(\\?(?:[a-z0-9._~!$&\'()*+,;=:@/?-]|%[0-9a-f]{2})+)|\\?)'; 4 | -------------------------------------------------------------------------------- /lib/host-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const authority = require('./authority'); 3 | // https://tools.ietf.org/html/rfc7230#section-5.4 4 | module.exports = new RegExp(`^(?:${authority})?\\s*$`, 'i'); 5 | -------------------------------------------------------------------------------- /lib/ipv4address.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://tools.ietf.org/html/rfc3986#appendix-A 3 | const decOctet = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])'; 4 | module.exports = `(?:(?:${decOctet}\\.){3}${decOctet})`; 5 | -------------------------------------------------------------------------------- /lib/ls32.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const h16 = require('./h16'); 3 | const ipv4address = require('./ipv4address'); 4 | // https://tools.ietf.org/html/rfc3986#appendix-A 5 | module.exports = `(?:${h16}:${h16}|${ipv4address})`; 6 | -------------------------------------------------------------------------------- /lib/absolute-path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const segment = require('./segment'); 3 | // https://tools.ietf.org/html/rfc7230#section-2.7 4 | // Also see the comments in lib/segment.js 5 | module.exports = `(?:(?:/${segment})+/?|/)`; 6 | -------------------------------------------------------------------------------- /lib/subdomain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://tools.ietf.org/html/rfc1034#section-3.5 3 | // https://tools.ietf.org/html/rfc1123#section-2.1 4 | const label = '(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)'; 5 | const topLevelLabel = `(?:(?![0-9-]+(?:[^a-z0-9-]|$))${label})`; 6 | module.exports = `(?:(?:${label}\\.)*${topLevelLabel}\\.?)`; 7 | -------------------------------------------------------------------------------- /lib/port.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://tools.ietf.org/html/rfc3986#section-3.2.3 3 | // Although the spec strangely allows any number of digits, we restrict it to 4 | // five, since the largest valid port number is 65535. If this were to be 5 | // adopted into core, it should probably follow the spec instead. 6 | module.exports = '(?:[0-9]{1,5})'; 7 | -------------------------------------------------------------------------------- /lib/authority.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const host = require('./host'); 3 | const port = require('./port'); 4 | // https://tools.ietf.org/html/rfc3986#section-3.2 5 | // https://tools.ietf.org/html/rfc3986#section-3.2.1 6 | // https://tools.ietf.org/html/rfc3986#section-7.5 7 | // https://tools.ietf.org/html/rfc7230#appendix-A.2 8 | module.exports = `(?:(${host})(?::(${port})?)?)`; 9 | -------------------------------------------------------------------------------- /test/14.idna.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { req } = require('./util'); 4 | const parse = require('../.'); 5 | 6 | // Fake punycode labels are not valid IDNA labels. 7 | // https://tools.ietf.org/html/rfc5890#section-2.3.1 8 | 9 | describe('IDNA', function () { 10 | it('should reject fake punycode in origin-form'); 11 | it('should reject fake punycode in absolute-form'); 12 | }); 13 | -------------------------------------------------------------------------------- /lib/request-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const query = require('./query'); 3 | const authority = require('./authority'); 4 | const absolutePath = require('./absolute-path'); 5 | // https://tools.ietf.org/html/rfc7230#section-5.3 6 | // A more robust implementation could allow non-http schemes, although the spec 7 | // is unclear about what such a thing would imply. 8 | module.exports = new RegExp(`^(?:(https?:)//${authority}(?!\\*)|(?=[/*]))(${absolutePath}|\\*$)?${query}?$`, 'i'); 9 | -------------------------------------------------------------------------------- /lib/segment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://tools.ietf.org/html/rfc3986#appendix-A 3 | // We are slightly more strict than the spec, forcing segments to be non-empty. 4 | // The reasoning is that double slashes in the path ("//") much more often 5 | // indicate an improperly-concatenated url, rather than an intentional resource 6 | // path. If this were to be adopted into core, empty segments should be allowed. 7 | module.exports = '(?:(?:[a-z0-9._~!$&\'()*+,;=:@-]|%[0-9a-f]{2})+)'; 8 | -------------------------------------------------------------------------------- /lib/host.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ipv4address = require('./ipv4address'); 3 | const ipv6address = require('./ipv6address'); 4 | const subdomain = require('./subdomain'); 5 | // https://tools.ietf.org/html/rfc3986#section-3.2.2 6 | // This only supports DNS names, even though the actual spec supports arbitrary 7 | // name registries. If this were to be adopted into core, it should probably 8 | // not have such a restriction. 9 | const ipliteral = `(?:\\[${ipv6address}\\])`; 10 | module.exports = `(?:${ipliteral}|${ipv4address}|${subdomain})`; 11 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { TLSSocket } = require('tls'); 3 | 4 | class FakeRequest { 5 | constructor(url) { 6 | this.url = url; 7 | this.method = 'GET'; 8 | this.headers = {}; 9 | this.socket = {}; 10 | } 11 | options() { 12 | this.method = 'OPTIONS'; 13 | return this; 14 | } 15 | host(str) { 16 | this.headers.host = str; 17 | return this; 18 | } 19 | secure() { 20 | this.socket = Object.create(TLSSocket.prototype); 21 | return this; 22 | } 23 | } 24 | 25 | exports.req = (...args) => new FakeRequest(...args); 26 | -------------------------------------------------------------------------------- /lib/ipv6address.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const h16 = require('./h16'); 3 | const ls32 = require('./ls32'); 4 | // https://tools.ietf.org/html/rfc3986#appendix-A 5 | const forms = [ 6 | `(?:${h16}:){6}${ls32}`, 7 | `::(?:${h16}:){5}${ls32}`, 8 | `${h16}?::(?:${h16}:){4}${ls32}`, 9 | `(?:(?:${h16}:)?${h16})?::(?:${h16}:){3}${ls32}`, 10 | `(?:(?:${h16}:){0,2}${h16})?::(?:${h16}:){2}${ls32}`, 11 | `(?:(?:${h16}:){0,3}${h16})?::${h16}:${ls32}`, 12 | `(?:(?:${h16}:){0,4}${h16})?::${ls32}`, 13 | `(?:(?:${h16}:){0,5}${h16})?::${h16}`, 14 | `(?:(?:${h16}:){0,6}${h16})?::`, 15 | ]; 16 | module.exports = `(?:${forms.join('|')})`; 17 | -------------------------------------------------------------------------------- /test/12.authority-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { req } = require('./util'); 4 | const parse = require('../.'); 5 | 6 | describe('authority-form', function () { 7 | it('should reject authority-form request-targets', function () { 8 | expect(parse(req('some.host'))) 9 | .to.equal(null); 10 | expect(parse(req('some.host:80'))) 11 | .to.equal(null); 12 | expect(parse(req('some.host:443'))) 13 | .to.equal(null); 14 | expect(parse(req('user@some.host:443'))) 15 | .to.equal(null); 16 | expect(parse(req('user:password@some.host:443'))) 17 | .to.equal(null); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const nodemark = require('nodemark'); 3 | const fake = require('./fake'); 4 | 5 | const legacy = require('url').parse; 6 | const whatwg = require('url').URL; 7 | const parseRequest = require('../.'); 8 | 9 | let request; 10 | const setup = () => request = ({ 11 | url: fake.choose(0.8) ? fake.path() : fake.url(), 12 | headers: { host: fake.host() }, 13 | method: fake.method(), 14 | }); 15 | 16 | const benchmark = (name, fn) => console.log(`${name} x ${nodemark(fn, setup)}`); 17 | 18 | benchmark('legacy url.parse()', () => legacy(request.url)); 19 | benchmark('whatwg new URL()', () => new whatwg(request.url, 'http://my.implied.origin')); 20 | benchmark('request-target', () => parseRequest(request)); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "request-target", 3 | "version": "1.0.2", 4 | "description": "A url parser for http requests, compliant with RFC 7230", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "nodemark": "^0.3.0", 8 | "chai": "^4.1.2", 9 | "mocha": "^5.0.0" 10 | }, 11 | "scripts": { 12 | "test": "$(npm bin)/mocha --exit", 13 | "benchmark": "node --expose-gc benchmark/" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/JoshuaWise/request-target.git" 18 | }, 19 | "keywords": [ 20 | "url", 21 | "uri", 22 | "http", 23 | "https", 24 | "parser", 25 | "idna", 26 | "dns" 27 | ], 28 | "author": "Joshua Wise ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/JoshuaWise/request-target/issues" 32 | }, 33 | "homepage": "https://github.com/JoshuaWise/request-target#readme" 34 | } 35 | -------------------------------------------------------------------------------- /test/13.asterisk-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { req } = require('./util'); 4 | const parse = require('../.'); 5 | 6 | const expected = { 7 | protocol: 'http:', 8 | hostname: 'some.host', 9 | port: '80', 10 | pathname: '*', 11 | search: '', 12 | }; 13 | 14 | describe('asterisk-form', function () { 15 | it('should parse valid asterisk-form request-targets', function () { 16 | // Note: the spec does not clearly state what to do when asterisk-form 17 | // is used with a method other than OPTIONS. It simply says it is "only 18 | // used for a server-wide OPTIONS request". 19 | // https://tools.ietf.org/html/rfc7230#section-5.3.4 20 | expect(parse(req('*').host('some.host'))) 21 | .to.deep.equal(expected); 22 | expect(parse(req('*').host('some.host').options())) 23 | .to.deep.equal(expected); 24 | }); 25 | it('should reject when there is a search component', function () { 26 | expect(parse(req('*?').host('some.host'))) 27 | .to.equal(null); 28 | expect(parse(req('*?foo=1&bar=2').host('some.host'))) 29 | .to.equal(null); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joshua Wise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { TLSSocket } = require('tls'); 3 | const requestTarget = require('./lib/request-target'); 4 | const hostHeader = require('./lib/host-header'); 5 | 6 | // https://tools.ietf.org/html/rfc7230#section-5.5 7 | module.exports = (req) => { 8 | const target = req.url.match(requestTarget); 9 | if (!target) return null; // Invalid url 10 | 11 | let [,scheme = null, host = null, port = null, path, query = ''] = target; 12 | 13 | if (scheme) { 14 | scheme = scheme.toLowerCase(); 15 | if (!path) path = req.method === 'OPTIONS' ? '*' : '/'; 16 | } else if (req.headers.host) { // Header is optional, to support HTTP/1.0 17 | const hostport = req.headers.host.match(hostHeader); 18 | if (!hostport) return null; // Invalid Host header 19 | if (hostport[1]) { 20 | [,host, port = null] = hostport; 21 | scheme = req.socket instanceof TLSSocket ? 'https:' : 'http:'; 22 | } 23 | } 24 | 25 | if (host) { 26 | // DNS names have a maximum length of 255, including the root domain. 27 | if (host.length > (host.charCodeAt(host.length - 1) === 46 ? 255 : 254)) return null; 28 | host = host.toLowerCase(); 29 | if (port) { 30 | const num = +port; 31 | if (num > 65535) return null; // Invalid port number 32 | if (port.charCodeAt(0) === 48) port = '' + num; // Remove leading zeros 33 | } else { 34 | port = scheme.length === 6 ? '443' : '80'; 35 | } 36 | } 37 | 38 | // Rename fields to make it familiar with standard APIs. 39 | return { protocol: scheme, hostname: host, port, pathname: path, search: query }; 40 | }; 41 | -------------------------------------------------------------------------------- /benchmark/fake.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD']; 3 | const seps = ['.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@', '-']; 4 | 5 | const gen = (fn, n) => { let r = ''; while (n--) r += fn(); return r; }; 6 | const rand = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a; 7 | const choose = (c = 0.5) => Math.random() < c; 8 | 9 | const letter = () => String.fromCharCode(rand(65, 90) + (choose(0.9) ? 32 : 0)); 10 | const digit = () => String.fromCharCode(rand(48, 57)); 11 | const ld = () => choose(0.95) ? letter() : digit(); 12 | const subdomain = () => gen(ld, rand(2, 16)) + '.'; 13 | const scheme = () => choose() ? 'http' : 'https'; 14 | const host = () => gen(subdomain, rand(1, 3)) + gen(letter, rand(2, 4)); 15 | const port = () => gen(digit, rand(1, 4)); 16 | const authority = () => choose() ? host() : `${host()}:${port()}`; 17 | const sep = () => seps[rand(0, seps.length - 1)]; 18 | const pctEncoded = () => '%' + rand(0, 15).toString(16) + rand(0, 15).toString(16); 19 | const pchar = () => choose(0.9) ? ld() : choose() ? sep() : pctEncoded(); 20 | const segment = () => '/' + gen(pchar, rand(1, 32)); 21 | const path = () => choose(0.1) ? '/' : gen(segment, rand(1, 6)); 22 | const query = () => choose(0.2) ? '' : '?' + gen(pchar, rand(2, 64)); 23 | 24 | // Normally, v8 uses fancy tricks to avoid concatenation. This forces v8 to 25 | // store the string as a single buffer, as it would be coming over the wire. 26 | const serialize = str => Buffer.from(str).toString(); 27 | 28 | exports.choose = choose; 29 | exports.path = () => serialize(path() + query()); 30 | exports.host = () => serialize(authority()); 31 | exports.url = () => serialize(scheme() + '://' + authority() + (choose(0.1) ? '' : path()) + query()); 32 | exports.method = () => methods[rand(0, methods.length - 1)]; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # request-target [![Build Status](https://travis-ci.org/JoshuaWise/request-target.svg?branch=master)](https://travis-ci.org/JoshuaWise/request-target) 2 | 3 | ## Another URL parser? 4 | 5 | The core [`url`](https://nodejs.org/api/url.html) module is great for parsing generic URLs. Unfortunately, the URL of an HTTP request (formally called the [`request-target`](https://tools.ietf.org/html/rfc7230#section-5.3)), is *not* just a generic URL. It's a URL that must obey the requirements of the [URL RFC 3986](https://tools.ietf.org/html/rfc3986) *as well* as the [HTTP RFC 7230](https://tools.ietf.org/html/rfc7230). 6 | 7 | ## The problems 8 | 9 | The core [`http`](https://nodejs.org/api/http.html) module does not validate or sanitize `req.url`. 10 | 11 | The legacy [`url.parse()`](https://nodejs.org/api/url.html#url_legacy_url_api) function also allows illegal characters to appear. 12 | 13 | The newer [`url.URL()`](https://nodejs.org/api/url.html#url_class_url) constructor will attempt to convert the input into a properly encoded URL with only legal characters. This is better for the general case, however, the official [http spec](https://tools.ietf.org/html/rfc7230#section-3.1.1) states: 14 | > A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain. 15 | 16 | This means a malformed URL should be treated as a violation of the http protocol. It's not something that should be accepted or autocorrected, and it's not something that higher-level code should ever have to worry about. 17 | 18 | ## Adoption into core 19 | 20 | Because of backwards compatibility, it's unlikely that the logic expressed in `request-target` will be incorporated into the core [`http`](https://nodejs.org/api/http.html) module. My recommendation is to incorporate it as an alternative function in the core [`url`](https://nodejs.org/api/url.html) module. If that never happens, just make sure you're using this package when parsing `req.url`. 21 | 22 | ## How to use 23 | 24 | The function takes a *request object* as input (not a URL string) because the http spec requires inspection of `req.method` and `req.headers.host` in order to properly interpret the URL of a request. If the function returns `null`, the request should not be processed further—either destroy the connection or respond with `Bad Request`. 25 | 26 | If the request is valid, it will return an object with five properties: `protocol`, `hostname`, `port`, `pathname`, and `search`. The first three properties are either non-empty strings or `null`, and are mutually dependant. The `pathname` property is always a non-empty string, and the `search` property is always a possibly empty string. 27 | 28 | If the first three properties are not `null`, it means the request was in [`absolute-form`](https://tools.ietf.org/html/rfc7230#section-5.3.2) or a valid non-empty [Host header](https://tools.ietf.org/html/rfc7230#section-5.4) was provided. 29 | 30 | ```js 31 | const result = parse(req); 32 | if (result) { 33 | // { protocol, hostname, port, pathname, search } 34 | } else { 35 | res.writeHead(400); 36 | res.end(); 37 | } 38 | ``` 39 | 40 | ## Unexpected benefits 41 | 42 | The goal of `request-target` was not to create a fast parser, but it turns out this implementation can be between 1.5–9x faster than the general-purpose parsers in core. 43 | 44 | ``` 45 | $ npm run benchmark 46 | legacy url.parse() x 371,681 ops/sec ±0.88% (297996 samples) 47 | whatwg new URL() x 58,766 ops/sec ±0.3% (118234 samples) 48 | request-target x 552,748 ops/sec ±0.54% (344809 samples) 49 | ``` 50 | 51 | > Run the benchmark yourself with `npm run benchmark`. 52 | -------------------------------------------------------------------------------- /test/11.absolute-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { req } = require('./util'); 4 | const parse = require('../.'); 5 | 6 | const expected = { 7 | protocol: 'http:', 8 | hostname: 'some.host', 9 | port: '80', 10 | pathname: '/path/to/resource', 11 | search: '?foo=1&bar=2', 12 | }; 13 | 14 | describe('absolute-form', function () { 15 | it('should parse valid absolute-form request-targets', function () { 16 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host'))) 17 | .to.deep.equal(expected); 18 | }); 19 | it('should use a default scheme', function () { 20 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host').secure())) 21 | .to.deep.equal({ ...expected, protocol: 'https:', port: '443' }); 22 | }); 23 | it('should respect the port', function () { 24 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host:1024'))) 25 | .to.deep.equal({ ...expected, port: '1024' }); 26 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host:9999').secure())) 27 | .to.deep.equal({ ...expected, protocol: 'https:', port: '9999' }); 28 | }); 29 | it('should allow a missing host header', function () { 30 | expect(parse(req('/path/to/resource?foo=1&bar=2'))) 31 | .to.deep.equal({ ...expected, protocol: null, hostname: null, port: null }); 32 | expect(parse(req('/path/to/resource?foo=1&bar=2').host(''))) 33 | .to.deep.equal({ ...expected, protocol: null, hostname: null, port: null }); 34 | }); 35 | it('should lowercase the host', function () { 36 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('sOme.hoSt'))) 37 | .to.deep.equal(expected); 38 | }); 39 | it('should trim whitespace from host', function () { 40 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host \t '))) 41 | .to.deep.equal({ ...expected, hostname: 'some.host' }); 42 | }); 43 | it('should accept the root domain in host', function () { 44 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host.'))) 45 | .to.deep.equal({ ...expected, hostname: 'some.host.' }); 46 | }); 47 | it('should accept subdomains starting with numbers', function () { 48 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('012.345.g0'))) 49 | .to.deep.equal({ ...expected, hostname: '012.345.g0' }); 50 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('012.345.0g'))) 51 | .to.deep.equal({ ...expected, hostname: '012.345.0g' }); 52 | }); 53 | it('should accept IPv4 addresses', function () { 54 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('111.22.0.255'))) 55 | .to.deep.equal({ ...expected, hostname: '111.22.0.255' }); 56 | }); 57 | it('should accept IPv6 addresses', function () { 58 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('[::]'))) 59 | .to.deep.equal({ ...expected, hostname: '[::]' }); 60 | }); 61 | it('should reject invalid hosts', function () { 62 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('http://some.host'))) 63 | .to.equal(null); 64 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host..'))) 65 | .to.equal(null); 66 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some..host'))) 67 | .to.equal(null); 68 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.999'))) 69 | .to.equal(null); 70 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.1-2'))) 71 | .to.equal(null); 72 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('111.22.0.256'))) 73 | .to.equal(null); 74 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('111.22.0'))) 75 | .to.equal(null); 76 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('012.345.0'))) 77 | .to.equal(null); 78 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('[12345::]'))) 79 | .to.equal(null); 80 | }); 81 | it('should reject invalid port numbers', function () { 82 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host:-1'))) 83 | .to.equal(null); 84 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host:65536'))) 85 | .to.equal(null); 86 | expect(parse(req('/path/to/resource?foo=1&bar=2').host('some.host:000080'))) 87 | .to.equal(null); 88 | }); 89 | it('should reject invalid paths', function () { 90 | expect(parse(req('/path/to//resource?foo=1&bar=2').host('some.host'))) 91 | .to.equal(null); 92 | expect(parse(req('/path/to/re{source?foo=1&bar=2').host('some.host'))) 93 | .to.equal(null); 94 | expect(parse(req('?foo=1&bar=2').host('some.host'))) 95 | .to.equal(null); 96 | expect(parse(req('').host('some.host'))) 97 | .to.equal(null); 98 | }); 99 | it('should reject invalid searches', function () { 100 | expect(parse(req('/path/to/resource?foo=1&bar=2#baz').host('some.host'))) 101 | .to.equal(null); 102 | expect(parse(req('/path/to/re{source?foo=1&bar=2{').host('some.host'))) 103 | .to.equal(null); 104 | }); 105 | it('should reject hosts that are too long', function () { 106 | expect(parse(req(`/path/to/resource?foo=1&bar=2`).host(`some${'.host'.repeat(50)}`))) 107 | .to.deep.equal({ ...expected, hostname: `some${'.host'.repeat(50)}` }); 108 | expect(parse(req(`/path/to/resource?foo=1&bar=2`).host(`some${'.host'.repeat(50)}.`))) 109 | .to.deep.equal({ ...expected, hostname: `some${'.host'.repeat(50)}.` }); 110 | expect(parse(req(`/path/to/resource?foo=1&bar=2`).host(`some${'.host'.repeat(50)}x`))) 111 | .to.equal(null); 112 | expect(parse(req(`/path/to/resource?foo=1&bar=2`).host(`some${'.host'.repeat(50)}x.`))) 113 | .to.equal(null); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/10.origin-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { req } = require('./util'); 4 | const parse = require('../.'); 5 | 6 | const expected = { 7 | protocol: 'http:', 8 | hostname: 'some.host', 9 | port: '80', 10 | pathname: '/path/to/resource', 11 | search: '?foo=1&bar=2', 12 | }; 13 | 14 | describe('origin-form', function () { 15 | it('should parse valid origin-form request-targets', function () { 16 | expect(parse(req('http://some.host/path/to/resource?foo=1&bar=2'))) 17 | .to.deep.equal(expected); 18 | }); 19 | it('should respect the scheme', function () { 20 | expect(parse(req('http://some.host/path/to/resource?foo=1&bar=2').secure())) 21 | .to.deep.equal(expected); 22 | expect(parse(req('https://some.host/path/to/resource?foo=1&bar=2'))) 23 | .to.deep.equal({ ...expected, protocol: 'https:', port: '443' }); 24 | }); 25 | it('should respect the port', function () { 26 | expect(parse(req('http://some.host:1024/path/to/resource?foo=1&bar=2'))) 27 | .to.deep.equal({ ...expected, port: '1024' }); 28 | expect(parse(req('https://some.host:9999/path/to/resource?foo=1&bar=2'))) 29 | .to.deep.equal({ ...expected, protocol: 'https:', port: '9999' }); 30 | }); 31 | it('should ignore the host header', function () { 32 | expect(parse(req('http://some.host/path/to/resource?foo=1&bar=2').host('other.host'))) 33 | .to.deep.equal(expected); 34 | expect(parse(req('http://some.host/path/to/resource?foo=1&bar=2').host('some.host:9999'))) 35 | .to.deep.equal(expected); 36 | expect(parse(req('http://some.host/path/to/resource?foo=1&bar=2').host('!....invalid....::-2'))) 37 | .to.deep.equal(expected); 38 | }); 39 | it('should use a default path if not provided', function () { 40 | expect(parse(req('http://some.host?foo=1&bar=2'))) 41 | .to.deep.equal({ ...expected, pathname: '/' }); 42 | expect(parse(req('http://some.host?foo=1&bar=2').options())) 43 | .to.deep.equal({ ...expected, pathname: '*' }); 44 | }); 45 | it('should lowercase the scheme and host', function () { 46 | expect(parse(req('HtTp://some.HOST/path/to/resource?foo=1&bar=2'))) 47 | .to.deep.equal(expected); 48 | }); 49 | it('should accept the root domain in host', function () { 50 | expect(parse(req('http://some.host./path/to/resource?foo=1&bar=2'))) 51 | .to.deep.equal({ ...expected, hostname: 'some.host.' }); 52 | }); 53 | it('should accept subdomains starting with numbers', function () { 54 | expect(parse(req('http://012.345.g0/path/to/resource?foo=1&bar=2'))) 55 | .to.deep.equal({ ...expected, hostname: '012.345.g0' }); 56 | expect(parse(req('http://012.345.0g/path/to/resource?foo=1&bar=2'))) 57 | .to.deep.equal({ ...expected, hostname: '012.345.0g' }); 58 | }); 59 | it('should accept IPv4 addresses', function () { 60 | expect(parse(req('http://111.22.0.255/path/to/resource?foo=1&bar=2'))) 61 | .to.deep.equal({ ...expected, hostname: '111.22.0.255' }); 62 | }); 63 | it('should accept IPv6 addresses', function () { 64 | expect(parse(req('http://[::]/path/to/resource?foo=1&bar=2'))) 65 | .to.deep.equal({ ...expected, hostname: '[::]' }); 66 | }); 67 | it('should reject invalid schemes', function () { 68 | expect(parse(req('httpss://some.host/path/to/resource?foo=1&bar=2'))) 69 | .to.equal(null); 70 | expect(parse(req('ws://some.host/path/to/resource?foo=1&bar=2'))) 71 | .to.equal(null); 72 | expect(parse(req('://some.host/path/to/resource?foo=1&bar=2'))) 73 | .to.equal(null); 74 | expect(parse(req('//some.host/path/to/resource?foo=1&bar=2'))) 75 | .to.equal(null); 76 | expect(parse(req('some.host/path/to/resource?foo=1&bar=2'))) 77 | .to.equal(null); 78 | }); 79 | it('should reject invalid hosts', function () { 80 | expect(parse(req('http://some.host../path/to/resource?foo=1&bar=2'))) 81 | .to.equal(null); 82 | expect(parse(req('http://some..host/path/to/resource?foo=1&bar=2'))) 83 | .to.equal(null); 84 | expect(parse(req('http://some.999/path/to/resource?foo=1&bar=2'))) 85 | .to.equal(null); 86 | expect(parse(req('http://some.1-2/path/to/resource?foo=1&bar=2'))) 87 | .to.equal(null); 88 | expect(parse(req('http://111.22.0.256/path/to/resource?foo=1&bar=2'))) 89 | .to.equal(null); 90 | expect(parse(req('http://111.22.0/path/to/resource?foo=1&bar=2'))) 91 | .to.equal(null); 92 | expect(parse(req('http://012.345.0/path/to/resource?foo=1&bar=2'))) 93 | .to.equal(null); 94 | expect(parse(req('http://[12345::]/path/to/resource?foo=1&bar=2'))) 95 | .to.equal(null); 96 | }); 97 | it('should reject invalid port numbers', function () { 98 | expect(parse(req('http://some.host:-1/path/to/resource?foo=1&bar=2'))) 99 | .to.equal(null); 100 | expect(parse(req('http://some.host:65536/path/to/resource?foo=1&bar=2'))) 101 | .to.equal(null); 102 | expect(parse(req('http://some.host:000080/path/to/resource?foo=1&bar=2'))) 103 | .to.equal(null); 104 | }); 105 | it('should reject invalid paths', function () { 106 | expect(parse(req('http://some.host/path/to//resource?foo=1&bar=2'))) 107 | .to.equal(null); 108 | expect(parse(req('http://some.host/path/to/re{source?foo=1&bar=2'))) 109 | .to.equal(null); 110 | expect(parse(req('http://some.host*?foo=1&bar=2'))) 111 | .to.equal(null); 112 | expect(parse(req('http://some.host*'))) 113 | .to.equal(null); 114 | }); 115 | it('should reject invalid searches', function () { 116 | expect(parse(req('http://some.host/path/to/resource?foo=1&bar=2#baz'))) 117 | .to.equal(null); 118 | expect(parse(req('http://some.host/path/to/re{source?foo=1&bar=2{'))) 119 | .to.equal(null); 120 | }); 121 | it('should reject hosts that are too long', function () { 122 | expect(parse(req(`http://some${'.host'.repeat(50)}/path/to/resource?foo=1&bar=2`))) 123 | .to.deep.equal({ ...expected, hostname: `some${'.host'.repeat(50)}` }); 124 | expect(parse(req(`http://some${'.host'.repeat(50)}./path/to/resource?foo=1&bar=2`))) 125 | .to.deep.equal({ ...expected, hostname: `some${'.host'.repeat(50)}.` }); 126 | expect(parse(req(`http://some${'.host'.repeat(50)}x/path/to/resource?foo=1&bar=2`))) 127 | .to.equal(null); 128 | expect(parse(req(`http://some${'.host'.repeat(50)}x./path/to/resource?foo=1&bar=2`))) 129 | .to.equal(null); 130 | }); 131 | }); 132 | --------------------------------------------------------------------------------