Bug 1947801: Add OHTTP client test. r=valentin,geckoview-reviewers,android-reviewers,ohall

Differential Revision: https://phabricator.services.mozilla.com/D239907
This commit is contained in:
Fatih Kilic
2025-03-31 22:01:49 +00:00
parent 501b14b0ff
commit b735a19de7
14 changed files with 537 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
/* -*- 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 androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.* // ktlint-disable no-wildcard-imports
import org.mozilla.geckoview.test.util.RuntimeCreator
@RunWith(AndroidJUnit4::class)
@MediumTest
class WebExecutorOhttpTest : BaseSessionTest() {
// We just want to make sure we don't crash when trying to use ohttp.
// We test the actual functionality in toolkit/components/resistfingerprinting/tests/xpcshell/test_ohttp_client.js.
@Test(expected = WebRequestError::class)
fun testOhttp() {
sessionRule.setPrefsUntilTestEnd(
mapOf(
// Don't make external requests.
"network.ohttp.configURL" to "https://example.com",
"network.ohttp.relayURL" to "https://example.com",
),
)
GeckoWebExecutor(RuntimeCreator.getRuntime()).fetch(WebRequest.Builder("https://example.com").build(), GeckoWebExecutor.FETCH_FLAGS_OHTTP).poll(5 * 100)
}
}

View File

@@ -119,6 +119,7 @@ public class WebRequest extends WebMessage {
}
/** Builder offers a convenient way for constructing {@link WebRequest} instances. */
@WrapForJNI
@AnyThread
public static class Builder extends WebMessage.Builder {
/* package */ String mMethod = "GET";

View File

@@ -6,4 +6,6 @@ BROWSER_CHROME_MANIFESTS += [
"browser/browser.toml",
]
XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.toml"]
TEST_DIRS += ["gtest"]

View File

@@ -0,0 +1,292 @@
/* 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/. */
"use strict";
const { AddonTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/AddonTestUtils.sys.mjs"
);
const API_OHTTP_CONFIG = "http://example.com/ohttp-config";
const API_OHTTP_RELAY = "http://example.com/relay/";
AddonTestUtils.maybeInit(this);
const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
const ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
Ci.nsIObliviousHttp
);
const ohttpServer = ohttp.server();
const serverContext = {
statusCode: 0,
configBody: ohttpServer.encodedConfig,
failure: false,
};
server.registerPathHandler(
new URL(API_OHTTP_CONFIG).pathname,
(request, response) => {
const bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
Ci.nsIBinaryOutputStream
);
bstream.setOutputStream(response.bodyOutputStream);
bstream.writeByteArray(serverContext.configBody);
}
);
let requestPromise, resolveRequest;
let responsePromise, resolveResponse;
server.registerPathHandler(
new URL(API_OHTTP_RELAY).pathname,
async (request, response) => {
const inputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
inputStream.setInputStream(request.bodyInputStream);
const requestBody = inputStream.readByteArray(inputStream.available());
const ohttpRequest = ohttpServer.decapsulate(requestBody);
const bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
Ci.nsIBinaryHttp
);
const decodedRequest = bhttp.decodeRequest(ohttpRequest.request);
response.processAsync();
if (serverContext.failure) {
response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
response.finish();
resolveRequest(decodedRequest);
return;
}
const BinaryHttpResponse = {
status: serverContext.statusCode,
headerNames: [],
headerValues: [],
content: new TextEncoder().encode(decodedRequest.content),
QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
};
const encResponse = ohttpRequest.encapsulate(
bhttp.encodeResponse(BinaryHttpResponse)
);
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "message/ohttp-res", false);
const bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
Ci.nsIBinaryOutputStream
);
bstream.setOutputStream(response.bodyOutputStream);
bstream.writeByteArray(encResponse);
response.finish();
resolveRequest(decodedRequest);
}
);
function resetPromises() {
const tmp1 = Promise.withResolvers();
requestPromise = tmp1.promise;
resolveRequest = tmp1.resolve;
const tmp2 = Promise.withResolvers();
responsePromise = tmp2.promise;
resolveResponse = tmp2.resolve;
}
async function test_success() {
resetPromises();
const request = {
method: "POST",
scheme: "https",
authority: "example.com",
path: "/my-path",
headerNames: ["User-Agent"],
headerValues: ["Mozilla/5.0"],
content: "Hello, world!",
};
const expectedResponse = {
url: `${request.scheme}://${request.authority}${request.path}`,
statusCode: 42,
error: "",
};
serverContext.statusCode = expectedResponse.statusCode;
const ohttpClientTester = Cc[
"@mozilla.org/ohttp-client-test;1"
].createInstance(Ci.nsIOhttpClientTest);
ohttpClientTester.fetch(
`${request.scheme}://${request.authority}${request.path}`,
request.method,
request.content,
request.headerNames,
request.headerValues,
(url, statusCode, headerKeys, headerValues, error) => {
resolveResponse({
url,
statusCode,
headerKeys,
headerValues,
error,
});
}
);
const [ohttpRequest, ohttpResponse] = await Promise.all([
requestPromise,
responsePromise,
]);
// Verify request
Assert.equal(ohttpRequest.method, request.method);
Assert.equal(ohttpRequest.scheme, request.scheme);
Assert.equal(ohttpRequest.authority, request.authority);
Assert.equal(ohttpRequest.path, request.path);
Assert.deepEqual(ohttpRequest.headerNames, request.headerNames);
Assert.deepEqual(ohttpRequest.headerValues, request.headerValues);
Assert.deepEqual(
ohttpRequest.content,
request.content.split("").map(s => s.charCodeAt(0))
);
// Verify response
Assert.equal(ohttpResponse.url, expectedResponse.url);
Assert.equal(ohttpResponse.statusCode, expectedResponse.statusCode);
Assert.equal(ohttpResponse.error, expectedResponse.error);
}
async function test_invalid_config() {
resetPromises();
const request = {
method: "POST",
scheme: "https",
authority: "example.com",
path: "/my-path",
headerNames: ["User-Agent"],
headerValues: ["Mozilla/5.0"],
content: "Hello, world!",
};
const expectedResponse = {
url: "",
statusCode: 0,
error: "Request failed, error=0x11, category=0x1",
};
// Provide invalid config
// We still set status code to verify that we don't
// get back 42 as the status.
serverContext.configBody = [0, 0, 0, 0];
serverContext.statusCode = 42;
const ohttpClientTester = Cc[
"@mozilla.org/ohttp-client-test;1"
].createInstance(Ci.nsIOhttpClientTest);
ohttpClientTester.fetch(
`${request.scheme}://${request.authority}${request.path}`,
request.method,
request.content,
request.headerNames,
request.headerValues,
(url, statusCode, headerKeys, headerValues, error) => {
resolveResponse({
url,
statusCode,
headerKeys,
headerValues,
error,
});
}
);
// Request promise never resolves as getting config fails
const ohttpResponse = await responsePromise;
// Verify response
Assert.equal(ohttpResponse.url, expectedResponse.url);
Assert.equal(ohttpResponse.statusCode, expectedResponse.statusCode);
Assert.equal(ohttpResponse.error, expectedResponse.error);
}
async function test_ohttp_failure() {
resetPromises();
const request = {
method: "POST",
scheme: "https",
authority: "example.com",
path: "/my-path",
headerNames: ["User-Agent"],
headerValues: ["Mozilla/5.0"],
content: "Hello, world!",
};
const expectedResponse = {
url: "",
statusCode: 0,
error: "Request failed, error=0x11, category=0x1",
};
// Provide a valid config, but make the server fail.
// We still set status code to verify that we don't
// get back 42 as the status.
serverContext.configBody = ohttpServer.encodedConfig;
serverContext.statusCode = 42;
serverContext.failure = true;
const ohttpClientTester = Cc[
"@mozilla.org/ohttp-client-test;1"
].createInstance(Ci.nsIOhttpClientTest);
ohttpClientTester.fetch(
`${request.scheme}://${request.authority}${request.path}`,
request.method,
request.content,
request.headerNames,
request.headerValues,
(url, statusCode, headerKeys, headerValues, error) => {
resolveResponse({
url,
statusCode,
headerKeys,
headerValues,
error,
});
}
);
const [ohttpRequest, ohttpResponse] = await Promise.all([
requestPromise,
responsePromise,
]);
// Verify request
Assert.equal(ohttpRequest.method, request.method);
Assert.equal(ohttpRequest.scheme, request.scheme);
Assert.equal(ohttpRequest.authority, request.authority);
Assert.equal(ohttpRequest.path, request.path);
Assert.deepEqual(ohttpRequest.headerNames, request.headerNames);
Assert.deepEqual(ohttpRequest.headerValues, request.headerValues);
Assert.deepEqual(
ohttpRequest.content,
request.content.split("").map(s => s.charCodeAt(0))
);
// Verify response
Assert.equal(ohttpResponse.url, expectedResponse.url);
Assert.equal(ohttpResponse.statusCode, expectedResponse.statusCode);
Assert.equal(ohttpResponse.error, expectedResponse.error);
}
add_task(async function run_tests() {
await test_success();
await test_invalid_config();
await test_ohttp_failure();
});

View File

@@ -0,0 +1,8 @@
[DEFAULT]
prefs = [
"network.ohttp.configURL='http://example.com/ohttp-config'",
"network.ohttp.relayURL='http://example.com/relay/'"
]
["test_ohttp_client.js"]
run-if = ["os == 'android'"]

View File

@@ -10,6 +10,7 @@
#include "ReferrerInfo.h"
#include "WebExecutorSupport.h"
#include "OhttpHelper.h"
#include "JavaExceptions.h"
#include "nsIAsyncVerifyRedirectCallback.h"
#include "nsICancelable.h"
@@ -277,6 +278,67 @@ nsresult WebExecutorSupport::PerformOrQueueOhttpRequest(
return OhttpHelper::FetchConfigAndFulfillRequests();
}
#if defined(ENABLE_TESTS)
void WebExecutorSupport::TestOhttp(const nsACString& url,
const nsACString& method,
const nsACString& body,
const nsTArray<nsCString>& headerKeys,
const nsTArray<nsCString>& headerValues,
ohttpClientTestCallback* callback) {
auto result = java::GeckoResult::New();
nsCOMPtr<ohttpClientTestCallback> callbackRef(callback);
auto resolve = jni::GeckoResultCallback::CreateAndAttach(
[callbackRef](jni::Object::Param aResolveVal) {
auto response = java::WebResponse::LocalRef(aResolveVal);
auto responseBase =
java::WebMessage::LocalRef(response.Cast<java::WebMessage>());
auto headerKeysJava = responseBase->GetHeaderKeys();
auto headerValuesJava = responseBase->GetHeaderValues();
nsTArray<nsCString> headersKeys(headerKeysJava->Length());
nsTArray<nsCString> headersValues(headerValuesJava->Length());
for (size_t i = 0; i < headerKeysJava->Length(); i++) {
headersKeys.AppendElement(
jni::String::LocalRef(headerKeysJava->GetElement(i))
->ToCString());
headersValues.AppendElement(
jni::String::LocalRef(headerValuesJava->GetElement(i))
->ToCString());
}
callbackRef->OnResponse(responseBase->Uri()->ToCString(),
response->StatusCode(), headersKeys,
headersValues, ""_ns);
});
auto reject = jni::GeckoResultCallback::CreateAndAttach(
[callbackRef](jni::Object::Param aRejectVal) {
auto error = java::sdk::Throwable::LocalRef(aRejectVal);
callbackRef->OnResponse(""_ns, 0, nsTArray<nsCString>(),
nsTArray<nsCString>(),
error->GetMessage()->ToCString());
});
result->NativeThen(resolve, reject);
const auto requestBuilder =
java::WebRequest::Builder::New(url)->Method(method)->Body(body);
for (size_t i = 0; i < headerKeys.Length(); i++) {
requestBuilder->AddHeader(headerKeys[i], headerValues[i]);
}
const auto request = requestBuilder->Build();
nsresult rv = PerformOrQueueOhttpRequest(
request, java::GeckoWebExecutor::FETCH_FLAGS_OHTTP, result, true);
if (NS_FAILED(rv)) {
CompleteWithError(result, rv);
}
}
#endif
static nsresult ConvertCacheMode(int32_t mode, int32_t& result) {
switch (mode) {
case java::WebRequest::CACHE_MODE_DEFAULT:

View File

@@ -10,6 +10,10 @@
#include "mozilla/java/GeckoResultWrappers.h"
#include "mozilla/java/WebRequestWrappers.h"
#if defined(ENABLE_TESTS)
# include "nsIOhttpClientTest.h"
#endif // defined(ENABLE_TESTS)
class nsIChannel;
namespace mozilla {
@@ -40,6 +44,15 @@ class WebExecutorSupport final
int32_t aFlags,
java::GeckoResult::Param aResult,
bool bypassConfigCache = false);
#if defined(ENABLE_TESTS)
// Used for testing OHTTP. Porting all of the OHTTP server code to Java would
// be quite a bit of work, so we're just going to test it in JS.
static void TestOhttp(const nsACString& url, const nsACString& method,
const nsACString& body,
const nsTArray<nsCString>& headerKeys,
const nsTArray<nsCString>& headerValues,
ohttpClientTestCallback* callback);
#endif // defined(ENABLE_TESTS)
};
} // namespace widget

View File

@@ -41,6 +41,14 @@ class GeckoResultCallback final
return java;
}
static java::GeckoResult::GeckoCallback::LocalRef CreateAndAttach(
OuterCallback&& aCallback) {
auto java = java::GeckoResult::GeckoCallback::New();
auto native = MakeUnique<GeckoResultCallback>(std::move(aCallback));
Base::AttachNative(java, std::move(native));
return java;
}
explicit GeckoResultCallback(OuterCallback&& aCallback)
: mCallback(std::move(aCallback)) {}

View File

@@ -15,6 +15,11 @@ DIRS += [
"jni",
]
if CONFIG["ENABLE_TESTS"]:
DIRS += [
"tests",
]
EXPORTS += [
"AndroidBridge.h",
]
@@ -110,6 +115,7 @@ EXPORTS.mozilla.widget += [
"GeckoViewSupport.h",
"InProcessAndroidCompositorWidget.h",
"nsWindow.h",
"WebExecutorSupport.h",
"WindowEvent.h",
]

View File

@@ -0,0 +1,28 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#if defined(ENABLE_TESTS)
# include "mozilla/widget/WebExecutorSupport.h"
# include "mozilla/widget/OhttpClientTest.h"
namespace mozilla::widget {
NS_IMETHODIMP
OhttpClientTest::Fetch(const nsACString& url, const nsACString& method,
const nsACString& body,
const nsTArray<nsCString>& headerKeys,
const nsTArray<nsCString>& headerValues,
ohttpClientTestCallback* callback) {
widget::WebExecutorSupport::TestOhttp(url, method, body, headerKeys,
headerValues, callback);
return NS_OK;
}
NS_IMPL_ISUPPORTS(OhttpClientTest, nsIOhttpClientTest)
} // namespace mozilla::widget
#endif // defined(ENABLE_TESTS)

View File

@@ -0,0 +1,29 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 mozilla_widget_android_tests
#define mozilla_widget_android_tests
#if defined(ENABLE_TESTS)
# include "nsIOhttpClientTest.h"
namespace mozilla::widget {
class OhttpClientTest final : public nsIOhttpClientTest {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIOHTTPCLIENTTEST
OhttpClientTest() = default;
private:
~OhttpClientTest() = default;
};
} // namespace mozilla::widget
#endif // defined(ENABLE_TESTS)
#endif // mozilla_widget_android_tests

View File

@@ -0,0 +1,15 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
Classes = [
{
'cid': '{b3150bd2-689c-4cf0-b4ee-4f90aaf1cbd3}',
'contract_ids': ['@mozilla.org/ohttp-client-test;1'],
'type': 'mozilla::widget::OhttpClientTest',
'headers': ['mozilla/widget/OhttpClientTest.h'],
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
]

View File

@@ -0,0 +1,21 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
EXPORTS.mozilla.widget += ["OhttpClientTest.h"]
UNIFIED_SOURCES += ["OhttpClientTest.cpp"]
XPCOM_MANIFESTS += ["components.conf"]
XPIDL_MODULE = "widget_android_tests"
XPIDL_SOURCES += [
"nsIOhttpClientTest.idl",
]
include("/ipc/chromium/chromium-config.mozbuild")
FINAL_LIBRARY = "xul"

View File

@@ -0,0 +1,21 @@
/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
/* 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 "nsISupports.idl"
[function, scriptable, uuid(cdec8db3-3868-41e7-a91a-68a3b5a24de0)]
interface ohttpClientTestCallback : nsISupports
{
void onResponse(in ACString url, in long statusCode, in Array<ACString> headerKeys, in Array<ACString> headerValues, in ACString errorMessage);
};
[scriptable, uuid(b3150bd2-689c-4cf0-b4ee-4f90aaf1cbd3)]
interface nsIOhttpClientTest : nsISupports
{
void fetch(
in ACString url, in ACString method, in ACString body, in Array<ACString> headerKeys, in Array<ACString> headerValues,
in ohttpClientTestCallback callback
);
};