Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test app id assignment #175

Merged
merged 2 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/bindings/CloudApiErrors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam" | "InvalidName" | "UnauthorizedOriginError" | "AppDoesNotExist" | "UserAlreadyInvitedToTheTeam" | "MaximumInvitesPerTeamReached" | "InviteNotFound" | "ActionForbiddenForPersonalTeam" | "InviteDoesNotExist" | "InvalidPaginationCursor" | "InvalidVerificationCode" | "InvalidDomainName" | "DomainAlreadyVerified" | "DomainVerificationFailure" | "DomainNotFound" | "DomainVerificationNotStarted" | "WebAuthnError" | "PasswordNotSet" | "UserDoesNotHavePasskey" | "PasskeyAlreadyExists" | "InvalidPasskeyCredential" | "PasskeyDoesNotExist" | "FailedToCreateTeam" | "DashboardImportFail";
export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam" | "InvalidName" | "UnauthorizedOriginError" | "AppDoesNotExist" | "UserAlreadyInvitedToTheTeam" | "MaximumInvitesPerTeamReached" | "InviteNotFound" | "ActionForbiddenForPersonalTeam" | "InviteDoesNotExist" | "InvalidPaginationCursor" | "InvalidVerificationCode" | "InvalidDomainName" | "DomainAlreadyVerified" | "DomainVerificationFailure" | "DomainNotFound" | "DomainVerificationNotStarted" | "WebAuthnError" | "PasswordNotSet" | "UserDoesNotHavePasskey" | "PasskeyAlreadyExists" | "InvalidPasskeyCredential" | "PasskeyDoesNotExist" | "FailedToCreateTeam" | "DashboardImportFail" | "OriginHeaderRequired";
2 changes: 1 addition & 1 deletion sdk/bindings/InitializeRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import type { AppMetadata } from "./AppMetadata";
import type { Network } from "./Network";
import type { Version } from "./Version";

export interface InitializeRequest { responseId: string, appMetadata: AppMetadata, network: Network, version: Version, persistent: boolean, persistentSessionId?: string, }
export interface InitializeRequest { responseId: string, appMetadata: AppMetadata, network: Network, version: Version, persistent: boolean, persistentSessionId?: string, appId?: string, }
2 changes: 1 addition & 1 deletion sdk/bindings/InitializeResponse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface InitializeResponse { responseId: string, sessionId: string, createdNew: boolean, publicKeys: Array<string>, metadata?: string, }
export interface InitializeResponse { responseId: string, sessionId: string, createdNew: boolean, publicKeys: Array<string>, metadata?: string, appId: string, }
1 change: 1 addition & 0 deletions sdk/packages/base/src/initializeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface AppBaseInitialize {
timeout?: number
persistentSessionId?: string
persistent?: boolean
appId?: string
}

export interface ClientBaseInitialize {
Expand Down
2 changes: 1 addition & 1 deletion server/bindings/CloudApiErrors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam" | "InvalidName" | "UnauthorizedOriginError" | "AppDoesNotExist" | "UserAlreadyInvitedToTheTeam" | "MaximumInvitesPerTeamReached" | "InviteNotFound" | "ActionForbiddenForPersonalTeam" | "InviteDoesNotExist" | "InvalidPaginationCursor" | "InvalidVerificationCode" | "InvalidDomainName" | "DomainAlreadyVerified" | "DomainVerificationFailure" | "DomainNotFound" | "DomainVerificationNotStarted" | "WebAuthnError" | "PasswordNotSet" | "UserDoesNotHavePasskey" | "PasskeyAlreadyExists" | "InvalidPasskeyCredential" | "PasskeyDoesNotExist" | "FailedToCreateTeam" | "DashboardImportFail";
export type CloudApiErrors = "TeamDoesNotExist" | "UserDoesNotExist" | "CloudFeatureDisabled" | "InsufficientPermissions" | "TeamHasNoRegisteredApps" | "DatabaseError" | "MaximumUsersPerTeamReached" | "UserAlreadyBelongsToTheTeam" | "IncorrectPassword" | "AccessTokenFailure" | "RefreshTokenFailure" | "AppAlreadyExists" | "MaximumAppsPerTeamReached" | "TeamAlreadyExists" | "PersonalTeamAlreadyExists" | "EmailAlreadyExists" | "InternalServerError" | "UserDoesNotBelongsToTheTeam" | "InvalidName" | "UnauthorizedOriginError" | "AppDoesNotExist" | "UserAlreadyInvitedToTheTeam" | "MaximumInvitesPerTeamReached" | "InviteNotFound" | "ActionForbiddenForPersonalTeam" | "InviteDoesNotExist" | "InvalidPaginationCursor" | "InvalidVerificationCode" | "InvalidDomainName" | "DomainAlreadyVerified" | "DomainVerificationFailure" | "DomainNotFound" | "DomainVerificationNotStarted" | "WebAuthnError" | "PasswordNotSet" | "UserDoesNotHavePasskey" | "PasskeyAlreadyExists" | "InvalidPasskeyCredential" | "PasskeyDoesNotExist" | "FailedToCreateTeam" | "DashboardImportFail" | "OriginHeaderRequired";
2 changes: 1 addition & 1 deletion server/bindings/InitializeRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import type { AppMetadata } from "./AppMetadata";
import type { Network } from "./Network";
import type { Version } from "./Version";

export interface InitializeRequest { responseId: string, appMetadata: AppMetadata, network: Network, version: Version, persistent: boolean, persistentSessionId?: string, }
export interface InitializeRequest { responseId: string, appMetadata: AppMetadata, network: Network, version: Version, persistent: boolean, persistentSessionId?: string, appId?: string, }
2 changes: 1 addition & 1 deletion server/bindings/InitializeResponse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface InitializeResponse { responseId: string, sessionId: string, createdNew: boolean, publicKeys: Array<string>, metadata?: string, }
export interface InitializeResponse { responseId: string, sessionId: string, createdNew: boolean, publicKeys: Array<string>, metadata?: string, appId: string, }
6 changes: 6 additions & 0 deletions server/src/http/cloud/events/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ pub async fn events(
Origin(origin): Origin,
Json(request): Json<HttpNightlyConnectCloudEvent>,
) -> Result<Json<()>, (StatusCode, String)> {
// Check if origin was provided
let origin = origin.ok_or((
StatusCode::BAD_REQUEST,
CloudApiErrors::OriginHeaderRequired.to_string(),
))?;

// Check if origin and app_id match in the database
match db.get_registered_app_by_app_id(&request.app_id).await {
Ok(Some(app)) => {
Expand Down
56 changes: 35 additions & 21 deletions server/src/middlewares/origin_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ use axum::{
extract::FromRequestParts,
http::{header::ORIGIN, request::Parts, StatusCode},
};
use log::warn;

#[derive(Debug, Clone)]
pub struct Origin(pub String);
pub struct Origin(pub Option<String>);

#[async_trait]
impl<B> FromRequestParts<B> for Origin
Expand All @@ -15,19 +16,27 @@ where
type Rejection = (StatusCode, String);

async fn from_request_parts(req: &mut Parts, _state: &B) -> Result<Self, Self::Rejection> {
let origin = req
.headers
.get(ORIGIN)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_owned())
.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
"Origin header is required".to_string(),
)
})?;
match req.headers.get(ORIGIN) {
Some(value) =>
// If anything goes wrong, return empty origin
{
match value.to_str() {
Ok(origin) => {
if origin.is_empty() {
warn!("Empty Origin header");
return Ok(Origin(None));
}

Ok(Origin(origin))
Ok(Origin(Some(origin.to_owned())))
}
Err(e) => {
warn!("Failed to parse Origin header {:?}", e);
Ok(Origin(None))
}
}
}
None => Ok(Origin(None)),
}
}
}

Expand All @@ -43,7 +52,9 @@ mod tests {
};
use tower::ServiceExt;

async fn origin_as_body(Origin(origin): Origin) -> Result<Json<String>, (StatusCode, String)> {
async fn origin_as_body(
Origin(origin): Origin,
) -> Result<Json<Option<String>>, (StatusCode, String)> {
Ok(Json(origin))
}

Expand All @@ -62,21 +73,24 @@ mod tests {
.unwrap();

let response = app.oneshot(request).await.unwrap();
let resp = convert_response::<String>(response).await.unwrap();
let resp = convert_response::<Option<String>>(response).await.unwrap();

assert_eq!(resp, "https://www.example.com");
assert_eq!(resp, Some("https://www.example.com".to_string()));
}

#[tokio::test]
async fn missing_origin_header() {
async fn origin_header_empty() {
let app = app();

let request = Request::builder().uri("/").body(Body::empty()).unwrap();
let request = Request::builder()
.uri("/")
.header("Origin", "")
.body(Body::empty())
.unwrap();

let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let resp = convert_response::<Option<String>>(response).await.unwrap();

let err = convert_response::<String>(response).await.unwrap_err();
assert_eq!(err.to_string(), "Origin header is required");
assert_eq!(resp, None);
}
}
3 changes: 3 additions & 0 deletions server/src/structs/app_messages/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub struct InitializeRequest {
pub persistent: bool,
#[ts(optional)]
pub persistent_session_id: Option<String>,
#[ts(optional)]
pub app_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
Expand All @@ -26,4 +28,5 @@ pub struct InitializeResponse {
pub public_keys: Vec<String>, // if session was restored, this is the list of public keys that were restored
#[ts(optional)]
pub metadata: Option<String>, // if session was restored, this is the metadata that was restored
pub app_id: String,
}
1 change: 1 addition & 0 deletions server/src/structs/cloud/api_cloud_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ pub enum CloudApiErrors {
PasskeyDoesNotExist,
FailedToCreateTeam,
DashboardImportFail,
OriginHeaderRequired,
}
165 changes: 152 additions & 13 deletions server/src/ws/app_handler/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ use super::methods::{
disconnect_session::disconnect_session, initialize_session::initialize_session_connection,
};
use crate::{
cloud_state::CloudState,
env::ONLY_RELAY_SERVICE,
middlewares::origin_middleware::Origin,
state::{
ClientSockets, ClientToSessions, SendToClient, SessionToApp, SessionToAppMap, Sessions,
},
structs::{
app_messages::app_messages::AppToServer,
app_messages::{app_messages::AppToServer, initialize::InitializeRequest},
client_messages::{client_messages::ServerToClient, new_payload_event::NewPayloadEvent},
common::PendingRequest,
notification_msg::{trigger_notification, NotificationPayload},
Expand All @@ -21,11 +24,13 @@ use axum::{
};
use database::structs::device_metadata::Device;
use futures::StreamExt;
use log::{debug, warn};
use std::net::SocketAddr;
use log::{debug, error, info, warn};
use std::{net::SocketAddr, sync::Arc};

pub async fn on_new_app_connection(
ConnectInfo(ip): ConnectInfo<SocketAddr>,
State(cloud): State<Option<Arc<CloudState>>>,
Origin(origin): Origin,
State(sessions): State<Sessions>,
State(client_sockets): State<ClientSockets>,
State(client_to_sessions): State<ClientToSessions>,
Expand All @@ -41,6 +46,8 @@ pub async fn on_new_app_connection(
client_sockets,
client_to_sessions,
session_to_app_map,
cloud,
origin,
)
.await;
debug!("CLOSE app connection from {}", ip);
Expand All @@ -53,6 +60,8 @@ pub async fn app_handler(
client_sockets: ClientSockets,
client_to_sessions: ClientToSessions,
session_to_app_map: SessionToAppMap,
cloud: Option<Arc<CloudState>>,
origin: Option<String>,
) {
let (sender, mut receiver) = socket.split();
let connection_id = uuid7::uuid7();
Expand Down Expand Up @@ -83,16 +92,21 @@ pub async fn app_handler(
// We only accept initialize messages here
match app_msg {
AppToServer::InitializeRequest(init_data) => {
// TEMP FIX
let app_id = match &init_data.persistent_session_id {
Some(session_id) => session_to_app_map
.get_app_id(&session_id)
.await
.unwrap_or_else(|| {
warn!("No app_id found for session: {}", session_id);
uuid7::uuid7().to_string()
}),
None => uuid7::uuid7().to_string(),
// If cloud is enabled, we will try to get app_id from the verified origin
let app_id = match get_app_id(
ONLY_RELAY_SERVICE(),
&cloud,
&origin,
&init_data,
&session_to_app_map,
)
.await
{
Ok(app_id) => app_id,
Err(_err) => {
// TODO explicit reject of the connection
return;
}
};

let session_id = initialize_session_connection(
Expand Down Expand Up @@ -242,3 +256,128 @@ pub async fn app_handler(
}
}
}

async fn get_app_id(
cloud_disabled: bool,
cloud_state: &Option<Arc<CloudState>>,
origin: &Option<String>,
init_data: &InitializeRequest,
session_to_app_map: &SessionToAppMap,
) -> Result<String, String> {
// By default cloud is disabled, normal relay flow
if cloud_disabled {
return Ok(
fetch_app_id_or_generate(&init_data.persistent_session_id, &session_to_app_map).await,
);
}

// If cloud is enabled, we will try to get app_id from the verified origin if it was provided
if let Some(cloud) = cloud_state {
match origin {
Some(origin) => {
// Origin was provided, check if it is verified
match cloud
.db
.get_domain_verification_by_domain_name(origin)
.await
{
// Domain verification has been found
Ok(Some(domain_verification)) => {
// Check if domain is verified
if domain_verification.finished_at.is_some() {
// Check if app_id has been provided in initial data
match &init_data.app_id {
Some(app_id) => {
// App id has been provided, check if it matches the verified app_id
if app_id == &domain_verification.app_id {
return Ok(app_id.clone());
} else {
info!(
"App id mismatch: {} != {}",
app_id, domain_verification.app_id
);
return Err("App id mismatch".to_string());
}
}
// App id has not been provided, return the verified app_id
None => {
return Ok(domain_verification.app_id);
}
}
} else {
info!("Domain verification has not been finished: {}", origin);
return Err("Domain verification has not been finished".to_string());
}
}
// Unverified domain, normal relay flow
Ok(None) | Err(_) => {
info!(
"Origin verification failed or error encountered: {}",
origin
);
return Ok(fetch_app_id_or_generate(
&init_data.persistent_session_id,
&session_to_app_map,
)
.await);
}
}
}
None => {
// If origin is missing, check if app id has been provided
match &init_data.app_id {
Some(app_id) => {
// Check if provided app_id has already been registered
match cloud.db.get_registered_app_by_app_id(app_id).await {
Ok(Some(_)) => {
// app id is already used by a registered app, reject the connection by the origin
info!("App id already registered, origin not provided: {}", app_id);
return Err(
"App id already registered, origin not provided".to_string()
);
}
// App id is not registered, or something has happened with the db, normal relay flow
Ok(None) | Err(_) => {
return Ok(fetch_app_id_or_generate(
&init_data.persistent_session_id,
&session_to_app_map,
)
.await);
}
}
}
None =>
// If app_id is not provided, normal relay flow
{
return Ok(fetch_app_id_or_generate(
&init_data.persistent_session_id,
&session_to_app_map,
)
.await);
}
}
}
}
} else {
error!("Cloud feature is enabled but cloud state is not initialized");
return Ok(
fetch_app_id_or_generate(&init_data.persistent_session_id, &session_to_app_map).await,
);
}
}

async fn fetch_app_id_or_generate(
session_id_option: &Option<String>,
session_to_app_map: &SessionToAppMap,
) -> String {
match session_id_option {
Some(session_id) => session_to_app_map
.get_app_id(session_id)
.await
.unwrap_or_else(|| {
warn!("No app_id found for session: {}", session_id);
uuid7::uuid7().to_string()
}),
None => uuid7::uuid7().to_string(),
}
}
Loading
Loading