mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-24 09:06:10 +03:00
Merge pull request #89 from uqbar-dao/dr/secure-subdomains-for-apps
Secure subdomains for apps
This commit is contained in:
commit
5903a5a364
5
modules/chess/Cargo.lock
generated
5
modules/chess/Cargo.lock
generated
@ -64,7 +64,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chess"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -73,6 +73,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uqbar_process_lib",
|
||||
"url",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
@ -631,7 +632,7 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
[[package]]
|
||||
name = "uqbar_process_lib"
|
||||
version = "0.3.0"
|
||||
source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=5e1b94a#5e1b94ae2f85c66da33ec52117a72b90d53c4d22"
|
||||
source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=d451af9#d451af9c2a273c004c0451f88b7f67387ba76416"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
@ -1,10 +1,8 @@
|
||||
[package]
|
||||
name = "chess"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
opt-level = "s"
|
||||
@ -15,9 +13,10 @@ anyhow = "1.0"
|
||||
base64 = "0.13"
|
||||
bincode = "1.3.3"
|
||||
pleco = "0.5"
|
||||
serde = {version = "1.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "5e1b94a" }
|
||||
url = "*"
|
||||
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "d451af9" }
|
||||
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" }
|
||||
|
||||
[lib]
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -5,8 +5,11 @@
|
||||
"on_panic": "Restart",
|
||||
"request_networking": true,
|
||||
"request_messaging": [
|
||||
"net:sys:uqbar"
|
||||
],
|
||||
"grant_messaging": [
|
||||
"http_server:sys:uqbar"
|
||||
],
|
||||
"public": false
|
||||
"public": true
|
||||
}
|
||||
]
|
||||
|
@ -1,13 +1,13 @@
|
||||
#![feature(let_chains)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
extern crate base64;
|
||||
extern crate pleco;
|
||||
use pleco::Board;
|
||||
use uqbar_process_lib::uqbar::process::standard as wit;
|
||||
use uqbar_process_lib::{
|
||||
get_payload, get_typed_state, grant_messaging, http, println, receive, set_state, Address,
|
||||
Message, Payload, ProcessId, Request, Response,
|
||||
get_payload, get_typed_state, http, println, receive, set_state, Address, Message, Payload,
|
||||
Request, Response,
|
||||
};
|
||||
|
||||
mod utils;
|
||||
@ -22,18 +22,23 @@ wit_bindgen::generate!({
|
||||
|
||||
struct Component;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Game {
|
||||
pub id: String, // the node with whom we are playing
|
||||
pub turns: u64,
|
||||
pub board: Board,
|
||||
pub white: String,
|
||||
pub black: String,
|
||||
pub ended: bool,
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum ChessRequest {
|
||||
NewGame { white: String, black: String },
|
||||
Move(String), // can only have one game with a given node at a time
|
||||
Resign,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
enum ChessResponse {
|
||||
NewGameAccepted,
|
||||
NewGameRejected,
|
||||
MoveAccepted,
|
||||
MoveRejected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct StoredGame {
|
||||
struct Game {
|
||||
pub id: String, // the node with whom we are playing
|
||||
pub turns: u64,
|
||||
pub board: String,
|
||||
@ -42,280 +47,280 @@ pub struct StoredGame {
|
||||
pub ended: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChessState {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ChessState {
|
||||
pub games: HashMap<String, Game>, // game is by opposing player id
|
||||
pub records: HashMap<String, (u64, u64, u64)>, // wins, losses, draws
|
||||
pub clients: HashSet<u32>, // doesn't get persisted
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct StoredChessState {
|
||||
pub games: HashMap<String, StoredGame>, // game is by opposing player id
|
||||
pub records: HashMap<String, (u64, u64, u64)>, // wins, losses, draws
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StoredChessState {
|
||||
pub games: HashMap<String, Game>, // game is by opposing player id
|
||||
}
|
||||
|
||||
const CHESS_PAGE: &str = include_str!("../pkg/chess.html");
|
||||
const CHESS_HTML: &str = include_str!("../pkg/chess.html");
|
||||
const CHESS_JS: &str = include_str!("../pkg/index.js");
|
||||
const CHESS_CSS: &str = include_str!("../pkg/index.css");
|
||||
|
||||
impl Guest for Component {
|
||||
fn init(our: String) {
|
||||
let our = Address::from_str(&our).unwrap();
|
||||
println!("{our}: start");
|
||||
|
||||
grant_messaging(
|
||||
&our,
|
||||
vec![ProcessId::new(Some("http_server"), "sys", "uqbar")],
|
||||
println!(
|
||||
"{} by {}: start",
|
||||
our.process.process_name, our.process.publisher_node
|
||||
);
|
||||
|
||||
// serve static page at /
|
||||
// serve static page at /index.html, /index.js, /index.css
|
||||
// dynamically handle requests to /games
|
||||
http::bind_http_static_path(
|
||||
"/",
|
||||
true,
|
||||
false,
|
||||
true, // only serve for ourselves
|
||||
false, // can access remotely
|
||||
Some("text/html".to_string()),
|
||||
CHESS_PAGE
|
||||
CHESS_HTML
|
||||
.replace("${node}", &our.node)
|
||||
.replace("${process}", &our.process.to_string())
|
||||
// TODO serve these independently on paths..
|
||||
// also build utils for just serving a vfs dir
|
||||
.replace("${js}", CHESS_JS)
|
||||
.replace("${css}", CHESS_CSS)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap();
|
||||
http::bind_http_static_path(
|
||||
"/index.js",
|
||||
true,
|
||||
false,
|
||||
Some("text/javascript".to_string()),
|
||||
CHESS_JS.as_bytes().to_vec(),
|
||||
)
|
||||
.unwrap();
|
||||
http::bind_http_static_path(
|
||||
"/index.css",
|
||||
true,
|
||||
false,
|
||||
Some("text/css".to_string()),
|
||||
CHESS_CSS.as_bytes().to_vec(),
|
||||
)
|
||||
.unwrap();
|
||||
http::bind_http_path("/games", true, false).unwrap();
|
||||
|
||||
let mut state: ChessState = match get_typed_state(|bytes| {
|
||||
Ok(bincode::deserialize::<StoredChessState>(bytes)?)
|
||||
}) {
|
||||
Some(mut state) => ChessState {
|
||||
games: state
|
||||
.games
|
||||
.iter_mut()
|
||||
.map(|(id, game)| {
|
||||
(
|
||||
id.clone(),
|
||||
Game {
|
||||
id: id.to_owned(),
|
||||
turns: game.turns,
|
||||
board: Board::from_fen(&game.board).unwrap_or(Board::start_pos()),
|
||||
white: game.white.to_owned(),
|
||||
black: game.black.to_owned(),
|
||||
ended: game.ended,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
records: state.records,
|
||||
},
|
||||
None => ChessState {
|
||||
games: HashMap::new(),
|
||||
records: HashMap::new(),
|
||||
},
|
||||
};
|
||||
// serve same content SECURELY at a subdomain:
|
||||
|
||||
let _res = Request::new()
|
||||
.target(("our", "http_server", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::to_vec(&http::HttpServerAction::SecureBind {
|
||||
path: "/secure/index.html".into(),
|
||||
cache: true,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.payload(Payload {
|
||||
mime: Some("text/html".to_string()),
|
||||
bytes: CHESS_HTML
|
||||
.replace("${node}", &our.node)
|
||||
.replace("${process}", &our.process.to_string())
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
})
|
||||
.send_and_await_response(5)
|
||||
.unwrap();
|
||||
let _res = Request::new()
|
||||
.target(("our", "http_server", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::to_vec(&http::HttpServerAction::SecureBind {
|
||||
path: "/secure/index.css".into(),
|
||||
cache: true,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.payload(Payload {
|
||||
mime: Some("text/css".to_string()),
|
||||
bytes: CHESS_CSS.as_bytes().to_vec(),
|
||||
})
|
||||
.send_and_await_response(5)
|
||||
.unwrap();
|
||||
let _res = Request::new()
|
||||
.target(("our", "http_server", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::to_vec(&http::HttpServerAction::SecureBind {
|
||||
path: "/secure/index.js".into(),
|
||||
cache: true,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.payload(Payload {
|
||||
mime: Some("text/javascript".to_string()),
|
||||
bytes: CHESS_JS.as_bytes().to_vec(),
|
||||
})
|
||||
.send_and_await_response(5)
|
||||
.unwrap();
|
||||
let _res = Request::new()
|
||||
.target(("our", "http_server", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::to_vec(&http::HttpServerAction::SecureBind {
|
||||
path: "/games".into(),
|
||||
cache: false,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.send_and_await_response(5)
|
||||
.unwrap();
|
||||
|
||||
let mut state: ChessState = utils::load_chess_state();
|
||||
main_loop(&our, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
fn main_loop(our: &Address, state: &mut ChessState) {
|
||||
loop {
|
||||
let Ok((source, message)) = receive() else {
|
||||
println!("chess: got network error");
|
||||
println!("{our}: got network error");
|
||||
continue;
|
||||
};
|
||||
// we don't expect any responses *here*, because for every
|
||||
// chess protocol request, we await its response right then and
|
||||
// there. this is appropriate for direct node<>node comms, less
|
||||
// appropriate for other circumstances...
|
||||
let Message::Request(request) = message else {
|
||||
println!("chess: got unexpected Response");
|
||||
// println!("{our}: got unexpected Response from {source}: {message:?}");
|
||||
continue;
|
||||
};
|
||||
match handle_request(&our, &source, &request, &mut state) {
|
||||
match handle_request(&our, &source, &request, state) {
|
||||
Ok(()) => continue,
|
||||
Err(e) => println!("chess: error handling request: {:?}", e),
|
||||
}
|
||||
Err(e) => println!("{our}: error handling request: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// handle all incoming requests
|
||||
fn handle_request(
|
||||
our: &Address,
|
||||
source: &Address,
|
||||
request: &wit::Request,
|
||||
state: &mut ChessState,
|
||||
) -> anyhow::Result<()> {
|
||||
if source.process == "chess:chess:uqbar" {
|
||||
let message_json = serde_json::from_slice::<serde_json::Value>(&request.ipc)?;
|
||||
handle_chess_request(our, source, message_json, state)
|
||||
} else if source.process.to_string() == "http_server:sys:uqbar" {
|
||||
let http_request = serde_json::from_slice::<http::IncomingHttpRequest>(&request.ipc)?;
|
||||
handle_http_request(our, http_request, state)
|
||||
println!("{}: handling request from {}", our.process.process_name, source);
|
||||
if source.process == our.process && source.node != our.node {
|
||||
// receive chess protocol messages from other nodes
|
||||
let chess_request = serde_json::from_slice::<ChessRequest>(&request.ipc)?;
|
||||
handle_chess_request(our, source, state, chess_request)
|
||||
} else if source.process == "http_server:sys:uqbar" && source.node == our.node {
|
||||
// receive HTTP requests and websocket connection messages from our server
|
||||
match serde_json::from_slice::<http::HttpServerRequest>(&request.ipc)? {
|
||||
http::HttpServerRequest::Http(incoming) => {
|
||||
match handle_http_request(our, state, incoming) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => {
|
||||
println!("chess: error handling http request: {:?}", e);
|
||||
http::send_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
None,
|
||||
"Service Unavailable".to_string().as_bytes().to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
http::HttpServerRequest::WebSocketOpen(channel_id) => {
|
||||
// client frontend opened a websocket
|
||||
state.clients.insert(channel_id);
|
||||
Ok(())
|
||||
}
|
||||
http::HttpServerRequest::WebSocketClose(channel_id) => {
|
||||
// client frontend closed a websocket
|
||||
state.clients.remove(&channel_id);
|
||||
Ok(())
|
||||
}
|
||||
http::HttpServerRequest::WebSocketPush { message_type, .. } => {
|
||||
// client frontend sent a websocket message
|
||||
// we don't expect this! we only use websockets to push updates
|
||||
// Err(anyhow::anyhow!("got unexpected websocket message!"))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("chess: got request from unexpected source"));
|
||||
Err(anyhow::anyhow!("got unexpected request from {source}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// handle chess protocol messages from other nodes
|
||||
fn handle_chess_request(
|
||||
our: &Address,
|
||||
source: &Address,
|
||||
message_json: serde_json::Value,
|
||||
state: &mut ChessState,
|
||||
action: ChessRequest,
|
||||
) -> anyhow::Result<()> {
|
||||
let action = message_json["action"].as_str().unwrap_or("");
|
||||
let game_id = &source.node;
|
||||
match action {
|
||||
"new_game" => {
|
||||
// make a new game with source.node if the current game has ended
|
||||
if let Some(game) = state.games.get(game_id) {
|
||||
if !game.ended {
|
||||
return Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: "conflict".as_bytes().to_vec(),
|
||||
})
|
||||
.send();
|
||||
}
|
||||
ChessRequest::NewGame { white, black } => {
|
||||
// make a new game with source.node
|
||||
// this will replace any existing game with source.node!
|
||||
if state.games.contains_key(game_id) {
|
||||
println!("chess: resetting game with {game_id} on their request!");
|
||||
}
|
||||
let game = Game {
|
||||
id: game_id.to_string(),
|
||||
turns: 0,
|
||||
board: Board::start_pos(),
|
||||
white: message_json["white"]
|
||||
.as_str()
|
||||
.unwrap_or(game_id)
|
||||
.to_string(),
|
||||
black: message_json["black"]
|
||||
.as_str()
|
||||
.unwrap_or(&our.node)
|
||||
.to_string(),
|
||||
board: Board::start_pos().fen(),
|
||||
white,
|
||||
black,
|
||||
ended: false,
|
||||
};
|
||||
state.games.insert(game_id.to_string(), game.clone());
|
||||
|
||||
utils::send_ws_update(&our, &game)?;
|
||||
utils::send_ws_update(&our, &game, &state.clients)?;
|
||||
state.games.insert(game_id.to_string(), game);
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
// tell them we've accepted the game
|
||||
// at the moment, we do not reject any new game requests!
|
||||
Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: "success".as_bytes().to_vec(),
|
||||
})
|
||||
.ipc(serde_json::to_vec(&ChessResponse::NewGameAccepted)?)
|
||||
.send()
|
||||
}
|
||||
"make_move" => {
|
||||
ChessRequest::Move(ref move_str) => {
|
||||
// check the move and then update if correct and send WS update
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
// if we don't have a game with them, reject the move
|
||||
return Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: "not found".as_bytes().to_vec(),
|
||||
})
|
||||
.ipc(serde_json::to_vec(&ChessResponse::MoveRejected)?)
|
||||
.send()
|
||||
};
|
||||
let mut board = Board::from_fen(&game.board).unwrap();
|
||||
if !board.apply_uci_move(move_str) {
|
||||
// reject invalid moves!
|
||||
return Response::new()
|
||||
.ipc(serde_json::to_vec(&ChessResponse::MoveRejected)?)
|
||||
.send();
|
||||
};
|
||||
let valid_move = game
|
||||
.board
|
||||
.apply_uci_move(message_json["move"].as_str().unwrap_or(""));
|
||||
if valid_move {
|
||||
}
|
||||
game.turns += 1;
|
||||
let checkmate = game.board.checkmate();
|
||||
let draw = game.board.stalemate();
|
||||
|
||||
if checkmate || draw {
|
||||
if board.checkmate() || board.stalemate() {
|
||||
game.ended = true;
|
||||
let winner = if checkmate {
|
||||
if game.turns % 2 == 1 {
|
||||
game.white.clone()
|
||||
} else {
|
||||
game.black.clone()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
// update the records
|
||||
if draw {
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
record.2 += 1;
|
||||
} else {
|
||||
state.records.insert(game.id.clone(), (0, 0, 1));
|
||||
}
|
||||
} else {
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
if winner == our.node {
|
||||
record.0 += 1;
|
||||
} else {
|
||||
record.1 += 1;
|
||||
}
|
||||
} else {
|
||||
if winner == our.node {
|
||||
state.records.insert(game.id.clone(), (1, 0, 0));
|
||||
} else {
|
||||
state.records.insert(game.id.clone(), (0, 1, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils::send_ws_update(&our, &game)?;
|
||||
game.board = board.fen();
|
||||
utils::send_ws_update(&our, &game, &state.clients)?;
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: "success".as_bytes().to_vec(),
|
||||
})
|
||||
.send()
|
||||
} else {
|
||||
Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: "invalid move".as_bytes().to_vec(),
|
||||
})
|
||||
.ipc(serde_json::to_vec(&ChessResponse::MoveAccepted)?)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
"end_game" => {
|
||||
// end the game and send WS update, update the standings
|
||||
ChessRequest::Resign => {
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
return Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: "not found".as_bytes().to_vec(),
|
||||
})
|
||||
.send();
|
||||
};
|
||||
|
||||
game.ended = true;
|
||||
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
record.0 += 1;
|
||||
} else {
|
||||
state.records.insert(game.id.clone(), (1, 0, 0));
|
||||
}
|
||||
|
||||
utils::send_ws_update(&our, &game)?;
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: "success".as_bytes().to_vec(),
|
||||
})
|
||||
.ipc(serde_json::to_vec(&ChessResponse::MoveRejected)?)
|
||||
.send()
|
||||
};
|
||||
game.ended = true;
|
||||
utils::send_ws_update(&our, &game, &state.clients)?;
|
||||
utils::save_chess_state(&state);
|
||||
// we don't respond to these
|
||||
Ok(())
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!("chess: got unexpected action")),
|
||||
}
|
||||
}
|
||||
|
||||
/// handle HTTP requests from our own frontend
|
||||
fn handle_http_request(
|
||||
our: &Address,
|
||||
http_request: http::IncomingHttpRequest,
|
||||
state: &mut ChessState,
|
||||
http_request: http::IncomingHttpRequest,
|
||||
) -> anyhow::Result<()> {
|
||||
if http_request.path()? != "/games" {
|
||||
if http_request.path()? != "games" {
|
||||
return http::send_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
None,
|
||||
@ -329,11 +334,7 @@ fn handle_http_request(
|
||||
String::from("Content-Type"),
|
||||
String::from("application/json"),
|
||||
)])),
|
||||
serde_json::to_vec(&serde_json::json!(state
|
||||
.games
|
||||
.iter()
|
||||
.map(|(id, game)| (id.to_string(), utils::json_game(game)))
|
||||
.collect::<HashMap<String, serde_json::Value>>()))?,
|
||||
serde_json::to_vec(&state.games)?,
|
||||
),
|
||||
"POST" => {
|
||||
// create a new game
|
||||
@ -361,35 +362,31 @@ fn handle_http_request(
|
||||
|
||||
// send the other player a new game request
|
||||
let response = Request::new()
|
||||
.target((game_id, "chess", "chess", "uqbar"))
|
||||
.ipc(serde_json::to_vec(&serde_json::json!({
|
||||
"action": "new_game",
|
||||
"white": player_white.clone(),
|
||||
"black": player_black.clone(),
|
||||
}))?)
|
||||
.send_and_await_response(30)?;
|
||||
.target((game_id, our.process.clone()))
|
||||
.ipc(serde_json::to_vec(&ChessRequest::NewGame {
|
||||
white: player_white.clone(),
|
||||
black: player_black.clone(),
|
||||
})?)
|
||||
.send_and_await_response(5)?;
|
||||
// if they accept, create a new game
|
||||
// otherwise, should surface error to FE...
|
||||
let Ok((_source, Message::Response((resp, _context)))) = response else {
|
||||
return http::send_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
None,
|
||||
"Service Unavailable".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("other player did not respond properly to new game request"));
|
||||
};
|
||||
if resp.ipc != "success".as_bytes() {
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
let resp = serde_json::from_slice::<ChessResponse>(&resp.ipc)?;
|
||||
if resp != ChessResponse::NewGameAccepted {
|
||||
return Err(anyhow::anyhow!("other player rejected new game request"));
|
||||
}
|
||||
// create a new game
|
||||
let game = Game {
|
||||
id: game_id.to_string(),
|
||||
turns: 0,
|
||||
board: Board::start_pos(),
|
||||
board: Board::start_pos().fen(),
|
||||
white: player_white,
|
||||
black: player_black,
|
||||
ended: false,
|
||||
};
|
||||
let body = serde_json::to_vec(&utils::json_game(&game))?;
|
||||
let body = serde_json::to_vec(&game)?;
|
||||
state.games.insert(game_id.to_string(), game);
|
||||
utils::save_chess_state(&state);
|
||||
http::send_response(
|
||||
@ -420,8 +417,11 @@ fn handle_http_request(
|
||||
} else if game.ended {
|
||||
return http::send_response(http::StatusCode::CONFLICT, None, vec![]);
|
||||
}
|
||||
let move_str = payload_json["move"].as_str().unwrap_or("");
|
||||
if !game.board.apply_uci_move(move_str) {
|
||||
let Some(move_str) = payload_json["move"].as_str() else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
let mut board = Board::from_fen(&game.board).unwrap();
|
||||
if !board.apply_uci_move(move_str) {
|
||||
// TODO surface illegal move to player or something here
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
}
|
||||
@ -429,61 +429,26 @@ fn handle_http_request(
|
||||
// check if the game is over
|
||||
// if so, update the records
|
||||
let response = Request::new()
|
||||
.target((game_id, "chess", "chess", "uqbar"))
|
||||
.ipc(serde_json::to_vec(&serde_json::json!({
|
||||
"action": "make_move",
|
||||
"move": move_str,
|
||||
}))?)
|
||||
.send_and_await_response(30)?;
|
||||
.target((game_id, our.process.clone()))
|
||||
.ipc(serde_json::to_vec(&ChessRequest::Move(
|
||||
move_str.to_string(),
|
||||
))?)
|
||||
.send_and_await_response(5)?;
|
||||
let Ok((_source, Message::Response((resp, _context)))) = response else {
|
||||
// TODO surface error to player, let them know other player is
|
||||
// offline or whatever they respond here was invalid
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
return Err(anyhow::anyhow!("other player did not respond properly to new game request"));
|
||||
};
|
||||
if resp.ipc != "success".as_bytes() {
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
let resp = serde_json::from_slice::<ChessResponse>(&resp.ipc)?;
|
||||
if resp != ChessResponse::MoveAccepted {
|
||||
return Err(anyhow::anyhow!("other player rejected new game request"));
|
||||
}
|
||||
// update the game
|
||||
game.turns += 1;
|
||||
let checkmate = game.board.checkmate();
|
||||
let draw = game.board.stalemate();
|
||||
|
||||
if checkmate || draw {
|
||||
if board.checkmate() || board.stalemate() {
|
||||
game.ended = true;
|
||||
let winner = if checkmate {
|
||||
if game.turns % 2 == 1 {
|
||||
&game.white
|
||||
} else {
|
||||
&game.black
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
// update the records
|
||||
if draw {
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
record.2 += 1;
|
||||
} else {
|
||||
state.records.insert(game.id.clone(), (0, 0, 1));
|
||||
}
|
||||
} else {
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
if winner == our.node {
|
||||
record.0 += 1;
|
||||
} else {
|
||||
record.1 += 1;
|
||||
}
|
||||
} else {
|
||||
if winner == our.node {
|
||||
state.records.insert(game.id.clone(), (1, 0, 0));
|
||||
} else {
|
||||
state.records.insert(game.id.clone(), (0, 1, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// game is not over, update state and return to FE
|
||||
let body = serde_json::to_vec(&utils::json_game(&game))?;
|
||||
game.board = board.fen();
|
||||
// update state and return to FE
|
||||
let body = serde_json::to_vec(&game)?;
|
||||
utils::save_chess_state(&state);
|
||||
// return the game
|
||||
http::send_response(
|
||||
@ -496,40 +461,21 @@ fn handle_http_request(
|
||||
)
|
||||
}
|
||||
"DELETE" => {
|
||||
// "end the game"?
|
||||
let query_params = http_request.query_params()?;
|
||||
let Some(game_id) = query_params.get("id") else {
|
||||
// end the game
|
||||
let Some(game_id) = http_request.query_params.get("id") else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
// send the other player an end game request
|
||||
let response = Request::new()
|
||||
.target((game_id, "chess", "chess", "uqbar"))
|
||||
.ipc(serde_json::to_vec(&serde_json::json!({
|
||||
"action": "end_game",
|
||||
}))?)
|
||||
.send_and_await_response(30)?;
|
||||
let Ok((_source, Message::Response((resp, _context)))) = response else {
|
||||
// TODO surface error to player, let them know other player is
|
||||
// offline or whatever they respond here was invalid
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
};
|
||||
if resp.ipc != "success".as_bytes() {
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
}
|
||||
|
||||
Request::new()
|
||||
.target((game_id.as_str(), our.process.clone()))
|
||||
.ipc(serde_json::to_vec(&ChessRequest::Resign)?)
|
||||
.send()?;
|
||||
game.ended = true;
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
record.1 += 1;
|
||||
} else {
|
||||
state.records.insert(game.id.clone(), (0, 1, 0));
|
||||
}
|
||||
// return the game
|
||||
let body = serde_json::to_vec(&utils::json_game(&game))?;
|
||||
let body = serde_json::to_vec(&game)?;
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
http::send_response(
|
||||
http::StatusCode::OK,
|
||||
Some(HashMap::from([(
|
||||
@ -539,11 +485,6 @@ fn handle_http_request(
|
||||
body,
|
||||
)
|
||||
}
|
||||
_ => Response::new()
|
||||
.ipc(serde_json::to_vec(&http::HttpResponse {
|
||||
status: 405,
|
||||
headers: HashMap::new(),
|
||||
})?)
|
||||
.send(),
|
||||
_ => http::send_response(http::StatusCode::METHOD_NOT_ALLOWED, None, vec![]),
|
||||
}
|
||||
}
|
||||
|
@ -1,82 +1,44 @@
|
||||
use crate::*;
|
||||
|
||||
pub fn save_chess_state(state: &ChessState) {
|
||||
let stored_state = convert_state(&state);
|
||||
set_state(&bincode::serialize(&stored_state).unwrap());
|
||||
set_state(&bincode::serialize(&state.games).unwrap());
|
||||
}
|
||||
|
||||
fn convert_game(game: Game) -> StoredGame {
|
||||
StoredGame {
|
||||
id: game.id,
|
||||
turns: game.turns,
|
||||
board: game.board.fen(),
|
||||
white: game.white,
|
||||
black: game.black,
|
||||
ended: game.ended,
|
||||
pub fn load_chess_state() -> ChessState {
|
||||
match get_typed_state(|bytes| Ok(bincode::deserialize::<HashMap<String, Game>>(bytes)?)) {
|
||||
Some(games) => ChessState {
|
||||
games,
|
||||
clients: HashSet::new(),
|
||||
},
|
||||
None => ChessState {
|
||||
games: HashMap::new(),
|
||||
clients: HashSet::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_state(state: &ChessState) -> StoredChessState {
|
||||
StoredChessState {
|
||||
games: state
|
||||
.games
|
||||
.iter()
|
||||
.map(|(id, game)| (id.to_string(), convert_game(game.clone())))
|
||||
.collect(),
|
||||
records: state.records.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_game(game: &Game) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"id": game.id,
|
||||
"turns": game.turns,
|
||||
"board": game.board.fen(),
|
||||
"white": game.white,
|
||||
"black": game.black,
|
||||
"ended": game.ended,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_ws_update(our: &Address, game: &Game) -> anyhow::Result<()> {
|
||||
pub fn send_ws_update(
|
||||
our: &Address,
|
||||
game: &Game,
|
||||
open_channels: &HashSet<u32>,
|
||||
) -> anyhow::Result<()> {
|
||||
for channel in open_channels {
|
||||
Request::new()
|
||||
.target((&our.node, "http_server", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
"EncryptAndForward": {
|
||||
"channel_id": our.process.to_string(),
|
||||
"forward_to": {
|
||||
"node": our.node.clone(),
|
||||
"process": {
|
||||
"process_name": "http_server",
|
||||
"package_name": "sys",
|
||||
"publisher_node": "uqbar"
|
||||
}
|
||||
}, // node, process
|
||||
"json": Some(serde_json::json!({ // this is the JSON to forward
|
||||
"WebSocketPush": {
|
||||
"target": {
|
||||
"node": our.node.clone(),
|
||||
"id": "chess", // If the message passed in an ID then we could send to just that ID
|
||||
}
|
||||
}
|
||||
})),
|
||||
}
|
||||
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.ipc(serde_json::to_vec(
|
||||
&http::HttpServerAction::WebSocketPush {
|
||||
channel_id: *channel,
|
||||
message_type: http::WsMessageType::Binary,
|
||||
},
|
||||
)?)
|
||||
.payload(Payload {
|
||||
mime: Some("application/json".to_string()),
|
||||
bytes: serde_json::json!({
|
||||
"kind": "game_update",
|
||||
"data": json_game(game),
|
||||
"data": game,
|
||||
}).to_string().into_bytes(),
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
})
|
||||
.send()
|
||||
.send()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -196,7 +196,7 @@
|
||||
|
||||
<h4>Apps:</h4>
|
||||
<!-- <a id="file-transfer" href="/file-transfer">File Transfer</a> -->
|
||||
<a id="chess" href="/chess:chess:uqbar/">Chess [NOT WORKING]</a>
|
||||
<a id="chess" href="/chess:chess:uqbar/">Chess</a>
|
||||
</div>
|
||||
<script>window.ourName = window.our = '${our}'</script>
|
||||
<script>
|
||||
|
298
src/http/login.html
Normal file
298
src/http/login.html
Normal file
File diff suppressed because one or more lines are too long
@ -1,11 +1,13 @@
|
||||
use crate::http::types::*;
|
||||
use crate::http::utils::*;
|
||||
use crate::register;
|
||||
use crate::types::*;
|
||||
use crate::{keygen, register};
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use http::uri::Authority;
|
||||
use route_recognizer::Router;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
@ -16,6 +18,8 @@ use warp::{Filter, Reply};
|
||||
|
||||
const HTTP_SELF_IMPOSED_TIMEOUT: u64 = 15;
|
||||
|
||||
const LOGIN_HTML: &str = include_str!("login.html");
|
||||
|
||||
/// mapping from a given HTTP request (assigned an ID) to the oneshot
|
||||
/// channel that will get a response from the app that handles the request,
|
||||
/// and a string which contains the path that the request was made to.
|
||||
@ -32,6 +36,7 @@ type PathBindings = Arc<RwLock<Router<BoundPath>>>;
|
||||
|
||||
struct BoundPath {
|
||||
pub app: ProcessId,
|
||||
pub secure_subdomain: Option<String>,
|
||||
pub authenticated: bool,
|
||||
pub local_only: bool,
|
||||
pub static_content: Option<Payload>, // TODO store in filesystem and cache
|
||||
@ -55,26 +60,28 @@ struct BoundPath {
|
||||
pub async fn http_server(
|
||||
our_name: String,
|
||||
our_port: u16,
|
||||
encoded_keyfile: Vec<u8>,
|
||||
jwt_secret_bytes: Vec<u8>,
|
||||
mut recv_in_server: MessageReceiver,
|
||||
send_to_loop: MessageSender,
|
||||
print_tx: PrintSender,
|
||||
) -> Result<()> {
|
||||
let our_name = Arc::new(our_name);
|
||||
let encoded_keyfile = Arc::new(encoded_keyfile);
|
||||
let jwt_secret_bytes = Arc::new(jwt_secret_bytes);
|
||||
let http_response_senders: HttpResponseSenders = Arc::new(DashMap::new());
|
||||
let ws_senders: WebSocketSenders = Arc::new(DashMap::new());
|
||||
|
||||
// Add RPC path
|
||||
// add RPC path
|
||||
let mut bindings_map: Router<BoundPath> = Router::new();
|
||||
let rpc_bound_path = BoundPath {
|
||||
app: ProcessId::from_str("rpc:sys:uqbar").unwrap(),
|
||||
secure_subdomain: None, // TODO maybe RPC should have subdomain?
|
||||
authenticated: false,
|
||||
local_only: true,
|
||||
static_content: None,
|
||||
};
|
||||
bindings_map.add("/rpc:sys:uqbar/message", rpc_bound_path);
|
||||
|
||||
let path_bindings: PathBindings = Arc::new(RwLock::new(bindings_map));
|
||||
|
||||
tokio::spawn(serve(
|
||||
@ -83,6 +90,7 @@ pub async fn http_server(
|
||||
http_response_senders.clone(),
|
||||
path_bindings.clone(),
|
||||
ws_senders.clone(),
|
||||
encoded_keyfile.clone(),
|
||||
jwt_secret_bytes.clone(),
|
||||
send_to_loop.clone(),
|
||||
print_tx.clone(),
|
||||
@ -95,9 +103,7 @@ pub async fn http_server(
|
||||
http_response_senders.clone(),
|
||||
path_bindings.clone(),
|
||||
ws_senders.clone(),
|
||||
jwt_secret_bytes.clone(),
|
||||
send_to_loop.clone(),
|
||||
print_tx.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -112,6 +118,7 @@ async fn serve(
|
||||
http_response_senders: HttpResponseSenders,
|
||||
path_bindings: PathBindings,
|
||||
ws_senders: WebSocketSenders,
|
||||
encoded_keyfile: Arc<Vec<u8>>,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
send_to_loop: MessageSender,
|
||||
print_tx: PrintSender,
|
||||
@ -123,7 +130,7 @@ async fn serve(
|
||||
})
|
||||
.await;
|
||||
|
||||
// Filter to receive websockets
|
||||
// filter to receive websockets
|
||||
let cloned_msg_tx = send_to_loop.clone();
|
||||
let cloned_our = our.clone();
|
||||
let cloned_jwt_secret_bytes = jwt_secret_bytes.clone();
|
||||
@ -155,10 +162,26 @@ async fn serve(
|
||||
})
|
||||
},
|
||||
);
|
||||
// Filter to receive HTTP requests
|
||||
|
||||
// filter to receive and handle login requests
|
||||
let cloned_our = our.clone();
|
||||
let login = warp::path("login").and(warp::path::end()).and(
|
||||
warp::get()
|
||||
.map(|| warp::reply::with_status(warp::reply::html(LOGIN_HTML), StatusCode::OK))
|
||||
.or(warp::post()
|
||||
.and(warp::body::content_length_limit(1024 * 16))
|
||||
.and(warp::body::json())
|
||||
.and(warp::any().map(move || cloned_our.clone()))
|
||||
.and(warp::any().map(move || encoded_keyfile.clone()))
|
||||
.and_then(login_handler)),
|
||||
);
|
||||
|
||||
// filter to receive all other HTTP requests
|
||||
let filter = warp::filters::method::method()
|
||||
.and(warp::addr::remote())
|
||||
.and(warp::filters::host::optional())
|
||||
.and(warp::path::full())
|
||||
.and(warp::query::<HashMap<String, String>>())
|
||||
.and(warp::filters::header::headers_cloned())
|
||||
.and(warp::filters::body::bytes())
|
||||
.and(warp::any().map(move || our.clone()))
|
||||
@ -166,18 +189,70 @@ async fn serve(
|
||||
.and(warp::any().map(move || path_bindings.clone()))
|
||||
.and(warp::any().map(move || jwt_secret_bytes.clone()))
|
||||
.and(warp::any().map(move || send_to_loop.clone()))
|
||||
.and(warp::any().map(move || print_tx.clone()))
|
||||
.and_then(http_handler);
|
||||
|
||||
let filter_with_ws = ws_route.or(filter);
|
||||
let filter_with_ws = ws_route.or(login).or(filter);
|
||||
warp::serve(filter_with_ws)
|
||||
.run(([0, 0, 0, 0], our_port))
|
||||
.await;
|
||||
}
|
||||
|
||||
/// handle non-GET requests on /login. if POST, validate password
|
||||
/// and return auth token, which will be stored in a cookie.
|
||||
/// then redirect to wherever they were trying to go.
|
||||
async fn login_handler(
|
||||
info: LoginInfo,
|
||||
our: Arc<String>,
|
||||
encoded_keyfile: Arc<Vec<u8>>,
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
match keygen::decode_keyfile(&encoded_keyfile, &info.password) {
|
||||
Ok(keyfile) => {
|
||||
let token = match register::generate_jwt(&keyfile.jwt_secret_bytes, our.as_ref()) {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&"Failed to generate JWT"),
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
};
|
||||
|
||||
let mut response = warp::reply::with_status(
|
||||
warp::reply::json(&base64::encode(encoded_keyfile.to_vec())),
|
||||
StatusCode::FOUND,
|
||||
)
|
||||
.into_response();
|
||||
|
||||
match HeaderValue::from_str(&format!("uqbar-auth_{}={};", our.as_ref(), &token)) {
|
||||
Ok(v) => {
|
||||
response.headers_mut().append(http::header::SET_COOKIE, v);
|
||||
Ok(response)
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&"Failed to generate Auth JWT"),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(warp::reply::with_status(
|
||||
warp::reply::json(&"Failed to decode keyfile"),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn http_handler(
|
||||
method: warp::http::Method,
|
||||
socket_addr: Option<SocketAddr>,
|
||||
host: Option<Authority>,
|
||||
path: warp::path::FullPath,
|
||||
query_params: HashMap<String, String>,
|
||||
headers: warp::http::HeaderMap,
|
||||
body: warp::hyper::body::Bytes,
|
||||
our: Arc<String>,
|
||||
@ -185,11 +260,18 @@ async fn http_handler(
|
||||
path_bindings: PathBindings,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
send_to_loop: MessageSender,
|
||||
print_tx: PrintSender,
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
// TODO this is all so dirty. Figure out what actually matters.
|
||||
|
||||
// trim trailing "/"
|
||||
let original_path = normalize_path(path.as_str());
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("got request for path {original_path}"),
|
||||
})
|
||||
.await;
|
||||
let id: u64 = rand::random();
|
||||
let serialized_headers = serialize_headers(&headers);
|
||||
let path_bindings = path_bindings.read().await;
|
||||
@ -200,11 +282,55 @@ async fn http_handler(
|
||||
let bound_path = route.handler();
|
||||
|
||||
if bound_path.authenticated {
|
||||
let auth_token = serialized_headers
|
||||
.get("cookie")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
match serialized_headers.get("cookie") {
|
||||
Some(auth_token) => {
|
||||
// they have an auth token, validate
|
||||
if !auth_cookie_valid(&our, &auth_token, &jwt_secret_bytes) {
|
||||
return Ok(
|
||||
warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response()
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// redirect to login page so they can get an auth token
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("redirecting request from {socket_addr:?} to login page"),
|
||||
})
|
||||
.await;
|
||||
return Ok(warp::http::Response::builder()
|
||||
.status(StatusCode::TEMPORARY_REDIRECT)
|
||||
.header(
|
||||
"Location",
|
||||
format!(
|
||||
"http://{}/login",
|
||||
host.unwrap_or(Authority::from_static("localhost"))
|
||||
),
|
||||
)
|
||||
.body(vec![])
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref subdomain) = bound_path.secure_subdomain {
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!(
|
||||
"got request for path {original_path} bound by subdomain {subdomain}"
|
||||
),
|
||||
})
|
||||
.await;
|
||||
// assert that host matches what this app wants it to be
|
||||
if host.is_none() {
|
||||
return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response());
|
||||
}
|
||||
let host = host.as_ref().unwrap();
|
||||
// parse out subdomain from host (there can only be one)
|
||||
let request_subdomain = host.host().split('.').next().unwrap_or("");
|
||||
if request_subdomain != subdomain {
|
||||
return Ok(warp::reply::with_status(vec![], StatusCode::UNAUTHORIZED).into_response());
|
||||
}
|
||||
}
|
||||
@ -258,14 +384,20 @@ async fn http_handler(
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: Some(HTTP_SELF_IMPOSED_TIMEOUT),
|
||||
ipc: serde_json::to_vec(&IncomingHttpRequest {
|
||||
ipc: serde_json::to_vec(&HttpServerRequest::Http(IncomingHttpRequest {
|
||||
source_socket_addr: socket_addr.map(|addr| addr.to_string()),
|
||||
method: method.to_string(),
|
||||
raw_path: format!("http://localhost{}", original_path),
|
||||
raw_path: format!(
|
||||
"http://{}{}",
|
||||
host.unwrap_or(Authority::from_static("localhost"))
|
||||
.to_string(),
|
||||
original_path
|
||||
),
|
||||
headers: serialized_headers,
|
||||
})
|
||||
query_params,
|
||||
}))
|
||||
.unwrap(),
|
||||
metadata: None,
|
||||
metadata: Some("http".into()),
|
||||
}),
|
||||
payload: Some(Payload {
|
||||
mime: None,
|
||||
@ -390,15 +522,28 @@ async fn maintain_websocket(
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
ws_senders: WebSocketSenders,
|
||||
send_to_loop: MessageSender,
|
||||
_print_tx: PrintSender,
|
||||
print_tx: PrintSender,
|
||||
) {
|
||||
let (mut write_stream, mut read_stream) = ws.split();
|
||||
|
||||
// first, receive a message from client that contains the target process
|
||||
// and the auth token
|
||||
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("got new client websocket connection"),
|
||||
})
|
||||
.await;
|
||||
|
||||
let Some(Ok(register_msg)) = read_stream.next().await else {
|
||||
// stream closed, exit
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("client failed to send registration message"),
|
||||
})
|
||||
.await;
|
||||
let stream = write_stream.reunite(read_stream).unwrap();
|
||||
let _ = stream.close().await;
|
||||
return;
|
||||
@ -406,6 +551,12 @@ async fn maintain_websocket(
|
||||
|
||||
let Ok(ws_register) = serde_json::from_slice::<WsRegister>(register_msg.as_bytes()) else {
|
||||
// stream error, exit
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("couldn't parse registration message from client"),
|
||||
})
|
||||
.await;
|
||||
let stream = write_stream.reunite(read_stream).unwrap();
|
||||
let _ = stream.close().await;
|
||||
return;
|
||||
@ -413,11 +564,24 @@ async fn maintain_websocket(
|
||||
|
||||
let Ok(owner_process) = ProcessId::from_str(&ws_register.target_process) else {
|
||||
// invalid process id, exit
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("client sent malformed process ID"),
|
||||
})
|
||||
.await;
|
||||
let stream = write_stream.reunite(read_stream).unwrap();
|
||||
let _ = stream.close().await;
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("channel is intended for {owner_process}"),
|
||||
})
|
||||
.await;
|
||||
|
||||
let Ok(our_name) = verify_auth_token(&ws_register.auth_token, &jwt_secret_bytes) else {
|
||||
// invalid auth token, exit
|
||||
let stream = write_stream.reunite(read_stream).unwrap();
|
||||
@ -453,8 +617,8 @@ async fn maintain_websocket(
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: None,
|
||||
ipc: serde_json::to_vec(&HttpServerAction::WebSocketOpen(ws_channel_id)).unwrap(),
|
||||
metadata: None,
|
||||
ipc: serde_json::to_vec(&HttpServerRequest::WebSocketOpen(ws_channel_id)).unwrap(),
|
||||
metadata: Some("ws".into()),
|
||||
}),
|
||||
payload: None,
|
||||
signed_capabilities: None,
|
||||
@ -477,6 +641,13 @@ async fn maintain_websocket(
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 1,
|
||||
content: format!("websocket channel {ws_channel_id} opened"),
|
||||
})
|
||||
.await;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
read = read_stream.next() => {
|
||||
@ -508,11 +679,11 @@ async fn maintain_websocket(
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: None,
|
||||
ipc: serde_json::to_vec(&HttpServerAction::WebSocketPush {
|
||||
ipc: serde_json::to_vec(&HttpServerRequest::WebSocketPush {
|
||||
channel_id: ws_channel_id,
|
||||
message_type: WsMessageType::Binary,
|
||||
}).unwrap(),
|
||||
metadata: None,
|
||||
metadata: Some("ws".into()),
|
||||
}),
|
||||
payload: Some(Payload {
|
||||
mime: None,
|
||||
@ -563,8 +734,8 @@ async fn websocket_close(
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: None,
|
||||
ipc: serde_json::to_vec(&HttpServerAction::WebSocketClose(channel_id)).unwrap(),
|
||||
metadata: None,
|
||||
ipc: serde_json::to_vec(&HttpServerRequest::WebSocketClose(channel_id)).unwrap(),
|
||||
metadata: Some("ws".into()),
|
||||
}),
|
||||
payload: Some(Payload {
|
||||
mime: None,
|
||||
@ -584,9 +755,7 @@ async fn handle_app_message(
|
||||
http_response_senders: HttpResponseSenders,
|
||||
path_bindings: PathBindings,
|
||||
ws_senders: WebSocketSenders,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
send_to_loop: MessageSender,
|
||||
print_tx: PrintSender,
|
||||
) {
|
||||
// when we get a Response, try to match it to an outstanding HTTP
|
||||
// request and send it there.
|
||||
@ -618,60 +787,10 @@ async fn handle_app_message(
|
||||
.unwrap(),
|
||||
));
|
||||
} else {
|
||||
let Ok(mut response) = serde_json::from_slice::<HttpResponse>(&response.ipc) else {
|
||||
let Ok(response) = serde_json::from_slice::<HttpResponse>(&response.ipc) else {
|
||||
// the receiver will automatically trigger a 503 when sender is dropped.
|
||||
return;
|
||||
};
|
||||
// XX REFACTOR THIS:
|
||||
// for the login case, todo refactor out?
|
||||
let segments: Vec<&str> = path
|
||||
.split('/')
|
||||
.filter(|&segment| !segment.is_empty())
|
||||
.collect();
|
||||
// If we're getting back a /login from a proxy (or our own node),
|
||||
// then we should generate a jwt from the secret + the name of the ship,
|
||||
// and then attach it to a header.
|
||||
if response.status < 400
|
||||
&& (segments.len() == 1 || segments.len() == 4)
|
||||
&& matches!(segments.last(), Some(&"login"))
|
||||
{
|
||||
if let Some(auth_cookie) = response.headers.get("set-cookie") {
|
||||
let mut ws_auth_username = km.source.node.clone();
|
||||
|
||||
if segments.len() == 4
|
||||
&& matches!(segments.first(), Some(&"http-proxy"))
|
||||
&& matches!(segments.get(1), Some(&"serve"))
|
||||
{
|
||||
if let Some(segment) = segments.get(2) {
|
||||
ws_auth_username = segment.to_string();
|
||||
}
|
||||
}
|
||||
if let Some(token) = register::generate_jwt(
|
||||
jwt_secret_bytes.to_vec().as_slice(),
|
||||
ws_auth_username.clone(),
|
||||
) {
|
||||
let auth_cookie_with_ws = format!(
|
||||
"{}; uqbar-ws-auth_{}={};",
|
||||
auth_cookie,
|
||||
ws_auth_username.clone(),
|
||||
token
|
||||
);
|
||||
response
|
||||
.headers
|
||||
.insert("set-cookie".to_string(), auth_cookie_with_ws);
|
||||
|
||||
let _ = print_tx
|
||||
.send(Printout {
|
||||
verbosity: 2,
|
||||
content: format!(
|
||||
"SET WS AUTH COOKIE WITH USERNAME: {}",
|
||||
ws_auth_username
|
||||
),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = sender.send((
|
||||
HttpResponse {
|
||||
status: response.status,
|
||||
@ -722,6 +841,7 @@ async fn handle_app_message(
|
||||
&normalize_path(&path),
|
||||
BoundPath {
|
||||
app: km.source.process.clone(),
|
||||
secure_subdomain: None,
|
||||
authenticated,
|
||||
local_only,
|
||||
static_content: None,
|
||||
@ -743,6 +863,7 @@ async fn handle_app_message(
|
||||
&normalize_path(&path),
|
||||
BoundPath {
|
||||
app: km.source.process.clone(),
|
||||
secure_subdomain: None,
|
||||
authenticated,
|
||||
local_only,
|
||||
static_content: Some(payload),
|
||||
@ -751,6 +872,52 @@ async fn handle_app_message(
|
||||
}
|
||||
send_action_response(km.id, km.source, &send_to_loop, Ok(())).await;
|
||||
}
|
||||
HttpServerAction::SecureBind { path, cache } => {
|
||||
// the process ID is hashed to generate a unique subdomain
|
||||
// only the first 32 chars, or 128 bits are used.
|
||||
// we hash because the process ID can contain many more than
|
||||
// simply alphanumeric characters that will cause issues as a subdomain.
|
||||
let process_id_hash =
|
||||
format!("{:x}", Sha256::digest(km.source.process.to_string()));
|
||||
let subdomain = process_id_hash.split_at(32).0.to_owned();
|
||||
let mut path_bindings = path_bindings.write().await;
|
||||
if !cache {
|
||||
// trim trailing "/"
|
||||
path_bindings.add(
|
||||
&normalize_path(&path),
|
||||
BoundPath {
|
||||
app: km.source.process.clone(),
|
||||
secure_subdomain: Some(subdomain),
|
||||
authenticated: true,
|
||||
local_only: false,
|
||||
static_content: None,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let Some(payload) = km.payload else {
|
||||
send_action_response(
|
||||
km.id,
|
||||
km.source,
|
||||
&send_to_loop,
|
||||
Err(HttpServerError::NoPayload),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
// trim trailing "/"
|
||||
path_bindings.add(
|
||||
&normalize_path(&path),
|
||||
BoundPath {
|
||||
app: km.source.process.clone(),
|
||||
secure_subdomain: Some(subdomain),
|
||||
authenticated: true,
|
||||
local_only: false,
|
||||
static_content: Some(payload),
|
||||
},
|
||||
);
|
||||
}
|
||||
send_action_response(km.id, km.source, &send_to_loop, Ok(())).await;
|
||||
}
|
||||
HttpServerAction::WebSocketOpen(_) => {
|
||||
// we cannot receive these, only send them to processes
|
||||
send_action_response(
|
||||
|
@ -5,12 +5,32 @@ use thiserror::Error;
|
||||
|
||||
/// HTTP Request type that can be shared over WASM boundary to apps.
|
||||
/// This is the one you receive from the `http_server:sys:uqbar` service.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum HttpServerRequest {
|
||||
Http(IncomingHttpRequest),
|
||||
/// Processes will receive this kind of request when a client connects to them.
|
||||
/// If a process does not want this websocket open, they should issue a *request*
|
||||
/// containing a [`type@HttpServerAction::WebSocketClose`] message and this channel ID.
|
||||
WebSocketOpen(u32),
|
||||
/// Processes can both SEND and RECEIVE this kind of request
|
||||
/// (send as [`type@HttpServerAction::WebSocketPush`]).
|
||||
/// When received, will contain the message bytes as payload.
|
||||
WebSocketPush {
|
||||
channel_id: u32,
|
||||
message_type: WsMessageType,
|
||||
},
|
||||
/// Receiving will indicate that the client closed the socket. Can be sent to close
|
||||
/// from the server-side, as [`type@HttpServerAction::WebSocketClose`].
|
||||
WebSocketClose(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IncomingHttpRequest {
|
||||
pub source_socket_addr: Option<String>, // will parse to SocketAddr
|
||||
pub method: String, // will parse to http::Method
|
||||
pub raw_path: String,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub query_params: HashMap<String, String>,
|
||||
// BODY is stored in the payload, as bytes
|
||||
}
|
||||
|
||||
@ -56,8 +76,8 @@ pub enum HttpClientError {
|
||||
}
|
||||
|
||||
/// Request type sent to `http_server:sys:uqbar` in order to configure it.
|
||||
/// You can also send [`WebSocketPush`], which allows you to push messages
|
||||
/// across an existing open WebSocket connection.
|
||||
/// You can also send [`type@HttpServerAction::WebSocketPush`], which
|
||||
/// allows you to push messages across an existing open WebSocket connection.
|
||||
///
|
||||
/// If a response is expected, all HttpServerActions will return a Response
|
||||
/// with the shape Result<(), HttpServerActionError> serialized to JSON.
|
||||
@ -67,23 +87,40 @@ pub enum HttpServerAction {
|
||||
/// be the static file to serve at this path.
|
||||
Bind {
|
||||
path: String,
|
||||
/// Set whether the HTTP request needs a valid login cookie, AKA, whether
|
||||
/// the user needs to be logged in to access this path.
|
||||
authenticated: bool,
|
||||
/// Set whether requests can be fielded from anywhere, or only the loopback address.
|
||||
local_only: bool,
|
||||
/// Set whether to bind the payload statically to this path. That is, take the
|
||||
/// payload bytes and serve them as the response to any request to this path.
|
||||
cache: bool,
|
||||
},
|
||||
/// SecureBind expects a payload if and only if `cache` is TRUE. The payload should
|
||||
/// be the static file to serve at this path.
|
||||
///
|
||||
/// SecureBind is the same as Bind, except that it forces requests to be made from
|
||||
/// the unique subdomain of the process that bound the path. These requests are
|
||||
/// *always* authenticated, and *never* local_only. The purpose of SecureBind is to
|
||||
/// serve elements of an app frontend or API in an exclusive manner, such that other
|
||||
/// apps installed on this node cannot access them. Since the subdomain is unique, it
|
||||
/// will require the user to be logged in separately to the general domain authentication.
|
||||
SecureBind {
|
||||
path: String,
|
||||
/// Set whether to bind the payload statically to this path. That is, take the
|
||||
/// payload bytes and serve them as the response to any request to this path.
|
||||
cache: bool,
|
||||
},
|
||||
/// Processes will RECEIVE this kind of request when a client connects to them.
|
||||
/// If a process does not want this websocket open, they should issue a *request*
|
||||
/// containing a [`enum@HttpServerAction::WebSocketClose`] message and this channel ID.
|
||||
/// containing a [`type@HttpServerAction::WebSocketClose`] message and this channel ID.
|
||||
WebSocketOpen(u32),
|
||||
/// Processes can both SEND and RECEIVE this kind of request.
|
||||
/// When sent, expects a payload containing the WebSocket message bytes to send.
|
||||
WebSocketPush {
|
||||
channel_id: u32,
|
||||
message_type: WsMessageType,
|
||||
},
|
||||
/// Processes can both SEND and RECEIVE this kind of request. Sending will
|
||||
/// close a socket the process controls. Receiving will indicate that the
|
||||
/// client closed the socket.
|
||||
/// Sending will close a socket the process controls.
|
||||
WebSocketClose(u32),
|
||||
}
|
||||
|
||||
|
@ -66,9 +66,9 @@ pub fn encode_keyfile(
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn decode_keyfile(keyfile: Vec<u8>, password: &str) -> Result<Keyfile, &'static str> {
|
||||
pub fn decode_keyfile(keyfile: &[u8], password: &str) -> Result<Keyfile, &'static str> {
|
||||
let (username, routers, salt, key_enc, jwt_enc, file_enc) =
|
||||
bincode::deserialize::<(String, Vec<String>, Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>)>(&keyfile)
|
||||
bincode::deserialize::<(String, Vec<String>, Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>)>(keyfile)
|
||||
.map_err(|_| "failed to deserialize keyfile")?;
|
||||
|
||||
// rederive disk key
|
||||
@ -112,9 +112,9 @@ pub fn decode_keyfile(keyfile: Vec<u8>, password: &str) -> Result<Keyfile, &'sta
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_username_and_routers(keyfile: Vec<u8>) -> Result<(String, Vec<String>), &'static str> {
|
||||
pub fn get_username_and_routers(keyfile: &[u8]) -> Result<(String, Vec<String>), &'static str> {
|
||||
let (username, routers, _salt, _key_enc, _jwt_enc, _file_enc) =
|
||||
bincode::deserialize::<(String, Vec<String>, Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>)>(&keyfile)
|
||||
bincode::deserialize::<(String, Vec<String>, Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>)>(keyfile)
|
||||
.map_err(|_| "failed to deserialize keyfile")?;
|
||||
|
||||
Ok((username, routers))
|
||||
|
26
src/main.rs
26
src/main.rs
@ -52,7 +52,7 @@ async fn serve_register_fe(
|
||||
our_ip: String,
|
||||
http_server_port: u16,
|
||||
rpc_url: String,
|
||||
) -> (Identity, Keyfile) {
|
||||
) -> (Identity, Vec<u8>, Keyfile) {
|
||||
// check if we have keys saved on disk, encrypted
|
||||
// if so, prompt user for "password" to decrypt with
|
||||
|
||||
@ -81,20 +81,16 @@ async fn serve_register_fe(
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"saving encrypted networking keys to {}/.keys",
|
||||
home_directory_path
|
||||
);
|
||||
|
||||
fs::write(format!("{}/.keys", home_directory_path), encoded_keyfile)
|
||||
fs::write(
|
||||
format!("{}/.keys", home_directory_path),
|
||||
encoded_keyfile.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("registration complete!");
|
||||
|
||||
let _ = kill_tx.send(true);
|
||||
|
||||
(our, decoded_keyfile)
|
||||
(our, encoded_keyfile, decoded_keyfile)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@ -272,13 +268,12 @@ async fn main() {
|
||||
};
|
||||
|
||||
let http_server_port = http::utils::find_open_port(port).await.unwrap();
|
||||
println!("runtime bound port {}\r", http_server_port);
|
||||
println!(
|
||||
"login or register at http://localhost:{}\r",
|
||||
http_server_port
|
||||
);
|
||||
#[cfg(not(feature = "simulation-mode"))]
|
||||
let (our, decoded_keyfile) = serve_register_fe(
|
||||
let (our, encoded_keyfile, decoded_keyfile) = serve_register_fe(
|
||||
&home_directory_path,
|
||||
our_ip.to_string(),
|
||||
http_server_port.clone(),
|
||||
@ -286,7 +281,7 @@ async fn main() {
|
||||
)
|
||||
.await;
|
||||
#[cfg(feature = "simulation-mode")]
|
||||
let (our, decoded_keyfile) = match fake_node_name {
|
||||
let (our, encoded_keyfile, decoded_keyfile) = match fake_node_name {
|
||||
None => {
|
||||
match password {
|
||||
None => match rpc_url {
|
||||
@ -322,7 +317,7 @@ async fn main() {
|
||||
ws_routing: None, // TODO
|
||||
allowed_routers: decoded_keyfile.routers.clone(),
|
||||
};
|
||||
(our, decoded_keyfile)
|
||||
(our, keyfile, decoded_keyfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -372,7 +367,7 @@ async fn main() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(our, decoded_keyfile)
|
||||
(our, encoded_keyfile, decoded_keyfile)
|
||||
}
|
||||
};
|
||||
|
||||
@ -483,6 +478,7 @@ async fn main() {
|
||||
tasks.spawn(http::server::http_server(
|
||||
our.name.clone(),
|
||||
http_server_port,
|
||||
encoded_keyfile,
|
||||
decoded_keyfile.jwt_secret_bytes.clone(),
|
||||
http_server_receiver,
|
||||
kernel_message_sender.clone(),
|
||||
|
@ -77,14 +77,14 @@ fn _hex_string_to_u8_array(hex_str: &str) -> Result<[u8; 32], &'static str> {
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn generate_jwt(jwt_secret_bytes: &[u8], username: String) -> Option<String> {
|
||||
pub fn generate_jwt(jwt_secret_bytes: &[u8], username: &str) -> Option<String> {
|
||||
let jwt_secret: Hmac<Sha256> = match Hmac::new_from_slice(jwt_secret_bytes) {
|
||||
Ok(secret) => secret,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let claims = crate::http::types::JwtClaims {
|
||||
username: username.clone(),
|
||||
username: username.to_string(),
|
||||
expiration: 0,
|
||||
};
|
||||
|
||||
@ -213,7 +213,7 @@ async fn get_unencrypted_info(
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
let (name, allowed_routers) = {
|
||||
match keyfile_arc.lock().unwrap().clone() {
|
||||
Some(encoded_keyfile) => match keygen::get_username_and_routers(encoded_keyfile) {
|
||||
Some(encoded_keyfile) => match keygen::get_username_and_routers(&encoded_keyfile) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return Ok(warp::reply::with_status(
|
||||
@ -278,7 +278,7 @@ async fn handle_keyfile_vet(
|
||||
false => base64::decode(payload.keyfile).unwrap(),
|
||||
};
|
||||
|
||||
let decoded_keyfile = match keygen::decode_keyfile(encoded_keyfile, &payload.password) {
|
||||
let decoded_keyfile = match keygen::decode_keyfile(&encoded_keyfile, &payload.password) {
|
||||
Ok(k) => k,
|
||||
Err(_) => return Err(warp::reject()),
|
||||
};
|
||||
@ -369,8 +369,7 @@ async fn handle_import_keyfile(
|
||||
.into_response());
|
||||
};
|
||||
|
||||
let (decoded_keyfile, our) =
|
||||
match keygen::decode_keyfile(encoded_keyfile.clone(), &info.password) {
|
||||
let (decoded_keyfile, our) = match keygen::decode_keyfile(&encoded_keyfile, &info.password) {
|
||||
Ok(k) => {
|
||||
let our = Identity {
|
||||
name: k.username.clone(),
|
||||
@ -440,8 +439,7 @@ async fn handle_login(
|
||||
.into_response());
|
||||
};
|
||||
|
||||
let (decoded_keyfile, our) =
|
||||
match keygen::decode_keyfile(encoded_keyfile.clone(), &info.password) {
|
||||
let (decoded_keyfile, our) = match keygen::decode_keyfile(&encoded_keyfile, &info.password) {
|
||||
Ok(k) => {
|
||||
let our = Identity {
|
||||
name: k.username.clone(),
|
||||
@ -504,8 +502,7 @@ async fn confirm_change_network_keys(
|
||||
}
|
||||
|
||||
// Get our name from our current keyfile
|
||||
let old_decoded_keyfile = match keygen::decode_keyfile(encoded_keyfile.clone(), &info.password)
|
||||
{
|
||||
let old_decoded_keyfile = match keygen::decode_keyfile(&encoded_keyfile, &info.password) {
|
||||
Ok(k) => {
|
||||
our.name = k.username.clone();
|
||||
k
|
||||
@ -563,7 +560,7 @@ async fn success_response(
|
||||
encoded_keyfile: Vec<u8>,
|
||||
encoded_keyfile_str: String,
|
||||
) -> Result<warp::reply::Response, Rejection> {
|
||||
let token = match generate_jwt(&decoded_keyfile.jwt_secret_bytes, our.name.clone()) {
|
||||
let token = match generate_jwt(&decoded_keyfile.jwt_secret_bytes, &our.name) {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return Ok(warp::reply::with_status(
|
||||
|
@ -146,7 +146,13 @@ pub async fn terminal(
|
||||
stdout,
|
||||
cursor::MoveTo(0, win_rows - 1),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
Print(format!("{} {}/{} {:02}:{:02} ",
|
||||
Print(format!("{}{} {}/{} {:02}:{:02} ",
|
||||
match printout.verbosity {
|
||||
0 => "",
|
||||
1 => "1️⃣ ",
|
||||
2 => "2️⃣ ",
|
||||
_ => "3️⃣ ",
|
||||
},
|
||||
now.weekday(),
|
||||
now.month(),
|
||||
now.day(),
|
||||
|
Loading…
Reference in New Issue
Block a user