Skip to content

Commit

Permalink
Merge branch 'unbundled-binaries'
Browse files Browse the repository at this point in the history
  • Loading branch information
aral committed Mar 7, 2021
2 parents b414c4b + f97322b commit a81c96a
Show file tree
Hide file tree
Showing 16 changed files with 2,422 additions and 1,867 deletions.
2 changes: 0 additions & 2 deletions .gitattributes

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
node_modules
.nyc_output
bin/pebble
bin/pebble.exe
coverage
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,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).

## [5.0.0] - 2021-03-07

### Changed

- __Breaking change:__ Binaries are no longer bundled in the package. They are downloaded as an npm postinstall task.
- Uses ECMAScript Modules (ESM; es6 modules).
- Zero runtime dependencies.
- Dev: now using @small-tech/esm-tape-runner.
- Dev: now using c8 instead of nyc for coverage.
- Dev: replaced tap-spec and tap-nyc with @small-tech/tap-monkey.

## [4.2.4] - 2020-10-29

### Improved
Expand Down
669 changes: 11 additions & 658 deletions LICENSE

Large diffs are not rendered by default.

40 changes: 21 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A Node.js wrapper for [Let’s Encrypt](https://letsencrypt.org)’s [Pebble](https://github.com/letsencrypt/pebble) (“a small RFC 8555 ACME test server not suited for a production certificate authority”).

- Downloads the correct Pebble binary for your platform.

- Launches and manages a single Pebble process.

- Returns a reference to the same process on future calls (safe to include in multiple unit tests where order of tests is undetermined)
Expand All @@ -10,7 +12,7 @@ A Node.js wrapper for [Let’s Encrypt](https://letsencrypt.org)’s [Pebble](ht

## Version and platform support

Supports [Pebble version 2.3.0](https://github.com/letsencrypt/pebble/releases/tag/v2.3.0) under [Node.js LTS](https://nodejs.org/en/about/releases/) on platforms with binary [Pebble releases](https://github.com/letsencrypt/pebble/releases/):
Supports [Pebble version 2.3.1](https://github.com/letsencrypt/pebble/releases/tag/v2.3.1) under [Node.js LTS](https://nodejs.org/en/about/releases/) on platforms with binary [Pebble releases](https://github.com/letsencrypt/pebble/releases/):

- Linux AMD 64.
- Windows AMD 64.
Expand All @@ -21,9 +23,13 @@ Supports [Pebble version 2.3.0](https://github.com/letsencrypt/pebble/releases/t
npm i @small-tech/node-pebble
```

As part of the post-installation process, Node Pebble will download the correct Pebble binary for your platform for use at runtime.

Node Pebble has zero runtime dependencies.

## API

### Pebble.ready ([args], [env]) -> Promise<ChidProcess>
### Pebble.ready ([args], [env]) -> Promise&lt;ChildProcess&gt;

Promises to get the Pebble server ready for use. Resolves once Pebble server is launched and ready and Node.js’s TLS module has been patched to accept Pebble server’s [test certificate](https://github.com/letsencrypt/pebble#avoiding-client-https-errors) as well as its [dynamically-generated root and intermediary CA certificates](https://github.com/letsencrypt/pebble#ca-root-and-intermediate-certificates).

Expand Down Expand Up @@ -88,27 +94,23 @@ Pebble.ready('-config customConfig.json')
The following listing launches the Pebble server with its default settings and then shuts it down.

```js
const Pebble = require('..')

async function main() {
console.log('\n⏳ Launching Pebble server…\n')
import Pebble from '@small-tech/node-pebble'

await Pebble.ready()
console.log('\n⏳ Launching Pebble server…\n')

console.log('✔ Pebble server launched and ready.')
console.log('✔ Node.js’s TLS module patched to accept Pebble’s CA certificates.')
await Pebble.ready()

// Do stuff that requires Pebble here.
//
console.log('✔ Pebble server launched and ready.')
console.log('✔ Node.js’s TLS module patched to accept Pebble’s CA certificates.')

console.log('\n⏳ Shutting down Pebble server…\n')
// Do stuff that requires Pebble here.
//

await Pebble.shutdown()
console.log('\n⏳ Shutting down Pebble server…\n')

console.log('✔ Pebble server shut down.\n')
}
await Pebble.shutdown()

main()
console.log('✔ Pebble server shut down.\n')
```

## Install development dependencies (for tests and coverage)
Expand All @@ -120,13 +122,13 @@ npm install
## Run test task

```sh
npm test
npm -s test
```

## Run coverage task

```sh
npm run coverage
npm -s run coverage
```

## Like this? Fund us!
Expand All @@ -137,7 +139,7 @@ We exist in part thanks to patronage by people like you. If you share [our visio

## Copyright

&copy; 2020 [Aral Balkan](https://ar.al), [Small Technology Foundation](https://small-tech.org).
&copy; 2020-2021 [Aral Balkan](https://ar.al), [Small Technology Foundation](https://small-tech.org).

Let’s Encrypt is a trademark of the Internet Security Research Group (ISRG). All rights reserved. Node.js is a trademark of Joyent, Inc. and is used with its permission. We are not endorsed by or affiliated with Joyent or ISRG.

Expand Down
3 changes: 0 additions & 3 deletions bin/pebble

This file was deleted.

3 changes: 0 additions & 3 deletions bin/pebble.exe

This file was deleted.

94 changes: 94 additions & 0 deletions bin/post-install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node

////////////////////////////////////////////////////////////////////////////////
//
// npm post-install script
//
// Downloads and installs the version of pebble specified in the code.
//
////////////////////////////////////////////////////////////////////////////////

import os from 'os'
import fs from 'fs'
import path from 'path'
import https from 'https'

const __dirname = new URL('.', import.meta.url).pathname

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)
})
})
})
}

//
// Sanity check: ensure we’re on a supported platform (Linux or Windows) and bail if not.
//

const _platform = os.platform()
if (_platform !== 'win32' && _platform !== 'linux') {
throw new Error(`Node Pebble Error: unsupported platform (only Linux and Windows is supported, not ${_platform}).`)
}

//
// Install the Pebble binary.
//

const PEBBLE_VERSION = 'v2.3.1'

const platform = _platform === 'win32' ? 'windows' : 'linux'
const binaryExtension = _platform === 'win32' ? '.exe' : ''

const binaryName = `pebble${binaryExtension}`
const downloadUrl = `https://github.com/letsencrypt/pebble/releases/download/${PEBBLE_VERSION}/pebble_${platform}-amd64${binaryExtension}`
const binaryPath = path.join(__dirname, binaryName)


console.log(' Node Pebble (postinstall)')
console.log(' ────────────────────────────────────────────────────────────────────────')

process.stdout.write(` ╰─ Removing old Pebble binary (if any)… `)
fs.rmSync(binaryPath, {force: true})
process.stdout.write('done.\n')

process.stdout.write(` ╰─ Downloading Pebble ${PEBBLE_VERSION} binary… `)
const binaryRedirectUrl = (await secureGet(downloadUrl)).location
await secureStreamToFile(binaryRedirectUrl, binaryPath)
process.stdout.write('done.\n')

process.stdout.write(` ╰─ Making the binary executable… `)
fs.chmodSync(binaryPath, 0o755)
process.stdout.write('done.\n')

console.log(' ────────────────────────────────────────────────────────────────────────')
2 changes: 1 addition & 1 deletion example/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* The Pebble Node example from the readme.
*/
const Pebble = require('..')
import Pebble from '../index.js'

async function main() {
console.log('\n ⏳ Launching Pebble server…\n')
Expand Down
18 changes: 9 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@
* not suited for a production certificate authority”).
*
* @module
* @copyright © 2020 Aral Balkan, Small Technology Foundation
* @Copyright © 2020-2021 Aral Balkan, Small Technology Foundation
* @license AGPL version 3.0 or later
*/
const os = require('os')
const path = require('path')
const childProcess = require('child_process')
const log = require('./lib/util/log')
const MonkeyPatchTls = require('./lib/MonkeyPatchTls')
import os from 'os'
import path from 'path'
import childProcess from 'child_process'
import log from './lib/util/log.js'
import MonkeyPatchTls from './lib/MonkeyPatchTls.js'

const spawn = childProcess.spawn

const __dirname = new URL('.', import.meta.url).pathname

/**
* @alias module
*/
class Pebble {
export default class Pebble {
/**
* Promises to spawn a Pebble process and resolve the promise when the server is ready for use.
*
Expand Down Expand Up @@ -128,5 +130,3 @@ class Pebble {
*/
static #pebbleProcess = null
}

module.exports = Pebble
35 changes: 21 additions & 14 deletions lib/MonkeyPatchTls.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,39 @@
* Based on the method provided by David Barral at https://link.medium.com/6xHYLeUVq5.
*
* @module
* @copyright Copyright © 2020 Aral Balkan, Small Technology Foundation.
* @copyright Copyright © 2020-2021 Aral Balkan, Small Technology Foundation.
* @license AGPLv3 or later.
*/
const fs = require('fs-extra')
const path = require('path')
const tls = require('tls')
const bent = require('bent')
const Throws = require('./util/Throws')
import fs from 'fs'
import path from 'path'
import https from 'https'
import tls from 'tls'
import Throws from './util/Throws.js'

const __dirname = new URL('.', import.meta.url).pathname

const throws = new Throws({
[Symbol.for('MonkeyPatchTls.certificateParseError')]:
(certificatePath, additionalCertificates) => `Could not parse certificate at path ${certificatePath}. Additional certificates: ${additionalCertificates}`
})

function httpsGetString (url) {
return new Promise((resolve, reject) => {
https.get(url, response => {
let str = ''
response.on('data', data => str += data)
response.on('end', () => { resolve(str) })
response.on('error', error => reject(error))
})
})
}

/**
* Monkey patches the TLS module to accept run-time root and intermediary Certificate Authority certificates.
*
* @alias module:lib/MonkeyPatchTls
*/
class MonkeyPatchTLS {
export default class MonkeyPatchTLS {
static #originalCreateSecureContext = null

/**
Expand Down Expand Up @@ -62,9 +75,7 @@ class MonkeyPatchTLS {
// Load the Pebbleserver’s own test CA certificate from disk.
// (See https://github.com/letsencrypt/pebble#avoiding-client-https-errors.)
// Note that this is not the the Pebble CA root or intermediary certificate (see below).
let pem = fs
.readFileSync(certificatePath, { encoding: 'ascii' })
.replace(/\r\n/g, "\n")
let pem = fs.readFileSync(certificatePath, { encoding: 'ascii' }).replace(/\r\n/g, "\n")

// Add any additional certificates that might have been provided to the PEM that’s loaded from disk.
// (e.g., to create the Pebble server’s chain of trust).
Expand Down Expand Up @@ -97,8 +108,6 @@ class MonkeyPatchTLS {
* @returns {String} The Pebble server’s CA root and intermediary certificates as a single PEM-formatted string.
*/
static async downloadPebbleCaRootAndIntermediaryCertificates() {
const httpsGetString = bent('GET', 'string')

const rootCaUrl = 'https://localhost:15000/roots/0'
const intermediaryCaUrl = 'https://localhost:15000/intermediates/0'

Expand All @@ -110,5 +119,3 @@ class MonkeyPatchTLS {
return pem
}
}

module.exports = MonkeyPatchTLS
8 changes: 3 additions & 5 deletions lib/util/Throws.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
// and predefined yet configurable lists of errors to make working with errors
// safer (fewer magic strings) and DRYer (Don’t Repeat Yourself).
//
// Copyright © 2020 Aral Balkan, Small Technology Foundation.
// Copyright © 2020-2021 Aral Balkan, Small Technology Foundation.
// License: AGPLv3 or later.
//
////////////////////////////////////////////////////////////////////////////////

const fs = require('fs-extra')
import fs from 'fs'

class SymbolicError extends Error {
symbol = null
Expand All @@ -21,7 +21,7 @@ class SymbolicError extends Error {
// at the end of the error message, in parentheses.
const hinted = (str, hint) => hint ? `${str} (${hint}).` : `${str}.`

class Throws {
export default class Throws {
// Define common global errors. These are mixed into a local ERRORS object
// if it exists on the object the mixin() method is called with a reference to.
static ERRORS = {
Expand Down Expand Up @@ -117,5 +117,3 @@ class Throws {
throw this.createError(symbol, ...args)
}
}

module.exports = Throws
4 changes: 1 addition & 3 deletions lib/util/log.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Conditionally log to console.
function log (...args) {
export default function log (...args) {
if (process.env.QUIET) {
return
}
console.log(...args)
}

module.exports = log
Loading

0 comments on commit a81c96a

Please sign in to comment.