diff options
| author | Martin Fischer <martin@push-f.com> | 2021-09-13 02:34:15 +0200 | 
|---|---|---|
| committer | Martin Fischer <martin@push-f.com> | 2021-09-13 02:34:15 +0200 | 
| commit | 5eaab2e99b6c477227eed59ec78dbcd7e1b0f4ff (patch) | |
| tree | 50fa86a01ec97e02fe4fe0991dd9ce4f976e29b1 | |
| parent | 8b9bcd0d86eaa27dc78b82ea65c9f60dfc7ce621 (diff) | |
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | README.md | 29 | ||||
| -rw-r--r-- | src/main.rs | 28 | 
4 files changed, 45 insertions, 16 deletions
| @@ -622,7 +622,7 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"  [[package]]  name = "webcat" -version = "0.1.0" +version = "0.2.0"  dependencies = [   "anyhow",   "atty", @@ -1,6 +1,6 @@  [package]  name = "webcat" -version = "0.1.0" +version = "0.2.0"  description = "netcat for websockets"  authors = ["Martin Fischer <martin@push-f.com>"]  license = "MIT" @@ -12,26 +12,39 @@ The server only accepts one client at a time. The client auto-reconnects.  ## Setting up a MITM debugging proxy -To debug stateful protocols on top of websockets, it is helpful to be able to -inject messages to the server or the client in a real session, acting as a -man-in-the-middle (MITM).  On Linux you can achieve this using `webcat` and -FIFOs as follows: +By redirecting the standard input/output streams of webcat you can turn it into +a man-in-the-middle proxy, particularily useful for debugging stateful protocols +on top of websocket. + +    [some client] <-> [webcat server] <-> [webcat client] <-> [some server] +                                     FIFOs + +All it takes to set this up is four commands, for example:      mkfifo client-in server-in      webcat ws://example.com:3000/ < client-in > server-in      webcat -l 4000 < server-in > client-in      echo > server-in # unblock the FIFO deadlock -You can now connect your client to ws://localhost:4000/. -And inject messages by writing to the named pipes: +You can now connect your client to `ws://localhost:4000/` +and inject messages by writing to the named pipes:      echo "Hello from webcat" > client-in -Note that when redirecting stdout the messages are automatically printed to -stderr, so you can still observe what's happening. +Webcat does two tricks to make this setup even more convenient: + +* When redirecting stdout, the messages are automatically printed +  to stderr, so you can still observe what's happening. + +* When the client recognizes the server output `accepted new client`, +  it automatically disconnects and reconnects its server connection +  to prevent stateful application protocols from becoming out of sync. +  ## Limitations +* no support for binary messages +  * no support for messages containing newlines    (cannot send them, cannot distinguish them from separate messages) diff --git a/src/main.rs b/src/main.rs index cc4163d..d93b24f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ async fn start_client(url: Url) -> Result<()> {          match tokio_tungstenite::connect_async(&url).await {              Ok((stream, _resp)) => {                  eprintln!("connected to server"); -                webcat("client >>>", stream, &mut stdin_receiver).await?; +                webcat(Mode::Client, stream, &mut stdin_receiver).await?;              }              Err(err) => {                  eprintln!("failed to connect to server: {}", err); @@ -58,6 +58,8 @@ async fn start_client(url: Url) -> Result<()> {      }  } +const SERVER_ACCEPTED_NEW_CONN: &str = "accepted new client"; +  async fn start_server(port: u16) -> Result<()> {      let listen_addr: SocketAddr = ([127, 0, 0, 1], port).into();      let listener = TcpListener::bind(listen_addr).await?; @@ -70,10 +72,10 @@ async fn start_server(port: u16) -> Result<()> {      loop {          let (stream, _client_addr) = listener.accept().await?; -        eprintln!("accepted TCP connection");          match tokio_tungstenite::accept_async(stream).await {              Ok(stream) => { -                webcat("server <<<", stream, &mut stdin_receiver).await?; +                println!("{}", SERVER_ACCEPTED_NEW_CONN); +                webcat(Mode::Server, stream, &mut stdin_receiver).await?;              }              Err(err) => {                  eprintln!("failed to upgrade {}", err); @@ -89,8 +91,14 @@ async fn send_stdin_to_channel(sender: UnboundedSender<String>) -> Result<()> {      Ok(())  } +#[derive(PartialEq)] +enum Mode { +    Server, +    Client, +} +  async fn webcat<S: Stream<Item = Result<Message, WsError>> + Sink<Message>>( -    name: &str, +    mode: Mode,      stream: S,      receiver: &mut UnboundedReceiver<String>,  ) -> Result<()> { @@ -98,7 +106,12 @@ async fn webcat<S: Stream<Item = Result<Message, WsError>> + Sink<Message>>(      loop {          tokio::select! {              Some(line) = receiver.recv() => { -                if write.send(Message::Text(line)).await.is_err() { +                if atty::isnt(atty::Stream::Stdin) && mode == Mode::Client && line == SERVER_ACCEPTED_NEW_CONN { +                    // Assuming the MITM setup described in the README, we +                    // disconnect and reconnect the client to prevent the +                    // application client and server from desynchronizing. +                    return Ok(()); +                } else if write.send(Message::Text(line)).await.is_err() {                      eprintln!("failed to send message");                  }              } @@ -111,7 +124,10 @@ async fn webcat<S: Stream<Item = Result<Message, WsError>> + Sink<Message>>(                                  if !atty::is(atty::Stream::Stdout) {                                      // if stdout is redirected we also print the                                      // messages to stderr for convenience -                                    eprintln!("{} {}", name, text); +                                    eprintln!("{} {}", match mode { +                                        Mode::Server => "server <<<", +                                        Mode::Client => "client >>>", +                                    }, text);                                  }                              }                              other => { | 
