@ -1,9 +1,11 @@
// Packages
const { readFileSync } = require ( 'fs' )
const publicSuffixList = require ( 'psl' )
const minimist = require ( 'minimist' )
const chalk = require ( 'chalk' )
// Ours
const exit = require ( './utils/exit' )
const copy = require ( './copy' )
const toHost = require ( './to-host' )
const resolve4 = require ( './dns' )
@ -93,146 +95,153 @@ module.exports = class Alias extends Now {
return depl
}
async set ( deployment , alias ) {
// make alias lowercase
alias = alias . toLowerCase ( )
async updatePathBasedroutes ( alias , rules ) {
await this . maybeSetUpDomain ( alias )
// trim leading and trailing dots
// for example: `google.com.` => `google.com`
alias = alias
. replace ( /^\.+/ , '' )
. replace ( /\.+$/ , '' )
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
}
return await this . upsertPathAlias ( alias , rules )
}
// evaluate the alias
if ( /\./ . test ( alias ) ) {
alias = toHost ( alias )
} else {
async upsertPathAlias ( alias , rules ) {
return this . retry ( async ( bail , attempt ) => {
if ( this . _ debug ) {
console . log ( ` > [debug] suffixing \` . now.sh \` to alias ${ alias } ` )
console . time ( ` > [debug] /now/aliases # ${ attempt } ` )
}
alias = ` ${ alias } .now.sh `
}
const rulesData = this . readRulesFile ( rules )
const ruleCount = rulesData . rules . length
const res = await this . _ fetch ( ` /now/aliases ` , {
method : 'POST' ,
body : { alias , rules : rulesData . rules }
} )
if ( ! domainRegex . test ( alias ) ) {
const err = new Error ( ` Invalid alias " ${ alias } " ` )
err . userError = true
throw err
}
const body = await res . json ( )
body . ruleCount = ruleCount
if ( this . _ debug ) {
console . timeEnd ( ` > [debug] /now/aliases # ${ attempt } ` )
}
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) ` )
// 409 conflict is returned if it already exists
if ( res . status === 409 ) {
return { uid : body . error . uid }
}
if ( res . status === 422 ) {
console . log ( body . error . message )
return new Error ( body . error . message )
}
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
// no retry on authorization problems
if ( res . status === 403 ) {
const code = body . error . code
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 ( 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 ( ! 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 ` )
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 )
}
}
try {
if ( ! skipDNSVerification ) {
await this . verifyOwnership ( alias )
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 )
}
} 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 ( /\.$/ , '' )
return bail ( new Error ( 'Authorization error' ) )
}
if ( _ record === '' ) {
await this . setupRecord ( _ domain , '*' )
}
// all other errors
if ( body . error ) {
const code = body . error . code
await this . setupRecord ( _ domain , _ record )
if ( code === 'cert_missing' ) {
console . log ( ` > Provisioning certificate for ${ chalk . underline ( chalk . bold ( alias ) ) } ` )
this . recordSetup = true
console . log ( '> DNS Configured! Verifying propagation…' )
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 {
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
}
// try again, but now having provisioned the certificate
throw err
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 )
}
} else {
throw err
}
return bail ( new Error ( body . error . message ) )
}
if ( ! usingZeitWorld && ! skipDNSVerification ) {
if ( this . _ debug ) {
console . log ( ` > [debug] Trying to register a non-ZeitWorld domain ${ domain } for the current user ` )
}
// the two expected succesful cods are 200 and 304
if ( res . status !== 200 && res . status !== 304 ) {
throw new Error ( 'Unhandled error' )
}
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
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 confirmation = ( await new Promise ( resolve => {
process . stdout . write ( ` Path alias excists with ${ aliasDepl . rules . length } rule ${ aliasDepl . rules . length > 1 ? 's' : '' } . \n ` )
process . stdout . write ( ` Are you sure you want to update ${ alias } to be a normal alias? \n ` )
process . stdin . on ( 'data' , d => {
process . stdin . pause ( )
resolve ( d . toString ( ) . trim ( ) )
} ) . resume ( )
} ) ) . toLowerCase ( )
if ( confirmation !== 'y' && confirmation !== 'yes' ) {
console . log ( '\n> Aborted' )
return exit ( 1 )
}
} catch ( err ) {
console . log ( err )
}
console . log ( ` ${ chalk . cyan ( '> Success!' ) } Domain ${ chalk . bold ( chalk . underline ( domain ) ) } ${ chalk . dim ( ` ( ${ uid } ) ` ) } added ` )
} else {
console . log ( ` Overwriting path alias with ${ aliasDepl . rules . length } rule ${ aliasDepl . rules . length > 1 ? 's' : '' } to be a normal alias. ` )
}
console . log ( ` > Verification ${ chalk . bold ( 'OK' ) } ! ` )
}
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 ) } ` )
@ -391,6 +400,139 @@ module.exports = class Alias extends Now {
} )
}
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' ) } ! ` )
}
}
verifyOwnership ( domain ) {
return this . retry ( async bail => {
const targets = await resolve4 ( 'alias.zeit.co' )