mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
396 lines
12 KiB
JavaScript
396 lines
12 KiB
JavaScript
// URL parsing and validation
|
|
// RPC to server (endpoint, arguments)
|
|
// see if RPC requires password
|
|
// prompt for password
|
|
// send RPC with or without password as required
|
|
|
|
var crypto = require('crypto');
|
|
var tty = require('tty');
|
|
var request = require('request');
|
|
var qs = require('querystring');
|
|
var path = require('path');
|
|
var files = require('./files.js');
|
|
var warehouse = require('./warehouse.js');
|
|
var _ = require('underscore');
|
|
var keypress = require('keypress');
|
|
var child_process = require('child_process');
|
|
var inFiber = require('./fiber-helpers.js').inFiber;
|
|
|
|
//
|
|
// configuration
|
|
//
|
|
|
|
var DEPLOY_HOSTNAME = process.env.DEPLOY_HOSTNAME || 'deploy.meteor.com';
|
|
|
|
if (process.env.EMACS == "t") {
|
|
// Hack to set stdin to be blocking, reversing node's normal setting of
|
|
// O_NONBLOCK on the evaluation of process.stdin (because Node unblocks stdio
|
|
// when forking). This fixes execution of Mongo from within Emacs shell.
|
|
process.stdin;
|
|
child_process.spawn('true', [], {stdio: 'inherit'});
|
|
}
|
|
|
|
// available RPCs are: deploy (with set-password), delete, logs,
|
|
// mongo_cred. each RPC might require a password, which we
|
|
// interactively prompt for here.
|
|
|
|
var meteor_rpc = function (rpc_name, method, site, query_params, callback) {
|
|
var url;
|
|
if (DEPLOY_HOSTNAME.indexOf("http://") === 0)
|
|
url = DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site;
|
|
else
|
|
url = "https://" + DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site;
|
|
|
|
if (!_.isEmpty(query_params)) {
|
|
url += '?' + qs.stringify(query_params);
|
|
}
|
|
|
|
var r = request({method: method, url: url}, function (error, response, body) {
|
|
if (error || ((response.statusCode !== 200)
|
|
&& (response.statusCode !== 201)))
|
|
// pass some non-falsy error back to callback
|
|
callback(error || response.statusCode, body);
|
|
else
|
|
callback(null, body);
|
|
});
|
|
|
|
return r;
|
|
};
|
|
|
|
// called by command-line `meteor deploy`
|
|
var deployCmd = function (options) {
|
|
var parsed_url = parse_url(options.url);
|
|
|
|
// a bit contorted here to make sure we ask for the password before
|
|
// launching the slow bundle process.
|
|
with_password(parsed_url.hostname, function (password) {
|
|
var deployOptions = {
|
|
site: parsed_url.hostname,
|
|
password: password,
|
|
settings: options.settings
|
|
};
|
|
if (options.setPassword)
|
|
get_new_password(function (newPassword) {
|
|
deployOptions.setPassword = newPassword;
|
|
deployToServer(options.appDir, options.bundleOptions, deployOptions);
|
|
});
|
|
else
|
|
deployToServer(options.appDir, options.bundleOptions, deployOptions);
|
|
});
|
|
};
|
|
|
|
// a utility used by `meteor deploy` and `meteor test-packages`
|
|
var deployToServer = function (app_dir, bundleOptions, deployOptions) {
|
|
// might be parsing twice if called from deploy_app but that's fine
|
|
var site = parse_url(deployOptions.site).hostname;
|
|
|
|
var password = deployOptions.password;
|
|
var set_password = deployOptions.setPassword;
|
|
var settings = deployOptions.settings;
|
|
var build_dir = path.join(app_dir, '.meteor', 'local', 'build_tar');
|
|
var bundle_path = path.join(build_dir, 'bundle');
|
|
|
|
process.stdout.write('Deploying to ' + site + '. Bundling ... ');
|
|
var bundler = require('./bundler.js');
|
|
var errors = bundler.bundle(app_dir, bundle_path, bundleOptions);
|
|
if (errors) {
|
|
process.stdout.write("\n\nErrors prevented deploying:\n");
|
|
_.each(errors, function (e) {
|
|
process.stdout.write(e + "\n");
|
|
});
|
|
files.rm_recursive(build_dir);
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write('uploading ... ');
|
|
|
|
var rpcOptions = {};
|
|
if (password) rpcOptions.password = password;
|
|
if (set_password) rpcOptions.set_password = set_password;
|
|
|
|
// When it hits the wire, all these opts will be URL-encoded.
|
|
if (settings !== undefined) rpcOptions.settings = settings;
|
|
|
|
var tar = files.createTarGzStream(path.join(build_dir, 'bundle'));
|
|
|
|
var rpc = meteor_rpc('deploy', 'POST', site, rpcOptions, function (err, body) {
|
|
if (err) {
|
|
var errorMessage = (body || ("Connection error (" + err.message + ")"));
|
|
process.stderr.write("\nError deploying application: " + errorMessage + "\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write('done.\n');
|
|
|
|
var hostname = null;
|
|
var response = null;
|
|
try {
|
|
response = JSON.parse(body);
|
|
} catch (e) {
|
|
// ... leave null
|
|
}
|
|
if (response && response.url) {
|
|
var url = require('url').parse(response.url);
|
|
if (url && url.hostname)
|
|
hostname = url.hostname;
|
|
}
|
|
|
|
if (!hostname) {
|
|
process.stdout.write('Error receiving hostname from deploy server.\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write('Now serving at ' + hostname + '\n');
|
|
files.rm_recursive(build_dir);
|
|
|
|
|
|
if (hostname && !hostname.match(/meteor\.com$/)) {
|
|
var dns = require('dns');
|
|
dns.resolve(hostname, 'CNAME', function (err, cnames) {
|
|
if (err || cnames[0] !== 'origin.meteor.com') {
|
|
dns.resolve(hostname, 'A', function (err, addresses) {
|
|
if (err || addresses[0] !== '107.22.210.133') {
|
|
process.stdout.write('-------------\n');
|
|
process.stdout.write("You've deployed to a custom domain.\n");
|
|
process.stdout.write("Please be sure to CNAME your hostname to origin.meteor.com,\n");
|
|
process.stdout.write("or set an A record to 107.22.210.133.\n");
|
|
process.stdout.write('-------------\n');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
tar.pipe(rpc);
|
|
};
|
|
|
|
var delete_app = function (url) {
|
|
var parsed_url = parse_url(url);
|
|
|
|
with_password(parsed_url.hostname, function (password) {
|
|
var opts = {};
|
|
if (password) opts.password = password;
|
|
|
|
meteor_rpc('deploy', 'DELETE', parsed_url.hostname, opts, function (err, body) {
|
|
if (err) {
|
|
process.stderr.write("Error deleting application: " + body + "\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write("Deleted.\n");
|
|
});
|
|
});
|
|
};
|
|
|
|
// either print the mongo credential (just_credential is true) or open
|
|
// a mongo shell.
|
|
var mongo = function (url, just_credential) {
|
|
var parsed_url = parse_url(url);
|
|
|
|
with_password(parsed_url.hostname, function (password) {
|
|
var opts = {};
|
|
if (password) opts.password = password;
|
|
|
|
meteor_rpc('mongo', 'GET', parsed_url.hostname, opts, function (err, body) {
|
|
if (err) {
|
|
process.stderr.write(body + "\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (just_credential) {
|
|
// just print the URL
|
|
process.stdout.write(body + "\n");
|
|
|
|
} else {
|
|
// pause stdin so we don't try to read it while mongo is
|
|
// running.
|
|
process.stdin.pause();
|
|
run_mongo_shell(body);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
var logs = function (url) {
|
|
var parsed_url = parse_url(url);
|
|
|
|
with_password(parsed_url.hostname, function (password) {
|
|
var opts = {};
|
|
if (password) opts.password = password;
|
|
|
|
meteor_rpc('logs', 'GET', parsed_url.hostname, opts, function (err, body) {
|
|
if (err) {
|
|
process.stderr.write(body + '\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write(body);
|
|
});
|
|
});
|
|
};
|
|
|
|
// accepts www.host.com, defaults domain to meteor, defaults
|
|
// protocol to http. on bad URL, prints error and exits the process.
|
|
//
|
|
// XXX shared w/ proxy.js
|
|
var parse_url = function (url) {
|
|
if (!url.match(':\/\/'))
|
|
url = 'http://' + url;
|
|
|
|
var parsed = require('url').parse(url);
|
|
|
|
delete parsed.host; // we use hostname
|
|
|
|
if (!parsed.hostname) {
|
|
process.stdout.write(
|
|
"Please specify a domain to connect to, such as www.example.com or\n" +
|
|
"http://www.example.com/\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (parsed.pathname != '/' || parsed.hash || parsed.query) {
|
|
process.stdout.write(
|
|
"Sorry, Meteor does not yet support specific path URLs, such as\n" +
|
|
"http://www.example.com/blog . Please specify the root of a domain.\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
return parsed;
|
|
};
|
|
|
|
var run_mongo_shell = function (url) {
|
|
var mongo_path = path.join(files.get_dev_bundle(), 'mongodb', 'bin', 'mongo');
|
|
var mongo_url = require('url').parse(url);
|
|
var auth = mongo_url.auth && mongo_url.auth.split(':');
|
|
|
|
var args = [];
|
|
if (auth) args.push('-u', auth[0]);
|
|
if (auth) args.push('-p', auth[1]);
|
|
args.push(mongo_url.hostname + ':' + mongo_url.port + mongo_url.pathname);
|
|
|
|
var proc = child_process.spawn(mongo_path,
|
|
args,
|
|
{ stdio: 'inherit' });
|
|
};
|
|
|
|
// hash the password so we never send plaintext over the wire. Doesn't
|
|
// actually make us more secure, but it means we won't leak a user's
|
|
// password, which they might use on other sites too.
|
|
var transform_password = function (password) {
|
|
var hash = crypto.createHash('sha1');
|
|
hash.update('S3krit Salt!');
|
|
hash.update(password);
|
|
return hash.digest('hex');
|
|
};
|
|
|
|
// read a password from stdin. return it in a callback.
|
|
var read_password = function (callback) {
|
|
// Password prompt code adapted from
|
|
// https://github.com/visionmedia/commander.js/blob/master/lib/commander.js
|
|
|
|
var buf = '';
|
|
if (process.stdin.setRawMode) {
|
|
// when piping password from bash to meteor we have no setRawMode() available
|
|
process.stdin.setRawMode(true);
|
|
}
|
|
|
|
// keypress
|
|
keypress(process.stdin);
|
|
process.stdin.on('keypress', inFiber(function(c, key){
|
|
if (key && 'enter' === key.name) {
|
|
console.log();
|
|
process.stdin.pause();
|
|
process.stdin.removeAllListeners('keypress');
|
|
if (process.stdin.setRawMode) {
|
|
// when piping password from bash to meteor we have no setRawMode() available
|
|
process.stdin.setRawMode(false);
|
|
}
|
|
|
|
// if they just hit enter, prompt again. let's not do this.
|
|
// This means empty password is a valid password.
|
|
//if (!buf.trim().length) return self.password(str, mask, fn);
|
|
|
|
callback(transform_password(buf));
|
|
return;
|
|
}
|
|
|
|
// deal with backspace
|
|
if (key && 'backspace' === key.name) {
|
|
buf = buf.substring(0, buf.length - 1);
|
|
return;
|
|
}
|
|
|
|
// raw mode masks control-c. make sure users can get out.
|
|
if (key && key.ctrl && 'c' === key.name) {
|
|
console.log();
|
|
process.stdin.pause();
|
|
process.stdin.removeAllListeners('keypress');
|
|
process.stdin.setRawMode(false);
|
|
|
|
process.kill(process.pid, 'SIGINT');
|
|
return;
|
|
}
|
|
|
|
buf += c;
|
|
}));
|
|
|
|
process.stdin.resume();
|
|
};
|
|
|
|
// Check if a particular endpoint requires a password. If so, prompt for
|
|
// it.
|
|
//
|
|
// takes an site name and callback function(password). This is always
|
|
// called exactly once. Calls callback with the entered password, or
|
|
// undefined if no password is required.
|
|
var with_password = function (site, callback) {
|
|
var check_url = "https://" + DEPLOY_HOSTNAME + "/has_password/" + site;
|
|
|
|
// XXX we've been using `inFiber` as needed, but I wish we'd instead
|
|
// always have callbacks that do nothing other than Future.ret or
|
|
// Future.throw. Basically, what Future.wrap does.
|
|
callback = inFiber(callback);
|
|
|
|
request(check_url, function (error, response, body) {
|
|
if (error || response.statusCode !== 200) {
|
|
callback();
|
|
|
|
} else if (body === "false") {
|
|
// XXX in theory we should JSON parse the result, and use
|
|
// that. But we happen to know we'll only ever get 'true' or
|
|
// 'false' if we got a 200, so don't bother.
|
|
callback();
|
|
|
|
} else {
|
|
process.stdout.write("Password: ");
|
|
read_password(callback);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Prompts for a new password, asking you twice so you don't typo
|
|
// it. Keeps prompting you until you have two that match.
|
|
var get_new_password = function (callback) {
|
|
process.stdout.write("New Password: ");
|
|
read_password(function (p1) {
|
|
process.stdout.write("New Password (again): ");
|
|
read_password(function (p2) {
|
|
if (p1 === p2) {
|
|
callback(p1);
|
|
return;
|
|
}
|
|
process.stdout.write("Passwords do not match! Try again.\n");
|
|
get_new_password(callback);
|
|
});
|
|
});
|
|
};
|
|
|
|
exports.deployCmd = deployCmd;
|
|
exports.deployToServer = deployToServer;
|
|
exports.delete_app = delete_app;
|
|
exports.mongo = mongo;
|
|
exports.logs = logs;
|
|
|
|
exports.run_mongo_shell = run_mongo_shell;
|