You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
817 lines
24 KiB
817 lines
24 KiB
// Packages
|
|
const { readFileSync } = require('fs')
|
|
const publicSuffixList = require('psl')
|
|
const minimist = require('minimist')
|
|
const ms = require('ms')
|
|
const chalk = require('chalk')
|
|
const { write: copy } = require('clipboardy')
|
|
|
|
// Ours
|
|
const promptBool = require('../lib/utils/input/prompt-bool')
|
|
const info = require('../lib/utils/output/info')
|
|
const param = require('../lib/utils/output/param')
|
|
const wait = require('../lib/utils/output/wait')
|
|
const success = require('../lib/utils/output/success')
|
|
const uid = require('../lib/utils/output/uid')
|
|
const eraseLines = require('../lib/utils/output/erase-lines')
|
|
const stamp = require('../lib/utils/output/stamp')
|
|
const error = require('../lib/utils/output/error')
|
|
const treatBuyError = require('../lib/utils/domains/treat-buy-error')
|
|
const scaleInfo = require('./scale-info')
|
|
const { DOMAIN_VERIFICATION_ERROR } = require('./errors')
|
|
const isZeitWorld = require('./is-zeit-world')
|
|
const resolve4 = require('./dns')
|
|
const toHost = require('./to-host')
|
|
const exit = require('./utils/exit')
|
|
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, domains) {
|
|
alias = await this.maybeSetUpDomain(alias, domains)
|
|
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, domains, currentTeam, user) {
|
|
alias = alias.replace(/^https:\/\//i, '')
|
|
|
|
if (alias.indexOf('.') === -1) {
|
|
// `.now.sh` domain is implied if just the subdomain is given
|
|
alias += '.now.sh'
|
|
}
|
|
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 exists 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, {
|
|
trailing: '\n'
|
|
})
|
|
|
|
if (!confirmation) {
|
|
info('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.`
|
|
)
|
|
}
|
|
}
|
|
|
|
let aliasedDeployment = null
|
|
let shouldScaleDown = false
|
|
|
|
if (aliasDepl && depl.scale) {
|
|
aliasedDeployment = await this.findDeployment(aliasDepl.deploymentId)
|
|
if (
|
|
aliasedDeployment &&
|
|
aliasedDeployment.scale &&
|
|
aliasedDeployment.scale.current >= depl.scale.current &&
|
|
(aliasedDeployment.scale.min > depl.scale.min ||
|
|
aliasedDeployment.scale.max > depl.scale.max)
|
|
) {
|
|
shouldScaleDown = true
|
|
console.log(
|
|
`> Alias ${alias} points to ${chalk.bold(aliasedDeployment.url)} (${chalk.bold(aliasedDeployment.scale.current + ' instances')})`
|
|
)
|
|
// Test if we need to change the scale or just update the rules
|
|
console.log(
|
|
`> Scaling ${depl.url} to ${chalk.bold(aliasedDeployment.scale.current + ' instances')} atomically`
|
|
)
|
|
if (depl.scale.current !== aliasedDeployment.scale.current) {
|
|
if (depl.scale.max < 1) {
|
|
if (this._debug) {
|
|
console.log(
|
|
'Updating max scale to 1 so that deployment may be unfrozen.'
|
|
)
|
|
}
|
|
await this.setScale(depl.uid, {
|
|
min: depl.scale.min,
|
|
max: Math.max(aliasedDeployment.scale.max, 1)
|
|
})
|
|
}
|
|
if (depl.scale.current < 1) {
|
|
if (this._debug) {
|
|
console.log(`> Deployment ${depl.url} is frozen, unfreezing...`)
|
|
}
|
|
await this.unfreeze(depl)
|
|
if (this._debug) {
|
|
console.log(
|
|
`> Deployment is now unfrozen, scaling it to match current instance count`
|
|
)
|
|
}
|
|
}
|
|
// Scale it to current limit
|
|
if (depl.scale.current !== aliasedDeployment.scale.current) {
|
|
if (this._debug) {
|
|
console.log(`> Scaling deployment to match current scale.`)
|
|
}
|
|
await this.setScale(depl.uid, {
|
|
min: aliasedDeployment.scale.current,
|
|
max: aliasedDeployment.scale.current
|
|
})
|
|
}
|
|
await scaleInfo(this, depl.url)
|
|
if (this._debug) {
|
|
console.log(`> Updating scaling rules for deployment.`)
|
|
}
|
|
}
|
|
|
|
await this.setScale(depl.uid, {
|
|
min: Math.max(aliasedDeployment.scale.min, depl.scale.min),
|
|
max: Math.max(aliasedDeployment.scale.max, depl.scale.max)
|
|
})
|
|
}
|
|
}
|
|
|
|
alias = await this.maybeSetUpDomain(alias, domains, currentTeam, user)
|
|
|
|
const aliasTime = Date.now()
|
|
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 output = `${chalk.cyan('> Success!')} ${alias} now points to ${chalk.bold(depl.url)}! ${chalk.grey('[' + ms(Date.now() - aliasTime) + ']')}`
|
|
if (isTTY && clipboard) {
|
|
try {
|
|
await copy(depl.url)
|
|
} catch (err) {
|
|
} finally {
|
|
console.log(output)
|
|
}
|
|
} else {
|
|
console.log(output)
|
|
}
|
|
} else {
|
|
console.log(
|
|
`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(`(${uid})`)}.`
|
|
)
|
|
}
|
|
if (aliasedDeployment && shouldScaleDown) {
|
|
const scaleDown = Date.now()
|
|
await this.setScale(aliasedDeployment.uid, { min: 0, max: 1 })
|
|
console.log(
|
|
`> Scaled ${chalk.gray(aliasedDeployment.url)} down to 1 instance ${chalk.gray('[' + ms(Date.now() - scaleDown) + ']')}`
|
|
)
|
|
}
|
|
}
|
|
|
|
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, domains, currentTeam, user) {
|
|
const gracefulExit = () => {
|
|
this.close()
|
|
domains.close()
|
|
// eslint-disable-next-line unicorn/no-process-exit
|
|
process.exit()
|
|
}
|
|
// 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.`)
|
|
|
|
let stopSpinner = wait('Fetching domain info')
|
|
|
|
let elapsed = stamp()
|
|
const parsed = publicSuffixList.parse(alias)
|
|
const pricePromise = domains.price(parsed.domain).catch(() => {
|
|
// Can be safely ignored
|
|
})
|
|
const canBePurchased = await domains.status(parsed.domain)
|
|
const aliasParam = param(parsed.domain)
|
|
let price
|
|
let period
|
|
|
|
stopSpinner()
|
|
|
|
if (canBePurchased) {
|
|
try {
|
|
const json = await pricePromise
|
|
price = json.price
|
|
period = json.period
|
|
} catch (err) {
|
|
// Can be safely ignored
|
|
}
|
|
}
|
|
if (canBePurchased && price && period) {
|
|
const periodMsg = `${period}yr${period > 1 ? 's' : ''}`
|
|
info(
|
|
`The domain ${aliasParam} is ${chalk.italic('available')} to buy under ${chalk.bold((currentTeam && currentTeam.slug) || user.username || user.email)}! ${elapsed()}`
|
|
)
|
|
const confirmation = await promptBool(
|
|
`Buy now for ${chalk.bold(`$${price}`)} (${periodMsg})?`
|
|
)
|
|
eraseLines(1)
|
|
if (!confirmation) {
|
|
info('Aborted')
|
|
gracefulExit()
|
|
}
|
|
elapsed = stamp()
|
|
stopSpinner = wait('Purchasing')
|
|
let domain
|
|
try {
|
|
domain = await domains.buy(parsed.domain)
|
|
} catch (err) {
|
|
stopSpinner()
|
|
treatBuyError(err)
|
|
gracefulExit()
|
|
}
|
|
|
|
stopSpinner()
|
|
success(`Domain purchased and created ${uid(domain.uid)} ${elapsed()}`)
|
|
|
|
stopSpinner = wait('Verifying nameservers')
|
|
|
|
let domainInfo
|
|
|
|
try {
|
|
domainInfo = await this.setupDomain(parsed.domain)
|
|
} catch (err) {
|
|
if (this._debug) {
|
|
console.log('> [debug] Error while trying to setup the domain', err)
|
|
}
|
|
}
|
|
|
|
stopSpinner()
|
|
|
|
if (!domainInfo.verified) {
|
|
const tld = param(`.${parsed.tld}`)
|
|
error(
|
|
'The nameservers are pending propagation. Please try again shortly'
|
|
)
|
|
info(
|
|
`The ${tld} servers might take some extra time to reflect changes`
|
|
)
|
|
gracefulExit()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
},
|
|
{ retries: 5 }
|
|
)
|
|
}
|
|
}
|
|
|