diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 42e299d..826d06f 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -23,7 +23,7 @@ actix-web-actors = "3.0.0" urlencoding = "2.1.0" serde = "1.0" serde_json = "1.0" - +trust-dns-resolver = "0.20" rand = "0.8" # log systems diff --git a/backend/src/agent/agent.rs b/backend/src/agent/agent.rs new file mode 100644 index 0000000..c89e3a3 --- /dev/null +++ b/backend/src/agent/agent.rs @@ -0,0 +1,64 @@ +use actix::{Actor, Addr, Context, Handler, Message, MessageResponse}; +use actix_web::web::Bytes; +use std::net::*; + +use log::info; +#[derive(MessageResponse)] +pub enum AgentResp { + Success, + Failed, +} + +#[derive(Message)] +#[rtype(result = "AgentResp")] +pub enum AgentMsg { + ConnectServer(SocketAddr), + SendToServer(Bytes), + SendToClient(Bytes), +} + +pub struct Agent { + id: u32, + server_info: Option, + server_stream: Option, + // client_info: SocketAddr, +} + +impl Actor for Agent { + type Context = Context; +} + +impl Handler for Agent { + type Result = AgentResp; + + fn handle(&mut self, msg: AgentMsg, _ctx: &mut Context) -> Self::Result { + match msg { + AgentMsg::ConnectServer(addr) => { + info!("connect to server: {}", addr); + self.server_info = Some(addr); + if let Ok(stream) = TcpStream::connect(addr) { + stream + .set_nonblocking(true) + .expect("set_nonblocking call failed"); + self.server_stream = Some(stream); + AgentResp::Success + } else { + AgentResp::Failed + } + } + AgentMsg::SendToServer(_data) => AgentResp::Success, + AgentMsg::SendToClient(_data) => AgentResp::Success, + } + } +} + +impl Agent { + pub fn new(id: u32) -> Addr { + Self { + id, + server_info: None, + server_stream: None, + } + .start() + } +} diff --git a/backend/src/agent/mod.rs b/backend/src/agent/mod.rs new file mode 100644 index 0000000..21052b4 --- /dev/null +++ b/backend/src/agent/mod.rs @@ -0,0 +1,4 @@ +pub mod remote; +pub mod ws; +pub mod resolver; +pub mod agent; \ No newline at end of file diff --git a/backend/src/agent/remote.rs b/backend/src/agent/remote.rs new file mode 100644 index 0000000..89223b1 --- /dev/null +++ b/backend/src/agent/remote.rs @@ -0,0 +1,88 @@ +use actix_session::Session; +use actix_web::*; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use log::info; +use rand::Rng; + +use crate::agent::resolver::*; +use crate::AppData; + +use super::agent; + +#[derive(Debug, Serialize, Deserialize)] +pub struct RemoteInfo { + #[serde(default)] + host: String, + #[serde(default)] + ip: String, + #[serde(default)] + port: u16, +} + +#[post("/target/validate")] +pub async fn target_validate( + data: web::Data, + params: web::Json, +) -> Result { + let remote = params.into_inner(); + info!("{:?}", remote); + let resolved = data.resolver.send(ResolveMsg::Resolve(remote.host)).await; + + match resolved.unwrap() { + ResolveResp::Success(ipaddr) => { + let json = json!({ + "status": "success", + "ip": ipaddr + }); + Ok(HttpResponse::Ok().json(json)) + } + _ => { + let json = json!({ + "status": "failed", + "message": "Failed to resolve the target name" + }); + Ok(HttpResponse::Ok().json(json)) + } + } +} + +#[post("/target/ssh")] +pub async fn target_ssh( + session: Session, + data: web::Data, + params: web::Json, +) -> Result { + let aid = rand::thread_rng().gen::(); + let remote = params.into_inner(); + let agent = agent::Agent::new(aid); + + match agent + .send(agent::AgentMsg::ConnectServer( + format!("{}:{}", remote.ip, remote.port).parse().unwrap(), + )) + .await + { + Ok(agent::AgentResp::Success) => { + // add to agent list + data.agents.write().unwrap().insert(aid, agent); + + // add session, so that the websocket can send message to the agent + let _ = session.set::("aid", aid); + + // send response + let json = json!({ + "status": "success", + }); + Ok(HttpResponse::Ok().json(json)) + } + _ => { + let json = json!({ + "status": "failed", + "message": "Failed to connect to the target" + }); + Ok(HttpResponse::Ok().json(json)) + } + } +} diff --git a/backend/src/agent/resolver.rs b/backend/src/agent/resolver.rs new file mode 100644 index 0000000..b1a0c6b --- /dev/null +++ b/backend/src/agent/resolver.rs @@ -0,0 +1,75 @@ +use actix::{Actor, Addr, Context, Handler, Message, MessageResponse}; + +use std::net::*; +use trust_dns_resolver::config::*; +use trust_dns_resolver::Resolver; + +use log::info; + +#[derive(MessageResponse)] +pub enum ResolveResp { + Success(IpAddr), + Failed, +} + +#[derive(Message)] +#[rtype(result = "ResolveResp")] +pub enum ResolveMsg { + Resolve(String), +} + +pub struct DnsResolver { + resolver: Resolver, +} + +impl Actor for DnsResolver { + type Context = Context; +} + +impl Handler for DnsResolver { + type Result = ResolveResp; + + fn handle(&mut self, msg: ResolveMsg, _: &mut Context) -> Self::Result { + match msg { + ResolveMsg::Resolve(name) => { + if let Ok(response) = self.resolver.lookup_ip(name.clone()) { + if let Some(address) = response.iter().next() { + info!("Resolved {} to {}", name, address); + ResolveResp::Success(address) + } else { + ResolveResp::Failed + } + } else { + info!("Failed to resolve {}", name); + ResolveResp::Failed + } + } + } + } +} + +impl DnsResolver { + pub fn new() -> Addr { + let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()).unwrap(); + + DnsResolver { resolver }.start() + } +} + +// Construct a new Resolver with default configuration options +// let mut resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()).unwrap(); + +// On Unix/Posix systems, this will read the /etc/resolv.conf +// let mut resolver = Resolver::from_system_conf().unwrap(); + +// Lookup the IP addresses associated with a name. +// let mut response = resolver.lookup_ip("www.example.com.").unwrap(); + +// There can be many addresses associated with the name, +// this can return IPv4 and/or IPv6 addresses +// let address = response.iter().next().expect("no addresses returned!"); +// if address.is_ipv4() { +// assert_eq!(address, IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34))); +// } else { +// assert_eq!(address, IpAddr::V6(Ipv6Addr::new(0x2606, 0x2800, 0x220, 0x1, 0x248, 0x1893, 0x25c8, 0x1946))); +// } diff --git a/backend/src/ws.rs b/backend/src/agent/ws.rs similarity index 52% rename from backend/src/ws.rs rename to backend/src/agent/ws.rs index ec7011c..2eee717 100644 --- a/backend/src/ws.rs +++ b/backend/src/agent/ws.rs @@ -1,18 +1,25 @@ -use actix::{Actor, StreamHandler}; +use actix::{Actor, Addr, StreamHandler}; +use actix_session::Session; use actix_web::*; use actix_web::{web, Error, HttpRequest, HttpResponse}; use actix_web_actors::ws; use log::*; -/// Define HTTP actor -struct MyWs; +use crate::AppData; -impl Actor for MyWs { +use super::agent::Agent; + +/// Define HTTP actor +struct WsSession { + agent: Addr, +} + +impl Actor for WsSession { type Context = ws::WebsocketContext; } /// Handler for ws::Message message -impl StreamHandler> for MyWs { +impl StreamHandler> for WsSession { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { match msg { Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), @@ -24,8 +31,16 @@ impl StreamHandler> for MyWs { } #[get("/ws")] -pub async fn ws_index(req: HttpRequest, stream: web::Payload) -> Result { - let resp = ws::start(MyWs {}, &req, stream); +pub async fn ws_index( + req: HttpRequest, + session: Session, + data: web::Data, + stream: web::Payload, +) -> Result { + let aid = session.get::("aid").unwrap_or(Some(0)).unwrap(); + let agent = data.agents.read().unwrap().get(&aid).unwrap().clone(); + let resp = ws::start(WsSession { agent }, &req, stream); + match &resp { Ok(resp) => info!("{:?}", resp), Err(e) => error!("{:?}", e), diff --git a/backend/src/main.rs b/backend/src/main.rs index 83b975d..f5827ec 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,24 +1,43 @@ +use std::{collections::HashMap, sync::RwLock}; + +use actix::Addr; use actix_files as fs; use actix_session::CookieSession; use actix_web::http::{ContentEncoding, StatusCode}; use actix_web::*; +use agent::{agent::Agent, resolver::DnsResolver}; use log::info; use rand::Rng; +mod agent; mod user; -mod ws; - -// pub struct AppState ; - -// impl Actor for AppState { -// type Context = actix::Context; -// } const STATIC_DIR: &str = "./static/"; const PAGE_INDEX: &str = "./static/index.html"; const PAGE_NOT_FOUND: &str = "./static/p404.html"; +pub struct AppData { + // session: CookieSession, + resolver: Addr, + agents: RwLock>>, +} + +impl AppData { + pub fn new() -> Self { + Self { + resolver: DnsResolver::new(), + agents: RwLock::new(HashMap::new()), + } + } +} + +impl Default for AppData { + fn default() -> Self { + Self::new() + } +} + fn setup_logger() { let logger = femme::pretty::Logger::new(); async_log::Logger::wrap(logger, || 12) @@ -43,12 +62,14 @@ async fn main() -> std::io::Result<()> { let private_key = rand::thread_rng().gen::<[u8; 32]>(); HttpServer::new(move || { App::new() - // .data(AppState) + .data(AppData::new()) .wrap(CookieSession::signed(&private_key).secure(false)) .wrap(middleware::Compress::new(ContentEncoding::Gzip)) .service(index) - .service(ws::ws_index) .service(user::auth::auth) + .service(agent::remote::target_validate) + .service(agent::remote::target_ssh) + .service(agent::ws::ws_index) .service( fs::Files::new("/static", STATIC_DIR) .prefer_utf8(true) diff --git a/backend/src/user/auth.rs b/backend/src/user/auth.rs index a471af6..64a41aa 100644 --- a/backend/src/user/auth.rs +++ b/backend/src/user/auth.rs @@ -1,20 +1,24 @@ use actix::{Actor, Context, Handler, Message, MessageResponse}; +use actix_session::Session; use actix_web::*; use serde::{Deserialize, Serialize}; use serde_json::json; use log::info; -#[derive(Message)] -#[rtype(result = "Self")] #[derive(MessageResponse)] #[allow(dead_code)] -enum AuthMessage { - DoAuth, +enum AuthResp { AuthSuccess, AuthFailure, } +#[derive(Message)] +#[rtype(result = "AuthResp")] +enum AuthMsg { + DoAuth, +} + #[derive(Serialize, Deserialize, Debug)] pub struct AuthInfo { username: String, @@ -34,12 +38,12 @@ impl Actor for AuthInfo { } } -impl Handler for AuthInfo { - type Result = AuthMessage; +impl Handler for AuthInfo { + type Result = AuthResp; - fn handle(&mut self, _msg: AuthMessage, _ctx: &mut Context) -> Self::Result { + fn handle(&mut self, _msg: AuthMsg, _ctx: &mut Context) -> Self::Result { info!("AuthInfo handle"); - AuthMessage::AuthSuccess + AuthResp::AuthSuccess } } @@ -47,15 +51,12 @@ impl Handler for AuthInfo { pub async fn auth(params: web::Json) -> Result { let auth = params.into_inner(); let auth_addr = auth.start(); - let res = auth_addr.send(AuthMessage::DoAuth).await; + let res = auth_addr.send(AuthMsg::DoAuth).await; match res { - Ok(AuthMessage::AuthSuccess) => Ok(HttpResponse::Ok().json(json!({ + Ok(AuthResp::AuthSuccess) => Ok(HttpResponse::Ok().json(json!({ "status": "success", }))), - Ok(AuthMessage::AuthFailure) => Ok(HttpResponse::Ok().json(json!({ - "status": "failure", - }))), _ => Ok(HttpResponse::Ok().json(json!({ "status": "failure", }))), diff --git a/frontend/src/components/host.rs b/frontend/src/components/host.rs new file mode 100644 index 0000000..a1aa19b --- /dev/null +++ b/frontend/src/components/host.rs @@ -0,0 +1,142 @@ +use serde_json::{json, Value}; +use yew::{ + format::Json, + prelude::*, + services::{ + fetch::{FetchTask, Request, Response}, + ConsoleService, FetchService, + }, +}; + +pub enum HostMsg { + UpdateHost(String), + UpdatePort(String), + ValidateResponse(Result), + ConnectHost, +} + +pub struct Host { + link: ComponentLink, + host: String, + port: u16, + error_msg: String, + onsubmit: Callback<(String, u16)>, + fetch_task: Option, +} + +// Props +#[derive(Clone, PartialEq, Properties)] +pub struct HostProps { + #[prop_or_default] + pub onsubmit: Callback<(String, u16)>, +} + +impl Component for Host { + type Message = HostMsg; + type Properties = HostProps; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + Host { + link, + host: "".to_string(), + port: 0, + error_msg: "".to_string(), + onsubmit: props.onsubmit, + fetch_task: None, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + HostMsg::UpdateHost(host) => { + self.host = host; + true + } + HostMsg::UpdatePort(port) => match port.parse::() { + Ok(port) => { + self.port = port; + true + } + Err(_) => { + self.error_msg = "Port must be a number".to_string(); + true + } + }, + HostMsg::ValidateResponse(response) => { + if let Ok(response) = response { + self.error_msg = response["status"].to_string(); + + if "\"success\"" == self.error_msg { + let mut ip = response["ip"].to_string(); + let _ = ip.pop(); + let _ = ip.remove(0); + self.onsubmit.emit((ip, self.port)); + } else { + self.error_msg = response["message"].to_string(); + } + } else { + self.error_msg = String::from("Valid host failed with unknown reason"); + ConsoleService::error(&format!("{:?}", response.unwrap_err().to_string())); + } + // release resources + self.fetch_task = None; + true + } + HostMsg::ConnectHost => { + let to_post = json!({ + "host": self.host, + }); + + // 1. build the request + let request = Request::post("/target/validate") + .header("Content-Type", "application/json") + .body(Json(&to_post)) + .expect("Could not build auth request."); + // 2. construct a callback + let callback = + self.link + .callback(|response: Response>>| { + // ConsoleService::error(&format!("{:?}", response)); + let Json(data) = response.into_body(); + HostMsg::ValidateResponse(data) + }); + // 3. pass the request and callback to the fetch service + let task = FetchService::fetch(request, callback).expect("failed to start request"); + // 4. store the task so it isn't canceled immediately + self.fetch_task = Some(task); + true + } + } + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + let updatehost = self.link.callback(|e: ChangeData| match e { + ChangeData::Value(val) => HostMsg::UpdateHost(val), + _ => panic!("unexpected message"), + }); + + let updateport = self.link.callback(|e: ChangeData| match e { + ChangeData::Value(val) => HostMsg::UpdatePort(val), + _ => panic!("unexpected message"), + }); + + let connecthost = self.link.callback(|_| HostMsg::ConnectHost); + + html! { +
+ + +
+ +
+ +
+ {self.error_msg.clone()} +
+ } + } +} diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index 0e4a05d..a7832bd 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -1 +1,2 @@ pub mod auth; +pub mod host; \ No newline at end of file diff --git a/frontend/src/pages/page_ssh.rs b/frontend/src/pages/page_ssh.rs index e958909..679cff1 100644 --- a/frontend/src/pages/page_ssh.rs +++ b/frontend/src/pages/page_ssh.rs @@ -1,19 +1,94 @@ -use yew::prelude::*; +use serde_json::{json, Value}; +use yew::{ + format::Json, + prelude::*, + services::{ + fetch::{FetchTask, Request, Response}, + ConsoleService, FetchService, + }, +}; -pub struct PageSsh {} +use crate::components; -pub enum Msg {} +pub struct PageSsh { + link: ComponentLink, + target: (String, u16), + error_msg: String, + fetch_task: Option, + connected: bool, +} + +pub enum SshMsg { + SshConnect((String, u16)), + SshConnectResp(Result), + SshConnected, +} impl Component for PageSsh { - type Message = Msg; + type Message = SshMsg; type Properties = (); - fn create(_: Self::Properties, _: ComponentLink) -> Self { - PageSsh {} + fn create(_: Self::Properties, link: ComponentLink) -> Self { + PageSsh { + link, + target: (String::from(""), 0), + error_msg: String::from(""), + fetch_task: None, + connected: false, + } } - fn update(&mut self, _msg: Self::Message) -> ShouldRender { - true + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + SshMsg::SshConnect(target) => { + self.target = target; + // ConsoleService::log(&self.target); + let to_post = json!({ + "ip": self.target.0, + "port": self.target.1, + }); + + // 1. build the request + let request = Request::post("/target/ssh") + .header("Content-Type", "application/json") + .body(Json(&to_post)) + .expect("Could not build auth request."); + // 2. construct a callback + let callback = + self.link + .callback(|response: Response>>| { + // ConsoleService::error(&format!("{:?}", response)); + let Json(data) = response.into_body(); + SshMsg::SshConnectResp(data) + }); + // 3. pass the request and callback to the fetch service + let task = FetchService::fetch(request, callback).expect("failed to start request"); + // 4. store the task so it isn't canceled immediately + self.fetch_task = Some(task); + true + } + SshMsg::SshConnectResp(response) => { + if let Ok(response) = response { + self.error_msg = response["status"].to_string(); + + if "\"success\"" == self.error_msg { + self.link.send_message(SshMsg::SshConnected); + } else { + self.error_msg = response["message"].to_string(); + } + } else { + self.error_msg = String::from("Connect host failed with unknown reason"); + ConsoleService::error(&format!("{:?}", response.unwrap_err().to_string())); + } + // release resources + self.fetch_task = None; + true + } + SshMsg::SshConnected => { + self.connected = true; + true + } + } } fn change(&mut self, _: Self::Properties) -> ShouldRender { @@ -21,8 +96,18 @@ impl Component for PageSsh { } fn view(&self) -> Html { - html! { -

{ "Hello ssh!\n\n\n\n" }

+ let connect_ssh = self.link.callback(SshMsg::SshConnect); + if !self.connected { + html! { + <> + + {self.error_msg.clone()} + + } + } else { + html! { + <> + } } } }