Bug 1405081 - Add WebRequest, WebResponse, and GeckoWebExecutor to GeckoView r=jchen,esawin

This exposes Gecko networking to GeckoView apps. It includes speculative connections, name resolution, and a Fetch-like HTTP API.

Differential Revision: https://phabricator.services.mozilla.com/D7799

squash to executor
This commit is contained in:
James Willcox 2018-10-18 08:28:09 -05:00
parent 3b0bbc9bfa
commit d64eea208c
12 changed files with 1392 additions and 0 deletions

View File

@ -0,0 +1,240 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.geckoview.test
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.support.test.filters.MediumTest
import android.support.test.filters.SdkSuppress
import android.support.test.runner.AndroidJUnit4
import java.net.URI
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.Charset
import java.util.concurrent.CountDownLatch
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
import org.json.JSONObject
import org.junit.*
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.WebRequest
import org.mozilla.geckoview.WebRequestError
import org.mozilla.geckoview.WebResponse
import org.mozilla.geckoview.test.util.Environment
import org.mozilla.geckoview.test.util.HttpBin
import org.mozilla.geckoview.test.util.RuntimeCreator
@MediumTest
@RunWith(AndroidJUnit4::class)
class WebExecutorTest {
companion object {
val TEST_ENDPOINT: String = "http://localhost:4242"
}
lateinit var executor: GeckoWebExecutor
lateinit var server: HttpBin
val env = Environment()
@get:Rule val thrown = ExpectedException.none()
@Before
fun setup() {
// Using @UiThreadTest here does not seem to block
// the tests which are not using @UiThreadTest, so we do that
// ourselves here as GeckoRuntime needs to be initialized
// on the UI thread.
val latch = CountDownLatch(1)
Handler(Looper.getMainLooper()).post {
executor = GeckoWebExecutor(RuntimeCreator.getRuntime())
server = HttpBin(URI.create(TEST_ENDPOINT))
server.start()
latch.countDown()
}
latch.await()
}
@After
fun cleanup() {
server.stop()
}
private fun fetch(request: WebRequest): WebResponse {
return fetch(request, GeckoWebExecutor.FETCH_FLAGS_NONE)
}
private fun fetch(request: WebRequest, @GeckoWebExecutor.FetchFlags flags: Int): WebResponse {
return executor.fetch(request, flags).poll(env.defaultTimeoutMillis)
}
fun String.toDirectByteBuffer(): ByteBuffer {
val chars = CharBuffer.wrap(this)
val buffer = ByteBuffer.allocateDirect(this.length)
Charset.forName("UTF-8").newEncoder().encode(chars, buffer, true)
return buffer
}
fun WebResponse.getJSONBody(): JSONObject {
return JSONObject(Charset.forName("UTF-8").decode(body).toString())
}
@Test
fun smoke() {
val uri = "$TEST_ENDPOINT/anything"
val bodyString = "This is the POST data"
val referrer = "http://foo/bar"
val request = WebRequest.Builder(uri)
.method("POST")
.header("Header1", "Clobbered")
.header("Header1", "Value")
.addHeader("Header2", "Value1")
.addHeader("Header2", "Value2")
.referrer(referrer)
.body(bodyString.toDirectByteBuffer())
.build()
val response = fetch(request)
assertThat("URI should match", response.uri, equalTo(uri))
assertThat("Status could should match", response.statusCode, equalTo(200))
assertThat("Content type should match", response.headers["Content-Type"], equalTo("application/json"))
assertThat("Redirected should match", response.redirected, equalTo(false))
val body = response.getJSONBody()
assertThat("Method should match", body.getString("method"), equalTo("POST"))
assertThat("Headers should match", body.getJSONObject("headers").getString("Header1"), equalTo("Value"))
assertThat("Headers should match", body.getJSONObject("headers").getString("Header2"), equalTo("Value1, Value2"))
assertThat("Referrer should match", body.getJSONObject("headers").getString("Referer"), equalTo(referrer))
assertThat("Data should match", body.getString("data"), equalTo(bodyString));
}
@Test
fun test404() {
val response = fetch(WebRequest("$TEST_ENDPOINT/status/404"))
assertThat("Status code should match", response.statusCode, equalTo(404))
}
@Test
fun testRedirect() {
val response = fetch(WebRequest("$TEST_ENDPOINT/redirect-to?url=/status/200"))
assertThat("URI should match", response.uri, equalTo(TEST_ENDPOINT +"/status/200"))
assertThat("Redirected should match", response.redirected, equalTo(true))
assertThat("Status code should match", response.statusCode, equalTo(200))
}
@Test
fun testRedirectLoop() {
thrown.expect(equalTo(WebRequestError(WebRequestError.ERROR_REDIRECT_LOOP, WebRequestError.ERROR_CATEGORY_NETWORK)))
fetch(WebRequest("$TEST_ENDPOINT/redirect/100"))
}
@Test
fun testAuth() {
// We don't support authentication yet, but want to make sure it doesn't do anything
// silly like try to prompt the user.
val response = fetch(WebRequest("$TEST_ENDPOINT/basic-auth/foo/bar"))
assertThat("Status code should match", response.statusCode, equalTo(401))
}
@Test
fun testSslError() {
val uri = if (env.isAutomation) {
"https://expired.example.com/"
} else {
"https://expired.badssl.com/"
}
thrown.expect(equalTo(WebRequestError(WebRequestError.ERROR_SECURITY_BAD_CERT, WebRequestError.ERROR_CATEGORY_SECURITY)))
fetch(WebRequest(uri))
}
@Test
fun testCookies() {
val uptimeMillis = SystemClock.uptimeMillis()
val response = fetch(WebRequest("$TEST_ENDPOINT/cookies/set/uptimeMillis/$uptimeMillis"))
// We get redirected to /cookies which returns the cookies that were sent in the request
assertThat("URI should match", response.uri, equalTo("$TEST_ENDPOINT/cookies"))
assertThat("Status code should match", response.statusCode, equalTo(200))
val body = response.getJSONBody()
assertThat("Body should match",
body.getJSONObject("cookies").getString("uptimeMillis"),
equalTo(uptimeMillis.toString()))
val anotherBody = fetch(WebRequest("$TEST_ENDPOINT/cookies")).getJSONBody()
assertThat("Body should match",
anotherBody.getJSONObject("cookies").getString("uptimeMillis"),
equalTo(uptimeMillis.toString()))
}
@Test
fun testAnonymous() {
// Ensure a cookie is set for the test server
testCookies();
val response = fetch(WebRequest("$TEST_ENDPOINT/cookies"),
GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS)
assertThat("Status code should match", response.statusCode, equalTo(200))
val cookies = response.getJSONBody().getJSONObject("cookies")
assertThat("Cookies should be empty", cookies.length(), equalTo(0))
}
@Test
fun testSpeculativeConnect() {
// We don't have a way to know if it succeeds or not, but at least we can ensure
// it doesn't explode.
executor.speculativeConnect("http://localhost")
// This is just a fence to ensure the above actually ran.
fetch(WebRequest("$TEST_ENDPOINT/cookies"))
}
@Test
fun testResolveV4() {
val addresses = executor.resolve("localhost").poll()
assertThat("Addresses should not be null",
addresses, notNullValue())
assertThat("First address should be loopback",
addresses.first().isLoopbackAddress, equalTo(true))
assertThat("First address size should be 4",
addresses.first().address.size, equalTo(4))
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
fun testResolveV6() {
val addresses = executor.resolve("ip6-localhost").poll()
assertThat("Addresses should not be null",
addresses, notNullValue())
assertThat("First address should be loopback",
addresses.first().isLoopbackAddress, equalTo(true))
assertThat("First address size should be 16",
addresses.first().address.size, equalTo(16))
}
@Test
fun testResolveError() {
thrown.expect(equalTo(WebRequestError(WebRequestError.ERROR_UNKNOWN_HOST, WebRequestError.ERROR_CATEGORY_URI)));
executor.resolve("this should not resolve").poll()
}
}

View File

@ -1,5 +1,6 @@
package org.mozilla.geckoview;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.util.ThreadUtils;
import android.os.Handler;
@ -180,6 +181,7 @@ public class GeckoResult<T> {
* Construct an incomplete GeckoResult. Call {@link #complete(Object)} or
* {@link #completeExceptionally(Throwable)} in order to fulfill the result.
*/
@WrapForJNI
public GeckoResult() {
if (ThreadUtils.isOnUiThread()) {
mHandler = ThreadUtils.getUiHandler();
@ -218,6 +220,7 @@ public class GeckoResult<T> {
* @param <U> Type for the result.
* @return The completed {@link GeckoResult}
*/
@WrapForJNI
public static @NonNull <U> GeckoResult<U> fromValue(@Nullable final U value) {
final GeckoResult<U> result = new GeckoResult<>();
result.complete(value);
@ -232,6 +235,7 @@ public class GeckoResult<T> {
* @param <T> Type for the result if the result had been completed without exception.
* @return The completed {@link GeckoResult}
*/
@WrapForJNI
public static @NonNull <T> GeckoResult<T> fromException(@NonNull final Throwable error) {
final GeckoResult<T> result = new GeckoResult<>();
result.completeExceptionally(error);
@ -501,6 +505,7 @@ public class GeckoResult<T> {
* @param value The value used to complete the result.
* @throws IllegalStateException If the result is already completed.
*/
@WrapForJNI
public synchronized void complete(final T value) {
if (mComplete) {
throw new IllegalStateException("result is already complete");
@ -520,6 +525,7 @@ public class GeckoResult<T> {
* @param exception The {@link Throwable} used to complete the result.
* @throws IllegalStateException If the result is already completed.
*/
@WrapForJNI
public synchronized void completeExceptionally(@NonNull final Throwable exception) {
if (mComplete) {
throw new IllegalStateException("result is already complete");

View File

@ -0,0 +1,159 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* 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.geckoview;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.List;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.annotation.WrapForJNI;
/**
* GeckoWebExecutor is responsible for fetching a {@link WebRequest} and delivering
* a {@link WebResponse} to the caller via {@link #fetch(WebRequest)}. Example:
* <pre>
* final GeckoWebExecutor executor = new GeckoWebExecutor();
*
* final GeckoResult&lt;WebResponse&gt; response = executor.fetch(
* new WebRequest.Builder("https://example.org/json")
* .header("Accept", "application/json")
* .build());
*
* response.then(response -&gt; {
* // Do something with response
* });
* </pre>
*/
public class GeckoWebExecutor {
// We don't use this right now because we access GeckoThread directly, but
// it's future-proofing for a world where we allow multiple GeckoRuntimes.
private final GeckoRuntime mRuntime;
@WrapForJNI(dispatchTo = "gecko", stubName = "Fetch")
private static native void nativeFetch(WebRequest request, int flags, GeckoResult<WebResponse> result);
@WrapForJNI(dispatchTo = "gecko", stubName = "Resolve")
private static native void nativeResolve(String host, GeckoResult<InetAddress[]> result);
@WrapForJNI(calledFrom = "gecko", exceptionMode = "nsresult")
private static ByteBuffer createByteBuffer(int capacity) {
return ByteBuffer.allocateDirect(capacity);
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({FETCH_FLAGS_NONE, FETCH_FLAGS_ANONYMOUS})
public @interface FetchFlags {};
/**
* No special treatment.
*/
public static final int FETCH_FLAGS_NONE = 0;
/**
* Don't send cookies or other user data along with the request.
*/
@WrapForJNI
public static final int FETCH_FLAGS_ANONYMOUS = 1;
/**
* Create a new GeckoWebExecutor instance.
*
* @param runtime A GeckoRuntime instance
*/
public GeckoWebExecutor(final @NonNull GeckoRuntime runtime) {
mRuntime = runtime;
}
/**
* Send the given {@link WebRequest}.
*
* @param request A {@link WebRequest} instance
* @return A GeckoResult which will be completed with a {@link WebResponse}
* @throws IllegalArgumentException if request is null or otherwise unusable.
*/
public @NonNull GeckoResult<WebResponse> fetch(final @NonNull WebRequest request) {
return fetch(request, FETCH_FLAGS_NONE);
}
/**
* Send the given {@link WebRequest} with specified flags.
*
* @param request A {@link WebRequest} instance
* @param flags The specified flags. One or more of {@link FetchFlags}.
* @return A GeckoResult which will be completed with a {@link WebResponse}
* @throws IllegalArgumentException if request is null or otherwise unusable.
*/
public @NonNull GeckoResult<WebResponse> fetch(final @NonNull WebRequest request,
final @FetchFlags int flags) {
if (request.body != null && !request.body.isDirect()) {
throw new IllegalArgumentException("Request body must be a direct ByteBuffer");
}
if (request.cacheMode < WebRequest.CACHE_MODE_FIRST ||
request.cacheMode > WebRequest.CACHE_MODE_LAST)
{
throw new IllegalArgumentException("Unknown cache mode");
}
// We don't need to fully validate the URI here, just a sanity check
if (!request.uri.toLowerCase().startsWith("http")) {
throw new IllegalArgumentException("URI scheme must be http or https");
}
final GeckoResult<WebResponse> result = new GeckoResult<>();
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
nativeFetch(request, flags, result);
} else {
GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, this,
"nativeFetch", WebRequest.class, request, flags,
GeckoResult.class, result);
}
return result;
}
/**
* Resolves the specified host name.
*
* @param host An Internet host name, e.g. mozilla.org.
* @return A {@link GeckoResult} which will be fulfilled with a {@link List}
* of {@link InetAddress}.
*/
public GeckoResult<InetAddress[]> resolve(final @NonNull String host) {
final GeckoResult<InetAddress[]> result = new GeckoResult<>();
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
nativeResolve(host, result);
} else {
GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, this,
"nativeResolve", String.class, host,
GeckoResult.class, result);
}
return result;
}
/**
* This causes a speculative connection to be made to the host
* in the specified URI. This is useful if an app thinks it may
* be making a request to that host in the near future. If no request
* is made, the connection will be cleaned up after an unspecified
* amount of time.
*
* @param uri A URI String.
*/
public void speculativeConnect(final @NonNull String uri) {
GeckoThread.speculativeConnect(uri);
}
}

View File

@ -0,0 +1,147 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* 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.geckoview;
import org.mozilla.gecko.annotation.WrapForJNI;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Map;
/**
* This is an abstract base class for HTTP request and response types.
*/
@WrapForJNI
public abstract class WebMessage {
/**
* The URI for the request or response.
*/
public final @NonNull String uri;
/**
* An unmodifiable Map of headers. Defaults to an empty instance.
*/
public final @NonNull Map<String, String> headers;
/**
* The body of the request or response. Must be a directly-allocated ByteBuffer.
* May be null.
*/
public final @Nullable ByteBuffer body;
/* package */ WebMessage(Builder builder) {
uri = builder.mUri;
headers = Collections.unmodifiableMap(builder.mHeaders);
if (builder.mBody != null) {
body = builder.mBody.asReadOnlyBuffer();
} else {
body = null;
}
}
// This is only used via JNI.
private String[] getHeaderKeys() {
String[] keys = new String[headers.size()];
headers.keySet().toArray(keys);
return keys;
}
// This is only used via JNI.
private String[] getHeaderValues() {
String[] values = new String[headers.size()];
headers.values().toArray(values);
return values;
}
/**
* This is a Builder used by subclasses of {@link WebMessage}.
*/
public static abstract class Builder {
/* package */ String mUri;
/* package */ Map<String, String> mHeaders = new ArrayMap<>();
/* package */ ByteBuffer mBody;
/**
* Construct a Builder instance with the specified URI.
*
* @param uri A URI String.
*/
/* package */ Builder(final @NonNull String uri) {
uri(uri);
}
/**
* Set the URI
*
* @param uri A URI String
* @return This Builder instance.
*/
public @NonNull Builder uri(final @NonNull String uri) {
mUri = uri;
return this;
}
/**
* Set a HTTP header. This may be called multiple times for additional headers. If an
* existing header of the same name exists, it will be replaced by this value.
*
* @param key The key for the HTTP header, e.g. "Content-Type".
* @param value The value for the HTTP header, e.g. "application/json".
* @return This Builder instance.
*/
public @NonNull Builder header(final @NonNull String key, final @NonNull String value) {
mHeaders.put(key, value);
return this;
}
/**
* Add a HTTP header. This may be called multiple times for additional headers. If an
* existing header of the same name exists, the values will be merged.
*
* @param key The key for the HTTP header, e.g. "Content-Type".
* @param value The value for the HTTP header, e.g. "application/json".
* @return This Builder instance.
*/
public @NonNull Builder addHeader(final @NonNull String key, final @NonNull String value) {
final String existingValue = mHeaders.get(key);
if (existingValue != null) {
final StringBuilder builder = new StringBuilder(existingValue);
builder.append(", ");
builder.append(value);
mHeaders.put(key, builder.toString());
} else {
mHeaders.put(key, value);
}
return this;
}
/**
* Set the body.
*
* @param buffer A {@link ByteBuffer} with the data.
* Must be allocated directly via {@link ByteBuffer#allocateDirect(int)}.
* @return This Builder instance.
*/
public @NonNull Builder body(final @Nullable ByteBuffer buffer) {
if (buffer != null && !buffer.isDirect()) {
throw new IllegalArgumentException("body must be directly allocated");
}
mBody = buffer;
return this;
}
}
}

View File

@ -0,0 +1,194 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* 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.geckoview;
import org.mozilla.gecko.annotation.WrapForJNI;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
/**
* WebRequest represents an HTTP[S] request. The typical pattern is to create instances of this
* class via {@link WebRequest.Builder}, and fetch responses via {@link GeckoWebExecutor#fetch(WebRequest)}.
*/
@WrapForJNI
public class WebRequest extends WebMessage {
/**
* The HTTP method for the request. Defaults to "GET".
*/
public final @NonNull String method;
/**
* The cache mode for the request. See {@link #CACHE_MODE_DEFAULT}.
* These modes match those from the DOM Fetch API.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/cache">DOM Fetch API cache modes</a>
*/
public final @CacheMode int cacheMode;
/**
* The value of the Referer header for this request.
*/
public final @Nullable String referrer;
@Retention(RetentionPolicy.SOURCE)
@IntDef({CACHE_MODE_DEFAULT, CACHE_MODE_NO_STORE,
CACHE_MODE_RELOAD, CACHE_MODE_NO_CACHE,
CACHE_MODE_FORCE_CACHE, CACHE_MODE_ONLY_IF_CACHED})
public @interface CacheMode {};
/**
* Default cache mode. Normal caching rules apply.
*/
public static final int CACHE_MODE_DEFAULT = 1;
/**
* The response will be fetched from the server without looking in
* the cache, and will not update the cache with the downloaded response.
*/
public static final int CACHE_MODE_NO_STORE = 2;
/**
* The response will be fetched from the server without looking in
* the cache. The cache will be updated with the downloaded response.
*/
public static final int CACHE_MODE_RELOAD = 3;
/**
* Forces a conditional request to the server if there is a cache match.
*/
public static final int CACHE_MODE_NO_CACHE = 4;
/**
* If a response is found in the cache, it will be returned, whether it's
* fresh or not. If there is no match, a normal request will be made
* and the cache will be updated with the downloaded response.
*/
public static final int CACHE_MODE_FORCE_CACHE = 5;
/**
* If a response is found in the cache, it will be returned, whether it's
* fresh or not. If there is no match from the cache, 504 Gateway Timeout
* will be returned.
*/
public static final int CACHE_MODE_ONLY_IF_CACHED = 6;
/* package */ static final int CACHE_MODE_FIRST = CACHE_MODE_DEFAULT;
/* package */ static final int CACHE_MODE_LAST = CACHE_MODE_ONLY_IF_CACHED;
/**
* Constructs a WebRequest with the specified URI.
* @param uri A URI String, e.g. https://mozilla.org
*/
public WebRequest(final @NonNull String uri) {
this(new Builder(uri));
}
/**
* Constructs a new WebRequest from a {@link WebRequest.Builder}.
*/
/* package */ WebRequest(@NonNull Builder builder) {
super(builder);
method = builder.mMethod;
cacheMode = builder.mCacheMode;
referrer = builder.mReferrer;
}
/**
* Builder offers a convenient way for constructing {@link WebRequest} instances.
*/
public static class Builder extends WebMessage.Builder {
/* package */ String mMethod = "GET";
/* package */ int mCacheMode = CACHE_MODE_DEFAULT;
/* package */ String mReferrer;
/**
* Construct a Builder instance with the specified URI.
*
* @param uri A URI String.
*/
public Builder(final @NonNull String uri) {
super(uri);
}
@Override
public @NonNull Builder uri(@NonNull String uri) {
super.uri(uri);
return this;
}
@Override
public @NonNull Builder header(final @NonNull String key, final @NonNull String value) {
super.header(key, value);
return this;
}
@Override
public @NonNull Builder addHeader(final @NonNull String key, final @NonNull String value) {
super.addHeader(key, value);
return this;
}
@Override
public @NonNull Builder body(@Nullable ByteBuffer buffer) {
super.body(buffer);
return this;
}
/**
* Set the HTTP method.
*
* @param method The HTTP method String.
* @return This Builder instance.
*/
public @NonNull Builder method(final @NonNull String method) {
mMethod = method;
return this;
}
/**
* Set the cache mode.
*
* @param mode One of {@link CacheMode}.
* @return This Builder instance.
*/
public @NonNull Builder cacheMode(final @CacheMode int mode) {
if (mode < CACHE_MODE_FIRST || mode > CACHE_MODE_LAST) {
throw new IllegalArgumentException("Unknown cache mode");
}
mCacheMode = mode;
return this;
}
/**
* Set the HTTP Referer header.
*
* @param referrer A URI String
* @return This Builder instance.
*/
public @NonNull Builder referrer(final @Nullable String referrer) {
mReferrer = referrer;
return this;
}
/**
* @return A {@link WebRequest} constructed with the values from this Builder instance.
*/
public @NonNull WebRequest build() {
if (mUri == null) {
throw new IllegalStateException("Must set URI");
}
return new WebRequest(this);
}
}
}

View File

@ -0,0 +1,108 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* 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.geckoview;
import org.mozilla.gecko.annotation.WrapForJNI;
import android.support.annotation.NonNull;
import java.nio.ByteBuffer;
/**
* WebResponse represents an HTTP[S] response. It is normally created
* by {@link GeckoWebExecutor#fetch(WebRequest)}.
*/
@WrapForJNI
public class WebResponse extends WebMessage {
/**
* The HTTP status code for the response, e.g. 200.
*/
public final int statusCode;
/**
* A boolean indicating whether or not this response is
* the result of a redirection.
*/
public final boolean redirected;
/* package */ WebResponse(Builder builder) {
super(builder);
this.statusCode = builder.mStatusCode;
this.redirected = builder.mRedirected;
}
/**
* Builder offers a convenient way to create WebResponse instances.
*/
@WrapForJNI
public static class Builder extends WebMessage.Builder {
/* package */ int mStatusCode;
/* package */ boolean mRedirected;
/**
* Constructs a new Builder instance with the specified URI.
*
* @param uri A URI String.
*/
public Builder(final @NonNull String uri) {
super(uri);
}
@Override
public @NonNull Builder uri(final @NonNull String uri) {
super.uri(uri);
return this;
}
@Override
public @NonNull Builder header(final @NonNull String key, final @NonNull String value) {
super.header(key, value);
return this;
}
@Override
public @NonNull Builder addHeader(final @NonNull String key, final @NonNull String value) {
super.addHeader(key, value);
return this;
}
@Override
public @NonNull Builder body(final @NonNull ByteBuffer buffer) {
super.body(buffer);
return this;
}
/**
* Set the HTTP status code, e.g. 200.
*
* @param code A int representing the HTTP status code.
* @return This Builder instance.
*/
public @NonNull Builder statusCode(int code) {
mStatusCode = code;
return this;
}
/**
* Set whether or not this response was the result of a redirect.
*
* @param redirected A boolean representing whether or not the request was redirected.
* @return This Builder instance.
*/
public @NonNull Builder redirected(final boolean redirected) {
mRedirected = redirected;
return this;
}
/**
* @return A {@link WebResponse} constructed with the values from this Builder instance.
*/
public @NonNull WebResponse build() {
return new WebResponse(this);
}
}
}

View File

@ -0,0 +1,501 @@
/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
* 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/. */
#include <algorithm>
#include "WebExecutorSupport.h"
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIHttpHeaderVisitor.h"
#include "nsIInputStream.h"
#include "nsIStreamLoader.h"
#include "nsINSSErrorsService.h"
#include "nsIUploadChannel2.h"
#include "nsIDNSService.h"
#include "nsIDNSListener.h"
#include "nsIDNSRecord.h"
#include "mozilla/net/DNS.h" // for NetAddr
#include "nsNetUtil.h" // for NS_NewURI, NS_NewChannel, NS_NewStreamLoader
#include "InetAddress.h" // for java::sdk::InetAddress
namespace mozilla {
using namespace net;
namespace widget {
static void CompleteWithError(java::GeckoResult::Param aResult, nsresult aStatus)
{
nsCOMPtr<nsINSSErrorsService> errSvc = do_GetService("@mozilla.org/nss_errors_service;1");
MOZ_ASSERT(errSvc);
uint32_t errorClass;
nsresult rv = errSvc->GetErrorClass(aStatus, &errorClass);
if (NS_FAILED(rv)) {
errorClass = 0;
}
java::WebRequestError::LocalRef error =
java::WebRequestError::FromGeckoError(int64_t(aStatus), NS_ERROR_GET_MODULE(aStatus), errorClass);
aResult->CompleteExceptionally(error.Cast<jni::Throwable>());
}
class ByteBufferStream final : public nsIInputStream
{
public:
NS_DECL_THREADSAFE_ISUPPORTS
ByteBufferStream(jni::ByteBuffer::Param buffer)
: mBuffer(buffer)
, mPosition(0)
, mClosed(false)
{
MOZ_ASSERT(mBuffer);
MOZ_ASSERT(mBuffer->Address());
}
NS_IMETHOD
Close() override
{
mClosed = true;
return NS_OK;
}
NS_IMETHOD
Available(uint64_t *aResult) override
{
if (mClosed) {
return NS_BASE_STREAM_CLOSED;
}
*aResult = (mBuffer->Capacity() - mPosition);
return NS_OK;
}
NS_IMETHOD
Read(char *aBuf, uint32_t aCount, uint32_t *aCountRead) override
{
if (mClosed) {
return NS_BASE_STREAM_CLOSED;
}
*aCountRead = uint32_t(std::min(uint64_t(mBuffer->Capacity() - mPosition),
uint64_t(aCount)));
if (*aCountRead > 0) {
memcpy(aBuf, (char*)mBuffer->Address(), *aCountRead);
mPosition += *aCountRead;
}
return NS_OK;
}
NS_IMETHOD
ReadSegments(nsWriteSegmentFun aWriter,
void *aClosure,
uint32_t aCount,
uint32_t *aResult) override
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD
IsNonBlocking(bool *aResult) override
{
*aResult = false;
return NS_OK;
}
protected:
virtual ~ByteBufferStream(){}
const jni::ByteBuffer::GlobalRef mBuffer;
uint64_t mPosition;
bool mClosed;
};
NS_IMPL_ISUPPORTS(ByteBufferStream, nsIInputStream)
class HeaderVisitor final : public nsIHttpHeaderVisitor
{
public:
NS_DECL_THREADSAFE_ISUPPORTS
HeaderVisitor(java::WebResponse::Builder::Param aBuilder)
: mBuilder(aBuilder)
{
}
NS_IMETHOD
VisitHeader(const nsACString& aHeader, const nsACString& aValue) override
{
mBuilder->Header(aHeader, aValue);
return NS_OK;
}
private:
virtual ~HeaderVisitor(){}
const java::WebResponse::Builder::GlobalRef mBuilder;
};
NS_IMPL_ISUPPORTS(HeaderVisitor, nsIHttpHeaderVisitor)
class LoaderListener final : public nsIStreamLoaderObserver
, public nsIRequestObserver
{
public:
NS_DECL_THREADSAFE_ISUPPORTS
LoaderListener(java::GeckoResult::Param aResult)
: mResult(aResult)
{
MOZ_ASSERT(mResult);
}
NS_IMETHOD
OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext,
nsresult aStatus, uint32_t aResultLength,
const uint8_t* aResult) override
{
nsresult rv = HandleWebResponse(aLoader, aStatus, aResultLength, aResult);
if (NS_FAILED(rv)) {
CompleteWithError(mResult, rv);
}
return NS_OK;
}
NS_IMETHOD
OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) override
{
return NS_OK;
}
NS_IMETHOD
OnStopRequest(nsIRequest* aRequest, nsISupports* aContext,
nsresult aStatusCode) override
{
return NS_OK;
}
private:
NS_IMETHOD
HandleWebResponse(nsIStreamLoader* aLoader,
nsresult aStatus, uint32_t aBodyLength,
const uint8_t* aBody)
{
NS_ENSURE_SUCCESS(aStatus, aStatus);
nsCOMPtr<nsIRequest> request;
nsresult rv = aLoader->GetRequest(getter_AddRefs(request));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(request, &rv);
NS_ENSURE_SUCCESS(rv, rv);
// URI
nsCOMPtr<nsIURI> uri;
rv = channel->GetURI(getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString spec;
rv = uri->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, rv);
java::WebResponse::Builder::LocalRef builder =
java::WebResponse::Builder::New(spec);
// Status code
uint32_t statusCode;
rv = channel->GetResponseStatus(&statusCode);
NS_ENSURE_SUCCESS(rv, rv);
builder->StatusCode(statusCode);
// Headers
RefPtr<HeaderVisitor> visitor = new HeaderVisitor(builder);
rv = channel->VisitResponseHeaders(visitor);
NS_ENSURE_SUCCESS(rv, rv);
// Redirected
nsCOMPtr<nsILoadInfo> loadInfo;
rv = channel->GetLoadInfo(getter_AddRefs(loadInfo));
NS_ENSURE_SUCCESS(rv, rv);
builder->Redirected(!loadInfo->RedirectChain().IsEmpty());
// Body
if (aBodyLength) {
jni::ByteBuffer::LocalRef buffer;
rv = java::GeckoWebExecutor::CreateByteBuffer(aBodyLength, &buffer);
NS_ENSURE_SUCCESS(rv, NS_ERROR_OUT_OF_MEMORY);
MOZ_ASSERT(buffer->Address());
MOZ_ASSERT(buffer->Capacity() == aBodyLength);
memcpy(buffer->Address(), aBody, aBodyLength);
builder->Body(buffer);
}
mResult->Complete(builder->Build());
return NS_OK;
}
virtual ~LoaderListener() {}
const java::GeckoResult::GlobalRef mResult;
};
NS_IMPL_ISUPPORTS(LoaderListener, nsIStreamLoaderObserver, nsIRequestObserver)
class DNSListener final : public nsIDNSListener
{
public:
NS_DECL_THREADSAFE_ISUPPORTS
DNSListener(const nsCString& aHost, java::GeckoResult::Param aResult)
: mHost(aHost), mResult(aResult)
{
}
NS_IMETHOD
OnLookupComplete(nsICancelable* aRequest, nsIDNSRecord* aRecord,
nsresult aStatus) override
{
if (NS_FAILED(aStatus)) {
CompleteWithError(mResult, aStatus);
return NS_OK;
}
nsresult rv = CompleteWithRecord(aRecord);
if (NS_FAILED(rv)) {
CompleteWithError(mResult, rv);
return NS_OK;
}
return NS_OK;
}
NS_IMETHOD
OnLookupByTypeComplete(nsICancelable* aRequest, nsIDNSByTypeRecord* aRecord,
nsresult aStatus) override
{
MOZ_ASSERT_UNREACHABLE("unxpected nsIDNSListener callback");
return NS_ERROR_UNEXPECTED;
}
private:
nsresult
CompleteWithRecord(nsIDNSRecord* aRecord)
{
nsTArray<NetAddr> addrs;
nsresult rv = aRecord->GetAddresses(addrs);
NS_ENSURE_SUCCESS(rv, rv);
jni::ByteArray::LocalRef bytes;
auto objects = jni::ObjectArray::New<java::sdk::InetAddress>(addrs.Length());
for (size_t i = 0; i < addrs.Length(); i++) {
const auto& addr = addrs[i];
if (addr.raw.family == AF_INET) {
bytes =
jni::ByteArray::New(reinterpret_cast<const int8_t*>(&addr.inet.ip), 4);
} else if (addr.raw.family == AF_INET6) {
bytes =
jni::ByteArray::New(reinterpret_cast<const int8_t*>(&addr.inet6.ip), 16);
} else {
// We don't handle this, skip it.
continue;
}
objects->SetElement(i, java::sdk::InetAddress::GetByAddress(mHost, bytes));
}
mResult->Complete(objects);
return NS_OK;
}
virtual ~DNSListener(){}
const nsCString mHost;
const java::GeckoResult::GlobalRef mResult;
};
NS_IMPL_ISUPPORTS(DNSListener, nsIDNSListener)
static nsresult
ConvertCacheMode(int32_t mode, int32_t& result)
{
switch (mode) {
case java::WebRequest::CACHE_MODE_DEFAULT:
result = nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT;
break;
case java::WebRequest::CACHE_MODE_NO_STORE:
result = nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE;
break;
case java::WebRequest::CACHE_MODE_RELOAD:
result = nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD;
break;
case java::WebRequest::CACHE_MODE_NO_CACHE:
result = nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE;
break;
case java::WebRequest::CACHE_MODE_FORCE_CACHE:
result = nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE;
break;
case java::WebRequest::CACHE_MODE_ONLY_IF_CACHED:
result = nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED;
break;
default:
return NS_ERROR_UNEXPECTED;
}
return NS_OK;
}
nsresult WebExecutorSupport::CreateStreamLoader(java::WebRequest::Param aRequest,
int32_t aFlags,
java::GeckoResult::Param aResult)
{
const auto req = java::WebRequest::LocalRef(aRequest);
const auto reqBase =
java::WebMessage::LocalRef(req.Cast<java::WebMessage>());
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), reqBase->Uri()->ToString());
NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
nsCOMPtr<nsIChannel> channel;
rv = NS_NewChannel(getter_AddRefs(channel),
uri,
nsContentUtils::GetSystemPrincipal(),
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
nsIContentPolicy::TYPE_OTHER);
NS_ENSURE_SUCCESS(rv, rv);
if (aFlags & java::GeckoWebExecutor::FETCH_FLAGS_ANONYMOUS) {
channel->SetLoadFlags(nsIRequest::LOAD_ANONYMOUS);
}
nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel, &rv));
NS_ENSURE_SUCCESS(rv, rv);
// Method
rv = httpChannel->SetRequestMethod(aRequest->Method()->ToCString());
NS_ENSURE_SUCCESS(rv, rv);
// Headers
const auto keys = reqBase->GetHeaderKeys();
const auto values = reqBase->GetHeaderValues();
for (size_t i = 0; i < keys->Length(); i++) {
const auto key = jni::String::LocalRef(keys->GetElement(i));
const auto value = jni::String::LocalRef(values->GetElement(i));
// We clobber any duplicate keys here because we've already merged them
// in the upstream WebRequest.
rv = httpChannel->SetRequestHeader(key->ToCString(), value->ToCString(),
false /* merge */);
NS_ENSURE_SUCCESS(rv, rv);
}
// Body
const auto body = reqBase->Body();
if (body) {
nsCOMPtr<nsIInputStream> stream = new ByteBufferStream(body);
nsCOMPtr<nsIUploadChannel2> uploadChannel(do_QueryInterface(channel, &rv));
NS_ENSURE_SUCCESS(rv, rv);
rv = uploadChannel->ExplicitSetUploadStream(stream, EmptyCString(), -1,
aRequest->Method()->ToCString(), false);
NS_ENSURE_SUCCESS(rv, rv);
}
// Referrer
RefPtr<nsIURI> referrerUri;
const auto referrer = req->Referrer();
if (referrer) {
rv = NS_NewURI(getter_AddRefs(referrerUri), referrer->ToString());
NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
}
rv = httpChannel->SetReferrer(referrerUri);
NS_ENSURE_SUCCESS(rv, rv);
// Cache mode
nsCOMPtr<nsIHttpChannelInternal> internalChannel(do_QueryInterface(channel, &rv));
NS_ENSURE_SUCCESS(rv, rv);
int32_t cacheMode;
rv = ConvertCacheMode(req->CacheMode(), cacheMode);
NS_ENSURE_SUCCESS(rv, rv);
rv = internalChannel->SetFetchCacheMode(cacheMode);
NS_ENSURE_SUCCESS(rv, rv);
// We don't have any UI
rv = internalChannel->SetBlockAuthPrompt(true);
NS_ENSURE_SUCCESS(rv, rv);
// All done, set up the stream loader
RefPtr<LoaderListener> listener = new LoaderListener(aResult);
nsCOMPtr<nsIStreamLoader> loader;
rv = NS_NewStreamLoader(getter_AddRefs(loader), listener);
NS_ENSURE_SUCCESS(rv, rv);
// Finally, open the channel
rv = httpChannel->AsyncOpen2(loader);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
void WebExecutorSupport::Fetch(jni::Object::Param aRequest, int32_t aFlags,
jni::Object::Param aResult)
{
const auto request = java::WebRequest::LocalRef(aRequest);
auto result = java::GeckoResult::LocalRef(aResult);
nsresult rv = CreateStreamLoader(request, aFlags, result);
if (NS_FAILED(rv)) {
CompleteWithError(result, rv);
}
}
static nsresult ResolveHost(nsCString& host, java::GeckoResult::Param result)
{
nsresult rv;
nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsICancelable> cancelable;
RefPtr<DNSListener> listener = new DNSListener(host, result);
rv = dns->AsyncResolveNative(host, 0, listener,
nullptr /* aListenerTarget */,
OriginAttributes(),
getter_AddRefs(cancelable));
return rv;
}
void WebExecutorSupport::Resolve(jni::String::Param aUri, jni::Object::Param aResult)
{
auto result = java::GeckoResult::LocalRef(aResult);
nsCString uri = aUri->ToCString();
nsresult rv = ResolveHost(uri, result);
if (NS_FAILED(rv)) {
CompleteWithError(result, rv);
}
}
} // widget
} // mozilla

View File

@ -0,0 +1,30 @@
/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
* 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/. */
#ifndef WebExecutorSupport_h__
#define WebExecutorSupport_h__
#include "GeneratedJNINatives.h"
namespace mozilla {
namespace widget {
class WebExecutorSupport final
: public java::GeckoWebExecutor::Natives<WebExecutorSupport>
{
public:
static void Fetch(jni::Object::Param request, int32_t flags, jni::Object::Param result);
static void Resolve(jni::String::Param aUri, jni::Object::Param result);
protected:
static nsresult CreateStreamLoader(java::WebRequest::Param aRequest,
int32_t aFlags,
java::GeckoResult::Param aResult);
};
} // namespace widget
} // namespace mozilla
#endif // WebExecutorSupport_h__

View File

@ -0,0 +1,3 @@
# We only want getByAddress(String, byte[])
[java.net.InetAddress = skip:true]
getByAddress(Ljava/lang/String;[B)Ljava/net/InetAddress; = skip:false

View File

@ -14,6 +14,7 @@ generated = [
'AndroidBuild',
'AndroidInputType',
'AndroidRect',
'InetAddress',
'JavaBuiltins',
'KeyEvent',
'MediaCodec',

View File

@ -61,6 +61,7 @@ UNIFIED_SOURCES += [
'nsWidgetFactory.cpp',
'nsWindow.cpp',
'ScreenHelperAndroid.cpp',
'WebExecutorSupport.cpp',
]
include('/ipc/chromium/chromium-config.mozbuild')

View File

@ -71,6 +71,7 @@
#include "fennec/Telemetry.h"
#include "fennec/ThumbnailHelper.h"
#include "ScreenHelperAndroid.h"
#include "WebExecutorSupport.h"
#ifdef DEBUG_ANDROID_EVENTS
#define EVLOG(args...) ALOG(args)
@ -433,6 +434,7 @@ nsAppShell::nsAppShell()
mozilla::GeckoScreenOrientation::Init();
mozilla::GeckoSystemStateListener::Init();
mozilla::PrefsHelper::Init();
mozilla::widget::WebExecutorSupport::Init();
nsWindow::InitNatives();
if (jni::IsFennec()) {