// Copyright (c) 2015-2025 Vector 35 Inc
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
#include
#include
#include
#include
#include "http.h"
#ifdef BINARYNINJACORE_LIBRARY
#include "log.h"
#endif
#ifdef BINARYNINJACORE_LIBRARY
using namespace BinaryNinjaCore;
#else
using namespace BinaryNinja;
using namespace std;
#endif
#ifdef BINARYNINJACORE_LIBRARY
namespace BinaryNinjaCore::Http
#else
namespace BinaryNinja::Http
#endif
{
#define HTTP_MAX_RETRIES 3
#define HTTP_BACKOFF_FACTOR 1
struct RequestContext
{
size_t uploadOffset;
size_t downloadLength;
bool cancelled;
const Request& request;
Response& response;
RequestContext(const Request& request, Response& response) :
uploadOffset(0), downloadLength(0), cancelled(false), request(request), response(response)
{}
};
int64_t HttpReadCallback(uint8_t* data, uint64_t len, void* ctxt)
{
auto* request = reinterpret_cast(ctxt);
uint64_t remain = request->request.m_body.size() - request->uploadOffset;
if (len < remain)
{
memcpy(data, &request->request.m_body[request->uploadOffset], len);
request->uploadOffset += len;
if (request->request.m_uploadProgress)
{
if (!request->request.m_uploadProgress(request->uploadOffset, request->request.m_body.size()))
{
request->cancelled = true;
return -1;
}
}
return len;
}
else if (remain > 0)
{
memcpy(data, &request->request.m_body[request->uploadOffset], remain);
request->uploadOffset += remain;
if (request->request.m_uploadProgress)
{
if (!request->request.m_uploadProgress(request->uploadOffset, request->request.m_body.size()))
{
request->cancelled = true;
return -1;
}
}
return remain;
}
else
{
return 0;
}
}
uint64_t HttpWriteCallback(uint8_t* data, uint64_t len, void* ctxt)
{
auto* request = reinterpret_cast(ctxt);
// copy can totally take pointers, pretty cool
copy(data, &data[len], back_inserter(request->response.body));
// Detect content length if it has not been found yet
if (request->downloadLength == 0)
{
const auto& headers = request->response.response.headers;
auto found = headers.find("Content-Length");
if (found != headers.end())
{
request->downloadLength = strtoll(found->second.c_str(), nullptr, 10);
request->response.body.reserve(request->downloadLength);
}
else
{
request->downloadLength = -1;
}
}
if (request->request.m_downloadProgress)
{
if (!request->request.m_downloadProgress(request->response.body.size(), request->downloadLength))
{
// Signal error by returning non-len
request->cancelled = true;
return 0;
}
}
return len;
}
string UrlEncode(const string& str)
{
string outStr;
outStr.reserve(str.size());
for (auto& ch : str)
{
if (isalnum(ch))
{
outStr += ch;
}
else
{
char buf[8];
snprintf(buf, 8, "%%%02hhx", ch);
outStr += string(buf);
}
}
return outStr;
}
string UrlEncode(const vector>& fields)
{
string outStr;
bool first = true;
for (auto& field : fields)
{
if (!first)
{
outStr += "&";
}
outStr += UrlEncode(field.first);
outStr += "=";
outStr += UrlEncode(field.second);
first = false;
}
return outStr;
}
vector MultipartEncode(const vector& fields, string& boundary)
{
boundary = string(4, '-') + "MultipartFormBoundary" + (string)BNGetUniqueIdentifierString();
vector boundaryVec;
boundaryVec.reserve(boundary.size());
copy(boundary.begin(), boundary.end(), back_inserter(boundaryVec));
vector result;
size_t expectedSize = boundaryVec.size() * fields.size();
for (const auto& field : fields)
{
expectedSize += field.name.size() + field.content.size();
}
result.reserve(expectedSize);
for (const auto& field : fields)
{
result.push_back('-');
result.push_back('-');
copy(boundaryVec.begin(), boundaryVec.end(), back_inserter(result));
result.push_back('\r');
result.push_back('\n');
string disposition;
if (field.filename)
{
disposition = string("Content-Disposition: form-data; name=\"") + field.name + "\"; filename=\""
+ *field.filename + "\"";
disposition += string("\r\nContent-Type: application/octet-stream");
}
else
{
disposition = string("Content-Disposition: form-data; name=\"") + field.name + "\"";
}
disposition += "\r\n\r\n";
copy(disposition.begin(), disposition.end(), back_inserter(result));
copy(field.content.begin(), field.content.end(), back_inserter(result));
result.push_back('\r');
result.push_back('\n');
}
result.push_back('-');
result.push_back('-');
copy(boundaryVec.begin(), boundaryVec.end(), back_inserter(result));
result.push_back('-');
result.push_back('-');
result.push_back('\r');
result.push_back('\n');
return result;
}
Request::Request(string method, string url, const unordered_map& headers,
vector> params, ProgressFunction downloadProgress,
ProgressFunction uploadProgress) :
m_method(method),
m_url(url), m_headers(headers), m_downloadProgress(downloadProgress), m_uploadProgress(uploadProgress)
{
if (!params.empty())
{
m_url += "?";
m_url += UrlEncode(params);
}
if (m_headers.find("Content-Length") == m_headers.end())
{
m_headers.insert({"Content-Length", to_string(m_body.size())});
}
if (m_headers.find("Content-Type") == m_headers.end())
{
m_headers.insert({"Content-Type", "application/octet-stream"});
}
}
Request::Request(string method, string url, const unordered_map& headers,
vector> params, vector body, ProgressFunction downloadProgress,
ProgressFunction uploadProgress) :
m_method(method),
m_url(url), m_headers(headers), m_body(body), m_downloadProgress(downloadProgress),
m_uploadProgress(uploadProgress)
{
if (!params.empty())
{
m_url += "?";
m_url += UrlEncode(params);
}
if (m_headers.find("Content-Length") == m_headers.end())
{
m_headers.insert({"Content-Length", to_string(m_body.size())});
}
if (m_headers.find("Content-Type") == m_headers.end())
{
m_headers.insert({"Content-Type", "application/octet-stream"});
}
}
Request::Request(string method, string url, const unordered_map& headers,
vector> params, vector> formFields,
ProgressFunction downloadProgress, ProgressFunction uploadProgress) :
m_method(method),
m_url(url), m_headers(headers), m_downloadProgress(downloadProgress), m_uploadProgress(uploadProgress)
{
if (!params.empty())
{
m_url += "?";
m_url += UrlEncode(params);
}
string encoded = UrlEncode(formFields);
copy(encoded.begin(), encoded.end(), back_inserter(m_body));
m_headers.insert({"Content-Type", "application/x-www-form-urlencoded"});
if (m_headers.find("Content-Length") == m_headers.end())
{
m_headers.insert({"Content-Length", to_string(m_body.size())});
}
if (m_headers.find("Content-Type") == m_headers.end())
{
m_headers.insert({"Content-Type", "application/octet-stream"});
}
}
Request::Request(string method, string url, const unordered_map& headers,
vector> params, vector formFields,
ProgressFunction downloadProgress, ProgressFunction uploadProgress) :
m_method(method),
m_url(url), m_headers(headers), m_downloadProgress(downloadProgress), m_uploadProgress(uploadProgress)
{
if (!params.empty())
{
m_url += "?";
m_url += UrlEncode(params);
}
string boundary;
m_body = MultipartEncode(formFields, boundary);
m_headers.insert({"Content-Type", string("multipart/form-data; boundary=\"") + boundary + "\""});
if (m_headers.find("Content-Length") == m_headers.end())
{
m_headers.insert({"Content-Length", to_string(m_body.size())});
}
if (m_headers.find("Content-Type") == m_headers.end())
{
m_headers.insert({"Content-Type", "application/octet-stream"});
}
}
Request Request::Get(string url, const unordered_map& headers, const vector>& params,
ProgressFunction downloadProgress, ProgressFunction uploadProgress)
{
return Request("GET", url, headers, params, downloadProgress, uploadProgress);
}
Request Request::Post(string url, const unordered_map& headers, const vector>& params,
const vector& body, ProgressFunction downloadProgress,
ProgressFunction uploadProgress)
{
return Request("POST", url, headers, params, body, downloadProgress, uploadProgress);
}
Request Request::Post(string url, const unordered_map& headers, const vector>& params,
const vector>& formFields, ProgressFunction downloadProgress,
ProgressFunction uploadProgress)
{
return Request("POST", url, headers, params, formFields, downloadProgress, uploadProgress);
}
Request Request::Post(string url, const unordered_map& headers, const vector>& params,
const vector& formFields, ProgressFunction downloadProgress,
ProgressFunction uploadProgress)
{
return Request("POST", url, headers, params, formFields, downloadProgress, uploadProgress);
}
int Perform(const Ref& instance, const Request& request, Response& response)
{
int result = -1;
int retry = 0;
while (true)
{
response.response.statusCode = 0;
response.response.headers.clear();
response.body.clear();
response.error.clear();
if (getenv("BN_DEBUG_HTTP"))
{
LogDebug("> %s %s", request.m_method.c_str(), request.m_url.c_str());
for (auto& header : request.m_headers)
{
LogDebug("> %s: %s", header.first.c_str(), header.second.c_str());
}
LogDebug("> ");
if (!request.m_body.empty())
{
for (size_t i = 0; i < request.m_body.size(); i += 1000)
{
LogDebug("> %.*s", (int)std::min(request.m_body.size() - i, (size_t)1000), request.m_body.data() + i);
}
}
}
RequestContext context {request, response};
BNDownloadInstanceInputOutputCallbacks callbacks {};
memset(&callbacks, 0, sizeof(BNDownloadInstanceInputOutputCallbacks));
callbacks.readContext = &context;
callbacks.readCallback = &HttpReadCallback;
callbacks.writeContext = &context;
callbacks.writeCallback = &HttpWriteCallback;
result = instance->PerformCustomRequest(
request.m_method, request.m_url, request.m_headers, response.response, &callbacks);
if (getenv("BN_DEBUG_HTTP"))
{
LogDebug("* Function returned: %d", result);
}
if (result >= 0)
break;
if (getenv("BN_DEBUG_HTTP"))
{
LogDebug("* Error: %s", instance->GetError().c_str());
}
// Request failed, grab its error and try again
response.error = instance->GetError();
if (retry == HTTP_MAX_RETRIES || context.cancelled)
break;
size_t backoff = 1000 * HTTP_BACKOFF_FACTOR * (2 * pow(2, retry - 1));
retry += 1;
LogWarn("Attempt %d to %s %s failed, trying again in %zums\n", retry, request.m_method.data(),
request.m_url.data(), backoff);
std::this_thread::sleep_for(std::chrono::milliseconds(backoff));
}
if (getenv("BN_DEBUG_HTTP"))
{
if (result >= 0)
{
LogDebug("< HTTP %d", response.response.statusCode);
for (auto& header : response.response.headers)
{
LogDebug("< %s: %s", header.first.c_str(), header.second.c_str());
}
LogDebug("< ");
if (!response.body.empty())
{
for (size_t i = 0; i < response.body.size(); i += 1000)
{
LogDebug("< %.*s", (int)std::min(response.body.size() - i, (size_t)1000), response.body.data() + i);
}
}
}
}
return result;
}
vector Response::GetRaw() const noexcept { return body; }
string Response::GetString() const noexcept
{
string str;
copy(body.begin(), body.end(), back_inserter(str));
return str;
}
Json::Value Response::GetJson() const
{
string str = GetString();
std::unique_ptr<:charreader> reader(Json::CharReaderBuilder().newCharReader());
string errors;
Json::Value value;
if (!reader->parse(str.data(), str.data() + str.size(), &value, &errors))
{
throw std::runtime_error(std::string("Could not parse JSON: ") + errors.c_str());
}
return value;
}
bool Response::GetJson(Json::Value& value) const noexcept
{
string str = GetString();
std::unique_ptr<:charreader> reader(Json::CharReaderBuilder().newCharReader());
string errors;
return reader->parse(str.data(), str.data() + str.size(), &value, &errors);
}
} // namespace BinaryNinjaCore::Http