Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add HTTPS support for Edge Functions in Netlify Dev #4567

Merged
merged 12 commits into from
May 3, 2022
Merged
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ jobs:
if_true: 'npm run test:affected ${{ github.event.pull_request.base.sha }}' # on pull requests test with the project graph only the affected tests
if_false: 'npm run test:ci:ava:integration' # on the base branch run all the tests as security measure
if: '${{ !steps.release-check.outputs.IS_RELEASE }}'
- name: Generate self-signed certificates
run: npm run certs
if: '${{!steps.release-check.outputs.IS_RELEASE}}'
shell: bash
- name: Prepare tests
run: npm run test:init
if: '${{ !steps.release-check.outputs.IS_RELEASE }}'
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ tests/integration/hugo-site/resources
tests/integration/hugo-site/out
tests/integration/hugo-site/.hugo_build.lock
_test_out/**
*.crt
*.key

8 changes: 8 additions & 0 deletions certconf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[dn]
CN=localhost
[req]
distinguished_name = dn
[EXT]
subjectAltName=DNS:localhost
keyUsage=digitalSignature
extendedKeyUsage=serverAuth
128 changes: 121 additions & 7 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
"format:check-fix:prettier": "run-e format:check:prettier format:fix:prettier",
"format:check:prettier": "cross-env-shell prettier --check $npm_package_config_prettier",
"format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
"test:dev": "run-s test:init:* test:dev:*",
"test:dev": "run-s certs test:init:* test:dev:*",
"test:init": "run-s test:init:*",
"test:init:cli-version": "npm run start -- --version",
"test:init:cli-help": "npm run start -- --help",
Expand All @@ -200,7 +200,8 @@
"site:build": "run-s site:build:*",
"site:build:install": "cd site && npm ci --no-audit",
"site:build:assets": "cd site && npm run build",
"postinstall": "node ./scripts/postinstall.js"
"postinstall": "node ./scripts/postinstall.js",
"certs": "openssl req -x509 -out localhost.crt -keyout localhost.key -newkey rsa:2048 -nodes -sha256 -subj \"/CN=localhost\" -extensions EXT -config certconf"
},
"config": {
"eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,site,tests,.github}/**/*.{mjs,cjs,js,md,html}\" \"*.{mjs,cjs,js,md,html}\" \".*.{mjs,cjs,js,md,html}\"",
Expand All @@ -209,7 +210,7 @@
"dependencies": {
"@netlify/build": "^27.0.1",
"@netlify/config": "^18.0.0",
"@netlify/edge-bundler": "^0.12.0",
"@netlify/edge-bundler": "^1.0.0",
"@netlify/framework-info": "^9.0.2",
"@netlify/local-functions-proxy": "^1.1.1",
"@netlify/plugins-list": "^6.19.0",
Expand Down
3 changes: 2 additions & 1 deletion src/lib/edge-functions/headers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module.exports = {
ForwardedHost: 'x-forwarded-host',
ForwardedProtocol: 'x-forwarded-proto',
Functions: 'x-deno-functions',
Geo: 'x-nf-geo',
PassHost: 'X-NF-Pass-Host',
Passthrough: 'x-deno-pass',
RequestID: 'X-NF-Request-ID',
}
9 changes: 8 additions & 1 deletion src/lib/edge-functions/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const initializeProxy = async ({ config, configPath, geolocationMode, getUpdated
// the network if needed. We don't want to wait for that to be completed, or
// the command will be left hanging.
const server = prepareServer({
certificatePath: settings.https ? settings.https.certFilePath : undefined,
config,
configPath,
directories: [internalFunctionsPath, userFunctionsPath].filter(Boolean),
Expand Down Expand Up @@ -100,18 +101,23 @@ const initializeProxy = async ({ config, configPath, geolocationMode, getUpdated

req[headersSymbol] = {
[headers.Functions]: functionNames.join(','),
[headers.PassHost]: `${LOCAL_HOST}:${mainPort}`,
[headers.ForwardedHost]: `localhost:${mainPort}`,
[headers.Passthrough]: 'passthrough',
[headers.RequestID]: generateUUID(),
}

if (settings.https) {
req[headersSymbol][headers.ForwardedProtocol] = 'https'
}

return `http://${LOCAL_HOST}:${isolatePort}`
}
}

const isEdgeFunctionsRequest = (req) => req[headersSymbol] !== undefined

const prepareServer = async ({
certificatePath,
config,
configPath,
directories,
Expand All @@ -124,6 +130,7 @@ const prepareServer = async ({
const distImportMapPath = getPathInProject([DIST_IMPORT_MAP_PATH])
const runIsolate = await bundler.serve({
...getDownloadUpdateFunctions(),
certificatePath,
debug: env.NETLIFY_DENO_DEBUG === 'true',
distImportMapPath,
formatExportTypeError: (name) =>
Expand Down
2 changes: 1 addition & 1 deletion src/utils/detect-server-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const readHttpsSettings = async (options) => {
throw new Error(`Error reading certificate file: ${certError.message}`)
}

return { key, cert }
return { key, cert, keyFilePath: path.resolve(keyFile), certFilePath: path.resolve(certFile) }
}

const validateStringProperty = ({ devConfig, property }) => {
Expand Down
28 changes: 25 additions & 3 deletions tests/integration/200.command.dev.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const path = require('path')
// eslint-disable-next-line ava/use-test
const avaTest = require('ava')
const { isCI } = require('ci-info')
const { Response } = require('node-fetch')

const { curl } = require('./utils/curl')
const { withDevServer } = require('./utils/dev-server')
Expand Down Expand Up @@ -194,7 +195,13 @@ export const handler = async function () {
config: {
build: { publish: 'public' },
functions: { directory: 'functions' },
dev: { https: { certFile: 'cert.pem', keyFile: 'key.pem' } },
dev: { https: { certFile: 'localhost.crt', keyFile: 'localhost.key' } },
edge_functions: [
{
function: 'hello',
path: '/',
},
],
},
})
.withContentFile({
Expand All @@ -211,15 +218,30 @@ export const handler = async function () {
body: 'Hello World',
}),
})
.withEdgeFunction({
handler: async (req, { next }) => {
if (!req.url.includes('?ef=true')) {
return
}

// eslint-disable-next-line n/callback-return
const res = await next()
const text = await res.text()

return new Response(text.toUpperCase(), res)
},
name: 'hello',
})
.buildAsync()

await Promise.all([
copyFile(`${__dirname}/assets/cert.pem`, `${builder.directory}/cert.pem`),
copyFile(`${__dirname}/assets/key.pem`, `${builder.directory}/key.pem`),
copyFile(`${__dirname}/../../localhost.crt`, `${builder.directory}/localhost.crt`),
copyFile(`${__dirname}/../../localhost.key`, `${builder.directory}/localhost.key`),
])
await withDevServer({ cwd: builder.directory, args }, async ({ port }) => {
const options = { https: { rejectUnauthorized: false } }
t.is(await got(`https://localhost:${port}`, options).text(), 'index')
t.is(await got(`https://localhost:${port}?ef=true`, options).text(), 'INDEX')
t.is(await got(`https://localhost:${port}/api/hello`, options).text(), 'Hello World')
})
})
Expand Down
Loading