Skip to content

Commit

Permalink
Add request signing and Next js app base
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlee85 committed Jan 9, 2024
1 parent a56ddaa commit 78bb863
Show file tree
Hide file tree
Showing 28 changed files with 13,244 additions and 5,896 deletions.
3 changes: 3 additions & 0 deletions examples/v7-edge-functions/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
40 changes: 40 additions & 0 deletions examples/v7-edge-functions/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# Temporary Edgio files
.edgio

# Node.js modules
node_modules

# Edgio generated build directory
.edgio
82 changes: 57 additions & 25 deletions examples/v7-edge-functions/edgio.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@
// This file was automatically added by edgio init.
// You should commit this file to source control.
// Learn more about this file at https://docs.edg.io/guides/edgio_config
require('dotenv').config();

module.exports = {
connector: '@edgio/next',

// The name of the site in Edgio to which this app should be deployed.
// name: 'my-site-name',

// The name of the organization in Edgio to which this app should be deployed.
// organization: 'my-organization-name',

// Overrides the default path to the routes file. The path should be relative to the root of your app.
// routes: 'routes.js',

// When set to true or omitted entirely, Edgio includes the deployment number in the cache key,
// effectively purging the cache each time you deploy.
// purgeCacheOnDeploy: false,

next: {
// Output sourcemaps so that stack traces have original source filenames and line numbers when tailing
// the logs in the Edgio developer console.
// This config options replaces the edgioSourceMaps option in next.config.js.
// @default true
// generateSourceMaps: true
//
// Disables the Edgio image optimizer and allows to use the Next's built in image optimizer.
// This config options replaces the disableImageOptimizer option in edgio.config.js root.
// @default false
// disableImageOptimizer: false
//
// Disables the Edgio development tools widget on the site.
// This config options replaces the disableEdgioDevTools option in next.config.js.
// @default false
// disableDevtools: false
//
// Disables the build of the service worker.
// @default false
// disableServiceWorker: false
//
// Forces the @edgio/next connector to use the server build.
// This config option replaces the NEXT_FORCE_SERVER_BUILD env variable.
// @default false
// forceServerBuild: false
//
// Optimizes the server build by bundling all server assets and decreasing the overall startup time.
// This option has no effect on apps with serverless build.
// This option is set to false for Next 13.x apps.
// @default true
// optimizeServerBuild: true
//
// Set this option to false to remove the default rule that proxies all requests to Next.js in serverless.
// This is useful if you want to proxy all unmatched pages to different origin.
// @default true
// proxyToServerlessByDefault: true
//
// Set this option to true to honor Next's internal redirects that either add or remove a trailing slash
// depending on the value of the `trailingSlash` config. When set to false, these internal redirects are not honored,
// so sites that fallback to serving from an origin do not add or remove the trailing slash for origin URLs.
// @default true
// enforceTrailingSlash: true
},

origins: [
{
// The name of the backend origin
Expand Down Expand Up @@ -66,28 +120,6 @@ module.exports = {
},
],

// Uncomment the following to specify environment specific configs
// environments: {
// production: {
// hostnames: [{ hostname: 'www.mysite.com' }],
// },
// staging: {
// hostnames: [{ hostname: 'staging.mysite.com' }],
// origins: [
// {
// name: 'origin',
// hosts: [{ location: 'staging-origin.mysite.com' }],
// override_host_header: 'staging-origin.mysite.com',
// tls_verify: {
// use_sni: true,
// sni_hint_and_strict_san_check: 'staging-origin.mysite.com',
// },
// shields: { us_east: 'DCD' },
// },
// ],
// },
// },

// Options for hosting serverless functions on Edgio
// serverless: {
// // Set to true to include all packages listed in the dependencies property of package.json when deploying to Edgio.
Expand All @@ -103,7 +135,7 @@ module.exports = {
// Defaults to 200, which is the maximum allowed value.
// prerenderConcurrency: 200,

// A list of glob patterns identifying which source files should be uploaded when running edgio deploy --includeSources.
// A list of glob patterns identifying which prerenderConcurrency source files should be uploaded when running edgio deploy --includeSources.
// This option is primarily used to share source code with Edgio support personnel for the purpose of debugging. If omitted,
// edgio deploy --includeSources will result in all files which are not gitignored being uploaded to Edgio.
//
Expand Down
16 changes: 16 additions & 0 deletions examples/v7-edge-functions/functions/general/client-ip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import '../../utils/polyfills/URL';

export async function handleHttpRequest(request, context) {
// Retrieve the client's IP address from the context object
const clientIP = context.client.dst_addr;

const newRequest = new Request(request.url, request);

// Add the true-client-ip header to the incoming request
newRequest.headers.set('true-client-ip', clientIP);

// Continue with the modified request
return fetch(newRequest, {
edgio: { origin: 'echo' },
});
}
18 changes: 18 additions & 0 deletions examples/v7-edge-functions/functions/general/redirect-country.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export async function handleHttpRequest(request, context) {
const country = 'DE'; // Choose a country code
const newUrl = `${request.url}/${country}`; // Change the redirect URL to your choice

if (context.geo.country === country) {
return new Response(null, {
status: 302,
statusText: 'Found',
headers: {
Location: newUrl,
},
});
}

return fetch(request.url, {
edgio: { origin: 'echo' },
});
}
10 changes: 10 additions & 0 deletions examples/v7-edge-functions/functions/general/sample-html-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export async function handleHttpRequest(request, context) {
overflow-wrap: break-word;
white-space: pre-wrap;
}
p pre {
display: inline;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -187,6 +191,12 @@ export async function handleHttpRequest(request, context) {
</ul>
</section>
<section>
<h2>Request Signing</h2>
<p>Request signing and verification using <strong>crypto-js</strong>. Generated URLs are valid for 60 seconds.</p>
<p><a href="/example/signed-request/sign/foo/bar">Generate Signed URL</a></p>
</section>
</div>
<div style="margin-top: 30px; text-align: center;">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export async function handleHttpRequest(request, context) {
// Fetch the response from the 'echo' origin
const response = await fetch(new Request(request.url, request), {
edgio: {
origin: 'echo',
},
});

// Set HTTP security headers
response.headers.set(
'strict-transport-security',
'max-age=63072000; includeSubdomains; preload'
);
response.headers.set(
'content-security-policy',
"default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; frame-ancestors 'none'"
);
response.headers.set('x-content-type-options', 'nosniff');
response.headers.set('x-frame-options', 'DENY');
response.headers.set('x-xss-protection', '1; mode=block');
response.headers.set('referrer-policy', 'same-origin');

// Return the response to the client
return response;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import '../../../utils/polyfills/URL';
import HmacSHA1 from 'crypto-js/hmac-sha1';
import Base64 from 'crypto-js/enc-base64';

export async function handleHttpRequest(request, context) {
// ** IMPORTANT **
// Secret key should be defined as an environment variable in the Edgio console
const secretKey = '$0m3th!ngS3cr3t'; // contet.environmentVars.REQ_SIGNING_SECRET_KEY;

return generateSignedUrl(new URL(request.url), secretKey);
}

async function generateSignedUrl(url, key) {
// Replace /sign/ with /verify/ in the URL since we are generating a signed URL for verification
url.pathname = url.pathname.replace('/sign/', '/verify/');

// The link will expire in 5 minutes
const expirationMs = 1000 * 60 * 5; // 5 minutes
const expiry = Date.now() + expirationMs;
const dataToAuthenticate = url.pathname + expiry;

const hash = HmacSHA1(dataToAuthenticate, secretKey);
const base64Mac = Base64.stringify(hash);

url.searchParams.set('mac', base64Mac);
url.searchParams.set('expiry', expiry.toString());

const validUrl = url.toString();
const modifiedExpiryUrl = new URL(validUrl);
modifiedExpiryUrl.searchParams.set('expiry', `${expiry + 5}`);
const modifiedMacUrl = new URL(validUrl);
modifiedMacUrl.searchParams.set('mac', `${base64Mac}x`);

console.log('Valid URL:\n', validUrl);
console.log('Modified expiry URL:\n', modifiedExpiryUrl.toString());
console.log('Modified MAC URL:\n', modifiedMacUrl.toString());

const htmlResponse = `
<html>
<body>
<p>Click the following links for verification:</p>
<ul>
<li><a href="${validUrl}">Valid URL</a><pre>(${validUrl})</pre></li>
<li><a href="${modifiedExpiryUrl}">Invalid with modified Expiry URL</a><pre>(${modifiedExpiryUrl})</pre></li>
<li><a href="${modifiedMacUrl}">Invalid with modified Mac URL</a><pre>(${modifiedMacUrl})</pre></li>
</ul>
</body>
</html>
`;

return new Response(htmlResponse, {
headers: { 'Content-Type': 'text/html' },
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import '../../../utils/polyfills/URL';
import { setEnvFromContext } from '../../../utils/polyfills/process.env';
import createFetchForOrigin from '../../../utils/createFetchForOrigin';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import Base64 from 'crypto-js/enc-base64';

const fetch = createFetchForOrigin('echo');

export async function handleHttpRequest(request, context) {
// Set environment variables from the request context
setEnvFromContext(context);

return verifyAndFetch(new Request(request.url, request));
}

async function verifyAndFetch(request) {
// ** IMPORTANT **
// Secret key should be defined as an environment variable in the Edgio console
const secretKey = '$0m3th!ngS3cr3t'; // process.env.REQ_SIGNING_SECRET_KEY;

const url = new URL(request.url);

if (!url.searchParams.has('mac') || !url.searchParams.has('expiry')) {
return new Response('Missing query parameter', { status: 403 });
}

const expiry = Number(url.searchParams.get('expiry'));
const dataToAuthenticate = url.pathname + expiry;

const receivedMacBase64 = url.searchParams.get('mac');
const receivedMac = Base64.parse(receivedMacBase64);

const hash = HmacSHA256(dataToAuthenticate, secretKey);
const hashInBase64 = Base64.stringify(hash);

if (hashInBase64 !== receivedMacBase64) {
return new Response(
JSON.stringify(
{
message: 'Invalid MAC',
hashInBase64,
receivedMacBase64,
},
null,
2
),
{ status: 403 }
);
}

if (Date.now() > expiry) {
const body = `URL expired at ${new Date(expiry)}`;
return new Response(body, { status: 403 });
}

// Forward the remaining request path after **/verify/* to the origin
url.pathname = url.pathname.split('/verify/')[1];

return fetch(url.toString());
}
8 changes: 8 additions & 0 deletions examples/v7-edge-functions/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"jsx": "react",
"paths": {
"@/*": ["./src/*"]
}
}
}
13 changes: 13 additions & 0 deletions examples/v7-edge-functions/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file was automatically added by edgio init.
// You should commit this file to source control.
const { withEdgio } = require('@edgio/next/config')

/** @type {import('next').NextConfig} */
const nextConfig = {}

const _preEdgioExport = nextConfig;;

module.exports = (phase, config) =>
withEdgio({
..._preEdgioExport
})
Loading

0 comments on commit 78bb863

Please sign in to comment.