UrlClient: Basic HTTP implementation.
This commit is contained in:
parent
a42033db1c
commit
6c760ee348
@ -13,6 +13,7 @@ SET (SRCS
|
||||
NameValueParser.cpp
|
||||
SslHTTPServerConnection.cpp
|
||||
TransferEncodingParser.cpp
|
||||
UrlClient.cpp
|
||||
UrlParser.cpp
|
||||
)
|
||||
|
||||
@ -27,6 +28,7 @@ SET (HDRS
|
||||
NameValueParser.h
|
||||
SslHTTPServerConnection.h
|
||||
TransferEncodingParser.h
|
||||
UrlClient.h
|
||||
UrlParser.h
|
||||
)
|
||||
|
||||
|
@ -31,7 +31,8 @@ public:
|
||||
/** Called when an error has occured while parsing. */
|
||||
virtual void OnError(const AString & a_ErrorDescription) = 0;
|
||||
|
||||
/** Called when the first line (request / status) is fully parsed. */
|
||||
/** Called when the first line of the request or response is fully parsed.
|
||||
Doesn't check the validity of the line, only extracts the first complete line. */
|
||||
virtual void OnFirstLine(const AString & a_FirstLine) = 0;
|
||||
|
||||
/** Called when a single header line is parsed. */
|
||||
|
611
src/HTTP/UrlClient.cpp
Normal file
611
src/HTTP/UrlClient.cpp
Normal file
@ -0,0 +1,611 @@
|
||||
|
||||
// UrlClient.cpp
|
||||
|
||||
// Implements the cUrlClient class for high-level URL interaction
|
||||
|
||||
#include "Globals.h"
|
||||
#include "UrlClient.h"
|
||||
#include "UrlParser.h"
|
||||
#include "HTTPMessageParser.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// fwd:
|
||||
class cSchemeHandler;
|
||||
typedef SharedPtr<cSchemeHandler> cSchemeHandlerPtr;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class cUrlClientRequest:
|
||||
public cNetwork::cConnectCallbacks,
|
||||
public cTCPLink::cCallbacks
|
||||
{
|
||||
friend class cHttpSchemeHandler;
|
||||
|
||||
public:
|
||||
static std::pair<bool, AString> Request(
|
||||
const AString & a_Method,
|
||||
const AString & a_URL,
|
||||
cUrlClient::cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
const AString & a_Body,
|
||||
AStringMap && a_Options
|
||||
)
|
||||
{
|
||||
// Create a new instance of cUrlClientRequest, wrapped in a SharedPtr, so that it has a controlled lifetime.
|
||||
// Cannot use std::make_shared, because the constructor is not public
|
||||
SharedPtr<cUrlClientRequest> ptr (new cUrlClientRequest(
|
||||
a_Method, a_URL, a_Callbacks, std::move(a_Headers), a_Body, std::move(a_Options)
|
||||
));
|
||||
return ptr->DoRequest(ptr);
|
||||
}
|
||||
|
||||
|
||||
/** Calls the error callback with the specified message, if it exists, and terminates the request. */
|
||||
void CallErrorCallback(const AString & a_ErrorMessage)
|
||||
{
|
||||
// Call the error callback:
|
||||
m_Callbacks.OnError(a_ErrorMessage);
|
||||
|
||||
// Terminate the request's TCP link:
|
||||
auto link = m_Link;
|
||||
if (link != nullptr)
|
||||
{
|
||||
link->Close();
|
||||
}
|
||||
m_Self.reset();
|
||||
}
|
||||
|
||||
|
||||
cUrlClient::cCallbacks & GetCallbacks() { return m_Callbacks; }
|
||||
|
||||
void RedirectTo(const AString & a_RedirectUrl);
|
||||
|
||||
bool ShouldAllowRedirects() const;
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
/** Method to be used for the request */
|
||||
AString m_Method;
|
||||
|
||||
/** URL that will be requested. */
|
||||
AString m_Url;
|
||||
|
||||
/** Individual components of the URL that will be requested. */
|
||||
AString m_UrlScheme, m_UrlUsername, m_UrlPassword, m_UrlHost, m_UrlPath, m_UrlQuery, m_UrlFragment;
|
||||
UInt16 m_UrlPort;
|
||||
|
||||
/** Callbacks that report progress and results of the request. */
|
||||
cUrlClient::cCallbacks & m_Callbacks;
|
||||
|
||||
/** Extra headers to be sent with the request (besides the normal ones). */
|
||||
AStringMap m_Headers;
|
||||
|
||||
/** Body to be sent with the request, if any. */
|
||||
AString m_Body;
|
||||
|
||||
/** Extra options to be used for the request. */
|
||||
AStringMap m_Options;
|
||||
|
||||
/** SharedPtr to self, so that this object can keep itself alive for as long as it needs,
|
||||
and pass self as callbacks to cNetwork functions. */
|
||||
SharedPtr<cUrlClientRequest> m_Self;
|
||||
|
||||
/** The handler that "talks" the protocol specified in m_UrlScheme, handles all the sending and receiving. */
|
||||
SharedPtr<cSchemeHandler> m_SchemeHandler;
|
||||
|
||||
/** The link handling the request. */
|
||||
cTCPLinkPtr m_Link;
|
||||
|
||||
/** The number of redirect attempts that will still be followed.
|
||||
If the response specifies a redirect and this is nonzero, the redirect is followed.
|
||||
If the response specifies a redirect and this is zero, a redirect loop is reported as an error. */
|
||||
int m_NumRemainingRedirects;
|
||||
|
||||
|
||||
cUrlClientRequest(
|
||||
const AString & a_Method,
|
||||
const AString & a_Url,
|
||||
cUrlClient::cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
const AString & a_Body,
|
||||
AStringMap && a_Options
|
||||
):
|
||||
m_Method(a_Method),
|
||||
m_Url(a_Url),
|
||||
m_Callbacks(a_Callbacks),
|
||||
m_Headers(std::move(a_Headers)),
|
||||
m_Body(a_Body),
|
||||
m_Options(std::move(a_Options))
|
||||
{
|
||||
m_NumRemainingRedirects = GetStringMapInteger(m_Options, "MaxRedirects", 30);
|
||||
}
|
||||
|
||||
|
||||
std::pair<bool, AString> DoRequest(SharedPtr<cUrlClientRequest> a_Self);
|
||||
|
||||
|
||||
// cNetwork::cConnectCallbacks override: TCP link connected:
|
||||
virtual void OnConnected(cTCPLink & a_Link) override;
|
||||
|
||||
// cNetwork::cConnectCallbacks override: An error has occurred:
|
||||
virtual void OnError(int a_ErrorCode, const AString & a_ErrorMsg) override
|
||||
{
|
||||
m_Callbacks.OnError(Printf("Network error %d (%s)", a_ErrorCode, a_ErrorMsg.c_str()));
|
||||
m_Self.reset();
|
||||
}
|
||||
|
||||
|
||||
// cTCPLink::cCallbacks override: TCP link created
|
||||
virtual void OnLinkCreated(cTCPLinkPtr a_Link) override
|
||||
{
|
||||
m_Link = a_Link;
|
||||
}
|
||||
|
||||
|
||||
/** Called when there's data incoming from the remote peer. */
|
||||
virtual void OnReceivedData(const char * a_Data, size_t a_Length) override;
|
||||
|
||||
|
||||
/** Called when the remote end closes the connection.
|
||||
The link is still available for connection information query (IP / port).
|
||||
Sending data on the link is not an error, but the data won't be delivered. */
|
||||
virtual void OnRemoteClosed(void) override;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** Represents a base class for an object that "talks" a specified URL protocol, such as HTTP or FTP.
|
||||
Also provides a static factory method for creating an instance based on the scheme.
|
||||
A descendant of this class is created for each request and handles all of the request's aspects,
|
||||
from right after connecting to the TCP link till the link is closed.
|
||||
For an example of a specific handler, see the cHttpSchemeHandler class. */
|
||||
class cSchemeHandler abstract
|
||||
{
|
||||
public:
|
||||
cSchemeHandler(cUrlClientRequest & a_ParentRequest):
|
||||
m_ParentRequest(a_ParentRequest)
|
||||
{
|
||||
}
|
||||
|
||||
// Force a virtual destructor in all descendants
|
||||
virtual ~cSchemeHandler() {}
|
||||
|
||||
/** Creates and returns a new handler for the specified scheme.
|
||||
a_ParentRequest is the request which is to be handled by the handler. */
|
||||
static cSchemeHandlerPtr Create(const AString & a_Scheme, cUrlClientRequest & a_ParentRequest);
|
||||
|
||||
/** Called when the link gets established. */
|
||||
virtual void OnConnected(cTCPLink & a_Link) = 0;
|
||||
|
||||
/** Called when there's data incoming from the remote peer. */
|
||||
virtual void OnReceivedData(const char * a_Data, size_t a_Length) = 0;
|
||||
|
||||
/** Called when the remote end closes the connection.
|
||||
The link is still available for connection information query (IP / port).
|
||||
Sending data on the link is not an error, but the data won't be delivered. */
|
||||
virtual void OnRemoteClosed(void) = 0;
|
||||
|
||||
protected:
|
||||
cUrlClientRequest & m_ParentRequest;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** cSchemeHandler descendant that handles HTTP (and HTTPS) requests. */
|
||||
class cHttpSchemeHandler:
|
||||
public cSchemeHandler,
|
||||
protected cHTTPMessageParser::cCallbacks
|
||||
{
|
||||
typedef cSchemeHandler Super;
|
||||
|
||||
public:
|
||||
cHttpSchemeHandler(cUrlClientRequest & a_ParentRequest, bool a_IsTls):
|
||||
Super(a_ParentRequest),
|
||||
m_Parser(*this),
|
||||
m_IsTls(a_IsTls),
|
||||
m_IsRedirect(false)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
virtual void OnConnected(cTCPLink & a_Link) override
|
||||
{
|
||||
m_Link = &a_Link;
|
||||
if (m_IsTls)
|
||||
{
|
||||
// TODO: Start TLS
|
||||
}
|
||||
else
|
||||
{
|
||||
SendRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void SendRequest()
|
||||
{
|
||||
// Send the request:
|
||||
auto requestLine = m_ParentRequest.m_UrlPath;
|
||||
if (requestLine.empty())
|
||||
{
|
||||
requestLine = "/";
|
||||
}
|
||||
if (!m_ParentRequest.m_UrlQuery.empty())
|
||||
{
|
||||
requestLine.push_back('?');
|
||||
requestLine.append(m_ParentRequest.m_UrlQuery);
|
||||
}
|
||||
m_Link->Send(Printf("%s %s HTTP/1.1\r\n", m_ParentRequest.m_Method.c_str(), requestLine.c_str()));
|
||||
m_Link->Send(Printf("Host: %s\r\n", m_ParentRequest.m_UrlHost.c_str()));
|
||||
m_Link->Send(Printf("Content-Length: %u\r\n", static_cast<unsigned>(m_ParentRequest.m_Body.size())));
|
||||
for (auto itr = m_ParentRequest.m_Headers.cbegin(), end = m_ParentRequest.m_Headers.cend(); itr != end; ++itr)
|
||||
{
|
||||
m_Link->Send(Printf("%s: %s\r\n", itr->first.c_str(), itr->second.c_str()));
|
||||
} // for itr - m_Headers[]
|
||||
m_Link->Send("\r\n", 2);
|
||||
m_Link->Send(m_ParentRequest.m_Body);
|
||||
|
||||
// Notify the callbacks that the request has been sent:
|
||||
m_ParentRequest.GetCallbacks().OnRequestSent();
|
||||
}
|
||||
|
||||
|
||||
virtual void OnReceivedData(const char * a_Data, size_t a_Length) override
|
||||
{
|
||||
auto res = m_Parser.Parse(a_Data, a_Length);
|
||||
if (res == AString::npos)
|
||||
{
|
||||
m_ParentRequest.CallErrorCallback("Failed to parse HTTP response");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
virtual void OnRemoteClosed(void) override
|
||||
{
|
||||
m_Link = nullptr;
|
||||
}
|
||||
|
||||
|
||||
// cHTTPResponseParser::cCallbacks overrides:
|
||||
virtual void OnError(const AString & a_ErrorDescription) override
|
||||
{
|
||||
m_ParentRequest.CallErrorCallback(a_ErrorDescription);
|
||||
m_Link = nullptr;
|
||||
}
|
||||
|
||||
|
||||
virtual void OnFirstLine(const AString & a_FirstLine) override
|
||||
{
|
||||
// Find the first space, parse the result code between it and the second space:
|
||||
auto idxFirstSpace = a_FirstLine.find(' ');
|
||||
if (idxFirstSpace == AString::npos)
|
||||
{
|
||||
m_ParentRequest.CallErrorCallback(Printf("Failed to parse HTTP status line \"%s\", no space delimiter.", a_FirstLine.c_str()));
|
||||
return;
|
||||
}
|
||||
auto idxSecondSpace = a_FirstLine.find(' ', idxFirstSpace + 1);
|
||||
if (idxSecondSpace == AString::npos)
|
||||
{
|
||||
m_ParentRequest.CallErrorCallback(Printf("Failed to parse HTTP status line \"%s\", missing second space delimiter.", a_FirstLine.c_str()));
|
||||
return;
|
||||
}
|
||||
int resultCode;
|
||||
auto resultCodeStr = a_FirstLine.substr(idxFirstSpace + 1, idxSecondSpace - idxFirstSpace - 1);
|
||||
if (!StringToInteger(resultCodeStr, resultCode))
|
||||
{
|
||||
m_ParentRequest.CallErrorCallback(Printf("Failed to parse HTTP result code from response \"%s\"", resultCodeStr.c_str()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for redirects, follow if allowed by the options:
|
||||
switch (resultCode)
|
||||
{
|
||||
case cUrlClient::HTTP_STATUS_MULTIPLE_CHOICES:
|
||||
case cUrlClient::HTTP_STATUS_MOVED_PERMANENTLY:
|
||||
case cUrlClient::HTTP_STATUS_FOUND:
|
||||
case cUrlClient::HTTP_STATUS_SEE_OTHER:
|
||||
case cUrlClient::HTTP_STATUS_TEMPORARY_REDIRECT:
|
||||
{
|
||||
m_IsRedirect = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_ParentRequest.GetCallbacks().OnStatusLine(a_FirstLine.substr(1, idxFirstSpace), resultCode, a_FirstLine.substr(idxSecondSpace + 1));
|
||||
}
|
||||
|
||||
|
||||
virtual void OnHeaderLine(const AString & a_Key, const AString & a_Value) override
|
||||
{
|
||||
if (m_IsRedirect)
|
||||
{
|
||||
if (a_Key == "Location")
|
||||
{
|
||||
m_RedirectLocation = a_Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ParentRequest.GetCallbacks().OnHeader(a_Key, a_Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Called when all the headers have been parsed. */
|
||||
virtual void OnHeadersFinished(void) override
|
||||
{
|
||||
if (!m_IsRedirect)
|
||||
{
|
||||
m_ParentRequest.GetCallbacks().OnHeadersFinished();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Called for each chunk of the incoming body data. */
|
||||
virtual void OnBodyData(const void * a_Data, size_t a_Size) override
|
||||
{
|
||||
if (!m_IsRedirect)
|
||||
{
|
||||
m_ParentRequest.GetCallbacks().OnBodyData(a_Data, a_Size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Called when the entire body has been reported by OnBodyData(). */
|
||||
virtual void OnBodyFinished(void) override
|
||||
{
|
||||
if (m_IsRedirect)
|
||||
{
|
||||
if (m_RedirectLocation.empty())
|
||||
{
|
||||
m_ParentRequest.CallErrorCallback("Invalid redirect, there's no location to redirect to");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ParentRequest.RedirectTo(m_RedirectLocation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ParentRequest.GetCallbacks().OnBodyFinished();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
/** The network link. */
|
||||
cTCPLink * m_Link;
|
||||
|
||||
/** If true, the TLS should be started on the link before sending the request (used for https). */
|
||||
bool m_IsTls;
|
||||
|
||||
/** Parser of the HTTP response message. */
|
||||
cHTTPMessageParser m_Parser;
|
||||
|
||||
/** Set to true if the first line contains a redirecting HTTP status code and the options specify to follow redirects.
|
||||
If true, and the parent request allows redirects, neither headers not the body contents are reported through the callbacks,
|
||||
and after the entire request is parsed, the redirect is attempted. */
|
||||
bool m_IsRedirect;
|
||||
|
||||
/** The Location where the request should be redirected.
|
||||
Only used when m_IsRedirect is true. */
|
||||
AString m_RedirectLocation;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// cSchemeHandler:
|
||||
|
||||
cSchemeHandlerPtr cSchemeHandler::Create(const AString & a_Scheme, cUrlClientRequest & a_ParentRequest)
|
||||
{
|
||||
auto lowerScheme = StrToLower(a_Scheme);
|
||||
if (lowerScheme == "http")
|
||||
{
|
||||
return std::make_shared<cHttpSchemeHandler>(a_ParentRequest, false);
|
||||
}
|
||||
else if (lowerScheme == "https")
|
||||
{
|
||||
return std::make_shared<cHttpSchemeHandler>(a_ParentRequest, true);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// cUrlClientRequest:
|
||||
|
||||
void cUrlClientRequest::RedirectTo(const AString & a_RedirectUrl)
|
||||
{
|
||||
// Check that redirection is allowed:
|
||||
m_Callbacks.OnRedirecting(a_RedirectUrl);
|
||||
if (!ShouldAllowRedirects())
|
||||
{
|
||||
CallErrorCallback(Printf("Redirect to \"%s\" not allowed", a_RedirectUrl.c_str()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Do the actual redirect:
|
||||
m_Link->Close();
|
||||
m_Url = a_RedirectUrl;
|
||||
m_NumRemainingRedirects = m_NumRemainingRedirects - 1;
|
||||
auto res = DoRequest(m_Self);
|
||||
if (!res.first)
|
||||
{
|
||||
m_Callbacks.OnError(Printf("Redirection failed: %s", res.second.c_str()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
bool cUrlClientRequest::ShouldAllowRedirects() const
|
||||
{
|
||||
return (m_NumRemainingRedirects > 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cUrlClientRequest::OnConnected(cTCPLink & a_Link)
|
||||
{
|
||||
m_Callbacks.OnConnected(a_Link);
|
||||
m_SchemeHandler->OnConnected(a_Link);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cUrlClientRequest::OnReceivedData(const char * a_Data, size_t a_Length)
|
||||
{
|
||||
auto handler = m_SchemeHandler;
|
||||
if (handler != nullptr)
|
||||
{
|
||||
handler->OnReceivedData(a_Data, a_Length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cUrlClientRequest::OnRemoteClosed()
|
||||
{
|
||||
// Notify the callback:
|
||||
auto handler = m_SchemeHandler;
|
||||
if (handler != nullptr)
|
||||
{
|
||||
handler->OnRemoteClosed();
|
||||
}
|
||||
|
||||
// Let ourselves be deleted
|
||||
m_Self.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
std::pair<bool, AString> cUrlClientRequest::DoRequest(SharedPtr<cUrlClientRequest> a_Self)
|
||||
{
|
||||
// We need a shared pointer to self, care must be taken not to pass any other ptr:
|
||||
ASSERT(a_Self.get() == this);
|
||||
|
||||
m_Self = a_Self;
|
||||
|
||||
// Parse the URL:
|
||||
auto res = cUrlParser::Parse(m_Url, m_UrlScheme, m_UrlUsername, m_UrlPassword, m_UrlHost, m_UrlPort, m_UrlPath, m_UrlQuery, m_UrlFragment);
|
||||
if (!res.first)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
// Get a handler that will work with the specified scheme:
|
||||
m_SchemeHandler = cSchemeHandler::Create(m_UrlScheme, *this);
|
||||
if (m_SchemeHandler == nullptr)
|
||||
{
|
||||
return std::make_pair(false, Printf("Unknown Url scheme: %s", m_UrlScheme.c_str()));
|
||||
}
|
||||
|
||||
if (!cNetwork::Connect(m_UrlHost, m_UrlPort, m_Self, m_Self))
|
||||
{
|
||||
return std::make_pair(false, "Network connection failed");
|
||||
}
|
||||
return std::make_pair(true, AString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// cUrlClient:
|
||||
|
||||
std::pair<bool, AString> cUrlClient::Request(
|
||||
const AString & a_Method,
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
AString && a_Body,
|
||||
AStringMap && a_Options
|
||||
)
|
||||
{
|
||||
return cUrlClientRequest::Request(
|
||||
a_Method, a_URL, a_Callbacks, std::move(a_Headers), std::move(a_Body), std::move(a_Options)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
std::pair<bool, AString> cUrlClient::Get(
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap a_Headers,
|
||||
AString a_Body,
|
||||
AStringMap a_Options
|
||||
)
|
||||
{
|
||||
return cUrlClientRequest::Request(
|
||||
"GET", a_URL, a_Callbacks, std::move(a_Headers), std::move(a_Body), std::move(a_Options)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
std::pair<bool, AString> cUrlClient::Post(
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
AString && a_Body,
|
||||
AStringMap && a_Options
|
||||
)
|
||||
{
|
||||
return cUrlClientRequest::Request(
|
||||
"POST", a_URL, a_Callbacks, std::move(a_Headers), std::move(a_Body), std::move(a_Options)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
std::pair<bool, AString> cUrlClient::Put(
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
AString && a_Body,
|
||||
AStringMap && a_Options
|
||||
)
|
||||
{
|
||||
return cUrlClientRequest::Request(
|
||||
"PUT", a_URL, a_Callbacks, std::move(a_Headers), std::move(a_Body), std::move(a_Options)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
141
src/HTTP/UrlClient.h
Normal file
141
src/HTTP/UrlClient.h
Normal file
@ -0,0 +1,141 @@
|
||||
|
||||
// UrlClient.h
|
||||
|
||||
// Declares the cUrlClient class for high-level URL interaction
|
||||
|
||||
/*
|
||||
Options that can be set via the Options parameter to the cUrlClient calls:
|
||||
"MaxRedirects": The maximum number of allowed redirects before the client refuses a redirect with an error
|
||||
|
||||
Behavior:
|
||||
- If a redirect is received, and redirection is allowed, the redirection is reported via OnRedirecting() callback
|
||||
and the request is restarted at the redirect URL, without reporting any of the redirect's headers nor body
|
||||
- If a redirect is received and redirection is not allowed (maximum redirection attempts have been reached),
|
||||
the OnRedirecting() callback is called with the redirect URL and then the request terminates with an OnError() callback,
|
||||
without reporting the redirect's headers nor body.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../OSSupport/Network.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class cUrlClient
|
||||
{
|
||||
public:
|
||||
/** Callbacks that are used for progress and result reporting. */
|
||||
class cCallbacks
|
||||
{
|
||||
public:
|
||||
/** Called when the TCP connection is established. */
|
||||
virtual void OnConnected(cTCPLink & a_Link) {};
|
||||
|
||||
/** Called for TLS connections, when the server certificate is received.
|
||||
Return true to continue with the request, false to abort.
|
||||
The default implementation does nothing and continues with the request.
|
||||
TODO: The certificate parameter needs a representation! */
|
||||
virtual bool OnCertificateReceived() { return true; }
|
||||
|
||||
/** Called after the entire request has been sent to the remote peer. */
|
||||
virtual void OnRequestSent() {};
|
||||
|
||||
/** Called after the first line of the response is parsed, unless the response is an allowed redirect. */
|
||||
virtual void OnStatusLine(const AString & a_HttpVersion, int a_StatusCode, const AString & a_Rest) {}
|
||||
|
||||
/** Called when a single HTTP header is received and parsed, unless the response is an allowed redirect
|
||||
Called once for each incoming header. */
|
||||
virtual void OnHeader(const AString & a_Key, const AString & a_Value) {};
|
||||
|
||||
/** Called when the HTTP headers have been fully parsed, unless the response is an allowed redirect.
|
||||
There will be no more OnHeader() calls. */
|
||||
virtual void OnHeadersFinished() {};
|
||||
|
||||
/** Called when the next fragment of the response body is received, unless the response is an allowed redirect.
|
||||
This can be called multiple times, as data arrives over the network. */
|
||||
virtual void OnBodyData(const void * a_Data, size_t a_Size) {};
|
||||
|
||||
/** Called after the response body has been fully reported by OnBody() calls, unless the response is an allowed redirect.
|
||||
There will be no more OnBody() calls. */
|
||||
virtual void OnBodyFinished() {};
|
||||
|
||||
/** Called when an asynchronous error is encountered. */
|
||||
virtual void OnError(const AString & a_ErrorMsg) {};
|
||||
|
||||
/** Called when a redirect is to be followed.
|
||||
This is called even if the redirecting is prohibited by the options; in such an event, this call will be
|
||||
followed by OnError().
|
||||
If a response indicates a redirect (and the request allows redirecting), the regular callbacks
|
||||
OnStatusLine(), OnHeader(), OnHeadersFinished(), OnBodyData() and OnBodyFinished() are not called
|
||||
for such a response; instead, the redirect is silently attempted. */
|
||||
virtual void OnRedirecting(const AString & a_NewLocation) {};
|
||||
};
|
||||
|
||||
|
||||
/** Used for HTTP status codes. */
|
||||
enum eHTTPStatus
|
||||
{
|
||||
HTTP_STATUS_OK = 200,
|
||||
HTTP_STATUS_MULTIPLE_CHOICES = 300, // MAY have a redirect using the "Location" header
|
||||
HTTP_STATUS_MOVED_PERMANENTLY = 301, // redirect using the "Location" header
|
||||
HTTP_STATUS_FOUND = 302, // redirect using the "Location" header
|
||||
HTTP_STATUS_SEE_OTHER = 303, // redirect using the "Location" header
|
||||
HTTP_STATUS_TEMPORARY_REDIRECT = 307, // redirect using the "Location" header
|
||||
};
|
||||
|
||||
|
||||
/** Makes a network request to the specified URL, using the specified method (if applicable).
|
||||
The response is reported via the a_ResponseCallback callback, in a single call.
|
||||
The metadata about the response (HTTP headers) are reported via a_InfoCallback before the a_ResponseCallback call.
|
||||
If there is an asynchronous error, it is reported in via the a_ErrorCallback.
|
||||
If there is an immediate error (misformatted URL etc.), the function returns false and an error message.
|
||||
a_Headers contains additional headers to use for the request.
|
||||
a_Body specifies optional body to include with the request, if applicable.
|
||||
a_Options contains various options for the request that govern the request behavior, but aren't sent to the server,
|
||||
such as the proxy server, whether to follow redirects, and client certificate for TLS. */
|
||||
static std::pair<bool, AString> Request(
|
||||
const AString & a_Method,
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
AString && a_Body,
|
||||
AStringMap && a_Options
|
||||
);
|
||||
|
||||
/** Alias for Request("GET", ...) */
|
||||
static std::pair<bool, AString> Get(
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap a_Headers = AStringMap(),
|
||||
AString a_Body = AString(),
|
||||
AStringMap a_Options = AStringMap()
|
||||
);
|
||||
|
||||
/** Alias for Request("POST", ...) */
|
||||
static std::pair<bool, AString> Post(
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
AString && a_Body,
|
||||
AStringMap && a_Options
|
||||
);
|
||||
|
||||
/** Alias for Request("PUT", ...) */
|
||||
static std::pair<bool, AString> Put(
|
||||
const AString & a_URL,
|
||||
cCallbacks & a_Callbacks,
|
||||
AStringMap && a_Headers,
|
||||
AString && a_Body,
|
||||
AStringMap && a_Options
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
@ -11,6 +11,8 @@ set (HTTP_SRCS
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/HTTPMessage.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/HTTPMessageParser.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/TransferEncodingParser.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/UrlClient.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/UrlParser.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/StringUtils.cpp
|
||||
)
|
||||
|
||||
@ -19,13 +21,20 @@ set (HTTP_HDRS
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/HTTPMessage.h
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/HTTPMessageParser.h
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/TransferEncodingParser.h
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/UrlClient.h
|
||||
${CMAKE_SOURCE_DIR}/src/HTTP/UrlParser.h
|
||||
${CMAKE_SOURCE_DIR}/src/StringUtils.h
|
||||
)
|
||||
|
||||
set (SHARED_SRCS
|
||||
${CMAKE_SOURCE_DIR}/src/OSSupport/Event.cpp
|
||||
)
|
||||
|
||||
add_library(HTTP
|
||||
${HTTP_SRCS}
|
||||
${HTTP_HDRS}
|
||||
)
|
||||
target_link_libraries(HTTP Network OSSupport)
|
||||
|
||||
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||
add_flags_cxx("-Wno-error=conversion -Wno-error=old-style-cast")
|
||||
@ -35,11 +44,21 @@ endif()
|
||||
|
||||
|
||||
|
||||
# Define individual tests:
|
||||
# Define individual test executables:
|
||||
|
||||
# HTTPMessageParser_file: Feed file contents into a cHTTPResponseParser and print the callbacks as they're called:
|
||||
add_executable(HTTPMessageParser_file-exe HTTPMessageParser_file.cpp)
|
||||
target_link_libraries(HTTPMessageParser_file-exe HTTP)
|
||||
target_link_libraries(HTTPMessageParser_file-exe HTTP Network OSSupport)
|
||||
|
||||
# UrlClientTest: Tests the UrlClient class by requesting a few things off the internet:
|
||||
add_executable(UrlClientTest-exe UrlClientTest.cpp)
|
||||
target_link_libraries(UrlClientTest-exe HTTP)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Define individual tests:
|
||||
|
||||
# Test parsing the response file in 2-byte chunks (should go from response line parsing through headers parsing to body parsing, each within a different step):
|
||||
add_test(NAME HTTPMessageParser_file-test1-2 COMMAND HTTPMessageParser_file-exe ${CMAKE_CURRENT_SOURCE_DIR}/HTTPResponse1.data 2)
|
||||
@ -63,7 +82,8 @@ add_test(NAME HTTPMessageParser_file-test4-512 COMMAND HTTPMessageParser_file-ex
|
||||
# Put all the tests into a solution folder (MSVC):
|
||||
set_target_properties(
|
||||
HTTPMessageParser_file-exe
|
||||
PROPERTIES FOLDER Tests
|
||||
UrlClientTest-exe
|
||||
PROPERTIES FOLDER Tests/HTTP
|
||||
)
|
||||
set_target_properties(
|
||||
HTTP
|
||||
|
162
tests/HTTP/UrlClientTest.cpp
Normal file
162
tests/HTTP/UrlClientTest.cpp
Normal file
@ -0,0 +1,162 @@
|
||||
|
||||
#include "Globals.h"
|
||||
#include "HTTP/UrlClient.h"
|
||||
#include "OSSupport/NetworkSingleton.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class cCallbacks:
|
||||
public cUrlClient::cCallbacks
|
||||
{
|
||||
public:
|
||||
cCallbacks(cEvent & a_Event):
|
||||
m_Event(a_Event)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void OnConnected(cTCPLink & a_Link) override
|
||||
{
|
||||
LOG("Link connected to %s:%u", a_Link.GetRemoteIP().c_str(), a_Link.GetRemotePort());
|
||||
}
|
||||
|
||||
virtual bool OnCertificateReceived() override
|
||||
{
|
||||
LOG("Server certificate received");
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void OnRequestSent() override
|
||||
{
|
||||
LOG("Request has been sent");
|
||||
}
|
||||
|
||||
virtual void OnHeader(const AString & a_Key, const AString & a_Value) override
|
||||
{
|
||||
LOG("HTTP Header: \"%s\" -> \"%s\"", a_Key.c_str(), a_Value.c_str());
|
||||
}
|
||||
|
||||
virtual void OnHeadersFinished() override
|
||||
{
|
||||
LOG("HTTP headers finished.");
|
||||
}
|
||||
|
||||
virtual void OnBodyData(const void * a_Data, size_t a_Size) override
|
||||
{
|
||||
AString body(reinterpret_cast<const char *>(a_Data), a_Size);
|
||||
LOG("Body part:\n%s", body.c_str());
|
||||
}
|
||||
|
||||
/** Called after the response body has been fully reported by OnBody() calls.
|
||||
There will be no more OnBody() calls. */
|
||||
virtual void OnBodyFinished() override
|
||||
{
|
||||
LOG("Body finished.");
|
||||
m_Event.Set();
|
||||
}
|
||||
|
||||
virtual void OnRedirecting(const AString & a_RedirectUrl) override
|
||||
{
|
||||
LOG("Redirecting to \"%s\".", a_RedirectUrl.c_str());
|
||||
}
|
||||
|
||||
virtual void OnError(const AString & a_ErrorMsg) override
|
||||
{
|
||||
LOG("Error: %s", a_ErrorMsg.c_str());
|
||||
m_Event.Set();
|
||||
}
|
||||
|
||||
protected:
|
||||
cEvent & m_Event;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
int TestRequest1()
|
||||
{
|
||||
LOG("Running test 1");
|
||||
cEvent evtFinished;
|
||||
cCallbacks callbacks(evtFinished);
|
||||
AStringMap options;
|
||||
options["MaxRedirects"] = "0";
|
||||
auto res = cUrlClient::Get("http://github.com", callbacks, AStringMap(), AString(), options);
|
||||
if (res.first)
|
||||
{
|
||||
evtFinished.Wait();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG("Immediate error: %s", res.second.c_str());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
int TestRequest2()
|
||||
{
|
||||
LOG("Running test 2");
|
||||
cEvent evtFinished;
|
||||
cCallbacks callbacks(evtFinished);
|
||||
auto res = cUrlClient::Get("http://github.com", callbacks);
|
||||
if (res.first)
|
||||
{
|
||||
evtFinished.Wait();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG("Immediate error: %s", res.second.c_str());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
int TestRequests()
|
||||
{
|
||||
auto res = TestRequest1();
|
||||
if (res != 0)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
res = TestRequest2();
|
||||
if (res != 0)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
LOGD("Test started");
|
||||
|
||||
LOGD("Initializing cNetwork...");
|
||||
cNetworkSingleton::Get().Initialise();
|
||||
|
||||
LOGD("Testing...");
|
||||
auto res = TestRequests();
|
||||
|
||||
LOGD("Terminating cNetwork...");
|
||||
cNetworkSingleton::Get().Terminate();
|
||||
LOGD("cUrlClient test finished");
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user