fbi-proxy 1.11.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fbi-proxy",
3
- "version": "1.11.0",
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",
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
+ }
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,