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.
 

433 lines
13 KiB

// Packages
const publicSuffixList = require('psl')
const minimist = require('minimist')
const chalk = require('chalk')
// Ours
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 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)) {
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 e = new Error(`> The domain ${domain} is already registered with now but additional verification is needed, please refer to 'now domain --help'.`)
e.userError = true
throw e
}
}
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, verifyToken, created} = await this.setupDomain(domain, {isExternal: true})
if (created && verified) {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domain))} ${chalk.dim(`(${uid})`)} added`)
} else if (verifyToken) {
const e = new Error(`> Verification required: Please add the following TXT record on the external DNS server: _now.${domain}: ${verifyToken}`)
e.userError = true
throw e
}
}
console.log(`> Verification ${chalk.bold('OK')}!`)
}
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 at ${chalk.underline('https://zeit.co/account')}.`)
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
})
}
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)
}
}
})
}
}