import Now from './' ;
import toHost from './to-host' ;
import chalk from 'chalk' ;
import isZeitWorld from './is-zeit-world' ;
import _ domainRegex from 'domain-regex' ;
import { DOMAIN_VERIFICATION_ERROR } from './errors' ;
import { resolve4 } from './dns' ;
const domainRegex = _ domainRegex ( ) ;
export default 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 ) ;
} else {
return this . listAliases ( ) ;
}
}
async rm ( _ alias ) {
return this . retry ( async ( bail , attempt ) => {
const res = await this . _ fetch ( ` /now/aliases/ ${ _ alias . uid } ` , {
method : 'DELETE'
} ) ;
if ( 403 === res . status ) {
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 , 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 set ( deployment , alias ) {
// make alias lowercase
alias = alias . toLowerCase ( ) ;
// 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 ;
}
// evaluate the alias
if ( ! /\./ . test ( alias ) ) {
if ( this . _ debug ) console . log ( ` > [debug] suffixing \` .now.sh \` to alias ${ alias } ` ) ;
alias = ` ${ alias } .now.sh ` ;
} else {
alias = toHost ( alias ) ;
}
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) ` ) ;
try {
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 {
const { domain , nameservers } = await this . getNameservers ( alias ) ;
if ( this . _ debug ) console . log ( ` > [debug] Found domain ${ domain } and nameservers ${ nameservers } ` ) ;
if ( isZeitWorld ( nameservers ) ) {
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 ;
}
}
console . log ( ` > Verification ${ chalk . bold ( 'OK' ) } ! ` ) ;
}
const { created , uid } = await this . createAlias ( depl , alias ) ;
if ( created ) {
console . log ( ` ${ chalk . cyan ( '> Success!' ) } Alias created ${ chalk . dim ( ` ( ${ uid } ) ` ) } : ${ chalk . bold ( chalk . underline ( ` https:// ${ alias } ` ) ) } now points to ${ chalk . bold ( ` https:// ${ depl . url } ` ) } ${ chalk . dim ( ` ( ${ depl . uid } ) ` ) } ` ) ;
} 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 ( 409 === res . status ) return { uid : body . error . uid } ;
// no retry on authorization problems
if ( 403 === res . status ) {
const code = body . error . code ;
if ( 'custom_domain_needs_upgrade' === code ) {
const err = new Error ( ` Custom domains are only enabled for premium accounts. Please upgrade at ${ chalk . underline ( 'https://zeit.co/account' ) } . ` ) ;
err . userError = true ;
return bail ( err ) ;
}
if ( 'alias_in_use' === code ) {
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 ) ;
}
return bail ( new Error ( 'Authorization error' ) ) ;
}
// all other errors
if ( body . error ) {
const code = body . error . code ;
if ( 'deployment_not_found' === code ) {
return bail ( new Error ( 'Deployment not found' ) ) ;
}
if ( 'cert_missing' === code ) {
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 ) ;
}
return bail ( new Error ( body . error . message ) ) ;
}
// the two expected succesful cods are 200 and 304
if ( 200 !== res . status && 304 !== res . status ) {
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 ( 403 === res . status ) {
return bail ( new Error ( 'Unauthorized' ) ) ;
}
const body = await res . json ( ) ;
if ( 200 !== res . status ) {
throw new Error ( body . error . message ) ;
}
return body ;
} ) ;
}
verifyOwnership ( domain ) {
return this . retry ( async ( bail , attempt ) => {
const targets = await resolve4 ( 'alias.zeit.co' ) ;
if ( ! targets . length ) {
return bail ( new Error ( 'Unable to resolve alias.zeit.co' ) ) ;
}
let ips = [ ] ;
try {
ips = await resolve4 ( domain ) ;
} catch ( err ) {
if ( 'ENODATA' === err . code || 'ESERVFAIL' === err . code || 'ENOTFOUND' === err . code ) {
// not errors per se, just absence of records
if ( this . _ debug ) console . log ( ` > [debug] No records found for " ${ domain } " ` ) ;
} else {
throw err ;
}
}
if ( ! ips . length ) {
const err = new Error ( DOMAIN_VERIFICATION_ERROR ) ;
err . userError = true ;
return bail ( err ) ;
}
for ( const ip of ips ) {
if ( ! ~ targets . indexOf ( ip ) ) {
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 ) ;
}
}
} ) ;
}
createCert ( domain ) {
return this . retry ( async ( bail , attempt ) => {
if ( this . _ debug ) console . time ( ` > [debug] /now/certs # ${ attempt } ` ) ;
const res = await this . _ fetch ( '/now/certs' , {
method : 'POST' ,
body : {
domains : [ domain ]
}
} ) ;
if ( 304 === res . status ) {
console . log ( '> Certificate already issued.' ) ;
return ;
}
const body = await res . json ( ) ;
if ( this . _ debug ) console . timeEnd ( ` > [debug] /now/certs # ${ attempt } ` ) ;
if ( body . error ) {
const { code } = body . error ;
if ( 'verification_failed' === code ) {
const err = new Error ( 'The certificate issuer failed to verify ownership of the domain. ' +
'This likely has to do with DNS propagation and caching issues. Please retry later!' ) ;
err . userError = true ;
// retry
throw err ;
} else if ( 'rate_limited' === code ) {
const err = new Error ( body . error . message ) ;
err . userError = true ;
// dont retry
return bail ( err ) ;
}
throw new Error ( body . message ) ;
}
if ( 200 !== res . status && 304 !== res . status ) {
throw new Error ( 'Unhandled error' ) ;
}
} , { retries : 5 , minTimeout : 30000 , maxTimeout : 90000 } ) ;
}
}