diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..37a825b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,9 @@ +# The backtrace for rust is almost as large as the entire binary. +# = Huge reduction in binary size by removing all that. +[profile.release] +panic = "immediate-abort" + +[unstable] +panic-immediate-abort = true +build-std = ["std", "panic_abort"] +build-std-features = ["optimize_for_size"] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dbcfb1f..82dba20 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,44 +1,10 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - version: 2 updates: - package-ecosystem: cargo - directory: "/" + directory: / schedule: interval: daily - time: "20:00" - assignees: - - xosnrdev - commit-message: - prefix: "chore" - include: "scope" - groups: - dev-deps: - dependency-type: development - update-types: - - patch - - minor - deps: - dependency-type: production - update-types: - - patch - - minor - - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: github-actions + directory: / schedule: - interval: weekly - time: "20:02" - assignees: - - xosnrdev - commit-message: - prefix: "chore" - include: "scope" - groups: - ci-deps: - update-types: - - patch - - minor \ No newline at end of file + interval: daily diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a421ed6..547b3d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,24 +2,41 @@ name: CI on: push: - branches: [main] + branches: + - main pull_request: - workflow_dispatch: + branches: + - main + +permissions: + contents: read env: CARGO_TERM_COLOR: always jobs: - lint: + check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Install rust toolchain - uses: dtolnay/rust-toolchain@stable - + - name: Checkout + uses: actions/checkout@v6 + # https://github.com/actions/cache/blob/main/examples.md#rust---cargo + # Depends on `Cargo.lock` --> Has to be after checkout. + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Install Rust + run: | + rustup toolchain install nightly --no-self-update --profile minimal --component rust-src,rustfmt,clippy - name: Check formatting run: cargo fmt --all -- --check - + - name: Run tests + run: cargo test --all-features --all-targets - name: Run clippy - run: cargo clippy --all-targets --all-features -- -D warnings + run: cargo clippy --all-features --all-targets -- --no-deps --deny warnings -W clippy::pedantic diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ae24e0..3c59af5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 @@ -47,7 +47,7 @@ on: jobs: # Run 'dist plan' (or host) to determine what tasks we need to do plan: - runs-on: "ubuntu-latest" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} @@ -58,12 +58,13 @@ jobs: steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@v4 with: @@ -117,6 +118,7 @@ jobs: git config --global core.longpaths true - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - name: Install Rust non-interactively if not already installed if: ${{ matrix.container }} @@ -168,13 +170,14 @@ jobs: needs: - plan - build-local-artifacts - runs-on: "ubuntu-latest" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@v4 @@ -214,16 +217,17 @@ jobs: - plan - build-local-artifacts - build-global-artifacts - # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-latest" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@v4 @@ -282,10 +286,11 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} - runs-on: "ubuntu-latest" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive diff --git a/Cargo.toml b/Cargo.toml index dae8109..ddfebe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,10 @@ [package] name = "rce-engine" version = "1.2.71" -authors = ["ToolKitHub"] +edition = "2024" +authors = ["ToolKitHub "] description = "A secure service for running untrusted code inside isolated Docker containers via a simple HTTP API" -homepage = "https://github.com/ToolKitHub/rce-engine?tab=readme-ov-file#readme" repository = "https://github.com/ToolKitHub/rce-engine" -edition = "2024" license = "MIT" [dependencies] @@ -18,18 +17,17 @@ log = "0.4.29" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" -# We use `opt-level = "s"` as it significantly reduces binary size. +# See https://doc.rust-lang.org/cargo/reference/profiles.html +# See default profiles: https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles [profile.release] -codegen-units = 1 # reduces binary size by ~2% -debug = "full" # No one needs an undebuggable release binary -lto = true # reduces binary size by ~14% -opt-level = "s" # reduces binary size by ~25% -panic = "abort" # reduces binary size by ~50% in combination with -Zbuild-std-features=panic_immediate_abort -split-debuginfo = "packed" # generates a separate *.dwp/*.dSYM so the binary can get stripped -strip = "symbols" # See split-debuginfo - allows us to drop the size by ~65% -incremental = true # Improves re-compile times +codegen-units = 1 # reduces binary size by ~2% +lto = true # reduces binary size by ~14% +panic = "abort" # reduces binary size by ~50% in combination with -Zbuild-std-features=panic_immediate_abort +split-debuginfo = "packed" # generates a separate *.dwp/*.dSYM so the binary can get stripped +strip = "symbols" # See split-debuginfo - allows us to drop the size by ~65% +incremental = true # Improves re-compile times # The profile that 'dist' will build with [profile.dist] inherits = "release" - +lto = "thin" diff --git a/README.md b/README.md index b63e150..2861e3a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # rce-engine -**rce-engine** is a secure service for running untrusted code inside isolated Docker containers via a simple HTTP API. See [supported languages](https://github.com/ToolKitHub/rce-runner) +A secure service for running untrusted code inside isolated Docker containers via a simple HTTP API. See [supported programming languages](https://github.com/ToolKitHub/rce-runner) -[View full documentation](DOCUMENTATION.md) +See [Documentation](DOCUMENTATION.md) for more details. -## Why Use rce-engine? +## Features - **Security First**: Run untrusted code safely in isolated containers - **Language Support**: Execute code in 41 programming languages @@ -14,14 +14,16 @@ ## Quick Start -**Requirements**: +**System Requirements**: + - Ubuntu 22.04+ - Docker installed ### Installation For installation instructions, see: -- [Standard Installation Guide](docs/install/ubuntu-22.04.md) (recommended) + +- [Standard Installation Guide](docs/install/ubuntu-22.04.md) - [Enhanced Security Installation with gVisor](docs/install/ubuntu-22.04-gvisor.md) ### Basic Usage @@ -33,9 +35,9 @@ curl --request POST \ --header 'X-Access-Token: your-token-here' \ --header 'Content-Type: application/json' \ --data '{ - "image": "toolkithub/python:latest", + "image": "toolkithub/python:latest", "payload": { - "language": "python", + "language": "python", "files": [{"name": "main.py", "content": "print(\"Hello world!\")"}] } }' \ @@ -43,6 +45,7 @@ curl --request POST \ ``` Response: + ```json { "stdout": "Hello world!\n", @@ -57,7 +60,6 @@ Response: - [API Reference](docs/api/run.md) - [Installation guides](docs/install/) - ## License -See [License](./LICENSE) +This project is licensed under the [MIT License](./LICENSE) diff --git a/dist-workspace.toml b/dist-workspace.toml index bc8fb4c..b876ecb 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,7 +4,7 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.0" +cargo-dist-version = "0.30.3" # CI backends to support ci = "github" # The installers to generate for each app @@ -12,10 +12,6 @@ installers = ["shell"] # Target platforms to build apps for (Rust target-triple syntax) targets = ["x86_64-unknown-linux-gnu"] # Path that installers should place binaries in -install-path = "CARGO_HOME" +install-path = "~/rce/bin" # Whether to install an updater program -install-updater = true - -[dist.github-custom-runners] -global = "ubuntu-latest" -x86_64-unknown-linux-gnu = "ubuntu-latest" +install-updater = false diff --git a/docs/install/ubuntu-22.04.md b/docs/install/ubuntu-22.04.md index 8d278b8..4ca127c 100644 --- a/docs/install/ubuntu-22.04.md +++ b/docs/install/ubuntu-22.04.md @@ -32,9 +32,9 @@ Since rce-engine will run as a service under the `rce` user, install it directly sudo mkdir -p /home/rce/bin # Install directly to the service user's directory -sudo -u rce RCE_ENGINE_INSTALL_DIR=/home/rce/bin curl --proto '=https' --tlsv1.2 -LsSf https://github.com/ToolKitHub/rce-engine/releases/download/v1.2.71/rce-engine-installer.sh | sh +sudo -u rce curl --proto '=https' --tlsv1.2 -LsSf https://github.com/ToolKitHub/rce-engine/releases/download/v1.2.71/rce-engine-installer.sh | sh -# Ensure correct permissions +# Set execute permissions sudo chmod +rx /home/rce/bin/rce-engine ``` diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7855e6d..5d56faf 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,2 @@ [toolchain] -channel = "1.88.0" -components = ["rustfmt", "clippy"] +channel = "nightly" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..90f2491 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +style_edition = "2024" +use_small_heuristics = "Max" +group_imports = "StdExternalCrate" +imports_granularity = "Module" +format_code_in_doc_comments = true +newline_style = "Unix" +use_field_init_shorthand = true diff --git a/src/rce_engine/api/mod.rs b/src/api/mod.rs similarity index 71% rename from src/rce_engine/api/mod.rs rename to src/api/mod.rs index 2b28c9b..627a48c 100644 --- a/src/rce_engine/api/mod.rs +++ b/src/api/mod.rs @@ -2,18 +2,17 @@ pub mod root; pub mod run; pub mod version; -#[derive(Debug, Clone)] +use std::borrow::Cow; + pub struct ApiConfig { pub access_token: String, } +#[must_use] pub fn authorization_error() -> ErrorResponse { ErrorResponse { status_code: 401, - body: ErrorBody { - error: "access_token".to_string(), - message: "Missing or wrong access token".to_string(), - }, + body: ErrorBody { error: "access_token", message: "Missing or wrong access token".into() }, } } @@ -29,7 +28,7 @@ pub enum JsonFormat { pub fn prepare_json_response( body: &T, - format: JsonFormat, + format: &JsonFormat, ) -> Result { let json_to_vec = match format { JsonFormat::Minimal => serde_json::to_vec, @@ -38,16 +37,13 @@ pub fn prepare_json_response( }; match json_to_vec(body) { - Ok(data) => Ok(SuccessResponse { - status_code: 200, - body: data, - }), + Ok(data) => Ok(SuccessResponse { status_code: 200, body: data }), Err(err) => Err(ErrorResponse { status_code: 500, body: ErrorBody { - error: "response.serialize".to_string(), - message: format!("Failed to serialize response: {err}"), + error: "response.serialize", + message: format!("Failed to serialize response: {err}").into(), }, }), } @@ -62,6 +58,6 @@ pub struct ErrorResponse { #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct ErrorBody { - pub error: String, - pub message: String, + pub error: &'static str, + pub message: Cow<'static, str>, } diff --git a/src/rce_engine/api/root.rs b/src/api/root.rs similarity index 50% rename from src/rce_engine/api/root.rs rename to src/api/root.rs index 9ca32eb..66fd5e1 100644 --- a/src/rce_engine/api/root.rs +++ b/src/api/root.rs @@ -1,23 +1,22 @@ use serde::Serialize; -use crate::rce_engine::api; +use crate::api; const VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Debug, Serialize)] struct ServiceInfo { - name: String, - version: String, - description: String, + name: &'static str, + version: &'static str, + description: &'static str, } pub fn handle() -> Result { let service_info = ServiceInfo { - name: "rce-engine".to_string(), - version: VERSION.to_string(), - description: "HTTP API for running untrusted code inside isolated Docker containers." - .to_string(), + name: "rce-engine", + version: VERSION, + description: "HTTP API for running untrusted code inside isolated Docker containers", }; - api::prepare_json_response(&service_info, api::JsonFormat::Pretty) + api::prepare_json_response(&service_info, &api::JsonFormat::Pretty) } diff --git a/src/api/run.rs b/src/api/run.rs new file mode 100644 index 0000000..74db27b --- /dev/null +++ b/src/api/run.rs @@ -0,0 +1,67 @@ +use serde_json::{Map, Value}; + +use crate::{api, config, docker, run}; + +#[derive(serde::Deserialize)] +pub struct RequestBody { + pub image: String, + pub payload: Map, +} + +pub fn handle( + config: &config::Config, + req_body: RequestBody, +) -> Result { + let container_config = run::prepare_container_config(req_body.image, config.container.clone()); + + let run_result = run::run( + config.unix_socket.clone(), + run::RunRequest { container_config, payload: req_body.payload, limits: config.run.clone() }, + config.debug.clone(), + ) + .map_err(|err| handle_error(&err))?; + + api::prepare_json_response(&run_result, &api::JsonFormat::Minimal) +} + +fn handle_error(err: &run::Error) -> api::ErrorResponse { + match err { + run::Error::UnixStream(_) => error_response(err, 500, "docker.unixsocket"), + + run::Error::CreateContainer(_) => error_response(err, 400, "docker.container.create"), + + run::Error::StartContainer(_) => error_response(err, 500, "docker.container.start"), + run::Error::AttachContainer(_) => error_response(err, 500, "docker.container.attach"), + + run::Error::SerializePayload(_) => { + error_response(err, 400, "docker.container.stream.payload.serialize") + } + + run::Error::ReadStream(stream_error) => match stream_error { + docker::StreamError::MaxExecutionTime() => { + error_response(err, 400, "limits.execution_time") + } + + docker::StreamError::MaxReadSize(_) => error_response(err, 400, "limits.read.size"), + + _ => error_response(err, 500, "docker.container.stream.read"), + }, + + run::Error::StreamStdinUnexpected(_) => error_response(err, 500, "coderunner.stdin"), + + run::Error::StreamStderr(_) => error_response(err, 500, "coderunner.stderr"), + + run::Error::StreamStdoutDecode(_) => error_response(err, 500, "coderunner.stdout.decode"), + } +} + +fn error_response( + err: &run::Error, + status_code: u16, + error_code: &'static str, +) -> api::ErrorResponse { + api::ErrorResponse { + status_code, + body: api::ErrorBody { error: error_code, message: err.to_string().into() }, + } +} diff --git a/src/rce_engine/api/version.rs b/src/api/version.rs similarity index 67% rename from src/rce_engine/api/version.rs rename to src/api/version.rs index f88fa21..3a94368 100644 --- a/src/rce_engine/api/version.rs +++ b/src/api/version.rs @@ -1,11 +1,8 @@ use std::fmt; -use crate::rce_engine::api; -use crate::rce_engine::config; -use crate::rce_engine::docker; -use crate::rce_engine::unix_stream; +use crate::{api, config, docker, unix_stream}; -#[derive(Debug, serde::Serialize)] +#[derive(serde::Serialize)] struct VersionInfo { docker: docker::VersionResponse, } @@ -13,7 +10,7 @@ struct VersionInfo { pub fn handle(config: &config::Config) -> Result { let data = get_version_info(&config.unix_socket).map_err(handle_error)?; - api::prepare_json_response(&data, api::JsonFormat::Pretty) + api::prepare_json_response(&data, &api::JsonFormat::Pretty) } fn get_version_info(stream_config: &unix_stream::Config) -> Result { @@ -21,27 +18,19 @@ fn get_version_info(stream_config: &unix_stream::Config) -> Result api::ErrorResponse { match err { Error::UnixStream(_) => api::ErrorResponse { status_code: 500, - body: api::ErrorBody { - error: "docker.unixsocket".to_string(), - message: err.to_string(), - }, + body: api::ErrorBody { error: "docker.unixsocket", message: err.to_string().into() }, }, Error::Version(_) => api::ErrorResponse { status_code: 500, - body: api::ErrorBody { - error: "docker.version".to_string(), - message: err.to_string(), - }, + body: api::ErrorBody { error: "docker.version", message: err.to_string().into() }, }, } } diff --git a/src/rce_engine/config.rs b/src/config.rs similarity index 71% rename from src/rce_engine/config.rs rename to src/config.rs index 32e0dda..a4d4da8 100644 --- a/src/rce_engine/config.rs +++ b/src/config.rs @@ -1,9 +1,5 @@ -use crate::rce_engine::api; -use crate::rce_engine::debug; -use crate::rce_engine::run; -use crate::rce_engine::unix_stream; +use crate::{api, debug, run, unix_stream}; -#[derive(Clone, Debug)] pub struct Config { pub server: ServerConfig, pub api: api::ApiConfig, diff --git a/src/rce_engine/debug.rs b/src/debug.rs similarity index 100% rename from src/rce_engine/debug.rs rename to src/debug.rs diff --git a/src/rce_engine/docker.rs b/src/docker.rs similarity index 92% rename from src/rce_engine/docker.rs rename to src/docker.rs index dfaa065..89a9f6a 100644 --- a/src/rce_engine/docker.rs +++ b/src/docker.rs @@ -1,12 +1,11 @@ use std::collections::HashMap; use std::convert::TryInto; -use std::fmt; -use std::io; use std::io::{Read, Write}; +use std::{fmt, io}; use serde::{Deserialize, Serialize}; -use crate::rce_engine::http_extra; +use crate::http_extra; #[derive(Debug, Serialize)] #[serde(rename_all = "PascalCase")] @@ -39,7 +38,7 @@ pub struct HostConfig { #[derive(Debug, Serialize)] #[serde(rename_all = "PascalCase")] pub struct Ulimit { - pub name: String, + pub name: &'static str, pub soft: i64, pub hard: i64, } @@ -213,9 +212,7 @@ pub fn attach_container_request( ) -> Result, http::Error> { let url = format!("/containers/{container_id}/attach?stream=1&stdout=1&stdin=1&stderr=1"); - http::Request::post(url) - .header("Host", "127.0.0.1") - .body(http_extra::Body::Empty()) + http::Request::post(url).header("Host", "127.0.0.1").body(http_extra::Body::Empty()) } pub fn attach_container( @@ -292,9 +289,7 @@ pub fn read_stream(r: R, max_read_size: usize) -> Result { @@ -312,17 +307,10 @@ pub fn read_stream(r: R, max_read_size: usize) -> Result StreamError { @@ -357,20 +345,14 @@ impl StreamType { fn read_stream_type(mut reader: R) -> Result { let mut buffer = [0; 4]; - reader - .read_exact(&mut buffer) - .map_err(StreamError::ReadStreamType)?; + reader.read_exact(&mut buffer).map_err(StreamError::ReadStreamType)?; StreamType::from_byte(buffer[0]).ok_or(StreamError::UnknownStreamType(buffer[0])) } fn read_stream_length(mut reader: R) -> Result { let mut buffer = [0; 4]; - reader - .read_exact(&mut buffer) - .map_err(StreamError::ReadStreamLength)?; + reader.read_exact(&mut buffer).map_err(StreamError::ReadStreamLength)?; - u32::from_be_bytes(buffer) - .try_into() - .map_err(StreamError::InvalidStreamLength) + u32::from_be_bytes(buffer).try_into().map_err(StreamError::InvalidStreamLength) } diff --git a/src/rce_engine/environment.rs b/src/environment.rs similarity index 59% rename from src/rce_engine/environment.rs rename to src/environment.rs index d860437..a009526 100644 --- a/src/rce_engine/environment.rs +++ b/src/environment.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; -use std::env; -use std::fmt; use std::str::FromStr; +use std::{env, fmt}; pub type Environment = HashMap; +#[must_use] pub fn get_environment() -> Environment { env::vars().collect() } @@ -14,15 +14,9 @@ where T: FromStr, T::Err: fmt::Display, { - environment - .get(key) - .ok_or(Error::KeyNotFound(key)) - .and_then(|string_value| { - string_value.parse::().map_err(|err| Error::Parse { - key, - details: err.to_string(), - }) - }) + environment.get(key).ok_or(Error::KeyNotFound(key)).and_then(|string_value| { + string_value.parse::().map_err(|err| Error::Parse { key, details: err.to_string() }) + }) } pub fn lookup_optional(environment: &Environment, key: &'static str) -> Result, Error> @@ -36,10 +30,7 @@ where Some(string_value) => string_value .parse::() .map(Some) - .map_err(|err| Error::Parse { - key, - details: err.to_string(), - }), + .map_err(|err| Error::Parse { key, details: err.to_string() }), } } @@ -54,17 +45,14 @@ impl fmt::Display for Error { match self { Error::KeyNotFound(key) => write!(f, "Environment key not found: «{key}»"), - Error::Parse { key, details } => write!( - f, - "Failed to parse value for environment key: «{key}», details: {details}" - ), + Error::Parse { key, details } => { + write!(f, "Failed to parse value for environment key: «{key}», details: {details}") + } } } } +#[must_use] pub fn space_separated_string(s: String) -> Vec { - s.split(' ') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() + s.split(' ').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect() } diff --git a/src/rce_engine/http_extra.rs b/src/http_extra.rs similarity index 89% rename from src/rce_engine/http_extra.rs rename to src/http_extra.rs index 0efb8fa..e1ff8b9 100644 --- a/src/rce_engine/http_extra.rs +++ b/src/http_extra.rs @@ -1,16 +1,9 @@ -use std::fmt; -use std::io; -use std::io::BufRead; -use std::io::BufReader; -use std::io::{Read, Write}; +use std::io::{BufRead, BufReader, Read, Write}; use std::str::FromStr; +use std::{fmt, io}; -use http::header; -use http::header::CONTENT_LENGTH; -use http::header::TRANSFER_ENCODING; -use http::response; -use http::status; -use http::{Request, Response}; +use http::header::{CONTENT_LENGTH, TRANSFER_ENCODING}; +use http::{Request, Response, header, response, status}; use serde::Deserialize; use serde::de::DeserializeOwned; @@ -57,7 +50,7 @@ impl fmt::Display for Error { } Error::BadStatus(status_code, body) => { - let msg = String::from_utf8(body.to_vec()).unwrap_or(format!("{body:?}")); + let msg = String::from_utf8(body.clone()).unwrap_or(format!("{body:?}")); write!(f, "Unexpected status code {status_code}: {msg}") } @@ -88,16 +81,13 @@ where let response_parts = parse_response_head(response_head).map_err(Error::ParseResponseHead)?; // Read response body - let raw_body = match get_transfer_encoding(&response_parts.headers) { - TransferEncoding::Chunked() => { + let raw_body = + if let TransferEncoding::Chunked() = get_transfer_encoding(&response_parts.headers) { read_chunked_response_body(reader).map_err(Error::ReadChunkedBody)? - } - - _ => { + } else { let content_length = get_content_length(&response_parts.headers); read_response_body(content_length, reader).map_err(Error::ReadBody)? - } - }; + }; err_if_false( response_parts.status.is_success(), @@ -162,7 +152,7 @@ fn read_chunked_response_body(mut reader: R) -> Result, Read break; } - body.append(&mut chunk) + body.append(&mut chunk); } Ok(body) @@ -171,9 +161,7 @@ fn read_chunked_response_body(mut reader: R) -> Result, Read fn read_response_chunk(mut reader: R) -> Result, ReadChunkError> { let mut buffer = String::new(); - reader - .read_line(&mut buffer) - .map_err(ReadChunkError::ReadChunkLength)?; + reader.read_line(&mut buffer).map_err(ReadChunkError::ReadChunkLength)?; let chunk_length = usize::from_str_radix(buffer.trim_end(), 16).map_err(ReadChunkError::ParseChunkLength)?; @@ -181,18 +169,13 @@ fn read_response_chunk(mut reader: R) -> Result, ReadChunkEr let chunk = read_response_body(chunk_length, &mut reader).map_err(ReadChunkError::ReadChunk)?; let mut void = String::new(); - reader - .read_line(&mut void) - .map_err(ReadChunkError::SkipLineFeed)?; + reader.read_line(&mut void).map_err(ReadChunkError::SkipLineFeed)?; Ok(chunk) } fn get_content_length(headers: &header::HeaderMap) -> usize { - headers - .get(CONTENT_LENGTH) - .map(|value| value.to_str().unwrap_or("").parse().unwrap_or(0)) - .unwrap_or(0) + headers.get(CONTENT_LENGTH).map_or(0, |value| value.to_str().unwrap_or("").parse().unwrap_or(0)) } #[allow(dead_code)] @@ -236,7 +219,7 @@ impl<'de> Deserialize<'de> for EmptyResponse { } pub fn format_request_line(req: &Request) -> String { - let path = req.uri().path_and_query().map(|x| x.as_str()).unwrap_or(""); + let path = req.uri().path_and_query().map_or("", http::uri::PathAndQuery::as_str); format!("{} {} {:?}", req.method(), path, req.version()) } @@ -368,9 +351,7 @@ impl fmt::Display for ResponseError { fn to_http_parts(parsed: httparse::Response) -> Result { let mut builder = Response::builder(); - let headers = builder - .headers_mut() - .ok_or(ResponseError::InvalidBuilder())?; + let headers = builder.headers_mut().ok_or(ResponseError::InvalidBuilder())?; for hdr in parsed.headers.iter() { let name = header::HeaderName::from_str(hdr.name).map_err(ResponseError::HeaderName)?; @@ -383,10 +364,7 @@ fn to_http_parts(parsed: httparse::Response) -> Result std::io::Result<()> { @@ -28,15 +18,10 @@ async fn main() -> std::io::Result<()> { let listen_addr = config.server.listen_addr.clone(); let listen_port = config.server.listen_port; let worker_threads = config.server.worker_threads; - - log::info!("Listening on {listen_addr}:{listen_port}",); + let data = web::Data::new(config); HttpServer::new(move || { - App::new() - .app_data(web::Data::new(config.clone())) - .service(index_api) - .service(version_api) - .service(run_api) + App::new().app_data(data.clone()).service(index_api).service(version_api).service(run_api) }) .workers(worker_threads) .client_request_timeout(Duration::from_secs(60)) @@ -47,19 +32,15 @@ async fn main() -> std::io::Result<()> { #[get("/")] async fn index_api() -> HttpResponse { - api::root::handle() - .map(prepare_success_response) - .unwrap_or_else(prepare_error_response) + api::root::handle().map_or_else(prepare_error_response, prepare_success_response) } #[get("/version")] async fn version_api(req: HttpRequest, config: web::Data) -> HttpResponse { - if !has_valid_access_token(&req, &config) { - prepare_error_response(api::authorization_error()) + if has_valid_access_token(&req, &config) { + api::version::handle(&config).map_or_else(prepare_error_response, prepare_success_response) } else { - api::version::handle(&config) - .map(prepare_success_response) - .unwrap_or_else(prepare_error_response) + prepare_error_response(api::authorization_error()) } } @@ -69,21 +50,18 @@ async fn run_api( req_body: web::Json, config: web::Data, ) -> HttpResponse { - if !has_valid_access_token(&req, &config) { - prepare_error_response(api::authorization_error()) - } else { + if has_valid_access_token(&req, &config) { api::run::handle(&config, req_body.into_inner()) - .map(prepare_success_response) - .unwrap_or_else(prepare_error_response) + .map_or_else(prepare_error_response, prepare_success_response) + } else { + prepare_error_response(api::authorization_error()) } } fn prepare_success_response(data: api::SuccessResponse) -> HttpResponse { let status_code = StatusCode::from_u16(data.status_code).unwrap_or(StatusCode::OK); - HttpResponse::build(status_code) - .content_type(ContentType::json()) - .body(data.body) + HttpResponse::build(status_code).content_type(ContentType::json()).body(data.body) } fn prepare_error_response(data: api::ErrorResponse) -> HttpResponse { @@ -93,16 +71,12 @@ fn prepare_error_response(data: api::ErrorResponse) -> HttpResponse { let body = serde_json::to_vec_pretty(&data.body) .unwrap_or_else(|_| b"Failed to serialize error body".to_vec()); - HttpResponse::build(status_code) - .content_type(ContentType::json()) - .body(body) + HttpResponse::build(status_code).content_type(ContentType::json()).body(body) } fn has_valid_access_token(request: &HttpRequest, config: &config::Config) -> bool { - let access_token = request - .headers() - .get("X-Access-Token") - .map(|token| token.to_str().unwrap_or("")); + let access_token = + request.headers().get("X-Access-Token").map(|token| token.to_str().unwrap_or("")); match access_token { Some(token) => token == config.api.access_token, @@ -127,16 +101,9 @@ fn build_config(env: &environment::Environment) -> Result Result { @@ -209,14 +172,8 @@ fn build_container_config( cap_add: environment::space_separated_string(cap_add), cap_drop: environment::space_separated_string(cap_drop), readonly_rootfs, - tmp_dir: tmp_dir_path.map(|path| run::Tmpfs { - path, - options: tmp_dir_options, - }), - work_dir: work_dir_path.map(|path| run::Tmpfs { - path, - options: work_dir_options, - }), + tmp_dir: tmp_dir_path.map(|path| run::Tmpfs { path, options: tmp_dir_options }), + work_dir: work_dir_path.map(|path| run::Tmpfs { path, options: work_dir_options }), }) } @@ -224,14 +181,11 @@ fn build_run_config(env: &environment::Environment) -> Result Result { +fn build_debug_config(env: &environment::Environment) -> debug::Config { let keep_container = environment::lookup(env, "DEBUG_KEEP_CONTAINER").unwrap_or(false); - Ok(debug::Config { keep_container }) + debug::Config { keep_container } } diff --git a/src/rce_engine/api/run.rs b/src/rce_engine/api/run.rs deleted file mode 100644 index 2421abe..0000000 --- a/src/rce_engine/api/run.rs +++ /dev/null @@ -1,74 +0,0 @@ -use serde_json::{Map, Value}; - -use crate::rce_engine::api; -use crate::rce_engine::config; -use crate::rce_engine::docker; -use crate::rce_engine::run; - -#[derive(Debug, serde::Deserialize)] -pub struct RequestBody { - pub image: String, - pub payload: Map, -} - -pub fn handle( - config: &config::Config, - req_body: RequestBody, -) -> Result { - let container_config = run::prepare_container_config(req_body.image, config.container.clone()); - - let run_result = run::run( - config.unix_socket.clone(), - run::RunRequest { - container_config, - payload: req_body.payload, - limits: config.run.clone(), - }, - config.debug.clone(), - ) - .map_err(handle_error)?; - - api::prepare_json_response(&run_result, api::JsonFormat::Minimal) -} - -fn handle_error(err: run::Error) -> api::ErrorResponse { - match &err { - run::Error::UnixStream(_) => error_response(&err, 500, "docker.unixsocket"), - - run::Error::CreateContainer(_) => error_response(&err, 400, "docker.container.create"), - - run::Error::StartContainer(_) => error_response(&err, 500, "docker.container.start"), - - run::Error::AttachContainer(_) => error_response(&err, 500, "docker.container.attach"), - - run::Error::SerializePayload(_) => { - error_response(&err, 400, "docker.container.stream.payload.serialize") - } - - run::Error::ReadStream(stream_error) => match stream_error { - docker::StreamError::MaxExecutionTime() => { - error_response(&err, 400, "limits.execution_time") - } - - docker::StreamError::MaxReadSize(_) => error_response(&err, 400, "limits.read.size"), - - _ => error_response(&err, 500, "docker.container.stream.read"), - }, - - run::Error::StreamStdinUnexpected(_) => error_response(&err, 500, "coderunner.stdin"), - - run::Error::StreamStderr(_) => error_response(&err, 500, "coderunner.stderr"), - - run::Error::StreamStdoutDecode(_) => error_response(&err, 500, "coderunner.stdout.decode"), - } -} - -fn error_response(err: &run::Error, status_code: u16, error_code: &str) -> api::ErrorResponse { - api::ErrorResponse { - status_code, - body: api::ErrorBody { - error: error_code.to_string(), - message: err.to_string(), - }, - } -} diff --git a/src/rce_engine/run.rs b/src/run.rs similarity index 92% rename from src/rce_engine/run.rs rename to src/run.rs index 9a129df..0bd5d28 100644 --- a/src/rce_engine/run.rs +++ b/src/run.rs @@ -1,16 +1,12 @@ use std::collections::HashMap; -use std::fmt; -use std::net; use std::os::unix::net::UnixStream; -use std::str; use std::time::Duration; +use std::{fmt, net, str}; use serde::Serialize; use serde_json::{Map, Value}; -use crate::rce_engine::debug; -use crate::rce_engine::docker; -use crate::rce_engine::unix_stream; +use crate::{debug, docker, unix_stream}; #[derive(Debug)] pub struct RunRequest { @@ -97,10 +93,7 @@ where .map_err(Error::ReadStream)?; // Return error if we recieved stdin or stderr data from the stream - err_if_false( - output.stdin.is_empty(), - Error::StreamStdinUnexpected(output.stdin), - )?; + err_if_false(output.stdin.is_empty(), Error::StreamStdinUnexpected(output.stdin))?; err_if_false(output.stderr.is_empty(), Error::StreamStderr(output.stderr))?; // Decode stdout data to dict @@ -131,6 +124,7 @@ pub struct Tmpfs { } impl ContainerConfig { + #[must_use] pub fn tmpfs_mounts(&self) -> HashMap { [&self.tmp_dir, &self.work_dir] .iter() @@ -140,6 +134,7 @@ impl ContainerConfig { } } +#[must_use] pub fn prepare_container_config( image_name: String, config: ContainerConfig, @@ -164,12 +159,12 @@ pub fn prepare_container_config( cap_drop: config.cap_drop, ulimits: vec![ docker::Ulimit { - name: "nofile".to_string(), + name: "nofile", soft: config.ulimit_nofile_soft, hard: config.ulimit_nofile_hard, }, docker::Ulimit { - name: "nproc".to_string(), + name: "nproc", soft: config.ulimit_nproc_soft, hard: config.ulimit_nproc_hard, }, @@ -221,13 +216,13 @@ impl fmt::Display for Error { } Error::StreamStdinUnexpected(bytes) => { - let msg = String::from_utf8(bytes.to_vec()).unwrap_or(format!("{bytes:?}")); + let msg = String::from_utf8(bytes.clone()).unwrap_or(format!("{bytes:?}")); write!(f, "Code runner returned unexpected stdin data: {msg}") } Error::StreamStderr(bytes) => { - let msg = String::from_utf8(bytes.to_vec()).unwrap_or(format!("{bytes:?}")); + let msg = String::from_utf8(bytes.clone()).unwrap_or(format!("{bytes:?}")); write!(f, "Code runner failed with the following message: {msg}") } diff --git a/src/rce_engine/unix_stream.rs b/src/unix_stream.rs similarity index 90% rename from src/rce_engine/unix_stream.rs rename to src/unix_stream.rs index d2816da..d9f643b 100644 --- a/src/rce_engine/unix_stream.rs +++ b/src/unix_stream.rs @@ -1,9 +1,8 @@ -use std::fmt; -use std::io; use std::net::Shutdown; use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::time::Duration; +use std::{fmt, io}; #[derive(Debug, Clone)] pub struct Config { @@ -22,9 +21,7 @@ where ErrorTagger: Copy, ErrorTagger: FnOnce(Error) -> E, { - let mut stream = UnixStream::connect(&config.path) - .map_err(Error::Connect) - .map_err(to_error)?; + let mut stream = UnixStream::connect(&config.path).map_err(Error::Connect).map_err(to_error)?; stream .set_read_timeout(Some(config.read_timeout))