Refactor, use websockify instead of self-backend server

This commit is contained in:
Jovi Hsu 2022-09-23 04:30:56 +00:00
parent 6b10b4bc74
commit 940b71eba2
50 changed files with 574 additions and 2327 deletions

3
.gitignore vendored
View File

@ -1,3 +1,2 @@
/target/
/build/
Cargo.lock
/build/

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "axum-websockify"]
path = axum-websockify
url = https://github.com/HsuJv/axum-websockify.git

View File

@ -1,6 +0,0 @@
[workspace]
members = [
"backend",
"frontend",
]

View File

@ -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"

View File

@ -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

@ -0,0 +1 @@
Subproject commit 74229048831bc6c2d0227de68f5b1644f0d6c3f6

View File

@ -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

View File

@ -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
'''

View File

@ -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
}
}
}
}

View File

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

View File

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

View File

@ -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
}
}
}

View File

@ -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
}

View File

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

View File

@ -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",
}))),
}
}

View File

@ -1 +0,0 @@
pub mod auth;

10
frontend/.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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 /> }
}
}
}
}

View File

@ -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>
}
}
}
}

View File

@ -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/>
</>
}
}
}

View File

@ -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>
}
}
}

View File

@ -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}
/>
}
}
}

View File

@ -1,5 +0,0 @@
pub mod auth;
pub mod clipboard;
pub mod host;
pub mod input;
pub mod ws;

View File

@ -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);
}
}
}

View File

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

View File

@ -1,3 +0,0 @@
pub mod page_home;
pub mod page_not_found;
pub mod page_vnc;

View File

@ -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
}
}

View File

@ -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
}
}

View File

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

View File

@ -1,2 +0,0 @@
pub mod common;
pub mod vnc;

View File

@ -1,4 +0,0 @@
mod des;
pub mod vnc;
mod x11cursor;
mod x11keyboard;

View File

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

View File

@ -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>

View File

@ -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
View File

@ -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
View 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
View File

@ -0,0 +1,6 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

50
webvnc/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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();
}

View File

@ -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 {

View File

@ -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,

View File

@ -1,4 +1,4 @@
use crate::protocal::common::MouseEventType;
use super::MouseEventType;
pub struct MouseUtils {
down: bool,

13
webvnc/tests/web.rs Normal file
View 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);
}