From 73adec6cbff89ea4842311dfae1700db2a770a8d Mon Sep 17 00:00:00 2001 From: Elliot Stirling Date: Mon, 9 Sep 2019 15:59:13 +0000 Subject: [PATCH] Bug 1567549 - Added overloads to GeckoSession.loadUri() that accept additional HTTP request headers r=geckoview-reviewers,snorp,mayhemer Adds overloads to GeckoSession.loadUri() that accept additional HTTP request headers Differential Revision: https://phabricator.services.mozilla.com/D40951 --HG-- extra : moz-landing-system : lando --- mobile/android/geckoview/api.txt | 5 + .../geckoview/test/NavigationDelegateTest.kt | 58 ++++++++++ .../org/mozilla/geckoview/GeckoSession.java | 105 +++++++++++++++++- .../mozilla/geckoview/doc-files/CHANGELOG.md | 5 +- .../modules/geckoview/GeckoViewNavigation.jsm | 28 ++++- 5 files changed, 194 insertions(+), 7 deletions(-) diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt index 75fbb6dbc0a7..df2ad8fe9f5b 100644 --- a/mobile/android/geckoview/api.txt +++ b/mobile/android/geckoview/api.txt @@ -479,12 +479,17 @@ package org.mozilla.geckoview { method @AnyThread public void loadData(@NonNull byte[], @Nullable String); method @AnyThread public void loadString(@NonNull String, @Nullable String); method @AnyThread public void loadUri(@NonNull String); + method @AnyThread public void loadUri(@NonNull String, @Nullable Map); method @AnyThread public void loadUri(@NonNull String, int); method @AnyThread public void loadUri(@NonNull String, @Nullable String, int); + method @AnyThread public void loadUri(@NonNull String, @Nullable String, int, @Nullable Map); method @AnyThread public void loadUri(@NonNull String, @Nullable GeckoSession, int); + method @AnyThread public void loadUri(@NonNull String, @Nullable GeckoSession, int, @Nullable Map); method @AnyThread public void loadUri(@NonNull Uri); + method @AnyThread public void loadUri(@NonNull Uri, @Nullable Map); method @AnyThread public void loadUri(@NonNull Uri, int); method @AnyThread public void loadUri(@NonNull Uri, @Nullable Uri, int); + method @AnyThread public void loadUri(@NonNull Uri, @Nullable Uri, int, @Nullable Map); method @UiThread public void open(@NonNull GeckoRuntime); method @AnyThread public void readFromParcel(@NonNull Parcel); method @UiThread public void releaseDisplay(@NonNull GeckoDisplay); diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt index d1abe12ff2be..f753f4229dda 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt @@ -21,6 +21,7 @@ import org.mozilla.geckoview.test.util.Callbacks import android.support.test.filters.MediumTest import android.support.test.runner.AndroidJUnit4 +import org.hamcrest.MatcherAssert import org.hamcrest.Matchers.* import org.json.JSONObject import org.junit.After @@ -1207,6 +1208,63 @@ class NavigationDelegateTest : BaseSessionTest() { }) } + @Test fun loadUriHeader() { + val headers = mapOf("Header1" to "Value", "Header2" to "Value1, Value2") + + sessionRule.session.loadUri("$TEST_ENDPOINT/anything", headers) + sessionRule.session.waitForPageStop() + + val content = sessionRule.session.evaluateJS("document.body.children[0].innerHTML") as String + val body = JSONObject(content) + + MatcherAssert.assertThat("Headers should match", body.getJSONObject("headers") + .getString("Header1"), equalTo("Value")) + MatcherAssert.assertThat("Headers should match", body.getJSONObject("headers") + .getString("Header2"), equalTo("Value1, Value2")) + } + + @Test fun loadUriHeaderBadOverrides() { + val headers = mapOf( + null to "BadNull", + "Connection" to "BadConnection", + "Host" to "BadHost", + "ValueLess1" to "", + "ValueLess2" to null, + "ValueLess3" to " ", + "ValueLess4" to "\t") + + sessionRule.session.loadUri("$TEST_ENDPOINT/anything", headers) + sessionRule.session.waitForPageStop() + + val content = sessionRule.session.evaluateJS("document.body.children[0].innerHTML") as String + val body = JSONObject(content) + val headersJSON = body.getJSONObject("headers") + + headersJSON.keys().forEach { key -> + MatcherAssert.assertThat( "No value field should be empty or null", + headersJSON.optString(key), not(isEmptyOrNullString())) + MatcherAssert.assertThat( "No value field should be only whitespace", + headersJSON.getString(key).trim(), not(isEmptyOrNullString())) + MatcherAssert.assertThat( "BadNull should not exist as a header value", + headersJSON.getString(key), not("BadNull")) + } + + MatcherAssert.assertThat("Headers should not match", headersJSON + .getString("Connection"), not("BadConnection")) + MatcherAssert.assertThat("Headers should not match", headersJSON + .getString("Host"), not("BadHost")) + + // As per RFC7230 all request headers must have a field value (Except Host, which we filter) + // RFC7230 makes RFC2616 obsolete but 2616 allowed empty field values. + MatcherAssert.assertThat("Header with no field value should not be included", + !headersJSON.has("ValueLess1")) + MatcherAssert.assertThat("Header with no field value should not be included", + !headersJSON.has("ValueLess2")) + MatcherAssert.assertThat("Header with no field value should not be included", + !headersJSON.has("ValueLess3")) + MatcherAssert.assertThat("Header with no field value should not be included", + !headersJSON.has("ValueLess4")) + } @Test(expected = GeckoResult.UncaughtException::class) fun onNewSession_doesNotAllowOpened() { diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java index fe1f05447216..7c3725a9d593 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java @@ -10,10 +10,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.AbstractSequentialList; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; @@ -1586,13 +1588,45 @@ public class GeckoSession implements Parcelable { */ public static final int LOAD_FLAGS_REPLACE_HISTORY = 1 << 6; + /** + * Formats the map of additional request headers into an ArrayList + * @param additionalHeaders Request headers to be formatted + * @return Correctly formatted request headers as a ArrayList + */ + private ArrayList additionalHeadersToStringArray(final @NonNull Map additionalHeaders) { + ArrayList headers = new ArrayList(); + for (String key : additionalHeaders.keySet()) { + // skip null key if one exists + if (key == null) + continue; + + String value = additionalHeaders.get(key); + + // As per RFC7230 headers must contain a field value + if (value != null && !value.equals("")) { + headers.add( String.format("%s:%s", key, additionalHeaders.get(key)) ); + } + } + return headers; + } + /** * Load the given URI. * @param uri The URI of the resource to load. */ @AnyThread public void loadUri(final @NonNull String uri) { - loadUri(uri, (GeckoSession)null, LOAD_FLAGS_NONE); + loadUri(uri, (GeckoSession)null, LOAD_FLAGS_NONE, (Map) null); + } + + /** + * Load the given URI with specified HTTP request headers. + * @param uri The URI of the resource to load. + * @param additionalHeaders any additional request headers used with the load + */ + @AnyThread + public void loadUri(final @NonNull String uri, final @Nullable Map additionalHeaders) { + loadUri(uri, (GeckoSession)null, LOAD_FLAGS_NONE, additionalHeaders); } /** @@ -1603,7 +1637,7 @@ public class GeckoSession implements Parcelable { */ @AnyThread public void loadUri(final @NonNull String uri, final @LoadFlags int flags) { - loadUri(uri, (GeckoSession)null, flags); + loadUri(uri, (GeckoSession)null, flags, (Map) null); } /** @@ -1616,6 +1650,20 @@ public class GeckoSession implements Parcelable { @AnyThread public void loadUri(final @NonNull String uri, final @Nullable String referrer, final @LoadFlags int flags) { + loadUri(uri, referrer, flags, (Map) null); + } + + /** + * Load the given URI with the specified referrer, load type and HTTP request headers. + * + * @param uri the URI to load + * @param referrer the referrer, may be null + * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*} + * @param additionalHeaders any additional request headers used with the load + */ + @AnyThread + public void loadUri(final @NonNull String uri, final @Nullable String referrer, + final @LoadFlags int flags, final @Nullable Map additionalHeaders) { final GeckoBundle msg = new GeckoBundle(); msg.putString("uri", uri); msg.putInt("flags", flags); @@ -1623,6 +1671,10 @@ public class GeckoSession implements Parcelable { if (referrer != null) { msg.putString("referrerUri", referrer); } + + if (additionalHeaders != null) { + msg.putStringArray("headers", additionalHeadersToStringArray(additionalHeaders)); + } mEventDispatcher.dispatch("GeckoView:LoadUri", msg); } @@ -1638,6 +1690,22 @@ public class GeckoSession implements Parcelable { @AnyThread public void loadUri(final @NonNull String uri, final @Nullable GeckoSession referrer, final @LoadFlags int flags) { + loadUri(uri, referrer, flags, (Map) null); + } + + /** + * Load the given URI with the specified referrer, load type and HTTP request headers. This + * method will also do any applicable checks to ensure that the specified URI is both safe and + * allowable according to the referring GeckoSession. + * + * @param uri the URI to load + * @param referrer the referring GeckoSession, may be null + * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*} + * @param additionalHeaders any additional request headers used with the load + */ + @AnyThread + public void loadUri(final @NonNull String uri, final @Nullable GeckoSession referrer, + final @LoadFlags int flags, final @Nullable Map additionalHeaders) { final GeckoBundle msg = new GeckoBundle(); msg.putString("uri", uri); msg.putInt("flags", flags); @@ -1645,6 +1713,10 @@ public class GeckoSession implements Parcelable { if (referrer != null) { msg.putString("referrerSessionId", referrer.mId); } + + if (additionalHeaders != null) { + msg.putStringArray("headers", additionalHeadersToStringArray(additionalHeaders)); + } mEventDispatcher.dispatch("GeckoView:LoadUri", msg); } @@ -1654,7 +1726,17 @@ public class GeckoSession implements Parcelable { */ @AnyThread public void loadUri(final @NonNull Uri uri) { - loadUri(uri, null, LOAD_FLAGS_NONE); + loadUri(uri.toString(), (GeckoSession)null, LOAD_FLAGS_NONE, (Map) null); + } + + /** + * Load the given URI with specified HTTP request headers. + * @param uri The URI of the resource to load. + * @param additionalHeaders any additional request headers used with the load + */ + @AnyThread + public void loadUri(final @NonNull Uri uri, final @Nullable Map additionalHeaders) { + loadUri(uri.toString(), (GeckoSession)null, LOAD_FLAGS_NONE, additionalHeaders); } /** @@ -1664,7 +1746,7 @@ public class GeckoSession implements Parcelable { */ @AnyThread public void loadUri(final @NonNull Uri uri, final @LoadFlags int flags) { - loadUri(uri.toString(), (GeckoSession)null, flags); + loadUri(uri.toString(), (GeckoSession)null, flags, (Map) null); } /** @@ -1676,7 +1758,20 @@ public class GeckoSession implements Parcelable { @AnyThread public void loadUri(final @NonNull Uri uri, final @Nullable Uri referrer, final @LoadFlags int flags) { - loadUri(uri.toString(), referrer != null ? referrer.toString() : null, flags); + loadUri(uri.toString(), referrer != null ? referrer.toString() : null, flags, (Map) null); + } + + /** + * Load the given URI with the specified referrer, load type and HTTP request headers. + * @param uri the URI to load + * @param referrer the Uri to use as the referrer + * @param flags the load flags to use, an OR-ed value of {@link #LOAD_FLAGS_NONE LOAD_FLAGS_*} + * @param additionalHeaders any additional request headers used with the load + */ + @AnyThread + public void loadUri(final @NonNull Uri uri, final @Nullable Uri referrer, + final @LoadFlags int flags, final @Nullable Map additionalHeaders) { + loadUri(uri.toString(), referrer != null ? referrer.toString() : null, flags, additionalHeaders); } /** diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md index e3f846b54e59..109e715a33eb 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md @@ -20,12 +20,15 @@ exclude: true `onTelemetryReceived` has been renamed to [`onHistogram`][71.4], and [`Metric`][71.5] now takes a type parameter. ([bug 1576730]({{bugzilla}}1576730)) +- Added overloads of [`GeckoSession.loadUri()`][71.6] that accept a map of + additional HTTP request headers. [71.1]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onBooleanScalar-org.mozilla.geckoview.RuntimeTelemetry.Metric- [71.2]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onLongScalar-org.mozilla.geckoview.RuntimeTelemetry.Metric- [71.3]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onStringScalar-org.mozilla.geckoview.RuntimeTelemetry.Metric- [71.4]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onHistogram-org.mozilla.geckoview.RuntimeTelemetry.Metric- [71.5]: {{javadoc_uri}}/RuntimeTelemetry.Metric.html +[71.6]: {{javadoc_uri}}/GeckoSession.html#loadUri-java.lang.String-java.io.File-java.util.Map- ## v70 - Added API for session context assignment @@ -348,4 +351,4 @@ exclude: true [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String- [65.25]: {{javadoc_uri}}/GeckoResult.html -[api-version]: c35dda98b313f389d0bb297a1226c801035f7c77 +[api-version]: ccc454a846e5d3c22667842aecbbdef182524f94 diff --git a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm index e42771263dee..4a10a0505aec 100644 --- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm +++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm @@ -101,7 +101,7 @@ class GeckoViewNavigation extends GeckoViewModule { this.browser.gotoIndex(aData.index); break; case "GeckoView:LoadUri": - const { uri, referrerUri, referrerSessionId, flags } = aData; + const { uri, referrerUri, referrerSessionId, flags, headers } = aData; let navFlags = 0; @@ -183,6 +183,31 @@ class GeckoViewNavigation extends GeckoViewModule { ); } + let additionalHeaders = null; + if (headers) { + // Filter out request headers as per discussion in Bug #1567549 + // CONNECTION: Used by Gecko to manage connections + // HOST: Relates to how gecko will ultimately interpret the resulting resource as that + // determines the effective request URI + const badHeaders = ["connection", "host"]; + additionalHeaders = ""; + headers.forEach(entry => { + const key = entry + .split(/:/)[0] + .toLowerCase() + .trim(); + + if (!badHeaders.includes(key)) { + additionalHeaders += entry + "\r\n"; + } + }); + if (additionalHeaders != "") { + additionalHeaders = E10SUtils.makeInputStream(additionalHeaders); + } else { + additionalHeaders = null; + } + } + // For any navigation here, we should have an appropriate triggeringPrincipal: // // 1) If we have a referring session, triggeringPrincipal is the contentPrincipal from the @@ -203,6 +228,7 @@ class GeckoViewNavigation extends GeckoViewModule { flags: navFlags, referrerInfo, triggeringPrincipal, + headers: additionalHeaders, csp, }); break;