2025-03-14 20:00:50 +07:00

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()
}