cruss-agent 1.0.5 → 1.0.7
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/package.json +1 -1
- package/server.js +41 -26
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -6,14 +6,24 @@ const https = require('https')
|
|
|
6
6
|
const url = require('url')
|
|
7
7
|
|
|
8
8
|
const PORT = 9119
|
|
9
|
-
const VERSION = '1.0.
|
|
10
|
-
|
|
9
|
+
const VERSION = '1.0.7'
|
|
10
|
+
|
|
11
|
+
// ── CORS ─────────────────────────────────────────────────────────────────
|
|
12
|
+
// Access-Control-Allow-Private-Network is required by Chrome 104+ (PNA policy).
|
|
13
|
+
// When an HTTPS page (e.g. cruss-ten.vercel.app) makes a non-simple request
|
|
14
|
+
// (POST with Content-Type: application/json) to localhost, Chrome first sends
|
|
15
|
+
// an OPTIONS preflight with "Access-Control-Request-Private-Network: true".
|
|
16
|
+
// The agent MUST respond with "Access-Control-Allow-Private-Network: true"
|
|
17
|
+
// or Chrome silently blocks the actual request. GET /health works without it
|
|
18
|
+
// because simple GET requests bypass the PNA preflight — which is exactly
|
|
19
|
+
// why health checks passed but POST /proxy was never received.
|
|
11
20
|
function corsHeaders() {
|
|
12
21
|
return {
|
|
13
|
-
'Access-Control-Allow-Origin':
|
|
14
|
-
'Access-Control-Allow-Methods':
|
|
15
|
-
'Access-Control-Allow-Headers':
|
|
16
|
-
'Access-Control-
|
|
22
|
+
'Access-Control-Allow-Origin': '*',
|
|
23
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
24
|
+
'Access-Control-Allow-Headers': '*',
|
|
25
|
+
'Access-Control-Allow-Private-Network': 'true',
|
|
26
|
+
'Access-Control-Max-Age': '86400',
|
|
17
27
|
}
|
|
18
28
|
}
|
|
19
29
|
|
|
@@ -31,10 +41,6 @@ const HOP_BY_HOP = new Set([
|
|
|
31
41
|
'te', 'trailers', 'transfer-encoding', 'upgrade',
|
|
32
42
|
])
|
|
33
43
|
|
|
34
|
-
// Returns all addresses to try for a given hostname.
|
|
35
|
-
// On macOS, python3 -m http.server binds to :: (IPv6 only).
|
|
36
|
-
// Node resolves localhost -> 127.0.0.1 (IPv4) -> ECONNREFUSED.
|
|
37
|
-
// Trying both 127.0.0.1 and ::1 guarantees one works.
|
|
38
44
|
function loopbackCandidates(hostname) {
|
|
39
45
|
if (hostname === 'localhost') return ['127.0.0.1', '::1']
|
|
40
46
|
if (hostname === '127.0.0.1') return ['127.0.0.1', '::1']
|
|
@@ -45,14 +51,16 @@ function loopbackCandidates(hostname) {
|
|
|
45
51
|
function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
46
52
|
return new Promise((resolve, reject) => {
|
|
47
53
|
const lib = isHttps ? https : http
|
|
54
|
+
const resolvedPort = port || (isHttps ? 443 : 80)
|
|
48
55
|
const options = {
|
|
49
56
|
hostname,
|
|
50
|
-
port:
|
|
57
|
+
port: resolvedPort,
|
|
51
58
|
path: path || '/',
|
|
52
59
|
method: method || 'GET',
|
|
53
60
|
headers: outHeaders,
|
|
54
61
|
timeout: 30000,
|
|
55
62
|
}
|
|
63
|
+
console.log(` → trying ${hostname}:${resolvedPort}${path || '/'}`)
|
|
56
64
|
const t0 = Date.now()
|
|
57
65
|
const req = lib.request(options, res => {
|
|
58
66
|
const chunks = []
|
|
@@ -64,10 +72,11 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
|
64
72
|
const respHeaders = {}
|
|
65
73
|
for (const [k, v] of Object.entries(res.headers || {})) {
|
|
66
74
|
if (!['access-control-allow-origin','access-control-allow-headers',
|
|
67
|
-
'access-control-allow-methods'].includes(k.toLowerCase())) {
|
|
75
|
+
'access-control-allow-methods','access-control-allow-private-network'].includes(k.toLowerCase())) {
|
|
68
76
|
respHeaders[k] = Array.isArray(v) ? v.join(', ') : v
|
|
69
77
|
}
|
|
70
78
|
}
|
|
79
|
+
console.log(` ✓ ${res.statusCode} ${res.statusMessage} (${elapsed}ms)`)
|
|
71
80
|
resolve({
|
|
72
81
|
status: res.statusCode, statusText: res.statusMessage,
|
|
73
82
|
headers: respHeaders, body: bodyStr,
|
|
@@ -77,7 +86,10 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
|
77
86
|
res.on('error', reject)
|
|
78
87
|
})
|
|
79
88
|
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out (30s)')) })
|
|
80
|
-
req.on('error',
|
|
89
|
+
req.on('error', (err) => {
|
|
90
|
+
console.log(` ✗ ${hostname}:${resolvedPort} — ${err.code || err.message}`)
|
|
91
|
+
reject(err)
|
|
92
|
+
})
|
|
81
93
|
const hasBody = body && !['GET','HEAD','OPTIONS'].includes((method||'GET').toUpperCase())
|
|
82
94
|
if (hasBody) req.write(typeof body === 'string' ? body : JSON.stringify(body))
|
|
83
95
|
req.end()
|
|
@@ -87,15 +99,14 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
|
87
99
|
async function proxyRequest(targetUrl, method, reqHeaders, body) {
|
|
88
100
|
const parsed = url.parse(targetUrl)
|
|
89
101
|
const isHttps = parsed.protocol === 'https:'
|
|
90
|
-
|
|
91
102
|
const outHeaders = {}
|
|
92
103
|
for (const [k, v] of Object.entries(reqHeaders || {})) {
|
|
93
104
|
if (!HOP_BY_HOP.has(k.toLowerCase())) outHeaders[k] = v
|
|
94
105
|
}
|
|
95
|
-
|
|
96
106
|
const candidates = loopbackCandidates(parsed.hostname)
|
|
107
|
+
console.log(`\n [${new Date().toISOString()}] ${method} ${targetUrl}`)
|
|
108
|
+
console.log(` candidates: ${candidates.join(', ')}`)
|
|
97
109
|
let lastErr
|
|
98
|
-
|
|
99
110
|
for (const host of candidates) {
|
|
100
111
|
try {
|
|
101
112
|
return await proxyOnce(host, parsed.port, parsed.path, method, outHeaders, body, isHttps)
|
|
@@ -104,10 +115,9 @@ async function proxyRequest(targetUrl, method, reqHeaders, body) {
|
|
|
104
115
|
if (err.code !== 'ECONNREFUSED' && err.code !== 'EADDRNOTAVAIL') throw err
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
|
-
|
|
118
|
+
const port = parsed.port || (isHttps ? 443 : 80)
|
|
108
119
|
throw new Error(
|
|
109
|
-
`Connection refused on all addresses (${candidates.join(', ')}) port ${
|
|
110
|
-
`Is your server running?`
|
|
120
|
+
`Connection refused on all addresses (${candidates.join(', ')}) port ${port}. Is your server running on port ${port}?`
|
|
111
121
|
)
|
|
112
122
|
}
|
|
113
123
|
|
|
@@ -115,8 +125,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
115
125
|
const cors = corsHeaders()
|
|
116
126
|
const reqPath = url.parse(req.url).pathname
|
|
117
127
|
|
|
128
|
+
// Log ALL incoming requests so we can see preflights
|
|
129
|
+
console.log(` [${req.method}] ${reqPath} — origin: ${req.headers['origin'] || '(none)'} pna: ${req.headers['access-control-request-private-network'] || 'no'}`)
|
|
130
|
+
|
|
118
131
|
if (req.method === 'OPTIONS') {
|
|
119
|
-
|
|
132
|
+
console.log(` ← preflight OK`)
|
|
133
|
+
res.writeHead(204, cors)
|
|
134
|
+
res.end()
|
|
135
|
+
return
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
function send(status, body) {
|
|
@@ -134,19 +150,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
134
150
|
try {
|
|
135
151
|
payload = JSON.parse(await readBody(req))
|
|
136
152
|
} catch {
|
|
137
|
-
send(400, { error: 'Request body must be valid JSON' })
|
|
153
|
+
send(400, { error: 'Request body must be valid JSON' })
|
|
154
|
+
return
|
|
138
155
|
}
|
|
139
|
-
|
|
140
156
|
const { method, url: targetUrl, headers, body } = payload || {}
|
|
141
|
-
|
|
142
157
|
if (!targetUrl || typeof targetUrl !== 'string') { send(400, { error: 'url is required' }); return }
|
|
143
158
|
if (!method || typeof method !== 'string') { send(400, { error: 'method is required' }); return }
|
|
144
|
-
|
|
145
159
|
try {
|
|
146
160
|
const result = await proxyRequest(targetUrl, method, headers || {}, body)
|
|
147
161
|
send(200, { data: result })
|
|
148
162
|
} catch (err) {
|
|
149
163
|
const msg = err instanceof Error ? err.message : String(err)
|
|
164
|
+
console.log(` ✗ proxy failed: ${msg}`)
|
|
150
165
|
send(200, { data: { status: 0, statusText: 'Network Error', headers: {}, body: null, error: msg, responseTime: 0, size: 0 } })
|
|
151
166
|
}
|
|
152
167
|
return
|
|
@@ -168,13 +183,13 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
168
183
|
console.log(' ║ Ctrl+C to stop ║')
|
|
169
184
|
console.log(' ╚═══════════════════════════════════════╝')
|
|
170
185
|
console.log('')
|
|
171
|
-
console.log('
|
|
186
|
+
console.log(' All requests logged below.')
|
|
172
187
|
console.log('')
|
|
173
188
|
})
|
|
174
189
|
|
|
175
190
|
server.on('error', err => {
|
|
176
191
|
if (err.code === 'EADDRINUSE') {
|
|
177
|
-
console.error(`\n Port ${PORT}
|
|
192
|
+
console.error(`\n Port ${PORT} already in use. Run: kill $(lsof -ti:${PORT})`)
|
|
178
193
|
} else {
|
|
179
194
|
console.error('\n Agent failed to start:', err.message)
|
|
180
195
|
}
|