Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add base base path for HTTP applications #76

Merged
merged 1 commit into from
Feb 15, 2022
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
65 changes: 32 additions & 33 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,12 @@ pub struct RawApplicationInformation {
/// Authors of the application.
pub authors: Option<Vec<String>>,
/// Trigger for the application.
///
/// Currently, all components of a given application must be
/// invoked as a result of the same trigger "type".
/// In the future, applications with mixed triggers might be allowed,
/// but for now, a component with a different trigger must be part of
/// a separate application.
pub trigger: TriggerType,
pub trigger: ApplicationTrigger,
/// TODO
pub namespace: Option<String>,
}
Expand All @@ -121,7 +120,7 @@ pub struct ApplicationInformation {
/// In the future, applications with mixed triggers might be allowed,
/// but for now, a component with a different trigger must be part of
/// a separate application.
pub trigger: TriggerType,
pub trigger: ApplicationTrigger,
/// TODO
pub namespace: Option<String>,
/// The location from which the application is loaded.
Expand Down Expand Up @@ -152,15 +151,28 @@ impl ApplicationInformation {

/// The trigger type.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub enum TriggerType {
#[serde(deny_unknown_fields, rename_all = "camelCase", tag = "type")]
pub enum ApplicationTrigger {
/// HTTP trigger type.
Http,
Http(HttpTriggerConfiguration),
}

impl Default for ApplicationTrigger {
fn default() -> Self {
Self::Http(HttpTriggerConfiguration::default())
}
}

impl Default for TriggerType {
/// HTTP trigger configuration.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct HttpTriggerConfiguration {
/// Base path for the HTTP application.
pub base: String,
}
impl Default for HttpTriggerConfiguration {
fn default() -> Self {
Self::Http
Self { base: "/".into() }
}
}

Expand Down Expand Up @@ -274,26 +286,14 @@ impl Default for TriggerConfig {
/// Currently, this map should either contain an interface that
/// should be satisfied by the host (through a host implementation),
/// or an exact reference (*not* a version range) to a component from the registry.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Dependency {
/// The dependency type.
#[serde(rename = "type")]
pub dependency_type: DependencyType,

/// Reference to a component from the registry.
#[serde(flatten)]
pub reference: Option<BindleComponentSource>,
}

/// The dependency type.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub enum DependencyType {
#[serde(deny_unknown_fields, rename_all = "camelCase", tag = "type")]
pub enum Dependency {
/// A host dependency.
Host,
/// A component dependency.
Component,
Component(BindleComponentSource),
}

#[cfg(test)]
Expand All @@ -306,8 +306,8 @@ mod tests {
version = "6.11.2"
description = "A simple application that returns the number of lights"
authors = [ "Gul Madred", "Edward Jellico", "JL" ]
trigger = "http"
trigger = { type = "http", base = "/" }

[[component]]
source = "path/to/wasm/file.wasm"
id = "four-lights"
Expand All @@ -334,13 +334,16 @@ mod tests {
#[test]
fn test_local_config() -> Result<()> {
let cfg: RawConfiguration<LinkableComponent> = toml::from_str(CFG_TEST)?;

assert_eq!(cfg.info.name, "chain-of-command");
assert_eq!(cfg.info.version, "6.11.2");
assert_eq!(
cfg.info.description,
Some("A simple application that returns the number of lights".to_string())
);

let ApplicationTrigger::Http(http) = cfg.info.trigger;
assert_eq!(http.base, "/".to_string());

assert_eq!(cfg.info.authors.unwrap().len(), 3);
assert_eq!(cfg.components[0].core.id, "four-lights".to_string());

Expand All @@ -353,14 +356,10 @@ mod tests {
let test_env = test_component.core.wasm.environment.as_ref().unwrap();
let test_files = test_component.core.wasm.files.as_ref().unwrap();

assert_eq!(test_deps.get("cache").unwrap(), &Dependency::Host);
assert_eq!(
test_deps.get("cache").unwrap().dependency_type,
DependencyType::Host
);

assert_eq!(
test_deps.get("markdown").unwrap().reference,
Some(BindleComponentSource {
test_deps.get("markdown").unwrap(),
&Dependency::Component(BindleComponentSource {
reference: "github/octo-markdown/1.0.0".to_string(),
parcel: "md.wasm".to_string()
})
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ mod tests {
version = "1.0.0"
description = "A simple application that returns hello and goodbye."
authors = [ "Radu Matei <radu@fermyon.com>" ]
trigger = "http"
trigger = { type = "http", base = "/" }

[[component]]
source = "target/wasm32-wasi/release/hello.wasm"
Expand Down
167 changes: 164 additions & 3 deletions crates/http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ mod wagi;
use crate::wagi::WagiHttpExecutor;
use anyhow::{Error, Result};
use async_trait::async_trait;
use http::StatusCode;
use http::{StatusCode, Uri};
use hyper::{
server::conn::AddrStream,
service::{make_service_fn, service_fn},
Body, Request, Response, Server,
};
use routes::Router;
use routes::{RoutePattern, Router};
use spin::SpinHttpExecutor;
use spin_config::{Configuration, CoreComponent, TriggerConfig};
use spin_config::{ApplicationTrigger, Configuration, CoreComponent, TriggerConfig};
use spin_engine::{Builder, ExecutionContextConfiguration};
use spin_http::SpinHttpData;
use std::{net::SocketAddr, sync::Arc};
Expand Down Expand Up @@ -82,6 +82,8 @@ impl HttpTrigger {
req.uri()
);

let ApplicationTrigger::Http(app_trigger) = &self.app.info.trigger.clone();

match req.uri().path() {
"/healthz" => Ok(Response::new(Body::from("OK"))),
route => match self.router.route(route) {
Expand All @@ -97,6 +99,7 @@ impl HttpTrigger {
SpinHttpExecutor::execute(
&self.engine,
&c.id,
&app_trigger.base,
&trigger.route,
req,
addr,
Expand All @@ -107,6 +110,7 @@ impl HttpTrigger {
WagiHttpExecutor::execute(
&self.engine,
&c.id,
&app_trigger.base,
&trigger.route,
req,
addr,
Expand Down Expand Up @@ -184,13 +188,59 @@ fn on_ctrl_c() -> Result<impl std::future::Future<Output = Result<(), tokio::tas
Ok(rx_future)
}

// The default headers set across both executors.
const X_FULL_URL_HEADER: &str = "X_FULL_URL";
const PATH_INFO_HEADER: &str = "PATH_INFO";
const X_MATCHED_ROUTE_HEADER: &str = "X_MATCHED_ROUTE";
const X_COMPONENT_ROUTE_HEADER: &str = "X_COMPONENT_ROUTE";
const X_RAW_COMPONENT_ROUTE_HEADER: &str = "X_RAW_COMPONENT_ROUTE";
const X_BASE_PATH_HEADER: &str = "X_BASE_PATH";

pub(crate) fn default_headers(
uri: &Uri,
raw: &str,
base: &str,
host: &str,
// scheme: &str,
) -> Result<Vec<(String, String)>> {
let mut res = vec![];
let abs_path = uri
.path_and_query()
.expect("cannot get path and query")
.as_str();

let path_info = RoutePattern::from(base, raw).relative(abs_path)?;

// TODO: check if TLS is enabled and change the scheme to "https".
let scheme = "http";
let full_url = format!("{}://{}{}", scheme, host, abs_path);
let matched_route = RoutePattern::sanitize_with_base(base, raw);

res.push((PATH_INFO_HEADER.to_string(), path_info));
res.push((X_FULL_URL_HEADER.to_string(), full_url));
res.push((X_MATCHED_ROUTE_HEADER.to_string(), matched_route));

res.push((X_BASE_PATH_HEADER.to_string(), base.to_string()));
res.push((X_RAW_COMPONENT_ROUTE_HEADER.to_string(), raw.to_string()));
res.push((
X_COMPONENT_ROUTE_HEADER.to_string(),
raw.to_string()
.strip_suffix("/...")
.unwrap_or(raw)
.to_string(),
));

Ok(res)
}

/// The HTTP executor trait.
/// All HTTP executors must implement this trait.
#[async_trait]
pub(crate) trait HttpExecutor: Clone + Send + Sync + 'static {
async fn execute(
Copy link
Member Author

Choose a reason for hiding this comment

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

At some point, we should refactor this, since the argument list keeps growing.

engine: &ExecutionContext,
component: &str,
base: &str,
raw_route: &str,
req: Request<Body>,
client_addr: SocketAddr,
Expand Down Expand Up @@ -230,6 +280,117 @@ mod tests {
spin_config::ApplicationOrigin::File(fake_path)
}

#[test]
fn test_default_headers_with_base_path() -> Result<()> {
let scheme = "https";
let host = "fermyon.dev";
let base = "/base";
let trigger_route = "/foo/...";
let component_path = "/foo";
let path_info = "/bar";

let req_uri = format!(
"{}://{}{}{}{}?key1=value1&key2=value2",
scheme, host, base, component_path, path_info
);

let req = http::Request::builder()
.method("POST")
.uri(req_uri)
.body("")?;

let default_headers = crate::default_headers(req.uri(), trigger_route, base, host)?;

// TODO: we currently replace the scheme with HTTP. When TLS is supported, this should be fixed.
assert_eq!(
search(X_FULL_URL_HEADER, &default_headers).unwrap(),
"http://fermyon.dev/base/foo/bar?key1=value1&key2=value2".to_string()
);
assert_eq!(
search(PATH_INFO_HEADER, &default_headers).unwrap(),
"/bar".to_string()
);
assert_eq!(
search(X_MATCHED_ROUTE_HEADER, &default_headers).unwrap(),
"/base/foo/...".to_string()
);
assert_eq!(
search(X_BASE_PATH_HEADER, &default_headers).unwrap(),
"/base".to_string()
);
assert_eq!(
search(X_RAW_COMPONENT_ROUTE_HEADER, &default_headers).unwrap(),
"/foo/...".to_string()
);
assert_eq!(
search(X_COMPONENT_ROUTE_HEADER, &default_headers).unwrap(),
"/foo".to_string()
);

Ok(())
}

#[test]
fn test_default_headers_without_base_path() -> Result<()> {
let scheme = "https";
let host = "fermyon.dev";
let base = "/";
let trigger_route = "/foo/...";
let component_path = "/foo";
let path_info = "/bar";

let req_uri = format!(
"{}://{}{}{}?key1=value1&key2=value2",
scheme, host, component_path, path_info
);

let req = http::Request::builder()
.method("POST")
.uri(req_uri)
.body("")?;

let default_headers = crate::default_headers(req.uri(), trigger_route, base, host)?;

// TODO: we currently replace the scheme with HTTP. When TLS is supported, this should be fixed.
assert_eq!(
search(X_FULL_URL_HEADER, &default_headers).unwrap(),
"http://fermyon.dev/foo/bar?key1=value1&key2=value2".to_string()
);
assert_eq!(
search(PATH_INFO_HEADER, &default_headers).unwrap(),
"/bar".to_string()
);
assert_eq!(
search(X_MATCHED_ROUTE_HEADER, &default_headers).unwrap(),
"/foo/...".to_string()
);
assert_eq!(
search(X_BASE_PATH_HEADER, &default_headers).unwrap(),
"/".to_string()
);
assert_eq!(
search(X_RAW_COMPONENT_ROUTE_HEADER, &default_headers).unwrap(),
"/foo/...".to_string()
);
assert_eq!(
search(X_COMPONENT_ROUTE_HEADER, &default_headers).unwrap(),
"/foo".to_string()
);

Ok(())
}

fn search(key: &str, headers: &[(String, String)]) -> Option<String> {
let mut res: Option<String> = None;
for (k, v) in headers {
if k == key {
res = Some(v.clone());
}
}

res
}

#[tokio::test]
async fn test_spin_http() -> Result<()> {
init();
Expand Down
Loading