fbi-proxy 1.6.2 → 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 +217 -49
- 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
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
use clap::{Arg, Command};
|
|
2
2
|
use futures_util::{SinkExt, StreamExt};
|
|
3
|
+
use http_body_util::{BodyExt, Full};
|
|
4
|
+
use hyper::body::{Bytes, Incoming};
|
|
3
5
|
use hyper::header::{HeaderValue, HOST};
|
|
4
|
-
use hyper::
|
|
5
|
-
use hyper::
|
|
6
|
+
use hyper::server::conn::http1;
|
|
7
|
+
use hyper::service::service_fn;
|
|
8
|
+
use hyper::{Method, Request, Response, StatusCode, Uri};
|
|
6
9
|
use hyper_tungstenite::{HyperWebsocket, WebSocketStream};
|
|
10
|
+
use hyper_util::client::legacy::{Client, connect::HttpConnector};
|
|
11
|
+
use hyper_util::rt::TokioIo;
|
|
7
12
|
use log::{error, info};
|
|
8
13
|
use regex::Regex;
|
|
9
14
|
use std::convert::Infallible;
|
|
10
15
|
use std::net::SocketAddr;
|
|
11
16
|
use std::sync::Arc;
|
|
17
|
+
use std::time::Duration;
|
|
18
|
+
use tokio::net::{TcpListener, TcpStream};
|
|
19
|
+
use tokio::time::timeout;
|
|
20
|
+
use tokio::io::copy_bidirectional;
|
|
12
21
|
use tokio_tungstenite::connect_async;
|
|
13
22
|
|
|
14
23
|
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
|
24
|
+
type BoxBody = http_body_util::combinators::BoxBody<Bytes, hyper::Error>;
|
|
15
25
|
|
|
16
26
|
pub struct FBIProxy {
|
|
17
|
-
client: Client<
|
|
27
|
+
client: Client<HttpConnector, BoxBody>,
|
|
18
28
|
number_regex: Regex,
|
|
19
29
|
domain_filter: Option<String>,
|
|
20
30
|
}
|
|
@@ -51,8 +61,15 @@ for subdomains
|
|
|
51
61
|
*/
|
|
52
62
|
impl FBIProxy {
|
|
53
63
|
pub fn new(domain_filter: Option<String>) -> Self {
|
|
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
|
+
|
|
68
|
+
let client = Client::builder(hyper_util::rt::TokioExecutor::new())
|
|
69
|
+
.build(connector);
|
|
70
|
+
|
|
54
71
|
Self {
|
|
55
|
-
client
|
|
72
|
+
client,
|
|
56
73
|
number_regex: Regex::new(r"^\d+$").unwrap(),
|
|
57
74
|
domain_filter,
|
|
58
75
|
}
|
|
@@ -131,7 +148,7 @@ impl FBIProxy {
|
|
|
131
148
|
Some((format!("{}:80", host), host.to_string()))
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
pub async fn handle_request(&self,
|
|
151
|
+
pub async fn handle_request(&self, req: Request<Incoming>) -> Result<Response<BoxBody>, BoxError> {
|
|
135
152
|
// Extract host from headers and process according to rules
|
|
136
153
|
let host_header = req
|
|
137
154
|
.headers()
|
|
@@ -157,13 +174,133 @@ impl FBIProxy {
|
|
|
157
174
|
);
|
|
158
175
|
return Ok(Response::builder()
|
|
159
176
|
.status(StatusCode::BAD_GATEWAY)
|
|
160
|
-
.body(
|
|
177
|
+
.body(Full::new(Bytes::from("Bad Gateway: Host not allowed")).map_err(|e| match e {}).boxed())?);
|
|
161
178
|
}
|
|
162
179
|
};
|
|
163
180
|
|
|
164
181
|
let method = req.method().clone();
|
|
165
182
|
let original_uri = req.uri().clone();
|
|
166
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
|
+
|
|
167
304
|
// Handle WebSocket upgrade requests
|
|
168
305
|
if hyper_tungstenite::is_upgrade_request(&req) {
|
|
169
306
|
return self
|
|
@@ -180,15 +317,26 @@ impl FBIProxy {
|
|
|
180
317
|
);
|
|
181
318
|
let target_uri: Uri = target_url.parse()?;
|
|
182
319
|
|
|
320
|
+
// Convert incoming body to a format the client can use
|
|
321
|
+
let (mut parts, incoming_body) = req.into_parts();
|
|
322
|
+
let body = incoming_body.map_err(|e| e).boxed();
|
|
323
|
+
|
|
183
324
|
// Update request URI and headers
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.insert(HOST, HeaderValue::from_str(&new_host)?);
|
|
325
|
+
parts.uri = target_uri;
|
|
326
|
+
parts.headers.insert(HOST, HeaderValue::from_str(&new_host)?);
|
|
187
327
|
// Preserve content-encoding header to maintain compression
|
|
188
328
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
329
|
+
// Rebuild the request with the converted body
|
|
330
|
+
let new_req = Request::from_parts(parts, body);
|
|
331
|
+
|
|
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)) => {
|
|
192
340
|
// Preserve content-encoding header in response to maintain compression
|
|
193
341
|
let status = response.status();
|
|
194
342
|
info!(
|
|
@@ -199,9 +347,12 @@ impl FBIProxy {
|
|
|
199
347
|
original_uri,
|
|
200
348
|
status.as_u16()
|
|
201
349
|
);
|
|
202
|
-
|
|
350
|
+
// Convert the response body back to BoxBody
|
|
351
|
+
let (parts, body) = response.into_parts();
|
|
352
|
+
let boxed_body = body.map_err(|e| e).boxed();
|
|
353
|
+
Ok(Response::from_parts(parts, boxed_body))
|
|
203
354
|
}
|
|
204
|
-
Err(e) => {
|
|
355
|
+
Ok(Err(e)) => {
|
|
205
356
|
error!(
|
|
206
357
|
"{} {}@{}{} 502 ({})",
|
|
207
358
|
method,
|
|
@@ -212,17 +363,29 @@ impl FBIProxy {
|
|
|
212
363
|
);
|
|
213
364
|
Ok(Response::builder()
|
|
214
365
|
.status(StatusCode::BAD_GATEWAY)
|
|
215
|
-
.body(
|
|
366
|
+
.body(Full::new(Bytes::from("FBIPROXY ERROR")).map_err(|e| match e {}).boxed())?)
|
|
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())?)
|
|
216
379
|
}
|
|
217
380
|
}
|
|
218
381
|
}
|
|
219
382
|
|
|
220
383
|
async fn handle_websocket_upgrade(
|
|
221
384
|
&self,
|
|
222
|
-
req: Request<
|
|
385
|
+
req: Request<Incoming>,
|
|
223
386
|
target_host: &str,
|
|
224
387
|
_new_host: &str, // Currently not used for WebSocket connections, but kept for consistency
|
|
225
|
-
) -> Result<Response<
|
|
388
|
+
) -> Result<Response<BoxBody>, BoxError> {
|
|
226
389
|
let uri = req.uri().clone();
|
|
227
390
|
let ws_url = format!(
|
|
228
391
|
"ws://{}{}",
|
|
@@ -230,22 +393,23 @@ impl FBIProxy {
|
|
|
230
393
|
uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
|
|
231
394
|
);
|
|
232
395
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// 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
|
|
237
398
|
let (upstream_ws, _) = match connect_async(&ws_url).await {
|
|
238
399
|
Ok(ws) => ws,
|
|
239
400
|
Err(e) => {
|
|
240
|
-
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);
|
|
241
402
|
return Ok(Response::builder()
|
|
242
403
|
.status(StatusCode::BAD_GATEWAY)
|
|
243
|
-
.body(
|
|
404
|
+
.body(Full::new(Bytes::from("WebSocket upstream unavailable")).map_err(|e| match e {}).boxed())?);
|
|
244
405
|
}
|
|
245
406
|
};
|
|
246
407
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
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
|
|
249
413
|
tokio::spawn(async move {
|
|
250
414
|
if let Err(e) = handle_websocket_forwarding(websocket, upstream_ws).await {
|
|
251
415
|
error!("WebSocket forwarding error: {}", e);
|
|
@@ -253,7 +417,9 @@ impl FBIProxy {
|
|
|
253
417
|
});
|
|
254
418
|
|
|
255
419
|
info!("WS :ws:{} => :ws:{}{} 101", target_host, target_host, uri);
|
|
256
|
-
|
|
420
|
+
let (parts, body) = response.into_parts();
|
|
421
|
+
let boxed_body = body.map_err(|_: std::convert::Infallible| unreachable!()).boxed();
|
|
422
|
+
Ok(Response::from_parts(parts, boxed_body))
|
|
257
423
|
}
|
|
258
424
|
}
|
|
259
425
|
|
|
@@ -304,39 +470,27 @@ async fn handle_websocket_forwarding(
|
|
|
304
470
|
}
|
|
305
471
|
|
|
306
472
|
async fn handle_connection(
|
|
307
|
-
req: Request<
|
|
473
|
+
req: Request<Incoming>,
|
|
308
474
|
proxy: Arc<FBIProxy>,
|
|
309
|
-
) -> Result<Response<
|
|
475
|
+
) -> Result<Response<BoxBody>, Infallible> {
|
|
310
476
|
match proxy.handle_request(req).await {
|
|
311
477
|
Ok(response) => Ok(response),
|
|
312
478
|
Err(e) => {
|
|
313
479
|
error!("Request handling error: {}", e);
|
|
314
480
|
Ok(Response::builder()
|
|
315
481
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
316
|
-
.body(
|
|
482
|
+
.body(Full::new(Bytes::from("Internal Server Error")).map_err(|e| match e {}).boxed())
|
|
317
483
|
.unwrap())
|
|
318
484
|
}
|
|
319
485
|
}
|
|
320
486
|
}
|
|
321
487
|
|
|
322
|
-
pub async fn start_proxy_server(port: u16) -> Result<(), BoxError> {
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
pub async fn start_proxy_server_with_host(host: &str, port: u16) -> Result<(), BoxError> {
|
|
327
|
-
start_proxy_server_with_options(host, port, None).await
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
pub async fn start_proxy_server_with_options(host: &str, port: u16, domain_filter: Option<String>) -> Result<(), BoxError> {
|
|
488
|
+
pub async fn start_proxy_server(host: Option<&str>, port: u16, domain_filter: Option<String>) -> Result<(), BoxError> {
|
|
489
|
+
let host = host.unwrap_or("127.0.0.1");
|
|
331
490
|
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
|
332
491
|
let proxy = Arc::new(FBIProxy::new(domain_filter.clone()));
|
|
333
492
|
|
|
334
|
-
let
|
|
335
|
-
let proxy = proxy.clone();
|
|
336
|
-
async move { Ok::<_, Infallible>(service_fn(move |req| handle_connection(req, proxy.clone()))) }
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
let server = Server::bind(&addr).serve(make_svc);
|
|
493
|
+
let listener = TcpListener::bind(addr).await?;
|
|
340
494
|
|
|
341
495
|
info!("FBI Proxy server running on http://{}", addr);
|
|
342
496
|
println!("FBI Proxy listening on: http://{}", addr);
|
|
@@ -350,11 +504,25 @@ pub async fn start_proxy_server_with_options(host: &str, port: u16, domain_filte
|
|
|
350
504
|
|
|
351
505
|
info!("Features: HTTP proxying + WebSocket forwarding + Port encoding + Domain filtering");
|
|
352
506
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
507
|
+
loop {
|
|
508
|
+
let (stream, _) = listener.accept().await?;
|
|
509
|
+
let io = TokioIo::new(stream);
|
|
510
|
+
let proxy = proxy.clone();
|
|
356
511
|
|
|
357
|
-
|
|
512
|
+
tokio::task::spawn(async move {
|
|
513
|
+
let service = service_fn(move |req| handle_connection(req, proxy.clone()));
|
|
514
|
+
|
|
515
|
+
if let Err(err) = http1::Builder::new()
|
|
516
|
+
.preserve_header_case(true)
|
|
517
|
+
.title_case_headers(true)
|
|
518
|
+
.serve_connection(io, service)
|
|
519
|
+
.with_upgrades()
|
|
520
|
+
.await
|
|
521
|
+
{
|
|
522
|
+
error!("Error serving connection: {:?}", err);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
358
526
|
}
|
|
359
527
|
|
|
360
528
|
fn main() {
|
|
@@ -453,7 +621,7 @@ TRY RUN:
|
|
|
453
621
|
"Starting FBI-Proxy on {}:{} with domain filter: {:?}",
|
|
454
622
|
host, port, domain_filter
|
|
455
623
|
);
|
|
456
|
-
if let Err(e) =
|
|
624
|
+
if let Err(e) = start_proxy_server(Some(host), port, domain_filter).await {
|
|
457
625
|
error!("Failed to start proxy server: {}", e);
|
|
458
626
|
}
|
|
459
627
|
});
|
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...");
|