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/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/README.md b/README.md
index 423d40f3..df2acda4 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"
@@ -217,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
+### Client Authentication
+
+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
+
+
+ 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.
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..b6ee79b9 100644
--- a/lua/DCS-gRPC/grpc.lua
+++ b/lua/DCS-gRPC/grpc.lua
@@ -21,6 +21,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..e19933db
--- /dev/null
+++ b/src/authentication.rs
@@ -0,0 +1,39 @@
+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> {
+ if !self.auth_config.enabled {
+ Ok(req)
+ } else {
+ match req.headers().get("X-API-Key").map(|v| v.to_str()) {
+ Some(Ok(token)) => {
+ 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")),
+ }
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
index f626090b..726fa7e2 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,22 @@ 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 tokens: Vec,
+}
+
+#[derive(Debug, Clone, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ApiKey {
+ #[serde(default)]
+ pub client: String,
+ 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()))