This repository has been archived by the owner on Aug 3, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 335
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #883 from cloudflare/avery/a-time-sync
[rc] [dev] Dev Server Feature Branch
- Loading branch information
Showing
14 changed files
with
1,642 additions
and
1,026 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
const HEADER_PREFIX: &str = "cf-ew-raw-"; | ||
|
||
use std::str::FromStr; | ||
|
||
use hyper::header::{HeaderMap, HeaderName}; | ||
use hyper::http::request::Parts as RequestParts; | ||
use hyper::http::response::Parts as ResponseParts; | ||
use hyper::http::status::StatusCode; | ||
|
||
pub fn structure_request(parts: &mut RequestParts) { | ||
prepend_request_headers_prefix(parts) | ||
} | ||
|
||
pub fn destructure_response(parts: &mut ResponseParts) -> Result<(), failure::Error> { | ||
set_response_status(parts)?; | ||
strip_response_headers_prefix(parts) | ||
} | ||
|
||
fn prepend_request_headers_prefix(parts: &mut RequestParts) { | ||
let mut headers: HeaderMap = HeaderMap::new(); | ||
|
||
for header in &parts.headers { | ||
let (name, value) = header; | ||
let forward_header = format!("{}{}", HEADER_PREFIX, name); | ||
let header_name = HeaderName::from_bytes(forward_header.as_bytes()) | ||
.unwrap_or_else(|_| panic!("Could not create header name for {}", name)); | ||
headers.insert(header_name, value.clone()); | ||
} | ||
parts.headers = headers; | ||
} | ||
|
||
fn strip_response_headers_prefix(parts: &mut ResponseParts) -> Result<(), failure::Error> { | ||
let mut headers = HeaderMap::new(); | ||
|
||
for header in &parts.headers { | ||
let (name, value) = header; | ||
let name = name.as_str(); | ||
if name.starts_with(HEADER_PREFIX) { | ||
let header_name = &name[HEADER_PREFIX.len()..]; | ||
let header_name = HeaderName::from_bytes(header_name.as_bytes())?; | ||
headers.insert(header_name, value.clone()); | ||
} | ||
} | ||
parts.headers = headers; | ||
Ok(()) | ||
} | ||
|
||
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 | ||
let status_vec: Vec<&str> = status.to_str()?.split(' ').collect(); | ||
parts.status = StatusCode::from_str(status_vec[0])?; | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
mod server_config; | ||
mod socket; | ||
use server_config::ServerConfig; | ||
mod headers; | ||
use headers::{destructure_response, structure_request}; | ||
mod watch; | ||
use watch::watch_for_changes; | ||
|
||
use std::sync::{Arc, Mutex}; | ||
use std::thread; | ||
|
||
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; | ||
|
||
use tokio::runtime::Runtime as TokioRuntime; | ||
|
||
use uuid::Uuid; | ||
|
||
use crate::commands; | ||
use crate::commands::preview::upload; | ||
|
||
use crate::settings::global_user::GlobalUser; | ||
use crate::settings::toml::Target; | ||
|
||
use crate::terminal::emoji; | ||
|
||
const PREVIEW_HOST: &str = "rawhttp.cloudflareworkers.com"; | ||
|
||
pub fn dev( | ||
target: Target, | ||
user: Option<GlobalUser>, | ||
host: Option<&str>, | ||
port: Option<&str>, | ||
ip: Option<&str>, | ||
verbose: bool, | ||
) -> Result<(), failure::Error> { | ||
commands::build(&target)?; | ||
let server_config = ServerConfig::new(host, ip, port)?; | ||
let session_id = get_session_id()?; | ||
let preview_id = get_preview_id( | ||
target.clone(), | ||
user.clone(), | ||
&server_config, | ||
&session_id.clone(), | ||
verbose, | ||
)?; | ||
let preview_id = Arc::new(Mutex::new(preview_id)); | ||
|
||
{ | ||
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, | ||
) | ||
}); | ||
} | ||
|
||
let mut runtime = TokioRuntime::new()?; | ||
|
||
let devtools_listener = socket::listen(&session_id); | ||
let server = serve(server_config, Arc::clone(&preview_id)); | ||
|
||
let runners = futures::future::join(devtools_listener, server); | ||
|
||
runtime.block_on(async { | ||
let (devtools_listener, server) = runners.await; | ||
devtools_listener?; | ||
server | ||
}) | ||
} | ||
|
||
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 | ||
let make_service = make_service_fn(move |_| { | ||
let client = client.to_owned(); | ||
let preview_id = preview_id.lock().unwrap().to_owned(); | ||
let server_config = server_config.to_owned(); | ||
async move { | ||
Ok::<_, failure::Error>(service_fn(move |req| { | ||
let client = client.to_owned(); | ||
let preview_id = preview_id.to_owned(); | ||
let server_config = server_config.to_owned(); | ||
async move { | ||
let resp = | ||
preview_request(req, client, preview_id.to_owned(), server_config).await?; | ||
let (mut parts, body) = resp.into_parts(); | ||
|
||
destructure_response(&mut parts)?; | ||
let resp = Response::from_parts(parts, body); | ||
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 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() | ||
} | ||
|
||
fn preview_request( | ||
req: Request<Body>, | ||
client: HyperClient<HttpsConnector<HttpConnector>>, | ||
preview_id: String, | ||
server_config: ServerConfig, | ||
) -> ResponseFuture { | ||
let (mut parts, body) = req.into_parts(); | ||
|
||
let path = get_path_as_str(&parts.uri); | ||
let method = parts.method.to_string(); | ||
let now: DateTime<Local> = Local::now(); | ||
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); | ||
|
||
println!( | ||
"[{}] \"{} {}{} {:?}\"", | ||
now.format("%Y-%m-%d %H:%M:%S"), | ||
method, | ||
server_config.host, | ||
path, | ||
req.version() | ||
); | ||
client.request(req) | ||
} | ||
|
||
fn get_session_id() -> Result<String, failure::Error> { | ||
Ok(Uuid::new_v4().to_simple().to_string()) | ||
} | ||
|
||
pub fn get_preview_id( | ||
mut target: Target, | ||
user: Option<GlobalUser>, | ||
server_config: &ServerConfig, | ||
session_id: &str, | ||
verbose: bool, | ||
) -> Result<String, failure::Error> { | ||
let sites_preview = false; | ||
let script_id = upload(&mut target, user.as_ref(), sites_preview, verbose)?; | ||
Ok(format!( | ||
"{}{}{}{}", | ||
&script_id, | ||
session_id, | ||
server_config.host.is_https() as u8, | ||
server_config.host | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
use std::fmt; | ||
|
||
use failure::format_err; | ||
use url::Url; | ||
|
||
#[derive(Clone)] | ||
pub struct Host { | ||
url: Url, | ||
} | ||
|
||
impl Host { | ||
pub fn new(host: &str) -> Result<Self, failure::Error> { | ||
// try to create a url from host | ||
let url = match Url::parse(&host) { | ||
Ok(host) => Ok(host), | ||
// if it doesn't work, it might be because there was no scheme | ||
// default to https | ||
Err(_) => Url::parse(&format!("https://{}", host)), | ||
}?; | ||
|
||
// validate scheme | ||
let scheme = url.scheme(); | ||
if scheme != "http" && scheme != "https" { | ||
failure::bail!("Your host scheme must be either http or https") | ||
} | ||
|
||
// validate host | ||
let host = url.host_str().ok_or_else(|| format_err!("Invalid host, accepted formats are example.com, http://example.com, or https://example.com"))?; | ||
|
||
// recreate url without any trailing path | ||
let url = Url::parse(&format!("{}://{}", scheme, host))?; | ||
Ok(Host { url }) | ||
} | ||
|
||
pub fn is_https(&self) -> bool { | ||
self.url.scheme() == "https" | ||
} | ||
} | ||
|
||
impl fmt::Display for Host { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
write!( | ||
f, | ||
"{}", | ||
self.url | ||
.host_str() | ||
.expect("could not parse host") | ||
.to_string() | ||
) | ||
} | ||
} |
Oops, something went wrong.