Commit 904a5a79 authored by Aral Balkan's avatar Aral Balkan
Browse files

Merge branch 'hybrid-approach'

parents 71192559 93ee00bb
......@@ -21,15 +21,12 @@
import https from 'https'
import os from 'os'
import path from 'path'
import { log, print } from '../lib/util/log.js'
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 => {
......@@ -76,9 +73,9 @@ async function secureStreamToFile (url, filePath) {
const settingsPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost')
console.log(' 🔒️ Auto Encrypt Localhost (postinstall)')
console.log(' ────────────────────────────────────────────────────────────────────────')
process.stdout.write(` ╰─ Installing mkcert v${version} binary… `)
log(' 🔒️ Auto Encrypt Localhost (postinstall)')
log(' ────────────────────────────────────────────────────────────────────────')
print(` ╰─ Installing mkcert v${version} binary… `)
// Delete and recreate the mkcert-bin folder.
fs.removeSync(settingsPath)
......@@ -93,124 +90,6 @@ await secureStreamToFile(binaryRedirectUrl, binaryPath)
// Make the binary executable.
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})
print('done.\n')
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(' ────────────────────────────────────────────────────────────────────────')
}
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… `)
print(` 📜 ❨auto-encrypt-localhost❩ Installing certutil if necessary… `)
const platform = os.platform()
if (platform === 'linux') {
installCertutilOnLinux()
......@@ -15,10 +15,9 @@ export default function installCertutil () {
} else if (platform === 'win32') {
// Do nothing. According to the mkcert documentation, certutil is not
// required on Windows.
console.log('done.')
} else {
// Unknown platform. This should have been caught earlier. Panic.
throw new Error(' ╰─ 🤯 Panic: Unknown platform detected.', platform)
throw new Error(' 🤯 ❨auto-encrypt-localhost❩ Panic: Unknown platform detected.', platform)
}
}
......@@ -32,11 +31,11 @@ export default function installCertutil () {
function installCertutilOnLinux () {
if (commandExists('certutil')) {
// Already installed
process.stdout.write('done.\n')
return
print('done.\n')
return
}
print('\n ╰─ Installing certutil dependency (Linux) ')
print(' 💽️ ❨auto-encrypt-localhost❩ Installing certutil dependency (Linux) ')
let options = { env: process.env }
try {
if (commandExists('apt')) {
......@@ -45,19 +44,19 @@ function installCertutilOnLinux () {
childProcess.execSync('sudo apt-get install -y -q libnss3-tools', options)
} else if (commandExists('yum')) {
// Untested: if you test this, please let me know https://github.com/indie-mirror/https-server/issues
log(' ╰─ 🤪 Attempting to install required dependency using yum. This is currently untested. If it works (or blows up) for you, I’d appreciate it if you could open an issue at https://github.com/indie-mirror/https-server/issues and let me know. Thanks! – Aral\n')
log(' 🤪 ❨auto-encrypt-localhost❩ Attempting to install required dependency using yum. This is currently untested. If it works (or blows up) for you, I’d appreciate it if you could open an issue at https://github.com/indie-mirror/https-server/issues and let me know. Thanks! – Aral\n')
childProcess.execSync('sudo yum install nss-tools', options)
log(' ╰─ Certutil installed using yum.')
log(' ✔️ ❨auto-encrypt-localhost❩ Certutil installed using yum.')
} else if (commandExists('pacman')) {
childProcess.execSync('sudo pacman -S nss', options)
log(' ╰─ Certutil installed using pacman.')
log(' ✔️ ❨auto-encrypt-localhost❩ Certutil installed using pacman.')
} else {
// Neither Homebrew nor MacPorts is installed. Warn the person.
log(' ╰─ ⚠️ Linux: No supported package manager found for installing certutil on Linux (tried apt, yum, and pacman. Please install certutil manually and run Auto Encrypt Localhost again. For more instructions on installing mkcert dependencies, please see https://github.com/FiloSottile/mkcert/\n')
log(' ⚠️ ❨auto-encrypt-localhost❩ Linux: No supported package manager found for installing certutil on Linux (tried apt, yum, and pacman. Please install certutil manually and run Auto Encrypt Localhost again. For more instructions on installing mkcert dependencies, please see https://github.com/FiloSottile/mkcert/\n')
}
} catch (error) {
// There was an error and we couldn’t install the dependency. Warn the person.
log(' ╰─ ⚠️ Linux: Failed to install nss. Please install it manually and run Auto Encrypt Localhost again if you want your certificate to work in Chrome and Firefox', error)
log(' ⚠️ ❨auto-encrypt-localhost❩ Linux: Failed to install nss. Please install it manually and run Auto Encrypt Localhost again if you want your certificate to work in Chrome and Firefox', error)
}
}
......@@ -81,11 +80,11 @@ function installCertutilOnDarwin() {
} catch (error) {
// NSS is not installed. Install it.
try {
print(' ╰─ Installing certutil dependency (Darwin) using Homebrew… ')
print(' 💽️ ❨auto-encrypt-localhost❩Installing certutil dependency (Darwin) using Homebrew… ')
childProcess.execSync('brew install nss >/dev/null 2>&1', options)
log('done.')
} catch (error) {
log(' ╰─ ⚠️ macOS: Failed to install nss via Homebrew. Please install it manually and run Auto Encrypt Localhost again if you want your certificate to work in Firefox', error)
log(' ⚠️ ❨auto-encrypt-localhost❩ macOS: Failed to install nss via Homebrew. Please install it manually and run Auto Encrypt Localhost again if you want your certificate to work in Firefox', error)
return
}
}
......@@ -93,7 +92,7 @@ function installCertutilOnDarwin() {
// Untested. This is based on the documentation at https://guide.macports.org/#using.port.installed. I don’t have MacPorts installed
// and it doesn’t play well with Homebrew so I won’t be testing this anytime soon. If you do, please let me know how it works
// by opening an issue on https://github.com/indie-mirror/https-server/issues
log(' ╰─ 🤪 Attempting to install required dependency using MacPorts. This is currently untested. If it works (or blows up) for you, I’d appreciate it if you could open an issue at https://github.com/indie-mirror/https-server/issues and let me know. Thanks! – Aral\n')
log(' 🤪 ❨auto-encrypt-localhost❩ Attempting to install required dependency using MacPorts. This is currently untested. If it works (or blows up) for you, I’d appreciate it if you could open an issue at https://github.com/indie-mirror/https-server/issues and let me know. Thanks! – Aral\n')
try {
childProcess.execSync('port installed nss', options)
......@@ -102,13 +101,13 @@ function installCertutilOnDarwin() {
try {
childProcess.execSync('sudo port install nss', options)
} catch (error) {
log(' ╰─ ⚠️ macOS: Failed to install nss via MacPorts. Please install it manually and run Auto Encrypt Localhost again if you want your certificate to work in Firefox', error)
log(' ⚠️ ❨auto-encrypt-localhost❩ macOS: Failed to install nss via MacPorts. Please install it manually and run Auto Encrypt Localhost again if you want your certificate to work in Firefox', error)
return
}
}
} else {
// Neither Homebrew nor MacPorts is installed. Warn the person.
log(' ╰─ ⚠️ macOS: Cannot install certutil (nss) as you don’t have Homebrew or MacPorts installed.\n\n If you want your certificate to work in Firefox, please install one of those package managers and then install nss manually:\n ╰─ * Homebrew (https://brew.sh): brew install nss ╰─ * MacPorts(https://macports.org): sudo port install nss\n')
log(' ⚠️ ❨auto-encrypt-localhost❩ macOS: Cannot install certutil (nss) as you don’t have Homebrew or MacPorts installed.\n\n If you want your certificate to work in Firefox, please install one of those package managers and then install nss manually:\n ╰─ * Homebrew (https://brew.sh): brew install nss ╰─ * MacPorts(https://macports.org): sudo port install nss\n')
return
}
}
......
......@@ -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": {
......
......@@ -23,12 +23,13 @@
],
"scripts": {
"postinstall": "node bin/post-install.js",
"test": "QUIET=true esm-tape-runner 'test/**/*.js' | tap-monkey",
"unlockSudo": "sudo echo 'Got sudo privileges.\n'",
"test": "npm run unlockSudo && QUIET=true esm-tape-runner 'test/**/*.js' | tap-monkey",
"test-on-windows": "node test/index.js",
"coverage": "QUIET=true c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
"coverage": "npm run unlockSudo && QUIET=true c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
"coverage-on-windows": "c8 node .\\test\\index.js",
"test-debug": "esm-tape-runner 'test/**/*.js' | tap-monkey",
"coverage-debug": "c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
"test-debug": "npm run unlockSudo && esm-tape-runner 'test/**/*.js' | tap-monkey",
"coverage-debug": "npm run unlockSudo && c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
"generate-dependency-diagram": "mkdir -p artefacts && node_modules/.bin/depcruise --max-depth 1 --output-type dot index.js | dot -T svg > artefacts/dependency-graph.svg",
"generate-developer-documentation": "npm run generate-dependency-diagram && node_modules/.bin/jsdoc2md --private --template developer-documentation.hbs --files index.js > developer-documentation.md"
},
......
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