fbi-proxy 1.9.0 → 1.10.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/README.md +71 -0
- package/dist/cli.js +772 -29
- package/package.json +16 -10
- 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 +123 -93
- package/rs/lib.rs +12 -0
- package/rs/routes.rs +976 -0
- package/ts/auth/authConfig.ts +141 -0
- package/ts/auth/caddyfileGen.test.ts +156 -0
- package/ts/auth/caddyfileGen.ts +142 -0
- package/ts/auth/downloadCaddy.test.ts +131 -0
- package/ts/auth/downloadCaddy.ts +213 -0
- package/ts/auth/setupWizard.ts +183 -0
- package/ts/auth/spawnCaddy.ts +125 -0
- package/ts/auth/spawnFbiAuth.ts +43 -0
- package/ts/buildFbiProxy.ts +3 -11
- package/ts/cli.ts +190 -7
- package/ts/dSpawn.ts +4 -9
- package/ts/getProxyFilename.ts +11 -9
- package/ts/routes.test.ts +182 -0
- package/ts/routes.ts +238 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fbi-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|
|
@@ -13,13 +13,12 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/snomiao/fbi-proxy/issues"
|
|
15
15
|
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "snomiao",
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
18
20
|
"url": "git+https://github.com/snomiao/fbi-proxy.git"
|
|
19
21
|
},
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"author": "snomiao",
|
|
22
|
-
"type": "module",
|
|
23
22
|
"bin": {
|
|
24
23
|
"fbi-proxy": "dist/cli.js"
|
|
25
24
|
},
|
|
@@ -29,6 +28,7 @@
|
|
|
29
28
|
"rs",
|
|
30
29
|
"ts"
|
|
31
30
|
],
|
|
31
|
+
"type": "module",
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "bun run build:rs && bun run build:js",
|
|
34
34
|
"build:js": "bun build ./ts/cli.ts --outdir dist --target node",
|
|
@@ -52,7 +52,10 @@
|
|
|
52
52
|
"execa": "^9.6.1",
|
|
53
53
|
"from-node-stream": "^0.1.2",
|
|
54
54
|
"get-port": "^7.1.0",
|
|
55
|
+
"hono": "^4.12.18",
|
|
55
56
|
"hot-memo": "^1.1.1",
|
|
57
|
+
"jose": "^6.2.3",
|
|
58
|
+
"oauth4webapi": "^3.8.6",
|
|
56
59
|
"sflow": "^1.27.0",
|
|
57
60
|
"tsa-composer": "^1.0.0",
|
|
58
61
|
"yargs": "^17.7.2"
|
|
@@ -70,6 +73,7 @@
|
|
|
70
73
|
"husky": "^9.1.7",
|
|
71
74
|
"lint-staged": "^16.2.7",
|
|
72
75
|
"node-fetch": "^3.3.2",
|
|
76
|
+
"oxmgr": "^0.4.0",
|
|
73
77
|
"playwright": "^1.58.2",
|
|
74
78
|
"prettier": "^3.8.1",
|
|
75
79
|
"semantic-release": "^24.0.0",
|
|
@@ -78,8 +82,8 @@
|
|
|
78
82
|
"vitest": "^3.2.4",
|
|
79
83
|
"ws": "^8.19.0"
|
|
80
84
|
},
|
|
81
|
-
"
|
|
82
|
-
"
|
|
85
|
+
"overrides": {
|
|
86
|
+
"@semantic-release/npm": "^13.1.3"
|
|
83
87
|
},
|
|
84
88
|
"lint-staged": {
|
|
85
89
|
"*.{ts,js}": [
|
|
@@ -89,8 +93,10 @@
|
|
|
89
93
|
"bun --bun prettier --write"
|
|
90
94
|
]
|
|
91
95
|
},
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": ">=22.14.0"
|
|
98
|
+
},
|
|
99
|
+
"trustedDependencies": [
|
|
100
|
+
"oxmgr"
|
|
101
|
+
]
|
|
96
102
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/rs/fbi-proxy.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use clap::{Arg, Command};
|
|
2
|
+
use fbi_proxy::routes::{self, CompiledRoute, RouteHit};
|
|
2
3
|
use futures_util::{SinkExt, StreamExt};
|
|
3
4
|
use http_body_util::{BodyExt, Full};
|
|
4
5
|
use hyper::body::{Bytes, Incoming};
|
|
@@ -23,18 +24,22 @@ use tokio_tungstenite::connect_async;
|
|
|
23
24
|
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
|
24
25
|
type BoxBody = http_body_util::combinators::BoxBody<Bytes, hyper::Error>;
|
|
25
26
|
|
|
27
|
+
/// Bundled default routes.yaml — reproduces the original `parse_host`
|
|
28
|
+
/// behavior. Loaded at compile-time so the binary works out-of-the-box.
|
|
29
|
+
const BUNDLED_ROUTES_YAML: &str = include_str!("../routes.yaml");
|
|
30
|
+
|
|
26
31
|
pub struct FBIProxy {
|
|
27
32
|
client: Client<HttpConnector, BoxBody>,
|
|
28
33
|
number_regex: Regex,
|
|
29
34
|
domain_filter: Option<String>,
|
|
35
|
+
compiled_routes: Vec<CompiledRoute>,
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/*
|
|
33
|
-
FBIProxy is a simple HTTP and WebSocket proxy server
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for localhost, it uses "localhost"
|
|
39
|
+
FBIProxy is a simple HTTP and WebSocket proxy server with rule-based
|
|
40
|
+
host header routing. The rules are loaded from `routes.yaml` (bundled
|
|
41
|
+
by default, overridable via --routes). See the bundled `routes.yaml`
|
|
42
|
+
for the default 8 rules; in short:
|
|
38
43
|
|
|
39
44
|
rule1: number host goes to local port
|
|
40
45
|
- Host="3000" => localhost:3000
|
|
@@ -52,15 +57,13 @@ rule3: subdomains are hoisted
|
|
|
52
57
|
- 3000.amd => proxies to http://amd:80, with host: 3000
|
|
53
58
|
- sur.amd => proxies to http://amd:80, with host: sur
|
|
54
59
|
- amd.sur.amd => proxies to http://amd:80, with host: amd.sur
|
|
55
|
-
- if sur also runs fbi-proxy, it will proxies to http://amd:80, with host: amd
|
|
56
|
-
- 3000.sur.amd => proxies to http://amd:80, with host: 3000.sur
|
|
57
|
-
|
|
58
|
-
for subdomains
|
|
59
|
-
*.amd => localhost:amd
|
|
60
60
|
|
|
61
|
+
When `--domain` (or `FBI_PROXY_DOMAIN`) is set, only hosts ending with
|
|
62
|
+
that suffix are accepted. The exact-domain host (e.g. `fbi.com`) serves
|
|
63
|
+
the landing page.
|
|
61
64
|
*/
|
|
62
65
|
impl FBIProxy {
|
|
63
|
-
pub fn new(domain_filter: Option<String>) -> Self {
|
|
66
|
+
pub fn new(domain_filter: Option<String>, compiled_routes: Vec<CompiledRoute>) -> Self {
|
|
64
67
|
let mut connector = HttpConnector::new();
|
|
65
68
|
// Set connection timeout to 5 seconds to avoid hanging on invalid hosts
|
|
66
69
|
connector.set_connect_timeout(Some(Duration::from_secs(3)));
|
|
@@ -72,6 +75,7 @@ impl FBIProxy {
|
|
|
72
75
|
client,
|
|
73
76
|
number_regex: Regex::new(r"^\d+$").unwrap(),
|
|
74
77
|
domain_filter,
|
|
78
|
+
compiled_routes,
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -165,77 +169,52 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
|
|
|
165
169
|
</html>"#.to_string()
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let host = if let Some(domain) = domain_filter {
|
|
178
|
-
if !domain.is_empty() {
|
|
179
|
-
// Check if host ends with the domain filter
|
|
180
|
-
if host_without_port.ends_with(domain) {
|
|
181
|
-
// Strip the domain suffix (including the dot)
|
|
182
|
-
let prefix_len = host_without_port.len() - domain.len();
|
|
183
|
-
if prefix_len > 0 && host_without_port.chars().nth(prefix_len - 1) == Some('.') {
|
|
184
|
-
// Remove the domain and the dot before it
|
|
185
|
-
&host_without_port[..prefix_len - 1]
|
|
186
|
-
} else if prefix_len == 0 {
|
|
187
|
-
// The host is exactly the domain, treat as root
|
|
188
|
-
"@"
|
|
189
|
-
} else {
|
|
190
|
-
// No dot separator, invalid format
|
|
191
|
-
return None;
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
// Host doesn't match domain filter
|
|
195
|
-
return None;
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
// Empty domain filter, accept all
|
|
199
|
-
host_without_port
|
|
200
|
-
}
|
|
201
|
-
} else {
|
|
202
|
-
// No domain filter, accept all
|
|
203
|
-
host_without_port
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// Handle special case: @ means root domain was accessed - serve landing page
|
|
207
|
-
if host == "@" {
|
|
208
|
-
return Some(("@LANDING".to_string(), "@LANDING".to_string()));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Rule 1: number host goes to local port (e.g., "3000" => "localhost:3000")
|
|
212
|
-
if self.number_regex.is_match(host) {
|
|
213
|
-
return Some((format!("localhost:{}", host), "localhost".to_string()));
|
|
172
|
+
/// Extract the hostname portion (before the first `:`) from a target
|
|
173
|
+
/// string like `"127.0.0.1:3000"`. This is the default `Host` header
|
|
174
|
+
/// value used when a matched rule doesn't specify an explicit
|
|
175
|
+
/// `headers.Host` rewrite — which preserves the original
|
|
176
|
+
/// `parse_host` semantics (numeric subdomain → Host: localhost).
|
|
177
|
+
fn host_from_target(target: &str) -> String {
|
|
178
|
+
match target.find(':') {
|
|
179
|
+
Some(i) => target[..i].to_string(),
|
|
180
|
+
None => target.to_string(),
|
|
214
181
|
}
|
|
182
|
+
}
|
|
215
183
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
184
|
+
/// Returns Some((target, new_host_header)) if the routing engine
|
|
185
|
+
/// matches `host_header`, accounting for:
|
|
186
|
+
/// * domain filter pre-check (host must end with the configured
|
|
187
|
+
/// domain suffix, if any),
|
|
188
|
+
/// * exact-domain match → ("@LANDING", "@LANDING") so the request
|
|
189
|
+
/// handler serves the landing page,
|
|
190
|
+
/// * normal rule match → (target, host_header).
|
|
191
|
+
///
|
|
192
|
+
/// Returns None if the host is rejected (filter mismatch or no
|
|
193
|
+
/// matching rule).
|
|
194
|
+
fn route(&self, host_header: &str) -> Option<(String, String)> {
|
|
195
|
+
// Drop port if present.
|
|
196
|
+
let host_without_port = match host_header.find(':') {
|
|
197
|
+
Some(i) => &host_header[..i],
|
|
198
|
+
None => host_header,
|
|
199
|
+
};
|
|
222
200
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
let subdomain = subdomain_parts.join(".");
|
|
230
|
-
|
|
231
|
-
// Target is the main domain on port 80
|
|
232
|
-
let target_host = format!("{}:80", main_domain);
|
|
233
|
-
// New host header is the subdomain
|
|
234
|
-
return Some((target_host, subdomain.to_string()));
|
|
201
|
+
// Exact-domain match → serve landing page. Only relevant when a
|
|
202
|
+
// domain filter is configured.
|
|
203
|
+
if let Some(ref domain) = self.domain_filter {
|
|
204
|
+
if !domain.is_empty() && host_without_port.eq_ignore_ascii_case(domain) {
|
|
205
|
+
return Some(("@LANDING".to_string(), "@LANDING".to_string()));
|
|
206
|
+
}
|
|
235
207
|
}
|
|
236
208
|
|
|
237
|
-
|
|
238
|
-
|
|
209
|
+
let hit = routes::match_host_with_domain(
|
|
210
|
+
&self.compiled_routes,
|
|
211
|
+
host_header,
|
|
212
|
+
self.domain_filter.as_deref(),
|
|
213
|
+
)?;
|
|
214
|
+
|
|
215
|
+
let RouteHit { target, host_header: rewrite, .. } = hit;
|
|
216
|
+
let new_host = rewrite.unwrap_or_else(|| Self::host_from_target(&target));
|
|
217
|
+
Some((target, new_host))
|
|
239
218
|
}
|
|
240
219
|
|
|
241
220
|
pub async fn handle_request(&self, req: Request<Incoming>) -> Result<Response<BoxBody>, BoxError> {
|
|
@@ -247,9 +226,9 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
|
|
|
247
226
|
.unwrap_or("localhost")
|
|
248
227
|
.to_string();
|
|
249
228
|
|
|
250
|
-
//
|
|
251
|
-
let parsed_host = self.
|
|
252
|
-
|
|
229
|
+
// Route the host via the rule engine.
|
|
230
|
+
let parsed_host = self.route(&host_header);
|
|
231
|
+
|
|
253
232
|
// If domain filter rejects the host, return 502 Bad Gateway
|
|
254
233
|
let (target_host, new_host) = match parsed_host {
|
|
255
234
|
Some(hosts) => hosts,
|
|
@@ -384,7 +363,8 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
|
|
|
384
363
|
);
|
|
385
364
|
return Ok(Response::builder()
|
|
386
365
|
.status(StatusCode::BAD_GATEWAY)
|
|
387
|
-
.
|
|
366
|
+
.header("Content-Type", "text/plain")
|
|
367
|
+
.body(Full::new(Bytes::from(format!("502 Bad Gateway: failed to connect to {}: {}", tunnel_target, e))).map_err(|e| match e {}).boxed())?);
|
|
388
368
|
}
|
|
389
369
|
Err(_) => {
|
|
390
370
|
error!(
|
|
@@ -395,7 +375,8 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
|
|
|
395
375
|
);
|
|
396
376
|
return Ok(Response::builder()
|
|
397
377
|
.status(StatusCode::BAD_GATEWAY)
|
|
398
|
-
.
|
|
378
|
+
.header("Content-Type", "text/plain")
|
|
379
|
+
.body(Full::new(Bytes::from(format!("502 Bad Gateway: connection to {} timed out", tunnel_target))).map_err(|e| match e {}).boxed())?);
|
|
399
380
|
}
|
|
400
381
|
}
|
|
401
382
|
}
|
|
@@ -462,7 +443,8 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
|
|
|
462
443
|
);
|
|
463
444
|
Ok(Response::builder()
|
|
464
445
|
.status(StatusCode::BAD_GATEWAY)
|
|
465
|
-
.
|
|
446
|
+
.header("Content-Type", "text/plain")
|
|
447
|
+
.body(Full::new(Bytes::from(format!("502 Bad Gateway: failed to connect to {}: {}", target_host, e))).map_err(|e| match e {}).boxed())?)
|
|
466
448
|
}
|
|
467
449
|
Err(_) => {
|
|
468
450
|
error!(
|
|
@@ -474,7 +456,8 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
|
|
|
474
456
|
);
|
|
475
457
|
Ok(Response::builder()
|
|
476
458
|
.status(StatusCode::BAD_GATEWAY)
|
|
477
|
-
.
|
|
459
|
+
.header("Content-Type", "text/plain")
|
|
460
|
+
.body(Full::new(Bytes::from(format!("502 Bad Gateway: request to {} timed out", target_host))).map_err(|e| match e {}).boxed())?)
|
|
478
461
|
}
|
|
479
462
|
}
|
|
480
463
|
}
|
|
@@ -500,7 +483,8 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
|
|
|
500
483
|
error!("WS :ws:{} => :ws:{}{} 502 (upstream connection failed: {})", target_host, target_host, uri, e);
|
|
501
484
|
return Ok(Response::builder()
|
|
502
485
|
.status(StatusCode::BAD_GATEWAY)
|
|
503
|
-
.
|
|
486
|
+
.header("Content-Type", "text/plain")
|
|
487
|
+
.body(Full::new(Bytes::from(format!("502 Bad Gateway: WebSocket upstream {} unavailable: {}", target_host, e))).map_err(|e| match e {}).boxed())?);
|
|
504
488
|
}
|
|
505
489
|
};
|
|
506
490
|
|
|
@@ -578,16 +562,36 @@ async fn handle_connection(
|
|
|
578
562
|
error!("Request handling error: {}", e);
|
|
579
563
|
Ok(Response::builder()
|
|
580
564
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
581
|
-
.
|
|
565
|
+
.header("Content-Type", "text/plain")
|
|
566
|
+
.body(Full::new(Bytes::from(format!("500 Internal Server Error: {}", e))).map_err(|e| match e {}).boxed())
|
|
582
567
|
.unwrap())
|
|
583
568
|
}
|
|
584
569
|
}
|
|
585
570
|
}
|
|
586
571
|
|
|
587
|
-
|
|
572
|
+
/// Load routes from `routes.yaml` source text and compile them. Panics
|
|
573
|
+
/// with a descriptive message on parse or compile failure — this is
|
|
574
|
+
/// only invoked at startup, so failing fast is the right policy.
|
|
575
|
+
fn load_routes(yaml_src: &str, source_label: &str) -> Vec<CompiledRoute> {
|
|
576
|
+
let parsed = match routes::parse_yaml(yaml_src) {
|
|
577
|
+
Ok(p) => p,
|
|
578
|
+
Err(e) => panic!("failed to parse {}: {}", source_label, e),
|
|
579
|
+
};
|
|
580
|
+
match routes::compile(parsed.routes) {
|
|
581
|
+
Ok(c) => c,
|
|
582
|
+
Err(e) => panic!("failed to compile {}: {}", source_label, e),
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
pub async fn start_proxy_server(
|
|
587
|
+
host: Option<&str>,
|
|
588
|
+
port: u16,
|
|
589
|
+
domain_filter: Option<String>,
|
|
590
|
+
compiled_routes: Vec<CompiledRoute>,
|
|
591
|
+
) -> Result<(), BoxError> {
|
|
588
592
|
let host = host.unwrap_or("127.0.0.1");
|
|
589
593
|
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
|
590
|
-
let proxy = Arc::new(FBIProxy::new(domain_filter.clone()));
|
|
594
|
+
let proxy = Arc::new(FBIProxy::new(domain_filter.clone(), compiled_routes));
|
|
591
595
|
|
|
592
596
|
let listener = TcpListener::bind(addr).await?;
|
|
593
597
|
|
|
@@ -600,7 +604,7 @@ pub async fn start_proxy_server(host: Option<&str>, port: u16, domain_filter: Op
|
|
|
600
604
|
}
|
|
601
605
|
println!();
|
|
602
606
|
println!("== HOW IT WORKS ==");
|
|
603
|
-
println!("Routes requests based on Host header:");
|
|
607
|
+
println!("Routes requests based on Host header (configurable via routes.yaml):");
|
|
604
608
|
println!(" 3000 -> localhost:3000 (port as host)");
|
|
605
609
|
println!(" api--8080 -> api:8080 (host--port syntax)");
|
|
606
610
|
println!(" 3000.fbi.com -> localhost:3000 (subdomain as port)");
|
|
@@ -654,11 +658,11 @@ fn main() {
|
|
|
654
658
|
|
|
655
659
|
FEATURES:
|
|
656
660
|
• HTTP and WebSocket proxying with bidirectional forwarding
|
|
657
|
-
• Smart host header parsing with multiple routing rules
|
|
661
|
+
• Smart host header parsing with multiple routing rules (configurable via routes.yaml)
|
|
658
662
|
• Port encoding support for easy local development
|
|
659
663
|
• Subdomain hoisting for multi-service architectures
|
|
660
664
|
|
|
661
|
-
HOST PARSING RULES:
|
|
665
|
+
HOST PARSING RULES (default routes.yaml):
|
|
662
666
|
1. Number host → local port: '3000' → localhost:3000
|
|
663
667
|
2. Host--port syntax: 'api--3000' → api:3000
|
|
664
668
|
3. Subdomain hoisting: 'api.service' → service:80 (host: api)
|
|
@@ -668,6 +672,7 @@ ENVIRONMENT VARIABLES:
|
|
|
668
672
|
FBI_PROXY_PORT Port to listen on (default: 2432)
|
|
669
673
|
FBI_PROXY_HOST Host/IP address to bind to (default: 127.0.0.1)
|
|
670
674
|
FBI_PROXY_DOMAIN Domain filter (only accept *.domain requests)
|
|
675
|
+
FBI_PROXY_ROUTES Path to a custom routes.yaml (default: bundled)
|
|
671
676
|
RUST_LOG Log level (error, warn, info, debug, trace)
|
|
672
677
|
|
|
673
678
|
EXAMPLES:
|
|
@@ -675,6 +680,7 @@ EXAMPLES:
|
|
|
675
680
|
fbi-proxy -p 8080 # Custom port
|
|
676
681
|
fbi-proxy -h 0.0.0.0 -p 3000 # Bind to all interfaces
|
|
677
682
|
fbi-proxy -d example.com # Only accept *.example.com requests
|
|
683
|
+
fbi-proxy -r ./my-routes.yaml # Use a custom routing config
|
|
678
684
|
FBI_PROXY_PORT=8080 fbi-proxy # Use environment variable
|
|
679
685
|
|
|
680
686
|
TRY RUN:
|
|
@@ -713,6 +719,15 @@ TRY RUN:
|
|
|
713
719
|
.env("FBI_PROXY_DOMAIN")
|
|
714
720
|
.default_value("")
|
|
715
721
|
)
|
|
722
|
+
.arg(
|
|
723
|
+
Arg::new("routes")
|
|
724
|
+
.short('r')
|
|
725
|
+
.long("routes")
|
|
726
|
+
.value_name("PATH")
|
|
727
|
+
.help("Path to a custom routes.yaml (env: FBI_PROXY_ROUTES, default: bundled)")
|
|
728
|
+
.env("FBI_PROXY_ROUTES")
|
|
729
|
+
.default_value("")
|
|
730
|
+
)
|
|
716
731
|
.get_matches();
|
|
717
732
|
|
|
718
733
|
let port = matches
|
|
@@ -726,20 +741,35 @@ TRY RUN:
|
|
|
726
741
|
|
|
727
742
|
let host = matches.get_one::<String>("host").unwrap();
|
|
728
743
|
let domain = matches.get_one::<String>("domain").unwrap();
|
|
729
|
-
|
|
744
|
+
let routes_path = matches.get_one::<String>("routes").unwrap();
|
|
745
|
+
|
|
730
746
|
let domain_filter = if domain.is_empty() {
|
|
731
747
|
None
|
|
732
748
|
} else {
|
|
733
749
|
Some(domain.clone())
|
|
734
750
|
};
|
|
735
751
|
|
|
752
|
+
// Load routes: either from --routes <path> or the bundled default.
|
|
753
|
+
let compiled_routes = if routes_path.is_empty() {
|
|
754
|
+
load_routes(BUNDLED_ROUTES_YAML, "bundled routes.yaml")
|
|
755
|
+
} else {
|
|
756
|
+
let src = match std::fs::read_to_string(routes_path) {
|
|
757
|
+
Ok(s) => s,
|
|
758
|
+
Err(e) => {
|
|
759
|
+
eprintln!("error: failed to read --routes file '{}': {}", routes_path, e);
|
|
760
|
+
std::process::exit(2);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
load_routes(&src, &format!("routes file '{}'", routes_path))
|
|
764
|
+
};
|
|
765
|
+
|
|
736
766
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
737
767
|
rt.block_on(async {
|
|
738
768
|
info!(
|
|
739
769
|
"Starting FBI-Proxy on {}:{} with domain filter: {:?}",
|
|
740
770
|
host, port, domain_filter
|
|
741
771
|
);
|
|
742
|
-
if let Err(e) = start_proxy_server(Some(host), port, domain_filter).await {
|
|
772
|
+
if let Err(e) = start_proxy_server(Some(host), port, domain_filter, compiled_routes).await {
|
|
743
773
|
error!("Failed to start proxy server: {}", e);
|
|
744
774
|
}
|
|
745
775
|
});
|
package/rs/lib.rs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//! Library entry point for `fbi-proxy`.
|
|
2
|
+
//!
|
|
3
|
+
//! This file exists primarily so that internal modules (like `routes`)
|
|
4
|
+
//! can be unit-tested via `cargo test --lib` without coupling them to
|
|
5
|
+
//! the binary's runtime concerns.
|
|
6
|
+
//!
|
|
7
|
+
//! The binary in `rs/fbi-proxy.rs` does not currently depend on this
|
|
8
|
+
//! library — the routing engine is intentionally not wired into the
|
|
9
|
+
//! live request path yet (see `docs/routing.md` for the migration
|
|
10
|
+
//! plan).
|
|
11
|
+
|
|
12
|
+
pub mod routes;
|