Initial Commit
This commit is contained in:
		
						commit
						c4ac31eccd
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | *secret* | ||||||
|  | node_modules | ||||||
							
								
								
									
										8
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  |   "bracketSpacing": true, | ||||||
|  |   "printWidth": 80, | ||||||
|  |   "singleQuote": true, | ||||||
|  |   "tabWidth": 2, | ||||||
|  |   "trailingComma": "none", | ||||||
|  |   "useTabs": true | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | Maciej Krüger <mkg20001@gmail.com> (https://mkg20001.io/) | ||||||
							
								
								
									
										77
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | |||||||
|  | # acme-dns-01-cloudflare | ||||||
|  | 
 | ||||||
|  | Cloudflare 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-cloudflare@3.x | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | # Usage | ||||||
|  | 
 | ||||||
|  | First you create an instance with your credentials: | ||||||
|  | 
 | ||||||
|  | ```js | ||||||
|  | var dns01 = require('acme-dns-01-cloudflare').create({ | ||||||
|  | 	authKey: '123yourkey', | ||||||
|  | 	authEmail: 'you@example.com' | ||||||
|  | }); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 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 auth-key auth-email | ||||||
|  | node ./test.js example.com xxxxxx you@example.com | ||||||
|  | ``` | ||||||
							
								
								
									
										3
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | 'use strict' | ||||||
|  | 
 | ||||||
|  | module.exports = require('./lib/index.js') | ||||||
							
								
								
									
										146
									
								
								lib/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								lib/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | |||||||
|  | 'use strict' | ||||||
|  | 
 | ||||||
|  | let request = require('@root/request') | ||||||
|  | request = require('util').promisify(request) | ||||||
|  | 
 | ||||||
|  | const Joi = require('@hapi/joi') | ||||||
|  | 
 | ||||||
|  | const schema = Joi.object().keys({ | ||||||
|  |   authKey: Joi.string().alphanum().required(), | ||||||
|  |   authEmail: Joi.string().email({ minDomainSegments: 2 }).required(), | ||||||
|  |   baseUrl: Joi.string() | ||||||
|  | }).with('username', 'birthyear').without('password', 'access_token') | ||||||
|  | 
 | ||||||
|  | function formatError (msg, resp) { | ||||||
|  |   const e = new Error(`${resp.statusCode}: ${msg}! (Check the Credentials)`) | ||||||
|  |   e.body = resp.body | ||||||
|  |   if (resp.body && resp.body.errors) { | ||||||
|  |     e.errors = resp.body.errors | ||||||
|  |     console.log(e.errors[0].error_chain) | ||||||
|  |   } | ||||||
|  |   e.statusCode = resp.statusCode | ||||||
|  |   throw e | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var defaults = { | ||||||
|  |   baseUrl: 'https://api.cloudflare.com/client/v4/' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports.create = function (config) { | ||||||
|  |   const baseUrl = (config.baseUrl || defaults.baseUrl).replace(/\/$/, '') | ||||||
|  |   Joi.validate(config, schema) | ||||||
|  | 
 | ||||||
|  |   function api (method, path, body) { | ||||||
|  |     return request({ | ||||||
|  |       method: method, | ||||||
|  |       url: baseUrl + path, | ||||||
|  |       headers: { | ||||||
|  |         'Content-Type': 'application/json', | ||||||
|  |         'X-Auth-Key': config.authKey, | ||||||
|  |         'X-Auth-Email': config.authEmail | ||||||
|  |       }, | ||||||
|  |       json: true, | ||||||
|  |       body | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function zones (domain) { | ||||||
|  |     const resp = await api('GET', '/zones?per_page=1000' + (domain ? '&name=' + domain : '')) // TODO: use proper pagination?!
 | ||||||
|  |     if (resp.statusCode !== 200) { | ||||||
|  |       formatError('Could not get list of Zones', resp) | ||||||
|  |     } | ||||||
|  |     return resp | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function getZone (domain) { | ||||||
|  |     const res = await zones(domain) | ||||||
|  |     return res.body.result.filter(zone => (zone.name === domain))[0] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     zones: async function (data) { | ||||||
|  |       const resp = await zones() | ||||||
|  |       return resp.body.result.map(x => x.name) | ||||||
|  |     }, | ||||||
|  |     set: async function (data) { | ||||||
|  |       const { | ||||||
|  |         challenge: { | ||||||
|  |           dnsZone: domain, | ||||||
|  |           dnsAuthorization: txtRecord, | ||||||
|  |           dnsPrefix | ||||||
|  |         } | ||||||
|  |       } = data | ||||||
|  | 
 | ||||||
|  |       const zone = await getZone(domain) | ||||||
|  |       if (zone.permissions.indexOf('#zone:edit') === -1) { | ||||||
|  |         throw new Error('Can not edit zone ' + JSON.stringify(domain) + ' from this account') | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const resp = await api('POST', `/zones/${zone.id}/dns_records`, {type: 'TXT', name: dnsPrefix, content: JSON.stringify(txtRecord), ttl: 300}) | ||||||
|  |       if (resp.statusCode !== 200) { | ||||||
|  |         formatError('Could not add record', resp) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return true | ||||||
|  |     }, | ||||||
|  |     remove: async function (data) { | ||||||
|  |       const { | ||||||
|  |         challenge: { | ||||||
|  |           dnsZone: domain, | ||||||
|  |           dnsPrefix | ||||||
|  |         } | ||||||
|  |       } = data | ||||||
|  | 
 | ||||||
|  |       const zone = await getZone(domain) | ||||||
|  |       if (zone.permissions.indexOf('#zone:edit') === -1) { | ||||||
|  |         throw new Error('Can not edit zone ' + JSON.stringify(domain) + ' from this account') | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const resp = await api('GET', `/zones/${zone.id}/dns_records?name=${encodeURI(dnsPrefix + '.' + domain)}`) | ||||||
|  |       if (resp.statusCode !== 200) { | ||||||
|  |         formatError('Could not read record', resp) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       let {result} = resp.body | ||||||
|  | 
 | ||||||
|  |       let record = result.filter(record => (record.type === 'TXT'))[0] | ||||||
|  | 
 | ||||||
|  |       if (record) { | ||||||
|  |         const resp = await api('DELETE', `/zones/${zone.id}/dns_records/${record.id}`) | ||||||
|  |         if (resp.statusCode !== 200) { | ||||||
|  |           formatError('Could not delete record', resp) | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         return null // TODO: not found. should this throw?!
 | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     get: async function (data) { | ||||||
|  |       const { | ||||||
|  |         challenge: { | ||||||
|  |           dnsZone: domain, | ||||||
|  |           dnsPrefix | ||||||
|  |         } | ||||||
|  |       } = data | ||||||
|  | 
 | ||||||
|  |       const zone = await getZone(domain) | ||||||
|  |       if (zone.permissions.indexOf('#zone:read') === -1) { | ||||||
|  |         throw new Error('Can not read zone ' + JSON.stringify(domain) + ' from this account') | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const resp = await api('GET', `/zones/${zone.id}/dns_records?name=${encodeURI(dnsPrefix + '.' + domain)}`) | ||||||
|  |       if (resp.statusCode !== 200) { | ||||||
|  |         formatError('Could not read record', resp) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       let {result} = resp.body | ||||||
|  | 
 | ||||||
|  |       let record = result.filter(record => (record.type === 'TXT'))[0] | ||||||
|  | 
 | ||||||
|  |       if (record) { | ||||||
|  |         return {dnsAuthorization: JSON.parse(record.content)} | ||||||
|  |       } else { | ||||||
|  |         return null // TODO: not found. should this throw?!
 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | { | ||||||
|  | 	"name": "acme-dns-01-cloudflare", | ||||||
|  | 	"version": "0.1.0", | ||||||
|  | 	"lockfileVersion": 1, | ||||||
|  | 	"requires": true, | ||||||
|  | 	"dependencies": { | ||||||
|  | 		"@hapi/address": { | ||||||
|  | 			"version": "2.0.0", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", | ||||||
|  | 			"integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==" | ||||||
|  | 		}, | ||||||
|  | 		"@hapi/hoek": { | ||||||
|  | 			"version": "6.2.4", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz", | ||||||
|  | 			"integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A==" | ||||||
|  | 		}, | ||||||
|  | 		"@hapi/joi": { | ||||||
|  | 			"version": "15.1.0", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.0.tgz", | ||||||
|  | 			"integrity": "sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==", | ||||||
|  | 			"requires": { | ||||||
|  | 				"@hapi/address": "2.x.x", | ||||||
|  | 				"@hapi/hoek": "6.x.x", | ||||||
|  | 				"@hapi/marker": "1.x.x", | ||||||
|  | 				"@hapi/topo": "3.x.x" | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		"@hapi/marker": { | ||||||
|  | 			"version": "1.0.0", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/@hapi/marker/-/marker-1.0.0.tgz", | ||||||
|  | 			"integrity": "sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA==" | ||||||
|  | 		}, | ||||||
|  | 		"@hapi/topo": { | ||||||
|  | 			"version": "3.1.0", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.0.tgz", | ||||||
|  | 			"integrity": "sha512-gZDI/eXOIk8kP2PkUKjWu9RW8GGVd2Hkgjxyr/S7Z+JF+0mr7bAlbw+DkTRxnD580o8Kqxlnba9wvqp5aOHBww==", | ||||||
|  | 			"requires": { | ||||||
|  | 				"@hapi/hoek": "6.x.x" | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		"@root/request": { | ||||||
|  | 			"version": "1.3.11", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz", | ||||||
|  | 			"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw==" | ||||||
|  | 		}, | ||||||
|  | 		"acme-challenge-test": { | ||||||
|  | 			"version": "3.2.1", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/acme-challenge-test/-/acme-challenge-test-3.2.1.tgz", | ||||||
|  | 			"integrity": "sha512-8MwL2oWx7vM/SBIeEQfeoRyW0kYCtLFS4FfgIx3lsQmSKhbDo9J88Ud6DejdupRp2T+DlEkWIBVI3qOCVViUaQ==", | ||||||
|  | 			"dev": true | ||||||
|  | 		}, | ||||||
|  | 		"acme-dns-01-test": { | ||||||
|  | 			"version": "3.2.1", | ||||||
|  | 			"resolved": "https://registry.npmjs.org/acme-dns-01-test/-/acme-dns-01-test-3.2.1.tgz", | ||||||
|  | 			"integrity": "sha512-m4UxltZzTNbQTK30iQQ6BQ99oRJA9p69+eg/u/Plxiw10bD+qsmRZR9rsqZuiSc62wfng/upGvXWMQZ/csn3lA==", | ||||||
|  | 			"dev": true, | ||||||
|  | 			"requires": { | ||||||
|  | 				"acme-challenge-test": "^3.2.0" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | { | ||||||
|  | 	"name": "acme-dns-01-cloudflare", | ||||||
|  | 	"version": "0.1.0", | ||||||
|  | 	"description": "Cloudflare 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.coolaj86.com/mkg20001/acme-dns-01-cloudflare.git" | ||||||
|  | 	}, | ||||||
|  | 	"keywords": [ | ||||||
|  | 		"cloudflare", | ||||||
|  | 		"dns", | ||||||
|  | 		"dns-01", | ||||||
|  | 		"letsencrypt", | ||||||
|  | 		"acme", | ||||||
|  | 		"greenlock" | ||||||
|  | 	], | ||||||
|  | 	"author": "Maciej Krüger <mkg20001@gmail.com> (https://mkg20001.io/)", | ||||||
|  | 	"license": "MPL-2.0", | ||||||
|  | 	"dependencies": { | ||||||
|  | 		"@hapi/joi": "^15.1.0", | ||||||
|  | 		"@root/request": "^1.3.11" | ||||||
|  | 	}, | ||||||
|  | 	"devDependencies": { | ||||||
|  | 		"acme-dns-01-test": "^3.2.1" | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | #!/usr/bin/env node
 | ||||||
|  | 'use strict' | ||||||
|  | 
 | ||||||
|  | // https://git.rootprojects.org/root/acme-dns-01-test.js
 | ||||||
|  | var tester = require('acme-dns-01-test') | ||||||
|  | 
 | ||||||
|  | // Usage: node ./test.js example.com xxxxxxxxx
 | ||||||
|  | let [zone, authKey, authEmail] = process.argv.slice(2) | ||||||
|  | var challenger = require('./index.js').create({ authKey, authEmail }) | ||||||
|  | 
 | ||||||
|  | // 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.info('FAIL', zone) | ||||||
|  |     console.error(e.message) | ||||||
|  |     console.error(e.stack) | ||||||
|  |   }) | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user