Skip to content

Commit a17339e

Browse files
committed
feat: nginx-lambda-gateway framework
1 parent f43e6e1 commit a17339e

File tree

13 files changed

+374
-10
lines changed

13 files changed

+374
-10
lines changed
File renamed without changes.

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ submodule:
1818
git submodule foreach 'git fetch origin; git checkout $$(git rev-parse --abbrev-ref HEAD); git reset --hard origin/$$(git rev-parse --abbrev-ref HEAD); git submodule update --recursive; git clean -dfx'
1919

2020
start-plus:
21-
docker build --file Dockerfile.plus --tag nginx-plus-s3-test --tag nginx-plus-s3-test:plus .
22-
docker run --env-file ./settings.test --publish 81:80 --name nginx-plus-s3-test nginx-plus-s3-test:plus
21+
# docker build --file Dockerfile.plus --tag nginx-plus-s3-test --tag nginx-plus-s3-test:plus .
22+
# docker run --env-file ./settings.test --publish 81:80 --name nginx-plus-s3-test nginx-plus-s3-test:plus
2323

2424
start:
2525
docker-compose up -d

common/etc/nginx/include/awssig4.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date';
4848
function signatureV4(r, timestamp, region, service, uri, queryParams, host, credentials) {
4949
const eightDigitDate = utils.getEightDigitDate(timestamp);
5050
const amzDatetime = utils.getAmzDatetime(timestamp, eightDigitDate);
51-
const canonicalRequest = _buildCanonicalRequest(
51+
const canonicalRequest = _buildCanonicalRequest(r,
5252
r.method, uri, queryParams, host, amzDatetime, credentials.sessionToken);
5353
const signature = _buildSignatureV4(r, amzDatetime, eightDigitDate,
5454
credentials, region, service, canonicalRequest);
@@ -73,7 +73,16 @@ function signatureV4(r, timestamp, region, service, uri, queryParams, host, cred
7373
* @returns {string} string with concatenated request parameters
7474
* @private
7575
*/
76-
function _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, sessionToken) {
76+
function _buildCanonicalRequest(r,
77+
method, uri, queryParams, host, amzDatetime, sessionToken) {
78+
r.log('##### ---------------------------- #####')
79+
r.log(' _buildCanonicalRequest(): ')
80+
r.log(' - uri : ' + uri)
81+
r.log(' - method : ' + method)
82+
r.log(' - queryParams: ' + queryParams)
83+
r.log(' - host : ' + host)
84+
r.log(' - amzDatetime: ' + amzDatetime)
85+
7786
let canonicalHeaders = 'host:' + host + '\n' +
7887
'x-amz-content-sha256:' + EMPTY_PAYLOAD_HASH + '\n' +
7988
'x-amz-date:' + amzDatetime + '\n';
+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright 2023 F5, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import awscred from "./awscredentials.js";
18+
import awssig4 from "./awssig4.js";
19+
import utils from "./utils.js";
20+
21+
/**
22+
* Constant defining the service requests are being signed for.
23+
* @type {string}
24+
*/
25+
const SERVICE = 'lambda';
26+
27+
utils.requireEnvVar('LAMBDA_SERVER');
28+
utils.requireEnvVar('LAMBDA_SERVER_PROTO');
29+
utils.requireEnvVar('LAMBDA_SERVER_PORT');
30+
utils.requireEnvVar('LAMBDA_REGION');
31+
32+
33+
/**
34+
* Creates an AWS authentication signature based on the global settings and
35+
* the passed request parameter.
36+
*
37+
* @param r {Request} HTTP request object
38+
* @returns {string} AWS authentication signature
39+
*/
40+
function lambdaAuth(r) {
41+
const host = process.env['LAMBDA_SERVER'];
42+
const region = process.env['LAMBDA_REGION'];
43+
// const uri = '/2015-03-31/' + r.variables.request_uri + '/invocations';
44+
r.log('#### URI for lambdaAuth(): ' + r.variables.request_uri)
45+
const queryParams = '';
46+
const credentials = awscred.readCredentials(r);
47+
48+
let signature = awssig4.signatureV4(
49+
r, awscred.getNow(), region, SERVICE,
50+
r.variables.request_uri, queryParams, host, credentials
51+
);
52+
return signature;
53+
}
54+
55+
/**
56+
* Redirects the request to the appropriate location.
57+
*
58+
* @param r {Request} HTTP request object
59+
*/
60+
function redirectToLambda(r) {
61+
r.internalRedirect("@lambda");
62+
}
63+
64+
/**
65+
* Returns the Lambda path given the incoming request
66+
*
67+
* @param r HTTP request
68+
* @returns {string} uri for Lambda request
69+
*/
70+
function lambdaURI(r) {
71+
let uriPath = r.variables.uri_path;
72+
let path = _escapeURIPath(uriPath);
73+
utils.debug_log(r, 'AWS Lambda Request URI: ' + path);
74+
return path;
75+
}
76+
77+
/**
78+
* Flag indicating debug mode operation. If true, additional information
79+
* about signature generation will be logged.
80+
* @type {boolean}
81+
*/
82+
83+
const ADDITIONAL_HEADER_PREFIXES_TO_STRIP = utils.parseArray(process.env['HEADER_PREFIXES_TO_STRIP']);
84+
85+
/**
86+
* Default filename for index pages to be read off of the backing object store.
87+
* @type {string}
88+
*/
89+
const INDEX_PAGE = "index.html";
90+
91+
/**
92+
* Transform the headers returned from Lambda such that there isn't information
93+
* leakage about Lambda and do other tasks needed for appropriate gateway output.
94+
* @param r HTTP request
95+
*/
96+
function editHeaders(r) {
97+
/* Strips all x-amz- headers from the output HTTP headers so that the
98+
* requesters to the gateway will not know you are proxying Lambda. */
99+
if ('headersOut' in r) {
100+
for (const key in r.headersOut) {
101+
if (_isHeaderToBeStripped(
102+
key.toLowerCase(), ADDITIONAL_HEADER_PREFIXES_TO_STRIP)) {
103+
delete r.headersOut[key];
104+
}
105+
}
106+
}
107+
}
108+
109+
/**
110+
* Determines if a given HTTP header should be removed before being
111+
* sent on to the requesting client.
112+
* @param headerName {string} Lowercase HTTP header name
113+
* @param additionalHeadersToStrip {Array[string]} array of additional headers to remove
114+
* @returns {boolean} true if header should be removed
115+
*/
116+
function _isHeaderToBeStripped(headerName, additionalHeadersToStrip) {
117+
if (headerName.indexOf('x-amz-', 0) >= 0) {
118+
return true;
119+
}
120+
121+
for (let i = 0; i < additionalHeadersToStrip.length; i++) {
122+
const headerToStrip = additionalHeadersToStrip[i];
123+
if (headerName.indexOf(headerToStrip, 0) >= 0) {
124+
return true;
125+
}
126+
}
127+
128+
return false;
129+
}
130+
131+
/**
132+
* Outputs the timestamp used to sign the request, so that it can be added to
133+
* the 'Date' header and sent by NGINX.
134+
*
135+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
136+
* @returns {string} RFC2616 timestamp
137+
*/
138+
function lambdaDate(r) {
139+
return awscred.getNow().toUTCString();
140+
}
141+
142+
/**
143+
* Outputs the timestamp used to sign the request, so that it can be added to
144+
* the 'x-amz-date' header and sent by NGINX. The output format is
145+
* ISO 8601: YYYYMMDD'T'HHMMSS'Z'.
146+
* @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-date-handling.html | Handling dates in Signature Version 4}
147+
*
148+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
149+
* @returns {string} ISO 8601 timestamp
150+
*/
151+
function awsHeaderDate(r) {
152+
return utils.getAmzDatetime(
153+
awscred.getNow(),
154+
utils.getEightDigitDate(awscred.getNow())
155+
);
156+
}
157+
158+
function trailslashControl(r) {
159+
if (APPEND_SLASH) {
160+
const hasExtension = /\/[^.\/]+\.[^.]+$/;
161+
if (!hasExtension.test(r.variables.uri_path) && !_isDirectory(r.variables.uri_path)){
162+
return r.internalRedirect("@trailslash");
163+
}
164+
}
165+
r.internalRedirect("@error404");
166+
}
167+
168+
/**
169+
* Adds additional encoding to a URI component
170+
*
171+
* @param string {string} string to encode
172+
* @returns {string} an encoded string
173+
* @private
174+
*/
175+
function _encodeURIComponent(string) {
176+
return encodeURIComponent(string)
177+
.replace(/[!*'()]/g, (c) =>
178+
`%${c.charCodeAt(0).toString(16).toUpperCase()}`);
179+
}
180+
181+
/**
182+
* Escapes the path portion of a URI without escaping the path separator
183+
* characters (/).
184+
*
185+
* @param uri {string} unescaped URI
186+
* @returns {string} URI with each path component separately escaped
187+
* @private
188+
*/
189+
function _escapeURIPath(uri) {
190+
// Check to see if the URI path was already encoded. If so, we decode it.
191+
let decodedUri = (uri.indexOf('%') >= 0) ? decodeURIComponent(uri) : uri;
192+
let components = [];
193+
194+
decodedUri.split('/').forEach(function (item, i) {
195+
components[i] = _encodeURIComponent(item);
196+
});
197+
198+
return components.join('/');
199+
}
200+
201+
/**
202+
* Determines if a given path is a directory based on whether or not the last
203+
* character in the path is a forward slash (/).
204+
*
205+
* @param path {string} path to parse
206+
* @returns {boolean} true if path is a directory
207+
* @private
208+
*/
209+
function _isDirectory(path) {
210+
if (path === undefined) {
211+
return false;
212+
}
213+
const len = path.length;
214+
215+
if (len < 1) {
216+
return false;
217+
}
218+
219+
return path.charAt(len - 1) === '/';
220+
}
221+
222+
223+
export default {
224+
awsHeaderDate,
225+
editHeaders,
226+
lambdaAuth,
227+
lambdaDate,
228+
lambdaURI,
229+
redirectToLambda,
230+
trailslashControl,
231+
// These functions do not need to be exposed, but they are exposed so that
232+
// unit tests can run against them.
233+
_encodeURIComponent,
234+
_escapeURIPath,
235+
_isHeaderToBeStripped
236+
};

common/etc/nginx/include/utils.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,27 @@ function getEightDigitDate(timestamp) {
127127
padWithLeadingZeros(day,2));
128128
}
129129

130+
131+
/**
132+
* Checks to see if the given environment variable is present. If not, an error
133+
* is thrown.
134+
* @param envVarName {string} environment variable to check for
135+
* @private
136+
*/
137+
function requireEnvVar(envVarName) {
138+
const isSet = envVarName in process.env;
139+
140+
if (!isSet) {
141+
throw('Required environment variable ' + envVarName + ' is missing');
142+
}
143+
}
144+
130145
export default {
131146
debug_log,
132147
getAmzDatetime,
133148
getEightDigitDate,
134149
padWithLeadingZeros,
135150
parseArray,
136-
parseBoolean
151+
parseBoolean,
152+
requireEnvVar
137153
}

common/etc/nginx/templates/default.conf.template

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
js_import /etc/nginx/include/awscredentials.js;
22
js_import /etc/nginx/include/s3gateway.js;
3+
js_import /etc/nginx/include/lambdagateway.js;
34

45
# We include only the variables needed for the authentication signatures that
56
# we plan to use.
@@ -18,12 +19,20 @@ map $S3_STYLE $s3_host_hdr {
1819
default "${S3_BUCKET_NAME}.${S3_SERVER}";
1920
}
2021

22+
map $host $lambda_host {
23+
default "https://lambda.us-east-2.amazonaws.com";
24+
}
25+
2126
js_var $indexIsEmpty true;
27+
2228
# This creates the HTTP authentication header to be sent to S3
2329
js_set $s3auth s3gateway.s3auth;
2430
js_set $awsSessionToken awscredentials.sessionToken;
2531
js_set $s3uri s3gateway.s3uri;
2632

33+
js_set $lambdaAuth lambdagateway.lambdaAuth;
34+
js_set $lambdaURI lambdagateway.lambdaURI;
35+
2736
server {
2837
include /etc/nginx/conf.d/gateway/server_variables.conf;
2938

@@ -83,7 +92,8 @@ server {
8392
# Redirect to the proper location based on the client request - either
8493
# @s3, @s3Listing or @error405.
8594

86-
js_content s3gateway.redirectToS3;
95+
#js_content s3gateway.redirectToS3;
96+
js_content lambdagateway.redirectToLambda;
8797
}
8898

8999
location /aws/credentials/retrieve {
@@ -93,6 +103,57 @@ server {
93103
include /etc/nginx/conf.d/gateway/js_fetch_trusted_certificate.conf;
94104
}
95105

106+
location @lambda {
107+
# We include only the headers needed for the authentication signatures that
108+
# we plan to use.
109+
include /etc/nginx/conf.d/gateway/v${AWS_SIGS_VERSION}_headers.conf;
110+
111+
# The CORS configuration needs to be imported in several places in order for
112+
# it to be applied within different contexts.
113+
# include /etc/nginx/conf.d/gateway/cors.conf;
114+
115+
# Don't allow any headers from the client - we don't want them messing
116+
# with Lambda at all.
117+
proxy_pass_request_headers off;
118+
119+
# Enable passing of the server name through TLS Server Name Indication extension.
120+
proxy_ssl_server_name on;
121+
#proxy_ssl_name ${LAMBDA_SERVER};
122+
123+
# Set the Authorization header to the AWS Signatures credentials
124+
proxy_set_header Authorization $lambdaAuth;
125+
proxy_set_header X-Amz-Security-Token $awsSessionToken;
126+
127+
# We set the host as the bucket name to inform the S3 API of the bucket
128+
#proxy_set_header Host $s3_host_hdr;
129+
130+
# Use keep alive connections in order to improve performance
131+
proxy_http_version 1.1;
132+
proxy_set_header Connection '';
133+
proxy_method POST;
134+
135+
# We strip off all of the AWS specific headers from the server so that
136+
# there is nothing identifying the object as having originated in an
137+
# object store.
138+
#js_header_filter s3gateway.editHeaders;
139+
140+
# Catch all errors from S3 and sanitize them so that the user can't
141+
# gain intelligence about the S3 bucket being proxied.
142+
proxy_intercept_errors on;
143+
144+
# Comment out this line to receive the error messages returned by Lambda
145+
#error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 500 501 502 503 504 505 506 507 508 509 510 511 =404 @error404;
146+
147+
#error_page 404 @trailslashControl;
148+
149+
#proxy_pass ${S3_SERVER_PROTO}://storage_urls$s3uri;
150+
#proxy_pass ${LAMBDA_SERVER_PROTO}://lambda_urls$lambdaURI;
151+
proxy_pass $lambda_host$lambdaURI;
152+
#proxy_pass https://lambda.us-east-2.amazonaws.com/2015-03-31/functions/nginx-serverless/invocations;
153+
154+
#include /etc/nginx/conf.d/gateway/s3_location.conf;
155+
}
156+
96157
location @s3 {
97158
# We include only the headers needed for the authentication signatures that
98159
# we plan to use.

0 commit comments

Comments
 (0)