Refactor, use websockify instead of self-backend server
This commit is contained in:
parent
6b10b4bc74
commit
940b71eba2
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
/target/
|
||||
/build/
|
||||
Cargo.lock
|
||||
/build/
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "axum-websockify"]
|
||||
path = axum-websockify
|
||||
url = https://github.com/HsuJv/axum-websockify.git
|
@ -1,6 +0,0 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"backend",
|
||||
"frontend",
|
||||
]
|
@ -1,11 +1,32 @@
|
||||
[tasks.install-debug]
|
||||
dependencies = ["build-debug", "member_flow"]
|
||||
dependencies = ["websockify", "wasm-debug"]
|
||||
|
||||
[tasks.install-release]
|
||||
dependencies = ["build-release", "member_flow"]
|
||||
dependencies = ["websockify", "wasm-release"]
|
||||
|
||||
[tasks.member_flow]
|
||||
run_task = { name = "member_flow", fork = true, parallel = true}
|
||||
[tasks.wasm-debug]
|
||||
script = '''
|
||||
cd ${VNC} && cargo make install-debug
|
||||
'''
|
||||
|
||||
[tasks.wasm-release]
|
||||
script = '''
|
||||
cd ${VNC} && cargo make install-release
|
||||
'''
|
||||
|
||||
|
||||
[tasks.websockify]
|
||||
dependencies = ["install-dir"]
|
||||
script = '''
|
||||
cd ${WEBSOCKIFY} && cargo build --release && cp ./target/release/${WEBSOCKIFY} $INSTALL_PATH/
|
||||
'''
|
||||
|
||||
[tasks.install-dir]
|
||||
script = '''
|
||||
mkdir -p $INSTALL_PATH
|
||||
'''
|
||||
|
||||
[env]
|
||||
INSTALL_PATH= "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/build"
|
||||
WEBSOCKIFY="axum-websockify"
|
||||
VNC="webvnc"
|
@ -1,5 +1,5 @@
|
||||
# A Remote Access Gateway
|
||||
* Full-stack project written with Rust / Yew + Actix
|
||||
* Webassembly Terminal Services written with Rust / Yew
|
||||
|
||||
## Dependencies
|
||||
|
||||
@ -25,6 +25,3 @@
|
||||
|
||||
* RDP Clients:
|
||||
- WIP
|
||||
|
||||
* Backend database
|
||||
- WIP
|
||||
|
1
axum-websockify
Submodule
1
axum-websockify
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 74229048831bc6c2d0227de68f5b1644f0d6c3f6
|
@ -1,55 +0,0 @@
|
||||
[package]
|
||||
authors = [
|
||||
"Jovi Hsu <jv.hsu@outlook.com>"
|
||||
]
|
||||
categories = ["wasm", "web-programming", "sslvpn"]
|
||||
description = ""
|
||||
edition = "2021"
|
||||
keywords = ["yew", "wasm", "wasm-bindgen", "web", "sslvpn"]
|
||||
license = "GPL3"
|
||||
name = "webgateway-be"
|
||||
readme = "README.md"
|
||||
version = "0.1.0"
|
||||
repository = "https://www.github.com/HsuJv/webgateway"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = {version="1.13.0", feature="io-util"}
|
||||
tokio-io = "0.1.13"
|
||||
tokio-core = "0.1.18"
|
||||
tokio-codec = "0.1.2"
|
||||
tokio-util = "0.6.9"
|
||||
|
||||
actix = "0.12.0"
|
||||
actix-session = "0.5.0-beta.3"
|
||||
actix-web = "4.0.0-beta.10"
|
||||
actix-files = "0.6.0-beta.8"
|
||||
actix-web-actors = "4.0.0-beta.7"
|
||||
actix-codec = "0.4"
|
||||
|
||||
urlencoding = "2.1.0"
|
||||
bytes = "1.1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
trust-dns-resolver = "0.20"
|
||||
rand = "0.8"
|
||||
rustls = "0.20.0"
|
||||
|
||||
futures = "0.3.17"
|
||||
futures-util= "0.3"
|
||||
|
||||
# log systems
|
||||
femme = "1.3"
|
||||
log = "0.4"
|
||||
async-log = "2.0.0"
|
||||
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
opt-level = 0
|
||||
|
||||
[profile.release]
|
||||
panic = 'abort'
|
||||
codegen-units = 1
|
||||
opt-level = 's'
|
||||
lto = true
|
@ -1,21 +0,0 @@
|
||||
[tasks.build-debug]
|
||||
command="cargo"
|
||||
args=["build"]
|
||||
|
||||
[tasks.build-release]
|
||||
command="cargo"
|
||||
args=["build", "--release"]
|
||||
|
||||
[tasks.install-debug]
|
||||
dependencies = ["build-debug"]
|
||||
script = '''
|
||||
mkdir -p $INSTALL_PATH
|
||||
cp $CARGO_MAKE_CRATE_TARGET_DIRECTORY/debug/$CARGO_MAKE_CRATE_NAME $INSTALL_PATH
|
||||
'''
|
||||
|
||||
[tasks.install-release]
|
||||
dependencies = ["build-release"]
|
||||
script = '''
|
||||
mkdir -p $INSTALL_PATH
|
||||
cp $CARGO_MAKE_CRATE_TARGET_DIRECTORY/release/$CARGO_MAKE_CRATE_NAME $INSTALL_PATH
|
||||
'''
|
@ -1,213 +0,0 @@
|
||||
use crate::agent::ws;
|
||||
use actix::prelude::*;
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use actix_web::web::Bytes;
|
||||
use bytes::BytesMut;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use tokio::net::{tcp::OwnedWriteHalf, TcpStream};
|
||||
use tokio_util::codec::FramedRead;
|
||||
|
||||
use log::*;
|
||||
|
||||
struct TcpCodec;
|
||||
|
||||
impl Encoder<Bytes> for TcpCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, _item: Bytes, _dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
// info!("encoding: {:?}", item);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for TcpCodec {
|
||||
type Item = Bytes;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
// info!("recv from server: {:?}", src);
|
||||
if src.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let web_bytes = Bytes::from(src.to_vec());
|
||||
src.clear();
|
||||
Ok(Some(web_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub enum AgentMsg {
|
||||
Ready(Addr<ws::WsSession>),
|
||||
SendToServer(Bytes),
|
||||
SendToClient(Bytes),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct Agent {
|
||||
id: u32,
|
||||
server_info: String,
|
||||
writer: OwnedWriteHalf,
|
||||
ws_addr: Option<Addr<ws::WsSession>>,
|
||||
pending: Vec<Bytes>,
|
||||
}
|
||||
|
||||
impl Actor for Agent {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, _ctx: &mut Self::Context) {
|
||||
info!("Agent {} started", self.id);
|
||||
// ctx.address().do_send(AgentMsg::ReadReady);
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<AgentMsg> for Agent {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: AgentMsg, ctx: &mut Context<Self>) -> Self::Result {
|
||||
match msg {
|
||||
AgentMsg::Ready(ws_addr) => {
|
||||
self.ws_addr = Some(ws_addr);
|
||||
info!("Agent {} - Websocket connect ready", self.server_info);
|
||||
for msg in self.pending.drain(..) {
|
||||
self.ws_addr
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.do_send(ws::WsMsg::SendToClient(msg));
|
||||
}
|
||||
}
|
||||
AgentMsg::SendToServer(data) => {
|
||||
let to_send = data.to_vec();
|
||||
self.writer.try_write(&to_send).unwrap();
|
||||
}
|
||||
AgentMsg::SendToClient(data) => {
|
||||
if self.ws_addr.is_some() {
|
||||
self.ws_addr
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.do_send(ws::WsMsg::SendToClient(data));
|
||||
}
|
||||
}
|
||||
AgentMsg::Shutdown => {
|
||||
info!("Agent {} - Shutdown", self.server_info);
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<Bytes, io::Error>> for Agent {
|
||||
fn handle(&mut self, msg: Result<Bytes, io::Error>, ctx: &mut Context<Self>) {
|
||||
match msg {
|
||||
Ok(data) => {
|
||||
// info!("recv from server: {:?}", data);
|
||||
if self.ws_addr.is_some() {
|
||||
ctx.address().do_send(AgentMsg::SendToClient(data));
|
||||
} else {
|
||||
info!("Websocket session not ready");
|
||||
self.pending.push(data);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("error: {:?}", err);
|
||||
if self.ws_addr.is_some() {
|
||||
self.ws_addr.as_ref().unwrap().do_send(ws::WsMsg::Close);
|
||||
}
|
||||
ctx.address().do_send(AgentMsg::Shutdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
pub async fn new(id: u32, target: (String, u16)) -> Option<Addr<Agent>> {
|
||||
let (host, port) = target;
|
||||
let server_info = format!("{}:{}", host, port);
|
||||
info!("connect to server: {}", server_info);
|
||||
let server_stream = TcpStream::connect(&server_info).await;
|
||||
if server_stream.is_err() {
|
||||
info!("connect to server failed: {}", server_info);
|
||||
}
|
||||
let server_stream = server_stream.unwrap();
|
||||
let addr = Agent::create(move |ctx| {
|
||||
let (r, w) = server_stream.into_split();
|
||||
let r = FramedRead::new(r, TcpCodec {});
|
||||
Agent::add_stream(r, ctx);
|
||||
Self {
|
||||
id,
|
||||
server_info,
|
||||
writer: w,
|
||||
ws_addr: None,
|
||||
pending: vec![],
|
||||
}
|
||||
});
|
||||
Some(addr)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MessageResponse)]
|
||||
pub enum AgentManagerResult {
|
||||
Success(Addr<Agent>),
|
||||
Failed,
|
||||
NoReturn,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "AgentManagerResult")]
|
||||
pub enum AgentManagerMsg {
|
||||
Add((u32, Addr<Agent>)),
|
||||
Get(u32),
|
||||
Del(u32),
|
||||
}
|
||||
|
||||
pub struct AgentManager {
|
||||
agents: HashMap<u32, Addr<Agent>>,
|
||||
}
|
||||
|
||||
impl AgentManager {
|
||||
pub fn new() -> Addr<Self> {
|
||||
Self {
|
||||
agents: HashMap::new(),
|
||||
}
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AgentManager {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, _ctx: &mut Context<Self>) {
|
||||
info!("AgentManager started");
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _ctx: &mut Context<Self>) {
|
||||
info!("AgentManager stopped");
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<AgentManagerMsg> for AgentManager {
|
||||
type Result = AgentManagerResult;
|
||||
|
||||
fn handle(&mut self, msg: AgentManagerMsg, _ctx: &mut Context<Self>) -> Self::Result {
|
||||
match msg {
|
||||
AgentManagerMsg::Add(addr) => {
|
||||
info!("add agent: {:?}", addr.0);
|
||||
self.agents.insert(addr.0, addr.1);
|
||||
AgentManagerResult::NoReturn
|
||||
}
|
||||
AgentManagerMsg::Get(aid) => {
|
||||
info!("get agent: {}", aid);
|
||||
if let Some(addr) = self.agents.get(&aid) {
|
||||
AgentManagerResult::Success(addr.clone())
|
||||
} else {
|
||||
AgentManagerResult::Failed
|
||||
}
|
||||
}
|
||||
AgentManagerMsg::Del(id) => {
|
||||
self.agents.remove(&id);
|
||||
AgentManagerResult::NoReturn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
pub mod agent;
|
||||
pub mod remote;
|
||||
pub mod resolver;
|
||||
pub mod ws;
|
@ -1,86 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_session::Session;
|
||||
use actix_web::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use log::info;
|
||||
use rand::Rng;
|
||||
|
||||
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(
|
||||
req: HttpRequest,
|
||||
params: web::Json<RemoteInfo>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let remote = params.into_inner();
|
||||
info!("{:?}", remote);
|
||||
let app_data = req.app_data::<Arc<crate::AppData>>().unwrap();
|
||||
|
||||
match app_data.resolver.lockup(remote.host).await {
|
||||
Some(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/remote")]
|
||||
pub async fn target_remote(
|
||||
req: HttpRequest,
|
||||
session: Session,
|
||||
params: web::Json<RemoteInfo>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let aid = rand::thread_rng().gen::<u32>();
|
||||
let app_data = req.app_data::<Arc<crate::AppData>>().unwrap();
|
||||
let remote = params.into_inner();
|
||||
let agent = agent::Agent::new(aid, (remote.ip, remote.port)).await;
|
||||
|
||||
match agent {
|
||||
Some(addr) => {
|
||||
// add to agent list
|
||||
let _ = app_data
|
||||
.agents
|
||||
.send(agent::AgentManagerMsg::Add((aid, addr)))
|
||||
.await;
|
||||
|
||||
// add session, so that the websocket can send message to the agent
|
||||
let _ = session.insert("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))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
// 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()
|
||||
// }
|
||||
// }
|
||||
use std::net::IpAddr;
|
||||
|
||||
use trust_dns_resolver::{
|
||||
config::*,
|
||||
name_server::{GenericConnection, GenericConnectionProvider, TokioRuntime},
|
||||
};
|
||||
use trust_dns_resolver::{AsyncResolver, TokioHandle};
|
||||
|
||||
use log::*;
|
||||
|
||||
pub struct DnsResolver {
|
||||
resolver: AsyncResolver<GenericConnection, GenericConnectionProvider<TokioRuntime>>,
|
||||
}
|
||||
|
||||
impl DnsResolver {
|
||||
pub fn new() -> Self {
|
||||
let resolver = AsyncResolver::new(
|
||||
ResolverConfig::default(),
|
||||
ResolverOpts::default(),
|
||||
TokioHandle,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Self { resolver }
|
||||
}
|
||||
|
||||
pub async fn lockup(&self, name: String) -> Option<IpAddr> {
|
||||
let lookup = self.resolver.lookup_ip(name.clone());
|
||||
|
||||
if let Ok(response) = lookup.await {
|
||||
if let Some(address) = response.iter().next() {
|
||||
info!("Resolved {} to {}", name, address);
|
||||
Some(address)
|
||||
} else {
|
||||
info!("Failed to resolve {}", name);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
info!("Failed to resolve {}", name);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::ActorContext;
|
||||
use actix::{Actor, Addr, Message, StreamHandler};
|
||||
use actix::{AsyncContext, Handler};
|
||||
use actix_session::Session;
|
||||
use actix_web::web::Bytes;
|
||||
use actix_web::*;
|
||||
use actix_web::{web, Error, HttpRequest, HttpResponse};
|
||||
use actix_web_actors::ws;
|
||||
use log::*;
|
||||
|
||||
use super::agent::*;
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub enum WsMsg {
|
||||
SendToClient(Bytes),
|
||||
Close,
|
||||
}
|
||||
|
||||
/// Define Websocket actor
|
||||
pub struct WsSession {
|
||||
agent: Addr<Agent>,
|
||||
}
|
||||
|
||||
impl Actor for WsSession {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// start heartbeats otherwise server will disconnect after 10 seconds
|
||||
self.agent.do_send(AgentMsg::Ready(ctx.address()));
|
||||
info!("Websocket connection is established.");
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<WsMsg> for WsSession {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: WsMsg, ctx: &mut Self::Context) {
|
||||
match msg {
|
||||
WsMsg::SendToClient(data) => {
|
||||
ctx.binary(data);
|
||||
}
|
||||
WsMsg::Close => {
|
||||
ctx.stop();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for ws::Message message
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsSession {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
match msg {
|
||||
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||
Ok(ws::Message::Text(text)) => ctx.text(text),
|
||||
Ok(ws::Message::Binary(bin)) => {
|
||||
self.agent.do_send(AgentMsg::SendToServer(bin));
|
||||
}
|
||||
Ok(ws::Message::Close(_)) => {
|
||||
self.agent.do_send(AgentMsg::Shutdown);
|
||||
ctx.stop();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/ws")]
|
||||
pub async fn ws_index(
|
||||
req: HttpRequest,
|
||||
session: Session,
|
||||
stream: web::Payload,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let aid = session.get::<u32>("aid").unwrap_or(Some(0)).unwrap();
|
||||
let app_data = req.app_data::<Arc<crate::AppData>>().unwrap();
|
||||
|
||||
let resp = match app_data
|
||||
.agents
|
||||
.send(AgentManagerMsg::Get(aid))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
AgentManagerResult::Success(agent) => ws::start(WsSession { agent }, &req, stream),
|
||||
_ => Err(actix_web::error::ErrorInternalServerError(
|
||||
"Agent not found",
|
||||
)),
|
||||
};
|
||||
|
||||
match &resp {
|
||||
Ok(resp) => info!("{:?}", resp),
|
||||
Err(e) => error!("{:?}", e),
|
||||
}
|
||||
resp
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_files as fs;
|
||||
use actix_session::CookieSession;
|
||||
use actix_web::http::{ContentEncoding, StatusCode};
|
||||
use actix_web::*;
|
||||
|
||||
use agent::{agent::AgentManager, resolver::DnsResolver};
|
||||
use log::info;
|
||||
use rand::Rng;
|
||||
use user::auth::Authenticator;
|
||||
|
||||
mod agent;
|
||||
mod user;
|
||||
|
||||
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: DnsResolver,
|
||||
authenticator: Addr<Authenticator>,
|
||||
agents: Addr<AgentManager>,
|
||||
}
|
||||
|
||||
impl AppData {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
resolver: DnsResolver::new(),
|
||||
authenticator: Authenticator::new(),
|
||||
agents: AgentManager::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)
|
||||
.start(log::LevelFilter::Warn)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(_: HttpRequest) -> Result<fs::NamedFile> {
|
||||
Ok(fs::NamedFile::open(PAGE_INDEX)?)
|
||||
}
|
||||
|
||||
async fn p404() -> Result<fs::NamedFile> {
|
||||
Ok(fs::NamedFile::open(PAGE_NOT_FOUND)?.set_status_code(StatusCode::NOT_FOUND))
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
setup_logger();
|
||||
|
||||
info!("Server starts at http://127.0.0.1:8080");
|
||||
let private_key = rand::thread_rng().gen::<[u8; 32]>();
|
||||
let app_data = Arc::new(AppData::new());
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(app_data.clone())
|
||||
.wrap(CookieSession::signed(&private_key).secure(false))
|
||||
.wrap(middleware::Compress::new(ContentEncoding::Gzip))
|
||||
.service(index)
|
||||
.service(user::auth::auth)
|
||||
.service(agent::remote::target_validate)
|
||||
.service(agent::remote::target_remote)
|
||||
.service(agent::ws::ws_index)
|
||||
.service(
|
||||
fs::Files::new("/static", STATIC_DIR)
|
||||
.prefer_utf8(true)
|
||||
.index_file(PAGE_INDEX)
|
||||
.use_etag(true)
|
||||
.default_handler(web::route().to(p404)),
|
||||
)
|
||||
.default_service(web::route().to(p404))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::{Actor, Addr, Context, Handler, Message, MessageResponse};
|
||||
use actix_web::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use log::info;
|
||||
|
||||
#[derive(MessageResponse)]
|
||||
#[allow(dead_code)]
|
||||
enum AuthResult {
|
||||
AuthSuccess,
|
||||
AuthFailure,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "AuthResult")]
|
||||
enum AuthMsg {
|
||||
DoAuth(AuthInfo),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AuthInfo {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
pub struct Authenticator;
|
||||
|
||||
impl Authenticator {
|
||||
pub fn new() -> Addr<Self> {
|
||||
Self {}.start()
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for Authenticator {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, _ctx: &mut Self::Context) {
|
||||
info!("AuthInfo started");
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
info!("AuthInfo stopped");
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<AuthMsg> for Authenticator {
|
||||
type Result = AuthResult;
|
||||
|
||||
fn handle(&mut self, msg: AuthMsg, _ctx: &mut Context<Self>) -> Self::Result {
|
||||
match msg {
|
||||
AuthMsg::DoAuth(_auth_info) => {
|
||||
// if auth_info.username == "admin" && auth_info.password == "admin" {
|
||||
// AuthResult::AuthSuccess
|
||||
// } else {
|
||||
// AuthResult::AuthFailure
|
||||
// }
|
||||
AuthResult::AuthSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/auth")]
|
||||
pub async fn auth(params: web::Json<AuthInfo>, req: HttpRequest) -> Result<HttpResponse, Error> {
|
||||
let auth_info = params.into_inner();
|
||||
let app_data = req.app_data::<Arc<crate::AppData>>().unwrap();
|
||||
let res = app_data
|
||||
.authenticator
|
||||
.send(AuthMsg::DoAuth(auth_info))
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(AuthResult::AuthSuccess) => Ok(HttpResponse::Ok().json(json!({
|
||||
"status": "success",
|
||||
}))),
|
||||
_ => Ok(HttpResponse::Ok().json(json!({
|
||||
"status": "failure",
|
||||
}))),
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
pub mod auth;
|
10
frontend/.gitignore
vendored
10
frontend/.gitignore
vendored
@ -1,10 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
@ -1,56 +0,0 @@
|
||||
[package]
|
||||
authors = [
|
||||
"Jovi Hsu <jv.hsu@outlook.com>"
|
||||
]
|
||||
categories = ["wasm", "web-programming", "sslvpn"]
|
||||
description = ""
|
||||
edition = "2021"
|
||||
keywords = ["yew", "wasm", "wasm-bindgen", "web", "sslvpn"]
|
||||
license = "GPL3"
|
||||
name = "webgateway-fe"
|
||||
readme = "README.md"
|
||||
version = "0.1.0"
|
||||
repository = "https://www.github.com/HsuJv/webgateway"
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "^0.2"
|
||||
yew = "0.18"
|
||||
js-sys = "0.3.55"
|
||||
web-sys = {version="0.3.55", features=["HtmlCanvasElement", "CanvasRenderingContext2d", "ImageData"]}
|
||||
gloo = "0.4.0"
|
||||
yew-router = "0.15"
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.6", optional = true }
|
||||
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
|
||||
# compared to the default allocator's ~10K. It is slower than the default
|
||||
# allocator, however.
|
||||
wee_alloc = { version = "0.4", optional = true }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
|
||||
magic-crypt= "3"
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook", "wee_alloc"]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.13"
|
||||
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
opt-level = 0
|
||||
|
||||
|
||||
|
||||
[profile.release]
|
||||
panic = 'abort'
|
||||
codegen-units = 1
|
||||
opt-level = 's'
|
||||
lto = true
|
@ -1,29 +0,0 @@
|
||||
[tasks.build-debug]
|
||||
command = "wasm-pack"
|
||||
args = ["build", "--target", "web", "--out-name", "wasm", "--out-dir", "./pkg", "--dev"]
|
||||
|
||||
[tasks.build-release]
|
||||
command = "wasm-pack"
|
||||
args = ["build", "--target", "web", "--out-name", "wasm", "--out-dir", "./pkg"]
|
||||
|
||||
[tasks.install-debug]
|
||||
dependencies=["build-debug", "install_wasm", "install_html"]
|
||||
|
||||
[tasks.install-release]
|
||||
dependencies=["build-release", "install_wasm", "install_html"]
|
||||
|
||||
[tasks.install_wasm]
|
||||
script = '''
|
||||
mkdir -p $FE_STATIC_INSTALL_PATH
|
||||
cp ./pkg/wasm.js $FE_STATIC_INSTALL_PATH
|
||||
cp ./pkg/wasm_bg.wasm $FE_STATIC_INSTALL_PATH
|
||||
'''
|
||||
|
||||
[tasks.install_html]
|
||||
script = '''
|
||||
mkdir -p $FE_STATIC_INSTALL_PATH
|
||||
cp static/* $FE_STATIC_INSTALL_PATH
|
||||
'''
|
||||
|
||||
[env]
|
||||
FE_STATIC_INSTALL_PATH="${INSTALL_PATH}/static"
|
@ -1,141 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::components::auth;
|
||||
use crate::pages::{page_home::PageHome, page_not_found::PageNotFound};
|
||||
use yew::html::IntoPropValue;
|
||||
use yew::prelude::*;
|
||||
use yew::services::ConsoleService;
|
||||
use yew::Component;
|
||||
use yew_router::prelude::*;
|
||||
use yew_router::{router::Router, Switch};
|
||||
|
||||
#[derive(Switch, Clone, Debug)]
|
||||
enum AppRoute {
|
||||
// #[at("/ssh/:id")]
|
||||
// Ssh(i32),
|
||||
// #[to = "/ssh"]
|
||||
// Ssh,
|
||||
#[to = "/!"]
|
||||
Home,
|
||||
#[to = ""]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl From<AppRoute> for &str {
|
||||
fn from(route: AppRoute) -> Self {
|
||||
match route {
|
||||
// AppRoute::Ssh => "/ssh",
|
||||
_ => "/",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPropValue<Option<Cow<'_, str>>> for AppRoute {
|
||||
fn into_prop_value(self: AppRoute) -> Option<Cow<'static, str>> {
|
||||
Some(Cow::Borrowed(self.into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
authdone: bool,
|
||||
link: ComponentLink<Self>,
|
||||
}
|
||||
|
||||
pub enum AppMsg {
|
||||
AuthDone,
|
||||
}
|
||||
|
||||
impl Component for App {
|
||||
type Message = AppMsg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Self {
|
||||
authdone: false,
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
AppMsg::AuthDone => self.authdone = true,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn change(&mut self, _: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
{
|
||||
|
||||
if self.authdone {
|
||||
html! {
|
||||
<>
|
||||
{self.view_nav()}
|
||||
|
||||
<main class="content">
|
||||
<Router<AppRoute>
|
||||
render = Router::render(Self::switch)
|
||||
redirect=Router::redirect(|route: Route| {
|
||||
ConsoleService::log(&format!("{:?}", route));
|
||||
AppRoute::NotFound
|
||||
})
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
}
|
||||
}
|
||||
else {
|
||||
let onauthdone = &self.link.callback(|_| AppMsg::AuthDone);
|
||||
html!{
|
||||
<auth::AuthComponents onauthdone=onauthdone/>
|
||||
}
|
||||
}
|
||||
}
|
||||
<footer class="footer">
|
||||
// { "Powered by " }
|
||||
// <a href="https://yew.rs">{ "Yew" }</a>
|
||||
</footer>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn view_nav(&self) -> Html {
|
||||
html! {
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class=classes!("navbar-menu")>
|
||||
<RouterAnchor<AppRoute> classes="navbar-item" route=AppRoute::Home>
|
||||
{ "Home" }
|
||||
</RouterAnchor<AppRoute>>
|
||||
// <RouterAnchor<AppRoute> classes="navbar-item" route=AppRoute::Ssh>
|
||||
// { "Ssh" }
|
||||
// </RouterAnchor<AppRoute>>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
fn switch(switch: AppRoute) -> Html {
|
||||
ConsoleService::log(&format!("{:?}", switch));
|
||||
match switch {
|
||||
// Route::Ssh(ip) => {
|
||||
// html! { <Ssh /> }
|
||||
// }
|
||||
// AppRoute::Ssh => {
|
||||
// html! {<PageSsh />}
|
||||
// }
|
||||
AppRoute::Home => {
|
||||
html! {<PageHome />}
|
||||
}
|
||||
AppRoute::NotFound => {
|
||||
html! { <PageNotFound /> }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
use super::input::Input;
|
||||
use anyhow;
|
||||
use serde_json::{json, Value};
|
||||
use std::fmt::Debug;
|
||||
use yew::services::{
|
||||
fetch::{FetchTask, Request},
|
||||
FetchService,
|
||||
};
|
||||
use yew::{format::Json, services::fetch::Response};
|
||||
use yew::{prelude::*, services::ConsoleService};
|
||||
pub enum AuthMsg {
|
||||
UpdateUsername(String),
|
||||
UpdatePassword(String),
|
||||
AuthRequest,
|
||||
AuthResponse(Result<Value, anyhow::Error>),
|
||||
}
|
||||
|
||||
pub struct AuthComponents {
|
||||
username: String,
|
||||
password: String,
|
||||
link: ComponentLink<Self>,
|
||||
auth_result: String,
|
||||
fetch_task: Option<FetchTask>,
|
||||
onauthdone: Callback<()>,
|
||||
}
|
||||
|
||||
impl Debug for AuthComponents {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"AuthComponents {{ username: {}, password: {} }}",
|
||||
self.username, self.password
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct AuthProps {
|
||||
#[prop_or_default]
|
||||
pub onauthdone: Callback<()>,
|
||||
}
|
||||
|
||||
impl Component for AuthComponents {
|
||||
type Message = AuthMsg;
|
||||
type Properties = AuthProps;
|
||||
|
||||
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
AuthComponents {
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
auth_result: String::new(),
|
||||
link,
|
||||
fetch_task: None,
|
||||
onauthdone: props.onauthdone,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
AuthMsg::UpdateUsername(username) => {
|
||||
self.username = username;
|
||||
self.auth_result.clear();
|
||||
}
|
||||
AuthMsg::UpdatePassword(password) => {
|
||||
self.password = password;
|
||||
self.auth_result.clear();
|
||||
}
|
||||
AuthMsg::AuthRequest => {
|
||||
let auth_info = json!({
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
});
|
||||
|
||||
// 1. build the request
|
||||
let request = Request::post("/auth")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Json(&auth_info))
|
||||
.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();
|
||||
AuthMsg::AuthResponse(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);
|
||||
}
|
||||
AuthMsg::AuthResponse(response) => {
|
||||
if let Ok(response) = response {
|
||||
self.auth_result = response["status"].to_string();
|
||||
if "\"success\"" == self.auth_result {
|
||||
self.onauthdone.emit(());
|
||||
}
|
||||
} else {
|
||||
self.auth_result = String::from("Auth failed with unknown reason");
|
||||
ConsoleService::error(&format!("{:?}", response.unwrap_err().to_string()));
|
||||
}
|
||||
// release resources
|
||||
self.fetch_task = None;
|
||||
}
|
||||
}
|
||||
// ConsoleService::log(&format!(
|
||||
// "username: {}, password {}",
|
||||
// self.username, self.password
|
||||
// ));
|
||||
true
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
let link = &self.link;
|
||||
|
||||
let update_uname = link.callback(AuthMsg::UpdateUsername);
|
||||
|
||||
let update_pword = link.callback(AuthMsg::UpdatePassword);
|
||||
|
||||
let auth_post = link.callback(|_| AuthMsg::AuthRequest);
|
||||
|
||||
html! {
|
||||
<div class="horizontal-centre vertical-centre">
|
||||
<label for="username">{"Username: "}</label>
|
||||
<Input id="username" type_="text" placeholder="Username" on_change={update_uname} />
|
||||
<br />
|
||||
<label for="password">{"Password: "}</label>
|
||||
<Input id="password" type_="password" placeholder="Password" on_change={update_pword} />
|
||||
<br />
|
||||
<button type="submit" onclick={auth_post}>{"Login"}</button>
|
||||
<br />
|
||||
{self.auth_result_view()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthComponents {
|
||||
fn auth_result_view(&self) -> Html {
|
||||
if self.fetch_task.is_some() {
|
||||
html! {
|
||||
<div>{"Authing..."}</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div>{self.auth_result.clone()}</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::utils::WeakComponentLink;
|
||||
|
||||
pub enum ClipboardMsg {
|
||||
UpdateClipboard(String),
|
||||
SendClipboard,
|
||||
}
|
||||
|
||||
pub struct Clipboard {
|
||||
link: ComponentLink<Self>,
|
||||
onsubmit: Callback<String>,
|
||||
text: String,
|
||||
}
|
||||
|
||||
// Props
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct ClipboardProps {
|
||||
#[prop_or_default]
|
||||
pub weak_link: WeakComponentLink<Clipboard>,
|
||||
pub onsubmit: Callback<String>,
|
||||
}
|
||||
|
||||
impl Component for Clipboard {
|
||||
type Message = ClipboardMsg;
|
||||
type Properties = ClipboardProps;
|
||||
|
||||
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
props.weak_link.borrow_mut().replace(link.clone());
|
||||
Clipboard {
|
||||
link,
|
||||
onsubmit: props.onsubmit,
|
||||
text: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
ClipboardMsg::UpdateClipboard(text) => {
|
||||
self.text = text;
|
||||
}
|
||||
ClipboardMsg::SendClipboard => {
|
||||
self.onsubmit.emit(self.text.clone());
|
||||
self.text.clear();
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
let update_clipboard = self.link.callback(|e: ChangeData| match e {
|
||||
ChangeData::Value(v) => ClipboardMsg::UpdateClipboard(v),
|
||||
_ => panic!("unexpected message"),
|
||||
});
|
||||
let set_clipboard = self.link.callback(|_| ClipboardMsg::SendClipboard);
|
||||
html! {
|
||||
<>
|
||||
<textarea rows="5" cols="60" id="clipboard" onchange=update_clipboard value=self.text.clone()/>
|
||||
<br/>
|
||||
<button id="clipboard-send" onclick=set_clipboard> {"Send to peer"} </button>
|
||||
<br/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
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 />
|
||||
<label for="port">{" Port: "}</label>
|
||||
<input id="port" type="text" placeholder="port" onchange={updateport}/>
|
||||
<br />
|
||||
<button onclick={connecthost}>{"Connect"}</button>
|
||||
<br />
|
||||
{self.error_msg.clone()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
// message on update
|
||||
pub enum InputMsg {
|
||||
Update(String),
|
||||
}
|
||||
|
||||
// props on_change
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct InputProps {
|
||||
pub on_change: Callback<String>,
|
||||
pub id: String,
|
||||
pub type_: String,
|
||||
pub placeholder: String,
|
||||
}
|
||||
|
||||
// component input
|
||||
pub struct Input {
|
||||
link: ComponentLink<Self>,
|
||||
on_change: Callback<String>,
|
||||
id: String,
|
||||
type_: String,
|
||||
placeholder: String,
|
||||
}
|
||||
|
||||
impl Component for Input {
|
||||
type Message = InputMsg;
|
||||
type Properties = InputProps;
|
||||
|
||||
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
Input {
|
||||
link,
|
||||
on_change: props.on_change,
|
||||
id: props.id,
|
||||
type_: props.type_,
|
||||
placeholder: props.placeholder,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
InputMsg::Update(text) => {
|
||||
self.on_change.emit(text);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
let on_change = self.link.callback(|e: ChangeData| match e {
|
||||
ChangeData::Value(v) => InputMsg::Update(v),
|
||||
_ => panic!("unexpected message"),
|
||||
});
|
||||
|
||||
html! {
|
||||
<input
|
||||
id={self.id.clone()}
|
||||
type={self.type_.clone()}
|
||||
placeholder={self.placeholder.clone()}
|
||||
onchange={on_change}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
pub mod auth;
|
||||
pub mod clipboard;
|
||||
pub mod host;
|
||||
pub mod input;
|
||||
pub mod ws;
|
@ -1,111 +0,0 @@
|
||||
use yew::prelude::*;
|
||||
use yew::services::websocket::{WebSocketService, WebSocketStatus, WebSocketTask};
|
||||
use yew::services::ConsoleService;
|
||||
use yew::{format::Binary, utils::host};
|
||||
|
||||
use crate::utils::WeakComponentLink;
|
||||
|
||||
pub struct WebsocketCtx {
|
||||
ws: Option<WebSocketTask>,
|
||||
link: ComponentLink<Self>,
|
||||
error_msg: String,
|
||||
onrecv: Callback<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct WebsocketProps {
|
||||
#[prop_or_default]
|
||||
pub onrecv: Callback<Vec<u8>>,
|
||||
pub weak_link: WeakComponentLink<WebsocketCtx>,
|
||||
}
|
||||
|
||||
pub enum WebsocketMsg {
|
||||
Connect,
|
||||
Disconnected,
|
||||
Ignore,
|
||||
Send(Binary),
|
||||
Recv(Binary),
|
||||
}
|
||||
|
||||
impl Component for WebsocketCtx {
|
||||
type Message = WebsocketMsg;
|
||||
type Properties = WebsocketProps;
|
||||
|
||||
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
props.weak_link.borrow_mut().replace(link.clone());
|
||||
Self {
|
||||
ws: None,
|
||||
link,
|
||||
error_msg: String::new(),
|
||||
onrecv: props.onrecv,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
WebsocketMsg::Connect => {
|
||||
ConsoleService::log("Connecting");
|
||||
let cbout = self.link.callback(WebsocketMsg::Recv);
|
||||
let cbnot = self.link.callback(|input| {
|
||||
ConsoleService::log(&format!("Notification: {:?}", input));
|
||||
match input {
|
||||
WebSocketStatus::Closed | WebSocketStatus::Error => {
|
||||
WebsocketMsg::Disconnected
|
||||
}
|
||||
_ => WebsocketMsg::Ignore,
|
||||
}
|
||||
});
|
||||
if self.ws.is_none() {
|
||||
let task = WebSocketService::connect_binary(
|
||||
&format!("ws://{}/ws", host().unwrap()),
|
||||
cbout,
|
||||
cbnot,
|
||||
);
|
||||
self.ws = Some(task.unwrap());
|
||||
}
|
||||
true
|
||||
}
|
||||
WebsocketMsg::Disconnected => {
|
||||
self.ws = None;
|
||||
self.error_msg = "Disconnected".to_string();
|
||||
true
|
||||
}
|
||||
WebsocketMsg::Ignore => false,
|
||||
WebsocketMsg::Send(data) => {
|
||||
if let Some(ref mut ws) = self.ws {
|
||||
ws.send_binary(data);
|
||||
}
|
||||
false
|
||||
}
|
||||
WebsocketMsg::Recv(Ok(s)) => {
|
||||
// ConsoleService::log(&format!("recv {:?}", s));
|
||||
self.onrecv.emit(s);
|
||||
false
|
||||
}
|
||||
WebsocketMsg::Recv(Err(s)) => {
|
||||
self.error_msg = format!("Error when reading from server: {}\n", &s.to_string());
|
||||
self.link.send_message(WebsocketMsg::Disconnected);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _prop: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<>
|
||||
{self.error_msg.clone()}
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered(&mut self, first_render: bool) {
|
||||
if first_render && self.ws.is_none() {
|
||||
ConsoleService::log(&"Start websocket".to_string());
|
||||
self.link.send_message(WebsocketMsg::Connect);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
mod app;
|
||||
mod components;
|
||||
mod pages;
|
||||
mod protocal;
|
||||
mod utils;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn run_app() -> Result<(), JsValue> {
|
||||
yew::start_app::<app::App>();
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
pub mod page_home;
|
||||
pub mod page_not_found;
|
||||
pub mod page_vnc;
|
@ -1,28 +0,0 @@
|
||||
use yew::prelude::*;
|
||||
use yew::ShouldRender;
|
||||
use yew::{html, Component, Html};
|
||||
|
||||
pub struct PageHome;
|
||||
|
||||
impl Component for PageHome {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<crate::pages::page_vnc::PageVnc/>
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
||||
true
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
use yew::prelude::*;
|
||||
use yew::Component;
|
||||
use yew::ShouldRender;
|
||||
|
||||
pub struct PageNotFound;
|
||||
|
||||
impl Component for PageNotFound {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
html! {
|
||||
<section class="hero is-danger is-bold is-large">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
{ "Page not found" }
|
||||
</h1>
|
||||
<h2 class="subtitle">
|
||||
{ "Page page does not seem to exist" }
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
}
|
@ -1,473 +0,0 @@
|
||||
use serde_json::{json, Value};
|
||||
use wasm_bindgen::{prelude::Closure, Clamped, JsCast, JsValue};
|
||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
|
||||
use yew::{
|
||||
format::Json,
|
||||
html,
|
||||
prelude::*,
|
||||
services::{
|
||||
fetch::{FetchTask, Request, Response},
|
||||
ConsoleService, FetchService,
|
||||
},
|
||||
};
|
||||
|
||||
use gloo::timers::callback::Interval;
|
||||
|
||||
use crate::{
|
||||
components::{self, input::Input, ws::WebsocketMsg},
|
||||
protocal::{common::*, vnc::vnc::VncHandler},
|
||||
utils::WeakComponentLink,
|
||||
};
|
||||
|
||||
pub struct PageVnc {
|
||||
link: ComponentLink<Self>,
|
||||
target: (String, u16),
|
||||
error_msg: String,
|
||||
fetch_task: Option<FetchTask>,
|
||||
connected: bool,
|
||||
handler: ProtocalHandler<VncHandler>,
|
||||
websocket: WeakComponentLink<components::ws::WebsocketCtx>,
|
||||
request_username: bool,
|
||||
request_password: bool,
|
||||
username: String,
|
||||
password: String,
|
||||
canvas: NodeRef,
|
||||
canvas_ctx: Option<CanvasRenderingContext2d>,
|
||||
interval: Option<Interval>,
|
||||
clipboard: WeakComponentLink<components::clipboard::Clipboard>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct VncProps {}
|
||||
|
||||
pub enum VncMsg {
|
||||
Connect((String, u16)),
|
||||
ConnectResp(Result<Value, anyhow::Error>),
|
||||
Connected,
|
||||
Recv(Vec<u8>),
|
||||
Send(Vec<u8>),
|
||||
UpdateUsername(String),
|
||||
UpdatePassword(String),
|
||||
UpdateClipboard(String),
|
||||
SendCredential,
|
||||
RequireFrame(u8),
|
||||
}
|
||||
|
||||
impl Component for PageVnc {
|
||||
type Message = VncMsg;
|
||||
type Properties = VncProps;
|
||||
|
||||
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||
PageVnc {
|
||||
link,
|
||||
target: (String::from(""), 0),
|
||||
error_msg: String::from(""),
|
||||
fetch_task: None,
|
||||
connected: false,
|
||||
handler: ProtocalHandler::new(),
|
||||
websocket: WeakComponentLink::default(),
|
||||
request_username: false,
|
||||
request_password: false,
|
||||
username: String::from(""),
|
||||
password: String::from(""),
|
||||
canvas: NodeRef::default(),
|
||||
canvas_ctx: None,
|
||||
interval: None,
|
||||
clipboard: WeakComponentLink::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
VncMsg::Connect(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/remote")
|
||||
.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();
|
||||
VncMsg::ConnectResp(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
|
||||
}
|
||||
VncMsg::ConnectResp(response) => {
|
||||
if let Ok(response) = response {
|
||||
self.error_msg = response["status"].to_string();
|
||||
|
||||
if "\"success\"" == self.error_msg {
|
||||
self.link.send_message(VncMsg::Connected);
|
||||
} 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
|
||||
}
|
||||
VncMsg::Connected => {
|
||||
self.connected = true;
|
||||
true
|
||||
}
|
||||
VncMsg::Recv(v) => {
|
||||
self.handler.do_input(v);
|
||||
self.protocal_out_handler()
|
||||
}
|
||||
VncMsg::Send(v) => {
|
||||
self.websocket
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send_message(WebsocketMsg::Send(Ok(v)));
|
||||
false
|
||||
}
|
||||
VncMsg::UpdateUsername(username) => {
|
||||
self.username = username;
|
||||
true
|
||||
}
|
||||
VncMsg::UpdatePassword(password) => {
|
||||
self.password = password;
|
||||
true
|
||||
}
|
||||
VncMsg::SendCredential => {
|
||||
self.request_username = false;
|
||||
self.request_password = false;
|
||||
self.handler.set_credential(&self.username, &self.password);
|
||||
self.protocal_out_handler()
|
||||
}
|
||||
VncMsg::RequireFrame(incremental) => {
|
||||
self.handler.require_frame(incremental);
|
||||
if self.interval.is_none() {
|
||||
let link = self.link.clone();
|
||||
let tick =
|
||||
Interval::new(20, move || link.send_message(VncMsg::RequireFrame(1)));
|
||||
self.interval = Some(tick);
|
||||
}
|
||||
self.protocal_out_handler()
|
||||
}
|
||||
VncMsg::UpdateClipboard(clipboard) => {
|
||||
if clipboard.len() > 0 {
|
||||
self.handler.set_clipboard(&clipboard);
|
||||
self.protocal_out_handler()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change(&mut self, _: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
if !self.connected {
|
||||
let connect_remote = self.link.callback(VncMsg::Connect);
|
||||
html! {
|
||||
<>
|
||||
<components::host::Host onsubmit=connect_remote/>
|
||||
{self.error_msg.clone()}
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
let recv_msg = self.link.callback(VncMsg::Recv);
|
||||
let clipboard_update = self.link.callback(VncMsg::UpdateClipboard);
|
||||
let websocket = &self.websocket;
|
||||
let clipboard = &self.clipboard;
|
||||
html! {
|
||||
<>
|
||||
<div class="horizontal-centre vertical-centre">
|
||||
{self.username_view()}
|
||||
{self.password_view()}
|
||||
{self.button_connect_view()}
|
||||
<components::ws::WebsocketCtx
|
||||
weak_link=websocket onrecv=recv_msg/>
|
||||
<canvas id="remote-canvas" ref=self.canvas.clone()
|
||||
tabIndex=1></canvas>
|
||||
<components::clipboard::Clipboard
|
||||
weak_link=clipboard onsubmit=clipboard_update/>
|
||||
{self.error_msg.clone()}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered(&mut self, first_render: bool) {
|
||||
if first_render {
|
||||
self.handler.set_resolution(1366, 768);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl PageRemote
|
||||
impl PageVnc {
|
||||
fn protocal_out_handler(&mut self) -> ShouldRender {
|
||||
let out = self.handler.get_output();
|
||||
let mut should_render = false;
|
||||
if !out.is_empty() {
|
||||
for o in out {
|
||||
match o {
|
||||
ProtocalHandlerOutput::Err(err) => {
|
||||
self.error_msg = err.clone();
|
||||
self.websocket
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send_message(WebsocketMsg::Disconnected);
|
||||
should_render = true;
|
||||
}
|
||||
ProtocalHandlerOutput::WsBuf(out) => {
|
||||
if out.len() > 0 {
|
||||
self.link.send_message(VncMsg::Send(out));
|
||||
}
|
||||
}
|
||||
ProtocalHandlerOutput::RequirePassword => {
|
||||
self.request_password = true;
|
||||
should_render = true;
|
||||
}
|
||||
ProtocalHandlerOutput::RenderCanvas(cr) => {
|
||||
let canvas = self.canvas.cast::<HtmlCanvasElement>().unwrap();
|
||||
let ctx = match &self.canvas_ctx {
|
||||
Some(ctx) => ctx,
|
||||
None => {
|
||||
let ctx = CanvasRenderingContext2d::from(JsValue::from(
|
||||
canvas.get_context("2d").unwrap().unwrap(),
|
||||
));
|
||||
self.canvas_ctx = Some(ctx);
|
||||
self.canvas_ctx.as_ref().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
match cr.type_ {
|
||||
1 => {
|
||||
//copy
|
||||
let sx = (cr.data[0] as u16) << 8 | cr.data[1] as u16;
|
||||
let sy = (cr.data[2] as u16) << 8 | cr.data[3] as u16;
|
||||
|
||||
let _ = ctx.
|
||||
draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
|
||||
&canvas,
|
||||
sx as f64,
|
||||
sy as f64,
|
||||
cr.width as f64,
|
||||
cr.height as f64,
|
||||
cr.x as f64,
|
||||
cr.y as f64,
|
||||
cr.width as f64,
|
||||
cr.height as f64
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let data = ImageData::new_with_u8_clamped_array_and_sh(
|
||||
Clamped(&cr.data),
|
||||
cr.width as u32,
|
||||
cr.height as u32,
|
||||
)
|
||||
.unwrap();
|
||||
// ConsoleService::log(&format!(
|
||||
// "renderring at ({}, {}), width {}, height {}",
|
||||
// cr.x, cr.y, cr.width, cr.height
|
||||
// ));
|
||||
let _ = ctx.put_image_data(&data, cr.x as f64, cr.y as f64);
|
||||
}
|
||||
}
|
||||
|
||||
should_render = true;
|
||||
}
|
||||
ProtocalHandlerOutput::SetCanvas(width, height) => {
|
||||
let canvas = self.canvas.cast::<HtmlCanvasElement>().unwrap();
|
||||
canvas.set_width(width as u32);
|
||||
canvas.set_height(height as u32);
|
||||
self.bind_mouse_and_key(&canvas);
|
||||
self.link.send_message(VncMsg::RequireFrame(0));
|
||||
let ctx = match &self.canvas_ctx {
|
||||
Some(ctx) => ctx,
|
||||
None => {
|
||||
let ctx = CanvasRenderingContext2d::from(JsValue::from(
|
||||
canvas.get_context("2d").unwrap().unwrap(),
|
||||
));
|
||||
self.canvas_ctx = Some(ctx);
|
||||
self.canvas_ctx.as_ref().unwrap()
|
||||
}
|
||||
};
|
||||
ctx.rect(0 as f64, 0 as f64, width as f64, height as f64);
|
||||
ctx.fill();
|
||||
should_render = true;
|
||||
}
|
||||
ProtocalHandlerOutput::SetClipboard(text) => {
|
||||
self.clipboard.borrow_mut().as_mut().unwrap().send_message(
|
||||
components::clipboard::ClipboardMsg::UpdateClipboard(text),
|
||||
);
|
||||
// ConsoleService::log(&self.error_msg);
|
||||
should_render = false;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
should_render
|
||||
}
|
||||
|
||||
fn username_view(&self) -> Html {
|
||||
if self.request_username {
|
||||
let update_username = self.link.callback(VncMsg::UpdateUsername);
|
||||
html! {
|
||||
<>
|
||||
<Input id="username" type_="text" placeholder="username" on_change={update_username}/>
|
||||
<br/>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
|
||||
fn password_view(&self) -> Html {
|
||||
if self.request_password {
|
||||
let update_password = self.link.callback(VncMsg::UpdatePassword);
|
||||
html! {
|
||||
<>
|
||||
<Input id="password" type_="password" placeholder="password" on_change={update_password}/>
|
||||
<br/>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
|
||||
fn button_connect_view(&self) -> Html {
|
||||
if self.request_username || self.request_password {
|
||||
let send_credential = self.link.callback(|_| VncMsg::SendCredential);
|
||||
html! {
|
||||
<>
|
||||
<button type="submit" onclick={send_credential}>{"Connect"}</button>
|
||||
<br/>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_mouse_and_key(&mut self, canvas: &HtmlCanvasElement) {
|
||||
let _window = web_sys::window().unwrap();
|
||||
let handler = self.handler.clone();
|
||||
let key_down = move |e: KeyboardEvent| {
|
||||
e.prevent_default();
|
||||
e.stop_propagation();
|
||||
handler.key_press(e, true);
|
||||
};
|
||||
|
||||
let handler = Box::new(key_down) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("keydown", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let handler = self.handler.clone();
|
||||
let key_up = move |e: KeyboardEvent| {
|
||||
e.prevent_default();
|
||||
e.stop_propagation();
|
||||
handler.key_press(e, false);
|
||||
};
|
||||
|
||||
let handler = Box::new(key_up) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("keyup", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
// On a conventional mouse, buttons 1, 2, and 3 correspond to the left,
|
||||
// middle, and right buttons on the mouse. On a wheel mouse, each step
|
||||
// of the wheel upwards is represented by a press and release of button
|
||||
// 4, and each step downwards is represented by a press and release of
|
||||
// button 5.
|
||||
|
||||
// to do:
|
||||
// calculate relation position
|
||||
let handler = self.handler.clone();
|
||||
let mouse_move = move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
handler.mouse_event(e, MouseEventType::MouseMove);
|
||||
};
|
||||
|
||||
let handler = Box::new(mouse_move) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let handler = self.handler.clone();
|
||||
let mouse_down = move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
handler.mouse_event(e, MouseEventType::MouseDown);
|
||||
};
|
||||
|
||||
let handler = Box::new(mouse_down) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("mousedown", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let handler = self.handler.clone();
|
||||
let mouse_up = move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
handler.mouse_event(e, MouseEventType::MouseUp);
|
||||
};
|
||||
|
||||
let handler = Box::new(mouse_up) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("mouseup", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let get_context_menu = move |e: MouseEvent| {
|
||||
e.prevent_default();
|
||||
e.stop_propagation();
|
||||
};
|
||||
|
||||
let handler = Box::new(get_context_menu) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("contextmenu", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
pub mod common;
|
||||
pub mod vnc;
|
@ -1,4 +0,0 @@
|
||||
mod des;
|
||||
pub mod vnc;
|
||||
mod x11cursor;
|
||||
mod x11keyboard;
|
@ -1,49 +0,0 @@
|
||||
use std::{cell::RefCell, ops::Deref, rc::Rc};
|
||||
use yew::{
|
||||
html::{ComponentLink, ImplicitClone},
|
||||
Component,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_panic_hook() {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
//
|
||||
// For more details see
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
// ComponentLink for sending message
|
||||
// Option to be default none, later assign a vaule
|
||||
// RefCell for container, mutable reference
|
||||
// Rc for multiple ownership
|
||||
pub struct WeakComponentLink<COMP: Component>(Rc<RefCell<Option<ComponentLink<COMP>>>>);
|
||||
impl<COMP: Component> Clone for WeakComponentLink<COMP> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(Rc::clone(&self.0))
|
||||
}
|
||||
}
|
||||
impl<COMP: Component> ImplicitClone for WeakComponentLink<COMP> {}
|
||||
|
||||
impl<COMP: Component> Default for WeakComponentLink<COMP> {
|
||||
fn default() -> Self {
|
||||
Self(Rc::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> Deref for WeakComponentLink<COMP> {
|
||||
type Target = Rc<RefCell<Option<ComponentLink<COMP>>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> PartialEq for WeakComponentLink<COMP> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Web Gateway</title>
|
||||
<style type="text/css">
|
||||
.navbar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #66ccff;
|
||||
}
|
||||
|
||||
.navbar .navbar-item {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.horizontal-centre {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vertical-centre {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: calc(100% - 58px - 40px);
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 58px;
|
||||
}
|
||||
</style>
|
||||
<script type="module" defer>
|
||||
import init from "/static/wasm.js";
|
||||
import { run_app } from "/static/wasm.js";
|
||||
await init();
|
||||
await run_app();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,9 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<body>
|
||||
<!-- 404 Not found -->
|
||||
<h1>404 Not found</h1>
|
||||
<p>The requested URL was not found on this server.</p>
|
||||
<p>Additionally, a 404 Not Found
|
||||
error was encountered while trying to use an ErrorDocument to handle the request.</p>
|
||||
</body>
|
2
run.sh
2
run.sh
@ -6,4 +6,4 @@ else
|
||||
cargo make install-debug
|
||||
fi
|
||||
|
||||
cd build && ./webgateway-be
|
||||
cd build && ./axum-websockify 8080 $2 --web `pwd`
|
||||
|
11
webvnc/.appveyor.yml
Normal file
11
webvnc/.appveyor.yml
Normal file
@ -0,0 +1,11 @@
|
||||
install:
|
||||
- appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
||||
- if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -V
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
test_script:
|
||||
- cargo test --locked
|
6
webvnc/.gitignore
vendored
Normal file
6
webvnc/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bin/
|
||||
pkg/
|
||||
wasm-pack.log
|
50
webvnc/Cargo.toml
Normal file
50
webvnc/Cargo.toml
Normal file
@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "webvnc"
|
||||
version = "0.1.0"
|
||||
authors = ["Jovi Hsu <jv.hsu@outlook.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.83"
|
||||
js-sys = "0.3"
|
||||
anyhow="1"
|
||||
# bytes="1"
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.6", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.22"
|
||||
features = [
|
||||
"BinaryType",
|
||||
"Blob",
|
||||
"CanvasRenderingContext2d",
|
||||
"Document",
|
||||
"ErrorEvent",
|
||||
"FileReader",
|
||||
"HtmlCanvasElement",
|
||||
"ImageData",
|
||||
"Location",
|
||||
"KeyboardEvent",
|
||||
"MouseEvent",
|
||||
"MessageEvent",
|
||||
"ProgressEvent",
|
||||
"Window",
|
||||
"WebSocket",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.13"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
25
webvnc/Makefile.toml
Normal file
25
webvnc/Makefile.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[tasks.build-debug]
|
||||
command = "wasm-pack"
|
||||
args = ["build", "--target", "web", "--out-name", "webvnc", "--out-dir", "./pkg", "--dev"]
|
||||
|
||||
[tasks.build-release]
|
||||
command = "wasm-pack"
|
||||
args = ["build", "--target", "web", "--out-name", "webvnc", "--out-dir", "./pkg"]
|
||||
|
||||
[tasks.install-debug]
|
||||
dependencies=["build-debug", "install_wasm", "install_html"]
|
||||
|
||||
[tasks.install-release]
|
||||
dependencies=["build-release", "install_wasm", "install_html"]
|
||||
|
||||
[tasks.install_wasm]
|
||||
script = '''
|
||||
mkdir -p $INSTALL_PATH
|
||||
cp ./pkg/webvnc.js $INSTALL_PATH
|
||||
cp ./pkg/webvnc_bg.wasm $INSTALL_PATH
|
||||
'''
|
||||
|
||||
[tasks.install_html]
|
||||
script = '''
|
||||
cp asserts/* $INSTALL_PATH
|
||||
'''
|
52
webvnc/asserts/vnc.html
Normal file
52
webvnc/asserts/vnc.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Web Gateway</title>
|
||||
<style type="text/css">
|
||||
.navbar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #66ccff;
|
||||
}
|
||||
|
||||
.navbar .navbar-item {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.horizontal-centre {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vertical-centre {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: calc(100% - 58px - 40px);
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 58px;
|
||||
}
|
||||
</style>
|
||||
<script type="module" defer>
|
||||
import init from "/webvnc.js";
|
||||
await init();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="horizontal-centre vertical-centre"><canvas id="vnc-canvas" tabIndex=1></canvas></div>
|
||||
</body>
|
310
webvnc/src/lib.rs
Normal file
310
webvnc/src/lib.rs
Normal file
@ -0,0 +1,310 @@
|
||||
mod utils;
|
||||
mod vnc;
|
||||
|
||||
use vnc::{MouseEventType, Vnc};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::{Clamped, JsCast};
|
||||
use web_sys::{
|
||||
ErrorEvent, HtmlCanvasElement, ImageData, KeyboardEvent, MessageEvent, MouseEvent, WebSocket,
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
|
||||
fn cancelInterval(token: f64);
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(s: &str);
|
||||
}
|
||||
|
||||
fn bind_mouse_and_key(vnc: &Vnc, canvas: &HtmlCanvasElement) {
|
||||
let _window = web_sys::window().unwrap();
|
||||
let handler = vnc.clone();
|
||||
let key_down = move |e: KeyboardEvent| {
|
||||
e.prevent_default();
|
||||
e.stop_propagation();
|
||||
handler.key_press(e, true);
|
||||
};
|
||||
|
||||
let handler = Box::new(key_down) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("keydown", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let handler = vnc.clone();
|
||||
let key_up = move |e: KeyboardEvent| {
|
||||
e.prevent_default();
|
||||
e.stop_propagation();
|
||||
handler.key_press(e, false);
|
||||
};
|
||||
|
||||
let handler = Box::new(key_up) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("keyup", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
// On a conventional mouse, buttons 1, 2, and 3 correspond to the left,
|
||||
// middle, and right buttons on the mouse. On a wheel mouse, each step
|
||||
// of the wheel upwards is represented by a press and release of button
|
||||
// 4, and each step downwards is represented by a press and release of
|
||||
// button 5.
|
||||
|
||||
// to do:
|
||||
// calculate relation position
|
||||
let handler = vnc.clone();
|
||||
let mouse_move = move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
handler.mouse_event(e, MouseEventType::MouseMove);
|
||||
};
|
||||
|
||||
let handler = Box::new(mouse_move) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let handler = vnc.clone();
|
||||
let mouse_down = move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
handler.mouse_event(e, MouseEventType::MouseDown);
|
||||
};
|
||||
|
||||
let handler = Box::new(mouse_down) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("mousedown", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let handler = vnc.clone();
|
||||
let mouse_up = move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
handler.mouse_event(e, MouseEventType::MouseUp);
|
||||
};
|
||||
|
||||
let handler = Box::new(mouse_up) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("mouseup", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
|
||||
let get_context_menu = move |e: MouseEvent| {
|
||||
e.prevent_default();
|
||||
e.stop_propagation();
|
||||
};
|
||||
|
||||
let handler = Box::new(get_context_menu) as Box<dyn FnMut(_)>;
|
||||
|
||||
let cb = Closure::wrap(handler);
|
||||
|
||||
canvas
|
||||
.add_event_listener_with_callback("contextmenu", cb.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
cb.forget();
|
||||
}
|
||||
|
||||
fn find_canvas() -> HtmlCanvasElement {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let canvas = document.get_element_by_id("vnc-canvas").unwrap();
|
||||
let canvas: web_sys::HtmlCanvasElement = canvas
|
||||
.dyn_into::<web_sys::HtmlCanvasElement>()
|
||||
.map_err(|_| ())
|
||||
.unwrap();
|
||||
canvas
|
||||
}
|
||||
|
||||
fn set_canvas(vnc: &Vnc, x: u16, y: u16) {
|
||||
let canvas = find_canvas();
|
||||
|
||||
// set hight & width
|
||||
canvas.set_height(y as u32);
|
||||
canvas.set_width(x as u32);
|
||||
|
||||
// bind keyboard & mouse
|
||||
bind_mouse_and_key(vnc, &canvas);
|
||||
|
||||
let ctx = canvas
|
||||
.get_context("2d")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::CanvasRenderingContext2d>()
|
||||
.unwrap();
|
||||
|
||||
ctx.rect(0 as f64, 0 as f64, x as f64, y as f64);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
fn vnc_out_handler(ws: &WebSocket, vnc: &Vnc) {
|
||||
let out = vnc.get_output();
|
||||
if !out.is_empty() {
|
||||
for ref o in out {
|
||||
match o {
|
||||
vnc::VncOutput::Err(err) => {
|
||||
console_log!("Err {}", err);
|
||||
}
|
||||
vnc::VncOutput::WsBuf(buf) => match ws.send_with_u8_array(&buf) {
|
||||
Ok(_) => {}
|
||||
Err(err) => console_log!("error sending message: {:?}", err),
|
||||
},
|
||||
// vnc::VncOutput::RequirePassword => {
|
||||
// self.request_password = true;
|
||||
// }
|
||||
vnc::VncOutput::RenderCanvas(cr) => {
|
||||
let canvas = find_canvas();
|
||||
let ctx = canvas
|
||||
.get_context("2d")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::CanvasRenderingContext2d>()
|
||||
.unwrap();
|
||||
|
||||
match cr.type_ {
|
||||
1 => {
|
||||
//copy
|
||||
let sx = (cr.data[0] as u16) << 8 | cr.data[1] as u16;
|
||||
let sy = (cr.data[2] as u16) << 8 | cr.data[3] as u16;
|
||||
|
||||
let _ = ctx.
|
||||
draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
|
||||
&canvas,
|
||||
sx as f64,
|
||||
sy as f64,
|
||||
cr.width as f64,
|
||||
cr.height as f64,
|
||||
cr.x as f64,
|
||||
cr.y as f64,
|
||||
cr.width as f64,
|
||||
cr.height as f64
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let data = ImageData::new_with_u8_clamped_array_and_sh(
|
||||
Clamped(&cr.data),
|
||||
cr.width as u32,
|
||||
cr.height as u32,
|
||||
)
|
||||
.unwrap();
|
||||
// ConsoleService::log(&format!(
|
||||
// "renderring at ({}, {}), width {}, height {}",
|
||||
// cr.x, cr.y, cr.width, cr.height
|
||||
// ));
|
||||
let _ = ctx.put_image_data(&data, cr.x as f64, cr.y as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
vnc::VncOutput::SetCanvas(x, y) => {
|
||||
set_canvas(&vnc, *x, *y);
|
||||
|
||||
let vnc_cloned = vnc.clone();
|
||||
let ws_cloned = ws.clone();
|
||||
let mut incremental = 0;
|
||||
|
||||
// set a interval for fps enhance
|
||||
let refresh = move || {
|
||||
vnc_cloned.require_frame(incremental);
|
||||
incremental = if incremental > 0 { incremental } else { 1 };
|
||||
vnc_out_handler(&ws_cloned, &vnc_cloned);
|
||||
};
|
||||
|
||||
let handler = Box::new(refresh) as Box<dyn FnMut()>;
|
||||
|
||||
let cb: wasm_bindgen::prelude::Closure<(dyn FnMut() + 'static)> =
|
||||
Closure::wrap(handler);
|
||||
|
||||
setInterval(&cb, 20);
|
||||
cb.forget();
|
||||
}
|
||||
// vnc::VncOutput::SetClipboard(text) => {
|
||||
// self.clipboard
|
||||
// .borrow_mut()
|
||||
// .as_mut()
|
||||
// .unwrap()
|
||||
// .send_message(components::clipboard::ClipboardMsg::UpdateClipboard(text));
|
||||
// // ConsoleService::log(&self.error_msg);
|
||||
// }
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_websocket() -> Result<(), JsValue> {
|
||||
// connect
|
||||
let url = format!(
|
||||
"{scheme}://{host}/websockify",
|
||||
scheme = if web_sys::window()
|
||||
.unwrap()
|
||||
.location()
|
||||
.protocol()?
|
||||
.starts_with("https")
|
||||
{
|
||||
"wss"
|
||||
} else {
|
||||
"ws"
|
||||
},
|
||||
host = web_sys::window().unwrap().location().host()?
|
||||
);
|
||||
let ws = WebSocket::new_with_str(&url, "binary")?;
|
||||
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
|
||||
|
||||
let vnc = Vnc::new();
|
||||
|
||||
let cloned_vnc = vnc.clone();
|
||||
// on message
|
||||
let cloned_ws = ws.clone();
|
||||
|
||||
let onmessage_callback = Closure::<dyn FnMut(_)>::new(move |e: MessageEvent| {
|
||||
if let Ok(abuf) = e.data().dyn_into::<js_sys::ArrayBuffer>() {
|
||||
let array = js_sys::Uint8Array::new(&abuf);
|
||||
// let mut canvas_ctx = None;
|
||||
cloned_vnc.do_input(array.to_vec());
|
||||
vnc_out_handler(&cloned_ws, &cloned_vnc);
|
||||
} else {
|
||||
console_log!("message event, received Unknown: {:?}", e.data());
|
||||
}
|
||||
});
|
||||
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||
// forget the callback to keep it alive
|
||||
onmessage_callback.forget();
|
||||
|
||||
// onerror
|
||||
let onerror_callback = Closure::<dyn FnMut(_)>::new(move |e: ErrorEvent| {
|
||||
console_log!("error event: {:?}", e);
|
||||
});
|
||||
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||
onerror_callback.forget();
|
||||
|
||||
// onopen
|
||||
let onopen_callback = Closure::<dyn FnMut()>::new(move || {
|
||||
console_log!("socket opened");
|
||||
});
|
||||
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||
onopen_callback.forget();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run_app() -> Result<(), JsValue> {
|
||||
start_websocket()
|
||||
}
|
10
webvnc/src/utils.rs
Normal file
10
webvnc/src/utils.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub fn set_panic_hook() {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
//
|
||||
// For more details see
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
@ -1,5 +1,18 @@
|
||||
mod des;
|
||||
mod vnc;
|
||||
mod x11cursor;
|
||||
mod x11keyboard;
|
||||
|
||||
pub enum MouseEventType {
|
||||
MouseDown,
|
||||
MouseUp,
|
||||
MouseMove,
|
||||
}
|
||||
|
||||
use std::{rc::Rc, sync::Mutex};
|
||||
|
||||
use crate::{console_log, log};
|
||||
|
||||
pub struct CanvasData {
|
||||
pub type_: u32,
|
||||
pub x: u16,
|
||||
@ -9,47 +22,23 @@ pub struct CanvasData {
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub enum MouseEventType {
|
||||
MouseDown,
|
||||
MouseUp,
|
||||
MouseMove,
|
||||
}
|
||||
|
||||
pub enum ProtocalHandlerOutput {
|
||||
pub enum VncOutput {
|
||||
WsBuf(Vec<u8>),
|
||||
Err(String),
|
||||
RequireUsername,
|
||||
RequirePassword,
|
||||
SetCanvas(u16, u16),
|
||||
RenderCanvas(CanvasData),
|
||||
SetClipboard(String),
|
||||
}
|
||||
|
||||
pub struct ProtocalHandler<T>
|
||||
where
|
||||
T: ProtocalImpl,
|
||||
{
|
||||
inner: Rc<Mutex<T>>,
|
||||
pub struct Vnc {
|
||||
inner: Rc<Mutex<vnc::Vnc>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for ProtocalHandler<T>
|
||||
where
|
||||
T: ProtocalImpl,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ProtocalHandler<T>
|
||||
where
|
||||
T: ProtocalImpl,
|
||||
{
|
||||
impl Vnc {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Rc::new(Mutex::new(T::new())),
|
||||
inner: Rc::new(Mutex::new(vnc::Vnc::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +46,7 @@ where
|
||||
self.inner.as_ref().lock().unwrap().do_input(input);
|
||||
}
|
||||
|
||||
pub fn get_output(&self) -> Vec<ProtocalHandlerOutput> {
|
||||
pub fn get_output(&self) -> Vec<VncOutput> {
|
||||
self.inner.as_ref().lock().unwrap().get_output()
|
||||
}
|
||||
|
||||
@ -69,18 +58,10 @@ where
|
||||
.set_credential(username, password);
|
||||
}
|
||||
|
||||
pub fn set_clipboard(&mut self, text: &str) {
|
||||
pub fn set_clipboard(&self, text: &str) {
|
||||
self.inner.as_ref().lock().unwrap().set_clipboard(text);
|
||||
}
|
||||
|
||||
pub fn set_resolution(&self, width: u16, height: u16) {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_resolution(width, height);
|
||||
}
|
||||
|
||||
pub fn require_frame(&self, incremental: u8) {
|
||||
self.inner
|
||||
.as_ref()
|
||||
@ -98,18 +79,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ProtocalImpl {
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
fn do_input(&mut self, input: Vec<u8>);
|
||||
fn get_output(&mut self) -> Vec<ProtocalHandlerOutput>;
|
||||
fn set_credential(&mut self, username: &str, password: &str);
|
||||
fn set_clipboard(&mut self, text: &str);
|
||||
fn set_resolution(&mut self, width: u16, height: u16);
|
||||
fn key_press(&mut self, key: web_sys::KeyboardEvent, down: bool);
|
||||
fn mouse_event(&mut self, mouse: web_sys::MouseEvent, et: MouseEventType);
|
||||
fn require_frame(&mut self, incremental: u8);
|
||||
impl Clone for Vnc {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StreamReader {
|
@ -1,11 +1,10 @@
|
||||
use crate::protocal::common::MouseEventType;
|
||||
|
||||
use super::{super::common::*, des, x11cursor::MouseUtils, x11keyboard};
|
||||
use yew::services::ConsoleService;
|
||||
use super::*;
|
||||
use super::{des, x11cursor::MouseUtils, x11keyboard, MouseEventType};
|
||||
use crate::{console_log, log};
|
||||
|
||||
const VNC_RFB33: &[u8; 12] = b"RFB 003.003\n";
|
||||
const VNC_RFB37: &[u8; 12] = b"RFB 003.007\n";
|
||||
const VNC_RFB38: &[u8; 12] = b"RFB 003.008\n";
|
||||
// const VNC_RFB37: &[u8; 12] = b"RFB 003.007\n";
|
||||
// const VNC_RFB38: &[u8; 12] = b"RFB 003.008\n";
|
||||
const VNC_VER_UNSUPPORTED: &str = "unsupported version";
|
||||
const VNC_FAILED: &str = "Connection failed with unknow reason";
|
||||
|
||||
@ -54,7 +53,7 @@ pub enum ServerMessage {
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct VncHandler {
|
||||
pub struct Vnc {
|
||||
state: VncState,
|
||||
// supported_versions: Vec<u8>,
|
||||
supported_encodings: Vec<VncEncoding>,
|
||||
@ -71,11 +70,11 @@ pub struct VncHandler {
|
||||
num_rect_left: u16,
|
||||
padding_rect: Option<VncRect>,
|
||||
outbuf: Vec<u8>,
|
||||
outs: Vec<ProtocalHandlerOutput>,
|
||||
outs: Vec<VncOutput>,
|
||||
}
|
||||
|
||||
impl ProtocalImpl for VncHandler {
|
||||
fn new() -> Self {
|
||||
impl Vnc {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: VncState::Init,
|
||||
supported_encodings: vec![
|
||||
@ -105,7 +104,8 @@ impl ProtocalImpl for VncHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn do_input(&mut self, input: Vec<u8>) {
|
||||
|
||||
pub fn do_input(&mut self, input: Vec<u8>) {
|
||||
// ConsoleService::info(&format!(
|
||||
// "VNC input {}, left {}, require {}",
|
||||
// input.len(),
|
||||
@ -123,15 +123,15 @@ impl ProtocalImpl for VncHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_output(&mut self) -> Vec<ProtocalHandlerOutput> {
|
||||
pub fn get_output(&mut self) -> Vec<VncOutput> {
|
||||
if let ServerMessage::None = self.msg_handling {
|
||||
let mut out = Vec::with_capacity(self.outs.len());
|
||||
// ConsoleService::log(&format!("Get {} output", self.outs.len()));
|
||||
// console_log!("Get {} output", self.outs.len());
|
||||
for o in self.outs.drain(..) {
|
||||
out.push(o);
|
||||
}
|
||||
if !self.outbuf.is_empty() {
|
||||
out.push(ProtocalHandlerOutput::WsBuf(self.outbuf.clone()));
|
||||
out.push(VncOutput::WsBuf(self.outbuf.clone()));
|
||||
self.outbuf.clear();
|
||||
}
|
||||
return out;
|
||||
@ -140,7 +140,7 @@ impl ProtocalImpl for VncHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_credential(&mut self, _username: &str, password: &str) {
|
||||
pub fn set_credential(&mut self, _username: &str, password: &str) {
|
||||
// referring
|
||||
// https://github.com/whitequark/rust-vnc/blob/0697238f2706dd34a9a95c1640e385f6d8c02961/src/client.rs
|
||||
// strange behavior
|
||||
@ -159,7 +159,7 @@ impl ProtocalImpl for VncHandler {
|
||||
}
|
||||
*key_i = cs;
|
||||
}
|
||||
// ConsoleService::log(&format!("challenge {:x?}", self.challenge));
|
||||
// console_log!("challenge {:x?}", self.challenge);
|
||||
let output = des::encrypt(&self.challenge, &key);
|
||||
|
||||
self.outbuf.extend_from_slice(&output);
|
||||
@ -167,15 +167,11 @@ impl ProtocalImpl for VncHandler {
|
||||
self.require = 4; // the auth result message length
|
||||
}
|
||||
|
||||
fn set_clipboard(&mut self, text: &str) {
|
||||
pub fn set_clipboard(&mut self, text: &str) {
|
||||
self.send_client_cut_text(text);
|
||||
}
|
||||
|
||||
fn set_resolution(&mut self, _width: u16, _height: u16) {
|
||||
// VNC client doen't support resolution change
|
||||
}
|
||||
|
||||
fn key_press(&mut self, key: web_sys::KeyboardEvent, down: bool) {
|
||||
pub fn key_press(&mut self, key: web_sys::KeyboardEvent, down: bool) {
|
||||
if self.state != VncState::Connected {
|
||||
return;
|
||||
}
|
||||
@ -183,7 +179,7 @@ impl ProtocalImpl for VncHandler {
|
||||
self.send_key_event(key, down);
|
||||
}
|
||||
|
||||
fn mouse_event(&mut self, mouse: web_sys::MouseEvent, et: MouseEventType) {
|
||||
pub fn mouse_event(&mut self, mouse: web_sys::MouseEvent, et: MouseEventType) {
|
||||
if self.state != VncState::Connected {
|
||||
return;
|
||||
}
|
||||
@ -191,7 +187,7 @@ impl ProtocalImpl for VncHandler {
|
||||
self.send_pointer_event(x, y, mask);
|
||||
}
|
||||
|
||||
fn require_frame(&mut self, incremental: u8) {
|
||||
pub fn require_frame(&mut self, incremental: u8) {
|
||||
if 0 == incremental {
|
||||
// first frame
|
||||
// set the client encoding
|
||||
@ -204,7 +200,7 @@ impl ProtocalImpl for VncHandler {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl VncHandler {
|
||||
impl Vnc {
|
||||
fn read_u8(&mut self) -> u8 {
|
||||
self.reader.read_u8()
|
||||
}
|
||||
@ -249,11 +245,11 @@ impl VncHandler {
|
||||
}
|
||||
}
|
||||
|
||||
impl VncHandler {
|
||||
impl Vnc {
|
||||
fn disconnect_with_err(&mut self, err: &str) {
|
||||
ConsoleService::error(err);
|
||||
console_log!("{:#?}", err);
|
||||
self.state = VncState::Disconnected;
|
||||
self.outs.push(ProtocalHandlerOutput::Err(err.to_string()));
|
||||
self.outs.push(VncOutput::Err(err.to_string()));
|
||||
}
|
||||
|
||||
fn send_client_initilize(&mut self) {
|
||||
@ -309,7 +305,7 @@ impl VncHandler {
|
||||
sw.write_u16(0); // padding
|
||||
sw.write_u32(key); // key
|
||||
|
||||
// ConsoleService::log(&format!("send key event {:x?} {:?}", key, down));
|
||||
// console_log!("send key event {:x?} {:?}", key, down);
|
||||
self.outbuf.extend_from_slice(&out);
|
||||
}
|
||||
|
||||
@ -329,7 +325,7 @@ impl VncHandler {
|
||||
sw.write_u16(x); // x
|
||||
sw.write_u16(y); // y
|
||||
|
||||
// ConsoleService::log(&format!("send mouse event {:x?} {:x?} {:#08b}", x, y, mask));
|
||||
// console_log!("send mouse event {:x?} {:x?} {:#08b}", x, y, mask);
|
||||
self.outbuf.extend_from_slice(&out);
|
||||
}
|
||||
|
||||
@ -351,7 +347,7 @@ impl VncHandler {
|
||||
sw.write_u32(len); // length
|
||||
sw.write_string(text); // text
|
||||
|
||||
// ConsoleService::log(&format!("send client cut text {:?}", len));
|
||||
// console_log!("send client cut text {:?}", len);
|
||||
self.outbuf.extend_from_slice(&out);
|
||||
}
|
||||
|
||||
@ -363,7 +359,7 @@ impl VncHandler {
|
||||
// 2 CARD16 width
|
||||
// 2 CARD16 height
|
||||
fn framebuffer_update_request(&mut self, incremental: u8) {
|
||||
// ConsoleService::log(&format!("VNC: framebuffer_update_request {}", incremental));
|
||||
// console_log!("VNC: framebuffer_update_request {}", incremental);
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
let mut sw = StreamWriter::new(&mut out);
|
||||
sw.write_u8(3);
|
||||
@ -407,7 +403,7 @@ impl VncHandler {
|
||||
}
|
||||
|
||||
fn do_authenticate(&mut self) {
|
||||
// ConsoleService::log(&format!("VNC: do_authenticate {}", self.reader.remain()));
|
||||
// console_log!("VNC: do_authenticate {}", self.reader.remain());
|
||||
if self.security_type == SecurityType::Invalid {
|
||||
let auth_type = self.read_u32();
|
||||
match auth_type {
|
||||
@ -426,13 +422,13 @@ impl VncHandler {
|
||||
let mut challenge = [0u8; 16];
|
||||
self.read_exact(&mut challenge, 16);
|
||||
self.challenge = challenge;
|
||||
self.outs.push(ProtocalHandlerOutput::RequirePassword);
|
||||
self.outs.push(VncOutput::RequirePassword);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_auth_result(&mut self) {
|
||||
let response = self.read_u32();
|
||||
ConsoleService::log(&format!("Auth resp {}", response));
|
||||
console_log!("Auth resp {}", response);
|
||||
match response {
|
||||
0 => self.send_client_initilize(),
|
||||
1 => {
|
||||
@ -456,12 +452,12 @@ impl VncHandler {
|
||||
self.read_exact(&mut pfb, 16);
|
||||
// This pixel format will be used unless the client requests a different format using the SetPixelFormat message
|
||||
self.pf = (&pfb).into();
|
||||
ConsoleService::log(&format!("VNC: {}x{}", self.width, self.height));
|
||||
console_log!("VNC: {}x{}", self.width, self.height);
|
||||
self.name = self.read_string_l32();
|
||||
self.state = VncState::Connected;
|
||||
self.require = 1; // any message from sever will be handled
|
||||
self.outs
|
||||
.push(ProtocalHandlerOutput::SetCanvas(self.width, self.height));
|
||||
.push(VncOutput::SetCanvas(self.width, self.height));
|
||||
}
|
||||
|
||||
fn handle_server_message(&mut self) {
|
||||
@ -491,7 +487,7 @@ impl VncHandler {
|
||||
fn handle_framebuffer_update(&mut self) {
|
||||
let _padding = self.read_u8();
|
||||
self.num_rect_left = self.read_u16();
|
||||
// ConsoleService::log(&format!("VNC: {} rects", self.num_rects_left));
|
||||
// console_log!("VNC: {} rects", self.num_rects_left);
|
||||
self.require = 12; // the length of the first rectangle hdr
|
||||
self.msg_handling = ServerMessage::FramebufferUpdate;
|
||||
}
|
||||
@ -562,7 +558,7 @@ impl VncHandler {
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
self.outs
|
||||
.push(ProtocalHandlerOutput::RenderCanvas(CanvasData {
|
||||
.push(VncOutput::RenderCanvas(CanvasData {
|
||||
type_: rect.encoding_type,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
@ -637,14 +633,14 @@ impl VncHandler {
|
||||
}
|
||||
self.require = self.read_u32() as usize;
|
||||
self.msg_handling = ServerMessage::ServerCutText;
|
||||
ConsoleService::log(&format!("VNC: ServerCutText {} bytes", self.require));
|
||||
console_log!("VNC: ServerCutText {} bytes", self.require);
|
||||
}
|
||||
|
||||
fn read_cut_text(&mut self) {
|
||||
let text = self.read_string(self.require);
|
||||
self.require = 1;
|
||||
self.msg_handling = ServerMessage::None;
|
||||
self.outs.push(ProtocalHandlerOutput::SetClipboard(text));
|
||||
self.outs.push(VncOutput::SetClipboard(text));
|
||||
}
|
||||
|
||||
fn handle_raw_encoding(&mut self, x: u16, y: u16, width: u16, height: u16) {
|
||||
@ -659,7 +655,7 @@ impl VncHandler {
|
||||
}
|
||||
|
||||
fn handle_copy_rect_encoding(&mut self, x: u16, y: u16, width: u16, height: u16) {
|
||||
ConsoleService::log(&format!("VNC: CopyRect {} {} {} {}", x, y, width, height));
|
||||
console_log!("VNC: CopyRect {} {} {} {}", x, y, width, height);
|
||||
self.require = 4;
|
||||
self.padding_rect = Some(VncRect {
|
||||
x,
|
@ -1,4 +1,4 @@
|
||||
use crate::protocal::common::MouseEventType;
|
||||
use super::MouseEventType;
|
||||
|
||||
pub struct MouseUtils {
|
||||
down: bool,
|
13
webvnc/tests/web.rs
Normal file
13
webvnc/tests/web.rs
Normal file
@ -0,0 +1,13 @@
|
||||
//! Test suite for the Web and headless browsers.
|
||||
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn pass() {
|
||||
assert_eq!(1 + 1, 2);
|
||||
}
|
Loading…
Reference in New Issue
Block a user