// Packages
const { readFileSync } = require ( 'fs' ) ;
const publicSuffixList = require ( 'psl' ) ;
const minimist = require ( 'minimist' ) ;
const chalk = require ( 'chalk' ) ;
// Ours
const promptBool = require ( '../lib/utils/input/prompt-bool' ) ;
const exit = require ( './utils/exit' ) ;
const copy = require ( './copy' ) ;
const toHost = require ( './to-host' ) ;
const resolve4 = require ( './dns' ) ;
const isZeitWorld = require ( './is-zeit-world' ) ;
const { DOMAIN_VERIFICATION_ERROR } = require ( './errors' ) ;
const Now = require ( './' ) ;
const argv = minimist ( process . argv . slice ( 2 ) , {
boolean : [ 'no-clipboard' ] ,
alias : { 'no-clipboard' : 'C' }
} ) ;
const isTTY = process . stdout . isTTY ;
const clipboard = ! argv [ 'no-clipboard' ] ;
const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/ ;
module . exports = class Alias extends Now {
async ls ( deployment ) {
if ( deployment ) {
const target = await this . findDeployment ( deployment ) ;
if ( ! target ) {
const err = new Error (
` Aliases not found by " ${ deployment } ". Run ${ chalk . dim ( '`now alias ls`' ) } to see your aliases. `
) ;
err . userError = true ;
throw err ;
}
return this . listAliases ( target . uid ) ;
}
return this . listAliases ( ) ;
}
async rm ( _ alias ) {
return this . retry ( async bail => {
const res = await this . _ fetch ( ` /now/aliases/ ${ _ alias . uid } ` , {
method : 'DELETE'
} ) ;
if ( res . status === 403 ) {
return bail ( new Error ( 'Unauthorized' ) ) ;
}
if ( res . status !== 200 ) {
const err = new Error ( 'Deletion failed. Try again later.' ) ;
throw err ;
}
} ) ;
}
async findDeployment ( deployment ) {
const list = await this . list ( ) ;
let key ;
let val ;
if ( /\./ . test ( deployment ) ) {
val = toHost ( deployment ) ;
key = 'url' ;
} else {
val = deployment ;
key = 'uid' ;
}
const depl = list . find ( d => {
if ( d [ key ] === val ) {
if ( this . _ debug ) {
console . log ( ` > [debug] matched deployment ${ d . uid } by ${ key } ${ val } ` ) ;
}
return true ;
}
// Match prefix
if ( ` ${ val } .now.sh ` === d . url ) {
if ( this . _ debug ) {
console . log ( ` > [debug] matched deployment ${ d . uid } by url ${ d . url } ` ) ;
}
return true ;
}
return false ;
} ) ;
return depl ;
}
async updatePathBasedroutes ( alias , rules ) {
alias = await this . maybeSetUpDomain ( alias ) ;
return this . upsertPathAlias ( alias , rules ) ;
}
async upsertPathAlias ( alias , rules ) {
return this . retry ( async ( bail , attempt ) => {
if ( this . _ debug ) {
console . time ( ` > [debug] /now/aliases # ${ attempt } ` ) ;
}
const rulesData = this . readRulesFile ( rules ) ;
const ruleCount = rulesData . rules . length ;
const res = await this . _ fetch ( ` /now/aliases ` , {
method : 'POST' ,
body : { alias , rules : rulesData . rules }
} ) ;
const body = await res . json ( ) ;
body . ruleCount = ruleCount ;
if ( this . _ debug ) {
console . timeEnd ( ` > [debug] /now/aliases # ${ attempt } ` ) ;
}
// 409 conflict is returned if it already exists
if ( res . status === 409 ) {
return { uid : body . error . uid } ;
}
if ( res . status === 422 ) {
return body ;
}
// No retry on authorization problems
if ( res . status === 403 ) {
const code = body . error . code ;
if ( code === 'custom_domain_needs_upgrade' ) {
const err = new Error (
` Custom domains are only enabled for premium accounts. Please upgrade by running ${ chalk . gray ( '`' ) } ${ chalk . cyan ( 'now upgrade' ) } ${ chalk . gray ( '`' ) } . `
) ;
err . userError = true ;
return bail ( err ) ;
}
if ( code === 'alias_in_use' ) {
const err = new Error (
` The alias you are trying to configure ( ${ chalk . underline ( chalk . bold ( alias ) ) } ) is already in use by a different account. `
) ;
err . userError = true ;
return bail ( err ) ;
}
if ( code === 'forbidden' ) {
const err = new Error (
'The domain you are trying to use as an alias is already in use by a different account.'
) ;
err . userError = true ;
return bail ( err ) ;
}
return bail ( new Error ( 'Authorization error' ) ) ;
}
// All other errors
if ( body . error ) {
const code = body . error . code ;
if ( code === 'cert_missing' ) {
console . log (
` > Provisioning certificate for ${ chalk . underline ( chalk . bold ( alias ) ) } `
) ;
try {
await this . createCert ( alias ) ;
} catch ( err ) {
// We bail to avoid retrying the whole process
// of aliasing which would involve too many
// retries on certificate provisioning
return bail ( err ) ;
}
// Try again, but now having provisioned the certificate
return this . upsertPathAlias ( alias , rules ) ;
}
if ( code === 'cert_expired' ) {
console . log (
` > Renewing certificate for ${ chalk . underline ( chalk . bold ( alias ) ) } `
) ;
try {
await this . createCert ( alias , { renew : true } ) ;
} catch ( err ) {
return bail ( err ) ;
}
}
return bail ( new Error ( body . error . message ) ) ;
}
// The two expected succesful cods are 200 and 304
if ( res . status !== 200 && res . status !== 304 ) {
throw new Error ( 'Unhandled error' ) ;
}
return body ;
} ) ;
}
readRulesFile ( rules ) {
try {
const rulesJson = readFileSync ( rules , 'utf8' ) ;
return JSON . parse ( rulesJson ) ;
} catch ( err ) {
console . error ( ` Reading rules file ${ rules } failed: ${ err } ` ) ;
}
}
async set ( deployment , alias ) {
const depl = await this . findDeployment ( deployment ) ;
if ( ! depl ) {
const err = new Error (
` Deployment not found by " ${ deployment } ". Run ${ chalk . dim ( '`now ls`' ) } to see your deployments. `
) ;
err . userError = true ;
throw err ;
}
const aliasDepl = ( await this . listAliases ( ) ) . find ( e => e . alias === alias ) ;
if ( aliasDepl && aliasDepl . rules ) {
if ( isTTY ) {
try {
const msg = ` > Path alias excists with ${ aliasDepl . rules . length } rule ${ aliasDepl . rules . length > 1 ? 's' : '' } . \n ` +
` > Are you sure you want to update ${ alias } to be a normal alias? \n ` ;
const confirmation = await promptBool ( msg ) ;
if ( ! confirmation ) {
console . log ( '\n> Aborted' ) ;
return exit ( 1 ) ;
}
} catch ( err ) {
console . log ( err ) ;
}
} else {
console . log (
` Overwriting path alias with ${ aliasDepl . rules . length } rule ${ aliasDepl . rules . length > 1 ? 's' : '' } to be a normal alias. `
) ;
}
}
alias = await this . maybeSetUpDomain ( alias ) ;
const newAlias = await this . createAlias ( depl , alias ) ;
if ( ! newAlias ) {
throw new Error (
` Unexpected error occurred while setting up alias: ${ JSON . stringify ( newAlias ) } `
) ;
}
const { created , uid } = newAlias ;
if ( created ) {
const pretty = ` https:// ${ alias } ` ;
const output = ` ${ chalk . cyan ( '> Success!' ) } Alias created ${ chalk . dim ( ` ( ${ uid } ) ` ) } : \n ${ chalk . bold ( chalk . underline ( pretty ) ) } now points to ${ chalk . bold ( ` https:// ${ depl . url } ` ) } ${ chalk . dim ( ` ( ${ depl . uid } ) ` ) } ` ;
if ( isTTY && clipboard ) {
let append ;
try {
await copy ( pretty ) ;
append = '[copied to clipboard]' ;
} catch ( err ) {
append = '' ;
} finally {
console . log ( ` ${ output } ${ append } ` ) ;
}
} else {
console . log ( output ) ;
}
} else {
console . log (
` ${ chalk . cyan ( '> Success!' ) } Alias already exists ${ chalk . dim ( ` ( ${ uid } ) ` ) } . `
) ;
}
}
createAlias ( depl , alias ) {
return this . retry ( async ( bail , attempt ) => {
if ( this . _ debug ) {
console . time (
` > [debug] /now/deployments/ ${ depl . uid } /aliases # ${ attempt } `
) ;
}
const res = await this . _ fetch ( ` /now/deployments/ ${ depl . uid } /aliases ` , {
method : 'POST' ,
body : { alias }
} ) ;
const body = await res . json ( ) ;
if ( this . _ debug ) {
console . timeEnd (
` > [debug] /now/deployments/ ${ depl . uid } /aliases # ${ attempt } `
) ;
}
// 409 conflict is returned if it already exists
if ( res . status === 409 ) {
return { uid : body . error . uid } ;
}
// No retry on authorization problems
if ( res . status === 403 ) {
const code = body . error . code ;
if ( code === 'custom_domain_needs_upgrade' ) {
const err = new Error (
` Custom domains are only enabled for premium accounts. Please upgrade by running ${ chalk . gray ( '`' ) } ${ chalk . cyan ( 'now upgrade' ) } ${ chalk . gray ( '`' ) } . `
) ;
err . userError = true ;
return bail ( err ) ;
}
if ( code === 'alias_in_use' ) {
const err = new Error (
` The alias you are trying to configure ( ${ chalk . underline ( chalk . bold ( alias ) ) } ) is already in use by a different account. `
) ;
err . userError = true ;
return bail ( err ) ;
}
if ( code === 'forbidden' ) {
const err = new Error (
'The domain you are trying to use as an alias is already in use by a different account.'
) ;
err . userError = true ;
return bail ( err ) ;
}
return bail ( new Error ( 'Authorization error' ) ) ;
}
// All other errors
if ( body . error ) {
const code = body . error . code ;
if ( code === 'deployment_not_found' ) {
return bail ( new Error ( 'Deployment not found' ) ) ;
}
if ( code === 'cert_missing' ) {
console . log (
` > Provisioning certificate for ${ chalk . underline ( chalk . bold ( alias ) ) } `
) ;
try {
await this . createCert ( alias ) ;
} catch ( err ) {
// We bail to avoid retrying the whole process
// of aliasing which would involve too many
// retries on certificate provisioning
return bail ( err ) ;
}
// Try again, but now having provisioned the certificate
return this . createAlias ( depl , alias ) ;
}
if ( code === 'cert_expired' ) {
console . log (
` > Renewing certificate for ${ chalk . underline ( chalk . bold ( alias ) ) } `
) ;
try {
await this . createCert ( alias , { renew : true } ) ;
} catch ( err ) {
return bail ( err ) ;
}
}
return bail ( new Error ( body . error . message ) ) ;
}
// The two expected succesful cods are 200 and 304
if ( res . status !== 200 && res . status !== 304 ) {
throw new Error ( 'Unhandled error' ) ;
}
return body ;
} ) ;
}
async setupRecord ( domain , name ) {
await this . setupDomain ( domain ) ;
if ( this . _ debug ) {
console . log ( ` > [debug] Setting up record " ${ name } " for " ${ domain } " ` ) ;
}
const type = name === '' ? 'ALIAS' : 'CNAME' ;
return this . retry ( async ( bail , attempt ) => {
if ( this . _ debug ) {
console . time ( ` > [debug] /domains/ ${ domain } /records # ${ attempt } ` ) ;
}
const res = await this . _ fetch ( ` /domains/ ${ domain } /records ` , {
method : 'POST' ,
body : {
type ,
name : name === '' ? name : '*' ,
value : 'alias.zeit.co'
}
} ) ;
if ( this . _ debug ) {
console . timeEnd ( ` > [debug] /domains/ ${ domain } /records # ${ attempt } ` ) ;
}
if ( res . status === 403 ) {
return bail ( new Error ( 'Unauthorized' ) ) ;
}
const body = await res . json ( ) ;
if ( res . status !== 200 ) {
throw new Error ( body . error . message ) ;
}
return body ;
} ) ;
}
async maybeSetUpDomain ( alias ) {
// Make alias lowercase
alias = alias . toLowerCase ( ) ;
// Trim leading and trailing dots
// for example: `google.com.` => `google.com`
alias = alias . replace ( /^\.+/ , '' ) . replace ( /\.+$/ , '' ) ;
// Evaluate the alias
if ( /\./ . test ( alias ) ) {
alias = toHost ( alias ) ;
} else {
if ( this . _ debug ) {
console . log ( ` > [debug] suffixing \` .now.sh \` to alias ${ alias } ` ) ;
}
alias = ` ${ alias } .now.sh ` ;
}
if ( ! domainRegex . test ( alias ) ) {
const err = new Error ( ` Invalid alias " ${ alias } " ` ) ;
err . userError = true ;
throw err ;
}
if ( ! /\.now\.sh$/ . test ( alias ) ) {
console . log (
` > ${ chalk . bold ( chalk . underline ( alias ) ) } is a custom domain. `
) ;
console . log (
` > Verifying the DNS settings for ${ chalk . bold ( chalk . underline ( alias ) ) } (see ${ chalk . underline ( 'https://zeit.world' ) } for help) `
) ;
const _ domain = publicSuffixList . parse ( alias ) . domain ;
const _ domainInfo = await this . getDomain ( _ domain ) ;
const domainInfo = _ domainInfo && ! _ domainInfo . error
? _ domainInfo
: undefined ;
const { domain , nameservers } = domainInfo
? { domain : _ domain }
: await this . getNameservers ( alias ) ;
const usingZeitWorld = domainInfo
? ! domainInfo . isExternal
: isZeitWorld ( nameservers ) ;
let skipDNSVerification = false ;
if ( this . _ debug ) {
if ( domainInfo ) {
console . log (
` > [debug] Found domain ${ domain } with verified: ${ domainInfo . verified } `
) ;
} else {
console . log (
` > [debug] Found domain ${ domain } and nameservers ${ nameservers } `
) ;
}
}
if ( ! usingZeitWorld && domainInfo ) {
if ( domainInfo . verified ) {
skipDNSVerification = true ;
} else if ( domainInfo . uid ) {
const { verified , created } = await this . setupDomain ( domain , {
isExternal : true
} ) ;
if ( ! ( created && verified ) ) {
const e = new Error (
` > Failed to verify the ownership of ${ domain } , please refer to 'now domain --help'. `
) ;
e . userError = true ;
throw e ;
}
console . log (
` ${ chalk . cyan ( '> Success!' ) } Domain ${ chalk . bold ( chalk . underline ( domain ) ) } verified `
) ;
}
}
try {
if ( ! skipDNSVerification ) {
await this . verifyOwnership ( alias ) ;
}
} catch ( err ) {
if ( err . userError ) {
// A user error would imply that verification failed
// in which case we attempt to correct the dns
// configuration (if we can!)
try {
if ( usingZeitWorld ) {
console . log (
` > Detected ${ chalk . bold ( chalk . underline ( 'zeit.world' ) ) } nameservers! Configuring records. `
) ;
const record = alias . substr ( 0 , alias . length - domain . length ) ;
// Lean up trailing and leading dots
const _ record = record . replace ( /^\./ , '' ) . replace ( /\.$/ , '' ) ;
const _ domain = domain . replace ( /^\./ , '' ) . replace ( /\.$/ , '' ) ;
if ( _ record === '' ) {
await this . setupRecord ( _ domain , '*' ) ;
}
await this . setupRecord ( _ domain , _ record ) ;
this . recordSetup = true ;
console . log ( '> DNS Configured! Verifying propagation…' ) ;
try {
await this . retry ( ( ) => this . verifyOwnership ( alias ) , {
retries : 10 ,
maxTimeout : 8000
} ) ;
} catch ( err2 ) {
const e = new Error (
'> We configured the DNS settings for your alias, but we were unable to ' +
"verify that they've propagated. Please try the alias again later."
) ;
e . userError = true ;
throw e ;
}
} else {
console . log (
` > Resolved IP: ${ err . ip ? ` ${ chalk . underline ( err . ip ) } (unknown) ` : chalk . dim ( 'none' ) } `
) ;
console . log (
` > Nameservers: ${ nameservers && nameservers . length ? nameservers
. map ( ns => chalk . underline ( ns ) )
. join ( ', ' ) : chalk . dim ( 'none' ) } `
) ;
throw err ;
}
} catch ( e ) {
if ( e . userError ) {
throw e ;
}
throw err ;
}
} else {
throw err ;
}
}
if ( ! usingZeitWorld && ! skipDNSVerification ) {
if ( this . _ debug ) {
console . log (
` > [debug] Trying to register a non-ZeitWorld domain ${ domain } for the current user `
) ;
}
const { uid , verified , created } = await this . setupDomain ( domain , {
isExternal : true
} ) ;
if ( ! ( created && verified ) ) {
const e = new Error (
` > Failed to verify the ownership of ${ domain } , please refer to 'now domain --help'. `
) ;
e . userError = true ;
throw e ;
}
console . log (
` ${ chalk . cyan ( '> Success!' ) } Domain ${ chalk . bold ( chalk . underline ( domain ) ) } ${ chalk . dim ( ` ( ${ uid } ) ` ) } added `
) ;
}
console . log ( ` > Verification ${ chalk . bold ( 'OK' ) } ! ` ) ;
}
return alias ;
}
verifyOwnership ( domain ) {
return this . retry ( async bail => {
const targets = await resolve4 ( 'alias.zeit.co' ) ;
if ( targets . length <= 0 ) {
return bail ( new Error ( 'Unable to resolve alias.zeit.co' ) ) ;
}
let ips = [ ] ;
try {
ips = await resolve4 ( domain ) ;
} catch ( err ) {
if (
err . code === 'ENODATA' ||
err . code === 'ESERVFAIL' ||
err . code === 'ENOTFOUND'
) {
// Not errors per se, just absence of records
if ( this . _ debug ) {
console . log ( ` > [debug] No records found for " ${ domain } " ` ) ;
}
const err = new Error ( DOMAIN_VERIFICATION_ERROR ) ;
err . userError = true ;
return bail ( err ) ;
}
throw err ;
}
if ( ips . length <= 0 ) {
const err = new Error ( DOMAIN_VERIFICATION_ERROR ) ;
err . userError = true ;
return bail ( err ) ;
}
for ( const ip of ips ) {
if ( targets . indexOf ( ip ) === - 1 ) {
const err = new Error (
` The domain ${ domain } has an A record ${ chalk . bold ( ip ) } that doesn't resolve to ${ chalk . bold ( chalk . underline ( 'alias.zeit.co' ) ) } . \n > ` +
DOMAIN_VERIFICATION_ERROR
) ;
err . ip = ip ;
err . userError = true ;
return bail ( err ) ;
}
}
} ) ;
}
} ;