Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions lambda-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ serde_urlencoded = "0.7.0"
query_map = { version = "0.5", features = ["url-query"] }
mime = "0.3.16"
encoding_rs = "0.8.31"
url = "2.2.2"

[dependencies.aws_lambda_events]
version = "^0.6.3"
Expand Down
214 changes: 114 additions & 100 deletions lambda-http/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ use aws_lambda_events::apigw::{ApiGatewayV2httpRequest, ApiGatewayV2httpRequestC
use aws_lambda_events::apigw::{ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext};
use aws_lambda_events::encodings::Body;
use http::header::HeaderName;
use http::HeaderMap;
use query_map::QueryMap;
use serde::Deserialize;
use serde_json::error::Error as JsonError;
use std::future::Future;
use std::pin::Pin;
use std::{io::Read, mem};
use url::Url;

/// Internal representation of an Lambda http event from
/// ALB, API Gateway REST and HTTP API proxy event perspectives
Expand Down Expand Up @@ -82,7 +84,13 @@ pub enum RequestOrigin {
#[cfg(feature = "apigw_http")]
fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Body> {
let http_method = ag.request_context.http.method.clone();
let host = ag
.headers
.get(http::header::HOST)
.and_then(|s| s.to_str().ok())
.or(ag.request_context.domain_name.as_deref());
let raw_path = ag.raw_path.unwrap_or_default();
let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);

// don't use the query_string_parameters from API GW v2 to
// populate the QueryStringParameters extension because
Expand All @@ -95,32 +103,14 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Bod
ag.query_string_parameters
};

let mut uri = build_request_uri(&path, &ag.headers, host, None, None);
if let Some(query) = ag.raw_query_string {
uri.push('?');
uri.push_str(&query);
}

let builder = http::Request::builder()
.uri({
let host = ag
.headers
.get(http::header::HOST)
.and_then(|s| s.to_str().ok())
.or(ag.request_context.domain_name.as_deref());
let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);

let mut url = match host {
None => path,
Some(host) => {
let scheme = ag
.headers
.get(x_forwarded_proto())
.and_then(|s| s.to_str().ok())
.unwrap_or("https");
format!("{}://{}{}", scheme, host, path)
}
};
if let Some(query) = ag.raw_query_string {
url.push('?');
url.push_str(&query);
}
url
})
.uri(uri)
.extension(RawHttpPath(raw_path))
.extension(QueryStringParameters(query_string_parameters))
.extension(PathParameters(QueryMap::from(ag.path_parameters)))
Expand Down Expand Up @@ -154,34 +144,22 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Bod
#[cfg(feature = "apigw_rest")]
fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
let http_method = ag.http_method;
let host = ag
.headers
.get(http::header::HOST)
.and_then(|s| s.to_str().ok())
.or(ag.request_context.domain_name.as_deref());
let raw_path = ag.path.unwrap_or_default();
let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);

let builder = http::Request::builder()
.uri({
let host = ag.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);

let mut url = match host {
None => path,
Some(host) => {
let scheme = ag
.headers
.get(x_forwarded_proto())
.and_then(|s| s.to_str().ok())
.unwrap_or("https");
format!("{}://{}{}", scheme, host, path)
}
};

if !ag.multi_value_query_string_parameters.is_empty() {
url.push('?');
url.push_str(&ag.multi_value_query_string_parameters.to_query_string());
} else if !ag.query_string_parameters.is_empty() {
url.push('?');
url.push_str(&ag.query_string_parameters.to_query_string());
}
url
})
.uri(build_request_uri(
&path,
&ag.headers,
host,
Some(&ag.multi_value_query_string_parameters),
Some(&ag.query_string_parameters),
))
.extension(RawHttpPath(raw_path))
// multi-valued query string parameters are always a super
// set of singly valued query string parameters,
Expand Down Expand Up @@ -221,34 +199,17 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
#[cfg(feature = "alb")]
fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
let http_method = alb.http_method;
let host = alb.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
let raw_path = alb.path.unwrap_or_default();

let builder = http::Request::builder()
.uri({
let host = alb.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());

let mut url = match host {
None => raw_path.clone(),
Some(host) => {
let scheme = alb
.headers
.get(x_forwarded_proto())
.and_then(|s| s.to_str().ok())
.unwrap_or("https");
format!("{}://{}{}", scheme, host, &raw_path)
}
};

if !alb.multi_value_query_string_parameters.is_empty() {
url.push('?');
url.push_str(&alb.multi_value_query_string_parameters.to_query_string());
} else if !alb.query_string_parameters.is_empty() {
url.push('?');
url.push_str(&alb.query_string_parameters.to_query_string());
}

url
})
.uri(build_request_uri(
&raw_path,
&alb.headers,
host,
Some(&alb.multi_value_query_string_parameters),
Some(&alb.query_string_parameters),
))
.extension(RawHttpPath(raw_path))
// multi valued query string parameters are always a super
// set of singly valued query string parameters,
Expand Down Expand Up @@ -287,32 +248,21 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
#[cfg(feature = "apigw_websockets")]
fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request<Body> {
let http_method = ag.http_method;
let host = ag
.headers
.get(http::header::HOST)
.and_then(|s| s.to_str().ok())
.or(ag.request_context.domain_name.as_deref());
let path = apigw_path_with_stage(&ag.request_context.stage, &ag.path.unwrap_or_default());

let builder = http::Request::builder()
.uri({
let host = ag.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
let path = apigw_path_with_stage(&ag.request_context.stage, &ag.path.unwrap_or_default());

let mut url = match host {
None => path,
Some(host) => {
let scheme = ag
.headers
.get(x_forwarded_proto())
.and_then(|s| s.to_str().ok())
.unwrap_or("https");
format!("{}://{}{}", scheme, host, path)
}
};

if !ag.multi_value_query_string_parameters.is_empty() {
url.push('?');
url.push_str(&ag.multi_value_query_string_parameters.to_query_string());
} else if !ag.query_string_parameters.is_empty() {
url.push('?');
url.push_str(&ag.query_string_parameters.to_query_string());
}
url
})
.uri(build_request_uri(
&path,
&ag.headers,
host,
Some(&ag.multi_value_query_string_parameters),
Some(&ag.query_string_parameters),
))
// multi-valued query string parameters are always a super
// set of singly valued query string parameters,
// when present, multi-valued query string parameters are preferred
Expand Down Expand Up @@ -438,6 +388,43 @@ fn x_forwarded_proto() -> HeaderName {
HeaderName::from_static("x-forwarded-proto")
}

fn build_request_uri(
path: &str,
headers: &HeaderMap,
host: Option<&str>,
multi_value_query: Option<&QueryMap>,
single_value_query: Option<&QueryMap>,
) -> String {
// let host = headers.get(http::header::HOST).and_then(|s| s.to_str().ok());

let mut url = match host {
None => {
let rel_url = Url::parse(&format!("http://localhost{}", path)).unwrap();
rel_url.path().to_string()
}
Some(host) => {
let scheme = headers
.get(x_forwarded_proto())
.and_then(|s| s.to_str().ok())
.unwrap_or("https");
let url = format!("{}://{}{}", scheme, host, path);
Url::parse(&url).unwrap().to_string()
}
};

if let (Some(mv), Some(sv)) = (multi_value_query, single_value_query) {
if !mv.is_empty() {
url.push('?');
url.push_str(&mv.to_query_string());
} else if !sv.is_empty() {
url.push('?');
url.push_str(&sv.to_query_string());
}
}

url
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -666,4 +653,31 @@ mod tests {
assert_eq!(req.method(), "GET");
assert_eq!(req.uri(), "/v1/health/");
}

#[test]
fn deserialize_apigw_path_with_space() {
// generated from ALB health checks
let input = include_str!("../tests/data/apigw_request_path_with_space.json");
let result = from_str(input);
assert!(
result.is_ok(),
"event was not parsed as expected {:?} given {}",
result,
input
);
let req = result.expect("failed to parse request");
assert_eq!(req.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path-with%20space?parameter1=value1&parameter1=value2&parameter2=value");
}

#[test]
fn parse_paths_with_spaces() {
let url = build_request_uri(
"/path with spaces/and multiple segments",
&HeaderMap::new(),
None,
None,
None,
);
assert_eq!("/path%20with%20spaces/and%20multiple%20segments", url);
}
}
57 changes: 57 additions & 0 deletions lambda-http/tests/data/apigw_request_path_with_space.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"version": "2.0",
"routeKey": "$default",
"rawPath": "/my/path-with space",
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
"cookies": [
"cookie1=value1",
"cookie2=value2"
],
"headers": {
"Header1": "value1",
"Header2": "value2"
},
"queryStringParameters": {
"parameter1": "value1,value2",
"parameter2": "value"
},
"requestContext": {
"accountId": "123456789012",
"apiId": "api-id",
"authorizer": {
"jwt": {
"claims": {
"claim1": "value1",
"claim2": "value2"
},
"scopes": [
"scope1",
"scope2"
]
}
},
"domainName": "id.execute-api.us-east-1.amazonaws.com",
"domainPrefix": "id",
"http": {
"method": "POST",
"path": "/my/path-with space",
"protocol": "HTTP/1.1",
"sourceIp": "IP",
"userAgent": "agent"
},
"requestId": "id",
"routeKey": "$default",
"stage": "$default",
"time": "12/Mar/2020:19:03:58 +0000",
"timeEpoch": 1583348638390
},
"body": "Hello from Lambda",
"pathParameters": {
"parameter1": "value1"
},
"isBase64Encoded": false,
"stageVariables": {
"stageVariable1": "value1",
"stageVariable2": "value2"
}
}