various: rework resource handlers

CefResourceRequestHandlers are now per-window using polymorphism.
This allows us to have way more maintainable handlers for specific
internal URLs and per-OS implementations, too.
This commit is contained in:
Adam
2023-07-16 05:35:09 +01:00
parent 609c3203bb
commit 536ad19ec3
10 changed files with 596 additions and 542 deletions

View File

@@ -23,8 +23,13 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CEF_ROOT}/cmake")
find_package(CEF REQUIRED)
add_subdirectory(${CEF_LIBCEF_DLL_WRAPPER_PATH} libcef_dll_wrapper)
set(WINDOW_LAUNCHER_OS_SPECIFIC src/browser/window_launcher_linux.cxx)
# This line needs to be updated manually with any new/deleted object files; cmake discourages GLOBbing source files
add_executable(bolt modules/fmt/src/format.cc src/main.cxx src/browser.cxx src/browser/app.cxx src/browser/client.cxx)
add_executable(bolt
modules/fmt/src/format.cc src/main.cxx src/browser.cxx src/browser/app.cxx src/browser/client.cxx
src/browser/resource_handler.cxx src/browser/window_launcher.cxx ${WINDOW_LAUNCHER_OS_SPECIFIC}
)
# Various build properties
target_include_directories(bolt PUBLIC ${CEF_ROOT} modules/fmt/include)

View File

@@ -5,10 +5,20 @@
#include <fmt/core.h>
Browser::Window::Window(Kind kind, CefRefPtr<CefClient> client, Browser::Details details, CefString url, bool show_devtools):
kind(kind), show_devtools(show_devtools), details(details), window(nullptr), browser_view(nullptr), browser(nullptr), pending_child(nullptr)
Browser::Window::Window(CefRefPtr<CefClient> client, Browser::Details details, CefString url, bool show_devtools):
show_devtools(show_devtools), details(details), window(nullptr), browser_view(nullptr), browser(nullptr), pending_child(nullptr)
{
fmt::print("[B] Browser::Window constructor, this={}\n", reinterpret_cast<uintptr_t>(this));
this->Init(client, details, url, show_devtools);
}
Browser::Window::Window(Browser::Details details, bool show_devtools):
show_devtools(show_devtools), details(details), window(nullptr), browser_view(nullptr), browser(nullptr), pending_child(nullptr)
{
fmt::print("[B] Browser::Window popup constructor, this={}\n", reinterpret_cast<uintptr_t>(this));
}
void Browser::Window::Init(CefRefPtr<CefClient> client, Browser::Details details, CefString url, bool show_devtools) {
CefBrowserSettings browser_settings;
browser_settings.background_color = CefColorSetARGB(0, 0, 0, 0);
if (details.controls_overlay) {
@@ -19,13 +29,6 @@ Browser::Window::Window(Kind kind, CefRefPtr<CefClient> client, Browser::Details
this->browser_view = CefBrowserView::CreateBrowserView(client, url, browser_settings, nullptr, nullptr, this);
}
CefWindow::CreateTopLevelWindow(this);
}
Browser::Window::Window(Kind kind, Browser::Details details, bool show_devtools):
kind(kind), show_devtools(show_devtools), details(details), window(nullptr), browser_view(nullptr), browser(nullptr), pending_child(nullptr)
{
fmt::print("[B] Browser::Window popup constructor, this={}\n", reinterpret_cast<uintptr_t>(this));
}
void Browser::Window::OnWindowCreated(CefRefPtr<CefWindow> window) {
@@ -113,7 +116,7 @@ CefRefPtr<CefBrowserViewDelegate> Browser::Window::GetDelegateForPopupBrowserVie
.controls_overlay = false,
.is_devtools = is_devtools,
};
this->pending_child = new Browser::Window(this->kind, details, this->show_devtools);
this->pending_child = new Browser::Window(details, this->show_devtools);
return this->pending_child;
}
@@ -157,14 +160,6 @@ cef_chrome_toolbar_type_t Browser::Window::GetChromeToolbarType() {
return CEF_CTT_NONE;
}
bool Browser::Window::IsLauncher() const {
return this->kind == Kind::Launcher;
}
bool Browser::Window::IsApp() const {
return this->kind == Kind::Applet;
}
bool Browser::Window::HasBrowser(CefRefPtr<CefBrowser> browser) const {
if (this->browser->IsSame(browser)) {
return true;

View File

@@ -7,13 +7,6 @@
#include "include/views/cef_browser_view.h"
namespace Browser {
/// The purpose of a browser. Affects what kind of requests the browser is allowed to make and
/// how they're handled. Children and devtools will have the same Kind as the parent.
enum Kind {
Launcher,
Applet,
};
/// Represents a visible browser window on the user's screen. This struct wraps a single pointer,
/// so it is safe to store anywhere and move around during operation.
/// The window will exist either until the user closes it or Window::CloseBrowser() is called.
@@ -24,14 +17,14 @@ namespace Browser {
/// https://github.com/chromiumembedded/cef/blob/5735/include/views/cef_browser_view_delegate.h
/// https://github.com/chromiumembedded/cef/blob/5735/include/cef_resource_request_handler.h
struct Window: CefWindowDelegate, CefBrowserViewDelegate, CefRequestHandler {
Window(Kind, CefRefPtr<CefClient> client, Details, CefString, bool);
Window(Kind, Details, bool);
/// Calls this->Init internally
Window(CefRefPtr<CefClient> client, Details, CefString, bool);
/// Returns true if this window is Kind::Launcher
bool IsLauncher() const;
/// Does not call this->Init internally
Window(Details, bool);
/// Returns true if this window is Kind::App
bool IsApp() const;
/// Initialise with a browser_view. Should be called from a constructor, if at all.
void Init(CefRefPtr<CefClient> client, Details, CefString, bool);
/// Returns true if the given browser is this window or one of its children, otherwise false
bool HasBrowser(CefRefPtr<CefBrowser>) const;
@@ -79,7 +72,6 @@ namespace Browser {
) override;
private:
Kind kind;
bool show_devtools;
Details details;
CefRefPtr<CefWindow> window;

View File

@@ -1,98 +1,14 @@
#include "client.hxx"
#include "include/cef_parser.h"
#include "include/cef_life_span_handler.h"
#include "include/internal/cef_types.h"
#include "include/internal/cef_types_wrappers.h"
#include "window_launcher.hxx"
#if defined(__linux__)
#include <archive.h>
#include <archive_entry.h>
#endif
#include "include/cef_life_span_handler.h"
#include <algorithm>
#include <fcntl.h>
#include <fmt/core.h>
#include <fstream>
#include <regex>
#include <spawn.h>
extern char **environ;
/// https://github.com/chromiumembedded/cef/blob/5735/include/cef_resource_request_handler.h
/// https://github.com/chromiumembedded/cef/blob/5735/include/cef_resource_handler.h
struct ResourceHandler: public CefResourceRequestHandler, CefResourceHandler {
ResourceHandler(const unsigned char* data, size_t len, int status, CefString mime):
data(data), data_len(len), status(status), mime(mime), has_location(false), cursor(0) { }
ResourceHandler(const unsigned char* data, size_t len, int status, CefString mime, CefString location):
data(data), data_len(len), status(status), mime(mime), location(location), has_location(true), cursor(0) { }
bool Open(CefRefPtr<CefRequest>, bool& handle_request, CefRefPtr<CefCallback>) override {
handle_request = true;
return true;
}
void GetResponseHeaders(CefRefPtr<CefResponse> response, int64& response_length, CefString& redirectUrl) override {
response->SetStatus(this->status);
response->SetMimeType(this->mime);
if (this->has_location) {
response->SetHeaderByName("Location", this->location, false);
}
response_length = this->data_len;
}
bool Read(void* data_out, int bytes_to_read, int& bytes_read, CefRefPtr<CefResourceReadCallback>) override {
if (this->cursor == this->data_len) {
// "To indicate response completion set |bytes_read| to 0 and return false."
bytes_read = 0;
return false;
}
if (this->cursor + bytes_to_read <= this->data_len) {
// requested read is entirely in bounds
bytes_read = bytes_to_read;
memcpy(data_out, this->data + this->cursor, bytes_read);
this->cursor += bytes_to_read;
} else {
// read only to end of string
bytes_read = this->data_len - this->cursor;
memcpy(data_out, this->data + this->cursor, bytes_read);
this->cursor = this->data_len;
}
return true;
}
bool Skip(int64 bytes_to_skip, int64& bytes_skipped, CefRefPtr<CefResourceSkipCallback>) override {
if (this->cursor + bytes_to_skip <= this->data_len) {
// skip in bounds
bytes_skipped = bytes_to_skip;
this->cursor += bytes_to_skip;
} else {
// skip to end of string
bytes_skipped = this->data_len - this->cursor;
this->cursor = this->data_len;
}
return true;
}
void Cancel() override {
this->cursor = this->data_len;
}
CefRefPtr<CefResourceHandler> GetResourceHandler(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame>, CefRefPtr<CefRequest>) override {
return this;
}
private:
const unsigned char* data;
size_t data_len;
int status;
CefString mime;
CefString location;
bool has_location;
size_t cursor;
IMPLEMENT_REFCOUNTING(ResourceHandler);
DISALLOW_COPY_AND_ASSIGN(ResourceHandler);
};
Browser::Client::Client(CefRefPtr<Browser::App> app, std::filesystem::path data_dir): show_devtools(true), data_dir(data_dir) {
CefString mime_type_html = "text/html";
CefString mime_type_js = "application/javascript";
@@ -101,12 +17,6 @@ Browser::Client::Client(CefRefPtr<Browser::App> app, std::filesystem::path data_
this->internal_pages["/oauth.html"] = InternalFile("html/oauth.html", mime_type_html);
this->internal_pages["/game_auth.html"] = InternalFile("html/game_auth.html", mime_type_html);
this->internal_pages["/frame.html"] = InternalFile("html/frame.html", mime_type_html);
this->env_count = 0;
char** env = environ;
while (*env != nullptr) {
this->env_count += 1;
env += 1;
}
}
CefRefPtr<CefLifeSpanHandler> Browser::Client::GetLifeSpanHandler() {
@@ -129,25 +39,9 @@ void Browser::Client::OnContextInitialized() {
.frame = true,
.controls_overlay = false,
};
std::string url = this->internal_url + this->launcher_uri;
#if defined(__linux__)
std::filesystem::path hash_path = this->data_dir;
hash_path.append("rs3linux.sha256");
int file = open(hash_path.c_str(), O_RDONLY);
if (file != -1) {
char buf[64];
ssize_t r = read(file, buf, 64);
if (r == 64) {
url += "&rs3_linux_installed_hash=";
url.append(buf, 64);
}
}
close(file);
#endif
this->windows_lock.lock();
CefRefPtr<Browser::Window> w = new Browser::Window(Browser::Kind::Launcher, this, details, url, this->show_devtools);
CefRefPtr<Browser::Window> w = new Browser::Launcher(this, details, this->show_devtools, &this->internal_pages, this->data_dir);
this->windows.push_back(w);
this->windows_lock.unlock();
}
@@ -229,16 +123,8 @@ CefRefPtr<CefResourceRequestHandler> Browser::Client::GetResourceRequestHandler(
const CefString& request_initiator,
bool& disable_default_handling
) {
// Some of the dreaded phrases that I don't want to show up in a grep/search
constexpr char provider[] = {106, 97, 103, 101, 120, 0};
constexpr char tar_xz_inner_path[] = {
46, 47, 117, 115, 114, 47, 115, 104, 97, 114, 101, 47, 103, 97, 109, 101,
115, 47, 114, 117, 110, 101, 115, 99, 97, 112, 101, 45, 108, 97, 117,
110, 99, 104, 101, 114, 47, 114, 117, 110, 101, 115, 99,97, 112, 101
};
// Find the Window responsible for this request, if any
this->windows_lock.lock();
std::lock_guard<std::mutex> _(this->windows_lock);
auto it = std::find_if(
this->windows.begin(),
this->windows.end(),
@@ -247,388 +133,7 @@ CefRefPtr<CefResourceRequestHandler> Browser::Client::GetResourceRequestHandler(
if (it == this->windows.end()) {
// Request came from no window?
return nullptr;
} else {
return (*it)->GetResourceRequestHandler(browser, frame, request, is_navigation, is_download, request_initiator, disable_default_handling);
}
const CefRefPtr<Browser::Window> window = *it;
this->windows_lock.unlock();
if (window->IsApp()) {
// TODO: make Browser::Window a valid request handler for sandboxing purposes
return nullptr;
}
if (window->IsLauncher()) {
// Parse URL
const std::string request_url = request->GetURL().ToString();
const std::string::size_type colon = request_url.find_first_of(':');
if (colon == std::string::npos) {
return nullptr;
}
const std::string_view schema(request_url.begin(), request_url.begin() + colon);
if (schema == provider) {
// parser for custom schema thing
bool has_code = false;
std::string_view code;
bool has_state = false;
std::string_view state;
std::string::size_type cursor = colon + 1;
while (true) {
std::string::size_type next_eq = request_url.find_first_of('=', cursor);
std::string::size_type next_comma = request_url.find_first_of(',', cursor);
if (next_eq == std::string::npos || next_comma < next_eq) return nullptr;
const bool last_pair = next_comma == std::string::npos;
std::string_view key(request_url.begin() + cursor, request_url.begin() + next_eq);
std::string_view value(request_url.begin() + next_eq + 1, last_pair ? request_url.end() : request_url.begin() + next_comma);
if (key == "code") {
code = value;
has_code = true;
}
if (key == "state") {
state = value;
has_state = true;
}
if (last_pair) {
break;
} else {
cursor = next_comma + 1;
}
}
if (has_code && has_state) {
disable_default_handling = true;
const char* data = "Moved\n";
CefString location = CefString(this->internal_url + "oauth.html?code=" + std::string(code) + "&state=" + std::string(state));
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 302, "text/plain", location);
} else {
return nullptr;
}
}
if ((schema != "http" && schema != "https") || request_url.size() < colon + 4
|| request_url.at(colon + 1) != '/' || request_url.at(colon + 2) != '/')
{
return nullptr;
}
// parse all the important parts of the URL as string_views
const std::string::size_type next_hash = request_url.find_first_of('#', colon + 3);
const auto url_end = next_hash == std::string::npos ? request_url.end() : request_url.begin() + next_hash;
const std::string::size_type next_sep = request_url.find_first_of('/', colon + 3);
const std::string::size_type next_question = request_url.find_first_of('?', colon + 3);
const auto domain_end = next_sep == std::string::npos && next_question == std::string::npos ? url_end : request_url.begin() + std::min(next_sep, next_question);
const std::string_view domain(request_url.begin() + colon + 3, domain_end);
const std::string_view path(domain_end, next_question == std::string::npos ? url_end : request_url.begin() + next_question);
const std::string_view query(request_url.begin() + next_question + 1, next_question == std::string::npos ? request_url.begin() + next_question + 1 : url_end);
const std::string_view comment(next_hash == std::string::npos ? request_url.end() : request_url.begin() + next_hash + 1, request_url.end());
// handler for another custom request thing, but this one uses localhost, for whatever reason
if (domain == "localhost" && path == "/" && comment.starts_with("code=")) {
disable_default_handling = true;
const char* data = "Moved\n";
CefString location = CefString(this->internal_url + "game_auth.html?" + std::string(comment));
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 302, "text/plain", location);
}
// TODO: everything below this is massively unmaintainable and needs splitting up probably into CefResourceRequestHandler impls
// internal pages
if (domain == "bolt-internal") {
disable_default_handling = true;
#if defined(__linux__)
// instruction to launch RS3 .deb
if (path == "/launch-deb") {
CefRefPtr<CefPostData> post_data = request->GetPostData();
std::filesystem::path path = this->data_dir;
path.append("rs3linux");
auto cursor = 0;
bool has_hash = false;
bool should_set_access_token = false;
bool should_set_refresh_token = false;
bool should_set_session_id = false;
bool should_set_character_id = false;
bool should_set_display_name = false;
std::string hash;
std::string env_access_token;
std::string env_refresh_token;
std::string env_session_id;
std::string env_character_id;
std::string env_display_name;
while (true) {
const std::string::size_type next_eq = query.find_first_of('=', cursor);
const std::string::size_type next_amp = query.find_first_of('&', cursor);
if (next_eq == std::string::npos) break;
if (next_eq >= next_amp) {
cursor = next_amp + 1;
continue;
}
std::string_view key(query.begin() + cursor, query.begin() + next_eq);
if (key == "hash") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
hash = CefURIDecode(value, true, UU_SPACES).ToString();
has_hash = true;
}
if (key == "jx_access_token") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_access_token = std::string("JX_ACCESS_TOKEN=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_access_token = true;
}
if (key == "jx_refresh_token") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_refresh_token = std::string("JX_REFRESH_TOKEN=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_refresh_token = true;
}
if (key == "jx_session_id") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_session_id = std::string("JX_SESSION_ID=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_session_id = true;
}
if (key == "jx_character_id") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_character_id = std::string("JX_CHARACTER_ID=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_character_id = true;
}
if (key == "jx_display_name") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_display_name = std::string("JX_DISPLAY_NAME=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_display_name = true;
}
if (next_amp == std::string::npos) break;
cursor = next_amp + 1;
}
if (has_hash) {
CefRefPtr<CefPostData> post_data = request->GetPostData();
if (post_data == nullptr || post_data->GetElementCount() != 1) {
const char* data = "Bad Request";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
CefPostData::ElementVector vec;
post_data->GetElements(vec);
size_t deb_size = vec[0]->GetBytesCount();
unsigned char* deb = new unsigned char[deb_size];
vec[0]->GetBytes(deb_size, deb);
struct archive* ar = archive_read_new();
archive_read_support_format_ar(ar);
archive_read_open_memory(ar, deb, deb_size);
bool entry_found = false;
struct archive_entry* entry;
while (true) {
int r = archive_read_next_header(ar, &entry);
if (r == ARCHIVE_EOF) break;
if (r != ARCHIVE_OK) {
archive_read_close(ar);
archive_read_free(ar);
delete[] deb;
const char* data = "Malformed .deb file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
if (strcmp(archive_entry_pathname(entry), "data.tar.xz") == 0) {
entry_found = true;
break;
}
}
if (!entry_found) {
archive_read_close(ar);
archive_read_free(ar);
delete[] deb;
const char* data = "No data in .deb file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
const long entry_size = archive_entry_size(entry);
unsigned char* tar_xz = new unsigned char[entry_size];
long written = 0;
while (written < entry_size) {
written += archive_read_data(ar, tar_xz + written, entry_size - written);
}
archive_read_close(ar);
archive_read_free(ar);
delete[] deb;
struct archive* xz = archive_read_new();
archive_read_support_format_tar(xz);
archive_read_support_filter_xz(xz);
archive_read_open_memory(xz, tar_xz, entry_size);
entry_found = false;
while (true) {
int r = archive_read_next_header(xz, &entry);
if (r == ARCHIVE_EOF) break;
if (r != ARCHIVE_OK) {
archive_read_close(xz);
archive_read_free(xz);
delete[] tar_xz;
const char* data = "Malformed .tar.xz file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
if (strcmp(archive_entry_pathname(entry), tar_xz_inner_path) == 0) {
entry_found = true;
break;
}
}
if (!entry_found) {
archive_read_close(xz);
archive_read_free(xz);
delete[] tar_xz;
const char* data = "No target executable in .tar.xz file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
const long game_size = archive_entry_size(entry);
char* game = new char[game_size];
written = 0;
while (written < game_size) {
written += archive_read_data(ar, game + written, game_size - written);
}
archive_read_close(xz);
archive_read_free(xz);
delete[] tar_xz;
written = 0;
int file = open(path.c_str(), O_WRONLY | O_CREAT, 0755);
if (file == -1) {
delete[] game;
const char* data = "Failed to save executable; if the game is already running, close it and try again\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 500, "text/plain");
}
while (written < game_size) {
written += write(file, game + written, game_size - written);
}
close(file);
delete[] game;
}
posix_spawn_file_actions_t file_actions;
posix_spawnattr_t attributes;
pid_t pid;
posix_spawn_file_actions_init(&file_actions);
posix_spawnattr_init(&attributes);
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSID);
std::string path_str(path.c_str());
char* argv[2];
argv[0] = path_str.data();
argv[1] = nullptr;
bool should_set_home = true;
std::string env_home = this->env_key_home + this->data_dir.c_str();
char** env = new char*[this->env_count + 7];
size_t i;
for (i = 0; i < this->env_count; i += 1) {
if (should_set_access_token && strncmp(environ[i], this->env_key_access_token.c_str(), this->env_key_access_token.size()) == 0) {
should_set_access_token = false;
env[i] = env_access_token.data();
} else if (should_set_refresh_token && strncmp(environ[i], this->env_key_refresh_token.c_str(), this->env_key_refresh_token.size()) == 0) {
should_set_refresh_token = false;
env[i] = env_refresh_token.data();
} else if (should_set_session_id && strncmp(environ[i], this->env_key_session_id.c_str(), this->env_key_session_id.size()) == 0) {
should_set_session_id = false;
env[i] = env_session_id.data();
} else if (should_set_character_id && strncmp(environ[i], this->env_key_character_id.c_str(), this->env_key_character_id.size()) == 0) {
should_set_character_id = false;
env[i] = env_character_id.data();
} else if (should_set_display_name && strncmp(environ[i], this->env_key_display_name.c_str(), this->env_key_display_name.size()) == 0) {
should_set_display_name = false;
env[i] = env_display_name.data();
} else if (strncmp(environ[i], this->env_key_home.c_str(), this->env_key_home.size()) == 0) {
should_set_home = false;
env[i] = env_home.data();
} else {
env[i] = environ[i];
}
}
if (should_set_access_token) {
env[i] = env_access_token.data();
i += 1;
}
if (should_set_refresh_token) {
env[i] = env_refresh_token.data();
i += 1;
}
if (should_set_session_id) {
env[i] = env_session_id.data();
i += 1;
}
if (should_set_character_id) {
env[i] = env_character_id.data();
i += 1;
}
if (should_set_display_name) {
env[i] = env_display_name.data();
i += 1;
}
if (should_set_home) {
env[i] = env_home.data();
i += 1;
}
env[i] = nullptr;
int r = posix_spawn(&pid, path_str.c_str(), &file_actions, &attributes, argv, env);
posix_spawnattr_destroy(&attributes);
posix_spawn_file_actions_destroy(&file_actions);
delete[] env;
if (r == 0) {
fmt::print("[B] Successfully spawned game process with pid {}\n", pid);
if (has_hash) {
size_t written = 0;
std::filesystem::path hash_path(this->data_dir);
hash_path.append("rs3linux.sha256");
int file = open(hash_path.c_str(), O_WRONLY | O_CREAT, 0644);
if (file == -1) {
const char* data = "OK, but unable to save hash file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 200, "text/plain");
}
while (written < hash.size()) {
written += write(file, hash.c_str() + written, hash.size() - written);
}
close(file);
}
const char* data = "OK\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 200, "text/plain");
} else {
fmt::print("[B] Error from posix_spawn: {}\n", r);
const char* data = "Error spawning process\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 500, "text/plain");
}
}
#endif
// respond using internal hashmap of filenames
auto it = std::find_if(
this->internal_pages.begin(),
this->internal_pages.end(),
[&path](const auto& e) { return e.first == path; }
);
if (it != this->internal_pages.end()) {
if (it->second.success) {
return new ResourceHandler(
it->second.data.data(),
it->second.data.size(),
200,
it->second.mime_type
);
} else {
// this file failed to load during startup for some reason - 500
const char* data = "Internal Server Error\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 500, "text/plain");
}
} else {
// no matching internal page - 404
const char* data = "Not Found\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 404, "text/plain");
}
}
}
// route the request normally, to a website or whatever
return nullptr;
}

View File

@@ -71,15 +71,7 @@ namespace Browser {
bool show_devtools;
std::filesystem::path data_dir;
size_t env_count;
std::string env_key_home = "HOME=";
std::string env_key_access_token = "JX_ACCESS_TOKEN=";
std::string env_key_refresh_token = "JX_REFRESH_TOKEN=";
std::string env_key_session_id = "JX_SESSION_ID=";
std::string env_key_character_id = "JX_CHARACTER_ID=";
std::string env_key_display_name = "JX_DISPLAY_NAME=";
std::string internal_url = "https://bolt-internal/";
std::map<std::string, InternalFile> internal_pages;
#if defined(__linux__)
std::string launcher_uri = "index.html?platform=linux";

View File

@@ -0,0 +1,56 @@
#include "resource_handler.hxx"
bool Browser::ResourceHandler::Open(CefRefPtr<CefRequest>, bool& handle_request, CefRefPtr<CefCallback>) {
handle_request = true;
return true;
}
void Browser::ResourceHandler::GetResponseHeaders(CefRefPtr<CefResponse> response, int64& response_length, CefString& redirectUrl) {
response->SetStatus(this->status);
response->SetMimeType(this->mime);
if (this->has_location) {
response->SetHeaderByName("Location", this->location, false);
}
response_length = this->data_len;
}
bool Browser::ResourceHandler::Read(void* data_out, int bytes_to_read, int& bytes_read, CefRefPtr<CefResourceReadCallback>) {
if (this->cursor == this->data_len) {
// "To indicate response completion set |bytes_read| to 0 and return false."
bytes_read = 0;
return false;
}
if (this->cursor + bytes_to_read <= this->data_len) {
// requested read is entirely in bounds
bytes_read = bytes_to_read;
memcpy(data_out, this->data + this->cursor, bytes_read);
this->cursor += bytes_to_read;
} else {
// read only to end of string
bytes_read = this->data_len - this->cursor;
memcpy(data_out, this->data + this->cursor, bytes_read);
this->cursor = this->data_len;
}
return true;
}
bool Browser::ResourceHandler::Skip(int64 bytes_to_skip, int64& bytes_skipped, CefRefPtr<CefResourceSkipCallback>) {
if (this->cursor + bytes_to_skip <= this->data_len) {
// skip in bounds
bytes_skipped = bytes_to_skip;
this->cursor += bytes_to_skip;
} else {
// skip to end of string
bytes_skipped = this->data_len - this->cursor;
this->cursor = this->data_len;
}
return true;
}
void Browser::ResourceHandler::Cancel() {
this->cursor = this->data_len;
}
CefRefPtr<CefResourceHandler> Browser::ResourceHandler::GetResourceHandler(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame>, CefRefPtr<CefRequest>) {
return this;
}

View File

@@ -0,0 +1,37 @@
#ifndef _BOLT_RESOURCE_HANDLER_HXX_
#define _BOLT_RESOURCE_HANDLER_HXX_
#include "include/cef_base.h"
#include "include/cef_request_handler.h"
namespace Browser {
/// Struct for sending some bytes from memory as an HTTP response. Store individual instances on the heap.
/// https://github.com/chromiumembedded/cef/blob/5735/include/cef_resource_request_handler.h
/// https://github.com/chromiumembedded/cef/blob/5735/include/cef_resource_handler.h
struct ResourceHandler: public CefResourceRequestHandler, CefResourceHandler {
ResourceHandler(const unsigned char* data, size_t len, int status, CefString mime):
data(data), data_len(len), status(status), mime(mime), has_location(false), cursor(0) { }
ResourceHandler(const unsigned char* data, size_t len, int status, CefString mime, CefString location):
data(data), data_len(len), status(status), mime(mime), location(location), has_location(true), cursor(0) { }
bool Open(CefRefPtr<CefRequest>, bool&, CefRefPtr<CefCallback>) override;
void GetResponseHeaders(CefRefPtr<CefResponse>, int64&, CefString&) override;
bool Read(void*, int, int&, CefRefPtr<CefResourceReadCallback>) override;
bool Skip(int64, int64&, CefRefPtr<CefResourceSkipCallback>) override;
void Cancel() override;
CefRefPtr<CefResourceHandler> GetResourceHandler(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame>, CefRefPtr<CefRequest>) override;
private:
const unsigned char* data;
size_t data_len;
int status;
CefString mime;
CefString location;
bool has_location;
size_t cursor;
IMPLEMENT_REFCOUNTING(ResourceHandler);
DISALLOW_COPY_AND_ASSIGN(ResourceHandler);
};
#endif
}

View File

@@ -0,0 +1 @@
#include "window_launcher.hxx"

View File

@@ -0,0 +1,39 @@
#ifndef _BOLT_WINDOW_LAUNCHER_HXX_
#define _BOLT_WINDOW_LAUNCHER_HXX_
#include "../browser.hxx"
#include <filesystem>
namespace Browser {
struct Launcher: public Window {
Launcher(CefRefPtr<CefClient>, Details, bool, const std::map<std::string, InternalFile>* const, std::filesystem::path);
CefRefPtr<CefResourceRequestHandler> GetResourceRequestHandler(
CefRefPtr<CefBrowser>,
CefRefPtr<CefFrame>,
CefRefPtr<CefRequest>,
bool,
bool,
const CefString&,
bool&
) override;
private:
std::string env_key_home = "HOME=";
std::string env_key_access_token = "JX_ACCESS_TOKEN=";
std::string env_key_refresh_token = "JX_REFRESH_TOKEN=";
std::string env_key_session_id = "JX_SESSION_ID=";
std::string env_key_character_id = "JX_CHARACTER_ID=";
std::string env_key_display_name = "JX_DISPLAY_NAME=";
std::string internal_url = "https://bolt-internal/";
const std::map<std::string, InternalFile>* internal_pages;
std::filesystem::path data_dir;
std::filesystem::path hash_path;
std::filesystem::path rs3_path;
std::filesystem::path rs3_hash_path;
size_t env_count;
};
}
#endif

View File

@@ -0,0 +1,432 @@
#include "window_launcher.hxx"
#include "resource_handler.hxx"
#include "include/cef_parser.h"
#include <algorithm>
#include <archive.h>
#include <archive_entry.h>
#include <fcntl.h>
#include <fmt/core.h>
#include <spawn.h>
extern char **environ;
Browser::Launcher::Launcher(
CefRefPtr<CefClient> client,
Details details,
bool show_devtools,
const std::map<std::string, InternalFile>* const internal_pages,
std::filesystem::path data_dir
): Window(details, show_devtools), data_dir(data_dir), internal_pages(internal_pages) {
std::string url = this->internal_url + std::string("index.html?platform=linux");
this->rs3_path = data_dir;
this->rs3_path.append("rs3linux");
this->rs3_hash_path = data_dir;
this->rs3_hash_path.append("rs3linux.sha256");
int file = open(hash_path.c_str(), O_RDONLY);
if (file != -1) {
char buf[64];
ssize_t r = read(file, buf, 64);
if (r == 64) {
url += "&rs3_linux_installed_hash=";
url.append(buf, 64);
}
}
close(file);
this->env_count = 0;
char** env = environ;
while (*env != nullptr) {
this->env_count += 1;
env += 1;
}
this->Init(client, details, url, show_devtools);
}
CefRefPtr<CefResourceRequestHandler> Browser::Launcher::GetResourceRequestHandler(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool is_navigation,
bool is_download,
const CefString& request_initiator,
bool& disable_default_handling
) {
// Some of the dreaded phrases that I don't want to show up in a grep/search
constexpr char provider[] = {106, 97, 103, 101, 120, 0};
constexpr char tar_xz_inner_path[] = {
46, 47, 117, 115, 114, 47, 115, 104, 97, 114, 101, 47, 103, 97, 109, 101,
115, 47, 114, 117, 110, 101, 115, 99, 97, 112, 101, 45, 108, 97, 117,
110, 99, 104, 101, 114, 47, 114, 117, 110, 101, 115, 99,97, 112, 101
};
// Parse URL
const std::string request_url = request->GetURL().ToString();
const std::string::size_type colon = request_url.find_first_of(':');
if (colon == std::string::npos) {
return nullptr;
}
const std::string_view schema(request_url.begin(), request_url.begin() + colon);
if (schema == provider) {
// parser for custom schema thing
bool has_code = false;
std::string_view code;
bool has_state = false;
std::string_view state;
std::string::size_type cursor = colon + 1;
while (true) {
std::string::size_type next_eq = request_url.find_first_of('=', cursor);
std::string::size_type next_comma = request_url.find_first_of(',', cursor);
if (next_eq == std::string::npos || next_comma < next_eq) return nullptr;
const bool last_pair = next_comma == std::string::npos;
std::string_view key(request_url.begin() + cursor, request_url.begin() + next_eq);
std::string_view value(request_url.begin() + next_eq + 1, last_pair ? request_url.end() : request_url.begin() + next_comma);
if (key == "code") {
code = value;
has_code = true;
}
if (key == "state") {
state = value;
has_state = true;
}
if (last_pair) {
break;
} else {
cursor = next_comma + 1;
}
}
if (has_code && has_state) {
disable_default_handling = true;
const char* data = "Moved\n";
CefString location = CefString(this->internal_url + "oauth.html?code=" + std::string(code) + "&state=" + std::string(state));
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 302, "text/plain", location);
} else {
return nullptr;
}
}
if ((schema != "http" && schema != "https") || request_url.size() < colon + 4
|| request_url.at(colon + 1) != '/' || request_url.at(colon + 2) != '/')
{
return nullptr;
}
// parse all the important parts of the URL as string_views
const std::string::size_type next_hash = request_url.find_first_of('#', colon + 3);
const auto url_end = next_hash == std::string::npos ? request_url.end() : request_url.begin() + next_hash;
const std::string::size_type next_sep = request_url.find_first_of('/', colon + 3);
const std::string::size_type next_question = request_url.find_first_of('?', colon + 3);
const auto domain_end = next_sep == std::string::npos && next_question == std::string::npos ? url_end : request_url.begin() + std::min(next_sep, next_question);
const std::string_view domain(request_url.begin() + colon + 3, domain_end);
const std::string_view path(domain_end, next_question == std::string::npos ? url_end : request_url.begin() + next_question);
const std::string_view query(request_url.begin() + next_question + 1, next_question == std::string::npos ? request_url.begin() + next_question + 1 : url_end);
const std::string_view comment(next_hash == std::string::npos ? request_url.end() : request_url.begin() + next_hash + 1, request_url.end());
// handler for another custom request thing, but this one uses localhost, for whatever reason
if (domain == "localhost" && path == "/" && comment.starts_with("code=")) {
disable_default_handling = true;
const char* data = "Moved\n";
CefString location = CefString(this->internal_url + "game_auth.html?" + std::string(comment));
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 302, "text/plain", location);
}
// internal pages
if (domain == "bolt-internal") {
disable_default_handling = true;
// instruction to launch RS3 .deb
if (path == "/launch-deb") {
CefRefPtr<CefPostData> post_data = request->GetPostData();
auto cursor = 0;
bool has_hash = false;
bool should_set_access_token = false;
bool should_set_refresh_token = false;
bool should_set_session_id = false;
bool should_set_character_id = false;
bool should_set_display_name = false;
std::string hash;
std::string env_access_token;
std::string env_refresh_token;
std::string env_session_id;
std::string env_character_id;
std::string env_display_name;
while (true) {
const std::string::size_type next_eq = query.find_first_of('=', cursor);
const std::string::size_type next_amp = query.find_first_of('&', cursor);
if (next_eq == std::string::npos) break;
if (next_eq >= next_amp) {
cursor = next_amp + 1;
continue;
}
std::string_view key(query.begin() + cursor, query.begin() + next_eq);
if (key == "hash") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
hash = CefURIDecode(value, true, UU_SPACES).ToString();
has_hash = true;
}
if (key == "jx_access_token") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_access_token = std::string("JX_ACCESS_TOKEN=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_access_token = true;
}
if (key == "jx_refresh_token") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_refresh_token = std::string("JX_REFRESH_TOKEN=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_refresh_token = true;
}
if (key == "jx_session_id") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_session_id = std::string("JX_SESSION_ID=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_session_id = true;
}
if (key == "jx_character_id") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_character_id = std::string("JX_CHARACTER_ID=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_character_id = true;
}
if (key == "jx_display_name") {
auto value_end = next_amp == std::string::npos ? query.end() : query.begin() + next_amp;
CefString value = std::string(std::string_view(query.begin() + next_eq + 1, value_end));
env_display_name = std::string("JX_DISPLAY_NAME=") + CefURIDecode(value, true, UU_SPACES).ToString();
should_set_display_name = true;
}
if (next_amp == std::string::npos) break;
cursor = next_amp + 1;
}
if (has_hash) {
CefRefPtr<CefPostData> post_data = request->GetPostData();
if (post_data == nullptr || post_data->GetElementCount() != 1) {
const char* data = "Bad Request";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
CefPostData::ElementVector vec;
post_data->GetElements(vec);
size_t deb_size = vec[0]->GetBytesCount();
unsigned char* deb = new unsigned char[deb_size];
vec[0]->GetBytes(deb_size, deb);
struct archive* ar = archive_read_new();
archive_read_support_format_ar(ar);
archive_read_open_memory(ar, deb, deb_size);
bool entry_found = false;
struct archive_entry* entry;
while (true) {
int r = archive_read_next_header(ar, &entry);
if (r == ARCHIVE_EOF) break;
if (r != ARCHIVE_OK) {
archive_read_close(ar);
archive_read_free(ar);
delete[] deb;
const char* data = "Malformed .deb file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
if (strcmp(archive_entry_pathname(entry), "data.tar.xz") == 0) {
entry_found = true;
break;
}
}
if (!entry_found) {
archive_read_close(ar);
archive_read_free(ar);
delete[] deb;
const char* data = "No data in .deb file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
const long entry_size = archive_entry_size(entry);
unsigned char* tar_xz = new unsigned char[entry_size];
long written = 0;
while (written < entry_size) {
written += archive_read_data(ar, tar_xz + written, entry_size - written);
}
archive_read_close(ar);
archive_read_free(ar);
delete[] deb;
struct archive* xz = archive_read_new();
archive_read_support_format_tar(xz);
archive_read_support_filter_xz(xz);
archive_read_open_memory(xz, tar_xz, entry_size);
entry_found = false;
while (true) {
int r = archive_read_next_header(xz, &entry);
if (r == ARCHIVE_EOF) break;
if (r != ARCHIVE_OK) {
archive_read_close(xz);
archive_read_free(xz);
delete[] tar_xz;
const char* data = "Malformed .tar.xz file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
if (strcmp(archive_entry_pathname(entry), tar_xz_inner_path) == 0) {
entry_found = true;
break;
}
}
if (!entry_found) {
archive_read_close(xz);
archive_read_free(xz);
delete[] tar_xz;
const char* data = "No target executable in .tar.xz file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 400, "text/plain");
}
const long game_size = archive_entry_size(entry);
char* game = new char[game_size];
written = 0;
while (written < game_size) {
written += archive_read_data(ar, game + written, game_size - written);
}
archive_read_close(xz);
archive_read_free(xz);
delete[] tar_xz;
written = 0;
int file = open(this->rs3_path.c_str(), O_WRONLY | O_CREAT, 0755);
if (file == -1) {
delete[] game;
const char* data = "Failed to save executable; if the game is already running, close it and try again\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 500, "text/plain");
}
while (written < game_size) {
written += write(file, game + written, game_size - written);
}
close(file);
delete[] game;
}
posix_spawn_file_actions_t file_actions;
posix_spawnattr_t attributes;
pid_t pid;
posix_spawn_file_actions_init(&file_actions);
posix_spawnattr_init(&attributes);
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSID);
std::string path_str(this->rs3_path.c_str());
char* argv[2];
argv[0] = path_str.data();
argv[1] = nullptr;
bool should_set_home = true;
std::string env_home = this->env_key_home + this->data_dir.c_str();
char** env = new char*[this->env_count + 7];
size_t i;
for (i = 0; i < this->env_count; i += 1) {
if (should_set_access_token && strncmp(environ[i], this->env_key_access_token.c_str(), this->env_key_access_token.size()) == 0) {
should_set_access_token = false;
env[i] = env_access_token.data();
} else if (should_set_refresh_token && strncmp(environ[i], this->env_key_refresh_token.c_str(), this->env_key_refresh_token.size()) == 0) {
should_set_refresh_token = false;
env[i] = env_refresh_token.data();
} else if (should_set_session_id && strncmp(environ[i], this->env_key_session_id.c_str(), this->env_key_session_id.size()) == 0) {
should_set_session_id = false;
env[i] = env_session_id.data();
} else if (should_set_character_id && strncmp(environ[i], this->env_key_character_id.c_str(), this->env_key_character_id.size()) == 0) {
should_set_character_id = false;
env[i] = env_character_id.data();
} else if (should_set_display_name && strncmp(environ[i], this->env_key_display_name.c_str(), this->env_key_display_name.size()) == 0) {
should_set_display_name = false;
env[i] = env_display_name.data();
} else if (strncmp(environ[i], this->env_key_home.c_str(), this->env_key_home.size()) == 0) {
should_set_home = false;
env[i] = env_home.data();
} else {
env[i] = environ[i];
}
}
if (should_set_access_token) {
env[i] = env_access_token.data();
i += 1;
}
if (should_set_refresh_token) {
env[i] = env_refresh_token.data();
i += 1;
}
if (should_set_session_id) {
env[i] = env_session_id.data();
i += 1;
}
if (should_set_character_id) {
env[i] = env_character_id.data();
i += 1;
}
if (should_set_display_name) {
env[i] = env_display_name.data();
i += 1;
}
if (should_set_home) {
env[i] = env_home.data();
i += 1;
}
env[i] = nullptr;
int r = posix_spawn(&pid, path_str.c_str(), &file_actions, &attributes, argv, env);
posix_spawnattr_destroy(&attributes);
posix_spawn_file_actions_destroy(&file_actions);
delete[] env;
if (r == 0) {
fmt::print("[B] Successfully spawned game process with pid {}\n", pid);
if (has_hash) {
size_t written = 0;
std::filesystem::path hash_path(this->data_dir);
hash_path.append("rs3linux.sha256");
int file = open(hash_path.c_str(), O_WRONLY | O_CREAT, 0644);
if (file == -1) {
const char* data = "OK, but unable to save hash file\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 200, "text/plain");
}
while (written < hash.size()) {
written += write(file, hash.c_str() + written, hash.size() - written);
}
close(file);
}
const char* data = "OK\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 200, "text/plain");
} else {
fmt::print("[B] Error from posix_spawn: {}\n", r);
const char* data = "Error spawning process\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 500, "text/plain");
}
}
// respond using internal hashmap of filenames
auto it = std::find_if(
this->internal_pages->begin(),
this->internal_pages->end(),
[&path](const auto& e) { return e.first == path; }
);
if (it != this->internal_pages->end()) {
if (it->second.success) {
return new ResourceHandler(
it->second.data.data(),
it->second.data.size(),
200,
it->second.mime_type
);
} else {
// this file failed to load during startup for some reason - 500
const char* data = "Internal Server Error\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 500, "text/plain");
}
} else {
// no matching internal page - 404
const char* data = "Not Found\n";
return new ResourceHandler(reinterpret_cast<const unsigned char*>(data), strlen(data), 404, "text/plain");
}
}
return nullptr;
}