Browse Source

Add zeit world (wip) (#67)

* alias: clean up the alias (trailing and leading dots)

* alias: improve domain validation and implement zeit.world

* is-zeit-world: detect valid zeit.world nameservers

* package: add domain-regex dep

* now-alias: fix edge case with older aliases or removed deployments

* alias: move listing aliases and retrying to `index`

* index: generalize retrying and alias listing

* alias: remove `retry` dep

* now-remove: print out alias warning

* typo

* now-alias: prevent double lookup

* now: add domain / domains command

* now-deploy: document `domain`

* agent: allow for tls-less, agent-less requests while testing

* is-zeit-world: fix nameserver typo

* dns: as-promised

* now-alias: fix `rm` table

* now-alias: no longer admit argument after `alias ls`

@rase- please verify this, but I think it was overkill?

* admit "aliases" as an alternative to alias

* make domain name resolution, creation and verification reusable

* index: add nameserver discover, domain setup functions (reused between alias and domains)

* now-domains: add command

* domains: commands

* package: bump eslint

* now-alias: simplify sort

* now-domains: sort list

* now-domains: improve deletion and output of empty elements

* errors: improve output

* domains: add more debug output

* domains: extra logging

* errors: improve logging

* now-remove: improve `now rm` error handling

* index: more reasonable retrying

* alias: support empty dns configurations

* dns: remove ns fn

* alias: improve post-dns-editing verification

* index: remove unneeded dns lookup

* index: implement new `getNameservers`

* index: customizable retries

* alias: improve error

* improve error handling and cert retrying

* customizable retries

* alias: better error handling

* alias: display both error messages

* better error handling

* improve error handling for certificate verification errors

* alias: set up a `*` CNAME to simplify further aliases

* alias: fewer retries for certs, more spaced out (prevent rate limiting issues)

* alias: ultimate error handling

* add whois fallback

* adjust timer labels for whois fallback

* index: whois fallback also upon 500 errors

* alias: fix error message

* fix duplicate aliases declaration
master
Guillermo Rauch 9 years ago
committed by GitHub
parent
commit
eb04657b2f
  1. 4
      bin/now
  2. 97
      bin/now-alias
  3. 3
      bin/now-deploy
  4. 244
      bin/now-domains
  5. 15
      bin/now-remove
  6. 21
      lib/agent.js
  7. 156
      lib/alias.js
  8. 10
      lib/dns.js
  9. 68
      lib/domains.js
  10. 11
      lib/errors.js
  11. 80
      lib/index.js
  12. 27
      lib/is-zeit-world.js
  13. 7
      package.json

4
bin/now

@ -19,8 +19,8 @@ const exit = (code) => {
};
const defaultCommand = 'deploy';
const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'ln']);
const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias']]);
const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'aliases', 'ln', 'domain', 'domains']);
const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias'], ['aliases', 'alias'], ['domain', 'domains']]);
let cmd = argv._[0];
let args = [];

97
bin/now-alias

@ -107,42 +107,35 @@ async function run (token) {
switch (subcommand) {
case 'list':
case 'ls':
const list = await alias.list();
const urls = new Map(list.map(l => [l.uid, l.url]));
const target = null != args[0] ? String(args[0]) : null;
const aliases = await alias.ls(target);
const byTarget = new Map();
if (target) {
byTarget.set(target, aliases);
} else {
aliases.forEach((_alias) => {
const _aliases = byTarget.get(_alias.deploymentId) || [];
byTarget.set(_alias.deploymentId, _aliases.concat(_alias));
});
if (0 !== args.length) {
error('Invalid number of arguments');
return exit(1);
}
const sorted = await sort([...byTarget]);
const list = await alias.list();
const urls = new Map(list.map(l => [l.uid, l.url]));
const aliases = await alias.ls();
aliases.sort((a, b) => new Date(b.created) - new Date(a.created));
const current = new Date();
const text = sorted.map(([target, _aliases]) => {
return table(_aliases.map((_alias) => {
const _url = chalk.underline(`https://${_alias.alias}`);
const _sourceUrl = urls.get(target)
? chalk.underline(`https://${urls.get(target)}`)
: chalk.gray('<null>');
const time = chalk.gray(ms(current - new Date(_alias.created)) + ' ago');
return [
// we default to `''` because some early aliases didn't
// have an uid associated
null == _alias.uid ? '' : _alias.uid,
_sourceUrl,
_url,
time
];
}), { align: ['l', 'r', 'l'], hsep: ' '.repeat(3) });
}).join('\n\n');
const text = table(aliases.map((_alias) => {
const _url = chalk.underline(`https://${_alias.alias}`);
const target = _alias.deploymentId;
const _sourceUrl = urls.get(target)
? chalk.underline(`https://${urls.get(target)}`)
: chalk.gray('<null>');
const time = chalk.gray(ms(current - new Date(_alias.created)) + ' ago');
return [
'',
// we default to `''` because some early aliases didn't
// have an uid associated
null == _alias.uid ? '' : _alias.uid,
_sourceUrl,
_url,
time
];
}), { align: ['l', 'r', 'l'], hsep: ' '.repeat(3) });
if (text) console.log('\n' + text + '\n');
break;
@ -156,6 +149,11 @@ async function run (token) {
throw err;
}
if (1 !== args.length) {
error('Invalid number of arguments');
return exit(1);
}
const _aliases = await alias.ls();
const _alias = findAlias(_target, _aliases);
@ -166,7 +164,7 @@ async function run (token) {
}
try {
const confirmation = (await readConfirmation(alias, _alias)).toLowerCase();
const confirmation = (await readConfirmation(alias, _alias, _aliases)).toLowerCase();
if ('y' !== confirmation && 'yes' !== confirmation) {
console.log('\n> Aborted');
process.exit(0);
@ -187,7 +185,7 @@ async function run (token) {
case 'set':
if (2 !== args.length) {
error('Invalid number of arguments');
return;
return exit(1);
}
await alias.set(String(args[0]), String(args[1]));
break;
@ -209,36 +207,13 @@ async function run (token) {
alias.close();
}
async function sort (aliases) {
let pkg;
try {
const json = await fs.readFile('package.json');
pkg = JSON.parse(json);
} catch (err) {
pkg = {};
}
return aliases
.map(([target, _aliases]) => {
_aliases = _aliases.slice().sort((a, b) => {
return b.created - a.created;
});
return [target, _aliases];
})
.sort(([targetA, aliasesA], [targetB, aliasesB]) => {
if (pkg.name === targetA) return -1;
if (pkg.name === targetB) return 1;
return aliasesB[0].created - aliasesA[0].created;
});
}
function indent (text, n) {
return text.split('\n').map((l) => ' '.repeat(n) + l).join('\n');
}
async function readConfirmation (alias, _alias) {
const list = await alias.list();
const urls = new Map(list.map(l => [l.uid, l.url]));
async function readConfirmation (alias, _alias, list) {
const deploymentsList = await alias.list();
const urls = new Map(deploymentsList.map(l => [l.uid, l.url]));
return new Promise((resolve, reject) => {
const time = chalk.gray(ms(new Date() - new Date(_alias.created)) + ' ago');

3
bin/now-deploy

@ -35,7 +35,8 @@ const help = () => {
deploy [path] performs a deployment ${chalk.bold('(default)')}
ls | list [app] list deployments
rm | remove [id] remove a deployment
ln | alias [id] [url] configures an alias / domain
ln | alias [id] [url] configures aliases for deployments
domains [name] manages your domain names
help [cmd] displays complete help for [cmd]
${chalk.dim('Options:')}

244
bin/now-domains

@ -0,0 +1,244 @@
#!/usr/bin/env node
import chalk from 'chalk';
import minimist from 'minimist';
import table from 'text-table';
import ms from 'ms';
import login from '../lib/login';
import * as cfg from '../lib/cfg';
import { error } from '../lib/error';
import toHost from '../lib/to-host';
import NowDomains from '../lib/domains';
const argv = minimist(process.argv.slice(2), {
boolean: ['help', 'debug'],
alias: {
help: 'h',
debug: 'd'
}
});
const subcommand = argv._[0];
// options
const help = () => {
console.log(`
${chalk.bold('𝚫 now domains')} <ls | set | rm> <domain>
${chalk.dim('Options:')}
-h, --help output usage information
-d, --debug debug mode [off]
${chalk.dim('Examples:')}
${chalk.gray('–')} Lists all your domains:
${chalk.cyan('$ now domains ls')}
${chalk.gray('–')} Adds a domain name:
${chalk.cyan(`$ now domains add ${chalk.underline('my-app.com')}`)}
Make sure the domain's DNS nameservers are at least 2 of these:
${chalk.gray('–')} ${chalk.underline('california.zeit.world')} ${chalk.dim('173.255.215.107')}
${chalk.gray('–')} ${chalk.underline('london.zeit.world')} ${chalk.dim('178.62.47.76')}
${chalk.gray('–')} ${chalk.underline('newark.zeit.world')} ${chalk.dim('173.255.231.87')}
${chalk.gray('–')} ${chalk.underline('amsterdam.zeit.world')} ${chalk.dim('188.226.197.55')}
${chalk.gray('–')} ${chalk.underline('dallas.zeit.world')} ${chalk.dim('173.192.101.194')}
${chalk.gray('–')} ${chalk.underline('paris.zeit.world')} ${chalk.dim('37.123.115.172')}
${chalk.gray('–')} ${chalk.underline('singapore.zeit.world')} ${chalk.dim('119.81.97.170')}
${chalk.gray('–')} ${chalk.underline('sydney.zeit.world')} ${chalk.dim('52.64.171.200')}
${chalk.gray('–')} ${chalk.underline('frankfurt.zeit.world')} ${chalk.dim('91.109.245.139')}
${chalk.gray('–')} ${chalk.underline('iowa.zeit.world')} ${chalk.dim('23.236.59.22')}
${chalk.yellow('NOTE:')} running ${chalk.dim('`now alias`')} will automatically register your domain
if it's configured with these nameservers (no need to ${chalk.dim('`domain add`')}).
For more details head to ${chalk.underline('https://zeit.world')}.
${chalk.gray('–')} Removing a domain:
${chalk.cyan('$ now domain rm my-app.com')}
or
${chalk.cyan('$ now domain rm domainId')}
To get the list of domain ids, use ${chalk.dim('`now domains ls`')}.
`);
};
// options
const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co';
const exit = (code) => {
// we give stdout some time to flush out
// because there's a node bug where
// stdout writes are asynchronous
// https://github.com/nodejs/node/issues/6456
setTimeout(() => process.exit(code || 0), 100);
};
if (argv.help || !subcommand) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(config.token || login(apiUrl))
.then(async (token) => {
try {
await run(token);
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err.stack}`);
}
exit(1);
}
})
.catch((e) => {
error(`Authentication error – ${e.message}`);
exit(1);
});
}
async function run (token) {
const domain = new NowDomains(apiUrl, token, { debug });
const args = argv._.slice(1);
switch (subcommand) {
case 'ls':
case 'list':
if (0 !== args.length) {
error('Invalid number of arguments');
return exit(1);
}
const domains = await domain.ls();
domains.sort((a, b) => new Date(b.created) - new Date(a.created));
const current = new Date();
const out = table(domains.map((domain) => {
const url = chalk.underline(`https://${domain.name}`);
const time = chalk.gray(ms(current - new Date(domain.created)) + ' ago');
return [
'',
domain.uid,
url,
time
];
}), { align: ['l', 'r', 'l'], hsep: ' '.repeat(3) });
if (out) console.log('\n' + out + '\n');
break;
case 'rm':
case 'remove':
if (1 !== args.length) {
error('Invalid number of arguments');
return exit(1);
}
const _target = String(args[0]);
if (!_target) {
const err = new Error('No domain specified');
err.userError = true;
throw err;
}
const _domains = await domain.ls();
const _domain = findDomain(_target, _domains);
if (!_domain) {
const err = new Error(`Domain not found by "${_target}". Run ${chalk.dim('`now domains ls`')} to see your domains.`);
err.userError = true;
throw err;
}
try {
const confirmation = (await readConfirmation(domain, _domain, _domains)).toLowerCase();
if ('y' !== confirmation && 'yes' !== confirmation) {
console.log('\n> Aborted');
process.exit(0);
}
const start = new Date();
await domain.rm(_domain.name);
const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(_domain.uid)} removed [${elapsed}]`);
} catch (err) {
error(err);
exit(1);
}
break;
case 'add':
case 'set':
if (1 !== args.length) {
error('Invalid number of arguments');
return exit(1);
}
const start = new Date();
const name = String(args[0]);
const { uid, created } = await domain.add(name);
const elapsed = ms(new Date() - start);
if (created) {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} added [${elapsed}]`);
} else {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} already exists [${elapsed}]`);
}
break;
default:
error('Please specify a valid subcommand: ls | add | rm');
help();
exit(1);
}
domain.close();
}
function indent (text, n) {
return text.split('\n').map((l) => ' '.repeat(n) + l).join('\n');
}
async function readConfirmation (domain, _domain, list) {
const urls = new Map(list.map(l => [l.uid, l.url]));
return new Promise((resolve, reject) => {
const time = chalk.gray(ms(new Date() - new Date(_domain.created)) + ' ago');
const tbl = table(
[[_domain.uid, chalk.underline(`https://${_domain.name}`), time]],
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
);
process.stdout.write('> The following deployment 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();
resolve(d.toString().trim());
}).resume();
});
}
function findDomain (val, list) {
return list.find((d) => {
if (d.uid === val) {
if (debug) console.log(`> [debug] matched domain ${d.uid} by uid`);
return true;
}
// match prefix
if (d.name === toHost(val)) {
if (debug) console.log(`> [debug] matched domain ${d.uid} by name ${d.name}`);
return true;
}
return false;
});
}

15
bin/now-remove

@ -51,11 +51,11 @@ const hard = argv.hard || false;
const config = cfg.read();
function readConfirmation (app, aliases) {
function readConfirmation (depl, aliases) {
return new Promise((resolve, reject) => {
const time = chalk.gray(ms(new Date() - app.created) + ' ago');
const time = chalk.gray(ms(new Date() - depl.created) + ' ago');
const tbl = table(
[[app.uid, chalk.underline(`https://${app.url}`), time]],
[[depl.uid, chalk.underline(`https://${depl.url}`), time]],
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
);
@ -95,12 +95,15 @@ async function remove (token) {
const now = new Now(apiUrl, token, { debug });
const deployments = await now.list();
const app = deployments.find((d) => d.uid === deploymentId);
const depl = deployments.find((d) => d.uid === deploymentId);
if (null != deploymentId && !depl) {
error(`Could not find a deployment by ${chalk.bold(`"${deploymentId}"`)}. Run ${chalk.dim(`\`now ls\``)} to list.`);
return process.exit(1);
}
const aliases = await now.listAliases(app.uid);
try {
const confirmation = (await readConfirmation(app, aliases)).toLowerCase();
const confirmation = (await readConfirmation(depl, aliases)).toLowerCase();
if ('y' !== confirmation && 'yes' !== confirmation) {
console.log('\n> Aborted');
process.exit(0);

21
lib/agent.js

@ -14,13 +14,14 @@ import { parse } from 'url';
*/
export default class Agent {
constructor (url, { debug } = {}) {
constructor (url, { tls = true, debug } = {}) {
this._url = url;
const { protocol, host } = parse(url);
this._protocol = protocol;
this._host = host;
const parsed = parse(url);
this._host = parsed.hostname;
this._port = parsed.port;
this._protocol = parsed.protocol;
this._debug = debug;
this._initAgent();
if (tls) this._initAgent();
}
_initAgent () {
@ -28,7 +29,7 @@ export default class Agent {
this._agent = http2.createAgent({
host: this._host,
port: 443
port: this._port || 443
}).once('error', (err) => this._onError(err));
}
@ -48,7 +49,9 @@ export default class Agent {
}
const { body } = opts;
opts.agent = this._agent;
if (this._agent) {
opts.agent = this._agent;
}
if (body && 'object' === typeof body && 'function' !== typeof body.pipe) {
opts.headers['Content-Type'] = 'application/json';
@ -64,6 +67,8 @@ export default class Agent {
close () {
if (this._debug) console.log('> [debug] closing agent');
if (this._agent) return this._agent.close();
if (this._agent) {
this._agent.close();
}
}
}

156
lib/alias.js

@ -1,7 +1,12 @@
import dns from 'dns';
import Now from '../lib';
import Now from './';
import toHost from './to-host';
import chalk from 'chalk';
import isZeitWorld from './is-zeit-world';
import _domainRegex from 'domain-regex';
import { DOMAIN_VERIFICATION_ERROR } from './errors';
import { resolve4 } from './dns';
const domainRegex = _domainRegex();
export default class Alias extends Now {
@ -67,8 +72,15 @@ export default class Alias extends Now {
}
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.`);
@ -84,16 +96,61 @@ export default class Alias extends Now {
alias = toHost(alias);
}
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 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);
console.log(`> Verifying the DNS settings for ${chalk.bold(chalk.underline(alias))} (see ${chalk.underline('https://zeit.world')} for help)`);
try {
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 {
const { domain, nameservers } = await this.getNameservers(alias);
if (this._debug) console.log(`> [debug] Found domain ${domain} and nameservers ${nameservers}`);
if (isZeitWorld(nameservers)) {
console.log(`> Detected ${chalk.bold(chalk.underline('zeit.world'))} nameservers! Configuring records.`);
const record = alias.substr(0, alias.length - domain.length);
await this.setupRecord(domain, record);
this.recordSetup = true;
console.log('> DNS Configured! Verifying propagation…');
try {
await this.retry(() => this.verifyOwnership(alias), { retries: 10, maxTimeout: 3000 });
} 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;
}
}
console.log(`> Verification ${chalk.bold('OK')}!`);
}
const { created, uid } = await this.createAlias(depl, alias);
if (created) {
console.log(`${chalk.cyan('> Success!')} Alias created ${chalk.dim(`(${uid})`)}: ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)} now points to ${chalk.bold(chalk.underline(`https://${alias}`))}`);
console.log(`${chalk.cyan('> Success!')} Alias created ${chalk.dim(`(${uid})`)}: ${chalk.bold(chalk.underline(`https://${alias}`))} now points to ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)}`);
} else {
console.log(`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(`(${uid})`)}.`);
}
@ -142,7 +199,15 @@ export default class Alias extends Now {
if ('cert_missing' === code) {
console.log(`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`);
await this.createCert(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);
@ -160,6 +225,43 @@ export default class Alias extends Now {
});
}
async setupRecord (domain, name) {
await this.setupDomain(domain);
// lean up trailing and leading dots
name = name.replace(/^\./, '');
name = name.replace(/\.$/, '');
domain = domain.replace(/^\./, '');
domain = domain.replace(/\.$/, '');
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 (403 === res.status) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (200 !== res.status) {
throw new Error(body.error.message);
}
return body;
});
}
verifyOwnership (domain) {
return this.retry(async (bail, attempt) => {
const targets = await resolve4('alias.zeit.co');
@ -168,16 +270,28 @@ export default class Alias extends Now {
return bail(new Error('Unable to resolve alias.zeit.co'));
}
const ips = await resolve4(domain);
let ips = [];
try {
ips = await resolve4(domain);
} catch (err) {
if ('ENODATA' === err.code || 'ENOTFOUND' === err.code) {
// not errors per se, just absence of records
if (this._debug) console.log(`> [debug] No records found for "${domain}"`);
} else {
throw err;
}
}
if (!ips.length) {
const err = new Error('The domain ${domain} A record in the DNS configuration is not returning any IPs.');
const err = new Error(DOMAIN_VERIFICATION_ERROR);
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.`);
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);
}
@ -207,8 +321,15 @@ export default class Alias extends Now {
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')}.`);
const err = new Error('The certificate issuer failed to verify ownership of the domain. ' +
'This likely has to do with DNS propagation and caching issues. Please retry later!');
err.userError = true;
// retry
throw err;
} else if ('rate_limited' === code) {
const err = new Error(body.error.message);
err.userError = true;
// dont retry
return bail(err);
}
@ -218,20 +339,7 @@ export default class Alias extends Now {
if (200 !== res.status && 304 !== res.status) {
throw new Error('Unhandled error');
}
});
}, { retries: 5, minTimeout: 30000, maxTimeout: 90000 });
}
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);
});
});
}

10
lib/dns.js

@ -0,0 +1,10 @@
import dns from 'dns';
export function resolve4 (host) {
return new Promise((resolve, reject) => {
return dns.resolve4(host, (err, answer) => {
if (err) return reject(err);
resolve(answer);
});
});
}

68
lib/domains.js

@ -0,0 +1,68 @@
import Now from '../lib';
import isZeitWorld from './is-zeit-world';
import _domainRegex from 'domain-regex';
import chalk from 'chalk';
import { DNS_VERIFICATION_ERROR } from './errors';
const domainRegex = _domainRegex();
export default class Domains extends Now {
async ls () {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] #${attempt} GET /domains`);
const res = await this._fetch('/domains');
if (this._debug) console.timeEnd(`> [debug] #${attempt} GET /domains`);
const body = await res.json();
return body.domains;
});
}
async rm (name) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] #${attempt} DELETE /domains/${name}`);
const res = await this._fetch(`/domains/${name}`, { method: 'DELETE' });
if (this._debug) console.timeEnd(`> [debug] #${attempt} DELETE /domains/${name}`);
if (403 === res.status) {
return bail(new Error('Unauthorized'));
}
if (res.status !== 200) {
const body = await res.json();
throw new Error(body.error.message);
}
});
}
async add (domain) {
if (!domainRegex.test(domain)) {
const err = new Error(`The supplied value ${chalk.bold(`"${domain}"`)} is not a valid domain.`);
err.userError = true;
throw err;
}
let ns;
try {
console.log('> Verifying nameservers…');
const res = await this.getNameservers(domain);
ns = res.nameservers;
} catch (err) {
const err2 = new Error(`Unable to fetch nameservers for ${chalk.underline(chalk.bold(domain))}.`);
err2.userError = true;
throw err2;
}
if (isZeitWorld(ns)) {
console.log(`> Verification ${chalk.bold('OK')}!`);
return this.setupDomain(domain);
} else {
if (this._debug) console.log(`> [debug] Supplied domain "${domain}" has non-zeit nameservers`);
const err3 = new Error(DNS_VERIFICATION_ERROR);
err3.userError = true;
throw err3;
}
}
}

11
lib/errors.js

@ -0,0 +1,11 @@
import chalk from 'chalk';
export const DNS_VERIFICATION_ERROR = `Please make sure that your nameservers point to ${chalk.underline('zeit.world')}.
> Examples: (full list at ${chalk.underline('https://zeit.world')})
> ${chalk.gray('-')} ${chalk.underline('california.zeit.world')} ${chalk.dim('173.255.215.107')}
> ${chalk.gray('-')} ${chalk.underline('newark.zeit.world')} ${chalk.dim('173.255.231.87')}
> ${chalk.gray('-')} ${chalk.underline('london.zeit.world')} ${chalk.dim('178.62.47.76')}
> ${chalk.gray('-')} ${chalk.underline('singapore.zeit.world')} ${chalk.dim('119.81.97.170')}`;
export const DOMAIN_VERIFICATION_ERROR = DNS_VERIFICATION_ERROR +
`\n> Alternatively, ensure it resolves to ${chalk.underline('alias.zeit.co')} via ${chalk.dim('CNAME')} / ${chalk.dim('ALIAS')}.`;

80
lib/index.js

@ -6,7 +6,7 @@ import hash from './hash';
import retry from 'async-retry';
import Agent from './agent';
import EventEmitter from 'events';
import { basename, resolve } from 'path';
import { basename, resolve as resolvePath } from 'path';
import { stat, readFile } from 'fs-promise';
import resumer from 'resumer';
import splitArray from 'split-array';
@ -40,7 +40,7 @@ export default class Now extends EventEmitter {
}
let pkg;
try {
pkg = await readFile(resolve(path, 'package.json'));
pkg = await readFile(resolvePath(path, 'package.json'));
pkg = JSON.parse(pkg);
} catch (err) {
const e = Error(`Failed to read JSON in "${path}/package.json"`);
@ -112,7 +112,7 @@ export default class Now extends EventEmitter {
}
return res.json();
}, { retries: 3, minTimeout: 2500, onRetry: this._onRetry });
});
// we report about files whose sizes are too big
let missingVersion = false;
@ -199,7 +199,7 @@ export default class Now extends EventEmitter {
}
this.emit('upload', file);
}, { retries: 5, randomize: true, onRetry: this._onRetry })))
}, { retries: 3, randomize: true, onRetry: this._onRetry })))
.then(() => parts.length ? uploadChunk() : this.emit('complete'))
.catch((err) => this.emit('error', err));
};
@ -243,6 +243,72 @@ export default class Now extends EventEmitter {
});
}
getNameservers (domain, { fallback = false } = {}) {
try {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] #${attempt} GET /whois-ns${fallback ? '-fallback' : ''}`);
const res = await this._fetch(`/whois-ns${fallback ? '-fallback' : ''}?domain=${encodeURIComponent(domain)}`);
if (this._debug) console.timeEnd(`> [debug] #${attempt} GET /whois-ns${fallback ? '-fallback' : ''}`);
const body = await res.json();
if (200 === res.status) {
if (!body.nameservers && !fallback) {
// if the nameservers are `null` it's likely
// that our whois service failed to parse it
return this.getNameservers(domain, { fallback: true });
}
return body;
} else {
throw new Error(`Whois error (${res.status}): ${body.error.message}`);
}
});
} catch (err) {
if (fallback) throw err;
return this.getNameservers(domain, { fallback: true });
}
}
// _ensures_ the domain is setup (idempotent)
setupDomain (name) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] #${attempt} POST /domains`);
const res = await this._fetch('/domains', {
method: 'POST',
body: { name }
});
if (this._debug) console.timeEnd(`> [debug] #${attempt} POST /domains`);
if (403 === res.status) {
const body = await res.json();
const code = body.error.code;
let err;
if ('custom_domain_needs_upgrade' === code) {
err = new Error(`Custom domains are only enabled for premium accounts. Please upgrade at ${chalk.underline('https://zeit.co/account')}.`);
} else {
err = new Error(`Not authorized to access domain ${name}`);
}
err.userError = true;
return bail(err);
}
const body = await res.json();
// domain already exists
if (409 === res.status) {
if (this._debug) console.log('> [debug] Domain already exists (noop)');
return { uid: body.error.uid };
}
if (200 !== res.status) {
throw new Error(body.error.message);
}
return body;
});
}
async remove (deploymentId, { hard }) {
const data = { deploymentId, hard };
@ -270,10 +336,10 @@ export default class Now extends EventEmitter {
return true;
}
retry (fn) {
retry (fn, { retries = 3, maxTimeout = Infinity } = {}) {
return retry(fn, {
retries: 5,
randomize: true,
retries,
maxTimeout,
onRetry: this._onRetry
});
}

27
lib/is-zeit-world.js

@ -0,0 +1,27 @@
/**
* List of `zeit.world` nameservers
*/
const nameservers = new Set([
'california.zeit.world',
'london.zeit.world',
'newark.zeit.world',
'sydney.zeit.world',
'iowa.zeit.world',
'dallas.zeit.world',
'amsterdam.zeit.world',
'paris.zeit.world',
'frankfurt.zeit.world',
'singapore.zeit.world'
]);
/**
* Given an array of nameservers (ie: as returned
* by `resolveNs` from Node, assert that they're
* zeit.world's.
*/
export default function isZeitWorld (ns) {
return ns.length > 1 && ns.every((host) => {
return nameservers.has(host);
});
}

7
package.json

@ -33,7 +33,8 @@
"spdy": "3.3.3",
"email-validator": "1.0.4",
"email-prompt": "0.1.8",
"async-retry": "0.2.1"
"async-retry": "0.2.1",
"domain-regex": "0.0.1"
},
"devDependencies": {
"alpha-sort": "1.0.2",
@ -53,8 +54,8 @@
"gulp-ext": "1.0.0",
"gulp-task-listing": "1.0.1",
"gulp-uglify": "1.5.3",
"eslint": "2.11.0",
"eslint-plugin-promise": "1.3.1",
"eslint": "2.12.0",
"eslint-plugin-promise": "1.3.2",
"estraverse-fb": "1.3.1"
},
"scripts": {

Loading…
Cancel
Save