added url override

This commit is contained in:
georgemoralis 2026-06-06 23:38:00 +03:00
parent 05fa358890
commit 545cbd362f

View File

@ -8,16 +8,19 @@
#include <condition_variable>
#include <cstring>
#include <deque>
#include <fstream>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <nlohmann/json.hpp>
#include "common/logging/log.h"
#include "common/path_util.h"
#include "core/emulator_settings.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/kernel/orbis_error.h"
@ -198,6 +201,35 @@ static bool ResolveEpollBinding(int id, int*& epoll_id_out, void**& user_arg_out
static bool HeaderNameMatches(std::string_view a, std::string_view b);
static std::string HttpStatusLabel(int sc);
// JSON shape: flat object mapping endpoint -> replacement URL. Example:
// {
// "api.something.dev": "http://localhost:8080",
// "discovery.something.com:5300": "http://localhost:8080",
// "https://api.example.com:443": "http://localhost:8081",
// "*": "http://localhost:8080"
// }
//
// Keys are tried in order of specificity, most-specific first:
// 1. "scheme://host:port" - matches that exact endpoint
// 2. "host:port" - matches host+port on any scheme
// 3. "host" - matches host on any scheme/port (most common)
// 4. "*" - catch-all fallback
//
// Replacement value is a URL with scheme + host + optional port. When port is
// omitted the default for the scheme is used (80 for http, 443 for https).
// When scheme is omitted the connection's original scheme is preserved.
struct HostOverrideTarget {
std::string scheme; // "http", "https", or "" to mean "preserve original"
std::string host;
u16 port = 0; // 0 means "preserve original (or default-for-scheme if scheme changed)"
};
// Parse a JSON string into a hostname to target map
std::unordered_map<std::string, HostOverrideTarget> ParseHostOverridesJson(
const std::string& json_text);
bool ApplyHostOverride(std::string& scheme, std::string& host, u16& port, bool& is_secure);
// Populate a response object with the shape a transport-level failure produces:
// no status line, no headers, no body. Used by the no-internet path.
static void SynthesizeTransportFailureResponse(HttpResponse& res) {
@ -209,6 +241,169 @@ static void SynthesizeTransportFailureResponse(HttpResponse& res) {
res.all_headers_blob.clear();
}
// Parse a single replacement value into a HostOverrideTarget. Accepts forms:
// "host" preserve scheme, preserve port
// "host:port" preserve scheme, set port
// "http://host" set scheme to http, port defaults to 80
// "https://host" set scheme to https, port defaults to 443
// "http://host:port" set scheme + port explicitly
// "https://host:port" set scheme + port explicitly
// Bad ports fall back to port=0
static HostOverrideTarget ParseHostOverrideTarget(std::string_view value) {
HostOverrideTarget out;
// Pull off scheme prefix if present.
if (const auto scheme_end = value.find("://"); scheme_end != std::string_view::npos) {
std::string scheme(value.substr(0, scheme_end));
std::transform(scheme.begin(), scheme.end(), scheme.begin(),
[](unsigned char c) { return std::tolower(c); });
if (scheme == "http" || scheme == "https") {
out.scheme = std::move(scheme);
}
// Unknown scheme,leave out.scheme empty
value = value.substr(scheme_end + 3);
}
// Now value is "host" or "host:port".
if (const auto colon = value.find(':'); colon != std::string_view::npos) {
out.host.assign(value.substr(0, colon));
try {
const unsigned long p = std::stoul(std::string(value.substr(colon + 1)));
if (p > 0 && p <= 65535) {
out.port = static_cast<u16>(p);
}
} catch (...) {
// ignore - port stays 0
}
} else {
out.host.assign(value);
}
return out;
}
std::unordered_map<std::string, HostOverrideTarget> ParseHostOverridesJson(
const std::string& json_text) {
std::unordered_map<std::string, HostOverrideTarget> out;
if (json_text.empty()) {
return out;
}
nlohmann::json root;
try {
root = nlohmann::json::parse(json_text);
} catch (const std::exception& e) {
LOG_ERROR(Lib_Http, "host overrides JSON parse failed: {}", e.what());
return out;
}
if (!root.is_object()) {
LOG_ERROR(Lib_Http, "host overrides JSON root must be an object");
return out;
}
for (auto it = root.begin(); it != root.end(); ++it) {
.if (!it.key().empty() && it.key().front() == '_') {
continue;
}
if (!it.value().is_string()) {
LOG_ERROR(Lib_Http, "host overrides JSON: value for '{}' is not a string; skipped",
it.key());
continue;
}
const std::string value = it.value().get<std::string>();
if (value.empty()) {
LOG_ERROR(Lib_Http, "host overrides JSON: value for '{}' is empty; skipped", it.key());
continue;
}
out.emplace(it.key(), ParseHostOverrideTarget(value));
}
return out;
}
struct HostOverrideState {
bool loaded = false;
std::unordered_map<std::string, HostOverrideTarget> entries;
};
static HostOverrideState LoadHostOverrideState() {
HostOverrideState s;
s.loaded = true;
std::filesystem::path path;
if (const char* path_env = std::getenv("SHADPS4_HTTP_HOST_OVERRIDES_JSON");
path_env && path_env[0]) {
// Explicit override path - useful for dev / testing.
path = path_env;
} else {
path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "host_overrides.json";
}
std::ifstream f(path);
if (!f.is_open()) {
return s;
}
std::stringstream buf;
buf << f.rdbuf();
s.entries = ParseHostOverridesJson(buf.str());
LOG_INFO(Lib_Http, "loaded {} host override entries from {}", s.entries.size(), path.string());
return s;
}
static const HostOverrideState& GetHostOverrideState() {
static const HostOverrideState s = LoadHostOverrideState();
return s;
}
bool ApplyHostOverride(std::string& scheme, std::string& host, u16& port, bool& is_secure) {
const auto& state = GetHostOverrideState();
if (state.entries.empty()) {
return false;
}
// Look up most-specific match first. Keys can be:
// "scheme://host:port" - matches that exact endpoint
// "host:port" - matches host+port on any scheme
// "host" - matches host on any scheme/port
// "*" - catch-all fallback
const std::string full_key = scheme + "://" + host + ":" + std::to_string(port);
const std::string host_port_key = host + ":" + std::to_string(port);
auto it = state.entries.find(full_key);
if (it == state.entries.end()) {
it = state.entries.find(host_port_key);
}
if (it == state.entries.end()) {
it = state.entries.find(host);
}
if (it == state.entries.end()) {
it = state.entries.find("*");
}
if (it == state.entries.end()) {
return false;
}
const std::string orig_scheme = scheme;
const std::string orig_host = host;
const u16 orig_port = port;
const HostOverrideTarget& target = it->second;
host = target.host;
// Scheme handling: explicit scheme in JSON wins; otherwise preserve.
if (!target.scheme.empty()) {
scheme = target.scheme;
is_secure = (target.scheme == "https");
}
if (target.port != 0) {
port = target.port;
} else if (!target.scheme.empty() && target.scheme != orig_scheme) {
if (orig_scheme == "https" && port == 443) {
port = 80;
} else if (orig_scheme == "http" && port == 80) {
port = 443;
}
}
LOG_INFO(Lib_Http, "host override active: {}://{}:{} -> {}://{}:{} (matched key '{}')",
orig_scheme, orig_host, orig_port, scheme, host, port, it->first);
return true;
}
struct SendRequestPlan {
std::string scheme;
std::string host;
@ -979,16 +1174,19 @@ int PS4_SYSV_ABI sceHttpCreateConnection(int tmplId, const char* serverName, con
return sc;
}
const std::string scheme_str = is_secure ? "https" : "http";
std::string scheme_str = is_secure ? "https" : "http";
std::string host_str = serverName;
u16 effective_port = port;
ApplyHostOverride(scheme_str, host_str, effective_port, is_secure);
const int conn_id = ++g_state.next_obj_id;
HttpConnection conn;
conn.tmpl_id = tmplId;
conn.scheme = scheme_str;
conn.hostname = serverName;
conn.port = port;
conn.hostname = host_str;
conn.port = effective_port;
conn.keep_alive = (isEnableKeepalive != 0);
conn.is_secure = is_secure;
conn.url = scheme_str + "://" + serverName + ":" + std::to_string(port);
conn.url = scheme_str + "://" + host_str + ":" + std::to_string(effective_port);
if (auto tmpl_it = g_state.templates.find(tmplId); tmpl_it != g_state.templates.end()) {
conn.settings = tmpl_it->second.settings;
conn.epoll_id = tmpl_it->second.epoll_id;
@ -1046,6 +1244,8 @@ int PS4_SYSV_ABI sceHttpCreateConnectionWithURL(int tmplId, const char* url, boo
scheme_str = is_secure ? "https" : "http";
u16 port = parsed.port;
std::string host_str = parsed.hostname;
ApplyHostOverride(scheme_str, host_str, port, is_secure);
std::lock_guard<std::mutex> lock(g_state.m_mutex);
if (!g_state.inited) {
@ -1062,7 +1262,7 @@ int PS4_SYSV_ABI sceHttpCreateConnectionWithURL(int tmplId, const char* url, boo
conn.tmpl_id = tmplId;
conn.url = url;
conn.scheme = scheme_str;
conn.hostname = parsed.hostname;
conn.hostname = host_str;
conn.port = port;
conn.keep_alive = enableKeepalive;
conn.is_secure = is_secure;
@ -1734,6 +1934,17 @@ int PS4_SYSV_ABI sceHttpTerm(int libhttpCtxId) {
// Last context torn down - wipe all dependent objects.
LOG_INFO(Lib_Http, "last context terminated, clearing state");
g_state.shutting_down.store(true);
size_t in_flight = 0;
for (auto& [id, req_ptr] : g_state.requests) {
if (req_ptr->state == HttpRequestState::Sending) {
++in_flight;
}
}
if (in_flight > 0) {
LOG_INFO(Lib_Http,
"Term: {} request(s) still in flight,results will be abandoned"),
in_flight);
}
for (auto& [id, req_ptr] : g_state.requests) {
req_ptr->deleted = true;
req_ptr->state = HttpRequestState::Aborted;