mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
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:
parent
3b0bbc9bfa
commit
d64eea208c
@ -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()
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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<WebResponse> response = executor.fetch(
|
||||
* new WebRequest.Builder("https://example.org/json")
|
||||
* .header("Accept", "application/json")
|
||||
* .build());
|
||||
*
|
||||
* response.then(response -> {
|
||||
* // 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
501
widget/android/WebExecutorSupport.cpp
Normal file
501
widget/android/WebExecutorSupport.cpp
Normal 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
|
30
widget/android/WebExecutorSupport.h
Normal file
30
widget/android/WebExecutorSupport.h
Normal 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__
|
3
widget/android/bindings/InetAddress-classes.txt
Normal file
3
widget/android/bindings/InetAddress-classes.txt
Normal 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
|
@ -14,6 +14,7 @@ generated = [
|
||||
'AndroidBuild',
|
||||
'AndroidInputType',
|
||||
'AndroidRect',
|
||||
'InetAddress',
|
||||
'JavaBuiltins',
|
||||
'KeyEvent',
|
||||
'MediaCodec',
|
||||
|
@ -61,6 +61,7 @@ UNIFIED_SOURCES += [
|
||||
'nsWidgetFactory.cpp',
|
||||
'nsWindow.cpp',
|
||||
'ScreenHelperAndroid.cpp',
|
||||
'WebExecutorSupport.cpp',
|
||||
]
|
||||
|
||||
include('/ipc/chromium/chromium-config.mozbuild')
|
||||
|
@ -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()) {
|
||||
|
Loading…
Reference in New Issue
Block a user