cruss-agent 1.0.7 → 1.0.8
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 +57 -42
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -6,17 +6,14 @@ const https = require('https')
|
|
|
6
6
|
const url = require('url')
|
|
7
7
|
|
|
8
8
|
const PORT = 9119
|
|
9
|
-
const VERSION = '1.0.
|
|
9
|
+
const VERSION = '1.0.8'
|
|
10
10
|
|
|
11
11
|
// ── CORS ─────────────────────────────────────────────────────────────────
|
|
12
|
-
// Access-Control-Allow-Private-Network
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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.
|
|
12
|
+
// Access-Control-Allow-Private-Network: Chrome 104+ PNA policy requires this
|
|
13
|
+
// on OPTIONS preflights from HTTPS pages to localhost. Without it Chrome
|
|
14
|
+
// silently blocks the request before it even leaves the browser — which is
|
|
15
|
+
// why health-check GETs work (simple requests bypass PNA) but POST /proxy
|
|
16
|
+
// never arrives.
|
|
20
17
|
function corsHeaders() {
|
|
21
18
|
return {
|
|
22
19
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -53,19 +50,15 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
|
53
50
|
const lib = isHttps ? https : http
|
|
54
51
|
const resolvedPort = port || (isHttps ? 443 : 80)
|
|
55
52
|
const options = {
|
|
56
|
-
hostname,
|
|
57
|
-
|
|
58
|
-
path: path || '/',
|
|
59
|
-
method: method || 'GET',
|
|
60
|
-
headers: outHeaders,
|
|
61
|
-
timeout: 30000,
|
|
53
|
+
hostname, port: resolvedPort, path: path || '/',
|
|
54
|
+
method: method || 'GET', headers: outHeaders, timeout: 30000,
|
|
62
55
|
}
|
|
63
56
|
console.log(` → trying ${hostname}:${resolvedPort}${path || '/'}`)
|
|
64
57
|
const t0 = Date.now()
|
|
65
58
|
const req = lib.request(options, res => {
|
|
66
59
|
const chunks = []
|
|
67
60
|
res.on('data', c => chunks.push(c))
|
|
68
|
-
res.on('end',
|
|
61
|
+
res.on('end', () => {
|
|
69
62
|
const elapsed = Date.now() - t0
|
|
70
63
|
const rawBody = Buffer.concat(chunks)
|
|
71
64
|
const bodyStr = rawBody.toString('utf8')
|
|
@@ -77,11 +70,8 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
|
77
70
|
}
|
|
78
71
|
}
|
|
79
72
|
console.log(` ✓ ${res.statusCode} ${res.statusMessage} (${elapsed}ms)`)
|
|
80
|
-
resolve({
|
|
81
|
-
|
|
82
|
-
headers: respHeaders, body: bodyStr,
|
|
83
|
-
responseTime: elapsed, size: rawBody.length,
|
|
84
|
-
})
|
|
73
|
+
resolve({ status: res.statusCode, statusText: res.statusMessage,
|
|
74
|
+
headers: respHeaders, body: bodyStr, responseTime: elapsed, size: rawBody.length })
|
|
85
75
|
})
|
|
86
76
|
res.on('error', reject)
|
|
87
77
|
})
|
|
@@ -121,15 +111,15 @@ async function proxyRequest(targetUrl, method, reqHeaders, body) {
|
|
|
121
111
|
)
|
|
122
112
|
}
|
|
123
113
|
|
|
124
|
-
|
|
114
|
+
// ── Request handler (shared by both IPv4 and IPv6 servers) ───────────────
|
|
115
|
+
async function requestHandler(req, res) {
|
|
125
116
|
const cors = corsHeaders()
|
|
126
117
|
const reqPath = url.parse(req.url).pathname
|
|
127
118
|
|
|
128
|
-
// Log ALL incoming requests so we can see preflights
|
|
129
119
|
console.log(` [${req.method}] ${reqPath} — origin: ${req.headers['origin'] || '(none)'} pna: ${req.headers['access-control-request-private-network'] || 'no'}`)
|
|
130
120
|
|
|
131
121
|
if (req.method === 'OPTIONS') {
|
|
132
|
-
console.log(` ← preflight
|
|
122
|
+
console.log(` ← preflight responded with PNA header`)
|
|
133
123
|
res.writeHead(204, cors)
|
|
134
124
|
res.end()
|
|
135
125
|
return
|
|
@@ -147,15 +137,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
147
137
|
|
|
148
138
|
if (req.method === 'POST' && reqPath === '/proxy') {
|
|
149
139
|
let payload
|
|
150
|
-
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
send(400, { error: 'Request body must be valid JSON' })
|
|
154
|
-
return
|
|
155
|
-
}
|
|
140
|
+
try { payload = JSON.parse(await readBody(req)) }
|
|
141
|
+
catch { send(400, { error: 'Request body must be valid JSON' }); return }
|
|
142
|
+
|
|
156
143
|
const { method, url: targetUrl, headers, body } = payload || {}
|
|
157
144
|
if (!targetUrl || typeof targetUrl !== 'string') { send(400, { error: 'url is required' }); return }
|
|
158
145
|
if (!method || typeof method !== 'string') { send(400, { error: 'method is required' }); return }
|
|
146
|
+
|
|
159
147
|
try {
|
|
160
148
|
const result = await proxyRequest(targetUrl, method, headers || {}, body)
|
|
161
149
|
send(200, { data: result })
|
|
@@ -168,9 +156,41 @@ const server = http.createServer(async (req, res) => {
|
|
|
168
156
|
}
|
|
169
157
|
|
|
170
158
|
send(404, { error: 'Not found' })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Two servers: one IPv4, one IPv6 ─────────────────────────────────────
|
|
162
|
+
// macOS resolves `localhost` to ::1 (IPv6) by default. Chrome's PNA
|
|
163
|
+
// preflight for POST requests goes to whichever address localhost resolves
|
|
164
|
+
// to. We must listen on BOTH stacks so the preflight always finds us,
|
|
165
|
+
// regardless of OS resolver behaviour.
|
|
166
|
+
const server4 = http.createServer(requestHandler)
|
|
167
|
+
const server6 = http.createServer(requestHandler)
|
|
168
|
+
|
|
169
|
+
function onError(label) {
|
|
170
|
+
return (err) => {
|
|
171
|
+
if (err.code === 'EADDRINUSE') {
|
|
172
|
+
console.error(`\n ${label} port ${PORT} already in use. Run: kill $(lsof -ti:${PORT})`)
|
|
173
|
+
} else if (err.code === 'EADDRNOTAVAIL') {
|
|
174
|
+
console.log(` ${label} not available on this system — skipping`)
|
|
175
|
+
} else {
|
|
176
|
+
console.error(`\n ${label} error:`, err.message)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
server4.on('error', onError('IPv4'))
|
|
182
|
+
server6.on('error', onError('IPv6'))
|
|
183
|
+
|
|
184
|
+
server4.listen(PORT, '127.0.0.1', () => {
|
|
185
|
+
console.log(` IPv4 listening on 127.0.0.1:${PORT}`)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
server6.listen(PORT, '::1', () => {
|
|
189
|
+
console.log(` IPv6 listening on [::1]:${PORT}`)
|
|
171
190
|
})
|
|
172
191
|
|
|
173
|
-
|
|
192
|
+
// Banner after brief delay to let both servers start
|
|
193
|
+
setTimeout(() => {
|
|
174
194
|
console.log('')
|
|
175
195
|
console.log(' ╔═══════════════════════════════════════╗')
|
|
176
196
|
console.log(' ║ Cruss Agent v' + VERSION + ' ║')
|
|
@@ -185,16 +205,11 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
185
205
|
console.log('')
|
|
186
206
|
console.log(' All requests logged below.')
|
|
187
207
|
console.log('')
|
|
188
|
-
})
|
|
208
|
+
}, 200)
|
|
189
209
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
console.error('\n Agent failed to start:', err.message)
|
|
195
|
-
}
|
|
196
|
-
process.exit(1)
|
|
210
|
+
process.on('SIGINT', () => {
|
|
211
|
+
server4.close(); server6.close()
|
|
212
|
+
console.log('\n Agent stopped.')
|
|
213
|
+
process.exit(0)
|
|
197
214
|
})
|
|
198
|
-
|
|
199
|
-
process.on('SIGINT', () => { console.log('\n Agent stopped.'); process.exit(0) })
|
|
200
|
-
process.on('SIGTERM', () => { process.exit(0) })
|
|
215
|
+
process.on('SIGTERM', () => { server4.close(); server6.close(); process.exit(0) })
|