dummy ssh connect

This commit is contained in:
Jovi Hsu 2021-11-05 14:50:05 +08:00
parent c87b60a738
commit bec3952861
11 changed files with 536 additions and 40 deletions

View File

@ -23,7 +23,7 @@ actix-web-actors = "3.0.0"
urlencoding = "2.1.0" urlencoding = "2.1.0"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
trust-dns-resolver = "0.20"
rand = "0.8" rand = "0.8"
# log systems # log systems

View File

@ -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<SocketAddr>,
server_stream: Option<TcpStream>,
// client_info: SocketAddr,
}
impl Actor for Agent {
type Context = Context<Self>;
}
impl Handler<AgentMsg> for Agent {
type Result = AgentResp;
fn handle(&mut self, msg: AgentMsg, _ctx: &mut Context<Self>) -> 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<Agent> {
Self {
id,
server_info: None,
server_stream: None,
}
.start()
}
}

4
backend/src/agent/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod remote;
pub mod ws;
pub mod resolver;
pub mod agent;

View File

@ -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<AppData>,
params: web::Json<RemoteInfo>,
) -> Result<HttpResponse, Error> {
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<AppData>,
params: web::Json<RemoteInfo>,
) -> Result<HttpResponse, Error> {
let aid = rand::thread_rng().gen::<u32>();
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::<u32>("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))
}
}
}

View File

@ -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<Self>;
}
impl Handler<ResolveMsg> for DnsResolver {
type Result = ResolveResp;
fn handle(&mut self, msg: ResolveMsg, _: &mut Context<Self>) -> 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<Self> {
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)));
// }

View File

@ -1,18 +1,25 @@
use actix::{Actor, StreamHandler}; use actix::{Actor, Addr, StreamHandler};
use actix_session::Session;
use actix_web::*; use actix_web::*;
use actix_web::{web, Error, HttpRequest, HttpResponse}; use actix_web::{web, Error, HttpRequest, HttpResponse};
use actix_web_actors::ws; use actix_web_actors::ws;
use log::*; use log::*;
/// Define HTTP actor use crate::AppData;
struct MyWs;
impl Actor for MyWs { use super::agent::Agent;
/// Define HTTP actor
struct WsSession {
agent: Addr<Agent>,
}
impl Actor for WsSession {
type Context = ws::WebsocketContext<Self>; type Context = ws::WebsocketContext<Self>;
} }
/// Handler for ws::Message message /// Handler for ws::Message message
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs { impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsSession {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) { fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg { match msg {
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
@ -24,8 +31,16 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
} }
#[get("/ws")] #[get("/ws")]
pub async fn ws_index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> { pub async fn ws_index(
let resp = ws::start(MyWs {}, &req, stream); req: HttpRequest,
session: Session,
data: web::Data<AppData>,
stream: web::Payload,
) -> Result<HttpResponse, Error> {
let aid = session.get::<u32>("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 { match &resp {
Ok(resp) => info!("{:?}", resp), Ok(resp) => info!("{:?}", resp),
Err(e) => error!("{:?}", e), Err(e) => error!("{:?}", e),

View File

@ -1,24 +1,43 @@
use std::{collections::HashMap, sync::RwLock};
use actix::Addr;
use actix_files as fs; use actix_files as fs;
use actix_session::CookieSession; use actix_session::CookieSession;
use actix_web::http::{ContentEncoding, StatusCode}; use actix_web::http::{ContentEncoding, StatusCode};
use actix_web::*; use actix_web::*;
use agent::{agent::Agent, resolver::DnsResolver};
use log::info; use log::info;
use rand::Rng; use rand::Rng;
mod agent;
mod user; mod user;
mod ws;
// pub struct AppState ;
// impl Actor for AppState {
// type Context = actix::Context<Self>;
// }
const STATIC_DIR: &str = "./static/"; const STATIC_DIR: &str = "./static/";
const PAGE_INDEX: &str = "./static/index.html"; const PAGE_INDEX: &str = "./static/index.html";
const PAGE_NOT_FOUND: &str = "./static/p404.html"; const PAGE_NOT_FOUND: &str = "./static/p404.html";
pub struct AppData {
// session: CookieSession,
resolver: Addr<DnsResolver>,
agents: RwLock<HashMap<u32, Addr<Agent>>>,
}
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() { fn setup_logger() {
let logger = femme::pretty::Logger::new(); let logger = femme::pretty::Logger::new();
async_log::Logger::wrap(logger, || 12) 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]>(); let private_key = rand::thread_rng().gen::<[u8; 32]>();
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
// .data(AppState) .data(AppData::new())
.wrap(CookieSession::signed(&private_key).secure(false)) .wrap(CookieSession::signed(&private_key).secure(false))
.wrap(middleware::Compress::new(ContentEncoding::Gzip)) .wrap(middleware::Compress::new(ContentEncoding::Gzip))
.service(index) .service(index)
.service(ws::ws_index)
.service(user::auth::auth) .service(user::auth::auth)
.service(agent::remote::target_validate)
.service(agent::remote::target_ssh)
.service(agent::ws::ws_index)
.service( .service(
fs::Files::new("/static", STATIC_DIR) fs::Files::new("/static", STATIC_DIR)
.prefer_utf8(true) .prefer_utf8(true)

View File

@ -1,20 +1,24 @@
use actix::{Actor, Context, Handler, Message, MessageResponse}; use actix::{Actor, Context, Handler, Message, MessageResponse};
use actix_session::Session;
use actix_web::*; use actix_web::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use log::info; use log::info;
#[derive(Message)]
#[rtype(result = "Self")]
#[derive(MessageResponse)] #[derive(MessageResponse)]
#[allow(dead_code)] #[allow(dead_code)]
enum AuthMessage { enum AuthResp {
DoAuth,
AuthSuccess, AuthSuccess,
AuthFailure, AuthFailure,
} }
#[derive(Message)]
#[rtype(result = "AuthResp")]
enum AuthMsg {
DoAuth,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct AuthInfo { pub struct AuthInfo {
username: String, username: String,
@ -34,12 +38,12 @@ impl Actor for AuthInfo {
} }
} }
impl Handler<AuthMessage> for AuthInfo { impl Handler<AuthMsg> for AuthInfo {
type Result = AuthMessage; type Result = AuthResp;
fn handle(&mut self, _msg: AuthMessage, _ctx: &mut Context<Self>) -> Self::Result { fn handle(&mut self, _msg: AuthMsg, _ctx: &mut Context<Self>) -> Self::Result {
info!("AuthInfo handle"); info!("AuthInfo handle");
AuthMessage::AuthSuccess AuthResp::AuthSuccess
} }
} }
@ -47,15 +51,12 @@ impl Handler<AuthMessage> for AuthInfo {
pub async fn auth(params: web::Json<AuthInfo>) -> Result<HttpResponse, Error> { pub async fn auth(params: web::Json<AuthInfo>) -> Result<HttpResponse, Error> {
let auth = params.into_inner(); let auth = params.into_inner();
let auth_addr = auth.start(); let auth_addr = auth.start();
let res = auth_addr.send(AuthMessage::DoAuth).await; let res = auth_addr.send(AuthMsg::DoAuth).await;
match res { match res {
Ok(AuthMessage::AuthSuccess) => Ok(HttpResponse::Ok().json(json!({ Ok(AuthResp::AuthSuccess) => Ok(HttpResponse::Ok().json(json!({
"status": "success", "status": "success",
}))), }))),
Ok(AuthMessage::AuthFailure) => Ok(HttpResponse::Ok().json(json!({
"status": "failure",
}))),
_ => Ok(HttpResponse::Ok().json(json!({ _ => Ok(HttpResponse::Ok().json(json!({
"status": "failure", "status": "failure",
}))), }))),

View File

@ -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<Value, anyhow::Error>),
ConnectHost,
}
pub struct Host {
link: ComponentLink<Self>,
host: String,
port: u16,
error_msg: String,
onsubmit: Callback<(String, u16)>,
fetch_task: Option<FetchTask>,
}
// 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>) -> 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::<u16>() {
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<Json<Result<Value, anyhow::Error>>>| {
// 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! {
<div class="horizontal-centre vertical-centre">
<label for="hostname">{"Hostname: "}</label>
<input id="hostname" type="text" placeholder="hostname" onchange={updatehost} />
<br />
<input id="port" type="text" placeholder="port" onchange={updateport}/>
<br />
<button onclick={connecthost}>{"Connect"}</button>
<br />
{self.error_msg.clone()}
</div>
}
}
}

View File

@ -1 +1,2 @@
pub mod auth; pub mod auth;
pub mod host;

View File

@ -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<Self>,
target: (String, u16),
error_msg: String,
fetch_task: Option<FetchTask>,
connected: bool,
}
pub enum SshMsg {
SshConnect((String, u16)),
SshConnectResp(Result<Value, anyhow::Error>),
SshConnected,
}
impl Component for PageSsh { impl Component for PageSsh {
type Message = Msg; type Message = SshMsg;
type Properties = (); type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self { fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
PageSsh {} PageSsh {
link,
target: (String::from(""), 0),
error_msg: String::from(""),
fetch_task: None,
connected: false,
}
} }
fn update(&mut self, _msg: Self::Message) -> ShouldRender { fn update(&mut self, msg: Self::Message) -> ShouldRender {
true 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<Json<Result<Value, anyhow::Error>>>| {
// 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 { fn change(&mut self, _: Self::Properties) -> ShouldRender {
@ -21,8 +96,18 @@ impl Component for PageSsh {
} }
fn view(&self) -> Html { fn view(&self) -> Html {
html! { let connect_ssh = self.link.callback(SshMsg::SshConnect);
<p>{ "Hello ssh!\n\n\n\n" }</p> if !self.connected {
html! {
<>
<components::host::Host onsubmit=connect_ssh/>
{self.error_msg.clone()}
</>
}
} else {
html! {
<></>
}
} }
} }
} }