fbi-proxy 1.0.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/rs/Cargo.toml ADDED
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "fbi-proxy"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [[bin]]
7
+ name = "proxy"
8
+ path = "proxy.rs"
9
+
10
+ [dependencies]
11
+ # Using simpler dependencies that build reliably on Windows
12
+ hyper = { version = "0.14", features = ["full"] }
13
+ hyper-tungstenite = "0.11"
14
+ tokio = { version = "1.0", features = ["full"] }
15
+ tokio-tungstenite = "0.20"
16
+ futures-util = "0.3"
17
+ regex = "1.0"
18
+ env_logger = "0.10"
19
+ log = "0.4"
@@ -0,0 +1,60 @@
1
+ @echo off
2
+ echo Installing required dependencies for Windows build...
3
+
4
+ echo.
5
+ echo Checking for required tools...
6
+
7
+ where cmake >nul 2>&1
8
+ if %errorlevel% neq 0 (
9
+ echo ❌ CMake not found. Installing via chocolatey...
10
+ where choco >nul 2>&1
11
+ if %errorlevel% neq 0 (
12
+ echo Please install Chocolatey first: https://chocolatey.org/install
13
+ echo Or install CMake manually: https://cmake.org/download/
14
+ pause
15
+ exit /b 1
16
+ )
17
+ choco install cmake -y
18
+ ) else (
19
+ echo ✅ CMake found
20
+ )
21
+
22
+ where perl >nul 2>&1
23
+ if %errorlevel% neq 0 (
24
+ echo ❌ Perl not found. Installing Strawberry Perl...
25
+ where choco >nul 2>&1
26
+ if %errorlevel% neq 0 (
27
+ echo Please install Chocolatey first or install Strawberry Perl manually
28
+ echo Strawberry Perl: https://strawberryperl.com/
29
+ pause
30
+ exit /b 1
31
+ )
32
+ choco install strawberryperl -y
33
+ ) else (
34
+ echo ✅ Perl found
35
+ )
36
+
37
+ echo.
38
+ echo Setting environment variables for Windows build...
39
+ set OPENSSL_NO_VENDOR=1
40
+ set CMAKE_GENERATOR=Visual Studio 17 2022
41
+
42
+ echo.
43
+ echo Building Rust proxy with Pingora...
44
+ cargo build --release
45
+
46
+ if %errorlevel% equ 0 (
47
+ echo.
48
+ echo ✅ Build successful!
49
+ echo Binary location: target\release\proxy.exe
50
+ ) else (
51
+ echo.
52
+ echo ❌ Build failed. Check the error messages above.
53
+ echo.
54
+ echo Common solutions:
55
+ echo 1. Restart your terminal after installing dependencies
56
+ echo 2. Make sure Visual Studio Build Tools are installed
57
+ echo 3. Try: cargo clean && cargo build --release
58
+ )
59
+
60
+ pause
package/rs/proxy.rs ADDED
@@ -0,0 +1,248 @@
1
+ use futures_util::{SinkExt, StreamExt};
2
+ use hyper::header::{HeaderValue, HOST};
3
+ use hyper::service::{make_service_fn, service_fn};
4
+ use hyper::{Body, Client, Request, Response, Server, StatusCode, Uri};
5
+ use hyper_tungstenite::{HyperWebsocket, WebSocketStream};
6
+ use log::{error, info};
7
+ use regex::Regex;
8
+ use std::convert::Infallible;
9
+ use std::net::SocketAddr;
10
+ use std::sync::Arc;
11
+ use tokio_tungstenite::connect_async;
12
+
13
+ type BoxError = Box<dyn std::error::Error + Send + Sync>;
14
+
15
+ pub struct FBIProxy {
16
+ client: Client<hyper::client::HttpConnector>,
17
+ port_regex: Regex,
18
+ }
19
+
20
+ impl FBIProxy {
21
+ pub fn new() -> Self {
22
+ Self {
23
+ client: Client::new(),
24
+ port_regex: Regex::new(r"--(\d+).*$").unwrap(),
25
+ }
26
+ }
27
+
28
+ fn extract_target_host(&self, host_header: &str) -> String {
29
+ self.port_regex.replace(host_header, ":$1").to_string()
30
+ }
31
+
32
+ pub async fn handle_request(&self, mut req: Request<Body>) -> Result<Response<Body>, BoxError> {
33
+ // Extract host from headers and process port encoding
34
+ let host_header = req
35
+ .headers()
36
+ .get(HOST)
37
+ .and_then(|h| h.to_str().ok())
38
+ .unwrap_or("localhost");
39
+
40
+ let target_host = self.extract_target_host(host_header);
41
+ info!("Proxying {} {} -> {}", req.method(), req.uri(), target_host);
42
+
43
+ // Handle WebSocket upgrade requests
44
+ if hyper_tungstenite::is_upgrade_request(&req) {
45
+ return self.handle_websocket_upgrade(req, &target_host).await;
46
+ }
47
+
48
+ // Build target URL for HTTP requests
49
+ let uri = req.uri();
50
+ let target_url = format!(
51
+ "http://{}{}",
52
+ target_host,
53
+ uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
54
+ );
55
+ let target_uri: Uri = target_url.parse()?;
56
+
57
+ // Update request URI and headers
58
+ *req.uri_mut() = target_uri;
59
+ req.headers_mut()
60
+ .insert(HOST, HeaderValue::from_str("localhost")?);
61
+ req.headers_mut().remove("content-encoding");
62
+
63
+ // Forward the request
64
+ match self.client.request(req).await {
65
+ Ok(mut response) => {
66
+ // Remove content-encoding header from response
67
+ response.headers_mut().remove("content-encoding");
68
+ info!("HTTP {} -> {}", target_url, response.status());
69
+ Ok(response)
70
+ }
71
+ Err(e) => {
72
+ error!("Proxy error for {}: {}", target_url, e);
73
+ Ok(Response::builder()
74
+ .status(StatusCode::BAD_GATEWAY)
75
+ .body(Body::from("Gateway Error"))?)
76
+ }
77
+ }
78
+ }
79
+
80
+ async fn handle_websocket_upgrade(
81
+ &self,
82
+ req: Request<Body>,
83
+ target_host: &str,
84
+ ) -> Result<Response<Body>, BoxError> {
85
+ let uri = req.uri();
86
+ let ws_url = format!(
87
+ "ws://{}{}",
88
+ target_host,
89
+ uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/")
90
+ );
91
+
92
+ info!("WebSocket upgrade to: {}", ws_url);
93
+
94
+ // Upgrade the HTTP connection to WebSocket
95
+ let (response, websocket) = hyper_tungstenite::upgrade(req, None)?;
96
+
97
+ // Connect to upstream WebSocket
98
+ info!("Connecting to upstream WebSocket: {}", ws_url);
99
+ let (upstream_ws, _) = match connect_async(&ws_url).await {
100
+ Ok(ws) => ws,
101
+ Err(e) => {
102
+ error!("Failed to connect to upstream WebSocket {}: {}", ws_url, e);
103
+ return Ok(Response::builder()
104
+ .status(StatusCode::BAD_GATEWAY)
105
+ .body(Body::from("WebSocket connection failed"))?);
106
+ }
107
+ };
108
+
109
+ // Spawn task to handle WebSocket forwarding
110
+ info!("Starting WebSocket forwarding for: {}", ws_url);
111
+ let ws_url_clone = ws_url.clone();
112
+ tokio::spawn(async move {
113
+ if let Err(e) = handle_websocket_forwarding(websocket, upstream_ws).await {
114
+ error!("WebSocket forwarding error for {}: {}", ws_url_clone, e);
115
+ }
116
+ });
117
+
118
+ info!("WebSocket upgrade successful for: {}", ws_url);
119
+ Ok(response)
120
+ }
121
+ }
122
+
123
+ async fn handle_websocket_forwarding(
124
+ websocket: HyperWebsocket,
125
+ upstream_ws: WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
126
+ ) -> Result<(), BoxError> {
127
+ // Get the client WebSocket stream
128
+ let client_ws = websocket.await?;
129
+
130
+ let (mut client_sink, mut client_stream) = client_ws.split();
131
+ let (mut upstream_sink, mut upstream_stream) = upstream_ws.split();
132
+
133
+ info!("WebSocket connection established, starting bidirectional forwarding");
134
+
135
+ // Forward messages from client to upstream
136
+ let client_to_upstream = async {
137
+ while let Some(msg) = client_stream.next().await {
138
+ match msg {
139
+ Ok(msg) => {
140
+ info!("Client -> Upstream: {:?}", msg);
141
+ if let Err(e) = upstream_sink.send(msg).await {
142
+ error!("Failed to forward message to upstream: {}", e);
143
+ break;
144
+ }
145
+ }
146
+ Err(e) => {
147
+ error!("Error receiving from client: {}", e);
148
+ break;
149
+ }
150
+ }
151
+ }
152
+ info!("Client-to-upstream forwarding ended");
153
+ };
154
+
155
+ // Forward messages from upstream to client
156
+ let upstream_to_client = async {
157
+ while let Some(msg) = upstream_stream.next().await {
158
+ match msg {
159
+ Ok(msg) => {
160
+ info!("Upstream -> Client: {:?}", msg);
161
+ if let Err(e) = client_sink.send(msg).await {
162
+ error!("Failed to forward message to client: {}", e);
163
+ break;
164
+ }
165
+ }
166
+ Err(e) => {
167
+ error!("Error receiving from upstream: {}", e);
168
+ break;
169
+ }
170
+ }
171
+ }
172
+ info!("Upstream-to-client forwarding ended");
173
+ };
174
+
175
+ // Run both forwarding tasks concurrently
176
+ tokio::select! {
177
+ _ = client_to_upstream => {
178
+ info!("Client disconnected");
179
+ }
180
+ _ = upstream_to_client => {
181
+ info!("Upstream disconnected");
182
+ }
183
+ }
184
+
185
+ info!("WebSocket forwarding session ended");
186
+ Ok(())
187
+ }
188
+
189
+ async fn handle_connection(
190
+ req: Request<Body>,
191
+ proxy: Arc<FBIProxy>,
192
+ ) -> Result<Response<Body>, Infallible> {
193
+ match proxy.handle_request(req).await {
194
+ Ok(response) => Ok(response),
195
+ Err(e) => {
196
+ error!("Request handling error: {}", e);
197
+ Ok(Response::builder()
198
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
199
+ .body(Body::from("Internal Server Error"))
200
+ .unwrap())
201
+ }
202
+ }
203
+ }
204
+
205
+ pub async fn start_proxy_server(port: u16) -> Result<(), BoxError> {
206
+ let addr = SocketAddr::from(([127, 0, 0, 1], port));
207
+ let proxy = Arc::new(FBIProxy::new());
208
+
209
+ let make_svc = make_service_fn(move |_conn| {
210
+ let proxy = proxy.clone();
211
+ async move { Ok::<_, Infallible>(service_fn(move |req| handle_connection(req, proxy.clone()))) }
212
+ });
213
+
214
+ let server = Server::bind(&addr).serve(make_svc);
215
+
216
+ info!("FBI Proxy server running on http://{}", addr);
217
+ info!("Features: HTTP proxying + WebSocket forwarding + Port encoding");
218
+
219
+ if let Err(e) = server.await {
220
+ error!("Server error: {}", e);
221
+ }
222
+
223
+ Ok(())
224
+ }
225
+
226
+ fn main() {
227
+ env_logger::init();
228
+
229
+ // Read port from environment variable, default to 24306
230
+ let port = std::env::var("PROXY_PORT")
231
+ .unwrap_or_else(|_| "24306".to_string())
232
+ .parse::<u16>()
233
+ .unwrap_or_else(|_| {
234
+ error!("Invalid PROXY_PORT value, using default 24306");
235
+ 24306
236
+ });
237
+
238
+ let rt = tokio::runtime::Runtime::new().unwrap();
239
+ rt.block_on(async {
240
+ info!(
241
+ "Starting FBI Proxy with Hyper + proper WebSocket forwarding on port {}",
242
+ port
243
+ );
244
+ if let Err(e) = start_proxy_server(port).await {
245
+ error!("Failed to start proxy server: {}", e);
246
+ }
247
+ });
248
+ }
package/ts/cli.ts ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bun
2
+ import getPort from "get-port";
3
+ import minimist from "minimist";
4
+ import hotMemo from "hot-memo";
5
+ import { exec } from "child_process";
6
+ import path from "path";
7
+ import { exists } from "fs/promises";
8
+ import { existsSync } from "fs";
9
+
10
+ // guide to install caddy
11
+ if (!(await Bun.$`caddy --version`.text().catch(() => ""))) {
12
+ console.error("Caddy is not installed. Please install Caddy first");
13
+ console.error(`For windows, try running:\n choco install caddy\n`);
14
+ console.error(`For linux, try running:\n sudo apt install caddy\n`);
15
+ process.exit(1);
16
+ }
17
+
18
+ const getProxyPath = () => {
19
+ const root = Bun.fileURLToPath(import.meta.url) + "../";
20
+ const filename =
21
+ {
22
+ "darwin-arm64": "fbi-proxy-darwin",
23
+ "darwin-x64": "fbi-proxy-darwin",
24
+ "linux-arm64": "fbi-proxy-linux-arm64",
25
+ "linux-x64": "fbi-proxy-linux-x64",
26
+ "linux-x86_64": "fbi-proxy-linux-x64",
27
+ "win32-arm64": "fbi-proxy-windows-arm64.exe",
28
+ "win32-x64": "fbi-proxy-windows-x64.exe",
29
+ }[process.platform + "-" + process.arch] || "fbi-proxy-linux-x64";
30
+
31
+ return [path.join(root, "rs/target/release", filename)].find((e) =>
32
+ existsSync(e)
33
+ );
34
+ };
35
+
36
+ // assume caddy is installed, launch proxy server now
37
+ const argv = minimist(process.argv.slice(2), {
38
+ default: {
39
+ dev: false,
40
+ d: false,
41
+ tls: "internal", // default to internal TLS
42
+ },
43
+ alias: {
44
+ dev: "d",
45
+ tls: "t",
46
+ },
47
+ });
48
+ console.log(argv);
49
+ if (argv.help) {
50
+ console.log(`Usage: fbi-proxy [options]
51
+ Options:
52
+ --dev, -d Enable development mode
53
+ --tls, -t Set TLS mode (internal|external)
54
+ --help Show this help message
55
+ `);
56
+ process.exit(0);
57
+ }
58
+
59
+ const isDev = argv.dev || argv.d || false;
60
+ const PROXY_PORT = String(await getPort({ port: 24306 }));
61
+ const proxyProcess = await hotMemo(async () => {
62
+ console.log("Starting Rust proxy server");
63
+
64
+ // TODO: in production, build and start the Rust proxy server
65
+ // using `cargo build --release` and then run the binary
66
+ const p = await (async () => {
67
+ if (isDev) {
68
+ // TODO: consider switch to bacon, cargo install bacon
69
+ // in dev mode, use cargo watch to run the Rust proxy server
70
+ const p = exec(`cargo watch -x "run --bin proxy"`, {
71
+ env: {
72
+ ...process.env,
73
+ PROXY_PORT,
74
+ },
75
+ cwd: path.join(__dirname, "../rs"),
76
+ });
77
+ return p;
78
+ }
79
+
80
+ const rsTargetDir = path.join(__dirname, "../rs", "target", "release");
81
+ const proxyBinary = process.platform === "win32" ? "proxy.exe" : "proxy";
82
+ const proxyPath = path.join(rsTargetDir, proxyBinary);
83
+ if (!(await exists(proxyPath).catch(() => false))) {
84
+ console.error("Proxy binary not found at " + proxyPath);
85
+ console.error(
86
+ "Please build the Rust proxy server first using `cargo build --release`"
87
+ );
88
+ process.exit(1);
89
+ }
90
+ const p = exec(proxyPath, {
91
+ env: {
92
+ ...process.env,
93
+ PROXY_PORT, // Rust proxy server port
94
+ },
95
+ });
96
+ return p;
97
+ })();
98
+
99
+ p.stdout?.pipe(process.stdout, { end: false });
100
+ p.stderr?.pipe(process.stderr, { end: false });
101
+ p.on("exit", (code) => {
102
+ console.log(`Proxy server exited with code ${code}`);
103
+ process.exit(code || 0);
104
+ });
105
+
106
+ console.log(`Rust proxy server started on port ${PROXY_PORT}`);
107
+ return p;
108
+ });
109
+
110
+ const caddyProcess = await hotMemo(async () => {
111
+ const Caddyfile = path.join(__dirname, "../Caddyfile");
112
+ if (!(await exists(Caddyfile).catch(() => false))) {
113
+ console.error("Caddyfile not found at " + Caddyfile);
114
+ console.error(
115
+ "Please create a Caddyfile in the root directory of the project."
116
+ );
117
+ process.exit(1);
118
+ }
119
+ console.log("Starting Caddy");
120
+ const p = exec(`caddy run ${isDev ? "--watch" : ""} --config ${Caddyfile}`, {
121
+ env: {
122
+ ...process.env,
123
+ PROXY_PORT, // Rust proxy server port
124
+ TLS: argv.tls || "internal", // Use internal TLS by default, or set via command line argument
125
+ },
126
+ cwd: path.dirname(Caddyfile),
127
+ });
128
+ // p.stdout?.pipe(process.stdout, { end: false });
129
+ // p.stderr?.pipe(process.stderr, { end: false });
130
+ p.on("exit", (code) => process.exit(code || 0));
131
+ console.log("Caddy started with config at " + Caddyfile);
132
+ return p;
133
+ });
134
+
135
+ console.log("all done");
136
+
137
+ const exit = () => {
138
+ console.log("Shutting down...");
139
+ proxyProcess?.kill?.();
140
+ caddyProcess?.kill?.();
141
+ process.exit(0);
142
+ };
143
+ process.on("SIGINT", exit);
144
+ process.on("SIGTERM", exit);
145
+ process.on("uncaughtException", (err) => {
146
+ console.error("Uncaught exception:", err);
147
+ exit();
148
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Enable latest features
4
+ "lib": ["ESNext", "DOM"],
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ }
22
+ }