webpeel 0.20.2 → 0.20.3
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/dist/server/app.d.ts +14 -0
- package/dist/server/app.js +384 -0
- package/dist/server/auth-store.d.ts +27 -0
- package/dist/server/auth-store.js +88 -0
- package/dist/server/email-service.d.ts +21 -0
- package/dist/server/email-service.js +79 -0
- package/dist/server/job-queue.d.ts +100 -0
- package/dist/server/job-queue.js +145 -0
- package/dist/server/logger.d.ts +10 -0
- package/dist/server/logger.js +37 -0
- package/dist/server/middleware/auth.d.ts +28 -0
- package/dist/server/middleware/auth.js +221 -0
- package/dist/server/middleware/rate-limit.d.ts +24 -0
- package/dist/server/middleware/rate-limit.js +167 -0
- package/dist/server/middleware/url-validator.d.ts +15 -0
- package/dist/server/middleware/url-validator.js +186 -0
- package/dist/server/openapi.yaml +6418 -0
- package/dist/server/pg-auth-store.d.ts +132 -0
- package/dist/server/pg-auth-store.js +472 -0
- package/dist/server/pg-job-queue.d.ts +59 -0
- package/dist/server/pg-job-queue.js +375 -0
- package/dist/server/premium/domain-intel.d.ts +16 -0
- package/dist/server/premium/domain-intel.js +133 -0
- package/dist/server/premium/index.d.ts +17 -0
- package/dist/server/premium/index.js +35 -0
- package/dist/server/premium/swr-cache.d.ts +14 -0
- package/dist/server/premium/swr-cache.js +34 -0
- package/dist/server/routes/activity.d.ts +6 -0
- package/dist/server/routes/activity.js +74 -0
- package/dist/server/routes/answer.d.ts +5 -0
- package/dist/server/routes/answer.js +125 -0
- package/dist/server/routes/ask.d.ts +28 -0
- package/dist/server/routes/ask.js +229 -0
- package/dist/server/routes/batch.d.ts +6 -0
- package/dist/server/routes/batch.js +493 -0
- package/dist/server/routes/cli-usage.d.ts +6 -0
- package/dist/server/routes/cli-usage.js +127 -0
- package/dist/server/routes/compat.d.ts +23 -0
- package/dist/server/routes/compat.js +652 -0
- package/dist/server/routes/deep-fetch.d.ts +8 -0
- package/dist/server/routes/deep-fetch.js +57 -0
- package/dist/server/routes/demo.d.ts +24 -0
- package/dist/server/routes/demo.js +517 -0
- package/dist/server/routes/do.d.ts +8 -0
- package/dist/server/routes/do.js +72 -0
- package/dist/server/routes/extract.d.ts +8 -0
- package/dist/server/routes/extract.js +235 -0
- package/dist/server/routes/fetch.d.ts +7 -0
- package/dist/server/routes/fetch.js +999 -0
- package/dist/server/routes/health.d.ts +7 -0
- package/dist/server/routes/health.js +19 -0
- package/dist/server/routes/jobs.d.ts +7 -0
- package/dist/server/routes/jobs.js +573 -0
- package/dist/server/routes/mcp.d.ts +14 -0
- package/dist/server/routes/mcp.js +141 -0
- package/dist/server/routes/oauth.d.ts +9 -0
- package/dist/server/routes/oauth.js +396 -0
- package/dist/server/routes/playground.d.ts +17 -0
- package/dist/server/routes/playground.js +283 -0
- package/dist/server/routes/screenshot.d.ts +22 -0
- package/dist/server/routes/screenshot.js +816 -0
- package/dist/server/routes/search.d.ts +6 -0
- package/dist/server/routes/search.js +303 -0
- package/dist/server/routes/session.d.ts +15 -0
- package/dist/server/routes/session.js +397 -0
- package/dist/server/routes/stats.d.ts +6 -0
- package/dist/server/routes/stats.js +71 -0
- package/dist/server/routes/stripe.d.ts +15 -0
- package/dist/server/routes/stripe.js +294 -0
- package/dist/server/routes/users.d.ts +8 -0
- package/dist/server/routes/users.js +1671 -0
- package/dist/server/routes/watch.d.ts +15 -0
- package/dist/server/routes/watch.js +309 -0
- package/dist/server/routes/webhooks.d.ts +26 -0
- package/dist/server/routes/webhooks.js +170 -0
- package/dist/server/routes/youtube.d.ts +6 -0
- package/dist/server/routes/youtube.js +130 -0
- package/dist/server/sentry.d.ts +13 -0
- package/dist/server/sentry.js +38 -0
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.js +7 -0
- package/dist/server/utils/response.d.ts +44 -0
- package/dist/server/utils/response.js +69 -0
- package/dist/server/utils/sse.d.ts +22 -0
- package/dist/server/utils/sse.js +38 -0
- package/package.json +2 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding window rate limiting middleware
|
|
3
|
+
*/
|
|
4
|
+
import '../types.js'; // Augments Express.Request with requestId
|
|
5
|
+
export class RateLimiter {
|
|
6
|
+
store = new Map();
|
|
7
|
+
windowMs;
|
|
8
|
+
constructor(windowMs = 60000) {
|
|
9
|
+
this.windowMs = windowMs;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check if request is allowed under rate limit
|
|
13
|
+
* @param cost - Number of credits this request costs (default: 1)
|
|
14
|
+
*/
|
|
15
|
+
checkLimit(identifier, limit, cost = 1) {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const windowStart = now - this.windowMs;
|
|
18
|
+
// Get or create entry
|
|
19
|
+
let entry = this.store.get(identifier);
|
|
20
|
+
if (!entry) {
|
|
21
|
+
entry = { timestamps: [] };
|
|
22
|
+
this.store.set(identifier, entry);
|
|
23
|
+
}
|
|
24
|
+
// Remove timestamps outside the window
|
|
25
|
+
entry.timestamps = entry.timestamps.filter(ts => ts > windowStart);
|
|
26
|
+
// Check if limit would be exceeded by this request's cost
|
|
27
|
+
if (entry.timestamps.length + cost > limit) {
|
|
28
|
+
const oldestTimestamp = entry.timestamps[0];
|
|
29
|
+
const retryAfter = oldestTimestamp
|
|
30
|
+
? Math.ceil((oldestTimestamp + this.windowMs - now) / 1000)
|
|
31
|
+
: 1;
|
|
32
|
+
return {
|
|
33
|
+
allowed: false,
|
|
34
|
+
remaining: Math.max(0, limit - entry.timestamps.length),
|
|
35
|
+
retryAfter,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Add `cost` timestamps to represent the weight of this request
|
|
39
|
+
for (let i = 0; i < cost; i++) {
|
|
40
|
+
entry.timestamps.push(now);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
allowed: true,
|
|
44
|
+
remaining: limit - entry.timestamps.length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Clean up old entries (call periodically)
|
|
49
|
+
*/
|
|
50
|
+
cleanup() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const windowStart = now - this.windowMs;
|
|
53
|
+
for (const [identifier, entry] of this.store.entries()) {
|
|
54
|
+
entry.timestamps = entry.timestamps.filter(ts => ts > windowStart);
|
|
55
|
+
if (entry.timestamps.length === 0) {
|
|
56
|
+
this.store.delete(identifier);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Hourly burst limits per tier.
|
|
63
|
+
* These are the hard caps enforced by the in-memory sliding window.
|
|
64
|
+
* Free: 50/hr, Pro: 100/hr, Max: 500/hr (matches pricing page + stripe.ts).
|
|
65
|
+
*/
|
|
66
|
+
const TIER_BURST_LIMITS = {
|
|
67
|
+
free: 50,
|
|
68
|
+
pro: 100,
|
|
69
|
+
max: 500,
|
|
70
|
+
admin: 999999,
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* System/documentation endpoints that should be exempt from rate limiting.
|
|
74
|
+
* These are low-cost informational endpoints that should never be throttled.
|
|
75
|
+
*/
|
|
76
|
+
const EXEMPT_PATHS = [
|
|
77
|
+
'/health',
|
|
78
|
+
'/openapi.json',
|
|
79
|
+
'/openapi.yaml',
|
|
80
|
+
'/docs',
|
|
81
|
+
'/v1/usage',
|
|
82
|
+
'/v1/me',
|
|
83
|
+
'/v1/keys',
|
|
84
|
+
'/v1/activity',
|
|
85
|
+
'/v1/stats',
|
|
86
|
+
];
|
|
87
|
+
export function createRateLimitMiddleware(limiter) {
|
|
88
|
+
return (req, res, next) => {
|
|
89
|
+
try {
|
|
90
|
+
// Skip rate limiting for system/documentation endpoints
|
|
91
|
+
if (EXEMPT_PATHS.some(p => req.path === p || req.path.startsWith(p + '/'))) {
|
|
92
|
+
return next();
|
|
93
|
+
}
|
|
94
|
+
// Use API key or real client IP as identifier.
|
|
95
|
+
// Prefer Cloudflare CF-Connecting-IP, then x-forwarded-for first
|
|
96
|
+
// entry (real client), then x-real-ip, then req.ip.
|
|
97
|
+
const forwardedFor = req.headers['x-forwarded-for'];
|
|
98
|
+
const firstForwardedIp = typeof forwardedFor === 'string'
|
|
99
|
+
? forwardedFor.split(',')[0].trim()
|
|
100
|
+
: Array.isArray(forwardedFor) ? forwardedFor[0] : undefined;
|
|
101
|
+
const clientIp = req.headers['cf-connecting-ip']
|
|
102
|
+
|| firstForwardedIp
|
|
103
|
+
|| req.headers['x-real-ip']
|
|
104
|
+
|| req.ip
|
|
105
|
+
|| 'unknown';
|
|
106
|
+
const identifier = req.auth?.keyInfo?.key || clientIp;
|
|
107
|
+
// Use tier-based hourly burst limits (matches the 1-hour sliding window)
|
|
108
|
+
const limit = TIER_BURST_LIMITS[req.auth?.tier || 'free'] || 50;
|
|
109
|
+
// Weighted cost based on route — heavier operations consume more credits
|
|
110
|
+
let cost = 1;
|
|
111
|
+
const path = req.path;
|
|
112
|
+
if (path.includes('/crawl') || path.includes('/map'))
|
|
113
|
+
cost = 5;
|
|
114
|
+
else if (path.includes('/batch'))
|
|
115
|
+
cost = 2;
|
|
116
|
+
else if (path.includes('/screenshot'))
|
|
117
|
+
cost = 2;
|
|
118
|
+
else if (req.query.render === 'true' || req.body?.render === true)
|
|
119
|
+
cost = 3;
|
|
120
|
+
const result = limiter.checkLimit(identifier, limit, cost);
|
|
121
|
+
// Calculate reset timestamp
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const resetTimestamp = Math.ceil((now + limiter['windowMs']) / 1000);
|
|
124
|
+
// Set rate limit headers on ALL responses
|
|
125
|
+
res.setHeader('X-RateLimit-Limit', limit.toString());
|
|
126
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, result.remaining).toString());
|
|
127
|
+
res.setHeader('X-RateLimit-Reset', resetTimestamp.toString());
|
|
128
|
+
// Add plan header if authenticated
|
|
129
|
+
if (req.auth?.tier) {
|
|
130
|
+
res.setHeader('X-WebPeel-Plan', req.auth.tier);
|
|
131
|
+
}
|
|
132
|
+
if (!result.allowed) {
|
|
133
|
+
const retryAfterSecs = result.retryAfter;
|
|
134
|
+
res.setHeader('Retry-After', retryAfterSecs.toString());
|
|
135
|
+
const tier = req.auth?.tier || 'free';
|
|
136
|
+
const upgradeHint = tier === 'free'
|
|
137
|
+
? ' Upgrade to Pro ($9/mo) for 100/hr burst limit → https://webpeel.dev/pricing'
|
|
138
|
+
: tier === 'pro'
|
|
139
|
+
? ' Upgrade to Max ($29/mo) for 500/hr burst limit → https://webpeel.dev/pricing'
|
|
140
|
+
: '';
|
|
141
|
+
res.status(429).json({
|
|
142
|
+
success: false,
|
|
143
|
+
error: {
|
|
144
|
+
type: 'rate_limited',
|
|
145
|
+
message: `Hourly rate limit exceeded (${limit} requests/hr on ${tier} plan). Try again in ${retryAfterSecs}s.`,
|
|
146
|
+
hint: `Retry after ${retryAfterSecs} seconds.${upgradeHint}`,
|
|
147
|
+
docs: 'https://webpeel.dev/docs/errors#rate-limited',
|
|
148
|
+
},
|
|
149
|
+
metadata: { requestId: req.requestId },
|
|
150
|
+
});
|
|
151
|
+
return; // Stop processing - rate limit exceeded
|
|
152
|
+
}
|
|
153
|
+
next();
|
|
154
|
+
}
|
|
155
|
+
catch (_error) {
|
|
156
|
+
res.status(500).json({
|
|
157
|
+
success: false,
|
|
158
|
+
error: {
|
|
159
|
+
type: 'internal_error',
|
|
160
|
+
message: 'Rate limiting failed',
|
|
161
|
+
docs: 'https://webpeel.dev/docs/errors#internal-error',
|
|
162
|
+
},
|
|
163
|
+
metadata: { requestId: req.requestId },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL validation middleware to prevent SSRF attacks
|
|
3
|
+
* Validates URLs BEFORE any network request is made
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate URL to prevent SSRF attacks
|
|
7
|
+
* Blocks localhost, private IPs, link-local addresses, and non-HTTP(S) protocols
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateUrlForSSRF(urlString: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* SSRF Error class
|
|
12
|
+
*/
|
|
13
|
+
export declare class SSRFError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL validation middleware to prevent SSRF attacks
|
|
3
|
+
* Validates URLs BEFORE any network request is made
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate URL to prevent SSRF attacks
|
|
7
|
+
* Blocks localhost, private IPs, link-local addresses, and non-HTTP(S) protocols
|
|
8
|
+
*/
|
|
9
|
+
export function validateUrlForSSRF(urlString) {
|
|
10
|
+
// Parse URL
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(urlString);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error('Invalid URL format');
|
|
17
|
+
}
|
|
18
|
+
// Only allow HTTP(S)
|
|
19
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
20
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
21
|
+
}
|
|
22
|
+
const hostname = url.hostname.toLowerCase();
|
|
23
|
+
// Block localhost patterns
|
|
24
|
+
const localhostPatterns = ['localhost', '0.0.0.0'];
|
|
25
|
+
if (localhostPatterns.some(pattern => hostname === pattern || hostname.endsWith('.' + pattern))) {
|
|
26
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
27
|
+
}
|
|
28
|
+
// Parse and validate IP addresses
|
|
29
|
+
const ipv4Info = parseIPv4(hostname);
|
|
30
|
+
if (ipv4Info) {
|
|
31
|
+
validateIPv4ForSSRF(ipv4Info);
|
|
32
|
+
}
|
|
33
|
+
// Validate IPv6
|
|
34
|
+
if (hostname.includes(':')) {
|
|
35
|
+
validateIPv6ForSSRF(hostname);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* SSRF Error class
|
|
40
|
+
*/
|
|
41
|
+
export class SSRFError extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = 'SSRFError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse IPv4 address in any format (dotted, hex, octal, decimal)
|
|
49
|
+
*/
|
|
50
|
+
function parseIPv4(hostname) {
|
|
51
|
+
const cleaned = hostname.replace(/^\[|\]$/g, '');
|
|
52
|
+
// Standard dotted notation: 192.168.1.1
|
|
53
|
+
const dottedRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
54
|
+
const dottedMatch = cleaned.match(dottedRegex);
|
|
55
|
+
if (dottedMatch) {
|
|
56
|
+
const octets = dottedMatch.slice(1).map(Number);
|
|
57
|
+
if (octets.every(o => o >= 0 && o <= 255)) {
|
|
58
|
+
return octets;
|
|
59
|
+
}
|
|
60
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
61
|
+
}
|
|
62
|
+
// Hex notation: 0x7f000001
|
|
63
|
+
if (/^0x[0-9a-fA-F]+$/.test(cleaned)) {
|
|
64
|
+
const num = parseInt(cleaned, 16);
|
|
65
|
+
return [
|
|
66
|
+
(num >>> 24) & 0xff,
|
|
67
|
+
(num >>> 16) & 0xff,
|
|
68
|
+
(num >>> 8) & 0xff,
|
|
69
|
+
num & 0xff,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
// Octal notation
|
|
73
|
+
if (/^0[0-7]/.test(cleaned)) {
|
|
74
|
+
if (/^0[0-7]+$/.test(cleaned)) {
|
|
75
|
+
const num = parseInt(cleaned, 8);
|
|
76
|
+
if (num <= 0xffffffff) {
|
|
77
|
+
return [
|
|
78
|
+
(num >>> 24) & 0xff,
|
|
79
|
+
(num >>> 16) & 0xff,
|
|
80
|
+
(num >>> 8) & 0xff,
|
|
81
|
+
num & 0xff,
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const parts = cleaned.split('.');
|
|
86
|
+
if (parts.length === 4) {
|
|
87
|
+
const octets = parts.map(p => parseInt(p, /^0[0-7]/.test(p) ? 8 : 10));
|
|
88
|
+
if (octets.every(o => o >= 0 && o <= 255)) {
|
|
89
|
+
return octets;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Decimal notation: 2130706433
|
|
94
|
+
if (/^\d+$/.test(cleaned)) {
|
|
95
|
+
const num = parseInt(cleaned, 10);
|
|
96
|
+
if (num <= 0xffffffff) {
|
|
97
|
+
return [
|
|
98
|
+
(num >>> 24) & 0xff,
|
|
99
|
+
(num >>> 16) & 0xff,
|
|
100
|
+
(num >>> 8) & 0xff,
|
|
101
|
+
num & 0xff,
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate IPv4 address against private/reserved ranges
|
|
109
|
+
*/
|
|
110
|
+
function validateIPv4ForSSRF(octets) {
|
|
111
|
+
const [a, b, c, d] = octets;
|
|
112
|
+
// Loopback: 127.0.0.0/8
|
|
113
|
+
if (a === 127) {
|
|
114
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
115
|
+
}
|
|
116
|
+
// Private: 10.0.0.0/8
|
|
117
|
+
if (a === 10) {
|
|
118
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
119
|
+
}
|
|
120
|
+
// Private: 172.16.0.0/12
|
|
121
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
122
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
123
|
+
}
|
|
124
|
+
// Private: 192.168.0.0/16
|
|
125
|
+
if (a === 192 && b === 168) {
|
|
126
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
127
|
+
}
|
|
128
|
+
// Link-local: 169.254.0.0/16 (includes AWS metadata endpoint)
|
|
129
|
+
if (a === 169 && b === 254) {
|
|
130
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
131
|
+
}
|
|
132
|
+
// Broadcast: 255.255.255.255
|
|
133
|
+
if (a === 255 && b === 255 && c === 255 && d === 255) {
|
|
134
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
135
|
+
}
|
|
136
|
+
// This network: 0.0.0.0/8
|
|
137
|
+
if (a === 0) {
|
|
138
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Validate IPv6 address against private/reserved ranges
|
|
143
|
+
*/
|
|
144
|
+
function validateIPv6ForSSRF(hostname) {
|
|
145
|
+
const addr = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
146
|
+
// Loopback: ::1
|
|
147
|
+
if (addr === '::1' || addr === '0:0:0:0:0:0:0:1') {
|
|
148
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
149
|
+
}
|
|
150
|
+
// IPv6 mapped IPv4: ::ffff:192.168.1.1
|
|
151
|
+
if (addr.startsWith('::ffff:')) {
|
|
152
|
+
const ipv4Part = addr.substring(7);
|
|
153
|
+
if (ipv4Part.includes('.')) {
|
|
154
|
+
const parts = ipv4Part.split('.');
|
|
155
|
+
if (parts.length === 4) {
|
|
156
|
+
const octets = parts.map(p => parseInt(p, 10));
|
|
157
|
+
if (octets.every(o => !isNaN(o) && o >= 0 && o <= 255)) {
|
|
158
|
+
validateIPv4ForSSRF(octets);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const hexStr = ipv4Part.replace(/:/g, '');
|
|
164
|
+
if (/^[0-9a-f]{1,8}$/.test(hexStr)) {
|
|
165
|
+
const num = parseInt(hexStr, 16);
|
|
166
|
+
const octets = [
|
|
167
|
+
(num >>> 24) & 0xff,
|
|
168
|
+
(num >>> 16) & 0xff,
|
|
169
|
+
(num >>> 8) & 0xff,
|
|
170
|
+
num & 0xff,
|
|
171
|
+
];
|
|
172
|
+
validateIPv4ForSSRF(octets);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
176
|
+
}
|
|
177
|
+
// Unique local addresses: fc00::/7
|
|
178
|
+
if (addr.startsWith('fc') || addr.startsWith('fd')) {
|
|
179
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
180
|
+
}
|
|
181
|
+
// Link-local: fe80::/10
|
|
182
|
+
if (addr.startsWith('fe8') || addr.startsWith('fe9') ||
|
|
183
|
+
addr.startsWith('fea') || addr.startsWith('feb')) {
|
|
184
|
+
throw new SSRFError('Cannot fetch localhost, private networks, or non-HTTP URLs');
|
|
185
|
+
}
|
|
186
|
+
}
|