Skip to content

Commit

Permalink
Split various web handlers out into their own source units, as web_ho…
Browse files Browse the repository at this point in the history
…st.rs was becoming unreadable.
  • Loading branch information
rdaum committed Oct 23, 2024
1 parent f222fc4 commit 0a37748
Show file tree
Hide file tree
Showing 6 changed files with 621 additions and 533 deletions.
2 changes: 1 addition & 1 deletion bacon.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ allow_warnings = true
need_stdout = true

[jobs.web]
command = ["cargo", "run", "-p", "moor-web-host"]
command = ["cargo", "run", "-p", "moor-web-host", "--", "--listen-address", "0.0.0.0:8080"]
allow_warnings = true

[jobs.test]
Expand Down
153 changes: 153 additions & 0 deletions crates/web-host/src/host/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (C) 2024 Ryan Daum <[email protected]>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, version 3.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//

use crate::host::web_host::{LoginType, WsHostError};
use crate::host::WebHost;
use axum::extract::{ConnectInfo, State};
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Form;
use rpc_async_client::rpc_client::RpcSendClient;
use rpc_common::{AuthToken, ClientToken, RpcRequest, RpcResponse, RpcResult};
use serde_derive::Deserialize;
use std::net::SocketAddr;
use tracing::{debug, error, warn};
use uuid::Uuid;

#[derive(Deserialize)]
pub struct AuthRequest {
player: String,
password: String,
}

pub async fn connect_auth_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(ws_host): State<WebHost>,
Form(AuthRequest { player, password }): Form<AuthRequest>,
) -> impl IntoResponse {
auth_handler(LoginType::Connect, addr, ws_host, player, password).await
}

pub async fn create_auth_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(ws_host): State<WebHost>,
Form(AuthRequest { player, password }): Form<AuthRequest>,
) -> impl IntoResponse {
auth_handler(LoginType::Create, addr, ws_host, player, password).await
}

/// Stand-alone HTTP POST authentication handler which connects and then gets a valid authentication token
/// which can then be used in the headers/query-string for subsequent websocket request.
async fn auth_handler(
login_type: LoginType,
addr: SocketAddr,
host: WebHost,
player: String,
password: String,
) -> impl IntoResponse {
debug!("Authenticating player: {}", player);
let (client_id, mut rpc_client, client_token) =
match host.establish_client_connection(addr).await {
Ok((client_id, rpc_client, client_token)) => (client_id, rpc_client, client_token),
Err(WsHostError::AuthenticationFailed) => {
warn!("Authentication failed for {}", player);
return Response::builder()
.status(StatusCode::FORBIDDEN)
.body("".to_string())
.unwrap();
}
Err(e) => {
error!("Unable to establish connection: {}", e);
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body("".to_string())
.unwrap();
}
};

let auth_verb = match login_type {
LoginType::Connect => "connect",
LoginType::Create => "create",
};

let words = vec![auth_verb.to_string(), player, password];
let response = rpc_client
.make_rpc_call(
client_id,
RpcRequest::LoginCommand(client_token.clone(), words, false),
)
.await
.expect("Unable to send login request to RPC server");
let RpcResult::Success(RpcResponse::LoginResult(Some((auth_token, _connect_type, player)))) =
response
else {
error!(?response, "Login failed");

return Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body("".to_string())
.unwrap();
};

// We now have a valid auth token for the player, so we return it in the response headers.
let mut headers = HeaderMap::new();
headers.insert(
"X-Moor-Auth-Token",
HeaderValue::from_str(&auth_token.0).expect("Invalid token"),
);

// We now need to wait for the login message completion.

// We're done with this RPC connection, so we detach it.
let _ = rpc_client
.make_rpc_call(client_id, RpcRequest::Detach(client_token.clone()))
.await
.expect("Unable to send detach to RPC server");

Response::builder()
.status(StatusCode::OK)
.header("X-Moor-Auth-Token", auth_token.0)
.body(format!("{} {}", player, auth_verb))
.unwrap()
}

pub async fn auth_auth(
host: WebHost,
addr: SocketAddr,
header_map: HeaderMap,
) -> Result<(AuthToken, Uuid, ClientToken, RpcSendClient), StatusCode> {
let auth_token = match header_map.get("X-Moor-Auth-Token") {
Some(auth_token) => match auth_token.to_str() {
Ok(auth_token) => AuthToken(auth_token.to_string()),
Err(e) => {
error!("Unable to parse auth token: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
},
None => {
error!("No auth token provided");
return Err(StatusCode::FORBIDDEN);
}
};

let (_player, client_id, client_token, rpc_client) = host
.attach_authenticated(auth_token.clone(), None, addr)
.await
.map_err(|e| match e {
WsHostError::AuthenticationFailed => StatusCode::UNAUTHORIZED,
_ => StatusCode::INTERNAL_SERVER_ERROR,
})?;

Ok((auth_token, client_id, client_token, rpc_client))
}
14 changes: 11 additions & 3 deletions crates/web-host/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,26 @@
// this program. If not, see <https://www.gnu.org/licenses/>.
//

mod auth;
mod props;
mod verbs;
pub mod web_host;
mod ws_connection;

pub use auth::connect_auth_handler;
pub use auth::create_auth_handler;
use moor_values::{v_err, v_float, v_int, v_list, v_map, v_none, v_obj, v_str, Var, Variant};
pub use props::properties_handler;
pub use props::property_retrieval_handler;
use serde::Serialize;
use serde_derive::Deserialize;
use serde_json::{json, Number};
pub use verbs::verb_program_handler;
pub use verbs::verb_retrieval_handler;
pub use verbs::verbs_handler;
pub use web_host::WebHost;
pub use web_host::{
connect_auth_handler, create_auth_handler, eval_handler, properties_handler,
property_retrieval_handler, verb_program_handler, verb_retrieval_handler, verbs_handler,
welcome_message_handler, ws_connect_attach_handler, ws_create_attach_handler,
eval_handler, welcome_message_handler, ws_connect_attach_handler, ws_create_attach_handler,
};

#[derive(serde_derive::Serialize, Deserialize)]
Expand Down
153 changes: 153 additions & 0 deletions crates/web-host/src/host/props.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (C) 2024 Ryan Daum <[email protected]>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, version 3.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//

use crate::host::{auth, var_as_json, web_host, WebHost};
use axum::extract::{ConnectInfo, Path, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use moor_values::model::ObjectRef;
use moor_values::Symbol;
use rpc_common::{EntityType, PropInfo, RpcRequest, RpcResponse};
use serde_json::json;
use std::net::SocketAddr;
use tracing::{debug, error};

pub async fn properties_handler(
State(host): State<WebHost>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
header_map: HeaderMap,
Path(object): Path<String>,
) -> Response {
let (auth_token, client_id, client_token, mut rpc_client) =
match auth::auth_auth(host.clone(), addr, header_map.clone()).await {
Ok(connection_details) => connection_details,
Err(status) => return status.into_response(),
};

let Some(object) = ObjectRef::parse_curie(&object) else {
return StatusCode::BAD_REQUEST.into_response();
};

let response = match web_host::rpc_call(
client_id,
&mut rpc_client,
RpcRequest::Properties(client_token.clone(), auth_token.clone(), object),
)
.await
{
Ok(RpcResponse::Properties(properties)) => Json(
properties
.iter()
.map(|prop| {
json!({
"definer": prop.definer.0,
"location": prop.location.0,
"name": prop.name.to_string(),
"owner": prop.owner.0,
"r": prop.r,
"w": prop.w,
"chown": prop.chown,
})
})
.collect::<Vec<serde_json::Value>>(),
)
.into_response(),
Ok(r) => {
error!("Unexpected response from RPC server: {:?}", r);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
Err(status) => status.into_response(),
};

// We're done with this RPC connection, so we detach it.
let _ = rpc_client
.make_rpc_call(client_id, RpcRequest::Detach(client_token.clone()))
.await
.expect("Unable to send detach to RPC server");

response
}

pub async fn property_retrieval_handler(
State(host): State<WebHost>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
header_map: HeaderMap,
Path((object, prop_name)): Path<(String, String)>,
) -> Response {
let (auth_token, client_id, client_token, mut rpc_client) =
match auth::auth_auth(host.clone(), addr, header_map.clone()).await {
Ok(connection_details) => connection_details,
Err(status) => return status.into_response(),
};

let Some(object) = ObjectRef::parse_curie(&object) else {
return StatusCode::BAD_REQUEST.into_response();
};

let prop_name = Symbol::mk(&prop_name);

let response = match web_host::rpc_call(
client_id,
&mut rpc_client,
RpcRequest::Retrieve(
client_token.clone(),
auth_token.clone(),
object,
EntityType::Property,
prop_name,
),
)
.await
{
Ok(RpcResponse::PropertyValue(
PropInfo {
definer,
location,
name,
owner,
r,
w,
chown,
},
value,
)) => {
debug!("Property value: {:?}", value);
Json(json!({
"definer": definer.0,
"name": name.to_string(),
"location": location.0,
"owner": owner.0,
"r": r,
"w": w,
"chown": chown,
"value": var_as_json(&value)
}))
.into_response()
}
Ok(r) => {
error!("Unexpected response from RPC server: {:?}", r);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
Err(status) => status.into_response(),
};

// We're done with this RPC connection, so we detach it.
let _ = rpc_client
.make_rpc_call(client_id, RpcRequest::Detach(client_token.clone()))
.await
.expect("Unable to send detach to RPC server");

response
}
Loading

0 comments on commit 0a37748

Please sign in to comment.