2017-10-28 22:18:07 +02:00
'use strict' ;
module . exports = exports = {
2020-12-23 15:34:23 -08:00
add ,
get ,
2021-08-13 17:22:28 -07:00
list ,
2020-12-23 15:34:23 -08:00
update ,
del ,
clear ,
2017-10-28 22:18:07 +02:00
2020-12-23 15:34:23 -08:00
removePrivateFields ,
removeRestrictedFields ,
2017-10-28 22:18:07 +02:00
} ;
2021-08-13 17:22:28 -07:00
const assert = require ( 'assert' ) ,
2019-10-23 10:02:04 -07:00
BoxError = require ( './boxerror.js' ) ,
2021-02-24 11:40:23 -08:00
crypto = require ( 'crypto' ) ,
2021-08-13 17:22:28 -07:00
database = require ( './database.js' ) ,
2018-11-10 00:43:46 -08:00
eventlog = require ( './eventlog.js' ) ,
2020-03-31 12:04:46 -07:00
mail = require ( './mail.js' ) ,
2018-01-30 12:23:27 -08:00
reverseProxy = require ( './reverseproxy.js' ) ,
2021-08-13 17:22:28 -07:00
safe = require ( 'safetydance' ) ,
2019-07-26 10:49:29 -07:00
settings = require ( './settings.js' ) ,
2017-10-28 23:23:58 +02:00
tld = require ( 'tldjs' ) ,
2021-08-13 17:22:28 -07:00
util = require ( 'util' ) ,
2018-04-27 11:38:09 -07:00
_ = require ( 'underscore' ) ;
2017-10-28 22:18:07 +02:00
2021-08-13 17:22:28 -07:00
const DOMAINS _FIELDS = [ 'domain' , 'zoneName' , 'provider' , 'configJson' , 'tlsConfigJson' , 'wellKnownJson' , 'fallbackCertificateJson' ] . join ( ',' ) ;
function postProcess ( data ) {
data . config = safe . JSON . parse ( data . configJson ) ;
delete data . configJson ;
data . tlsConfig = safe . JSON . parse ( data . tlsConfigJson ) ;
delete data . tlsConfigJson ;
data . wellKnown = safe . JSON . parse ( data . wellKnownJson ) ;
delete data . wellKnownJson ;
data . fallbackCertificate = safe . JSON . parse ( data . fallbackCertificateJson ) ;
delete data . fallbackCertificateJson ;
return data ;
}
2017-10-29 00:15:11 +02:00
// choose which subdomain backend we use for test purpose we use route53
function api ( provider ) {
assert . strictEqual ( typeof provider , 'string' ) ;
switch ( provider ) {
2017-11-21 19:18:03 -08:00
case 'cloudflare' : return require ( './dns/cloudflare.js' ) ;
case 'route53' : return require ( './dns/route53.js' ) ;
case 'gcdns' : return require ( './dns/gcdns.js' ) ;
case 'digitalocean' : return require ( './dns/digitalocean.js' ) ;
2018-05-06 18:57:27 -07:00
case 'gandi' : return require ( './dns/gandi.js' ) ;
2018-05-06 22:22:42 -07:00
case 'godaddy' : return require ( './dns/godaddy.js' ) ;
2020-03-12 17:13:13 -07:00
case 'linode' : return require ( './dns/linode.js' ) ;
2021-05-29 22:30:26 -07:00
case 'vultr' : return require ( './dns/vultr.js' ) ;
2018-05-09 12:24:33 +02:00
case 'namecom' : return require ( './dns/namecom.js' ) ;
2019-01-16 18:05:42 +02:00
case 'namecheap' : return require ( './dns/namecheap.js' ) ;
2021-01-18 19:43:47 +01:00
case 'netcup' : return require ( './dns/netcup.js' ) ;
2017-11-21 19:18:03 -08:00
case 'noop' : return require ( './dns/noop.js' ) ;
case 'manual' : return require ( './dns/manual.js' ) ;
2018-09-06 20:26:24 -07:00
case 'wildcard' : return require ( './dns/wildcard.js' ) ;
2017-11-21 19:18:03 -08:00
default : return null ;
2017-10-29 00:15:11 +02:00
}
}
2021-08-27 09:52:24 -07:00
function maybePromisify ( func ) {
if ( util . types . isAsyncFunction ( func ) ) return func ;
return util . promisify ( func ) ;
}
async function verifyDnsConfig ( dnsConfig , domain , zoneName , provider ) {
2018-09-11 21:24:04 -07:00
assert ( dnsConfig && typeof dnsConfig === 'object' ) ; // the dns config to test with
2017-10-29 00:15:11 +02:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof zoneName , 'string' ) ;
2018-01-09 14:46:38 -08:00
assert . strictEqual ( typeof provider , 'string' ) ;
2017-10-29 00:15:11 +02:00
2021-08-27 09:52:24 -07:00
const backend = api ( provider ) ;
if ( ! backend ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid provider' , { field : 'provider' } ) ;
2017-10-29 00:15:11 +02:00
2019-01-04 18:44:54 -08:00
const domainObject = { config : dnsConfig , domain : domain , zoneName : zoneName } ;
2021-08-27 09:52:24 -07:00
const [ error , result ] = await safe ( maybePromisify ( api ( provider ) . verifyDnsConfig ) ( domainObject ) ) ;
if ( error && error . reason === BoxError . ACCESS _DENIED ) return { error : new BoxError ( BoxError . BAD _FIELD , ` Access denied: ${ error . message } ` ) } ;
if ( error && error . reason === BoxError . NOT _FOUND ) return { error : new BoxError ( BoxError . BAD _FIELD , ` Zone not found: ${ error . message } ` ) } ;
if ( error && error . reason === BoxError . EXTERNAL _ERROR ) return { error : new BoxError ( BoxError . BAD _FIELD , ` Configuration error: ${ error . message } ` ) } ;
if ( error ) return { error } ;
return { error : null , sanitizedConfig : result } ;
2018-08-22 11:53:23 +02:00
}
2017-10-29 00:15:11 +02:00
2018-09-12 12:25:07 -07:00
function validateTlsConfig ( tlsConfig , dnsProvider ) {
2018-09-11 21:53:18 -07:00
assert . strictEqual ( typeof tlsConfig , 'object' ) ;
2018-09-12 12:25:07 -07:00
assert . strictEqual ( typeof dnsProvider , 'string' ) ;
switch ( tlsConfig . provider ) {
case 'letsencrypt-prod' :
case 'letsencrypt-staging' :
case 'fallback' :
break ;
default :
2020-08-07 11:47:08 -07:00
return new BoxError ( BoxError . BAD _FIELD , 'tlsConfig.provider must be fallback, letsencrypt-prod/staging' , { field : 'tlsProvider' } ) ;
2018-09-12 12:25:07 -07:00
}
2018-09-11 21:53:18 -07:00
2018-09-12 12:25:07 -07:00
if ( tlsConfig . wildcard ) {
2019-10-23 10:02:04 -07:00
if ( ! tlsConfig . provider . startsWith ( 'letsencrypt' ) ) return new BoxError ( BoxError . BAD _FIELD , 'wildcard can only be set with letsencrypt' , { field : 'wildcard' } ) ;
if ( dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard' ) return new BoxError ( BoxError . BAD _FIELD , 'wildcard cert requires a programmable DNS backend' , { field : 'tlsProvider' } ) ;
2018-09-11 21:53:18 -07:00
}
return null ;
}
2020-12-23 15:34:23 -08:00
function validateWellKnown ( wellKnown ) {
assert . strictEqual ( typeof wellKnown , 'object' ) ;
return null ;
}
2021-08-13 17:22:28 -07:00
async function add ( domain , data , auditSource ) {
2017-10-28 22:18:07 +02:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-11-10 00:43:46 -08:00
assert . strictEqual ( typeof data . zoneName , 'string' ) ;
assert . strictEqual ( typeof data . provider , 'string' ) ;
assert . strictEqual ( typeof data . config , 'object' ) ;
assert . strictEqual ( typeof data . fallbackCertificate , 'object' ) ;
assert . strictEqual ( typeof data . tlsConfig , 'object' ) ;
2017-10-28 22:18:07 +02:00
2020-03-31 12:04:46 -07:00
let { zoneName , provider , config , fallbackCertificate , tlsConfig , dkimSelector } = data ;
2018-11-10 00:43:46 -08:00
2021-08-13 17:22:28 -07:00
if ( ! tld . isValid ( domain ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid domain' , { field : 'domain' } ) ;
if ( domain . endsWith ( '.' ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid domain' , { field : 'domain' } ) ;
2018-01-23 18:54:05 -08:00
if ( zoneName ) {
2021-08-13 17:22:28 -07:00
if ( ! tld . isValid ( zoneName ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid zoneName' , { field : 'zoneName' } ) ;
if ( zoneName . endsWith ( '.' ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid zoneName' , { field : 'zoneName' } ) ;
2018-01-23 18:54:05 -08:00
} else {
2018-01-24 14:17:26 -08:00
zoneName = tld . getDomain ( domain ) || domain ;
2018-01-23 18:54:05 -08:00
}
2017-10-28 22:18:07 +02:00
2017-11-09 02:06:36 +01:00
if ( fallbackCertificate ) {
2018-11-10 00:43:46 -08:00
let error = reverseProxy . validateCertificate ( 'test' , { domain , config } , fallbackCertificate ) ;
2021-08-13 17:22:28 -07:00
if ( error ) throw error ;
2018-11-05 19:09:58 -08:00
} else {
2021-08-17 14:04:29 -07:00
fallbackCertificate = await reverseProxy . generateFallbackCertificate ( domain ) ;
2017-11-09 02:06:36 +01:00
}
2018-09-12 12:25:07 -07:00
let error = validateTlsConfig ( tlsConfig , provider ) ;
2021-08-13 17:22:28 -07:00
if ( error ) throw error ;
2018-01-31 18:20:11 +01:00
2021-10-11 19:51:29 -07:00
const dkimKey = await mail . generateDkimKey ( ) ;
2021-02-24 11:40:23 -08:00
if ( ! dkimSelector ) {
// create a unique suffix. this lets one add this domain can be added in another cloudron instance and not have their dkim selector conflict
2021-05-05 12:29:04 -07:00
const suffix = crypto . createHash ( 'sha256' ) . update ( settings . dashboardDomain ( ) ) . digest ( 'hex' ) . substr ( 0 , 6 ) ;
2021-02-24 11:40:23 -08:00
dkimSelector = ` cloudron- ${ suffix } ` ;
}
2020-03-31 12:04:46 -07:00
2021-08-27 09:52:24 -07:00
const result = await verifyDnsConfig ( config , domain , zoneName , provider ) ;
if ( result . error ) throw result . error ;
2017-10-28 23:23:58 +02:00
2021-08-13 17:22:28 -07:00
let queries = [
{ query : 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson, fallbackCertificateJson) VALUES (?, ?, ?, ?, ?, ?)' ,
2021-08-27 09:52:24 -07:00
args : [ domain , zoneName , provider , JSON . stringify ( result . sanitizedConfig ) , JSON . stringify ( tlsConfig ) , JSON . stringify ( fallbackCertificate ) ] } ,
2021-10-11 19:51:29 -07:00
{ query : 'INSERT INTO mail (domain, dkimKeyJson, dkimSelector) VALUES (?, ?, ?)' , args : [ domain , JSON . stringify ( dkimKey ) , dkimSelector || 'cloudron' ] } ,
2021-08-13 17:22:28 -07:00
] ;
2017-10-28 23:23:58 +02:00
2021-08-13 17:22:28 -07:00
[ error ] = await safe ( database . transaction ( queries ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'Domain already exists' ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
2018-01-26 19:16:43 -08:00
2021-08-13 17:22:28 -07:00
await reverseProxy . setFallbackCertificate ( domain , fallbackCertificate ) ;
2020-03-31 12:04:46 -07:00
2021-08-13 17:22:28 -07:00
eventlog . add ( eventlog . ACTION _DOMAIN _ADD , auditSource , { domain , zoneName , provider } ) ;
2021-08-27 09:52:24 -07:00
safe ( mail . onDomainAdded ( domain ) ) ; // background
2017-10-28 22:18:07 +02:00
}
2021-08-13 17:22:28 -07:00
async function get ( domain ) {
2017-10-28 22:18:07 +02:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-13 17:22:28 -07:00
const result = await database . query ( ` SELECT ${ DOMAINS _FIELDS } FROM domains WHERE domain=? ` , [ domain ] ) ;
if ( result . length === 0 ) return null ;
return postProcess ( result [ 0 ] ) ;
2017-10-28 22:18:07 +02:00
}
2021-08-13 17:22:28 -07:00
async function list ( ) {
const results = await database . query ( ` SELECT ${ DOMAINS _FIELDS } FROM domains ORDER BY domain ` ) ;
results . forEach ( postProcess ) ;
return results ;
2017-10-28 22:18:07 +02:00
}
2021-08-13 17:22:28 -07:00
async function update ( domain , data , auditSource ) {
2017-10-28 22:18:07 +02:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-11-10 00:43:46 -08:00
assert . strictEqual ( typeof data . zoneName , 'string' ) ;
assert . strictEqual ( typeof data . provider , 'string' ) ;
assert . strictEqual ( typeof data . config , 'object' ) ;
assert . strictEqual ( typeof data . fallbackCertificate , 'object' ) ;
assert . strictEqual ( typeof data . tlsConfig , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2017-10-28 22:18:07 +02:00
2020-12-23 15:34:23 -08:00
let { zoneName , provider , config , fallbackCertificate , tlsConfig , wellKnown } = data ;
2021-08-13 17:22:28 -07:00
let error ;
2018-11-10 00:43:46 -08:00
2021-08-13 17:22:28 -07:00
if ( settings . isDemo ( ) && ( domain === settings . dashboardDomain ( ) ) ) throw new BoxError ( BoxError . CONFLICT , 'Not allowed in demo mode' ) ;
2020-02-19 10:45:55 -08:00
2021-08-13 17:22:28 -07:00
const domainObject = await get ( domain ) ;
if ( zoneName ) {
if ( ! tld . isValid ( zoneName ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid zoneName' , { field : 'zoneName' } ) ;
} else {
zoneName = domainObject . zoneName ;
}
2017-10-28 22:18:07 +02:00
2021-08-13 17:22:28 -07:00
if ( fallbackCertificate ) {
let error = reverseProxy . validateCertificate ( 'test' , domainObject , fallbackCertificate ) ;
if ( error ) throw error ;
}
2018-05-15 13:51:00 -07:00
2021-08-13 17:22:28 -07:00
error = validateTlsConfig ( tlsConfig , provider ) ;
if ( error ) throw error ;
2017-11-09 02:06:36 +01:00
2021-08-13 17:22:28 -07:00
error = validateWellKnown ( wellKnown , provider ) ;
if ( error ) throw error ;
2018-01-31 18:20:11 +01:00
2021-08-13 17:22:28 -07:00
if ( provider === domainObject . provider ) api ( provider ) . injectPrivateFields ( config , domainObject . config ) ;
2020-12-23 15:34:23 -08:00
2021-08-27 09:52:24 -07:00
const result = await verifyDnsConfig ( config , domain , zoneName , provider ) ;
if ( result . error ) throw result . error ;
2017-10-28 23:23:58 +02:00
2021-08-27 09:52:24 -07:00
const newData = {
config : result . sanitizedConfig ,
2021-08-13 17:22:28 -07:00
zoneName ,
provider ,
tlsConfig ,
wellKnown ,
} ;
2019-02-08 20:35:05 -08:00
2021-08-13 17:22:28 -07:00
if ( fallbackCertificate ) newData . fallbackCertificate = fallbackCertificate ;
2021-05-04 21:40:11 -07:00
2021-08-13 17:22:28 -07:00
let args = [ ] , fields = [ ] ;
for ( const k in newData ) {
if ( k === 'config' || k === 'tlsConfig' || k === 'wellKnown' || k === 'fallbackCertificate' ) { // json fields
fields . push ( ` ${ k } Json = ? ` ) ;
args . push ( JSON . stringify ( newData [ k ] ) ) ;
} else {
fields . push ( k + ' = ?' ) ;
args . push ( newData [ k ] ) ;
}
}
args . push ( domain ) ;
2017-10-28 23:23:58 +02:00
2021-08-13 17:22:28 -07:00
[ error ] = await safe ( database . query ( 'UPDATE domains SET ' + fields . join ( ', ' ) + ' WHERE domain=?' , args ) ) ;
if ( error && error . reason === BoxError . NOT _FOUND ) throw new BoxError ( BoxError . NOT _FOUND , 'Domain not found' ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
2018-01-26 22:27:32 -08:00
2021-08-13 17:22:28 -07:00
if ( ! fallbackCertificate ) return ;
2018-01-30 12:23:27 -08:00
2021-08-13 17:22:28 -07:00
await reverseProxy . setFallbackCertificate ( domain , fallbackCertificate ) ;
2018-11-10 00:43:46 -08:00
2021-08-13 17:22:28 -07:00
eventlog . add ( eventlog . ACTION _DOMAIN _UPDATE , auditSource , { domain , zoneName , provider } ) ;
2017-10-28 22:18:07 +02:00
}
2021-08-13 17:22:28 -07:00
async function del ( domain , auditSource ) {
2017-10-28 22:18:07 +02:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-11-10 00:43:46 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-23 20:25:45 -08:00
2021-08-13 17:22:28 -07:00
if ( domain === settings . dashboardDomain ( ) ) throw new BoxError ( BoxError . CONFLICT , 'Cannot remove admin domain' ) ;
if ( domain === settings . mailDomain ( ) ) throw new BoxError ( BoxError . CONFLICT , 'Cannot remove mail domain. Change the mail server location first' ) ;
2018-01-23 20:25:45 -08:00
2021-08-13 17:22:28 -07:00
let queries = [
{ query : 'DELETE FROM mail WHERE domain = ?' , args : [ domain ] } ,
{ query : 'DELETE FROM domains WHERE domain = ?' , args : [ domain ] } ,
] ;
2017-10-29 00:15:11 +02:00
2021-08-13 17:22:28 -07:00
const [ error , results ] = await safe ( database . transaction ( queries ) ) ;
if ( error && error . code === 'ER_ROW_IS_REFERENCED_2' ) {
if ( error . message . indexOf ( 'apps_mailDomain_constraint' ) !== - 1 ) throw new BoxError ( BoxError . CONFLICT , 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.' ) ;
if ( error . message . indexOf ( 'subdomains' ) !== - 1 ) throw new BoxError ( BoxError . CONFLICT , 'Domain is in use by one or more app(s).' ) ;
if ( error . message . indexOf ( 'mail' ) !== - 1 ) throw new BoxError ( BoxError . CONFLICT , 'Domain is in use by one or more mailboxes. Delete them first in the Email view.' ) ;
throw new BoxError ( BoxError . CONFLICT , error . message ) ;
}
if ( error ) throw error ;
if ( results [ 1 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Domain not found' ) ;
2017-10-29 00:15:11 +02:00
2021-08-13 17:22:28 -07:00
eventlog . add ( eventlog . ACTION _DOMAIN _REMOVE , auditSource , { domain } ) ;
2017-10-29 00:15:11 +02:00
2021-08-25 19:41:46 -07:00
safe ( mail . onDomainRemoved ( domain ) ) ;
2017-11-21 19:18:03 -08:00
}
2017-10-29 00:15:11 +02:00
2021-08-13 17:22:28 -07:00
async function clear ( ) {
await database . query ( 'DELETE FROM domains' ) ;
2017-11-11 22:02:34 +01:00
}
2018-01-01 19:19:07 -08:00
2018-06-25 15:12:20 -07:00
// removes all fields that are strictly private and should never be returned by API calls
2018-04-27 11:38:09 -07:00
function removePrivateFields ( domain ) {
2020-12-23 15:34:23 -08:00
var result = _ . pick ( domain , 'domain' , 'zoneName' , 'provider' , 'config' , 'tlsConfig' , 'fallbackCertificate' , 'wellKnown' ) ;
2019-02-08 11:11:49 +01:00
return api ( result . provider ) . removePrivateFields ( result ) ;
2018-04-29 11:20:12 -07:00
}
2018-06-25 15:12:20 -07:00
// removes all fields that are not accessible by a normal user
function removeRestrictedFields ( domain ) {
2020-02-13 21:12:49 -08:00
var result = _ . pick ( domain , 'domain' , 'zoneName' , 'provider' ) ;
2018-08-22 17:19:18 +02:00
2020-08-15 18:40:59 -07:00
result . config = { } ; // always ensure config object
2018-08-22 17:19:18 +02:00
2018-06-25 15:12:20 -07:00
return result ;
2018-09-05 22:58:43 -07:00
}