use crate::request::ApiGatewayV2;
use core::convert::TryFrom;
use core::future::Future;
use std::convert::Infallible;
use std::pin::Pin;
use lamedh_runtime::{
run as lambda_runtime_run, Context as LambdaContext, Error as LambdaError,
Handler as LambdaHandler,
};
type WarpRequest = warp::http::Request<warp::hyper::Body>;
type WarpResponse = warp::http::Response<warp::hyper::Body>;
pub async fn run_warp_on_lambda<S>(svc: S) -> Result<(), LambdaError>
where
S: warp::hyper::service::Service<WarpRequest, Response = WarpResponse, Error = Infallible>
+ Clone
+ Send
+ 'static,
S::Future: Send,
{
lambda_runtime_run(WarpHandler(svc)).await?;
Ok(())
}
struct WarpHandler<S>(S)
where
S: warp::hyper::service::Service<WarpRequest, Response = WarpResponse, Error = Infallible>
+ 'static;
impl<S> LambdaHandler<ApiGatewayV2<'_>, serde_json::Value> for WarpHandler<S>
where
S: warp::hyper::service::Service<WarpRequest, Response = WarpResponse, Error = Infallible>
+ 'static,
{
type Error = LambdaError;
type Fut = Pin<Box<dyn Future<Output = Result<serde_json::Value, Self::Error>> + 'static>>;
fn call(&mut self, event: ApiGatewayV2, _context: LambdaContext) -> Self::Fut {
use serde_json::json;
let client_br = crate::brotli::client_supports_brotli(&event);
let warp_request = WarpRequest::try_from(event);
let svc_call = warp_request.map(|req| self.0.call(req));
let fut = async move {
match svc_call {
Ok(svc_fut) => {
if let Ok(response) = svc_fut.await {
api_gateway_response_from_warp(response, client_br).await
} else {
Ok(json!({
"isBase64Encoded": false,
"statusCode": 500u16,
"headers": { "content-type": "text/plain"},
"body": "Internal Server Error"
}))
}
}
Err(_request_err) => {
Ok(json!({
"isBase64Encoded": false,
"statusCode": 400u16,
"headers": { "content-type": "text/plain"},
"body": "Bad Request"
}))
}
}
};
Box::pin(fut)
}
}
impl TryFrom<ApiGatewayV2<'_>> for WarpRequest {
type Error = LambdaError;
fn try_from(event: ApiGatewayV2) -> Result<Self, Self::Error> {
use std::str::FromStr;
use warp::http::header::{HeaderName, HeaderValue, COOKIE};
use warp::http::Method;
let uri = if event.raw_query_string.is_empty() {
format!(
"https://{}{}",
event.request_context.domain_name,
event.encoded_path()
)
} else {
format!(
"https://{}{}?{}",
event.request_context.domain_name,
event.encoded_path(),
event.raw_query_string
)
};
let method = Method::try_from(&event.request_context.http.method as &str)?;
let mut reqbuilder = warp::http::Request::builder().method(method).uri(&uri);
if let Some(headers_mut) = reqbuilder.headers_mut() {
for (k, v) in &event.headers {
if let (Ok(k), Ok(v)) = (
HeaderName::from_str(k as &str),
HeaderValue::from_str(v as &str),
) {
headers_mut.insert(k, v);
}
}
if let Some(cookies) = event.cookies {
if let Ok(cookie_value) = HeaderValue::from_str(&cookies.join(";")) {
headers_mut.insert(COOKIE, cookie_value);
}
}
}
let req = if let Some(eventbody) = event.body {
if event.is_base64_encoded {
let binarybody = base64::decode(&eventbody as &str)?;
reqbuilder.body(warp::hyper::Body::from(binarybody))?
} else {
reqbuilder.body(warp::hyper::Body::from(eventbody.into_owned()))?
}
} else {
reqbuilder.body(warp::hyper::Body::empty())?
};
Ok(req)
}
}
impl crate::brotli::ResponseCompression for WarpResponse {
fn content_encoding<'a>(&'a self) -> Option<&'a str> {
self.headers()
.get(warp::hyper::header::CONTENT_ENCODING)
.and_then(|val| val.to_str().ok())
}
fn content_type<'a>(&'a self) -> Option<&'a str> {
self.headers()
.get(warp::hyper::header::CONTENT_TYPE)
.and_then(|val| val.to_str().ok())
}
}
async fn api_gateway_response_from_warp(
response: WarpResponse,
client_support_br: bool,
) -> Result<serde_json::Value, LambdaError> {
use crate::brotli::ResponseCompression;
use serde_json::json;
let compress = client_support_br && response.can_brotli_compress();
let (parts, res_body) = response.into_parts();
let status_code = parts.status.as_u16();
let mut headers = serde_json::Map::new();
for (k, v) in parts.headers.iter() {
if let Ok(value_str) = v.to_str() {
headers.insert(k.as_str().to_string(), json!(value_str));
}
}
let body_bytes = warp::hyper::body::to_bytes(res_body).await?;
let body_base64 = if compress {
headers.insert("content-encoding".to_string(), json!("br"));
crate::brotli::compress_response_body(&body_bytes)
} else {
base64::encode(body_bytes)
};
Ok(json!({
"isBase64Encoded": true,
"statusCode": status_code,
"headers": headers,
"body": body_base64,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{request::ApiGatewayV2, test_consts::*};
#[test]
fn test_path_decode() {
let reqjson: ApiGatewayV2 = serde_json::from_str(API_GATEWAY_V2_GET_ROOT_NOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().path(), "/");
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_SOMEWHERE_NOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().path(), "/somewhere");
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_SPACEPATH_NOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().path(), "/path%20with/space");
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_PERCENTPATH_NOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().path(), "/path%25with/percent");
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_UTF8PATH_NOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(
req.uri().path(),
"/%E6%97%A5%E6%9C%AC%E8%AA%9E/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D"
);
}
#[test]
fn test_query_decode() {
let reqjson: ApiGatewayV2 = serde_json::from_str(API_GATEWAY_V2_GET_ROOT_ONEQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().query(), Some("key=value"));
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_SOMEWHERE_ONEQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().query(), Some("key=value"));
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_SOMEWHERE_TWOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().query(), Some("key1=value1&key2=value2"));
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_SOMEWHERE_SPACEQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().query(), Some("key=value1+value2"));
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_GET_SOMEWHERE_UTF8QUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.uri().query(), Some("key=%E6%97%A5%E6%9C%AC%E8%AA%9E"));
}
#[tokio::test]
async fn test_form_post() {
use warp::http::method::Method;
use warp::hyper::body::to_bytes;
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_POST_FORM_URLENCODED).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.method(), Method::POST);
assert_eq!(
to_bytes(req.into_body()).await.unwrap().as_ref(),
b"key1=value1&key2=value2&Ok=Ok"
);
let reqjson: ApiGatewayV2 =
serde_json::from_str(API_GATEWAY_V2_POST_FORM_URLENCODED_B64).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.method(), Method::POST);
assert_eq!(
to_bytes(req.into_body()).await.unwrap().as_ref(),
b"key1=value1&key2=value2&Ok=Ok"
);
}
#[test]
fn test_parse_header() {
let reqjson: ApiGatewayV2 = serde_json::from_str(API_GATEWAY_V2_GET_ROOT_NOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.headers().get("x-forwarded-port").unwrap(), &"443");
assert_eq!(req.headers().get("x-forwarded-proto").unwrap(), &"https");
}
#[test]
fn test_parse_cookies() {
let reqjson: ApiGatewayV2 = serde_json::from_str(API_GATEWAY_V2_GET_ROOT_NOQUERY).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.headers().get("cookie"), None);
let reqjson: ApiGatewayV2 = serde_json::from_str(API_GATEWAY_V2_GET_ONE_COOKIE).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(req.headers().get("cookie").unwrap(), &"cookie1=value1");
let reqjson: ApiGatewayV2 = serde_json::from_str(API_GATEWAY_V2_GET_TWO_COOKIES).unwrap();
let req = WarpRequest::try_from(reqjson).unwrap();
assert_eq!(
req.headers().get("cookie").unwrap(),
&"cookie2=value2;cookie1=value1"
);
}
}