|
| 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 | +}; |
0 commit comments