Skip to content
This repository has been archived by the owner on Aug 3, 2023. It is now read-only.

refactor and document wrangler dev #1220

Merged
merged 1 commit into from
Apr 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ exitfailure = "0.5.1"
failure = "0.1.5"
flate2 = "1.0.7"
fs2 = "0.4.3"
futures = "0.3"
futures-util = "0.3"
http = "0.2.0"
hyper = "0.13.1"
Expand All @@ -53,7 +52,7 @@ sha2 = "0.8.0"
tempfile = "3.1.0"
term_size = "0.3"
text_io = "0.1.7"
tokio = { version = "0.2", default-features = false, features = ["io-std", "time"] }
tokio = { version = "0.2", default-features = false, features = ["io-std", "time", "macros"] }
tokio-tungstenite = { version = "0.10.1", features = ["tls"] }
toml = "0.5.5"
url = "2.1.0"
Expand Down
Empty file removed src/commands/dev/edge/headers.rs
Empty file.
Empty file removed src/commands/dev/edge/server.rs
Empty file.
Empty file removed src/commands/dev/edge/setup.rs
Empty file.
31 changes: 28 additions & 3 deletions src/commands/dev/headers.rs → src/commands/dev/gcs/headers.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
/// The preview service runs on cloudflareworkers.com and then sends parts
/// of the incoming request to the `request` in the Workers Runtime
///
/// The way it does this is by prefixing the _real_ headers sent to
/// and returned by the worker with this header prefix
const HEADER_PREFIX: &str = "cf-ew-raw-";

/// Any request headers sent to `wrangler dev` must be prefixed
/// before sending it to the preview service
/// and any response headers sent from the preview service
/// that don't have the prefix must be removed
/// and response headers that do have the prefix
/// must have the prefix stripped
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v. nice

use std::str::FromStr;

use hyper::header::{HeaderMap, HeaderName};
use hyper::http::request::Parts as RequestParts;
use hyper::http::response::Parts as ResponseParts;
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
use hyper::http::status::StatusCode;

/// modify an incoming request before sending it to the preview service
pub fn structure_request(parts: &mut RequestParts) {
prepend_request_headers_prefix(parts)
}

/// modify a response from the preview service before returning it to the user
pub fn destructure_response(parts: &mut ResponseParts) -> Result<(), failure::Error> {
set_response_status(parts)?;
strip_response_headers_prefix(parts)
}

/// every header sent to `wrangler dev` must be prefixed
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
/// before sending it along to the preview service
/// so it is sent along to the Workers runtime and not
/// consumed directly by the preview service
fn prepend_request_headers_prefix(parts: &mut RequestParts) {
let mut headers: HeaderMap = HeaderMap::new();

Expand All @@ -29,6 +45,12 @@ fn prepend_request_headers_prefix(parts: &mut RequestParts) {
parts.headers = headers;
}

/// every header returned by the Worker is prefixed
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
/// and headers without a prefix are specific to the preview service
/// here we parse those headers and construct a new header map
///
/// discard headers without that prefix
/// strip the prefix from real Workers headers
fn strip_response_headers_prefix(parts: &mut ResponseParts) -> Result<(), failure::Error> {
let mut headers = HeaderMap::new();

Expand All @@ -45,13 +67,16 @@ fn strip_response_headers_prefix(parts: &mut ResponseParts) -> Result<(), failur
Ok(())
}

/// parse the response status from headers sent by the preview service
/// and apply the parsed result to mutable ResponseParts
fn set_response_status(parts: &mut ResponseParts) -> Result<(), failure::Error> {
let status = parts
.headers
.get("cf-ew-status")
.expect("Could not determine status code of response");
// status will be "404 not found" or "200 ok"
// we need to split that string to create hyper's status code

// `status` above will be "404 not found" or "200 ok"
// we need to parse that string to create hyper's status code
let status_vec: Vec<&str> = status.to_str()?.split(' ').collect();
parts.status = StatusCode::from_str(status_vec[0])?;
Ok(())
Expand Down
83 changes: 83 additions & 0 deletions src/commands/dev/gcs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
mod headers;
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::settings::global_user::GlobalUser;
use crate::settings::toml::Target;

use std::sync::{Arc, Mutex};
use std::thread;

use tokio::runtime::Runtime as TokioRuntime;

/// spin up a local server that routes requests to the preview service
/// that has a Cloudflare Workers runtime without access to zone-specific features
pub fn dev(
target: Target,
user: Option<GlobalUser>,
server_config: ServerConfig,
verbose: bool,
) -> Result<(), failure::Error> {
// setup the session
let session_id = get_session_id()?;

// upload the initial script
let preview_id = get_preview_id(
target.clone(),
user.clone(),
&server_config,
&session_id,
verbose,
)?;

// the local server needs the preview ID to properly route
// HTTP requests
//
// the file watcher updates the preview ID when there is a new
// Worker
//
// Since these run on separate threads, we must stuff the
// preview ID into an Arc<Mutex so that the server waits on the
// file watcher to release the lock before routing a request
let preview_id = Arc::new(Mutex::new(preview_id));
// a new scope is created to satisfy the borrow checker
{
// we must clone each of these variables in order to
// safely use them in another thread
let session_id = session_id.clone();
let preview_id = preview_id.clone();
let server_config = server_config.clone();
thread::spawn(move || {
watch_for_changes(
target,
user,
&server_config,
Arc::clone(&preview_id),
&session_id,
verbose,
)
});
}

// in order to spawn futures we must create a tokio runtime
let mut runtime = TokioRuntime::new()?;

// and we must block the main thread on the completion of
// said futures
runtime.block_on(async {
let devtools_listener = tokio::spawn(socket::listen(session_id.to_string()));
let server = tokio::spawn(serve(server_config, Arc::clone(&preview_id)));
let res = tokio::try_join!(async { devtools_listener.await? }, async { server.await? });

match res {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
})
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
}
135 changes: 135 additions & 0 deletions src/commands/dev/gcs/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use crate::commands::dev::gcs::headers::{destructure_response, structure_request};
use crate::commands::dev::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::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";

/// performs all logic that takes an incoming request
/// and routes it to the Workers runtime preview service
pub(super) async fn serve(
server_config: ServerConfig,
preview_id: Arc<Mutex<String>>,
) -> Result<(), failure::Error> {
// 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.clone();

// create a closure that hyper will use later to handle HTTP requests
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
// this takes care of sending an incoming request along to
// the uploaded Worker script and returning its response
let make_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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mebbe not here, but i'd suggest pulling these nested anonymous functions into named functions for clarity. not 100% necessary tho.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually spent like an hour trying to figure out how to do this but couldn't because of the move statements. Happy for some help on this one

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> = 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)
}
}))
}
});

let server = Server::bind(&listening_address.address).serve(make_service);
println!("{} Listening on http://{}", emoji::EAR, listening_address);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
Ok(())
}

fn preview_request(
req: Request<Body>,
client: HyperClient<HttpsConnector<HttpConnector>>,
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)
}

fn get_preview_url(path_string: &str) -> Result<Uri, InvalidUri> {
format!("https://{}{}", PREVIEW_HOST, path_string).parse()
}

fn get_path_as_str(uri: &Uri) -> String {
uri.path_and_query()
.map(|x| x.as_str())
.unwrap_or("")
.to_string()
}
38 changes: 38 additions & 0 deletions src/commands/dev/gcs/setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::commands::dev::ServerConfig;
use crate::commands::preview::upload;
use crate::settings::global_user::GlobalUser;
use crate::settings::toml::Target;

use uuid::Uuid;

/// generate a unique uuid that lasts the entirety of the
/// `wrangler dev` session
pub(super) fn get_session_id() -> Result<String, failure::Error> {
Ok(Uuid::new_v4().to_simple().to_string())
}

/// upload the script to the Workers API, and combine its response
/// with the session id to get the preview ID
///
/// this is used when sending requests to the Workers Runtime
/// so it executes the correct Worker
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
pub fn get_preview_id(
mut target: Target,
user: Option<GlobalUser>,
server_config: &ServerConfig,
session_id: &str,
verbose: bool,
) -> Result<String, failure::Error> {
// setting sites_preview to `true` would print a message to the terminal
// directing the user to open the browser to view the output
// this message makes sense for `wrangler preview` but not `wrangler dev`
let sites_preview = false;
let script_id = upload(&mut target, user.as_ref(), sites_preview, verbose).map_err(|_| failure::format_err!("Could not upload your script. Check your internet connection or https://www.cloudflarestatus.com/ for rare incidents impacting the Cloudflare Workers API."))?;
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
Ok(format!(
"{}{}{}{}",
&script_id,
session_id,
server_config.host.is_https() as u8,
server_config.host
))
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::{mpsc, Arc, Mutex};

use crate::commands;
use crate::commands::dev::get_preview_id;
use crate::commands::dev::gcs::setup::get_preview_id;
use crate::commands::dev::server_config::ServerConfig;

use crate::settings::global_user::GlobalUser;
Expand All @@ -23,7 +23,14 @@ pub fn watch_for_changes(
let target = target.clone();
commands::build(&target)?;

// acquire the lock so incoming requests are halted
// until the new script is ready for them
let mut preview_id = preview_id.lock().unwrap();

// while holding the lock, assign a new preview id
//
// this allows the server to route subsequent requests
// to the proper script
EverlastingBugstopper marked this conversation as resolved.
Show resolved Hide resolved
*preview_id = get_preview_id(target, user, server_config, session_id, verbose)?;
}

Expand Down
Loading