Skip to content

feat: add --tunnel=cloudflare flag with embedded frontend#145

Closed
S4tyendra wants to merge 1 commit intoekzhang:mainfrom
S4tyendra:cloudflare-tunnel
Closed

feat: add --tunnel=cloudflare flag with embedded frontend#145
S4tyendra wants to merge 1 commit intoekzhang:mainfrom
S4tyendra:cloudflare-tunnel

Conversation

@S4tyendra
Copy link

No description provided.

Copilot AI review requested due to automatic review settings March 4, 2026 12:43
@S4tyendra S4tyendra closed this Mar 4, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9141e9bd1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


/// The SvelteKit static build, embedded at compile time.
/// Ensure `npm run build` has been run from the workspace root before compiling.
static BUILD_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/../../build");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid compile-time dependency on an untracked build dir

Embedding ../../build with include_dir! makes Rust compilation depend on frontend artifacts that are not checked into git (git ls-tree for this commit has no build/ entries), so a clean checkout now fails unless npm run build is run first. This regresses existing Rust-only flows (e.g., .github/workflows/ci.yaml rust jobs run cargo test/cargo clippy without a web build step), and it also breaks source installs in environments that only expect Cargo to be required.

Useful? React with 👍 / 👎.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a self-hosted “quick tunnel” mode that can expose a locally spawned sshx-server via Cloudflare, and switches the server’s web UI to be served from an embedded (compile-time) frontend build.

Changes:

  • Add a --tunnel <provider> CLI flag (initially cloudflare) that starts a local sshx-server and a cloudflared quick tunnel.
  • Introduce a new sshx::tunnel module that manages the local server + cloudflared process lifecycle.
  • Replace filesystem-based static serving in sshx-server with embedded static assets (plus precompressed .br/.gz content negotiation).

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
package-lock.json Updates lockfile entries (npm dependency graph change).
crates/sshx/src/tunnel.rs New tunnel orchestration (local server + cloudflared) and guard lifecycle.
crates/sshx/src/main.rs Adds --tunnel flag and routes controller to local server when tunneling.
crates/sshx/src/lib.rs Exposes the new tunnel module.
crates/sshx/Cargo.toml Adds sshx-server as a dependency for the CLI crate.
crates/sshx-server/src/web.rs Serves embedded frontend build with gzip/brotli negotiation + SPA fallback.
crates/sshx-server/Cargo.toml Adds include_dir + mime_guess for embedded static serving.
Cargo.lock Locks new Rust dependencies and adds sshx-server to sshx deps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +21
/// The SvelteKit static build, embedded at compile time.
/// Ensure `npm run build` has been run from the workspace root before compiling.
static BUILD_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/../../build");

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include_dir!("$CARGO_MANIFEST_DIR/../../build") requires the build/ directory to exist at compile time, but the repo .gitignore excludes /build. On a clean clone, cargo build / cargo test for sshx-server will fail unless a manual npm run build was run first. Consider adding a build.rs to generate (or at least validate and emit a clear error about) the frontend build, or gating the embedded-asset path behind a feature and keeping the previous filesystem ServeDir fallback for dev builds.

Copilot uses AI. Check for mistakes.
Comment on lines 23 to +46
pub fn app() -> Router<Arc<ServerState>> {
let root_spa = ServeFile::new("build/spa.html")
.precompressed_gzip()
.precompressed_br();

// Serves static SvelteKit build files.
let static_files = ServeDir::new("build")
.precompressed_gzip()
.precompressed_br()
.fallback(root_spa);

Router::new()
.nest("/api", backend())
.fallback_service(get_service(static_files))
.fallback(serve_static)
}

/// Routes for the backend web API server.
fn backend() -> Router<Arc<ServerState>> {
Router::new().route("/s/{name}", any(socket::get_session_ws))
}

/// Serve an embedded static file with content-negotiation for precompressed variants.
///
/// Resolution order for a request path `P`:
/// 1. `P` with brotli encoding (`P.br`) if client accepts `br`
/// 2. `P` with gzip encoding (`P.gz`) if client accepts `gzip`
/// 3. `P` raw
/// 4. SPA fallback: `spa.html` (same compression priority) for unknown paths
async fn serve_static(req: Request) -> Response {
let path = req.uri().path().trim_start_matches('/');

// Empty path → "index.html", which SvelteKit puts at root.
let path = if path.is_empty() { "index.html" } else { path };

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Router::fallback(serve_static) will route all HTTP methods (POST/PUT/…) to the SPA/static handler. Previously, the static handler was wrapped in get_service(...), which limited it to GET/HEAD behavior. To avoid surprising behavior and potential security footguns, consider returning 405 Method Not Allowed for non-GET/HEAD requests in serve_static (or route static handling via method-specific routing).

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +130
fn build_response(
body: &'static [u8],
content_type: &str,
encoding: Option<&str>,
cache_control: &str,
) -> Response {
let mut builder = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type)
.header(header::CACHE_CONTROL, cache_control);

if let Some(enc) = encoding {
builder = builder.header(header::CONTENT_ENCODING, enc);
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static responses are content-negotiated on Accept-Encoding, but the response does not set Vary: Accept-Encoding. Without Vary, intermediaries (CDNs/proxies/browsers) can incorrectly cache and serve a brotli/gzip-encoded response to clients that don't support it. Add a Vary: Accept-Encoding header (at least when a compressed variant is served, and ideally always for these static routes).

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +28
impl Drop for TunnelGuard {
fn drop(&mut self) {
self.server_task.abort();
}
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TunnelGuard::drop aborts the server_task, but sshx_server::Server::listen spawns additional background tasks (e.g. listen_for_transfers / close_old_sessions) that are not tied to the join handle and will keep running unless Server::shutdown() is called. This can leak tasks after the guard is dropped. Consider keeping a shutdown handle (e.g., store an Arc<Server> inside TunnelGuard) and calling server.shutdown() in Drop, letting the server task exit cleanly (optionally with a timeout before aborting).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants