Files
2025-09-18 17:55:52 -04:00

674 lines
21 KiB
C++

#include "stdafx.h"
#include "HttpTools.h"
#include "SharedHelpers.h"
#include "atlutil.h"
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include "wininet.h"
#include <strstream>
#pragma comment (lib, "Wininet.lib")
#include <boost/foreach.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/lexical_cast.hpp>
static const std::string sContentLength = "content-length: ";
static const std::string sEtag = "etag: ";
namespace HttpTools
{
class WININETHINTERNET : boost::noncopyable
{
HINTERNET handle;
public:
WININETHINTERNET(HINTERNET handle):handle(handle) {}
WININETHINTERNET():handle(0) {}
WININETHINTERNET& operator = (HINTERNET handle)
{
::InternetCloseHandle(this->handle);
this->handle = handle;
return *this;
}
operator bool() { return handle!=0; }
operator HINTERNET() { return handle; }
~WININETHINTERNET()
{
::InternetCloseHandle(handle);
}
};
int httpWinInet(IInstallerSite *site, const char* method, const std::string& host, const std::string& path, std::istream& input, const char* contentType, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress);
int httpBoost(IInstallerSite *site, const char* method, const std::string& host, const std::string& path, std::istream& input, const char* contentType, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress);
int http(IInstallerSite *site, const char* method, const std::string& host, const std::string& path, std::istream& input, const char* contentType, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress, bool log = true);
static void dummyProgress(int, int) {}
const std::string getPrimaryCdnHost(IInstallerSite *site)
{
static std::string cdnHost;
static bool cdnHostLoaded = false;
static bool validCdnHost = false;
if(!cdnHostLoaded)
{
if (site->ReplaceCdnTxt()) {
std::string host = site->InstallHost();
std::string prod ("setup.watrbx.wtf");
if (host.compare(prod) == 0) {
cdnHost = "setup.watrbx.wtf";
validCdnHost = true;
LLOG_ENTRY1(site->Logger(), "primaryCdn: %s", cdnHost.c_str());
} else {
cdnHost = host;
validCdnHost = true;
LLOG_ENTRY1(site->Logger(), "primaryCdn: %s", cdnHost.c_str());
}
} else {
try
{
std::ostrstream result;
int status_code;
switch(status_code = httpGet(site, site->InstallHost(), "/cdn.txt", std::string(), result, false, boost::bind(&dummyProgress, _1, _2)))
{
case 200:
case 304:
result << (char) 0;
cdnHost = result.str();
LLOG_ENTRY1(site->Logger(), "primaryCdn: %s", cdnHost.c_str());
validCdnHost = true;
break;
default:
validCdnHost = false;
LLOG_ENTRY1(site->Logger(), "primaryCdn failure code=%d, falling back to secondary installHost", status_code);
break;
}
}
catch(std::exception&)
{
//Quash exceptions, set validCdnHost to false
validCdnHost = false;
LLOG_ENTRY(site->Logger(), "primaryCdn exception, falling back to secondary installHost");
}
}
//Only try to load the CDN once, then give up
cdnHostLoaded = true;
}
return cdnHost;
}
const std::string getCdnHost(IInstallerSite *site)
{
static std::string cdnHost;
static bool cdnHostLoaded = false;
static bool validCdnHost = false;
if(!cdnHostLoaded)
{
try
{
std::ostrstream result;
int status_code;
switch(status_code = httpGet(site, site->BaseHost(), "/install/GetInstallerCdns.ashx", std::string(), result, false, boost::bind(&dummyProgress, _1, _2)))
{
case 200:
case 304:
{
result << (char) 0;
LLOG_ENTRY1(site->Logger(), "primaryCdns: %s", result.str());
std::stringstream stream(result.str());
boost::property_tree::ptree ptree;
boost::property_tree::json_parser::read_json(stream, ptree);
std::vector<std::pair<std::string, int>> cdns;
int totalValue = 0;
BOOST_FOREACH(const boost::property_tree::ptree::value_type& child, ptree.get_child(""))
{
int value = boost::lexical_cast<int>(child.second.data());
if (cdns.size() > 0)
cdns.push_back(std::make_pair(child.first.data(), cdns.back().second + value));
else
cdns.push_back(std::make_pair(child.first.data(), boost::lexical_cast<int>(child.second.data())));
totalValue += value;
}
if (cdns.size() && (totalValue > 0))
{
// randomly pick a cdn
int r = rand() % totalValue;
for (unsigned int i = 0; i < cdns.size(); i++)
{
if (r < cdns[i].second)
{
cdnHost = cdns[i].first;
validCdnHost = true;
break;
}
}
}
break;
}
default:
validCdnHost = false;
LLOG_ENTRY1(site->Logger(), "primaryCdn failure code=%d, falling back to secondary installHost", status_code);
break;
}
}
catch(std::exception&)
{
LLOG_ENTRY(site->Logger(), "primaryCdn exception, falling back to secondary installHost");
}
//Only try to load the CDN once, then give up
cdnHostLoaded = true;
}
return cdnHost;
}
using boost::asio::ip::tcp;
class CInternet : boost::noncopyable
{
HINTERNET handle;
public:
CInternet(HINTERNET handle):handle(handle) {}
CInternet():handle(0) {}
CInternet& operator = (HINTERNET handle)
{
::InternetCloseHandle(handle);
this->handle = handle;
return *this;
}
operator bool() { return handle!=0; }
operator HINTERNET() { return handle; }
~CInternet()
{
::InternetCloseHandle(handle);
}
};
class Buffer : boost::noncopyable
{
void* const data;
public:
Buffer(size_t size):data(malloc(size)) {}
~Buffer() { free(data); }
operator const void*() const { return data; }
operator void*() { return data; }
operator char*() { return (char*)data; }
};
int httpGet(IInstallerSite *site, std::string host, std::string path, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress, bool log)
{
try
{
std::string tmp = etag;
std::strstream input;
int i = http(site, "GET", host, path, input, NULL, tmp, result, ignoreCancel, progress, log);
etag = tmp;
return i;
}
catch (silent_exception&)
{
throw;
}
catch (std::exception& e)
{
LLOG_ENTRY2(site->Logger(), "WARNING: First HTTP GET error for %s: %s", path.c_str(), exceptionToString(e).c_str());
std::strstream input;
result.seekp(0);
result.clear();
// try again
return http(site, "GET", host, path, input, NULL, etag, result, ignoreCancel, progress, log);
}
}
std::string httpGetString(const std::string& url)
{
CUrl u;
u.CrackUrl(convert_s2w(url).c_str());
const bool isSecure = u.GetScheme() == ATL_URL_SCHEME_HTTPS;
// Initialize the User Agent
WININETHINTERNET session = InternetOpen(L"Roblox/WinInet", PRE_CONFIG_INTERNET_ACCESS, NULL, NULL, 0);
if (!session)
{
throw std::runtime_error("httpGetString - InternetOpen ERROR");
}
WININETHINTERNET connection = ::InternetConnect(session, u.GetHostName(), u.GetPortNumber(), u.GetUserName(), u.GetPassword(), INTERNET_SERVICE_HTTP, 0, 0);
if (!connection)
{
throw std::runtime_error("httpGetString - InternetConnect ERROR");
}
CString s = u.GetUrlPath();
s += u.GetExtraInfo();
WININETHINTERNET request = ::HttpOpenRequest(connection, _T("GET"), s, HTTP_VERSION, _T(""), NULL, isSecure ? INTERNET_FLAG_SECURE : 0, 0);
if (!request)
{
throw std::runtime_error("httpGetString - HttpOpenRequest ERROR");
}
DWORD httpSendResult = ::HttpSendRequest(request, NULL, 0, 0, 0);
if (!httpSendResult)
{
throw std::runtime_error("httpGetString - HttpSendRequest ERROR");
}
std::ostringstream data;
while (true)
{
DWORD numBytes;
if (!::InternetQueryDataAvailable(request, &numBytes, 0, 0))
{
DWORD err = GetLastError();
if(err == ERROR_IO_PENDING)
{
Sleep(100); // we block on data.
continue;
}
else
{
throw std::runtime_error("httpGetString - InternetQueryDataAvailable ERROR");
}
}
if (numBytes==0)
break; // EOF
if (numBytes == -1)
{
throw std::runtime_error("httpGetString - No Settings ERROR");
}
char* buffer = (char*)malloc(numBytes + 1);
DWORD bytesRead;
throwLastError(::InternetReadFile(request, (LPVOID) buffer, numBytes, &bytesRead), "InternetReadFile failed");
data.write(buffer, bytesRead);
free(buffer);
}
return data.str();
}
int httpWinInet(IInstallerSite *site, const char* method, const std::string& host, const std::string& path, std::istream& input, const char* contentType, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress)
{
CUrl u;
BOOL urlCracked;
#ifdef UNICODE
urlCracked = u.CrackUrl(convert_s2w(host).c_str());
#else
urlCracked = u.CrackUrl(host.c_str());
#endif
// Initialize the User Agent
CInternet session = InternetOpen(_T("Roblox/WinInet"), PRE_CONFIG_INTERNET_ACCESS, NULL, NULL, 0);
if (!session)
throw std::runtime_error(format_string("InternetOpen failed for %s http://%s%s, Error Code: %d", method, host.c_str(), path.c_str(), GetLastError()).c_str());
CInternet connection;
if (urlCracked)
connection = ::InternetConnect(session, u.GetHostName(), u.GetPortNumber(), NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
else
connection = ::InternetConnect(session, convert_s2w(host).c_str(), 80, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 1);
if (!connection)
throw std::runtime_error(format_string("InternetConnect failed for %s http://%s%s, Error Code: %d, Port Number: %d", method, host.c_str(), path.c_str(), GetLastError() , urlCracked ? u.GetPortNumber() : 80).c_str());
// 1. Open HTTP Request (pass method type [get/post/..] and URL path (except server name))
CInternet request = ::HttpOpenRequest(
connection, convert_s2w(method).c_str(), convert_s2w(path).c_str(), NULL, NULL, NULL,
INTERNET_FLAG_KEEP_CONNECTION |
INTERNET_FLAG_EXISTING_CONNECT |
INTERNET_FLAG_NEED_FILE, // ensure that it gets cached
1);
if (!request)
throw std::runtime_error(format_string("HttpOpenRequest failed for %s http://%s%s, Error Code: %d", method, host.c_str(), path.c_str(), GetLastError()).c_str());
if (contentType)
{
std::string header = format_string("Content-Type: %s\r\n", contentType);
throwLastError(::HttpAddRequestHeaders(request, convert_s2w(header).c_str(), header.size(), HTTP_ADDREQ_FLAG_ADD), "HttpAddRequestHeaders failed");
}
size_t uploadSize;
{
size_t x = input.tellg();
input.seekg (0, std::ios::end);
size_t y = input.tellg();
uploadSize = y - x;
input.seekg (0, std::ios::beg);
}
if (uploadSize==0)
{
throwLastError(::HttpSendRequest(request, NULL, 0, 0, 0), "HttpSendRequest failed");
}
else
{
Buffer uploadBuffer(uploadSize);
input.read((char*)uploadBuffer, uploadSize);
// Send the request
{
INTERNET_BUFFERS buffer;
memset(&buffer, 0, sizeof(buffer));
buffer.dwStructSize = sizeof(buffer);
buffer.dwBufferTotal = uploadSize;
if (!HttpSendRequestEx(request, &buffer, NULL, 0, 0))
throw std::runtime_error("HttpSendRequestEx failed");
try
{
DWORD bytesWritten;
throwLastError(::InternetWriteFile(request, uploadBuffer, uploadSize, &bytesWritten), "InternetWriteFile failed");
if (bytesWritten!=uploadSize)
throw std::runtime_error("Failed to upload content");
}
catch (std::exception&)
{
::HttpEndRequest(request, NULL, 0, 0);
throw;
}
throwLastError(::HttpEndRequest(request, NULL, 0, 0), "HttpEndRequest failed");
}
}
// Check the return HTTP Status Code
DWORD statusCode;
{
DWORD dwLen = sizeof(DWORD);
throwLastError(::HttpQueryInfo(request, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &statusCode, &dwLen, NULL), "HttpQueryInfo HTTP_QUERY_STATUS_CODE failed");
}
DWORD contentLength = 0;
{
DWORD dwLen = sizeof(DWORD);
::HttpQueryInfo(request, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &contentLength, &dwLen, NULL);
}
{
CHAR buffer[256];
DWORD dwLen = sizeof(buffer);
if (::HttpQueryInfo(request, HTTP_QUERY_ETAG, &buffer, &dwLen, NULL))
{
etag = buffer;
etag = etag.substr(1, etag.size()-2); // remove the quotes
}
else
etag.clear();
}
DWORD readSoFar = 0;
while (true)
{
DWORD numBytes;
if (!::InternetQueryDataAvailable(request, &numBytes, 0, 0))
numBytes = 0;
if (numBytes==0)
break; // EOF
char buffer[1024];
DWORD bytesRead;
throwLastError(::InternetReadFile(request, (LPVOID) buffer, 1024, &bytesRead), "InternetReadFile failed");
result.write(buffer, bytesRead);
readSoFar += bytesRead;
progress(bytesRead, contentLength);
}
if (statusCode!=HTTP_STATUS_OK)
{
TCHAR buffer[512];
DWORD length = 512;
if (::HttpQueryInfo(request, HTTP_QUERY_STATUS_TEXT, (LPVOID) buffer, &length, 0))
throw std::runtime_error(convert_w2s(buffer).c_str());
else
throw std::runtime_error(format_string("statusCode = %d", statusCode));
}
return statusCode;
}
int httpBoost(IInstallerSite *site, const char* method, const std::string& host, const std::string& path, std::istream& input, const char* contentType, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress, bool log)
{
if (log)
{
if (!etag.empty())
{
LLOG_ENTRY4(site->Logger(), "%s http://%s%s If-None-Match: \"%s\"", method, host.c_str(), path.c_str(), etag.c_str());
}
else
{
LLOG_ENTRY3(site->Logger(), "%s http://%s%s", method, host.c_str(), path.c_str());
}
}
CUrl u;
BOOL urlCracked;
#ifdef UNICODE
urlCracked = u.CrackUrl(convert_s2w(host).c_str());
#else
urlCracked = u.CrackUrl(host.c_str());
#endif
boost::asio::io_service io_service;
tcp::socket socket(io_service);
// Get a list of endpoints corresponding to the server name.
tcp::resolver resolver(io_service);
std::string port = urlCracked ? boost::lexical_cast<std::string>(u.GetPortNumber()) : "http";
std::string hostName = urlCracked ? convert_w2s(u.GetHostName()) : host;
tcp::resolver::query query(hostName, port);
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
tcp::resolver::iterator end;
if (httpBoostPostTimeout > 0 && !std::strcmp(method, "POST"))
{
DWORD timeout = httpBoostPostTimeout * 1000;
setsockopt(socket.native(), SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
setsockopt(socket.native(), SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
}
// Try each endpoint until we successfully establish a connection.
boost::system::error_code error = boost::asio::error::host_not_found;
while (error && endpoint_iterator != end)
{
socket.close();
socket.connect(*endpoint_iterator++, error);
}
if (error)
{
if (log)
{
LLOG_ENTRY1(site->Logger(), "Failed to find an endpoint: %s", error.message().c_str());
LLOG_ENTRY3(site->Logger(), "Trying WinInet for %s http://%s%s", method, host.c_str(), path.c_str());
}
return httpWinInet(site, method, host, path, input, contentType, etag, result, ignoreCancel, progress);
}
size_t inputSize;
{
size_t x = input.tellg();
input.seekg (0, std::ios::end);
size_t y = input.tellg();
inputSize = y - x;
input.seekg (0, std::ios::beg);
}
// Form the request. We specify the "Connection: close" header so that the
// server will close the socket after transmitting the response. This will
// allow us to treat all data up until the EOF as the content.
boost::asio::streambuf request;
std::ostream request_stream(&request);
request_stream << method << " " << path << " HTTP/1.0\r\n";
request_stream << "Host: " << host << "\r\n";
request_stream << "Accept: */*\r\n";
if (inputSize || !std::strcmp(method, "POST"))
request_stream << "Content-Length: " << inputSize << "\r\n";
if (contentType)
request_stream << "Content-Type: " << contentType << "\r\n";
request_stream << "Connection: close\r\n";
if (!etag.empty())
request_stream << "If-None-Match: \"" << etag << "\"\r\n";
// TODO: Accept gzip encoding!
request_stream << "\r\n";
if (inputSize)
std::copy(
std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>(),
std::ostreambuf_iterator<char>(request_stream)
);
// Send the request.
boost::asio::write(socket, request);
// Read the response status line.
boost::asio::streambuf response;
boost::asio::read_until(socket, response, "\r\n");
// Check that response is OK.
std::istream response_stream(&response);
std::string http_version;
response_stream >> http_version;
unsigned int status_code;
response_stream >> status_code;
std::string status_message;
std::getline(response_stream, status_message);
if (!response_stream || http_version.substr(0, 5) != "HTTP/")
throw std::runtime_error("Invalid response");
if (log)
LLOG_ENTRY4(site->Logger(), "%s http://%s%s status_code:%d", method, host.c_str(), path.c_str(), status_code);
switch (status_code)
{
case 200:
case 304:
break;
default:
{
if (log)
{
LLOG_ENTRY2(site->Logger(), "Response returned with bad status code %d: %s", status_code, status_message.c_str());
LLOG_ENTRY3(site->Logger(), "Trying WinInet for %s http://%s%s", method, host.c_str(), path.c_str());
}
return httpWinInet(site, method, host, path, input, contentType, etag, result, ignoreCancel, progress);
}
}
// Read the response headers, which are terminated by a blank line.
boost::asio::read_until(socket, response, "\r\n\r\n");
// Process the response headers.
size_t contentLength = 0;
etag.clear();
std::string header;
while (std::getline(response_stream, header) && header != "\r")
{
// convert header to lower case
std::transform(header.begin(), header.end(), header.begin(), tolower);
if (header.find(sContentLength)==0)
contentLength = atoi(header.substr(sContentLength.size()).c_str());
else if (header.find(sEtag)==0)
// Strip the double-quotes
etag = header.substr(sEtag.size()+1, header.size()-sEtag.size()-3);
if (!ignoreCancel)
site->CheckCancel();
}
if (status_code!=304)
{
// Write whatever content we already have to output.
if (response.size() > 0)
result << &response;
progress(result.tellp(), contentLength);
// Read until EOF, writing data to output as we go.
while (boost::asio::read(socket, response, boost::asio::transfer_at_least(1), error))
{
result << &response;
progress(result.tellp(), contentLength);
if (!ignoreCancel)
site->CheckCancel();
}
if (error != boost::asio::error::eof)
throw boost::system::system_error(error, "Failed to read response");
}
else
progress(1, 1);
return status_code;
}
int http(IInstallerSite *site, const char* method, const std::string& host, const std::string& path, std::istream& input, const char* contentType, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress, bool log)
{
try
{
return httpBoost(site, method, host, path, input, contentType, etag, result, ignoreCancel, progress, log);
}
catch (std::exception&)
{
LLOG_ENTRY(site->Logger(), "httpBoost failed, falling back to winInet");
return httpWinInet(site, method, host, path, input, contentType, etag, result, ignoreCancel, progress);
}
}
int httpPost(IInstallerSite *site, std::string host, std::string path, std::istream& input, const char* contentType, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress, bool log)
{
return http(site, "POST", host, path, input, contentType, std::string(), result, ignoreCancel, progress, log);
}
int httpGetCdn(IInstallerSite *site, std::string secondaryHost, std::string path, std::string& etag, std::ostream& result, bool ignoreCancel, boost::function<void(int, int)> progress)
{
std::string cdnHost;
if (site->UseNewCdn())
cdnHost = getCdnHost(site);
if (cdnHost.empty())
cdnHost = getPrimaryCdnHost(site);
if(!cdnHost.empty()){
try
{
std::string tmp = etag;
int status_code = httpGet(site, cdnHost, path, tmp, result, ignoreCancel, progress);
switch(status_code){
case 200:
case 304:
//We succeeded so save the etag and return success
etag = tmp;
return status_code;
default:
LLOG_ENTRY3(site->Logger(), "Failure getting '%s' from cdnHost='%s', falling back to secondaryHost='%s'", path.c_str(), cdnHost.c_str(), secondaryHost.c_str());
//Failure of some kind, fall back to secondaryHost below
break;
}
}
catch(std::exception&)
{
//Trap first exception and try again with secondaryHost
}
}
//Reset our result vector
result.seekp(0);
result.clear();
return httpGet(site, secondaryHost,path,etag,result,ignoreCancel,progress);
}
}