@ocap/util 1.25.2 → 1.25.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/url.d.ts +13 -2
- package/esm/url.js +93 -31
- package/lib/url.d.ts +13 -2
- package/lib/url.js +119 -32
- package/package.json +3 -2
package/esm/url.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
1
|
+
import * as ipaddr from 'ipaddr.js';
|
|
2
|
+
export declare function parseIp(host: string): ipaddr.IPv4 | ipaddr.IPv6 | null;
|
|
3
|
+
export declare function isUnicastAddress(host: string): boolean;
|
|
4
|
+
export declare function resolveHostAddresses(host: string): Promise<string[]>;
|
|
5
|
+
export interface VerifyUrlOptions {
|
|
6
|
+
allowIp?: boolean;
|
|
7
|
+
maxLength?: number;
|
|
8
|
+
protocols?: string[];
|
|
9
|
+
blockedHosts?: string[];
|
|
10
|
+
blockedSuffixes?: string[];
|
|
11
|
+
checkDns?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function verifyUrl(url: string, options?: VerifyUrlOptions): Promise<boolean>;
|
package/esm/url.js
CHANGED
|
@@ -1,39 +1,101 @@
|
|
|
1
1
|
import { URL } from 'url';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import * as ipaddr from 'ipaddr.js';
|
|
3
|
+
import { Resolver } from 'dns/promises';
|
|
4
|
+
const MAX_URL_LENGTH = 256;
|
|
5
|
+
export function parseIp(host) {
|
|
6
|
+
if (!ipaddr.isValid(host))
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
return ipaddr.parse(host);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
12
14
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function stripTrailingDots(host) {
|
|
16
|
+
return host.replace(/\.+$/, '');
|
|
17
|
+
}
|
|
18
|
+
function isBlockedHostname(host, blockedHosts, blockedSuffixes) {
|
|
19
|
+
const normalized = host.toLowerCase();
|
|
20
|
+
if (blockedHosts.includes(normalized))
|
|
21
|
+
return true;
|
|
22
|
+
if (blockedSuffixes.some((suffix) => normalized.endsWith(suffix)))
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export function isUnicastAddress(host) {
|
|
27
|
+
const parsed = parseIp(host);
|
|
28
|
+
if (!parsed)
|
|
17
29
|
return false;
|
|
30
|
+
const range = parsed.range();
|
|
31
|
+
if (range === 'ipv4Mapped' && parsed.kind() === 'ipv6') {
|
|
32
|
+
const mapped = parsed.toIPv4Address();
|
|
33
|
+
return mapped.range() === 'unicast';
|
|
34
|
+
}
|
|
35
|
+
return range === 'unicast';
|
|
36
|
+
}
|
|
37
|
+
export async function resolveHostAddresses(host) {
|
|
18
38
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const host = u.hostname.toLowerCase();
|
|
27
|
-
// Block IP addresses
|
|
28
|
-
if (isIPv4(host) || host.includes(':'))
|
|
29
|
-
return false; // treat ":" as IPv6
|
|
30
|
-
// Block localhost
|
|
31
|
-
if (host === 'localhost')
|
|
32
|
-
return false;
|
|
39
|
+
const resolver = new Resolver();
|
|
40
|
+
resolver.setServers(['8.8.8.8', '1.1.1.1']);
|
|
41
|
+
const addresses = [];
|
|
42
|
+
// resolve ipv4
|
|
43
|
+
try {
|
|
44
|
+
const ipv4Addresses = await resolver.resolve4(host);
|
|
45
|
+
addresses.push(...ipv4Addresses);
|
|
33
46
|
}
|
|
34
|
-
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.warn(`IPv4 resolution failed for ${host}:`, error?.message);
|
|
49
|
+
}
|
|
50
|
+
// resolve ipv6
|
|
51
|
+
if (!addresses.length) {
|
|
52
|
+
try {
|
|
53
|
+
const ipv6Addresses = await resolver.resolve6(host);
|
|
54
|
+
addresses.push(...ipv6Addresses);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.warn(`IPv6 resolution failed for ${host}:`, error?.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return addresses.filter(Boolean);
|
|
35
61
|
}
|
|
36
|
-
catch {
|
|
37
|
-
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.warn(`DNS resolution failed for host: ${host}, error: ${error?.message}`);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function verifyUrl(url, options = {}) {
|
|
68
|
+
if (!url)
|
|
69
|
+
throw new Error('URL is required');
|
|
70
|
+
const { maxLength = MAX_URL_LENGTH, protocols = ['https:'], blockedHosts = ['localhost'], blockedSuffixes = [], allowIp = false, checkDns = false, } = options;
|
|
71
|
+
if (url.length > maxLength) {
|
|
72
|
+
throw new Error(`URL exceeds maximum length of ${maxLength} characters: ${url.length}`);
|
|
73
|
+
}
|
|
74
|
+
const { hostname, protocol } = new URL(url);
|
|
75
|
+
if (!protocols.includes(protocol)) {
|
|
76
|
+
throw new Error(`Protocol '${protocol}' is not allowed. Allowed protocols: ${protocols.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
const normalizedHost = stripTrailingDots(hostname);
|
|
79
|
+
if (isBlockedHostname(normalizedHost, blockedHosts, blockedSuffixes)) {
|
|
80
|
+
throw new Error(`Hostname '${normalizedHost}' is blocked`);
|
|
81
|
+
}
|
|
82
|
+
const isIP = Boolean(parseIp(normalizedHost));
|
|
83
|
+
if (isIP) {
|
|
84
|
+
if (!allowIp) {
|
|
85
|
+
throw new Error(`IP addresses are not allowed: ${normalizedHost}`);
|
|
86
|
+
}
|
|
87
|
+
if (!isUnicastAddress(normalizedHost)) {
|
|
88
|
+
throw new Error(`IP address is not unicast: ${normalizedHost}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (checkDns && !isIP) {
|
|
92
|
+
const resolved = await resolveHostAddresses(normalizedHost);
|
|
93
|
+
if (resolved.length === 0) {
|
|
94
|
+
throw new Error(`DNS resolution failed: no addresses found for ${normalizedHost}`);
|
|
95
|
+
}
|
|
96
|
+
if (!resolved.every((address) => isUnicastAddress(address))) {
|
|
97
|
+
throw new Error(`DNS resolved to non-unicast addresses for ${normalizedHost}`);
|
|
98
|
+
}
|
|
38
99
|
}
|
|
100
|
+
return true;
|
|
39
101
|
}
|
package/lib/url.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
1
|
+
import * as ipaddr from 'ipaddr.js';
|
|
2
|
+
export declare function parseIp(host: string): ipaddr.IPv4 | ipaddr.IPv6 | null;
|
|
3
|
+
export declare function isUnicastAddress(host: string): boolean;
|
|
4
|
+
export declare function resolveHostAddresses(host: string): Promise<string[]>;
|
|
5
|
+
export interface VerifyUrlOptions {
|
|
6
|
+
allowIp?: boolean;
|
|
7
|
+
maxLength?: number;
|
|
8
|
+
protocols?: string[];
|
|
9
|
+
blockedHosts?: string[];
|
|
10
|
+
blockedSuffixes?: string[];
|
|
11
|
+
checkDns?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function verifyUrl(url: string, options?: VerifyUrlOptions): Promise<boolean>;
|
package/lib/url.js
CHANGED
|
@@ -1,43 +1,130 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
26
|
+
exports.parseIp = parseIp;
|
|
27
|
+
exports.isUnicastAddress = isUnicastAddress;
|
|
28
|
+
exports.resolveHostAddresses = resolveHostAddresses;
|
|
4
29
|
exports.verifyUrl = verifyUrl;
|
|
5
30
|
const url_1 = require("url");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
const ipaddr = __importStar(require("ipaddr.js"));
|
|
32
|
+
const promises_1 = require("dns/promises");
|
|
33
|
+
const MAX_URL_LENGTH = 256;
|
|
34
|
+
function parseIp(host) {
|
|
35
|
+
if (!ipaddr.isValid(host))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
return ipaddr.parse(host);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
16
43
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
function stripTrailingDots(host) {
|
|
45
|
+
return host.replace(/\.+$/, '');
|
|
46
|
+
}
|
|
47
|
+
function isBlockedHostname(host, blockedHosts, blockedSuffixes) {
|
|
48
|
+
const normalized = host.toLowerCase();
|
|
49
|
+
if (blockedHosts.includes(normalized))
|
|
50
|
+
return true;
|
|
51
|
+
if (blockedSuffixes.some((suffix) => normalized.endsWith(suffix)))
|
|
52
|
+
return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function isUnicastAddress(host) {
|
|
56
|
+
const parsed = parseIp(host);
|
|
57
|
+
if (!parsed)
|
|
21
58
|
return false;
|
|
59
|
+
const range = parsed.range();
|
|
60
|
+
if (range === 'ipv4Mapped' && parsed.kind() === 'ipv6') {
|
|
61
|
+
const mapped = parsed.toIPv4Address();
|
|
62
|
+
return mapped.range() === 'unicast';
|
|
63
|
+
}
|
|
64
|
+
return range === 'unicast';
|
|
65
|
+
}
|
|
66
|
+
async function resolveHostAddresses(host) {
|
|
22
67
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const host = u.hostname.toLowerCase();
|
|
31
|
-
// Block IP addresses
|
|
32
|
-
if (isIPv4(host) || host.includes(':'))
|
|
33
|
-
return false; // treat ":" as IPv6
|
|
34
|
-
// Block localhost
|
|
35
|
-
if (host === 'localhost')
|
|
36
|
-
return false;
|
|
68
|
+
const resolver = new promises_1.Resolver();
|
|
69
|
+
resolver.setServers(['8.8.8.8', '1.1.1.1']);
|
|
70
|
+
const addresses = [];
|
|
71
|
+
// resolve ipv4
|
|
72
|
+
try {
|
|
73
|
+
const ipv4Addresses = await resolver.resolve4(host);
|
|
74
|
+
addresses.push(...ipv4Addresses);
|
|
37
75
|
}
|
|
38
|
-
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.warn(`IPv4 resolution failed for ${host}:`, error?.message);
|
|
78
|
+
}
|
|
79
|
+
// resolve ipv6
|
|
80
|
+
if (!addresses.length) {
|
|
81
|
+
try {
|
|
82
|
+
const ipv6Addresses = await resolver.resolve6(host);
|
|
83
|
+
addresses.push(...ipv6Addresses);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn(`IPv6 resolution failed for ${host}:`, error?.message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return addresses.filter(Boolean);
|
|
39
90
|
}
|
|
40
|
-
catch {
|
|
41
|
-
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.warn(`DNS resolution failed for host: ${host}, error: ${error?.message}`);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function verifyUrl(url, options = {}) {
|
|
97
|
+
if (!url)
|
|
98
|
+
throw new Error('URL is required');
|
|
99
|
+
const { maxLength = MAX_URL_LENGTH, protocols = ['https:'], blockedHosts = ['localhost'], blockedSuffixes = [], allowIp = false, checkDns = false, } = options;
|
|
100
|
+
if (url.length > maxLength) {
|
|
101
|
+
throw new Error(`URL exceeds maximum length of ${maxLength} characters: ${url.length}`);
|
|
102
|
+
}
|
|
103
|
+
const { hostname, protocol } = new url_1.URL(url);
|
|
104
|
+
if (!protocols.includes(protocol)) {
|
|
105
|
+
throw new Error(`Protocol '${protocol}' is not allowed. Allowed protocols: ${protocols.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
const normalizedHost = stripTrailingDots(hostname);
|
|
108
|
+
if (isBlockedHostname(normalizedHost, blockedHosts, blockedSuffixes)) {
|
|
109
|
+
throw new Error(`Hostname '${normalizedHost}' is blocked`);
|
|
110
|
+
}
|
|
111
|
+
const isIP = Boolean(parseIp(normalizedHost));
|
|
112
|
+
if (isIP) {
|
|
113
|
+
if (!allowIp) {
|
|
114
|
+
throw new Error(`IP addresses are not allowed: ${normalizedHost}`);
|
|
115
|
+
}
|
|
116
|
+
if (!isUnicastAddress(normalizedHost)) {
|
|
117
|
+
throw new Error(`IP address is not unicast: ${normalizedHost}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (checkDns && !isIP) {
|
|
121
|
+
const resolved = await resolveHostAddresses(normalizedHost);
|
|
122
|
+
if (resolved.length === 0) {
|
|
123
|
+
throw new Error(`DNS resolution failed: no addresses found for ${normalizedHost}`);
|
|
124
|
+
}
|
|
125
|
+
if (!resolved.every((address) => isUnicastAddress(address))) {
|
|
126
|
+
throw new Error(`DNS resolved to non-unicast addresses for ${normalizedHost}`);
|
|
127
|
+
}
|
|
42
128
|
}
|
|
129
|
+
return true;
|
|
43
130
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocap/util",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.4",
|
|
4
4
|
"description": "utils shared across multiple forge js libs, works in both node.js and browser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arcblock",
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
"base64-url": "^2.3.3",
|
|
16
16
|
"bn.js": "5.2.2",
|
|
17
17
|
"bs58": "^5.0.0",
|
|
18
|
+
"ipaddr.js": "^2.1.0",
|
|
18
19
|
"lodash": "^4.17.21",
|
|
19
20
|
"utf8": "^3.0.0",
|
|
20
|
-
"@ocap/types": "^1.25.
|
|
21
|
+
"@ocap/types": "^1.25.4"
|
|
21
22
|
},
|
|
22
23
|
"resolutions": {
|
|
23
24
|
"elliptic": "6.5.3"
|