diff --git a/ChangeLog b/ChangeLog index 6dd3d19e1c3114b1e3bc726c093759c2ca1856c0..63313b66470814d106127312a39006ebbce6acce 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,22 @@ -train-2011.01.25 (in progress): +train-2011.02.08 (in progress): + +train-2011.02.02: + * i18n support, now BrowserID speaks your language: #926, #936, #977, #1013, #1031 + * improved error screens on slow server responses: #913, #915 + * better cache headers on all html resources (which Vary by Accept-Languages): #226, #620, #920, #938 + * cosmetic fixes: #918, #947, #966, #981, #1020, #987 + * preliminary work to improve messaging when cookies are disabled: #835 + * remove dead code: #925 + * fix include.orig.js: #921, #911 + * load testing compatibility and minified resources are no longer mutually exclusive: #939 + * improve usability via default button focus (just hit enter in more places): #946, #960 + * scripts to deploy to an amazon EC2 instance. + * improve configuration mechanism: #582, #1006 + * limit post bodies to verifier: #878 + * cancel from forgot password doesn't cause your email to be, uh, forgotten: #1001 + * remember the users email as they move from screen to screen in the dialog: #984, #1001, #1002, #1003, #1004 + * secondary "cancel" style buttons have a smaller font: #1020 + * build fixes: #1021, #1024 train-2011.01.18: * support for 3rd party primary identity providers: #761, #904, #865 @@ -12,6 +30,16 @@ train-2011.01.18: * language/rendering refinements: #850, #439, #622, #818, #901, #630, #888, #345, #815 * front end performance improvements: #899, #910 * better UX for network timeouts: #905 + * (hotfix 2012.01.23) Remove unwanted scrollbar in dialog: issue #947 + * (hotfix 2012.01.23) Fix black backgrounds on IE8: issue #929 + * (hotfix 2012.01.23) fix broken transition to "check your email": #933, #934, #935 + * (hotfix 2012.01.24) Fix "slow script" error on IE8 during keygen on behalf of primary: #956 + * (hotfix 2012.01.24) Publish javascript API to provide a native-support compatible for primaries' auth pages: #909 + * (hotfix 2012.01.24) Allow load testing hooks to be enabled with minified resources: #939 + * (hotfix 2012.01.24) IE8 fixes for primary flow: #962, #961, #958, #955 + * (hotfix 2012.01.24) print correct url for where the user will be directed: #964 + * (hotfix 2012.01.31) fix silent assertions: #972 + * (hotfix 2012.02.01) fix verification of email on a browser other than the initiator: #973, #1026 (and maybe others) train-2011.01.05: * client entropy pool mixes in randomness from server for better browser RNG: #298, #800 diff --git a/example/primary/sign_in.html b/example/primary/sign_in.html index 0e89ba67bb5343504e29942180f10bfb85b2afa4..51a0eaccae58c47b4ffa31f88220cb0a9b2aec60 100644 --- a/example/primary/sign_in.html +++ b/example/primary/sign_in.html @@ -16,6 +16,8 @@ body { margin: auto; font: 13px/1.5 Helvetica, Arial, 'Liberation Sans', FreeSan .intro { font-size: 1.2em; width: 600px; margin: auto; } .main { text-align: center; margin-top: 2em; font-size: 1.2em; width: 500px; margin: auto; } #who { font-weight: bold; } +#cancel { font-size: small; } +button { line-height: 20px; } </style> </head> @@ -27,6 +29,7 @@ body { margin: auto; font: 13px/1.5 Helvetica, Arial, 'Liberation Sans', FreeSan <div class="main" id="logged_out"> Sign in as <span id="who">...</span> <button>doit</button> + <a href="#" id="cancel">cancel</a> </div> <script type="text/javascript" src="jquery.js"></script> @@ -57,6 +60,11 @@ $(document).ready(function() { window.location = getParameterByName('return_to'); }); }); + + $("#cancel").click(function(e) { + e.preventDefault(); + window.location = getParameterByName('return_to'); + }); }); </script> </body> diff --git a/lib/browserid/views.js b/lib/browserid/views.js index 819d8b271828819c0b27aee460765567070892a7..d87269bd1bb467ce5ee0f20174bf28cd17569286 100644 --- a/lib/browserid/views.js +++ b/lib/browserid/views.js @@ -52,10 +52,9 @@ exports.setup = function(app) { }); app.get('/include.js', function(req, res, next) { - var env = config.get('env'); - req.url = "/include_js/include.js"; - if (env === 'production') { + + if (config.get('use_minified_resources') === true) { req.url = "/production/include.js" } diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index 3a2aa0eea5f2e68e6a6cc9a9e1d025cba78060cd..9f90bc380fb24ae5848238673c8452d41680ed9f 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -147,6 +147,10 @@ BrowserID.Modules.Actions = (function() { startService("verify_primary_user", info); }, + doCannotVerifyRequiredPrimary: function(info) { + this.renderError("cannot_verify_required_email", info); + }, + doPrimaryUserProvisioned: function(info) { startService("primary_user_provisioned", info); }, diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js index d7acf4f3ca37aa4338797c5254e4152e31c94794..7192887df535bad47060c338a62ee6acefc9c327 100644 --- a/resources/static/dialog/controllers/dialog.js +++ b/resources/static/dialog/controllers/dialog.js @@ -128,19 +128,27 @@ BrowserID.Modules.Dialog = (function() { // XXX Perhaps put this into the state machine. self.bind(win, "unload", onWindowUnload); + if(hash.indexOf("#CREATE_EMAIL=") === 0) { var email = hash.replace(/#CREATE_EMAIL=/, ""); - self.renderDialog("primary_user_verified", { email: email }); - self.close("primary_user", { email: email, add: false }); + params.type = "primary"; + params.email = email; + params.add = false; } else if(hash.indexOf("#ADD_EMAIL=") === 0) { var email = hash.replace(/#ADD_EMAIL=/, ""); - self.renderDialog("primary_user_verified", { email: email }); - self.close("primary_user", { email: email, add: true }); + params.type = "primary"; + params.email = email; + params.add = true; } - else { - self.publish("start", params); + + /* + if(hash.indexOf("REQUIRED=true") > -1) { + params.requiredEmail = params.email; } + */ + + self.publish("start", params); } } diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js index d29f7ffa4b5ab3d9bfb7b82a8063730e01e82158..22a3098e78db35eba06155414b2251cf0a8f3f13 100644 --- a/resources/static/dialog/resources/state.js +++ b/resources/static/dialog/resources/state.js @@ -10,10 +10,12 @@ BrowserID.State = (function() { publish = mediator.publish.bind(mediator), user = bid.User, moduleManager = bid.module, + complete = bid.Helpers.complete, controller, addPrimaryUser = false, email, - requiredEmail; + requiredEmail, + primaryVerificationInfo; function startStateMachine() { var self = this, @@ -27,7 +29,7 @@ BrowserID.State = (function() { var func = controller[msg].bind(controller); self.gotoState(save, func, options); - } + }, cancelState = self.popState.bind(self); subscribe("offline", function(msg, info) { @@ -41,11 +43,14 @@ BrowserID.State = (function() { self.allowPersistent = !!info.allowPersistent; requiredEmail = info.requiredEmail; - if ((typeof(requiredEmail) !== "undefined") - && (!bid.verifyEmail(requiredEmail))) { + if ((typeof(requiredEmail) !== "undefined") && (!bid.verifyEmail(requiredEmail))) { // Invalid format startState("doError", "invalid_required_email", {email: requiredEmail}); } + else if(info.email && info.type === "primary") { + primaryVerificationInfo = info; + publish("primary_user", info); + } else { startState("doCheckAuth"); } @@ -95,8 +100,6 @@ BrowserID.State = (function() { addPrimaryUser = !!info.add; email = info.email; - //updateCurrentStateInfo(info); - var idInfo = storage.getEmail(email); if(idInfo && idInfo.cert) { publish("primary_user_ready", info); @@ -120,7 +123,24 @@ BrowserID.State = (function() { info.add = !!addPrimaryUser; info.email = email; info.requiredEmail = !!requiredEmail; - startState("doVerifyPrimaryUser", info); + if(primaryVerificationInfo) { + primaryVerificationInfo = null; + if(requiredEmail) { + startState("doCannotVerifyRequiredPrimary", info); + } + else if(info.add) { + // Add the pick_email in case the user cancels the add_email screen. + // The user needs something to go "back" to. + publish("pick_email", info); + publish("add_email", info); + } + else { + publish("authenticate", info); + } + } + else { + startState("doVerifyPrimaryUser", info); + } }); subscribe("primary_user_authenticating", function(msg, info) { @@ -142,11 +162,11 @@ BrowserID.State = (function() { }); subscribe("email_chosen", function(msg, info) { - var email = info.email + var email = info.email, idInfo = storage.getEmail(email); - function complete() { - info.complete && info.complete(); + function oncomplete() { + complete(info.complete); } if(idInfo) { @@ -176,8 +196,8 @@ BrowserID.State = (function() { else { startState("doEmailChosen", info); } - complete(); - }, complete); + oncomplete(); + }, oncomplete); } } else { @@ -219,7 +239,7 @@ BrowserID.State = (function() { }); subscribe("add_email", function(msg, info) { - startState("doAddEmail"); + startState("doAddEmail", info); }); subscribe("email_staged", function(msg, info) { diff --git a/resources/static/dialog/views/cannot_verify_required_email.ejs b/resources/static/dialog/views/cannot_verify_required_email.ejs new file mode 100644 index 0000000000000000000000000000000000000000..3bd7f68169ab8aee0d0a5a2f0107dd3d933b006c --- /dev/null +++ b/resources/static/dialog/views/cannot_verify_required_email.ejs @@ -0,0 +1,12 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + + <h2 id="cannotVerifyRequiredEmail"><%= format(gettext('Cannot verify %s'), [email]) %></h2> + + <p> + <%= format(gettext('%s is a required address, but we cannot verify that you own this address.'), [email]) %> + </p> + + diff --git a/resources/static/dialog/views/test_template_no_input.ejs b/resources/static/dialog/views/test_template_no_input.ejs index 825dc5a89ae4ae28507c68f1625b0c327a2c5a5c..1fc8e84c44d2cc16a95e1ff59749c88519263196 100644 --- a/resources/static/dialog/views/test_template_no_input.ejs +++ b/resources/static/dialog/views/test_template_no_input.ejs @@ -3,7 +3,5 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -<%= gettext("translated text") %> - <button id="focusButton">Button!</button> diff --git a/resources/static/dialog/views/test_template_with_input.ejs b/resources/static/dialog/views/test_template_with_input.ejs index 60e33f244b2597b7a2acdf0e235ef904cf1f8a73..86d4ffa2a8d0dfcc8d44e6b930926dd6a4f887e1 100644 --- a/resources/static/dialog/views/test_template_with_input.ejs +++ b/resources/static/dialog/views/test_template_with_input.ejs @@ -4,5 +4,3 @@ <input id="templateInput" type="text" value="" /> -<%= gettext("translated text") %> - diff --git a/resources/static/dialog/views/verify_primary_user.ejs b/resources/static/dialog/views/verify_primary_user.ejs index 105cb872e2b771bb171849ffd32f92a9723318f0..ea43a338118f5409606a1296c3e32299830d74dc 100644 --- a/resources/static/dialog/views/verify_primary_user.ejs +++ b/resources/static/dialog/views/verify_primary_user.ejs @@ -17,7 +17,6 @@ <li> <%= gettext("You must sign in with your email provider to verify ownership of this address. This window will be redirected to") %> - <p> <strong><%= auth_url %></strong>. </p> @@ -34,7 +33,6 @@ <div class="cf form_section"> <%= gettext("You must sign in with your email provider to verify ownership of this address. This window will be redirected to") %> - <p> <strong><%= auth_url %></strong>. </p> diff --git a/resources/static/shared/wait-messages.js b/resources/static/shared/wait-messages.js index 20b11a4b50e2d817073f9599e41f0104b3d13327..00a64aa65609bbe0036a9bda3033220cced2ca87 100644 --- a/resources/static/shared/wait-messages.js +++ b/resources/static/shared/wait-messages.js @@ -17,7 +17,7 @@ BrowserID.Wait = (function(){ slowXHR: { title: gettext("We are sorry, this request is taking a LOOONG time."), - message: gettext("This message will go away when the request completes (hopefully soon). If you wait too long, close this window and try again."), + message: gettext("This message will go away when the request completes (hopefully soon). If you wait too long, close this window and try again."), id: "slowXHR" } diff --git a/resources/static/test/cases/controllers/actions.js b/resources/static/test/cases/controllers/actions.js index 74ef5b3a55aec42bb78eb8e76ea7b2ee4a36c910..20593ecac0c0a8920a89d67386f861beaa5cafa0 100644 --- a/resources/static/test/cases/controllers/actions.js +++ b/resources/static/test/cases/controllers/actions.js @@ -97,6 +97,18 @@ }); }); + asyncTest("doCannotVerifyRequiredPrimary - show the error screen", function() { + createController({ + ready: function() { + controller.doCannotVerifyRequiredPrimary({ email: "testuser@testuser.com"}); + + testHelpers.testErrorVisible(); + start(); + } + }); + + }); + asyncTest("doPrimaryUserProvisioned - start the primary_user_verified service", function() { createController({ ready: function() { diff --git a/resources/static/test/cases/controllers/dialog.js b/resources/static/test/cases/controllers/dialog.js index 643309043d13baaf436bc3ca8ce25626adc7c5fd..ee944e7d4e4b471795188f44523add65d394d051 100644 --- a/resources/static/test/cases/controllers/dialog.js +++ b/resources/static/test/cases/controllers/dialog.js @@ -131,12 +131,13 @@ }); }); - asyncTest("initialization with #CREATE_EMAIL=testuser@testuser.com", function() { + asyncTest("initialization with #CREATE_EMAIL=testuser@testuser.com - trigger start with correct params", function() { winMock.location.hash = "#CREATE_EMAIL=testuser@testuser.com"; createController({ ready: function() { - mediator.subscribe("primary_user", function(msg, info) { + mediator.subscribe("start", function(msg, info) { + equal(info.type, "primary", "correct type"); equal(info.email, "testuser@testuser.com", "email_chosen with correct email"); equal(info.add, false, "add is not specified with CREATE_EMAIL option"); start(); @@ -153,12 +154,13 @@ }); }); - asyncTest("initialization with #ADD_EMAIL=testuser@testuser.com", function() { + asyncTest("initialization with #ADD_EMAIL=testuser@testuser.com - trigger start with correct params", function() { winMock.location.hash = "#ADD_EMAIL=testuser@testuser.com"; createController({ ready: function() { - mediator.subscribe("primary_user", function(msg, info) { + mediator.subscribe("start", function(msg, info) { + equal(info.type, "primary", "correct type"); equal(info.email, "testuser@testuser.com", "email_chosen with correct email"); equal(info.add, true, "add is specified with ADD_EMAIL option"); start(); diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js index 84f7961a57efadd5b86798f9992d884aa4270f3d..a9e95a48b27d6f9bcdddb780e85731a8b2c1059c 100644 --- a/resources/static/test/cases/resources/state.js +++ b/resources/static/test/cases/resources/state.js @@ -9,6 +9,7 @@ var bid = BrowserID, mediator = bid.Mediator, State = bid.State, + user = bid.User, machine, actions, storage = bid.Storage, @@ -106,27 +107,47 @@ equal(actions.info.doConfirmEmail.required, true, "doConfirmEmail called with required flag"); }); - test("primary_user with already provisioned primary user calls doEmailChosen", function() { + test("primary_user with already provisioned primary user - call doEmailChosen", function() { storage.addEmail("testuser@testuser.com", { type: "primary", cert: "cert" }); mediator.publish("primary_user", { email: "testuser@testuser.com" }); ok(actions.called.doEmailChosen, "doEmailChosen called"); }); - test("primary_user with unprovisioned primary user doProvisionPrimaryUser", function() { + test("primary_user with unprovisioned primary user - call doProvisionPrimaryUser", function() { mediator.publish("primary_user", { email: "testuser@testuser.com" }); ok(actions.called.doProvisionPrimaryUser, "doPrimaryUserProvisioned called"); }); - test("primary_user_provisioned calls doEmailChosen", function() { + test("primary_user_provisioned - call doEmailChosen", function() { mediator.publish("primary_user_provisioned", { email: "testuser@testuser.com" }); ok(actions.called.doPrimaryUserProvisioned, "doPrimaryUserProvisioned called"); }); - test("primary_user_unauthenticated calls doVerifyPrimaryUser", function() { + test("primary_user_unauthenticated before verification - call doVerifyPrimaryUser", function() { + mediator.publish("start"); mediator.publish("primary_user_unauthenticated"); ok(actions.called.doVerifyPrimaryUser, "doVerifyPrimaryUser called"); }); + test("primary_user_unauthenticated after required email - call doCannotVerifyRequiredPrimary", function() { + mediator.publish("start", { requiredEmail: "testuser@testuser.com", type: "primary", add: false, email: "testuser@testuser.com" }); + mediator.publish("primary_user_unauthenticated"); + ok(actions.called.doCannotVerifyRequiredPrimary, "doCannotVerifyRequiredPrimary called"); + }); + + test("primary_user_unauthenticated after verification of new user - call doAuthenticate", function() { + mediator.publish("start", { email: "testuser@testuser.com", type: "primary", add: false }); + mediator.publish("primary_user_unauthenticated"); + ok(actions.called.doAuthenticate, "doAuthenticate called"); + }); + + test("primary_user_unauthenticated after verification of additional email to current user - call doPickEmail and doAddEmail", function() { + mediator.publish("start", { email: "testuser@testuser.com", type: "primary", add: true }); + mediator.publish("primary_user_unauthenticated"); + ok(actions.called.doPickEmail, "doPickEmail called"); + ok(actions.called.doAddEmail, "doAddEmail called"); + }); + test("primary_user_authenticating stops all modules", function() { try { mediator.publish("primary_user_authenticating"); @@ -137,13 +158,13 @@ } }); - test("primary_user calls doProvisionPrimaryUser", function() { + test("primary_user - call doProvisionPrimaryUser", function() { mediator.publish("primary_user", { email: "testuser@testuser.com", assertion: "assertion" }); ok(actions.called.doProvisionPrimaryUser, "doProvisionPrimaryUser called"); }); - test("primary_user_ready calls doEmailChosen", function() { + test("primary_user_ready - call doEmailChosen", function() { mediator.publish("primary_user_ready", { email: "testuser@testuser.com", assertion: "assertion" }); ok(actions.called.doEmailChosen, "doEmailChosen called"); @@ -200,11 +221,11 @@ equal(actions.info.doAssertionGenerated, "assertion", "assertion generated with good assertion"); }); - test("add_email", function() { - // XXX rename add_email to request_add_email - mediator.publish("add_email"); + test("add_email - call doAddEmail", function() { + mediator.publish("add_email", { email: "testuser@testuser.com" }); ok(actions.called.doAddEmail, "user wants to add an email"); + ok(actions.info.doAddEmail.email, "testuser@testuser.com", "correct email passed"); }); test("email_confirmed", function() { @@ -237,13 +258,13 @@ equal(actions.info.doAuthenticate.email, "testuser@testuser.com", "authenticate with testuser@testuser.com"); }); - test("start with no required email address should go straight to checking auth", function() { + test("start with no special parameters - go straight to checking auth", function() { mediator.publish("start"); equal(actions.called.doCheckAuth, true, "checking auth on start"); }); - test("start with invalid requiredEmail prints error screen", function() { + test("start with invalid requiredEmail - print error screen", function() { mediator.publish("start", { requiredEmail: "bademail" }); @@ -251,7 +272,7 @@ equal(actions.called.doError, true, "error screen is shown"); }); - test("start with empty requiredEmail prints error screen", function() { + test("start with empty requiredEmail - prints error screen", function() { mediator.publish("start", { requiredEmail: "" }); @@ -259,14 +280,22 @@ equal(actions.called.doError, true, "error screen is shown"); }); - test("start with valid requiredEmail goes to auth", function() { - mediator.publish("start", { - requiredEmail: "testuser@testuser.com" - }); + test("start with valid requiredEmail - go to doCheckAuth", function() { + mediator.publish("start", { requiredEmail: "testuser@testuser.com" }); equal(actions.called.doCheckAuth, true, "checking auth on start"); }); + asyncTest("start to complete successful primary email verification - goto 'primary_user'", function() { + mediator.subscribe("primary_user", function(msg, info) { + equal(info.email, "testuser@testuser.com", "correct email given"); + equal(info.add, true, "correct add flag"); + start(); + }); + + mediator.publish("start", { email: "testuser@testuser.com", type: "primary", add: true }); + }); + test("cancel", function() { mediator.publish("cancel"); diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs index 3d779ae93a12675cc253596a09df76ae2e3022e1..7a5e9b63989afabc87bb253356d4166713eaa68c 100644 --- a/resources/views/dialog_layout.ejs +++ b/resources/views/dialog_layout.ejs @@ -20,7 +20,7 @@ <link href="/dialog/css/m.css" rel="stylesheet" type="text/css"> <% } %> <link href="https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic" rel="stylesheet" type="text/css"> - <title><%= gettext('Browser ID') %></title> + <title><%= gettext('BrowserID') %></title> </head> <body class="waiting"> <div id="wrapper"> diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs index e8ead6f6ffad837d1c43ecd3c9222d7940cbc97d..4df7506ed2be239db5aeeff28049931b1820ede6 100644 --- a/resources/views/verify_email_address.ejs +++ b/resources/views/verify_email_address.ejs @@ -5,7 +5,7 @@ <div id="vAlign" class="display_always"> <div id="signUpFormWrap"> <ul class="notifications"> - <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li> + <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li> <li class="notification error" id="cannotcommunicate"><%= gettext('Error comunicating with server.') %></li> <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li> </ul> diff --git a/scripts/browserid.spec b/scripts/browserid.spec index 0f7f1a708c0c8be4b73dc7ef4e0a888335470865..6e98d59e4c32965287b071fed0ed3b00bd46a01c 100644 --- a/scripts/browserid.spec +++ b/scripts/browserid.spec @@ -1,7 +1,7 @@ %define _rootdir /opt/browserid Name: browserid-server -Version: 0.2012.01.25 +Version: 0.2012.02.08 Release: 1%{?dist} Summary: BrowserID server Packager: Pete Fritchman <petef@mozilla.com> diff --git a/scripts/deploy.js b/scripts/deploy.js index b5df5f74d24527b72f3c70481ea3482ecedb9a32..aebb6c72ff31fdd28b0bc0a31e197f44d53f68b2 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -40,25 +40,32 @@ verbs['destroy'] = function(args) { } var name = args[0]; validateName(name); + var hostname = name + ".hacksign.in"; - process.stdout.write("trying to destroy VM for " + name + ".hacksign.in: "); - vm.destroy(name, function(err) { + process.stdout.write("trying to destroy VM for " + hostname + ": "); + vm.destroy(name, function(err, deets) { console.log(err ? ("failed: " + err) : "done"); - process.stdout.write("trying to remove DNS for " + name + ".hacksign.in: "); - dns.deleteRecord(name, function(err) { + process.stdout.write("trying to remove DNS for " + hostname + ": "); + dns.deleteRecord(hostname, function(err) { console.log(err ? "failed: " + err : "done"); + if (deets && deets.ipAddress) { + process.stdout.write("trying to remove git remote: "); + git.removeRemote(name, deets.ipAddress, function(err) { + console.log(err ? "failed: " + err : "done"); + }); + } }); }); } verbs['test'] = function() { // let's see if we can contact aws and zerigo - process.stdout.write("Checking DNS management access: "); + process.stdout.write("Checking DNS management access: "); dns.inUse("somerandomname", function(err) { console.log(err ? "NOT ok: " + err : "good"); - process.stdout.write("Checking AWS access: "); + process.stdout.write("Checking AWS access: "); vm.list(function(err) { - console.log(err ? "NOT ok: " + err : "good"); + console.log(err ? "NOT ok: " + err : "good"); }); }); } @@ -69,10 +76,12 @@ verbs['deploy'] = function(args) { } var name = args[0]; validateName(name); + var hostname = name + ".hacksign.in"; + var longName = 'browserid deployment (' + name + ')'; console.log("attempting to set up " + name + ".hacksign.in"); - dns.inUse(name, function(err, r) { + dns.inUse(hostname, function(err, r) { checkErr(err); if (r) checkErr("sorry! that name '" + name + "' is already being used. so sad"); @@ -83,11 +92,11 @@ verbs['deploy'] = function(args) { vm.waitForInstance(r.instanceId, function(err, deets) { checkErr(err); console.log(" ... Instance ready, setting up DNS"); - dns.updateRecord(name, deets.ipAddress, function(err) { + dns.updateRecord(name, "hacksign.in", deets.ipAddress, function(err) { checkErr(err); console.log(" ... DNS set up, setting human readable name in aws"); - vm.setName(r.instanceId, args[0], function(err) { + vm.setName(r.instanceId, longName, function(err) { checkErr(err); console.log(" ... name set, waiting for ssh access and configuring"); var config = { public_url: "https://" + name + ".hacksign.in"}; @@ -99,7 +108,7 @@ verbs['deploy'] = function(args) { if (err && /already exists/.test(err)) { console.log("OOPS! you already have a git remote named 'test'!"); console.log("to create a new one: git remote add <name> " + - "app@" + deets.ipAddress + ":git"); + "app@" + deets.ipAddress + ":git"); } else { checkErr(err); } diff --git a/scripts/deploy/dns.js b/scripts/deploy/dns.js index 522dc538bb9e90d20298fe96d28de9c473de8898..effcd7d49f51814080340718981134de446ffcac 100644 --- a/scripts/deploy/dns.js +++ b/scripts/deploy/dns.js @@ -39,11 +39,11 @@ function doRequest(method, path, body, cb) { req.end(); }; -exports.updateRecord = function (hostname, ip, cb) { +exports.updateRecord = function (hostname, zone, ip, cb) { doRequest('GET', '/api/1.1/zones.xml', null, function(err, r) { if (err) return cb(err); var m = jsel.match('object:has(:root > .domain:val(?)) > .id .$t', - [ 'hacksign.in' ], r); + [ zone ], r); if (m.length != 1) return cb("couldn't extract domain id from zerigo"); var path = '/api/1.1/hosts.xml?zone_id=' + m[0]; var body = '<host><data>' + ip + '</data><host-type>A</host-type>'; @@ -56,7 +56,7 @@ exports.updateRecord = function (hostname, ip, cb) { }; exports.deleteRecord = function (hostname, cb) { - doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname + ".hacksign.in", null, function(err, r) { + doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { if (err) return cb(err); var m = jsel.match('.host .id > .$t', r); if (!m.length) return cb("no such DNS record"); @@ -73,7 +73,7 @@ exports.deleteRecord = function (hostname, cb) { }; exports.inUse = function (hostname, cb) { - doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname + ".hacksign.in", null, function(err, r) { + doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { if (err) return cb(err); var m = jsel.match('.hosts object:.host', r); // we shouldn't have multiple! oops! let's return the first one diff --git a/scripts/deploy/git.js b/scripts/deploy/git.js index 34b88c26be8b74f78d8b546da26ea7e99759de5b..757e6678ff1046abc464df1f5a38197f01c1590a 100644 --- a/scripts/deploy/git.js +++ b/scripts/deploy/git.js @@ -1,7 +1,70 @@ const child_process = require('child_process'); +spawn = child_process.spawn; exports.addRemote = function(name, host, cb) { var cmd = 'git remote add ' + name + ' app@'+ host + ':git'; child_process.exec(cmd, cb); }; + +// remove a remote, but only if it is pointed to a specific +// host. This will keep deploy from killing manuall remotes +// that you've set up +exports.removeRemote = function(name, host, cb) { + var desired = 'app@'+ host + ':git'; + var cmd = 'git remote -v show | grep push'; + child_process.exec(cmd, function(err, r) { + try { + var remotes = {}; + r.split('\n').forEach(function(line) { + if (!line.length) return; + var line = line.split('\t'); + if (!line.length == 2) return; + remotes[line[0]] = line[1].split(" ")[0]; + }); + if (remotes[name] && remotes[name] === desired) { + child_process.exec('git remote rm ' + name, cb); + } else { + throw "no such remote"; + } + } catch(e) { + cb(e); + } + }); +}; + +exports.currentSHA = function(dir, cb) { + if (typeof dir === 'function' && cb === undefined) { + cb = dir; + dir = path.join(__dirname, '..', '..'); + } + + var p = spawn('git', [ 'log', '--pretty=%h', '-1' ], { cwd: dir }); + var buf = ""; + p.stdout.on('data', function(d) { + buf += d; + }); + p.on('exit', function(code, signal) { + var gitsha = buf.toString().trim(); + if (gitsha && gitsha.length === 7) { + return cb(null, gitsha); + } + cb("can't extract git sha from " + dir); + }); +}; + +exports.push = function(dir, host, pr, cb) { + if (typeof host === 'function' && cb === undefined) { + cb = pr; + pr = host; + host = dir; + dir = path.join(__dirname, '..', '..'); + } + + var p = spawn('git', [ 'push', 'app@' + host + ":git", 'dev:master' ], { cwd: dir }); + p.stdout.on('data', pr); + p.stderr.on('data', pr); + p.on('exit', function(code, signal) { + return cb(code = 0); + }); +}; \ No newline at end of file diff --git a/scripts/deploy/ssh.js b/scripts/deploy/ssh.js index 9f76461c1520dc2ef8548c49513522a9bb7cbbc0..29c504ab24be86c8e8123d978c7ab8c7ab2dcf25 100644 --- a/scripts/deploy/ssh.js +++ b/scripts/deploy/ssh.js @@ -24,3 +24,15 @@ exports.copyUpConfig = function(host, config, cb) { oneTry(); }); }; + +exports.copySSL = function(host, pub, priv, cb) { + var cmd = 'scp -o "StrictHostKeyChecking no" ' + pub + ' ec2-user@' + host + ":/etc/ssl/certs/hacksign.in.crt"; + child_process.exec(cmd, function(err, r) { + if (err) return cb(err); + var cmd = 'scp -o "StrictHostKeyChecking no" ' + priv + ' ec2-user@' + host + ":/etc/ssl/certs/hacksign.in.key"; + child_process.exec(cmd, function(err, r) { + var cmd = 'ssh -o "StrictHostKeyChecking no" ec2-user@' + host + " 'sudo /etc/init.d/nginx restart'"; + child_process.exec(cmd, cb); + }); + }); +}; diff --git a/scripts/deploy/vm.js b/scripts/deploy/vm.js index f6efc45e6e606873fab39ed77dce584c8a3e65cd..ae94b2a11691e3daaab585be16b0429a4f9dbee3 100644 --- a/scripts/deploy/vm.js +++ b/scripts/deploy/vm.js @@ -4,7 +4,7 @@ jsel = require('JSONSelect'), key = require('./key.js'), sec = require('./sec.js'); -const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-1553827c'; +const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-6978a900'; function extractInstanceDeets(horribleBlob) { var instance = {}; @@ -44,7 +44,7 @@ exports.destroy = function(name, cb) { InstanceId: r[name].instanceId }, function(result) { try { return cb(result.Errors.Error.Message); } catch(e) {}; - cb(null); + cb(null, r[name]); }); }); }; @@ -105,8 +105,6 @@ exports.waitForInstance = function(id, cb) { }; exports.setName = function(id, name, cb) { - name = 'browserid deployment (' + name + ')'; - aws.call('CreateTags', { "ResourceId.0": id, "Tag.0.Key": 'Name', diff --git a/scripts/deploy_dev.js b/scripts/deploy_dev.js new file mode 100755 index 0000000000000000000000000000000000000000..f615705719ea99e2a8f7c75fc5fbdcd9a34dbb46 --- /dev/null +++ b/scripts/deploy_dev.js @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +/* + * Deploy dev.diresworb.org, for fun and profit. + */ + +const +aws = require('./deploy/aws.js'); +path = require('path'); +vm = require('./deploy/vm.js'), +key = require('./deploy/key.js'), +ssh = require('./deploy/ssh.js'), +git = require('./deploy/git.js'), +dns = require('./deploy/dns.js'), +util = require('util'), +events = require('events'), +fs = require('fs'); + +// verify we have files we need + +// a class capable of deploying and emmitting events along the way +function DevDeployer() { + events.EventEmitter.call(this); + + this.sslpub = process.env['DEV_SSL_PUB']; + this.sslpriv = process.env['DEV_SSL_PRIV']; + + if (!this.sslpub || !this.sslpriv) { + throw("you must provide ssl cert paths via DEV_SSL_PUB & DEV_SSL_PRIV"); + } + + if (!fs.statSync(this.sslpub).isFile() || !fs.statSync(this.sslpriv).isFile()) { + throw("DEV_SSL_PUB & DEV_SSL_PRIV must be paths to actual files. duh"); + } +} + +util.inherits(DevDeployer, events.EventEmitter); + +DevDeployer.prototype.setup = function(cb) { + var self = this; + git.currentSHA(function(err, r) { + if (err) return cb(err); + self.sha = r; + vm.startImage(function(err, r) { + if (err) return cb(err); + self.emit('progress', "starting new image"); + vm.waitForInstance(r.instanceId, function(err, d) { + if (err) return cb(err); + self.deets = d; + self.emit('progress', "image started"); + vm.setName(r.instanceId, "dev.diresworb.org (" + self.sha + ")", function(err, r) { + if (err) return cb(err); + self.emit('progress', "name set"); + cb(null); + }); + }); + }); + }); +} + +DevDeployer.prototype.configure = function(cb) { + var self = this; + var config = { public_url: "https://dev.diresworb.org" }; + ssh.copyUpConfig(self.deets.ipAddress, config, function (err) { + if (err) return cb(err); + ssh.copySSL(self.deets.ipAddress, self.sslpub, self.sslpriv, cb); + }); +} + +DevDeployer.prototype.pushCode = function(cb) { + var self = this; + git.push(this.deets.ipAddress, function(d) { self.emit('build_output', d); }, cb); +} + +DevDeployer.prototype.updateDNS = function(cb) { + var self = this; + dns.deleteRecord('dev.diresworb.org', function() { + dns.updateRecord('', 'dev.diresworb.org', self.deets.ipAddress, cb); + }); +} + +var deployer = new DevDeployer(); + +deployer.on('progress', function(d) { + console.log("PR: " + d); +}); + +deployer.on('build_output', function(d) { + console.log("BO: " + d); +}); + +function checkerr(err) { + if (err) { + process.stderr.write("fatal error: " + err + "\n"); + process.exit(1); + } +} + +var startTime = new Date(); +deployer.setup(function(err) { + checkerr(err); + deployer.configure(function(err) { + checkerr(err); + deployer.updateDNS(function(err) { + checkerr(err); + deployer.pushCode(function(err) { + checkerr(err); + console.log("dev.diresworb.org (" + deployer.sha + ") deployed to " + + deployer.deets.ipAddress + " in " + + ((new Date() - startTime) / 1000.0).toFixed(2) + "s"); + }); + }); + }); +}); diff --git a/scripts/production_locales b/scripts/production_locales index 1f8fb02ec8c924917c7092d1481231987f73a1a4..7245ecbfea8ce0ae0f4ba1698ccf3c75734783d3 100755 --- a/scripts/production_locales +++ b/scripts/production_locales @@ -1,11 +1,13 @@ #!/usr/bin/env node /* This is a helper script for compress.sh */ +var path = require('path'); + // configuration will create directories under VAR_PATH process.env['VAR_PATH'] = '/tmp/browserid'; // Pick up production languages -process.env['NODE_ENV'] = 'production'; +process.env['CONFIG_FILES'] = process.env['CONFIG_FILES'] || path.join(__dirname, '..', 'config', 'local.json'); var path = require('path'), format = require('util').format, @@ -13,4 +15,4 @@ var path = require('path'), i18n = require(path.join(__dirname, '../lib/i18n.js')); var langs = config.get('supported_languages'); -process.stdout.write(format("%s\n", langs.map(i18n.localeFrom).join(' '))); \ No newline at end of file +process.stdout.write(format("%s\n", langs.map(i18n.localeFrom).join(' '))); diff --git a/scripts/show_config.js b/scripts/show_config.js index db4073ad7a39882f5e8c7c45ac704835a0df874d..857e511ddbf29967157a348eab0fbd5ee1aa7778 100755 --- a/scripts/show_config.js +++ b/scripts/show_config.js @@ -1,3 +1,9 @@ #!/usr/bin/env node +var path = require('path'); + +// use the 'local' configuration if one isn't explicitly specified in the environment +process.env['CONFIG_FILES'] = process.env['CONFIG_FILES'] || + path.join(__dirname, '..', 'config', 'local.json'); + console.log(require("../lib/configuration.js").toString());