fbi-proxy 1.7.0 → 1.8.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/dist/cli.js +9 -7
- package/package.json +30 -12
- package/release/fbi-proxy-linux-arm64 +0 -0
- package/release/fbi-proxy-linux-x64 +0 -0
- package/release/fbi-proxy-macos-arm64 +0 -0
- package/release/fbi-proxy-macos-x64 +0 -0
- package/release/fbi-proxy-windows-arm64.exe +0 -0
- package/release/fbi-proxy-windows-x64.exe +0 -0
- package/rs/fbi-proxy.rs +159 -15
- package/ts/cli.ts +4 -3
package/dist/cli.js
CHANGED
|
@@ -2365,14 +2365,14 @@ function usage(yargs, shim2) {
|
|
|
2365
2365
|
const logger = yargs.getInternalMethods().getLoggerInstance();
|
|
2366
2366
|
if (fails.length) {
|
|
2367
2367
|
for (let i = fails.length - 1;i >= 0; --i) {
|
|
2368
|
-
const
|
|
2369
|
-
if (isBoolean(
|
|
2368
|
+
const fail2 = fails[i];
|
|
2369
|
+
if (isBoolean(fail2)) {
|
|
2370
2370
|
if (err)
|
|
2371
2371
|
throw err;
|
|
2372
2372
|
else if (msg)
|
|
2373
2373
|
throw Error(msg);
|
|
2374
2374
|
} else {
|
|
2375
|
-
|
|
2375
|
+
fail2(msg, err, self);
|
|
2376
2376
|
}
|
|
2377
2377
|
}
|
|
2378
2378
|
} else {
|
|
@@ -2429,7 +2429,7 @@ function usage(yargs, shim2) {
|
|
|
2429
2429
|
examples.push([cmd, description || ""]);
|
|
2430
2430
|
};
|
|
2431
2431
|
let commands = [];
|
|
2432
|
-
self.command = function
|
|
2432
|
+
self.command = function command2(cmd, description, isDefault, aliases, deprecated = false) {
|
|
2433
2433
|
if (isDefault) {
|
|
2434
2434
|
commands = commands.map((cmdArray) => {
|
|
2435
2435
|
cmdArray[2] = false;
|
|
@@ -5009,6 +5009,8 @@ import { spawn } from "child_process";
|
|
|
5009
5009
|
|
|
5010
5010
|
// node_modules/from-node-stream/dist/index.js
|
|
5011
5011
|
function fromReadable(i) {
|
|
5012
|
+
if (i instanceof ReadableStream)
|
|
5013
|
+
return i;
|
|
5012
5014
|
return new ReadableStream({
|
|
5013
5015
|
start: (c) => {
|
|
5014
5016
|
i.on("data", (data) => c.enqueue(data));
|
|
@@ -5232,14 +5234,14 @@ await yargs_default(hideBin(process.argv)).option("dev", {
|
|
|
5232
5234
|
description: "Run in development mode"
|
|
5233
5235
|
}).help().argv;
|
|
5234
5236
|
console.log("Preparing Binaries");
|
|
5235
|
-
var
|
|
5237
|
+
var FBI_PROXY_PORT = process.env.FBI_PROXY_PORT || String(await getPorts({ port: 2432 }));
|
|
5236
5238
|
var proxyProcess = await hotMemo(async () => {
|
|
5237
5239
|
const proxy = await getFbiProxyBinary();
|
|
5238
5240
|
console.log("Starting Rust proxy server");
|
|
5239
5241
|
const p = $2.opt({
|
|
5240
5242
|
env: {
|
|
5241
5243
|
...process.env,
|
|
5242
|
-
|
|
5244
|
+
FBI_PROXY_PORT
|
|
5243
5245
|
}
|
|
5244
5246
|
})`${proxy}`.process;
|
|
5245
5247
|
p.on("exit", (code) => {
|
|
@@ -5250,7 +5252,7 @@ var proxyProcess = await hotMemo(async () => {
|
|
|
5250
5252
|
});
|
|
5251
5253
|
console.log("All services started successfully!");
|
|
5252
5254
|
console.log(`Proxy server PID: ${proxyProcess.pid}`);
|
|
5253
|
-
console.log(`Proxy server running on port: ${
|
|
5255
|
+
console.log(`Proxy server running on port: ${FBI_PROXY_PORT}`);
|
|
5254
5256
|
var exit = () => {
|
|
5255
5257
|
console.log("Shutting down...");
|
|
5256
5258
|
proxyProcess?.kill?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fbi-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "FBI-Proxy provides easy HTTPS access to your local services with intelligent domain routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"development-tools",
|
|
@@ -40,31 +40,46 @@
|
|
|
40
40
|
"lint": "prettier --check .",
|
|
41
41
|
"prepare": "husky",
|
|
42
42
|
"start": "bun ts/cli.ts",
|
|
43
|
-
"start:rs": "./target/release/fbi-proxy"
|
|
43
|
+
"start:rs": "./target/release/fbi-proxy",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"test:ui": "vitest --ui",
|
|
47
|
+
"test:e2e": "vitest run e2e",
|
|
48
|
+
"test:e2e:watch": "vitest e2e",
|
|
49
|
+
"test:coverage": "vitest run --coverage"
|
|
44
50
|
},
|
|
45
51
|
"dependencies": {
|
|
46
|
-
"execa": "^9.6.
|
|
47
|
-
"from-node-stream": "^0.1.
|
|
52
|
+
"execa": "^9.6.1",
|
|
53
|
+
"from-node-stream": "^0.1.2",
|
|
48
54
|
"get-port": "^7.1.0",
|
|
49
55
|
"hot-memo": "^1.1.1",
|
|
50
|
-
"sflow": "^1.
|
|
56
|
+
"sflow": "^1.27.0",
|
|
51
57
|
"tsa-composer": "^1.0.0",
|
|
52
58
|
"yargs": "^17.7.2"
|
|
53
59
|
},
|
|
54
60
|
"devDependencies": {
|
|
61
|
+
"@playwright/test": "^1.58.2",
|
|
55
62
|
"@semantic-release/changelog": "^6.0.3",
|
|
56
63
|
"@semantic-release/git": "^10.0.1",
|
|
57
|
-
"@semantic-release/github": "^9.2.
|
|
64
|
+
"@semantic-release/github": "^9.2.6",
|
|
65
|
+
"@semantic-release/npm": "^13.1.3",
|
|
58
66
|
"@types/bun": "latest",
|
|
59
67
|
"@types/minimist": "^1.2.5",
|
|
68
|
+
"@types/ws": "^8.18.1",
|
|
69
|
+
"@vitest/ui": "^3.2.4",
|
|
60
70
|
"husky": "^9.1.7",
|
|
61
|
-
"lint-staged": "^16.
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
71
|
+
"lint-staged": "^16.2.7",
|
|
72
|
+
"node-fetch": "^3.3.2",
|
|
73
|
+
"playwright": "^1.58.2",
|
|
74
|
+
"prettier": "^3.8.1",
|
|
75
|
+
"semantic-release": "^24.0.0",
|
|
76
|
+
"semantic-release-cargo": "^2.4.2",
|
|
77
|
+
"vite": "^7.3.1",
|
|
78
|
+
"vitest": "^3.2.4",
|
|
79
|
+
"ws": "^8.19.0"
|
|
65
80
|
},
|
|
66
81
|
"engines": {
|
|
67
|
-
"node": ">=
|
|
82
|
+
"node": ">=22.14.0"
|
|
68
83
|
},
|
|
69
84
|
"lint-staged": {
|
|
70
85
|
"*.{ts,js}": [
|
|
@@ -74,5 +89,8 @@
|
|
|
74
89
|
"bun --bun prettier --write"
|
|
75
90
|
]
|
|
76
91
|
},
|
|
77
|
-
"trustedDependencies": []
|
|
92
|
+
"trustedDependencies": [],
|
|
93
|
+
"overrides": {
|
|
94
|
+
"@semantic-release/npm": "^13.1.3"
|
|
95
|
+
}
|
|
78
96
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/rs/fbi-proxy.rs
CHANGED
|
@@ -5,7 +5,7 @@ use hyper::body::{Bytes, Incoming};
|
|
|
5
5
|
use hyper::header::{HeaderValue, HOST};
|
|
6
6
|
use hyper::server::conn::http1;
|
|
7
7
|
use hyper::service::service_fn;
|
|
8
|
-
use hyper::{Request, Response, StatusCode, Uri};
|
|
8
|
+
use hyper::{Method, Request, Response, StatusCode, Uri};
|
|
9
9
|
use hyper_tungstenite::{HyperWebsocket, WebSocketStream};
|
|
10
10
|
use hyper_util::client::legacy::{Client, connect::HttpConnector};
|
|
11
11
|
use hyper_util::rt::TokioIo;
|
|
@@ -14,7 +14,10 @@ use regex::Regex;
|
|
|
14
14
|
use std::convert::Infallible;
|
|
15
15
|
use std::net::SocketAddr;
|
|
16
16
|
use std::sync::Arc;
|
|
17
|
-
use
|
|
17
|
+
use std::time::Duration;
|
|
18
|
+
use tokio::net::{TcpListener, TcpStream};
|
|
19
|
+
use tokio::time::timeout;
|
|
20
|
+
use tokio::io::copy_bidirectional;
|
|
18
21
|
use tokio_tungstenite::connect_async;
|
|
19
22
|
|
|
20
23
|
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
|
@@ -58,7 +61,10 @@ for subdomains
|
|
|
58
61
|
*/
|
|
59
62
|
impl FBIProxy {
|
|
60
63
|
pub fn new(domain_filter: Option<String>) -> Self {
|
|
61
|
-
let connector = HttpConnector::new();
|
|
64
|
+
let mut connector = HttpConnector::new();
|
|
65
|
+
// Set connection timeout to 5 seconds to avoid hanging on invalid hosts
|
|
66
|
+
connector.set_connect_timeout(Some(Duration::from_secs(3)));
|
|
67
|
+
|
|
62
68
|
let client = Client::builder(hyper_util::rt::TokioExecutor::new())
|
|
63
69
|
.build(connector);
|
|
64
70
|
|
|
@@ -175,6 +181,126 @@ impl FBIProxy {
|
|
|
175
181
|
let method = req.method().clone();
|
|
176
182
|
let original_uri = req.uri().clone();
|
|
177
183
|
|
|
184
|
+
// Handle HTTP CONNECT tunneling (used by browsers for WebSocket/HTTPS through proxy)
|
|
185
|
+
if method == Method::CONNECT {
|
|
186
|
+
// For CONNECT, the URI contains the target authority (host:port)
|
|
187
|
+
// Parse the target from the URI
|
|
188
|
+
let connect_target = if let Some(authority) = req.uri().authority() {
|
|
189
|
+
authority.to_string()
|
|
190
|
+
} else {
|
|
191
|
+
// Fallback to parsed host
|
|
192
|
+
target_host.clone()
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Apply domain filtering to CONNECT target
|
|
196
|
+
let connect_host = connect_target.split(':').next().unwrap_or(&connect_target);
|
|
197
|
+
if let Some(ref domain) = self.domain_filter {
|
|
198
|
+
if !domain.is_empty() && !connect_host.ends_with(domain) {
|
|
199
|
+
info!(
|
|
200
|
+
"CONNECT {} => REJECTED{} 502",
|
|
201
|
+
host_header,
|
|
202
|
+
original_uri
|
|
203
|
+
);
|
|
204
|
+
return Ok(Response::builder()
|
|
205
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
206
|
+
.body(Full::new(Bytes::from("Bad Gateway: Host not allowed")).map_err(|e| match e {}).boxed())?);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Parse the connect target for routing
|
|
211
|
+
let tunnel_target = if let Some(ref domain) = self.domain_filter {
|
|
212
|
+
if !domain.is_empty() && connect_host.ends_with(domain) {
|
|
213
|
+
// Strip domain and apply routing rules
|
|
214
|
+
let prefix_len = connect_host.len() - domain.len();
|
|
215
|
+
let stripped = if prefix_len > 0 && connect_host.chars().nth(prefix_len - 1) == Some('.') {
|
|
216
|
+
&connect_host[..prefix_len - 1]
|
|
217
|
+
} else if prefix_len == 0 {
|
|
218
|
+
"localhost"
|
|
219
|
+
} else {
|
|
220
|
+
connect_host
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Apply number rule: if stripped is numeric, route to localhost:port
|
|
224
|
+
if self.number_regex.is_match(stripped) {
|
|
225
|
+
// Get the port from the connect target
|
|
226
|
+
let _port = connect_target.split(':').nth(1).unwrap_or("80");
|
|
227
|
+
format!("localhost:{}", stripped)
|
|
228
|
+
} else {
|
|
229
|
+
connect_target.clone()
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
connect_target.clone()
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
connect_target.clone()
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
info!(
|
|
239
|
+
"CONNECT {}@{}{} tunneling",
|
|
240
|
+
host_header,
|
|
241
|
+
tunnel_target,
|
|
242
|
+
original_uri
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Connect to upstream with timeout
|
|
246
|
+
let connect_result = timeout(
|
|
247
|
+
Duration::from_secs(3),
|
|
248
|
+
TcpStream::connect(&tunnel_target)
|
|
249
|
+
).await;
|
|
250
|
+
|
|
251
|
+
match connect_result {
|
|
252
|
+
Ok(Ok(upstream)) => {
|
|
253
|
+
// Spawn a task to handle the tunnel
|
|
254
|
+
tokio::spawn(async move {
|
|
255
|
+
// The upgrade happens after we return the response
|
|
256
|
+
// We need to use hyper's upgrade mechanism
|
|
257
|
+
match hyper::upgrade::on(req).await {
|
|
258
|
+
Ok(upgraded) => {
|
|
259
|
+
let mut upgraded = TokioIo::new(upgraded);
|
|
260
|
+
let mut upstream = upstream;
|
|
261
|
+
|
|
262
|
+
// Bidirectional copy
|
|
263
|
+
if let Err(e) = copy_bidirectional(&mut upgraded, &mut upstream).await {
|
|
264
|
+
error!("Tunnel error: {}", e);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
Err(e) => {
|
|
268
|
+
error!("Upgrade error: {}", e);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Return 200 Connection Established
|
|
274
|
+
return Ok(Response::builder()
|
|
275
|
+
.status(StatusCode::OK)
|
|
276
|
+
.body(Full::new(Bytes::new()).map_err(|e| match e {}).boxed())?);
|
|
277
|
+
}
|
|
278
|
+
Ok(Err(e)) => {
|
|
279
|
+
error!(
|
|
280
|
+
"CONNECT {}@{}{} 502 ({})",
|
|
281
|
+
host_header,
|
|
282
|
+
tunnel_target,
|
|
283
|
+
original_uri,
|
|
284
|
+
e
|
|
285
|
+
);
|
|
286
|
+
return Ok(Response::builder()
|
|
287
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
288
|
+
.body(Full::new(Bytes::from("FBIPROXY CONNECT ERROR")).map_err(|e| match e {}).boxed())?);
|
|
289
|
+
}
|
|
290
|
+
Err(_) => {
|
|
291
|
+
error!(
|
|
292
|
+
"CONNECT {}@{}{} 502 (connection timeout)",
|
|
293
|
+
host_header,
|
|
294
|
+
tunnel_target,
|
|
295
|
+
original_uri
|
|
296
|
+
);
|
|
297
|
+
return Ok(Response::builder()
|
|
298
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
299
|
+
.body(Full::new(Bytes::from("FBIPROXY CONNECT TIMEOUT")).map_err(|e| match e {}).boxed())?);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
178
304
|
// Handle WebSocket upgrade requests
|
|
179
305
|
if hyper_tungstenite::is_upgrade_request(&req) {
|
|
180
306
|
return self
|
|
@@ -203,9 +329,14 @@ impl FBIProxy {
|
|
|
203
329
|
// Rebuild the request with the converted body
|
|
204
330
|
let new_req = Request::from_parts(parts, body);
|
|
205
331
|
|
|
206
|
-
// Forward the request
|
|
207
|
-
|
|
208
|
-
|
|
332
|
+
// Forward the request with timeout
|
|
333
|
+
let request_result = timeout(
|
|
334
|
+
Duration::from_secs(3),
|
|
335
|
+
self.client.request(new_req)
|
|
336
|
+
).await;
|
|
337
|
+
|
|
338
|
+
match request_result {
|
|
339
|
+
Ok(Ok(response)) => {
|
|
209
340
|
// Preserve content-encoding header in response to maintain compression
|
|
210
341
|
let status = response.status();
|
|
211
342
|
info!(
|
|
@@ -221,7 +352,7 @@ impl FBIProxy {
|
|
|
221
352
|
let boxed_body = body.map_err(|e| e).boxed();
|
|
222
353
|
Ok(Response::from_parts(parts, boxed_body))
|
|
223
354
|
}
|
|
224
|
-
Err(e) => {
|
|
355
|
+
Ok(Err(e)) => {
|
|
225
356
|
error!(
|
|
226
357
|
"{} {}@{}{} 502 ({})",
|
|
227
358
|
method,
|
|
@@ -234,6 +365,18 @@ impl FBIProxy {
|
|
|
234
365
|
.status(StatusCode::BAD_GATEWAY)
|
|
235
366
|
.body(Full::new(Bytes::from("FBIPROXY ERROR")).map_err(|e| match e {}).boxed())?)
|
|
236
367
|
}
|
|
368
|
+
Err(_) => {
|
|
369
|
+
error!(
|
|
370
|
+
"{} {}@{}{} 502 (request timeout)",
|
|
371
|
+
method,
|
|
372
|
+
host_header,
|
|
373
|
+
target_host,
|
|
374
|
+
original_uri
|
|
375
|
+
);
|
|
376
|
+
Ok(Response::builder()
|
|
377
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
378
|
+
.body(Full::new(Bytes::from("FBIPROXY TIMEOUT")).map_err(|e| match e {}).boxed())?)
|
|
379
|
+
}
|
|
237
380
|
}
|
|
238
381
|
}
|
|
239
382
|
|
|
@@ -250,22 +393,23 @@ impl FBIProxy {
|
|
|
250
393
|
uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
|
|
251
394
|
);
|
|
252
395
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// Connect to upstream WebSocket
|
|
396
|
+
// Step 1: Connect to upstream WebSocket FIRST before upgrading client
|
|
397
|
+
// This ensures we can return proper errors if upstream is unavailable
|
|
257
398
|
let (upstream_ws, _) = match connect_async(&ws_url).await {
|
|
258
399
|
Ok(ws) => ws,
|
|
259
400
|
Err(e) => {
|
|
260
|
-
error!("WS :ws:{} => :ws:{}{} 502 ({})", target_host, target_host, uri, e);
|
|
401
|
+
error!("WS :ws:{} => :ws:{}{} 502 (upstream connection failed: {})", target_host, target_host, uri, e);
|
|
261
402
|
return Ok(Response::builder()
|
|
262
403
|
.status(StatusCode::BAD_GATEWAY)
|
|
263
|
-
.body(Full::new(Bytes::from("WebSocket
|
|
404
|
+
.body(Full::new(Bytes::from("WebSocket upstream unavailable")).map_err(|e| match e {}).boxed())?);
|
|
264
405
|
}
|
|
265
406
|
};
|
|
266
407
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
408
|
+
// Step 2: Now upgrade the HTTP connection to WebSocket
|
|
409
|
+
// Only do this after confirming upstream is available
|
|
410
|
+
let (response, websocket) = hyper_tungstenite::upgrade(req, None)?;
|
|
411
|
+
|
|
412
|
+
// Step 3: Spawn task to handle WebSocket forwarding
|
|
269
413
|
tokio::spawn(async move {
|
|
270
414
|
if let Err(e) = handle_websocket_forwarding(websocket, upstream_ws).await {
|
|
271
415
|
error!("WebSocket forwarding error: {}", e);
|
package/ts/cli.ts
CHANGED
|
@@ -21,7 +21,8 @@ await yargs(hideBin(process.argv))
|
|
|
21
21
|
|
|
22
22
|
console.log("Preparing Binaries");
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const FBI_PROXY_PORT =
|
|
25
|
+
process.env.FBI_PROXY_PORT || String(await getPort({ port: 2432 }));
|
|
25
26
|
|
|
26
27
|
const proxyProcess = await hotMemo(async () => {
|
|
27
28
|
const proxy = await getFbiProxyBinary();
|
|
@@ -29,7 +30,7 @@ const proxyProcess = await hotMemo(async () => {
|
|
|
29
30
|
const p = $.opt({
|
|
30
31
|
env: {
|
|
31
32
|
...process.env,
|
|
32
|
-
|
|
33
|
+
FBI_PROXY_PORT, // Rust proxy server port
|
|
33
34
|
},
|
|
34
35
|
})`${proxy}`.process;
|
|
35
36
|
|
|
@@ -42,7 +43,7 @@ const proxyProcess = await hotMemo(async () => {
|
|
|
42
43
|
|
|
43
44
|
console.log("All services started successfully!");
|
|
44
45
|
console.log(`Proxy server PID: ${proxyProcess.pid}`);
|
|
45
|
-
console.log(`Proxy server running on port: ${
|
|
46
|
+
console.log(`Proxy server running on port: ${FBI_PROXY_PORT}`);
|
|
46
47
|
|
|
47
48
|
const exit = () => {
|
|
48
49
|
console.log("Shutting down...");
|