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
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|