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.
648 lines
18 KiB
648 lines
18 KiB
// Packages
|
|
const { readFileSync } = require('fs');
|
|
const publicSuffixList = require('psl');
|
|
const minimist = require('minimist');
|
|
const chalk = require('chalk');
|
|
|
|
// Ours
|
|
const promptBool = require('../lib/utils/input/prompt-bool');
|
|
const exit = require('./utils/exit');
|
|
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 updatePathBasedroutes(alias, rules) {
|
|
alias = await this.maybeSetUpDomain(alias);
|
|
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) {
|
|
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 excists 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);
|
|
|
|
if (!confirmation) {
|
|
console.log('\n> 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.`
|
|
);
|
|
}
|
|
}
|
|
|
|
alias = 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)}`
|
|
);
|
|
}
|
|
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 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) {
|
|
// 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')}!`);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|