nassh 0.8.22.3: add support for new relay dance

Convert the old relay-host and relay-port prefs into a single
'relay-options' pref.  The 'relay-options' pref has always existed,
but never had a field in the connect dialog.

Old host/port preferences are merged into the options pref.

Add a canned configuration that corresponds to the correct settings
for google corp access on the new relay proxy front-end.

Fix the inifinite backoff.

Change-Id: I43de6205e5120d89a2b001bc5d242b453fba7401
Reviewed-on: https://chromium-review.googlesource.com/178097
Reviewed-by: Marvelous Marius <mschilder@chromium.org>
Tested-by: Robert Ginda <rginda@chromium.org>
diff --git a/nassh/_locales/en/messages.json b/nassh/_locales/en/messages.json
index 1b2adf3..4c10132 100644
--- a/nassh/_locales/en/messages.json
+++ b/nassh/_locales/en/messages.json
@@ -116,24 +116,14 @@
     "description": "Text use as the aria-label for the 'port' field."
   },
 
-  "FIELD_RELAY_HOST_PLACEHOLDER": {
-    "message": "relay hostname",
-    "description": "Text use as the placeholder for the 'relay-host' field."
+  "FIELD_RELAY_OPTIONS_PLACEHOLDER": {
+    "message": "relay options",
+    "description": "Text use as the placeholder for the 'relay-options' field."
   },
 
-  "FIELD_RELAY_HOST_ARIA_LABEL": {
-    "message": "relay host",
-    "description": "Text use as the aria-label for the 'relay-host' field."
-  },
-
-  "FIELD_RELAY_PORT_ARIA_LABEL": {
-    "message": "port",
-    "description": "Text use as the aria-label for the 'relay-port' field."
-  },
-
-  "FIELD_RELAY_PORT_PLACEHOLDER": {
-    "message": "port",
-    "description": "Text use as the placeholder for the 'relay-port' field."
+  "FIELD_RELAY_OPTIONS_ARIA_LABEL": {
+    "message": "relay options",
+    "description": "Text use as the aria-label for the 'relay-options' field."
   },
 
   "IDENTITY_LABEL": {
diff --git a/nassh/doc/changelog.txt b/nassh/doc/changelog.txt
index 9a6715d..b8364e2 100644
--- a/nassh/doc/changelog.txt
+++ b/nassh/doc/changelog.txt
@@ -1,3 +1,9 @@
+0.8.22.3, 2013-11-26, Learn the new relay dance.
+
+* Update the google relay code to understand the new proxy front-end to the
+  relay servers.  Should be backwards compatible.  Googlers can now specify
+  the relay option "--config=google" to magically specify the correct config.
+
 0.8.22.2, 2013-11-26, Crosh polish.
 
 * Display reconnect menu on non-zero exit status.
diff --git a/nassh/html/nassh_connect_dialog.html b/nassh/html/nassh_connect_dialog.html
index 94c109e..d193ab2 100644
--- a/nassh/html/nassh_connect_dialog.html
+++ b/nassh/html/nassh_connect_dialog.html
@@ -35,10 +35,8 @@
                  min='1' max='65535'>
         </x-hbox>
         <x-hbox>
-          <input x-box id='field-relay-host' x-flex='1' type='text'>
-          <input x-box id='field-relay-port' type='number' min='1' max='65535'
-                 i18n='{"aria-label": "$id"}'
-                 min='1' max='65535'>
+          <input x-box id='field-relay-options' x-flex='1' type='text'
+                 i18n='{"aria-label": "$id", "placeholder": "$id"}'>
         </x-hbox>
         <br>
         <x-hbox x-align='baseline'>
diff --git a/nassh/js/nassh_command_instance.js b/nassh/js/nassh_command_instance.js
index a6bea6e..dcbd8b8 100644
--- a/nassh/js/nassh_command_instance.js
+++ b/nassh/js/nassh_command_instance.js
@@ -376,8 +376,6 @@
       username: prefs.get('username'),
       hostname: prefs.get('hostname'),
       port: prefs.get('port'),
-      relayHost: prefs.get('relay-host'),
-      relayPort: prefs.get('relay-port'),
       relayOptions: prefs.get('relay-options'),
       identity: prefs.get('identity'),
       argstr: prefs.get('argstr'),
@@ -415,12 +413,19 @@
   // some callers may come directly to connectToDestination.
   document.location.hash = destination;
 
+  var relayOptions = '';
+  if (ary[4]) {
+    relayOptions = '--proxy-host=' + ary[4];
+
+    if (ary[5])
+      relayOptions += ' --proxy-port=' + ary[5];
+  }
+
   return this.connectTo({
       username: ary[1],
       hostname: ary[2],
       port: ary[3],
-      relayHost: ary[4],
-      relayPort: ary[5]
+      relayOptions: relayOptions
   });
 };
 
@@ -444,10 +449,8 @@
     return;
   }
 
-  if (params.relayHost) {
+  if (params.relayOptions) {
     this.relay_ = new nassh.GoogleRelay(this.io,
-                                        params.relayHost,
-                                        params.relayPort,
                                         params.relayOptions);
     this.io.println(nassh.msg(
         'INITIALIZING_RELAY',
diff --git a/nassh/js/nassh_connect_dialog.js b/nassh/js/nassh_connect_dialog.js
index a0ed6bf..1135dde 100644
--- a/nassh/js/nassh_connect_dialog.js
+++ b/nassh/js/nassh_connect_dialog.js
@@ -218,7 +218,7 @@
     });
 
   // These fields interact with each-other's placeholder text.
-  ['description', 'username', 'hostname', 'port', 'relay-host', 'relay-port'
+  ['description', 'username', 'hostname', 'port'
   ].forEach(function(name) {
       var field = this.$f(name);
 
@@ -243,7 +243,7 @@
                    this.maybeDirty_.bind(this, name));
     }.bind(this));
 
-  ['description', 'username', 'hostname', 'port', 'relay-host', 'relay-port',
+  ['description', 'username', 'hostname', 'port', 'relay-options',
    'identity', 'argstr', 'terminal-profile'
   ].forEach(function(name) {
       addListeners(this.$f(name), ['focus', 'blur'],
@@ -329,14 +329,14 @@
 
   var prefs = this.currentProfileRecord_.prefs;
 
-  ['description', 'username', 'hostname', 'port', 'relay-host', 'relay-port',
+  ['description', 'username', 'hostname', 'port', 'relay-options',
    'identity', 'argstr', 'terminal-profile'].forEach(function(name) {
        if (name == 'identity' && this.$f('identity').selectedIndex === 0)
          return;
 
        var value = this.$f(name).value;
 
-       if (name == 'port' || name == 'relay-port')
+       if (name == 'port')
          value = value ? parseInt(value) : '';
 
        if ((!prefs && !value) || (prefs && value == prefs.get(name)))
@@ -410,7 +410,7 @@
  * to bulk-default.
  */
 nassh.ConnectDialog.prototype.maybeCopyPlaceholders_ = function() {
-  ['description', 'username', 'hostname', 'port', 'relay-host', 'relay-port'
+  ['description', 'username', 'hostname', 'port'
   ].forEach(this.maybeCopyPlaceholder_.bind(this));
   this.syncButtons_();
 };
@@ -458,7 +458,7 @@
   // Copy the remaining match elements into the appropriate placeholder
   // attribute.  Set the default placeholder text from this.str.placeholders
   // for any field that was not matched.
-  ['username', 'hostname', 'port', 'relay-host', 'relay-port',
+  ['username', 'hostname', 'port'
   ].forEach(function(name) {
       var value = ary.shift();
       if (!value) {
@@ -484,10 +484,6 @@
     var v = this.$f('port').value;
     if (v)
       placeholder += ':' + v;
-
-    v = this.$f('relay-host').value;
-    if (v)
-      placeholder += '@' + v;
   } else {
     placeholder = this.msg('FIELD_DESCRIPTION_PLACEHOLDER');
   }
@@ -499,8 +495,8 @@
  * Sync the form with the current profile record.
  */
 nassh.ConnectDialog.prototype.syncForm_ = function() {
-  ['description', 'username', 'hostname', 'port', 'argstr', 'relay-host',
-   'relay-port', 'identity', 'terminal-profile'
+  ['description', 'username', 'hostname', 'port', 'argstr', 'relay-options',
+   'identity', 'terminal-profile'
   ].forEach(function(n) {
       var emptyValue = '';
       if (n == 'identity')
diff --git a/nassh/js/nassh_google_relay.js b/nassh/js/nassh_google_relay.js
index 4be9b50..1dd782f 100644
--- a/nassh/js/nassh_google_relay.js
+++ b/nassh/js/nassh_google_relay.js
@@ -68,16 +68,53 @@
  * 6. Writes are queued up and sent to /write.
  */
 
-nassh.GoogleRelay = function(io, proxyHost, proxyPort, options) {
+nassh.GoogleRelay = function(io, optionString) {
   this.io = io;
-  this.proxyHost = proxyHost;
-  this.proxyPort = proxyPort || 8022;
-  this.useSecure = options.search('--use-ssl') != -1;
-  this.useWebsocket = !(options.search('--use-xhr') != -1);
+  this.options = nassh.GoogleRelay.parseOptionString(optionString);
+  this.proxyHost = this.options['--proxy-host'];
+  this.proxyPort = this.options['--proxy-port'] || 8022;
+  this.useSecure = this.options['--use-ssl'];
+  this.useWebsocket = !this.options['--use-xhr'];
   this.relayServer = null;
   this.relayServerSocket = null;
 };
 
+nassh.GoogleRelay.parseOptionString = function(optionString) {
+  var rv = {};
+
+  var optionList = optionString.split(/\s+/g);
+  for (var i = 0; i < optionList.length; i++) {
+    var option = optionList[i];
+    if (option.substr(0, 1) != '-') {
+      // Bare option overrides --host.
+      rv['--proxy-host'] = option;
+    } else {
+      var pos = option.indexOf('=');
+      if (pos != -1) {
+        rv[option.substr(0, pos)] = option.substr(pos + 1);
+      } else {
+        var ary = option.match(/--no-(.*)/);
+        if (ary) {
+          rv['--' + ary[1]] = false;
+        } else {
+          rv[option] = true;
+        }
+      }
+    }
+  }
+
+  if (rv['--config'] == 'google') {
+    if (!('--proxy-host' in rv))
+      rv['--proxy-host'] = 'spdy-proxy.ext.google.com';
+    if (!('--use-ssl' in rv))
+      rv['--use-ssl'] = true;
+    if (!('--relay-prefix-field' in rv))
+      rv['--relay-prefix-field'] = '2';
+  }
+
+  return rv;
+};
+
 /**
  * The pattern for the cookie server's url.
  */
@@ -128,9 +165,32 @@
   // if we succeed at finding a relay host.
   var relayHost = sessionStorage.getItem('googleRelay.relayHost');
   var relayPort = sessionStorage.getItem('googleRelay.relayPort') ||
-    (this.useWebsocket ? 8022 : 8023);
+    (this.useXHR ? 8023 : 8022);
 
   if (relayHost) {
+    var relayPrefixField = parseInt(this.options['--relay-prefix-field']);
+    if (relayPrefixField) {
+      // If this option is set, we're supposed to assume the relayHost is a
+      // '.' delimited list of fields (like a hostname), isolate the field
+      // at the 1-based relayPrefixField position, and create the actual
+      // relayHost by prepending this field to the original proxyHost.
+      //
+      // TODO(rginda): Yes, this is a kludge.  Returning the correct hostname
+      // from the proxy is sometimes difficult, for a reason unknown to me.
+      var relayPrefix = relayHost.split(/\./g)[relayPrefixField - 1];
+      if (relayPrefix) {
+        if (this.proxyHost.substr(0, relayPrefix.length + 1) !=
+            relayPrefix + '.') {
+          // Only add the prefix if the proxyHost doesn't already include it.
+          relayHost = relayPrefix + '.' + this.proxyHost;
+        }
+      } else {
+        console.warn('Error getting relay prefix field: ' + relayPrefixField +
+                     ' from: ' + relayHost);
+        this.relayHost = null;
+      }
+    }
+
     var expectedResumePath =
         sessionStorage.getItem('googleRelay.resumePath');
     if (expectedResumePath == resumePath) {
@@ -138,7 +198,7 @@
       var pattern = this.relayServerPattern;
       this.relayServer = lib.f.replaceVars(pattern,
           {host: relayHost, port: relayPort, protocol: protocol});
-      if (this.useWebsocket) {
+      if (!this.useXHR) {
         protocol = this.useSecure ? 'wss' : 'ws';
         this.relayServerSocket = lib.f.replaceVars(pattern,
             {host: relayHost, port: relayPort, protocol: protocol});
diff --git a/nassh/js/nassh_preference_manager.js b/nassh/js/nassh_preference_manager.js
index 0b8e0a2..3f78661 100644
--- a/nassh/js/nassh_preference_manager.js
+++ b/nassh/js/nassh_preference_manager.js
@@ -107,3 +107,37 @@
 nassh.ProfilePreferenceManager.prototype = {
   __proto__: lib.PreferenceManager.prototype
 };
+
+nassh.ProfilePreferenceManager.prototype.readStorage = function(opt_callback) {
+  var appendOption = function(str) {
+    var options = this.get('relay-options');
+    if (options) {
+      options += ' ' + str;
+    } else {
+      options = str;
+    }
+
+    this.set('relay-option', options);
+  }.bind(this);
+
+  var onRead = function() {
+    var host = this.get('relay-host');
+    if (host) {
+      console.warn('Merging relay-host preference with relay-options');
+      this.reset('relay-host');
+      appendOption('--proxy-host=' + host);
+    }
+
+    var port = this.get('relay-port');
+    if (port) {
+      this.reset('relay-port');
+      console.warn('Merging relay-host preference with relay-options');
+      appendOption('--proxy-port=' + port);
+    }
+
+    if (opt_callback)
+      opt_callback();
+  }.bind(this);
+
+  lib.PreferenceManager.prototype.readStorage.call(this, onRead);
+};
diff --git a/nassh/js/nassh_stream_google_relay.js b/nassh/js/nassh_stream_google_relay.js
index 14071af..e378cf8 100644
--- a/nassh/js/nassh_stream_google_relay.js
+++ b/nassh/js/nassh_stream_google_relay.js
@@ -153,7 +153,9 @@
   if (!this.backoffMS_) {
     this.backoffMS_ = 1;
   } else {
-    this.backoffMS_ = this.backoffMS_ * 2 + 93;  // Exponential backoff.
+    this.backoffMS_ = this.backoffMS_ * 2 + 13;
+    if (this.backoffMS_ > 10000)
+      this.backoffMS_ = 10000 - (x % 9000);
   }
 
   var requestType = isRead ? 'read' : 'write';
diff --git a/nassh/manifest.json b/nassh/manifest.json
index e077f15..c23c6a7 100644
--- a/nassh/manifest.json
+++ b/nassh/manifest.json
@@ -4,7 +4,7 @@
   "manifest_version": 2,
   "content_security_policy": "script-src 'self'; object-src 'self'",
   "name": "Secure Shell (tot)",
-  "version": "0.8.22.2",
+  "version": "0.8.22.3",
   "default_locale": "en",
   "icons": {
     "128": "images/dev/icon-128.png",