├── .envrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── crates ├── alerion_cli │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── alerion_core │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── config │ │ └── wings_compat.rs │ │ ├── filesystem.rs │ │ ├── lib.rs │ │ ├── servers.rs │ │ ├── servers │ │ └── remote.rs │ │ ├── sftp │ │ └── server.rs │ │ ├── webserver.rs │ │ ├── webserver │ │ ├── middleware.rs │ │ ├── middleware │ │ │ └── bearer_auth.rs │ │ ├── websocket.rs │ │ └── websocket │ │ │ └── auth.rs │ │ └── websocket.rs └── alerion_datamodel │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── remote.rs │ ├── remote │ └── server.rs │ ├── webserver.rs │ ├── webserver │ └── update.rs │ └── websocket.rs ├── flake.lock ├── flake.nix ├── rust-toolchain.toml └── rustfmt.toml /.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | [push] 3 | 4 | name: Build and Lint 5 | permissions: 6 | contents: write 7 | 8 | env: 9 | RUSTFLAGS: -Dwarnings 10 | # VCPKG_ROOT: ${{ github.workspace }}/vcpkg 11 | # LIBCLANG_PATH: ${{ runner.temp }}/llvm/lib 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | token: ${{ secrets.PAT }} 20 | 21 | - name: Install Rust 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: stable 25 | override: true 26 | 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: nightly 30 | profile: minimal 31 | components: rustfmt 32 | 33 | - name: Set up cache 34 | uses: actions/cache/restore@v4 35 | with: 36 | path: | 37 | ~/.cargo/bin/ 38 | ~/.cargo/registry/index/ 39 | ~/.cargo/registry/cache/ 40 | ~/.cargo/git/db/ 41 | ./target/ 42 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 43 | 44 | - name: Clippy Lints 45 | run: | 46 | cargo clippy --all-targets --all-features 47 | 48 | # - id: mirai_check 49 | # run: | 50 | # echo "mirai=$(cargo mirai --help && echo 1 || echo 0)" >> $GITHUB_OUTPUT 51 | 52 | #- name: Install LLVM and Clang 53 | # uses: KyleMayes/install-llvm-action@v2 54 | #with: 55 | # version: "10.0" 56 | # directory: ${{ runner.temp }}/llvm 57 | 58 | #- run: | 59 | # echo "LIBCLANG_PATH=$( ${{ runner.temp }}/llvm/bin )" >> $GITHUB_ENV 60 | 61 | #- if: steps.mirai_check.outputs.mirai == 0 62 | # working-directory: ${{ runner.temp }} 63 | #run: | 64 | #sudo ln -s ${{ env.LLVM_PATH }}/lib/libclang-11.so.1 /lib/x86_64-linux-gnu/libclang.so 65 | # git clone https://github.com/facebookexperimental/MIRAI.git 66 | #git clone --depth=1 https://github.com/llvm/llvm-project.git 67 | 68 | #sudo apt update 69 | # sudo apt install -y cmake build-essential libtool autoconf libncurses-dev 70 | 71 | #cd llvm-project 72 | #mkdir build 73 | #cd build 74 | #cmake -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" ../llvm 75 | #make 76 | 77 | #echo "${{ runner.temp }}/llvm/build/bin" >> $GITHUB_PATH 78 | #echo "LIBCLANG_PATH=${{ runner.temp }}/llvm/build/lib" >> $GITHUB_ENV 79 | 80 | #- name: Install Z3 81 | # if: steps.mirai_check.outputs.mirai == 0 82 | #uses: johnwason/vcpkg-action@v5 83 | #with: 84 | # pkgs: z3 85 | # triplet: x64-linux 86 | # cache-key: ${{ runner.os }} 87 | # revision: master 88 | # token: ${{ secrets.PAT }} 89 | # extra-args: --clean-buildtrees-after-build 90 | 91 | #- name: Install mirai 92 | # working-directory: ${{ runner.temp }} 93 | # if: steps.mirai_check.outputs.mirai == 0 94 | #run: | 95 | # echo $LIBCLANG_PATH 96 | # ls -la $LIBCLANG_PATH 97 | # cd MIRAI 98 | # cargo install --locked --force --path ./checker --no-default-features --features=vcpkg 99 | 100 | #- name: Run mirai 101 | # run: | 102 | # cargo mirai --diag=default 103 | 104 | - name: Build 105 | run: | 106 | cargo build --release 107 | 108 | - name: Format 109 | run: | 110 | cargo +nightly fmt 111 | git config --global user.name "GitHub Actions" 112 | git config --global user.email "team@pyro.host" 113 | 114 | if [ "`git status --porcelain`" ]; then git commit -am "refactor: rustfmt [skip ci]" && git push; fi 115 | 116 | - name: Set up cache 117 | uses: actions/cache/restore@v4 118 | with: 119 | path: | 120 | ~/.cargo/bin/ 121 | ~/.cargo/registry/index/ 122 | ~/.cargo/registry/cache/ 123 | ~/.cargo/git/db/ 124 | ./target/ 125 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 126 | 127 | - name: Upload Artifacts 128 | uses: actions/upload-artifact@v4 129 | with: 130 | path: ./target/release/alerion* 131 | -------------------------------------------------------------------------------- /.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 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | 21 | .direnv/ 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "crates/*" ] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Pyro Alerion, available at https://github.com/pyrohost/alerion, is licensed by Pyro Host Inc. under the [Pyro Source Available License (PSAL)](https://github.com/pyrohost/legal/blob/main/licenses/PSAL.md). Your access to and use of content in this repository is governed by the terms of the PSAL. If you don't agree to the terms of the PSAL, you are not permitted to access or use content available in this repository. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation Notice 2 | 3 | **Development of Alerion has ceased**. Pyro is working on new innovative technologies to bring unparalleled experiences to gamers around the globe. 4 | 5 | See you soon on [pyro.host](https://pyro.host). 6 | 7 | 8 | ### License 9 | 10 | Pyro Alerion, available at https://github.com/pyrohost/alerion, is licensed by Pyro Host Inc. under the [Pyro Source Available License (PSAL)](https://github.com/pyrohost/legal/blob/main/licenses/PSAL.md). Your access to and use of content in this repository is governed by the terms of the PSAL. If you don't agree to the terms of the PSAL, you are not permitted to access or use content available in this repository. 11 | -------------------------------------------------------------------------------- /crates/alerion_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "alerion_cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | alerion_core = { version = "0.1.0", path = "../alerion_core" } 8 | anyhow = "1.0.82" 9 | tokio = { version = "1.37.0", features = ["rt", "macros"] } 10 | tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] } 11 | -------------------------------------------------------------------------------- /crates/alerion_cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::unwrap_used)] 2 | #![allow(dead_code)] 3 | 4 | use tracing_subscriber::filter; 5 | 6 | #[tokio::main(flavor = "current_thread")] 7 | async fn main() -> anyhow::Result<()> { 8 | tracing_subscriber::fmt() 9 | .with_env_filter(filter::EnvFilter::from_default_env()) 10 | .init(); 11 | 12 | alerion_core::alerion_main().await?; 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /crates/alerion_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "alerion_core" 3 | description = "Core alerion functionality" 4 | version = "0.1.0" 5 | edition = "2021" 6 | 7 | [features] 8 | wings_compat = [] 9 | 10 | [dependencies] 11 | alerion_datamodel = { version = "0.1.0", path = "../alerion_datamodel" } 12 | env_logger = "0.11.3" 13 | anyhow = "1.0.82" 14 | tokio = { version = "1.37.0", features = ["rt", "fs", "time", "sync"] } 15 | futures = "0.3.30" 16 | serde = { version = "1.0.197", features = ["derive"] } 17 | serde_yaml = "0.9.34" 18 | serde_json = "1.0.115" 19 | thiserror = "1.0.58" 20 | uuid = { version = "1.8.0", features = ["serde"] } 21 | bytestring = "1.3.1" 22 | jsonwebtoken = "9.3.0" 23 | tracing = { version = "0.1.40", features = ["log"] } 24 | pin-project-lite = "0.2.14" 25 | reqwest = { version = "0.12.3" } 26 | smallvec = { version = "1.13.2", features = ["serde"] } 27 | directories = "5.0.1" 28 | bollard = "0.16.1" 29 | bitflags = "2.5.0" 30 | num_cpus = "1.16.0" 31 | sysinfo = "0.30.11" 32 | poem = { version = "3.0.0", features = ["websocket"] } 33 | -------------------------------------------------------------------------------- /crates/alerion_core/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use anyhow::anyhow; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Clone)] 7 | pub struct AlerionApiSsl { 8 | pub enabled: bool, 9 | pub cert: String, 10 | pub key: String, 11 | } 12 | 13 | #[derive(Debug, Serialize, Deserialize, Clone)] 14 | pub struct AlerionApi { 15 | pub host: IpAddr, 16 | pub port: u16, 17 | pub ssl: AlerionApiSsl, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize, Clone)] 21 | pub struct AlerionAuthentication { 22 | pub token: String, 23 | pub token_id: String, 24 | } 25 | 26 | #[derive(Debug, Serialize, Deserialize, Clone)] 27 | pub struct AlerionConfig { 28 | pub debug: bool, 29 | pub uuid: String, 30 | pub api: AlerionApi, 31 | pub auth: AlerionAuthentication, 32 | pub remote: String, 33 | } 34 | 35 | impl AlerionConfig { 36 | pub fn load(project_dirs: &directories::ProjectDirs) -> anyhow::Result { 37 | tracing::info!( 38 | "Loading Alerion config from {}", 39 | project_dirs.config_dir().display() 40 | ); 41 | let config_path = project_dirs.config_dir().join("config.json"); 42 | let config = std::fs::read_to_string(&config_path).map_err(|e| { 43 | anyhow!( 44 | "Could not read Alerion config from {}: {}", 45 | config_path.display(), 46 | e 47 | ) 48 | })?; 49 | 50 | let config: AlerionConfig = serde_json::from_str(&config)?; 51 | tracing::debug!("Loaded Alerion config: {:?}", config); 52 | Ok(config) 53 | } 54 | 55 | pub fn save(&self, project_dirs: &directories::ProjectDirs) -> anyhow::Result<()> { 56 | let config_path = project_dirs.config_dir().join("config.json"); 57 | let config = serde_json::to_string_pretty(self)?; 58 | 59 | std::fs::write(&config_path, config).map_err(|e| { 60 | anyhow!( 61 | "Could not write Alerion config to {}: {}", 62 | config_path.display(), 63 | e 64 | ) 65 | })?; 66 | 67 | tracing::info!("Saved Alerion config to {}", config_path.display()); 68 | 69 | Ok(()) 70 | } 71 | 72 | #[cfg(feature = "wings_compat")] 73 | pub fn import_wings(&self) -> anyhow::Result { 74 | if !cfg!(target_os = "linux") { 75 | return Err(anyhow!("Wings is not supported on this platform")); 76 | } 77 | 78 | let config = std::fs::read_to_string(wings_compat::WINGS_CONFIG_PATH).map_err(|e| { 79 | anyhow!( 80 | "Could not read Wings config from {}: {}", 81 | wings_compat::WINGS_CONFIG_PATH, 82 | e 83 | ) 84 | })?; 85 | 86 | let config: wings_compat::Config = serde_yaml::from_str(&config)?; 87 | 88 | tracing::debug!("Imported Wings config: {:?}", config); 89 | 90 | Ok(config.into()) 91 | } 92 | } 93 | 94 | #[cfg(feature = "wings_compat")] 95 | mod wings_compat; 96 | -------------------------------------------------------------------------------- /crates/alerion_core/src/config/wings_compat.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, Ipv4Addr}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | 6 | use super::{AlerionApi, AlerionApiSsl, AlerionAuthentication, AlerionConfig}; 7 | 8 | pub const WINGS_CONFIG_PATH: &str = "/etc/pterodactyl/config.yml"; 9 | 10 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 11 | pub struct Config { 12 | pub debug: bool, 13 | pub app_name: String, 14 | pub uuid: String, 15 | pub token_id: String, 16 | pub token: String, 17 | pub api: Api, 18 | pub system: System, 19 | pub docker: Docker, 20 | pub throttles: Throttles, 21 | pub remote: String, 22 | pub remote_query: RemoteQuery, 23 | pub allowed_mounts: Vec, 24 | pub allowed_origins: Vec, 25 | pub allow_cors_private_network: bool, 26 | pub ignore_panel_config_updates: bool, 27 | } 28 | 29 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 30 | pub struct Api { 31 | pub host: String, 32 | pub port: i64, 33 | pub ssl: Ssl, 34 | pub disable_remote_download: bool, 35 | pub upload_limit: i64, 36 | pub trusted_proxies: Vec, 37 | } 38 | 39 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 40 | pub struct Ssl { 41 | pub enabled: bool, 42 | pub cert: String, 43 | pub key: String, 44 | } 45 | 46 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 47 | pub struct System { 48 | pub root_directory: String, 49 | pub log_directory: String, 50 | pub data: String, 51 | pub archive_directory: String, 52 | pub backup_directory: String, 53 | pub tmp_directory: String, 54 | pub username: String, 55 | pub timezone: String, 56 | pub user: User, 57 | pub disk_check_interval: i64, 58 | pub activity_send_interval: i64, 59 | pub activity_send_count: i64, 60 | pub check_permissions_on_boot: bool, 61 | pub enable_log_rotate: bool, 62 | pub websocket_log_count: i64, 63 | pub sftp: Sftp, 64 | pub crash_detection: CrashDetection, 65 | pub backups: Backups, 66 | pub transfers: Transfers, 67 | pub openat_mode: String, 68 | } 69 | 70 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 71 | pub struct User { 72 | pub rootless: Rootless, 73 | pub uid: i64, 74 | pub gid: i64, 75 | } 76 | 77 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 78 | pub struct Rootless { 79 | pub enabled: bool, 80 | pub container_uid: i64, 81 | pub container_gid: i64, 82 | } 83 | 84 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 85 | pub struct Sftp { 86 | pub bind_address: String, 87 | pub bind_port: i64, 88 | pub read_only: bool, 89 | } 90 | 91 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 92 | pub struct CrashDetection { 93 | pub enabled: bool, 94 | pub detect_clean_exit_as_crash: bool, 95 | pub timeout: i64, 96 | } 97 | 98 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 99 | pub struct Backups { 100 | pub write_limit: i64, 101 | pub compression_level: String, 102 | } 103 | 104 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 105 | pub struct Transfers { 106 | pub download_limit: i64, 107 | } 108 | 109 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 110 | pub struct Docker { 111 | pub network: Network, 112 | pub domainname: String, 113 | pub registries: Registries, 114 | pub tmpfs_size: i64, 115 | pub container_pid_limit: i64, 116 | pub installer_limits: InstallerLimits, 117 | pub overhead: Overhead, 118 | pub use_performant_inspect: bool, 119 | pub userns_mode: String, 120 | pub log_config: LogConfig, 121 | } 122 | 123 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 124 | pub struct Network { 125 | pub interface: String, 126 | pub dns: Vec, 127 | pub name: String, 128 | pub ispn: bool, 129 | pub driver: String, 130 | pub network_mode: String, 131 | pub is_internal: bool, 132 | pub enable_icc: bool, 133 | pub network_mtu: i64, 134 | pub interfaces: Interfaces, 135 | } 136 | 137 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 138 | pub struct Interfaces { 139 | pub v4: V4, 140 | pub v6: V6, 141 | } 142 | 143 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 144 | pub struct V4 { 145 | pub subnet: String, 146 | pub gateway: String, 147 | } 148 | 149 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 150 | pub struct V6 { 151 | pub subnet: String, 152 | pub gateway: String, 153 | } 154 | 155 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 156 | pub struct Registries {} 157 | 158 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 159 | pub struct InstallerLimits { 160 | pub memory: i64, 161 | pub cpu: i64, 162 | } 163 | 164 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 165 | pub struct Overhead { 166 | #[serde(rename = "override")] 167 | pub override_field: bool, 168 | pub default_multiplier: f64, 169 | pub multipliers: Multipliers, 170 | } 171 | 172 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 173 | pub struct Multipliers {} 174 | 175 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 176 | pub struct LogConfig { 177 | #[serde(rename = "type")] 178 | pub type_field: String, 179 | pub config: LogFileConfig, 180 | } 181 | 182 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 183 | pub struct LogFileConfig { 184 | pub compress: String, 185 | #[serde(rename = "max-file")] 186 | pub max_file: String, 187 | #[serde(rename = "max-size")] 188 | pub max_size: String, 189 | pub mode: String, 190 | } 191 | 192 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 193 | pub struct Throttles { 194 | pub enabled: bool, 195 | pub lines: i64, 196 | pub line_reset_interval: i64, 197 | } 198 | 199 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 200 | pub struct RemoteQuery { 201 | pub timeout: i64, 202 | pub boot_servers_per_page: i64, 203 | } 204 | 205 | impl From for AlerionConfig { 206 | fn from(root: Config) -> Self { 207 | let api = AlerionApi { 208 | host: root 209 | .api 210 | .host 211 | .parse() 212 | .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)), 213 | port: root.api.port as u16, 214 | ssl: AlerionApiSsl { 215 | enabled: root.api.ssl.enabled, 216 | cert: root.api.ssl.cert, 217 | key: root.api.ssl.key, 218 | }, 219 | }; 220 | 221 | let auth = AlerionAuthentication { 222 | token: root.token, 223 | token_id: root.token_id, 224 | }; 225 | 226 | AlerionConfig { 227 | remote: root.remote, 228 | debug: root.debug, 229 | uuid: root.uuid, 230 | api, 231 | auth, 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /crates/alerion_core/src/filesystem.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use directories::ProjectDirs; 3 | 4 | #[tracing::instrument] 5 | pub async fn setup_directories() -> anyhow::Result { 6 | let project_dirs = ProjectDirs::from("host", "pyro", "alerion") 7 | .context("couldn't determine a home directory for your operating system")?; 8 | 9 | tokio::fs::create_dir_all(project_dirs.config_dir()).await?; 10 | tokio::fs::create_dir_all(project_dirs.data_dir()).await?; 11 | tokio::fs::create_dir_all(project_dirs.cache_dir()).await?; 12 | 13 | tracing::info!("Directories created"); 14 | 15 | Ok(project_dirs) 16 | } 17 | -------------------------------------------------------------------------------- /crates/alerion_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::unwrap_used)] 2 | 3 | use std::sync::Arc; 4 | 5 | use config::AlerionConfig; 6 | use futures::stream::{FuturesUnordered, StreamExt}; 7 | 8 | use crate::filesystem::setup_directories; 9 | use crate::servers::ServerPool; 10 | 11 | pub fn splash() { 12 | println!( 13 | " 14 | 15 | █████ ██ ███████ ██████ ██ ██████ ███ ██ 16 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ 17 | ███████ ██ █████ ██████ ██ ██ ██ ██ ██ ██ 18 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 19 | ██ ██ ███████ ███████ ██ ██ ██ ██████ ██ ████ 20 | 21 | Copyright (c) 2024 Pyro Host Inc. All Right Reserved. 22 | 23 | Pyro Alerion is licensed under the Pyro Source Available 24 | License (PSAL). Your use of this software is governed by 25 | the terms of the PSAL. If you don't agree to the terms of 26 | the PSAL, you are not permitted to use this software. 27 | 28 | License: https://github.com/pyrohost/legal/blob/main/licenses/PSAL.md 29 | Source code: https://github.com/pyrohost/alerion" 30 | ); 31 | } 32 | 33 | /// Alerion main entrypoint. Expects a tokio runtime to be setup. 34 | pub async fn alerion_main() -> anyhow::Result<()> { 35 | splash(); 36 | 37 | tracing::info!("Starting Alerion"); 38 | 39 | let project_dirs = setup_directories().await?; 40 | let config = AlerionConfig::load(&project_dirs)?; 41 | 42 | let server_pool = Arc::new(ServerPool::new(&config).await?); 43 | 44 | //server_pool.create_server("0e4059ca-d79b-46a5-8ec4-95bd0736d150".try_into().unwrap()).await; 45 | 46 | let webserver_handle = tokio::spawn(async move { 47 | let cfg = config.clone(); 48 | let result = webserver::serve(&cfg, Arc::clone(&server_pool)).await; 49 | 50 | match result { 51 | Ok(()) => tracing::info!("webserver exited gracefully"), 52 | Err(e) => tracing::error!("webserver exited with an error: {e}"), 53 | } 54 | }); 55 | 56 | let mut handles = FuturesUnordered::new(); 57 | handles.push(webserver_handle); 58 | 59 | loop { 60 | match handles.next().await { 61 | None => break, 62 | Some(_result) => {} 63 | } 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | pub mod config; 70 | pub mod filesystem; 71 | pub mod servers; 72 | pub mod webserver; 73 | pub mod websocket; 74 | -------------------------------------------------------------------------------- /crates/alerion_core/src/servers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::atomic::{AtomicU32, Ordering}; 3 | use std::sync::Arc; 4 | use std::time::Instant; 5 | 6 | use alerion_datamodel::remote::server::{ContainerConfig, ServerSettings}; 7 | use bollard::container::{Config, CreateContainerOptions}; 8 | use bollard::Docker; 9 | use serde::{Deserialize, Serialize}; 10 | use thiserror::Error; 11 | use tokio::sync::{mpsc, Mutex, RwLock}; 12 | use uuid::Uuid; 13 | 14 | use crate::config::AlerionConfig; 15 | use crate::webserver::websocket::SendWebsocketEvent; 16 | 17 | #[derive(Debug, Error)] 18 | pub enum ServerError { 19 | #[error("docker error: {0}")] 20 | Docker(#[from] bollard::errors::Error), 21 | #[error("panel remote API error: {0}")] 22 | RemoteApi(#[from] remote::ResponseError), 23 | } 24 | 25 | pub struct ServerPool { 26 | servers: RwLock>>, 27 | remote_api: Arc, 28 | docker: Arc, 29 | } 30 | 31 | impl ServerPool { 32 | #[tracing::instrument(skip(config))] 33 | pub async fn new(config: &AlerionConfig) -> Result { 34 | tracing::info!("Initializing managed servers..."); 35 | 36 | let remote_api = remote::RemoteClient::new(config)?; 37 | 38 | tracing::info!("Initiating connection to Docker Engine"); 39 | let docker = Docker::connect_with_defaults()?; 40 | 41 | Ok(Self { 42 | servers: RwLock::new(HashMap::new()), 43 | remote_api: Arc::new(remote_api), 44 | docker: Arc::new(docker), 45 | }) 46 | } 47 | 48 | #[tracing::instrument(skip(self))] 49 | pub async fn fetch_existing_servers(&self) -> Result<(), ServerError> { 50 | tracing::info!("Fetching existing servers on this node"); 51 | 52 | let servers = self.remote_api.get_servers().await?; 53 | 54 | for s in servers { 55 | tracing::info!("Adding server {}...", s.uuid); 56 | 57 | let uuid = s.uuid; 58 | let info = ServerInfo::from_remote_info(s.settings); 59 | let server = Server::new( 60 | uuid, 61 | info, 62 | Arc::clone(&self.remote_api), 63 | Arc::clone(&self.docker), 64 | ) 65 | .await?; 66 | 67 | self.servers.write().await.insert(uuid, server); 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | #[tracing::instrument(skip(self))] 74 | pub async fn register_server(&self, uuid: Uuid, start: bool) -> Result, ServerError> { 75 | tracing::info!("Adding server {uuid}..."); 76 | 77 | let remote_api = Arc::clone(&self.remote_api); 78 | let docker = Arc::clone(&self.docker); 79 | 80 | tracing::debug!("Fetching server configuration from remote"); 81 | let config = remote_api.get_server_configuration(uuid).await?; 82 | let server_info = ServerInfo::from_remote_info(config.settings); 83 | 84 | let server = Server::new(uuid, server_info, remote_api, docker).await?; 85 | self.servers.write().await.insert(uuid, Arc::clone(&server)); 86 | 87 | Ok(server) 88 | } 89 | 90 | pub async fn get_server(&self, uuid: Uuid) -> Option> { 91 | self.servers.read().await.get(&uuid).cloned() 92 | } 93 | } 94 | 95 | //TODO: Remove allow(dead_code) when implemented 96 | #[allow(dead_code)] 97 | pub struct ServerInfo { 98 | container: ContainerConfig, 99 | } 100 | 101 | impl ServerInfo { 102 | pub fn from_remote_info(server_settings: ServerSettings) -> Self { 103 | Self { 104 | container: server_settings.container, 105 | } 106 | } 107 | } 108 | 109 | #[derive(Serialize, Deserialize, Default, Copy, Clone, PartialEq, Eq, Hash)] 110 | struct IntoStringZst; 111 | 112 | impl From for String { 113 | fn from(_value: IntoStringZst) -> Self { 114 | String::new() 115 | } 116 | } 117 | 118 | //TODO: Remove allow(dead_code) when implemented 119 | #[allow(dead_code)] 120 | pub struct Server { 121 | start_time: Instant, 122 | uuid: Uuid, 123 | container_name: String, 124 | websocket_id_counter: AtomicU32, 125 | websocket_connections: Mutex>>, 126 | server_info: ServerInfo, 127 | remote_api: Arc, 128 | docker: Arc, 129 | } 130 | 131 | impl Server { 132 | #[tracing::instrument(skip(server_info, remote_api, docker))] 133 | pub async fn new( 134 | uuid: Uuid, 135 | server_info: ServerInfo, 136 | remote_api: Arc, 137 | docker: Arc, 138 | ) -> Result, ServerError> { 139 | tracing::debug!("Creating new server {uuid}"); 140 | 141 | let server = Arc::new(Self { 142 | start_time: Instant::now(), 143 | uuid, 144 | container_name: format!("{}_container", uuid.as_hyphenated()), 145 | websocket_id_counter: AtomicU32::new(0), 146 | websocket_connections: Mutex::new(HashMap::new()), 147 | server_info, 148 | remote_api, 149 | docker, 150 | }); 151 | 152 | Ok(server) 153 | } 154 | 155 | pub async fn add_websocket_connection(&self) -> mpsc::Receiver { 156 | let id = self.websocket_id_counter.fetch_add(1, Ordering::SeqCst); 157 | 158 | let (send, recv) = mpsc::channel(64); 159 | 160 | self.websocket_connections.lock().await.insert(id, send); 161 | 162 | recv 163 | } 164 | 165 | async fn create_docker_container(&self) -> Result { 166 | tracing::info!( 167 | "Creating docker container for server {}", 168 | self.uuid.as_hyphenated() 169 | ); 170 | 171 | let opts = CreateContainerOptions { 172 | name: self.container_name.clone(), 173 | platform: None, 174 | }; 175 | 176 | let config: Config = Config { 177 | attach_stdin: Some(true), 178 | attach_stdout: Some(true), 179 | attach_stderr: Some(true), 180 | ..Config::default() 181 | }; 182 | 183 | let response = self.docker.create_container(Some(opts), config).await?; 184 | 185 | tracing::debug!("Created docker container: {response:#?}"); 186 | 187 | Ok(response.id) 188 | } 189 | 190 | pub fn server_time(&self) -> u64 { 191 | self.start_time.elapsed().as_millis() as u64 192 | } 193 | } 194 | 195 | pub mod remote; 196 | -------------------------------------------------------------------------------- /crates/alerion_core/src/servers/remote.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use alerion_datamodel::remote::server::{ 4 | GetServerByUuidResponse, GetServerInstallByUuidResponse, GetServersResponse, PostServerInstallByUuidRequest, ServerData 5 | }; 6 | use reqwest::header::{self, HeaderMap}; 7 | use reqwest::StatusCode; 8 | use thiserror::Error; 9 | use uuid::Uuid; 10 | 11 | use crate::config::AlerionConfig; 12 | 13 | #[derive(Debug, Error)] 14 | pub enum ResponseError { 15 | #[error("failed to process request or response: {0}")] 16 | Protocol(#[from] reqwest::Error), 17 | #[error("server with uuid {0} was not found")] 18 | NotFound(Uuid), 19 | #[error("failed to parse response: {0}")] 20 | InvalidJson(serde_json::Error), 21 | #[error("failed to authenticate")] 22 | Unauthorized, 23 | #[error("unknown error (status: {0})")] 24 | Unknown(StatusCode), 25 | } 26 | 27 | /// A wrapper around the simple pyrodactyl remote API 28 | pub struct RemoteClient { 29 | remote: String, 30 | http: reqwest::Client, 31 | } 32 | 33 | impl RemoteClient { 34 | pub fn new(config: &AlerionConfig) -> Result { 35 | let token_id = &config.auth.token_id; 36 | let token = &config.auth.token; 37 | 38 | let mut headers = HeaderMap::new(); 39 | 40 | let authorization = format!("Bearer {token_id}.{token}") 41 | .parse() 42 | .expect("valid header value"); 43 | 44 | headers.insert(header::AUTHORIZATION, authorization); 45 | 46 | let accept = "application/vnd.pterodactyl.v1+json" 47 | .parse() 48 | .expect("valid header value"); 49 | 50 | headers.insert(header::ACCEPT, accept); 51 | 52 | Ok(Self { 53 | remote: config.remote.clone(), 54 | http: reqwest::Client::builder() 55 | .user_agent("alerion/0.1.0") 56 | .default_headers(headers) 57 | .build()?, 58 | }) 59 | } 60 | 61 | pub async fn post_installation_status( 62 | &self, 63 | uuid: Uuid, 64 | successful: bool, 65 | reinstall: bool, 66 | ) -> Result<(), ResponseError> { 67 | let req = PostServerInstallByUuidRequest { 68 | successful, 69 | reinstall, 70 | }; 71 | 72 | let url = format!( 73 | "{}/api/remote/servers/{}/install", 74 | self.remote, 75 | uuid.as_hyphenated() 76 | ); 77 | 78 | tracing::debug!("remote: POST {url}"); 79 | 80 | let resp = self 81 | .http 82 | .post(url) 83 | .body(serde_json::to_string(&req).expect("JSON serialization should not fail")) 84 | .send() 85 | .await?; 86 | 87 | if resp.status() == StatusCode::NOT_FOUND { 88 | Err(ResponseError::NotFound(uuid)) 89 | } else { 90 | Ok(()) 91 | } 92 | } 93 | 94 | pub async fn get_install_instructions( 95 | &self, 96 | uuid: Uuid, 97 | ) -> Result { 98 | let url = format!( 99 | "{}/api/remote/servers/{}/install", 100 | self.remote, 101 | uuid.as_hyphenated() 102 | ); 103 | 104 | tracing::debug!("remote: GET {url}"); 105 | 106 | let resp = self.http.get(url).send().await?; 107 | 108 | match resp.status() { 109 | StatusCode::NOT_FOUND => Err(ResponseError::NotFound(uuid)), 110 | StatusCode::UNAUTHORIZED => Err(ResponseError::Unauthorized), 111 | StatusCode::OK => { 112 | let bytes = resp.bytes().await?; 113 | 114 | serde_json::from_slice::(&bytes) 115 | .map_err(ResponseError::InvalidJson) 116 | } 117 | 118 | _ => Err(ResponseError::Unknown(resp.status())), 119 | } 120 | } 121 | 122 | pub async fn get_server_configuration( 123 | &self, 124 | uuid: Uuid, 125 | ) -> Result { 126 | let url = format!( 127 | "{}/api/remote/servers/{}", 128 | self.remote, 129 | uuid.as_hyphenated() 130 | ); 131 | 132 | tracing::debug!("remote: GET {url}"); 133 | 134 | let resp = self.http.get(url).send().await?; 135 | 136 | match resp.status() { 137 | StatusCode::NOT_FOUND => Err(ResponseError::NotFound(uuid)), 138 | StatusCode::UNAUTHORIZED => Err(ResponseError::Unauthorized), 139 | StatusCode::OK => { 140 | let bytes = resp.bytes().await?; 141 | 142 | serde_json::from_slice::(&bytes) 143 | .map_err(ResponseError::InvalidJson) 144 | } 145 | 146 | _ => Err(ResponseError::Unknown(resp.status())), 147 | } 148 | } 149 | 150 | pub async fn get_servers(&self) -> Result, ResponseError> { 151 | let mut servers: Option> = None; 152 | let mut page = 1; 153 | 154 | loop { 155 | let url = format!( 156 | "{}/api/remote/servers?page={}&per_page=2", 157 | self.remote, page 158 | ); 159 | 160 | tracing::debug!("remote: GET {url}"); 161 | 162 | let resp = self.http.get(url).send().await?; 163 | 164 | let parsed = match resp.status() { 165 | StatusCode::UNAUTHORIZED => Err(ResponseError::Unauthorized), 166 | StatusCode::OK => { 167 | let bytes = resp.bytes().await?; 168 | 169 | serde_json::from_slice::(&bytes) 170 | .map_err(ResponseError::InvalidJson) 171 | } 172 | 173 | _ => { 174 | let status = resp.status(); 175 | //log::debug!("{}", resp.text().await.unwrap()); 176 | Err(ResponseError::Unknown(status)) 177 | } 178 | }; 179 | 180 | let mut parsed = parsed?; 181 | 182 | let server_data = mem::take(&mut parsed.data); 183 | 184 | servers = Some(match servers { 185 | None => server_data, 186 | Some(mut s) => { 187 | s.extend(server_data); 188 | s 189 | } 190 | }); 191 | 192 | if parsed.meta.current_page == parsed.meta.last_page { 193 | return Ok(unsafe { servers.unwrap_unchecked() }); 194 | } 195 | 196 | page += 1; 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /crates/alerion_core/src/sftp/server.rs: -------------------------------------------------------------------------------- 1 | // This file intentionally left blank. 2 | -------------------------------------------------------------------------------- /crates/alerion_core/src/webserver.rs: -------------------------------------------------------------------------------- 1 | use std::env::consts::{ARCH, OS}; 2 | use std::io; 3 | use std::sync::Arc; 4 | 5 | use alerion_datamodel::webserver::CreateServerRequest; 6 | use poem::listener::TcpListener; 7 | use poem::middleware::{Tracing, Cors}; 8 | use poem::web::websocket::WebSocket; 9 | use poem::web::{Data, Json, Path}; 10 | use poem::{endpoint, get, handler, post, Body, EndpointExt, IntoResponse, Route, Server}; 11 | use reqwest::StatusCode; 12 | use serde::{Deserialize, Serialize}; 13 | use sysinfo::System; 14 | use uuid::Uuid; 15 | 16 | use self::middleware::bearer_auth::BearerAuthMiddleware; 17 | use crate::config::AlerionConfig; 18 | use crate::servers::ServerPool; 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | struct SystemResponseV1 { 22 | architecture: String, 23 | cpu_count: usize, 24 | kernel_version: String, 25 | os: String, 26 | version: String, 27 | } 28 | 29 | #[handler] 30 | async fn get_system_info() -> impl IntoResponse { 31 | let Some(kernel_version) = System::kernel_version() else { 32 | return StatusCode::INTERNAL_SERVER_ERROR.into_response(); 33 | }; 34 | 35 | Json(SystemResponseV1 { 36 | architecture: ARCH.to_owned(), 37 | cpu_count: num_cpus::get(), 38 | kernel_version, 39 | os: OS.to_owned(), 40 | version: env!("CARGO_PKG_VERSION").to_owned(), 41 | }) 42 | .into_response() 43 | } 44 | 45 | #[handler] 46 | async fn initialize_websocket( 47 | Path(uuid): Path, 48 | Data(server_pool): Data<&Arc>, 49 | ws: WebSocket, 50 | ) -> impl IntoResponse { 51 | if let Some(server) = server_pool.get_server(uuid).await { 52 | let recv = server.add_websocket_connection().await; 53 | 54 | ws.on_upgrade(move |mut socket| websocket::websocket_handler(socket, recv, uuid)) 55 | .into_response() 56 | } else { 57 | StatusCode::NOT_FOUND.into_response() 58 | } 59 | } 60 | 61 | #[handler] 62 | async fn create_server(Json(options): Json, Data(server_pool): Data<&Arc>) -> impl IntoResponse { 63 | let server = match server_pool.get_server(options.uuid).await { 64 | Some(s) => s, 65 | None => { 66 | let server_fut = server_pool.register_server(options.uuid, options.start_on_completion); 67 | let Ok(server) = server_fut.await else { 68 | return StatusCode::INTERNAL_SERVER_ERROR.into_response(); 69 | }; 70 | 71 | server 72 | } 73 | }; 74 | 75 | ().into_response() 76 | } 77 | 78 | pub async fn serve(config: &AlerionConfig, server_pool: Arc) -> io::Result<()> { 79 | // TODO: restrict origins 80 | let cors = Cors::new().allow_credentials(true); 81 | 82 | let system_endpoint = get(get_system_info) 83 | .options(endpoint::make_sync(|_| StatusCode::NO_CONTENT)) 84 | .with(BearerAuthMiddleware::new(config.auth.token.clone())); 85 | 86 | let ws_endpoint = get(initialize_websocket); 87 | 88 | let install_endpoint = post(create_server) 89 | .with(BearerAuthMiddleware::new(config.auth.token.clone())); 90 | 91 | let api = Route::new() 92 | .nest( 93 | "api", 94 | Route::new() 95 | .at("system", system_endpoint) 96 | .at("servers", install_endpoint) 97 | .at("servers/:uuid/ws", ws_endpoint), 98 | ) 99 | .with(cors) 100 | .with(Tracing::default()) 101 | .data(server_pool); 102 | 103 | Server::new(TcpListener::bind((config.api.host, config.api.port))) 104 | .run(api) 105 | .await 106 | } 107 | 108 | pub mod middleware; 109 | pub mod websocket; 110 | -------------------------------------------------------------------------------- /crates/alerion_core/src/webserver/middleware.rs: -------------------------------------------------------------------------------- 1 | pub mod bearer_auth; 2 | -------------------------------------------------------------------------------- /crates/alerion_core/src/webserver/middleware/bearer_auth.rs: -------------------------------------------------------------------------------- 1 | use poem::{Endpoint, Middleware, Request}; 2 | use reqwest::{Method, StatusCode}; 3 | 4 | pub struct BearerAuthMiddleware { 5 | token: String, 6 | } 7 | 8 | impl BearerAuthMiddleware { 9 | pub fn new(token: String) -> Self { 10 | Self { token } 11 | } 12 | } 13 | 14 | impl Middleware for BearerAuthMiddleware { 15 | type Output = BearerAuthMiddlewareImpl; 16 | 17 | fn transform(&self, ep: E) -> Self::Output { 18 | BearerAuthMiddlewareImpl { 19 | ep, 20 | token: self.token.clone(), 21 | } 22 | } 23 | } 24 | 25 | /// The new endpoint type generated by the TokenMiddleware. 26 | pub struct BearerAuthMiddlewareImpl { 27 | ep: E, 28 | token: String, 29 | } 30 | 31 | /// Token data 32 | impl Endpoint for BearerAuthMiddlewareImpl { 33 | type Output = E::Output; 34 | 35 | async fn call(&self, req: Request) -> poem::Result { 36 | if req.method() == Method::OPTIONS { 37 | return self.ep.call(req).await; 38 | } 39 | 40 | if let Some(value) = req 41 | .headers() 42 | .get("Authorization") 43 | .and_then(|value| value.to_str().ok()) 44 | { 45 | let token = value.to_string(); 46 | 47 | if token == format!("Bearer {}", self.token) { 48 | self.ep.call(req).await 49 | } else { 50 | Err(poem::Error::from_string( 51 | "Token does not match", 52 | StatusCode::UNAUTHORIZED, 53 | )) 54 | } 55 | } else { 56 | Err(poem::Error::from_string( 57 | "No token provided", 58 | StatusCode::UNAUTHORIZED, 59 | )) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/alerion_core/src/webserver/websocket.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::stream::{SplitSink, StreamExt}; 4 | use poem::web::websocket::{Message, WebSocketStream}; 5 | use serde::{Deserialize, Serialize}; 6 | use tokio::sync::{mpsc, Mutex}; 7 | use uuid::Uuid; 8 | 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct RecvWebsocketEvent { 11 | event: RecvEventType, 12 | args: Option>, 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | pub struct SendWebsocketEvent { 17 | event: SendEventType, 18 | args: Option>, 19 | } 20 | 21 | #[derive(Debug, Serialize, Deserialize)] 22 | struct AuthDetails { 23 | data: AuthDetailsInner, 24 | } 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | struct AuthDetailsInner { 28 | token: String, 29 | socket: String, 30 | } 31 | 32 | #[derive(Debug, Serialize, Deserialize)] 33 | pub enum RecvEventType { 34 | #[serde(rename = "auth")] 35 | Auth, 36 | #[serde(rename = "set state")] 37 | SetState, 38 | #[serde(rename = "send command")] 39 | SendCommand, 40 | #[serde(rename = "send logs")] 41 | SendLogs, 42 | #[serde(rename = "send stats")] 43 | SendStats, 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize)] 47 | pub enum SendEventType { 48 | #[serde(rename = "auth success")] 49 | AuthSuccess, 50 | #[serde(rename = "backup complete")] 51 | BackupComplete, 52 | #[serde(rename = "backup restore completed")] 53 | BackupRestoreCompleted, 54 | #[serde(rename = "console output")] 55 | ConsoleOutput, 56 | #[serde(rename = "daemon error")] 57 | DaemonError, 58 | #[serde(rename = "daemon message")] 59 | DaemonMessage, 60 | #[serde(rename = "install completed")] 61 | InstallCompleted, 62 | #[serde(rename = "install output")] 63 | InstallOutput, 64 | #[serde(rename = "install started")] 65 | InstallStarted, 66 | #[serde(rename = "jwt error")] 67 | JwtError, 68 | #[serde(rename = "stats")] 69 | Stats, 70 | #[serde(rename = "status")] 71 | Status, 72 | #[serde(rename = "token expired")] 73 | TokenExpired, 74 | #[serde(rename = "token expiring")] 75 | TokenExpiring, 76 | #[serde(rename = "transfer logs")] 77 | TransferLogs, 78 | #[serde(rename = "transfer status")] 79 | TransferStatus, 80 | } 81 | 82 | pub async fn websocket_handler( 83 | stream: WebSocketStream, 84 | _recv: mpsc::Receiver, 85 | uuid: Uuid, 86 | ) { 87 | let (sink, mut stream) = stream.split(); 88 | let sink = Arc::new(Mutex::new(sink)); 89 | 90 | let direct_responder = Arc::clone(&sink); 91 | let inbound_handle = tokio::spawn(async move { 92 | while let Some(result) = stream.next().await { 93 | if let Ok(msg) = result { 94 | match msg { 95 | Message::Text(text) => { 96 | let data = serde_json::from_str::(text.as_str()); 97 | 98 | match data { 99 | Ok(json) => handle_incoming_message(json, &direct_responder, uuid), 100 | Err(_e) => todo!(), 101 | } 102 | } 103 | 104 | Message::Close(_maybe_reason) => { 105 | return; 106 | } 107 | 108 | _ => { 109 | // TODO 110 | return; 111 | } 112 | } 113 | } 114 | } 115 | }); 116 | 117 | let outbound_handle = tokio::spawn(async move { 118 | loop { 119 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 120 | //sink.send() 121 | } 122 | }); 123 | 124 | tokio::select! { 125 | _ = inbound_handle => {}, 126 | _ = outbound_handle => {} 127 | } 128 | } 129 | 130 | fn handle_incoming_message( 131 | msg: RecvWebsocketEvent, 132 | _sink: &Mutex>, 133 | _uuid: Uuid, 134 | ) { 135 | match msg.event { 136 | RecvEventType::Auth => { 137 | // handle auth.. 138 | } 139 | 140 | _ => todo!(), 141 | } 142 | } 143 | 144 | pub mod auth; 145 | -------------------------------------------------------------------------------- /crates/alerion_core/src/webserver/websocket/auth.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use bitflags::bitflags; 4 | use jsonwebtoken::{Algorithm, DecodingKey, Validation}; 5 | use serde::{Deserialize, Serialize}; 6 | use uuid::Uuid; 7 | 8 | use crate::config::AlerionConfig; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | struct Claims { 12 | iss: String, 13 | aud: Vec, 14 | jti: String, 15 | iat: usize, 16 | nbf: usize, 17 | exp: usize, 18 | server_uuid: Uuid, 19 | permissions: Vec, 20 | user_uuid: Uuid, 21 | user_id: usize, 22 | unique_id: String, 23 | } 24 | 25 | bitflags! { 26 | #[derive(Debug, Clone, Copy)] 27 | pub struct Permissions: u32 { 28 | const CONNECT = 1; 29 | const START = 1 << 1; 30 | const STOP = 1 << 2; 31 | const RESTART = 1 << 3; 32 | const CONSOLE = 1 << 4; 33 | const BACKUP_READ = 1 << 5; 34 | const ADMIN_ERRORS = 1 << 6; 35 | const ADMIN_INSTALL = 1 << 7; 36 | const ADMIN_TRANSFER = 1 << 8; 37 | } 38 | } 39 | 40 | impl Permissions { 41 | pub fn from_strings(strings: &[impl AsRef]) -> Self { 42 | let mut this = Permissions::empty(); 43 | 44 | for s in strings { 45 | match s.as_ref() { 46 | "*" => { 47 | this.insert(Permissions::CONNECT); 48 | this.insert(Permissions::START); 49 | this.insert(Permissions::STOP); 50 | this.insert(Permissions::RESTART); 51 | this.insert(Permissions::CONSOLE); 52 | this.insert(Permissions::BACKUP_READ); 53 | } 54 | "websocket.connect" => { 55 | this.insert(Permissions::CONNECT); 56 | } 57 | "control.start" => { 58 | this.insert(Permissions::START); 59 | } 60 | "control.stop" => { 61 | this.insert(Permissions::STOP); 62 | } 63 | "control.restart" => { 64 | this.insert(Permissions::RESTART); 65 | } 66 | "control.console" => { 67 | this.insert(Permissions::CONSOLE); 68 | } 69 | "backup.read" => { 70 | this.insert(Permissions::BACKUP_READ); 71 | } 72 | "admin.websocket.errors" => { 73 | this.insert(Permissions::ADMIN_ERRORS); 74 | } 75 | "admin.websocket.install" => { 76 | this.insert(Permissions::ADMIN_INSTALL); 77 | } 78 | "admin.websocket.transfer" => { 79 | this.insert(Permissions::ADMIN_TRANSFER); 80 | } 81 | _ => {} 82 | } 83 | } 84 | 85 | this 86 | } 87 | } 88 | 89 | pub struct Auth { 90 | validation: Validation, 91 | key: DecodingKey, 92 | } 93 | 94 | impl Auth { 95 | pub fn from_config(cfg: &AlerionConfig) -> Self { 96 | let mut validation = Validation::new(Algorithm::HS256); 97 | 98 | let spec_claims = ["exp", "nbf", "aud", "iss"].map(ToOwned::to_owned); 99 | 100 | validation.required_spec_claims = HashSet::from(spec_claims); 101 | validation.leeway = 10; 102 | validation.reject_tokens_expiring_in_less_than = 0; 103 | validation.validate_exp = false; 104 | validation.validate_nbf = false; 105 | validation.validate_aud = false; 106 | validation.aud = None; 107 | validation.iss = Some(HashSet::from([cfg.remote.clone()])); 108 | validation.sub = None; 109 | 110 | let key = DecodingKey::from_secret(cfg.auth.token.as_ref()); 111 | 112 | Self { validation, key } 113 | } 114 | 115 | pub fn validate(&self, auth: &str, server_uuid: &Uuid) -> Option { 116 | jsonwebtoken::decode::(auth, &self.key, &self.validation) 117 | .ok() 118 | .filter(|result| &result.claims.server_uuid == server_uuid) 119 | .map(|result| Permissions::from_strings(&result.claims.permissions)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/alerion_core/src/websocket.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/alerion_datamodel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "alerion_datamodel" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bytestring = "1.3.1" 8 | serde = { version = "1.0.198", features = ["derive"] } 9 | serde_json = "1.0.116" 10 | smallvec = { version = "1.13.2", features = ["serde"] } 11 | uuid = { version = "1.8.0", features = ["serde"] } 12 | -------------------------------------------------------------------------------- /crates/alerion_datamodel/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::unwrap_used)] 2 | #![allow(dead_code)] 3 | 4 | pub mod remote; 5 | pub mod webserver; 6 | pub mod websocket; 7 | -------------------------------------------------------------------------------- /crates/alerion_datamodel/src/remote.rs: -------------------------------------------------------------------------------- 1 | pub mod server; 2 | -------------------------------------------------------------------------------- /crates/alerion_datamodel/src/remote/server.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::de::IgnoredAny; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use smallvec::SmallVec; 7 | use uuid::Uuid; 8 | 9 | #[derive(Debug, Deserialize)] 10 | pub struct SearchReplaceMatcher { 11 | #[serde(rename = "match")] 12 | pub match_item: String, 13 | pub replace_with: String, 14 | } 15 | 16 | #[derive(Debug, Deserialize)] 17 | pub struct FileParser { 18 | pub parser: String, 19 | pub file: String, 20 | pub replace: Vec, 21 | } 22 | 23 | #[derive(Debug, Deserialize)] 24 | pub enum StopSignalType { 25 | #[serde(rename = "command")] 26 | Command, 27 | } 28 | 29 | #[derive(Debug, Deserialize)] 30 | pub struct StopConfig { 31 | #[serde(rename = "type")] 32 | pub kind: StopSignalType, 33 | pub value: String, 34 | } 35 | 36 | #[derive(Debug, Deserialize)] 37 | pub struct StartupConfig { 38 | pub done: Vec, 39 | pub user_interaction: Vec, 40 | pub strip_ansi: bool, 41 | } 42 | 43 | #[derive(Debug, Deserialize)] 44 | pub struct ProcessConfig { 45 | pub startup: StartupConfig, 46 | pub stop: StopConfig, 47 | pub configs: SmallVec<[FileParser; 1]>, 48 | } 49 | 50 | #[derive(Debug, Deserialize)] 51 | pub struct Egg { 52 | pub id: Uuid, 53 | // todo: figure out what is inside this array 54 | pub file_denylist: Vec, 55 | } 56 | 57 | #[derive(Debug, Deserialize)] 58 | pub struct Mount { 59 | pub source: String, 60 | pub target: String, 61 | pub read_only: bool, 62 | } 63 | 64 | #[derive(Debug, Deserialize)] 65 | pub struct Allocation { 66 | pub ip: String, 67 | pub port: u16, 68 | } 69 | 70 | #[derive(Debug, Deserialize)] 71 | pub struct AllocationConfig { 72 | pub force_outgoing_ip: bool, 73 | pub default: Allocation, 74 | pub mappings: HashMap>, 75 | } 76 | 77 | #[derive(Debug, Deserialize)] 78 | pub struct ContainerConfig { 79 | pub image: String, 80 | pub oom_disabled: bool, 81 | pub requires_rebuild: bool, 82 | } 83 | 84 | #[derive(Debug, Deserialize)] 85 | pub struct BuildConfig { 86 | pub memory_limit: isize, 87 | pub swap: isize, 88 | pub io_weight: u32, 89 | pub cpu_limit: u32, 90 | pub threads: Option, 91 | pub disk_space: usize, 92 | pub oom_disabled: bool, 93 | } 94 | 95 | #[derive(Debug, Deserialize)] 96 | pub struct ServerMetadata { 97 | pub name: String, 98 | pub description: String, 99 | } 100 | 101 | #[derive(Debug, Deserialize)] 102 | pub struct ServerSettings { 103 | pub uuid: Uuid, 104 | pub meta: ServerMetadata, 105 | pub suspended: bool, 106 | pub environment: HashMap, 107 | pub invocation: String, 108 | pub skip_egg_scripts: bool, 109 | pub build: BuildConfig, 110 | pub container: ContainerConfig, 111 | pub allocations: AllocationConfig, 112 | pub mounts: Vec, 113 | pub egg: Egg, 114 | } 115 | 116 | #[derive(Debug, Deserialize)] 117 | pub struct ServerData { 118 | pub uuid: Uuid, 119 | pub settings: ServerSettings, 120 | pub process_configuration: ProcessConfig, 121 | } 122 | 123 | #[derive(Debug, Deserialize)] 124 | pub struct GetServersResponseMetadata { 125 | pub current_page: usize, 126 | pub from: Option, 127 | pub last_page: usize, 128 | pub links: IgnoredAny, 129 | pub path: IgnoredAny, 130 | pub per_page: usize, 131 | pub to: Option, 132 | pub total: usize, 133 | } 134 | 135 | /// Response to `GET /api/remote/servers`. 136 | #[derive(Debug, Deserialize)] 137 | pub struct GetServersResponse { 138 | pub data: Vec, 139 | pub links: IgnoredAny, 140 | pub meta: GetServersResponseMetadata, 141 | } 142 | 143 | /// Response to `GET /api/remote/servers/{uuid}`. 144 | #[derive(Debug, Deserialize)] 145 | pub struct GetServerByUuidResponse { 146 | pub settings: ServerSettings, 147 | pub process_configuration: ProcessConfig, 148 | } 149 | 150 | /// Response to `GET /api/remote/servers/{uuid}/install` 151 | #[derive(Debug, Deserialize)] 152 | pub struct GetServerInstallByUuidResponse { 153 | pub container_image: String, 154 | pub entrypoint: String, 155 | pub script: String, 156 | } 157 | 158 | /// Request to `POST /api/remote/servers/{uuid}/install` 159 | #[derive(Debug, Serialize)] 160 | pub struct PostServerInstallByUuidRequest { 161 | pub successful: bool, 162 | pub reinstall: bool, 163 | } 164 | -------------------------------------------------------------------------------- /crates/alerion_datamodel/src/webserver.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | pub struct SystemOptions { 6 | pub architecture: &'static str, 7 | pub cpu_count: u32, 8 | pub kernel_version: &'static str, 9 | pub os: &'static str, 10 | pub version: &'static str, 11 | } 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct CreateServerRequest { 15 | pub uuid: Uuid, 16 | pub start_on_completion: bool, 17 | } 18 | 19 | pub mod update; 20 | -------------------------------------------------------------------------------- /crates/alerion_datamodel/src/webserver/update.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | #[derive(Debug, Deserialize)] 7 | pub struct SslConfig { 8 | pub enabled: bool, 9 | pub cert: String, 10 | pub key: String, 11 | } 12 | 13 | #[derive(Debug, Deserialize)] 14 | pub struct ApiConfig { 15 | pub host: IpAddr, 16 | pub port: u16, 17 | pub ssl: SslConfig, 18 | pub upload_limit: u32, 19 | } 20 | 21 | #[derive(Debug, Deserialize)] 22 | pub struct SftpConfig { 23 | pub bind_port: u16, 24 | } 25 | 26 | #[derive(Debug, Deserialize)] 27 | pub struct SystemConfig { 28 | pub data: String, 29 | pub sftp: SftpConfig, 30 | } 31 | 32 | #[derive(Debug, Deserialize)] 33 | pub struct ConfigUpdateRequest { 34 | pub debug: bool, 35 | pub uuid: Uuid, 36 | // todo: string w/ length? 37 | pub token_id: String, 38 | // todo: string w/ length? 39 | pub token: String, 40 | pub api: ApiConfig, 41 | pub system: SystemConfig, 42 | pub allowed_mounts: Vec, 43 | // todo: uri? 44 | pub remote: String, 45 | } 46 | 47 | #[derive(Debug, Serialize)] 48 | pub struct ConfigUpdateResponse { 49 | pub applied: bool, 50 | } 51 | -------------------------------------------------------------------------------- /crates/alerion_datamodel/src/websocket.rs: -------------------------------------------------------------------------------- 1 | use bytestring::ByteString; 2 | use serde::{Deserialize, Serialize}; 3 | use smallvec::{smallvec, SmallVec}; 4 | 5 | #[derive(Copy, Clone, Debug, Serialize, PartialEq, Eq)] 6 | pub enum ServerStatus { 7 | #[serde(rename = "running")] 8 | Running, 9 | #[serde(rename = "starting")] 10 | Starting, 11 | #[serde(rename = "stopping")] 12 | Stopping, 13 | #[serde(rename = "offline")] 14 | Offline, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize)] 18 | pub struct NetworkStatistics { 19 | pub rx_bytes: usize, 20 | pub tx_bytes: usize, 21 | } 22 | 23 | #[derive(Debug, Clone, Serialize)] 24 | pub struct PerformanceStatisics { 25 | pub memory_bytes: usize, 26 | pub memory_limit_bytes: usize, 27 | pub cpu_absolute: f64, 28 | pub network: NetworkStatistics, 29 | pub uptime: u64, 30 | pub state: ServerStatus, 31 | pub disk_bytes: usize, 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 35 | pub enum EventType { 36 | #[serde(rename = "auth")] 37 | Authentication, 38 | #[serde(rename = "auth success")] 39 | AuthenticationSuccess, 40 | #[serde(rename = "stats")] 41 | Stats, 42 | #[serde(rename = "logs")] 43 | Logs, 44 | #[serde(rename = "console output")] 45 | ConsoleOutput, 46 | #[serde(rename = "install output")] 47 | InstallOutput, 48 | #[serde(rename = "install completed")] 49 | InstallCompleted, 50 | #[serde(rename = "status")] 51 | Status, 52 | #[serde(rename = "send logs")] 53 | SendLogs, 54 | #[serde(rename = "send stats")] 55 | SendStats, 56 | #[serde(rename = "send command")] 57 | SendCommand, 58 | #[serde(rename = "set state")] 59 | SetState, 60 | #[serde(rename = "daemon error")] 61 | DaemonError, 62 | #[serde(rename = "jwt error")] 63 | JwtError, 64 | } 65 | 66 | #[derive(Debug, Serialize, Deserialize)] 67 | pub struct RawMessage { 68 | event: EventType, 69 | #[serde(default, skip_serializing_if = "Option::is_none")] 70 | args: Option>, 71 | } 72 | 73 | impl From for ByteString { 74 | fn from(value: RawMessage) -> Self { 75 | serde_json::to_string(&value) 76 | .expect("infallible struct-to-json conversion") 77 | .into() 78 | } 79 | } 80 | 81 | impl RawMessage { 82 | pub fn new_no_args(event: EventType) -> Self { 83 | Self { event, args: None } 84 | } 85 | 86 | pub fn new(event: EventType, args: String) -> Self { 87 | Self { 88 | event, 89 | args: Some(smallvec![serde_json::Value::String(args)]), 90 | } 91 | } 92 | 93 | pub fn into_first_arg(self) -> Option { 94 | let mut args = self.args?; 95 | let json_str = args.get_mut(0)?.take(); 96 | 97 | match json_str { 98 | serde_json::Value::String(s) => Some(s), 99 | _ => None, 100 | } 101 | } 102 | 103 | pub fn event(&self) -> EventType { 104 | self.event 105 | } 106 | 107 | pub fn into_args(self) -> Option> { 108 | self.args 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "inputs": { 23 | "systems": "systems_2" 24 | }, 25 | "locked": { 26 | "lastModified": 1705309234, 27 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1713297878, 42 | "narHash": "sha256-hOkzkhLT59wR8VaMbh1ESjtZLbGi+XNaBN6h49SPqEc=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "66adc1e47f8784803f2deb6cacd5e07264ec2d5c", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "ref": "nixos-unstable", 51 | "repo": "nixpkgs", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs_2": { 56 | "locked": { 57 | "lastModified": 1706487304, 58 | "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "NixOS", 66 | "ref": "nixpkgs-unstable", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "root": { 72 | "inputs": { 73 | "flake-utils": "flake-utils", 74 | "nixpkgs": "nixpkgs", 75 | "rust-overlay": "rust-overlay" 76 | } 77 | }, 78 | "rust-overlay": { 79 | "inputs": { 80 | "flake-utils": "flake-utils_2", 81 | "nixpkgs": "nixpkgs_2" 82 | }, 83 | "locked": { 84 | "lastModified": 1713492869, 85 | "narHash": "sha256-Zv+ZQq3X+EH6oogkXaJ8dGN8t1v26kPZgC5bki04GnM=", 86 | "owner": "oxalica", 87 | "repo": "rust-overlay", 88 | "rev": "1e9264d1214d3db00c795b41f75d55b5e153758e", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "oxalica", 93 | "repo": "rust-overlay", 94 | "type": "github" 95 | } 96 | }, 97 | "systems": { 98 | "locked": { 99 | "lastModified": 1681028828, 100 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 101 | "owner": "nix-systems", 102 | "repo": "default", 103 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "type": "github" 110 | } 111 | }, 112 | "systems_2": { 113 | "locked": { 114 | "lastModified": 1681028828, 115 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 116 | "owner": "nix-systems", 117 | "repo": "default", 118 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 119 | "type": "github" 120 | }, 121 | "original": { 122 | "owner": "nix-systems", 123 | "repo": "default", 124 | "type": "github" 125 | } 126 | } 127 | }, 128 | "root": "root", 129 | "version": 7 130 | } 131 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "DevShell for Alerion"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { 11 | self, 12 | nixpkgs, 13 | rust-overlay, 14 | flake-utils, 15 | ... 16 | }: 17 | flake-utils.lib.eachDefaultSystem ( 18 | system: let 19 | overlays = [(import rust-overlay)]; 20 | pkgs = import nixpkgs { 21 | inherit system overlays; 22 | }; 23 | darwinPkgs = nixpkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin; [ 24 | apple_sdk.frameworks.AppKit 25 | apple_sdk.frameworks.Carbon 26 | apple_sdk.frameworks.Cocoa 27 | apple_sdk.frameworks.CoreFoundation 28 | apple_sdk.frameworks.IOKit 29 | apple_sdk.frameworks.WebKit 30 | apple_sdk.frameworks.Security 31 | apple_sdk.frameworks.DisplayServices 32 | ]); 33 | in 34 | with pkgs; { 35 | devShells = { 36 | default = mkShell { 37 | buildInputs = 38 | [ 39 | bacon 40 | openssl 41 | pkg-config 42 | (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) 43 | (rust-bin.nightly."2024-04-19".rustfmt) 44 | ] 45 | ++ darwinPkgs; 46 | }; 47 | 48 | nightly = mkShell { 49 | buildInputs = 50 | [ 51 | bacon 52 | openssl 53 | pkg-config 54 | (rust-bin.nightly."2024-04-19".default) 55 | ] 56 | ++ darwinPkgs; 57 | }; 58 | }; 59 | } 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.77.2" 3 | profile = "minimal" 4 | components = [ "clippy", "rust-docs", "rust-analyzer" ] 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Some options in this file are unstable. Use `cargo +nightly fmt`. 2 | 3 | group_imports = "StdExternalCrate" 4 | imports_granularity = "Module" 5 | imports_layout = "Horizontal" 6 | reorder_impl_items = true 7 | --------------------------------------------------------------------------------