diff --git a/Cargo.lock b/Cargo.lock index beb993bc9..6fab40dfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ct-logs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c8e13110a84b6315df212c045be706af261fd364791cad863285439ebba672e" +dependencies = [ + "sct", +] + [[package]] name = "curl" version = "0.4.31" @@ -1022,6 +1031,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" +dependencies = [ + "bytes 0.5.6", + "ct-logs", + "futures-util", + "hyper", + "log 0.4.11", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "webpki", +] + [[package]] name = "hyper-tls" version = "0.4.3" @@ -1379,8 +1406,8 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", - "security-framework-sys", + "security-framework 0.4.4", + "security-framework-sys 0.4.3", "tempfile", ] @@ -1812,6 +1839,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rle-decode-fast" version = "1.0.1" @@ -1842,6 +1884,31 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +[[package]] +name = "rustls" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac94b333ee2aac3284c5b8a1b7fb4dd11cba88c244e3fe33cdbd047af0eb693" +dependencies = [ + "base64 0.12.3", + "log 0.4.11", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629d439a7672da82dd955498445e496ee2096fe2117b9f796558a43fdb9e59b8" +dependencies = [ + "openssl-probe", + "rustls", + "schannel", + "security-framework 1.0.0", +] + [[package]] name = "ryu" version = "1.0.5" @@ -1867,6 +1934,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "0.4.4" @@ -1877,7 +1954,20 @@ dependencies = [ "core-foundation", "core-foundation-sys", "libc", - "security-framework-sys", + "security-framework-sys 0.4.3", +] + +[[package]] +name = "security-framework" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad502866817f0575705bd7be36e2b2535cc33262d493aa733a2ec862baa2bc2b" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys 1.0.0", ] [[package]] @@ -1890,6 +1980,16 @@ dependencies = [ "libc", ] +[[package]] +name = "security-framework-sys" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ceb04988b17b6d1dcd555390fa822ca5637b4a14e1f5099f13d351bed4d6c7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.10.0" @@ -2139,6 +2239,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2370,6 +2476,18 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228139ddd4fea3fa345a29233009635235833e52807af7ea6448ead03890d6a9" +dependencies = [ + "futures-core", + "rustls", + "tokio", + "webpki", +] + [[package]] name = "tokio-tls" version = "0.3.1" @@ -2555,6 +2673,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.1.1" @@ -2713,6 +2837,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "which" version = "4.0.2" @@ -2801,7 +2935,7 @@ dependencies = [ "futures-util", "http", "hyper", - "hyper-tls", + "hyper-rustls", "ignore", "indicatif", "lazy_static", @@ -2815,6 +2949,7 @@ dependencies = [ "rand", "regex", "reqwest", + "rustls", "semver", "serde 1.0.115", "serde_json", @@ -2823,6 +2958,7 @@ dependencies = [ "term_size", "text_io", "tokio", + "tokio-rustls", "tokio-tls", "tokio-tungstenite", "toml", diff --git a/Cargo.toml b/Cargo.toml index 5259cb285..dacdf1f61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ fs2 = "0.4.3" futures-util = "0.3" http = "0.2.1" hyper = "0.13.7" -hyper-tls = "0.4.3" +hyper-rustls = "0.21.0" ignore = "0.4.16" indicatif = "0.15.0" lazy_static = "1.4.0" @@ -53,6 +53,7 @@ tempfile = "3.1.0" term_size = "0.3" text_io = "0.1.8" tokio = { version = "0.2", default-features = false, features = ["io-std", "time", "macros", "process", "signal", "sync"] } +tokio-rustls = "0.14.0" tokio-tls = "0.3.1" tokio-tungstenite = { version = "0.10.1", features = ["tls"] } toml = "0.5.6" @@ -61,6 +62,7 @@ url = "2.1.1" uuid = { version = "0.8", features = ["v4"] } which = "4.0.2" ws = "0.9.1" +rustls = "0.18.0" [dev-dependencies] assert_cmd = "1.0.1" diff --git a/src/commands/dev/edge/mod.rs b/src/commands/dev/edge/mod.rs index e96de2f90..d5d3444e1 100644 --- a/src/commands/dev/edge/mod.rs +++ b/src/commands/dev/edge/mod.rs @@ -2,11 +2,10 @@ mod server; mod setup; mod watch; -use server::serve; use setup::{upload, Session}; use watch::watch_for_changes; -use crate::commands::dev::{socket, ServerConfig}; +use crate::commands::dev::{socket, Protocol, ServerConfig}; use crate::settings::global_user::GlobalUser; use crate::settings::toml::{DeployConfig, Target}; @@ -20,6 +19,8 @@ pub fn dev( user: GlobalUser, server_config: ServerConfig, deploy_config: DeployConfig, + local_protocol: Protocol, + upstream_protocol: Protocol, verbose: bool, ) -> Result<(), failure::Error> { let session = Session::new(&target, &user, &deploy_config)?; @@ -54,13 +55,21 @@ pub fn dev( let mut runtime = TokioRuntime::new()?; runtime.block_on(async { let devtools_listener = tokio::spawn(socket::listen(session.websocket_url)); - let server = tokio::spawn(serve( - server_config, - Arc::clone(&preview_token), - session.host, - )); - let res = tokio::try_join!(async { devtools_listener.await? }, async { server.await? }); + let server = match local_protocol { + Protocol::Https => tokio::spawn(server::https( + server_config.clone(), + Arc::clone(&preview_token), + session.host.clone(), + )), + Protocol::Http => tokio::spawn(server::http( + server_config, + Arc::clone(&preview_token), + session.host, + upstream_protocol, + )), + }; + let res = tokio::try_join!(async { devtools_listener.await? }, async { server.await? }); match res { Ok(_) => Ok(()), Err(e) => Err(e), diff --git a/src/commands/dev/edge/server.rs b/src/commands/dev/edge/server/http.rs similarity index 67% rename from src/commands/dev/edge/server.rs rename to src/commands/dev/edge/server/http.rs index 1b78720fe..80a961ffa 100644 --- a/src/commands/dev/edge/server.rs +++ b/src/commands/dev/edge/server/http.rs @@ -1,20 +1,20 @@ -use crate::commands::dev::server_config::ServerConfig; +use super::preview_request; use crate::commands::dev::utils::get_path_as_str; +use crate::commands::dev::{Protocol, ServerConfig}; use crate::terminal::emoji; use std::sync::{Arc, Mutex}; use chrono::prelude::*; -use hyper::client::{HttpConnector, ResponseFuture}; -use hyper::header::{HeaderName, HeaderValue}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Client as HyperClient, Request, Server}; -use hyper_tls::HttpsConnector; +use hyper_rustls::HttpsConnector; -pub(super) async fn serve( +pub async fn http( server_config: ServerConfig, preview_token: Arc>, host: String, + upstream_protocol: Protocol, ) -> Result<(), failure::Error> { // set up https client to connect to the preview service let https = HttpsConnector::new(); @@ -44,6 +44,7 @@ pub(super) async fn serve( client, preview_token.to_owned(), host.clone(), + upstream_protocol, ) .await?; @@ -64,38 +65,10 @@ pub(super) async fn serve( let server = Server::bind(&listening_address).serve(make_service); println!("{} Listening on http://{}", emoji::EAR, listening_address); + if let Err(e) = server.await { - eprintln!("server error: {}", e) + eprintln!("{}", e); } - Ok(()) -} - -fn preview_request( - req: Request, - client: HyperClient>, - preview_token: String, - host: String, -) -> ResponseFuture { - let (mut parts, body) = req.into_parts(); - - let path = get_path_as_str(&parts.uri); - parts.headers.insert( - HeaderName::from_static("host"), - HeaderValue::from_str(&host).expect("Could not create host header"), - ); - - parts.headers.insert( - HeaderName::from_static("cf-workers-preview-token"), - HeaderValue::from_str(&preview_token).expect("Could not create token header"), - ); - - // TODO: figure out how to http _or_ https - parts.uri = format!("https://{}{}", host, path) - .parse() - .expect("Could not construct preview url"); - - let req = Request::from_parts(parts, body); - - client.request(req) + Ok(()) } diff --git a/src/commands/dev/edge/server/https.rs b/src/commands/dev/edge/server/https.rs new file mode 100644 index 000000000..601b288d9 --- /dev/null +++ b/src/commands/dev/edge/server/https.rs @@ -0,0 +1,104 @@ +use super::preview_request; +use crate::commands::dev::utils::get_path_as_str; +use crate::commands::dev::{tls, Protocol, ServerConfig}; +use crate::terminal::{emoji, message}; + +use std::sync::{Arc, Mutex}; + +use chrono::prelude::*; +use futures_util::stream::StreamExt; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Client as HyperClient, Request, Server}; +use hyper_rustls::HttpsConnector; +use tokio::net::TcpListener; + +pub async fn https( + server_config: ServerConfig, + preview_token: Arc>, + host: String, +) -> Result<(), failure::Error> { + tls::generate_cert()?; + + // set up https client to connect to the preview service + let https = HttpsConnector::new(); + let client = HyperClient::builder().build::<_, Body>(https); + + // create a closure that hyper will use later to handle HTTP requests + let service = make_service_fn(move |_| { + let client = client.to_owned(); + let preview_token = preview_token.to_owned(); + let host = host.to_owned(); + + async move { + Ok::<_, failure::Error>(service_fn(move |req| { + let client = client.to_owned(); + let preview_token = preview_token.lock().unwrap().to_owned(); + let host = host.to_owned(); + let version = req.version(); + let (parts, body) = req.into_parts(); + let req_method = parts.method.to_string(); + let now: DateTime = Local::now(); + let path = get_path_as_str(&parts.uri); + async move { + let resp = preview_request( + Request::from_parts(parts, body), + client, + preview_token.to_owned(), + host.clone(), + Protocol::Https, + ) + .await?; + + println!( + "[{}] {} {}{} {:?} {}", + now.format("%Y-%m-%d %H:%M:%S"), + req_method, + host, + path, + version, + resp.status() + ); + Ok::<_, failure::Error>(resp) + } + })) + } + }); + + let listening_address = server_config.listening_address; + + let mut tcp = TcpListener::bind(&listening_address).await?; + let tls_acceptor = &tls::get_tls_acceptor()?; + let incoming_tls_stream = tcp + .incoming() + .filter_map(move |s| async move { + let client = match s { + Ok(x) => x, + Err(e) => { + eprintln!("Failed to accept client {}", e); + return None; + } + }; + match tls_acceptor.accept(client).await { + Ok(x) => Some(Ok(x)), + Err(e) => { + eprintln!("Client connection error {}", e); + message::info("Make sure to use https and `--insecure` with curl"); + None + } + } + }) + .boxed(); + let server = Server::builder(tls::HyperAcceptor { + acceptor: incoming_tls_stream, + }) + .serve(service); + + println!("{} Listening on https://{}", emoji::EAR, listening_address); + message::info("Generated certificate is not verified, browsers will give a warning and curl will require `--insecure`"); + + if let Err(e) = server.await { + eprintln!("{}", e); + } + + Ok(()) +} diff --git a/src/commands/dev/edge/server/mod.rs b/src/commands/dev/edge/server/mod.rs new file mode 100644 index 000000000..5a73eb2f6 --- /dev/null +++ b/src/commands/dev/edge/server/mod.rs @@ -0,0 +1,46 @@ +mod http; +mod https; + +pub use self::http::http; +pub use self::https::https; + +use crate::commands::dev::utils::get_path_as_str; +use crate::commands::dev::Protocol; + +use hyper::client::{HttpConnector, ResponseFuture}; +use hyper::header::{HeaderName, HeaderValue}; +use hyper::{Body, Client as HyperClient, Request}; +use hyper_rustls::HttpsConnector; + +fn preview_request( + req: Request, + client: HyperClient>, + preview_token: String, + host: String, + protocol: Protocol, +) -> ResponseFuture { + let (mut parts, body) = req.into_parts(); + + let path = get_path_as_str(&parts.uri); + + parts.headers.insert( + HeaderName::from_static("host"), + HeaderValue::from_str(&host).expect("Could not create host header"), + ); + + parts.headers.insert( + HeaderName::from_static("cf-workers-preview-token"), + HeaderValue::from_str(&preview_token).expect("Could not create token header"), + ); + + parts.uri = match protocol { + Protocol::Http => format!("http://{}{}", host, path), + Protocol::Https => format!("https://{}{}", host, path), + } + .parse() + .expect("Could not construct preview url"); + + let req = Request::from_parts(parts, body); + + client.request(req) +} diff --git a/src/commands/dev/gcs/mod.rs b/src/commands/dev/gcs/mod.rs index 3311a7342..de59f452e 100644 --- a/src/commands/dev/gcs/mod.rs +++ b/src/commands/dev/gcs/mod.rs @@ -3,11 +3,10 @@ mod server; mod setup; mod watch; -use server::serve; use setup::{get_preview_id, get_session_id}; use watch::watch_for_changes; -use crate::commands::dev::{socket, ServerConfig}; +use crate::commands::dev::{socket, Protocol, ServerConfig}; use crate::settings::toml::Target; use std::sync::{Arc, Mutex}; @@ -20,6 +19,7 @@ use url::Url; pub fn dev( target: Target, server_config: ServerConfig, + local_protocol: Protocol, verbose: bool, ) -> Result<(), failure::Error> { println!("unauthenticated"); @@ -73,10 +73,19 @@ pub fn dev( // and we must block the main thread on the completion of // said futures runtime.block_on(async { - let devtools_listener = tokio::spawn(socket::listen(socket_url)); - let server = tokio::spawn(serve(server_config, Arc::clone(&preview_id))); - let res = tokio::try_join!(async { devtools_listener.await? }, async { server.await? }); + let devtools_listener = tokio::spawn(socket::listen(socket_url.clone())); + + let server = match local_protocol { + Protocol::Https => tokio::spawn(server::https( + server_config.clone(), + Arc::clone(&preview_id), + )), + Protocol::Http => { + tokio::spawn(server::http(server_config.clone(), Arc::clone(&preview_id))) + } + }; + let res = tokio::try_join!(async { devtools_listener.await? }, async { server.await? }); match res { Ok(_) => Ok(()), Err(e) => Err(e), diff --git a/src/commands/dev/gcs/server.rs b/src/commands/dev/gcs/server/http.rs similarity index 73% rename from src/commands/dev/gcs/server.rs rename to src/commands/dev/gcs/server/http.rs index 06a3e0f4e..cc26b63c2 100644 --- a/src/commands/dev/gcs/server.rs +++ b/src/commands/dev/gcs/server/http.rs @@ -1,4 +1,5 @@ -use crate::commands::dev::gcs::headers::{destructure_response, structure_request}; +use super::preview_request; +use crate::commands::dev::gcs::headers::destructure_response; use crate::commands::dev::server_config::ServerConfig; use crate::commands::dev::utils::get_path_as_str; use crate::terminal::emoji; @@ -6,18 +7,13 @@ use crate::terminal::emoji; use std::sync::{Arc, Mutex}; use chrono::prelude::*; -use hyper::client::{HttpConnector, ResponseFuture}; -use hyper::header::{HeaderName, HeaderValue}; -use hyper::http::uri::InvalidUri; use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Client as HyperClient, Request, Response, Server, Uri}; -use hyper_tls::HttpsConnector; - -const PREVIEW_HOST: &str = "rawhttp.cloudflareworkers.com"; +use hyper::{Body, Client as HyperClient, Request, Response, Server}; +use hyper_rustls::HttpsConnector; /// performs all logic that takes an incoming request /// and routes it to the Workers runtime preview service -pub(super) async fn serve( +pub async fn http( server_config: ServerConfig, preview_id: Arc>, ) -> Result<(), failure::Error> { @@ -96,36 +92,3 @@ pub(super) async fn serve( } Ok(()) } - -fn get_preview_url(path_string: &str) -> Result { - format!("https://{}{}", PREVIEW_HOST, path_string).parse() -} - -fn preview_request( - req: Request, - client: HyperClient>, - preview_id: String, -) -> ResponseFuture { - let (mut parts, body) = req.into_parts(); - - let path = get_path_as_str(&parts.uri); - let preview_id = &preview_id; - - structure_request(&mut parts); - - parts.headers.insert( - HeaderName::from_static("host"), - HeaderValue::from_static(PREVIEW_HOST), - ); - - parts.headers.insert( - HeaderName::from_static("cf-ew-preview"), - HeaderValue::from_str(preview_id).expect("Could not create header for preview id"), - ); - - parts.uri = get_preview_url(&path).expect("Could not get preview url"); - - let req = Request::from_parts(parts, body); - - client.request(req) -} diff --git a/src/commands/dev/gcs/server/https.rs b/src/commands/dev/gcs/server/https.rs new file mode 100644 index 000000000..aaf6f36f3 --- /dev/null +++ b/src/commands/dev/gcs/server/https.rs @@ -0,0 +1,130 @@ +use super::preview_request; +use crate::commands::dev::gcs::headers::destructure_response; +use crate::commands::dev::server_config::ServerConfig; +use crate::commands::dev::tls; +use crate::commands::dev::utils::get_path_as_str; +use crate::terminal::{emoji, message}; + +use std::sync::{Arc, Mutex}; + +use chrono::prelude::*; +use futures_util::stream::StreamExt; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Client as HyperClient, Request, Response, Server}; +use hyper_rustls::HttpsConnector; +use tokio::net::TcpListener; + +/// performs all logic that takes an incoming request +/// and routes it to the Workers runtime preview service +pub async fn https( + server_config: ServerConfig, + preview_id: Arc>, +) -> Result<(), failure::Error> { + tls::generate_cert()?; + + // set up https client to connect to the preview service + let https = HttpsConnector::new(); + let client = HyperClient::builder().build::<_, Body>(https); + + let listening_address = server_config.listening_address; + + // create a closure that hyper will use later to handle HTTP requests + // this takes care of sending an incoming request along to + // the uploaded Worker script and returning its response + let service = make_service_fn(move |_| { + let client = client.to_owned(); + let server_config = server_config.to_owned(); + let preview_id = preview_id.to_owned(); + async move { + Ok::<_, failure::Error>(service_fn(move |req| { + let client = client.to_owned(); + let server_config = server_config.to_owned(); + let preview_id = preview_id.lock().unwrap().to_owned(); + let version = req.version(); + + // record the time of the request + let now: DateTime = Local::now(); + + // split the request into parts so we can read + // what it contains and display in logs + let (parts, body) = req.into_parts(); + + let req_method = parts.method.to_string(); + + // parse the path so we can send it to the preview service + // we don't want to send "localhost:8787/path", just "/path" + let path = get_path_as_str(&parts.uri); + + async move { + // send the request to the preview service + let resp = preview_request( + Request::from_parts(parts, body), + client, + preview_id.to_owned(), + ) + .await?; + let (mut parts, body) = resp.into_parts(); + + // format the response for the user + destructure_response(&mut parts)?; + let resp = Response::from_parts(parts, body); + + // print information about the response + // [2020-04-20 15:25:54] GET example.com/ HTTP/1.1 200 OK + println!( + "[{}] {} {}{} {:?} {}", + now.format("%Y-%m-%d %H:%M:%S"), + req_method, + server_config.host, + path, + version, + resp.status() + ); + Ok::<_, failure::Error>(resp) + } + })) + } + }); + + // Create a TCP listener via tokio. + let mut tcp = TcpListener::bind(&listening_address).await?; + let tls_acceptor = &tls::get_tls_acceptor()?; + let incoming_tls_stream = tcp + .incoming() + .filter_map(move |s| async move { + let client = match s { + Ok(x) => x, + Err(e) => { + eprintln!("Failed to accept client {}", e); + return None; + } + }; + match tls_acceptor.accept(client).await { + Ok(x) => Some(Ok(x)), + Err(e) => { + eprintln!("Client connection error {}", e); + message::info("Make sure to use https and `--insecure` with curl"); + None + } + } + }) + .boxed(); + + let server = Server::builder(tls::HyperAcceptor { + acceptor: incoming_tls_stream, + }) + .serve(service); + println!( + "{} Listening on https://{}", + emoji::EAR, + listening_address.to_string() + ); + + message::info("Generated certificate is not verified, browsers will give a warning and curl will require `--insecure`"); + + if let Err(e) = server.await { + eprintln!("{}", e); + } + + Ok(()) +} diff --git a/src/commands/dev/gcs/server/mod.rs b/src/commands/dev/gcs/server/mod.rs new file mode 100644 index 000000000..e991a80a0 --- /dev/null +++ b/src/commands/dev/gcs/server/mod.rs @@ -0,0 +1,49 @@ +mod http; +mod https; + +pub use self::http::http; +pub use self::https::https; + +use crate::commands::dev::gcs::headers::structure_request; +use crate::commands::dev::utils::get_path_as_str; + +use hyper::client::{HttpConnector, ResponseFuture}; +use hyper::header::{HeaderName, HeaderValue}; +use hyper::http::uri::InvalidUri; +use hyper::{Body, Client as HyperClient, Request, Uri}; +use hyper_rustls::HttpsConnector; + +const PREVIEW_HOST: &str = "rawhttp.cloudflareworkers.com"; + +fn get_preview_url(path_string: &str) -> Result { + format!("https://{}{}", PREVIEW_HOST, path_string).parse() +} + +pub fn preview_request( + req: Request, + client: HyperClient>, + preview_id: String, +) -> ResponseFuture { + let (mut parts, body) = req.into_parts(); + + let path = get_path_as_str(&parts.uri); + let preview_id = &preview_id; + + structure_request(&mut parts); + + parts.headers.insert( + HeaderName::from_static("host"), + HeaderValue::from_static(PREVIEW_HOST), + ); + + parts.headers.insert( + HeaderName::from_static("cf-ew-preview"), + HeaderValue::from_str(preview_id).expect("Could not create header for preview id"), + ); + + parts.uri = get_preview_url(&path).expect("Could not get preview url"); + + let req = Request::from_parts(parts, body); + + client.request(req) +} diff --git a/src/commands/dev/mod.rs b/src/commands/dev/mod.rs index 9bd91a294..e3a3ea8db 100644 --- a/src/commands/dev/mod.rs +++ b/src/commands/dev/mod.rs @@ -2,13 +2,16 @@ mod edge; mod gcs; mod server_config; mod socket; +mod tls; mod utils; -use server_config::ServerConfig; +pub use server_config::Protocol; +pub use server_config::ServerConfig; use crate::build; use crate::settings::global_user::GlobalUser; use crate::settings::toml::{DeployConfig, Target}; +use crate::terminal::styles; /// `wrangler dev` starts a server on a dev machine that routes incoming HTTP requests /// to a Cloudflare Workers runtime and returns HTTP responses @@ -16,21 +19,40 @@ pub fn dev( target: Target, deploy_config: DeployConfig, user: Option, - host: Option<&str>, - port: Option, - ip: Option<&str>, + server_config: ServerConfig, + local_protocol: Protocol, + upstream_protocol: Protocol, verbose: bool, ) -> Result<(), failure::Error> { - let server_config = ServerConfig::new(host, ip, port)?; - // before serving requests we must first build the Worker build(&target)?; + let host_str = styles::highlight("--host"); + let local_str = styles::highlight("--local-protocol"); + let upstream_str = styles::highlight("--upstream-protocol"); + + if server_config.host.is_https() != upstream_protocol.is_https() { + failure::bail!(format!( + "Protocol mismatch: protocol in {} and protocol in {} must match", + host_str, upstream_str + )) + } else if local_protocol.is_https() && upstream_protocol.is_http() { + failure::bail!("{} cannot be https if {} is http", local_str, upstream_str) + } + match user { // authenticated users connect to the edge - Some(user) => edge::dev(target, user, server_config, deploy_config, verbose), + Some(user) => edge::dev( + target, + user, + server_config, + deploy_config, + local_protocol, + upstream_protocol, + verbose, + ), // unauthenticated users connect to gcs - None => gcs::dev(target, server_config, verbose), + None => gcs::dev(target, server_config, local_protocol, verbose), } } diff --git a/src/commands/dev/server_config/mod.rs b/src/commands/dev/server_config/mod.rs index b9b836b98..fe6f06a3e 100644 --- a/src/commands/dev/server_config/mod.rs +++ b/src/commands/dev/server_config/mod.rs @@ -1,4 +1,7 @@ mod host; +mod protocol; + +pub use protocol::Protocol; use host::Host; @@ -15,6 +18,7 @@ impl ServerConfig { host: Option<&str>, ip: Option<&str>, port: Option, + upstream_protocol: Protocol, ) -> Result { let ip = ip.unwrap_or("127.0.0.1"); let port = port.unwrap_or(8787); @@ -23,9 +27,14 @@ impl ServerConfig { Ok(socket) => socket.local_addr(), Err(_) => failure::bail!("{} is unavailable, try binding to another address with the --port and --ip flags, or stop other `wrangler dev` processes.", &addr) }?; - let host = host - .unwrap_or("https://tutorial.cloudflareworkers.com") - .to_string(); + let host = match upstream_protocol { + Protocol::Http => host + .unwrap_or("http://tutorial.cloudflareworkers.com") + .to_string(), + Protocol::Https => host + .unwrap_or("https://tutorial.cloudflareworkers.com") + .to_string(), + }; let host = Host::new(&host)?; diff --git a/src/commands/dev/server_config/protocol.rs b/src/commands/dev/server_config/protocol.rs new file mode 100644 index 000000000..71146dfdb --- /dev/null +++ b/src/commands/dev/server_config/protocol.rs @@ -0,0 +1,35 @@ +pub use std::convert::TryFrom; + +#[derive(Clone, Copy)] +pub enum Protocol { + Http, + Https, +} + +impl Protocol { + pub fn is_http(&self) -> bool { + match self { + Protocol::Http => true, + _ => false, + } + } + + pub fn is_https(&self) -> bool { + match self { + Protocol::Https => true, + _ => false, + } + } +} + +impl TryFrom<&str> for Protocol { + type Error = failure::Error; + + fn try_from(p: &str) -> Result { + match p { + "http" => Ok(Protocol::Http), + "https" => Ok(Protocol::Https), + _ => failure::bail!("Invalid protocol, must be http or https"), + } + } +} diff --git a/src/commands/dev/tls/certs.rs b/src/commands/dev/tls/certs.rs new file mode 100644 index 000000000..654a53371 --- /dev/null +++ b/src/commands/dev/tls/certs.rs @@ -0,0 +1,170 @@ +use openssl::asn1::Asn1Time; +use openssl::bn::{BigNum, MsbOption}; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private}; +use openssl::rsa::Rsa; +use openssl::x509::extension::{ + AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectAlternativeName, + SubjectKeyIdentifier, +}; +use openssl::x509::{X509NameBuilder, X509Req, X509ReqBuilder, X509}; +use std::fs; +use std::path::PathBuf; + +use crate::settings::get_wrangler_home_dir; +use crate::terminal::message; + +/// Create files for cert and private key +fn create_output_files() -> Result, failure::Error> { + let home = get_wrangler_home_dir()?.join("config"); + let cert = home.join("dev-cert.pem"); + let privkey = home.join("dev-privkey.rsa"); + + if cert.exists() && privkey.exists() { + Ok(None) + } else { + fs::create_dir_all(&home)?; + + message::info(format!("Generating certificate and private key for https server, if you would like to use your own you can replace `dev-cert.pem` and `dev-privkey.rsa` at {}", home.to_str().unwrap()).as_str()); + + Ok(Some((cert, privkey))) + } +} + +/// Generate certificate authority to sign cert +fn create_ca() -> Result<(X509, PKey), failure::Error> { + let rsa = Rsa::generate(2048)?; + let privkey = PKey::from_rsa(rsa)?; + + let mut x509_name = X509NameBuilder::new()?; + x509_name.append_entry_by_text("C", "US")?; + x509_name.append_entry_by_text("ST", "TX")?; + x509_name.append_entry_by_text("O", "Wrangler")?; + x509_name.append_entry_by_text("CN", "Wrangler")?; + let x509_name = x509_name.build(); + + let mut cert_builder = X509::builder()?; + cert_builder.set_version(2)?; + let serial_number = { + let mut serial = BigNum::new()?; + serial.rand(159, MsbOption::MAYBE_ZERO, false)?; + serial.to_asn1_integer()? + }; + cert_builder.set_serial_number(&serial_number)?; + cert_builder.set_subject_name(&x509_name)?; + cert_builder.set_issuer_name(&x509_name)?; + cert_builder.set_pubkey(&privkey)?; + let not_before = Asn1Time::days_from_now(0)?; + cert_builder.set_not_before(¬_before)?; + let not_after = Asn1Time::days_from_now(365)?; + cert_builder.set_not_after(¬_after)?; + + cert_builder.append_extension(BasicConstraints::new().critical().ca().build()?)?; + cert_builder.append_extension( + KeyUsage::new() + .critical() + .key_cert_sign() + .crl_sign() + .build()?, + )?; + + let subject_key_identifier = + SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(None, None))?; + cert_builder.append_extension(subject_key_identifier)?; + + cert_builder.sign(&privkey, MessageDigest::sha256())?; + let cert = cert_builder.build(); + + Ok((cert, privkey)) +} + +fn create_req(privkey: &PKey) -> Result { + let mut req_builder = X509ReqBuilder::new()?; + req_builder.set_pubkey(&privkey)?; + + let mut x509_name = X509NameBuilder::new()?; + x509_name.append_entry_by_text("C", "US")?; + x509_name.append_entry_by_text("ST", "TX")?; + x509_name.append_entry_by_text("O", "Some organization")?; + x509_name.append_entry_by_text("CN", "www.example.com")?; + let x509_name = x509_name.build(); + req_builder.set_subject_name(&x509_name)?; + + req_builder.sign(&privkey, MessageDigest::sha256())?; + let req = req_builder.build(); + Ok(req) +} + +/// Generate cert and private key +pub fn generate_cert() -> Result<(), failure::Error> { + let files = create_output_files()?; + if files.is_none() { + return Ok(()); + } + + let (cert_file, priv_file) = files.unwrap(); + + let (ca, ca_key) = create_ca()?; + + let rsa = Rsa::generate(2048)?; + let privkey = PKey::from_rsa(rsa)?; + + let req = create_req(&privkey)?; + + let rsa = Rsa::generate(2048)?; + let privkey = PKey::from_rsa(rsa)?; + + let mut cert_builder = X509::builder()?; + cert_builder.set_version(2)?; + let serial_number = { + let mut serial = BigNum::new()?; + serial.rand(159, MsbOption::MAYBE_ZERO, false)?; + serial.to_asn1_integer()? + }; + cert_builder.set_serial_number(&serial_number)?; + cert_builder.set_subject_name(req.subject_name())?; + cert_builder.set_issuer_name(ca.subject_name())?; + cert_builder.set_pubkey(&privkey)?; + let not_before = Asn1Time::days_from_now(0)?; + cert_builder.set_not_before(¬_before)?; + let not_after = Asn1Time::days_from_now(365)?; + cert_builder.set_not_after(¬_after)?; + + cert_builder.append_extension(BasicConstraints::new().build()?)?; + + cert_builder.append_extension( + // Extensions requried by browsers to be a valid cert + KeyUsage::new() + .critical() + .non_repudiation() + .digital_signature() + .key_encipherment() + .build()?, + )?; + + let subject_key_identifier = + SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(Some(&ca), None))?; + cert_builder.append_extension(subject_key_identifier)?; + + let auth_key_identifier = AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&cert_builder.x509v3_context(Some(&ca), None))?; + cert_builder.append_extension(auth_key_identifier)?; + + let subject_alt_name = SubjectAlternativeName::new() + .dns("*.example.com") + .dns("hello.com") + .build(&cert_builder.x509v3_context(Some(&ca), None))?; + cert_builder.append_extension(subject_alt_name)?; + + cert_builder.sign(&ca_key, MessageDigest::sha256())?; + + let cert_str = cert_builder.build().to_pem().unwrap(); + let priv_str = privkey.private_key_to_pem_pkcs8().unwrap(); + + fs::write(cert_file, cert_str)?; + fs::write(priv_file, priv_str)?; + + Ok(()) +} diff --git a/src/commands/dev/tls/mod.rs b/src/commands/dev/tls/mod.rs new file mode 100644 index 000000000..a8cd99462 --- /dev/null +++ b/src/commands/dev/tls/mod.rs @@ -0,0 +1,89 @@ +mod certs; +pub use certs::generate_cert; + +use core::task::{Context, Poll}; +use fs::File; +use futures_util::stream::Stream; +use rustls::internal::pemfile; +use rustls::{NoClientAuth, ServerConfig}; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; +use std::vec::Vec; +use std::{fs, io}; +use tokio::net::TcpStream; +use tokio_rustls::{server::TlsStream, TlsAcceptor}; + +use crate::settings::get_wrangler_home_dir; + +// Build TLS configuration +pub(super) fn get_tls_acceptor() -> Result { + let home = get_wrangler_home_dir()?.join("config"); + let cert = home.join("dev-cert.pem"); + let privkey = home.join("dev-privkey.rsa"); + + // Load public certificate + let certs = load_certs(cert)?; + + // Load private key + let key = load_private_key(privkey)?; + + // Do not use client certificate authentication. + let mut cfg = ServerConfig::new(NoClientAuth::new()); + + // Select a certificate to use. + cfg.set_single_cert(certs, key) + .map_err(|e| io_error(format!("{}", e)))?; + + Ok(TlsAcceptor::from(Arc::new(cfg))) +} + +pub(super) fn io_error(err: String) -> io::Error { + io::Error::new(io::ErrorKind::Other, err) +} + +pub(super) struct HyperAcceptor<'a> { + pub(super) acceptor: + Pin, io::Error>> + Send + 'a>>, +} + +impl hyper::server::accept::Accept for HyperAcceptor<'_> { + type Conn = TlsStream; + type Error = io::Error; + + fn poll_accept( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll>> { + Pin::new(&mut self.acceptor).poll_next(cx) + } +} + +fn get_tls_file(file: PathBuf) -> Result { + File::open(&file) +} + +// Load public certificate from file. +fn load_certs(file: PathBuf) -> io::Result> { + // Open certificate file. + let certfile = get_tls_file(file)?; + let mut reader = io::BufReader::new(certfile); + + // Load and return certificate. + pemfile::certs(&mut reader).map_err(|_| io_error("failed to load certificate".into())) +} + +// Load private key from file. +fn load_private_key(file: PathBuf) -> io::Result { + // Open keyfile. + let keyfile = get_tls_file(file)?; + let mut reader = io::BufReader::new(keyfile); + + // Load and return a single private key. + let keys = pemfile::pkcs8_private_keys(&mut reader) + .map_err(|_| io_error("failed to load private key".into()))?; + if keys.len() != 1 { + return Err(io_error("expected a single private key".into())); + } + Ok(keys[0].clone()) +} diff --git a/src/main.rs b/src/main.rs index 629d3a1b2..5c00ae1c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ extern crate text_io; extern crate tokio; +use std::convert::TryFrom; use std::env; use std::path::Path; use std::str::FromStr; @@ -521,6 +522,18 @@ fn run() -> Result<(), failure::Error> { .long("ip") .takes_value(true) ) + .arg( + Arg::with_name("local-protocol") + .help("sets the protocol on which the wrangler dev listens, by default this is http but can be set to https") + .long("local-protocol") + .takes_value(true) + ) + .arg( + Arg::with_name("upstream-protocol") + .help("sets the protocol on which requests are sent to the host, by default this is https but can be set to http") + .long("upstream-protocol") + .takes_value(true) + ) .arg(verbose_arg.clone()) .arg(wrangler_file.clone()) ) @@ -766,6 +779,10 @@ fn run() -> Result<(), failure::Error> { .value_of("port") .map(|p| p.parse().expect("--port expects a number")); + type Protocol = commands::dev::Protocol; + let mut local_protocol_str: Option<&str> = matches.value_of("local-protocol"); + let mut upstream_protocol_str: Option<&str> = matches.value_of("upstream-protocol"); + // Check if arg not given but present in wrangler.toml if let Some(d) = &manifest.dev { if ip.is_none() && d.ip.is_some() { @@ -775,6 +792,14 @@ fn run() -> Result<(), failure::Error> { if port.is_none() && d.port.is_some() { port = d.port; } + + if local_protocol_str.is_none() && d.local_protocol.is_some() { + local_protocol_str = d.local_protocol.as_deref(); + } + + if upstream_protocol_str.is_none() && d.upstream_protocol.is_some() { + upstream_protocol_str = d.upstream_protocol.as_deref(); + } } let env = matches.value_of("env"); @@ -783,7 +808,21 @@ fn run() -> Result<(), failure::Error> { let target = manifest.get_target(env, is_preview)?; let user = settings::global_user::GlobalUser::new().ok(); let verbose = matches.is_present("verbose"); - commands::dev::dev(target, deploy_config, user, host, port, ip, verbose)?; + + let local_protocol = Protocol::try_from(local_protocol_str.unwrap_or("http"))?; + let upstream_protocol = Protocol::try_from(upstream_protocol_str.unwrap_or("https"))?; + + let server_config = commands::dev::ServerConfig::new(host, ip, port, upstream_protocol)?; + + commands::dev::dev( + target, + deploy_config, + user, + server_config, + local_protocol, + upstream_protocol, + verbose, + )?; } else if matches.subcommand_matches("whoami").is_some() { log::info!("Getting User settings"); let user = settings::global_user::GlobalUser::new()?; diff --git a/src/settings/toml/dev.rs b/src/settings/toml/dev.rs index d3fd9cf69..1271b483b 100644 --- a/src/settings/toml/dev.rs +++ b/src/settings/toml/dev.rs @@ -5,4 +5,6 @@ use serde::{Deserialize, Serialize}; pub struct Dev { pub ip: Option, pub port: Option, + pub local_protocol: Option, + pub upstream_protocol: Option, }