Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 111 additions & 7 deletions include/optionx_cpp/platforms/IntradeBarPlatform/RequestManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,117 @@ namespace optionx::platforms::intrade_bar {
return;
}

auto login_result = parse_login(response->content);
if (!login_result) {
// Newer legacy-broker responses contain an opaque token in a
// JavaScript redirect. Follow it so the broker can establish the
// user_id/user_hash cookies before continuing with /auth.
const auto redirect_url = parse_login_redirect_url(response->content);
const bool has_legacy_credentials = redirect_url &&
redirect_url->find("id=") != std::string::npos &&
redirect_url->find("hash=") != std::string::npos;

std::string redirect_path;
if (redirect_url) {
const auto redirect_target = resolve_login_redirect_target(*redirect_url);
if (!redirect_target) {
const std::string reason("Malformed login redirect URL.");
LOGIT_ERROR(reason);
result_callback(
false,
std::string(),
std::string(),
std::string(),
reason);
return;
}

redirect_path = redirect_target->path;
if (redirect_target->origin) {
if (!is_allowed_intrade_redirect_origin(*redirect_target->origin)) {
const std::string reason("Rejected login redirect origin.");
LOGIT_ERROR(reason, " origin=", *redirect_target->origin);
result_callback(
false,
std::string(),
std::string(),
std::string(),
reason);
return;
}

// The landing page may issue the one-time token on a
// different legacy-broker origin (for example, intrade.bar
// -> intrade35.bar). Keep the follow-up request and the
// subsequent /auth call on the origin selected by the broker.
auto& client = get_http_client();
client.set_host(*redirect_target->origin);
client.set_origin(*redirect_target->origin);
client.set_referer(*redirect_target->origin + "/");
}
}

if (!redirect_url || has_legacy_credentials) {
auto login_result = parse_login(response->content);
if (login_result) {
const auto [user_id, user_hash] = *login_result;
result_callback(true, user_id, user_hash, cookies, std::string());
return;
}
}

if (redirect_url) {
const std::string login_response_cookies = merge_set_cookies(
cookies,
response->headers);
auto redirect_future = get_http_client().get(
redirect_path,
kurlyk::QueryParams(),
{
{"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"},
{"Upgrade-Insecure-Requests", "1"},
{"Connection", "keep-alive"},
{"Cookie", login_response_cookies}
},
get_rate_limit(RateLimitType::ACCOUNT_INFO)
);

auto redirect_callback = [cookies = login_response_cookies, result_callback](
kurlyk::HttpResponsePtr redirect_response) {
if (!validate_response(redirect_response, [&result_callback](const std::string& error_text){
result_callback(false, std::string(), std::string(), std::string(), error_text);
})) {
return;
}

const std::string merged_cookies = merge_set_cookies(
cookies,
redirect_response->headers);
const auto parsed_cookies = parse_cookies(merged_cookies);
if (!parsed_cookies) {
LOGIT_PRINT_ERROR("Failed to parse cookies after login redirect.");
result_callback(
false,
std::string(),
std::string(),
std::string(),
"Failed to parse login cookies.");
return;
}

const auto& [user_id, user_hash] = *parsed_cookies;
result_callback(
true,
user_id,
user_hash,
merged_cookies,
std::string());
};

add_http_request_task(
std::move(redirect_future),
std::move(redirect_callback));
return;
}

# ifdef OPTIONX_LOG_UNIQUE_FILE_INDEX
const int log_index = OPTIONX_LOG_UNIQUE_FILE_INDEX;
LOGIT_STREAM_ERROR_TO(log_index) << response->content;
Expand All @@ -580,11 +689,6 @@ namespace optionx::platforms::intrade_bar {
LOGIT_PRINT_ERROR("Failed to parse login.");
# endif
result_callback(false, std::string(), std::string(), std::string(), "Failed to parse login.");
return;
}

const auto [user_id, user_hash] = *login_result;
result_callback(true, user_id, user_hash, cookies, std::string());
};

// Add the task to handle the HTTP request
Expand Down
117 changes: 116 additions & 1 deletion include/optionx_cpp/platforms/IntradeBarPlatform/http_parsers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ namespace optionx::platforms::intrade_bar {
try {
std::string user_id, user_hash, fragment;
// Extract "/auth/" fragment
if (utils::extract_between(content, "/auth/", "'", fragment) == std::string::npos || fragment.empty()) {
if (utils::extract_between(content, "/auth/", "'", fragment) == std::string::npos) {
// Some deployments quote the redirect with double quotes.
utils::extract_between(content, "/auth/", "\"", fragment);
}
if (fragment.empty()) {
LOGIT_ERROR("Failed to extract auth fragment.");
return std::nullopt;
}
Expand All @@ -65,6 +69,117 @@ namespace optionx::platforms::intrade_bar {
}
}

/// \brief Extracts the redirect URL emitted by the current legacy login page.
///
/// Older responses embedded `id` and `hash` directly in an `/auth/` URL.
/// The broker now returns an opaque one-time token from JavaScript instead,
/// for example `window.location.replace('https://intrade35.bar/auth/token')`.
/// \param content The HTML/JavaScript response body.
/// \return The redirect URL or path when one is present.
inline std::optional<std::string> parse_login_redirect_url(const std::string& content) {
static const std::regex redirect_regex(
R"(window\s*\.\s*location\s*\.\s*replace\s*\(\s*(['"])((?:https?://[^'"]+|/auth/[^'"]+))\1\s*\))",
std::regex::icase);

std::smatch match;
if (!std::regex_search(content, match, redirect_regex) || match.size() < 3) {
return std::nullopt;
}

const std::string redirect_url = match[2].str();
if (redirect_url.find("/auth/") == std::string::npos) {
return std::nullopt;
}
return redirect_url;
}

/// \brief Extracts the origin from an absolute login redirect URL.
/// \param redirect_url Absolute or relative redirect URL.
/// \return The `scheme://authority` origin for an absolute URL; empty for a relative path.
inline std::optional<std::string> parse_login_redirect_origin(
const std::string& redirect_url) {
const auto scheme_end = redirect_url.find("://");
if (scheme_end == std::string::npos || scheme_end == 0) {
return std::nullopt;
}

const auto authority_start = scheme_end + 3;
if (authority_start >= redirect_url.size()) {
return std::nullopt;
}

const auto path_start = redirect_url.find_first_of("/?#", authority_start);
const std::string origin = path_start == std::string::npos
? redirect_url
: redirect_url.substr(0, path_start);
if (origin.size() <= authority_start) {
return std::nullopt;
}
return origin;
}

/// \brief Validates the trusted origin family used by legacy Intrade login.
/// \param origin The `scheme://authority` origin to validate.
/// \return True only for HTTPS `intrade.bar` or `intrade<N>.bar` origins.
inline bool is_allowed_intrade_redirect_origin(const std::string& origin) {
static const std::regex allowed_origin_regex(
R"(^https://intrade(?:[0-9]+)?\.bar$)",
std::regex::icase);
return std::regex_match(origin, allowed_origin_regex);
}

/// \brief Resolves a login redirect into a request path and optional origin.
/// \param redirect_url Absolute or relative redirect URL.
/// \return The path and origin, or empty when an absolute URL is malformed.
struct LoginRedirectTarget {
std::string path;
std::optional<std::string> origin;
};

inline std::optional<LoginRedirectTarget> resolve_login_redirect_target(
const std::string& redirect_url) {
LoginRedirectTarget target;
target.path = redirect_url;
target.origin = parse_login_redirect_origin(redirect_url);

if (!target.origin) {
if (redirect_url.find("://") != std::string::npos) {
return std::nullopt;
}
return target;
}

const auto scheme_end = redirect_url.find("://");
const auto path_start = redirect_url.find('/', scheme_end + 3);
target.path = path_start == std::string::npos
? "/"
: redirect_url.substr(path_start);
return target;
}

/// \brief Merges response `Set-Cookie` headers into an existing cookie string.
/// \param cookies Cookies collected before the response.
/// \param headers HTTP response headers that may contain `Set-Cookie` values.
/// \return Cookie header value containing the existing and newly received cookies.
inline std::string merge_set_cookies(
const std::string& cookies,
const kurlyk::Headers& headers) {
kurlyk::Cookies merged = kurlyk::utils::parse_cookie(cookies);
bool changed = false;

const auto range = headers.equal_range("set-cookie");
for (auto it = range.first; it != range.second; ++it) {
const kurlyk::Cookies response_cookies = kurlyk::utils::parse_cookie(it->second);
for (const auto& cookie : response_cookies) {
merged.erase(cookie.first);
merged.emplace(cookie.first, cookie.second);
changed = true;
}
}

return changed ? kurlyk::utils::to_cookie_string(merged) : cookies;
}

/// \brief Parses balance information and detects the currency.
/// \param content Raw HTML fragment containing the balance value and currency symbol.
/// \return Optional pair of balance amount and detected currency type. Returns
Expand Down
Loading
Loading