@@ -13,7 +13,6 @@ const { encrypt, decrypt } = require('./encrypt');
13
13
const { SettingsHandler } = require ( './settings-handler' ) ;
14
14
const { Resolver } = require ( 'dns' ) . promises ;
15
15
const resolver = new Resolver ( ) ;
16
- const punycode = require ( 'punycode.js' ) ;
17
16
const { getCertificate } = require ( './acme/certs' ) ;
18
17
19
18
const { promisify } = require ( 'util' ) ;
@@ -22,8 +21,6 @@ const generateKeyPair = promisify(crypto.generateKeyPair);
22
21
const CERT_RENEW_TTL = 30 * 24 * 3600 * 1000 ;
23
22
const CERT_RENEW_DELAY = 24 * 3600 * 100 ;
24
23
25
- const CAA_DOMAIN = 'letsencrypt.org' ;
26
-
27
24
class CertHandler {
28
25
constructor ( options ) {
29
26
options = options || { } ;
@@ -35,6 +32,8 @@ class CertHandler {
35
32
this . database = options . database ;
36
33
this . redis = options . redis ;
37
34
35
+ this . users = options . users ;
36
+
38
37
this . acmeConfig = options . acmeConfig ;
39
38
40
39
this . ctxCache = new Map ( ) ;
@@ -450,6 +449,7 @@ class CertHandler {
450
449
description : certData . description ,
451
450
fingerprint : certData . fingerprint || certData . fp ,
452
451
expires : certData . expires ,
452
+ autogenerated : certData . autogenerated ,
453
453
altNames : certData . altNames ,
454
454
acme : ! ! certData . acme ,
455
455
hasCert : ( ! ! certData . privateKey && certData . cert ) || false ,
@@ -632,35 +632,50 @@ class CertHandler {
632
632
return context ;
633
633
}
634
634
635
- normalizeDomain ( domain ) {
636
- domain = ( domain || '' ) . toString ( ) . toLowerCase ( ) . trim ( ) ;
637
- try {
638
- if ( / [ \x80 - \uFFFF ] / . test ( domain ) ) {
639
- domain = punycode . toASCII ( domain ) ;
640
- }
641
- } catch ( E ) {
642
- // ignore
643
- }
644
-
645
- return domain ;
646
- }
647
-
648
635
async precheckAcmeCertificate ( domain ) {
649
- let typePrefix = domain . split ( '.' ) . shift ( ) . toLowerCase ( ) . trim ( ) ;
650
-
651
- let subdomainTargets = ( ( await this . settingsHandler . get ( 'const:acme:subdomains' ) ) || '' )
652
- . toString ( )
653
- . split ( ',' )
654
- . map ( entry => entry . trim ( ) )
655
- . filter ( entry => entry ) ;
636
+ const dotPos = domain . indexOf ( '.' ) ;
637
+ if ( dotPos < 0 ) {
638
+ // not a FQDN
639
+ return false ;
640
+ }
641
+ const subdomain = domain . substring ( 0 , dotPos ) . toLowerCase ( ) . trim ( ) ;
642
+ const maindomain = domain
643
+ . substring ( dotPos + 1 )
644
+ . toLowerCase ( )
645
+ . trim ( ) ;
656
646
657
- if ( ! subdomainTargets . includes ( typePrefix ) ) {
647
+ let subdomainTargets = [ ] . concat ( this . acmeConfig . autogenerate ?. cnameMapping ?. [ subdomain ] || [ ] ) ;
648
+ if ( ! subdomainTargets . length ) {
658
649
// unsupported subdomain
659
650
log . verbose ( 'Certs' , 'Skip ACME. reason="unsupported subdomain" action=precheck domain=%s' , domain ) ;
660
651
return false ;
661
652
}
662
653
654
+ // CNAME check
655
+ let resolved ;
656
+ try {
657
+ resolved = await resolver . resolveCname ( domain ) ;
658
+ } catch ( err ) {
659
+ log . error ( 'Certs' , 'DNS CNAME query failed. action=precheck domain=%s error=%s' , domain , err . message ) ;
660
+ return false ;
661
+ }
662
+
663
+ if ( ! resolved || ! resolved . length ) {
664
+ log . verbose ( 'Certs' , 'Skip ACME. reason="empty CNAME result" action=precheck domain=%s' , domain ) ;
665
+ return false ;
666
+ }
667
+
668
+ for ( let row of resolved ) {
669
+ if ( ! subdomainTargets . includes ( row ) ) {
670
+ log . verbose ( 'Certs' , 'Skip ACME. reason="unknown CNAME target" action=precheck domain=%s target=%s' , domain , row ) ;
671
+ return false ;
672
+ }
673
+ }
674
+
663
675
// CAA check
676
+
677
+ const caaDomains = this . acmeConfig . caaDomains ?. map ( domain => tools . normalizeDomain ( domain ) ) . filter ( d => d ) ;
678
+
664
679
let parts = domain . split ( '.' ) ;
665
680
for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
666
681
let subdomain = parts . slice ( i ) . join ( '.' ) ;
@@ -671,59 +686,53 @@ class CertHandler {
671
686
// assume not found
672
687
}
673
688
674
- if ( caaRes ?. length && ! caaRes . some ( r => ( r ?. issue || '' ) . trim ( ) . toLowerCase ( ) === CAA_DOMAIN ) ) {
689
+ if ( caaRes ?. length && ! caaRes . some ( r => caaDomains . includes ( tools . normalizeDomain ( r ?. issue ) ) ) ) {
675
690
log . verbose ( 'Certs' , 'Skip ACME. reason="LE not listed in the CAA record". action=precheck domain=%s subdomain=%s' , domain , subdomain ) ;
676
691
return false ;
677
692
} else if ( caaRes ?. length ) {
678
- log . verbose ( 'Certs' , 'CAA record found. action=precheck domain=%s subdomain=%s' , domain , subdomain ) ;
693
+ log . verbose ( 'Certs' , 'CAA record found. action=precheck domain=%s subdomain=%s caa=%s ' , domain , subdomain , caaRes . join ( ',' ) ) ;
679
694
break ;
680
695
}
681
696
}
682
697
683
- // check if the domain points to correct cname
684
- let cnameTargets = ( ( await this . settingsHandler . get ( 'const:acme:cname' ) ) || '' )
685
- . toString ( )
686
- . split ( ',' )
687
- . map ( entry => entry . trim ( ) )
688
- . filter ( entry => entry ) ;
689
-
690
- if ( ! cnameTargets ) {
691
- log . verbose ( 'Certs' , 'Skip ACME. reason="no cname targets" action=precheck domain=%s' , domain ) ;
692
- return false ;
693
- }
694
-
695
- let resolved ;
696
- try {
697
- resolved = await resolver . resolveCname ( domain ) ;
698
- } catch ( err ) {
699
- log . error ( 'Certs' , 'DNS CNAME query failed. action=precheck domain=%s error=%s' , domain , err . message ) ;
700
- return false ;
701
- }
698
+ // Address check
699
+ const addressMatchRegex = tools . escapeRegexStr ( `@${ maindomain } ` ) ;
700
+ const addressData = await this . users . collection ( 'addresses' ) . findOne ( {
701
+ addrview : {
702
+ $regex : `${ addressMatchRegex } $`
703
+ }
704
+ } ) ;
702
705
703
- if ( ! resolved || ! resolved . length ) {
704
- log . verbose ( 'Certs' , 'Skip ACME. reason="empty CNAME result" action=precheck domain=%s' , domain ) ;
706
+ if ( ! addressData ) {
707
+ log . verbose ( 'Certs' , 'Skip ACME. reason="No addresses found for the domain". action=precheck domain=%s subdomain=%s ' , domain , subdomain ) ;
705
708
return false ;
706
709
}
707
710
708
- for ( let row of resolved ) {
709
- if ( ! cnameTargets . includes ( row ) ) {
710
- log . verbose ( 'Certs' , 'Skip ACME. reason="unknown CNAME target" action=precheck domain=%s target=%s' , domain , row ) ;
711
- return false ;
712
- }
713
- }
714
-
715
711
return true ;
716
712
}
717
713
718
714
async autogenerateAcmeCertificate ( servername ) {
719
- let domain = this . normalizeDomain ( servername ) ;
715
+ let domain = tools . normalizeDomain ( servername ) ;
716
+
717
+ if ( ! this . acmeConfig . autogenerate ?. enabled ) {
718
+ // can not create autogenerated TLS certificates
719
+ log . verbose ( 'Certs' , 'Skip ACME. reason="Certificate autogeneration not enabled" action=precheck domain=%s' , domain ) ;
720
+ return false ;
721
+ }
722
+
720
723
let valid = await this . precheckAcmeCertificate ( domain ) ;
721
724
if ( ! valid ) {
722
725
return false ;
723
726
}
724
727
725
728
log . verbose ( 'Certs' , 'ACME precheck passed. action=precheck domain=%s' , domain ) ;
726
729
730
+ this . loggelf ( {
731
+ short_message : ` Autogenerating TLS certificate for ${ domain } ` ,
732
+ _sni_servername : domain ,
733
+ _cert_action : 'sni_autogenerate'
734
+ } ) ;
735
+
727
736
// add row to db
728
737
let certInsertResult = await this . set ( {
729
738
servername,
0 commit comments