Bug 983350 - Include User-Agent header for FxAccount and TokenServer requests. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Thu, 13 Mar 2014 17:13:58 -0700
changeset 173539 f5109b04e0839942c4a8c9d9d26f853a368731fc
parent 173538 1d147df673e3fab296f404ac7fb900400b7ef277
child 173540 c425c825c08b6f345866a224a14e8cc54f119a71
push idunknown
push userunknown
push dateunknown
reviewersrnewman
bugs983350
milestone30.0a1
Bug 983350 - Include User-Agent header for FxAccount and TokenServer requests. r=rnewman
mobile/android/base/background/announcements/AnnouncementsConstants.java.in
mobile/android/base/background/announcements/AnnouncementsFetchResourceDelegate.java
mobile/android/base/background/announcements/AnnouncementsService.java
mobile/android/base/background/bagheera/BagheeraClient.java
mobile/android/base/background/bagheera/BagheeraRequestDelegate.java
mobile/android/base/background/fxa/FxAccountClient10.java
mobile/android/base/background/healthreport/HealthReportConstants.java.in
mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java
mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java
mobile/android/base/fxa/FxAccountConstants.java.in
mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
mobile/android/base/sync/SyncConstants.java.in
mobile/android/base/sync/jpake/stage/DeleteChannel.java
mobile/android/base/sync/jpake/stage/GetChannelStage.java
mobile/android/base/sync/jpake/stage/GetRequestStage.java
mobile/android/base/sync/jpake/stage/PutRequestStage.java
mobile/android/base/sync/net/BaseResource.java
mobile/android/base/sync/net/ResourceDelegate.java
mobile/android/base/sync/net/SyncStorageRequest.java
mobile/android/base/sync/setup/auth/AuthenticateAccountStage.java
mobile/android/base/sync/setup/auth/EnsureUserExistenceStage.java
mobile/android/base/sync/setup/auth/FetchUserNodeStage.java
mobile/android/base/sync/stage/EnsureClusterURLStage.java
mobile/android/base/tokenserver/TokenServerClient.java
mobile/android/base/tokenserver/TokenServerClientDelegate.java
--- a/mobile/android/base/background/announcements/AnnouncementsConstants.java.in
+++ b/mobile/android/base/background/announcements/AnnouncementsConstants.java.in
@@ -40,11 +40,11 @@ public class AnnouncementsConstants {
   // Stop reporting idle counts once they hit one year.
   public static long MAX_SANE_IDLE_DAYS = 365;
 
   // Don't track last launch if the timestamp is ridiculously out of range:
   // four years after build.
   public static long LATEST_ACCEPTED_LAUNCH_TIMESTAMP_MSEC = GlobalConstants.BUILD_TIMESTAMP_MSEC +
                                                              4 * 365 * MILLISECONDS_PER_DAY;
 
-  public static String ANNOUNCE_USER_AGENT = "Firefox Announcements " + GlobalConstants.MOZ_APP_VERSION;
+  public static String USER_AGENT = "Firefox Announcements " + GlobalConstants.MOZ_APP_VERSION;
   public static String ANNOUNCE_CHANNEL = GlobalConstants.MOZ_UPDATE_CHANNEL.replace("default", GlobalConstants.MOZ_OFFICIAL_BRANDING ? "release" : "dev");
 }
--- a/mobile/android/base/background/announcements/AnnouncementsFetchResourceDelegate.java
+++ b/mobile/android/base/background/announcements/AnnouncementsFetchResourceDelegate.java
@@ -42,21 +42,25 @@ public class AnnouncementsFetchResourceD
 
   public AnnouncementsFetchResourceDelegate(Resource resource, AnnouncementsFetchDelegate delegate) {
     super(resource);
     this.startTime = System.currentTimeMillis();
     this.delegate  = delegate;
   }
 
   @Override
+  public String getUserAgent() {
+    return delegate.getUserAgent();
+  }
+
+  @Override
   public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
     super.addHeaders(request, client);
 
     // The basics.
-    request.addHeader("User-Agent",      delegate.getUserAgent());
     request.addHeader("Accept-Language", delegate.getLocale().toString());
     request.addHeader("Accept",          ACCEPT_HEADER);
 
     // We never want to keep connections alive.
     request.addHeader("Connection", "close");
 
     // Set If-Modified-Since to avoid re-fetching content.
     final String ifModifiedSince = delegate.getLastDate();
@@ -171,9 +175,9 @@ public class AnnouncementsFetchResourceD
   /**
    * Be very thorough in case the superclass implementation changes.
    * We never want this to be an authenticated request.
    */
   @Override
   public AuthHeaderProvider getAuthHeaderProvider() {
     return null;
   }
-}
\ No newline at end of file
+}
--- a/mobile/android/base/background/announcements/AnnouncementsService.java
+++ b/mobile/android/base/background/announcements/AnnouncementsService.java
@@ -162,16 +162,17 @@ public class AnnouncementsService extend
   protected long getEarliestNextFetch() {
     return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, 0L);
   }
 
   protected void setLastFetch(final long fetch) {
     this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, fetch).commit();
   }
 
+  @Override
   public long getLastFetch() {
     return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME, 0L);
   }
 
   protected String setLastDate(final String fetch) {
     if (fetch == null) {
       this.getSharedPreferences().edit().remove(AnnouncementsConstants.PREF_LAST_FETCH_SERVER_DATE).commit();
       return null;
@@ -220,17 +221,17 @@ public class AnnouncementsService extend
 
   @Override
   public Locale getLocale() {
     return Locale.getDefault();
   }
 
   @Override
   public String getUserAgent() {
-    return AnnouncementsConstants.ANNOUNCE_USER_AGENT;
+    return AnnouncementsConstants.USER_AGENT;
   }
 
   protected void persistTimes(long fetched, String date) {
     setLastFetch(fetched);
     if (date != null) {
       setLastDate(date);
     }
   }
--- a/mobile/android/base/background/bagheera/BagheeraClient.java
+++ b/mobile/android/base/background/bagheera/BagheeraClient.java
@@ -162,16 +162,21 @@ public class BagheeraClient {
                                     final BagheeraRequestDelegate delegate) {
       super(resource);
       this.namespace = namespace;
       this.id = id;
       this.delegate = delegate;
     }
 
     @Override
+    public String getUserAgent() {
+      return delegate.getUserAgent();
+    }
+
+    @Override
     public int socketTimeout() {
       return DEFAULT_SOCKET_TIMEOUT_MSEC;
     }
 
     @Override
     public void handleHttpResponse(HttpResponse response) {
       final int status = response.getStatusLine().getStatusCode();
       switch (status) {
--- a/mobile/android/base/background/bagheera/BagheeraRequestDelegate.java
+++ b/mobile/android/base/background/bagheera/BagheeraRequestDelegate.java
@@ -5,9 +5,11 @@
 package org.mozilla.gecko.background.bagheera;
 
 import ch.boye.httpclientandroidlib.HttpResponse;
 
 public interface BagheeraRequestDelegate {
   void handleSuccess(int status, String namespace, String id, HttpResponse response);
   void handleError(Exception e);
   void handleFailure(int status, String namespace, HttpResponse response);
+
+  public String getUserAgent();
 }
--- a/mobile/android/base/background/fxa/FxAccountClient10.java
+++ b/mobile/android/base/background/fxa/FxAccountClient10.java
@@ -15,16 +15,17 @@ import java.util.Arrays;
 import java.util.Locale;
 import java.util.concurrent.Executor;
 
 import javax.crypto.Mac;
 
 import org.json.simple.JSONObject;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.HKDF;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.Resource;
@@ -201,16 +202,21 @@ public class FxAccountClient10 {
     public AuthHeaderProvider getAuthHeaderProvider() {
       if (tokenId != null && reqHMACKey != null) {
         return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload, skewHandler.getSkewInSeconds());
       }
       return super.getAuthHeaderProvider();
     }
 
     @Override
+    public String getUserAgent() {
+      return FxAccountConstants.USER_AGENT;
+    }
+
+    @Override
     public void handleHttpResponse(HttpResponse response) {
       try {
         final int status = validateResponse(response);
         skewHandler.updateSkew(response, now());
         invokeHandleSuccess(status, response);
       } catch (FxAccountClientRemoteException e) {
         if (!skewHandler.updateSkew(response, now())) {
           // If we couldn't update skew, but we got a failure, let's try clearing the skew.
--- a/mobile/android/base/background/healthreport/HealthReportConstants.java.in
+++ b/mobile/android/base/background/healthreport/HealthReportConstants.java.in
@@ -6,16 +6,18 @@
 package org.mozilla.gecko.background.healthreport;
 
 import org.mozilla.gecko.background.common.GlobalConstants;
 
 public class HealthReportConstants {
   public static final String HEALTH_AUTHORITY = "@ANDROID_PACKAGE_NAME@.health";
   public static final String GLOBAL_LOG_TAG = "GeckoHealth";
 
+  public static final String USER_AGENT = "Firefox-Android-HealthReport/ (" + GlobalConstants.MOZ_APP_DISPLAYNAME + " " + GlobalConstants.MOZ_APP_VERSION + ")";
+
   /**
    * The earliest allowable value for the last ping time, corresponding to May 2nd 2013.
    * Used for sanity checks.
    */
   public static final long EARLIEST_LAST_PING = 1367500000000L;
 
   // Not `final` so we have the option to turn this on at runtime with a magic addon.
   public static boolean UPLOAD_FEATURE_DISABLED = false;
--- a/mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java
+++ b/mobile/android/base/background/healthreport/upload/AndroidSubmissionClient.java
@@ -200,16 +200,21 @@ public class AndroidSubmissionClient imp
       this.delegate = delegate;
       this.localTime = localTime;
       this.isUpload = isUpload;
       this.methodString = this.isUpload ? "upload" : "delete";
       this.id = id;
     }
 
     @Override
+    public String getUserAgent() {
+      return HealthReportConstants.USER_AGENT;
+    }
+
+    @Override
     public void handleSuccess(int status, String namespace, String id, HttpResponse response) {
       BaseResource.consumeEntity(response);
       if (isUpload) {
         setLastUploadLocalTimeAndDocumentId(localTime, id);
       }
       Logger.debug(LOG_TAG, "Successful " + methodString + " at " + localTime + ".");
       delegate.onSuccess(localTime, id);
     }
--- a/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java
+++ b/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java
@@ -4,17 +4,16 @@
 
 package org.mozilla.gecko.browserid.verifier;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException;
 import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.net.BaseResource;
@@ -33,16 +32,21 @@ public class BrowserIDRemoteVerifierClie
     private final BrowserIDVerifierDelegate delegate;
 
     protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) {
       super(resource);
       this.delegate = delegate;
     }
 
     @Override
+    public String getUserAgent() {
+      return null;
+    }
+
+    @Override
     public void handleHttpResponse(HttpResponse response) {
       SyncResponse res = new SyncResponse(response);
       int statusCode = res.getStatusCode();
       Logger.debug(LOG_TAG, "Got response with status code " + statusCode + ".");
 
       if (statusCode != 200) {
         delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200."));
         return;
--- a/mobile/android/base/fxa/FxAccountConstants.java.in
+++ b/mobile/android/base/fxa/FxAccountConstants.java.in
@@ -1,15 +1,16 @@
 #filter substitution
 /* 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/. */
 
 package org.mozilla.gecko.fxa;
 
+import org.mozilla.gecko.background.common.GlobalConstants;
 import org.mozilla.gecko.background.common.log.Logger;
 
 public class FxAccountConstants {
   public static final String GLOBAL_LOG_TAG = "FxAccounts";
   public static final String ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@";
 
   public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1";
   public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5";
@@ -26,9 +27,11 @@ public class FxAccountConstants {
     }
   }
 
   // You must be at least 14 years old to create a Firefox Account.
   public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 14;
 
   // You must wait 15 minutes after failing an age check before trying to create a different account.
   public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;
+
+  public static final String USER_AGENT = "Firefox-Android-FxAccounts/ (" + GlobalConstants.MOZ_APP_DISPLAYNAME + " " + GlobalConstants.MOZ_APP_VERSION + ")";
 }
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -293,16 +293,21 @@ public class FxAccountSyncAdapter extend
                                    final KeyBundle syncKeyBundle,
                                    final String clientState,
                                    final SessionCallback callback,
                                    final Bundle extras) {
     final TokenServerClientDelegate delegate = new TokenServerClientDelegate() {
       private boolean didReceiveBackoff = false;
 
       @Override
+      public String getUserAgent() {
+        return FxAccountConstants.USER_AGENT;
+      }
+
+      @Override
       public void handleSuccess(final TokenServerToken token) {
         FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
 
         if (!didReceiveBackoff) {
           // We must be OK to touch this token server.
           tokenBackoffHandler.setEarliestNextRequest(0L);
         }
 
--- a/mobile/android/base/sync/SyncConstants.java.in
+++ b/mobile/android/base/sync/SyncConstants.java.in
@@ -13,19 +13,19 @@ import org.mozilla.gecko.background.comm
 public class SyncConstants {
   public static final String GLOBAL_LOG_TAG = "FxSync";
   public static final String SYNC_MAJOR_VERSION  = "1";
   public static final String SYNC_MINOR_VERSION  = "0";
   public static final String SYNC_VERSION_STRING = SYNC_MAJOR_VERSION + "." +
                                                    GlobalConstants.MOZ_APP_VERSION + "." +
                                                    SYNC_MINOR_VERSION;
 
-  public static final String SYNC_USER_AGENT = "Firefox AndroidSync " +
-                                               SYNC_VERSION_STRING + " (" +
-                                               GlobalConstants.MOZ_APP_DISPLAYNAME + ")";
+  public static final String USER_AGENT = "Firefox AndroidSync " +
+                                          SYNC_VERSION_STRING + " (" +
+                                          GlobalConstants.MOZ_APP_DISPLAYNAME + ")";
 
   public static final String ACCOUNTTYPE_SYNC = "@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@";
 
   /**
    * Bug 790931: this action is broadcast when an Android Sync Account is
    * deleted.  This allows each installed Firefox to delete any Sync Account
    * pickle file and to (try to) wipe its client record from the Sync server.
    * <p>
--- a/mobile/android/base/sync/jpake/stage/DeleteChannel.java
+++ b/mobile/android/base/sync/jpake/stage/DeleteChannel.java
@@ -4,16 +4,17 @@
 
 package org.mozilla.gecko.sync.jpake.stage;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.jpake.JPakeClient;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.setup.auth.AccountAuthenticator;
 
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
@@ -31,16 +32,20 @@ public class DeleteChannel {
     try {
       httpResource = new BaseResource(jClient.channelUrl);
     } catch (URISyntaxException e) {
       Logger.debug(LOG_TAG, "Encountered URISyntax exception, displaying abort anyway.");
       jClient.displayAbort(reason);
       return;
     }
     httpResource.delegate = new BaseResourceDelegate(httpResource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       @Override
       public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
         request.setHeader(new BasicHeader(KEYEXCHANGE_ID_HEADER,  jClient.clientId));
         request.setHeader(new BasicHeader(KEYEXCHANGE_CID_HEADER, jClient.channel));
       }
 
       @Override
--- a/mobile/android/base/sync/jpake/stage/GetChannelStage.java
+++ b/mobile/android/base/sync/jpake/stage/GetChannelStage.java
@@ -5,16 +5,17 @@
 package org.mozilla.gecko.sync.jpake.stage;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 
 import org.json.simple.parser.JSONParser;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.jpake.JPakeClient;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.SyncResponse;
 import org.mozilla.gecko.sync.setup.Constants;
 
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
@@ -75,16 +76,20 @@ public class GetChannelStage extends JPa
       jClient.abort(Constants.JPAKE_ERROR_CHANNEL);
       return;
     }
   }
 
   private void makeChannelRequest(final GetChannelStageDelegate callbackDelegate, String getChannelUrl, final String clientId) throws URISyntaxException {
     final BaseResource httpResource = new BaseResource(getChannelUrl);
     httpResource.delegate = new BaseResourceDelegate(httpResource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       @Override
       public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
         request.setHeader(new BasicHeader("X-KeyExchange-Id", clientId));
       }
 
       @Override
       public void handleHttpResponse(HttpResponse response) {
--- a/mobile/android/base/sync/jpake/stage/GetRequestStage.java
+++ b/mobile/android/base/sync/jpake/stage/GetRequestStage.java
@@ -6,16 +6,17 @@ package org.mozilla.gecko.sync.jpake.sta
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 import java.util.Timer;
 import java.util.TimerTask;
 
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.jpake.JPakeClient;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
 import org.mozilla.gecko.sync.net.SyncResponse;
 import org.mozilla.gecko.sync.setup.Constants;
 
 import ch.boye.httpclientandroidlib.Header;
@@ -96,16 +97,20 @@ public class GetRequestStage extends JPa
     Logger.debug(LOG_TAG, "Scheduling GET request.");
     getStepTimerTask = new GetStepTimerTask(httpRequest);
     timerScheduler.schedule(getStepTimerTask, jClient.jpakePollInterval);
   }
 
   private Resource createGetRequest(final GetRequestStageDelegate callbackDelegate, final JPakeClient jpakeClient) throws URISyntaxException {
     BaseResource httpResource = new BaseResource(jpakeClient.channelUrl);
     httpResource.delegate = new BaseResourceDelegate(httpResource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       @Override
       public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
         request.setHeader(new BasicHeader("X-KeyExchange-Id", jpakeClient.clientId));
         if (jpakeClient.myEtag != null) {
           request.setHeader(new BasicHeader("If-None-Match", jpakeClient.myEtag));
         }
       }
--- a/mobile/android/base/sync/jpake/stage/PutRequestStage.java
+++ b/mobile/android/base/sync/jpake/stage/PutRequestStage.java
@@ -7,20 +7,21 @@ package org.mozilla.gecko.sync.jpake.sta
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 import java.util.Timer;
 import java.util.TimerTask;
 
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.jpake.JPakeClient;
 import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
-import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.setup.Constants;
 
 import ch.boye.httpclientandroidlib.Header;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
 import ch.boye.httpclientandroidlib.message.BasicHeader;
@@ -87,16 +88,20 @@ public class PutRequestStage extends JPa
       return;
     }
     Logger.debug(LOG_TAG, "Outgoing message: " + jClient.jOutgoing.toJSONString());
   }
 
   private Resource createPutRequest(final PutRequestStageDelegate callbackDelegate, final JPakeClient jpakeClient) throws URISyntaxException {
     BaseResource httpResource = new BaseResource(jpakeClient.channelUrl);
     httpResource.delegate = new BaseResourceDelegate(httpResource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       @Override
       public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
         request.setHeader(new BasicHeader("X-KeyExchange-Id", jpakeClient.clientId));
         if (jpakeClient.theirEtag != null) {
           request.setHeader(new BasicHeader("If-Match", jpakeClient.theirEtag));
         } else {
           request.setHeader(new BasicHeader("If-None-Match", "*"));
--- a/mobile/android/base/sync/net/BaseResource.java
+++ b/mobile/android/base/sync/net/BaseResource.java
@@ -172,16 +172,20 @@ public class BaseResource implements Res
     addAuthCacheToContext(request, context);
 
     HttpParams params = client.getParams();
     HttpConnectionParams.setConnectionTimeout(params, delegate.connectionTimeout());
     HttpConnectionParams.setSoTimeout(params, delegate.socketTimeout());
     HttpConnectionParams.setStaleCheckingEnabled(params, false);
     HttpProtocolParams.setContentCharset(params, charset);
     HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
+    final String userAgent = delegate.getUserAgent();
+    if (userAgent != null) {
+      HttpProtocolParams.setUserAgent(params, userAgent);
+    }
     delegate.addHeaders(request, client);
   }
 
   private static Object connManagerMonitor = new Object();
   private static ClientConnectionManager connManager;
 
   // Call within a synchronized block on connManagerMonitor.
   private static ClientConnectionManager enableTLSConnectionManager() throws KeyManagementException, NoSuchAlgorithmException  {
--- a/mobile/android/base/sync/net/ResourceDelegate.java
+++ b/mobile/android/base/sync/net/ResourceDelegate.java
@@ -21,16 +21,23 @@ import ch.boye.httpclientandroidlib.impl
  * @author rnewman
  *
  */
 public interface ResourceDelegate {
   // Request augmentation.
   AuthHeaderProvider getAuthHeaderProvider();
   void addHeaders(HttpRequestBase request, DefaultHttpClient client);
 
+  /**
+   * The value of the User-Agent header to include with the request.
+   *
+   * @return User-Agent header value; null means do not set User-Agent header.
+   */
+  public String getUserAgent();
+
   // Response handling.
 
   /**
    * Override this to handle an HttpResponse.
    *
    * ResourceDelegate implementers <b>must</b> ensure that HTTP responses are
    * fully consumed to ensure that connections are returned to the pool, for
    * example by calling <code>EntityUtils.consume(response.getEntity())</code>.
--- a/mobile/android/base/sync/net/SyncStorageRequest.java
+++ b/mobile/android/base/sync/net/SyncStorageRequest.java
@@ -13,17 +13,16 @@ import java.util.HashMap;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.SyncConstants;
 
 import ch.boye.httpclientandroidlib.HttpEntity;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
-import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
 
 public class SyncStorageRequest implements Resource {
   public static HashMap<String, String> SERVER_ERROR_MESSAGES;
   static {
     HashMap<String, String> errors = new HashMap<String, String>();
 
     // Sync protocol errors.
     errors.put("1", "Illegal method/protocol");
@@ -106,16 +105,21 @@ public class SyncStorageRequest implemen
     }
 
     @Override
     public AuthHeaderProvider getAuthHeaderProvider() {
       return request.delegate.getAuthHeaderProvider();
     }
 
     @Override
+    public String getUserAgent() {
+      return SyncConstants.USER_AGENT;
+    }
+
+    @Override
     public void handleHttpResponse(HttpResponse response) {
       Logger.debug(LOG_TAG, "SyncStorageResourceDelegate handling response: " + response.getStatusLine() + ".");
       SyncStorageRequestDelegate d = this.request.delegate;
       SyncStorageResponse res = new SyncStorageResponse(response);
       // It is the responsibility of the delegate handlers to completely consume the response.
       if (res.wasSuccessful()) {
         d.handleRequestSuccess(res);
       } else {
@@ -141,18 +145,16 @@ public class SyncStorageRequest implemen
 
     @Override
     public void handleTransportException(GeneralSecurityException e) {
       this.request.delegate.handleRequestError(e);
     }
 
     @Override
     public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
-      client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, SyncConstants.SYNC_USER_AGENT);
-
       // Clients can use their delegate interface to specify X-If-Unmodified-Since.
       String ifUnmodifiedSince = this.request.delegate.ifUnmodifiedSince();
       if (ifUnmodifiedSince != null) {
         Logger.debug(LOG_TAG, "Making request with X-If-Unmodified-Since = " + ifUnmodifiedSince);
         request.setHeader("x-if-unmodified-since", ifUnmodifiedSince);
       }
       if (request.getMethod().equalsIgnoreCase("DELETE")) {
         request.addHeader("x-confirm-delete", "1");
@@ -168,24 +170,28 @@ public class SyncStorageRequest implemen
     super();
   }
 
   // Default implementation. Override this.
   protected BaseResourceDelegate makeResourceDelegate(SyncStorageRequest request) {
     return new SyncStorageResourceDelegate(request);
   }
 
+  @Override
   public void get() {
     this.resource.get();
   }
 
+  @Override
   public void delete() {
     this.resource.delete();
   }
 
+  @Override
   public void post(HttpEntity body) {
     this.resource.post(body);
   }
 
+  @Override
   public void put(HttpEntity body) {
     this.resource.put(body);
   }
 }
--- a/mobile/android/base/sync/setup/auth/AuthenticateAccountStage.java
+++ b/mobile/android/base/sync/setup/auth/AuthenticateAccountStage.java
@@ -91,23 +91,26 @@ public class AuthenticateAccountStage im
    * @param authRequestUrl
    * @param authHeader
    * @throws URISyntaxException
    */
   // Made public for testing.
   public void authenticateAccount(final AuthenticateAccountStageDelegate callbackDelegate, final String authRequestUrl, final String authHeader) throws URISyntaxException {
     final BaseResource httpResource = new BaseResource(authRequestUrl);
     httpResource.delegate = new BaseResourceDelegate(httpResource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       @Override
       public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
         // Make reference to request, to abort if necessary.
         httpRequest = request;
         client.log.enableDebug(true);
-        request.setHeader(new BasicHeader("User-Agent", SyncConstants.SYNC_USER_AGENT));
         // Host header is not set for some reason, so do it explicitly.
         try {
           URI authServerUri = new URI(authRequestUrl);
           request.setHeader(new BasicHeader("Host", authServerUri.getHost()));
         } catch (URISyntaxException e) {
           Logger.error(LOG_TAG, "Malformed uri, will be caught elsewhere.", e);
         }
         request.setHeader(new BasicHeader("Authorization", authHeader));
--- a/mobile/android/base/sync/setup/auth/EnsureUserExistenceStage.java
+++ b/mobile/android/base/sync/setup/auth/EnsureUserExistenceStage.java
@@ -8,16 +8,17 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 
 public class EnsureUserExistenceStage implements AuthenticatorStage {
   private final String LOG_TAG = "EnsureUserExistence";
@@ -51,16 +52,20 @@ public class EnsureUserExistenceStage im
       }
 
     };
 
     // This is not the same as Utils.nodeWeaveURL: it's missing the trailing node/weave.
     String userRequestUrl = aa.nodeServer + "user/1.0/" + aa.username;
     final BaseResource httpResource = new BaseResource(userRequestUrl);
     httpResource.delegate = new BaseResourceDelegate(httpResource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       @Override
       public void handleHttpResponse(HttpResponse response) {
         int statusCode = response.getStatusLine().getStatusCode();
         switch(statusCode) {
         case 200:
           try {
             InputStream content = response.getEntity().getContent();
--- a/mobile/android/base/sync/setup/auth/FetchUserNodeStage.java
+++ b/mobile/android/base/sync/setup/auth/FetchUserNodeStage.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.sync.setup.aut
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 
 public class FetchUserNodeStage implements AuthenticatorStage {
@@ -73,16 +74,20 @@ public class FetchUserNodeStage implemen
       }
     });
   }
 
   private BaseResource makeFetchNodeRequest(final FetchNodeStageDelegate callbackDelegate, String fetchNodeUrl) throws URISyntaxException {
     // Fetch node containing user.
     final BaseResource httpResource = new BaseResource(fetchNodeUrl);
     httpResource.delegate = new BaseResourceDelegate(httpResource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       @Override
       public void handleHttpResponse(HttpResponse response) {
         int statusCode = response.getStatusLine().getStatusCode();
         switch(statusCode) {
         case 200:
           try {
             InputStream content = response.getEntity().getContent();
--- a/mobile/android/base/sync/stage/EnsureClusterURLStage.java
+++ b/mobile/android/base/sync/stage/EnsureClusterURLStage.java
@@ -10,16 +10,17 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.NodeAuthenticationException;
 import org.mozilla.gecko.sync.NullClusterURLException;
+import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.ThreadPool;
 import org.mozilla.gecko.sync.delegates.NodeAssignmentCallback;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 
 import ch.boye.httpclientandroidlib.HttpEntity;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
@@ -71,16 +72,20 @@ public class EnsureClusterURLStage exten
    * @throws URISyntaxException
    */
   public static void fetchClusterURL(final String nodeWeaveURL,
                                      final ClusterURLFetchDelegate delegate) throws URISyntaxException {
     Logger.info(LOG_TAG, "In fetchClusterURL: node/weave is " + nodeWeaveURL);
 
     BaseResource resource = new BaseResource(nodeWeaveURL);
     resource.delegate = new BaseResourceDelegate(resource) {
+      @Override
+      public String getUserAgent() {
+        return SyncConstants.USER_AGENT;
+      }
 
       /**
        * Handle the response for GET https://server/pathname/version/username/node/weave.
        *
        * Returns the Sync Node that the client is located on.
        * Storage operations should be directed to that node.
        *
        * Return value: the node URL, an unadorned (not JSON) string.
@@ -174,16 +179,17 @@ public class EnsureClusterURLStage exten
       public void handleTransportException(GeneralSecurityException e) {
         delegate.handleError(e);
       }
     };
 
     resource.get();
   }
 
+  @Override
   public void execute() throws NoSuchStageException {
     final URI oldClusterURL = session.config.getClusterURL();
     final boolean wantNodeAssignment = callback.wantNodeAssignment();
 
     if (!wantNodeAssignment && oldClusterURL != null) {
       Logger.info(LOG_TAG, "Cluster URL is already set and not stale. Continuing with sync.");
       session.advance();
       return;
--- a/mobile/android/base/tokenserver/TokenServerClient.java
+++ b/mobile/android/base/tokenserver/TokenServerClient.java
@@ -252,16 +252,21 @@ public class TokenServerClient {
       this.delegate = delegate;
       this.assertion = assertion;
       this.clientState = clientState;
       this.resource = resource;
       this.conditionsAccepted = conditionsAccepted;
     }
 
     @Override
+    public String getUserAgent() {
+      return delegate.getUserAgent();
+    }
+
+    @Override
     public void handleHttpResponse(HttpResponse response) {
       // Skew.
       SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource);
       skewHandler.updateSkew(response, System.currentTimeMillis());
 
       // Extract backoff regardless of whether this was an error response, and
       // Retry-After for 503 responses. The error will be handled elsewhere.)
       SyncResponse res = new SyncResponse(response);
--- a/mobile/android/base/tokenserver/TokenServerClientDelegate.java
+++ b/mobile/android/base/tokenserver/TokenServerClientDelegate.java
@@ -1,16 +1,19 @@
 /* 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/. */
 
 package org.mozilla.gecko.tokenserver;
 
+
 public interface TokenServerClientDelegate {
   void handleSuccess(TokenServerToken token);
   void handleFailure(TokenServerException e);
   void handleError(Exception e);
 
   /**
    * Might be called multiple times, in addition to the other terminating handler methods.
    */
   void handleBackoff(int backoffSeconds);
-}
\ No newline at end of file
+
+  public String getUserAgent();
+}