commit d64f91b74eb5b81bf78ab6e6a76bd3ed14dbd1d4 Author: nyaundi brian Date: Wed Nov 13 10:05:25 2019 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bb61b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +*secret* +node_modules diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..420e082 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "bracketSpacing": true, + "printWidth": 80, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": true +} diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..0d29c56 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +AJ ONeal (https://coolaj86.com/) +Nyaundi Brian (https://danleyb2.online/) diff --git a/README.md b/README.md new file mode 100644 index 0000000..51e5deb --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# [acme-dns-01-ovh](https://git.rootprojects.org/root/acme-dns-01-ovh.js) | a [Root](https://rootprojects.org) project + +OVH DNS + Let's Encrypt for Node.js + +This handles ACME dns-01 challenges, compatible with ACME.js and Greenlock.js. +Passes [acme-dns-01-test](https://git.rootprojects.org/root/acme-dns-01-test.js). + +# Install + +```bash +npm install --save acme-dns-01-ovh@3.x +``` + +# Usage + +First you need to create API keys on the OVH portal : + +Go to the page https://api.ovh.com/createToken/index.cgi?GET=/domain/zone&PUT=/domain/zone&POST=/domain/zone&DELETE=/domain/zone + +Then you create an instance with your API keys + +```js +var dns01 = require('acme-dns-01-ovh').create({ + applicationKey: 'xxxx', // default + applicationSecret: 'xxxx', + consumerKey: 'xxxx', + region: 'ovh-eu', // one of ovh-eu, ovh-ca, kimsufi-eu, kimsufi-ca, soyoustart-eu, soyoustart-ca, runabove-ca +}); +``` + +Then you can use it with any compatible ACME module, +such as Greenlock.js or ACME.js. + +### Greenlock.js + +```js +var Greenlock = require('greenlock-express'); +var greenlock = Greenlock.create({ + challenges: { + 'dns-01': dns01 + // ... + } +}); +``` + +See [Greenlockā„¢ Express](https://git.rootprojects.org/root/greenlock-express.js) +and/or [Greenlock.js](https://git.rootprojects.org/root/greenlock.js) documentation for more details. + +### ACME.js + +```js +// TODO +``` + +See the [ACME.js](https://git.rootprojects.org/root/acme-v2.js) for more details. + +### Build your own + +```js +dns01 + .set({ + identifier: { value: 'foo.example.com' }, + wildcard: false, + dnsHost: '_acme-challenge.foo.example.com', + dnsAuthorization: 'xxx_secret_xxx' + }) + .then(function() { + console.log('TXT record set'); + }) + .catch(function() { + console.log('Failed to set TXT record'); + }); +``` + +See [acme-dns-01-test](https://git.rootprojects.org/root/acme-dns-01-test.js) +for more implementation details. + +# Tests + +```bash +# node ./test.js domain-zone api-key +node ./test.js example.com xxxxxx +``` diff --git a/example.env b/example.env new file mode 100644 index 0000000..ce65184 --- /dev/null +++ b/example.env @@ -0,0 +1,10 @@ +ZONE=example.co.uk + + +APPLICATION_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +APPLICATION_SECRET=xxxxxxx +CONSUMER_KEY=xxxxxxxxxx + + +# one of ovh-eu, ovh-ca, kimsufi-eu, kimsufi-ca, soyoustart-eu, soyoustart-ca, runabove-ca +REGION='ovh-eu' 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/index.js b/lib/index.js new file mode 100644 index 0000000..660c58b --- /dev/null +++ b/lib/index.js @@ -0,0 +1,196 @@ +'use strict'; +var request = require('@root/request'); +request = require('util').promisify(request); +var crypto = require('crypto'); +var querystring = require('querystring'); + +var defaults = { + region:'ovh-eu', + basePath:'/1.0' +}; + +var recordsStore = {}; + +module.exports.create = function(config) { + var applicationKey = config.applicationKey||null; + var applicationSecret = config.applicationSecret||null; + var consumerKey = config.consumerKey||null; + // one of ovh-eu, ovh-ca, kimsufi-eu, kimsufi-ca, soyoustart-eu, soyoustart-ca, runabove-ca + var region = config.region || defaults.region; + var apiTimeDiff = config.apiTimeDiff || null; + var basePath = config.basePath || defaults.basePath; + + if (typeof(applicationKey) !== 'string' || typeof(applicationSecret) !== 'string') { + throw new Error('[OVH] You should precise an application key / secret'); + } + + var endpoints = { + 'ovh-eu': 'eu.api.ovh.com', + 'ovh-ca': 'ca.api.ovh.com', + 'kimsufi-eu': 'eu.api.kimsufi.com', + 'kimsufi-ca': 'ca.api.kimsufi.com', + 'soyoustart-eu': 'eu.api.soyoustart.com', + 'soyoustart-ca': 'ca.api.soyoustart.com', + 'runabove-ca': 'api.runabove.com' + }; + + var baseUrl = 'https://' + endpoints[region] + basePath; + + /** + * Signs an API request + * + * @param {String} httpMethod + * @param {String} url + * @param {String} body + * @param {Number|String} timestamp + * @return {String} The signature + */ + function signRequest(httpMethod, url, body, timestamp) { + var s = [ + applicationSecret, + consumerKey, + httpMethod, + url, + body || '', + timestamp + ]; + return '$1$' + crypto.createHash('sha1').update(s.join('+')).digest('hex'); + } + + function api(httpMethod, path, params) { + // Time drift + if (apiTimeDiff === null && path !== '/auth/time') { + return api('GET', '/auth/time', {}) + .then(function(res) { + + apiTimeDiff = res.body - Math.round(Date.now() / 1000); + return api(httpMethod, path, params); + }).catch(function(err) { + // todo + throw new Error('[OVH] Unable to fetch OVH API time'); + }); + } + + var headers = { + 'X-Ovh-Application': applicationKey + }; + + if (httpMethod === 'GET') { + path += '?' + querystring.stringify(params); + } + + var url = baseUrl + path; + + if (path.indexOf('/auth') < 0) { + headers['X-Ovh-Timestamp'] = Math.round(Date.now() / 1000) + apiTimeDiff; + // Sign request + if (typeof(consumerKey) === 'string') { + headers['X-Ovh-Consumer'] = consumerKey; + headers['X-Ovh-Signature'] = signRequest( + httpMethod, url, params, headers['X-Ovh-Timestamp'] + ); + } + } + + console.debug( + '[OVH] API call:', + httpMethod, + path, + params || '' + ); + + return request({ + method: httpMethod, + url: url, + headers: headers, + json: true, + form: params + }); + } + + return { + init: function(deps) { + request = deps.request; + return null; + }, + zones: function(data) { + return api('GET', '/domain/zone') + .then(function(resp) { + if (200 !== resp.statusCode) { + console.error(resp.statusCode); + console.error(resp.body); + throw new Error('Could not get list of zones.'); + } + + // list of zones + return resp.body; + + }).catch(function(err) { + + }); + }, + set: function(data) { + var ch = data.challenge; + var txt = ch.dnsAuthorization; + + // record name received as argument : www.qwerty.sampledomain.com + // received zone id : sampledomain.com required record name by OVH : www.qwerty + //console.debug('adding txt', data); + return api('POST', '/domain/zone/' + ch.dnsZone + '/record', { + fieldType: 'TXT', + subDomain: ch.dnsPrefix, + target: txt, + ttl: 1 + }).then(function(resp) { + if (200 !== resp.statusCode) { + console.error(resp.statusCode); + console.error(resp.body); + throw new Error('record did not set.'); + } + // save id for remove + recordsStore[ch.dnsPrefix] = resp.body['id'] + + }).then(function() { + // Apply zone modification on DNS servers + return api('POST', '/domain/zone/' + ch.dnsZone + '/record', {}) + .then(function() { + return true; + }); + }); + }, + remove: function(data) { + var ch = data.challenge; + return api('DELETE', '/domain/zone/' + ch.dnsZone + '/record/'+recordsStore[ch.dnsPrefix]) + .then(function(resp) { + if (200 !== resp.statusCode) { + throw new Error('record did not remove.'); + } + }) + .then(function() { + return api('POST', '/domain/zone/' + ch.dnsZone + '/record', {}) + .then(function(resp) { + if (200 !== resp.statusCode) { + console.error(resp.statusCode); + console.error(resp.body); + throw new Error('record did not remove.'); + } + return true; + }); + }); + }, + get: function(data) { + var ch = data.challenge; + + return api('GET', '/domain/zone/' + ch.dnsZone + '/record/'+recordsStore[ch.dnsPrefix]) + .then(function(resp) { + if (200 !== resp.statusCode) { + throw new Error('record did not remove.'); + } + return { + dnsAuthorization:resp.body.target + }; + + }); + } + }; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1bc7940 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "acme-dns-01-ovh", + "version": "3.0.2", + "description": "OVH DNS for Let's Encrypt / ACME dns-01 challenges with ACME.js and Greenlock.js", + "main": "index.js", + "scripts": { + "test": "node ./test.js" + }, + "repository": { + "type": "git", + "url": "https://git.rootprojects.org/root/acme-dns-01-ovh.js.git" + }, + "keywords": [ + "OVH", + "dns", + "dns-01", + "letsencrypt", + "acme", + "greenlock" + ], + "author": "Nyaundi Brian (https://danleyb2.online/)", + "license": "MPL-2.0", + "dependencies": { + "@root/request": "^1.4.2" + }, + "devDependencies": { + "acme-dns-01-test": "^3.2.1", + "dotenv": "^8.0.0" + } +} diff --git a/test.js b/test.js new file mode 100755 index 0000000..6af5b62 --- /dev/null +++ b/test.js @@ -0,0 +1,27 @@ +#!/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 challenger = require('./index.js').create({ + applicationKey: process.argv[3] || process.env.APPLICATION_KEY, + applicationSecret: process.argv[4] || process.env.APPLICATION_SECRET, + consumerKey: process.argv[5] || process.env.CONSUMER_KEY, + region: process.env.REGION +}); + +// The dry-run tests can pass on, literally, 'example.com' +// but the integration tests require that you have control over the domain +tester + .testZone('dns-01', zone, challenger) + .then(function() { + console.info('PASS', zone); + }) + .catch(function(e) { + console.error(e.message); + console.error(e.stack); + });