Files
UnrealEngineUWP/Engine/Source/Programs/Unsync/Private/UnsyncHttp.cpp
Yuriy ODonnell b5709042fb Import Unsync into the main source tree
This is a binary patching and incremental downloading tool, similar to rsync or zsync. It aims to improve the large binary download processes that previously were served by robocopy (i.e. full packages produced by the build farm).

The original code can be found in `//depot/usr/yuriy.odonnell/unsync`. This commit is a branch from the original location to preserve history.

While the codebase is designed to be self-contained and does not depend on any engine libraries, it mostly follows the UE coding guidelines and can be built with UBT.

Currently only Windows is supported, however the tool is expected to also work on Mac and Linux in the future.

#rb Martin.Ridgers
#preflight skip

[CL 18993571 by Yuriy ODonnell in ue5-main branch]
2022-02-15 04:30:27 -05:00

511 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "UnsyncHttp.h"
#include "UnsyncCore.h"
#include "UnsyncUtil.h"
#include <http_parser.h>
#include <string.h>
#include <functional>
#ifdef __GNUC__
# define _strnicmp strncasecmp
#endif
namespace unsync {
enum class EViewUpdateResult
{
Append,
Replace,
};
static EViewUpdateResult
UpdateView(std::string_view& View, const char* Data, size_t Size)
{
if (View.data() + View.length() == Data)
{
View = std::string_view(View.data(), View.length() + Size);
return EViewUpdateResult::Append;
}
else
{
View = std::string_view(Data, Size);
return EViewUpdateResult::Replace;
}
}
using HttpMessageCallback = std::function<void(FHttpResponse&& Response)>;
struct FHttpParser
{
static FHttpParser* ToThis(http_parser* Parser) { return (FHttpParser*)(Parser->data); }
FHttpParser(HttpMessageCallback InResponseCallback, uint8* InScratchBuffer, uint64 InScratchSize, http_parser_type Type)
: ResponseCallback(InResponseCallback)
, ScratchBuffer(InScratchBuffer)
, ScratchSize(InScratchSize)
{
http_parser_settings_init(&Settings);
http_parser_init(&Parser, Type);
Parser.data = this;
Settings.on_message_begin = [](http_parser* P) { return ToThis(P)->OnMsgBegin(); };
Settings.on_message_complete = [](http_parser* P) { return ToThis(P)->OnMsgComplete(); };
Settings.on_header_field = [](http_parser* P, const char* Data, size_t Size) { return ToThis(P)->OnHdrField(Data, Size); };
Settings.on_header_value = [](http_parser* P, const char* Data, size_t Size) { return ToThis(P)->OnHdrValue(Data, Size); };
Settings.on_headers_complete = [](http_parser* P) { return ToThis(P)->OnHdrComplete(); };
Settings.on_body = [](http_parser* P, const char* Data, size_t Size) { return ToThis(P)->OnBody(Data, Size); };
Settings.on_status = [](http_parser* P, const char* Data, size_t Size) { return ToThis(P)->OnStatus(Data, Size); };
Settings.on_chunk_header = [](http_parser* P) { return ToThis(P)->OnChunkHeader(); };
Settings.on_chunk_complete = [](http_parser* P) { return ToThis(P)->OnChunkComplete(); };
}
void Reset()
{
bComplete = false;
bHeaderComplete = false;
PendingHeader = {};
PendingValue = {};
ScratchCursor = 0;
Response = FHttpResponse();
}
int OnMsgBegin()
{
Reset();
return 0;
}
int OnMsgComplete()
{
bComplete = true;
ResponseCallback(std::move(Response));
return 0;
}
int OnChunkHeader() { return 0; }
int OnChunkComplete() { return 0; }
int OnHdrField(const char* Data, size_t Size)
{
if (UpdateView(PendingHeader, Data, Size) == EViewUpdateResult::Replace)
{
PendingValue = {};
}
return 0;
}
int OnHdrValue(const char* Data, size_t Size)
{
auto MatchUncased = [](std::string_view A, std::string_view B) {
return _strnicmp(A.data(), B.data(), std::min(A.length(), B.length())) == 0;
};
UpdateView(PendingValue, Data, Size);
if (MatchUncased(PendingHeader, "content-length"))
{
ContentLength = atoi(Data);
Response.Buffer.Reserve(ContentLength);
}
else if (MatchUncased(PendingHeader, "content-type"))
{
if (MatchUncased(PendingValue, "application/json"))
{
Response.ContentType = EHttpContentType::Application_Json;
}
else if (MatchUncased(PendingValue, "application/octet-stream"))
{
Response.ContentType = EHttpContentType::Application_OctetStream;
}
else if (MatchUncased(PendingValue, "application/x-ue-cb"))
{
Response.ContentType = EHttpContentType::Application_UECB;
}
else if (MatchUncased(PendingValue, "text/html"))
{
Response.ContentType = EHttpContentType::Text_Html;
}
else if (MatchUncased(PendingValue, "text/plain"))
{
Response.ContentType = EHttpContentType::Text_Plain;
}
}
return 0;
}
int OnHdrComplete()
{
Response.Code = Parser.status_code;
bHeaderComplete = true;
return 0;
}
int OnStatus(const char* Data, size_t Size) { return 0; }
int OnBody(const char* Data, size_t Size)
{
// TODO: make a zero-copy path
Response.Buffer.Append((const uint8*)Data, Size);
ScratchCursor = 0;
return 0;
}
bool Recv(FSocketBase& Socket)
{
int32 RecvSize = 0;
uint8* RecvBuffer = nullptr;
uint64 RecvMaxSize = 0;
RecvBuffer = ScratchBuffer + ScratchCursor;
RecvMaxSize = ScratchSize - std::min<uint64>(ScratchCursor, ScratchSize);
RecvSize = SocketRecvAny(Socket, RecvBuffer, RecvMaxSize);
std::string_view RecvString((const char*)RecvBuffer, RecvSize);
uint64 ParsedBytes = 0;
if (RecvSize > 0)
{
ScratchCursor += RecvSize;
ParsedBytes = http_parser_execute(&Parser, &Settings, RecvString.data(), RecvString.size());
TotalReceivedBytes += RecvSize;
TotalParsedBytes += ParsedBytes;
}
return RecvSize > 0 && ParsedBytes != 0 && Parser.http_errno == 0;
}
HttpMessageCallback ResponseCallback;
FHttpResponse Response;
http_parser_settings Settings;
http_parser Parser;
uint8* ScratchBuffer;
uint64 ScratchSize;
uint64 ScratchCursor = 0;
bool bComplete = false;
bool bHeaderComplete = false;
std::string_view PendingHeader = {};
std::string_view PendingValue = {};
int32 ContentLength = 0;
uint64 TotalReceivedBytes = 0;
uint64 TotalParsedBytes = 0;
};
FHttpResponse
HttpRequest(FHttpConnection& Connection, const FHttpRequest& Request)
{
FHttpResponse Result;
if (HttpRequestBegin(Connection, Request))
{
Result = HttpRequestEnd(Connection);
}
return Result;
}
bool
HttpRequestBegin(FHttpConnection& Connection, const FHttpRequest& Request)
{
bool bConnected = Connection.Open();
if (!bConnected)
{
return false;
}
// TODO: use a string builder
std::string HttpHeader;
switch (Request.Method)
{
default:
UNSYNC_FATAL(L"Unexpected HTTP method %d", (int)Request.Method);
return false;
case EHttpMethod::GET:
HttpHeader = "GET ";
break;
// case HttpMethod::HEAD: // < TODO: support requests that don't return a body
// http_header = "HEAD";
// break;
case EHttpMethod::POST:
HttpHeader = "POST ";
break;
case EHttpMethod::PUT:
HttpHeader = "PUT ";
break;
}
HttpHeader.append(Request.Url);
HttpHeader.append(" HTTP/1.1\r\n");
if (!Request.CustomHeaders.empty())
{
HttpHeader.append(Request.CustomHeaders);
if (!HttpHeader.ends_with("\r\n"))
{
HttpHeader += "\r\n";
}
}
HttpHeader += "Host: " + Connection.HostAddress + "\r\n";
HttpHeader += "User-Agent: unsync v" + GetVersionString() + "\r\n";
if (Connection.bKeepAlive)
{
HttpHeader += "Connection: keep-alive\r\n";
}
if (Request.Payload.Size)
{
switch (Request.PayloadContentType)
{
case EHttpContentType::Text_Html:
HttpHeader += "Content-type: text/html\r\n";
break;
case EHttpContentType::Text_Plain:
HttpHeader += "Content-type: text/plain\r\n";
break;
case EHttpContentType::Application_OctetStream:
HttpHeader += "Content-type: application/octet-stream\r\n";
break;
case EHttpContentType::Application_UECB:
HttpHeader += "Content-type: application/x-ue-cb\r\n";
break;
default:
UNSYNC_FATAL(L"HTTP content type not supported");
}
}
if (Request.Payload.Size || Request.Method == EHttpMethod::POST)
{
char LengthStr[64];
snprintf(LengthStr, sizeof(LengthStr), "Content-length: %llu\r\n", (long long unsigned)Request.Payload.Size);
HttpHeader += LengthStr;
}
if (Request.AcceptContentType == EHttpContentType::Unknown)
{
HttpHeader += "Accept: */*\r\n";
}
else
{
switch (Request.AcceptContentType)
{
case EHttpContentType::Text_Html:
HttpHeader += "Accept: text/html\r\n";
break;
case EHttpContentType::Text_Plain:
HttpHeader += "Accept: text/plain\r\n";
break;
case EHttpContentType::Application_OctetStream:
HttpHeader += "Accept: application/octet-stream\r\n";
break;
case EHttpContentType::Application_UECB:
HttpHeader += "Accept: application/x-ue-cb\r\n";
break;
case EHttpContentType::Application_Json:
HttpHeader += "Accept: application/json\r\n";
break;
default:
UNSYNC_FATAL(L"HTTP content type not supported");
}
}
// Finish HTTP header section
HttpHeader += "\r\n";
int32 SentBytes = 0;
Connection.NumActiveRequests += 1;
// TODO: detect and handle errors
SentBytes += SocketSend(Connection.GetSocket(), HttpHeader.c_str(), HttpHeader.length());
if (Request.Payload.Size)
{
SentBytes += SocketSend(Connection.GetSocket(), Request.Payload.Data, Request.Payload.Size);
}
uint64 ExpectedSentBytes = HttpHeader.length() + Request.Payload.Size;
return SentBytes == ExpectedSentBytes;
}
FHttpResponse
HttpRequestEnd(FHttpConnection& Connection)
{
FHttpResponse Result;
if (!Connection.ResponseQueue.empty())
{
std::swap(Result, Connection.ResponseQueue.front());
Connection.ResponseQueue.pop_front();
Connection.NumActiveRequests -= 1;
return Result;
}
uint64 NumMessages = 0;
auto MessageCallback = [&NumMessages, &Result, &Connection](FHttpResponse&& Response) {
if (NumMessages == 0)
{
Result = std::move(Response);
}
else
{
Connection.ResponseQueue.push_back(std::move(Response));
}
NumMessages++;
};
// TODO: dynamic scratch buffer, if headers are pathologically large
// TODO: user-provided scratch buffer
uint8 ScratchBuffer[256_KB];
ScratchBuffer[0] = 0;
FHttpParser Parser(MessageCallback, ScratchBuffer, sizeof(ScratchBuffer), HTTP_RESPONSE);
while (!Parser.bComplete)
{
if (!Parser.Recv(Connection.GetSocket()))
{
break;
}
}
if (Parser.TotalParsedBytes != Parser.TotalReceivedBytes && !Parser.bComplete)
{
UNSYNC_FATAL(L"TODO: save the unparsed scratch buffer for next time!");
}
Connection.NumActiveRequests -= 1;
// TODO: report errors
return Result;
}
FHttpConnection::FHttpConnection(const std::string& InHostAddress, uint16 InPort, const FTlsClientSettings* InTlsSettings)
: HostAddress(InHostAddress)
, HostPort(InPort)
, bUseTls(InTlsSettings != nullptr)
{
if (InTlsSettings)
{
TlsSubject = InTlsSettings->Subject ? InTlsSettings->Subject : std::string();
bTlsVerifyCertificate = InTlsSettings->bVerifyCertificate;
if (InTlsSettings->CacertData)
{
TlsCacert = std::make_shared<FBuffer>();
TlsCacert->Append(InTlsSettings->CacertData, InTlsSettings->CacertSize);
}
}
}
FHttpConnection::FHttpConnection(const FHttpConnection& Other)
: HostAddress(Other.HostAddress)
, HostPort(Other.HostPort)
, bUseTls(Other.bUseTls)
, bKeepAlive(Other.bKeepAlive)
, TlsSubject(Other.TlsSubject)
, bTlsVerifyCertificate(Other.bTlsVerifyCertificate)
, TlsCacert(Other.TlsCacert)
{
}
bool
FHttpConnection::Open()
{
if (Socket.get())
{
if (SocketValid(*Socket))
{
return true;
}
}
FSocketHandle RawSocketHandle = SocketConnectTcp(HostAddress.c_str(), HostPort);
if (RawSocketHandle < 0)
{
return false;
}
if (bUseTls)
{
#if UNSYNC_USE_TLS
FTlsClientSettings ClientSettings;
ClientSettings.bVerifyCertificate = bTlsVerifyCertificate;
if (!TlsSubject.empty())
{
ClientSettings.Subject = TlsSubject.c_str();
}
if (TlsCacert && !TlsCacert->Empty())
{
ClientSettings.CacertData = TlsCacert->Data();
ClientSettings.CacertSize = TlsCacert->Size();
}
FSocketTls* TlsSocket = new FSocketTls(RawSocketHandle, ClientSettings);
if (TlsSocket->IsTlsValid())
{
Socket = std::unique_ptr<FSocketTls>(TlsSocket);
}
else
{
delete TlsSocket;
}
#else // UNSYNC_USE_TLS
UNSYNC_ERROR(L"Unsync is not compiled with TLS support");
#endif // UNSYNC_USE_TLS
}
else
{
Socket = std::unique_ptr<FSocketRaw>(new FSocketRaw(RawSocketHandle));
}
return Socket.get() && SocketValid(*Socket);
}
void
FHttpConnection::Close()
{
NumActiveRequests = 0;
ResponseQueue.clear();
Socket = {};
}
const char*
HttpStatusToString(int32 Code)
{
if (Code == 0)
{
return "Failed to establish connection";
}
else
{
return http_status_str((http_status)Code);
}
}
} // namespace unsync