Make LE work with hyphenated domains

This commit is contained in:
Girish Ramakrishnan
2018-10-31 15:41:02 -07:00
parent 00d032616f
commit c09aa2a498
5 changed files with 183 additions and 20 deletions

View File

@@ -21,7 +21,8 @@ exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme'
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
};
function Acme2Error(reason, errorOrMessage) {
@@ -435,11 +436,14 @@ function getChallengeSubdomain(hostname, domain) {
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
challengeSubdomain = hostname.replace('*', '_acme-challenge').slice(0, -domain.length - 1);
let subdomain = hostname.slice(0, -domain.length - 1);
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
return challengeSubdomain;
}
@@ -466,7 +470,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
domains.waitForDnsRecord(`${challengeSubdomain}`, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
callback(null, challenge);

View File

@@ -26,7 +26,10 @@ module.exports = exports = {
makeWildcard: makeWildcard,
DomainsError: DomainsError
DomainsError: DomainsError,
// exported for testing
_getName: getName
};
var assert = require('assert'),
@@ -347,19 +350,25 @@ function del(domain, callback) {
// returns the 'name' that needs to be inserted into zone
function getName(domain, subdomain, type) {
// support special caas domains
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
if (domain.provider === 'caas') return subdomain;
if (domain.domain === domain.zoneName) return subdomain;
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (subdomain === '') return part;
if (subdomain === '') {
return part;
} else if (type === 'TXT') {
return `${subdomain}.${part}`;
if (!domain.config.hyphenatedSubdomains) return part ? `${subdomain}.${part}` : subdomain;
// hyphenatedSubdomains
if (type !== 'TXT') return `${subdomain}-${part}`;
if (subdomain.startsWith('_acme-challenge.')) {
return `${subdomain}-${part}`;
} else if (subdomain === '_acme-challenge') {
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
return up ? `${subdomain}.${up}` : subdomain;
} else {
return subdomain + (domain.config.hyphenatedSubdomains ? '-' : '.') + part;
return `${subdomain}.${part}`;
}
}
@@ -432,7 +441,8 @@ function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
const hostname = fqdn(subdomain, domainObject);
const name = getName(domainObject, subdomain, type);
const hostname = `${name}.${domainObject.zoneName}`;
api(domainObject.provider).waitForDns(hostname, domainObject.zoneName, type, value, options, callback);
});

View File

@@ -34,7 +34,7 @@ var acme2 = require('./cert/acme2.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:certificates'),
debug = require('debug')('box:reverseproxy'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),

44
src/test/acme2-test.js Normal file
View File

@@ -0,0 +1,44 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../config.js'),
database = require('../database.js'),
acme2 = require('../cert/acme2.js'),
expect = require('expect.js'),
_ = require('underscore');
describe('Acme2', function () {
before(function (done) {
config._reset();
async.series([
database.initialize,
database._clear
], done);
});
after(function (done) {
async.series([
database._clear,
database.uninitialize
], done);
});
describe('getChallengeSubdomain', function () {
it('non-wildcard', function () {
expect(acme2._getChallengeSubdomain('example.com', 'example.com')).to.be('_acme-challenge');
expect(acme2._getChallengeSubdomain('git.example.com', 'example.com')).to.be('_acme-challenge.git');
});
it('wildcard', function () {
expect(acme2._getChallengeSubdomain('*.example.com', 'example.com')).to.be('_acme-challenge');
expect(acme2._getChallengeSubdomain('*.git.example.com', 'example.com')).to.be('_acme-challenge.git');
expect(acme2._getChallengeSubdomain('*.example.com', 'customer.example.com')).to.be('_acme-challenge'); // for hyphenatedSubdomains
});
});
});

View File

@@ -29,13 +29,13 @@ describe('Domains', function () {
], done);
});
const domain = {
domain: 'example.com',
zoneName: 'example.com',
config: {}
};
describe('validateHostname', function () {
const domain = {
domain: 'example.com',
zoneName: 'example.com',
config: {}
};
it('does not allow admin subdomain', function () {
config.setFqdn('example.com');
config.setAdminFqdn('my.example.com');
@@ -85,4 +85,109 @@ describe('Domains', function () {
expect(domains.validateHostname('a0.x', domain)).to.be.an(Error);
});
});
describe('getName', function () {
it('works with zoneName==domain (not hyphenated)', function () {
const domain = {
domain: 'example.com',
zoneName: 'example.com',
config: {}
};
expect(domains._getName(domain, '', 'A')).to.be('');
expect(domains._getName(domain, 'www', 'A')).to.be('www');
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev');
expect(domains._getName(domain, '', 'MX')).to.be('');
expect(domains._getName(domain, '', 'TXT')).to.be('');
expect(domains._getName(domain, 'www', 'TXT')).to.be('www');
expect(domains._getName(domain, 'www.dev', 'TXT')).to.be('www.dev');
});
it('works when zoneName!=domain (not hyphenated)', function () {
const domain = {
domain: 'dev.example.com',
zoneName: 'example.com',
config: {}
};
expect(domains._getName(domain, '', 'A')).to.be('dev');
expect(domains._getName(domain, 'www', 'A')).to.be('www.dev');
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev.dev');
expect(domains._getName(domain, '', 'MX')).to.be('dev');
expect(domains._getName(domain, '', 'TXT')).to.be('dev');
expect(domains._getName(domain, 'www', 'TXT')).to.be('www.dev');
expect(domains._getName(domain, 'www.dev', 'TXT')).to.be('www.dev.dev');
});
it('works when hyphenated - level1', function () {
const domain = {
domain: 'customer.example.com',
zoneName: 'example.com',
config: {
hyphenatedSubdomains: true
}
};
expect(domains._getName(domain, '', 'A')).to.be('customer');
expect(domains._getName(domain, 'www', 'A')).to.be('www-customer');
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev-customer');
expect(domains._getName(domain, '', 'MX')).to.be('customer');
expect(domains._getName(domain, '', 'TXT')).to.be('customer');
expect(domains._getName(domain, '_dmarc', 'TXT')).to.be('_dmarc.customer');
expect(domains._getName(domain, 'cloudron._domainkey', 'TXT')).to.be('cloudron._domainkey.customer');
expect(domains._getName(domain, '_acme-challenge.my', 'TXT')).to.be('_acme-challenge.my-customer');
expect(domains._getName(domain, '_acme-challenge', 'TXT')).to.be('_acme-challenge');
});
it('works when hyphenated - level2', function () {
const domain = {
domain: 'customer.dev.example.com',
zoneName: 'example.com',
config: {
hyphenatedSubdomains: true
}
};
expect(domains._getName(domain, '', 'A')).to.be('customer.dev');
expect(domains._getName(domain, 'www', 'A')).to.be('www-customer.dev');
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev-customer.dev');
expect(domains._getName(domain, '', 'MX')).to.be('customer.dev');
expect(domains._getName(domain, '', 'TXT')).to.be('customer.dev');
expect(domains._getName(domain, '_dmarc', 'TXT')).to.be('_dmarc.customer.dev');
expect(domains._getName(domain, 'cloudron._domainkey', 'TXT')).to.be('cloudron._domainkey.customer.dev');
expect(domains._getName(domain, '_acme-challenge.my', 'TXT')).to.be('_acme-challenge.my-customer.dev');
expect(domains._getName(domain, '_acme-challenge', 'TXT')).to.be('_acme-challenge.dev');
});
it('works with caas', function () {
const domain = {
domain: 'customer.example.com',
provider: 'caas',
zoneName: 'example.com',
config: {
hyphenatedSubdomains: true
}
};
expect(domains._getName(domain, '', 'A')).to.be('');
expect(domains._getName(domain, 'www', 'A')).to.be('www');
expect(domains._getName(domain, 'www.dev', 'A')).to.be('www.dev');
expect(domains._getName(domain, '', 'MX')).to.be('');
expect(domains._getName(domain, '', 'TXT')).to.be('');
expect(domains._getName(domain, '_dmarc', 'TXT')).to.be('_dmarc');
expect(domains._getName(domain, 'cloudron._domainkey', 'TXT')).to.be('cloudron._domainkey');
expect(domains._getName(domain, '_acme-challenge.my', 'TXT')).to.be('_acme-challenge.my');
expect(domains._getName(domain, '_acme-challenge', 'TXT')).to.be('_acme-challenge');
});
});
});