From b19d9d25f7ab1d395cce19f03394f910ef9e0465 Mon Sep 17 00:00:00 2001 From: minco Date: Tue, 12 May 2026 21:51:19 +0900 Subject: [PATCH] add dockerfile --- Dockerfile | 24 +++++++- crates/rsh-backend/Cargo.toml | 1 + crates/rsh-backend/build.rs | 25 ++++++++ crates/rsh-backend/src/dist.rs | 109 +++++++++++++++++++++++++++++++++ crates/rsh-backend/src/main.rs | 4 ++ justfile | 27 ++++++++ 6 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 crates/rsh-backend/build.rs create mode 100644 crates/rsh-backend/src/dist.rs create mode 100644 justfile diff --git a/Dockerfile b/Dockerfile index da7c88d..7d5b5a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,29 @@ -FROM rust:1.95.0-trixie AS builder +FROM messense/rust-musl-cross:x86_64-musl AS stub-amd64 WORKDIR /build COPY Cargo.toml Cargo.lock* ./ COPY crates ./crates +RUN --mount=type=cache,target=/root/.cargo/registry \ + --mount=type=cache,target=/build/target \ + cargo build --release --target x86_64-unknown-linux-musl -p rsh && \ + cp target/x86_64-unknown-linux-musl/release/rsh /rsh-x86_64 + +FROM messense/rust-musl-cross:aarch64-musl AS stub-arm64 +WORKDIR /build +COPY Cargo.toml Cargo.lock* ./ +COPY crates ./crates +RUN --mount=type=cache,target=/root/.cargo/registry \ + --mount=type=cache,target=/build/target \ + cargo build --release --target aarch64-unknown-linux-musl -p rsh && \ + cp target/aarch64-unknown-linux-musl/release/rsh /rsh-aarch64 + +FROM rust:1.95.0-trixie AS builder +WORKDIR /build +COPY --from=stub-amd64 /rsh-x86_64 /stubs/rsh-x86_64 +COPY --from=stub-arm64 /rsh-aarch64 /stubs/rsh-aarch64 +COPY Cargo.toml Cargo.lock* ./ +COPY crates ./crates +ENV RSH_STUB_X86_64=/stubs/rsh-x86_64 \ + RSH_STUB_AARCH64=/stubs/rsh-aarch64 RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/build/target \ cargo build --release -p rsh-backend && \ diff --git a/crates/rsh-backend/Cargo.toml b/crates/rsh-backend/Cargo.toml index e1789fe..0f0ce5a 100644 --- a/crates/rsh-backend/Cargo.toml +++ b/crates/rsh-backend/Cargo.toml @@ -2,6 +2,7 @@ name = "rsh-backend" version = "0.1.0" edition.workspace = true +build = "build.rs" [dependencies] rsh-types = { workspace = true } diff --git a/crates/rsh-backend/build.rs b/crates/rsh-backend/build.rs new file mode 100644 index 0000000..f2d432e --- /dev/null +++ b/crates/rsh-backend/build.rs @@ -0,0 +1,25 @@ +use std::path::{Path, PathBuf}; + +fn resolve_stub(env_var: &str, out_name: &str, out_dir: &Path) -> PathBuf { + println!("cargo:rerun-if-env-changed={env_var}"); + let dest = out_dir.join(out_name); + if let Ok(src) = std::env::var(env_var) { + let src = PathBuf::from(&src); + if src.exists() { + println!("cargo:rerun-if-changed={}", src.display()); + std::fs::copy(&src, &dest).expect("copy stub"); + return dest; + } + println!("cargo:warning={env_var} set to {src:?} but file not found; serving 503 for this arch"); + } + std::fs::write(&dest, []).expect("write placeholder"); + dest +} + +fn main() { + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let x86 = resolve_stub("RSH_STUB_X86_64", "rsh-x86_64", &out_dir); + let arm = resolve_stub("RSH_STUB_AARCH64", "rsh-aarch64", &out_dir); + println!("cargo:rustc-env=STUB_X86_64_PATH={}", x86.display()); + println!("cargo:rustc-env=STUB_AARCH64_PATH={}", arm.display()); +} diff --git a/crates/rsh-backend/src/dist.rs b/crates/rsh-backend/src/dist.rs new file mode 100644 index 0000000..f8d5162 --- /dev/null +++ b/crates/rsh-backend/src/dist.rs @@ -0,0 +1,109 @@ +use axum::extract::{Path, Query}; +use axum::http::{header, HeaderMap, StatusCode}; +use axum::response::{IntoResponse, Response}; +use serde::Deserialize; + +static STUB_X86_64: &[u8] = include_bytes!(env!("STUB_X86_64_PATH")); +static STUB_AARCH64: &[u8] = include_bytes!(env!("STUB_AARCH64_PATH")); + +const SCRIPT_TEMPLATE: &str = r#"#!/bin/sh +set -eu +SESSION='__SESSION__' +BASE='__BASE__' +WS='__WS__' +A=$(uname -m) +case "$A" in + x86_64|amd64) ARCH=x86_64 ;; + aarch64|arm64) ARCH=aarch64 ;; + *) echo "rsh: unsupported arch $A" >&2; exit 1 ;; +esac +TMP=$(mktemp /tmp/rsh.XXXXXX) +trap 'rm -f "$TMP"' EXIT +if command -v curl >/dev/null 2>&1; then + curl -fsSL "$BASE/rsh/$ARCH" -o "$TMP" +elif command -v wget >/dev/null 2>&1; then + wget -qO "$TMP" "$BASE/rsh/$ARCH" +else + echo "rsh: need curl or wget" >&2; exit 1 +fi +chmod +x "$TMP" +printf 'Password (empty for none): ' >&2 +stty -echo 2>/dev/null || true +IFS= read -r PW || PW='' +stty echo 2>/dev/null || true +echo >&2 +trap - EXIT +if [ -n "$PW" ]; then + exec "$TMP" --url "$WS/ws/stub" --session "$SESSION" --password "$PW" +else + exec "$TMP" --url "$WS/ws/stub" --session "$SESSION" +fi +"#; + +#[derive(Deserialize)] +pub struct RunQuery { + s: Option, +} + +pub async fn run_sh(headers: HeaderMap, Query(q): Query) -> Response { + let session = q.s.unwrap_or_else(|| "default".into()); + if !is_valid_session(&session) { + return (StatusCode::BAD_REQUEST, "invalid session id").into_response(); + } + let (scheme, ws_scheme) = detect_scheme(&headers); + let host = headers + .get(header::HOST) + .and_then(|v| v.to_str().ok()) + .unwrap_or("localhost"); + let base = format!("{scheme}://{host}"); + let ws = format!("{ws_scheme}://{host}"); + let script = SCRIPT_TEMPLATE + .replace("__SESSION__", &session) + .replace("__BASE__", &base) + .replace("__WS__", &ws); + ( + [(header::CONTENT_TYPE, "text/x-shellscript; charset=utf-8")], + script, + ) + .into_response() +} + +pub async fn stub(Path(arch): Path) -> Response { + let bytes: &[u8] = match arch.as_str() { + "x86_64" | "amd64" => STUB_X86_64, + "aarch64" | "arm64" => STUB_AARCH64, + _ => return StatusCode::NOT_FOUND.into_response(), + }; + if bytes.is_empty() { + return (StatusCode::SERVICE_UNAVAILABLE, "stub not embedded in this build").into_response(); + } + ( + [ + (header::CONTENT_TYPE, "application/octet-stream"), + ( + header::CONTENT_DISPOSITION, + "attachment; filename=\"rsh\"", + ), + ], + bytes, + ) + .into_response() +} + +fn is_valid_session(s: &str) -> bool { + !s.is_empty() + && s.len() <= 64 + && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-') +} + +fn detect_scheme(headers: &HeaderMap) -> (&'static str, &'static str) { + let proto = headers + .get("x-forwarded-proto") + .and_then(|v| v.to_str().ok()) + .unwrap_or("http"); + if proto == "https" { + ("https", "wss") + } else { + ("http", "ws") + } +} diff --git a/crates/rsh-backend/src/main.rs b/crates/rsh-backend/src/main.rs index b6f15c8..64e96aa 100644 --- a/crates/rsh-backend/src/main.rs +++ b/crates/rsh-backend/src/main.rs @@ -1,5 +1,6 @@ mod auth; mod config; +mod dist; mod keys; mod persist; mod state; @@ -47,6 +48,9 @@ async fn main() -> anyhow::Result<()> { } let app = Router::new() + .route("/", get(dist::run_sh)) + .route("/run.sh", get(dist::run_sh)) + .route("/rsh/:arch", get(dist::stub)) .route("/healthz", get(|| async { "ok" })) .route("/ws/stub", get(ws_stub::handler)) .route("/ws/op", get(ws_op::handler)) diff --git a/justfile b/justfile new file mode 100644 index 0000000..55a0884 --- /dev/null +++ b/justfile @@ -0,0 +1,27 @@ +_default: + @just --list + +stubs: + rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl + cargo build --release --target x86_64-unknown-linux-musl -p rsh + cargo build --release --target aarch64-unknown-linux-musl -p rsh + @echo "stubs built:" + @ls -lh target/x86_64-unknown-linux-musl/release/rsh target/aarch64-unknown-linux-musl/release/rsh + +backend: stubs + RSH_STUB_X86_64=target/x86_64-unknown-linux-musl/release/rsh \ + RSH_STUB_AARCH64=target/aarch64-unknown-linux-musl/release/rsh \ + cargo build --release -p rsh-backend + +dev: stubs + RSH_STUB_X86_64=target/x86_64-unknown-linux-musl/release/rsh \ + RSH_STUB_AARCH64=target/aarch64-unknown-linux-musl/release/rsh \ + RSH_DATA=/tmp/rsh-dev \ + RSH_BIND=127.0.0.1:7777 \ + cargo run -p rsh-backend + +docker-build: + docker build -t rsh-backend:local . + +docker-run: + docker run --rm -p 7777:7777 -v rsh-data:/var/lib/rsh rsh-backend:local