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.

300 lines
8.8 KiB

import retry from 'async-retry';
import dns from 'dns';
import Now from '../lib';
import toHost from './to-host';
import chalk from 'chalk';
import table from 'text-table';
import ms from 'ms';
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.retry(async (bail, attempt) => {
const res = await this._fetch(`/now/deployments/${target.uid}/aliases`);
const body = await res.json();
return body.aliases;
});
9 years ago
}
return this.retry(async (bail, attempt) => {
const res = await this._fetch('/now/aliases');
const body = await res.json();
return body.aliases;
});
9 years ago
}
9 years ago
async rm (alias) {
const _alias = await this.findAlias(alias);
if (!_alias) {
const err = new Error(`Alias not found by "${alias}". Run ${chalk.dim('`now alias ls`')} to see your aliases.`);
9 years ago
err.userError = true;
throw err;
}
if (await readConfirmation(_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;
}
console.log(`${chalk.cyan('> Success!')} The alias has been removed.`);
});
} else {
console.log('> Aborted');
}
}
async findAlias (alias) {
const list = await this.ls();
let key, val;
if (/\./.test(alias)) {
val = toHost(alias);
key = 'alias';
} else {
val = alias;
key = 'uid';
}
const _alias = list.find((d) => {
if (d[key] === val) {
if (this._debug) console.log(`> [debug] matched alias ${d.uid} by ${key} ${val}`);
return true;
}
// match prefix
if (`${val}.now.sh` === d.alias) {
if (this._debug) console.log(`> [debug] matched alias ${d.uid} by url ${d.host}`);
return true;
}
return false;
});
return _alias;
9 years ago
}
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) {
alias = alias.toLowerCase();
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 (!/\.now\.sh$/.test(alias)) {
console.log(`> ${chalk.bold(chalk.underline(alias))} is a custom domain.`);
console.log(`> Verifying that ${chalk.bold(chalk.underline(alias))} has a ${chalk.cyan('`CNAME`')} or ${chalk.cyan('`ALIAS`')} record pointing to ${chalk.bold(chalk.underline('alias.zeit.co'))}.`);
await this.verifyOwnership(alias);
}
9 years ago
await this.createAlias(depl, alias);
console.log(`${chalk.cyan('> Success!')} Deployment ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)} now points to ${chalk.bold(chalk.underline(`https://${alias}`))}`);
}
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 }
});
if (304 === res.status) return;
const body = await res.json();
if (this._debug) console.timeEnd(`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`);
// 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.cyan(alias))}`);
await this.createCert();
// 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');
}
});
}
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'));
}
const ips = await resolve4(domain);
if (!ips.length) {
const err = new Error('The domain ${domain} A record in the DNS configuration is not returning any IPs.');
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'))}. Please check your DNS settings.`);
err.userError = true;
return bail(err);
}
}
});
}
createCert (domain) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] /certs #${attempt}`);
const res = await this._fetch('/certs', {
method: 'POST',
body: {
domains: [domain]
}
});
const body = await res.json();
if (this._debug) console.timeEnd(`> [debug] /certs #${attempt}`);
if (body.error) {
const { code } = body.error;
if ('verification_failed' === code) {
const err = new Error(`We couldn't verify ownership of the domain ${domain}. Make sure the appropriate \`ALIAS\` or \`CNAME\` records are configured and pointing to ${chalk.bold('alias.zeit.co')}.`);
err.userError = true;
return bail(err);
}
throw new Error(body.message);
}
if (200 !== res.status && 304 !== res.status) {
throw new Error('Unhandled error');
}
});
}
retry (fn) {
return retry(fn, { retries: 5, randomize: true, onRetry: this._onRetry });
}
}
function resolve4 (host) {
return new Promise((resolve, reject) => {
return dns.resolve4(host, (err, answer) => {
if (err) return reject(err);
resolve(answer);
});
});
}
function readConfirmation (alias) {
return new Promise((resolve, reject) => {
const time = chalk.gray(ms(new Date() - new Date(alias.created)) + ' ago');
const tbl = table(
[[alias.uid, `https://${alias.alias}`, time]],
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(3) }
);
process.stdout.write('> The following alias will be removed permanently\n');
process.stdout.write(' ' + tbl + '\n');
process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`);
process.stdin.on('data', (d) => {
process.stdin.pause();
process.stdin.write('\n');
resolve('y' === d.toString().trim());
}).resume();
});
}