diff --git a/Cargo.lock b/Cargo.lock index 0f49dbed8..9b359f9c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,15 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -78,6 +87,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192ec435945d87bc2f70992b4d818154b5feede43c09fb7592146374eac90a6" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -99,6 +123,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d7d63395147b81a9e570bcc6243aaf71c017bd666d4909cfef0085bdda8d73" +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + [[package]] name = "arrayvec" version = "0.5.2" @@ -229,6 +259,17 @@ dependencies = [ "radium", ] +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block" version = "0.1.6" @@ -281,6 +322,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "brotli" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f29919120f08613aadcd4383764e00526fc9f18b6c0895814faeed0dd78613e" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1052e1c3b8d4d80eb84a8b94f0a1498797b5fb96314c001156a1c761940ef4ec" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.6.1" @@ -441,6 +503,24 @@ dependencies = [ "bitflags", ] +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom", + "serde", + "yaml-rust", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "cookie" version = "0.12.0" @@ -678,8 +758,9 @@ dependencies = [ "packet", "pcap-file", "picky", - "reqwest", + "reqwest 0.9.24", "ring", + "rust-argon2", "saphir", "serde", "serde_derive", @@ -690,9 +771,10 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-scope", - "slog-scope-futures", + "slog-scope-futures 0.1.1 (git+https://github.com/Devolutions/slog-scope-futures.git)", "slog-stdlog", "slog-term", + "sogar-core", "spsc-bip-buffer", "sspi", "tempfile", @@ -1125,6 +1207,25 @@ dependencies = [ "tracing-futures", ] +[[package]] +name = "h2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" +dependencies = [ + "bytes 1.0.1", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.4", + "indexmap", + "slab", + "tokio 1.5.0", + "tokio-util 0.6.6", + "tracing", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -1233,6 +1334,17 @@ dependencies = [ "http 0.2.4", ] +[[package]] +name = "http-body" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +dependencies = [ + "bytes 1.0.1", + "http 0.2.4", + "pin-project-lite 0.2.6", +] + [[package]] name = "httparse" version = "1.3.6" @@ -1307,13 +1419,37 @@ dependencies = [ "httpdate", "itoa", "pin-project", - "socket2", + "socket2 0.3.19", "tokio 0.2.25", "tower-service", "tracing", "want 0.3.0", ] +[[package]] +name = "hyper" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf09f61b52cfcf4c00de50df88ae423d6c02354e385a86341133b5338630ad1" +dependencies = [ + "bytes 1.0.1", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.3", + "http 0.2.4", + "http-body 0.4.2", + "httparse", + "httpdate", + "itoa", + "pin-project", + "socket2 0.4.0", + "tokio 1.5.0", + "tower-service", + "tracing", + "want 0.3.0", +] + [[package]] name = "hyper-tls" version = "0.3.2" @@ -1327,6 +1463,19 @@ dependencies = [ "tokio-io", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.0.1", + "hyper 0.14.5", + "native-tls", + "tokio 1.5.0", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.1.5" @@ -1390,6 +1539,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + [[package]] name = "ironrdp" version = "0.4.1" @@ -1432,7 +1587,7 @@ dependencies = [ "ironrdp", "lazy_static", "log", - "reqwest", + "reqwest 0.9.24", "serde", "serde_derive", "serde_json", @@ -1531,6 +1686,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.3.4" @@ -2052,7 +2213,7 @@ checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" dependencies = [ "base64 0.13.0", "once_cell", - "regex", + "regex 1.4.5", ] [[package]] @@ -2528,15 +2689,37 @@ dependencies = [ "redox_syscall 0.2.5", ] +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +dependencies = [ + "aho-corasick 0.6.10", + "memchr", + "regex-syntax 0.5.6", + "thread_local 0.3.6", + "utf8-ranges", +] + [[package]] name = "regex" version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.15", "memchr", - "regex-syntax", + "regex-syntax 0.6.23", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", ] [[package]] @@ -2569,7 +2752,7 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", - "hyper-tls", + "hyper-tls 0.3.2", "log", "mime", "mime_guess", @@ -2588,6 +2771,41 @@ dependencies = [ "winreg 0.6.2", ] +[[package]] +name = "reqwest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2296f2fac53979e8ccbc4a1136b25dcefd37be9ed7e4a1f6b05a6029c84ff124" +dependencies = [ + "base64 0.13.0", + "bytes 1.0.1", + "encoding_rs", + "futures-core", + "futures-util", + "http 0.2.4", + "http-body 0.4.2", + "hyper 0.14.5", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding 2.1.0", + "pin-project-lite 0.2.6", + "serde", + "serde_json", + "serde_urlencoded 0.7.0", + "tokio 1.5.0", + "tokio-native-tls", + "url 2.2.1", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.7.0", +] + [[package]] name = "ring" version = "0.16.20" @@ -2625,6 +2843,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.0", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils 0.8.3", +] + [[package]] name = "rustc-demangle" version = "0.1.18" @@ -2699,14 +2929,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e2dd7c860fa4b3c61763e2b3408a809deae1d607a91082a87d0009b7e7f5afa" dependencies = [ "base64 0.13.0", + "brotli", + "chrono", + "flate2", "futures 0.3.14", "futures-util", "http 0.2.4", "http-body 0.3.1", "hyper 0.13.10", "log", + "mime", + "mime_guess", + "nom", "parking_lot 0.11.1", - "regex", + "percent-encoding 2.1.0", + "regex 1.4.5", "rustls 0.18.1", "saphir-cookie", "saphir_macro", @@ -2875,6 +3112,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "sha-1" version = "0.9.4" @@ -2978,7 +3227,7 @@ dependencies = [ "crossbeam-channel", "slog", "take_mut", - "thread_local", + "thread_local 1.1.3", ] [[package]] @@ -2988,7 +3237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "906a1a0bc43fed692df4b82a5e2fbfc3733db8dad8bb514ab27a4f23ad04f5c0" dependencies = [ "log", - "regex", + "regex 1.4.5", "slog", "slog-async", "slog-scope", @@ -3007,6 +3256,16 @@ dependencies = [ "slog", ] +[[package]] +name = "slog-scope-futures" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ad55b694ada4dc7097672e5a32fc16c35b183f9fa2531d17c7f07d25918eca" +dependencies = [ + "slog", + "slog-scope", +] + [[package]] name = "slog-scope-futures" version = "0.1.1" @@ -3038,7 +3297,7 @@ dependencies = [ "chrono", "slog", "term", - "thread_local", + "thread_local 1.1.3", ] [[package]] @@ -3067,6 +3326,48 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "sogar-core" +version = "0.1.0" +source = "git+https://github.com/Devolutions/sogar.git?branch=sogar_registry#ca7876696f9659d2b14732e331740b89e82f90bc" +dependencies = [ + "clap 2.33.3", + "config", + "dirs-next", + "futures 0.3.14", + "hyper 0.13.10", + "regex 0.2.11", + "reqwest 0.11.3", + "saphir", + "serde", + "serde_json", + "serde_yaml", + "sha2 0.8.2", + "slog", + "slog-async", + "slog-envlogger", + "slog-scope", + "slog-scope-futures 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "slog-stdlog", + "slog-term", + "tempfile", + "thiserror", + "tokio 0.2.25", + "tokio 1.5.0", + "tokio-util 0.6.6", + "url 1.7.2", +] + [[package]] name = "spin" version = "0.5.2" @@ -3323,6 +3624,15 @@ dependencies = [ "syn 1.0.69", ] +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +dependencies = [ + "lazy_static", +] + [[package]] name = "thread_local" version = "1.1.3" @@ -3776,6 +4086,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "ucd-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85f514e095d348c279b1e5cd76795082cf15bd59b93207832abe0b1d8fed236" + [[package]] name = "unicase" version = "2.6.0" @@ -3882,6 +4198,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + [[package]] name = "uuid" version = "0.7.4" @@ -3985,6 +4307,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ "cfg-if 1.0.0", + "serde", + "serde_json", "wasm-bindgen-macro", ] @@ -4003,6 +4327,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.73" @@ -4161,6 +4497,15 @@ dependencies = [ "time", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.2.0" diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index 24fdebc41..a997046e0 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -58,6 +58,10 @@ indexmap = "1.0" dlopen = "0.1.8" dlopen_derive = "0.1.4" +sogar-core = { version = "0.1.0", git = "https://github.com/Devolutions/sogar.git", branch = "sogar_registry" } +tempfile = "3.0" +rust-argon2 = "0.8" + [dependencies.saphir] version = "2.8" default-features = false @@ -70,7 +74,6 @@ winapi = { version = "0.3", features = ["winbase", "winuser", "winsvc", "libload embed-resource = "1.3" [dev-dependencies] -tempfile = "3.0" reqwest = "0.9.20" exitcode = "1.1" diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index fe0a09f51..c889e52ff 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -80,15 +80,39 @@ pub struct CertificateConfig { pub private_key_data: Option, } -#[derive(Debug, Default, Clone)] -pub struct RecordingInfo { - pub sogar_path: Option, +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct SogarPushRegistryInfo { + pub sogar_util_path: Option, pub registry_url: Option, pub username: Option, pub password: Option, pub image_name: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SogarPermission { + Push, + Pull, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct SogarUser { + pub password: Option, + pub username: Option, + pub permission: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct SogarRegistryConfig { + pub serve_as_registry: Option, + pub local_registry_name: Option, + pub local_registry_image: Option, + pub keep_files: Option, + pub keep_time: Option, + pub push_files: Option, + pub sogar_push_registry_info: SogarPushRegistryInfo, +} + #[derive(Debug, Clone)] pub struct Config { pub service_mode: bool, @@ -110,8 +134,9 @@ pub struct Config { pub provisioner_public_key: Option, pub delegation_private_key: Option, pub plugins: Option>, - pub recording_path: Option, - pub recording_info: RecordingInfo, + pub recording_path: Option, + pub sogar_registry_config: SogarRegistryConfig, + pub sogar_user: Vec, } impl Default for Config { @@ -144,13 +169,22 @@ impl Default for Config { delegation_private_key: None, plugins: None, recording_path: None, - recording_info: RecordingInfo { - sogar_path: None, - registry_url: None, - username: None, - password: None, - image_name: None, + sogar_registry_config: SogarRegistryConfig { + serve_as_registry: None, + local_registry_name: None, + local_registry_image: None, + keep_files: None, + keep_time: None, + push_files: None, + sogar_push_registry_info: SogarPushRegistryInfo { + sogar_util_path: None, + registry_url: None, + username: None, + password: None, + image_name: None, + }, }, + sogar_user: Vec::new(), } } } @@ -226,7 +260,7 @@ pub struct ConfigFile { #[serde(rename = "RecordingPath")] pub recording_path: Option, #[serde(rename = "SogarPath")] - pub sogar_path: Option, + pub sogar_util_path: Option, #[serde(rename = "SogarRegistryUrl")] pub registry_url: Option, #[serde(rename = "SogarUsername")] @@ -235,6 +269,20 @@ pub struct ConfigFile { pub password: Option, #[serde(rename = "SogarImageName")] pub image_name: Option, + #[serde(rename = "SogarUsersList")] + pub sogar_users_list: Option>, + #[serde(rename = "ServeAsRegistry")] + pub serve_as_registry: Option, + #[serde(rename = "RegistryName")] + pub registry_name: Option, + #[serde(rename = "RegistryImage")] + pub registry_image: Option, + #[serde(rename = "KeepFiles")] + pub keep_files: Option, + #[serde(rename = "KeepTime")] + pub keep_time: Option, + #[serde(rename = "PushFiles")] + pub push_files: Option, // unstable options (subject to change) #[serde(rename = "ApiKey")] @@ -566,7 +614,14 @@ impl Config { ARG_SOGAR_USERNAME, ARG_SOGAR_PASSWORD, ARG_SOGAR_IMAGE_NAME, - ]), + ]) + .validator(|sogar_path| { + if PathBuf::from(sogar_path).is_file() { + Ok(()) + } else { + Err(String::from("The value does not exist or is not a file.")) + } + }), ) .arg( Arg::with_name(ARG_SOGAR_REGISTRY_URL) @@ -737,23 +792,23 @@ impl Config { } if let Some(sogar_path) = matches.value_of(ARG_SOGAR_UTIL_PATH) { - config.recording_info.sogar_path = Some(sogar_path.to_owned()); + config.sogar_registry_config.sogar_push_registry_info.sogar_util_path = Some(PathBuf::from(sogar_path)); } if let Some(registry_url) = matches.value_of(ARG_SOGAR_REGISTRY_URL) { - config.recording_info.registry_url = Some(registry_url.to_owned()); + config.sogar_registry_config.sogar_push_registry_info.registry_url = Some(registry_url.to_owned()); } if let Some(username) = matches.value_of(ARG_SOGAR_USERNAME) { - config.recording_info.username = Some(username.to_owned()); + config.sogar_registry_config.sogar_push_registry_info.username = Some(username.to_owned()); } if let Some(password) = matches.value_of(ARG_SOGAR_PASSWORD) { - config.recording_info.password = Some(password.to_owned()); + config.sogar_registry_config.sogar_push_registry_info.password = Some(password.to_owned()); } if let Some(image_name) = matches.value_of(ARG_SOGAR_IMAGE_NAME) { - config.recording_info.image_name = Some(image_name.to_owned()); + config.sogar_registry_config.sogar_push_registry_info.image_name = Some(image_name.to_owned()); } // listeners parsing @@ -828,7 +883,7 @@ impl Config { } if let Some(recording_path) = matches.value_of(ARG_RECORDING_PATH) { - config.recording_path = Some(recording_path.to_owned()); + config.recording_path = Some(PathBuf::from(recording_path)); } config @@ -922,12 +977,21 @@ impl Config { .map(|pem| PrivateKey::from_pem(pem).unwrap()); let plugins = config_file.plugins; - let recording_path = config_file.recording_path; - let sogar_path = config_file.sogar_path; + let recording_path = config_file.recording_path.map(PathBuf::from); + + let sogar_util_path = config_file.sogar_util_path.map(PathBuf::from); + let registry_url = config_file.registry_url; let username = config_file.username; let password = config_file.password; let image_name = config_file.image_name; + let serve_as_registry = config_file.serve_as_registry; + let registry_name = config_file.registry_name; + let registry_image = config_file.registry_image; + let keep_files = config_file.keep_files; + let keep_time = config_file.keep_time; + let push_files = config_file.push_files; + let sogar_user = config_file.sogar_users_list.unwrap_or_default(); // unstable options (subject to change) let api_key = config_file.api_key; @@ -952,13 +1016,22 @@ impl Config { delegation_private_key, plugins, recording_path, - recording_info: RecordingInfo { - sogar_path, - registry_url, - username, - password, - image_name, + sogar_registry_config: SogarRegistryConfig { + serve_as_registry, + local_registry_name: registry_name, + local_registry_image: registry_image, + keep_files, + keep_time, + push_files, + sogar_push_registry_info: SogarPushRegistryInfo { + sogar_util_path, + registry_url, + username, + password, + image_name, + }, }, + sogar_user, ..Default::default() }) } diff --git a/devolutions-gateway/src/http/controllers/mod.rs b/devolutions-gateway/src/http/controllers/mod.rs index 0bbe909ee..de6623dad 100644 --- a/devolutions-gateway/src/http/controllers/mod.rs +++ b/devolutions-gateway/src/http/controllers/mod.rs @@ -1,3 +1,4 @@ pub mod health; pub mod jet; pub mod sessions; +pub mod sogar_token; diff --git a/devolutions-gateway/src/http/controllers/sogar_token.rs b/devolutions-gateway/src/http/controllers/sogar_token.rs new file mode 100644 index 000000000..a60d04877 --- /dev/null +++ b/devolutions-gateway/src/http/controllers/sogar_token.rs @@ -0,0 +1,92 @@ +use crate::config::{Config, SogarUser}; +use picky::{ + jose::{jws::JwsAlg, jwt::JwtSig}, + key::PrivateKey, +}; +use saphir::{ + controller::Controller, + http::{Method, StatusCode}, + macros::controller, + prelude::Request, +}; +use serde::{Deserialize, Serialize}; +use slog_scope::error; +use sogar_core::AccessToken; +use std::sync::Arc; + +pub struct TokenController { + config: Arc, +} + +impl TokenController { + pub fn new(config: Arc) -> Self { + Self { config } + } +} + +#[controller(name = "registry")] +impl TokenController { + #[post("/oauth2/token")] + async fn get_token(&self, mut req: Request) -> (StatusCode, Option) { + match req.form::().await { + Ok(body) => { + let password_out = body.password; + let username_out = body.username; + + for user in &self.config.sogar_user { + if let (Some(username), Some(hashed_password)) = (&user.username, &user.password) { + if username == &username_out { + let matched = argon2::verify_encoded(hashed_password.as_str(), password_out.as_bytes()); + if matched.is_err() || !matched.unwrap() { + return (StatusCode::UNAUTHORIZED, None); + } + + return create_token(&self.config.delegation_private_key, user); + } + } + } + + (StatusCode::UNAUTHORIZED, None) + } + Err(e) => { + error!("Failed to read request body! Error is {}", e); + (StatusCode::BAD_REQUEST, None) + } + } + } +} + +fn create_token(private_key: &Option, user: &SogarUser) -> (StatusCode, Option) { + #[derive(Serialize, Deserialize, Debug)] + struct ResponseAccessToken { + access_token: String, + } + + match private_key { + Some(private_key) => { + let signed_result = JwtSig::new(JwsAlg::RS256, user).encode(private_key); + + match signed_result { + Ok(access_token) => { + let response = ResponseAccessToken { access_token }; + + match serde_json::to_string(&response) { + Ok(token) => (StatusCode::OK, Some(token)), + Err(e) => { + error!("Failed serialize token! Error is {}", e); + (StatusCode::BAD_REQUEST, None) + } + } + } + Err(e) => { + error!("Failed to create token! Error is {}", e); + (StatusCode::BAD_REQUEST, None) + } + } + } + None => { + error!("Private key is missing. Not able to create the jwt token."); + (StatusCode::BAD_REQUEST, None) + } + } +} diff --git a/devolutions-gateway/src/http/http_server.rs b/devolutions-gateway/src/http/http_server.rs index 17bf25900..dc576159f 100644 --- a/devolutions-gateway/src/http/http_server.rs +++ b/devolutions-gateway/src/http/http_server.rs @@ -2,13 +2,19 @@ use crate::config::Config; use crate::http::controllers::health::HealthController; use crate::http::controllers::jet::JetController; use crate::http::controllers::sessions::SessionsController; +use crate::http::controllers::sogar_token::TokenController; use crate::http::middlewares::auth::AuthMiddleware; use crate::http::middlewares::log::LogMiddleware; +use crate::http::middlewares::sogar_auth::SogarAuthMiddleware; use crate::jet_client::JetAssociationsMap; use saphir::server::Server as SaphirServer; use slog_scope::info; +use sogar_core::registry::{SogarController, BLOB_GET_LOCATION_PATH, BLOB_PATH, MANIFEST_PATH, UPLOAD_BLOB_PATH}; use std::sync::Arc; +pub const REGISTRY_NAME: &str = "devolutions_registry"; +pub const NAMESPACE: &str = "videos"; + pub fn configure_http_server(config: Arc, jet_associations: JetAssociationsMap) -> Result<(), String> { SaphirServer::builder() .configure_middlewares(|middlewares| { @@ -30,6 +36,11 @@ pub fn configure_http_server(config: Arc, jet_associations: JetAssociati auth_include_path, Some(auth_exclude_path), ) + .apply( + SogarAuthMiddleware::new(config.clone()), + vec![BLOB_PATH, BLOB_GET_LOCATION_PATH, UPLOAD_BLOB_PATH, MANIFEST_PATH], + vec!["registry/oauth2/token"], + ) .apply(LogMiddleware, vec!["/"], None) }) .configure_router(|router| { @@ -37,8 +48,29 @@ pub fn configure_http_server(config: Arc, jet_associations: JetAssociati let health = HealthController::new(config.clone()); let jet = JetController::new(config.clone(), jet_associations.clone()); let session = SessionsController::default(); + + let registry_name = config + .sogar_registry_config + .local_registry_name + .clone() + .unwrap_or_else(|| String::from(REGISTRY_NAME)); + + let registry_namespace = config + .sogar_registry_config + .local_registry_image + .clone() + .unwrap_or_else(|| String::from(NAMESPACE)); + + let sogar = SogarController::new(registry_name.as_str(), registry_namespace.as_str()); + let token_controller = TokenController::new(config.clone()); + info!("Configuring HTTP router"); - router.controller(health).controller(jet).controller(session) + router + .controller(health) + .controller(jet) + .controller(session) + .controller(sogar) + .controller(token_controller) }) .configure_listener(|listener| listener.server_name("Devolutions Gateway")) .build_stack_only() diff --git a/devolutions-gateway/src/http/middlewares/log.rs b/devolutions-gateway/src/http/middlewares/log.rs index f99f0e38d..fb94baa81 100644 --- a/devolutions-gateway/src/http/middlewares/log.rs +++ b/devolutions-gateway/src/http/middlewares/log.rs @@ -3,7 +3,6 @@ use saphir::http_context::HttpContext; use saphir::middleware::MiddlewareChain; use saphir::prelude::*; use slog::{o, slog_debug, slog_info}; -use slog_scope; use slog_scope_futures::future03::FutureExt as SLogFutureEx; use std::time::Instant; diff --git a/devolutions-gateway/src/http/middlewares/mod.rs b/devolutions-gateway/src/http/middlewares/mod.rs index 1d0aabde1..4b4d42dc7 100644 --- a/devolutions-gateway/src/http/middlewares/mod.rs +++ b/devolutions-gateway/src/http/middlewares/mod.rs @@ -1,2 +1,3 @@ pub mod auth; pub mod log; +pub mod sogar_auth; diff --git a/devolutions-gateway/src/http/middlewares/sogar_auth.rs b/devolutions-gateway/src/http/middlewares/sogar_auth.rs new file mode 100644 index 000000000..afb1beaf6 --- /dev/null +++ b/devolutions-gateway/src/http/middlewares/sogar_auth.rs @@ -0,0 +1,106 @@ +use crate::{ + config::{Config, SogarPermission, SogarUser}, + http::middlewares::auth::{parse_auth_header, AuthHeaderType}, +}; +use picky::jose::jwt::{JwtSig, JwtValidator}; +use saphir::{http, http_context::State, prelude::*, response::Builder as ResponseBuilder}; +use slog_scope::error; +use sogar_core::registry::{ + BLOB_DOWNLOAD_ENDPOINT, BLOB_EXIST_ENDPOINT, BLOB_GET_LOCATION_ENDPOINT, BLOB_UPLOAD_ENDPOINT, + MANIFEST_DOWNLOAD_ENDPOINT, MANIFEST_EXIST_ENDPOINT, MANIFEST_UPLOAD_ENDPOINT, +}; +use std::sync::Arc; + +pub struct SogarAuthMiddleware { + config: Arc, +} + +impl SogarAuthMiddleware { + pub fn new(config: Arc) -> Self { + Self { config } + } +} + +impl Middleware for SogarAuthMiddleware { + fn next( + &'static self, + ctx: HttpContext, + chain: &'static dyn MiddlewareChain, + ) -> BoxFuture<'static, Result> { + auth_middleware(ctx, chain, self.config.clone()).boxed() + } +} + +async fn auth_middleware( + ctx: HttpContext, + chain: &'static dyn MiddlewareChain, + config: Arc, +) -> Result { + if let Some(metadata) = ctx.metadata.name { + let auth_header = ctx + .state + .request() + .expect("Invalid middleware state") + .headers() + .get(http::header::AUTHORIZATION); + + let auth_str = match auth_header.and_then(|header| header.to_str().ok()) { + None => { + error!("Authorization header is missing or wrong format."); + //to be able to play video in the browser + return if metadata == BLOB_DOWNLOAD_ENDPOINT || metadata == MANIFEST_DOWNLOAD_ENDPOINT { + chain.next(ctx).await + } else { + let response = ResponseBuilder::new().status(StatusCode::UNAUTHORIZED).build()?; + + let mut ctx = ctx.clone_with_empty_state(); + ctx.state = State::After(Box::new(response)); + Ok(ctx) + }; + } + Some(auth_str) => auth_str, + }; + + let private_key = config.delegation_private_key.clone(); + if let (Some((AuthHeaderType::Bearer, token)), Some(private_key)) = (parse_auth_header(auth_str), private_key) { + let public_key = private_key.to_public_key(); + match JwtSig::::decode(token.as_str(), &public_key, &JwtValidator::no_check()) { + Ok(user) => { + if let Some(permission) = user.claims.permission { + if metadata == BLOB_EXIST_ENDPOINT || metadata == MANIFEST_EXIST_ENDPOINT { + return chain.next(ctx).await; + } + + match permission { + SogarPermission::Push => { + if metadata == BLOB_GET_LOCATION_ENDPOINT + || metadata == BLOB_UPLOAD_ENDPOINT + || metadata == MANIFEST_UPLOAD_ENDPOINT + { + return chain.next(ctx).await; + } + } + SogarPermission::Pull => { + if metadata == BLOB_DOWNLOAD_ENDPOINT || metadata == MANIFEST_DOWNLOAD_ENDPOINT { + return chain.next(ctx).await; + } + } + } + } + } + Err(e) => { + error!("Failed to decode jwt token! Error is: {}", e); + } + } + } + + error!("Invalid authorization type"); + let response = ResponseBuilder::new().status(StatusCode::UNAUTHORIZED).build()?; + + let mut ctx = ctx.clone_with_empty_state(); + ctx.state = State::After(Box::new(response)); + return Ok(ctx); + } + + Ok(chain.next(ctx).await?) +} diff --git a/devolutions-gateway/src/jet_client.rs b/devolutions-gateway/src/jet_client.rs index 7f491c12a..253e94971 100644 --- a/devolutions-gateway/src/jet_client.rs +++ b/devolutions-gateway/src/jet_client.rs @@ -20,13 +20,12 @@ use crate::interceptor::pcap_recording::PcapRecordingInterceptor; use crate::jet::association::Association; use crate::jet::candidate::{Candidate, CandidateState}; use crate::jet::TransportType; -use crate::plugin_manager::SogarData; +use crate::registry::Registry; use crate::transport::tcp::TcpTransport; use crate::transport::{JetTransport, Transport}; use crate::utils::association::{remove_jet_association, ACCEPT_REQUEST_TIMEOUT}; use crate::utils::{create_tls_connector, into_other_io_error as error_other}; use crate::Proxy; - use std::path::PathBuf; use tokio_rustls::{TlsAcceptor, TlsStream}; @@ -114,8 +113,8 @@ async fn handle_build_proxy( ) -> Result<(), io::Error> { let mut recording_interceptor: Option = None; let association_id = response.association_id; - let mut remote_data = None; let mut recording_dir = None; + let mut file_pattern = None; let associations = jet_associations.lock().await; if let Some(association) = associations.get(&association_id) { @@ -129,24 +128,14 @@ async fn handle_build_proxy( ); recording_dir = match &config.recording_path { - Some(path) => { - interceptor.set_recording_directory(path.as_str()); + Some(path) if path.to_str().is_some() => { + interceptor.set_recording_directory(path.to_str().unwrap()); Some(PathBuf::from(path)) } - None => interceptor.get_recording_directory(), + _ => interceptor.get_recording_directory(), }; - let file_pattern = interceptor.get_filename_pattern(); - - let recording_info = config.recording_info.clone(); - remote_data = SogarData::new( - recording_info.sogar_path.clone(), - recording_info.registry_url.clone(), - recording_info.username.clone(), - recording_info.password.clone(), - recording_info.image_name, - Some(file_pattern), - ); + file_pattern = Some(interceptor.get_filename_pattern()); recording_interceptor = Some(interceptor); } @@ -154,9 +143,11 @@ async fn handle_build_proxy( } if let Some(interceptor) = recording_interceptor { - let proxy_result = handle_build_tls_proxy(config, response, interceptor, tls_acceptor).await; - if let (Some(push_data), Some(dir)) = (remote_data, recording_dir) { - push_data.push(dir.as_path(), association_id.clone().to_string()) + let proxy_result = handle_build_tls_proxy(config.clone(), response, interceptor, tls_acceptor).await; + + if let (Some(dir), Some(pattern)) = (recording_dir, file_pattern) { + let registry = Registry::new(config); + registry.manage_files(association_id.to_string(), pattern, dir.as_path()); }; proxy_result diff --git a/devolutions-gateway/src/lib.rs b/devolutions-gateway/src/lib.rs index 34d516251..9edf82c40 100644 --- a/devolutions-gateway/src/lib.rs +++ b/devolutions-gateway/src/lib.rs @@ -15,6 +15,7 @@ pub mod logger; pub mod plugin_manager; pub mod proxy; pub mod rdp; +pub mod registry; pub mod routing_client; pub mod service; pub mod transport; diff --git a/devolutions-gateway/src/main.rs b/devolutions-gateway/src/main.rs index 33e3749ea..5f1c3c355 100644 --- a/devolutions-gateway/src/main.rs +++ b/devolutions-gateway/src/main.rs @@ -43,7 +43,7 @@ Service!("gateway", gateway_service_main); #[tokio::main] async fn main() -> Result<(), String> { let args: Vec = std::env::args().collect(); - if (args.len() > 1) && (!args[1].starts_with("-")) { + if (args.len() > 1) && (!args[1].starts_with('-')) { let cli_app = App::new(crate_name!()) .author("Devolutions Inc.") .version(concat!(crate_version!(), "\n")) diff --git a/devolutions-gateway/src/plugin_manager.rs b/devolutions-gateway/src/plugin_manager.rs index d265cd90b..d4f0304d2 100644 --- a/devolutions-gateway/src/plugin_manager.rs +++ b/devolutions-gateway/src/plugin_manager.rs @@ -6,12 +6,11 @@ use std::sync::{Arc, Mutex}; mod packets_parsing; mod plugin_info; -mod push_files; mod recording; + use crate::utils::into_other_io_error; pub use packets_parsing::PacketsParser; use plugin_info::{PluginCapabilities, PluginInformation}; -pub use push_files::SogarData; pub use recording::Recorder; #[derive(Clone)] diff --git a/devolutions-gateway/src/plugin_manager/packets_parsing.rs b/devolutions-gateway/src/plugin_manager/packets_parsing.rs index 6c6edef07..eefae707a 100644 --- a/devolutions-gateway/src/plugin_manager/packets_parsing.rs +++ b/devolutions-gateway/src/plugin_manager/packets_parsing.rs @@ -136,8 +136,8 @@ impl PacketsParser { ImageUpdate { update_x, update_y, - update_height, update_width, + update_height, surface_step, image_buff, } diff --git a/devolutions-gateway/src/plugin_manager/recording.rs b/devolutions-gateway/src/plugin_manager/recording.rs index e2ea49703..e6b218523 100644 --- a/devolutions-gateway/src/plugin_manager/recording.rs +++ b/devolutions-gateway/src/plugin_manager/recording.rs @@ -29,7 +29,7 @@ pub struct RecordingApi<'a> { updateY: u32, updateWidth: u32, updateHeight: u32, - surfaceStep: *const u32, + surfaceStep: u32, ), >, NowRecording_Timeout: Symbol<'a, unsafe extern "C" fn(ctx: RecordingContext)>, @@ -71,7 +71,7 @@ impl Recorder { image_data.update_y, image_data.update_width, image_data.update_height, - (&mut image_data.surface_step) as *mut u32, + image_data.surface_step, ); } } diff --git a/devolutions-gateway/src/registry/mod.rs b/devolutions-gateway/src/registry/mod.rs new file mode 100644 index 000000000..fb9b1e6e7 --- /dev/null +++ b/devolutions-gateway/src/registry/mod.rs @@ -0,0 +1,339 @@ +mod push_files; + +use crate::registry::push_files::{get_file_list_from_path, SogarData}; +use crate::{ + config::Config, + http::http_server::{NAMESPACE, REGISTRY_NAME}, +}; +use slog_scope::{debug, error}; +use sogar_core::{create_annotation_for_filename, parse_digest, read_file_data, registry, FileInfo, Layer}; +use std::{ + fs, + path::{Path, PathBuf}, + sync::Arc, + thread, + time::Duration, +}; +use tempfile::NamedTempFile; + +pub struct Registry { + config: Arc, + registry_path: PathBuf, +} + +impl Registry { + pub fn new(config: Arc) -> Self { + let registry_name = config + .sogar_registry_config + .local_registry_name + .clone() + .unwrap_or_else(|| String::from(REGISTRY_NAME)); + + let registry_namespace = config + .sogar_registry_config + .local_registry_image + .clone() + .unwrap_or_else(|| String::from(NAMESPACE)); + + let registry_path = format!("{}/{}", registry_name, registry_namespace); + + Self { + config, + registry_path: PathBuf::from(registry_path), + } + } + + pub fn manage_files(&self, tag: String, file_pattern: String, recording_dir: &Path) { + let config = self.config.sogar_registry_config.clone(); + + let files = get_file_list_from_path(file_pattern.as_str(), recording_dir); + + if let Some(true) = config.serve_as_registry { + self.move_file_to_registry(files.clone(), tag.as_str()); + } + + if let Some(true) = config.push_files { + self.push_files(file_pattern, recording_dir, tag); + } + + if let Some(true) = config.keep_files { + thread::spawn(move || { + if let Some(duration) = config.keep_time { + thread::sleep(Duration::from_secs(duration as u64)); + remove_files(files); + } + }); + } else { + remove_files(files); + } + } + + fn move_file_to_registry(&self, files: Vec, tag: &str) { + let mut layers = Vec::new(); + for file in files { + if let Some(file_data) = move_blob(file, self.registry_path.as_path()) { + layers.push(file_data.layer.clone()); + } + } + + let config_file = NamedTempFile::new(); + if let Err(e) = &config_file { + error!("Failed to create config file file with error {}.", e); + return; + } + + let config_file = config_file.unwrap(); + let config_data = sogar_core::create_config(config_file.path()); + + if let Err(e) = &config_data { + error!("Failed to create file info about config with error {}. Skipping it.", e); + return; + } + + let manifest_mime = create_and_move_manifest(self.registry_path.as_path(), config_data.unwrap(), layers, tag); + + registry::add_artifacts_info(tag.to_string(), manifest_mime, self.registry_path.as_path()); + } + + fn push_files(&self, file_pattern: String, recording_dir: &Path, tag: String) { + let sogar_push_data = self.config.sogar_registry_config.sogar_push_registry_info.clone(); + + let remote_data = SogarData::new( + sogar_push_data.sogar_util_path.clone(), + sogar_push_data.registry_url, + sogar_push_data.username.clone(), + sogar_push_data.password.clone(), + sogar_push_data.image_name, + Some(file_pattern), + ); + + if let Some(push_data) = remote_data { + push_data.push(recording_dir, tag) + }; + } +} + +fn remove_files(files: Vec) { + for file in files { + if let Err(e) = fs::remove_file(Path::new(&file)) { + error!("Failed to remove file {} with error {}", file, e); + } + } +} + +fn create_and_move_manifest( + registry_path: &Path, + config_data: FileInfo, + layers: Vec, + tag: &str, +) -> Option { + let manifest_file = NamedTempFile::new(); + if let Err(e) = &manifest_file { + error!("Failed to create manifest file with error {}.", e); + return None; + } + + let manifest_file = manifest_file.unwrap(); + let manifest = sogar_core::Manifest { + schema_version: 2, + config: config_data.layer, + layers, + }; + + let manifest_file_info = sogar_core::create_file_info(manifest, manifest_file.path()); + + if let Err(e) = &manifest_file_info { + error!("Failed to create manifest with error {}.", e); + return None; + } + + let manifest_file_info = manifest_file_info.unwrap(); + let manifest_path = registry_path.join(registry::ARTIFACTS_DIR).join(tag); + + if let Err(e) = fs::copy(manifest_file_info.path, manifest_path) { + error!("Failed to copy manifest to the registry with error {}!", e); + return None; + } + + Some(manifest_file_info.layer.media_type) +} + +fn move_blob(file: String, registry_path: &Path) -> Option { + let file_path = Path::new(file.as_str()); + let mime_type = sogar_core::config::get_mime_type_from_file_extension(file.clone()); + let annotations = create_annotation_for_filename(file_path); + let file_data = read_file_data(file_path, mime_type, Some(annotations)); + + if let Err(e) = &file_data { + error!( + "Failed to create file info about file {:?} with error {}. Skipping it.", + file_path, e + ); + return None; + } + + let file_data = file_data.unwrap(); + let digest = parse_digest(file_data.layer.digest.clone()); + if digest.is_none() { + error!("Failed to parse digest for the file {}", file); + return None; + } + + let digest = digest.unwrap(); + let blob_dir = registry_path.join(registry::ARTIFACTS_DIR).join(&digest.digest_type); + + let blob_path = blob_dir.join(&digest.value); + + if !blob_dir.exists() { + if let Err(e) = fs::create_dir_all(blob_dir) { + error!("Failed to create dir for the blob with error {}!", e); + return None; + } + } else if blob_path.exists() { + debug!("File {} already saved in registry!", file.clone()); + return None; + } + + if let Err(e) = fs::copy(file_path, blob_path) { + error!("Failed to copy blob to the registry with error {}!", e); + } + + Some(file_data) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + + #[test] + fn test_files_moved_to_registry() { + let files_dir_name = "dir_with_file1"; + let file_name = "test1.txt"; + let file_path = format!("{}/{}", files_dir_name, file_name); + + let path_buf = PathBuf::from("test_registry1/test_image1").join(sogar_registry::ARTIFACTS_DIR); + create_file_and_registry(String::from(files_dir_name), file_path.clone(), path_buf.as_path()); + + let mut config = Config::default(); + config.sogar_registry_config.serve_as_registry = Some(true); + config.sogar_registry_config.push_files = Some(false); + config.sogar_registry_config.keep_files = Some(false); + config.sogar_registry_config.local_registry_name = Some(String::from("test_registry1")); + config.sogar_registry_config.local_registry_image = Some(String::from("test_image1")); + + let registry = Registry::new(Arc::new(config)); + + assert_eq!(path_buf.exists(), true); + assert_eq!(path_buf.is_dir(), true); + + registry.manage_files(String::from("tag"), String::from("test1"), Path::new(files_dir_name)); + + assert_eq!(path_buf.join("tag").exists(), true); + assert_eq!(path_buf.join("sha256").exists(), true); + assert_eq!( + path_buf + .join("sha256") + .join("71f98783dc1d803d41c0e7586a636a8cbaac8b6fc739681123a8f674d3d0f544") + .exists(), + true + ); + assert_eq!(PathBuf::from(file_path.as_str()).exists(), false); + + fs::remove_dir_all(Path::new("test_registry1")).unwrap(); + fs::remove_dir_all(Path::new(files_dir_name)).unwrap(); + } + + #[test] + fn test_files_not_removed() { + let files_dir_name = "dir_with_file2"; + let file_name = "test2.txt"; + let file_path = format!("{}/{}", files_dir_name, file_name); + + let path_buf = PathBuf::from("test_registry2/test_image2").join(sogar_registry::ARTIFACTS_DIR); + create_file_and_registry(String::from(files_dir_name), file_path.clone(), path_buf.as_path()); + + let mut config = Config::default(); + config.sogar_registry_config.serve_as_registry = Some(true); + config.sogar_registry_config.push_files = Some(false); + config.sogar_registry_config.keep_files = Some(true); + config.sogar_registry_config.keep_time = None; + config.sogar_registry_config.local_registry_name = Some(String::from("test_registry2")); + config.sogar_registry_config.local_registry_image = Some(String::from("test_image2")); + + let registry = Registry::new(Arc::new(config)); + + assert_eq!(path_buf.exists(), true); + assert_eq!(path_buf.is_dir(), true); + + registry.manage_files(String::from("tag"), String::from("test2"), Path::new(files_dir_name)); + + assert_eq!(path_buf.join("tag").exists(), true); + assert_eq!(path_buf.join("sha256").exists(), true); + assert_eq!( + path_buf + .join("sha256") + .join("71f98783dc1d803d41c0e7586a636a8cbaac8b6fc739681123a8f674d3d0f544") + .exists(), + true + ); + assert_eq!(PathBuf::from(file_path.as_str()).exists(), true); + + fs::remove_dir_all(Path::new("test_registry2")).unwrap(); + fs::remove_dir_all(Path::new(files_dir_name)).unwrap(); + } + + #[test] + fn test_files_removed_after_timeout() { + let files_dir_name = "dir_with_file3"; + let file_name = "test3.txt"; + let file_path = format!("{}/{}", files_dir_name, file_name); + + let path_buf = PathBuf::from("test_registry3/test_image3").join(sogar_registry::ARTIFACTS_DIR); + create_file_and_registry(String::from(files_dir_name), file_path.clone(), path_buf.as_path()); + + let mut config = Config::default(); + config.sogar_registry_config.serve_as_registry = Some(true); + config.sogar_registry_config.push_files = Some(false); + config.sogar_registry_config.keep_files = Some(true); + config.sogar_registry_config.keep_time = Some(30); + config.sogar_registry_config.local_registry_name = Some(String::from("test_registry3")); + config.sogar_registry_config.local_registry_image = Some(String::from("test_image3")); + + let registry = Registry::new(Arc::new(config)); + + assert_eq!(path_buf.exists(), true); + assert_eq!(path_buf.is_dir(), true); + + registry.manage_files(String::from("tag"), String::from("test3"), Path::new(files_dir_name)); + + assert_eq!(path_buf.join("tag").exists(), true); + assert_eq!(path_buf.join("sha256").exists(), true); + assert_eq!( + path_buf + .join("sha256") + .join("71f98783dc1d803d41c0e7586a636a8cbaac8b6fc739681123a8f674d3d0f544") + .exists(), + true + ); + assert_eq!(PathBuf::from(file_path.as_str()).exists(), true); + + std::thread::sleep(Duration::from_secs(40)); + assert_eq!(PathBuf::from(file_path.as_str()).exists(), false); + + fs::remove_dir_all(Path::new("test_registry3")).unwrap(); + fs::remove_dir_all(Path::new(files_dir_name)).unwrap(); + } + + fn create_file_and_registry(files_dir_name: String, file_path: String, registry: &Path) { + let path_buf = PathBuf::from(files_dir_name); + fs::create_dir_all(path_buf.as_path()).unwrap(); + let mut file = File::create(file_path.as_str()).unwrap(); + file.write_all(b"Some text!").unwrap(); + + if !registry.exists() { + fs::create_dir_all(registry).unwrap(); + } + } +} diff --git a/devolutions-gateway/src/plugin_manager/push_files.rs b/devolutions-gateway/src/registry/push_files.rs similarity index 51% rename from devolutions-gateway/src/plugin_manager/push_files.rs rename to devolutions-gateway/src/registry/push_files.rs index f630df1fa..d85f28f8c 100644 --- a/devolutions-gateway/src/plugin_manager/push_files.rs +++ b/devolutions-gateway/src/registry/push_files.rs @@ -1,11 +1,10 @@ use slog_scope::{debug, error}; -use std::fs::DirEntry; -use std::path::Path; +use std::fs; +use std::path::{Path, PathBuf}; use std::process::Command; -use std::{fs, io}; pub struct SogarData { - sogar_path: String, + sogar_path: PathBuf, registry_url: String, username: String, password: String, @@ -15,7 +14,7 @@ pub struct SogarData { impl SogarData { pub fn new( - sogar_path: Option, + sogar_path: Option, registry_url: Option, username: Option, password: Option, @@ -46,59 +45,32 @@ impl SogarData { } pub fn push(&self, path: &Path, tag: String) { - let filtered_files = self.get_filtered_files(path); - if !filtered_files.is_empty() { - let mut file_paths = Vec::new(); - for file in filtered_files { - match file { - Ok(entry) => { - if let Some(path) = entry.path().to_str() { - file_paths.push(path.to_string()) - } - } - Err(e) => error!("Failed to get filename for the push: {}", e), - } - } - - if file_paths.is_empty() { - debug!( - "The recording folder does not contain the files with the specified file name {}", - self.file_pattern - ); - return; - } - - let reference = format!("{}:{}", self.image_name.clone(), tag); - let joined_path: &str = &file_paths.join(";"); - self.invoke_command(joined_path, reference); - for filepath in file_paths { - if let Err(e) = fs::remove_file(filepath.as_str()) { - error!("Failed to delete file {} after push: {}", filepath, e); - } - } + let file_paths = get_file_list_from_path(self.file_pattern.as_str(), path); + if file_paths.is_empty() { + debug!( + "The recording folder does not contain the files with the specified file name {}", + self.file_pattern + ); + return; } - } - fn get_filtered_files(&self, path: &Path) -> Vec> { - match fs::read_dir(path) { - Ok(paths) => paths - .filter(|path| match path { - Ok(dir_entry) => match dir_entry.file_name().into_string() { - Ok(filename) => filename.starts_with(self.file_pattern.as_str()), - Err(_) => false, - }, - Err(_) => false, - }) - .collect::>(), - Err(e) => { - error!("Failed to read dir {:?} with error {}", path, e); - Vec::new() + let reference = format!("{}:{}", self.image_name, tag); + let joined_path: &str = &file_paths.join(";"); + self.invoke_command(joined_path, reference); + for filepath in file_paths { + if let Err(e) = fs::remove_file(filepath.as_str()) { + error!("Failed to delete file {} after push: {}", filepath, e); } } } fn invoke_command(&self, file_path: &str, reference: String) { - let mut command = Command::new(self.sogar_path.clone()); + if self.sogar_path.to_str().is_none() || !self.sogar_path.is_file() { + error!("Failed to retrieve path string or path is not a file."); + return; + } + + let mut command = Command::new(self.sogar_path.to_str().unwrap()); let args = command .arg("--registry-url") .arg(self.registry_url.clone().as_str()) @@ -125,3 +97,27 @@ impl SogarData { } } } + +pub fn get_file_list_from_path(file_pattern: &str, path: &Path) -> Vec { + match fs::read_dir(path) { + Ok(paths) => paths + .filter_map(|path| match path { + Ok(dir_entry) => match (dir_entry.file_name().into_string(), dir_entry.path().to_str()) { + (Ok(filename), Some(path)) => { + if filename.starts_with(file_pattern) { + Some(path.to_string()) + } else { + None + } + } + _ => None, + }, + Err(_) => None, + }) + .collect::>(), + Err(e) => { + error!("Failed to read dir {:?} with error {}", path, e); + Vec::new() + } + } +} diff --git a/devolutions-gateway/src/websocket_client.rs b/devolutions-gateway/src/websocket_client.rs index 9d9353849..ad824a537 100644 --- a/devolutions-gateway/src/websocket_client.rs +++ b/devolutions-gateway/src/websocket_client.rs @@ -229,7 +229,7 @@ async fn handle_jet_connect_impl( let association_id = candidate.association_id(); let candidate_id = candidate.id(); - let mut remote_data = None; + let mut file_pattern = None; let mut recording_dir = None; let mut recording_interceptor: Option> = None; let mut has_interceptor = false; @@ -244,24 +244,14 @@ async fn handle_jet_connect_impl( ); recording_dir = match &config.recording_path { - Some(path) => { - interceptor.set_recording_directory(path.as_str()); + Some(path) if path.to_str().is_some() => { + interceptor.set_recording_directory(path.to_str().unwrap()); Some(std::path::PathBuf::from(path)) } - None => interceptor.get_recording_directory(), + _ => interceptor.get_recording_directory(), }; - let file_pattern = interceptor.get_filename_pattern(); - - let recording_info = config.recording_info.clone(); - remote_data = crate::plugin_manager::SogarData::new( - recording_info.sogar_path.clone(), - recording_info.registry_url.clone(), - recording_info.username.clone(), - recording_info.password.clone(), - recording_info.image_name, - Some(file_pattern), - ); + file_pattern = Some(interceptor.get_filename_pattern()); recording_interceptor = Some(Box::new(interceptor)); has_interceptor = true; @@ -272,13 +262,14 @@ async fn handle_jet_connect_impl( // Rust does not drop it automatically before end of the function std::mem::drop(jet_assc); - let proxy_result = Proxy::new(config) + let proxy_result = Proxy::new(config.clone()) .build_with_packet_interceptor(server_transport, client_transport, recording_interceptor) .await; if has_interceptor { - if let (Some(push_data), Some(dir)) = (remote_data, recording_dir) { - push_data.push(dir.as_path(), association_id.clone().to_string()) + if let (Some(dir), Some(pattern)) = (recording_dir, file_pattern) { + let registry = crate::registry::Registry::new(config); + registry.manage_files(association_id.clone().to_string(), pattern, dir.as_path()); }; }