From 22c1e761e0c50367d387214a81914ec66849dc8c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 23 Jul 2019 00:53:39 -0600 Subject: [PATCH] add template, implement auth and zones --- .gitignore | 4 +++ AUTHORS | 1 + README.md | 28 +++++++++++++++++++- example.env | 3 +++ index.js | 3 +++ lib/auth.js | 36 +++++++++++++++++++++++++ lib/index.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ test.js | 37 ++++++++++++++++++++++++++ 8 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 AUTHORS create mode 100644 example.env create mode 100644 index.js create mode 100644 lib/auth.js create mode 100644 lib/index.js create mode 100755 test.js diff --git a/.gitignore b/.gitignore index 144585f..c4ee6d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +*.json +service_account.json +credentials.json + # ---> Node # Logs logs diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..f2496e6 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +AJ ONeal (https://coolaj86.com/) diff --git a/README.md b/README.md index 5e4d3a8..881a3d6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ # acme-dns-01-googlecloud.js -Google Cloud DNS for Let's Encrypt / ACME dns-01 challenges with ACME.js and Greenlock.js \ No newline at end of file +Google Domains + Let's Encrypt for Node.js - ACME dns-01 challenges w/ ACME.js and Greenlock.js + +In Progress. Would love help. Please contact @coolaj86 on Keybase. + +- [x] zones +- [ ] set +- [ ] get +- [ ] remove + +Implementation Details + +- https://cloud.google.com/dns/docs/reference/v1/ +- https://cloud.google.com/service-usage/docs/getting-started#api +- https://github.com/google/oauth2l + +# Usage + +First you create an instance with your credentials: + +```js +var dns01 = require('acme-dns-01-googlecloud').create({ + baseUrl: 'https://www.googleapis.com/dns/v1/', // default + + // contains private_key, private_key_id, project_id, and client_email + serviceAccountPath: __dirname + '/service_account.json' +}); +``` diff --git a/example.env b/example.env new file mode 100644 index 0000000..a455a16 --- /dev/null +++ b/example.env @@ -0,0 +1,3 @@ +# NOT credentials.json +GOOGLE_APPLICATION_CREDENTIALS=/Users/me/service_account.json +ZONE=example.co.uk diff --git a/index.js b/index.js new file mode 100644 index 0000000..647221a --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./lib/index.js'); diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 0000000..29df035 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,36 @@ +'use strict'; + +var Keypairs = require('keypairs'); + +module.exports.getToken = function(serviceAccount) { + var jwt = ''; + var exp = 0; + + if (exp - Date.now() > 0) { + return Promise.resolve(jwt); + } + + return module.exports.generateToken(serviceAccount).then(function(_jwt) { + jwt = _jwt; + exp = Math.round(Date.now()) - 15 * 60 * 60 * 1000; + return jwt; + }); +}; + +module.exports.generateToken = function(serviceAccount) { + var sa = serviceAccount; + return Keypairs.import({ pem: sa.private_key }).then(function(key) { + return Keypairs.signJwt({ + jwk: key, + iss: sa.client_email, + exp: '1h', + header: { + kid: sa.private_key_id + }, + claims: { + aud: 'ndev.clouddns.readwrite', + sub: sa.client_email + } + }); + }); +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..daf8462 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,74 @@ +'use strict'; + +var auth = require('./auth.js'); +var defaults = { + baseUrl: 'https://www.googleapis.com/dns/v1/' +}; + +module.exports.create = function(config) { + var request; + var baseUrl = (config.baseUrl || defaults.baseUrl).replace(/\/$/, ''); + var sa = getServiceAccount(config); + + return { + init: function(opts) { + request = opts.request; + return null; + }, + zones: function(data) { + //console.info('List Zones', data); + return api({ + url: baseUrl + '/projects/' + sa.project_id + '/managedZones', + json: true + }).then(function(resp) { + return resp.body.managedZones.map(function(zone) { + // slice out the leading and trailing single quotes, and the trailing dot + // (assuming that all 'dnsName's probably look the same) + return zone.dnsName.slice(1, zone.dnsName.length - 2); + }); + }); + }, + set: function(data) { + // console.info('Add TXT', data); + throw Error('setting TXT not implemented'); + }, + remove: function(data) { + // console.info('Remove TXT', data); + throw Error('removing TXT not implemented'); + }, + get: function(data) { + // console.info('List TXT', data); + throw Error('listing TXTs not implemented'); + } + }; + + function api(opts) { + return auth.getToken(sa).then(function(token) { + opts.headers = opts.headers || {}; + opts.headers.Authorization = 'Bearer ' + token; + return request(opts); + }); + } + + function getServiceAccount(config) { + var saPath = + config.serviceAccountPath || + process.env.GOOGLE_APPLICATION_CREDENTIALS; + var sa = config.serviceAccount || require(saPath); + + if ( + !sa || + !( + sa.private_key && + sa.private_key_id && + sa.client_email && + sa.project_id + ) + ) { + throw new Error( + 'missing or incomplete service_account.json: set serviceAccount serviceAccountPath' + ); + } + return sa; + } +}; diff --git a/test.js b/test.js new file mode 100755 index 0000000..c44b520 --- /dev/null +++ b/test.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +'use strict'; + +// See https://git.coolaj86.com/coolaj86/acme-challenge-test.js +var tester = require('acme-challenge-test'); +require('dotenv').config(); + +// Usage: node ./test.js example.com xxxxxxxxx +var zone = process.argv[2] || process.env.ZONE; +var config = { + serviceAccountPath: + process.argv[3] || process.env.GOOGLE_APPLICATION_CREDENTIALS +}; +var challenger = require('./index.js').create(config); + +// Google has its own special authentication +var sa = require(config.serviceAccountPath); +require('./lib/auth.js') + .getToken(sa) + .then(function(jwt) { + console.info('\nAuthorization: Bearer ' + jwt + '\n'); + + // The dry-run tests can pass on, literally, 'example.com' + // but the integration tests require that you have control over the domain + return tester + .testZone('dns-01', zone, challenger) + .then(function() { + console.info('PASS', zone); + }) + .catch(function(e) { + console.error(e.message); + console.error(e.stack); + }); + }) + .catch(function(err) { + console.error(err.stack); + });