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