fbi-proxy 1.8.1 → 1.9.1

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # fbi-proxy
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/fbi-proxy)](https://www.npmjs.com/package/fbi-proxy)
4
+ [![crates.io](https://img.shields.io/crates/v/fbi-proxy)](https://crates.io/crates/fbi-proxy)
5
+ [![GitHub release](https://img.shields.io/github/v/release/snomiao/fbi-proxy)](https://github.com/snomiao/fbi-proxy/releases/latest)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
3
8
  FBI-Proxy provides easy HTTPS access to your local services with intelligent domain routing.
4
9
 
5
10
  ## Features
@@ -22,12 +27,14 @@ FBI-Proxy provides easy HTTPS access to your local services with intelligent dom
22
27
  ## Roadmap
23
28
 
24
29
  ### Next Up 🚧
30
+
25
31
  - [ ] **Configuration File Support** - YAML/JSON config for persistent routing rules
26
32
  - [ ] **Access Control** - Domain filtering, host/port whitelisting
27
33
  - [ ] **Request Logging** - Basic access logs for debugging
28
34
  - [ ] **Health Checks** - Simple upstream service availability monitoring
29
35
 
30
36
  ### Future Improvements 🔮
37
+
31
38
  - [ ] **Load Balancing** - Round-robin between multiple upstream targets
32
39
  - [ ] **Metrics** - Basic statistics (requests, response times, errors)
33
40
  - [ ] **Hot Reload** - Update configuration without restart
@@ -134,12 +141,12 @@ bun run build && bun run start
134
141
 
135
142
  FBI-Proxy supports the following environment variables for configuration:
136
143
 
137
- | Variable | Description | Default |
138
- |----------|-------------|---------|
139
- | `FBI_PROXY_PORT` | Port for the proxy server to listen on | `2432` |
140
- | `FBI_PROXY_HOST` | Host/IP address to bind to | `127.0.0.1` |
141
- | `RUST_LOG` | Log level for the Rust proxy (error, warn, info, debug, trace) | `info` |
142
- | `FBIPROXY_PORT` | Internal proxy port (auto-assigned) | Auto |
144
+ | Variable | Description | Default |
145
+ | ---------------- | -------------------------------------------------------------- | ----------- |
146
+ | `FBI_PROXY_PORT` | Port for the proxy server to listen on | `2432` |
147
+ | `FBI_PROXY_HOST` | Host/IP address to bind to | `127.0.0.1` |
148
+ | `RUST_LOG` | Log level for the Rust proxy (error, warn, info, debug, trace) | `info` |
149
+ | `FBIPROXY_PORT` | Internal proxy port (auto-assigned) | Auto |
143
150
 
144
151
  Command-line arguments take precedence over environment variables.
145
152
 
package/dist/cli.js CHANGED
@@ -132,7 +132,7 @@ async function hotMemo(fn, args = [], key = `_HOTMEMO_${g["_HOTMEMO_SALT_"]}_${S
132
132
  hotMemo.cache = cache;
133
133
 
134
134
  // ts/cli.ts
135
- import path from "path";
135
+ import path2 from "path";
136
136
 
137
137
  // node_modules/yargs/lib/platform-shims/esm.mjs
138
138
  import { notStrictEqual, strictEqual } from "assert";
@@ -4990,6 +4990,7 @@ var yargs_default = Yargs;
4990
4990
  // ts/buildFbiProxy.ts
4991
4991
  import { existsSync } from "fs";
4992
4992
  import { chmod } from "fs/promises";
4993
+ import path from "path";
4993
4994
 
4994
4995
  // ts/getProxyFilename.ts
4995
4996
  function getFbiProxyFilename() {
@@ -5117,15 +5118,27 @@ var $ = Object.assign(dSpawn(), {
5117
5118
 
5118
5119
  // ts/buildFbiProxy.ts
5119
5120
  if (false) {}
5120
- async function getFbiProxyBinary({ rebuild = false } = {}) {
5121
+ async function getFbiProxyBinary({
5122
+ rebuild = false,
5123
+ originalCwd = ""
5124
+ } = {}) {
5121
5125
  const isWin = process.platform === "win32";
5122
5126
  const binaryName = getFbiProxyFilename();
5127
+ const binarySuffix = isWin ? ".exe" : "";
5128
+ if (!rebuild && originalCwd) {
5129
+ const localBuilt = path.join(originalCwd, `target/release/fbi-proxy${binarySuffix}`);
5130
+ if (existsSync(localBuilt)) {
5131
+ console.log(`Using local build: ${localBuilt}`);
5132
+ await chmod(localBuilt, 493).catch(() => {});
5133
+ return localBuilt;
5134
+ }
5135
+ }
5123
5136
  const dockerBinary = "/app/bin/fbi-proxy";
5124
5137
  if (!rebuild && existsSync(dockerBinary)) {
5125
5138
  return dockerBinary;
5126
5139
  }
5127
5140
  const release = "./release/" + binaryName;
5128
- const built = `./target/release/fbi-proxy${isWin ? ".exe" : ""}`;
5141
+ const built = `./target/release/fbi-proxy${binarySuffix}`;
5129
5142
  if (!rebuild && existsSync(built)) {
5130
5143
  await chmod(built, 493).catch(() => {});
5131
5144
  return built;
@@ -5222,11 +5235,12 @@ var dSpawn2 = ({
5222
5235
  });
5223
5236
  var $2 = Object.assign(dSpawn2(), {
5224
5237
  opt: dSpawn2,
5225
- cwd: (path) => dSpawn2({ cwd: path })
5238
+ cwd: (path2) => dSpawn2({ cwd: path2 })
5226
5239
  });
5227
5240
 
5228
5241
  // ts/cli.ts
5229
- process.chdir(path.resolve(import.meta.dir, ".."));
5242
+ var originalCwd = process.cwd();
5243
+ process.chdir(path2.resolve(import.meta.dir, ".."));
5230
5244
  await yargs_default(hideBin(process.argv)).option("dev", {
5231
5245
  alias: "d",
5232
5246
  type: "boolean",
@@ -5236,7 +5250,7 @@ await yargs_default(hideBin(process.argv)).option("dev", {
5236
5250
  console.log("Preparing Binaries");
5237
5251
  var FBI_PROXY_PORT = process.env.FBI_PROXY_PORT || String(await getPorts({ port: 2432 }));
5238
5252
  var proxyProcess = await hotMemo(async () => {
5239
- const proxy = await getFbiProxyBinary();
5253
+ const proxy = await getFbiProxyBinary({ originalCwd });
5240
5254
  console.log("Starting Rust proxy server");
5241
5255
  const p = $2.opt({
5242
5256
  env: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fbi-proxy",
3
- "version": "1.8.1",
3
+ "version": "1.9.1",
4
4
  "description": "FBI-Proxy provides easy HTTPS access to your local services with intelligent domain routing",
5
5
  "keywords": [
6
6
  "development-tools",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/rs/fbi-proxy.rs CHANGED
@@ -75,6 +75,96 @@ impl FBIProxy {
75
75
  }
76
76
  }
77
77
 
78
+ fn landing_page_html() -> String {
79
+ r#"<!DOCTYPE html>
80
+ <html lang="en">
81
+ <head>
82
+ <meta charset="UTF-8">
83
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
84
+ <title>FBI-Proxy</title>
85
+ <style>
86
+ * { box-sizing: border-box; }
87
+ body {
88
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
89
+ max-width: 800px;
90
+ margin: 0 auto;
91
+ padding: 2rem;
92
+ background: #0d1117;
93
+ color: #c9d1d9;
94
+ line-height: 1.6;
95
+ }
96
+ h1 { color: #58a6ff; margin-bottom: 0.5rem; }
97
+ h2 { color: #8b949e; border-bottom: 1px solid #30363d; padding-bottom: 0.5rem; }
98
+ code {
99
+ background: #161b22;
100
+ padding: 0.2rem 0.4rem;
101
+ border-radius: 4px;
102
+ font-size: 0.9em;
103
+ }
104
+ pre {
105
+ background: #161b22;
106
+ padding: 1rem;
107
+ border-radius: 8px;
108
+ overflow-x: auto;
109
+ }
110
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
111
+ th, td {
112
+ text-align: left;
113
+ padding: 0.5rem;
114
+ border-bottom: 1px solid #30363d;
115
+ }
116
+ th { color: #8b949e; }
117
+ .arrow { color: #7ee787; }
118
+ a { color: #58a6ff; }
119
+ .warning {
120
+ background: #3d1f00;
121
+ border: 1px solid #f85149;
122
+ padding: 1rem;
123
+ border-radius: 8px;
124
+ margin: 1rem 0;
125
+ }
126
+ </style>
127
+ </head>
128
+ <body>
129
+ <h1>🔀 FBI-Proxy</h1>
130
+ <p>A reverse proxy with intelligent host header routing.</p>
131
+
132
+ <h2>How It Works</h2>
133
+ <p>FBI-Proxy routes requests based on the <code>Host</code> header:</p>
134
+ <table>
135
+ <tr><th>Host Header</th><th></th><th>Routes To</th><th>Description</th></tr>
136
+ <tr><td><code>3000</code></td><td class="arrow">→</td><td><code>localhost:3000</code></td><td>Port as host</td></tr>
137
+ <tr><td><code>api--8080</code></td><td class="arrow">→</td><td><code>api:8080</code></td><td>host--port syntax</td></tr>
138
+ <tr><td><code>3000.fbi.com</code></td><td class="arrow">→</td><td><code>localhost:3000</code></td><td>Subdomain as port</td></tr>
139
+ <tr><td><code>app.server</code></td><td class="arrow">→</td><td><code>server:80</code></td><td>Subdomain hoisting</td></tr>
140
+ </table>
141
+
142
+ <h2>Quick Start</h2>
143
+ <pre>npx fbi-proxy # Start proxy on :2432
144
+ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
145
+
146
+ <h2>Caddy Setup</h2>
147
+ <p>Expose local ports via HTTPS with wildcard domain:</p>
148
+ <pre># Caddyfile
149
+ *.fbi.example.com {
150
+ tls { dns cloudflare {env.CF_API_TOKEN} }
151
+ reverse_proxy localhost:2432
152
+ }</pre>
153
+ <p>Then access:</p>
154
+ <ul>
155
+ <li><code>https://3000.fbi.example.com</code> → <code>localhost:3000</code></li>
156
+ <li><code>https://8080.fbi.example.com</code> → <code>localhost:8080</code></li>
157
+ </ul>
158
+
159
+ <div class="warning">
160
+ ⚠️ <strong>Security Warning:</strong> Set up an auth gateway before exposing to the internet.
161
+ </div>
162
+
163
+ <p><a href="https://github.com/snomiao/fbi-proxy">GitHub</a> · <a href="https://www.npmjs.com/package/fbi-proxy">npm</a> · <a href="https://crates.io/crates/fbi-proxy">crates.io</a></p>
164
+ </body>
165
+ </html>"#.to_string()
166
+ }
167
+
78
168
  fn parse_host(&self, host_header: &str, domain_filter: &Option<String>) -> Option<(String, String)> {
79
169
  // Remove port if present (e.g., "localhost:8080" -> "localhost")
80
170
  let host_without_port = if let Some(colon_pos) = host_header.find(':') {
@@ -113,9 +203,9 @@ impl FBIProxy {
113
203
  host_without_port
114
204
  };
115
205
 
116
- // Handle special case: @ means root domain was accessed
206
+ // Handle special case: @ means root domain was accessed - serve landing page
117
207
  if host == "@" {
118
- return Some(("localhost:80".to_string(), "localhost".to_string()));
208
+ return Some(("@LANDING".to_string(), "@LANDING".to_string()));
119
209
  }
120
210
 
121
211
  // Rule 1: number host goes to local port (e.g., "3000" => "localhost:3000")
@@ -177,7 +267,16 @@ impl FBIProxy {
177
267
  .body(Full::new(Bytes::from("Bad Gateway: Host not allowed")).map_err(|e| match e {}).boxed())?);
178
268
  }
179
269
  };
180
-
270
+
271
+ // Serve landing page for root domain access
272
+ if target_host == "@LANDING" {
273
+ info!("GET {} => LANDING 200", host_header);
274
+ return Ok(Response::builder()
275
+ .status(StatusCode::OK)
276
+ .header("Content-Type", "text/html; charset=utf-8")
277
+ .body(Full::new(Bytes::from(Self::landing_page_html())).map_err(|e| match e {}).boxed())?);
278
+ }
279
+
181
280
  let method = req.method().clone();
182
281
  let original_uri = req.uri().clone();
183
282
 
@@ -285,7 +384,8 @@ impl FBIProxy {
285
384
  );
286
385
  return Ok(Response::builder()
287
386
  .status(StatusCode::BAD_GATEWAY)
288
- .body(Full::new(Bytes::from("FBIPROXY CONNECT ERROR")).map_err(|e| match e {}).boxed())?);
387
+ .header("Content-Type", "text/plain")
388
+ .body(Full::new(Bytes::from(format!("502 Bad Gateway: failed to connect to {}: {}", tunnel_target, e))).map_err(|e| match e {}).boxed())?);
289
389
  }
290
390
  Err(_) => {
291
391
  error!(
@@ -296,7 +396,8 @@ impl FBIProxy {
296
396
  );
297
397
  return Ok(Response::builder()
298
398
  .status(StatusCode::BAD_GATEWAY)
299
- .body(Full::new(Bytes::from("FBIPROXY CONNECT TIMEOUT")).map_err(|e| match e {}).boxed())?);
399
+ .header("Content-Type", "text/plain")
400
+ .body(Full::new(Bytes::from(format!("502 Bad Gateway: connection to {} timed out", tunnel_target))).map_err(|e| match e {}).boxed())?);
300
401
  }
301
402
  }
302
403
  }
@@ -363,7 +464,8 @@ impl FBIProxy {
363
464
  );
364
465
  Ok(Response::builder()
365
466
  .status(StatusCode::BAD_GATEWAY)
366
- .body(Full::new(Bytes::from("FBIPROXY ERROR")).map_err(|e| match e {}).boxed())?)
467
+ .header("Content-Type", "text/plain")
468
+ .body(Full::new(Bytes::from(format!("502 Bad Gateway: failed to connect to {}: {}", target_host, e))).map_err(|e| match e {}).boxed())?)
367
469
  }
368
470
  Err(_) => {
369
471
  error!(
@@ -375,7 +477,8 @@ impl FBIProxy {
375
477
  );
376
478
  Ok(Response::builder()
377
479
  .status(StatusCode::BAD_GATEWAY)
378
- .body(Full::new(Bytes::from("FBIPROXY TIMEOUT")).map_err(|e| match e {}).boxed())?)
480
+ .header("Content-Type", "text/plain")
481
+ .body(Full::new(Bytes::from(format!("502 Bad Gateway: request to {} timed out", target_host))).map_err(|e| match e {}).boxed())?)
379
482
  }
380
483
  }
381
484
  }
@@ -401,7 +504,8 @@ impl FBIProxy {
401
504
  error!("WS :ws:{} => :ws:{}{} 502 (upstream connection failed: {})", target_host, target_host, uri, e);
402
505
  return Ok(Response::builder()
403
506
  .status(StatusCode::BAD_GATEWAY)
404
- .body(Full::new(Bytes::from("WebSocket upstream unavailable")).map_err(|e| match e {}).boxed())?);
507
+ .header("Content-Type", "text/plain")
508
+ .body(Full::new(Bytes::from(format!("502 Bad Gateway: WebSocket upstream {} unavailable: {}", target_host, e))).map_err(|e| match e {}).boxed())?);
405
509
  }
406
510
  };
407
511
 
@@ -479,7 +583,8 @@ async fn handle_connection(
479
583
  error!("Request handling error: {}", e);
480
584
  Ok(Response::builder()
481
585
  .status(StatusCode::INTERNAL_SERVER_ERROR)
482
- .body(Full::new(Bytes::from("Internal Server Error")).map_err(|e| match e {}).boxed())
586
+ .header("Content-Type", "text/plain")
587
+ .body(Full::new(Bytes::from(format!("500 Internal Server Error: {}", e))).map_err(|e| match e {}).boxed())
483
588
  .unwrap())
484
589
  }
485
590
  }
@@ -499,6 +604,25 @@ pub async fn start_proxy_server(host: Option<&str>, port: u16, domain_filter: Op
499
604
  println!("Domain filter: Only accepting requests for *.{}", domain);
500
605
  }
501
606
  }
607
+ println!();
608
+ println!("== HOW IT WORKS ==");
609
+ println!("Routes requests based on Host header:");
610
+ println!(" 3000 -> localhost:3000 (port as host)");
611
+ println!(" api--8080 -> api:8080 (host--port syntax)");
612
+ println!(" 3000.fbi.com -> localhost:3000 (subdomain as port)");
613
+ println!(" app.server -> server:80 (subdomain hoisting)");
614
+ println!();
615
+ println!("== CADDY SETUP ==");
616
+ println!("# Caddyfile - expose *.fbi.example.com to local ports");
617
+ println!("*.fbi.example.com {{");
618
+ println!(" tls {{ dns cloudflare {{env.CF_API_TOKEN}} }}");
619
+ println!(" reverse_proxy localhost:2432");
620
+ println!("}}");
621
+ println!();
622
+ println!("Then: fbi-proxy -d fbi.example.com");
623
+ println!(" https://3000.fbi.example.com -> localhost:3000");
624
+ println!(" https://8080.fbi.example.com -> localhost:8080");
625
+ println!();
502
626
  println!("⚠️ FBI-Proxy WARNING: ENSURE YOU KNOW WHAT YOU'RE DOING and be sure to set up an auth gateway before exposing to the internet");
503
627
  println!(" This proxy is production ready but requires proper security measures.");
504
628
 
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "fs";
2
2
  import { chmod } from "fs/promises";
3
+ import path from "path";
3
4
  import { getFbiProxyFilename } from "./getProxyFilename";
4
5
  import { $ } from "./dSpawn";
5
6
 
@@ -7,9 +8,27 @@ if (import.meta.main) {
7
8
  await getFbiProxyBinary();
8
9
  }
9
10
 
10
- export async function getFbiProxyBinary({ rebuild = false } = {}) {
11
+ export async function getFbiProxyBinary({
12
+ rebuild = false,
13
+ originalCwd = "",
14
+ } = {}) {
11
15
  const isWin = process.platform === "win32";
12
16
  const binaryName = getFbiProxyFilename();
17
+ const binarySuffix = isWin ? ".exe" : "";
18
+
19
+ // Check for local build in original working directory first
20
+ // This allows users to run `bunx fbi-proxy` from their local repo and use their own build
21
+ if (!rebuild && originalCwd) {
22
+ const localBuilt = path.join(
23
+ originalCwd,
24
+ `target/release/fbi-proxy${binarySuffix}`,
25
+ );
26
+ if (existsSync(localBuilt)) {
27
+ console.log(`Using local build: ${localBuilt}`);
28
+ await chmod(localBuilt, 0o755).catch(() => {});
29
+ return localBuilt;
30
+ }
31
+ }
13
32
 
14
33
  // Check for pre-built binary in Docker container
15
34
  const dockerBinary = "/app/bin/fbi-proxy";
@@ -18,7 +37,7 @@ export async function getFbiProxyBinary({ rebuild = false } = {}) {
18
37
  }
19
38
 
20
39
  const release = "./release/" + binaryName;
21
- const built = `./target/release/fbi-proxy${isWin ? ".exe" : ""}`;
40
+ const built = `./target/release/fbi-proxy${binarySuffix}`;
22
41
 
23
42
  // return built if exists
24
43
  if (!rebuild && existsSync(built)) {
package/ts/cli.ts CHANGED
@@ -7,6 +7,8 @@ import { hideBin } from "yargs/helpers";
7
7
  import { getFbiProxyBinary } from "./buildFbiProxy";
8
8
  import { $ } from "./dSpawn";
9
9
 
10
+ // Save original cwd before changing (user might have local build there)
11
+ const originalCwd = process.cwd();
10
12
  process.chdir(path.resolve(import.meta.dir, "..")); // Change to project root directory
11
13
 
12
14
  // Parse command line arguments with yargs
@@ -25,7 +27,7 @@ const FBI_PROXY_PORT =
25
27
  process.env.FBI_PROXY_PORT || String(await getPort({ port: 2432 }));
26
28
 
27
29
  const proxyProcess = await hotMemo(async () => {
28
- const proxy = await getFbiProxyBinary();
30
+ const proxy = await getFbiProxyBinary({ originalCwd });
29
31
  console.log("Starting Rust proxy server");
30
32
  const p = $.opt({
31
33
  env: {