diff --git a/src/client/launcher/html/doc_host_ui_handler.cpp b/src/client/launcher/html/doc_host_ui_handler.cpp new file mode 100644 index 00000000..2ec43a22 --- /dev/null +++ b/src/client/launcher/html/doc_host_ui_handler.cpp @@ -0,0 +1,115 @@ +#include +#include "html_frame.hpp" + +doc_host_ui_handler::doc_host_ui_handler(html_frame* frame): frame_(frame) +{ +} + +HRESULT doc_host_ui_handler::QueryInterface(REFIID riid, LPVOID* ppvObj) +{ + auto client_site = this->frame_->get_client_site(); + if (client_site) + { + return client_site->QueryInterface(riid, ppvObj); + } + + return E_NOINTERFACE; +} + +ULONG doc_host_ui_handler::AddRef() +{ + return 1; +} + +ULONG doc_host_ui_handler::Release() +{ + return 1; +} + +HRESULT doc_host_ui_handler::ShowContextMenu(DWORD /*dwID*/, POINT* /*ppt*/, IUnknown* /*pcmdtReserved*/, + IDispatch* /*pdispReserved*/) +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::ShowUI(DWORD /*dwID*/, IOleInPlaceActiveObject* /*pActiveObject*/, + IOleCommandTarget* /*pCommandTarget*/, + IOleInPlaceFrame* /*pFrame*/, IOleInPlaceUIWindow* /*pDoc*/) +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::HideUI() +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::UpdateUI() +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::EnableModeless(BOOL /*fEnable*/) +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::OnDocWindowActivate(BOOL /*fActivate*/) +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::OnFrameWindowActivate(BOOL /*fActivate*/) +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::ResizeBorder(LPCRECT /*prcBorder*/, IOleInPlaceUIWindow* /*pUIWindow*/, + BOOL /*fRameWindow*/) +{ + return S_OK; +} + +HRESULT doc_host_ui_handler::TranslateAcceleratorA(LPMSG /*lpMsg*/, const GUID* pguidCmdGroup, DWORD /*nCmdID*/) +{ + pguidCmdGroup = nullptr; + return S_FALSE; +} + +HRESULT doc_host_ui_handler::GetOptionKeyPath(LPOLESTR* /*pchKey*/, DWORD /*dw*/) +{ + return S_FALSE; +} + +HRESULT doc_host_ui_handler::GetDropTarget(IDropTarget* /*pDropTarget*/, IDropTarget** /*ppDropTarget*/) +{ + return S_FALSE; +} + +HRESULT doc_host_ui_handler::GetExternal(IDispatch** ppDispatch) +{ + *ppDispatch = this->frame_->get_html_dispatch(); + return (*ppDispatch) ? S_OK : S_FALSE; +} + +HRESULT doc_host_ui_handler::FilterDataObject(IDataObject* /*pDO*/, IDataObject** ppDORet) +{ + *ppDORet = nullptr; + return S_FALSE; +} + +HRESULT STDMETHODCALLTYPE doc_host_ui_handler::TranslateUrl(DWORD /*dwTranslate*/, OLECHAR __RPC_FAR* /*pchURLIn*/, + OLECHAR __RPC_FAR* __RPC_FAR* ppchURLOut) +{ + *ppchURLOut = nullptr; + return S_FALSE; +} + +HRESULT doc_host_ui_handler::GetHostInfo(DOCHOSTUIINFO __RPC_FAR * pInfo) +{ + pInfo->cbSize = sizeof(DOCHOSTUIINFO); + pInfo->dwFlags = DOCHOSTUIFLAG_NO3DBORDER | DOCHOSTUIFLAG_DPI_AWARE /*| DOCHOSTUIFLAG_SCROLL_NO*/; + pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT; + + return S_OK; +} diff --git a/src/client/launcher/html/doc_host_ui_handler.hpp b/src/client/launcher/html/doc_host_ui_handler.hpp new file mode 100644 index 00000000..0b538b41 --- /dev/null +++ b/src/client/launcher/html/doc_host_ui_handler.hpp @@ -0,0 +1,47 @@ +#pragma once + +class html_frame; + +class doc_host_ui_handler final : public IDocHostUIHandler +{ +public: + doc_host_ui_handler(html_frame* frame); + virtual ~doc_host_ui_handler() = default; + +private: + html_frame* frame_; + +public: // IDocHostUIHandler interface + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppvObj) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + HRESULT STDMETHODCALLTYPE ShowContextMenu( + DWORD dwID, + POINT __RPC_FAR * ppt, + IUnknown __RPC_FAR * pcmdtReserved, + IDispatch __RPC_FAR * pdispReserved) override; + HRESULT STDMETHODCALLTYPE ShowUI( + DWORD dwID, + IOleInPlaceActiveObject __RPC_FAR * pActiveObject, + IOleCommandTarget __RPC_FAR * pCommandTarget, + IOleInPlaceFrame __RPC_FAR * pFrame, + IOleInPlaceUIWindow __RPC_FAR * pDoc) override; + HRESULT STDMETHODCALLTYPE GetHostInfo(DOCHOSTUIINFO __RPC_FAR * pInfo) override; + HRESULT STDMETHODCALLTYPE HideUI() override; + HRESULT STDMETHODCALLTYPE UpdateUI() override; + HRESULT STDMETHODCALLTYPE EnableModeless(BOOL fEnable) override; + HRESULT STDMETHODCALLTYPE OnDocWindowActivate(BOOL fActivate) override; + HRESULT STDMETHODCALLTYPE OnFrameWindowActivate(BOOL fActivate) override; + HRESULT STDMETHODCALLTYPE ResizeBorder(LPCRECT prcBorder, IOleInPlaceUIWindow __RPC_FAR * pUIWindow, + BOOL fRameWindow) override; + HRESULT STDMETHODCALLTYPE TranslateAccelerator(LPMSG lpMsg, const GUID __RPC_FAR * pguidCmdGroup, DWORD nCmdID) + override; + HRESULT STDMETHODCALLTYPE GetOptionKeyPath(LPOLESTR __RPC_FAR * pchKey, DWORD dw) override; + HRESULT STDMETHODCALLTYPE GetDropTarget(IDropTarget __RPC_FAR * pDropTarget, + IDropTarget __RPC_FAR *__RPC_FAR * ppDropTarget) override; + HRESULT STDMETHODCALLTYPE GetExternal(IDispatch __RPC_FAR *__RPC_FAR * ppDispatch) override; + HRESULT STDMETHODCALLTYPE TranslateUrl(DWORD dwTranslate, OLECHAR __RPC_FAR * pchURLIn, + OLECHAR __RPC_FAR *__RPC_FAR * ppchURLOut) override; + HRESULT STDMETHODCALLTYPE FilterDataObject(IDataObject __RPC_FAR * pDO, IDataObject __RPC_FAR *__RPC_FAR * ppDORet) + override; +}; diff --git a/src/client/launcher/html/html_argument.cpp b/src/client/launcher/html/html_argument.cpp new file mode 100644 index 00000000..181dc6e5 --- /dev/null +++ b/src/client/launcher/html/html_argument.cpp @@ -0,0 +1,48 @@ +#include +#include "html_argument.hpp" + +html_argument::html_argument(VARIANT* val) : value_(val) +{ +} + +bool html_argument::is_empty() const +{ + return this->value_ == nullptr || this->value_->vt == VT_EMPTY; +} + +bool html_argument::is_string() const +{ + if (this->is_empty()) return false; + return this->value_->vt == VT_BSTR; +} + +bool html_argument::is_number() const +{ + if (this->is_empty()) return false; + return this->value_->vt == VT_I4; +} + +bool html_argument::is_bool() const +{ + if (this->is_empty()) return false; + return this->value_->vt == VT_BOOL; +} + +std::string html_argument::get_string() const +{ + if (!this->is_string()) return {}; + std::wstring wide_string(this->value_->bstrVal); + return std::string(wide_string.begin(), wide_string.end()); +} + +int html_argument::get_number() const +{ + if (!this->is_number()) return 0; + return this->value_->intVal; +} + +bool html_argument::get_bool() const +{ + if (!this->is_bool()) return false; + return this->value_->boolVal != FALSE; +} diff --git a/src/client/launcher/html/html_argument.hpp b/src/client/launcher/html/html_argument.hpp new file mode 100644 index 00000000..6e460e29 --- /dev/null +++ b/src/client/launcher/html/html_argument.hpp @@ -0,0 +1,20 @@ +#pragma once + +class html_argument final +{ +public: + html_argument(VARIANT* val); + + bool is_empty() const; + + bool is_string() const; + bool is_number() const; + bool is_bool() const; + + std::string get_string() const; + int get_number() const; + bool get_bool() const; + +private: + VARIANT* value_; +}; diff --git a/src/client/launcher/html/html_dispatch.cpp b/src/client/launcher/html/html_dispatch.cpp new file mode 100644 index 00000000..0cbea58d --- /dev/null +++ b/src/client/launcher/html/html_dispatch.cpp @@ -0,0 +1,61 @@ +#include +#include "html_frame.hpp" + +html_dispatch::html_dispatch(html_frame* frame) : frame_(frame) +{ +} + +HRESULT html_dispatch::QueryInterface(const IID& riid, LPVOID* ppvObj) +{ + if (!memcmp(&riid, &IID_IUnknown, sizeof(GUID)) || + !memcmp(&riid, &IID_IDispatch, sizeof(GUID))) + { + *ppvObj = this; + this->AddRef(); + return S_OK; + } + + *ppvObj = nullptr; + return E_NOINTERFACE; +} + +ULONG html_dispatch::AddRef() +{ + return 1; +} + +ULONG html_dispatch::Release() +{ + return 1; +} + +HRESULT html_dispatch::GetTypeInfoCount(UINT* pctinfo) +{ + return S_FALSE; +} + +HRESULT html_dispatch::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) +{ + return S_FALSE; +} + +HRESULT html_dispatch::GetIDsOfNames(const IID& riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId) +{ + for (unsigned int i = 0; i < cNames; ++i) + { + std::wstring wide_name(rgszNames[i]); + std::string name(wide_name.begin(), wide_name.end()); + + rgDispId[i] = this->frame_->get_callback_id(name); + } + + return S_OK; +} + +HRESULT html_dispatch::Invoke(DISPID dispIdMember, const IID& riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, + VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) +{ + html_frame::callback_params params(pDispParams, pVarResult); + this->frame_->invoke_callback(dispIdMember, ¶ms); + return S_OK; +} diff --git a/src/client/launcher/html/html_dispatch.hpp b/src/client/launcher/html/html_dispatch.hpp new file mode 100644 index 00000000..869af7a8 --- /dev/null +++ b/src/client/launcher/html/html_dispatch.hpp @@ -0,0 +1,24 @@ +#pragma once + +class html_frame; + +class html_dispatch final : public IDispatch +{ +public: + html_dispatch(html_frame* frame); + virtual ~html_dispatch() = default; + +private: + html_frame* frame_; + +public: // IDispatch interface + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID FAR* ppvObj) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT* pctinfo) override; + HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) override; + HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId) + override; + HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, + VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) override; +}; diff --git a/src/client/launcher/html/html_frame.cpp b/src/client/launcher/html/html_frame.cpp new file mode 100644 index 00000000..0c5c992e --- /dev/null +++ b/src/client/launcher/html/html_frame.cpp @@ -0,0 +1,372 @@ +#include +#include "html_frame.hpp" +#include "utils/nt.hpp" +#include "utils/io.hpp" +#include "utils/hook.hpp" + +std::atomic html_frame::frame_count_ = 0; + +namespace +{ + void* original_func{}; + GUID browser_emulation_guid{ 0xac969931, 0x3566, 0x4b50, {0xae, 0x48, 0x71, 0xb9, 0x6a, 0x75, 0xc8, 0x79} }; + + int WINAPI co_internet_feature_value_internal_stub(const GUID* guid, uint32_t* result) + { + const auto res = static_cast(original_func)(guid, result); + + if (IsEqualGUID(*guid, browser_emulation_guid)) + { + *result = 11000; + return 0; + } + + return res; + } + + void patch_cached_browser_emulator(const utils::nt::library& urlmon) + { + std::string data{}; + if (!utils::io::read_file(urlmon.get_path().generic_string(), &data)) + { + return; + } + + const utils::nt::library file_lib(reinterpret_cast(data.data())); + + auto translate_file_offset_to_rva = [&](const size_t file_offset) -> size_t + { + const auto sections = file_lib.get_section_headers(); + for (const auto* section : sections) + { + if (section->PointerToRawData <= file_offset && section->PointerToRawData + section->SizeOfRawData > file_offset) + { + const auto section_va = file_offset - section->PointerToRawData; + return section_va + section->VirtualAddress; + } + } + + return 0; + }; + + const auto guid_pos = data.find(std::string(reinterpret_cast(&browser_emulation_guid), sizeof(browser_emulation_guid))); + if (guid_pos == std::string::npos) + { + return; + } + + const auto guid_rva = translate_file_offset_to_rva(guid_pos); + const auto guid_va = reinterpret_cast(urlmon.get_ptr() + guid_rva); + + if (!IsEqualGUID(*guid_va, browser_emulation_guid)) + { + return; + } + + const size_t unrelocated_guid_va = file_lib.get_optional_header()->ImageBase + guid_rva; + const auto guid_ptr_pos = data.find(std::string(reinterpret_cast(&unrelocated_guid_va), sizeof(unrelocated_guid_va))); + if (guid_ptr_pos == std::string::npos) + { + return; + } + + const auto guid_ptr_rva = translate_file_offset_to_rva(guid_ptr_pos); + *reinterpret_cast(urlmon.get_ptr() + guid_ptr_rva) = guid_va; + } + + void setup_ie_hooks() + { + static const auto _ = [] + { + const auto urlmon = utils::nt::library::load("urlmon.dll"s); + const auto target = urlmon.get_iat_entry("iertutil.dll", MAKEINTRESOURCEA(700)); + + original_func = *target; + utils::hook::set(target, co_internet_feature_value_internal_stub); + + patch_cached_browser_emulator(urlmon); + + return 0; + }(); + (void)_; + } +} + +html_frame::callback_params::callback_params(DISPPARAMS* params, VARIANT* res) : result(res) +{ + for (auto i = params->cArgs; i > 0; --i) + { + auto param = ¶ms->rgvarg[i - 1]; + this->arguments.emplace_back(param); + } +} + +html_frame::html_frame() : in_place_frame_(this), in_place_site_(this), ui_handler_(this), client_site_(this), +html_dispatch_(this) +{ + setup_ie_hooks(); + if (frame_count_++ == 0 && OleInitialize(nullptr) != S_OK) + { + throw std::runtime_error("Unable to initialize the OLE library"); + } + + set_browser_feature("FEATURE_BROWSER_EMULATION", 11000); + set_browser_feature("FEATURE_GPU_RENDERING", 1); +} + +html_frame::~html_frame() +{ + if (--frame_count_ <= 0) + { + frame_count_ = 0; + OleUninitialize(); + } +} + +void html_frame::object_deleter(IUnknown* object) +{ + if (object) + { + object->Release(); + } +} + +HWND html_frame::get_window() const +{ + return this->window_; +} + +std::shared_ptr html_frame::get_browser_object() const +{ + return this->browser_object_; +} + +ole_in_place_frame* html_frame::get_in_place_frame() +{ + return &this->in_place_frame_; +} + +ole_in_place_site* html_frame::get_in_place_site() +{ + return &this->in_place_site_; +} + +doc_host_ui_handler* html_frame::get_ui_handler() +{ + return &this->ui_handler_; +} + +ole_client_site* html_frame::get_client_site() +{ + return &this->client_site_; +} + +html_dispatch* html_frame::get_html_dispatch() +{ + return &this->html_dispatch_; +} + +std::shared_ptr html_frame::get_web_browser() const +{ + if (!this->browser_object_) return {}; + + IWebBrowser2* web_browser = nullptr; + if (FAILED(this->browser_object_->QueryInterface(IID_IWebBrowser2, reinterpret_cast(&web_browser))) + || !web_browser) + return {}; + + return std::shared_ptr(web_browser, object_deleter); +} + +std::shared_ptr html_frame::get_dispatch() const +{ + const auto web_browser = this->get_web_browser(); + if (!web_browser) return {}; + + IDispatch* dispatch = nullptr; + if (FAILED(web_browser->get_Document(&dispatch)) || !dispatch) return {}; + + return std::shared_ptr(dispatch, object_deleter); +} + +std::shared_ptr html_frame::get_document() const +{ + const auto dispatch = this->get_dispatch(); + if (!dispatch) return {}; + + IHTMLDocument2* document = nullptr; + if (FAILED(dispatch->QueryInterface(IID_IHTMLDocument2, reinterpret_cast(&document))) + || !document) + return {}; + + return std::shared_ptr(document, object_deleter); +} + +void html_frame::initialize(const HWND window) +{ + if (this->window_) return; + this->window_ = window; + + this->create_browser(); + this->initialize_browser(); +} + +void html_frame::create_browser() +{ + LPCLASSFACTORY class_factory = nullptr; + if (FAILED( + CoGetClassObject(CLSID_WebBrowser, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, nullptr, IID_IClassFactory, + reinterpret_cast(&class_factory))) || !class_factory) + { + throw std::runtime_error("Unable to get the class factory"); + } + + IOleObject* browser_object = nullptr; + class_factory->CreateInstance(nullptr, IID_IOleObject, reinterpret_cast(&browser_object)); + class_factory->Release(); + + if (!browser_object) + { + throw std::runtime_error("Unable to create browser object"); + } + + this->browser_object_ = std::shared_ptr(browser_object, [](IOleObject* browser_object) + { + if (browser_object) + { + browser_object->Close(OLECLOSE_NOSAVE); + object_deleter(browser_object); + } + }); +} + +void html_frame::initialize_browser() +{ + this->browser_object_->SetClientSite(this->get_client_site()); + this->browser_object_->SetHostNames(L"Hostname", nullptr); + + RECT rect; + GetClientRect(this->get_window(), &rect); + OleSetContainedObject(this->browser_object_.get(), TRUE); + + this->browser_object_->DoVerb(OLEIVERB_SHOW, nullptr, this->get_client_site(), -1, this->get_window(), &rect); + this->resize(rect.right, rect.bottom); +} + +bool html_frame::set_browser_feature(const std::string& feature, DWORD value) +{ + const auto registry_path = R"(SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\)" + feature; + + HKEY key = nullptr; + if (RegCreateKeyA(HKEY_CURRENT_USER, registry_path.data(), &key) == ERROR_SUCCESS) + { + RegCloseKey(key); + } + + key = nullptr; + if (RegOpenKeyExA( + HKEY_CURRENT_USER, registry_path.data(), 0, + KEY_ALL_ACCESS, &key) != ERROR_SUCCESS) + { + return false; // Error :( + } + + const utils::nt::library self; + const auto name = self.get_name(); + + DWORD type{}; + auto is_new = true; + if (RegQueryValueExA(key, name.data(), nullptr, &type, nullptr, nullptr) == ERROR_SUCCESS) + { + is_new = false; + } + + RegSetValueExA(key, name.data(), 0, REG_DWORD, reinterpret_cast(&value), sizeof(value)); + RegCloseKey(key); + + return is_new; +} + +void html_frame::resize(const DWORD width, const DWORD height) const +{ + auto web_browser = this->get_web_browser(); + if (web_browser) + { + web_browser->put_Left(0); + web_browser->put_Top(0); + web_browser->put_Width(width); + web_browser->put_Height(height); + } +} + +bool html_frame::load_url(const std::string& url) const +{ + auto web_browser = this->get_web_browser(); + if (!web_browser) return false; + + std::wstring wide_url(url.begin(), url.end()); + + VARIANT my_url; + VariantInit(&my_url); + my_url.vt = VT_BSTR; + my_url.bstrVal = SysAllocString(wide_url.data()); + + const auto _ = gsl::finally([&my_url]() { VariantClear(&my_url); }); + if (!my_url.bstrVal) return false; + + return SUCCEEDED(web_browser->Navigate2(&my_url, nullptr, nullptr, nullptr, nullptr)); +} + +bool html_frame::load_html(const std::string& html) const +{ + if (!this->load_url("about:blank")) return false; + + const auto document = this->get_document(); + if (!document) return false; + + SAFEARRAYBOUND safe_array_bound = {1, 0}; + auto safe_array = SafeArrayCreate(VT_VARIANT, 1, &safe_array_bound); + if (!safe_array) return false; + + const auto _ = gsl::finally([safe_array]() { SafeArrayDestroy(safe_array); }); + + VARIANT* variant = nullptr; + if (FAILED(SafeArrayAccessData(safe_array, reinterpret_cast(&variant))) || !variant) return false; + + std::wstring wide_html(html.begin(), html.end()); + + variant->vt = VT_BSTR; + variant->bstrVal = SysAllocString(wide_html.data()); + if (!variant->bstrVal) return false; + + document->write(safe_array); + document->close(); + + return true; +} + +int html_frame::get_callback_id(const std::string& name) +{ + for (auto i = 0u; i < this->callbacks_.size(); ++i) + { + if (this->callbacks_[i].first == name) + { + return i; + } + } + + return -1; +} + +void html_frame::invoke_callback(const int id, callback_params* params) +{ + if (id >= 0 && static_cast(id) < this->callbacks_.size()) + { + this->callbacks_[id].second(params); + } +} + +void html_frame::register_callback(const std::string& name, const std::function& callback) +{ + this->callbacks_.emplace_back(name, callback); +} diff --git a/src/client/launcher/html/html_frame.hpp b/src/client/launcher/html/html_frame.hpp new file mode 100644 index 00000000..04d8108c --- /dev/null +++ b/src/client/launcher/html/html_frame.hpp @@ -0,0 +1,67 @@ +#pragma once +#include "ole_in_place_frame.hpp" +#include "ole_in_place_site.hpp" +#include "doc_host_ui_handler.hpp" +#include "ole_client_site.hpp" +#include "html_dispatch.hpp" +#include "html_argument.hpp" + +class html_frame +{ +public: + class callback_params final + { + public: + callback_params(DISPPARAMS* params, VARIANT* res); + + std::vector arguments; + html_argument result; + }; + + html_frame(); + virtual ~html_frame(); + + void initialize(HWND window); + + void resize(DWORD width, DWORD height) const; + bool load_url(const std::string& url) const; + bool load_html(const std::string& html) const; + + HWND get_window() const; + + std::shared_ptr get_browser_object() const; + std::shared_ptr get_web_browser() const; + std::shared_ptr get_dispatch() const; + std::shared_ptr get_document() const; + + ole_in_place_frame* get_in_place_frame(); + ole_in_place_site* get_in_place_site(); + doc_host_ui_handler* get_ui_handler(); + ole_client_site* get_client_site(); + html_dispatch* get_html_dispatch(); + + int get_callback_id(const std::string& name); + void invoke_callback(int id, callback_params* params); + + void register_callback(const std::string& name, const std::function& callback); + +private: + HWND window_ = nullptr; + std::shared_ptr browser_object_; + + ole_in_place_frame in_place_frame_; + ole_in_place_site in_place_site_; + doc_host_ui_handler ui_handler_; + ole_client_site client_site_; + html_dispatch html_dispatch_; + + std::vector>> callbacks_; + + void create_browser(); + void initialize_browser(); + + static bool set_browser_feature(const std::string& feature, DWORD value); + static void object_deleter(IUnknown* object); + + static std::atomic frame_count_; +}; diff --git a/src/client/launcher/html/html_window.cpp b/src/client/launcher/html/html_window.cpp new file mode 100644 index 00000000..bf01098c --- /dev/null +++ b/src/client/launcher/html/html_window.cpp @@ -0,0 +1,29 @@ +#include +#include "html_window.hpp" + +window* html_window::get_window() +{ + return this; +} + +html_frame* html_window::get_html_frame() +{ + return this; +} + +LRESULT html_window::processor(const UINT message, const WPARAM w_param, const LPARAM l_param) +{ + if (message == WM_SIZE) + { + this->resize(LOWORD(l_param), HIWORD(l_param)); + return 0; + } + + if (message == WM_CREATE) + { + this->initialize(*this); + return 0; + } + + return window::processor(message, w_param, l_param); +} diff --git a/src/client/launcher/html/html_window.hpp b/src/client/launcher/html/html_window.hpp new file mode 100644 index 00000000..5b9f25be --- /dev/null +++ b/src/client/launcher/html/html_window.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "../window.hpp" +#include "html_frame.hpp" + +class html_window final : public window, public html_frame +{ +public: + ~html_window() = default; + + window* get_window(); + html_frame* get_html_frame(); + +private: + LRESULT processor(UINT message, WPARAM w_param, LPARAM l_param) override; +}; diff --git a/src/client/launcher/html/ole_client_site.cpp b/src/client/launcher/html/ole_client_site.cpp new file mode 100644 index 00000000..6e3914ea --- /dev/null +++ b/src/client/launcher/html/ole_client_site.cpp @@ -0,0 +1,77 @@ +#include +#include "html_frame.hpp" + +ole_client_site::ole_client_site(html_frame* frame): frame_(frame) +{ +} + +HRESULT ole_client_site::QueryInterface(REFIID riid, LPVOID* ppvObject) +{ + if (!memcmp(&riid, &IID_IUnknown, sizeof(GUID)) || + !memcmp(&riid, &IID_IOleClientSite, sizeof(GUID))) + { + *ppvObject = this; + this->AddRef(); + return S_OK; + } + + if (!memcmp(&riid, &IID_IOleInPlaceSite, sizeof(GUID))) + { + auto in_place_site = this->frame_->get_in_place_site(); + in_place_site->AddRef(); + *ppvObject = in_place_site; + return S_OK; + } + + if (!memcmp(&riid, &IID_IDocHostUIHandler, sizeof(GUID))) + { + auto ui_handler = this->frame_->get_ui_handler(); + ui_handler->AddRef(); + *ppvObject = ui_handler; + return S_OK; + } + + *ppvObject = nullptr; + return E_NOINTERFACE; +} + +ULONG ole_client_site::AddRef() +{ + return 1; +} + +ULONG ole_client_site::Release() +{ + return 1; +} + +HRESULT ole_client_site::SaveObject() +{ + return E_NOTIMPL; +} + +HRESULT ole_client_site::GetMoniker(DWORD /*dwAssign*/, DWORD /*dwWhichMoniker*/, IMoniker** /*ppmk*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_client_site::GetContainer(LPOLECONTAINER* ppContainer) +{ + *ppContainer = nullptr; + return E_NOINTERFACE; +} + +HRESULT ole_client_site::ShowObject() +{ + return NOERROR; +} + +HRESULT ole_client_site::OnShowWindow(BOOL /*fShow*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_client_site::RequestNewObjectLayout() +{ + return E_NOTIMPL; +} diff --git a/src/client/launcher/html/ole_client_site.hpp b/src/client/launcher/html/ole_client_site.hpp new file mode 100644 index 00000000..d0adc80d --- /dev/null +++ b/src/client/launcher/html/ole_client_site.hpp @@ -0,0 +1,24 @@ +#pragma once + +class html_frame; + +class ole_client_site final : public IOleClientSite +{ +public: + ole_client_site(html_frame* frame); + virtual ~ole_client_site() = default; + +private: + html_frame* frame_; + +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppvObject) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + HRESULT STDMETHODCALLTYPE SaveObject() override; + HRESULT STDMETHODCALLTYPE GetMoniker(DWORD dwAssign, DWORD dwWhichMoniker, IMoniker** ppmk) override; + HRESULT STDMETHODCALLTYPE GetContainer(LPOLECONTAINER FAR* ppContainer) override; + HRESULT STDMETHODCALLTYPE ShowObject() override; + HRESULT STDMETHODCALLTYPE OnShowWindow(BOOL fShow) override; + HRESULT STDMETHODCALLTYPE RequestNewObjectLayout() override; +}; diff --git a/src/client/launcher/html/ole_in_place_frame.cpp b/src/client/launcher/html/ole_in_place_frame.cpp new file mode 100644 index 00000000..172e5db0 --- /dev/null +++ b/src/client/launcher/html/ole_in_place_frame.cpp @@ -0,0 +1,82 @@ +#include +#include "html_frame.hpp" + +ole_in_place_frame::ole_in_place_frame(html_frame* frame): frame_(frame) +{ +} + +HRESULT ole_in_place_frame::QueryInterface(REFIID /*riid*/, LPVOID* /*ppvObj*/) +{ + return E_NOTIMPL; +} + +ULONG ole_in_place_frame::AddRef() +{ + return 1; +} + +ULONG ole_in_place_frame::Release() +{ + return 1; +} + +HRESULT ole_in_place_frame::GetWindow(HWND* lphwnd) +{ + *lphwnd = this->frame_->get_window(); + return S_OK; +} + +HRESULT ole_in_place_frame::ContextSensitiveHelp(BOOL /*fEnterMode*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_frame::GetBorder(LPRECT /*lprectBorder*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_frame::RequestBorderSpace(LPCBORDERWIDTHS /*pborderwidths*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_frame::SetBorderSpace(LPCBORDERWIDTHS /*pborderwidths*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_frame::SetActiveObject(IOleInPlaceActiveObject* /*pActiveObject*/, LPCOLESTR /*pszObjName*/) +{ + return S_OK; +} + +HRESULT ole_in_place_frame::InsertMenus(HMENU /*hmenuShared*/, LPOLEMENUGROUPWIDTHS /*lpMenuWidths*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_frame::SetMenu(HMENU /*hmenuShared*/, HOLEMENU /*holemenu*/, HWND /*hwndActiveObject*/) +{ + return S_OK; +} + +HRESULT ole_in_place_frame::RemoveMenus(HMENU /*hmenuShared*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_frame::SetStatusText(LPCOLESTR /*pszStatusText*/) +{ + return S_OK; +} + +HRESULT ole_in_place_frame::EnableModeless(BOOL /*fEnable*/) +{ + return S_OK; +} + +HRESULT ole_in_place_frame::TranslateAcceleratorA(LPMSG /*lpmsg*/, WORD /*wID*/) +{ + return E_NOTIMPL; +} diff --git a/src/client/launcher/html/ole_in_place_frame.hpp b/src/client/launcher/html/ole_in_place_frame.hpp new file mode 100644 index 00000000..4a39d7f3 --- /dev/null +++ b/src/client/launcher/html/ole_in_place_frame.hpp @@ -0,0 +1,30 @@ +#pragma once + +class html_frame; + +class ole_in_place_frame final : public IOleInPlaceFrame +{ +public: + ole_in_place_frame(html_frame* frame); + virtual ~ole_in_place_frame() = default; + +private: + html_frame* frame_; + +public: // IOleInPlaceFrame interface + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID FAR* ppvObj) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + HRESULT STDMETHODCALLTYPE GetWindow(HWND FAR* lphwnd) override; + HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode) override; + HRESULT STDMETHODCALLTYPE GetBorder(LPRECT lprectBorder) override; + HRESULT STDMETHODCALLTYPE RequestBorderSpace(LPCBORDERWIDTHS pborderwidths) override; + HRESULT STDMETHODCALLTYPE SetBorderSpace(LPCBORDERWIDTHS pborderwidths) override; + HRESULT STDMETHODCALLTYPE SetActiveObject(IOleInPlaceActiveObject* pActiveObject, LPCOLESTR pszObjName) override; + HRESULT STDMETHODCALLTYPE InsertMenus(HMENU hmenuShared, LPOLEMENUGROUPWIDTHS lpMenuWidths) override; + HRESULT STDMETHODCALLTYPE SetMenu(HMENU hmenuShared, HOLEMENU holemenu, HWND hwndActiveObject) override; + HRESULT STDMETHODCALLTYPE RemoveMenus(HMENU hmenuShared) override; + HRESULT STDMETHODCALLTYPE SetStatusText(LPCOLESTR pszStatusText) override; + HRESULT STDMETHODCALLTYPE EnableModeless(BOOL fEnable) override; + HRESULT STDMETHODCALLTYPE TranslateAccelerator(LPMSG lpmsg, WORD wID) override; +}; diff --git a/src/client/launcher/html/ole_in_place_site.cpp b/src/client/launcher/html/ole_in_place_site.cpp new file mode 100644 index 00000000..d6cd3d82 --- /dev/null +++ b/src/client/launcher/html/ole_in_place_site.cpp @@ -0,0 +1,105 @@ +#include +#include "html_frame.hpp" + +ole_in_place_site::ole_in_place_site(html_frame* frame) : frame_(frame) +{ +} + +HRESULT ole_in_place_site::QueryInterface(REFIID riid, LPVOID FAR* ppvObj) +{ + auto client_site = this->frame_->get_client_site(); + if (client_site) + { + return client_site->QueryInterface(riid, ppvObj); + } + + return E_NOINTERFACE; +} + +ULONG ole_in_place_site::AddRef() +{ + return 1; +} + +ULONG ole_in_place_site::Release() +{ + return 1; +} + +HRESULT ole_in_place_site::GetWindow(HWND* lphwnd) +{ + *lphwnd = this->frame_->get_window(); + return S_OK; +} + +HRESULT ole_in_place_site::ContextSensitiveHelp(BOOL /*fEnterMode*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_site::CanInPlaceActivate() +{ + return S_OK; +} + +HRESULT ole_in_place_site::OnInPlaceActivate() +{ + return S_OK; +} + +HRESULT ole_in_place_site::OnUIActivate() +{ + return S_OK; +} + +HRESULT ole_in_place_site::GetWindowContext(LPOLEINPLACEFRAME* lplpFrame, LPOLEINPLACEUIWINDOW* lplpDoc, + LPRECT /*lprcPosRect*/, LPRECT /*lprcClipRect*/, + LPOLEINPLACEFRAMEINFO lpFrameInfo) +{ + *lplpFrame = this->frame_->get_in_place_frame(); + *lplpDoc = nullptr; + + lpFrameInfo->fMDIApp = FALSE; + lpFrameInfo->hwndFrame = this->frame_->get_window(); + lpFrameInfo->haccel = nullptr; + lpFrameInfo->cAccelEntries = 0; + + return S_OK; +} + +HRESULT ole_in_place_site::Scroll(SIZE /*scrollExtent*/) +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_site::OnUIDeactivate(BOOL /*fUndoable*/) +{ + return S_OK; +} + +HRESULT ole_in_place_site::OnInPlaceDeactivate() +{ + return S_OK; +} + +HRESULT ole_in_place_site::DiscardUndoState() +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_site::DeactivateAndUndo() +{ + return E_NOTIMPL; +} + +HRESULT ole_in_place_site::OnPosRectChange(LPCRECT lprcPosRect) +{ + IOleInPlaceObject* in_place = nullptr; + if (!this->frame_->get_browser_object()->QueryInterface(IID_IOleInPlaceObject, reinterpret_cast(&in_place))) + { + in_place->SetObjectRects(lprcPosRect, lprcPosRect); + in_place->Release(); + } + + return S_OK; +} diff --git a/src/client/launcher/html/ole_in_place_site.hpp b/src/client/launcher/html/ole_in_place_site.hpp new file mode 100644 index 00000000..3dad18cf --- /dev/null +++ b/src/client/launcher/html/ole_in_place_site.hpp @@ -0,0 +1,32 @@ +#pragma once + +class html_frame; + +class ole_in_place_site final : public IOleInPlaceSite +{ +public: + ole_in_place_site(html_frame* frame); + virtual ~ole_in_place_site() = default; + +private: + html_frame* frame_; + +public: // IOleInPlaceSite interface + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID FAR* ppvObj) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + HRESULT STDMETHODCALLTYPE GetWindow(HWND FAR* lphwnd) override; + HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode) override; + HRESULT STDMETHODCALLTYPE CanInPlaceActivate() override; + HRESULT STDMETHODCALLTYPE OnInPlaceActivate() override; + HRESULT STDMETHODCALLTYPE OnUIActivate() override; + HRESULT STDMETHODCALLTYPE GetWindowContext(LPOLEINPLACEFRAME FAR* lplpFrame, LPOLEINPLACEUIWINDOW FAR* lplpDoc, + LPRECT lprcPosRect, LPRECT lprcClipRect, + LPOLEINPLACEFRAMEINFO lpFrameInfo) override; + HRESULT STDMETHODCALLTYPE Scroll(SIZE scrollExtent) override; + HRESULT STDMETHODCALLTYPE OnUIDeactivate(BOOL fUndoable) override; + HRESULT STDMETHODCALLTYPE OnInPlaceDeactivate() override; + HRESULT STDMETHODCALLTYPE DiscardUndoState() override; + HRESULT STDMETHODCALLTYPE DeactivateAndUndo() override; + HRESULT STDMETHODCALLTYPE OnPosRectChange(LPCRECT lprcPosRect) override; +}; diff --git a/src/client/launcher/launcher.cpp b/src/client/launcher/launcher.cpp new file mode 100644 index 00000000..ccd1baca --- /dev/null +++ b/src/client/launcher/launcher.cpp @@ -0,0 +1,66 @@ +#include +#include "launcher.hpp" + +#include + +launcher::launcher() +{ + this->create_main_menu(); +} + +void launcher::create_main_menu() +{ + this->main_window_.register_callback("openUrl", [](html_frame::callback_params* params) + { + if (params->arguments.empty()) return; + + const auto param = params->arguments[0]; + if (!param.is_string()) return; + + const auto url = param.get_string(); + ShellExecuteA(nullptr, "open", url.data(), nullptr, nullptr, SW_SHOWNORMAL); + }); + + this->main_window_.register_callback("selectMode", [this](html_frame::callback_params* params) + { + if (params->arguments.empty()) return; + + const auto param = params->arguments[0]; + if (!param.is_number()) return; + + const auto number = static_cast(param.get_number()); + this->select_mode(number); + }); + + this->main_window_.set_callback( + [](window* window, const UINT message, const WPARAM w_param, const LPARAM l_param) -> LRESULT + { + if (message == WM_CLOSE) + { + window::close_all(); + } + + return DefWindowProcA(*window, message, w_param, l_param); + }); + + this->main_window_.create("S2-Mod", 750, 420); + this->main_window_.load_html(load_content(MENU_MAIN)); + this->main_window_.show(); +} + +launcher::mode launcher::run() const +{ + window::run(); + return this->mode_; +} + +void launcher::select_mode(const mode mode) +{ + this->mode_ = mode; + this->main_window_.close(); +} + +std::string launcher::load_content(const int res) +{ + return utils::nt::load_resource(res); +} diff --git a/src/client/launcher/launcher.hpp b/src/client/launcher/launcher.hpp new file mode 100644 index 00000000..1808376c --- /dev/null +++ b/src/client/launcher/launcher.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "html/html_window.hpp" + +class launcher final +{ +public: + enum class mode + { + none, + singleplayer, + multiplayer, + server, + // Surrogates + survival, + zombies, + }; + + launcher(); + + mode run() const; + +private: + mode mode_ = mode::none; + + html_window main_window_; + + void select_mode(mode mode); + + void create_main_menu(); + + static std::string load_content(int res); +}; diff --git a/src/client/launcher/window.cpp b/src/client/launcher/window.cpp new file mode 100644 index 00000000..eceaf84b --- /dev/null +++ b/src/client/launcher/window.cpp @@ -0,0 +1,216 @@ +#include +#include "window.hpp" + +#include + +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +std::mutex window::mutex_; +std::vector window::windows_; + +window::window() +{ + ZeroMemory(&this->wc_, sizeof(this->wc_)); + + this->classname_ = "window-base-" + std::to_string(time(nullptr)); + + this->wc_.cbSize = sizeof(this->wc_); + this->wc_.style = CS_HREDRAW | CS_VREDRAW; + this->wc_.lpfnWndProc = static_processor; + this->wc_.hInstance = GetModuleHandle(nullptr); + this->wc_.hCursor = LoadCursor(nullptr, IDC_ARROW); + this->wc_.hIcon = LoadIcon(this->wc_.hInstance, MAKEINTRESOURCE(102)); + this->wc_.hIconSm = this->wc_.hIcon; + this->wc_.hbrBackground = HBRUSH(COLOR_WINDOW); + this->wc_.lpszClassName = this->classname_.data(); + RegisterClassEx(&this->wc_); +} + +void window::create(const std::string& title, const int width, const int height, const long flags) +{ + { + std::lock_guard _(mutex_); + windows_.push_back(this); + } + + const auto x = (GetSystemMetrics(SM_CXSCREEN) - width) / 2; + const auto y = (GetSystemMetrics(SM_CYSCREEN) - height) / 2; + + this->handle_ = CreateWindowExA(NULL, this->wc_.lpszClassName, title.data(), flags, x, y, width, height, nullptr, + nullptr, this->wc_.hInstance, this); + + BOOL value = TRUE; + DwmSetWindowAttribute(this->handle_, + DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value)); + + SendMessageA(this->handle_, WM_DPICHANGED, 0, 0); +} + +window::~window() +{ + this->close(); + UnregisterClass(this->wc_.lpszClassName, this->wc_.hInstance); +} + +void window::close() +{ + if (!this->handle_) return; + + DestroyWindow(this->handle_); + this->handle_ = nullptr; +} + +void window::run() +{ + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +void window::close_all() +{ + std::unique_lock lock(mutex_); + auto window_list = windows_; + lock.unlock(); + + const auto current_thread_id = GetCurrentThreadId(); + for (auto& window : window_list) + { + const auto thread_id = GetWindowThreadProcessId(*window, nullptr); + + if (thread_id == current_thread_id) + { + window->close(); + } + } +} + +void window::remove_window(const window* window) +{ + std::lock_guard _(mutex_); + + for (auto i = windows_.begin(); i != windows_.end(); ++i) + { + if (*i == window) + { + windows_.erase(i); + break; + } + } +} + +int window::get_window_count() +{ + std::lock_guard _(mutex_); + + auto count = 0; + const auto current_thread_id = GetCurrentThreadId(); + + for (const auto& window : windows_) + { + const auto thread_id = GetWindowThreadProcessId(*window, nullptr); + + if (thread_id == current_thread_id) + { + ++count; + } + } + + return count; +} + +void window::show() const +{ + ShowWindow(this->handle_, SW_SHOW); + UpdateWindow(this->handle_); +} + +void window::hide() const +{ + ShowWindow(this->handle_, SW_HIDE); + UpdateWindow(this->handle_); +} + +void window::set_callback(const std::function& callback) +{ + this->callback_ = callback; +} + +LRESULT window::processor(const UINT message, const WPARAM w_param, const LPARAM l_param) +{ + if (message == WM_DPICHANGED) + { + const utils::nt::library user32{"user32.dll"}; + const auto get_dpi = user32 ? user32.get_proc("GetDpiForWindow") : nullptr; + + if (get_dpi) + { + const auto dpi = get_dpi(*this); + if (dpi != this->last_dpi_) + { + RECT rect; + GetWindowRect(*this, &rect); + + const auto scale = dpi * 1.0 / this->last_dpi_; + this->last_dpi_ = dpi; + + const auto width = rect.right - rect.left; + const auto height = rect.bottom - rect.top; + + MoveWindow(*this, rect.left, rect.top, int(width * scale), int(height * scale), TRUE); + } + } + } + + if (message == WM_DESTROY) + { + remove_window(this); + + if (get_window_count() == 0) + { + PostQuitMessage(0); + } + + return TRUE; + } + + if (message == WM_KILL_WINDOW) + { + DestroyWindow(*this); + return 0; + } + + if (this->callback_) + { + return this->callback_(this, message, w_param, l_param); + } + + return DefWindowProc(*this, message, w_param, l_param); +} + +LRESULT CALLBACK window::static_processor(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param) +{ + if (message == WM_CREATE) + { + auto data = reinterpret_cast(l_param); + SetWindowLongPtrA(hwnd, GWLP_USERDATA, LONG_PTR(data->lpCreateParams)); + + static_cast(data->lpCreateParams)->handle_ = hwnd; + } + + const auto self = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self) return self->processor(message, w_param, l_param); + + return DefWindowProc(hwnd, message, w_param, l_param); +} + + +window::operator HWND() const +{ + return this->handle_; +} diff --git a/src/client/launcher/window.hpp b/src/client/launcher/window.hpp new file mode 100644 index 00000000..3bf83b15 --- /dev/null +++ b/src/client/launcher/window.hpp @@ -0,0 +1,46 @@ +#pragma once +#pragma comment (lib, "dwmapi.lib") +#include + +#define WM_KILL_WINDOW (WM_USER+0) + +class window +{ +public: + window(); + virtual ~window(); + + void create(const std::string& title, int width, int height, + long flags = (WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX))); + + void close(); + + void show() const; + void hide() const; + + void set_callback(const std::function& callback); + + operator HWND() const; + + static void run(); + static void close_all(); + +protected: + virtual LRESULT processor(UINT message, WPARAM w_param, LPARAM l_param); + +private: + uint32_t last_dpi_ = 96; + + WNDCLASSEX wc_{}; + HWND handle_ = nullptr; + std::string classname_; + std::function callback_; + + static LRESULT CALLBACK static_processor(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param); + + static std::mutex mutex_; + static std::vector windows_; + + static void remove_window(const window* window); + static int get_window_count(); +};