mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
620 lines
19 KiB
JavaScript
620 lines
19 KiB
JavaScript
var _ = require('underscore');
|
|
var path = require('path');
|
|
var fs = require('fs');
|
|
var utils = require('./utils.js');
|
|
var files = require('./files.js');
|
|
var config = require('./config.js');
|
|
var httpHelpers = require('./http-helpers.js');
|
|
var archinfo = require('./archinfo.js');
|
|
var querystring = require('querystring');
|
|
var url = require('url');
|
|
|
|
var auth = exports;
|
|
|
|
var getSessionFilePath = function () {
|
|
return process.env.SESSION_FILE_PATH ||
|
|
path.join(process.env.HOME, '.meteorsession');
|
|
};
|
|
|
|
var readSessionData = function () {
|
|
var sessionPath = getSessionFilePath();
|
|
if (! fs.existsSync(sessionPath))
|
|
return {};
|
|
return JSON.parse(fs.readFileSync(sessionPath, { encoding: 'utf8' }));
|
|
};
|
|
|
|
var writeSessionData = function (data) {
|
|
var sessionPath = getSessionFilePath();
|
|
|
|
var tries = 0;
|
|
while (true) {
|
|
if (tries++ > 10)
|
|
throw new Error("can't find a unique name for temporary file?");
|
|
|
|
// Create a temporary file in the same directory where we
|
|
// ultimately want to write the session file. Use the exclusive
|
|
// flag to atomically ensure that the file doesn't exist, create
|
|
// it, and make it readable and writable only by the current
|
|
// user (mode 0600).
|
|
var tempPath =
|
|
path.join(path.dirname(sessionPath), '.meteorsession.' +
|
|
Math.floor(Math.random() * 999999));
|
|
try {
|
|
var fd = fs.openSync(tempPath, 'wx', 0600);
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
|
|
// Write `data` to the file.
|
|
var buf = new Buffer(JSON.stringify(data, undefined, 2), 'utf8');
|
|
fs.writeSync(fd, buf, 0, buf.length, 0);
|
|
fs.closeSync(fd);
|
|
|
|
// Atomically remove the old file (if any) and replace it with
|
|
// the temporary file we just created.
|
|
fs.renameSync(tempPath, sessionPath);
|
|
return;
|
|
}
|
|
};
|
|
|
|
var getSession = function (sessionData, domain) {
|
|
if (typeof (sessionData.sessions) !== "object")
|
|
sessionData.sessions = {};
|
|
if (typeof (sessionData.sessions[domain]) !== "object")
|
|
sessionData.sessions[domain] = {};
|
|
return sessionData.sessions[domain];
|
|
};
|
|
|
|
// types:
|
|
// - "meteor-account": a login to your Meteor Account
|
|
// - "galaxy": a login to a Galaxy
|
|
var ensureSessionType = function (session, type) {
|
|
if (! _.has(session, 'type'))
|
|
session.type = type;
|
|
else if (session.type !== type) {
|
|
// Blow away whatever was there. We lose pendingRevokes but that's
|
|
// OK since this should never happen in normal operation. (It
|
|
// would happen if the Meteor Accounts server mode somewhere else
|
|
// and a Galaxy was deployed at its old address, for example).
|
|
_.each(_.keys(session), function (key) {
|
|
delete session[key];
|
|
});
|
|
session.type = type;
|
|
}
|
|
}
|
|
|
|
// Given an object 'data' in the format returned by readSessionData,
|
|
// modify it to make the user logged out.
|
|
var logOutAllSessions = function (data) {
|
|
var crypto = require('crypto');
|
|
|
|
_.each(data.sessions, function (session, domain) {
|
|
delete session.username;
|
|
delete session.userId;
|
|
|
|
if (_.has(session, 'token')) {
|
|
if (! (session.pendingRevoke instanceof Array))
|
|
session.pendingRevoke = [];
|
|
|
|
// Delete the auth token itself, but save the tokenId, which
|
|
// is useless for authentication. The next time we're online,
|
|
// we'll send the tokenId to the server to revoke the token on
|
|
// the server side too.
|
|
if (typeof session.tokenId === "string")
|
|
session.pendingRevoke.push(session.tokenId);
|
|
delete session.token;
|
|
delete session.tokenId;
|
|
}
|
|
});
|
|
};
|
|
|
|
// Given an object 'data' in the format returned by readSessionData,
|
|
// return true if logged in, else false.
|
|
var loggedIn = function (data) {
|
|
return !! getSession(data, config.getAccountsDomain()).userId;
|
|
};
|
|
|
|
// Given an object 'data' in the format returned by readSessionData,
|
|
// return the currently logged in user, or null if not logged in or if
|
|
// the logged in user doesn't have a username.
|
|
var currentUsername = function (data) {
|
|
return getSession(data, config.getAccountsDomain()).username || null;
|
|
};
|
|
|
|
// If there are any logged out (pendingRevoke) tokens that haven't
|
|
// been sent to the server for revocation yet, try to send
|
|
// them. Reads the session file and then writes it back out to
|
|
// disk. If the server can't be contacted, fail silently (and leave
|
|
// the pending invalidations in the session file for next time).
|
|
//
|
|
// options:
|
|
// - timeout: request timeout in milliseconds
|
|
// - firstTry: cosmetic. set to true if we recently logged out a
|
|
// session. just changes the error message.
|
|
var tryRevokeOldTokens = function (options) {
|
|
options = _.extend({
|
|
timeout: 5000
|
|
}, options || {});
|
|
|
|
var warned = false;
|
|
var domainsWithRevokedTokens = [];
|
|
_.each(readSessionData().sessions || {}, function (session, domain) {
|
|
if (session.pendingRevoke &&
|
|
session.pendingRevoke.length)
|
|
domainsWithRevokedTokens.push(domain);
|
|
});
|
|
|
|
var logoutFailWarning = function (domain) {
|
|
if (! warned) {
|
|
// This isn't ideal but is probably better that saying nothing at all
|
|
process.stderr.write("warning: " +
|
|
(options.firstTry ?
|
|
"couldn't" : "still trying to") +
|
|
" confirm logout with " + domain +
|
|
"\n");
|
|
warned = true;
|
|
}
|
|
};
|
|
|
|
_.each(domainsWithRevokedTokens, function (domain) {
|
|
var data = readSessionData();
|
|
var session = data.sessions[domain] || {};
|
|
var tokenIds = session.pendingRevoke || [];
|
|
if (! tokenIds.length)
|
|
return;
|
|
|
|
var url;
|
|
if (session.type === "meteor-account") {
|
|
url = config.getAccountsApiUrl() + "/revoke";
|
|
} else if (session.type === "galaxy") {
|
|
var oauthInfo = fetchGalaxyOAuthInfo(domain, options.timeout);
|
|
if (oauthInfo) {
|
|
url = oauthInfo.revokeUri;
|
|
} else {
|
|
logoutFailWarning(domain);
|
|
return;
|
|
}
|
|
} else {
|
|
// don't know how to revoke tokens of this type
|
|
logoutFailWarning(domain);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var result = httpHelpers.request({
|
|
url: url,
|
|
method: "POST",
|
|
form: {
|
|
tokenId: tokenIds.join(',')
|
|
},
|
|
useSessionHeader: true,
|
|
timeout: options.timeout
|
|
});
|
|
} catch (e) {
|
|
// most likely we don't have a net connection
|
|
return;
|
|
}
|
|
var response = result.response;
|
|
|
|
if (response.statusCode === 200) {
|
|
// Server confirms that the tokens have been revoked
|
|
// (Be careful to reread session data in case httpHelpers changed it)
|
|
data = readSessionData();
|
|
var session = getSession(data, domain);
|
|
session.pendingRevoke = _.difference(session.pendingRevoke, tokenIds);
|
|
if (! session.pendingRevoke.length)
|
|
delete session.pendingRevoke;
|
|
writeSessionData(data);
|
|
} else {
|
|
logoutFailWarning(domain);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Sends a request to https://<galaxyName>:<DISCOVERY_PORT> to find out the
|
|
// galaxy's OAuth client id and redirect_uri that should be used for
|
|
// authorization codes for this galaxy. Returns an object with keys
|
|
// 'oauthClientId', 'redirectUri', and 'revokeUri', or null if the
|
|
// request failed.
|
|
//
|
|
// 'timeout' is an optional request timeout in milliseconds.
|
|
var fetchGalaxyOAuthInfo = function (galaxyName, timeout) {
|
|
var galaxyAuthUrl = 'https://' + galaxyName + ':' +
|
|
config.getDiscoveryPort() + '/_GALAXYAUTH_';
|
|
try {
|
|
var result = httpHelpers.request({
|
|
url: galaxyAuthUrl,
|
|
json: true,
|
|
// on by default in our version of request, but just in case
|
|
strictSSL: true,
|
|
followRedirect: false,
|
|
timeout: timeout || 5000
|
|
});
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
|
|
if (result.response.statusCode === 200 &&
|
|
result.body &&
|
|
result.body.oauthClientId &&
|
|
result.body.redirectUri &&
|
|
result.body.revokeUri) {
|
|
return result.body;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Uses meteor accounts to log in to the specified galaxy. Returns an
|
|
// object with keys `token` and `tokenId` if the login was
|
|
// successful. If an error occurred, returns one of:
|
|
// { error: 'access-denied' }
|
|
// { error: 'no-galaxy' }
|
|
// { error: 'no-account-server' }
|
|
var logInToGalaxy = function (galaxyName) {
|
|
var oauthInfo = fetchGalaxyOAuthInfo(galaxyName);
|
|
if (! oauthInfo) {
|
|
return { error: 'no-galaxy' };
|
|
}
|
|
|
|
var galaxyClientId = oauthInfo.oauthClientId;
|
|
var galaxyRedirect = oauthInfo.redirectUri;
|
|
|
|
// Ask the accounts server for an authorization code.
|
|
var crypto = require('crypto');
|
|
var session = crypto.randomBytes(16).toString('hex');
|
|
var stateInfo = { session: session };
|
|
|
|
var authCodeUrl = config.getOauthUrl() + "/authorize?" +
|
|
querystring.stringify({
|
|
state: encodeURIComponent(JSON.stringify(stateInfo)),
|
|
response_type: "code",
|
|
client_id: galaxyClientId,
|
|
redirect_uri: galaxyRedirect
|
|
});
|
|
|
|
// It's very important that we don't have request follow the
|
|
// redirect for us, but instead issue the second request ourselves,
|
|
// since request would pass our credentials along to the redirected
|
|
// URL. See comments in http-helpers.js.
|
|
try {
|
|
var codeResult = httpHelpers.request({
|
|
url: authCodeUrl,
|
|
method: 'POST',
|
|
strictSSL: true,
|
|
useAuthHeader: true
|
|
});
|
|
} catch (e) {
|
|
return { error: 'no-account-server' };
|
|
}
|
|
var response = codeResult.response;
|
|
if (response.statusCode !== 302 || ! response.headers.location) {
|
|
return { error: 'access-denied' };
|
|
}
|
|
|
|
if (url.parse(response.headers.location).hostname !== galaxyName) {
|
|
// If we didn't get an immediate redirect to the redirectUri
|
|
// (which had better be in DNS namespace that belongs to the
|
|
// Galaxy) then presumably the oauth server is trying to interact
|
|
// with us (make us log in, authorize the client, or something
|
|
// like that). We're not a web browser so we can't participate in
|
|
// such things.
|
|
return { error: 'access-denied' };
|
|
}
|
|
|
|
// Ask the galaxy to log us in with our auth code.
|
|
try {
|
|
var galaxyResult = httpHelpers.request({
|
|
url: response.headers.location,
|
|
method: 'GET',
|
|
strictSSL: true,
|
|
headers: {
|
|
cookie: 'GALAXY_OAUTH_SESSION=' + session
|
|
}
|
|
});
|
|
var body = JSON.parse(galaxyResult.body);
|
|
} catch (e) {
|
|
return { error: 'no-galaxy' };
|
|
}
|
|
response = galaxyResult.response;
|
|
|
|
// 'access-denied' isn't exactly right because it's possible that the galaxy
|
|
// went down since our last request, but close enough.
|
|
|
|
if (response.statusCode !== 200 ||
|
|
! body ||
|
|
! _.has(galaxyResult.setCookie, 'GALAXY_AUTH'))
|
|
return { error: 'access-denied' };
|
|
|
|
return {
|
|
token: galaxyResult.setCookie.GALAXY_AUTH,
|
|
tokenId: body.tokenId
|
|
};
|
|
};
|
|
|
|
// Prompt the user for a password, and then log in. Returns true if a
|
|
// successful login was accomplished, else false.
|
|
//
|
|
// Options should include either 'email' or 'username', and may also
|
|
// include:
|
|
// - retry: if true, then if the user gets the password wrong,
|
|
// reprompt.
|
|
var doInteractivePasswordLogin = function (options) {
|
|
var loginData = utils.getAgentInfo();
|
|
|
|
if (_.has(options, 'username'))
|
|
loginData.username = options.username;
|
|
else if (_.has(options, 'email'))
|
|
loginData.email = options.email;
|
|
else
|
|
throw new Error("Need username or email");
|
|
|
|
while (true) {
|
|
loginData.password = utils.readLine({
|
|
echo: false,
|
|
prompt: "Password: "
|
|
});
|
|
|
|
var result;
|
|
try {
|
|
result = httpHelpers.request({
|
|
url: config.getAccountsApiUrl() + "/login",
|
|
method: "POST",
|
|
form: loginData,
|
|
useSessionHeader: true
|
|
});
|
|
var body = JSON.parse(result.body);
|
|
} catch (e) {
|
|
process.stderr.write("\nCouldn't connect to server. " +
|
|
"Check your internet connection.\n");
|
|
return false;
|
|
}
|
|
|
|
if (result.response.statusCode === 200 &&
|
|
_.has(result.response.headers, 'x-meteor-auth'))
|
|
break;
|
|
|
|
process.stderr.write("Login failed.\n");
|
|
if (options.retry) {
|
|
process.stderr.write("\n");
|
|
continue;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
var data = readSessionData();
|
|
logOutAllSessions(data);
|
|
var session = getSession(data, config.getAccountsDomain());
|
|
ensureSessionType(session, "meteor-account");
|
|
session.username = body.username;
|
|
session.userId = body.userId;
|
|
session.token = result.response.headers['x-meteor-auth'];
|
|
session.tokenId = body.tokenId;
|
|
writeSessionData(data);
|
|
return true;
|
|
};
|
|
|
|
exports.loginCommand = function (options) {
|
|
config.printUniverseBanner();
|
|
|
|
var data = readSessionData();
|
|
var galaxy = options.galaxy;
|
|
|
|
if (! galaxy || ! getSession(data, config.getAccountsDomain()).token) {
|
|
var loginOptions = {};
|
|
|
|
if (options.email) {
|
|
loginOptions.email = utils.readLine({ prompt: "Email: " });
|
|
} else {
|
|
loginOptions.username = utils.readLine({ prompt: "Username: " });
|
|
}
|
|
|
|
if (! doInteractivePasswordLogin(loginOptions))
|
|
return 1;
|
|
}
|
|
|
|
if (galaxy) {
|
|
var galaxyLoginResult = logInToGalaxy(galaxy);
|
|
if (galaxyLoginResult.error) {
|
|
// XXX add human readable error messages
|
|
process.stdout.write('\nLogin to ' + galaxy + ' failed: ' +
|
|
galaxyLoginResult.error + '\n');
|
|
return 1
|
|
}
|
|
data = readSessionData(); // be careful to reread data file after RPC
|
|
var session = getSession(data, galaxy);
|
|
ensureSessionType(session, "galaxy");
|
|
session.token = galaxyLoginResult.token;
|
|
session.tokenId = galaxyLoginResult.tokenId;
|
|
writeSessionData(data);
|
|
}
|
|
|
|
tryRevokeOldTokens({ firstTry: true });
|
|
|
|
data = readSessionData();
|
|
process.stdout.write("\nLogged in" + (galaxy ? " to " + galaxy : "") +
|
|
(currentUsername(data) ?
|
|
" as " + currentUsername(data) : "") + ".\n" +
|
|
"Thanks for being a Meteor developer!\n");
|
|
};
|
|
|
|
exports.logoutCommand = function (options) {
|
|
config.printUniverseBanner();
|
|
|
|
var data = readSessionData();
|
|
var wasLoggedIn = !! loggedIn(data);
|
|
logOutAllSessions(data);
|
|
writeSessionData(data);
|
|
|
|
tryRevokeOldTokens({ firstTry: true });
|
|
|
|
if (wasLoggedIn)
|
|
process.stderr.write("Logged out.\n");
|
|
else
|
|
// We called logOutAllSessions/writeSessionData anyway, out of an
|
|
// abundance of caution.
|
|
process.stderr.write("Not logged in.\n");
|
|
};
|
|
|
|
exports.whoAmICommand = function (options) {
|
|
config.printUniverseBanner();
|
|
|
|
var data = readSessionData();
|
|
if (! loggedIn(data)) {
|
|
process.stderr.write("Not logged in. 'meteor login' to log in.\n");
|
|
return 1;
|
|
}
|
|
|
|
var username = currentUsername(data);
|
|
if (username) {
|
|
process.stdout.write(username + "\n");
|
|
return 0;
|
|
}
|
|
|
|
var url = getSession(data, config.getAccountsDomain()).registrationUrl;
|
|
if (url) {
|
|
process.stderr.write(
|
|
"You haven't chosen your username yet. To pick it, go here:\n" +
|
|
"\n" +
|
|
url + "\n");
|
|
} else {
|
|
// Won't happen in normal operation
|
|
process.stderr.write("You haven't chosen your username yet.\n")
|
|
}
|
|
|
|
return 1;
|
|
};
|
|
|
|
// Prompt for an email address. If it doesn't belong to a user, create
|
|
// a new deferred registration account and log in as it. If it does,
|
|
// try to log the user into it. Returns true on success (user is now
|
|
// logged in) or false on failure (user gave up, can't talk to
|
|
// network..)
|
|
exports.registerOrLogIn = function () {
|
|
// Get their email
|
|
while (true) {
|
|
var email = utils.readLine({ prompt: "Email: " });
|
|
if (utils.validEmail(email))
|
|
break;
|
|
if (email.trim().length)
|
|
process.stderr.write("Please double-check that address.\n\n");
|
|
}
|
|
|
|
// Try to register
|
|
var result;
|
|
try {
|
|
result = httpHelpers.request({
|
|
url: config.getAccountsApiUrl() + "/register",
|
|
method: "POST",
|
|
form: _.extend(utils.getAgentInfo(), {
|
|
email: email
|
|
}),
|
|
useSessionHeader: true
|
|
});
|
|
var body = JSON.parse(result.body);
|
|
} catch (e) {
|
|
process.stderr.write("\nCouldn't connect to server. " +
|
|
"Check your internet connection.\n");
|
|
return false;
|
|
}
|
|
|
|
if (result.response.statusCode === 200) {
|
|
if (! _.has(result.response.headers, 'x-meteor-auth')) {
|
|
process.stdout.write("\nSorry, the server is having a problem.\n" +
|
|
"Please try again later.\n");
|
|
return false;
|
|
}
|
|
|
|
var data = readSessionData();
|
|
logOutAllSessions(data);
|
|
var session = getSession(data, config.getAccountsDomain());
|
|
ensureSessionType(session, "meteor-account");
|
|
session.token = result.response.headers['x-meteor-auth'];
|
|
session.tokenId = body.tokenId;
|
|
session.userId = body.userId;
|
|
session.registrationUrl = body.registrationUrl;
|
|
writeSessionData(data);
|
|
return true;
|
|
}
|
|
|
|
if (body.error === "already_registered" &&
|
|
body.sentRegistrationEmail) {
|
|
process.stderr.write(
|
|
"\n" +
|
|
"That email address is already in use. We need to confirm that it belongs\n" +
|
|
"to you. Luckily this will only take a moment.\n" +
|
|
"\n" +
|
|
"Check your mail! We've sent you a link. Click it, pick a password,\n" +
|
|
"and then come back here to deploy your app.\n");
|
|
|
|
var unipackage = require('./unipackage.js');
|
|
var Package = unipackage.load({
|
|
library: release.current.library,
|
|
packages: [ 'meteor', 'livedata' ],
|
|
release: release.current.name
|
|
})
|
|
var DDP = Package.livedata.DDP;
|
|
var authService = DDP.connect(config.getAuthDDPUrl());
|
|
try {
|
|
var result = authService.call("waitForRegistration", email);
|
|
} catch (e) {
|
|
if (! (e instanceof Package.meteor.Meteor.Error))
|
|
throw e;
|
|
process.stderr.write(
|
|
"\nWhen you've picked your password, run 'meteor login' and then you'll\n" +
|
|
"be good to go.\n");
|
|
return false;
|
|
}
|
|
|
|
process.stderr.write("\nGreat! Nice to meet you, " + result.username +
|
|
"! Now log in with your new password.\n");
|
|
return doInteractivePasswordLogin({
|
|
username: result.username,
|
|
retry: true
|
|
});
|
|
}
|
|
|
|
if (body.error === "already_registered" && body.username) {
|
|
process.stderr.write("\nLogging in as " + body.username + ".\n");
|
|
|
|
return doInteractivePasswordLogin({
|
|
username: body.username,
|
|
retry: true
|
|
});
|
|
}
|
|
|
|
// Hmm, got an email we don't understand.
|
|
process.stderr.write(
|
|
"\nThere was a problem. Please log in with 'meteor login'.\n");
|
|
return false;
|
|
};
|
|
|
|
exports.tryRevokeOldTokens = tryRevokeOldTokens;
|
|
|
|
exports.getSessionId = function (domain) {
|
|
return getSession(readSessionData(), domain).session;
|
|
};
|
|
|
|
exports.setSessionId = function (domain, sessionId) {
|
|
var data = readSessionData();
|
|
getSession(data, domain).session = sessionId;
|
|
writeSessionData(data);
|
|
};
|
|
|
|
exports.getSessionToken = function (domain) {
|
|
return getSession(readSessionData(), domain).token;
|
|
};
|
|
|
|
exports.isLoggedIn = function () {
|
|
return loggedIn(readSessionData());
|
|
};
|
|
|
|
// Return the username of the currently logged in user, or false if
|
|
// not logged in, or null if the logged in user doesn't have a
|
|
// username.
|
|
exports.loggedInUsername = function () {
|
|
var data = readSessionData();
|
|
return loggedIn(data) ? currentUsername(data) : false;
|
|
};
|