tshttpurl
Version:
Http URL and IP address parser and normalizer
201 lines • 7.72 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryParam = exports.HttpURL = void 0;
const IPv6_1 = require("./IPv6");
/**
* HttpURL parses and normalizes http and https URLs.
* * A missing scheme is assumed to be `http`
* * The scheme and host are converted to lowercase
* * If the host is an IP V6 address, then IPv6::toString value is used
* * A missing port is assumed to be `80` or `443`
* * Percent encoded triplets are converted to upper case
* * Unnecessary percent encoded triplets are decoded
* * Path traversals:
* * `.` and `..`are interpreted
* * `//` is collapsed
* * The path is verified to not climb out past the root
* * Query parameters are ordered by name, then value
*/
class HttpURL {
constructor(url) {
this.query = [];
// normalize the percent-ecoded triplets.
url = String(url).replace(HttpURL.pctXX, (match, hexNum, offset, str) => {
hexNum = parseInt(hexNum, 16);
// decode unreserved characters
if ((0x41 <= hexNum && hexNum <= 0x5A) || // [A-Z]
(0x61 <= hexNum && hexNum <= 0x7A) || // [a-z]
(0x30 <= hexNum && hexNum <= 0x39) || // [0-9]
0x2D === hexNum || // hyphen
0x2E === hexNum || // period
0x5F === hexNum || // underscore
0x7E === hexNum) { // tilde
return decodeURIComponent(match);
}
// make all hex uppercase. %5b => %5B
return match.toUpperCase();
});
let parts = HttpURL.regEx.exec(url);
if (null == parts) {
throw new Error('Invalid URL');
}
this.scheme = parts[1] ? parts[1].toLowerCase() : 'http';
if (parts[2]) {
this.ipAddress = new IPv6_1.IPv6(parts[2]);
this.host = this.ipAddress.toString();
}
else if (parts[3]) {
this.ipAddress = new IPv6_1.IPv6(parts[3]);
this.host = this.ipAddress.toString();
}
else {
this.host = parts[4].toLowerCase();
}
if (parts[5]) {
this.port = parseInt(parts[5], 10);
if (this.port < 0 || 65535 < this.port) {
throw new Error('Invalid port');
}
}
else {
this.port = ('http' === this.scheme) ? 80 : 443;
}
// normalize the path (remove ./, ../, //) & check path doesn't reference above parent (/foo/../../..)
let pathStack = [];
let segment;
if (parts[6]) {
this.path = parts[6].split('/');
for (let i = 0; i < this.path.length; i += 1) {
segment = this.path[i];
if (segment && '.' !== segment) {
if ('..' === segment) {
if (undefined === pathStack.pop()) {
throw new Error('Invalid path');
}
;
}
else {
pathStack.push(segment);
}
}
}
this.path = pathStack;
}
else {
this.path = [];
}
Object.freeze(this.path);
this.isDir = 0 === this.path.length ||
'/' === parts[6].charAt(parts[6].length - 1);
if (parts[7]) {
let queries = parts[7].match(HttpURL.qSep);
if (queries)
queries.forEach(q => this.query.push(new QueryParam(q)));
this.query.sort(QueryParam.compare);
}
Object.freeze(this.query);
if (parts[8]) {
this.fragment = parts[8];
}
}
/**
* @returns The normalized http URL.
*/
toString() {
let theHost = this.host;
if (this.ipAddress && !this.ipAddress.isIPv4()) {
theHost = `[${this.host}]`;
}
const str = `${this.scheme}://${theHost}:${this.port}` +
(this.path.length ? '/' + this.path.join('/') : '') +
(this.isDir ? '/' : '') +
(this.query.length ? `?${this.query.join('&')}` : '') +
(this.fragment || '');
this.toString = () => str; // memoize the string representation
return str;
}
/**
* Function used to determine the order of `HttpURL`s. It is suitable
* for use with `Array<T>.sort(fn)`. If an argument is not an `HttpURL`,
* then its toString() value is used to create one.
*
* `HttpURL`s are ordered by thier string representation
*
* @param a The first http URL to compare
* @param b The second http URL to compare
*/
static compare(a, b) {
let ua = (a instanceof HttpURL ? a : new HttpURL(a.toString())).toString();
let ub = (b instanceof HttpURL ? b : new HttpURL(b.toString())).toString();
return ua < ub
? -1
: (ua > ub ? 1 : 0);
}
}
exports.HttpURL = HttpURL;
HttpURL.pctXX = new RegExp('%([\\da-f]{2})', 'gi');
HttpURL.qSep = new RegExp('[^&;]+', 'g');
HttpURL.regEx = new RegExp('^\\s*' + // leading whitespace
'(?:(http|https):\\/\\/)?' + // 1 scheme
'(?:((?:\\d{1,3}\\.){3}\\d{1,3})|' + // 2 ipv4
'(?:\\[([:\\.\\da-f]+)\\])|' + // 3 ipv6
'((?:(?:\\w\\.)|(?:\\w[\\w-]*\\w\\.))*' + // 4 subdomains
// allow leading digits in tld (if usr doesn't type fqdn)
//'(?:[a-z]|(?:[a-z][\\w-]*\\w))))' + // 4 tld
'(?:\\w|(?:\\w[\\w-]*\\w))))' + // 4 tld
'(?::(-?\\d{1,10}))?' + // 5 port
'(?:\\/((?:(?:%[\\da-f]{2})|[-\\w_\\.!~*\'():&=+\\$,/])*))?' + // 6 path
'(?:\\?((?:(?:%[\\da-f]{2})|[-\\w_\\.!~*\'();&=+\\$,/?:@[\\]])*))?' + // 7 query
'(?:#((?:(?:%[\\da-f]{2})|[-\\w_\\.!~*\'();&=+\\$,/?:@[\\]])*))?' + // 8 fragment
'\\s*$', // trailing whitespace
'i');
class QueryParam {
/**
* Creates a QueryParam instance.
* @param name The name of the query parameter. The value may optionally
* be included. E.g. "age=65"
* @param value The value, if not included in the name parameter.
*
* @throws Error if a value is included in both name and value
*/
constructor(name, value) {
let eqIdx = name.indexOf('=');
if (0 <= eqIdx) {
if (value)
throw new Error('Value specified in both name and value parameters.');
value = name.substr(eqIdx + 1);
name = name.substr(0, eqIdx);
}
this.name = name;
this.value = value;
}
;
toString() {
const str = this.value ? `${this.name}=${this.value}` : this.name;
this.toString = () => str; // memoize the string representation
return str;
}
;
/**
* Function used to determine the order of `QueryParam`s. It is suitable
* for use with `Array<T>.sort(fn)`. If an argument is not a `QueryParam`,
* then its toString() value is used to create one.
*
* `QueryParam`s are ordered by thier name, and then their value.
*
* @param a The first query parameter to compare
* @param b The second query parameter to compare
*/
static compare(a, b) {
let qa = a instanceof QueryParam ? a : new QueryParam(a.toString());
let qb = b instanceof QueryParam ? b : new QueryParam(b.toString());
let an = qa.name, av = qa.value || '', bn = qb.name, bv = qb.value || '', comp = an < bn ? -1 : (an > bn ? 1 : 0);
if (0 === comp) {
comp = av < bv ? -1 : (av > bv ? 1 : 0);
}
return comp;
}
;
}
exports.QueryParam = QueryParam;
//# sourceMappingURL=HttpURL.js.map