Skip to content

Commit

Permalink
Add tests and docs and refine TCP proxying
Browse files Browse the repository at this point in the history
  • Loading branch information
jsdw committed Nov 16, 2019
1 parent d26569c commit ea32a21
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 32 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 0.4.0

## Additions

- Add support for TCP proxying.

## Improvements

- Support stable Rust and bump a few dependencies

# 0.3.1

## Improvements
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "weave"
version = "0.3.1"
version = "0.4.0"
authors = ["James Wilson <james@jsdw.me>"]
edition = "2018"

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

# Weave

A simple CLI based HTTP router/proxy. Useful if you need to wire together a few things and expose them behind a single host/port, or just as a fast, single-binary alternative to `php -s` or `static-server`.
A simple CLI based HTTP/TCP router/proxy. Useful if you need to wire together a few things and expose them behind a single host/port, or just as a fast, single-binary alternative to `php -s` or `static-server`. Also useful if you need to proxy TCP traffic to another location.

# Examples

Forward TCP connections from `localhost:2222` to `1.2.3.4:22`:
```
weave tcp://localhost:2222 to 1.2.3.4:22
```

Serve static files from the current directory on `localhost:8080`:
```
weave 8080 to .
Expand Down
8 changes: 7 additions & 1 deletion src/examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use regex::{ Regex, Captures};
pub fn text() -> String {
prettify_code(&format!("{EXAMPLES}
Forward TCP connections from `localhost:2222` to `1.2.3.4:22`:
{tcp_example1}
Serve static files from `./client/files` on `localhost:8080`, and redirect HTTP
requests starting with `localhost:8080/api` to `localhost:9090`:
Expand Down Expand Up @@ -56,6 +60,8 @@ local folder:
",
EXAMPLES="EXAMPLES:".bold(),

tcp_example1="weave tcp://localhost:2222 to 1.2.3.4:22".cyan(),

example1a="weave 8080 to ./client/files and 8080/api to 9090".cyan(),
example1b="# Examples of routing given the above:
# http://localhost:8080/api/foo => http://localhost:9090/foo
Expand Down Expand Up @@ -105,7 +111,7 @@ local folder:
# http://localhost:8080/1/2/3/api/foo => ./files/foo.json
# http://localhost:8080/wibble/api/foo => ./files/foo.json
# http://localhost:8080/bar/api/foo => ./files/foo.json
# http://localhost:8080/api/foo => No route matches this".white(),
# http://localhost:8080/api/foo => No route matches this".white()

))
}
Expand Down
50 changes: 47 additions & 3 deletions src/location/dest_location.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,10 @@ mod test {

use super::*;

fn u (u: &str) -> DestLocation { DestLocation::parse(u).unwrap() }
fn u (u: &str) -> DestLocation {
let src: SrcLocation = "http://localhost:1234".parse().unwrap();
DestLocation::parse(u, &src).unwrap()
}

#[test]
fn dest_location_can_parse_valid_inputs() {
Expand Down Expand Up @@ -287,7 +290,8 @@ mod test {
];

for (actual, expected) in urls {
let actual_loc: Result<DestLocation, _> = actual.parse();
let src: SrcLocation = "localhost".parse().unwrap();
let actual_loc: Result<DestLocation, _> = DestLocation::parse(actual, &src);
assert!(actual_loc.is_ok(), "Location could not be parsed: '{}', result: {:?}", actual, actual_loc);
assert_eq!(actual_loc.unwrap(), expected, "(Original was '{}')", actual);
}
Expand All @@ -301,9 +305,49 @@ mod test {
];

for actual in urls {
let actual_loc: Result<DestLocation, _> = actual.parse();
let src: SrcLocation = "localhost".parse().unwrap();
let actual_loc: Result<DestLocation, _> = DestLocation::parse(actual, &src);
assert!(actual_loc.is_err(), "This invalid location should not have successfully parsed: {}", actual);
}
}

#[test]
fn dest_location_relates_to_src() {
const VALID: bool = true;
const INVALID: bool = false;

let routes = vec![
(VALID, "tcp://localhost:22", "tcp://localhost:2222"),
(VALID, "tcp://localhost:22", "localhost:2222"),
(VALID, "tcp://localhost:22", "2222"), // assume localhost for dest if not given
(VALID, "http://localhost:22", "2222"), // assume localhost for dest if not given
(VALID, "tcp://localhost:22", "localhost"), // assume same port as src if not given
(VALID, "http://localhost", "localhost:2222"),
(INVALID, "https://localhost", "localhost:2222"), // https is not a valid src protocol
(INVALID, "tcp://localhost", "localhost:22"), // src needs port if TCP
(INVALID, "tcp://127.0.0.1:2222", "http://localhost"), // protocol mismatch
(INVALID, "http://127.0.0.1:2222", "tcp://localhost"), // protocol mismatch
(INVALID, "tcp://localhost/foo", "80"), // no paths allowed on TCP
(INVALID, "tcp://localhost", "80/foo"), // no paths allowed on TCP
];

for (is_valid, src, dest) in routes {
let src_l: SrcLocation = match src.parse() {
Ok(src) => src,
Err(e) => {
if is_valid {
assert!(true, "{} should be valid src but got error: {}", src, e);
}
continue
}
};
let dest_l = DestLocation::parse(dest, &src_l);
if is_valid {
assert!(dest_l.is_ok(), "{} => {} should be VALID but got error: {}", src, dest, dest_l.unwrap_err());
} else {
assert!(dest_l.is_err(), "{} => {} should be INVALID", src, dest);
}
}
}

}
20 changes: 15 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async fn run() -> Result<(), Error> {

let _ = App::new("weave")
.author("James Wilson <james@jsdw.me>")
.about("A lightweight HTTP router and file server.")
.about("A lightweight HTTP/TCP router and file server.")
.version(crate_version!())
.after_help(&*examples::text())
.usage("weave SOURCE to DEST [and SOURCE to DEST ...] [OPTIONS]")
Expand Down Expand Up @@ -120,27 +120,37 @@ async fn do_handle_tcp_requests(socket_addr: SocketAddr, route: Route) -> Result
// Accept an incoming connection:
let (mut src_socket, _) = match listener.accept().await {
Ok(sock) => sock,
Err(e) => { warn!("Error accepting connection on {}: {}", socket_addr, e); continue }
Err(e) => {
warn!("{}", format!("[tcp] error accepting connection on {}: {}",
socket_addr, e).red());
continue
}
};
// Proxy data to the outbound route provided:
tokio::spawn(async move {
let (mut src_read, mut src_write) = src_socket.split();

let mut dest_socket = match TcpStream::connect(dest_socket_addr).await {
Ok(sock) => sock,
Err(e) => { warn!("Error connecting to destination TCP socket at {}: {}", dest_socket_addr, e); return }
Err(e) => {
warn!("{}", format!("[tcp] error connecting to destination {}: {}",
dest_socket_addr, e).red());
return
}
};
let (mut dest_read, mut dest_write) = dest_socket.split();

join!(
async move {
if let Err(e) = src_read.copy(&mut dest_write).await {
warn!("Error copying data from TCP source to destination: {}", e);
warn!("{}", format!("[tcp] error streaming out from {} to {}: {}",
socket_addr, dest_socket_addr, e).yellow());
}
},
async move {
if let Err(e) = dest_read.copy(&mut src_write).await {
warn!("Error copying data from TCP destination to source: {}", e);
warn!("{}", format!("[tcp] error streaming back from {} to {}: {}",
dest_socket_addr, socket_addr, e).yellow());
}
}
);
Expand Down
7 changes: 4 additions & 3 deletions src/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl Matcher {
mod test {

use hyper::Uri;
use crate::location::{ DestLocation, ResolvedLocation };
use crate::location::{ SrcLocation, DestLocation, ResolvedLocation };

use super::*;

Expand All @@ -40,9 +40,10 @@ mod test {
fn none () -> Option<ResolvedLocation> { None }
fn test_route_matches(routes: Vec<(&str,&str)>, cases: Vec<(&str, Option<ResolvedLocation>)>) {
let routes: Vec<Route> = routes.into_iter().map(|(src,dest)| {
let src: SrcLocation = src.parse().unwrap();
Route {
src: src.parse().unwrap(),
dest: DestLocation::parse(dest).unwrap()
src: src.clone(),
dest: DestLocation::parse(dest, &src).unwrap()
}
}).collect();
let matcher = Matcher::new(routes);
Expand Down
29 changes: 11 additions & 18 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,13 @@ mod test {
use super::*;

fn s (s: &str) -> String { s.to_owned() }
fn dest_url (u: &str) -> DestLocation { DestLocation::parse(u).unwrap() }
fn src_url (u: &str) -> SrcLocation { u.parse().unwrap() }
fn route(src: &str, dest: &str) -> Route {
let src: SrcLocation = src.parse().unwrap();
Route {
src: src.clone(),
dest: DestLocation::parse(dest, &src).unwrap()
}
}

#[test]
fn routes_can_be_parsed() {
Expand All @@ -144,20 +149,14 @@ mod test {
(
vec![s("8080"), s("to"), s("9090")],
vec![
Route {
src: src_url("http://localhost:8080/"),
dest: dest_url("http://localhost:9090")
}
route("http://localhost:8080/", "http://localhost:9090")
],
0
),
(
vec![s("8080/foo/bar"), s("to"), s("9090/foo"), s("more"), s("args")],
vec![
Route {
src: src_url("http://localhost:8080/foo/bar"),
dest: dest_url("http://localhost:9090/foo")
}
route("http://localhost:8080/foo/bar", "http://localhost:9090/foo")
],
2
),
Expand All @@ -166,14 +165,8 @@ mod test {
s("9091"), s("to"), s("9090/lark"),
s("more"), s("args")],
vec![
Route {
src: src_url("http://localhost:8080/foo/bar"),
dest: dest_url("http://localhost:9090/foo")
},
Route {
src: src_url("http://localhost:9091/"),
dest: dest_url("http://localhost:9090/lark")
}
route("http://localhost:8080/foo/bar", "http://localhost:9090/foo"),
route("http://localhost:9091/", "http://localhost:9090/lark")
],
2
),
Expand Down

0 comments on commit ea32a21

Please sign in to comment.