Browse Source

Add support for path based routes

master
Jarmo Isotalo 8 years ago
parent
commit
61962e841f
  1. 74
      bin/now-alias.js
  2. 364
      lib/alias.js

74
bin/now-alias.js

@ -18,11 +18,12 @@ const exit = require('../lib/utils/exit')
const logo = require('../lib/utils/output/logo')
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'],
string: ['config', 'token', 'rules'],
boolean: ['help', 'debug'],
alias: {
help: 'h',
config: 'c',
rules: 'r',
debug: 'd',
token: 't'
}
@ -37,10 +38,11 @@ const help = () => {
${chalk.dim('Options:')}
-h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token
-h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file
-r ${chalk.bold.underline('RULES_FILE')}, --rules=${chalk.bold.underline('RULES_FILE')} Rules file
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token
${chalk.dim('Examples:')}
@ -67,6 +69,16 @@ const help = () => {
${chalk.dim('–')} The subcommand ${chalk.dim('`set`')} is the default and can be skipped.
${chalk.dim('–')} ${chalk.dim('`http(s)://`')} in the URLs is unneeded / ignored.
${chalk.gray('–')} Add and modify path based aliases for ${chalk.underline('zeit.ninja')}:
${chalk.cyan(`$ now alias ${chalk.underline('zeit.ninja')} -r ${chalk.underline('rules.json')}`)}
Export effective routing rules:
${chalk.cyan(`$ now alias ls aliasId --json > ${chalk.underline('rules.json')}`)}
${chalk.cyan(`$ now alias ls zeit.ninja`)}
${chalk.gray('–')} Removing an alias:
${chalk.cyan('$ now alias rm aliasId')}
@ -117,7 +129,31 @@ async function run(token) {
switch (subcommand) {
case 'ls':
case 'list': {
if (args.length !== 0) {
if (args.length === 1) {
const list = await alias.listAliases()
const item = list.find(e => e.uid === argv._[1] || e.alias === argv._[1])
if (!item || !item.rules) {
error(`Could not match path alias for: ${argv._[1]}`)
return exit(1)
}
if (argv.json) {
console.log(JSON.stringify({rules: item.rules}, null, 2))
} else {
const header = [['', 'pathname', 'method', 'dest'].map(s => chalk.dim(s))]
const text = list.length === 0 ? null : table(header.concat(item.rules.map(rule => {
return [
'',
rule.pathname ? rule.pathname : '',
rule.method ? rule.method : '*',
rule.dest
]
})), {align: ['l', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen})
console.log(text)
}
break
} else if (args.length !== 0) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias ls`')}`)
return exit(1)
}
@ -133,7 +169,14 @@ async function run(token) {
const text = list.length === 0 ? null : table(header.concat(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>')
let _sourceUrl
if (urls.get(target)) {
_sourceUrl = chalk.underline(`https://${urls.get(target)}`)
} else if (_alias.rules) {
_sourceUrl = chalk.gray(`[${_alias.rules.length} custom rule${_alias.rules.length > 1 ? 's' : ''}]`)
} else {
_sourceUrl = chalk.gray('<null>')
}
const time = chalk.gray(ms(current - new Date(_alias.created)) + ' ago')
return [
@ -199,6 +242,10 @@ async function run(token) {
}
case 'add':
case 'set': {
if (argv.rules) {
await updatePathAlias(alias, argv._[0], argv.rules)
break
}
if (args.length !== 2) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias set <id> <domain>`')}`)
return exit(1)
@ -212,7 +259,9 @@ async function run(token) {
break
}
if (argv._.length === 2) {
if (argv.rules) {
await updatePathAlias(alias, argv._[0], argv.rules)
} else if (argv._.length === 2) {
await alias.set(String(argv._[0]), String(argv._[1]))
} else if (argv._.length >= 3) {
error('Invalid number of arguments')
@ -287,3 +336,12 @@ function findAlias(alias, list) {
return _alias
}
async function updatePathAlias(alias, aliasName, rules) {
const start = new Date()
const res = await alias.updatePathBasedroutes(String(aliasName), rules)
const elapsed = ms(new Date() - start)
if (!res.error) {
console.log(`${chalk.cyan('> Success!')} ${res.ruleCount} rules configured for ${chalk.underline(res.alias)} [${elapsed}]`)
}
}

364
lib/alias.js

@ -1,9 +1,11 @@
// Packages
const {readFileSync} = require('fs')
const publicSuffixList = require('psl')
const minimist = require('minimist')
const chalk = require('chalk')
// Ours
const exit = require('./utils/exit')
const copy = require('./copy')
const toHost = require('./to-host')
const resolve4 = require('./dns')
@ -93,146 +95,153 @@ module.exports = class Alias extends Now {
return depl
}
async set(deployment, alias) {
// make alias lowercase
alias = alias.toLowerCase()
async updatePathBasedroutes(alias, rules) {
await this.maybeSetUpDomain(alias)
// 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.`)
err.userError = true
throw err
}
return await this.upsertPathAlias(alias, rules)
}
// evaluate the alias
if (/\./.test(alias)) {
alias = toHost(alias)
} else {
async upsertPathAlias(alias, rules) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.log(`> [debug] suffixing \`.now.sh\` to alias ${alias}`)
console.time(`> [debug] /now/aliases #${attempt}`)
}
alias = `${alias}.now.sh`
}
const rulesData = this.readRulesFile(rules)
const ruleCount = rulesData.rules.length
const res = await this._fetch(`/now/aliases`, {
method: 'POST',
body: {alias, rules: rulesData.rules}
})
if (!domainRegex.test(alias)) {
const err = new Error(`Invalid alias "${alias}"`)
err.userError = true
throw err
}
const body = await res.json()
body.ruleCount = ruleCount
if (this._debug) {
console.timeEnd(`> [debug] /now/aliases #${attempt}`)
}
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)`)
// 409 conflict is returned if it already exists
if (res.status === 409) {
return {uid: body.error.uid}
}
if (res.status === 422) {
console.log(body.error.message)
return new Error(body.error.message)
}
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
// no retry on authorization problems
if (res.status === 403) {
const code = body.error.code
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 (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 (!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`)
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)
}
}
try {
if (!skipDNSVerification) {
await this.verifyOwnership(alias)
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)
}
} 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(/\.$/, '')
return bail(new Error('Authorization error'))
}
if (_record === '') {
await this.setupRecord(_domain, '*')
}
// all other errors
if (body.error) {
const code = body.error.code
await this.setupRecord(_domain, _record)
if (code === 'cert_missing') {
console.log(`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`)
this.recordSetup = true
console.log('> DNS Configured! Verifying propagation…')
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 {
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
}
// try again, but now having provisioned the certificate
throw err
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)
}
} else {
throw err
}
return bail(new Error(body.error.message))
}
if (!usingZeitWorld && !skipDNSVerification) {
if (this._debug) {
console.log(`> [debug] Trying to register a non-ZeitWorld domain ${domain} for the current user`)
}
// the two expected succesful cods are 200 and 304
if (res.status !== 200 && res.status !== 304) {
throw new Error('Unhandled error')
}
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
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 confirmation = (await new Promise(resolve => {
process.stdout.write(`Path alias excists with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? 's' : ''}.\n`)
process.stdout.write(`Are you sure you want to update ${alias} to be a normal alias?\n`)
process.stdin.on('data', d => {
process.stdin.pause()
resolve(d.toString().trim())
}).resume()
})).toLowerCase()
if (confirmation !== 'y' && confirmation !== 'yes') {
console.log('\n> Aborted')
return exit(1)
}
} catch (err) {
console.log(err)
}
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domain))} ${chalk.dim(`(${uid})`)} added`)
} else {
console.log(`Overwriting path alias with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? 's' : ''} to be a normal alias.`)
}
console.log(`> Verification ${chalk.bold('OK')}!`)
}
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)}`)
@ -391,6 +400,139 @@ module.exports = class Alias extends Now {
})
}
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')}!`)
}
}
verifyOwnership(domain) {
return this.retry(async bail => {
const targets = await resolve4('alias.zeit.co')

Loading…
Cancel
Save