fbi-proxy 1.11.0 → 1.13.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 CHANGED
@@ -11,42 +11,50 @@ FBI-Proxy provides easy HTTPS access to your local services with intelligent dom
11
11
 
12
12
  ### Current Features ✅
13
13
 
14
- - **Intelligent Domain Routing**: Multiple routing patterns for flexible service access
14
+ - **One-command HTTPS gateway**: `bunx fbi-proxy --with-caddy --with-auth --provider snolab --domain fbi.com` brings up Caddy (auto-downloaded), fbi-auth (Firebase-backed Google sign-in), and the Rust proxy together — zero config needed on `.fbi.com`.
15
+ - **Rule-based Domain Routing** via `routes.yaml`:
15
16
  - Port-based routing (e.g., `3000.fbi.com` → `localhost:3000`)
16
17
  - Host--Port routing (e.g., `api--3001.fbi.com` → `api:3001`)
17
18
  - Subdomain routing with Host headers (e.g., `admin.app.fbi.com` → `app:80`)
18
19
  - Direct host forwarding (e.g., `myserver.fbi.com` → `myserver:80`)
19
- - **WebSocket Support**: Full WebSocket connection support for all routing patterns
20
- - **High Performance**: Built with Rust for optimal performance and low resource usage
21
- - **Easy Setup**: Simple one-command installation and startup
22
- - **Docker Support**: Available as a Docker image for containerized deployments
23
- - **Flexible Configuration**: Environment variables and CLI options for customization
24
- - **Cross-Platform**: Works on macOS, Linux, and Windows
25
- - **Integration Ready**: Compatible with reverse proxies like Caddy for HTTPS
20
+ - Placeholder syntax (`{name}`, `{name:int}`, `{name:slug}`, `{name:multi}`) for custom rules — see [docs/routing.md](docs/routing.md)
21
+ - **HTTPS Upstreams**: Targets with an `https://` prefix connect to upstream over TLS (Mozilla webpki roots).
22
+ - **WebSocket Support**: Full WebSocket forwarding (`ws://` and `wss://`) for all routing patterns.
23
+ - **Auth Gateway**: Google OAuth / Firebase Auth / zero-config snolab default IdP — JWT cookie scoped to `Domain=.your-domain` for cross-subdomain SSO. Audit log at `~/.config/fbi-proxy/audit.log`.
24
+ - **High Performance**: Built with Rust for optimal performance and low resource usage.
25
+ - **Easy Setup**: Simple one-command installation and startup.
26
+ - **Docker Support**: Available as a Docker image for containerized deployments.
27
+ - **Flexible Configuration**: Environment variables, CLI options, and `routes.yaml` overrides.
28
+ - **Cross-Platform**: Pre-built binaries for macOS, Linux, and Windows (x64 + arm64).
29
+ - **Integration Ready**: Compatible with reverse proxies like Caddy for HTTPS (and bundles its own `--with-caddy` automation).
26
30
 
27
31
  ## Roadmap
28
32
 
29
33
  ### Shipped ✅
30
34
 
31
- - [x] **Auto Caddy Setup** - One-command bootstrap that generates a Caddyfile for the chosen domain and supervises Caddy alongside fbi-proxy and fbi-auth (`bunx fbi-proxy --with-caddy --with-auth --domain example.dev`). Caddy binary is auto-downloaded from GitHub Releases on first run (SHA-512 verified against the release's `checksums.txt`), cached at `~/.fbi-proxy/bin/caddy`. Set `FBI_CADDY_AUTO_DOWNLOAD=false` to opt out. See [docs/auth/setup.md](lib/fbi-auth/docs/setup.md#automatic-setup-with---with-caddy-phase-3--shipped).
35
+ - [x] **Auto Caddy Setup** One-command bootstrap that generates a Caddyfile for the chosen domain and supervises Caddy alongside fbi-proxy and fbi-auth (`bunx fbi-proxy --with-caddy --with-auth --domain example.dev`). Caddy binary is auto-downloaded from GitHub Releases on first run (SHA-512 verified against the release's `checksums.txt`), cached at `~/.fbi-proxy/bin/caddy`. Set `FBI_CADDY_AUTO_DOWNLOAD=false` to opt out.
36
+ - [x] **Auth Gateway** — Google OAuth, Firebase Auth, and a **zero-config snolab default IdP** (Firebase-based, live on `fbi.com`). Cookie-based SSO across `*.your-domain`. Sliding-window refresh, configurable threshold, JSONL audit log at `~/.config/fbi-proxy/audit.log`. See [lib/fbi-auth/docs/setup.md](lib/fbi-auth/docs/setup.md) and [lib/fbi-auth/docs/snolab.md](lib/fbi-auth/docs/snolab.md).
37
+ - [x] **Rule-based Routing** — `routes.yaml` with placeholder syntax (`{name}`, `{name:int}`, `{name:slug}`, `{name:multi}`). DNS-passthrough, k8s, Docker, and PR-preview recipes in [docs/routing.md](docs/routing.md). Override the bundled defaults with `--routes` or `FBI_PROXY_ROUTES`.
38
+ - [x] **HTTPS Upstream Support** — Route target with an `https://` prefix triggers TLS to upstream via `hyper-rustls` + Mozilla webpki roots. Backward compatible — plain `host:port` still uses HTTP. WebSocket upgrades flip to `wss://` automatically.
39
+ - [x] **Cross-platform Releases** — Every push builds six platforms in parallel (linux x64/arm64, macOS x64/arm64, windows x64/arm64). See [docs/cross-compile-tradeoffs.html](docs/cross-compile-tradeoffs.html).
32
40
 
33
41
  ### Next Up 🚧
34
42
 
35
- - [ ] **Custom Domain Wizard** - Interactive setup that prints the DNS records to add (`*.example.dev → <ip>`) and generates the matching Caddyfile / DNS-01 TLS block
36
- - [ ] **Built-in HTTPS (optional)** - Native TLS termination via rustls + ACME so Caddy becomes optional for simple setups
37
- - [ ] **Configuration File Support** - YAML/JSON config for persistent routing rules
38
- - [ ] **Access Control** - Domain filtering, host/port whitelisting
39
- - [ ] **Request Logging** - Basic access logs for debugging
40
- - [ ] **Health Checks** - Simple upstream service availability monitoring
43
+ - [ ] **Custom Domain Wizard polish** Print the DNS A-records to add (`*.example.dev → <ip>`) and a Caddyfile-with-DNS-01 sample for Cloudflare during `--reconfigure` on a non-fbi.com domain
44
+ - [ ] **Hot Reload** Watch `routes.yaml` and recompile rules without a restart
45
+ - [ ] **Metrics** `/varz`-style counters: requests, 2xx/4xx/5xx, upstream-connect-failures, sessions-issued, sessions-refreshed (Prometheus format)
46
+ - [ ] **Health Checks** Active upstream liveness probes, not just per-request failure detection
47
+ - [ ] **Cloudflare Tunnel / ngrok Integration** — Expose `*.your-domain` publicly without owning a static IP
41
48
 
42
49
  ### Future Improvements 🔮
43
50
 
44
- - [ ] **Load Balancing** - Round-robin between multiple upstream targets
45
- - [ ] **Metrics** - Basic statistics (requests, response times, errors)
46
- - [ ] **Hot Reload** - Update configuration without restart
47
- - [ ] **Custom Headers** - Add/modify headers for specific routes
48
- - [ ] **Cloudflare Tunnel / ngrok Integration** - Expose `*.your-domain` to the public internet without owning a static IP
49
- - [ ] **Auth Gateway** - Built-in basic auth / OIDC so public exposure is safe by default
51
+ - [ ] **Load Balancing** Round-robin between multiple upstream targets for one route
52
+ - [ ] **Custom Headers per route** Beyond `Host:`, add response headers or rewrite request headers
53
+
54
+ ### Won't do
55
+
56
+ - ~~**Built-in HTTPS via rustls + ACME**~~ — Caddy already does this very well, and the `--with-caddy` UX is one extra flag. Adding another ACME client to the Rust binary is more code, more attack surface, and another implementation of a solved problem. Caddy stays the canonical TLS path.
57
+ - ~~**SQLite session storage**~~ — JWT + `sessionSecret` rotation covers the threat model for fbi-proxy's intended scale (solo / small-team self-hosted). See [revoking sessions](lib/fbi-auth/docs/setup.md#revoking-sessions).
50
58
 
51
59
  ## Routing Examples
52
60
 
package/dist/cli.js CHANGED
@@ -5529,8 +5529,40 @@ async function runWizard(prompter, opts) {
5529
5529
  prompter.print("Config preview:");
5530
5530
  prompter.print(JSON.stringify(redact(cfg), null, 2));
5531
5531
  prompter.print("");
5532
+ if (cleanDomain !== "fbi.com") {
5533
+ printCustomDomainHints(prompter, cleanDomain);
5534
+ }
5532
5535
  return cfg;
5533
5536
  }
5537
+ function printCustomDomainHints(prompter, domain) {
5538
+ prompter.print("─── Custom-domain DNS + TLS hints ───");
5539
+ prompter.print("");
5540
+ prompter.print(`Your domain is '${domain}' (not the default fbi.com).`);
5541
+ prompter.print(`You'll need DNS A-records pointing the wildcard + sso host`);
5542
+ prompter.print(`at the public IP of the machine running fbi-proxy:`);
5543
+ prompter.print("");
5544
+ prompter.print(` *.${domain} A <your-public-ip>`);
5545
+ prompter.print(` sso.${domain} A <your-public-ip> (covered by the wildcard,`);
5546
+ prompter.print(` but call it out explicitly)`);
5547
+ prompter.print("");
5548
+ prompter.print(`For wildcard TLS via Let's Encrypt you need DNS-01 (HTTP-01`);
5549
+ prompter.print(`can't issue wildcards). With Cloudflare DNS:`);
5550
+ prompter.print("");
5551
+ prompter.print(` 1. Create a Cloudflare API token with Zone:DNS:Edit on '${domain}'.`);
5552
+ prompter.print(` 2. Export it: CLOUDFLARE_API_TOKEN=...`);
5553
+ prompter.print(` 3. Run with --with-caddy --tls-mode auto. fbi-proxy will generate`);
5554
+ prompter.print(` a Caddyfile that uses Caddy's cloudflare DNS plugin:`);
5555
+ prompter.print("");
5556
+ prompter.print(` *.${domain} {`);
5557
+ prompter.print(` tls { dns cloudflare {env.CLOUDFLARE_API_TOKEN} }`);
5558
+ prompter.print(` reverse_proxy 127.0.0.1:{$FBI_PROXY_PORT}`);
5559
+ prompter.print(` }`);
5560
+ prompter.print("");
5561
+ prompter.print(`If you're just testing locally without public DNS, point your`);
5562
+ prompter.print(`/etc/hosts at 127.0.0.1 and pass --tls-mode internal — Caddy will`);
5563
+ prompter.print(`use its local CA (the cert won't be trusted by other machines).`);
5564
+ prompter.print("");
5565
+ }
5534
5566
  function redact(c) {
5535
5567
  return {
5536
5568
  ...c,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fbi-proxy",
3
- "version": "1.11.0",
3
+ "version": "1.13.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",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/rs/fbi-proxy.rs CHANGED
@@ -10,6 +10,7 @@ use hyper::{Method, Request, Response, StatusCode, Uri};
10
10
  use hyper_tungstenite::{HyperWebsocket, WebSocketStream};
11
11
  use hyper_util::client::legacy::{Client, connect::HttpConnector};
12
12
  use hyper_util::rt::TokioIo;
13
+ use hyper_rustls::HttpsConnector;
13
14
  use log::{error, info};
14
15
  use regex::Regex;
15
16
  use std::convert::Infallible;
@@ -29,7 +30,7 @@ type BoxBody = http_body_util::combinators::BoxBody<Bytes, hyper::Error>;
29
30
  const BUNDLED_ROUTES_YAML: &str = include_str!("../routes.yaml");
30
31
 
31
32
  pub struct FBIProxy {
32
- client: Client<HttpConnector, BoxBody>,
33
+ client: Client<HttpsConnector<HttpConnector>, BoxBody>,
33
34
  number_regex: Regex,
34
35
  domain_filter: Option<String>,
35
36
  compiled_routes: Vec<CompiledRoute>,
@@ -64,12 +65,21 @@ the landing page.
64
65
  */
65
66
  impl FBIProxy {
66
67
  pub fn new(domain_filter: Option<String>, compiled_routes: Vec<CompiledRoute>) -> Self {
67
- let mut connector = HttpConnector::new();
68
- // Set connection timeout to 5 seconds to avoid hanging on invalid hosts
69
- connector.set_connect_timeout(Some(Duration::from_secs(3)));
70
-
71
- let client = Client::builder(hyper_util::rt::TokioExecutor::new())
72
- .build(connector);
68
+ let mut http = HttpConnector::new();
69
+ // Connect timeout avoid hanging on unreachable hosts.
70
+ http.set_connect_timeout(Some(Duration::from_secs(3)));
71
+ // Allow http:// scheme through the HTTPS-enabled connector below.
72
+ http.enforce_http(false);
73
+
74
+ // HttpsConnector handles both http:// and https:// upstream URLs,
75
+ // using Mozilla's webpki root store for TLS validation.
76
+ let https = hyper_rustls::HttpsConnectorBuilder::new()
77
+ .with_webpki_roots()
78
+ .https_or_http()
79
+ .enable_http1()
80
+ .wrap_connector(http);
81
+
82
+ let client = Client::builder(hyper_util::rt::TokioExecutor::new()).build(https);
73
83
 
74
84
  Self {
75
85
  client,
@@ -170,14 +180,14 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
170
180
  }
171
181
 
172
182
  /// 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).
183
+ /// string like `"127.0.0.1:3000"` or `"https://api.github.com:443"`.
184
+ /// This is the default `Host` header value used when a matched rule
185
+ /// doesn't specify an explicit `headers.Host` rewrite.
177
186
  fn host_from_target(target: &str) -> String {
178
- match target.find(':') {
179
- Some(i) => target[..i].to_string(),
180
- None => target.to_string(),
187
+ let authority = parse_target_scheme(target).1;
188
+ match authority.find(':') {
189
+ Some(i) => authority[..i].to_string(),
190
+ None => authority.to_string(),
181
191
  }
182
192
  }
183
193
 
@@ -388,11 +398,16 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
388
398
  .await;
389
399
  }
390
400
 
391
- // Build target URL for HTTP requests
401
+ // Build target URL for HTTP requests. parse_target_scheme handles
402
+ // an optional `http://` / `https://` prefix on the matched target
403
+ // so routes like `target: "https://api.github.com:443"` reach
404
+ // upstream over TLS via the HttpsConnector wired in FBIProxy::new.
392
405
  let uri = req.uri();
406
+ let (scheme, authority) = parse_target_scheme(&target_host);
393
407
  let target_url = format!(
394
- "http://{}{}",
395
- target_host,
408
+ "{}://{}{}",
409
+ scheme,
410
+ authority,
396
411
  uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
397
412
  );
398
413
  let target_uri: Uri = target_url.parse()?;
@@ -469,9 +484,12 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
469
484
  _new_host: &str, // Currently not used for WebSocket connections, but kept for consistency
470
485
  ) -> Result<Response<BoxBody>, BoxError> {
471
486
  let uri = req.uri().clone();
487
+ let (scheme, authority) = parse_target_scheme(target_host);
488
+ let ws_scheme = if scheme == "https" { "wss" } else { "ws" };
472
489
  let ws_url = format!(
473
- "ws://{}{}",
474
- target_host,
490
+ "{}://{}{}",
491
+ ws_scheme,
492
+ authority,
475
493
  uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
476
494
  );
477
495
 
@@ -506,6 +524,21 @@ npx fbi-proxy -d fbi.example.com # Only accept *.fbi.example.com</pre>
506
524
  }
507
525
  }
508
526
 
527
+ /// Parse a route target into (scheme, authority). Supports an optional
528
+ /// `http://` or `https://` prefix; defaults to `http` so existing
529
+ /// `host:port`-style targets keep working unchanged. Used by both the
530
+ /// HTTP forwarder (chooses URL scheme) and the WebSocket upgrade path
531
+ /// (chooses `ws` vs `wss`).
532
+ fn parse_target_scheme(target: &str) -> (&'static str, &str) {
533
+ if let Some(rest) = target.strip_prefix("https://") {
534
+ ("https", rest)
535
+ } else if let Some(rest) = target.strip_prefix("http://") {
536
+ ("http", rest)
537
+ } else {
538
+ ("http", target)
539
+ }
540
+ }
541
+
509
542
  async fn handle_websocket_forwarding(
510
543
  websocket: HyperWebsocket,
511
544
  upstream_ws: WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
@@ -774,3 +807,32 @@ TRY RUN:
774
807
  }
775
808
  });
776
809
  }
810
+
811
+ #[cfg(test)]
812
+ mod tests {
813
+ use super::parse_target_scheme;
814
+
815
+ #[test]
816
+ fn parse_target_scheme_defaults_to_http_with_no_prefix() {
817
+ assert_eq!(parse_target_scheme("localhost:3000"), ("http", "localhost:3000"));
818
+ assert_eq!(parse_target_scheme("api"), ("http", "api"));
819
+ assert_eq!(parse_target_scheme("127.0.0.1:80"), ("http", "127.0.0.1:80"));
820
+ }
821
+
822
+ #[test]
823
+ fn parse_target_scheme_strips_http_prefix() {
824
+ assert_eq!(parse_target_scheme("http://localhost:3000"), ("http", "localhost:3000"));
825
+ }
826
+
827
+ #[test]
828
+ fn parse_target_scheme_strips_https_prefix() {
829
+ assert_eq!(
830
+ parse_target_scheme("https://api.github.com:443"),
831
+ ("https", "api.github.com:443"),
832
+ );
833
+ assert_eq!(
834
+ parse_target_scheme("https://example.dev"),
835
+ ("https", "example.dev"),
836
+ );
837
+ }
838
+ }
@@ -133,9 +133,57 @@ export async function runWizard(
133
133
  prompter.print(JSON.stringify(redact(cfg), null, 2));
134
134
  prompter.print("");
135
135
 
136
+ if (cleanDomain !== "fbi.com") {
137
+ printCustomDomainHints(prompter, cleanDomain);
138
+ }
139
+
136
140
  return cfg;
137
141
  }
138
142
 
143
+ function printCustomDomainHints(prompter: WizardPrompter, domain: string) {
144
+ prompter.print("─── Custom-domain DNS + TLS hints ───");
145
+ prompter.print("");
146
+ prompter.print(`Your domain is '${domain}' (not the default fbi.com).`);
147
+ prompter.print(`You'll need DNS A-records pointing the wildcard + sso host`);
148
+ prompter.print(`at the public IP of the machine running fbi-proxy:`);
149
+ prompter.print("");
150
+ prompter.print(` *.${domain} A <your-public-ip>`);
151
+ prompter.print(
152
+ ` sso.${domain} A <your-public-ip> (covered by the wildcard,`,
153
+ );
154
+ prompter.print(
155
+ ` but call it out explicitly)`,
156
+ );
157
+ prompter.print("");
158
+ prompter.print(`For wildcard TLS via Let's Encrypt you need DNS-01 (HTTP-01`);
159
+ prompter.print(`can't issue wildcards). With Cloudflare DNS:`);
160
+ prompter.print("");
161
+ prompter.print(
162
+ ` 1. Create a Cloudflare API token with Zone:DNS:Edit on '${domain}'.`,
163
+ );
164
+ prompter.print(` 2. Export it: CLOUDFLARE_API_TOKEN=...`);
165
+ prompter.print(
166
+ ` 3. Run with --with-caddy --tls-mode auto. fbi-proxy will generate`,
167
+ );
168
+ prompter.print(` a Caddyfile that uses Caddy's cloudflare DNS plugin:`);
169
+ prompter.print("");
170
+ prompter.print(` *.${domain} {`);
171
+ prompter.print(` tls { dns cloudflare {env.CLOUDFLARE_API_TOKEN} }`);
172
+ prompter.print(` reverse_proxy 127.0.0.1:{$FBI_PROXY_PORT}`);
173
+ prompter.print(` }`);
174
+ prompter.print("");
175
+ prompter.print(
176
+ `If you're just testing locally without public DNS, point your`,
177
+ );
178
+ prompter.print(
179
+ `/etc/hosts at 127.0.0.1 and pass --tls-mode internal — Caddy will`,
180
+ );
181
+ prompter.print(
182
+ `use its local CA (the cert won't be trusted by other machines).`,
183
+ );
184
+ prompter.print("");
185
+ }
186
+
139
187
  function redact(c: AuthConfigShape): AuthConfigShape {
140
188
  return {
141
189
  ...c,
package/ts/routes.test.ts CHANGED
@@ -99,6 +99,28 @@ describe("validateRoute", () => {
99
99
  expect(validateRoute(good)).toEqual({ valid: true });
100
100
  });
101
101
 
102
+ it("accepts an https:// target prefix (R5)", () => {
103
+ expect(
104
+ validateRoute({
105
+ name: "passthrough",
106
+ match: "{anything:multi}",
107
+ target: "https://api.github.com:443",
108
+ headers: { Host: "api.github.com" },
109
+ }),
110
+ ).toEqual({ valid: true });
111
+ });
112
+
113
+ it("accepts an http:// target prefix (R5)", () => {
114
+ expect(
115
+ validateRoute({
116
+ name: "passthrough",
117
+ match: "{anything:multi}",
118
+ target: "http://example.com:80",
119
+ headers: { Host: "example.com" },
120
+ }),
121
+ ).toEqual({ valid: true });
122
+ });
123
+
102
124
  it("rejects empty name", () => {
103
125
  expect(validateRoute({ ...good, name: "" })).toEqual({
104
126
  valid: false,