fbi-proxy 1.10.1 → 1.12.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 +36 -2
- package/package.json +3 -2
- 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 +81 -19
- package/ts/cli.ts +41 -2
- package/ts/routes.test.ts +22 -0
package/dist/cli.js
CHANGED
|
@@ -5960,10 +5960,26 @@ async function startFbiAuth(opts) {
|
|
|
5960
5960
|
console.error("[fbi-auth] --reconfigure requires a TTY (interactive terminal).");
|
|
5961
5961
|
return;
|
|
5962
5962
|
}
|
|
5963
|
+
if (cfg) {
|
|
5964
|
+
console.log(`[fbi-auth] --reconfigure: existing config at ${configPath} will be replaced.`);
|
|
5965
|
+
console.log(`[fbi-auth] previous values used as defaults; press Enter to keep each.`);
|
|
5966
|
+
}
|
|
5963
5967
|
const prompter = readlinePrompter();
|
|
5964
|
-
|
|
5968
|
+
const next = await runWizard(prompter, {
|
|
5969
|
+
domain: opts.domain,
|
|
5970
|
+
existing: cfg
|
|
5971
|
+
});
|
|
5972
|
+
if (cfg) {
|
|
5973
|
+
const changed = changedFields(cfg, next);
|
|
5974
|
+
if (changed.length === 0) {
|
|
5975
|
+
console.log("[fbi-auth] no changes \u2014 skipping write.");
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
console.log(`[fbi-auth] changed fields: ${changed.join(", ")}`);
|
|
5979
|
+
}
|
|
5965
5980
|
console.log(`[fbi-auth] writing config from wizard \u2192 ${configPath}`);
|
|
5966
|
-
await writeConfig(
|
|
5981
|
+
await writeConfig(next, configPath);
|
|
5982
|
+
cfg = next;
|
|
5967
5983
|
} else if (!cfg) {
|
|
5968
5984
|
if (isTty()) {
|
|
5969
5985
|
const prompter = readlinePrompter();
|
|
@@ -6021,3 +6037,21 @@ async function startCaddy(opts) {
|
|
|
6021
6037
|
}
|
|
6022
6038
|
return handle;
|
|
6023
6039
|
}
|
|
6040
|
+
function changedFields(prev, next) {
|
|
6041
|
+
const fields = [
|
|
6042
|
+
"domain",
|
|
6043
|
+
"cookieDomain",
|
|
6044
|
+
"ssoHost",
|
|
6045
|
+
"provider",
|
|
6046
|
+
"clientId",
|
|
6047
|
+
"clientSecret",
|
|
6048
|
+
"firebase",
|
|
6049
|
+
"allowlist"
|
|
6050
|
+
];
|
|
6051
|
+
const changed = [];
|
|
6052
|
+
for (const k of fields) {
|
|
6053
|
+
if (JSON.stringify(prev[k]) !== JSON.stringify(next[k]))
|
|
6054
|
+
changed.push(k);
|
|
6055
|
+
}
|
|
6056
|
+
return changed;
|
|
6057
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fbi-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"test:ui": "vitest --ui",
|
|
47
47
|
"test:e2e": "vitest run e2e",
|
|
48
48
|
"test:e2e:watch": "vitest e2e",
|
|
49
|
-
"test:coverage": "vitest run --coverage"
|
|
49
|
+
"test:coverage": "vitest run --coverage",
|
|
50
|
+
"dl-artifacts": "gh run list --workflow='Build and Release' --branch=main --limit=1 --json databaseId -q '.[0].databaseId' | xargs -I{} gh run download {} --dir release"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"execa": "^9.6.1",
|
|
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
|
|
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
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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"
|
|
174
|
-
/// value used when a matched rule
|
|
175
|
-
/// `headers.Host` rewrite
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
"
|
|
395
|
-
|
|
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
|
-
"
|
|
474
|
-
|
|
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
|
+
}
|
package/ts/cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
helpfulSetupMessage,
|
|
13
13
|
readConfigOrNull,
|
|
14
14
|
writeConfig,
|
|
15
|
+
type AuthConfigShape,
|
|
15
16
|
} from "./auth/authConfig";
|
|
16
17
|
import { spawnFbiAuth, type FbiAuthHandle } from "./auth/spawnFbiAuth";
|
|
17
18
|
import { isTty, readlinePrompter, runWizard } from "./auth/setupWizard";
|
|
@@ -149,10 +150,30 @@ async function startFbiAuth(opts: {
|
|
|
149
150
|
);
|
|
150
151
|
return undefined;
|
|
151
152
|
}
|
|
153
|
+
if (cfg) {
|
|
154
|
+
console.log(
|
|
155
|
+
`[fbi-auth] --reconfigure: existing config at ${configPath} will be replaced.`,
|
|
156
|
+
);
|
|
157
|
+
console.log(
|
|
158
|
+
`[fbi-auth] previous values used as defaults; press Enter to keep each.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
152
161
|
const prompter = readlinePrompter();
|
|
153
|
-
|
|
162
|
+
const next = await runWizard(prompter, {
|
|
163
|
+
domain: opts.domain,
|
|
164
|
+
existing: cfg,
|
|
165
|
+
});
|
|
166
|
+
if (cfg) {
|
|
167
|
+
const changed = changedFields(cfg, next);
|
|
168
|
+
if (changed.length === 0) {
|
|
169
|
+
console.log("[fbi-auth] no changes — skipping write.");
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
console.log(`[fbi-auth] changed fields: ${changed.join(", ")}`);
|
|
173
|
+
}
|
|
154
174
|
console.log(`[fbi-auth] writing config from wizard → ${configPath}`);
|
|
155
|
-
await writeConfig(
|
|
175
|
+
await writeConfig(next, configPath);
|
|
176
|
+
cfg = next;
|
|
156
177
|
} else if (!cfg) {
|
|
157
178
|
if (isTty()) {
|
|
158
179
|
const prompter = readlinePrompter();
|
|
@@ -241,3 +262,21 @@ async function startCaddy(opts: {
|
|
|
241
262
|
}
|
|
242
263
|
return handle;
|
|
243
264
|
}
|
|
265
|
+
|
|
266
|
+
function changedFields(prev: AuthConfigShape, next: AuthConfigShape): string[] {
|
|
267
|
+
const fields: (keyof AuthConfigShape)[] = [
|
|
268
|
+
"domain",
|
|
269
|
+
"cookieDomain",
|
|
270
|
+
"ssoHost",
|
|
271
|
+
"provider",
|
|
272
|
+
"clientId",
|
|
273
|
+
"clientSecret",
|
|
274
|
+
"firebase",
|
|
275
|
+
"allowlist",
|
|
276
|
+
];
|
|
277
|
+
const changed: string[] = [];
|
|
278
|
+
for (const k of fields) {
|
|
279
|
+
if (JSON.stringify(prev[k]) !== JSON.stringify(next[k])) changed.push(k);
|
|
280
|
+
}
|
|
281
|
+
return changed;
|
|
282
|
+
}
|
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,
|