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

Merge branch 'unbundled-binaries'

parents b414c4b5 f97322bc
bin/pebble filter=lfs diff=lfs merge=lfs -text
bin/pebble.exe filter=lfs diff=lfs merge=lfs -text
node_modules
.nyc_output
bin/pebble
bin/pebble.exe
coverage
......@@ -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
......
This diff is collapsed.
......@@ -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)
......@@ -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.
......@@ -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).
......@@ -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)
......@@ -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!
......@@ -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.
......
File deleted
File deleted
#!/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(' ────────────────────────────────────────────────────────────────────────')
/**
* 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')
......
......@@ -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.
*
......@@ -128,5 +130,3 @@ class Pebble {
*/
static #pebbleProcess = null
}
module.exports = Pebble
......@@ -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
/**
......@@ -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).
......@@ -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'
......@@ -110,5 +119,3 @@ class MonkeyPatchTLS {
return pem
}
}
module.exports = MonkeyPatchTLS
......@@ -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
......@@ -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 = {
......@@ -117,5 +117,3 @@ class Throws {
throw this.createError(symbol, ...args)
}
}
module.exports = Throws
// Conditionally log to console.
function log (...args) {
export default function log (...args) {
if (process.env.QUIET) {
return
}
console.log(...args)
}
module.exports = log
This diff is collapsed.
{
"name": "@small-tech/node-pebble",
"version": "4.2.4",
"version": "5.0.0",
"description": "A Node.js wrapper for Let’s Encrypt’s Pebble (“a small RFC 8555 ACME test server not suited for a production certificate authority”).",
"type": "module",
"main": "index.js",
"files": [
"lib",
"bin/post-install.js",
"bin/test"
],
"os": [
"linux",
"win32"
......@@ -23,20 +29,23 @@
"rfc 8555"
],
"scripts": {
"test": "QUIET=true tape test/*.js | tap-spec",
"coverage": "QUIET=true nyc tape test/*.js | tap-nyc",
"test-debug": "tape test/*.js | tap-spec",
"coverage-debug": "nyc tape test/*.js | tap-nyc"
"postinstall": "bin/post-install.js",
"test": "QUIET=true esm-tape-runner test/*.js | tap-monkey",
"coverage": "QUIET=true c8 esm-tape-runner test/*.js | tap-monkey",
"test-debug": "tape test/*.js | tap-monkey",
"coverage-debug": "nyc tape test/*.js | tap-monkey"
},
"nyc": {
"c8": {
"exclude": [
"lib/util/*.js",
"test/*.js"
]
},
"homepage": "https://github.com/small-tech/node-pebble",
"bugs": "https://github.com/small-tech/node-pebble/issues",
"repository": {
"type": "git",
"url": "https://source.small-tech.org/site.js/lib/node-pebble.git"
"url": "https://github.com/small-tech/node-pebble"
},
"author": {
"name": "Aral Balkan",
......@@ -45,13 +54,9 @@
},
"license": "AGPL-3.0-or-later",
"devDependencies": {
"nyc": "^15.1.0",
"tap-nyc": "^1.0.3",
"tap-spec": "https://github.com/small-tech/tap-spec",
"@small-tech/esm-tape-runner": "^1.0.3",
"@small-tech/tap-monkey": "^1.3.0",
"c8": "^7.6.0",
"tape": "^5.0.1"
},
"dependencies": {
"bent": "^7.3.12",
"fs-extra": "^9.0.1"
}
}
const test = require('tape')
const Pebble = require('..')
import test from 'tape'
import Pebble from '../index.js'
test ('Node Pebble', async t => {
//
......
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