Browse Source
* 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 declarationmaster
Guillermo Rauch
9 years ago
committed by
GitHub
13 changed files with 631 additions and 112 deletions
@ -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; |
||||
|
}); |
||||
|
} |
@ -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); |
||||
|
}); |
||||
|
}); |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
@ -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')}.`; |
@ -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); |
||||
|
}); |
||||
|
} |
Loading…
Reference in new issue