cruss-agent 1.0.6 → 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 +29 -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
|
|
|
@@ -50,9 +60,7 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
|
50
60
|
headers: outHeaders,
|
|
51
61
|
timeout: 30000,
|
|
52
62
|
}
|
|
53
|
-
|
|
54
63
|
console.log(` → trying ${hostname}:${resolvedPort}${path || '/'}`)
|
|
55
|
-
|
|
56
64
|
const t0 = Date.now()
|
|
57
65
|
const req = lib.request(options, res => {
|
|
58
66
|
const chunks = []
|
|
@@ -64,7 +72,7 @@ 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
|
}
|
|
@@ -91,33 +99,25 @@ function proxyOnce(hostname, port, path, method, outHeaders, body, isHttps) {
|
|
|
91
99
|
async function proxyRequest(targetUrl, method, reqHeaders, body) {
|
|
92
100
|
const parsed = url.parse(targetUrl)
|
|
93
101
|
const isHttps = parsed.protocol === 'https:'
|
|
94
|
-
|
|
95
102
|
const outHeaders = {}
|
|
96
103
|
for (const [k, v] of Object.entries(reqHeaders || {})) {
|
|
97
104
|
if (!HOP_BY_HOP.has(k.toLowerCase())) outHeaders[k] = v
|
|
98
105
|
}
|
|
99
|
-
|
|
100
106
|
const candidates = loopbackCandidates(parsed.hostname)
|
|
101
107
|
console.log(`\n [${new Date().toISOString()}] ${method} ${targetUrl}`)
|
|
102
108
|
console.log(` candidates: ${candidates.join(', ')}`)
|
|
103
|
-
|
|
104
109
|
let lastErr
|
|
105
110
|
for (const host of candidates) {
|
|
106
111
|
try {
|
|
107
112
|
return await proxyOnce(host, parsed.port, parsed.path, method, outHeaders, body, isHttps)
|
|
108
113
|
} catch (err) {
|
|
109
114
|
lastErr = err
|
|
110
|
-
if (err.code !== 'ECONNREFUSED' && err.code !== 'EADDRNOTAVAIL')
|
|
111
|
-
console.log(` ✗ fatal error: ${err.message}`)
|
|
112
|
-
throw err
|
|
113
|
-
}
|
|
115
|
+
if (err.code !== 'ECONNREFUSED' && err.code !== 'EADDRNOTAVAIL') throw err
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
|
-
|
|
117
118
|
const port = parsed.port || (isHttps ? 443 : 80)
|
|
118
119
|
throw new Error(
|
|
119
|
-
`Connection refused on all addresses (${candidates.join(', ')}) port ${port}.
|
|
120
|
-
`Is your server running on port ${port}?`
|
|
120
|
+
`Connection refused on all addresses (${candidates.join(', ')}) port ${port}. Is your server running on port ${port}?`
|
|
121
121
|
)
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -125,8 +125,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
125
125
|
const cors = corsHeaders()
|
|
126
126
|
const reqPath = url.parse(req.url).pathname
|
|
127
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
|
+
|
|
128
131
|
if (req.method === 'OPTIONS') {
|
|
129
|
-
|
|
132
|
+
console.log(` ← preflight OK`)
|
|
133
|
+
res.writeHead(204, cors)
|
|
134
|
+
res.end()
|
|
135
|
+
return
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
function send(status, body) {
|
|
@@ -135,7 +141,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
if (req.method === 'GET' && reqPath === '/health') {
|
|
138
|
-
console.log(` [health check] OK`)
|
|
139
144
|
send(200, { ok: true, version: VERSION, agent: 'cruss-agent', port: PORT })
|
|
140
145
|
return
|
|
141
146
|
}
|
|
@@ -145,14 +150,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
145
150
|
try {
|
|
146
151
|
payload = JSON.parse(await readBody(req))
|
|
147
152
|
} catch {
|
|
148
|
-
send(400, { error: 'Request body must be valid JSON' })
|
|
153
|
+
send(400, { error: 'Request body must be valid JSON' })
|
|
154
|
+
return
|
|
149
155
|
}
|
|
150
|
-
|
|
151
156
|
const { method, url: targetUrl, headers, body } = payload || {}
|
|
152
|
-
|
|
153
157
|
if (!targetUrl || typeof targetUrl !== 'string') { send(400, { error: 'url is required' }); return }
|
|
154
158
|
if (!method || typeof method !== 'string') { send(400, { error: 'method is required' }); return }
|
|
155
|
-
|
|
156
159
|
try {
|
|
157
160
|
const result = await proxyRequest(targetUrl, method, headers || {}, body)
|
|
158
161
|
send(200, { data: result })
|
|
@@ -180,7 +183,7 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
180
183
|
console.log(' ║ Ctrl+C to stop ║')
|
|
181
184
|
console.log(' ╚═══════════════════════════════════════╝')
|
|
182
185
|
console.log('')
|
|
183
|
-
console.log('
|
|
186
|
+
console.log(' All requests logged below.')
|
|
184
187
|
console.log('')
|
|
185
188
|
})
|
|
186
189
|
|