@ -37,7 +37,7 @@ function promiseCheckAndCatch(obj, name) {
) ;
}
function shouldntBeNull ( result ) {
function shouldntBeUndefined ( result ) {
if ( 'undefined' === typeof result ) {
throw new Error (
"'challenge.'" +
@ -50,7 +50,7 @@ function promiseCheckAndCatch(obj, name) {
}
return function ( opts ) {
return promiser ( opts ) . then ( shouldntBeNull ) ;
return promiser ( opts ) . then ( shouldntBeUndefined ) ;
} ;
}
@ -70,145 +70,170 @@ function mapAsync(els, doer) {
return next ( ) ;
}
function newZoneRegExp ( zonename ) {
// (^|\.)example\.com$
// which matches:
// foo.example.com
// example.com
// but not:
// fooexample.com
return new RegExp ( '(^|\\.)' + zonename . replace ( /\./g , '\\.' ) + '$' ) ;
}
function pluckZone ( zones , dnsHost ) {
return zones
. filter ( function ( zonename ) {
// the only character that needs to be escaped for regex
// and is allowed in a domain name is '.'
return newZoneRegExp ( zonename ) . test ( dnsHost ) ;
} )
. sort ( function ( a , b ) {
// longest match first
return b . length - a . length ;
} ) [ 0 ] ;
}
// Here's the meat, where the tests are happening:
function testEach ( type , domains , challenger ) {
var chr = wrapChallenger ( challenger ) ;
var chr = wrapChallenger ( type , challenger ) ;
var all = domains . map ( function ( d ) {
return { domain : d } ;
} ) ;
var rnd = crypto . randomBytes ( 2 ) . toString ( 'hex' ) ;
return mapAsync ( all , function ( opts ) {
console . info ( "TEST '%s'" , opts . domain ) ;
opts . challenge = fakeChallenge ( type , opts . domain , rnd ) ;
var ch = opts . challenge ;
if ( 'http-01' === ch . type && ch . wildname ) {
throw new Error ( 'http-01 cannot be used for wildcard domains' ) ;
}
// The first time we just check it against itself
// this will cause the prompt to appear
return chr . set ( opts ) . then ( function ( ) {
// this will cause the final completion message to appear
// _test is used by the manual cli reference implementations
var query = { type : ch . type , /*debug*/ status : ch . status , _ test : true } ;
if ( 'http-01' === ch . type ) {
query . identifier = ch . identifier ;
query . token = ch . token ;
// For testing only
query . url = ch . challengeUrl ;
} else if ( 'dns-01' === ch . type ) {
query . identifier = { type : 'dns' , value : ch . dnsHost } ;
// For testing only
query . altname = ch . altname ;
// there should only be two possible TXT records per challenge domain:
// one for the bare domain, and the other if and only if there's a wildcard
query . wildcard = ch . wildcard ;
query . dnsAuthorization = ch . dnsAuthorization ;
} else {
query = JSON . parse ( JSON . stringify ( ch ) ) ;
query . comment = 'unknown challenge type, supplying everything' ;
return chr . zones ( { dnsHosts : domains } ) . then ( function ( zones ) {
return mapAsync ( all , function ( opts ) {
console . info ( "TEST '%s'" , opts . domain ) ;
var zone = pluckZone ( zones , opts . domain ) ;
opts . challenge = fakeChallenge ( type , zone , opts . domain , rnd ) ;
var ch = opts . challenge ;
if ( 'http-01' === ch . type && ch . wildname ) {
throw new Error ( 'http-01 cannot be used for wildcard domains' ) ;
}
opts . query = query ;
return opts ;
} ) ;
} )
. then ( function ( all ) {
return mapAsync ( all , function ( opts ) {
var ch = opts . challenge ;
return chr . get ( { challenge : opts . query } ) . then ( function ( secret ) {
if ( 'string' === typeof secret ) {
console . info (
'secret was passed as a string, which works historically, but should be an object instead:'
) ;
console . info ( '{ "keyAuthorization": "' + secret + '" }' ) ;
console . info ( 'or' ) ;
// TODO this should be "keyAuthorizationDigest"
console . info ( '{ "dnsAuthorization": "' + secret + '" }' ) ;
console . info (
'This is to help keep acme / greenlock (and associated plugins) future-proof for new challenge types'
) ;
}
// historically 'secret' has been a string, but I'd like it to transition to be an object.
// to make it backwards compatible in v2.7 to change it,
// so I'm not sure that we really need to.
if ( 'http-01' === ch . type ) {
secret = secret . keyAuthorization || secret ;
if ( ch . keyAuthorization !== secret ) {
throw new Error (
"http-01 challenge.get() returned '" +
secret +
"', which does not match the keyAuthorization" +
" saved with challenge.set(), which was '" +
ch . keyAuthorization +
"'"
// The first time we just check it against itself
// this will cause the prompt to appear
return chr . set ( opts ) . then ( function ( ) {
// this will cause the final completion message to appear
// _test is used by the manual cli reference implementations
var query = { type : ch . type , /*debug*/ status : ch . status , _ test : true } ;
if ( 'http-01' === ch . type ) {
query . identifier = ch . identifier ;
query . token = ch . token ;
// For testing only
query . url = ch . challengeUrl ;
} else if ( 'dns-01' === ch . type ) {
query . identifier = { type : 'dns' , value : ch . dnsHost } ;
// For testing only
query . altname = ch . altname ;
// there should only be two possible TXT records per challenge domain:
// one for the bare domain, and the other if and only if there's a wildcard
query . wildcard = ch . wildcard ;
query . dnsAuthorization = ch . dnsAuthorization ;
} else {
query = JSON . parse ( JSON . stringify ( ch ) ) ;
query . comment = 'unknown challenge type, supplying everything' ;
}
opts . query = query ;
return opts ;
} ) ;
} )
. then ( function ( all ) {
return mapAsync ( all , function ( opts ) {
var ch = opts . challenge ;
return chr . get ( { challenge : opts . query } ) . then ( function ( secret ) {
if ( 'string' === typeof secret ) {
console . info (
'secret was passed as a string, which works historically, but should be an object instead:'
) ;
}
} else if ( 'dns-01' === ch . type ) {
secret = secret . dnsAuthorization || secret ;
if ( ch . dnsAuthorization !== secret ) {
throw new Error (
"dns-01 challenge.get() returned '" +
secret +
"', which does not match the dnsAuthorization" +
" (keyAuthDigest) saved with challenge.set(), which was '" +
ch . dnsAuthorization +
"'"
console . info ( '{ "keyAuthorization": "' + secret + '" }' ) ;
console . info ( 'or' ) ;
// TODO this should be "keyAuthorizationDigest"
console . info ( '{ "dnsAuthorization": "' + secret + '" }' ) ;
console . info (
'This is to help keep acme / greenlock (and associated plugins) future-proof for new challenge types'
) ;
}
} else {
if ( 'tls-alpn-01' === ch . type ) {
console . warn (
"'tls-alpn-01' support is in development" +
" (or developed and we haven't update this yet). Please contact us."
) ;
// historically 'secret' has been a string, but I'd like it to transition to be an object.
// to make it backwards compatible in v2.7 to change it,
// so I'm not sure that we really need to.
if ( 'http-01' === ch . type ) {
secret = secret . keyAuthorization || secret ;
if ( ch . keyAuthorization !== secret ) {
throw new Error (
"http-01 challenge.get() returned '" +
secret +
"', which does not match the keyAuthorization" +
" saved with challenge.set(), which was '" +
ch . keyAuthorization +
"'"
) ;
}
} else if ( 'dns-01' === ch . type ) {
secret = secret . dnsAuthorization || secret ;
if ( ch . dnsAuthorization !== secret ) {
throw new Error (
"dns-01 challenge.get() returned '" +
secret +
"', which does not match the dnsAuthorization" +
" (keyAuthDigest) saved with challenge.set(), which was '" +
ch . dnsAuthorization +
"'"
) ;
}
} else {
console . warn (
"We don't know how to test '" +
ch . type +
"'... are you sure that's a thing?"
) ;
if ( 'tls-alpn-01' === ch . type ) {
console . warn (
"'tls-alpn-01' support is in development" +
" (or developed and we haven't update this yet). Please contact us."
) ;
} else {
console . warn (
"We don't know how to test '" +
ch . type +
"'... are you sure that's a thing?"
) ;
}
secret = secret . keyAuthorization || secret ;
if ( ch . keyAuthorization !== secret ) {
console . warn (
"The returned value doesn't match keyAuthorization" ,
ch . keyAuthorization ,
secret
) ;
}
}
secret = secret . keyAuthorization || secret ;
if ( ch . keyAuthorization !== secret ) {
console . warn (
"The returned value doesn't match keyAuthorization" ,
ch . keyAuthorization ,
secret
) ;
}
}
} ) ;
} ) ;
} ) ;
} )
. then ( function ( ) {
return mapAsync ( all , function ( opts ) {
return chr . remove ( opts ) . then ( function ( ) {
return chr . get ( opts ) . then ( function ( result ) {
if ( result ) {
throw new Error (
'challenge.remove() should have made it not possible for challenge.get() to return a value'
) ;
}
if ( null !== result ) {
throw new Error (
'challenge.get() should return null when the value is not set'
) ;
}
console . info ( "PASS '%s'" , opts . domain ) ;
} )
. then ( function ( ) {
return mapAsync ( all , function ( opts ) {
return chr . remove ( opts ) . then ( function ( ) {
return chr . get ( opts ) . then ( function ( result ) {
if ( result ) {
throw new Error (
'challenge.remove() should have made it not possible for challenge.get() to return a value'
) ;
}
if ( null !== result ) {
throw new Error (
'challenge.get() should return null when the value is not set'
) ;
}
console . info ( "PASS '%s'" , opts . domain ) ;
} ) ;
} ) ;
} ) ;
} )
. then ( function ( ) {
console . info ( 'All soft tests: PASS' ) ;
console . warn (
'Hard tests (actually checking http URLs and dns records) is implemented in acme-v2.'
) ;
console . warn (
"We'll copy them over here as well, but that's a TODO for next week."
) ;
} ) ;
} )
. then ( function ( ) {
console . info ( 'All soft tests: PASS' ) ;
console . warn (
'Hard tests (actually checking http URLs and dns records) is implemented in acme-v2.'
) ;
console . warn (
"We'll copy them over here as well, but that's a TODO for next week."
) ;
} ) ;
} ) ;
}
function testZone ( type , zone , challenger ) {
@ -219,23 +244,47 @@ function testZone(type, zone, challenger) {
return testEach ( type , domains , challenger ) ;
}
function wrapChallenger ( challenger ) {
var set = promiseCheckAndCatch ( challenger , 'set' ) ;
function wrapChallenger ( type , challenger ) {
var zones ;
if ( 'dns-01' === type ) {
if ( 'function' !== typeof challenger . zones ) {
console . error (
'You must implement `zones` to return an array of strings.' +
" If you're testing a special type of service that doesn't support" +
' domain zone listing (as opposed to domain record listing),' +
' such as DuckDNS, return an empty array.'
) ;
process . exit ( 28 ) ;
return ;
}
zones = promiseCheckAndCatch ( challenger , 'zones' ) ;
} else {
zones = function ( ) {
return Promise . resolve ( [ ] ) ;
} ;
}
if ( 'function' !== typeof challenger . get ) {
throw new Error (
console . e rror(
"'challenge.get' should be implemented for the sake of testing." +
' It should be implemented as the internal method for fetching the challenge' +
' (i.e. reading from a database, file system or API, not return internal),' +
' not the external check (the http call, dns query, etc), which will already be done as part of this test.'
' not the external check (the http call, dns query, etc),' +
' which will already be done as part of this test.'
) ;
process . exit ( 29 ) ;
return ;
}
var get = promiseCheckAndCatch ( challenger , 'get' ) ;
var remove = promiseCheckAndCatch ( challenger , 'remove' ) ;
return { set : set , get : get , remove : remove } ;
return {
zones : zones ,
set : promiseCheckAndCatch ( challenger , 'set' ) ,
get : promiseCheckAndCatch ( challenger , 'get' ) ,
remove : promiseCheckAndCatch ( challenger , 'remove' )
} ;
}
function fakeChallenge ( type , altname , rnd ) {
function fakeChallenge ( type , zone , altname , rnd ) {
var expires = new Date ( Date . now ( ) + 10 * 60 * 1000 ) . toISOString ( ) ;
var token = crypto . randomBytes ( 8 ) . toString ( 'hex' ) ;
var thumb = crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
@ -258,11 +307,18 @@ function fakeChallenge(type, altname, rnd) {
thumbprint : thumb ,
keyAuthorization : keyAuth ,
url : null , // completed below
dnsZone : zone ,
dnsHost : '_' + rnd . slice ( 0 , 2 ) + '-acme-challenge-' + rnd . slice ( 2 ) + '.' , // completed below
dnsAuthorization : dnsAuth ,
altname : altname ,
_ test : true // used by CLI referenced implementations
} ;
if ( zone ) {
challenge . dnsZone = zone ;
challenge . dnsPrefix = challenge . dnsHost
. replace ( newZoneRegExp ( zone ) , '' )
. replace ( /\.$/ , '' ) ;
}
if ( '*.' === altname . slice ( 0 , 2 ) ) {
challenge . wildcard = true ;
altname = altname . slice ( 2 ) ;