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

feat: authenticate calls to preview service when possible #429

Merged
merged 15 commits into from
Aug 15, 2019
Merged
Show file tree
Hide file tree
Changes from 11 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
66 changes: 1 addition & 65 deletions src/commands/publish/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::terminal::message;
pub fn publish(user: &GlobalUser, project: &Project, release: bool) -> Result<(), failure::Error> {
info!("release = {}", release);

validate_project(project, release)?;
project.validate(release)?;
ashleymichal marked this conversation as resolved.
Show resolved Hide resolved
commands::build(&project)?;
publish_script(&user, &project, release)?;
if release {
Expand Down Expand Up @@ -115,67 +115,3 @@ fn make_public_on_subdomain(project: &Project, user: &GlobalUser) -> Result<(),
}
Ok(())
}

fn validate_project(project: &Project, release: bool) -> Result<(), failure::Error> {
let mut missing_fields = Vec::new();

if project.account_id.is_empty() {
missing_fields.push("account_id")
};
if project.name.is_empty() {
missing_fields.push("name")
};

match &project.kv_namespaces {
Some(kv_namespaces) => {
for kv in kv_namespaces {
if kv.binding.is_empty() {
missing_fields.push("kv-namespace binding")
}

if kv.id.is_empty() {
missing_fields.push("kv-namespace id")
}
}
}
None => {}
}

let destination = if release {
//check required fields for release
if project
.zone_id
.as_ref()
.unwrap_or(&"".to_string())
.is_empty()
{
missing_fields.push("zone_id")
};
if project.route.as_ref().unwrap_or(&"".to_string()).is_empty() {
missing_fields.push("route")
};
//zoned deploy destination
"a route"
} else {
//zoneless deploy destination
"your subdomain"
};

let (field_pluralization, is_are) = match missing_fields.len() {
n if n >= 2 => ("fields", "are"),
1 => ("field", "is"),
_ => ("", ""),
};

if !missing_fields.is_empty() {
failure::bail!(
"Your wrangler.toml is missing the {} {:?} which {} required to publish to {}!",
field_pluralization,
missing_fields,
is_are,
destination
);
};

Ok(())
}
154 changes: 122 additions & 32 deletions src/commands/publish/preview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,63 +10,158 @@ use uuid::Uuid;

use crate::commands;
use crate::http;
use crate::settings::global_user::GlobalUser;
use crate::settings::project::Project;
use crate::terminal::message;
use reqwest::Client;

// Using this instead of just `https://cloudflareworkers.com` returns just the worker response to the CLI
const PREVIEW_ADDRESS: &str = "https://00000000000000000000000000000000.cloudflareworkers.com";
ashleymichal marked this conversation as resolved.
Show resolved Hide resolved

#[derive(Debug, Deserialize)]
struct Preview {
pub id: String,
}

impl From<ApiPreview> for Preview {
fn from(api_preview: ApiPreview) -> Preview {
Preview {
id: api_preview.preview_id,
}
}
}

// When making authenticated preview requests, we go through the v4 Workers API rather than
// hitting the preview service directly, so its response is formatted like a v4 API response.
// These structs are here to convert from this format into the Preview defined above.
#[derive(Debug, Deserialize)]
struct ApiPreview {
pub preview_id: String,
}

#[derive(Debug, Deserialize)]
struct V4ApiResponse {
pub result: ApiPreview,
}

pub fn preview(
project: &Project,
method: Result<HTTPMethod, failure::Error>,
mut project: Project,
user: Option<GlobalUser>,
method: HTTPMethod,
body: Option<String>,
) -> Result<(), failure::Error> {
let create_address = "https://cloudflareworkers.com/script";
let client: Client;

let preview = match &user {
Some(user) => {
log::info!("GlobalUser set, running with authentication");

project.validate(false)?;

commands::build(&project)?;
client = http::auth_client(&user);

authenticated_upload(&client, &project)?
}
None => {
log::info!("GlobalUser not set, running without authentication");

// KV namespaces are not supported by the preview service unless you authenticate
// so we omit them and provide the user with a little guidance. We don't error out, though,
// because there are valid workarounds for this for testing purposes.
if project.kv_namespaces.is_some() {
message::warn("KV Namespaces are not supported without setting API credentials");
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

message::help(
"Run `wrangler config` or set $CF_API_KEY and $CF_EMAIL to configure your user.",
);
project.kv_namespaces = None;
}

commands::build(&project)?;
client = http::client();

unauthenticated_upload(&client, &project)?
}
};

let worker_res = call_worker(&client, preview, method, body)?;

let msg = format!("Your worker responded with: {}", worker_res);
ashleymichal marked this conversation as resolved.
Show resolved Hide resolved
message::preview(&msg);

let client = http::client();
Ok(())
}

commands::build(&project)?;
fn authenticated_upload(client: &Client, project: &Project) -> Result<Preview, failure::Error> {
let create_address = format!(
"https://api.cloudflare.com/client/v4/accounts/{}/workers/scripts/{}/preview",
project.account_id, project.name
);
log::info!("address: {}", create_address);

let script_upload_form = publish::build_script_upload_form(&project)?;

let mut res = client
.post(&create_address)
.multipart(script_upload_form)
.send()?
.error_for_status()?;

let text = &res.text()?;
log::info!("Response from preview: {:#?}", text);

let response: V4ApiResponse =
serde_json::from_str(text).expect("could not create a script on cloudflareworkers.com");

Ok(Preview::from(response.result))
}

fn unauthenticated_upload(client: &Client, project: &Project) -> Result<Preview, failure::Error> {
ashleymichal marked this conversation as resolved.
Show resolved Hide resolved
let create_address = "https://cloudflareworkers.com/script";
log::info!("address: {}", create_address);

let script_upload_form = publish::build_script_upload_form(project)?;

let res = client
let mut res = client
ashleymichal marked this conversation as resolved.
Show resolved Hide resolved
.post(create_address)
.multipart(script_upload_form)
.send()?
.error_for_status();
.error_for_status()?;

let text = &res?.text()?;
log::info!("Response from preview: {:?}", text);
let text = &res.text()?;
log::info!("Response from preview: {:#?}", text);
ashleymichal marked this conversation as resolved.
Show resolved Hide resolved

let p: Preview =
let preview: Preview =
ashleymichal marked this conversation as resolved.
Show resolved Hide resolved
serde_json::from_str(text).expect("could not create a script on cloudflareworkers.com");

Ok(preview)
}

fn call_worker(
client: &Client,
preview: Preview,
method: HTTPMethod,
body: Option<String>,
) -> Result<String, failure::Error> {
let session = Uuid::new_v4().to_simple();

let preview_host = "example.com";
let https = 1;
let script_id = &p.id;
let script_id = &preview.id;

let preview_address = "https://00000000000000000000000000000000.cloudflareworkers.com";
let cookie = format!(
"__ew_fiddle_preview={}{}{}{}",
script_id, session, https, preview_host
);

let method = method.unwrap_or_default();

let worker_res = match method {
HTTPMethod::Get => get(preview_address, cookie, client)?,
HTTPMethod::Post => post(preview_address, cookie, client, body)?,
let res = match method {
HTTPMethod::Get => get(cookie, &client)?,
HTTPMethod::Post => post(cookie, &client, body)?,
};
let msg = format!("Your worker responded with: {}", worker_res);
message::preview(&msg);

open(preview_host, https, script_id)?;

Ok(())
Ok(res)
}

fn open(preview_host: &str, https: u8, script_id: &str) -> Result<(), failure::Error> {
Expand Down Expand Up @@ -96,32 +191,27 @@ fn open(preview_host: &str, https: u8, script_id: &str) -> Result<(), failure::E
Ok(())
}

fn get(
preview_address: &str,
cookie: String,
client: reqwest::Client,
) -> Result<String, failure::Error> {
let res = client.get(preview_address).header("Cookie", cookie).send();
let msg = format!("GET {}", preview_address);
fn get(cookie: String, client: &reqwest::Client) -> Result<String, failure::Error> {
let res = client.get(PREVIEW_ADDRESS).header("Cookie", cookie).send();
let msg = format!("GET {}", PREVIEW_ADDRESS);
message::preview(&msg);
Ok(res?.text()?)
}

fn post(
preview_address: &str,
cookie: String,
client: reqwest::Client,
client: &reqwest::Client,
body: Option<String>,
) -> Result<String, failure::Error> {
let res = match body {
Some(s) => client
.post(preview_address)
.post(PREVIEW_ADDRESS)
.header("Cookie", cookie)
.body(s)
.send(),
None => client.post(preview_address).header("Cookie", cookie).send(),
None => client.post(PREVIEW_ADDRESS).header("Cookie", cookie).send(),
};
let msg = format!("POST {}", preview_address);
let msg = format!("POST {}", PREVIEW_ADDRESS);
message::preview(&msg);
Ok(res?.text()?)
}
8 changes: 6 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,18 @@ fn run() -> Result<(), failure::Error> {
info!("Getting project settings");
let project = settings::project::Project::new()?;

let method = HTTPMethod::from_str(matches.value_of("method").unwrap_or("get"));
// the preview command can be called with or without a Global User having been config'd
// so we convert this Result into an Option
let user = settings::global_user::GlobalUser::new().ok();

let method = HTTPMethod::from_str(matches.value_of("method").unwrap_or("get"))?;

let body = match matches.value_of("body") {
Some(s) => Some(s.to_string()),
None => None,
};

commands::preview(&project, method, body)?;
commands::preview(project, user, method, body)?;
} else if matches.subcommand_matches("whoami").is_some() {
info!("Getting User settings");
let user = settings::global_user::GlobalUser::new()?;
Expand Down
59 changes: 59 additions & 0 deletions src/settings/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,65 @@ impl Project {
pub fn kv_namespaces(&self) -> Vec<KvNamespace> {
self.kv_namespaces.clone().unwrap_or_else(Vec::new)
}

pub fn validate(&self, release: bool) -> Result<(), failure::Error> {
let mut missing_fields = Vec::new();

if self.account_id.is_empty() {
missing_fields.push("account_id")
};
if self.name.is_empty() {
missing_fields.push("name")
};

match &self.kv_namespaces {
Some(kv_namespaces) => {
for kv in kv_namespaces {
if kv.binding.is_empty() {
missing_fields.push("kv-namespaces binding")
}

if kv.id.is_empty() {
missing_fields.push("kv-namespaces id")
}
}
}
None => {}
}

let destination = if release {
//check required fields for release
if self.zone_id.as_ref().unwrap_or(&"".to_string()).is_empty() {
missing_fields.push("zone_id")
};
if self.route.as_ref().unwrap_or(&"".to_string()).is_empty() {
missing_fields.push("route")
};
//zoned deploy destination
"a route"
} else {
//zoneless deploy destination
"your subdomain"
};

let (field_pluralization, is_are) = match missing_fields.len() {
n if n >= 2 => ("fields", "are"),
1 => ("field", "is"),
_ => ("", ""),
};

if !missing_fields.is_empty() {
failure::bail!(
"Your wrangler.toml is missing the {} {:?} which {} required to publish to {}!",
field_pluralization,
missing_fields,
is_are,
destination
);
};

Ok(())
}
}

fn get_project_config(config_path: &Path) -> Result<Project, failure::Error> {
Expand Down