tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 63b0f9430fae51732f8c0e10ba4d43cde098c9a1
parent 92fb4d19cea2c9bf22939e86a9a0d5879bd7ca1e
Author: Bastian Gruber <foreach@me.com>
Date:   Tue,  2 Dec 2025 14:33:43 +0000

Bug 1983135 - Implement a Necko backend for viaduct. r=bdk,valentin,supply-chain-reviewers

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

Diffstat:
MCargo.lock | 12++++++++++++
MCargo.toml | 1+
Aservices/application-services/components/viaduct-necko/Cargo.toml | 15+++++++++++++++
Aservices/application-services/components/viaduct-necko/backend.cpp | 670+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aservices/application-services/components/viaduct-necko/backend.h | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aservices/application-services/components/viaduct-necko/moz.build | 16++++++++++++++++
Aservices/application-services/components/viaduct-necko/src/lib.rs | 351+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aservices/application-services/components/viaduct-necko/tests/test_viaduct_necko_backend.js | 318+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aservices/application-services/components/viaduct-necko/tests/xpcshell.toml | 5+++++
Mservices/moz.build | 1+
Msupply-chain/audits.toml | 5+++++
Msupply-chain/imports.lock | 1+
Mtoolkit/library/rust/shared/Cargo.toml | 1+
Mtoolkit/library/rust/shared/lib.rs | 6++++++
14 files changed, 1474 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2659,6 +2659,7 @@ dependencies = [ "urlpattern", "urlpattern_glue", "viaduct", + "viaduct-necko", "webext-storage", "webrender_bindings", "wgpu_bindings", @@ -7675,6 +7676,17 @@ dependencies = [ ] [[package]] +name = "viaduct-necko" +version = "0.1.0" +dependencies = [ + "async-trait", + "error-support", + "futures-channel", + "url", + "viaduct", +] + +[[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "toolkit/crashreporter/mozwer-rust", "toolkit/library/gtest/rust", "toolkit/library/rust/", + "services/application-services/components/viaduct-necko", ] # Excluded crates may be built as dependencies, but won't be considered members diff --git a/services/application-services/components/viaduct-necko/Cargo.toml b/services/application-services/components/viaduct-necko/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "viaduct-necko" +version = "0.1.0" +edition = "2021" +rust-version.workspace = true + +[dependencies] +async-trait = "0.1" +error-support = "0.1" +futures-channel = "0.3" +url = "2" +viaduct = "0.1" + +[lib] +path = "src/lib.rs" diff --git a/services/application-services/components/viaduct-necko/backend.cpp b/services/application-services/components/viaduct-necko/backend.cpp @@ -0,0 +1,670 @@ +/* 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 "backend.h" + +#include "mozilla/Logging.h" +#include "mozilla/Span.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIInputStream.h" +#include "nsIStreamListener.h" +#include "nsITimer.h" +#include "nsIUploadChannel2.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsStringStream.h" +#include "nsThreadUtils.h" +#include "nsTArray.h" + +#include <cinttypes> +#include <utility> + +using namespace mozilla; + +// Logger for viaduct-necko backend +static LazyLogModule gViaductLogger("viaduct"); + +/** + * Manages viaduct Request/Result pointers + * + * This class ensures that we properly manage the `ViaductRequest` and + * `ViaductResult` pointers, avoiding use-after-free bugs. It ensures that + * either `viaduct_necko_result_complete` or + * `viaduct_necko_result_complete_error` will be called exactly once and the + * pointers won't be used after that. + * + * This class is designed to be created outside of NS_DispatchToMainThread and + * moved into the closure. This way, even if the closure never runs, the + * destructor will still be called and we'll complete with an error. + */ +class ViaductRequestGuard { + private: + const ViaductRequest* mRequest; + ViaductResult* mResult; + + public: + // Constructor + ViaductRequestGuard(const ViaductRequest* aRequest, ViaductResult* aResult) + : mRequest(aRequest), mResult(aResult) { + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("ViaductRequestGuard: Created with request=%p, result=%p", + mRequest, mResult)); + } + + // Move Constructor + // Transfers ownership of the pointers from other to this. + ViaductRequestGuard(ViaductRequestGuard&& other) noexcept + : mRequest(std::exchange(other.mRequest, nullptr)), + mResult(std::exchange(other.mResult, nullptr)) { + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("ViaductRequestGuard: Move constructed, request=%p, result=%p", + mRequest, mResult)); + } + + // Move assignment operator + ViaductRequestGuard& operator=(ViaductRequestGuard&& other) noexcept { + if (this != &other) { + // If we already own pointers, complete with error before replacing + if (mResult) { + MOZ_LOG(gViaductLogger, LogLevel::Warning, + ("ViaductRequestGuard: Move assignment replacing existing " + "pointers, completing with error")); + viaduct_necko_result_complete_error( + mResult, static_cast<uint32_t>(NS_ERROR_ABORT), + "Request replaced by move assignment"); + } + mRequest = std::exchange(other.mRequest, nullptr); + mResult = std::exchange(other.mResult, nullptr); + } + return *this; + } + + // Disable copy constructor and assignment + // We prevent copying since we only want to complete the result once. + ViaductRequestGuard(const ViaductRequestGuard& other) = delete; + ViaductRequestGuard& operator=(const ViaductRequestGuard& other) = delete; + + ~ViaductRequestGuard() { + // If mResult is non-null, the request was destroyed before completing. + // This can happen if the closure never runs (e.g., shutdown). + if (mResult) { + MOZ_LOG(gViaductLogger, LogLevel::Warning, + ("ViaductRequestGuard: Destructor called with non-null result, " + "completing with error")); + viaduct_necko_result_complete_error( + mResult, static_cast<uint32_t>(NS_ERROR_ABORT), + "Request destroyed without completion"); + } + } + + // Get the request pointer (for reading request data) + // Returns nullptr if already consumed. + const ViaductRequest* Request() const { + MOZ_ASSERT(mRequest, + "ViaductRequestGuard::Request called after completion"); + return mRequest; + } + + // Get the result pointer (for building up the response) + // Returns nullptr if already consumed. + ViaductResult* Result() const { + MOZ_ASSERT(mResult, "ViaductRequestGuard::Result called after completion"); + return mResult; + } + + // Check if the guard still owns valid pointers + bool IsValid() const { return mResult != nullptr; } + + // Complete the result successfully and release ownership. + // After this call, the guard no longer owns the pointers. + void Complete() { + MOZ_ASSERT(mResult, "ViaductRequestGuard::Complete called twice"); + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("ViaductRequestGuard: Completing successfully")); + viaduct_necko_result_complete(mResult); + mResult = nullptr; + mRequest = nullptr; + } + + // Complete the result with an error and release ownership. + // After this call, the guard no longer owns the pointers. + void CompleteWithError(nsresult aError, const char* aMessage) { + MOZ_ASSERT(mResult, "ViaductRequestGuard::CompleteWithError called twice"); + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("ViaductRequestGuard: Completing with error: %s (0x%08x)", + aMessage, static_cast<uint32_t>(aError))); + viaduct_necko_result_complete_error(mResult, static_cast<uint32_t>(aError), + aMessage); + mResult = nullptr; + mRequest = nullptr; + } +}; + +// Listener that collects the complete HTTP response (headers and body) +class ViaductResponseListener final : public nsIHttpHeaderVisitor, + public nsIStreamListener, + public nsITimerCallback, + public nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIHTTPHEADERVISITOR + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + explicit ViaductResponseListener(ViaductRequestGuard&& aGuard, + uint32_t aTimeoutSecs) + : mGuard(std::move(aGuard)), mChannel(nullptr) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: ViaductResponseListener constructor called with timeout: " + "%u seconds, guard valid: %s", + aTimeoutSecs, mGuard.IsValid() ? "true" : "false")); + + // Create timeout timer if timeout > 0 + if (aTimeoutSecs > 0) { + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Setting timeout timer for %u seconds", aTimeoutSecs)); + nsresult rv = + NS_NewTimerWithCallback(getter_AddRefs(mTimeoutTimer), this, + aTimeoutSecs * 1000, nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("Failed to create timeout timer: 0x%08x", + static_cast<uint32_t>(rv))); + } + } + } + + void SetChannel(nsIChannel* aChannel) { mChannel = aChannel; } + + private: + ~ViaductResponseListener() { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: ViaductResponseListener destructor called")); + + ClearTimer(); + + // The guard's destructor will handle completion if needed + } + + void ClearTimer() { + if (mTimeoutTimer) { + mTimeoutTimer->Cancel(); + mTimeoutTimer = nullptr; + } + } + + // Error handling: logs error and completes the result with error via the + // guard. + void HandleError(nsresult aError, const char* aMessage); + + // Wrapper methods that use the guard to safely access the result + void SetStatusCode(uint16_t aStatusCode); + void SetUrl(const char* aUrl, size_t aLength); + void AddHeader(const char* aKey, size_t aKeyLength, const char* aValue, + size_t aValueLength); + void ExtendBody(const uint8_t* aData, size_t aLength); + void Complete(); + + ViaductRequestGuard mGuard; + nsCOMPtr<nsITimer> mTimeoutTimer; + nsCOMPtr<nsIChannel> mChannel; +}; + +NS_IMPL_ISUPPORTS(ViaductResponseListener, nsIHttpHeaderVisitor, + nsIStreamListener, nsIRequestObserver, nsITimerCallback, + nsINamed) + +void ViaductResponseListener::HandleError(nsresult aError, + const char* aMessage) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("TRACE: HandleError called with message: %s (0x%08x)", aMessage, + static_cast<uint32_t>(aError))); + + if (mGuard.IsValid()) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: Calling CompleteWithError via guard")); + mGuard.CompleteWithError(aError, aMessage); + } else { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("TRACE: HandleError called but guard is invalid")); + } +} + +void ViaductResponseListener::SetStatusCode(uint16_t aStatusCode) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: SetStatusCode called with code: %u", aStatusCode)); + if (!mGuard.IsValid()) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("SetStatusCode called but guard is invalid")); + return; + } + viaduct_necko_result_set_status_code(mGuard.Result(), aStatusCode); + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Set status code: %u", aStatusCode)); +} + +void ViaductResponseListener::SetUrl(const char* aUrl, size_t aLength) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: SetUrl called with URL (length %zu)", aLength)); + if (!mGuard.IsValid()) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("SetUrl called but guard is invalid")); + return; + } + viaduct_necko_result_set_url(mGuard.Result(), aUrl, aLength); + MOZ_LOG(gViaductLogger, LogLevel::Debug, ("Set URL")); +} + +void ViaductResponseListener::AddHeader(const char* aKey, size_t aKeyLength, + const char* aValue, + size_t aValueLength) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: AddHeader called - key length: %zu, value length: %zu", + aKeyLength, aValueLength)); + if (!mGuard.IsValid()) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("AddHeader called but guard is invalid")); + return; + } + viaduct_necko_result_add_header(mGuard.Result(), aKey, aKeyLength, aValue, + aValueLength); + MOZ_LOG(gViaductLogger, LogLevel::Debug, ("Added header")); +} + +void ViaductResponseListener::ExtendBody(const uint8_t* aData, size_t aLength) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: ExtendBody called with %zu bytes", aLength)); + if (!mGuard.IsValid()) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("ExtendBody called but guard is invalid")); + return; + } + viaduct_necko_result_extend_body(mGuard.Result(), aData, aLength); + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Extended body with %zu bytes", aLength)); +} + +void ViaductResponseListener::Complete() { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: Complete called - marking request as successful")); + if (!mGuard.IsValid()) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("Complete called but guard is invalid")); + return; + } + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: Calling Complete via guard")); + mGuard.Complete(); +} + +NS_IMETHODIMP +ViaductResponseListener::VisitHeader(const nsACString& aHeader, + const nsACString& aValue) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: VisitHeader called for header: %s", + PromiseFlatCString(aHeader).get())); + AddHeader(aHeader.BeginReading(), aHeader.Length(), aValue.BeginReading(), + aValue.Length()); + return NS_OK; +} + +NS_IMETHODIMP +ViaductResponseListener::OnStartRequest(nsIRequest* aRequest) { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: ========== OnStartRequest called ==========")); + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + if (!httpChannel) { + HandleError(NS_ERROR_FAILURE, "Request is not an HTTP channel"); + return NS_ERROR_FAILURE; + } + + // Get status code from HTTP channel + uint32_t responseStatus; + nsresult rv = httpChannel->GetResponseStatus(&responseStatus); + if (NS_FAILED(rv)) { + HandleError(rv, "Failed to get response status"); + return rv; + } + SetStatusCode(static_cast<uint16_t>(responseStatus)); + + // Get final URL + nsCOMPtr<nsIURI> uri; + rv = httpChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + HandleError(rv, "Failed to get URI"); + return rv; + } + + if (!uri) { + HandleError(NS_ERROR_FAILURE, "HTTP channel has null URI"); + return NS_ERROR_FAILURE; + } + + nsAutoCString spec; + rv = uri->GetSpec(spec); + if (NS_FAILED(rv)) { + HandleError(rv, "Failed to get URI spec"); + return rv; + } + SetUrl(spec.get(), spec.Length()); + + // Collect response headers - using 'this' since we implement + // nsIHttpHeaderVisitor + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("TRACE: About to visit response headers")); + rv = httpChannel->VisitResponseHeaders(this); + if (NS_FAILED(rv)) { + HandleError(rv, "Failed to visit response headers"); + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +ViaductResponseListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("OnDataAvailable called with %u bytes at offset %" PRIu64, aCount, + aOffset)); + + // Read the data from the input stream + nsTArray<uint8_t> buffer; + buffer.SetLength(aCount); + + uint32_t bytesRead; + nsresult rv = aInputStream->Read(reinterpret_cast<char*>(buffer.Elements()), + aCount, &bytesRead); + if (NS_FAILED(rv)) { + HandleError(rv, "Failed to read from input stream"); + return rv; + } + + if (bytesRead > 0) { + ExtendBody(buffer.Elements(), bytesRead); + } else { + MOZ_LOG(gViaductLogger, LogLevel::Warning, + ("Read 0 bytes from input stream")); + } + + return NS_OK; +} + +NS_IMETHODIMP +ViaductResponseListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("OnStopRequest called with status: 0x%08x", + static_cast<uint32_t>(aStatus))); + + // Cancel timer since request is complete + ClearTimer(); + + if (NS_SUCCEEDED(aStatus)) { + Complete(); + } else { + HandleError(aStatus, "Request failed"); + } + + return NS_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// nsITimerCallback implementation + +NS_IMETHODIMP +ViaductResponseListener::Notify(nsITimer* aTimer) { + MOZ_LOG(gViaductLogger, LogLevel::Warning, + ("TRACE: Request timeout fired - cancelling request")); + + ClearTimer(); + + // Cancel the channel, which will trigger OnStopRequest with an error + if (mChannel) { + mChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL); + mChannel = nullptr; + } + + return NS_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// nsINamed implementation + +NS_IMETHODIMP +ViaductResponseListener::GetName(nsACString& aName) { + aName.AssignLiteral("ViaductResponseListener"); + return NS_OK; +} + +// Convert ViaductMethod to HTTP method string +static const char* GetMethodString(ViaductMethod method) { + switch (method) { + case VIADUCT_METHOD_GET: + return "GET"; + case VIADUCT_METHOD_HEAD: + return "HEAD"; + case VIADUCT_METHOD_POST: + return "POST"; + case VIADUCT_METHOD_PUT: + return "PUT"; + case VIADUCT_METHOD_DELETE: + return "DELETE"; + case VIADUCT_METHOD_CONNECT: + return "CONNECT"; + case VIADUCT_METHOD_OPTIONS: + return "OPTIONS"; + case VIADUCT_METHOD_TRACE: + return "TRACE"; + case VIADUCT_METHOD_PATCH: + return "PATCH"; + default: + MOZ_LOG(gViaductLogger, LogLevel::Warning, + ("Unknown ViaductMethod: %d, defaulting to GET", method)); + return "GET"; + } +} + +extern "C" { + +void viaduct_necko_backend_init() { + MOZ_LOG(gViaductLogger, LogLevel::Info, + ("Viaduct Necko backend initialized")); +} + +void viaduct_necko_backend_send_request(const ViaductRequest* request, + ViaductResult* result) { + MOZ_LOG(gViaductLogger, LogLevel::Debug, ("send_request called")); + + MOZ_ASSERT(request, "Request pointer should not be null"); + MOZ_ASSERT(result, "Result pointer should not be null"); + + // Create a guard to manage the request/result pointer lifetime. + // This ensures that either viaduct_necko_result_complete or + // viaduct_necko_result_complete_error is called exactly once, + // even if the closure never runs (e.g., during shutdown). + ViaductRequestGuard guard(request, result); + + // This function is called from Rust on a background thread. + // We need to dispatch to the main thread to use Necko. + NS_DispatchToMainThread(NS_NewRunnableFunction( + "ViaductNeckoRequest", [guard = std::move(guard)]() mutable { + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Executing request on main thread")); + + MOZ_ASSERT(guard.Request() && guard.Result(), + "Guard should have valid pointers"); + + nsresult rv; + + // Parse the URL + nsCOMPtr<nsIURI> uri; + nsAutoCString urlSpec(guard.Request()->url); + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Parsing URL: %s", urlSpec.get())); + + rv = NS_NewURI(getter_AddRefs(uri), urlSpec); + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to parse URL"); + return; + } + + // Create the channel + nsSecurityFlags secFlags = + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL | + nsILoadInfo::SEC_COOKIES_OMIT; + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), uri, + nsContentUtils::GetSystemPrincipal(), secFlags, + nsIContentPolicy::TYPE_OTHER); + + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to create channel"); + return; + } + + if (!channel) { + guard.CompleteWithError(NS_ERROR_FAILURE, + "NS_NewChannel returned null channel"); + return; + } + + // Get the HTTP channel interface + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel); + if (!httpChannel) { + guard.CompleteWithError(NS_ERROR_FAILURE, + "Channel is not an HTTP channel"); + return; + } + + // Set HTTP method + const char* methodStr = GetMethodString(guard.Request()->method); + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Setting HTTP method: %s", methodStr)); + rv = httpChannel->SetRequestMethod(nsDependentCString(methodStr)); + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to set request method"); + return; + } + + // Set request headers + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Setting %zu request headers", guard.Request()->header_count)); + for (size_t i = 0; i < guard.Request()->header_count; i++) { + nsAutoCString key(guard.Request()->headers[i].key); + nsAutoCString value(guard.Request()->headers[i].value); + rv = httpChannel->SetRequestHeader(key, value, false); + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to set request header"); + return; + } + } + + // Set redirect limit + if (guard.Request()->redirect_limit == 0) { + // Disable redirects entirely + MOZ_LOG(gViaductLogger, LogLevel::Debug, ("Disabling redirects")); + nsCOMPtr<nsIHttpChannelInternal> httpInternal = + do_QueryInterface(httpChannel); + if (!httpInternal) { + guard.CompleteWithError( + NS_ERROR_FAILURE, + "Failed to get nsIHttpChannelInternal interface"); + return; + } + rv = httpInternal->SetRedirectMode( + nsIHttpChannelInternal::REDIRECT_MODE_ERROR); + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to set redirect mode"); + return; + } + } else { + // Set a specific redirect limit + MOZ_LOG( + gViaductLogger, LogLevel::Debug, + ("Setting redirect limit: %u", guard.Request()->redirect_limit)); + rv = + httpChannel->SetRedirectionLimit(guard.Request()->redirect_limit); + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to set redirection limit"); + return; + } + } + + // Set request body if present + if (guard.Request()->body != nullptr && guard.Request()->body_len > 0) { + MOZ_LOG( + gViaductLogger, LogLevel::Debug, + ("Setting request body (%zu bytes)", guard.Request()->body_len)); + nsCOMPtr<nsIUploadChannel2> uploadChannel = + do_QueryInterface(httpChannel); + if (!uploadChannel) { + guard.CompleteWithError( + NS_ERROR_FAILURE, "Failed to get nsIUploadChannel2 interface"); + return; + } + + nsCOMPtr<nsIInputStream> bodyStream; + rv = NS_NewByteInputStream( + getter_AddRefs(bodyStream), + Span(reinterpret_cast<const char*>(guard.Request()->body), + guard.Request()->body_len), + NS_ASSIGNMENT_COPY); + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to create body stream"); + return; + } + + rv = uploadChannel->ExplicitSetUploadStream( + bodyStream, VoidCString(), guard.Request()->body_len, + nsDependentCString(methodStr), false); + if (NS_FAILED(rv)) { + guard.CompleteWithError(rv, "Failed to set upload stream"); + return; + } + } + + // Get timeout before moving the guard + uint32_t timeout = guard.Request()->timeout; + + // Create listener with timeout support. + // Move the guard into the listener so it owns the request/result + // pointers. + RefPtr<ViaductResponseListener> listener = + new ViaductResponseListener(std::move(guard), timeout); + + // Store the channel in the listener so it can cancel it on + // timeout + listener->SetChannel(channel); + + MOZ_LOG(gViaductLogger, LogLevel::Debug, ("Opening HTTP channel")); + rv = httpChannel->AsyncOpen(listener); + + if (NS_FAILED(rv)) { + MOZ_LOG(gViaductLogger, LogLevel::Error, + ("AsyncOpen failed: 0x%08x. Guard was moved to listener, " + "destructor will handle cleanup and complete with error.", + static_cast<uint32_t>(rv))); + return; + } + + MOZ_LOG(gViaductLogger, LogLevel::Debug, + ("Request initiated successfully")); + // The request is now in progress. The listener will handle + // completion. + })); +} + +} // extern "C" diff --git a/services/application-services/components/viaduct-necko/backend.h b/services/application-services/components/viaduct-necko/backend.h @@ -0,0 +1,72 @@ +/* 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 VIADUCT_NECKO_H +#define VIADUCT_NECKO_H + +#include <stdint.h> +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +// HTTP Method enumeration (must match Rust side) +enum ViaductMethod : uint8_t { + VIADUCT_METHOD_GET = 0, + VIADUCT_METHOD_HEAD = 1, + VIADUCT_METHOD_POST = 2, + VIADUCT_METHOD_PUT = 3, + VIADUCT_METHOD_DELETE = 4, + VIADUCT_METHOD_CONNECT = 5, + VIADUCT_METHOD_OPTIONS = 6, + VIADUCT_METHOD_TRACE = 7, + VIADUCT_METHOD_PATCH = 8, +}; + +// Header structure +struct ViaductHeader { + const char* key; + const char* value; +}; + +// Request structure +struct ViaductRequest { + uint32_t timeout; + uint32_t redirect_limit; + ViaductMethod method; + const char* url; + const ViaductHeader* headers; + size_t header_count; + const uint8_t* body; // Body remains uint8_t* since it's binary data + size_t body_len; +}; + +// Opaque result pointer (points to Rust FfiResult) +struct ViaductResult; + +// Functions that C++ must implement +void viaduct_necko_backend_init(); +void viaduct_necko_backend_send_request(const ViaductRequest* request, + ViaductResult* result); + +// Functions that Rust provides for C++ to call +void viaduct_necko_result_set_url(ViaductResult* result, const char* url, + size_t length); +void viaduct_necko_result_set_status_code(ViaductResult* result, uint16_t code); +void viaduct_necko_result_add_header(ViaductResult* result, const char* key, + size_t key_length, const char* value, + size_t value_length); +void viaduct_necko_result_extend_body(ViaductResult* result, + const uint8_t* data, size_t length); +void viaduct_necko_result_complete(ViaductResult* result); +void viaduct_necko_result_complete_error(ViaductResult* result, + uint32_t error_code, + const char* message); + +#ifdef __cplusplus +} +#endif + +#endif // VIADUCT_NECKO_H diff --git a/services/application-services/components/viaduct-necko/moz.build b/services/application-services/components/viaduct-necko/moz.build @@ -0,0 +1,16 @@ +# -*- 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/. + +FINAL_LIBRARY = "xul" + +with Files("**"): + BUG_COMPONENT = ("Application Services", "General") + +UNIFIED_SOURCES += [ + "backend.cpp", +] + +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell.toml"] diff --git a/services/application-services/components/viaduct-necko/src/lib.rs b/services/application-services/components/viaduct-necko/src/lib.rs @@ -0,0 +1,351 @@ +/* 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 error_support::{info, warn}; +use futures_channel::oneshot; +use std::{ffi::{CStr, c_char}, ptr, slice, sync::Arc}; +use url::Url; +use viaduct::{ + init_backend, Backend, ClientSettings, Method, Request, Response, Result, ViaductError, +}; + +const NULL: char = '\0'; + +/// Request for the C++ backend +#[repr(C)] +pub struct FfiRequest { + pub timeout: u32, + pub redirect_limit: u32, + pub method: Method, + pub url: *mut u8, + pub headers: *mut FfiHeader, + pub header_count: usize, + pub body: *mut u8, + pub body_len: usize, +} + +#[repr(C)] +pub struct FfiHeader { + pub key: *mut u8, + pub value: *mut u8, +} + +/// Result from the backend +/// +/// This is built-up piece by piece using the extern "C" API. +pub struct FfiResult { + // oneshot sender that the Rust code is awaiting. If `Ok(())` is sent, then the Rust code + // should return the response. If an error is sent, then that should be returned instead. + sender: Option<oneshot::Sender<Result<Response>>>, + response: Response, + // Owned values stored in the [FfiRequest]. These are copied from the request. By storing + // them in the result, we ensure they stay alive while the C code may access them. + pub url: String, + pub headers: Vec<(String, String)>, + pub body: Option<Vec<u8>>, + // The request struct that we pass to C++. This must be kept alive as long as the C++ code is + // using it. + pub request: FfiRequest, + pub ffi_headers: Vec<FfiHeader>, +} + +// Functions that the C++ library exports for us +extern "C" { + fn viaduct_necko_backend_init(); + + #[allow(improper_ctypes)] + fn viaduct_necko_backend_send_request(request: *const FfiRequest, result: *mut FfiResult); +} + +// Functions that we provide to the C++ library + +/// Set the URL for a result +/// +/// # Safety +/// +/// - `result` must be valid. +/// - `url` and `length` must refer to a valid byte string. +/// +/// Note: URLs are expected to be ASCII. Non-ASCII URLs will be logged and skipped. +#[no_mangle] +pub unsafe extern "C" fn viaduct_necko_result_set_url( + result: *mut FfiResult, + url: *const u8, + length: usize, +) { + let result = unsafe { &mut *result }; + + // Safety: Creating a slice from raw parts is safe if the backend passes valid pointers and lengths + let url_bytes = unsafe { slice::from_raw_parts(url, length) }; + + // Validate that the URL is ASCII before converting to String + if !url_bytes.is_ascii() { + warn!( + "Non-ASCII URL received - length: {} - skipping URL update", + length + ); + return; + } + + // Safety: We just verified the bytes are ASCII, which is valid UTF-8 + let url_str = unsafe { std::str::from_utf8_unchecked(url_bytes) }; + + match Url::parse(url_str) { + Ok(url) => { + result.response.url = url; + } + Err(e) => { + warn!("Error parsing URL from C backend: {e}") + } + } +} + +/// Set the status code for a result +/// +/// # Safety +/// +/// `result` must be valid. +#[no_mangle] +pub unsafe extern "C" fn viaduct_necko_result_set_status_code(result: *mut FfiResult, code: u16) { + let result = unsafe { &mut *result }; + result.response.status = code; +} + +/// Set a header for a result +/// +/// # Safety +/// +/// - `result` must be valid. +/// - `key` and `key_length` must refer to a valid byte string. +/// - `value` and `value_length` must refer to a valid byte string. +/// +/// Note: HTTP headers are expected to be ASCII. Non-ASCII headers will be logged and skipped. +#[no_mangle] +pub unsafe extern "C" fn viaduct_necko_result_add_header( + result: *mut FfiResult, + key: *const u8, + key_length: usize, + value: *const u8, + value_length: usize, +) { + let result = unsafe { &mut *result }; + + // Safety: Creating slices from raw parts is safe if the backend passes valid pointers and lengths + let key_bytes = unsafe { slice::from_raw_parts(key, key_length) }; + let value_bytes = unsafe { slice::from_raw_parts(value, value_length) }; + + // Validate that headers are ASCII before converting to String + // HTTP headers should be ASCII per best practices, though the spec technically allows other encodings + if !key_bytes.is_ascii() || !value_bytes.is_ascii() { + warn!( + "Non-ASCII HTTP header received - key_len: {}, value_len: {} - skipping header", + key_length, value_length + ); + return; + } + + // Safety: We just verified the bytes are ASCII, which is valid UTF-8 + let (key, value) = unsafe { + ( + String::from_utf8_unchecked(key_bytes.to_vec()), + String::from_utf8_unchecked(value_bytes.to_vec()), + ) + }; + + let _ = result.response.headers.insert(key, value); +} + +/// Append data to a result body +/// +/// This method can be called multiple times to build up the body in chunks. +/// +/// # Safety +/// +/// - `result` must be valid. +/// - `data` and `length` must refer to a binary string. +#[no_mangle] +pub unsafe extern "C" fn viaduct_necko_result_extend_body( + result: *mut FfiResult, + data: *const u8, + length: usize, +) { + let result = unsafe { &mut *result }; + // Safety: this is safe as long as the backend passes us valid data + result + .response + .body + .extend_from_slice(unsafe { slice::from_raw_parts(data, length) }); +} + +/// Complete a result +/// +/// # Safety +/// +/// `result` must be valid. After calling this function it must not be used again. +#[no_mangle] +pub unsafe extern "C" fn viaduct_necko_result_complete(result: *mut FfiResult) { + let mut result = unsafe { Box::from_raw(result) }; + match result.sender.take() { + Some(sender) => { + // Ignore any errors when sending the result. This happens when the receiver is + // closed, which happens when a future is cancelled. + let _ = sender.send(Ok(result.response)); + } + None => warn!("viaduct-necko: result completed twice"), + } +} + +/// Complete a result with an error message +/// +/// # Safety +/// +/// - `result` must be valid. After calling this function it must not be used again. +/// - `message` and `length` must refer to a valid UTF-8 string. +#[no_mangle] +pub unsafe extern "C" fn viaduct_necko_result_complete_error( + result: *mut FfiResult, + error_code: u32, + message: *const u8, +) { + let mut result = unsafe { Box::from_raw(result) }; + // Safety: this is safe as long as the backend passes us valid data + let msg_str = unsafe { + CStr::from_ptr(message as *const c_char) + .to_string_lossy() + .into_owned() + }; + let msg = format!("{} (0x{:08x})", msg_str, error_code); + match result.sender.take() { + Some(sender) => { + // Ignore any errors when sending the result. This happens when the receiver is + // closed, which happens when a future is cancelled. + let _ = sender.send(Err(ViaductError::BackendError(msg))); + } + None => warn!("viaduct-necko: result completed twice"), + } +} + +// The Necko backend is a zero-sized type, since all the backend functionality is statically linked +struct NeckoBackend; + +/// Initialize the Necko backend +/// +/// This should be called once at startup before any HTTP requests are made. +pub fn init_necko_backend() -> Result<()> { + info!("Initializing viaduct Necko backend"); + // Safety: this is safe as long as the C++ code is correct. + unsafe { viaduct_necko_backend_init() }; + init_backend(Arc::new(NeckoBackend)) +} + +#[async_trait::async_trait] +impl Backend for NeckoBackend { + async fn send_request(&self, request: Request, settings: ClientSettings) -> Result<Response> { + // Convert the request for the backend + let mut url = request.url.to_string(); + url.push(NULL); + + // Convert headers to null-terminated strings for C++ + // Note: Headers iterates over Header objects, not tuples + let header_strings: Vec<(String, String)> = request + .headers + .iter() + .map(|h| { + let mut key_str = h.name().to_string(); + key_str.push(NULL); + let mut value_str = h.value().to_string(); + value_str.push(NULL); + (key_str, value_str) + }) + .collect(); + + // Prepare an FfiResult with an empty response + let (sender, receiver) = oneshot::channel(); + let mut result = Box::new(FfiResult { + sender: Some(sender), + response: Response { + request_method: request.method, + url: request.url.clone(), + status: 0, + headers: viaduct::Headers::new(), + body: Vec::default(), + }, + url, + headers: header_strings, + body: request.body, + request: FfiRequest { + timeout: settings.timeout, + redirect_limit: settings.redirect_limit, + method: request.method, + url: ptr::null_mut(), + headers: ptr::null_mut(), + header_count: 0, + body: ptr::null_mut(), + body_len: 0, + }, + ffi_headers: Vec::new(), + }); + + // Now that we have the result box, we can set up the pointers in the request. + // By doing this after creating the box, we minimize the chance that a value moves after a pointer is created. + result.ffi_headers = result + .headers + .iter_mut() + .map(|(key, value)| FfiHeader { + key: key.as_mut_ptr(), + value: value.as_mut_ptr(), + }) + .collect(); + + let (body_ptr, body_len) = match &result.body { + Some(body) => (body.as_ptr() as *mut u8, body.len()), + None => (ptr::null_mut(), 0), + }; + + result.request.url = result.url.as_mut_ptr(); + result.request.headers = result.ffi_headers.as_mut_ptr(); + result.request.header_count = result.ffi_headers.len(); + result.request.body = body_ptr; + result.request.body_len = body_len; + + let request_ptr = &result.request as *const FfiRequest; + + // Safety: this is safe if the C backend implements the API correctly. + unsafe { + viaduct_necko_backend_send_request(request_ptr, Box::into_raw(result)); + }; + + receiver.await.unwrap_or_else(|_| { + Err(ViaductError::BackendError( + "Error receiving result from C++ backend".to_string(), + )) + }) + } +} + +// Mark FFI types as Send to allow them to be used across an await point. This is safe as long as +// the backend code uses them correctly. +unsafe impl Send for FfiRequest {} +unsafe impl Send for FfiResult {} +unsafe impl Send for FfiHeader {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_method_layout() { + // Assert that the viaduct::Method enum matches the layout expected by the C++ backend. + // See ViaductMethod in backend.h + assert_eq!(Method::Get as u8, 0); + assert_eq!(Method::Head as u8, 1); + assert_eq!(Method::Post as u8, 2); + assert_eq!(Method::Put as u8, 3); + assert_eq!(Method::Delete as u8, 4); + assert_eq!(Method::Connect as u8, 5); + assert_eq!(Method::Options as u8, 6); + assert_eq!(Method::Trace as u8, 7); + assert_eq!(Method::Patch as u8, 8); + } +} diff --git a/services/application-services/components/viaduct-necko/tests/test_viaduct_necko_backend.js b/services/application-services/components/viaduct-necko/tests/test_viaduct_necko_backend.js @@ -0,0 +1,318 @@ +/* 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"; + +/* global add_setup, add_task, Assert, info, do_get_profile, do_timeout, registerCleanupFunction, Services */ + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +// Create a test HTTP server +let gHttpServer = null; +let gServerPort = -1; +let gServerURL = ""; + +// Track requests received by the server +let gRequestsReceived = []; + +/** + * Helper to decode potentially gzipped request body + */ +function decodeRequestBody(request) { + let bodyStream = request.bodyInputStream; + let avail = bodyStream.available(); + if (avail === 0) { + return ""; + } + + if ( + request.hasHeader("content-encoding") && + request.getHeader("content-encoding") === "gzip" + ) { + // For gzipped content, we'd need to decompress + // For now, just note it was gzipped + return "[gzipped content]"; + } + + return NetUtil.readInputStreamToString(bodyStream, avail); +} + +/** + * Setup function to initialize the HTTP server + */ +add_setup(async function () { + info("Setting up viaduct-necko test environment"); + + // FOG needs a profile directory to store its data + do_get_profile(); + + // Create and start the HTTP server + gHttpServer = new HttpServer(); + + // Register various test endpoints + setupTestEndpoints(); + + gHttpServer.start(-1); + gServerPort = gHttpServer.identity.primaryPort; + gServerURL = `http://localhost:${gServerPort}`; + + info(`Test HTTP server started on port ${gServerPort}`); + + // Set the telemetry port preference to use our test server + Services.prefs.setIntPref("telemetry.fog.test.localhost_port", gServerPort); + + // Enable telemetry upload (needed for viaduct to be used) + Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true); + + // Initialize FOG/Glean which should trigger viaduct-necko initialization + // This internally calls init_necko_backend() through GkRust_Init + Services.fog.testResetFOG(); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("telemetry.fog.test.localhost_port"); + Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled"); + await new Promise(resolve => gHttpServer.stop(resolve)); + }); +}); + +/** + * Setup test endpoints on the HTTP server + */ +function setupTestEndpoints() { + // Glean telemetry submission endpoints + gHttpServer.registerPrefixHandler("/submit/", (request, response) => { + const path = request.path; + info(`Viaduct request received: ${request.method} ${path}`); + + // Read the request body + const body = decodeRequestBody(request); + + // Extract ping type from path + // Path format: /submit/firefox-desktop/{ping-type}/1/{uuid} + const pathParts = path.split("/"); + const pingType = pathParts[3] || "unknown"; + + gRequestsReceived.push({ + path, + method: request.method, + pingType, + body, + bodySize: request.hasHeader("content-length") + ? parseInt(request.getHeader("content-length"), 10) + : body.length, + headers: { + "content-type": request.hasHeader("content-type") + ? request.getHeader("content-type") + : null, + "content-encoding": request.hasHeader("content-encoding") + ? request.getHeader("content-encoding") + : null, + "user-agent": request.hasHeader("user-agent") + ? request.getHeader("user-agent") + : null, + }, + }); + + // Return a response similar to what a telemetry server would return + // Using 501 to match your test scenario + response.setStatusLine(request.httpVersion, 501, "Not Implemented"); + response.setHeader("Server", "TestServer/1.0 (Viaduct Backend)"); + response.setHeader("Date", new Date().toUTCString()); + response.setHeader("Connection", "close"); + response.setHeader("Content-Type", "application/json"); + + const responseBody = JSON.stringify({ + status: "not_implemented", + message: "Test server response", + }); + response.setHeader("Content-Length", responseBody.length.toString()); + response.write(responseBody); + }); +} + +/** + * Test that the viaduct-necko backend was initialized and is processing requests + */ +add_task(async function test_viaduct_backend_working() { + info("Testing viaduct-necko backend initialization and request processing"); + + // Clear previous requests (though some health pings may have already been sent) + const initialRequestCount = gRequestsReceived.length; + info( + `Already received ${initialRequestCount} requests during initialization` + ); + + // Wait for any pending requests to complete using do_timeout + // This ensures we capture the health pings that are automatically sent + await new Promise(resolve => do_timeout(500, resolve)); + + // Check that we've received requests through viaduct + Assert.ok( + !!gRequestsReceived.length, + `Viaduct-necko backend is processing requests. Received ${gRequestsReceived.length} requests.` + ); + + // Verify the requests are health pings (as shown in your logs) + const healthPings = gRequestsReceived.filter(r => r.pingType === "health"); + info(`Received ${healthPings.length} health pings through viaduct-necko`); + + // All telemetry submissions should be POST requests + for (const request of gRequestsReceived) { + Assert.equal( + request.method, + "POST", + `Request to ${request.path} should be POST` + ); + } + + // Log summary of what was processed + const pingTypes = [...new Set(gRequestsReceived.map(r => r.pingType))]; + info( + `Test successful: Viaduct-necko backend processed ${gRequestsReceived.length} requests` + ); + info(`Ping types received: ${pingTypes.join(", ")}`); + info( + `The C++ backend successfully handled requests from Rust through the FFI layer` + ); +}); + +/** + * Test different HTTP parameters and methods + * We verify different body sizes and headers are handled correctly + */ +add_task(async function test_different_parameters() { + info("Testing different HTTP parameters through viaduct-necko"); + + // Clear request tracking + gRequestsReceived = []; + + // Submit different types of pings with varying sizes + // This will test different body sizes and headers + + // Reset FOG to trigger new pings + Services.fog.testResetFOG(); + + // Wait to collect the requests + await new Promise(resolve => do_timeout(1000, resolve)); + + const requestsAfterReset = gRequestsReceived.length; + info(`Received ${requestsAfterReset} requests after FOG reset`); + + // Verify different content types and encodings were handled + const contentTypes = new Set(); + const contentEncodings = new Set(); + const bodySizes = new Set(); + + for (const request of gRequestsReceived) { + if (request.headers["content-type"]) { + contentTypes.add(request.headers["content-type"]); + } + if (request.headers["content-encoding"]) { + contentEncodings.add(request.headers["content-encoding"]); + } + if (request.bodySize) { + bodySizes.add(request.bodySize); + } + } + + info(`Content types seen: ${Array.from(contentTypes).join(", ")}`); + info(`Content encodings seen: ${Array.from(contentEncodings).join(", ")}`); + info( + `Body sizes seen: ${Array.from(bodySizes) + .sort((a, b) => a - b) + .join(", ")}` + ); + + Assert.ok( + !!gRequestsReceived.length, + "Different parameters were processed successfully" + ); + + // Verify we're seeing variation in body sizes (different ping types have different sizes) + Assert.ok( + bodySizes.size > 1, + `Multiple body sizes handled: ${Array.from(bodySizes).join(", ")}` + ); +}); + +/** + * Test that headers are properly passed through the FFI layer + */ +add_task(async function test_header_handling() { + info("Testing header handling through viaduct-necko"); + + // Check the headers that were sent in previous requests + let hasHeaders = false; + let headerCount = 0; + + for (const request of gRequestsReceived) { + if (request.headers && Object.keys(request.headers).length) { + hasHeaders = true; + const nonNullHeaders = Object.entries(request.headers).filter( + ([_, value]) => value !== null + ); + + if (nonNullHeaders.length) { + headerCount++; + info( + `Request headers found: ${JSON.stringify(Object.fromEntries(nonNullHeaders))}` + ); + } + } + } + + Assert.ok( + hasHeaders, + "Headers are properly transmitted through viaduct-necko" + ); + + Assert.ok(headerCount > 0, `Found ${headerCount} requests with headers`); + + // Verify specific headers we expect to see + const hasContentType = gRequestsReceived.some( + r => r.headers && r.headers["content-type"] !== null + ); + const hasContentEncoding = gRequestsReceived.some( + r => r.headers && r.headers["content-encoding"] !== null + ); + const hasUserAgent = gRequestsReceived.some( + r => r.headers && r.headers["user-agent"] !== null + ); + + Assert.ok(hasContentType, "Content-Type header is present"); + Assert.ok(hasContentEncoding, "Content-Encoding header is present"); + Assert.ok(hasUserAgent, "User-Agent header is present"); + + info("Headers are properly handled through the Rust → C++ → Necko chain"); +}); + +/** + * Test configuration validation + * While we can't directly test redirects and timeouts, we can verify + * that the configuration is being passed correctly from logs + */ +add_task(async function test_configuration_validation() { + info("Validating viaduct-necko configuration"); + + // We can verify at least that requests are completing successfully + // which means the configuration isn't breaking anything + const successfulRequests = gRequestsReceived.filter( + r => r.method === "POST" && r.path.includes("/submit/") + ); + + Assert.ok( + !!successfulRequests.length, + `Configuration is valid: ${successfulRequests.length} successful requests processed` + ); + + info( + "Viaduct-necko backend configuration validated through successful request processing" + ); +}); diff --git a/services/application-services/components/viaduct-necko/tests/xpcshell.toml b/services/application-services/components/viaduct-necko/tests/xpcshell.toml @@ -0,0 +1,5 @@ +[DEFAULT] +head = "" +firefox-appdir = "browser" + +["test_viaduct_necko_backend.js"] diff --git a/services/moz.build b/services/moz.build @@ -32,6 +32,7 @@ if not CONFIG["RELEASE_OR_BETA"] or CONFIG["MOZ_DEBUG"]: if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": DIRS += [ "fxaccounts", + "application-services/components/viaduct-necko", ] if CONFIG["MOZ_SERVICES_SYNC"]: diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml @@ -4623,6 +4623,11 @@ The git branch is my fork of the official code that removes the `loom` target to This doesn't change any of the functionality -- the `loom` target is only used for testing. """ +[[audits.oneshot]] +who = "Bastian Gruber <foreach@me.com>" +criteria = "safe-to-deploy" +version = "0.1.11" + [[audits.oneshot-uniffi]] who = "Ben Dean-Kawamura <bdk@mozilla.com>" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock @@ -2807,6 +2807,7 @@ criteria = "safe-to-deploy" user-id = 213776 # divviup-github-automation start = "2020-09-28" end = "2026-01-07" +renew = false [[audits.isrg.audits.base64]] who = "Tim Geoghegan <timg@letsencrypt.org>" diff --git a/toolkit/library/rust/shared/Cargo.toml b/toolkit/library/rust/shared/Cargo.toml @@ -125,6 +125,7 @@ osclientcerts = { path = "../../../../security/manager/ssl/osclientcerts" } gkrust-uniffi-components = { path = "../../../components/uniffi-bindgen-gecko-js/components/", features = ["xpcom"] } uniffi-bindgen-gecko-js-test-fixtures = { path = "../../../components/uniffi-bindgen-gecko-js/test-fixtures/", optional = true } viaduct = "0.1" +viaduct-necko = { path = "../../../../services/application-services/components/viaduct-necko" } webext-storage = "0.1" [target.'cfg(target_os = "windows")'.dependencies] diff --git a/toolkit/library/rust/shared/lib.rs b/toolkit/library/rust/shared/lib.rs @@ -107,6 +107,8 @@ extern crate uniffi_bindgen_gecko_js_test_fixtures; #[cfg(not(target_os = "android"))] extern crate viaduct; +#[cfg(not(target_os = "android"))] +extern crate viaduct_necko; extern crate gecko_logger; extern crate gecko_tracing; @@ -162,6 +164,10 @@ pub extern "C" fn GkRust_Init() { let _ = GeckoLogger::init(); // Initialize tracing. gecko_tracing::initialize_tracing(); + #[cfg(not(target_os = "android"))] + if let Err(e) = viaduct_necko::init_necko_backend() { + log::warn!("Failed to initialize viaduct-necko backend: {:?}", e); + } } #[no_mangle]