Verified Commit 124ca734 authored by Aral Balkan's avatar Aral Balkan
Browse files

Closes #12, #13: redirects HTTP→HTTPS and serves rootCA.pem at /.ca

parent ae2c558e
......@@ -5,6 +5,13 @@ 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).
## [5.3.0] - 2020-07-07
### Added
- Serves the local root certificate authority’s public key at route /.ca (you can hit this route from a device like an iPhone on your local area network to install the key and trust it on your device to test your local server with that device over your local area network).
- Redirects HTTP to HTTPS (#13).
## [5.2.2] - 2020-07-06
### Added
......
......@@ -33,6 +33,8 @@ npm i @small-tech/auto-encrypt-localhost
### Example
(You can find this example in the _example/_ folder in the source code. Run it by typing `node example`.)
```js
// Create an https server using locally-trusted certificates.
......@@ -57,11 +59,21 @@ Object.entries(os.networkInterfaces())
.map(addresses => addresses.address)).flat()
```
To access your local machine from a different device on your local area network, you must transfer the public key of your generated local root certificate authority to that device and install and trust it. By default, once you’ve created your first server, you can find the key at `~/.small-tech/auto-encrypt-localhost/rootCA.pem`. For more details, please refer to [the relevant section in the mkcert documentation](https://github.com/FiloSottile/mkcert#mobile-devices).
### Accessing your local machine from other devices on your local area network
Note that 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)).
To access your local machine from a different device on your local area network, you must transfer the public key of your generated local root certificate authority to that device and install and trust it.
For example, hit the `/.ca` route on the external IPv4 address of your local machine from your iPhone. e.g., if your local machine is reachable via 192.168.2.42 on your local area network, going to the following addres will prompt you to install the public key (‘profile‘) on your iPhone. You will still have to go to Settings → General → About → :
You can find this example in the _example/_ folder in the source code. Run it by typing `node example`.
```
http://192.168.2.42/.ca
```
You can also tranfer 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).
### A note on privileged ports on Linux
Note that 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)).
## Configuration
......
......@@ -15,6 +15,7 @@ const childProcess = require('child_process')
const syswidecas = require('syswide-cas')
const mkcertBinaryForThisMachine = require('./lib/mkcertBinaryForThisMachine')
const installCertutil = require('./lib/installCertutil')
const HttpServer = require('./lib/HttpServer')
const { log } = require('./lib/util/log')
/**
......@@ -69,6 +70,8 @@ class AutoEncryptLocalhost {
// Ensure the Auto Encrypt Localhost directory exists.
fs.ensureDirSync(settingsPath)
this.settingsPath = settingsPath
// Get a path to the mkcert binary for this machine.
const mkcertBinary = mkcertBinaryForThisMachine(settingsPath)
......@@ -136,6 +139,35 @@ class AutoEncryptLocalhost {
options.cert = fs.readFileSync(certFilePath, 'utf-8')
const server = https.createServer(options, listener)
//
// Monkey-patch the server.
//
server.__autoEncryptLocalhost__self = this
// Monkey-patch the server’s listen method so that we can start up the HTTP
// Server at the same time.
server.__autoEncryptLocalhost__originalListen = server.listen
server.listen = function(...args) {
// Start the HTTP server.
HttpServer.getSharedInstance(settingsPath).then(() => {
// Start the HTTPS server.
return this.__autoEncryptLocalhost__originalListen.apply(this, args)
})
}
// Monkey-patch the server’s close method so that we can perform clean-up and
// shut down the HTTP server transparently when server.close() is called.
server.__autoEncryptLocalhost__originalClose = server.close
server.close = function (...args) {
// Shut down the HTTP server.
HttpServer.destroySharedInstance().then(() => {
// Shut down the HTTPS server.
return this.__autoEncryptLocalhost__originalClose.apply(this, args)
})
}
return server
}
}
......
////////////////////////////////////////////////////////////////////////////////
//
// HttpServer
//
// (Singleton; please use HttpServer.getSharedInstance() to access.)
//
// A simple HTTP server that:
//
// 1. Forwards http requests to https requests using a 307 redirect.
// 2. Serves the local root certificate authority public key at /.ca
//
// Copyright © 2020 Aral Balkan, Small Technology Foundation.
// License: AGPLv3 or later.
//
////////////////////////////////////////////////////////////////////////////////
const fs = require('fs')
const path = require('path')
const http = require('http')
const encodeUrl = require('encodeurl')
const enableDestroy = require('server-destroy')
const { log } = require('./util/log')
class HttpServer {
//
// Singleton access (async).
//
static instance = null
static isBeingInstantiatedViaSingletonFactoryMethod = false
static async getSharedInstance (settingsPath) {
if (HttpServer.instance === null) {
HttpServer.isBeingInstantiatedViaSingletonFactoryMethod = true
HttpServer.instance = new HttpServer(settingsPath)
await HttpServer.instance.init()
}
return HttpServer.instance
}
static async destroySharedInstance () {
if (HttpServer.instance === null) {
log(' 🚮 ❨auto-encrypt-localhost❩ HTTP Server was never setup. Nothing to destroy.')
return
}
log(' 🚮 ❨auto-encrypt-localhost❩ Destroying HTTP Server…')
await HttpServer.instance.destroy()
HttpServer.instance = null
log(' 🚮 ❨auto-encrypt-localhost❩ HTTP Server is destroyed.')
}
//
// Private.
//
constructor (settingsPath) {
// Ensure singleton access.
if (HttpServer.isBeingInstantiatedViaSingletonFactoryMethod === false) {
throw new Error('HttpServer is a singleton. Please instantiate using the HttpServer.getSharedInstance() method.')
}
HttpServer.isBeingInstantiatedViaSingletonFactoryMethod = false
const localRootCertificateAuthorityPublicKeyPath = path.join(settingsPath, 'rootCA.pem')
this.server = http.createServer((request, response) => {
if (request.url === '/.ca') {
log(' 📜 ❨auto-encrypt-localhost❩ Serving local root certificate authority public key at /.ca')
if (!fs.existsSync(localRootCertificateAuthorityPublicKeyPath)) {
log(' ❌ ❨auto-encrypt-localhost❩ Error: could not fing rootCA.pem file at ${localRootCertificateAuthorityPublicKeyPath}.')
response.writeHead(404, {'Content-Type': 'text/plain'})
response.end('Not found.')
return
}
response.writeHead(
200,
{
'Content-Type': 'application/x-pem-file',
'Content-Disposition': 'attachment; filename="rootCA.pem"'
}
)
const stream = fs.createReadStream(localRootCertificateAuthorityPublicKeyPath)
stream.pipe(response)
response.on('error', error => {
log(` ❌ ❨auto-encrypt-localhost❩ Error while writing rootCA.pem to response: ${error}`)
})
stream.on('error', error => {
log(` ❌ ❨auto-encrypt-localhost❩ Error while reading rootCA.pem: ${error}`)
})
} else {
// Act as an HTTP to HTTPS forwarder.
// (This means that servers using Auto Encrypt will get automatic HTTP to HTTPS forwarding
// and will not fail if they are accessed over HTTP.)
let httpsUrl = null
try {
httpsUrl = new URL(`https://${request.headers.host}${request.url}`)
} catch (error) {
log(` ⚠ ❨auto-encrypt-localhost❩ Failed to redirect HTTP request: ${error}`)
response.statusCode = 403
response.end('403: forbidden')
return
}
// Redirect HTTP to HTTPS.
log(` 👉 ❨auto-encrypt-localhost❩ Redirecting HTTP request to HTTPS.`)
response.statusCode = 307
response.setHeader('Location', encodeUrl(httpsUrl))
response.end()
}
})
// Enable server to be destroyed without waiting for any existing connections to close.
// (While there shouldn’t be any existing connections and while the likelihood of someone
// trying to denial-of-service this very low, it’s still the right thing to do.)
enableDestroy(this.server)
}
async init () {
// Note: the server is created on Port 80. On Linux, you must ensure that the Node.js process has
// ===== the correct privileges for this to work. Looking forward to removing this notice once Linux
// leaves the world of 1960s mainframe computers and catches up to other prominent operating systems
// that don’t have this archaic restriction which is security theatre at best and a security
// vulnerability at worst in the global digital network age.
await new Promise((resolve, reject) => {
try {
this.server.listen(80, () => {
log(` ✨ ❨auto-encrypt-localhost❩ HTTP server is listening on port 80.`)
resolve()
})
} catch (error) {
reject(error)
}
})
}
async destroy () {
// Starts killing all connections and closes the server.
this.server.destroy()
// Wait until the server is closed before returning.
await new Promise((resolve, reject) => {
this.server.on('close', () => {
resolve()
})
this.server.on('error', (error) => {
reject(error)
})
})
}
}
module.exports = HttpServer
{
"name": "@small-tech/auto-encrypt-localhost",
"version": "5.0.0",
"version": "5.2.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -809,6 +809,11 @@
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
......@@ -2428,6 +2433,11 @@
}
}
},
"server-destroy": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
"integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0="
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
......
{
"name": "@small-tech/auto-encrypt-localhost",
"version": "5.2.2",
"version": "5.3.0",
"description": "Automatically provisions and installs locally-trusted TLS certificates for Node.js https servers (including Express.js, etc.) using mkcert.",
"keywords": [
"mkcert",
......@@ -43,7 +43,9 @@
]
},
"dependencies": {
"encodeurl": "^1.0.2",
"fs-extra": "^8.1.0",
"server-destroy": "^1.0.1",
"syswide-cas": "^5.3.0"
},
"devDependencies": {
......
......@@ -5,7 +5,8 @@ const bent = require('bent')
const test = require('tape')
const AutoEncryptLocalhost = require('..')
const getHttpsString = bent('GET', 'string')
const downloadString = bent('GET', 'string')
const downloadBuffer = bent('GET', 'buffer')
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
......@@ -49,7 +50,7 @@ test('certificate creation', async t => {
})
})
const response = await getHttpsString('https://localhost')
const response = await downloadString('https://localhost')
t.strictEquals(response, 'ok', 'Response from server is as expected for access via localhost.')
......@@ -62,10 +63,16 @@ test('certificate creation', async t => {
.map(addresses => addresses.address)).flat()
await asyncForEach(localIPv4Addresses, async localIPv4Address => {
const response = await getHttpsString(`https://${localIPv4Address}`)
const response = await downloadString(`https://${localIPv4Address}`)
t.strictEquals(response, 'ok', `Response from server is as expected for access via ${localIPv4Address}`)
})
// Test downloading the local root certificate authority public key via /.ca route.
const downloadedRootCABuffer = await downloadBuffer('http://localhost/.ca')
const localRootCABuffer = fs.readFileSync(path.join(AutoEncryptLocalhost.settingsPath, 'rootCA.pem'))
t.strictEquals(Buffer.compare(localRootCABuffer, downloadedRootCABuffer), 0, 'The local root certificate authority public key is served correctly.')
server.close()
//
......
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