| 1 | // Alchemy modifications are licensed under Apache-2.0. |
| 2 | // This file includes third-party code; see /THIRD_PARTY_LICENSES.md. |
| 3 | /** |
| 4 | * CloudFront Function injection code ported from SST's Router component. |
| 5 | * These are JavaScript code strings that get injected into CloudFront Function handlers. |
| 6 | */ |
| 7 | |
| 8 | /** |
| 9 | * Host-based 301 handling injected at the very top of a generated |
| 10 | * viewer-request handler (before route matching). Redirects requests whose |
| 11 | * `Host` is one of the configured redirect hostnames — and, when |
| 12 | * `cloudfrontUrl: false`, requests arriving via the distribution's default |
| 13 | * `*.cloudfront.net` domain (the default domain is not knowable inside its |
| 14 | * own function config before the distribution exists, but only requests |
| 15 | * served via the default domain ever carry a `.cloudfront.net` Host; |
| 16 | * alternate domain names never do) — to the canonical hostname with path |
| 17 | * and query preserved. |
| 18 | * |
| 19 | * Relies on `buildHostRedirectResponse` declared by |
| 20 | * {@link CF_ROUTER_INJECTION} (function declarations hoist within the |
| 21 | * handler). Returns an empty string when there is nothing to redirect. |
| 22 | */ |
| 23 | export const buildHostRedirectInjection = ({ |
| 24 | to, |
| 25 | hosts, |
| 26 | cloudfrontDefault, |
| 27 | }: { |
| 28 | /** Canonical hostname redirected to. */ |
| 29 | to: string | undefined; |
| 30 | /** Exact redirect hostnames. */ |
| 31 | hosts: string[]; |
| 32 | /** Also redirect the distribution's default `*.cloudfront.net` domain. */ |
| 33 | cloudfrontDefault: boolean; |
| 34 | }): string => { |
| 35 | if (!to || (hosts.length === 0 && !cloudfrontDefault)) return ""; |
| 36 | const conditions = [ |
| 37 | ...(hosts.length > 0 |
| 38 | ? [`${JSON.stringify(hosts)}.indexOf(redirectHost) !== -1`] |
| 39 | : []), |
| 40 | ...(cloudfrontDefault ? [`redirectHost.endsWith(".cloudfront.net")`] : []), |
| 41 | ]; |
| 42 | return ` |
| 43 | var redirectHost = event.request.headers.host.value; |
| 44 | if (${conditions.join(" || ")}) { |
| 45 | return buildHostRedirectResponse(${JSON.stringify(to)}); |
| 46 | }`; |
| 47 | }; |
| 48 | |
| 49 | const CLOUDFRONT_FUNCTION_SAFE_HEADER_LIMIT = 10240 - 512; |
| 50 | |
| 51 | export const CF_ROUTER_INJECTION = ` |
| 52 | async function routeSite(kvNamespace, metadata) { |
| 53 | if (metadata.redirect && metadata.redirect.hosts.indexOf(event.request.headers.host.value) !== -1) { |
| 54 | return buildHostRedirectResponse(metadata.redirect.to); |
| 55 | } |
| 56 | |
| 57 | var baselessUri = metadata.base |
| 58 | ? event.request.uri.replace(metadata.base, "") |
| 59 | : event.request.uri; |
| 60 | |
| 61 | try { |
| 62 | var u = decodeURIComponent(baselessUri); |
| 63 | var postfixes = u.endsWith("/") |
| 64 | ? ["index.html"] |
| 65 | : ["", ".html", "/index.html"]; |
| 66 | var v = await Promise.any(postfixes.map(function(p) { return cf.kvs().get(kvNamespace + ":" + u + p).then(function() { return p; }); })); |
| 67 | event.request.uri = metadata.s3.dir + event.request.uri + v; |
| 68 | setS3Origin(metadata.s3.domain); |
| 69 | return; |
| 70 | } catch (e) {} |
| 71 | |
| 72 | if (metadata.s3 && metadata.s3.routes) { |
| 73 | for (var i=0, l=metadata.s3.routes.length; i<l; i++) { |
| 74 | var route = metadata.s3.routes[i]; |
| 75 | if (baselessUri.startsWith(route)) { |
| 76 | event.request.uri = metadata.s3.dir + event.request.uri; |
| 77 | if (event.request.uri.endsWith("/")) { |
| 78 | event.request.uri += "index.html"; |
| 79 | } else if (!event.request.uri.split("/").pop().includes(".")) { |
| 80 | event.request.uri += "/index.html"; |
| 81 | } |
| 82 | setS3Origin(metadata.s3.domain); |
| 83 | return; |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | if (metadata.custom404 && !metadata.errorResponseCode) { |
| 89 | event.request.uri = metadata.s3.dir + (metadata.base ? metadata.base : "") + metadata.custom404; |
| 90 | setS3Origin(metadata.s3.domain); |
| 91 | return; |
| 92 | } |
| 93 | |
| 94 | if (metadata.s3 && !metadata.servers) { |
| 95 | event.request.uri = metadata.s3.dir + event.request.uri; |
| 96 | setS3Origin(metadata.s3.domain); |
| 97 | return; |
| 98 | } |
| 99 | |
| 100 | if (metadata.image && baselessUri.startsWith(metadata.image.route)) { |
| 101 | setForwardedHost(); |
| 102 | if (isRequestHeaderTooLarge()) return buildOversizedHeadersResponse(); |
| 103 | setUrlOrigin(metadata.image.host, metadata.image.originAccessControlConfig ? { originAccessControlConfig: metadata.image.originAccessControlConfig } : undefined); |
| 104 | return; |
| 105 | } |
| 106 | |
| 107 | if (metadata.servers) { |
| 108 | setForwardedHost(); |
| 109 | for (var key in event.request.querystring) { |
| 110 | if (key.includes("/")) { |
| 111 | event.request.querystring[encodeURIComponent(key)] = event.request.querystring[key]; |
| 112 | delete event.request.querystring[key]; |
| 113 | } |
| 114 | } |
| 115 | if (isRequestHeaderTooLarge()) return buildOversizedHeadersResponse(); |
| 116 | setUrlOrigin(findNearestServer(metadata.servers), metadata.origin); |
| 117 | } |
| 118 | |
| 119 | function findNearestServer(servers) { |
| 120 | if (servers.length === 1) return servers[0][0]; |
| 121 | var h = event.request.headers; |
| 122 | var lat = h["cloudfront-viewer-latitude"] && h["cloudfront-viewer-latitude"].value; |
| 123 | var lon = h["cloudfront-viewer-longitude"] && h["cloudfront-viewer-longitude"].value; |
| 124 | if (!lat || !lon) return servers[0][0]; |
| 125 | return servers.map(function(s) { return { distance: haversineDistance(lat, lon, s[1], s[2]), host: s[0] }; }).sort(function(a,b) { return a.distance - b.distance; })[0].host; |
| 126 | } |
| 127 | |
| 128 | function haversineDistance(lat1, lon1, lat2, lon2) { |
| 129 | var toRad = function(a) { return a * Math.PI / 180; }; |
| 130 | var dLat = toRad(lat2 - lat1); |
| 131 | var dLon = toRad(lon2 - lon1); |
| 132 | var a = Math.sin(dLat/2)*Math.sin(dLat/2) + Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)*Math.sin(dLon/2); |
| 133 | return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
| 134 | } |
| 135 | |
| 136 | function isRequestHeaderTooLarge() { |
| 137 | return getRequestHeaderSize() > ${CLOUDFRONT_FUNCTION_SAFE_HEADER_LIMIT}; |
| 138 | } |
| 139 | |
| 140 | function buildOversizedHeadersResponse() { |
| 141 | return { |
| 142 | statusCode: 431, |
| 143 | statusDescription: "Request Header Fields Too Large", |
| 144 | headers: { "cache-control": { value: "no-store" }, "content-type": { value: "text/plain; charset=utf-8" } }, |
| 145 | body: { encoding: "text", data: "Request headers are too large. Reduce cookie size and try again." }, |
| 146 | }; |
| 147 | } |
| 148 | |
| 149 | function getRequestHeaderSize() { |
| 150 | var size = 0; |
| 151 | for (var key in event.request.headers) { |
| 152 | var header = event.request.headers[key]; |
| 153 | if (header.multiValue) { |
| 154 | for (var i=0; i<header.multiValue.length; i++) size += key.length + header.multiValue[i].value.length + 4; |
| 155 | } else if (header.value) { |
| 156 | size += key.length + header.value.length + 4; |
| 157 | } |
| 158 | } |
| 159 | var cookies = []; |
| 160 | for (var key in event.request.cookies) { |
| 161 | var cookie = event.request.cookies[key]; |
| 162 | if (cookie.multiValue) { |
| 163 | for (var i=0; i<cookie.multiValue.length; i++) cookies.push(key + "=" + cookie.multiValue[i].value); |
| 164 | } else { |
| 165 | cookies.push(key + "=" + cookie.value); |
| 166 | } |
| 167 | } |
| 168 | if (cookies.length) size += 10 + cookies.join("; ").length; |
| 169 | return size; |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | function setForwardedHost() { |
| 174 | event.request.headers["x-forwarded-host"] = event.request.headers.host; |
| 175 | } |
| 176 | |
| 177 | function serializeQuerystring() { |
| 178 | var parts = []; |
| 179 | for (var key in event.request.querystring) { |
| 180 | var q = event.request.querystring[key]; |
| 181 | if (q.multiValue) { |
| 182 | for (var i = 0; i < q.multiValue.length; i++) parts.push(key + "=" + q.multiValue[i].value); |
| 183 | } else { |
| 184 | parts.push(q.value === "" ? key : key + "=" + q.value); |
| 185 | } |
| 186 | } |
| 187 | return parts.length ? "?" + parts.join("&") : ""; |
| 188 | } |
| 189 | |
| 190 | function buildHostRedirectResponse(toHost) { |
| 191 | return { |
| 192 | statusCode: 301, |
| 193 | statusDescription: "Moved Permanently", |
| 194 | headers: { |
| 195 | location: { value: "https://" + toHost + event.request.uri + serializeQuerystring() }, |
| 196 | "cache-control": { value: "no-store" }, |
| 197 | }, |
| 198 | }; |
| 199 | } |
| 200 | |
| 201 | function setUrlOrigin(urlHost, override) { |
| 202 | setForwardedHost(); |
| 203 | var origin = { |
| 204 | domainName: urlHost, |
| 205 | customOriginConfig: { |
| 206 | port: 443, |
| 207 | protocol: "https", |
| 208 | sslProtocols: ["TLSv1.2"], |
| 209 | }, |
| 210 | originAccessControlConfig: { |
| 211 | enabled: false, |
| 212 | } |
| 213 | }; |
| 214 | override = override || {}; |
| 215 | if (override.protocol === "http") delete origin.customOriginConfig; |
| 216 | if (override.connectionAttempts) origin.connectionAttempts = override.connectionAttempts; |
| 217 | if (override.timeouts) origin.timeouts = override.timeouts; |
| 218 | if (override.originAccessControlConfig) origin.originAccessControlConfig = override.originAccessControlConfig; |
| 219 | cf.updateRequestOrigin(origin); |
| 220 | } |
| 221 | |
| 222 | function setS3Origin(s3Domain, override) { |
| 223 | delete event.request.headers["Cookies"]; |
| 224 | delete event.request.headers["cookies"]; |
| 225 | delete event.request.cookies; |
| 226 | var origin = { |
| 227 | domainName: s3Domain, |
| 228 | originAccessControlConfig: { |
| 229 | enabled: true, |
| 230 | signingBehavior: "always", |
| 231 | signingProtocol: "sigv4", |
| 232 | originType: "s3", |
| 233 | } |
| 234 | }; |
| 235 | override = override || {}; |
| 236 | if (override.connectionAttempts) origin.connectionAttempts = override.connectionAttempts; |
| 237 | if (override.timeouts) origin.timeouts = override.timeouts; |
| 238 | cf.updateRequestOrigin(origin); |
| 239 | }`; |