├── .dockerignore ├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.md ├── docker-compose.yml └── src ├── api_helpers.rs ├── isolate.rs ├── main.rs ├── routes ├── mod.rs └── run_post.rs ├── runner ├── mod.rs ├── phase_settings.rs └── runner.rs └── utils ├── mod.rs └── parsed_env.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | ./target 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Docker build 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build_image: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Set up QEMU 12 | uses: docker/setup-qemu-action@v1 13 | 14 | - name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v1 16 | 17 | - name: Login to registry 18 | uses: docker/login-action@v1 19 | with: 20 | username: ${{ secrets.DOCKER_REGISTRY_USER }} 21 | password: ${{ secrets.DOCKER_REGISTRY_PASS }} 22 | 23 | - name: Tag name 24 | id: tag_name 25 | run: | 26 | TAG_NAME=${GITHUB_REF/refs\/tags\//} 27 | echo ::set-output name=TAG_NAME::$TAG_NAME 28 | echo ::set-output name=TAG_NAME_MAJOR::$(echo $TAG_NAME | cut -d. -f1) 29 | echo ::set-output name=TAG_NAME_MINOR::$(echo $TAG_NAME | cut -d. -f2) 30 | echo ::set-output name=TAG_NAME_PATCH::$(echo $TAG_NAME | cut -d. -f3) 31 | 32 | - name: Build and push 33 | id: docker_build 34 | uses: docker/build-push-action@v2 35 | with: 36 | push: true 37 | tags: | 38 | quantumsheep/godbox:${{ steps.tag_name.outputs.TAG_NAME }} 39 | quantumsheep/godbox:${{ steps.tag_name.outputs.TAG_NAME_MAJOR }}.${{ steps.tag_name.outputs.TAG_NAME_MINOR }} 40 | quantumsheep/godbox:${{ steps.tag_name.outputs.TAG_NAME_MAJOR }} 41 | quantumsheep/godbox:latest 42 | - name: Image digest 43 | run: echo ${{ steps.docker_build.outputs.digest }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # Environment file 14 | .env 15 | 16 | # macOS specific 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "godbox" 3 | version = "1.0.0" 4 | authors = ["quantumsheep "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | actix-web = "3" 11 | actix-web-validator = "2" 12 | validator = { version = "0.12", features = ["derive"] } 13 | derive_more = { version = "0.99", features = ["display", "error"] } 14 | serde = { version = "1", features = ["derive"] } 15 | serde_json = "1.0" 16 | base64 = "0.13.0" 17 | rand = "0.8.3" 18 | derive_builder = "0.10.2" 19 | merge = "0.1.0" 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.52 as build 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY src src 6 | 7 | COPY Cargo.toml . 8 | 9 | RUN cargo install --path . 10 | 11 | FROM quantumsheep/godbox-base:latest 12 | 13 | COPY --from=build /usr/local/cargo/bin/godbox /usr/local/bin/godbox 14 | 15 | CMD godbox 16 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM quantumsheep/godbox-base:latest 2 | 3 | RUN set -xe && \ 4 | apt-get update && \ 5 | rm -rf /var/lib/apt/lists/* && \ 6 | curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain=stable && \ 7 | . $HOME/.cargo/env 8 | 9 | RUN ~/.cargo/bin/cargo install cargo-watch 10 | 11 | WORKDIR /usr/src/app 12 | 13 | COPY src src 14 | 15 | COPY Cargo.toml . 16 | 17 | CMD ~/.cargo/bin/cargo watch -x run 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nathanael Demacon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godbox 2 | Secure sandboxing system for untrusted code execution. 3 | 4 | It uses [isolate](https://github.com/ioi/isolate) which uses specific functionnalities of the Linux kernel, thus godbox not able to run properly outside of Linux. 5 | 6 | # Installation 7 | ## Kubernetes 8 | ### Helm Chart 9 | * [Repository](https://github.com/quantumsheep/godbox-helm) 10 | * [Artifact Hub](https://artifacthub.io/packages/helm/godbox/godbox) 11 | 12 | ```bash 13 | helm repo add godbox-charts https://quantumsheep.github.io/godbox-helm/charts 14 | helm install my-godbox godbox-charts/godbox 15 | ``` 16 | 17 | ## Docker Compose 18 | ```yml 19 | version: "3" 20 | 21 | services: 22 | godbox: 23 | image: quantumsheep/godbox:2 24 | privileged: true 25 | ports: 26 | - 8080:8080 27 | ``` 28 | 29 | ## Docker 30 | ```sh 31 | docker run -it -d --privileged -p 8080:8080 quantumsheep/godbox:2 32 | ``` 33 | 34 | # Environment variables 35 | | Name | Type | Default | Description | 36 | |-------------------------|-----------|---------|-----------------------------| 37 | | API_MAX_PAYLOAD_SIZE | `number` | 32768 | API maximum payload size | 38 | | ALLOW_PROFILING | `boolean` | true | Enable or disable profiling | 39 | | MAX_RUN_TIME_LIMIT | `number` | 5 | Maximum run time limit | 40 | | MAX_EXTRA_TIME_LIMIT | `number` | 0 | Maximum extra time limit | 41 | | MAX_WALL_TIME_LIMIT | `number` | 10 | Maximum wall time limit | 42 | | MAX_STACK_SIZE_LIMIT | `number` | 128000 | Maximum stack size limit | 43 | | MAX_PROCESS_COUNT_LIMIT | `number` | 120 | Maximum process count limit | 44 | | MAX_MEMORY_LIMIT | `number` | 512000 | Maximum memory limit | 45 | | MAX_STORAGE_LIMIT | `number` | 10240 | Maximum storage limit | 46 | 47 | # Run commands 48 | Send a `POST` HTTP request to `http://localhost:8080/run` containing the wanted configuration in JSON. See below for properties. 49 | 50 | ## Properties 51 | | Name | Type | Description | 52 | |------------------|--------------------------|-----------------------------------------------------------------| 53 | | phases* | `Phase[]` | Execution phases (check examples bellow) | 54 | | files* | `string` | Base64-encoded zip file containing the files used in the phases | 55 | | environment | `Record` | Environment variables used in all phases | 56 | | sandbox_settings | `SandboxSettings` | Override default sandbox limitation settings | 57 | 58 | ### Phase 59 | | Name | Type | Default | Description | 60 | |------------------|-------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------| 61 | | script* | `string` | | Multi-line bash script that will be executed inside the isolated environment | 62 | | name | `string` | Phase's index | Name that will be used in result output | 63 | | stdin | `string` | | Content used in `stdin` | 64 | | environment | `Record` | | Environment variables available inside `script` execution. This will override global environment variables with the same given keys | 65 | | sandbox_settings | `SandboxSettings` | | Overrides default sandbox limitation settings. This will override global sandbox settings with the same given keys | 66 | | profiling | `boolean` | false | Run a profiler on `script`. This functionnality is WIP | 67 | 68 | ### SandboxSettings 69 | | Name | Type | Default | Description | 70 | |---------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 71 | | run_time_limit | `number` | 5 | Limit run time of the whole control group in seconds. Fractional numbers are allowed | 72 | | extra_time_limit | `number` | 0 | When a time limit is exceeded, wait for extra time seconds before killing the program. This has the advantage that the real execution time is reported, even though it slightly exceeds the limit. Fractional numbers are again allowed | 73 | | wall_time_limit | `number` | 10 | Limit wall-clock time to time seconds. Fractional values are allowed. This clock measures the time from the start of the program to its exit, so it does not stop when the program has lost the CPU or when it is for an external event. It is recommend to use `run_time_limit` as the main limit, but set `wall_time_limit` to a much higher value as a precaution against sleeping programs | 74 | | stack_size_limit | `number` | 128000 | Limit process stack to size kilobytes. It is subject to `memory_limit` | 75 | | process_count_limit | `number` | 120 | Permit the program to create up to max processes and/or threads | 76 | | memory_limit | `number` | 512000 | Limit total memory usage by the whole control group in kilobytes | 77 | | storage_limit | `number` | 10240 | Limit size of files created (or modified) by the program in kilobytes | 78 | 79 | ## Example 80 | **The files should be passed as a base64 zip archive.** 81 | 82 | The folowing demonstration uses the folowing file architecture: 83 | ``` 84 | . 85 | └── src 86 | └── main.c 87 | ``` 88 | 89 | Encoded using command `zip -q -r - * | base64` (could have been a library, it doesn't matter while it keeps beeing `files -> zip -> base64`). 90 | 91 | ```json 92 | { 93 | "phases": [ 94 | { 95 | "name": "Compilation", 96 | "script": "/usr/local/gcc-11.1.0/bin/gcc src/main.c -o out", 97 | "sandbox_settings": { 98 | "run_time_limit": 20, 99 | "wall_time_limit": 40 100 | } 101 | }, 102 | { 103 | "name": "Execution", 104 | "script": "./out" 105 | } 106 | ], 107 | "environment": { 108 | "ENABLE_AWESOME_SHEEP": "true" 109 | }, 110 | "files": "UEsDBAoAAAAAAJe1pVIAAAAAAAAAAAAAAAAEABwAc3JjL1VUCQADvgOTYNQDk2B1eAsAAQT1AQAABBQAAABQSwMEFAAIAAgABbalUgAAAAAAAAAATQAAAAoAHABzcmMvbWFpbi5jVVQJAAOKBJNgjASTYHV4CwABBPUBAAAEFAAAAFPOzEvOKU1JVbApLknJzNfLsOPiyswrUchNzMzT0OSq5lIAgoLSkmINJY/UnJx8HYXw/KKcFEUlTWsusFxRaklpUZ6CgTVXLRcAUEsHCMUkHr9KAAAATQAAAFBLAQIeAwoAAAAAAJe1pVIAAAAAAAAAAAAAAAAEABgAAAAAAAAAEADtQQAAAABzcmMvVVQFAAO+A5NgdXgLAAEE9QEAAAQUAAAAUEsBAh4DFAAIAAgABbalUsUkHr9KAAAATQAAAAoAGAAAAAAAAQAAAKSBPgAAAHNyYy9tYWluLmNVVAUAA4oEk2B1eAsAAQT1AQAABBQAAABQSwUGAAAAAAIAAgCaAAAA3AAAAAAA" 111 | } 112 | ``` 113 | 114 | ### Output 115 | ```json 116 | { 117 | "phases": [ 118 | { 119 | "name": "Compilation", 120 | "status": 0, 121 | "stdout": "", 122 | "stderr": "", 123 | "time": 0.037, 124 | "time_wall": 0.043, 125 | "used_memory": 6640, 126 | "sandbox_status": null, 127 | "csw_voluntary": 18, 128 | "csw_forced": 16 129 | }, 130 | { 131 | "name": "Execution", 132 | "status": 0, 133 | "stdout": "Hello, World!\n", 134 | "stderr": "", 135 | "time": 0.002, 136 | "time_wall": 0.007, 137 | "used_memory": 856, 138 | "sandbox_status": null, 139 | "csw_voluntary": 7, 140 | "csw_forced": 0 141 | } 142 | ] 143 | } 144 | ``` 145 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | api: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.dev 8 | privileged: true 9 | volumes: 10 | - ./src:/usr/src/app/src 11 | - ./target:/usr/src/app/target 12 | - ./Cargo.lock:/usr/src/app/Cargo.lock 13 | - ./Cargo.toml:/usr/src/app/Cargo.toml 14 | ports: 15 | - 8080:8080 16 | tty: true 17 | environment: 18 | API_MAX_PAYLOAD_SIZE: 32768 19 | ALLOW_PROFILING: "true" 20 | MAX_RUN_TIME_LIMIT: -1 21 | MAX_EXTRA_TIME_LIMIT: -1 22 | MAX_WALL_TIME_LIMIT: -1 23 | MAX_STACK_SIZE_LIMIT: -1 24 | MAX_PROCESS_COUNT_LIMIT: -1 25 | MAX_MEMORY_LIMIT: -1 26 | MAX_STORAGE_LIMIT: -1 27 | -------------------------------------------------------------------------------- /src/api_helpers.rs: -------------------------------------------------------------------------------- 1 | use actix_web::dev::Body; 2 | use actix_web::http::header::CONTENT_TYPE; 3 | use actix_web::http::HeaderValue; 4 | use actix_web::http::StatusCode; 5 | use actix_web::HttpResponse; 6 | use actix_web::ResponseError; 7 | use actix_web::Result as ActixResult; 8 | use actix_web::web::Json; 9 | use derive_more::{Display, Error}; 10 | use serde::Serialize; 11 | 12 | pub type ApiResult = ActixResult>; 13 | 14 | #[derive(Debug, Serialize, Display, Error)] 15 | #[display(fmt = "API Error {}: {}", status, message)] 16 | pub struct ApiError { 17 | pub status: u16, 18 | pub message: String, 19 | } 20 | 21 | impl ApiError { 22 | pub fn new>(status: StatusCode, message: S) -> ApiError { 23 | ApiError { 24 | status: status.as_u16(), 25 | message: message.into(), 26 | } 27 | } 28 | 29 | pub fn not_found>(message: S) -> ApiError { 30 | ApiError::new(StatusCode::NOT_FOUND, message) 31 | } 32 | 33 | pub fn bad_request>(message: S) -> ApiError { 34 | ApiError::new(StatusCode::BAD_REQUEST, message) 35 | } 36 | 37 | pub fn internal_server_error>(message: S) -> ApiError { 38 | ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, message) 39 | } 40 | } 41 | 42 | impl ResponseError for ApiError { 43 | fn status_code(&self) -> StatusCode { 44 | StatusCode::from_u16(self.status).unwrap() 45 | } 46 | 47 | fn error_response(&self) -> HttpResponse { 48 | let mut resp = HttpResponse::new(self.status_code()); 49 | resp.headers_mut().insert( 50 | CONTENT_TYPE, 51 | HeaderValue::from_static("application/json; charset=utf-8"), 52 | ); 53 | 54 | resp.set_body(Body::from(serde_json::to_string(&self).unwrap())) 55 | } 56 | } 57 | 58 | impl From for Result { 59 | fn from(error: ApiError) -> Result { 60 | Err(error) 61 | } 62 | } 63 | 64 | impl From for ActixResult { 65 | fn from(error: ApiError) -> ActixResult { 66 | Err(error.into()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/isolate.rs: -------------------------------------------------------------------------------- 1 | use rand::{thread_rng, Rng}; 2 | use serde::Serialize; 3 | use std::fs::{self, File}; 4 | use std::io; 5 | use std::io::prelude::*; 6 | use std::os::unix::prelude::ExitStatusExt; 7 | use std::path::{Path, PathBuf}; 8 | use std::process::Command; 9 | use std::process::ExitStatus; 10 | use std::{collections::HashMap, process::Stdio}; 11 | 12 | use crate::utils; 13 | 14 | #[derive(Debug)] 15 | pub struct ExecutedCommandResult { 16 | pub status: ExitStatus, 17 | pub stdout: String, 18 | pub stderr: String, 19 | } 20 | 21 | fn exec_command( 22 | args: I, 23 | stdout: Option, 24 | stderr: Option, 25 | stdin: Option, 26 | ) -> io::Result 27 | where 28 | I: IntoIterator, 29 | S: Into, 30 | { 31 | let mut args_string: Vec = args.into_iter().map(Into::into).collect(); 32 | 33 | let program = args_string.remove(0); 34 | 35 | println!( 36 | "Executing command: {} {}", 37 | program, 38 | args_string.join(" ").to_string() 39 | ); 40 | 41 | let mut child = Command::new(program) 42 | .args(args_string) 43 | .stdout(stdout.unwrap_or(Stdio::piped())) 44 | .stderr(stderr.unwrap_or(Stdio::piped())) 45 | .stdin(Stdio::piped()) 46 | .spawn()?; 47 | 48 | if let Some(stdin_string) = stdin { 49 | child 50 | .stdin 51 | .take() 52 | .unwrap() 53 | .write_all(stdin_string.as_bytes())?; 54 | } 55 | 56 | let output = child.wait_with_output()?; 57 | 58 | let stdout = String::from_utf8_lossy(&output.stdout).to_string(); 59 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 60 | 61 | Ok(ExecutedCommandResult { 62 | status: output.status, 63 | stdout, 64 | stderr, 65 | }) 66 | } 67 | 68 | #[derive(Default, Debug, Builder, Clone, Serialize)] 69 | #[builder(default)] 70 | pub struct IsolateMetadata { 71 | pub time: Option, 72 | pub time_wall: Option, 73 | pub max_rss: Option, 74 | pub csw_voluntary: Option, 75 | pub csw_forced: Option, 76 | pub cg_mem: Option, 77 | pub exit_code: Option, 78 | pub status: Option, 79 | } 80 | 81 | impl From for IsolateMetadata { 82 | fn from(string: String) -> Self { 83 | let mut builder = IsolateMetadataBuilder::default(); 84 | 85 | for metadata in string.lines() { 86 | let values = metadata.split(':').collect::>(); 87 | 88 | if values.len() < 2 { 89 | continue; 90 | } 91 | 92 | let key = values[0]; 93 | let value = values[1]; 94 | 95 | match key { 96 | "time" => builder.time(Some(value.parse().unwrap())), 97 | "time-wall" => builder.time_wall(Some(value.parse().unwrap())), 98 | "max-rss" => builder.max_rss(Some(value.parse().unwrap())), 99 | "csw-voluntary" => builder.csw_voluntary(Some(value.parse().unwrap())), 100 | "csw-forced" => builder.csw_forced(Some(value.parse().unwrap())), 101 | "cg-mem" => builder.cg_mem(Some(value.parse().unwrap())), 102 | "exitcode" => builder.exit_code(Some(value.parse().unwrap())), 103 | "status" => builder.status(Some(value.to_string())), 104 | _ => &mut builder, 105 | }; 106 | } 107 | 108 | builder.build().unwrap() 109 | } 110 | } 111 | 112 | #[derive(Debug, Clone)] 113 | pub struct IsolatedExecutedCommandResult { 114 | pub status: ExitStatus, 115 | pub stdout: String, 116 | pub stderr: String, 117 | pub metadata: IsolateMetadata, 118 | } 119 | 120 | #[derive(Debug, Clone)] 121 | pub struct IsolatedBox { 122 | pub box_id: u32, 123 | pub workdir: String, 124 | 125 | stdout_file: String, 126 | stderr_file: String, 127 | metadata_file: String, 128 | } 129 | 130 | #[derive(Default, Debug, Builder, Clone)] 131 | #[builder(setter(into))] 132 | pub struct IsolatedBoxOptions { 133 | #[builder(default)] 134 | pub environment: Option>, 135 | 136 | #[builder(default)] 137 | pub stdin: Option, 138 | 139 | #[builder(default = "false")] 140 | pub profiling: bool, 141 | 142 | #[builder(default = "utils::parsed_env::get(\"MAX_RUN_TIME_LIMIT\", 5)")] 143 | pub run_time_limit: u64, 144 | 145 | #[builder(default = "utils::parsed_env::get(\"MAX_EXTRA_TIME_LIMIT\", 0)")] 146 | pub extra_time_limit: u64, 147 | 148 | #[builder(default = "utils::parsed_env::get(\"MAX_WALL_TIME_LIMIT\", 10)")] 149 | pub wall_time_limit: u64, 150 | 151 | #[builder(default = "utils::parsed_env::get(\"MAX_STACK_SIZE_LIMIT\", 128000)")] 152 | pub stack_size_limit: u64, 153 | 154 | #[builder(default = "utils::parsed_env::get(\"MAX_PROCESS_COUNT_LIMIT\", 120)")] 155 | pub process_count_limit: u64, 156 | 157 | #[builder(default = "utils::parsed_env::get(\"MAX_MEMORY_LIMIT\", 512000)")] 158 | pub memory_limit: u64, 159 | 160 | #[builder(default = "utils::parsed_env::get(\"MAX_STORAGE_LIMIT\", 10240)")] 161 | pub storage_limit: u64, 162 | } 163 | 164 | impl IsolatedBox { 165 | pub fn new(box_id: u32) -> io::Result { 166 | let output = exec_command( 167 | vec!["isolate", "--cg", &format!("-b {}", box_id), "--init"], 168 | None, 169 | None, 170 | None, 171 | )?; 172 | 173 | let workdir = output.stdout.trim().to_string(); 174 | 175 | let stdout_file = Self::create_file(workdir.clone(), "stdout")?; 176 | let stderr_file = Self::create_file(workdir.clone(), "stderr")?; 177 | let metadata_file = Self::create_file(workdir.clone(), "metadata")?; 178 | 179 | Ok(IsolatedBox { 180 | box_id, 181 | workdir, 182 | stdout_file, 183 | stderr_file, 184 | metadata_file, 185 | }) 186 | } 187 | 188 | fn create_file(workdir: S1, filename: S2) -> io::Result 189 | where 190 | S1: Into, 191 | S2: Into, 192 | { 193 | let filepath = format!("{}/{}", workdir.into(), filename.into()); 194 | 195 | exec_command(vec!["touch", &filepath], None, None, None)?; 196 | exec_command(vec!["chown", "$(whoami):", &filepath], None, None, None)?; 197 | 198 | Ok(filepath) 199 | } 200 | 201 | pub fn upload_file>(&self, path_string: S, buf: &[u8]) -> io::Result { 202 | let path = Path::new(&path_string.into()).to_owned(); 203 | 204 | let separator = match path.is_absolute() { 205 | true => "", 206 | false => "/", 207 | }; 208 | 209 | if let Some(parent) = path.parent() { 210 | let directory = format!("{}{}{}", self.workdir, separator, parent.to_string_lossy()); 211 | fs::create_dir_all(directory)?; 212 | } 213 | 214 | let file_absolute_path = format!("{}{}{}", self.workdir, separator, path.to_string_lossy()); 215 | 216 | let mut file = File::create(&file_absolute_path)?; 217 | file.write_all(buf)?; 218 | 219 | Ok(Path::new(&file_absolute_path).to_owned()) 220 | } 221 | 222 | pub fn exec( 223 | &self, 224 | script: S, 225 | options: IsolatedBoxOptions, 226 | ) -> io::Result 227 | where 228 | S: Into, 229 | { 230 | let box_id_arg = format!("-b {}", self.box_id); 231 | let metadata_arg = format!("-M{}", self.metadata_file); 232 | let run_time_limit_arg = format!("-t {}", options.run_time_limit); 233 | let extra_time_limit_arg = format!("-x {}", options.extra_time_limit); 234 | let wall_time_limit_arg = format!("-w {}", options.wall_time_limit); 235 | let stack_size_limit_arg = format!("-k {}", options.stack_size_limit); 236 | let process_count_limit_arg = format!("-p{}", options.process_count_limit); 237 | let memory_limit_arg = format!("--cg-mem={}", options.memory_limit); 238 | let storage_limit_arg = format!("-f {}", options.storage_limit); 239 | 240 | let isolate_args = vec![ 241 | "isolate", 242 | "--cg", 243 | // Silent mode - Disable status messages printed to stderr, except for fatal errors of the sandbox itself 244 | "-s", 245 | // Box ID 246 | &box_id_arg, 247 | // Metadata file 248 | &metadata_arg, 249 | // Run time limit 250 | &run_time_limit_arg, 251 | // Extra time limit 252 | &extra_time_limit_arg, 253 | // Wall Time limit 254 | &wall_time_limit_arg, 255 | // Stack size limit 256 | &stack_size_limit_arg, 257 | // Process count limit 258 | &process_count_limit_arg, 259 | // Enable per process/thread time limit 260 | "--cg-timing", 261 | // Memory limit in KB 262 | &memory_limit_arg, 263 | // Storage size limit in KB 264 | &storage_limit_arg, 265 | ]; 266 | 267 | let mut environment_variables = vec![ 268 | "-EHOME=/tmp".into(), 269 | "-EPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(), 270 | ]; 271 | 272 | if let Some(environment) = options.environment.clone() { 273 | for (key, value) in environment.iter() { 274 | environment_variables.push(format!( 275 | "-E{}={}", 276 | key.replace("\\", "\\\\").replace("\"", "\\\""), 277 | value.replace("\\", "\\\\").replace("\"", "\\\"") 278 | )); 279 | } 280 | } 281 | 282 | let mut args: Vec = vec![]; 283 | args.append(&mut isolate_args.iter().map(|&v| v.into()).collect()); 284 | 285 | args.append(&mut environment_variables); 286 | 287 | args.append(&mut vec![ 288 | // Run a command 289 | "--run".into(), 290 | "--".into(), 291 | ]); 292 | 293 | let script_name = format!("/box/.script-{}.sh", thread_rng().gen::()); 294 | 295 | self.upload_file( 296 | script_name.clone(), 297 | format!("{}\n", script.into()).as_bytes(), 298 | )?; 299 | 300 | if options.profiling { 301 | args.append(&mut vec![ 302 | "/usr/bin/perf_5.10".into(), 303 | "record".into(), 304 | "-g".into(), 305 | ]); 306 | } 307 | 308 | args.append(&mut vec!["/bin/bash".into(), script_name.clone()]); 309 | 310 | let stdout_stream = File::create(self.stdout_file.clone())?; 311 | let stderr_stream = File::create(self.stderr_file.clone())?; 312 | 313 | let result = exec_command( 314 | args, 315 | Some(Stdio::from(stdout_stream)), 316 | Some(Stdio::from(stderr_stream)), 317 | options.stdin, 318 | )?; 319 | 320 | let stdout = fs::read_to_string(self.stdout_file.clone())?; 321 | let stderr = fs::read_to_string(self.stderr_file.clone())?; 322 | let metadata_string = fs::read_to_string(self.metadata_file.clone())?; 323 | 324 | let metadata = IsolateMetadata::from(metadata_string); 325 | 326 | Ok(IsolatedExecutedCommandResult { 327 | status: match metadata.exit_code { 328 | Some(exit_code) => ExitStatus::from_raw(exit_code), 329 | None => result.status, 330 | }, 331 | stdout, 332 | stderr, 333 | metadata, 334 | }) 335 | } 336 | } 337 | 338 | #[derive(Debug)] 339 | pub struct Isolate { 340 | pub boxes: HashMap, 341 | } 342 | 343 | impl Isolate { 344 | pub fn new() -> Isolate { 345 | Isolate { 346 | boxes: HashMap::new(), 347 | } 348 | } 349 | 350 | pub fn init_box(&mut self) -> io::Result { 351 | let box_id = thread_rng().gen_range(0..=(i32::MAX as u32)); 352 | let isolated_box = IsolatedBox::new(box_id)?; 353 | 354 | self.boxes.insert(box_id, isolated_box.clone()); 355 | 356 | Ok(isolated_box) 357 | } 358 | 359 | fn cleanup(&self, isolated_box_id: u32) -> io::Result { 360 | let box_id_arg = format!("-b {}", isolated_box_id); 361 | 362 | let isolate_args = vec!["isolate", "--cg", &box_id_arg, "--cleanup"]; 363 | 364 | exec_command(isolate_args, None, None, None) 365 | } 366 | 367 | pub fn destroy_box(&mut self, isolated_box_id: u32) -> io::Result<()> { 368 | self.cleanup(isolated_box_id)?; 369 | 370 | self.boxes.remove(&isolated_box_id); 371 | 372 | Ok(()) 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{error::InternalError, App, HttpResponse, HttpServer}; 2 | use actix_web_validator::{Error, JsonConfig}; 3 | use serde::Serialize; 4 | use std::{env, io}; 5 | use validator::ValidationErrors; 6 | 7 | extern crate derive_more; 8 | 9 | #[macro_use] 10 | extern crate derive_builder; 11 | 12 | mod utils; 13 | mod api_helpers; 14 | mod isolate; 15 | mod routes; 16 | mod runner; 17 | 18 | #[derive(Serialize)] 19 | pub struct ValidationErrorDTO { 20 | pub message: String, 21 | pub fields: Vec, 22 | } 23 | 24 | impl From<&ValidationErrors> for ValidationErrorDTO { 25 | fn from(error: &ValidationErrors) -> Self { 26 | ValidationErrorDTO { 27 | message: "Validation error".to_owned(), 28 | fields: error 29 | .field_errors() 30 | .iter() 31 | .map(|(field, _)| field.to_string()) 32 | .collect(), 33 | } 34 | } 35 | } 36 | 37 | #[actix_web::main] 38 | async fn main() -> io::Result<()> { 39 | HttpServer::new(|| { 40 | App::new() 41 | .app_data( 42 | JsonConfig::default() 43 | .limit( 44 | match env::var("API_MAX_PAYLOAD_SIZE") 45 | .ok() 46 | .and_then(|value| value.parse::().ok()) 47 | { 48 | Some(value) => value.round() as usize, 49 | None => 32768, 50 | }, 51 | ) 52 | .error_handler(|err, _| { 53 | let json_error = match &err { 54 | Error::Validate(error) => ValidationErrorDTO::from(error), 55 | _ => ValidationErrorDTO { 56 | message: err.to_string(), 57 | fields: Vec::new(), 58 | }, 59 | }; 60 | 61 | InternalError::from_response(err, HttpResponse::Conflict().json(json_error)) 62 | .into() 63 | }), 64 | ) 65 | .service(routes::run_post::route) 66 | }) 67 | .bind("0.0.0.0:8080")? 68 | .run() 69 | .await 70 | } 71 | -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod run_post; 2 | -------------------------------------------------------------------------------- /src/routes/run_post.rs: -------------------------------------------------------------------------------- 1 | use crate::api_helpers::{ApiError, ApiResult}; 2 | use crate::runner::phase_settings::{PhaseSandboxSettings, PhaseSettings}; 3 | use crate::runner::runner::Runner; 4 | use crate::runner::runner::RunnerPhaseResult; 5 | use crate::utils; 6 | use actix_web::{post, web::Json}; 7 | use merge::Merge; 8 | use serde::{Deserialize, Serialize}; 9 | use std::collections::HashMap; 10 | use std::env; 11 | use validator::Validate; 12 | 13 | #[derive(Deserialize, Debug, Validate)] 14 | pub struct RunBodyDTO { 15 | phases: Vec, 16 | 17 | environment: Option>, 18 | sandbox_settings: Option, 19 | 20 | files: String, 21 | } 22 | 23 | #[derive(Serialize, Debug, Default, Builder)] 24 | #[builder(setter(into, strip_option), default)] 25 | pub struct RunResponseDTO { 26 | phases: Vec, 27 | } 28 | 29 | fn is_over_cap_limit_env(current_option: Option, name: &str) -> bool { 30 | match current_option { 31 | Some(current) => current > utils::parsed_env::get(name, u64::MAX), 32 | None => false, 33 | } 34 | } 35 | 36 | fn check_body(body: &actix_web_validator::Json) -> Result<(), ApiError> { 37 | fn setting_max_value_error(origin: &str, env_name: &str) -> ApiError { 38 | ApiError::bad_request(format!( 39 | "{}: maximum allowed value is {}", 40 | origin, 41 | env::var(env_name).unwrap_or(u64::MAX.to_string()) 42 | )) 43 | } 44 | 45 | macro_rules! check_cap_limit { 46 | ($origin_str:expr, $origin_expr:expr, $env_name:expr) => { 47 | if is_over_cap_limit_env($origin_expr, $env_name) { 48 | return setting_max_value_error($origin_str, $env_name).into(); 49 | } 50 | }; 51 | } 52 | 53 | if let Some(sandbox_settings) = &body.sandbox_settings { 54 | #[cfg_attr(rustfmt, rustfmt_skip)] 55 | { 56 | check_cap_limit!("sandbox_settings.run_time_limit", sandbox_settings.run_time_limit, "MAX_RUN_TIME_LIMIT"); 57 | check_cap_limit!("sandbox_settings.extra_time_limit", sandbox_settings.extra_time_limit, "MAX_EXTRA_TIME_LIMIT"); 58 | check_cap_limit!("sandbox_settings.wall_time_limit", sandbox_settings.wall_time_limit, "MAX_WALL_TIME_LIMIT"); 59 | check_cap_limit!("sandbox_settings.stack_size_limit", sandbox_settings.stack_size_limit, "MAX_STACK_SIZE_LIMIT"); 60 | check_cap_limit!("sandbox_settings.process_count_limit", sandbox_settings.process_count_limit, "MAX_PROCESS_COUNT_LIMIT"); 61 | check_cap_limit!("sandbox_settings.memory_limit", sandbox_settings.memory_limit, "MAX_MEMORY_LIMIT"); 62 | check_cap_limit!("sandbox_settings.storage_limit", sandbox_settings.storage_limit, "MAX_STORAGE_LIMIT"); 63 | } 64 | } 65 | 66 | let allow_profiling = match env::var("ALLOW_PROFILING") { 67 | Ok(value) => { 68 | let lowercase_value = value.to_lowercase(); 69 | ["true", "yes"].iter().any(|&s| s == lowercase_value) 70 | }, 71 | Err(_) => true, 72 | }; 73 | 74 | for i in 0..body.phases.len() { 75 | let phase_settings = body.phases[i].clone(); 76 | 77 | if let Some(profiling) = phase_settings.profiling { 78 | if profiling && !allow_profiling { 79 | return ApiError::bad_request("Profiling is not allowed").into(); 80 | } 81 | } 82 | 83 | if let Some(sandbox_settings) = phase_settings.sandbox_settings { 84 | #[cfg_attr(rustfmt, rustfmt_skip)] 85 | { 86 | check_cap_limit!(&format!("phases[{}].sandbox_settings.run_time_limit", i), sandbox_settings.run_time_limit, "MAX_RUN_TIME_LIMIT"); 87 | check_cap_limit!(&format!("phases[{}].sandbox_settings.extra_time_limit", i), sandbox_settings.extra_time_limit, "MAX_EXTRA_TIME_LIMIT"); 88 | check_cap_limit!(&format!("phases[{}].sandbox_settings.wall_time_limit", i), sandbox_settings.wall_time_limit, "MAX_WALL_TIME_LIMIT"); 89 | check_cap_limit!(&format!("phases[{}].sandbox_settings.stack_size_limit", i), sandbox_settings.stack_size_limit, "MAX_STACK_SIZE_LIMIT"); 90 | check_cap_limit!(&format!("phases[{}].sandbox_settings.process_count_limit", i), sandbox_settings.process_count_limit, "MAX_PROCESS_COUNT_LIMIT"); 91 | check_cap_limit!(&format!("phases[{}].sandbox_settings.memory_limit", i), sandbox_settings.memory_limit, "MAX_MEMORY_LIMIT"); 92 | check_cap_limit!(&format!("phases[{}].sandbox_settings.storage_limit", i), sandbox_settings.storage_limit, "MAX_STORAGE_LIMIT"); 93 | } 94 | } 95 | } 96 | 97 | Ok(()) 98 | } 99 | 100 | #[post("/run")] 101 | pub async fn route(body: actix_web_validator::Json) -> ApiResult { 102 | if let Err(e) = check_body(&body) { 103 | return e.into(); 104 | } 105 | 106 | let mut runner = match Runner::new() { 107 | Ok(v) => v, 108 | Err(e) => { 109 | return ApiError::internal_server_error(format!( 110 | "Failed to initialize the isolated environment: {}", 111 | e 112 | )) 113 | .into(); 114 | } 115 | }; 116 | 117 | let mut results = vec![]; 118 | 119 | let isolated_box_id = match runner.setup(&body.files) { 120 | Ok(v) => v, 121 | Err(e) => return e.into(), 122 | }; 123 | 124 | for i in 0..body.phases.len() { 125 | let mut phase_settings = body.phases[i].clone(); 126 | 127 | phase_settings.name = phase_settings.name.or(Some(i.to_string())); 128 | 129 | if let Some(environment) = body.environment.clone() { 130 | if let Some(phase_environment) = &mut phase_settings.environment { 131 | phase_environment.extend(environment.into_iter()); 132 | } else { 133 | phase_settings.environment = Some(environment); 134 | } 135 | } 136 | 137 | if let Some(sandbox_settings) = body.sandbox_settings.clone() { 138 | if let Some(phase_sandbox_settings) = &mut phase_settings.sandbox_settings { 139 | phase_sandbox_settings.merge(sandbox_settings); 140 | } else { 141 | phase_settings.sandbox_settings = Some(sandbox_settings); 142 | } 143 | } 144 | 145 | let result = match runner.run_phase(isolated_box_id, &phase_settings) { 146 | Ok(v) => v, 147 | Err(e) => return e.into(), 148 | }; 149 | 150 | let status = result.status; 151 | 152 | results.push(result); 153 | 154 | if status != 0 { 155 | break; 156 | } 157 | } 158 | 159 | Ok(Json(RunResponseDTO { phases: results })) 160 | } 161 | -------------------------------------------------------------------------------- /src/runner/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod phase_settings; 2 | pub mod runner; 3 | -------------------------------------------------------------------------------- /src/runner/phase_settings.rs: -------------------------------------------------------------------------------- 1 | use crate::isolate::{IsolatedBoxOptions, IsolatedBoxOptionsBuilder}; 2 | use merge::Merge; 3 | use serde::Deserialize; 4 | use std::collections::HashMap; 5 | use validator::Validate; 6 | 7 | #[derive(Deserialize, Debug, Clone, Default, Merge, Validate)] 8 | pub struct PhaseSandboxSettings { 9 | pub run_time_limit: Option, 10 | pub extra_time_limit: Option, 11 | pub wall_time_limit: Option, 12 | pub stack_size_limit: Option, 13 | pub process_count_limit: Option, 14 | pub memory_limit: Option, 15 | pub storage_limit: Option, 16 | } 17 | 18 | #[derive(Deserialize, Debug, Clone, Validate)] 19 | pub struct PhaseSettings { 20 | pub name: Option, 21 | 22 | pub script: String, 23 | pub stdin: Option, 24 | 25 | pub environment: Option>, 26 | 27 | pub sandbox_settings: Option, 28 | pub profiling: Option, 29 | } 30 | 31 | impl From for IsolatedBoxOptions { 32 | fn from(settings: PhaseSettings) -> Self { 33 | let mut options = IsolatedBoxOptionsBuilder::default(); 34 | 35 | if let Some(sandbox_settings) = settings.sandbox_settings { 36 | if let Some(run_time_limit) = sandbox_settings.run_time_limit { 37 | options.run_time_limit(run_time_limit); 38 | } 39 | 40 | if let Some(extra_time_limit) = sandbox_settings.extra_time_limit { 41 | options.extra_time_limit(extra_time_limit); 42 | } 43 | 44 | if let Some(wall_time_limit) = sandbox_settings.wall_time_limit { 45 | options.wall_time_limit(wall_time_limit); 46 | } 47 | 48 | if let Some(stack_size_limit) = sandbox_settings.stack_size_limit { 49 | options.stack_size_limit(stack_size_limit); 50 | } 51 | 52 | if let Some(process_count_limit) = sandbox_settings.process_count_limit { 53 | options.process_count_limit(process_count_limit); 54 | } 55 | 56 | if let Some(memory_limit) = sandbox_settings.memory_limit { 57 | options.memory_limit(memory_limit); 58 | } 59 | 60 | if let Some(storage_limit) = sandbox_settings.storage_limit { 61 | options.storage_limit(storage_limit); 62 | } 63 | } 64 | 65 | options.environment(settings.environment.clone()); 66 | 67 | if let Some(stdin) = settings.stdin { 68 | options.stdin(stdin); 69 | } 70 | 71 | if let Some(profiling) = settings.profiling { 72 | options.profiling(profiling); 73 | } 74 | 75 | options.build().unwrap() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/runner/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::api_helpers::ApiError; 2 | use crate::isolate::{ 3 | Isolate, IsolateMetadataBuilder, IsolatedBox, IsolatedBoxOptions, IsolatedBoxOptionsBuilder, 4 | IsolatedExecutedCommandResult, 5 | }; 6 | use serde::Serialize; 7 | use std::io; 8 | use std::os::unix::prelude::ExitStatusExt; 9 | use std::process::ExitStatus; 10 | 11 | use super::phase_settings::PhaseSettings; 12 | 13 | #[derive(Serialize, Debug, Clone)] 14 | pub struct RunnerPhaseResult { 15 | pub name: Option, 16 | pub status: i32, 17 | pub stdout: String, 18 | pub stderr: String, 19 | 20 | pub time: Option, 21 | pub time_wall: Option, 22 | pub used_memory: Option, 23 | pub sandbox_status: Option, 24 | pub csw_voluntary: Option, 25 | pub csw_forced: Option, 26 | } 27 | 28 | pub struct Runner { 29 | isolate: Isolate, 30 | } 31 | 32 | impl Runner { 33 | pub fn new() -> io::Result { 34 | let runner = Runner { 35 | isolate: Isolate::new(), 36 | }; 37 | 38 | Ok(runner) 39 | } 40 | 41 | fn get_isolated_box(&self, isolated_box_id: u32) -> Result<&IsolatedBox, ApiError> { 42 | match self.isolate.boxes.get(&isolated_box_id) { 43 | Some(v) => Ok(v), 44 | None => { 45 | return ApiError::internal_server_error(format!( 46 | "Unknown isolated box ID: {}", 47 | isolated_box_id 48 | )) 49 | .into() 50 | } 51 | } 52 | } 53 | 54 | fn cleanup_isolated_box(&mut self, isolated_box_id: u32) -> Result<(), ApiError> { 55 | if let Err(e) = self.isolate.destroy_box(isolated_box_id) { 56 | println!("Failed to cleanup the box {}: {}", isolated_box_id, e); 57 | } 58 | 59 | Ok(()) 60 | } 61 | 62 | pub fn setup(&mut self, files: &String) -> Result { 63 | let isolated_box = match self.isolate.init_box() { 64 | Ok(v) => v, 65 | Err(e) => { 66 | return ApiError::internal_server_error(format!( 67 | "Failed to initialize a new box: {}", 68 | e 69 | )) 70 | .into() 71 | } 72 | }; 73 | 74 | let files_buffer = match base64::decode(&files) { 75 | Ok(buf) => buf, 76 | Err(e) => { 77 | return ApiError::bad_request(format!("Error while reading files: {}", e)).into() 78 | } 79 | }; 80 | 81 | if let Err(e) = isolated_box.upload_file("/box/files.zip", &files_buffer) { 82 | return ApiError::internal_server_error(format!( 83 | "Failed to upload files into the isolated environment: {}", 84 | e, 85 | )) 86 | .into(); 87 | } 88 | 89 | let unzip_result = self.exec_isolated_box( 90 | &isolated_box, 91 | "/usr/bin/unzip -n -qq /box/files.zip && /bin/rm /box/files.zip", 92 | IsolatedBoxOptionsBuilder::default().build().unwrap(), 93 | ); 94 | 95 | if !unzip_result.status.success() { 96 | self.cleanup_isolated_box(isolated_box.box_id)?; 97 | 98 | return ApiError::bad_request(format!( 99 | "Error while unzipping files: {}", 100 | unzip_result.stderr 101 | )) 102 | .into(); 103 | } 104 | 105 | Ok(isolated_box.box_id) 106 | } 107 | 108 | fn exec_isolated_box( 109 | &self, 110 | isolated_box: &IsolatedBox, 111 | script: S, 112 | options: IsolatedBoxOptions, 113 | ) -> IsolatedExecutedCommandResult 114 | where 115 | S: Into, 116 | { 117 | match isolated_box.exec(script, options) { 118 | Ok(result) => result, 119 | Err(e) => IsolatedExecutedCommandResult { 120 | status: ExitStatus::from_raw(1), 121 | stderr: e.to_string(), 122 | stdout: "".to_string(), 123 | metadata: IsolateMetadataBuilder::default().build().unwrap(), 124 | }, 125 | } 126 | } 127 | 128 | fn exec( 129 | &self, 130 | isolated_box_id: u32, 131 | script: S, 132 | options: IsolatedBoxOptions, 133 | ) -> Result 134 | where 135 | S: Into, 136 | { 137 | let isolated_box = self.get_isolated_box(isolated_box_id)?; 138 | 139 | Ok(self.exec_isolated_box(&isolated_box, script, options)) 140 | } 141 | 142 | pub fn run_phase( 143 | &mut self, 144 | isolated_box_id: u32, 145 | settings: &PhaseSettings, 146 | ) -> Result { 147 | let result = self.exec(isolated_box_id, &settings.script, settings.clone().into())?; 148 | 149 | if !result.status.success() { 150 | self.cleanup_isolated_box(isolated_box_id)?; 151 | } 152 | 153 | Ok(RunnerPhaseResult { 154 | name: settings.name.clone(), 155 | status: result.status.code().unwrap_or(1), 156 | stderr: result.stderr, 157 | stdout: result.stdout, 158 | 159 | time: result.metadata.time, 160 | time_wall: result.metadata.time_wall, 161 | used_memory: result.metadata.cg_mem, 162 | sandbox_status: result.metadata.status, 163 | csw_voluntary: result.metadata.csw_voluntary, 164 | csw_forced: result.metadata.csw_forced, 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod parsed_env; 2 | -------------------------------------------------------------------------------- /src/utils/parsed_env.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | pub fn get(name: S, default: u64) -> u64 4 | where 5 | S: Into, 6 | { 7 | let name_string = name.into(); 8 | 9 | match env::var(name_string.clone()) { 10 | Ok(value) => match value.as_str() { 11 | "-1" => default, 12 | _ => match value.parse() { 13 | Ok(max) => max, 14 | Err(e) => { 15 | eprintln!( 16 | "Failed to parse environment variable '{}' as an `u64`: {}", 17 | name_string, e 18 | ); 19 | 20 | default 21 | } 22 | }, 23 | }, 24 | Err(_) => default, 25 | } 26 | } 27 | --------------------------------------------------------------------------------