ControllerAuthLayer {
+ pub fn new(auth: P) -> Self {
+ Self { auth }
+ }
+}
+
+const ALLOWED_UNAUTHORIZED_PATHS: [&str; 2] = [
+ "/nymvpn.controller.ControllerService/AccountSignIn",
+ "/nymvpn.controller.ControllerService/IsAuthenticated",
+];
+
+#[derive(Debug, Clone)]
+pub struct ControllerAuthMiddleware {
+ auth: P,
+ inner: S,
+}
+
+impl Layer for ControllerAuthLayer
{
+ type Service = ControllerAuthMiddleware;
+ fn layer(&self, inner: S) -> Self::Service {
+ ControllerAuthMiddleware {
+ auth: self.auth.clone(),
+ inner,
+ }
+ }
+}
+
+impl Service> for ControllerAuthMiddleware
+where
+ S: Service, Response = hyper::Response> + Clone + Send + 'static,
+ S::Future: Send + 'static,
+ P: Auth + 'static,
+{
+ type Response = S::Response;
+ type Error = S::Error;
+ type Future = futures::future::BoxFuture<'static, Result>;
+
+ fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
+ self.inner.poll_ready(cx)
+ }
+
+ fn call(&mut self, req: hyper::Request) -> Self::Future {
+ // This is necessary because tonic internally uses `tower::buffer::Buffer`.
+ // See https://github.com/tower-rs/tower/issues/547#issuecomment-767629149
+ // for details on why this is necessary
+ let clone = self.inner.clone();
+ let mut inner = std::mem::replace(&mut self.inner, clone);
+
+ let auth = self.auth.clone();
+ Box::pin(async move {
+ if ALLOWED_UNAUTHORIZED_PATHS.contains(&req.uri().path())
+ || auth.is_authenticated().await
+ {
+ return inner.call(req).await;
+ }
+
+ Ok(tonic::Status::unauthenticated("please sign in first").to_http())
+ })
+ }
+}
diff --git a/nym-vpn/desktop/nymvpn-controller/src/conversions/account.rs b/nym-vpn/desktop/nymvpn-controller/src/conversions/account.rs
new file mode 100644
index 00000000000..cb0e87f35af
--- /dev/null
+++ b/nym-vpn/desktop/nymvpn-controller/src/conversions/account.rs
@@ -0,0 +1,8 @@
+impl From for nymvpn_types::nymvpn_server::UserCredentials {
+ fn from(value: crate::proto::SignInRequest) -> Self {
+ Self {
+ email: value.email,
+ password: value.password,
+ }
+ }
+}
diff --git a/nym-vpn/desktop/nymvpn-controller/src/conversions/location.rs b/nym-vpn/desktop/nymvpn-controller/src/conversions/location.rs
new file mode 100644
index 00000000000..f6493ac6c21
--- /dev/null
+++ b/nym-vpn/desktop/nymvpn-controller/src/conversions/location.rs
@@ -0,0 +1,48 @@
+impl From for crate::proto::Location {
+ fn from(value: nymvpn_types::location::Location) -> Self {
+ Self {
+ code: value.code,
+ country: value.country,
+ country_code: value.country_code,
+ city: value.city,
+ city_code: value.city_code,
+ state: value.state,
+ state_code: value.state_code,
+ }
+ }
+}
+
+impl From for nymvpn_types::location::Location {
+ fn from(value: crate::proto::Location) -> Self {
+ Self {
+ code: value.code,
+ country: value.country,
+ country_code: value.country_code,
+ city: value.city,
+ city_code: value.city_code,
+ state: value.state,
+ state_code: value.state_code,
+ }
+ }
+}
+
+impl From> for crate::proto::Locations {
+ fn from(value: Vec) -> Self {
+ Self {
+ location: value
+ .into_iter()
+ .map(crate::proto::Location::from)
+ .collect(),
+ }
+ }
+}
+
+impl From for Vec {
+ fn from(value: crate::proto::Locations) -> Self {
+ value
+ .location
+ .into_iter()
+ .map(nymvpn_types::location::Location::from)
+ .collect()
+ }
+}
diff --git a/nym-vpn/desktop/nymvpn-controller/src/conversions/mod.rs b/nym-vpn/desktop/nymvpn-controller/src/conversions/mod.rs
new file mode 100644
index 00000000000..42fc2a973ff
--- /dev/null
+++ b/nym-vpn/desktop/nymvpn-controller/src/conversions/mod.rs
@@ -0,0 +1,4 @@
+pub mod account;
+pub mod location;
+pub mod notification;
+pub mod vpn_status;
diff --git a/nym-vpn/desktop/nymvpn-controller/src/conversions/notification.rs b/nym-vpn/desktop/nymvpn-controller/src/conversions/notification.rs
new file mode 100644
index 00000000000..09b2a369319
--- /dev/null
+++ b/nym-vpn/desktop/nymvpn-controller/src/conversions/notification.rs
@@ -0,0 +1,77 @@
+use crate::timestamp_to_datetime_utc;
+
+impl From for nymvpn_types::notification::NotificationType {
+ fn from(value: crate::proto::NotificationType) -> Self {
+ match value {
+ crate::proto::NotificationType::ServerFailed => {
+ nymvpn_types::notification::NotificationType::ServerFailed
+ }
+ crate::proto::NotificationType::ClientFailed => {
+ nymvpn_types::notification::NotificationType::ClientFailed
+ }
+ }
+ }
+}
+
+impl From for crate::proto::NotificationType {
+ fn from(value: nymvpn_types::notification::NotificationType) -> Self {
+ match value {
+ nymvpn_types::notification::NotificationType::ServerFailed => {
+ crate::proto::NotificationType::ServerFailed
+ }
+ nymvpn_types::notification::NotificationType::ClientFailed => {
+ crate::proto::NotificationType::ClientFailed
+ }
+ }
+ }
+}
+
+impl TryFrom for nymvpn_types::notification::Notification {
+ type Error = String;
+ fn try_from(value: crate::proto::Notification) -> Result {
+ Ok(Self {
+ id: value.id,
+ message: value.message,
+ notification_type: value.notification_type.try_into()?,
+ timestamp: timestamp_to_datetime_utc(value.timestamp)?,
+ })
+ }
+}
+
+impl From for crate::proto::Notification {
+ fn from(value: nymvpn_types::notification::Notification) -> Self {
+ let seconds = value.timestamp.timestamp();
+ let nanos = value.timestamp.timestamp_subsec_nanos();
+ Self {
+ id: value.id,
+ notification_type: value.notification_type.into(),
+ message: value.message,
+ timestamp: Some(prost_types::Timestamp {
+ seconds,
+ nanos: nanos as i32,
+ }),
+ }
+ }
+}
+
+impl From> for crate::proto::Notifications {
+ fn from(value: Vec) -> Self {
+ Self {
+ notification: value
+ .into_iter()
+ .map(crate::proto::Notification::from)
+ .collect(),
+ }
+ }
+}
+
+impl TryFrom for Vec {
+ type Error = String;
+ fn try_from(value: crate::proto::Notifications) -> Result {
+ let mut notifications = vec![];
+ for notification in value.notification {
+ notifications.push(notification.try_into()?)
+ }
+ Ok(notifications)
+ }
+}
diff --git a/nym-vpn/desktop/nymvpn-controller/src/conversions/vpn_status.rs b/nym-vpn/desktop/nymvpn-controller/src/conversions/vpn_status.rs
new file mode 100644
index 00000000000..d49370fa17e
--- /dev/null
+++ b/nym-vpn/desktop/nymvpn-controller/src/conversions/vpn_status.rs
@@ -0,0 +1,110 @@
+use crate::{datetime_utc_to_timestamp, timestamp_to_datetime_utc};
+
+impl From for crate::proto::VpnStatus {
+ fn from(value: nymvpn_types::vpn_session::VpnStatus) -> Self {
+ match value {
+ nymvpn_types::vpn_session::VpnStatus::Accepted(location) => crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::Accepted(
+ crate::proto::vpn_status::Accepted {
+ location: Some(location.into()),
+ },
+ )),
+ },
+ nymvpn_types::vpn_session::VpnStatus::Connecting(location) => crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::Connecting(
+ crate::proto::vpn_status::Connecting {
+ location: Some(location.into()),
+ },
+ )),
+ },
+ nymvpn_types::vpn_session::VpnStatus::ServerRunning(location) => {
+ crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::ServerRunning(
+ crate::proto::vpn_status::ServerRunning {
+ location: Some(location.into()),
+ },
+ )),
+ }
+ }
+ nymvpn_types::vpn_session::VpnStatus::ServerReady(location) => crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::ServerReady(
+ crate::proto::vpn_status::ServerReady {
+ location: Some(location.into()),
+ },
+ )),
+ },
+ nymvpn_types::vpn_session::VpnStatus::Connected(location, connected_time) => {
+ crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::Connected(
+ crate::proto::vpn_status::Connected {
+ location: Some(location.into()),
+ timestamp: Some(datetime_utc_to_timestamp(connected_time)),
+ },
+ )),
+ }
+ }
+ nymvpn_types::vpn_session::VpnStatus::Disconnecting(location) => {
+ crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::Disconnecting(
+ crate::proto::vpn_status::Disconnecting {
+ location: Some(location.into()),
+ },
+ )),
+ }
+ }
+ nymvpn_types::vpn_session::VpnStatus::Disconnected => crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::Disconnected(
+ crate::proto::vpn_status::Disconnected {},
+ )),
+ },
+ nymvpn_types::vpn_session::VpnStatus::ServerCreated(location) => {
+ crate::proto::VpnStatus {
+ vpn_status: Some(crate::proto::vpn_status::VpnStatus::ServerCreated(
+ crate::proto::vpn_status::ServerCreated {
+ location: Some(location.into()),
+ },
+ )),
+ }
+ }
+ }
+ }
+}
+
+impl From for nymvpn_types::vpn_session::VpnStatus {
+ fn from(value: crate::proto::VpnStatus) -> Self {
+ let vpn_status = value.vpn_status.unwrap();
+ match vpn_status {
+ crate::proto::vpn_status::VpnStatus::Accepted(accepted) => {
+ nymvpn_types::vpn_session::VpnStatus::Accepted(accepted.location.unwrap().into())
+ }
+ crate::proto::vpn_status::VpnStatus::Connecting(connecting) => {
+ nymvpn_types::vpn_session::VpnStatus::Connecting(connecting.location.unwrap().into())
+ }
+ crate::proto::vpn_status::VpnStatus::ServerRunning(srun) => {
+ nymvpn_types::vpn_session::VpnStatus::ServerRunning(srun.location.unwrap().into())
+ }
+ crate::proto::vpn_status::VpnStatus::ServerReady(sr) => {
+ nymvpn_types::vpn_session::VpnStatus::ServerReady(sr.location.unwrap().into())
+ }
+ crate::proto::vpn_status::VpnStatus::Connected(connected) => {
+ nymvpn_types::vpn_session::VpnStatus::Connected(
+ connected.location.unwrap().into(),
+ timestamp_to_datetime_utc(connected.timestamp).unwrap(),
+ )
+ }
+ crate::proto::vpn_status::VpnStatus::Disconnecting(disconnecting) => {
+ nymvpn_types::vpn_session::VpnStatus::Disconnecting(
+ disconnecting.location.unwrap().into(),
+ )
+ }
+ crate::proto::vpn_status::VpnStatus::Disconnected(_) => {
+ nymvpn_types::vpn_session::VpnStatus::Disconnected
+ }
+ crate::proto::vpn_status::VpnStatus::ServerCreated(server_created) => {
+ nymvpn_types::vpn_session::VpnStatus::ServerCreated(
+ server_created.location.unwrap().into(),
+ )
+ }
+ }
+ }
+}
diff --git a/nym-vpn/desktop/nymvpn-controller/src/lib.rs b/nym-vpn/desktop/nymvpn-controller/src/lib.rs
new file mode 100644
index 00000000000..bb19f07eab3
--- /dev/null
+++ b/nym-vpn/desktop/nymvpn-controller/src/lib.rs
@@ -0,0 +1,153 @@
+use std::future::Future;
+use std::pin::Pin;
+use std::task::Context;
+use std::task::Poll;
+
+use auth::Auth;
+use parity_tokio_ipc::Endpoint as IpcEndpoint;
+use prost_types::Timestamp;
+use thiserror::Error;
+use tokio::io::AsyncRead;
+use tokio::io::AsyncWrite;
+use tokio::io::ReadBuf;
+use tonic::transport::server::Connected;
+use tonic::transport::Channel;
+use tonic::transport::Endpoint as TonicEndpoint;
+use tonic::transport::Server;
+use tonic::transport::Uri;
+use tower::service_fn;
+
+pub mod proto {
+ tonic::include_proto!("nymvpn.controller");
+}
+
+pub mod auth;
+pub mod conversions;
+
+use chrono::{TimeZone, Utc};
+pub use proto::controller_service_server::{ControllerService, ControllerServiceServer};
+use nymvpn_types::DateTimeUtc;
+
+use crate::auth::ControllerAuthLayer;
+
+pub type ControllerServiceClient =
+ proto::controller_service_client::ControllerServiceClient;
+
+pub type GrpcServerJoinHandle = tokio::task::JoinHandle>;
+
+#[derive(Debug, Error)]
+pub enum ControllerError {
+ #[error("{0}")]
+ TonicTransportError(tonic::transport::Error),
+ #[error("security attributes error {0:#?}")]
+ SecurityAttributesError(std::io::Error),
+ #[error("incoming connection error {0:#?}")]
+ IncomingConnectionError(std::io::Error),
+}
+
+pub async fn new_grpc_client() -> Result {
+ let ipc_path = nymvpn_config::config().socket_path();
+
+ // URI is unused
+ let channel = TonicEndpoint::from_static("http://[::]:50051")
+ .connect_with_connector(service_fn(move |_: Uri| {
+ IpcEndpoint::connect(ipc_path.clone())
+ }))
+ .await
+ .map_err(ControllerError::TonicTransportError)?;
+
+ Ok(ControllerServiceClient::new(channel))
+}
+
+pub async fn spawn_grpc_server(
+ service: S,
+ auth: P,
+ shutdown: F,
+) -> std::result::Result
+where
+ S: proto::controller_service_server::ControllerService,
+ F: Future