Initial implementation of clipboard forwarding
This commit is contained in:
parent
fb86cbd0de
commit
a40a493d39
7 changed files with 776 additions and 44 deletions
|
|
@ -1,8 +1,10 @@
|
|||
use crate::message::{Message, MessageReader, MessageWriter};
|
||||
use anyhow::{bail, Result};
|
||||
use bytes::BytesMut;
|
||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use log::LevelFilter;
|
||||
use log::{debug, error, info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
use tokio::io::{
|
||||
AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite,
|
||||
|
|
@ -15,6 +17,14 @@ use tokio::sync::mpsc;
|
|||
mod config;
|
||||
mod ui;
|
||||
|
||||
// 256MB clipboard. Operating systems do not generally have a maximum size
|
||||
// but I need to buffer it all in memory in order to use the `copypasta`
|
||||
// crate and so I want to set a limit somewhere and this seems.... rational.
|
||||
// You should use another transfer mechanism other than this if you need more
|
||||
// than 256 MB of data. (Obviously future me will come along and shake his
|
||||
// head at the limitations of my foresight, but oh well.)
|
||||
const MAX_CLIPBOARD_SIZE: usize = 256 * 1024 * 1024;
|
||||
|
||||
/// Wait for the server to be ready; we know the server is there and
|
||||
/// listening when we see the special sync marker, which is 8 NUL bytes in a
|
||||
/// row.
|
||||
|
|
@ -162,25 +172,22 @@ async fn client_handle_connection(
|
|||
/// Listen on a port that we are currently forwarding, and use the SOCKS5
|
||||
/// proxy on the specified port to handle the connections.
|
||||
async fn client_listen(port: u16, socks_port: u16) -> Result<()> {
|
||||
let listener =
|
||||
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)).await?;
|
||||
loop {
|
||||
let listener =
|
||||
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port))
|
||||
.await?;
|
||||
loop {
|
||||
// The second item contains the IP and port of the new
|
||||
// connection, but we don't care.
|
||||
let (socket, _) = listener.accept().await?;
|
||||
// The second item contains the IP and port of the new
|
||||
// connection, but we don't care.
|
||||
let (socket, _) = listener.accept().await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
client_handle_connection(socks_port, port, socket).await
|
||||
{
|
||||
error!("Error handling connection: {:?}", e);
|
||||
} else {
|
||||
debug!("Done???");
|
||||
}
|
||||
});
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
client_handle_connection(socks_port, port, socket).await
|
||||
{
|
||||
error!("Error handling connection: {:?}", e);
|
||||
} else {
|
||||
debug!("Done???");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,20 +195,60 @@ async fn client_handle_messages<T: AsyncRead + Unpin>(
|
|||
mut reader: MessageReader<T>,
|
||||
events: mpsc::Sender<ui::UIEvent>,
|
||||
) -> Result<()> {
|
||||
let mut clipboard_messages = HashMap::new();
|
||||
loop {
|
||||
use Message::*;
|
||||
match reader.read().await? {
|
||||
Ping => (),
|
||||
|
||||
Ports(ports) => {
|
||||
if let Err(_) = events.send(ui::UIEvent::Ports(ports)).await {
|
||||
// TODO: Log
|
||||
if let Err(e) = events.send(ui::UIEvent::Ports(ports)).await {
|
||||
error!("Error sending ports request: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Browse(url) => {
|
||||
// TODO: Uh, security?
|
||||
info!("Browsing to {url}...");
|
||||
_ = open::that(url);
|
||||
}
|
||||
|
||||
ClipStart(id) => {
|
||||
info!("Starting clip op {id}");
|
||||
clipboard_messages.insert(id, Vec::new());
|
||||
}
|
||||
|
||||
ClipData(id, mut data) => match clipboard_messages.get_mut(&id) {
|
||||
Some(bytes) => {
|
||||
info!(
|
||||
"Received data for clip op {id} ({len} bytes)",
|
||||
len = data.len()
|
||||
);
|
||||
if bytes.len() < MAX_CLIPBOARD_SIZE {
|
||||
bytes.append(&mut data);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Received data for unknown clip op {id}");
|
||||
}
|
||||
},
|
||||
|
||||
ClipEnd(id) => {
|
||||
let Some(data) = clipboard_messages.remove(&id) else {
|
||||
warn!("Received end for unknown clip op {id}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(data) = String::from_utf8(data) else {
|
||||
warn!("Received invalid utf8 for clipboard on op {id}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
if let Err(e) = ctx.set_contents(data) {
|
||||
error!("Unable to set clipboard data for op {id}: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
message => error!("Unsupported: {:?}", message),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ mod server;
|
|||
|
||||
pub use client::run_client;
|
||||
pub use reverse::browse_url;
|
||||
pub use reverse::clip_file;
|
||||
pub use server::run_server;
|
||||
|
|
|
|||
26
src/main.rs
26
src/main.rs
|
|
@ -14,6 +14,10 @@ connect to.
|
|||
On a server that already has a client connected to it you can use `fwd browse
|
||||
<url>` to open `<url>` in the default browser of the client.
|
||||
|
||||
On a server that already has a client connected to it you can use `fwd clip`
|
||||
to read stdin and send it to the clipboard of the client, or `fwd clip <file>`
|
||||
to send the the contents of `file`.
|
||||
|
||||
Options:
|
||||
--version Print the version of fwd and exit
|
||||
--sudo, -s Run the server side of fwd with `sudo`. This allows the
|
||||
|
|
@ -32,6 +36,7 @@ enum Args {
|
|||
Server,
|
||||
Client(String, bool),
|
||||
Browse(String),
|
||||
Clip(Option<String>),
|
||||
Error,
|
||||
}
|
||||
|
||||
|
|
@ -60,13 +65,21 @@ fn parse_args(args: Vec<String>) -> Args {
|
|||
} else {
|
||||
Args::Error
|
||||
}
|
||||
} else if rest.len() > 1 {
|
||||
} else if rest.len() >= 1 {
|
||||
if rest[0] == "browse" {
|
||||
if rest.len() == 2 {
|
||||
Args::Browse(rest[1].to_string())
|
||||
} else {
|
||||
Args::Error
|
||||
}
|
||||
} else if rest[0] == "clip" {
|
||||
if rest.len() == 1 {
|
||||
Args::Clip(None)
|
||||
} else if rest.len() == 2 {
|
||||
Args::Clip(Some(rest[1].to_string()))
|
||||
} else {
|
||||
Args::Error
|
||||
}
|
||||
} else {
|
||||
Args::Error
|
||||
}
|
||||
|
|
@ -85,6 +98,14 @@ async fn browse_url(url: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn clip_file(file: Option<String>) {
|
||||
if let Err(e) = fwd::clip_file(file.as_deref()).await {
|
||||
eprintln!("Unable to copy to the clipboard");
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
match parse_args(std::env::args().collect()) {
|
||||
|
|
@ -100,6 +121,9 @@ async fn main() {
|
|||
Args::Browse(url) => {
|
||||
browse_url(&url).await;
|
||||
}
|
||||
Args::Clip(file) => {
|
||||
clip_file(file).await;
|
||||
}
|
||||
Args::Client(server, sudo) => {
|
||||
fwd::run_client(&server, sudo).await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ pub enum Message {
|
|||
|
||||
// Browse a thing
|
||||
Browse(String),
|
||||
|
||||
// Send data to the remote clipboard
|
||||
ClipStart(u64),
|
||||
ClipData(u64, Vec<u8>),
|
||||
ClipEnd(u64),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
|
|
@ -103,6 +108,19 @@ impl Message {
|
|||
result.put_u8(0x07);
|
||||
put_string(result, url);
|
||||
}
|
||||
ClipStart(id) => {
|
||||
result.put_u8(0x08);
|
||||
result.put_u64(*id);
|
||||
}
|
||||
ClipData(id, data) => {
|
||||
result.put_u8(0x09);
|
||||
result.put_u64(*id);
|
||||
put_data(result, data);
|
||||
}
|
||||
ClipEnd(id) => {
|
||||
result.put_u8(0x0A);
|
||||
result.put_u64(*id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +150,20 @@ impl Message {
|
|||
Ok(Ports(ports))
|
||||
}
|
||||
0x07 => Ok(Browse(get_string(cursor)?)),
|
||||
b => Err(Error::Unknown(b).into()),
|
||||
0x08 => {
|
||||
let id = get_u64(cursor)?;
|
||||
Ok(ClipStart(id))
|
||||
}
|
||||
0x09 => {
|
||||
let id = get_u64(cursor)?;
|
||||
let data = get_data(cursor)?;
|
||||
Ok(Self::ClipData(id, data))
|
||||
}
|
||||
0x0A => {
|
||||
let id = get_u64(cursor)?;
|
||||
Ok(ClipEnd(id))
|
||||
}
|
||||
b => Err(Error::Unknown(b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -151,6 +182,13 @@ fn get_u16(cursor: &mut Cursor<&[u8]>) -> Result<u16> {
|
|||
Ok(cursor.get_u16())
|
||||
}
|
||||
|
||||
fn get_u64(cursor: &mut Cursor<&[u8]>) -> Result<u64> {
|
||||
if cursor.remaining() < 8 {
|
||||
return Err(Error::Incomplete);
|
||||
}
|
||||
Ok(cursor.get_u64())
|
||||
}
|
||||
|
||||
fn get_bytes(cursor: &mut Cursor<&[u8]>, length: usize) -> Result<Bytes> {
|
||||
if cursor.remaining() < length {
|
||||
return Err(Error::Incomplete);
|
||||
|
|
@ -182,6 +220,22 @@ fn put_string<T: BufMut>(target: &mut T, str: &str) {
|
|||
target.put_slice(str.as_bytes());
|
||||
}
|
||||
|
||||
fn put_data<T: BufMut>(target: &mut T, data: &[u8]) {
|
||||
target.put_u16(data.len().try_into().expect("Buffer is too long"));
|
||||
target.put_slice(data);
|
||||
}
|
||||
|
||||
fn get_data(cursor: &mut Cursor<&[u8]>) -> Result<Vec<u8>> {
|
||||
let length = get_u16(cursor)?;
|
||||
if cursor.remaining() < length.into() {
|
||||
return Err(Error::Incomplete);
|
||||
}
|
||||
|
||||
let mut data: Vec<u8> = vec![0; length.into()];
|
||||
cursor.copy_to_slice(&mut data);
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Message IO
|
||||
|
||||
|
|
@ -193,14 +247,14 @@ impl<T: AsyncWrite + Unpin> MessageWriter<T> {
|
|||
pub fn new(writer: T) -> MessageWriter<T> {
|
||||
MessageWriter { writer }
|
||||
}
|
||||
pub async fn write(self: &mut Self, msg: Message) -> Result<()> {
|
||||
pub async fn write(&mut self, msg: Message) -> Result<()> {
|
||||
// TODO: Optimize buffer usage please this is bad
|
||||
// eprintln!("? {:?}", msg);
|
||||
let mut buffer = msg.encode();
|
||||
let buffer = msg.encode();
|
||||
self.writer
|
||||
.write_u32(buffer.len().try_into().expect("Message too large"))
|
||||
.await?;
|
||||
self.writer.write_all(&mut buffer).await?;
|
||||
self.writer.write_all(&buffer).await?;
|
||||
self.writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -214,7 +268,8 @@ impl<T: AsyncRead + Unpin> MessageReader<T> {
|
|||
pub fn new(reader: T) -> MessageReader<T> {
|
||||
MessageReader { reader }
|
||||
}
|
||||
pub async fn read(self: &mut Self) -> Result<Message> {
|
||||
|
||||
pub async fn read(&mut self) -> Result<Message> {
|
||||
let frame_length = self.reader.read_u32().await?;
|
||||
let mut data = vec![0; frame_length.try_into().unwrap()];
|
||||
self.reader.read_exact(&mut data).await?;
|
||||
|
|
@ -283,6 +338,12 @@ mod message_tests {
|
|||
},
|
||||
]));
|
||||
assert_round_trip(Browse("https://google.com/".to_string()));
|
||||
assert_round_trip(ClipStart(0x1234567890ABCDEF));
|
||||
assert_round_trip(ClipData(
|
||||
0x1234567890ABCDEF,
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
));
|
||||
assert_round_trip(ClipEnd(0x1234567890ABCDEF));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use anyhow::Result;
|
||||
use rand::random;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
mod unix;
|
||||
|
|
@ -27,3 +29,48 @@ pub async fn handle_reverse_connections(
|
|||
pub async fn browse_url(url: &str) -> Result<()> {
|
||||
send_reverse_message(Message::Browse(url.to_string())).await
|
||||
}
|
||||
|
||||
async fn clip_reader<T: AsyncRead + Unpin>(reader: &mut T) -> Result<()> {
|
||||
let clip_id: u64 = random();
|
||||
send_reverse_message(Message::ClipStart(clip_id)).await?;
|
||||
|
||||
let mut count = 0;
|
||||
let mut buf = vec![0; 1024];
|
||||
loop {
|
||||
let read = reader.read(&mut buf[count..]).await?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
count += read;
|
||||
if count == buf.len() {
|
||||
send_reverse_message(Message::ClipData(clip_id, buf)).await?;
|
||||
buf = vec![0; 1024];
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
buf.resize(count, 0);
|
||||
send_reverse_message(Message::ClipData(clip_id, buf)).await?;
|
||||
}
|
||||
|
||||
send_reverse_message(Message::ClipEnd(clip_id)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn clip_file(file: Option<&str>) -> Result<()> {
|
||||
// send_reverse_message(Message::Browse(url.to_string())).await
|
||||
match file {
|
||||
Some(path) => {
|
||||
let mut file = tokio::fs::File::open(path).await?;
|
||||
clip_reader(&mut file).await?;
|
||||
}
|
||||
None => {
|
||||
let mut stdin = tokio::io::stdin();
|
||||
clip_reader(&mut stdin).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue