Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
25 changes: 25 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,14 @@ namespace UrlLib

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

// Registers (or, with a null resolver, clears) 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.
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.

UrlResponseType ResponseType() const;

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

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

namespace UrlLib
{
Expand All @@ -21,6 +24,106 @@ 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.
static void RegisterSchemeResolver(std::string scheme, UrlSchemeResolver resolver)
{
ToLower(scheme);
auto& registry = Registry();
const std::lock_guard<std::mutex> lock{registry.mutex};
if (resolver)
{
registry.resolvers[std::move(scheme)] = std::move(resolver);
}
else
{
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) leaves the status at 0 (None) and
// records a transport-style error, mirroring how a genuine network failure surfaces.
void ResolveScheme()
{
if (!m_pendingResolver)
{
return;
}

const UrlSchemeResolverResult result = m_pendingResolver(m_pendingResolverUrl);
m_responseUrl = m_pendingResolverUrl;

if (!result.handled)
{
SetError("urllib", "SchemeResolverNotFound", 0, "no live entry for '" + m_pendingResolverUrl + "'");
return;
}
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
Outdated

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 +216,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 +330,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 +349,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;
}
};
}
26 changes: 26 additions & 0 deletions Source/UrlRequest_Shared.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,21 @@ 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));
}

UrlResponseType UrlRequest::ResponseType() const
{
return m_impl->ResponseType();
Expand Down Expand Up @@ -59,6 +71,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 +120,11 @@ namespace UrlLib

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

return m_impl->ResponseBuffer();
}
}