diff --git a/CHANGELOG.md b/CHANGELOG.md index 315a012..c963431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 39b7b94..e00939f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "weave" -version = "0.3.1" +version = "0.4.0" authors = ["James Wilson "] edition = "2018" diff --git a/README.md b/README.md index 12e5977..6e0823e 100644 --- a/README.md +++ b/README.md @@ -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 . diff --git a/src/examples.rs b/src/examples.rs index 5fac28f..2c0e544 100644 --- a/src/examples.rs +++ b/src/examples.rs @@ -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`: @@ -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 @@ -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() )) } diff --git a/src/location/dest_location.rs b/src/location/dest_location.rs index 06f213e..f7daebd 100644 --- a/src/location/dest_location.rs +++ b/src/location/dest_location.rs @@ -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() { @@ -287,7 +290,8 @@ mod test { ]; for (actual, expected) in urls { - let actual_loc: Result = actual.parse(); + let src: SrcLocation = "localhost".parse().unwrap(); + let actual_loc: Result = 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); } @@ -301,9 +305,49 @@ mod test { ]; for actual in urls { - let actual_loc: Result = actual.parse(); + let src: SrcLocation = "localhost".parse().unwrap(); + let actual_loc: Result = 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); + } + } + } + } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 371cfe0..e898bae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ async fn run() -> Result<(), Error> { let _ = App::new("weave") .author("James Wilson ") - .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]") @@ -120,7 +120,11 @@ 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 { @@ -128,19 +132,25 @@ async fn do_handle_tcp_requests(socket_addr: SocketAddr, route: Route) -> Result 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()); } } ); diff --git a/src/matcher.rs b/src/matcher.rs index 2b626ec..0fbd5eb 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -31,7 +31,7 @@ impl Matcher { mod test { use hyper::Uri; - use crate::location::{ DestLocation, ResolvedLocation }; + use crate::location::{ SrcLocation, DestLocation, ResolvedLocation }; use super::*; @@ -40,9 +40,10 @@ mod test { fn none () -> Option { None } fn test_route_matches(routes: Vec<(&str,&str)>, cases: Vec<(&str, Option)>) { let routes: Vec = 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); diff --git a/src/routes.rs b/src/routes.rs index 3057321..289853e 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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() { @@ -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 ), @@ -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 ),