fbi-proxy 1.5.0 → 1.6.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 +34 -18
- package/dist/cli.js +5304 -0
- package/package.json +56 -52
- 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 +461 -0
- package/ts/buildFbiProxy.ts +10 -4
- package/ts/cli.ts +3 -3
- package/ts/getProxyFilename.ts +1 -1
package/package.json
CHANGED
|
@@ -1,66 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fbi-proxy",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"semantic-release": "^22.0.0"
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "FBI-Proxy provides easy HTTPS access to your local services with intelligent domain routing",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"development-tools",
|
|
7
|
+
"http-proxy",
|
|
8
|
+
"proxy",
|
|
9
|
+
"reverse-proxy",
|
|
10
|
+
"websocket"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/snomiao/fbi-proxy#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/snomiao/fbi-proxy/issues"
|
|
16
15
|
},
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/snomiao/fbi-proxy.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "snomiao",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"fbi-proxy": "dist/cli.js"
|
|
20
25
|
},
|
|
21
26
|
"files": [
|
|
27
|
+
"Caddyfile",
|
|
28
|
+
"dist",
|
|
22
29
|
"release",
|
|
23
30
|
"rs",
|
|
24
|
-
"ts"
|
|
25
|
-
"dist",
|
|
26
|
-
"Caddyfile"
|
|
27
|
-
],
|
|
28
|
-
"trustedDependencies": [
|
|
29
|
-
"@radically-straightforward/caddy"
|
|
31
|
+
"ts"
|
|
30
32
|
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "bun run build:rs && bun run build:js",
|
|
35
|
+
"build:js": "bun build ./ts/cli.ts --outdir dist --target node",
|
|
36
|
+
"build:ts": "tsc --noEmit",
|
|
37
|
+
"build:rs": "cargo build --release",
|
|
38
|
+
"dev": "bun --hot ts/cli.ts --dev",
|
|
39
|
+
"dev:rs": "cargo watch -x run",
|
|
40
|
+
"format": "prettier --write .",
|
|
41
|
+
"lint": "prettier --check .",
|
|
42
|
+
"prepare": "husky",
|
|
43
|
+
"start": "bun ts/cli.ts",
|
|
44
|
+
"start:rs": "./target/release/fbi-proxy"
|
|
45
|
+
},
|
|
31
46
|
"dependencies": {
|
|
32
|
-
"@radically-straightforward/caddy": "^2.0.6",
|
|
33
|
-
"bun": "^1.2.19",
|
|
34
47
|
"execa": "^9.6.0",
|
|
35
48
|
"from-node-stream": "^0.1.0",
|
|
36
49
|
"get-port": "^7.1.0",
|
|
37
50
|
"hot-memo": "^1.1.1",
|
|
38
|
-
"
|
|
39
|
-
"npm-run-all": "^4.1.5",
|
|
40
|
-
"phpdie": "^1.7.0",
|
|
41
|
-
"promise-all-properties": "^5.0.0",
|
|
42
|
-
"sflow": "^1.23.0",
|
|
43
|
-
"ts-toolbelt": "^9.6.0",
|
|
51
|
+
"sflow": "^1.24.5",
|
|
44
52
|
"tsa-composer": "^1.0.0",
|
|
45
|
-
"yargs": "^17.7.2"
|
|
46
|
-
"zx": "^8.7.2"
|
|
53
|
+
"yargs": "^17.7.2"
|
|
47
54
|
},
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"start:rs": "./target/release/fbi-proxy",
|
|
63
|
-
"prepare": "husky"
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
57
|
+
"@semantic-release/git": "^10.0.1",
|
|
58
|
+
"@semantic-release/github": "^9.2.0",
|
|
59
|
+
"@types/bun": "latest",
|
|
60
|
+
"@types/minimist": "^1.2.5",
|
|
61
|
+
"husky": "^9.1.7",
|
|
62
|
+
"lint-staged": "^16.1.2",
|
|
63
|
+
"prettier": "^3.6.2",
|
|
64
|
+
"semantic-release": "^22.0.0",
|
|
65
|
+
"semantic-release-cargo": "^2.4.0"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18.0.0"
|
|
64
69
|
},
|
|
65
70
|
"lint-staged": {
|
|
66
71
|
"*.{ts,js}": [
|
|
@@ -70,8 +75,7 @@
|
|
|
70
75
|
"bun --bun prettier --write"
|
|
71
76
|
]
|
|
72
77
|
},
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
"version": "1.5.0"
|
|
78
|
+
"trustedDependencies": [
|
|
79
|
+
"@radically-straightforward/caddy"
|
|
80
|
+
]
|
|
77
81
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/rs/fbi-proxy.rs
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
use clap::{Arg, Command};
|
|
2
|
+
use futures_util::{SinkExt, StreamExt};
|
|
3
|
+
use hyper::header::{HeaderValue, HOST};
|
|
4
|
+
use hyper::service::{make_service_fn, service_fn};
|
|
5
|
+
use hyper::{Body, Client, Request, Response, Server, StatusCode, Uri};
|
|
6
|
+
use hyper_tungstenite::{HyperWebsocket, WebSocketStream};
|
|
7
|
+
use log::{error, info};
|
|
8
|
+
use regex::Regex;
|
|
9
|
+
use std::convert::Infallible;
|
|
10
|
+
use std::net::SocketAddr;
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
use tokio_tungstenite::connect_async;
|
|
13
|
+
|
|
14
|
+
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
|
15
|
+
|
|
16
|
+
pub struct FBIProxy {
|
|
17
|
+
client: Client<hyper::client::HttpConnector>,
|
|
18
|
+
number_regex: Regex,
|
|
19
|
+
domain_filter: Option<String>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
FBIProxy is a simple HTTP and WebSocket proxy server that supports port encoding in the Host header.
|
|
24
|
+
|
|
25
|
+
parse incoming Host headers and convert them to a target URL format:
|
|
26
|
+
|
|
27
|
+
for localhost, it uses "localhost"
|
|
28
|
+
|
|
29
|
+
rule1: number host goes to local port
|
|
30
|
+
- Host="3000" => localhost:3000
|
|
31
|
+
|
|
32
|
+
rule1.2 host--port goes to host:port
|
|
33
|
+
- Host="localhost--3000" => localhost:3000
|
|
34
|
+
- Host="sur--3000" => sur:3000
|
|
35
|
+
|
|
36
|
+
rule2: other host goes to that host:80
|
|
37
|
+
- localhost => proxy to http://localhost
|
|
38
|
+
- amd => proxy to http://amd
|
|
39
|
+
|
|
40
|
+
rule3: subdomains are hoisted
|
|
41
|
+
- 3000.localhost => proxies to http://localhost:80, with host: 3000
|
|
42
|
+
- 3000.amd => proxies to http://amd:80, with host: 3000
|
|
43
|
+
- sur.amd => proxies to http://amd:80, with host: sur
|
|
44
|
+
- amd.sur.amd => proxies to http://amd:80, with host: amd.sur
|
|
45
|
+
- if sur also runs fbi-proxy, it will proxies to http://amd:80, with host: amd
|
|
46
|
+
- 3000.sur.amd => proxies to http://amd:80, with host: 3000.sur
|
|
47
|
+
|
|
48
|
+
for subdomains
|
|
49
|
+
*.amd => localhost:amd
|
|
50
|
+
|
|
51
|
+
*/
|
|
52
|
+
impl FBIProxy {
|
|
53
|
+
pub fn new(domain_filter: Option<String>) -> Self {
|
|
54
|
+
Self {
|
|
55
|
+
client: Client::new(),
|
|
56
|
+
number_regex: Regex::new(r"^\d+$").unwrap(),
|
|
57
|
+
domain_filter,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn parse_host(&self, host_header: &str, domain_filter: &Option<String>) -> Option<(String, String)> {
|
|
62
|
+
// Remove port if present (e.g., "localhost:8080" -> "localhost")
|
|
63
|
+
let host_without_port = if let Some(colon_pos) = host_header.find(':') {
|
|
64
|
+
&host_header[..colon_pos]
|
|
65
|
+
} else {
|
|
66
|
+
host_header
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Apply domain filter if specified
|
|
70
|
+
let host = if let Some(domain) = domain_filter {
|
|
71
|
+
if !domain.is_empty() {
|
|
72
|
+
// Check if host ends with the domain filter
|
|
73
|
+
if host_without_port.ends_with(domain) {
|
|
74
|
+
// Strip the domain suffix (including the dot)
|
|
75
|
+
let prefix_len = host_without_port.len() - domain.len();
|
|
76
|
+
if prefix_len > 0 && host_without_port.chars().nth(prefix_len - 1) == Some('.') {
|
|
77
|
+
// Remove the domain and the dot before it
|
|
78
|
+
&host_without_port[..prefix_len - 1]
|
|
79
|
+
} else if prefix_len == 0 {
|
|
80
|
+
// The host is exactly the domain, treat as root
|
|
81
|
+
"@"
|
|
82
|
+
} else {
|
|
83
|
+
// No dot separator, invalid format
|
|
84
|
+
return None;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Host doesn't match domain filter
|
|
88
|
+
return None;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Empty domain filter, accept all
|
|
92
|
+
host_without_port
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// No domain filter, accept all
|
|
96
|
+
host_without_port
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Handle special case: @ means root domain was accessed
|
|
100
|
+
if host == "@" {
|
|
101
|
+
return Some(("localhost:80".to_string(), "localhost".to_string()));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Rule 1: number host goes to local port (e.g., "3000" => "localhost:3000")
|
|
105
|
+
if self.number_regex.is_match(host) {
|
|
106
|
+
return Some((format!("localhost:{}", host), "localhost".to_string()));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Rule 1.2: host--port goes to host:port (e.g., "localhost--3000" => "localhost:3000")
|
|
110
|
+
if let Some(double_dash_pos) = host.find("--") {
|
|
111
|
+
let hostname = &host[..double_dash_pos];
|
|
112
|
+
let port = &host[double_dash_pos + 2..];
|
|
113
|
+
return Some((format!("{}:{}", hostname, port), hostname.to_string()));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Rule 3: subdomains are hoisted
|
|
117
|
+
let parts: Vec<&str> = host.split('.').collect();
|
|
118
|
+
if parts.len() > 1 {
|
|
119
|
+
// The last part is the main domain, everything before is subdomain
|
|
120
|
+
let main_domain = parts.last().unwrap();
|
|
121
|
+
let subdomain_parts = &parts[..parts.len() - 1];
|
|
122
|
+
let subdomain = subdomain_parts.join(".");
|
|
123
|
+
|
|
124
|
+
// Target is the main domain on port 80
|
|
125
|
+
let target_host = format!("{}:80", main_domain);
|
|
126
|
+
// New host header is the subdomain
|
|
127
|
+
return Some((target_host, subdomain.to_string()));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Rule 2: other host goes to that host:80 (e.g., "localhost" => "localhost:80")
|
|
131
|
+
Some((format!("{}:80", host), host.to_string()))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
pub async fn handle_request(&self, mut req: Request<Body>) -> Result<Response<Body>, BoxError> {
|
|
135
|
+
// Extract host from headers and process according to rules
|
|
136
|
+
let host_header = req
|
|
137
|
+
.headers()
|
|
138
|
+
.get(HOST)
|
|
139
|
+
.and_then(|h| h.to_str().ok())
|
|
140
|
+
.unwrap_or("localhost")
|
|
141
|
+
.to_string();
|
|
142
|
+
|
|
143
|
+
// Parse host with domain filtering
|
|
144
|
+
let parsed_host = self.parse_host(&host_header, &self.domain_filter);
|
|
145
|
+
|
|
146
|
+
// If domain filter rejects the host, return 502 Bad Gateway
|
|
147
|
+
let (target_host, new_host) = match parsed_host {
|
|
148
|
+
Some(hosts) => hosts,
|
|
149
|
+
None => {
|
|
150
|
+
let method = req.method();
|
|
151
|
+
let uri = req.uri();
|
|
152
|
+
info!(
|
|
153
|
+
"{} {} => REJECTED{} 502",
|
|
154
|
+
method,
|
|
155
|
+
host_header,
|
|
156
|
+
uri
|
|
157
|
+
);
|
|
158
|
+
return Ok(Response::builder()
|
|
159
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
160
|
+
.body(Body::from("Bad Gateway: Host not allowed"))?);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
let method = req.method().clone();
|
|
165
|
+
let original_uri = req.uri().clone();
|
|
166
|
+
|
|
167
|
+
// Handle WebSocket upgrade requests
|
|
168
|
+
if hyper_tungstenite::is_upgrade_request(&req) {
|
|
169
|
+
return self
|
|
170
|
+
.handle_websocket_upgrade(req, &target_host, &new_host)
|
|
171
|
+
.await;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build target URL for HTTP requests
|
|
175
|
+
let uri = req.uri();
|
|
176
|
+
let target_url = format!(
|
|
177
|
+
"http://{}{}",
|
|
178
|
+
target_host,
|
|
179
|
+
uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
|
|
180
|
+
);
|
|
181
|
+
let target_uri: Uri = target_url.parse()?;
|
|
182
|
+
|
|
183
|
+
// Update request URI and headers
|
|
184
|
+
*req.uri_mut() = target_uri;
|
|
185
|
+
req.headers_mut()
|
|
186
|
+
.insert(HOST, HeaderValue::from_str(&new_host)?);
|
|
187
|
+
req.headers_mut().remove("content-encoding");
|
|
188
|
+
|
|
189
|
+
// Forward the request
|
|
190
|
+
match self.client.request(req).await {
|
|
191
|
+
Ok(mut response) => {
|
|
192
|
+
// Remove content-encoding header from response
|
|
193
|
+
response.headers_mut().remove("content-encoding");
|
|
194
|
+
let status = response.status();
|
|
195
|
+
info!(
|
|
196
|
+
"{} {}@{}{} {}",
|
|
197
|
+
method,
|
|
198
|
+
host_header,
|
|
199
|
+
target_host,
|
|
200
|
+
original_uri,
|
|
201
|
+
status.as_u16()
|
|
202
|
+
);
|
|
203
|
+
Ok(response)
|
|
204
|
+
}
|
|
205
|
+
Err(e) => {
|
|
206
|
+
error!(
|
|
207
|
+
"{} {}@{}{} 502 ({})",
|
|
208
|
+
method,
|
|
209
|
+
host_header,
|
|
210
|
+
target_host,
|
|
211
|
+
original_uri,
|
|
212
|
+
e
|
|
213
|
+
);
|
|
214
|
+
Ok(Response::builder()
|
|
215
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
216
|
+
.body(Body::from("FBIPROXY ERROR"))?)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async fn handle_websocket_upgrade(
|
|
222
|
+
&self,
|
|
223
|
+
req: Request<Body>,
|
|
224
|
+
target_host: &str,
|
|
225
|
+
_new_host: &str, // Currently not used for WebSocket connections, but kept for consistency
|
|
226
|
+
) -> Result<Response<Body>, BoxError> {
|
|
227
|
+
let uri = req.uri().clone();
|
|
228
|
+
let ws_url = format!(
|
|
229
|
+
"ws://{}{}",
|
|
230
|
+
target_host,
|
|
231
|
+
uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Upgrade the HTTP connection to WebSocket
|
|
235
|
+
let (response, websocket) = hyper_tungstenite::upgrade(req, None)?;
|
|
236
|
+
|
|
237
|
+
// Connect to upstream WebSocket
|
|
238
|
+
let (upstream_ws, _) = match connect_async(&ws_url).await {
|
|
239
|
+
Ok(ws) => ws,
|
|
240
|
+
Err(e) => {
|
|
241
|
+
error!("WS :ws:{} => :ws:{}{} 502 ({})", target_host, target_host, uri, e);
|
|
242
|
+
return Ok(Response::builder()
|
|
243
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
244
|
+
.body(Body::from("WebSocket connection failed"))?);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Spawn task to handle WebSocket forwarding
|
|
249
|
+
// let ws_url_clone = ws_url.clone();
|
|
250
|
+
tokio::spawn(async move {
|
|
251
|
+
if let Err(e) = handle_websocket_forwarding(websocket, upstream_ws).await {
|
|
252
|
+
error!("WebSocket forwarding error: {}", e);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
info!("WS :ws:{} => :ws:{}{} 101", target_host, target_host, uri);
|
|
257
|
+
Ok(response)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async fn handle_websocket_forwarding(
|
|
262
|
+
websocket: HyperWebsocket,
|
|
263
|
+
upstream_ws: WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
|
|
264
|
+
) -> Result<(), BoxError> {
|
|
265
|
+
// Get the client WebSocket stream
|
|
266
|
+
let client_ws = websocket.await?;
|
|
267
|
+
|
|
268
|
+
let (mut client_sink, mut client_stream) = client_ws.split();
|
|
269
|
+
let (mut upstream_sink, mut upstream_stream) = upstream_ws.split();
|
|
270
|
+
|
|
271
|
+
// Forward messages from client to upstream
|
|
272
|
+
let client_to_upstream = async {
|
|
273
|
+
while let Some(msg) = client_stream.next().await {
|
|
274
|
+
match msg {
|
|
275
|
+
Ok(msg) => {
|
|
276
|
+
if let Err(_) = upstream_sink.send(msg).await {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
Err(_) => break,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Forward messages from upstream to client
|
|
286
|
+
let upstream_to_client = async {
|
|
287
|
+
while let Some(msg) = upstream_stream.next().await {
|
|
288
|
+
match msg {
|
|
289
|
+
Ok(msg) => {
|
|
290
|
+
if let Err(_) = client_sink.send(msg).await {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
Err(_) => break,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Run both forwarding tasks concurrently
|
|
300
|
+
tokio::select! {
|
|
301
|
+
_ = client_to_upstream => {},
|
|
302
|
+
_ = upstream_to_client => {},
|
|
303
|
+
}
|
|
304
|
+
Ok(())
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async fn handle_connection(
|
|
308
|
+
req: Request<Body>,
|
|
309
|
+
proxy: Arc<FBIProxy>,
|
|
310
|
+
) -> Result<Response<Body>, Infallible> {
|
|
311
|
+
match proxy.handle_request(req).await {
|
|
312
|
+
Ok(response) => Ok(response),
|
|
313
|
+
Err(e) => {
|
|
314
|
+
error!("Request handling error: {}", e);
|
|
315
|
+
Ok(Response::builder()
|
|
316
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
317
|
+
.body(Body::from("Internal Server Error"))
|
|
318
|
+
.unwrap())
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
pub async fn start_proxy_server(port: u16) -> Result<(), BoxError> {
|
|
324
|
+
start_proxy_server_with_options("127.0.0.1", port, None).await
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
pub async fn start_proxy_server_with_host(host: &str, port: u16) -> Result<(), BoxError> {
|
|
328
|
+
start_proxy_server_with_options(host, port, None).await
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
pub async fn start_proxy_server_with_options(host: &str, port: u16, domain_filter: Option<String>) -> Result<(), BoxError> {
|
|
332
|
+
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
|
333
|
+
let proxy = Arc::new(FBIProxy::new(domain_filter.clone()));
|
|
334
|
+
|
|
335
|
+
let make_svc = make_service_fn(move |_conn: &hyper::server::conn::AddrStream| {
|
|
336
|
+
let proxy = proxy.clone();
|
|
337
|
+
async move { Ok::<_, Infallible>(service_fn(move |req| handle_connection(req, proxy.clone()))) }
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
let server = Server::bind(&addr).serve(make_svc);
|
|
341
|
+
|
|
342
|
+
info!("FBI Proxy server running on http://{}", addr);
|
|
343
|
+
println!("FBI Proxy listening on: http://{}", addr);
|
|
344
|
+
if let Some(ref domain) = domain_filter {
|
|
345
|
+
if !domain.is_empty() {
|
|
346
|
+
println!("Domain filter: Only accepting requests for *.{}", domain);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
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");
|
|
350
|
+
println!(" This proxy is production ready but requires proper security measures.");
|
|
351
|
+
|
|
352
|
+
info!("Features: HTTP proxying + WebSocket forwarding + Port encoding + Domain filtering");
|
|
353
|
+
|
|
354
|
+
if let Err(e) = server.await {
|
|
355
|
+
error!("Server error: {}", e);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
Ok(())
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn main() {
|
|
362
|
+
env_logger::init();
|
|
363
|
+
|
|
364
|
+
let matches = Command::new("fbi-proxy")
|
|
365
|
+
.version("0.1.1")
|
|
366
|
+
.about("A fast and flexible proxy server with smart host header parsing and WebSocket support")
|
|
367
|
+
.long_about(
|
|
368
|
+
"FBI Proxy - A any-host-port reverse-proxy server with intelligent host header parsing
|
|
369
|
+
|
|
370
|
+
FEATURES:
|
|
371
|
+
• HTTP and WebSocket proxying with bidirectional forwarding
|
|
372
|
+
• Smart host header parsing with multiple routing rules
|
|
373
|
+
• Port encoding support for easy local development
|
|
374
|
+
• Subdomain hoisting for multi-service architectures
|
|
375
|
+
|
|
376
|
+
HOST PARSING RULES:
|
|
377
|
+
1. Number host → local port: '3000' → localhost:3000
|
|
378
|
+
2. Host--port syntax: 'api--3000' → api:3000
|
|
379
|
+
3. Subdomain hoisting: 'api.service' → service:80 (host: api)
|
|
380
|
+
4. Default routing: 'localhost' → localhost:80
|
|
381
|
+
|
|
382
|
+
ENVIRONMENT VARIABLES:
|
|
383
|
+
FBI_PROXY_PORT Port to listen on (default: 2432)
|
|
384
|
+
FBI_PROXY_HOST Host/IP address to bind to (default: 127.0.0.1)
|
|
385
|
+
FBI_PROXY_DOMAIN Domain filter (only accept *.domain requests)
|
|
386
|
+
RUST_LOG Log level (error, warn, info, debug, trace)
|
|
387
|
+
|
|
388
|
+
EXAMPLES:
|
|
389
|
+
fbi-proxy # Start on 127.0.0.1:2432, accept all
|
|
390
|
+
fbi-proxy -p 8080 # Custom port
|
|
391
|
+
fbi-proxy -h 0.0.0.0 -p 3000 # Bind to all interfaces
|
|
392
|
+
fbi-proxy -d example.com # Only accept *.example.com requests
|
|
393
|
+
FBI_PROXY_PORT=8080 fbi-proxy # Use environment variable
|
|
394
|
+
|
|
395
|
+
TRY RUN:
|
|
396
|
+
# HOST_A:
|
|
397
|
+
npx serve --port 3000
|
|
398
|
+
fbi-proxy -h 0.0.0.0 -p 2432
|
|
399
|
+
|
|
400
|
+
# HOST_B:
|
|
401
|
+
curl http://HOST_A:2432 -H 'Host: localhost--3000'
|
|
402
|
+
"
|
|
403
|
+
)
|
|
404
|
+
.arg(
|
|
405
|
+
Arg::new("port")
|
|
406
|
+
.short('p')
|
|
407
|
+
.long("port")
|
|
408
|
+
.value_name("PORT")
|
|
409
|
+
.help("Port to listen on (env: FBI_PROXY_PORT, default: 2432)")
|
|
410
|
+
.env("FBI_PROXY_PORT")
|
|
411
|
+
.default_value("2432")
|
|
412
|
+
)
|
|
413
|
+
.arg(
|
|
414
|
+
Arg::new("host")
|
|
415
|
+
.short('h')
|
|
416
|
+
.long("host")
|
|
417
|
+
.value_name("HOST")
|
|
418
|
+
.help("Host/IP address to bind to (env: FBI_PROXY_HOST, default: 127.0.0.1)")
|
|
419
|
+
.env("FBI_PROXY_HOST")
|
|
420
|
+
.default_value("127.0.0.1")
|
|
421
|
+
)
|
|
422
|
+
.arg(
|
|
423
|
+
Arg::new("domain")
|
|
424
|
+
.short('d')
|
|
425
|
+
.long("domain")
|
|
426
|
+
.value_name("DOMAIN")
|
|
427
|
+
.help("Domain filter - only accept requests for *.domain (env: FBI_PROXY_DOMAIN)")
|
|
428
|
+
.env("FBI_PROXY_DOMAIN")
|
|
429
|
+
.default_value("")
|
|
430
|
+
)
|
|
431
|
+
.get_matches();
|
|
432
|
+
|
|
433
|
+
let port = matches
|
|
434
|
+
.get_one::<String>("port")
|
|
435
|
+
.unwrap()
|
|
436
|
+
.parse::<u16>()
|
|
437
|
+
.unwrap_or_else(|_| {
|
|
438
|
+
error!("Invalid port value, using default 2432");
|
|
439
|
+
2432
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
let host = matches.get_one::<String>("host").unwrap();
|
|
443
|
+
let domain = matches.get_one::<String>("domain").unwrap();
|
|
444
|
+
|
|
445
|
+
let domain_filter = if domain.is_empty() {
|
|
446
|
+
None
|
|
447
|
+
} else {
|
|
448
|
+
Some(domain.clone())
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
452
|
+
rt.block_on(async {
|
|
453
|
+
info!(
|
|
454
|
+
"Starting FBI-Proxy on {}:{} with domain filter: {:?}",
|
|
455
|
+
host, port, domain_filter
|
|
456
|
+
);
|
|
457
|
+
if let Err(e) = start_proxy_server_with_options(host, port, domain_filter).await {
|
|
458
|
+
error!("Failed to start proxy server: {}", e);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
package/ts/buildFbiProxy.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import fsp from "fs/promises";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
|
-
import {
|
|
3
|
+
import { getFbiProxyFilename } from "./getProxyFilename";
|
|
4
4
|
import { copyFile } from "fs/promises";
|
|
5
5
|
import { $ } from "./dSpawn";
|
|
6
6
|
import { mkdir } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
if (import.meta.main) {
|
|
9
|
-
await
|
|
9
|
+
await getFbiProxyBinary();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export async function
|
|
12
|
+
export async function getFbiProxyBinary({ rebuild = false } = {}) {
|
|
13
13
|
const isWin = process.platform === "win32";
|
|
14
|
-
const binaryName =
|
|
14
|
+
const binaryName = getFbiProxyFilename();
|
|
15
|
+
|
|
16
|
+
// Check for pre-built binary in Docker container
|
|
17
|
+
const dockerBinary = "/app/bin/fbi-proxy";
|
|
18
|
+
if (!rebuild && existsSync(dockerBinary)) {
|
|
19
|
+
return dockerBinary;
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
const release = "./release/" + binaryName;
|
|
17
23
|
const built = `./target/release/fbi-proxy${isWin ? ".exe" : ""}`;
|
package/ts/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import hotMemo from "hot-memo";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import yargs from "yargs";
|
|
6
6
|
import { hideBin } from "yargs/helpers";
|
|
7
|
-
import {
|
|
7
|
+
import { getFbiProxyBinary } from "./buildFbiProxy";
|
|
8
8
|
import { $ } from "./dSpawn";
|
|
9
9
|
import { downloadCaddy } from "./downloadCaddy";
|
|
10
10
|
import { execa } from "execa";
|
|
@@ -37,7 +37,7 @@ const FBIHOST = argv.fbihost;
|
|
|
37
37
|
const FBIPROXY_PORT = String(await getPort({ port: 2432 }));
|
|
38
38
|
|
|
39
39
|
const proxyProcess = await hotMemo(async () => {
|
|
40
|
-
const proxy = await
|
|
40
|
+
const proxy = await getFbiProxyBinary();
|
|
41
41
|
console.log("Starting Rust proxy server");
|
|
42
42
|
const p = $.opt({
|
|
43
43
|
env: {
|
|
@@ -74,7 +74,7 @@ if (argv.caddy) {
|
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
console.log("
|
|
77
|
+
console.log("All services started successfully!");
|
|
78
78
|
// show process pids
|
|
79
79
|
console.log(`Proxy server PID: ${proxyProcess.pid}`);
|
|
80
80
|
if (caddyProcess) {
|
package/ts/getProxyFilename.ts
CHANGED