use serde::Deserialize;
use std::borrow::Cow;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub(crate) enum LambdaHttpEvent<'a> {
ApiGatewayHttpV2(ApiGatewayHttpV2Event<'a>),
ApiGatewayRestOrAlb(ApiGatewayRestEvent<'a>),
}
impl LambdaHttpEvent<'_> {
pub fn method<'a>(&'a self) -> &'a str {
match self {
Self::ApiGatewayHttpV2(event) => &event.request_context.http.method,
Self::ApiGatewayRestOrAlb(event) => &event.http_method,
}
}
#[allow(dead_code)]
pub fn hostname<'a>(&'a self) -> Option<&'a str> {
match self {
Self::ApiGatewayHttpV2(event) => Some(&event.request_context.domain_name),
Self::ApiGatewayRestOrAlb(event) => {
if let RestOrAlbRequestContext::Rest(context) = &event.request_context {
Some(&context.domain_name)
} else if let Some(host_headers) = event.multi_value_headers.get("host") {
host_headers.first().map(|h| h as &str)
} else {
None
}
}
}
}
pub fn path_query(&self) -> String {
match self {
Self::ApiGatewayHttpV2(event) => {
let path = encode_path_query(&event.raw_path);
let query = &event.raw_query_string as &str;
if query.is_empty() {
path.into_owned()
} else {
format!("{}?{}", path, query)
}
}
Self::ApiGatewayRestOrAlb(event) => {
let path = if let RestOrAlbRequestContext::Rest(context) = &event.request_context {
&context.path
} else {
&event.path
};
if let Some(query_string_parameters) = &event.multi_value_query_string_parameters {
let querystr = query_string_parameters
.iter()
.flat_map(|(k, vec)| {
let k_enc = encode_path_query(&k);
vec.iter()
.map(move |v| format!("{}={}", k_enc, encode_path_query(&v)))
})
.collect::<Vec<_>>()
.join("&");
format!("{}?{}", path, querystr)
} else {
path.clone()
}
}
}
}
pub fn headers<'a>(&'a self) -> Vec<(&'a str, Cow<'a, str>)> {
match self {
Self::ApiGatewayHttpV2(event) => {
let mut headers: Vec<(&'a str, Cow<'a, str>)> = event
.headers
.iter()
.map(|(k, v)| (k as &str, Cow::from(v as &str)))
.collect();
if let Some(cookies) = &event.cookies {
let cookie_value = cookies.join("; ");
headers.push(("cookie", Cow::from(cookie_value)));
}
headers
}
Self::ApiGatewayRestOrAlb(event) => event
.multi_value_headers
.iter()
.flat_map(|(k, vec)| vec.iter().map(move |v| (k as &str, Cow::from(v as &str))))
.collect(),
}
}
#[allow(dead_code)]
pub fn cookies<'a>(&'a self) -> Vec<&'a str> {
match self {
Self::ApiGatewayHttpV2(event) => {
if let Some(cookies) = &event.cookies {
cookies.iter().map(|c| c.as_str()).collect()
} else {
Vec::new()
}
}
Self::ApiGatewayRestOrAlb(event) => {
if let Some(cookie_headers) = event.multi_value_headers.get("cookie") {
cookie_headers
.iter()
.flat_map(|v| v.split(";"))
.map(|c| c.trim())
.collect()
} else {
Vec::new()
}
}
}
}
#[cfg(feature = "br")]
pub fn client_supports_brotli(&self) -> bool {
match self {
Self::ApiGatewayHttpV2(event) => {
if let Some(header_val) = event.headers.get("accept-encoding") {
for elm in header_val.to_ascii_lowercase().split(',') {
if let Some(algo_name) = elm.split(';').next() {
if algo_name.trim() == "br" {
return true;
}
}
}
false
} else {
false
}
}
Self::ApiGatewayRestOrAlb(event) => {
if let Some(header_vals) = event.multi_value_headers.get("accept-encoding") {
for header_val in header_vals {
for elm in header_val.to_ascii_lowercase().split(',') {
if let Some(algo_name) = elm.split(';').next() {
if algo_name.trim() == "br" {
return true;
}
}
}
}
false
} else {
false
}
}
}
}
#[cfg(not(feature = "br"))]
pub fn client_supports_brotli(&self) -> bool {
false
}
pub fn multi_value(&self) -> bool {
match self {
Self::ApiGatewayHttpV2(_) => false,
Self::ApiGatewayRestOrAlb(_) => true,
}
}
pub fn body(self) -> Result<Vec<u8>, base64::DecodeError> {
let (body, b64_encoded) = match self {
Self::ApiGatewayHttpV2(event) => (event.body, event.is_base64_encoded),
Self::ApiGatewayRestOrAlb(event) => (event.body, event.is_base64_encoded),
};
if let Some(body) = body {
if b64_encoded {
base64::decode(&body as &str)
} else {
Ok(body.into_owned().into_bytes())
}
} else {
Ok(Vec::new())
}
}
#[allow(dead_code)]
pub fn source_ip(&self) -> Option<std::net::IpAddr> {
use std::net::IpAddr;
use std::str::FromStr;
match self {
Self::ApiGatewayHttpV2(event) => {
IpAddr::from_str(&event.request_context.http.source_ip).ok()
}
Self::ApiGatewayRestOrAlb(event) => {
if let RestOrAlbRequestContext::Rest(context) = &event.request_context {
IpAddr::from_str(&context.identity.source_ip).ok()
} else {
None
}
}
}
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ApiGatewayHttpV2Event<'a> {
#[allow(dead_code)]
version: String,
raw_path: String,
raw_query_string: String,
cookies: Option<Vec<String>>,
headers: HashMap<String, String>,
body: Option<Cow<'a, str>>,
#[serde(default)]
is_base64_encoded: bool,
request_context: ApiGatewayV2RequestContext,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct ApiGatewayV2RequestContext {
domain_name: String,
http: Http,
}
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct Http {
method: String,
source_ip: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ApiGatewayRestEvent<'a> {
path: String,
http_method: String,
body: Option<Cow<'a, str>>,
#[serde(default)]
is_base64_encoded: bool,
multi_value_headers: HashMap<String, Vec<String>>,
#[serde(default)]
multi_value_query_string_parameters: Option<HashMap<String, Vec<String>>>,
request_context: RestOrAlbRequestContext,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum RestOrAlbRequestContext {
Rest(ApiGatewayRestRequestContext),
Alb(AlbRequestContext),
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ApiGatewayRestRequestContext {
domain_name: String,
identity: ApiGatewayRestIdentity,
path: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ApiGatewayRestIdentity {
#[allow(dead_code)]
access_key: Option<String>,
source_ip: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct AlbRequestContext {}
const RFC3986_PATH_ESCAPE_SET: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'+')
.add(b':')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
fn encode_path_query<'a>(pathstr: &'a str) -> Cow<'a, str> {
Cow::from(percent_encoding::utf8_percent_encode(
pathstr,
&RFC3986_PATH_ESCAPE_SET,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_consts::*;
#[test]
fn test_decode() {
let _: ApiGatewayHttpV2Event =
serde_json::from_str(API_GATEWAY_V2_GET_ROOT_NOQUERY).unwrap();
let _: LambdaHttpEvent = serde_json::from_str(API_GATEWAY_V2_GET_ROOT_NOQUERY).unwrap();
let _: ApiGatewayRestEvent =
serde_json::from_str(API_GATEWAY_REST_GET_ROOT_NOQUERY).unwrap();
let _: LambdaHttpEvent = serde_json::from_str(API_GATEWAY_REST_GET_ROOT_NOQUERY).unwrap();
}
#[test]
fn test_cookie() {
let event: LambdaHttpEvent = serde_json::from_str(API_GATEWAY_V2_GET_TWO_COOKIES).unwrap();
assert_eq!(
event.cookies(),
vec!["cookie1=value1".to_string(), "cookie2=value2".to_string()]
);
let event: LambdaHttpEvent =
serde_json::from_str(API_GATEWAY_REST_GET_TWO_COOKIES).unwrap();
assert_eq!(
event.cookies(),
vec!["cookie1=value1".to_string(), "cookie2=value2".to_string()]
);
}
}