diff --git a/tools/auth.js b/tools/auth.js index a76efe3a92..4df9681a98 100644 --- a/tools/auth.js +++ b/tools/auth.js @@ -408,15 +408,10 @@ var fetchGalaxyOAuthInfo = function (galaxyName, timeout) { } }; -// XXX De-dup with logInToGalaxy -// XXX make args options -var oauthFlow = function (conn, clientId, redirectUri, - domain, sessionType) { - var crypto = require('crypto'); - var credentialToken = crypto.randomBytes(16).toString('hex'); +var sendAuthorizeRequest = function (clientId, redirectUri, state) { var authCodeUrl = config.getOauthUrl() + "/authorize?" + querystring.stringify({ - state: credentialToken, + state: state, response_type: "code", client_id: clientId, redirect_uri: redirectUri @@ -450,9 +445,36 @@ var oauthFlow = function (conn, clientId, redirectUri, return { error: 'access-denied' }; } + return { location: response.headers.location }; +}; + +// Do an OAuth flow with the Meteor developer accounts server to log in +// to an OAuth client. `conn` is expected to be a DDP connection to the +// OAuth client app. Options are: +// - clientId: OAuth client id parameter +// - redirectUri: OAuth redirect_uri parameter +// - domain: the domain for saving the received login token on success +// in the Meteor session file +// - sessionType: the value of the 'type' field for the session saved +// in the Meteor session file on success +// All options are required. +var oauthFlow = function (conn, options) { + var crypto = require('crypto'); + var credentialToken = crypto.randomBytes(16).toString('hex'); + + var authorizeResult = sendAuthorizeRequest( + options.clientId, + options.redirectUri, + credentialToken + ); + + if (authorizeResult.error) { + return authorizeResult; + } + try { var redirectResult = httpHelpers.request({ - url: response.headers.location, + url: authorizeResult.location, method: 'GET', strictSSL: true }); @@ -460,7 +482,7 @@ var oauthFlow = function (conn, clientId, redirectUri, return { error: 'no-package-server' }; } - response = redirectResult.response; + var response = redirectResult.response; // 'access-denied' isn't exactly right because it's possible that the server // went down since our last request, but close enough. @@ -475,14 +497,14 @@ var oauthFlow = function (conn, clientId, redirectUri, if (loginResult.token && loginResult.id) { var data = readSessionData(); - var session = getSession(data, domain); - ensureSessionType(session, sessionType); + var session = getSession(data, options.domain); + ensureSessionType(session, options.sessionType); session.token = loginResult.token; writeSessionData(data); - return 0; + return true; } else { process.stderr.write('Login failed'); - return 1; + return false; } }; @@ -501,52 +523,32 @@ var logInToGalaxy = function (galaxyName) { var galaxyClientId = oauthInfo.oauthClientId; var galaxyRedirect = oauthInfo.redirectUri; + // If the redirect URI is not in the DNS namespace that belongs to the + // Galaxy, then something is wrong. + if (url.parse(galaxyRedirect).hostname !== galaxyName) { + // XXX It's more like 'bad-galaxy' than 'no-galaxy'. + return { error: 'no-galaxy' }; + } + // 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 - }); + var authorizeResult = sendAuthorizeRequest( + galaxyClientId, + galaxyRedirect, + encodeURIComponent(JSON.stringify(stateInfo)) + ); - // 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' }; + if (authorizeResult.error) { + return authorizeResult; } // Ask the galaxy to log us in with our auth code. try { var galaxyResult = httpHelpers.request({ - url: response.headers.location, + url: authorizeResult.location, method: 'GET', strictSSL: true, headers: { @@ -559,7 +561,7 @@ var logInToGalaxy = function (galaxyName) { } catch (e) { return { error: (body && body.error) || 'no-galaxy' }; } - response = galaxyResult.response; + var 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. diff --git a/tools/commands.js b/tools/commands.js index 82d8622d07..0eeaee5b5b 100644 --- a/tools/commands.js +++ b/tools/commands.js @@ -1306,7 +1306,6 @@ main.registerCommand({ minArgs: 0, maxArgs: 0, options: { - versionString: { type: String, short: "v", required: true }, // XXX A temporary option to create the package, until we sync // package metadata to the client. create: { type: Boolean } @@ -1318,11 +1317,16 @@ main.registerCommand({ )); var name = pkg.name; - var version = options.version; + var version = pkg.metadata.version; + + if (! version) { + process.stderr.write('Package must have a version\n'); + return 1; + } var conn = packageClient.loggedInPackagesConnection(); if (! conn) { - process.stderr.write('Publish failed'); + process.stderr.write('Publish failed\n'); return 1; } @@ -1338,7 +1342,7 @@ main.registerCommand({ process.stdout.write('Creating package version...\n'); var uploadInfo = conn.call('createPackageVersion', { packageName: pkg.name, - version: options.versionString, + version: version, description: pkg.metadata.summary }); @@ -1385,7 +1389,7 @@ main.registerCommand({ conn.call('publishPackageVersion', uploadInfo.uploadToken, tarballHash); conn.close(); process.stdout.write('Published ' + pkg.name + - ', version ' + options.versionString); + ', version ' + version); process.stdout.write('\nDone!\n'); return 0; diff --git a/tools/package-client.js b/tools/package-client.js index 93326ce285..79a4ee135b 100644 --- a/tools/package-client.js +++ b/tools/package-client.js @@ -23,7 +23,6 @@ var openPackageServerConnection = function () { }); }; - var loadLocalPackageData = function () { var finalCollections = {}; // var packages = ["versions", "packages", "builds"]; @@ -110,55 +109,84 @@ loadPackageData = function() { } // XXX onReconnect +// Returns a logged-in DDP connection to the package server, or null if +// we cannot log in. +// XXX needs a timeout exports.loggedInPackagesConnection = function () { - + // Make sure that we are logged in with Meteor Accounts so that we can + // do an OAuth flow. if (! auth.isLoggedIn()) { auth.doUsernamePasswordLogin({ retry: true }); } var conn = openPackageServerConnection(); - var serviceConfigurations = new (getLoadedPackages()['meteor']. - Meteor.Collection)('meteor_accounts_loginServiceConfiguration', { - connection: conn - }); - var fut = new Future(); - var serviceConfigurationsSub = conn.subscribe( - 'meteor.loginServiceConfiguration', - fut.resolver() + + var setUpOnReconnect = function () { + conn.onReconnect = function () { + conn.apply('login', [{ + resume: auth.getSessionToken(config.getPackageServerDomain()) + }], { wait: true }, function () { }); + }; + }; + + // Subscribe to the package server's service configurations so that we + // can get the OAuth client ID to kick off the OAuth flow. + var serviceConfigurations = new (getLoadedPackages().meteor.Meteor.Collection)( + 'meteor_accounts_loginServiceConfiguration', + { connection: conn } ); - fut.wait(); + var serviceConfigurationsSub = conn. + _subscribeAndWait('meteor.loginServiceConfiguration'); var accountsConfiguration = serviceConfigurations.findOne({ service: 'meteor-developer' }); - if (! accountsConfiguration) { + var cleanUp = function () { + serviceConfigurationsSub.stop(); + conn.close(); + }; + + if (! accountsConfiguration || ! accountsConfiguration.clientId) { + cleanUp(); return null; } var clientId = accountsConfiguration.clientId; var loginResult; - if (! auth.getSessionToken(config.getPackageServerDomain())) { - // Since we passed retry: true, we shouldn't ever get to this point - // unless we are now logged in with the accounts server. - var redirectUri = config.getPackageServerUrl() + - '/_oauth/meteor-developer?close'; - loginResult = auth.oauthFlow(conn, clientId, redirectUri, - config.getPackageServerDomain(), - 'package-server'); - if (! loginResult) { - conn.close(); - return null; - } - } else { + // Try to log in with an existing login token, if we have one. + var existingToken = auth.getSessionToken(config.getPackageServerDomain()); + if (existingToken) { loginResult = conn.apply('login', [{ - resume: auth.getSessionToken(config.getPackageServerDomain()) + resume: existingToken }], { wait: true }); - if (! loginResult || ! loginResult.token || ! loginResult.id) { - conn.close(); - return null; + + if (loginResult && loginResult.token && loginResult.id) { + // Success! + setUpOnReconnect(); + return conn; } } - return conn; + + // Either we didn't have an existing token, or it didn't work. Do an + // OAuth flow to log in. + var redirectUri = config.getPackageServerUrl() + + '/_oauth/meteor-developer?close'; + loginResult = auth.oauthFlow(conn, { + clientId: clientId, + redirectUri: redirectUri, + domain: config.getPackageServerDomain(), + sessionType: 'package-server' + }); + + if (loginResult && ! loginResult.error) { + setUpOnReconnect(); + return conn; + } else { + process.stderr.write('Error logging in to package server: ' + + loginResult.error + '\n'); + cleanUp(); + return null; + } }; diff --git a/tools/packages.js b/tools/packages.js index bca0e56012..650e9989f0 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -840,7 +840,7 @@ var Package = function (library, packageDirectoryForBuildInfo) { // dependencies self.library = library; - // Package metadata. Keys are 'summary' and 'internal'. + // Package metadata. Keys are 'summary', 'internal', 'version'. self.metadata = {}; // Available editions/subpackages ("slices") of this package. Array @@ -1190,6 +1190,7 @@ _.extend(Package.prototype, { // Set package metadata. Options: // - summary: for 'meteor list' // - internal: if true, hide in list + // - version: package version string (semver) // There used to be a third option documented here, // 'environments', but it was never implemented and no package // ever used it. @@ -1940,7 +1941,8 @@ _.extend(Package.prototype, { self.name = name; self.metadata = { summary: mainJson.summary, - internal: mainJson.internal + internal: mainJson.internal, + version: mainJson.version }; self.defaultSlices = mainJson.defaultSlices; self.testSlices = mainJson.testSlices; @@ -2121,6 +2123,7 @@ _.extend(Package.prototype, { format: "unipackage-pre1", summary: self.metadata.summary, internal: self.metadata.internal, + version: self.metadata.version, slices: [], defaultSlices: self.defaultSlices, testSlices: self.testSlices,