api-ape 3.0.2 → 4.1.0
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/README.md +59 -572
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +322 -71
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -219
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -224
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -311
- package/server/lib/broadcast.js +0 -146
|
@@ -1,21 +1,149 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Root Domain Extraction Utility
|
|
3
|
+
*
|
|
4
|
+
* This module provides a utility function to extract the root domain from a URL
|
|
5
|
+
* or hostname. This is useful for security checks like CORS origin validation,
|
|
6
|
+
* where you need to compare domains regardless of subdomains or ports.
|
|
7
|
+
*
|
|
8
|
+
* The function handles various input formats:
|
|
9
|
+
* - Full URLs: `https://sub.example.com:3000/path` → `example.com`
|
|
10
|
+
* - Hostnames with port: `api.example.com:8080` → `example.com`
|
|
11
|
+
* - Simple hostnames: `www.example.com` → `example.com`
|
|
12
|
+
* - Already root domains: `example.com` → `example.com`
|
|
13
|
+
*
|
|
14
|
+
* @module server/security/extractRootDomain
|
|
15
|
+
* @see {@link module:server/security/origin} - Origin security validation
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const extractRootDomain = require('./extractRootDomain')
|
|
19
|
+
*
|
|
20
|
+
* // Full URL with subdomain and port
|
|
21
|
+
* extractRootDomain('https://api.example.com:3000/v1/users')
|
|
22
|
+
* // Returns: 'example.com'
|
|
23
|
+
*
|
|
24
|
+
* // Hostname with subdomain
|
|
25
|
+
* extractRootDomain('www.mysite.org')
|
|
26
|
+
* // Returns: 'mysite.org'
|
|
27
|
+
*
|
|
28
|
+
* // Already a root domain
|
|
29
|
+
* extractRootDomain('example.com')
|
|
30
|
+
* // Returns: 'example.com'
|
|
4
31
|
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts the root domain from a URL or hostname.
|
|
35
|
+
*
|
|
36
|
+
* The root domain is the registrable domain (e.g., `example.com`) without
|
|
37
|
+
* any subdomains (e.g., `www`, `api`, `staging`).
|
|
38
|
+
*
|
|
39
|
+
* Algorithm:
|
|
40
|
+
* 1. If input contains `://`, parse as full URL and extract hostname
|
|
41
|
+
* 2. Otherwise, treat as hostname and remove port if present
|
|
42
|
+
* 3. Split hostname by `.` and take the last two segments
|
|
43
|
+
*
|
|
44
|
+
* **Country Code TLD Handling**:
|
|
45
|
+
* - Handles common ccTLDs like `.co.uk`, `.com.au`, `.me.uk`
|
|
46
|
+
* - Detection: If TLD is 2 chars AND SLD is a known pattern (co, com, net, org, me, ac, gov)
|
|
47
|
+
* - Example: `api.example.co.uk` → `example.co.uk` (not `co.uk`)
|
|
48
|
+
*
|
|
49
|
+
* **Limitations**:
|
|
50
|
+
* - For complex cases (e.g., `.pvt.k12.ma.us`), consider using
|
|
51
|
+
* a proper public suffix list library
|
|
52
|
+
*
|
|
53
|
+
* @function extractRootDomain
|
|
54
|
+
* @param {string} url - Full URL or hostname to extract root domain from
|
|
55
|
+
* @returns {string} The root domain, or empty string if input is falsy
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Full URL with subdomain
|
|
59
|
+
* extractRootDomain('https://sub.example.com:3000/path')
|
|
60
|
+
* // Returns: 'example.com'
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Hostname with port
|
|
64
|
+
* extractRootDomain('api.example.com:8080')
|
|
65
|
+
* // Returns: 'example.com'
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Simple subdomain
|
|
69
|
+
* extractRootDomain('www.example.com')
|
|
70
|
+
* // Returns: 'example.com'
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Already root domain
|
|
74
|
+
* extractRootDomain('example.com')
|
|
75
|
+
* // Returns: 'example.com'
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Two-part domain
|
|
79
|
+
* extractRootDomain('localhost')
|
|
80
|
+
* // Returns: 'localhost'
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Null/undefined input
|
|
84
|
+
* extractRootDomain(null)
|
|
85
|
+
* // Returns: ''
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Invalid URL falls back gracefully
|
|
89
|
+
* extractRootDomain('not-a-valid-url')
|
|
90
|
+
* // Returns: 'not-a-valid-url'
|
|
91
|
+
*/
|
|
92
|
+
// Common second-level domains used with country code TLDs
|
|
93
|
+
const CCTLD_SLDS = new Set(["co", "com", "net", "org", "me", "ac", "gov", "edu"]);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a domain uses a country code TLD pattern
|
|
97
|
+
* @param {string[]} parts - Domain parts split by '.'
|
|
98
|
+
* @returns {boolean} True if this looks like a ccTLD domain
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
function isCcTLD(parts) {
|
|
102
|
+
if (parts.length < 3) return false;
|
|
103
|
+
const tld = parts[parts.length - 1];
|
|
104
|
+
const sld = parts[parts.length - 2];
|
|
105
|
+
// TLD must be 2 chars (country code) AND SLD is a known pattern
|
|
106
|
+
return tld.length === 2 && CCTLD_SLDS.has(sld.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
|
|
5
109
|
module.exports = function extractRootDomain(url) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
110
|
+
// Handle null/undefined/empty input
|
|
111
|
+
if (!url) return "";
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Check if this is a full URL (has protocol)
|
|
115
|
+
if (url.includes("://")) {
|
|
116
|
+
// Parse as URL to extract hostname
|
|
117
|
+
const hostname = new URL(url).hostname;
|
|
118
|
+
const parts = hostname.split(".");
|
|
119
|
+
|
|
120
|
+
if (parts.length > 2) {
|
|
121
|
+
// Check for country code TLD (e.g., .co.uk, .com.au, .me.uk)
|
|
122
|
+
if (isCcTLD(parts)) {
|
|
123
|
+
// Include the third-level domain for ccTLDs
|
|
124
|
+
return parts.slice(-3).join(".");
|
|
13
125
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return parts.length > 2 ? parts.slice(-2).join('.') : hostname
|
|
18
|
-
} catch {
|
|
19
|
-
return url.split(':')[0]
|
|
126
|
+
return parts.slice(-2).join(".");
|
|
127
|
+
}
|
|
128
|
+
return hostname;
|
|
20
129
|
}
|
|
21
|
-
|
|
130
|
+
|
|
131
|
+
// Handle hostname:port format (no protocol)
|
|
132
|
+
// Remove port by splitting on ':' and taking first part
|
|
133
|
+
const hostname = url.split(":")[0];
|
|
134
|
+
const parts = hostname.split(".");
|
|
135
|
+
|
|
136
|
+
if (parts.length > 2) {
|
|
137
|
+
// Check for country code TLD (e.g., .co.uk, .com.au, .me.uk)
|
|
138
|
+
if (isCcTLD(parts)) {
|
|
139
|
+
return parts.slice(-3).join(".");
|
|
140
|
+
}
|
|
141
|
+
return parts.slice(-2).join(".");
|
|
142
|
+
}
|
|
143
|
+
return hostname;
|
|
144
|
+
} catch {
|
|
145
|
+
// If URL parsing fails, try to extract hostname from raw string
|
|
146
|
+
// This handles malformed URLs gracefully
|
|
147
|
+
return url.split(":")[0];
|
|
148
|
+
}
|
|
149
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Security Module Files
|
|
2
|
+
|
|
3
|
+
This module provides security features to protect api-ape connections from common web vulnerabilities, including CSRF protection and duplicate request detection. Origin validation is enabled by default.
|
|
4
|
+
|
|
5
|
+
## Guidelines
|
|
6
|
+
|
|
7
|
+
- **Origin validation is critical** — Changes to `origin.js` affect all connection security; test thoroughly
|
|
8
|
+
- **Domain matching** — Use `extractRootDomain.js` for domain comparisons; don't implement custom logic
|
|
9
|
+
- **Subdomain support** — Origin checks must work with subdomains (e.g., `app.example.com` vs `example.com`)
|
|
10
|
+
- **No configuration required** — Security features should work out-of-the-box with sensible defaults
|
|
11
|
+
- **Fail secure** — When in doubt, reject the connection; false positives are better than security holes
|
|
12
|
+
- **Localhost exceptions** — Development on localhost should work without origin issues
|
|
13
|
+
|
|
14
|
+
## Directory Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
security/
|
|
18
|
+
├── extractRootDomain.js # Domain extraction for origin validation
|
|
19
|
+
├── origin.js # Origin verification (CSRF protection)
|
|
20
|
+
└── reply.js # Duplicate request protection
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Files
|
|
24
|
+
|
|
25
|
+
### `origin.js`
|
|
26
|
+
|
|
27
|
+
Origin validation to prevent Cross-Site Request Forgery (CSRF) attacks:
|
|
28
|
+
|
|
29
|
+
- Validates the `Origin` header against the `Host` header
|
|
30
|
+
- Ensures WebSocket connections only come from the same origin
|
|
31
|
+
- Automatically rejects cross-origin requests
|
|
32
|
+
- Works with both Express.js and raw Node.js servers
|
|
33
|
+
- Returns `true` if valid, `false` to reject connection
|
|
34
|
+
|
|
35
|
+
### `extractRootDomain.js`
|
|
36
|
+
|
|
37
|
+
Extracts the root domain from a hostname for flexible origin matching:
|
|
38
|
+
|
|
39
|
+
- Handles subdomains (e.g., `app.example.com` → `example.com`)
|
|
40
|
+
- Handles IP addresses and localhost
|
|
41
|
+
- Handles ports in host strings (strips them for comparison)
|
|
42
|
+
- Used by `origin.js` for domain comparison
|
|
43
|
+
|
|
44
|
+
### `reply.js`
|
|
45
|
+
|
|
46
|
+
Duplicate request protection to prevent replay attacks:
|
|
47
|
+
|
|
48
|
+
- Tracks recently processed `queryId` values
|
|
49
|
+
- Rejects duplicate requests within a configurable time window
|
|
50
|
+
- Prevents attackers from replaying captured WebSocket messages
|
|
51
|
+
- Automatically cleans up expired entries
|
|
@@ -1,25 +1,207 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Origin Security Check for api-ape WebSocket Connections
|
|
3
|
+
*
|
|
4
|
+
* This module provides Cross-Site Request Forgery (CSRF) protection for
|
|
5
|
+
* WebSocket connections by validating that the request's Origin header
|
|
6
|
+
* matches the server's Host header.
|
|
7
|
+
*
|
|
8
|
+
* ## Why Origin Validation Matters
|
|
9
|
+
*
|
|
10
|
+
* WebSocket connections bypass the Same-Origin Policy that protects regular
|
|
11
|
+
* HTTP requests. Without origin validation, a malicious website could:
|
|
12
|
+
*
|
|
13
|
+
* 1. Connect to your WebSocket server from any domain
|
|
14
|
+
* 2. Send requests using the victim's cookies/credentials
|
|
15
|
+
* 3. Access data or perform actions as the authenticated user
|
|
16
|
+
*
|
|
17
|
+
* ## How It Works
|
|
18
|
+
*
|
|
19
|
+
* ```
|
|
20
|
+
* Browser Request:
|
|
21
|
+
* Origin: https://evil.com
|
|
22
|
+
* Host: api.example.com
|
|
23
|
+
*
|
|
24
|
+
* Validation:
|
|
25
|
+
* extractRootDomain('https://evil.com') → 'evil.com'
|
|
26
|
+
* extractRootDomain('api.example.com') → 'example.com'
|
|
27
|
+
* 'evil.com' !== 'example.com' → REJECT
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* ## Allowed Scenarios
|
|
31
|
+
*
|
|
32
|
+
* | Origin | Host | Result | Reason |
|
|
33
|
+
* |----------------------|-----------------------|---------|---------------------------|
|
|
34
|
+
* | https://example.com | api.example.com | ✓ Allow | Same root domain |
|
|
35
|
+
* | https://app.example.com | example.com | ✓ Allow | Same root domain |
|
|
36
|
+
* | (none) | example.com | ✓ Allow | No origin = same-origin |
|
|
37
|
+
* | https://evil.com | example.com | ✗ Reject| Different domains |
|
|
38
|
+
* | https://example.com.evil.com | example.com | ✗ Reject| Fake subdomain attack |
|
|
39
|
+
*
|
|
40
|
+
* ## Subdomains
|
|
41
|
+
*
|
|
42
|
+
* The validation uses root domain comparison (via `extractRootDomain`),
|
|
43
|
+
* which means subdomains of the same root are allowed:
|
|
44
|
+
* - `app.example.com` and `api.example.com` share root `example.com`
|
|
45
|
+
* - Both are allowed to connect to each other
|
|
46
|
+
*
|
|
47
|
+
* @module server/security/origin
|
|
48
|
+
* @see {@link module:server/security/extractRootDomain} for domain extraction
|
|
49
|
+
* @see {@link module:server/socket/open} for usage in connection handling
|
|
50
|
+
*
|
|
51
|
+
* @example <caption>Basic Usage in Connection Handler</caption>
|
|
52
|
+
* const originSecurity = require('./security/origin')
|
|
53
|
+
*
|
|
54
|
+
* function handleConnection(socket, req) {
|
|
55
|
+
* const isSecure = originSecurity(socket, req, (err) => {
|
|
56
|
+
* console.error('Security error:', err)
|
|
57
|
+
* })
|
|
58
|
+
*
|
|
59
|
+
* if (!isSecure) {
|
|
60
|
+
* return // Connection was rejected
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // Continue with connection setup...
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* @example <caption>Integration with api-ape</caption>
|
|
67
|
+
* // This is called internally by socket/open.js
|
|
68
|
+
* const isOk = originSecurity(socket, req, onError)
|
|
69
|
+
*
|
|
70
|
+
* if (!isOk) {
|
|
71
|
+
* // Socket was destroyed, client removed
|
|
72
|
+
* return
|
|
73
|
+
* }
|
|
74
|
+
*
|
|
75
|
+
* // Proceed with message handling
|
|
76
|
+
*/
|
|
2
77
|
|
|
3
|
-
|
|
78
|
+
const extractRootDomain = require("./extractRootDomain");
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a header value from an HTTP request
|
|
82
|
+
*
|
|
83
|
+
* Handles both Express-style requests (with `.header()` method) and
|
|
84
|
+
* raw Node.js http.IncomingMessage requests (with `.headers` object).
|
|
85
|
+
*
|
|
86
|
+
* @param {http.IncomingMessage|express.Request} req - HTTP request object
|
|
87
|
+
* @param {string} name - Header name (case-insensitive for raw requests)
|
|
88
|
+
* @returns {string|undefined} Header value, or undefined if not present
|
|
89
|
+
* @private
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // Express request
|
|
93
|
+
* getHeader(expressReq, 'Origin') // Uses req.header('Origin')
|
|
94
|
+
*
|
|
95
|
+
* // Raw Node.js request
|
|
96
|
+
* getHeader(httpReq, 'Origin') // Uses req.headers['origin']
|
|
97
|
+
*/
|
|
4
98
|
function getHeader(req, name) {
|
|
5
|
-
// Express-style
|
|
6
|
-
if (typeof req.header ===
|
|
7
|
-
return req.header(name)
|
|
99
|
+
// Express-style request with .header() method
|
|
100
|
+
if (typeof req.header === "function") {
|
|
101
|
+
return req.header(name);
|
|
8
102
|
}
|
|
9
|
-
// Raw Node.js http request
|
|
10
|
-
return req.headers[name.toLowerCase()]
|
|
103
|
+
// Raw Node.js http request - headers are lowercase
|
|
104
|
+
return req.headers[name.toLowerCase()];
|
|
11
105
|
}
|
|
12
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Verify that request origin matches host to prevent CSRF attacks
|
|
109
|
+
*
|
|
110
|
+
* Compares the `Origin` header (where the request came from) with the
|
|
111
|
+
* `Host` header (the server being accessed). If they don't match,
|
|
112
|
+
* the connection is rejected and the socket is destroyed.
|
|
113
|
+
*
|
|
114
|
+
* ## Security Notes
|
|
115
|
+
*
|
|
116
|
+
* - **Missing Origin**: Allowed, as same-origin requests may omit Origin
|
|
117
|
+
* - **Matching Root Domain**: Both headers are reduced to root domains,
|
|
118
|
+
* so `app.example.com` and `api.example.com` both match `example.com`
|
|
119
|
+
* - **Socket Destruction**: Rejected connections have their socket destroyed
|
|
120
|
+
* to immediately terminate the connection
|
|
121
|
+
*
|
|
122
|
+
* ## When Connections Are Rejected
|
|
123
|
+
*
|
|
124
|
+
* - Origin domain doesn't match Host domain
|
|
125
|
+
* - Potential subdomain spoofing (e.g., `example.com.evil.com`)
|
|
126
|
+
*
|
|
127
|
+
* ## When Connections Are Allowed
|
|
128
|
+
*
|
|
129
|
+
* - No Origin header (browser same-origin requests)
|
|
130
|
+
* - Origin root domain matches Host root domain
|
|
131
|
+
*
|
|
132
|
+
* @param {WebSocket} socket - WebSocket instance (must have `.destroy()` method)
|
|
133
|
+
* @param {http.IncomingMessage} req - HTTP upgrade request
|
|
134
|
+
* @param {Function} [onError=console.error] - Error callback for logging rejections
|
|
135
|
+
* @returns {boolean} True if origin is valid and connection should proceed,
|
|
136
|
+
* false if connection was rejected and destroyed
|
|
137
|
+
*
|
|
138
|
+
* @example <caption>Basic Validation</caption>
|
|
139
|
+
* const isValid = originSecurity(socket, req)
|
|
140
|
+
*
|
|
141
|
+
* if (!isValid) {
|
|
142
|
+
* // Socket was destroyed, connection rejected
|
|
143
|
+
* return
|
|
144
|
+
* }
|
|
145
|
+
*
|
|
146
|
+
* // Origin is valid, proceed
|
|
147
|
+
* setupMessageHandlers(socket)
|
|
148
|
+
*
|
|
149
|
+
* @example <caption>With Custom Error Handler</caption>
|
|
150
|
+
* const isValid = originSecurity(socket, req, (errorMessage) => {
|
|
151
|
+
* logger.security.warn(errorMessage, {
|
|
152
|
+
* ip: req.socket.remoteAddress,
|
|
153
|
+
* origin: req.headers.origin,
|
|
154
|
+
* host: req.headers.host
|
|
155
|
+
* })
|
|
156
|
+
* })
|
|
157
|
+
*
|
|
158
|
+
* @example <caption>Scenarios</caption>
|
|
159
|
+
* // Scenario 1: Same domain - ALLOWED
|
|
160
|
+
* // Origin: https://example.com, Host: example.com
|
|
161
|
+
* // extractRootDomain → both 'example.com' → Match ✓
|
|
162
|
+
*
|
|
163
|
+
* // Scenario 2: Subdomain - ALLOWED
|
|
164
|
+
* // Origin: https://app.example.com, Host: api.example.com
|
|
165
|
+
* // extractRootDomain → both 'example.com' → Match ✓
|
|
166
|
+
*
|
|
167
|
+
* // Scenario 3: Different domain - REJECTED
|
|
168
|
+
* // Origin: https://evil.com, Host: example.com
|
|
169
|
+
* // extractRootDomain → 'evil.com' vs 'example.com' → No match ✗
|
|
170
|
+
*
|
|
171
|
+
* // Scenario 4: No origin - ALLOWED
|
|
172
|
+
* // Origin: (not present), Host: example.com
|
|
173
|
+
* // Empty origin doesn't trigger check ✓
|
|
174
|
+
*
|
|
175
|
+
* // Scenario 5: Spoofed subdomain - REJECTED
|
|
176
|
+
* // Origin: https://example.com.evil.com, Host: example.com
|
|
177
|
+
* // extractRootDomain → 'evil.com' vs 'example.com' → No match ✗
|
|
178
|
+
*/
|
|
13
179
|
module.exports = function (socket, req, onError) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
180
|
+
// Default error handler to console.error if not provided
|
|
181
|
+
onError = onError || console.error;
|
|
182
|
+
|
|
183
|
+
// Extract root domains for comparison
|
|
184
|
+
const origin = extractRootDomain(getHeader(req, "Origin") || "");
|
|
185
|
+
const host = extractRootDomain(getHeader(req, "Host"));
|
|
186
|
+
|
|
187
|
+
// Check for origin mismatch (only if Origin header is present)
|
|
17
188
|
if (origin && origin !== host) {
|
|
18
|
-
|
|
189
|
+
// Log the rejection
|
|
190
|
+
onError(
|
|
191
|
+
"REJECTING socket from " +
|
|
192
|
+
getHeader(req, "Origin") +
|
|
193
|
+
" mismatch with " +
|
|
194
|
+
getHeader(req, "Host"),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Destroy the socket to terminate the connection
|
|
19
198
|
if (socket && socket.destroy) {
|
|
20
|
-
socket.destroy()
|
|
199
|
+
socket.destroy();
|
|
21
200
|
}
|
|
22
|
-
|
|
201
|
+
|
|
202
|
+
return false;
|
|
23
203
|
}
|
|
24
|
-
|
|
25
|
-
|
|
204
|
+
|
|
205
|
+
// Origin is valid (or not present)
|
|
206
|
+
return true;
|
|
207
|
+
};
|