Commit aecf96a9 authored by Aral Balkan's avatar Aral Balkan
Browse files

Implement hybrid approach

(mkcert binary is downloaded in postinstall but the certificate authority and certficates are created at runtime as necessary)
parent 71192559
......@@ -22,14 +22,10 @@ import https from 'https'
import os from 'os'
import path from 'path'
import { binaryPath as mkcertBinary } from '../lib/mkcert.js'
import installCertutil from '../lib/installCertutil.js'
import { version, binaryName } from '../lib/mkcert.js'
import fs from 'fs-extra'
import sudoPrompt from 'sudo-prompt'
async function secureGet (url) {
return new Promise((resolve, reject) => {
https.get(url, response => {
......@@ -95,122 +91,4 @@ fs.chmodSync(binaryPath, 0o755)
process.stdout.write('done.\n')
//
// Create the root certificate authority and certificates.
//
const caCertFilePath = path.join(settingsPath, 'rootCA.pem')
const caKeyFilePath = path.join(settingsPath, 'rootCA-key.pem')
const keyFilePath = path.join(settingsPath, 'localhost-key.pem')
const certFilePath = path.join(settingsPath, 'localhost.pem')
const allOK = () => {
return fs.existsSync(caCertFilePath)
&& fs.existsSync(caKeyFilePath)
&& fs.existsSync(certFilePath)
&& fs.existsSync(keyFilePath)
}
// On Linux and on macOS, mkcert uses the Mozilla nss library.
// Try to install this automatically and warn the person if we can’t so
// that they can do it manually themselves.
installCertutil()
// Support all local interfaces so that the machine can be reached over the local network via IPv4.
// This is very useful for testing with multiple devices over the local area network without needing to expose
// the machine over the wide area network/Internet using a service like ngrok.
const localIPv4Addresses =
Object.entries(os.networkInterfaces())
.map(iface =>
iface[1].filter(addresses =>
addresses.family === 'IPv4')
.map(addresses => addresses.address)).flat()
const certificateDetails = [
`-key-file=${keyFilePath}`,
`-cert-file=${certFilePath}`,
'localhost'
].concat(localIPv4Addresses).join(' ')
const account = os.userInfo().username
// Create the local certificate authority.
process.stdout.write(` ╰─ Initialising mkcert… `)
// We are using the sudo-prompt package here, instead of childProcess.execFileSync() because
// this script is meant to run as an npm script and it appears that npm scripts fail to show
// the system sudo prompt (and instead hang).
//
// See: https://github.com/npm/cli/issues/2887
//
// To workaround this issue, we use sudo-prompt here to display a graphical sudo prompt
// that works well with npm scripts.
//
// Since on macOS the certificate files are created with root permissions, we need to set
// files back to regular account permissions afterwards (this is not an issue on Linux
// where the files are created with regular account permissions even when the mkcert command
// is launched using sudo). In any case, because of this, and because we want the person
// to only enter their password once instead of multiple times, once for each command
// (due to how sudo-prompt works), we first create a shell script and then we execute that.
//
// For Windows, we create and use a separate PowerShell script.
let shellScriptTemplate
let shellScriptFileName
let shellScriptCommand
const platform = os.platform()
if (platform === 'linux' || platform === 'darwin') {
shellScriptFileName = 'install-mkcert.sh'
shellScriptCommand = `CAROOT=${settingsPath} /tmp/install-mkcert.sh`
shellScriptTemplate = `#!/bin/bash
set -e
# Install mkcert and create the certificate authority
${mkcertBinary} -install
# Create the certificates
${mkcertBinary} ${certificateDetails}
# Reset file permissions to regular account
chown -R ${account} ${settingsPath}
`
} else if (platform === 'win32') {
shellScriptFileName = 'install-mkcert.ps1'
shellScriptCommand = 'powershell.exe /tmp/install-mkcert.ps1'
shellScriptTemplate = `
# Set the environment variable
$env:CAROOT="${settingsPath}"
# Install mkcert and create the certificate authority
${mkcertBinary} -install
# Create the certificates
${mkcertBinary} ${certificateDetails}
`
} else {
throw new Error(`Sorry, this module is not tested or supported on your platform (${platform}).`)
}
fs.writeFileSync(`/tmp/${shellScriptFileName}`, shellScriptTemplate, {mode: 0o755})
await (() => {
return new Promise((resolve, reject) => {
const options = { name: 'Auto Encrypt Localhost' }
// Note: mkcert uses the CAROOT environment variable to know where to create/find the certificate authority.
sudoPrompt.exec(shellScriptCommand, options, function(error, stdout, stderr) {
if (error) reject(error)
resolve()
})
})
})()
process.stdout.write('done.\n')
// This should never happen as an error in the above, if there is one,
// should exit the process, but just in case.
if (!allOK()) {
console.log(' ╰─ ❌️ Certificate creation failed. Panic!')
process.exit(1)
} else {
console.log(' ────────────────────────────────────────────────────────────────────────')
}
console.log(' ────────────────────────────────────────────────────────────────────────')
......@@ -11,8 +11,13 @@ import os from 'os'
import fs from 'fs-extra'
import path from 'path'
import https from 'https'
import childProcess from 'child_process'
import installCertutil from './lib/installCertutil.js'
import syswidecas from 'syswide-cas'
import HttpServer from './lib/HttpServer.js'
import { log } from './lib/util/log.js'
import { binaryName } from './lib/mkcert.js'
/**
* Auto Encrypt Localhost is a static class. Please do not instantiate.
......@@ -56,6 +61,8 @@ export default class AutoEncryptLocalhost {
const settingsPath = AutoEncryptLocalhost.settingsPath
this.settingsPath = settingsPath
const mkcertBinary = path.join(settingsPath, binaryName)
const options = _options || {}
const listener = _listener || null
......@@ -64,11 +71,62 @@ export default class AutoEncryptLocalhost {
const rootCAKeyFilePath = path.join(settingsPath, 'rootCA-key.pem')
const rootCACertFilePath = path.join(settingsPath, 'rootCA.pem')
const allOK = fs.existsSync(rootCACertFilePath) && fs.existsSync(rootCAKeyFilePath) && fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)
function allOK () {
return fs.existsSync(rootCACertFilePath) && fs.existsSync(rootCAKeyFilePath) && fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)
}
if (!allOK) {
console.log('Could not find all necessary certificate information. Panic!')
process.exit(1)
// Create certificates.
if (!allOK()) {
log(' 📜 ❨auto-encrypt-localhost❩ Setting up…')
// On Linux and on macOS, mkcert uses the Mozilla nss library.
// Try to install this automatically and warn the person if we can’t so
// that they can do it manually themselves.
installCertutil()
// mkcert uses the CAROOT environment variable to know where to create/find the certificate authority.
// We also pass the rest of the system environment to the spawned processes.
const mkcertProcessOptions = {
env: process.env,
stdio: 'pipe' // suppress output
}
mkcertProcessOptions.env.CAROOT = settingsPath
try {
// Create the local certificate authority.
log(' 📜 ❨auto-encrypt-localhost❩ Creating local certificate authority (local CA) using mkcert…')
childProcess.execFileSync(mkcertBinary, ['-install'], mkcertProcessOptions)
log(' 📜 ❨auto-encrypt-localhost❩ Local certificate authority created.')
// Create the local certificate.
log(' 📜 ❨auto-encrypt-localhost❩ Creating local TLS certificates using mkcert…')
// Support all local interfaces so that the machine can be reached over the local network via IPv4.
// This is very useful for testing with multiple devices over the local area network without needing to expose
// the machine over the wide area network/Internet using a service like ngrok.
const localIPv4Addresses =
Object.entries(os.networkInterfaces())
.map(iface =>
iface[1].filter(addresses =>
addresses.family === 'IPv4')
.map(addresses => addresses.address)).flat()
const certificateDetails = [
`-key-file=${keyFilePath}`,
`-cert-file=${certFilePath}`,
'localhost'
].concat(localIPv4Addresses)
childProcess.execFileSync(mkcertBinary, certificateDetails, mkcertProcessOptions)
log(' 📜 ❨auto-encrypt-localhost❩ Local TLS certificates created.')
} catch (error) {
log('\n', error)
}
if (!allOK()) {
process.exit(1)
}
} else {
log(' 📜 ❨auto-encrypt-localhost❩ Local development TLS certificate exists.')
}
// Add root store to Node to ensure Node recognises the certificates (e.g., when using https.get(), etc.)
......
......@@ -6,7 +6,7 @@ import { log, print } from './util/log.js'
// and on macOS for Firefox. Ensure it exists.
// Source: https://github.com/FiloSottile/mkcert/blob/master/README.md#installation
export default function installCertutil () {
process.stdout.write(` ╰─ Installing certutil if necessary… `)
process.stdout.write(` 📜 ❨auto-encrypt-localhost❩ Installing certutil if necessary… `)
const platform = os.platform()
if (platform === 'linux') {
installCertutilOnLinux()
......
......@@ -9,8 +9,6 @@
//////////////////////////////////////////////////////////////////////
import os from 'os'
import path from 'path'
import AutoEncryptLocalhost from '../index.js'
export const version = '1.4.3'
......@@ -33,4 +31,3 @@ if (platform === undefined) throw new Error('Unsupported platform', os.platform(
if (architecture === undefined) throw new Error('Unsupported architecture', os.arch())
export const binaryName = `mkcert-v${version}-${platform}-${architecture}${platform === 'windows' ? '.exe' : ''}`
export const binaryPath = path.join(AutoEncryptLocalhost.settingsPath, binaryName)
{
"name": "@small-tech/auto-encrypt-localhost",
"version": "7.0.4",
"version": "7.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@small-tech/auto-encrypt-localhost",
"version": "7.0.4",
"version": "7.0.5",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment