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

Merge branch 'esm'

parents 831dd8f7 9b17d1fd
node_modules
.nyc_output
coverage
mkcert-bin
......@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [7.0.0] - 2021-03-06
This version is optimised for use on development machines via npm install. It carries out the mkcert binary installation in a postinstall script. In version 6.x and earlier, all binaries for all platforms were bundled as the library also supported use from a binary install (see [Site.js](https://sitejs.org)). The 6.x branch will still be updated with new mkcert versions but the 7.x and later versions will be used in [Place](https://github.com/small-tech/place).
### Changed
- Uses ECMAScript Modules (ESM; es6 modules)
- __Breaking change:__ mkcert binary is now downloaded during installation and the root certificate authority and TLS certificates are created at this time also.
- __Breaking change:__ The settings path is no longer configurable and is shared by all installations of this package. This means whenever you install this package, it will update to the latest version of mkcert and recreate the root certificate authorities and local certificates and these will be used by all instances of Auto Encrypt Localhost on your dev machine (and this is most likely the behaviour you want).
- __Breaking change:__ Removed command-line interface as it is no longer nececessary. Your local certificate authority and TLS certificates are ready to be used once your npm install is complete.
## [6.1.0] - 2020-11-04
### Changed
......
......@@ -4,9 +4,9 @@ Automatically provisions and installs locally-trusted TLS certificates for Node.
## How it works
Before creating your HTTPS server, uses mkcert to create a local certificate authority, adds it to the various trust stores, and uses it to create locally-trusted TLS certificates that are installed in your server.
At installation time, Auto Encrypt Localhost uses mkcert to create a local certificate authority, adds it to the various trust stores, and uses it to create locally-trusted TLS certificates that are installed in your server.
You can reach your server via the local loopback addresses (localhost, 127.0.0.1) on the device itself and also from other devices on the local area network by using your device’s external IPv4 address.
At runtime, you can reach your server via the local loopback addresses (localhost, 127.0.0.1) on the device itself and also from other devices on the local area network by using your device’s external IPv4 address(es).
## Installation
......@@ -14,16 +14,6 @@ You can reach your server via the local loopback addresses (localhost, 127.0.0.1
npm i @small-tech/auto-encrypt-localhost
```
### On Linux
Make sure you disable privileged ports:
```
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0
```
(On Linux, ports 80 and 443 require special privileges. Please see [A note on Linux and the security farce that is “privileged ports”](#a-note-on-linux-and-the-security-farce-that-is-priviliged-ports). If you just need a Node web server that handles all that and more for you – or to see how to implement privilege escalation seamlessly in your own servers – see [Site.js](https://sitejs.org)).
## Usage
### Instructions
......@@ -31,7 +21,7 @@ sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0
1. Import the module:
```js
const AutoEncryptLocalhost = require('@small-tech/auto-encrypt-localhost')
import AutoEncryptLocalhost from '@small-tech/auto-encrypt-localhost'
```
2. Prefix your server creation code with a reference to the Auto Encrypt Localhost class:
......@@ -48,18 +38,18 @@ sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0
```js
// Create an https server using locally-trusted certificates.
const AutoEncryptLocalhost = require('@small-tech/auto-encrypt-localhost')
import AutoEncryptLocalhost from '@small-tech/auto-encrypt-localhost'
const server = AutoEncryptLocalhost.https.createServer((request, response) => {
response.end('Hello, world!')
})
server.listen(() => {
server.listen(443, () => {
console.log('Web server is running at https://localhost')
})
```
You can now reach your server via https://localhost, https://127.0.0.1, and via its external IPv4 address on your local area network. To find out what that is, you can run the following in the Node interpreter:
You can now reach your server via https://localhost, https://127.0.0.1, and via its external IPv4 address(es) on your local area network. To find the list of IP addresses that your local server is reachable from, you can run the following code in the Node interpreter:
```js
Object.entries(os.networkInterfaces())
......@@ -69,6 +59,48 @@ Object.entries(os.networkInterfaces())
.map(addresses => addresses.address)).flat()
```
### Plain Node.js example
If you just want to use the TLS certificates generated at installation time without using the Auto Encrypt Localhost library itself at runtime, you should install Auto Encrypt Localhost into your `dev-dependencies`. Post install, you can find your certificates in the _~/.small-tech.org/auto-encrypt-localhost_ folder.
Here’s a somewhat equivalent example to the one above but using Node’s regular `https` module instead of Auto Encrypt Localhost at runtime:
```js
import os from 'os'
import fs from 'fs'
import path from 'path'
import https from 'https'
const certificatesPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost')
const keyFilePath = path.join(certificatesPath, 'localhost-key.pem')
const certFilePath = path.join(certificatesPath, 'localhost.pem')
const options = {
key: fs.readFileSync(keyFilePath, 'utf-8'),
cert: fs.readFileSync(certFilePath, 'utf-8')
}
const server = https.createServer(options, (request, response) => {
response.end('Hello, world!')
})
server.listen(443, () => {
console.log('Web server is running at https://localhost')
})
```
_Note that if you don’t use Auto Encrypt Localhost at runtime, you won’t get some of the benefits that it provides, like automatically adding the certificate authority to Node’s trust store (for hitting your server using Node.js without certificate errors, the `/.ca` convenience route, and HTTP to HTTPS forwarding, etc.)_
### On Linux
To access your server on port 443, make sure you’ve disabled privileged ports:
```
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0
```
(On Linux, ports 80 and 443 require special privileges. Please see [A note on Linux and the security farce that is “privileged ports”](#a-note-on-linux-and-the-security-farce-that-is-priviliged-ports). If you just need a Node web server that handles all that and more for you – or to see how to implement privilege escalation seamlessly in your own servers – see [Site.js](https://sitejs.org)).
### Multiple servers
You are not limited to running your server on port 443. You can listen on any port you like and you can have multiple servers with the following caveat: the HTTP server that redirects HTTP calls to HTTPS and serves your local root certificate authority public key (see below) will only be created for the first server and then only if port 80 is free.
......@@ -91,29 +123,6 @@ The browser will download the local root certificate authority’s public key an
You can also transfer your key manually. You can find the key at `~/.small-tech/auto-encrypt-localhost/rootCA.pem` after you’ve created at least one server. For more details on transferring your key to other devices, please refer to [the relevant section in the mkcert documentation](https://github.com/FiloSottile/mkcert#mobile-devices).
## Configuration
You can specify a custom settings path for your local certificate authority and certificate data to be stored in by adding the Auto Encrypt Localhost-specific `settingsPath` option to the options object you pass to the Node `https` server. If not specified, the default settings path (_~/.small-tech.org/auto-encrypt-localhost/_) is used.
### Example
```js
const AutoEncrypt = require('@small-tech/auto-encrypt-localhost')
const options = {
// Regular HTTPS server and TLS server options, if any, go here.
// Optional Auto Encrypt options:
settingsPath: '/custom/settings/path'
}
// Pass the options object to https.createServer()
const server = AutoEncryptLocalhost.https.createServer(options, listener)
// …
```
## Developer documentation
If you want to help improve Auto Encrypt Localhost or better understand how it is structured and operates, please see the [developer documentation](developer-documentation.md).
......@@ -130,41 +139,28 @@ This is [small technology](https://small-tech.org/about/#small-technology).
If you’re evaluating this for a “startup” or an enterprise, let us save you some time: this is not the right tool for you. This tool is for individual developers to build personal web sites and apps for themselves and for others in a non-colonial manner that respects the human rights of the people who use them.
## Command-line interface
### Install
```sh
npm i -g @small-tech/auto-encrypt-localhost
```
### Use
```sh
auto-encrypt-localhost
```
Your certificates will be created in the _~/.small-tech.org/auto-encrypt-localhost_ directory.
## Caveats
### Windows
Locally-trusted certificates do not work under Firefox. Please use Edge or Chrome on this platform. This is [a mkcert limitation](https://github.com/FiloSottile/mkcert#supported-root-stores).
__Version 7.x is currently not tested under Windows.__ It may not be able to set the executable bit on the binary download if that’s necessary. __This notice will be removed once it’s been tested and confirmed to be working.__
## Related projects
From lower-level to higher-level:
### Auto Encrypt
- Source: https://source.small-tech.org/site.js/lib/auto-encrypt
- Source: https://github.com/small-tech/auto-encrypt
- Package: [@small-tech/auto-encrypt](https://www.npmjs.com/package/@small-tech/auto-encrypt)
Adds automatic provisioning and renewal of [Let’s Encrypt](https://letsencrypt.org) TLS certificates with [OCSP Stapling](https://letsencrypt.org/docs/integration-guide/#implement-ocsp-stapling) to [Node.js](https://nodejs.org) [https](https://nodejs.org/dist/latest-v12.x/docs/api/https.html) servers (including [Express.js](https://expressjs.com/), etc.)
### HTTPS
- Source: https://source.small-tech.org/site.js/lib/https
- Source: https://github.com/small-tech/https
- Package: [@small-tech/https](https://www.npmjs.com/package/@small-tech/https)
A drop-in replacement for the [standard Node.js HTTPS module](https://nodejs.org/dist/latest-v12.x/docs/api/https.html) with automatic development-time (localhost) certificates via Auto Encrypt Localhost and automatic production certificates via Auto Encrypt.
......@@ -172,9 +168,15 @@ A drop-in replacement for the [standard Node.js HTTPS module](https://nodejs.org
### Site.js
- Web site: https://sitejs.org
- Source: https://source.small-tech.org/site.js/app
- Source: https://github.com/small-tech/site.js
A tool for developing, testing, and deploying a secure static or dynamic personal web site or app with zero configuration.
### Place (work-in-progress)
Small Web Protocol Reference Server.
A complete [small technology](https://small-tech.org/about/#small-technology) tool for developing, testing, and deploying a secure static or dynamic personal web site or app with zero configuration.
- Source: https://github.com/small-tech/place
## A note on Linux and the security farce that is “privileged ports”
......@@ -207,7 +209,7 @@ We exist in part thanks to patronage by people like you. If you share [our visio
## Copyright
Copyright © [Aral Balkan](https://ar.al), [Small Technology Foundation](https://small-tech.org).
Copyright © 2019-2021 [Aral Balkan](https://ar.al), [Small Technology Foundation](https://small-tech.org).
## License
......
This diff is collapsed.
#!/usr/bin/env node
const AutoEncryptLocalhost = require('../index.js')
AutoEncryptLocalhost.https.createServer()
#!/usr/bin/env node
////////////////////////////////////////////////////////////////////////////////
//
// npm post-install script
//
// 1. Downloads and installs the version of mkcert specified in lib/mkcert.js
// for the platform this script is running on.
//
// 2. Attempts to install certutil if it isn’t already installed and
// if it can.
//
// 3. Creates the local root certificate authority using mkcert.
//
// 4. Generates TLS certificates for localhost as well as any IP addresses
// that the machine is reachable from on the network (if you
// change networks and you want to be reachable by IP, re-run npm i).
//
////////////////////////////////////////////////////////////////////////////////
import https from 'https'
import os from 'os'
import path from 'path'
import childProcess from 'child_process'
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'
async function secureGet (url) {
return new Promise((resolve, reject) => {
https.get(url, response => {
const statusCode = response.statusCode
const location = response.headers.location
// Reject if it’s not one of the status codes we are testing.
if (statusCode !== 200 && statusCode !== 302) {
reject({statusCode})
}
let body = ''
response.on('data', _ => body += _)
response.on('end', () => {
resolve({statusCode, location, body})
})
})
})
}
async function secureStreamToFile (url, filePath) {
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(filePath)
https.get(url, response => {
response.pipe(fileStream)
fileStream.on('finish', () => {
fileStream.close()
resolve()
})
fileStream.on('error', error => {
fs.unlinkSync(filePath)
reject(error)
})
})
})
}
//
// Install the mkcert binary, create the host, and the certificates.
// This is done after every npm install. (Better to always have the
// latest and greatest mkcert available to all projects on an account
// that make use of it.)
//
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… `)
// Delete and recreate the mkcert-bin folder.
fs.removeSync(settingsPath)
fs.mkdirpSync(settingsPath)
const mkcertBinaryUrl = `https://github.com/FiloSottile/mkcert/releases/download/v${version}/${binaryName}`
const binaryRedirectUrl = (await secureGet(mkcertBinaryUrl)).location
const binaryPath = path.join(settingsPath, binaryName)
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 keyFilePath = path.join(settingsPath, 'localhost-key.pem')
const certFilePath = path.join(settingsPath, 'localhost.pem')
const allOK = () => {
return fs.existsSync(path.join(settingsPath, 'rootCA.pem')) && fs.existsSync(path.join(settingsPath, 'rootCA-key.pem')) && fs.existsSync(path.join(settingsPath, 'localhost.pem')) && fs.existsSync(path.join(settingsPath, 'localhost-key.pem'))
}
// 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.
process.stdout.write(` ╰─ Installing certutil if necessary… `)
installCertutil()
process.stdout.write('done.\n')
// 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
// Create the local certificate authority.
process.stdout.write(` ╰─ Creating local certificate authority (local CA) using mkcert… `)
childProcess.execFileSync(mkcertBinary, ['-install'], mkcertProcessOptions)
process.stdout.write('done.\n')
// Create the local certificate.
process.stdout.write(' ╰─ 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)
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(' ────────────────────────────────────────────────────────────────────────')
}
#!/usr/bin/env node
const https = require('https')
const fs = require('fs-extra')
const childProcess = require('child_process')
const path = require('path')
const assert = require('assert')
async function secureGet (url) {
return new Promise((resolve, reject) => {
https.get(url, response => {
const statusCode = response.statusCode
const location = response.headers.location
// Reject if it’s not one of the status codes we are testing.
if (statusCode !== 200 && statusCode !== 302) {
reject({statusCode})
}
let body = ''
response.on('data', _ => body += _)
response.on('end', () => {
resolve({statusCode, location, body})
})
})
})
}
async function secureStreamToFile (url, filePath) {
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(filePath)
https.get(url, response => {
response.pipe(fileStream)
fileStream.on('finish', () => {
fileStream.close()
resolve()
})
fileStream.on('error', error => {
fs.unlinkSync(filePath)
reject(error)
})
})
})
}
// Compare two semver strings (nn.nn.nn) and return 0 if they’re equal,
// 1 if a > b and -1 if a < b.
function semverCompare (a, b) {
const [aMajor, aMinor, aPatch] = a.split('.').map(string => parseInt(string))
const [bMajor, bMinor, bPatch] = b.split('.').map(string => parseInt(string))
const aIsGreaterThanB =
(aMajor > bMajor)
|| (aMajor === bMajor && aMinor > bMinor)
|| (aMajor === bMajor && aMinor === bMinor && aPatch > bPatch)
return (a === b) ? 0 : aIsGreaterThanB ? 1 : -1
}
;(async ()=> {
console.log('')
console.log(' Update mkcert-bin script')
console.log(' ════════════════════════')
console.log('')
// Get the version of the current release.
const mkcertBinariesDirectory = path.resolve(path.join(__dirname, '..', 'mkcert-bin'))
if (!fs.existsSync(mkcertBinariesDirectory)) {
console.log(' Error: No mkcert-bin folder found. Exiting.\n')
process.exit(1)
}
const currentMkcertBinaries = fs.readdirSync(mkcertBinariesDirectory)
const currentMkcertVersionMatch = currentMkcertBinaries[0].match(/^mkcert-v(\d+\.\d+\.\d+)-/)
if (currentMkcertVersionMatch === null) {
console.log(' Error: Unable to ascertain current mkcert version. Exiting.\n')
process.exit(1)
}
const currentMkcertVersion = currentMkcertVersionMatch[1]
console.log(` Current mkcert version: ${currentMkcertVersion}`)
// Get the location of the latest release page.
const latestMkcertReleasesPage = await secureGet('https://github.com/FiloSottile/mkcert/releases/latest')
assert (latestMkcertReleasesPage.location !== undefined, 'Location must exist (302 redirect).')
// Get the latest release page.
const actualLatestReleasePage = await secureGet(latestMkcertReleasesPage.location)
assert(actualLatestReleasePage.location === undefined, 'Actual page should not be a redirect.')
const page = actualLatestReleasePage.body
const versionMatch = page.match(/href=\"\/FiloSottile\/mkcert\/releases\/tag\/v(\d+\.\d+\.\d+)\"/)
assert(versionMatch !== null, 'Version should be found on page.')
const latestMkcertVersion = versionMatch[1]
assert(latestMkcertVersion !== undefined, 'Version capturing group should exist.')
console.log(` Latest mkcert version : ${latestMkcertVersion}\n`)
switch(semverCompare(currentMkcertVersion, latestMkcertVersion)) {
case 0:
console.log('You already have the latest release version of mkcert included in auto-encrypt-localhost. Exiting.')
process.exit()
case 1:
console.log('Warning: It appears you have a later version than the release version included. Exiting.')
process.exit()
}
console.log(' Upgrading the binaries to the latest version…\n')
// Delete and recreate the mkcert-bin folder.
fs.removeSync(mkcertBinariesDirectory)
fs.mkdirpSync(mkcertBinariesDirectory)
const mkcertReleaseUrlPrefix = `https://github.com/FiloSottile/mkcert/releases/download/v${latestMkcertVersion}`
const latestMkcertBinaries = [
{
platform: 'Linux AMD 64-bit',
binaryName: `mkcert-v${latestMkcertVersion}-linux-amd64`
},
{
platform: 'Linux ARM',
binaryName: `mkcert-v${latestMkcertVersion}-linux-arm`
},
{
platform: 'Linux ARM64',
binaryName: `mkcert-v${latestMkcertVersion}-linux-arm64`
},
{
platform: 'Darwin (masOS) AMD 64-bit',
binaryName: `mkcert-v${latestMkcertVersion}-darwin-amd64`
},
{
platform: 'Windows AMD 64-bit',
binaryName: `mkcert-v${latestMkcertVersion}-windows-amd64.exe`
}
]
for (mkcertBinary of latestMkcertBinaries) {
const mkcertBinaryUrl = `${mkcertReleaseUrlPrefix}/${mkcertBinary.binaryName}`
console.log(` ${mkcertBinary.platform}`)
console.log(` ${''.repeat(mkcertBinary.platform.length)}`)
console.log(' ├ Downloading binary…')
const binaryRedirectUrl = (await secureGet(mkcertBinaryUrl)).location
const binaryPath = path.join(mkcertBinariesDirectory, mkcertBinary.binaryName)
await secureStreamToFile(binaryRedirectUrl, binaryPath)
console.log(` ╰ Upgraded to ${mkcertBinary.binaryName}\n`)
}
console.log(' Done.\n')
})()
......@@ -14,7 +14,7 @@ We exist in part thanks to patronage by people like you. If you share [our visio
Auto Encrypt Localhost is supported on:
- __Node:__ LTS (currently 12.16.1).
- __Node:__ LTS (currently 14.16.0).
- __ECMAScript:__ [ES2019](https://node.green/#ES2019)
## Overview of relationships
......@@ -25,44 +25,48 @@ __Not shown (for clarity):__ third-party Node modules, the `util` namespace with
Generated using [dependency cruiser](https://github.com/sverweij/dependency-cruiser).
To run dependency cruiser, you will need to [install Graphviz](https://graphviz.org/download/).
## How it works in more detail
Auto Encrypt Localhost is a Node.js wrapper for [mkcert](https://github.com/FiloSottile/mkcert/) that:
Auto Encrypt Localhost is a Node.js wrapper for [mkcert](https://github.com/FiloSottile/mkcert/) that, at the npm post-install stage:
* Uses the 64-bit release binaries to support Linux, macOS, and Windows.
- Downloads and uses correct mkcert release binary for you machine on Linux, macOS, and Windows.
* Automatically installs the _certutil_ (nss) dependency on Linux on systems with apt, pacman, yum (untested) and and on macOS if you have [Homebrew](https://brew.sh) or [MacPorts](https://www.macports.org/) (untested).
- Automatically installs the _certutil_ (nss) dependency on Linux on systems with apt, pacman, yum (untested) and and on macOS if you have [Homebrew](https://brew.sh) or [MacPorts](https://www.macports.org/) (untested).
* Creates a root Certificate Authority.
- Creates a root Certificate Authority.
* Creates locally-trusted TLS certificates for localhost, 127.0.0.1, and ::1.
- Creates locally-trusted TLS certificates for localhost, 127.0.0.1, and ::1.
You can use these certificates for local development without triggering self-signed certificate errors.
At runtime, you can use the library to create your HTTPS servers instead of using the built-in Node.js `https` module.
For more details on how Auto Encrypt Localhost works behind the scenes, please [see the mkcert README](https://github.com/FiloSottile/mkcert/blob/master/README.md).
## Tests
```sh
npm test
npm -s test
```
To see debug output, run `npm run test-debug` instead.
To see debug output, run `npm -s run test-debug` instead.
## Coverage
```sh
npm run coverage
npm -s run coverage
```
To see debug output, run `npm run coverage-debug` instead.
To see debug output, run `npm -s run coverage-debug` instead.