diff --git a/CMakeLists.txt b/CMakeLists.txt index f61c165..2f4afdb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,8 @@ add_library( src/dullahan_callback_manager.cpp src/dullahan_callback_manager.h src/dullahan_debug.h + src/dullahan_embed_scheme.cpp + src/dullahan_embed_scheme.h src/dullahan_impl.cpp src/dullahan_impl.h src/dullahan_version.h diff --git a/examples/opengl-example/src/opengl-example.cpp b/examples/opengl-example/src/opengl-example.cpp index cfac92a..bcd0f80 100644 --- a/examples/opengl-example/src/opengl-example.cpp +++ b/examples/opengl-example/src/opengl-example.cpp @@ -329,7 +329,7 @@ bool openglExample::init() mDullahan->setOnPageChangedCallback(std::bind(&openglExample::onPageChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); mDullahan->setOnRequestExitCallback(std::bind(&openglExample::onRequestExitCallback, this)); - mDullahan->setOnJStoCPPMsgCallback(std::bind(&openglExample::onJStoCPPMsgCallback, this, std::placeholders::_1, std::placeholders::_2)); + mDullahan->setOnJStoCPPMsgCallback(std::bind(&openglExample::onJStoCPPMsgCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); mDullahan->navigate(mHomeUrl); } @@ -482,9 +482,9 @@ void openglExample::onRequestExitCallback() glfwSetWindowShouldClose(mWindow, GLFW_TRUE); } -std::string openglExample::onJStoCPPMsgCallback(const std::string id, const std::string msg) +std::string openglExample::onJStoCPPMsgCallback(const std::string id, const std::string msg, const std::string frame_url) { - std::cout << "Received message with ID: " << id << " from JavaScript: " << msg << std::endl; + std::cout << "Received message with ID: " << id << " from JavaScript: " << msg << " (origin: " << frame_url << ")" << std::endl; return "Message received loud and clear by C++!"; } diff --git a/examples/opengl-example/src/opengl-example.h b/examples/opengl-example/src/opengl-example.h index d234f07..9129109 100644 --- a/examples/opengl-example/src/opengl-example.h +++ b/examples/opengl-example/src/opengl-example.h @@ -69,7 +69,7 @@ class openglExample // callbacks void onPageChanged(const unsigned char* pixels, int x, int y, const int width, const int height); void onRequestExitCallback(); - std::string onJStoCPPMsgCallback(const std::string id, const std::string msg); + std::string onJStoCPPMsgCallback(const std::string id, const std::string msg, const std::string frame_url); private: GLFWwindow* mWindow; diff --git a/src/dullahan.cpp b/src/dullahan.cpp index 5b9dbe4..43a1f67 100644 --- a/src/dullahan.cpp +++ b/src/dullahan.cpp @@ -344,6 +344,26 @@ std::vector& dullahan::getCustomSchemes() return mImpl->getCustomSchemes(); } +void dullahan::setEmbedSchemeRoot(const std::string& root_dir) +{ + mImpl->setEmbedSchemeRoot(root_dir); +} + +const std::string& dullahan::getEmbedSchemeRoot() +{ + return mImpl->getEmbedSchemeRoot(); +} + +void dullahan::setEmbedRegistry(const std::vector& allowed_paths) +{ + mImpl->setEmbedRegistry(allowed_paths); +} + +const std::vector& dullahan::getEmbedRegistry() +{ + return mImpl->getEmbedRegistry(); +} + void dullahan::setOnAddressChangeCallback(std::function callback) { mImpl->getCallbackManager()->setOnAddressChangeCallback(callback); @@ -447,7 +467,7 @@ void dullahan::setOnJSBeforeUnloadCallback(std::function callback) mImpl->getCallbackManager()->setOnJSBeforeUnloadCallback(callback); } -void dullahan::setOnJStoCPPMsgCallback(std::function callback) +void dullahan::setOnJStoCPPMsgCallback(std::function callback) { mImpl->getCallbackManager()->setOnJStoCPPMsgCallback(callback); } diff --git a/src/dullahan.h b/src/dullahan.h index 06911e5..40960b8 100644 --- a/src/dullahan.h +++ b/src/dullahan.h @@ -330,6 +330,14 @@ class dullahan void setCustomSchemes(std::vector custom_schemes); std::vector& getCustomSchemes(); + // The embed:// scheme is intended to host trusted local resources bundled by the host application. + // Resources are mapped as: embed:/// => // + // Only paths that appear in the registry (relative to , using forward slashes) are served. + void setEmbedSchemeRoot(const std::string& root_dir); + const std::string& getEmbedSchemeRoot(); + void setEmbedRegistry(const std::vector& allowed_paths); + const std::vector& getEmbedRegistry(); + //////////// callback setters //////////// // URL changes - e.g. redirect void setOnAddressChangeCallback(std::function callback); @@ -397,8 +405,10 @@ class dullahan // JS before unload callback (alert) void setOnJSBeforeUnloadCallback(std::function callback); - // Message from JS to CPP - void setOnJStoCPPMsgCallback(std::function callback); + // Message from JS to CPP. + // The frame_url argument identifies the origin of the calling frame, + // allowing consumers to filter messages by origin if needed. + void setOnJStoCPPMsgCallback(std::function callback); private: std::unique_ptr mImpl; diff --git a/src/dullahan_browser_client.cpp b/src/dullahan_browser_client.cpp index f3054e6..a7720a6 100644 --- a/src/dullahan_browser_client.cpp +++ b/src/dullahan_browser_client.cpp @@ -32,6 +32,7 @@ #include "dullahan_render_handler.h" #include "dullahan_browser_client.h" #include "dullahan_callback_manager.h" +#include "dullahan_embed_scheme.h" #include "dullahan_impl.h" @@ -72,8 +73,16 @@ bool dullahan_browser_client::OnProcessMessageReceived(CefRefPtr bro CefRefPtr args = message->GetArgumentList(); if (args) { + // Args: [0] id, [1] json, [2] frame_url (added for embed:// origin checks). + std::string frame_url; + if (args->GetSize() >= 3 && args->GetType(2) == VTYPE_STRING) + { + frame_url = args->GetString(2).ToString(); + } //std::cout << ">>> Received JSONtoCPP_MSG from render process: " << args->GetString(0).ToString() << std::endl; - mParent->getCallbackManager()->onJStoCPPMsgCallback(args->GetString(0).ToString(), args->GetString(1).ToString()); + mParent->getCallbackManager()->onJStoCPPMsgCallback(args->GetString(0).ToString(), + args->GetString(1).ToString(), + frame_url); } // Indicate we processed this message and it should not be sent to other handlers @@ -300,10 +309,26 @@ bool dullahan_browser_client::OnBeforeBrowse(CefRefPtr browser, std::string url = request->GetURL(); // for comparison, use lowercase - std::transform(url.begin(), url.end(), url.begin(), [](char c) + url = ascii_tolower(url); + + // Block any user-initiated navigation or redirect into embed://. + // Only the host process may load embed:// URLs directly. + static const std::string embed_scheme(kEmbedSchemePrefix); + if (url.compare(0, embed_scheme.size(), embed_scheme) == 0) + { + if (user_gesture || isRedirect) + { + return true; // cancel navigation + } + } + + // Track whether the main frame is currently loading embed:// content so + // OnBeforeResourceLoad has a reliable trust-context signal that doesn't + // depend on the racy frame URL update visible on the IO thread. + if (frame && frame->IsMain()) { - return static_cast(tolower(c)); - }); + mEmbedScoped = (url.compare(0, embed_scheme.size(), embed_scheme) == 0); + } std::vector::iterator iter = mParent->getCustomSchemes().begin(); while (iter != mParent->getCustomSchemes().end()) @@ -355,6 +380,78 @@ bool dullahan_browser_client::GetAuthCredentials(CefRefPtr browser, } } +// CefRequestHandler override - route resource requests through this same object. +CefRefPtr dullahan_browser_client::GetResourceRequestHandler( + CefRefPtr /*browser*/, + CefRefPtr /*frame*/, + CefRefPtr /*request*/, + bool /*is_navigation*/, + bool /*is_download*/, + const CefString& /*request_initiator*/, + bool& /*disable_default_handling*/) +{ + return this; +} + +// CefResourceRequestHandler override - recursive inheritance for embed://. +// +// A browser whose top-level document is an embed:// page must not make sub- +// resource requests to any other origin. +// Conversely, a browser NOT running embed:// content must not fetch embed:// +// resources. +// Top-level main-frame navigations are excluded here, because those are gated by +// OnBeforeBrowse, so the initial embed:// document load is not blocked. +cef_return_value_t dullahan_browser_client::OnBeforeResourceLoad( + CefRefPtr /*browser*/, + CefRefPtr /*frame*/, + CefRefPtr request, + CefRefPtr /*callback*/) +{ + // Skip the main-frame document itself; OnBeforeBrowse governs it. + if (request->GetResourceType() == RT_MAIN_FRAME) + { + return RV_CONTINUE; + } + + static const std::string embed_prefix(kEmbedSchemePrefix); + static const std::string data_prefix("data:"); + + auto starts_with = [](const std::string& s, const std::string& prefix) + { + if (s.size() < prefix.size()) return false; + for (size_t i = 0; i < prefix.size(); ++i) + { + if (ascii_tolower(s[i]) != prefix[i]) return false; + } + return true; + }; + + // Trust context is set by OnBeforeBrowse when the main frame navigates. + // OnBeforeBrowse is guaranteed to fire before any sub-resource of the new + // page, so this is not subject to the IO-thread staleness of frame URLs. + const bool embed_context = mEmbedScoped.load(std::memory_order_relaxed); + + std::string req_url = request->GetURL(); + const bool req_is_embed = starts_with(req_url, embed_prefix); + const bool req_is_data = starts_with(req_url, data_prefix); + + if (embed_context) + { + // embed:// documents may only load embed:// or data: resources. + if (!req_is_embed && !req_is_data) + { + return RV_CANCEL; + } + } + else if (req_is_embed) + { + // Non-embed documents may not touch embed:// resources at all. + return RV_CANCEL; + } + + return RV_CONTINUE; +} + // CefDownloadHandler overrides bool dullahan_browser_client::OnBeforeDownload(CefRefPtr browser, CefRefPtr download_item, diff --git a/src/dullahan_browser_client.h b/src/dullahan_browser_client.h index 64e97a2..a692f99 100644 --- a/src/dullahan_browser_client.h +++ b/src/dullahan_browser_client.h @@ -27,6 +27,7 @@ #ifndef _DULLAHAN_BROWSER_CLIENT #define _DULLAHAN_BROWSER_CLIENT +#include #include #include "cef_client.h" @@ -40,6 +41,7 @@ class dullahan_browser_client : public CefDisplayHandler, public CefLoadHandler, public CefRequestHandler, + public CefResourceRequestHandler, public CefDownloadHandler, public CefDialogHandler, public CefJSDialogHandler @@ -126,6 +128,16 @@ class dullahan_browser_client : const CefString& host, int port, const CefString& realm, const CefString& scheme, CefRefPtr callback) override; + // CefRequestHandler -> CefResourceRequestHandler routing + CefRefPtr GetResourceRequestHandler(CefRefPtr browser, CefRefPtr frame, + CefRefPtr request, bool is_navigation, + bool is_download, const CefString& request_initiator, + bool& disable_default_handling) override; + + // CefResourceRequestHandler override - enforces recursive inheritance for the embed:// scheme + cef_return_value_t OnBeforeResourceLoad(CefRefPtr browser, CefRefPtr frame, + CefRefPtr request, CefRefPtr callback) override; + // CefDownloadHandler overrides CefRefPtr GetDownloadHandler() override { @@ -177,6 +189,8 @@ class dullahan_browser_client : typedef std::list> BrowserList; BrowserList mBrowserList; + std::atomic mEmbedScoped{false}; + public: IMPLEMENT_REFCOUNTING(dullahan_browser_client); }; diff --git a/src/dullahan_callback_manager.cpp b/src/dullahan_callback_manager.cpp index d350fad..395bbaa 100644 --- a/src/dullahan_callback_manager.cpp +++ b/src/dullahan_callback_manager.cpp @@ -293,16 +293,16 @@ bool dullahan_callback_manager::onJSBeforeUnloadCallback() } void dullahan_callback_manager::setOnJStoCPPMsgCallback( - std::function callback) + std::function callback) { mOnJStoCPPMsgCallbackFunc = callback; } -std::string dullahan_callback_manager::onJStoCPPMsgCallback(const std::string id, const std::string msg) +std::string dullahan_callback_manager::onJStoCPPMsgCallback(const std::string id, const std::string msg, const std::string frame_url) { if (mOnJStoCPPMsgCallbackFunc) { - return mOnJStoCPPMsgCallbackFunc(id, msg); + return mOnJStoCPPMsgCallbackFunc(id, msg, frame_url); } return std::string(); diff --git a/src/dullahan_callback_manager.h b/src/dullahan_callback_manager.h index 86a098f..cac7952 100644 --- a/src/dullahan_callback_manager.h +++ b/src/dullahan_callback_manager.h @@ -91,8 +91,8 @@ class dullahan_callback_manager void setOnJSBeforeUnloadCallback(std::function callback); bool onJSBeforeUnloadCallback(); - void setOnJStoCPPMsgCallback(std::function callback); - std::string onJStoCPPMsgCallback(const std::string id, const std::string msg); + void setOnJStoCPPMsgCallback(std::function callback); + std::string onJStoCPPMsgCallback(const std::string id, const std::string msg, const std::string frame_url); private: std::function mOnAddressChangeCallbackFunc; @@ -114,7 +114,7 @@ class dullahan_callback_manager std::function(dullahan::EFileDialogType, const std::string, const std::string, const std::string, bool&)> mOnFileDialogCallbackFunc; std::function mOnJSDialogCallbackFunc; std::function mOnJSBeforeUnloadCallbackFunc; - std::function mOnJStoCPPMsgCallbackFunc; + std::function mOnJStoCPPMsgCallbackFunc; }; #endif //_DULLAHAN_CALLBACK_MANAGER diff --git a/src/dullahan_embed_scheme.cpp b/src/dullahan_embed_scheme.cpp new file mode 100644 index 0000000..2c39f78 --- /dev/null +++ b/src/dullahan_embed_scheme.cpp @@ -0,0 +1,225 @@ +/* + @brief Dullahan - a headless browser rendering engine + based around the Chromium Embedded Framework + + Copyright (c) 2026, Linden Research, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +// embed:// custom scheme handler implementation + +#define NOMINMAX + +#include "dullahan_embed_scheme.h" +#include "dullahan_impl.h" + +#include "cef_parser.h" +#include "cef_stream.h" +#include "wrapper/cef_stream_resource_handler.h" + +#include +#include +#include +#include + +namespace +{ + // MIME map for the file types the embed scheme uses. + std::string mime_for_path(const std::string& path) + { + auto dot = path.find_last_of('.'); + std::string ext = (dot == std::string::npos) ? std::string() : path.substr(dot + 1); + ext = ascii_tolower(ext); + + if (ext == "html" || ext == "htm") return "text/html"; + if (ext == "js") return "application/javascript"; + if (ext == "css") return "text/css"; + if (ext == "json") return "application/json"; + if (ext == "svg") return "image/svg+xml"; + if (ext == "png") return "image/png"; + if (ext == "jpg" || ext == "jpeg") return "image/jpeg"; + if (ext == "gif") return "image/gif"; + return "application/octet-stream"; + } + + // Strip query string / fragment and any leading '/'; also normalise backslashes. + std::string canonical_relpath(const std::string& host, const std::string& path) + { + std::string p = path; + auto q = p.find_first_of("?#"); + if (q != std::string::npos) p.erase(q); + while (!p.empty() && p.front() == '/') p.erase(0, 1); + std::replace(p.begin(), p.end(), '\\', '/'); + std::string relpath = host; + if (!relpath.empty() && !p.empty()) relpath.push_back('/'); + relpath += p; + return relpath; + } + + // Reject any relpath containing "..", empty segments, or non-relative form. + bool relpath_is_safe(const std::string& relpath) + { + if (relpath.empty()) return false; + if (relpath.front() == '/') return false; + if (relpath.find("..") != std::string::npos) return false; + return true; + } + + // In-memory CefReadHandler backed by a std::string. Used for the short + // canned bodies of error responses so we can serve them through the same + // CefStreamResourceHandler that we use for real file bodies. + class string_read_handler : public CefReadHandler + { + public: + explicit string_read_handler(std::string body) : + mData(std::move(body)), + mOffset(0) + { + } + + size_t Read(void* ptr, size_t elem_size, size_t n) override + { + size_t bytes_wanted = elem_size * n; + size_t remaining = mData.size() - mOffset; + size_t to_copy = std::min(bytes_wanted, remaining); + if (to_copy > 0) + { + std::memcpy(ptr, mData.data() + mOffset, to_copy); + mOffset += to_copy; + } + return elem_size ? (to_copy / elem_size) : 0; + } + + int Seek(int64_t /*offset*/, int /*whence*/) override { return -1; } + int64_t Tell() override { return static_cast(mOffset); } + int Eof() override { return mOffset >= mData.size() ? 1 : 0; } + bool MayBlock() override { return false; } + + private: + std::string mData; + size_t mOffset; + + IMPLEMENT_REFCOUNTING(string_read_handler); + DISALLOW_COPY_AND_ASSIGN(string_read_handler); + }; + + CefResponse::HeaderMap default_headers() + { + CefResponse::HeaderMap headers; + headers.insert(std::make_pair("Cache-Control", "no-store")); + headers.insert(std::make_pair("Access-Control-Allow-Origin", "*")); + return headers; + } + + CefRefPtr make_error(int status, const std::string& reason) + { + std::string body = "embed:// " + reason; + CefRefPtr stream = + CefStreamReader::CreateForHandler(new string_read_handler(std::move(body))); + return new CefStreamResourceHandler(status, reason, "text/plain", default_headers(), stream); + } +} + +dullahan_embed_scheme_factory::dullahan_embed_scheme_factory(dullahan_impl* parent) : + mParent(parent) +{ +} + +CefRefPtr dullahan_embed_scheme_factory::Create( + CefRefPtr /*browser*/, + CefRefPtr /*frame*/, + const CefString& /*scheme_name*/, + CefRefPtr request) +{ + if (!mParent) + { + return make_error(500, "no host"); + } + + const std::string& root = mParent->getEmbedSchemeRoot(); + const std::vector& registry = mParent->getEmbedRegistry(); + if (root.empty()) + { + return make_error(500, "embed root not configured"); + } + + // Parse the URL into pieces. + CefURLParts parts; + if (!CefParseURL(request->GetURL(), parts)) + { + return make_error(400, "bad url"); + } + std::string host = CefString(&parts.host).ToString(); + std::string path = CefString(&parts.path).ToString(); + host = ascii_tolower(host); + + std::string relpath = canonical_relpath(host, path); + if (!relpath_is_safe(relpath)) + { + return make_error(400, "invalid path"); + } + + // Registry lookup, relative paths use forward slashes. An entry ending + // in '/' is treated as a directory prefix - any relpath under that folder + // is allowed. Any other entry must match relpath exactly. + bool matched = false; + for (const std::string& entry : registry) + { + if (entry.empty()) continue; + if (entry.back() == '/') + { + if (relpath.compare(0, entry.size(), entry) == 0) + { + matched = true; + break; + } + } + else if (relpath == entry) + { + matched = true; + break; + } + } + if (!matched) + { + return make_error(404, "not in registry"); + } + + // Compose on-disk path. + std::string disk_path = root; + if (!disk_path.empty() && + disk_path.back() != '/' && + disk_path.back() != '\\') + { + disk_path.push_back('/'); + } + disk_path += relpath; + + // Delegate file I/O to CEF. CefStreamReader::CreateForFile takes a + // CefString (UTF-16 internally), so any UTF-8 path works correctly on + // Windows - unlike std::ifstream, which silently fails on UTF-8 paths. + CefRefPtr stream = CefStreamReader::CreateForFile(disk_path); + if (!stream) + { + return make_error(404, "file not found"); + } + + return new CefStreamResourceHandler(200, "OK", mime_for_path(relpath), default_headers(), stream); +} diff --git a/src/dullahan_embed_scheme.h b/src/dullahan_embed_scheme.h new file mode 100644 index 0000000..7772f07 --- /dev/null +++ b/src/dullahan_embed_scheme.h @@ -0,0 +1,83 @@ +/* + @brief Dullahan - a headless browser rendering engine + based around the Chromium Embedded Framework + + Copyright (c) 2026, Linden Research, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +// embed:// custom scheme handler +// +// Implements the embed:// scheme used to serve trusted local resources +// bundled by the host application. Files are mapped as +// +// embed:/// => // +// +// Only paths listed in the registry (relative to , using forward +// slashes) are served; any other request produces a 404. + +#ifndef _DULLAHAN_EMBED_SCHEME +#define _DULLAHAN_EMBED_SCHEME + +#include "cef_scheme.h" + +#include +#include +#include + +// Canonical scheme name and URL prefix for the embed:// custom scheme. +#define DULLAHAN_EMBED_SCHEME_NAME "embed" +constexpr const char* kEmbedScheme = DULLAHAN_EMBED_SCHEME_NAME; +constexpr const char* kEmbedSchemePrefix = DULLAHAN_EMBED_SCHEME_NAME "://"; + +// ASCII-only, locale-independent lowercase. +// Use for protocol / scheme / prefix comparisons, NOT for user-facing text. +constexpr char ascii_tolower(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c + ('a' - 'A')) : c; +} + +inline std::string ascii_tolower(std::string_view s) +{ + auto view = s | std::views::transform([](char c) { return ascii_tolower(c); }); + return std::string(view.begin(), view.end()); +} + +class dullahan_impl; + +// Factory returned to CEF via CefRegisterSchemeHandlerFactory. +// Holds a raw pointer to the owning dullahan_impl for access to +// the configured filesystem root and allowlist. +class dullahan_embed_scheme_factory : public CefSchemeHandlerFactory +{ + public: + explicit dullahan_embed_scheme_factory(dullahan_impl* parent); + + CefRefPtr Create(CefRefPtr browser, + CefRefPtr frame, + const CefString& scheme_name, + CefRefPtr request) override; + + private: + dullahan_impl* mParent; + IMPLEMENT_REFCOUNTING(dullahan_embed_scheme_factory); +}; + +#endif // _DULLAHAN_EMBED_SCHEME diff --git a/src/dullahan_impl.cpp b/src/dullahan_impl.cpp index a5e0123..80f7195 100644 --- a/src/dullahan_impl.cpp +++ b/src/dullahan_impl.cpp @@ -33,9 +33,11 @@ #include "dullahan_render_handler.h" #include "dullahan_browser_client.h" #include "dullahan_callback_manager.h" +#include "dullahan_embed_scheme.h" #include "include/cef_request_context.h" #include "include/cef_request_context_handler.h" +#include "include/cef_scheme.h" #include "include/cef_waitable_event.h" #include "include/base/cef_logging.h" @@ -1093,6 +1095,47 @@ std::vector& dullahan_impl::getCustomSchemes() return mCustomSchemes; } +void dullahan_impl::setEmbedSchemeRoot(const std::string& root_dir) +{ + mEmbedSchemeRoot = root_dir; +} + +const std::string& dullahan_impl::getEmbedSchemeRoot() +{ + return mEmbedSchemeRoot; +} + +void dullahan_impl::setEmbedRegistry(const std::vector& allowed_paths) +{ + mEmbedRegistry = allowed_paths; +} + +const std::vector& dullahan_impl::getEmbedRegistry() +{ + return mEmbedRegistry; +} + +// CefApp override. Called in every process (browser + all sub-processes) early during initialisation. +// Registers embed:// with both CEF's network stack (so requests are routed through our scheme handler factory) +// and Chromium's renderer (so embed:// documents are treated as a proper secure origin). +void dullahan_impl::OnRegisterCustomSchemes(CefRawPtr registrar) +{ + registrar->AddCustomScheme(kEmbedScheme, + CEF_SCHEME_OPTION_STANDARD | + CEF_SCHEME_OPTION_SECURE | + CEF_SCHEME_OPTION_CORS_ENABLED | + CEF_SCHEME_OPTION_FETCH_ENABLED | + CEF_SCHEME_OPTION_LOCAL); +} + +// CefBrowserProcessHandler override. Runs in the browser process after the +// CEF context is initialised. Register the embed:// scheme handler factory +// here so the factory pointer to dullahan_impl is guaranteed to be valid. +void dullahan_impl::OnContextInitialized() +{ + CefRegisterSchemeHandlerFactory(kEmbedScheme, CefString(), new dullahan_embed_scheme_factory(this)); +} + CefRefPtr dullahan_impl::getBrowser() { return mBrowser; diff --git a/src/dullahan_impl.h b/src/dullahan_impl.h index a72a7fb..4a3b735 100644 --- a/src/dullahan_impl.h +++ b/src/dullahan_impl.h @@ -47,6 +47,7 @@ class CefRequestContext; class dullahan_impl : public CefApp, + public CefBrowserProcessHandler, public CefPdfPrintCallback { void platormInitWidevine(std::string cachePath); @@ -57,6 +58,11 @@ class dullahan_impl : // CefApp overrides virtual void OnBeforeCommandLineProcessing(const CefString& process_type, CefRefPtr command_line) override; + virtual void OnRegisterCustomSchemes(CefRawPtr registrar) override; + virtual CefRefPtr GetBrowserProcessHandler() override { return this; } + + // CefBrowserProcessHandler overrides + virtual void OnContextInitialized() override; bool init(dullahan::dullahan_settings& user_settings); void shutdown(); @@ -140,6 +146,11 @@ class dullahan_impl : void setCustomSchemes(std::vector custom_schemes); std::vector& getCustomSchemes(); + void setEmbedSchemeRoot(const std::string& root_dir); + const std::string& getEmbedSchemeRoot(); + void setEmbedRegistry(const std::vector& allowed_paths); + const std::vector& getEmbedRegistry(); + CefRefPtr getBrowser(); void setBrowser(CefRefPtr browser); @@ -183,6 +194,8 @@ class dullahan_impl : double mRequestedPageZoom; const int mViewDepth = 4; std::vector mCustomSchemes; + std::string mEmbedSchemeRoot; + std::vector mEmbedRegistry; IMPLEMENT_REFCOUNTING(dullahan_impl); }; diff --git a/src/host/dullahan_host.cpp b/src/host/dullahan_host.cpp index 56bbaf8..3a2abd0 100644 --- a/src/host/dullahan_host.cpp +++ b/src/host/dullahan_host.cpp @@ -27,15 +27,38 @@ #define NOMINMAX #include "cef_app.h" +#include "cef_scheme.h" + +#include "../dullahan_embed_scheme.h" + +#include + +namespace +{ + bool url_is_embed(const std::string& url) + { + static const std::string prefix(kEmbedSchemePrefix); + if (url.size() < prefix.size()) return false; + // Scheme comparison is case-insensitive. + for (size_t i = 0; i < prefix.size(); ++i) + { + if (ascii_tolower(url[i]) != prefix[i]) return false; + } + return true; + } +} // Shared by Windows and Mac sub-process entry points class JSONtoCPPHandler : public CefV8Handler { public: - JSONtoCPPHandler(CefRefPtr browser) : - // Save the browser reference from OnContextCreated() - // for later use in IPC communication - mBrowser(browser) + JSONtoCPPHandler(CefRefPtr browser, CefRefPtr frame) : + // Save the browser + frame references from OnContextCreated() + // for later use in IPC communication. The frame reference is used + // so the browser process can identify which document originated + // each message (for embed:// origin checks). + mBrowser(browser), + mFrame(frame) { } @@ -76,6 +99,8 @@ class JSONtoCPPHandler : public CefV8Handler CefRefPtr args = msg->GetArgumentList(); args->SetString(0, id); args->SetString(1, json); + std::string frame_url = mFrame ? std::string(mFrame->GetURL()) : std::string(); + args->SetString(2, frame_url); mBrowser->GetMainFrame()->SendProcessMessage(PID_BROWSER, msg); // Acknowledge receipt of the JSON string @@ -83,6 +108,7 @@ class JSONtoCPPHandler : public CefV8Handler } CefRefPtr mBrowser; + CefRefPtr mFrame; IMPLEMENT_REFCOUNTING(JSONtoCPPHandler); }; @@ -95,12 +121,32 @@ class MyApp : public CefApp, return this; } + // CefApp override. Runs in every process. Must register the embed:// + // scheme with the same flags as the browser process so Chromium's renderer + // treats embed:// documents as a proper (secure) origin. + void OnRegisterCustomSchemes(CefRawPtr registrar) override + { + registrar->AddCustomScheme(kEmbedScheme, + CEF_SCHEME_OPTION_STANDARD | + CEF_SCHEME_OPTION_SECURE | + CEF_SCHEME_OPTION_CORS_ENABLED | + CEF_SCHEME_OPTION_FETCH_ENABLED | + CEF_SCHEME_OPTION_LOCAL); + } + void OnContextCreated(CefRefPtr browser, CefRefPtr frame, CefRefPtr context) override { + // Only inject the window.JSONtoCPP bridge on trusted embed:// frames. + std::string url = frame ? std::string(frame->GetURL()) : std::string(); + if (!url_is_embed(url)) + { + return; + } + CefRefPtr global = context->GetGlobal(); - CefRefPtr handler = new JSONtoCPPHandler(browser); + CefRefPtr handler = new JSONtoCPPHandler(browser, frame); CefRefPtr func = CefV8Value::CreateFunction("JSONtoCPP", handler); global->SetValue("JSONtoCPP", func, V8_PROPERTY_ATTRIBUTE_NONE); @@ -109,6 +155,7 @@ class MyApp : public CefApp, CefRefPtr args = msg->GetArgumentList(); args->SetString(0, "INFO"); args->SetString(1, "Hello from the OnContextCreated in the sub-process!"); + args->SetString(2, url); browser->GetMainFrame()->SendProcessMessage(PID_BROWSER, msg); } @@ -123,7 +170,8 @@ NO_STACK_PROTECTOR int main(int argc, char* argv[]) { CefMainArgs main_args(argc, argv); - return CefExecuteProcess(main_args, nullptr, nullptr); + const CefRefPtr app = new MyApp(); + return CefExecuteProcess(main_args, app, nullptr); } #endif #ifdef WIN32 @@ -187,7 +235,7 @@ int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, CefMainArgs args(GetModuleHandle(nullptr)); - // Important: Create the CefApp instance in the main function to ensure it is available + // Create the CefApp instance in the main function to ensure it is available // to CefExecuteProcess() when the sub-process is launched. Creating the CefApp instance const CefRefPtr app = new MyApp();