From 27f4a408233b33d6c9f4677b9fd90ebdc19b3bcf Mon Sep 17 00:00:00 2001 From: dutchie032 Date: Wed, 11 Sep 2024 19:24:19 +0200 Subject: [PATCH 1/7] Added authentication interceptor to all layers --- Cargo.lock | 27 ++++++++++++++++++++------- Cargo.toml | 1 + lua/DCS-gRPC/grpc-mission.lua | 1 + lua/DCS-gRPC/grpc.lua | 27 +++++++++++++++++++++++++++ lua/Hooks/DCS-gRPC.lua | 1 + src/authentication.rs | 27 +++++++++++++++++++++++++++ src/config.rs | 9 +++++++++ src/lib.rs | 1 + src/server.rs | 23 +++++++++++++++++------ 9 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 src/authentication.rs diff --git a/Cargo.lock b/Cargo.lock index 158d0e31..60996af2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,6 +440,7 @@ dependencies = [ "tokio", "tokio-stream", "tonic", + "tonic-middleware", "walkdir", ] @@ -918,9 +919,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -958,7 +959,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "rustls 0.22.4", "rustls-pki-types", @@ -981,16 +982,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.3.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -1639,7 +1640,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-rustls 0.26.0", "hyper-util", "ipnet", @@ -2361,6 +2362,18 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "tonic-middleware" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d34dab0f18194ddb9164685a3d8cf777ff35042752aba2be208b1384d7a304" +dependencies = [ + "async-trait", + "futures-util", + "tonic", + "tower", +] + [[package]] name = "tower" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index c0fb3404..bd9e10c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ time = { version = "0.3", features = ["formatting", "parsing"] } tokio.workspace = true tokio-stream.workspace = true tonic.workspace = true +tonic-middleware = "0.1.4" [build-dependencies] walkdir = "2.3" diff --git a/lua/DCS-gRPC/grpc-mission.lua b/lua/DCS-gRPC/grpc-mission.lua index da41487e..e1d90a50 100644 --- a/lua/DCS-gRPC/grpc-mission.lua +++ b/lua/DCS-gRPC/grpc-mission.lua @@ -3,6 +3,7 @@ if not GRPC then -- scaffold nested tables to allow direct assignment in config file tts = { provider = { gcloud = {}, aws = {}, azure = {}, win = {} } }, srs = {}, + auth = {} } end diff --git a/lua/DCS-gRPC/grpc.lua b/lua/DCS-gRPC/grpc.lua index 6fa42a7b..9e47b9b9 100644 --- a/lua/DCS-gRPC/grpc.lua +++ b/lua/DCS-gRPC/grpc.lua @@ -8,6 +8,32 @@ end -- load and start RPC -- +function table_print(tt, indent, done) + done = done or {} + indent = indent or 0 + if type(tt) == "table" then + local sb = {} + for key, value in pairs(tt) do + table.insert(sb, string.rep(" ", indent)) -- indent it + if type(value) == "table" and not done[value] then + done[value] = true + table.insert(sb, key .. " = {\n"); + table.insert(sb, table_print(value, indent + 2, done)) + table.insert(sb, string.rep(" ", indent)) -- indent it + table.insert(sb, "}\n"); + elseif "number" == type(key) then + table.insert(sb, string.format("\"%s\"\n", tostring(value))) + else + table.insert(sb, string.format( + "%s = \"%s\"\n", tostring(key), tostring(value))) + end + end + return table.concat(sb) + else + return tt .. "\n" + end +end + if isMissionEnv then assert(grpc.start({ version = GRPC.version, @@ -21,6 +47,7 @@ if isMissionEnv then integrityCheckDisabled = GRPC.integrityCheckDisabled, tts = GRPC.tts, srs = GRPC.srs, + auth = GRPC.auth })) end diff --git a/lua/Hooks/DCS-gRPC.lua b/lua/Hooks/DCS-gRPC.lua index c14e8a97..09f659b7 100644 --- a/lua/Hooks/DCS-gRPC.lua +++ b/lua/Hooks/DCS-gRPC.lua @@ -9,6 +9,7 @@ local function init() -- scaffold nested tables to allow direct assignment in config file tts = { provider = { gcloud = {}, aws = {}, azure = {}, win = {} } }, srs = {}, + auth = {} } end diff --git a/src/authentication.rs b/src/authentication.rs new file mode 100644 index 00000000..ca2b290b --- /dev/null +++ b/src/authentication.rs @@ -0,0 +1,27 @@ +use crate::config::AuthConfig; +use tonic::codegen::http::Request; +use tonic::transport::Body; +use tonic::{async_trait, Status}; +use tonic_middleware::RequestInterceptor; + +#[derive(Clone)] +pub struct AuthInterceptor { + pub auth_config: AuthConfig, +} + +#[async_trait] +impl RequestInterceptor for AuthInterceptor { + async fn intercept(&self, req: Request) -> Result, Status> { + match req.headers().get("bearer").map(|v| v.to_str()) { + Some(Ok(token)) => { + //check if token is correct if auth is enabled + if self.auth_config.enabled == false || token == self.auth_config.token { + Ok(req) + } else { + Err(Status::unauthenticated("Unauthenticated")) + } + } + _ => Err(Status::unauthenticated("Unauthenticated")), + } + } +} diff --git a/src/config.rs b/src/config.rs index f626090b..a24395a6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,7 @@ pub struct Config { pub integrity_check_disabled: bool, pub tts: Option, pub srs: Option, + pub auth: Option, } #[derive(Debug, Clone, Default, Deserialize, Serialize)] @@ -87,6 +88,14 @@ pub struct SrsConfig { pub addr: Option, } +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthConfig { + #[serde(default)] + pub enabled: bool, + pub token: String, +} + fn default_host() -> String { String::from("127.0.0.1") } diff --git a/src/lib.rs b/src/lib.rs index fa518a1b..888ba849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] #![recursion_limit = "256"] +mod authentication; mod config; mod fps; #[cfg(feature = "hot-reload")] diff --git a/src/server.rs b/src/server.rs index c70b8acf..28803b1b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,6 +3,12 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; +use crate::authentication::AuthInterceptor; +use crate::config::{AuthConfig, Config, SrsConfig, TtsConfig}; +use crate::rpc::{HookRpc, MissionRpc, Srs}; +use crate::shutdown::{Shutdown, ShutdownHandle}; +use crate::srs::SrsClients; +use crate::stats::Stats; use dcs_module_ipc::IPC; use futures_util::FutureExt; use stubs::atmosphere::v0::atmosphere_service_server::AtmosphereServiceServer; @@ -25,12 +31,7 @@ use tokio::sync::oneshot::{self, Receiver}; use tokio::sync::{mpsc, Mutex}; use tokio::time::sleep; use tonic::transport; - -use crate::config::{Config, SrsConfig, TtsConfig}; -use crate::rpc::{HookRpc, MissionRpc, Srs}; -use crate::shutdown::{Shutdown, ShutdownHandle}; -use crate::srs::SrsClients; -use crate::stats::Stats; +use tonic_middleware::RequestInterceptorLayer; pub struct Server { runtime: Runtime, @@ -50,6 +51,7 @@ struct ServerState { tts_config: TtsConfig, srs_config: SrsConfig, srs_transmit: Arc>>, + auth_config: AuthConfig, } impl Server { @@ -71,6 +73,7 @@ impl Server { tts_config: config.tts.clone().unwrap_or_default(), srs_config: config.srs.clone().unwrap_or_default(), srs_transmit: Arc::new(Mutex::new(rx)), + auth_config: config.auth.clone().unwrap_or_default(), }, srs_transmit: tx, shutdown, @@ -203,6 +206,7 @@ async fn try_run( tts_config, srs_config, srs_transmit, + auth_config, } = state; let mut mission_rpc = @@ -242,7 +246,14 @@ async fn try_run( } }); + let auth_interceptor = AuthInterceptor { + auth_config: auth_config.clone(), + }; + + log::info!("Authentication enabled: {}", auth_config.enabled); + transport::Server::builder() + .layer(RequestInterceptorLayer::new(auth_interceptor.clone())) .add_service(AtmosphereServiceServer::new(mission_rpc.clone())) .add_service(CoalitionServiceServer::new(mission_rpc.clone())) .add_service(ControllerServiceServer::new(mission_rpc.clone())) From 3b7f738543c6104e20e93aeca9131c96234cf535 Mon Sep 17 00:00:00 2001 From: dutchie032 Date: Wed, 11 Sep 2024 19:40:50 +0200 Subject: [PATCH 2/7] cleaned up PR --- lua/DCS-gRPC/grpc.lua | 26 -------------------------- src/authentication.rs | 2 +- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/lua/DCS-gRPC/grpc.lua b/lua/DCS-gRPC/grpc.lua index 9e47b9b9..b6ee79b9 100644 --- a/lua/DCS-gRPC/grpc.lua +++ b/lua/DCS-gRPC/grpc.lua @@ -8,32 +8,6 @@ end -- load and start RPC -- -function table_print(tt, indent, done) - done = done or {} - indent = indent or 0 - if type(tt) == "table" then - local sb = {} - for key, value in pairs(tt) do - table.insert(sb, string.rep(" ", indent)) -- indent it - if type(value) == "table" and not done[value] then - done[value] = true - table.insert(sb, key .. " = {\n"); - table.insert(sb, table_print(value, indent + 2, done)) - table.insert(sb, string.rep(" ", indent)) -- indent it - table.insert(sb, "}\n"); - elseif "number" == type(key) then - table.insert(sb, string.format("\"%s\"\n", tostring(value))) - else - table.insert(sb, string.format( - "%s = \"%s\"\n", tostring(key), tostring(value))) - end - end - return table.concat(sb) - else - return tt .. "\n" - end -end - if isMissionEnv then assert(grpc.start({ version = GRPC.version, diff --git a/src/authentication.rs b/src/authentication.rs index ca2b290b..69dde31a 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -12,7 +12,7 @@ pub struct AuthInterceptor { #[async_trait] impl RequestInterceptor for AuthInterceptor { async fn intercept(&self, req: Request) -> Result, Status> { - match req.headers().get("bearer").map(|v| v.to_str()) { + match req.headers().get("X-API-Key").map(|v| v.to_str()) { Some(Ok(token)) => { //check if token is correct if auth is enabled if self.auth_config.enabled == false || token == self.auth_config.token { From e2fbedc7bab071604045c0fff4cb866bfc2b6986 Mon Sep 17 00:00:00 2001 From: dutchie032 Date: Wed, 11 Sep 2024 21:39:48 +0200 Subject: [PATCH 3/7] Updated authentication mechanism to take multiple token and clients --- src/authentication.rs | 29 +++++++++++++++++++++-------- src/config.rs | 8 ++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/authentication.rs b/src/authentication.rs index 69dde31a..d5172610 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -12,16 +12,29 @@ pub struct AuthInterceptor { #[async_trait] impl RequestInterceptor for AuthInterceptor { async fn intercept(&self, req: Request) -> Result, Status> { - match req.headers().get("X-API-Key").map(|v| v.to_str()) { - Some(Ok(token)) => { - //check if token is correct if auth is enabled - if self.auth_config.enabled == false || token == self.auth_config.token { - Ok(req) - } else { - Err(Status::unauthenticated("Unauthenticated")) + if !self.auth_config.enabled { + Ok(req) + } else { + match req.headers().get("X-API-Key").map(|v| v.to_str()) { + Some(Ok(token)) => { + //check if token is correct if auth is + let mut client: Option<&String> = None; + for key in &self.auth_config.tokens { + if key.token == token { + client = Some(&key.client); + break; + } + } + + if client.is_some() { + log::debug!("Authenticated client: {}", client.unwrap()); + Ok(req) + } else { + Err(Status::unauthenticated("Unauthenticated")) + } } + _ => Err(Status::unauthenticated("Unauthenticated")), } - _ => Err(Status::unauthenticated("Unauthenticated")), } } } diff --git a/src/config.rs b/src/config.rs index a24395a6..726fa7e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,6 +93,14 @@ pub struct SrsConfig { pub struct AuthConfig { #[serde(default)] pub enabled: bool, + pub tokens: Vec, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiKey { + #[serde(default)] + pub client: String, pub token: String, } From 3577e9be49febac55f38d7b8513f7368de35d73b Mon Sep 17 00:00:00 2001 From: dutchie032 Date: Wed, 11 Sep 2024 21:52:13 +0200 Subject: [PATCH 4/7] minor cleanup --- src/authentication.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/authentication.rs b/src/authentication.rs index d5172610..e19933db 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -17,7 +17,6 @@ impl RequestInterceptor for AuthInterceptor { } else { match req.headers().get("X-API-Key").map(|v| v.to_str()) { Some(Ok(token)) => { - //check if token is correct if auth is let mut client: Option<&String> = None; for key in &self.auth_config.tokens { if key.token == token { From 6c25a024ad7812b24884dcfa8a429afd6d472696 Mon Sep 17 00:00:00 2001 From: dutchie032 Date: Wed, 11 Sep 2024 22:41:39 +0200 Subject: [PATCH 5/7] updated documentation changelog --- CHANGELOG.md | 1 + README.md | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3383bb5e..450e623f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `GetClients` to `SrsService`, which retrieves a list of units that are connected to SRS and the frequencies they are connected to. - Added `SrsConnectEvent` and `SrsDisconnectEvent` events - Added `GetDrawArgumentValue` API for units, which returns the value for drawing. (useful for "hook down", "doors open" checks) +- Added Authentication Interceptor. This enables authentication on a per client basis. ### Fixed - Fixed `MarkAddEvent`, `MarkChangeEvent` and `MarkRemoveEvent` position diff --git a/README.md b/README.md index 423d40f3..8bc07499 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,15 @@ throughputLimit = 600 -- Whether the integrity check, meant to spot installation issues, is disabled. integrityCheckDisabled = false +-- Whether or not authentication is required +auth.enabled = false +-- Authentication tokens table with client names and their tokens for split tokens. +auth.tokens = { + -- client => clientName, token => Any token. Advice to use UTF-8 only. Length not limited explicitly + { client = "SomeClient", token = "SomeToken" }, + { client = "SomeClient2", token = "SomeOtherToken" } +} + -- The default TTS provider to use if a TTS request does not explicitly specify another one. tts.defaultProvider = "win" From 00e5a3bf3929cd112e4e94eae1870a29082258fc Mon Sep 17 00:00:00 2001 From: dutchie032 Date: Tue, 24 Sep 2024 19:45:39 +0200 Subject: [PATCH 6/7] Updated README.md with dotnet example (collabled so it's not taking too much space --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index 8bc07499..09236bfb 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,52 @@ In order to develop clients for `DCS-gRPC` you must be familiar with gRPC concep The gRPC .proto files are available in the `Docs/DCS-gRPC` folder and also available in the Github repo +### Authentication + +If authentication is enabled you will have to add `X-API-Key` to the metadata/headers. +Below are some example on what it could look like in your code. + +#### Examples + +
+ dotnet / c# + +You can either set the `Metadata` for each request or you can create a `GrpcChannel` with an interceptor that will set the key each time. + +For a single request: + +```c# +var client = new MissionService.MissionServiceClient(channel); + +Metadata metadata = new Metadata() +{ + { "X-API-Key", "" } +}; + +var response = client.GetScenarioCurrentTime(new GetScenarioCurrentTimeRequest { }, headers: metadata, deadline: DateTime.UtcNow.AddSeconds(2)); +``` + +For all requests on a channel: +```c# +public GrpcChannel CreateChannel(string host, string post, string? apiKey) +{ + GrpcChannelOptions options = new GrpcChannelOptions(); + if (apiKey != null) + { + CallCredentials credentials = CallCredentials.FromInterceptor(async (context, metadata) => + { + metadata.Add("X-API-Key", apiKey); + }); + + options.Credentials = ChannelCredentials.Create(ChannelCredentials.Insecure, credentials) ; + } + + return GrpcChannel.ForAddress($"http://{host}:{port}", options); +} +``` + +
+ ## Server Development The following section is only applicable to people who want to developer the DCS-gRPC server itself. From b798bc36b82bcac163ea8552ea522633929fd481 Mon Sep 17 00:00:00 2001 From: dutchie032 Date: Tue, 24 Sep 2024 19:52:13 +0200 Subject: [PATCH 7/7] minor update to make the readme more exact --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 09236bfb..df2acda4 100644 --- a/README.md +++ b/README.md @@ -226,9 +226,9 @@ In order to develop clients for `DCS-gRPC` you must be familiar with gRPC concep The gRPC .proto files are available in the `Docs/DCS-gRPC` folder and also available in the Github repo -### Authentication +### Client Authentication -If authentication is enabled you will have to add `X-API-Key` to the metadata/headers. +If authentication is enabled on the server you will have to add `X-API-Key` to the metadata/headers. Below are some example on what it could look like in your code. #### Examples