355 lines
12 KiB
Rust
355 lines
12 KiB
Rust
use crate::hyperware::process::contacts;
|
|
use anyhow;
|
|
use hyperware_process_lib::http::client::send_request_await_response;
|
|
use hyperware_process_lib::http::server::{send_response, HttpBindingConfig, WsBindingConfig};
|
|
use hyperware_process_lib::http::{Method, StatusCode};
|
|
use hyperware_process_lib::logging::{info, init_logging, Level};
|
|
use hyperware_process_lib::{
|
|
await_message, call_init, eth, get_blob, get_typed_state, homepage, http, hypermap, kiprintln,
|
|
set_state, Address, Capability, LazyLoadBlob, Message, Request,
|
|
};
|
|
use process_macros::SerdeJsonInto;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use url::Url;
|
|
mod proxy;
|
|
|
|
// DEVS: replace with your Web2 site URL, login endpoint and login nonce
|
|
// TODO if testing with memedeck etc. disable the auth check below, see the other TODO comment
|
|
// const WEB2_URL: &str = "https://www.memedeck.xyz";
|
|
const WEB2_URL: &str = "http://localhost:3000";
|
|
const WEB2_LOGIN_ENDPOINT: &str = "http://localhost:3000/api/hypr-login";
|
|
const WEB2_LOGIN_NONCE: &str = "lorem ipsum";
|
|
|
|
wit_bindgen::generate!({
|
|
path: "target/wit",
|
|
world: "contacts-sys-v0",
|
|
generate_unused_types: true,
|
|
additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],
|
|
});
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
enum FrontendRequest {
|
|
Sign,
|
|
CheckAuth,
|
|
Logout,
|
|
Debug(String),
|
|
}
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
enum LoginRequest {
|
|
Sign(SignRequest),
|
|
Verify { from: Address, data: SignResponse },
|
|
}
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct SignRequest {
|
|
pub site: String,
|
|
pub time: u64,
|
|
pub nonce: Option<String>,
|
|
}
|
|
#[derive(Debug, Serialize, Deserialize, SerdeJsonInto)]
|
|
struct SignResponse {
|
|
pub body: SignRequest,
|
|
pub message: Vec<u8>,
|
|
pub signature: Vec<u8>,
|
|
}
|
|
const ICON: &str = include_str!("icon");
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct ProxyStateV1 {
|
|
// TODO this should probably be generic and go on login:sys:sys
|
|
pub auth: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(tag = "version")]
|
|
enum VersionedState {
|
|
/// State fully stored in memory, persisted using serde_json.
|
|
/// Future state version will use SQLite.
|
|
V1(ProxyStateV1),
|
|
}
|
|
|
|
impl VersionedState {
|
|
fn load() -> Self {
|
|
get_typed_state(|bytes| serde_json::from_slice(bytes))
|
|
.unwrap_or(Self::V1(ProxyStateV1 { auth: None }))
|
|
}
|
|
fn _save(&self) {
|
|
set_state(&serde_json::to_vec(&self).expect("Failed to serialize state!"));
|
|
}
|
|
fn save_auth(&self, auth: String) {
|
|
let ns = Self::V1(ProxyStateV1 { auth: Some(auth) });
|
|
set_state(&serde_json::to_vec(&ns).expect("Failed to serialize state!"));
|
|
}
|
|
fn wipe_auth(&self) {
|
|
let ns = Self::V1(ProxyStateV1 { auth: None });
|
|
set_state(&serde_json::to_vec(&ns).expect("Failed to serialize state!"));
|
|
}
|
|
fn get_auth(&self) -> Option<String> {
|
|
match self {
|
|
Self::V1(ps) => ps.auth.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
call_init!(initialize);
|
|
fn initialize(our: Address) {
|
|
init_logging(Level::DEBUG, Level::INFO, None, None, None).unwrap();
|
|
info!("begin");
|
|
|
|
homepage::add_to_homepage("Login", Some(ICON), Some("/"), None);
|
|
|
|
let mut state = VersionedState::load();
|
|
let auth = state.get_auth();
|
|
kiprintln!("auth {:#?}", auth);
|
|
|
|
let mut http_server = http::server::HttpServer::new(5);
|
|
let http_config = HttpBindingConfig::default().secure_subdomain(false);
|
|
|
|
http_server
|
|
.serve_ui("ui", vec!["/hypr-login"], http_config.clone())
|
|
.expect("Failed to serve UI");
|
|
http_server.bind_http_path("/", http_config).unwrap();
|
|
http_server
|
|
.bind_ws_path("/", WsBindingConfig::default())
|
|
.unwrap();
|
|
// let http_config = HttpBindingConfig::default().secure_subdomain(true);
|
|
|
|
// http_server
|
|
// .serve_ui("ui", vec!["/hypr-login"], http_config.clone())
|
|
// .expect("Failed to serve UI");
|
|
// http_server.secure_bind_http_path("/").unwrap();
|
|
|
|
main_loop(&our, &mut state, &mut http_server);
|
|
}
|
|
|
|
fn main_loop(
|
|
our: &Address,
|
|
state: &mut VersionedState,
|
|
http_server: &mut http::server::HttpServer,
|
|
) {
|
|
loop {
|
|
match await_message() {
|
|
Err(_send_error) => {
|
|
// ignore send errors, local-only process
|
|
continue;
|
|
}
|
|
Ok(Message::Request {
|
|
source,
|
|
body,
|
|
capabilities,
|
|
..
|
|
}) => {
|
|
// ignore messages from other nodes -- technically superfluous check
|
|
// since manifest does not acquire networking capability
|
|
if source.node() != our.node {
|
|
continue;
|
|
}
|
|
let _ = handle_request(our, &source, &body, capabilities, state, http_server);
|
|
}
|
|
_ => continue, // ignore responses
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_request(
|
|
our: &Address,
|
|
source: &Address,
|
|
body: &[u8],
|
|
_capabilities: Vec<Capability>,
|
|
state: &mut VersionedState,
|
|
http_server: &mut http::server::HttpServer,
|
|
) -> anyhow::Result<()> {
|
|
// source node is ALWAYS ourselves since networking is disabled
|
|
if source.process == "http-server:distro:sys" {
|
|
// receive HTTP requests and websocket connection messages from our server
|
|
let server_request = http_server.parse_request(body).unwrap();
|
|
match server_request {
|
|
http::server::HttpServerRequest::Http(request) => {
|
|
handle_http_request(our, state, &request)?;
|
|
}
|
|
// TODO handle websockets
|
|
_ => (),
|
|
};
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle HTTP requests from our own frontend.
|
|
fn handle_http_request(
|
|
our: &Address,
|
|
state: &mut VersionedState,
|
|
http_request: &http::server::IncomingHttpRequest,
|
|
) -> anyhow::Result<()> {
|
|
let method = http_request.method().unwrap();
|
|
let url = http_request.url().unwrap();
|
|
let paths = url.path_segments().unwrap();
|
|
let last = paths.last().unwrap();
|
|
if last == "hypr-login" {
|
|
handle_login_request(our, state, method)?;
|
|
} else {
|
|
handle_page_request(our, state, http_request)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_login_request(
|
|
our: &Address,
|
|
state: &mut VersionedState,
|
|
method: Method,
|
|
) -> anyhow::Result<()> {
|
|
match method {
|
|
Method::POST => {
|
|
let blob = get_blob().unwrap();
|
|
let request_bytes = blob.bytes();
|
|
let request = serde_json::from_slice::<FrontendRequest>(request_bytes)?;
|
|
match request {
|
|
FrontendRequest::Sign => {
|
|
let target = Address::new(our.node(), ("login", "login", "sys"));
|
|
let lr = LoginRequest::Sign(SignRequest {
|
|
site: WEB2_URL.to_string(),
|
|
nonce: Some(WEB2_LOGIN_NONCE.to_string()),
|
|
time: get_now(),
|
|
});
|
|
// Get the signature from login:sys:sys
|
|
let res: SignResponse = Request::to(target)
|
|
.body(serde_json::to_vec(&lr)?)
|
|
.send_and_await_response(10)??
|
|
.body()
|
|
.try_into()?;
|
|
// Send signature to designated endpoint on Web2 app
|
|
attempt_login(our, state, res)?;
|
|
}
|
|
FrontendRequest::CheckAuth => {
|
|
let auth = state.get_auth();
|
|
let json = match auth {
|
|
None => serde_json::Value::Null,
|
|
Some(s) => json!(s),
|
|
};
|
|
send_response(StatusCode::OK, None, serde_json::to_vec(&json)?)
|
|
}
|
|
FrontendRequest::Logout => {
|
|
state.wipe_auth();
|
|
let json = serde_json::to_vec(&json!(true))?;
|
|
send_response(StatusCode::OK, None, json);
|
|
}
|
|
FrontendRequest::Debug(s) => {
|
|
let value = serde_json::from_str::<serde_json::Value>(&s)?;
|
|
kiprintln!("frontend log\n{:#?}\n", value);
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
send_response(StatusCode::METHOD_NOT_ALLOWED, None, vec![]);
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
fn attempt_login(
|
|
our: &Address,
|
|
state: &mut VersionedState,
|
|
signature_response: SignResponse,
|
|
) -> anyhow::Result<()> {
|
|
let mut json_headers = HashMap::new();
|
|
json_headers.insert("Content-type".to_string(), "application/json".to_string());
|
|
let node = our.node();
|
|
let message = signature_response.message;
|
|
let signature = signature_response.signature;
|
|
let json =
|
|
serde_json::to_vec(&json!({"node":node, "message": message, "signature": signature}))?;
|
|
let url = Url::parse(WEB2_LOGIN_ENDPOINT)?;
|
|
let res = send_request_await_response(Method::POST, url, Some(json_headers), 5000, json)?;
|
|
let resbody = res.body();
|
|
let resjson = serde_json::from_slice::<serde_json::Value>(resbody)?;
|
|
let okres = resjson.get("ok");
|
|
match okres {
|
|
None => {
|
|
send_json_response(
|
|
StatusCode::OK,
|
|
&json!({"error": "Signature verification failed"}),
|
|
)?;
|
|
}
|
|
Some(_) => {
|
|
let auth_header = res.headers().get("x-hyperware-auth");
|
|
match auth_header {
|
|
None => {
|
|
send_json_response(
|
|
StatusCode::OK,
|
|
&json!({"error": "No auth string found in response"}),
|
|
)?;
|
|
}
|
|
Some(av) => {
|
|
let auth_string = av.to_str()?;
|
|
state.save_auth(auth_string.to_string());
|
|
send_json_response(StatusCode::OK, &json!({"ok": "go ahead"}))?;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
fn handle_page_request(
|
|
our: &Address,
|
|
state: &mut VersionedState,
|
|
http_request: &http::server::IncomingHttpRequest,
|
|
) -> anyhow::Result<()> {
|
|
// TODO remove before release
|
|
let auth = state.get_auth();
|
|
// let auth = Some(String::new());
|
|
match auth {
|
|
None => {
|
|
let package = our.package_id();
|
|
let process = our.process();
|
|
let redirect_to = format!("/{}:{}/hypr-login", process, package);
|
|
let mut redirect_headers = HashMap::new();
|
|
redirect_headers.insert("Location".to_string(), redirect_to);
|
|
send_response(
|
|
StatusCode::TEMPORARY_REDIRECT,
|
|
Some(redirect_headers),
|
|
vec![],
|
|
);
|
|
Ok(())
|
|
}
|
|
Some(auth_token) => proxy::run_proxy(http_request, WEB2_URL, &auth_token),
|
|
}
|
|
}
|
|
|
|
fn _invalid_node(
|
|
hypermap: &hypermap::Hypermap,
|
|
node: &str,
|
|
) -> Option<(contacts::Response, Option<LazyLoadBlob>)> {
|
|
if hypermap
|
|
.get(&node)
|
|
.map(|(tba, _, _)| tba != eth::Address::ZERO)
|
|
.unwrap_or(false)
|
|
{
|
|
None
|
|
} else {
|
|
Some((
|
|
contacts::Response::Err("Node name invalid or does not exist".to_string()),
|
|
None,
|
|
))
|
|
}
|
|
}
|
|
|
|
fn send_json_response<T: serde::Serialize>(status: StatusCode, data: &T) -> anyhow::Result<()> {
|
|
let json_data = serde_json::to_vec(data)?;
|
|
send_response(
|
|
status,
|
|
Some(HashMap::from([(
|
|
String::from("Content-Type"),
|
|
String::from("application/json"),
|
|
)])),
|
|
json_data,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn get_now() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
}
|