Skip to content

Commit fd2ea23

Browse files
authored
Fix paths with spaces in HTTP requests. (#516)
* Fix paths with spaces in HTTP requests. Encode the characters not allowed in URLs before creating the request. Signed-off-by: David Calavera <[email protected]> * Cleanup parameters and comments. Signed-off-by: David Calavera <[email protected]> Signed-off-by: David Calavera <[email protected]>
1 parent f77b044 commit fd2ea23

File tree

3 files changed

+160
-100
lines changed

3 files changed

+160
-100
lines changed

lambda-http/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ serde_urlencoded = "0.7.0"
3232
query_map = { version = "0.5", features = ["url-query"] }
3333
mime = "0.3.16"
3434
encoding_rs = "0.8.31"
35+
url = "2.2.2"
3536

3637
[dependencies.aws_lambda_events]
3738
version = "^0.6.3"

lambda-http/src/request.rs

Lines changed: 102 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ use aws_lambda_events::apigw::{ApiGatewayV2httpRequest, ApiGatewayV2httpRequestC
1414
use aws_lambda_events::apigw::{ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext};
1515
use aws_lambda_events::encodings::Body;
1616
use http::header::HeaderName;
17+
use http::HeaderMap;
1718
use query_map::QueryMap;
1819
use serde::Deserialize;
1920
use serde_json::error::Error as JsonError;
2021
use std::future::Future;
2122
use std::pin::Pin;
2223
use std::{io::Read, mem};
24+
use url::Url;
2325

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

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

106+
let mut uri = build_request_uri(&path, &ag.headers, host, None);
107+
if let Some(query) = ag.raw_query_string {
108+
uri.push('?');
109+
uri.push_str(&query);
110+
}
111+
98112
let builder = http::Request::builder()
99-
.uri({
100-
let host = ag
101-
.headers
102-
.get(http::header::HOST)
103-
.and_then(|s| s.to_str().ok())
104-
.or(ag.request_context.domain_name.as_deref());
105-
let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);
106-
107-
let mut url = match host {
108-
None => path,
109-
Some(host) => {
110-
let scheme = ag
111-
.headers
112-
.get(x_forwarded_proto())
113-
.and_then(|s| s.to_str().ok())
114-
.unwrap_or("https");
115-
format!("{}://{}{}", scheme, host, path)
116-
}
117-
};
118-
if let Some(query) = ag.raw_query_string {
119-
url.push('?');
120-
url.push_str(&query);
121-
}
122-
url
123-
})
113+
.uri(uri)
124114
.extension(RawHttpPath(raw_path))
125115
.extension(QueryStringParameters(query_string_parameters))
126116
.extension(PathParameters(QueryMap::from(ag.path_parameters)))
@@ -154,34 +144,21 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Bod
154144
#[cfg(feature = "apigw_rest")]
155145
fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
156146
let http_method = ag.http_method;
147+
let host = ag
148+
.headers
149+
.get(http::header::HOST)
150+
.and_then(|s| s.to_str().ok())
151+
.or(ag.request_context.domain_name.as_deref());
157152
let raw_path = ag.path.unwrap_or_default();
153+
let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);
158154

159155
let builder = http::Request::builder()
160-
.uri({
161-
let host = ag.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
162-
let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);
163-
164-
let mut url = match host {
165-
None => path,
166-
Some(host) => {
167-
let scheme = ag
168-
.headers
169-
.get(x_forwarded_proto())
170-
.and_then(|s| s.to_str().ok())
171-
.unwrap_or("https");
172-
format!("{}://{}{}", scheme, host, path)
173-
}
174-
};
175-
176-
if !ag.multi_value_query_string_parameters.is_empty() {
177-
url.push('?');
178-
url.push_str(&ag.multi_value_query_string_parameters.to_query_string());
179-
} else if !ag.query_string_parameters.is_empty() {
180-
url.push('?');
181-
url.push_str(&ag.query_string_parameters.to_query_string());
182-
}
183-
url
184-
})
156+
.uri(build_request_uri(
157+
&path,
158+
&ag.headers,
159+
host,
160+
Some((&ag.multi_value_query_string_parameters, &ag.query_string_parameters)),
161+
))
185162
.extension(RawHttpPath(raw_path))
186163
// multi-valued query string parameters are always a super
187164
// set of singly valued query string parameters,
@@ -221,34 +198,16 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
221198
#[cfg(feature = "alb")]
222199
fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
223200
let http_method = alb.http_method;
201+
let host = alb.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
224202
let raw_path = alb.path.unwrap_or_default();
225203

226204
let builder = http::Request::builder()
227-
.uri({
228-
let host = alb.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
229-
230-
let mut url = match host {
231-
None => raw_path.clone(),
232-
Some(host) => {
233-
let scheme = alb
234-
.headers
235-
.get(x_forwarded_proto())
236-
.and_then(|s| s.to_str().ok())
237-
.unwrap_or("https");
238-
format!("{}://{}{}", scheme, host, &raw_path)
239-
}
240-
};
241-
242-
if !alb.multi_value_query_string_parameters.is_empty() {
243-
url.push('?');
244-
url.push_str(&alb.multi_value_query_string_parameters.to_query_string());
245-
} else if !alb.query_string_parameters.is_empty() {
246-
url.push('?');
247-
url.push_str(&alb.query_string_parameters.to_query_string());
248-
}
249-
250-
url
251-
})
205+
.uri(build_request_uri(
206+
&raw_path,
207+
&alb.headers,
208+
host,
209+
Some((&alb.multi_value_query_string_parameters, &alb.query_string_parameters)),
210+
))
252211
.extension(RawHttpPath(raw_path))
253212
// multi valued query string parameters are always a super
254213
// set of singly valued query string parameters,
@@ -287,32 +246,20 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
287246
#[cfg(feature = "apigw_websockets")]
288247
fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request<Body> {
289248
let http_method = ag.http_method;
249+
let host = ag
250+
.headers
251+
.get(http::header::HOST)
252+
.and_then(|s| s.to_str().ok())
253+
.or(ag.request_context.domain_name.as_deref());
254+
let path = apigw_path_with_stage(&ag.request_context.stage, &ag.path.unwrap_or_default());
255+
290256
let builder = http::Request::builder()
291-
.uri({
292-
let host = ag.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
293-
let path = apigw_path_with_stage(&ag.request_context.stage, &ag.path.unwrap_or_default());
294-
295-
let mut url = match host {
296-
None => path,
297-
Some(host) => {
298-
let scheme = ag
299-
.headers
300-
.get(x_forwarded_proto())
301-
.and_then(|s| s.to_str().ok())
302-
.unwrap_or("https");
303-
format!("{}://{}{}", scheme, host, path)
304-
}
305-
};
306-
307-
if !ag.multi_value_query_string_parameters.is_empty() {
308-
url.push('?');
309-
url.push_str(&ag.multi_value_query_string_parameters.to_query_string());
310-
} else if !ag.query_string_parameters.is_empty() {
311-
url.push('?');
312-
url.push_str(&ag.query_string_parameters.to_query_string());
313-
}
314-
url
315-
})
257+
.uri(build_request_uri(
258+
&path,
259+
&ag.headers,
260+
host,
261+
Some((&ag.multi_value_query_string_parameters, &ag.query_string_parameters)),
262+
))
316263
// multi-valued query string parameters are always a super
317264
// set of singly valued query string parameters,
318265
// when present, multi-valued query string parameters are preferred
@@ -438,6 +385,40 @@ fn x_forwarded_proto() -> HeaderName {
438385
HeaderName::from_static("x-forwarded-proto")
439386
}
440387

388+
fn build_request_uri(
389+
path: &str,
390+
headers: &HeaderMap,
391+
host: Option<&str>,
392+
queries: Option<(&QueryMap, &QueryMap)>,
393+
) -> String {
394+
let mut url = match host {
395+
None => {
396+
let rel_url = Url::parse(&format!("http://localhost{}", path)).unwrap();
397+
rel_url.path().to_string()
398+
}
399+
Some(host) => {
400+
let scheme = headers
401+
.get(x_forwarded_proto())
402+
.and_then(|s| s.to_str().ok())
403+
.unwrap_or("https");
404+
let url = format!("{}://{}{}", scheme, host, path);
405+
Url::parse(&url).unwrap().to_string()
406+
}
407+
};
408+
409+
if let Some((mv, sv)) = queries {
410+
if !mv.is_empty() {
411+
url.push('?');
412+
url.push_str(&mv.to_query_string());
413+
} else if !sv.is_empty() {
414+
url.push('?');
415+
url.push_str(&sv.to_query_string());
416+
}
417+
}
418+
419+
url
420+
}
421+
441422
#[cfg(test)]
442423
mod tests {
443424
use super::*;
@@ -666,4 +647,25 @@ mod tests {
666647
assert_eq!(req.method(), "GET");
667648
assert_eq!(req.uri(), "/v1/health/");
668649
}
650+
651+
#[test]
652+
fn deserialize_apigw_path_with_space() {
653+
// generated from ALB health checks
654+
let input = include_str!("../tests/data/apigw_request_path_with_space.json");
655+
let result = from_str(input);
656+
assert!(
657+
result.is_ok(),
658+
"event was not parsed as expected {:?} given {}",
659+
result,
660+
input
661+
);
662+
let req = result.expect("failed to parse request");
663+
assert_eq!(req.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path-with%20space?parameter1=value1&parameter1=value2&parameter2=value");
664+
}
665+
666+
#[test]
667+
fn parse_paths_with_spaces() {
668+
let url = build_request_uri("/path with spaces/and multiple segments", &HeaderMap::new(), None, None);
669+
assert_eq!("/path%20with%20spaces/and%20multiple%20segments", url);
670+
}
669671
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "$default",
4+
"rawPath": "/my/path-with space",
5+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
6+
"cookies": [
7+
"cookie1=value1",
8+
"cookie2=value2"
9+
],
10+
"headers": {
11+
"Header1": "value1",
12+
"Header2": "value2"
13+
},
14+
"queryStringParameters": {
15+
"parameter1": "value1,value2",
16+
"parameter2": "value"
17+
},
18+
"requestContext": {
19+
"accountId": "123456789012",
20+
"apiId": "api-id",
21+
"authorizer": {
22+
"jwt": {
23+
"claims": {
24+
"claim1": "value1",
25+
"claim2": "value2"
26+
},
27+
"scopes": [
28+
"scope1",
29+
"scope2"
30+
]
31+
}
32+
},
33+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
34+
"domainPrefix": "id",
35+
"http": {
36+
"method": "POST",
37+
"path": "/my/path-with space",
38+
"protocol": "HTTP/1.1",
39+
"sourceIp": "IP",
40+
"userAgent": "agent"
41+
},
42+
"requestId": "id",
43+
"routeKey": "$default",
44+
"stage": "$default",
45+
"time": "12/Mar/2020:19:03:58 +0000",
46+
"timeEpoch": 1583348638390
47+
},
48+
"body": "Hello from Lambda",
49+
"pathParameters": {
50+
"parameter1": "value1"
51+
},
52+
"isBase64Encoded": false,
53+
"stageVariables": {
54+
"stageVariable1": "value1",
55+
"stageVariable2": "value2"
56+
}
57+
}

0 commit comments

Comments
 (0)