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.
649 lines
18 KiB
649 lines
18 KiB
// Packages
|
|
const { readFileSync } = require("fs");
|
|
const publicSuffixList = require("psl");
|
|
const minimist = require("minimist");
|
|
const chalk = require("chalk");
|
|
|
|
// Ours
|
|
const promptBool = require("../lib/utils/input/prompt-bool");
|
|
const exit = require("./utils/exit");
|
|
const copy = require("./copy");
|
|
const toHost = require("./to-host");
|
|
const resolve4 = require("./dns");
|
|
const isZeitWorld = require("./is-zeit-world");
|
|
const { DOMAIN_VERIFICATION_ERROR } = require("./errors");
|
|
const Now = require("./");
|
|
|
|
const argv = minimist(process.argv.slice(2), {
|
|
boolean: ["no-clipboard"],
|
|
alias: { "no-clipboard": "C" }
|
|
});
|
|
|
|
const isTTY = process.stdout.isTTY;
|
|
const clipboard = !argv["no-clipboard"];
|
|
const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/;
|
|
|
|
module.exports = 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.listAliases(target.uid);
|
|
}
|
|
|
|
return this.listAliases();
|
|
}
|
|
|
|
async rm(_alias) {
|
|
return this.retry(async bail => {
|
|
const res = await this._fetch(`/now/aliases/${_alias.uid}`, {
|
|
method: "DELETE"
|
|
});
|
|
|
|
if (res.status === 403) {
|
|
return bail(new Error("Unauthorized"));
|
|
}
|
|
|
|
if (res.status !== 200) {
|
|
const err = new Error("Deletion failed. Try again later.");
|
|
throw err;
|
|
}
|
|
});
|
|
}
|
|
|
|
async findDeployment(deployment) {
|
|
const list = await this.list();
|
|
|
|
let key;
|
|
let 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 updatePathBasedroutes(alias, rules) {
|
|
alias = await this.maybeSetUpDomain(alias);
|
|
|
|
return await this.upsertPathAlias(alias, rules);
|
|
}
|
|
|
|
async upsertPathAlias(alias, rules) {
|
|
return this.retry(async (bail, attempt) => {
|
|
if (this._debug) {
|
|
console.time(`> [debug] /now/aliases #${attempt}`);
|
|
}
|
|
|
|
const rulesData = this.readRulesFile(rules);
|
|
const ruleCount = rulesData.rules.length;
|
|
const res = await this._fetch(`/now/aliases`, {
|
|
method: "POST",
|
|
body: { alias, rules: rulesData.rules }
|
|
});
|
|
|
|
const body = await res.json();
|
|
body.ruleCount = ruleCount;
|
|
if (this._debug) {
|
|
console.timeEnd(`> [debug] /now/aliases #${attempt}`);
|
|
}
|
|
|
|
// 409 conflict is returned if it already exists
|
|
if (res.status === 409) {
|
|
return { uid: body.error.uid };
|
|
}
|
|
if (res.status === 422) {
|
|
return body;
|
|
}
|
|
|
|
// no retry on authorization problems
|
|
if (res.status === 403) {
|
|
const code = body.error.code;
|
|
|
|
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 (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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
return bail(new Error("Authorization error"));
|
|
}
|
|
|
|
// all other errors
|
|
if (body.error) {
|
|
const code = body.error.code;
|
|
|
|
if (code === "cert_missing") {
|
|
console.log(
|
|
`> Provisioning certificate for ${chalk.underline(chalk.bold(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.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);
|
|
}
|
|
}
|
|
|
|
return bail(new Error(body.error.message));
|
|
}
|
|
|
|
// the two expected succesful cods are 200 and 304
|
|
if (res.status !== 200 && res.status !== 304) {
|
|
throw new Error("Unhandled error");
|
|
}
|
|
|
|
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 msg = `> Path alias excists with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? "s" : ""}.\n` +
|
|
`> Are you sure you want to update ${alias} to be a normal alias?\n`;
|
|
|
|
const confirmation = await promptBool(msg);
|
|
|
|
if (!confirmation) {
|
|
console.log("\n> Aborted");
|
|
return exit(1);
|
|
}
|
|
} catch (err) {
|
|
console.log(err);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`Overwriting path alias with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? "s" : ""} to be a normal alias.`
|
|
);
|
|
}
|
|
}
|
|
|
|
alias = 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)}`
|
|
);
|
|
}
|
|
const { created, uid } = newAlias;
|
|
if (created) {
|
|
const pretty = `https://${alias}`;
|
|
const output = `${chalk.cyan("> Success!")} Alias created ${chalk.dim(`(${uid})`)}:\n${chalk.bold(chalk.underline(pretty))} now points to ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)}`;
|
|
if (isTTY && clipboard) {
|
|
let append;
|
|
try {
|
|
await copy(pretty);
|
|
append = "[copied to clipboard]";
|
|
} catch (err) {
|
|
append = "";
|
|
} finally {
|
|
console.log(`${output} ${append}`);
|
|
}
|
|
} else {
|
|
console.log(output);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`${chalk.cyan("> Success!")} Alias already exists ${chalk.dim(`(${uid})`)}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
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 }
|
|
});
|
|
|
|
const body = await res.json();
|
|
if (this._debug) {
|
|
console.timeEnd(
|
|
`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`
|
|
);
|
|
}
|
|
|
|
// 409 conflict is returned if it already exists
|
|
if (res.status === 409) {
|
|
return { uid: body.error.uid };
|
|
}
|
|
|
|
// no retry on authorization problems
|
|
if (res.status === 403) {
|
|
const code = body.error.code;
|
|
|
|
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 (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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
return bail(new Error("Authorization error"));
|
|
}
|
|
|
|
// all other errors
|
|
if (body.error) {
|
|
const code = body.error.code;
|
|
|
|
if (code === "deployment_not_found") {
|
|
return bail(new Error("Deployment not found"));
|
|
}
|
|
|
|
if (code === "cert_missing") {
|
|
console.log(
|
|
`> Provisioning certificate for ${chalk.underline(chalk.bold(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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
return bail(new Error(body.error.message));
|
|
}
|
|
|
|
// the two expected succesful cods are 200 and 304
|
|
if (res.status !== 200 && res.status !== 304) {
|
|
throw new Error("Unhandled error");
|
|
}
|
|
|
|
return body;
|
|
});
|
|
}
|
|
|
|
async setupRecord(domain, name) {
|
|
await this.setupDomain(domain);
|
|
|
|
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 (res.status === 403) {
|
|
return bail(new Error("Unauthorized"));
|
|
}
|
|
|
|
const body = await res.json();
|
|
|
|
if (res.status !== 200) {
|
|
throw new Error(body.error.message);
|
|
}
|
|
|
|
return body;
|
|
});
|
|
}
|
|
|
|
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")}!`);
|
|
}
|
|
return alias;
|
|
}
|
|
|
|
verifyOwnership(domain) {
|
|
return this.retry(async bail => {
|
|
const targets = await resolve4("alias.zeit.co");
|
|
|
|
if (targets.length <= 0) {
|
|
return bail(new Error("Unable to resolve alias.zeit.co"));
|
|
}
|
|
|
|
let ips = [];
|
|
|
|
try {
|
|
ips = await resolve4(domain);
|
|
} catch (err) {
|
|
if (
|
|
err.code === "ENODATA" ||
|
|
err.code === "ESERVFAIL" ||
|
|
err.code === "ENOTFOUND"
|
|
) {
|
|
// not errors per se, just absence of records
|
|
if (this._debug) {
|
|
console.log(`> [debug] No records found for "${domain}"`);
|
|
}
|
|
|
|
const err = new Error(DOMAIN_VERIFICATION_ERROR);
|
|
err.userError = true;
|
|
return bail(err);
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
if (ips.length <= 0) {
|
|
const err = new Error(DOMAIN_VERIFICATION_ERROR);
|
|
err.userError = true;
|
|
return bail(err);
|
|
}
|
|
|
|
for (const ip of ips) {
|
|
if (targets.indexOf(ip) === -1) {
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|