Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Include/UrlLib/UrlLib.h
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#pragma once

#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <arcana/threading/task.h>
#include <unordered_map>
#include <vector>

namespace UrlLib
{
Expand All @@ -28,6 +30,21 @@ namespace UrlLib
Buffer,
};

// Result returned by a custom URL scheme resolver (see UrlRequest::RegisterSchemeResolver).
// When `handled` is false the URL had no live entry (e.g. a revoked blob: URL) and the request
// surfaces as a network error (status stays 0/None), mirroring the transport-failure contract.
struct UrlSchemeResolverResult
{
bool handled{false};
UrlStatusCode statusCode{UrlStatusCode::None};
std::string statusText{};
std::string contentType{};
std::shared_ptr<const std::vector<std::byte>> body{};
};

// Resolves a URL of a registered non-transport scheme (e.g. "blob") to an in-memory response.
using UrlSchemeResolver = std::function<UrlSchemeResolverResult(const std::string& url)>;

class UrlRequest final
{
public:
Expand All @@ -46,6 +63,27 @@ namespace UrlLib

void Open(UrlMethod method, const std::string& url);

// Registers a resolver for a non-transport URL scheme such as "blob". Registration is
// process-global. When a UrlRequest is opened with a URL whose scheme has a registered
// resolver, the platform transport is bypassed and the resolver supplies the response at
// SendAsync() time, so every consumer (fetch, XMLHttpRequest, image / video src, texture
// loaders, ...) resolves such URLs uniformly through UrlRequest instead of each carrying
// its own branch.
//
// A resolver that reports the URL as not handled -- or that throws -- surfaces as a
// transport-style failure (status stays None, with an error symbol recorded); an exception
// never escapes SendAsync().
//
// Throws std::invalid_argument if `scheme` is empty or `resolver` is null. Removal is the
// explicit UnregisterSchemeResolver call rather than registering an empty std::function, so
// a moved-from or default-constructed resolver cannot silently unregister a scheme.
static void RegisterSchemeResolver(std::string scheme, UrlSchemeResolver resolver);
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
Comment thread
bghgary marked this conversation as resolved.

// Removes the resolver registered for `scheme`, if any. Unknown schemes are ignored.
// In-flight requests already diverted to that resolver are unaffected: each request holds
// its own reference to the resolver from the time it was opened.
static void UnregisterSchemeResolver(std::string scheme);

UrlResponseType ResponseType() const;

void ResponseType(UrlResponseType value);
Expand Down
192 changes: 191 additions & 1 deletion Source/UrlRequest_Base.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

#include <UrlLib/UrlLib.h>
#include <arcana/threading/cancellation.h>
#include <string>
#include <cctype>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

namespace UrlLib
{
Expand All @@ -21,6 +25,151 @@ namespace UrlLib
m_cancellationSource.cancel();
}

// ---- Custom scheme resolvers (e.g. blob:) ---------------------------------------------
// Resolution for registered schemes is handled entirely in the shared layer; the platform
// transport is never involved. RegisterSchemeResolver installs a process-global resolver;
// BeginSchemeResolution is called from Open() to divert a matching URL; ResolveScheme is
// called from SendAsync() so revoke-after-open is honored; ResolvedResponseBuffer backs
// ResponseBuffer() for the Buffer response type.
//
// A null resolver is rejected rather than treated as an implicit removal: a resolver can
// capture state that must outlive registration, so silently unregistering on a moved-from
// or default-constructed std::function would be an easy mistake to miss. Removal is the
// explicit UnregisterSchemeResolver call.
static void RegisterSchemeResolver(std::string scheme, UrlSchemeResolver resolver)
{
if (!resolver)
{
throw std::invalid_argument{"RegisterSchemeResolver: resolver must not be null (use UnregisterSchemeResolver to remove a scheme)"};
}

ToLower(scheme);
if (scheme.empty())
{
throw std::invalid_argument{"RegisterSchemeResolver: scheme must not be empty"};
}

auto& registry = Registry();
const std::lock_guard<std::mutex> lock{registry.mutex};
registry.resolvers[std::move(scheme)] = std::move(resolver);
}

// Removes the resolver registered for `scheme`, if any. Unknown schemes are ignored.
static void UnregisterSchemeResolver(std::string scheme)
{
ToLower(scheme);
auto& registry = Registry();
const std::lock_guard<std::mutex> lock{registry.mutex};
registry.resolvers.erase(scheme);
}

// Returns true (and defers the actual work to ResolveScheme()) when `url`'s scheme has a
// registered resolver, in which case the caller must not touch the platform transport.
bool BeginSchemeResolution(const std::string& url)
{
const std::string scheme = SchemeOf(url);
if (scheme.empty())
{
return false;
}

UrlSchemeResolver resolver{};
{
auto& registry = Registry();
const std::lock_guard<std::mutex> lock{registry.mutex};
const auto it = registry.resolvers.find(scheme);
if (it == registry.resolvers.end())
{
return false;
}
resolver = it->second;
}

ResetForOpen();
m_pendingResolver = std::move(resolver);
m_pendingResolverUrl = url;
m_usingSchemeResolver = true;
return true;
}

bool IsSchemeResolution() const
{
return m_usingSchemeResolver;
}

// Invokes the registered resolver and populates the response state. A resolver that reports
// the URL as not handled (e.g. a revoked blob: URL) -- or that throws -- leaves the status at
// 0 (None) and records a transport-style error, mirroring how a genuine network failure
// surfaces. The pending resolver is consumed here so a second SendAsync() on the same request
// does not re-run the resolver (which could re-trigger side effects or clobber the response);
// the resolved response state is retained and IsSchemeResolution() stays true so
// ResponseBuffer() keeps serving the resolved bytes.
void ResolveScheme()
{
if (!m_pendingResolver)
{
return;
}

// Move the resolver/url out and clear them before invoking, so this runs exactly once.
const UrlSchemeResolver resolver = std::move(m_pendingResolver);
const std::string url = std::move(m_pendingResolverUrl);
m_pendingResolver = nullptr;
m_pendingResolverUrl.clear();

m_responseUrl = url;

// A throwing resolver is contained here and reported through the same error surface as
// any other failure, so it cannot escape SendAsync() synchronously -- callers observe a
// failed request (status 0 + error) exactly as they would for a transport failure.
UrlSchemeResolverResult result{};
try
{
result = resolver(url);
}
catch (const std::exception& e)
{
SetError("urllib", "SchemeResolverThrew", 0, e.what());
return;
}
catch (...)
{
SetError("urllib", "SchemeResolverThrew", 0, "unknown exception");
return;
}

if (!result.handled)
{
SetError("urllib", "SchemeResolverNotFound", 0, "no live entry for '" + url + "'");
return;
}

m_statusCode = result.statusCode;
if (!result.statusText.empty())
Comment thread
bghgary marked this conversation as resolved.
{
m_statusText = result.statusText;
}
if (!result.contentType.empty())
{
m_headers["content-type"] = result.contentType;
}

m_resolvedBuffer = result.body ? result.body : std::make_shared<const std::vector<std::byte>>();
if (m_responseType == UrlResponseType::String)
{
m_responseString.assign(reinterpret_cast<const char*>(m_resolvedBuffer->data()), m_resolvedBuffer->size());
}
}

gsl::span<const std::byte> ResolvedResponseBuffer() const
{
if (m_resolvedBuffer)
{
return {m_resolvedBuffer->data(), m_resolvedBuffer->size()};
}
return {};
}

void SetRequestBody(std::string requestBody) {
m_requestBody = requestBody;
}
Expand Down Expand Up @@ -113,6 +262,20 @@ namespace UrlLib
std::transform(s.cbegin(), s.cend(), s.begin(), [](auto c) { return static_cast<decltype(c)>(std::tolower(c)); });
}

// Returns the lower-cased scheme of `url` (the substring before the first ':'), or "" if
// the URL has no scheme.
static std::string SchemeOf(const std::string& url)
{
const auto pos = url.find(':');
if (pos == std::string::npos)
{
return {};
}
std::string scheme = url.substr(0, pos);
ToLower(scheme);
return scheme;
}

// Canonical HTTP reason phrases, used as a fallback when the transport does not carry
// a reason phrase on the wire (HTTP/2+ status lines omit it, and some platform HTTP
// stacks don't surface it). Returns "" for codes not in the table.
Expand Down Expand Up @@ -213,6 +376,10 @@ namespace UrlLib
m_errorCode = 0;
m_errorSymbol.clear();
m_errorString.clear();
m_usingSchemeResolver = false;
m_pendingResolver = nullptr;
m_pendingResolverUrl.clear();
m_resolvedBuffer.reset();
}

arcana::cancellation_source m_cancellationSource{};
Expand All @@ -228,5 +395,28 @@ namespace UrlLib
std::unordered_map<std::string, std::string> m_headers;
std::string m_requestBody{};
std::unordered_map<std::string, std::string> m_requestHeaders;

// Custom-scheme (e.g. blob:) resolution state. Populated by BeginSchemeResolution() /
// ResolveScheme(); inert for ordinary transport requests.
bool m_usingSchemeResolver{false};
UrlSchemeResolver m_pendingResolver{};
std::string m_pendingResolverUrl{};
std::shared_ptr<const std::vector<std::byte>> m_resolvedBuffer{};

private:
// Process-global registry of scheme resolvers, keyed by lower-cased scheme (no trailing
// ':'). Guarded by a mutex since RegisterSchemeResolver and request handling can run on
// different threads.
struct ResolverRegistry
{
std::mutex mutex;
std::unordered_map<std::string, UrlSchemeResolver> resolvers;
};

static ResolverRegistry& Registry()
{
static ResolverRegistry registry;
return registry;
}
};
}
31 changes: 31 additions & 0 deletions Source/UrlRequest_Shared.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,26 @@ namespace UrlLib

void UrlRequest::Open(UrlMethod method, const std::string& url)
{
// Divert URLs whose scheme has a registered resolver (e.g. blob:) away from the platform
// transport; the resolver supplies the response in SendAsync().
if (m_impl->BeginSchemeResolution(url))
{
return;
}

m_impl->Open(method, url);
}

void UrlRequest::RegisterSchemeResolver(std::string scheme, UrlSchemeResolver resolver)
{
Impl::RegisterSchemeResolver(std::move(scheme), std::move(resolver));
}

void UrlRequest::UnregisterSchemeResolver(std::string scheme)
{
Impl::UnregisterSchemeResolver(std::move(scheme));
}

UrlResponseType UrlRequest::ResponseType() const
{
return m_impl->ResponseType();
Expand Down Expand Up @@ -59,6 +76,15 @@ namespace UrlLib

arcana::task<void, std::exception_ptr> UrlRequest::SendAsync()
{
// Registered-scheme requests (e.g. blob:) are served synchronously from the resolver; the
// resolution is deferred to here (rather than Open) so a blob: URL revoked between open()
// and send() is honored.
if (m_impl->IsSchemeResolution())
{
m_impl->ResolveScheme();
return arcana::task_from_result<std::exception_ptr>();
}

return m_impl->SendAsync();
}

Expand Down Expand Up @@ -99,6 +125,11 @@ namespace UrlLib

gsl::span<const std::byte> UrlRequest::ResponseBuffer() const
{
if (m_impl->IsSchemeResolution())
{
return m_impl->ResolvedResponseBuffer();
}

return m_impl->ResponseBuffer();
}
}
2 changes: 1 addition & 1 deletion Tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ FetchContent_MakeAvailable(googletest)
set_property(TARGET gtest PROPERTY FOLDER Dependencies)
set_property(TARGET gtest_main PROPERTY FOLDER Dependencies)

add_executable(UrlLibTests UrlRequestErrorReporting.cpp)
add_executable(UrlLibTests UrlRequestErrorReporting.cpp SchemeResolver.cpp)

target_link_libraries(UrlLibTests
PRIVATE UrlLib
Expand Down
Loading