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 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 fail = fails[i];
2369
- if (isBoolean(fail)) {
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
- fail(msg, err, self);
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 command(cmd, description, isDefault, aliases, deprecated = false) {
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 FBIPROXY_PORT = String(await getPorts({ port: 2432 }));
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
- FBIPROXY_PORT
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: ${FBIPROXY_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.7.0",
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.0",
47
- "from-node-stream": "^0.1.0",
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.24.5",
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.0",
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.1.2",
62
- "prettier": "^3.6.2",
63
- "semantic-release": "^22.0.0",
64
- "semantic-release-cargo": "^2.4.0"
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": ">=18.0.0"
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 tokio::net::TcpListener;
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
- match self.client.request(new_req).await {
208
- Ok(response) => {
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
- // Upgrade the HTTP connection to WebSocket
254
- let (response, websocket) = hyper_tungstenite::upgrade(req, None)?;
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 connection failed")).map_err(|e| match e {}).boxed())?);
404
+ .body(Full::new(Bytes::from("WebSocket upstream unavailable")).map_err(|e| match e {}).boxed())?);
264
405
  }
265
406
  };
266
407
 
267
- // Spawn task to handle WebSocket forwarding
268
- // let ws_url_clone = ws_url.clone();
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 FBIPROXY_PORT = String(await getPort({ port: 2432 }));
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
- FBIPROXY_PORT, // Rust proxy server port
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: ${FBIPROXY_PORT}`);
46
+ console.log(`Proxy server running on port: ${FBI_PROXY_PORT}`);
46
47
 
47
48
  const exit = () => {
48
49
  console.log("Shutting down...");