DotNetDupe 4.0.6
C++17/20 Implementation of the .NET Base Class Library (BCL)
Loading...
Searching...
No Matches
HttpClient.cpp
Go to the documentation of this file.
1#include "pch.h"
4#include "System/Net/Dns.h"
12#include "System/Convert.h"
13#include <sstream>
14#include <vector>
15#include <cctype>
16#include <cstdlib>
17#include <cstring>
18#include <string>
19
20namespace DotNetDupe {
21 namespace System {
22 namespace Net {
23 namespace Http {
24
25 static std::string ReadLine(const SmartPointer<IO::Stream>& stream) {
26 std::string line;
27 char c = 0;
28 while (true) {
29 int read = stream->Read(&c, 0, 1);
30 if (read <= 0) break;
31 if (c == '\n') break;
32 if (c != '\r') {
33 line += c;
34 }
35 }
36 return line;
37 }
38
39 struct HttpClient::Impl : public Object {
41 SmartPointer<Sockets::TcpClient> m_pLastTcpClient;
42
43 String ResolveHost(const Uri& uri, int& riPort) {
44 String sHost = uri.GetHost();
45 riPort = uri.GetPort();
46 if (riPort <= 0) {
47 riPort = 80;
48 }
49
50 // Resolve hostname to IP Address
51 Array<String> arrIpAddresses = Dns::GetHostAddresses(sHost);
52 if (arrIpAddresses.GetLength() == 0) {
53 throw HttpRequestException("Could not resolve host.");
54 }
55 return arrIpAddresses[0];
56 }
57
58 std::string PrepareHeaders(const HttpRequestMessagePtr& spRequest, const Uri& uri) {
59 String sHost = uri.GetHost();
60 String sPath = uri.GetAbsolutePath();
61 if (sPath.IsEmpty()) {
62 sPath = "/";
63 }
64 String sQuery = uri.GetQuery();
65 std::string sRequestPath = sPath.GetRawString();
66 if (!sQuery.IsEmpty()) {
67 sRequestPath += "?";
68 sRequestPath += sQuery.GetRawString();
69 }
70
71 // Write HTTP request headers
72 std::ostringstream ssHeadersStream;
73 ssHeadersStream << spRequest->GetMethod().GetMethod().GetRawString() << " " << sRequestPath << " HTTP/1.1\r\n";
74
75 // Host header
76 ssHeadersStream << "Host: " << sHost.GetRawString();
77 if (!uri.IsDefaultPort() && uri.GetPort() > 0) {
78 ssHeadersStream << ":" << uri.GetPort();
79 }
80 ssHeadersStream << "\r\n";
81
82 // Default request headers
83 auto arrDefaultHeadersKeys = m_defaultRequestHeaders.GetKeys();
84 for (int i = 0; i < arrDefaultHeadersKeys.GetLength(); ++i) {
85 String sKey = arrDefaultHeadersKeys[i];
86 ssHeadersStream << sKey.GetRawString() << ": " << m_defaultRequestHeaders[sKey].GetRawString() << "\r\n";
87 }
88
89 // Request headers
90 auto arrRequestHeadersKeys = spRequest->GetHeaders().GetKeys();
91 for (int i = 0; i < arrRequestHeadersKeys.GetLength(); ++i) {
92 String sKey = arrRequestHeadersKeys[i];
93 ssHeadersStream << sKey.GetRawString() << ": " << spRequest->GetHeaders()[sKey].GetRawString() << "\r\n";
94 }
95
96 // Content headers
97 auto spContent = spRequest->GetContent();
98 if (!spContent.IsNull()) {
99 auto arrContentHeadersKeys = spContent->GetHeaders().GetKeys();
100 for (int i = 0; i < arrContentHeadersKeys.GetLength(); ++i) {
101 String sKey = arrContentHeadersKeys[i];
102 ssHeadersStream << sKey.GetRawString() << ": " << spContent->GetHeaders()[sKey].GetRawString() << "\r\n";
103 }
104
105 long lLen = spContent->GetLength();
106 if (lLen >= 0) {
107 ssHeadersStream << "Content-Length: " << lLen << "\r\n";
108 }
109 }
110
111 ssHeadersStream << "Connection: close\r\n\r\n";
112
113 return ssHeadersStream.str();
114 }
115
116 void SendRequest(const SmartPointer<IO::Stream>& spStream, const std::string& sHeaders, const HttpContentPtr& spContent) {
117 spStream->Write(sHeaders.data(), 0, static_cast<int>(sHeaders.size()));
118
119 // Write content
120 if (!spContent.IsNull()) {
121 spContent->CopyTo(spStream);
122 }
123 }
124
125 HttpResponseMessagePtr ParseStatusLine(const SmartPointer<IO::Stream>& spStream) {
126 std::string sStatusLine = ReadLine(spStream);
127 if (sStatusLine.empty()) {
128 throw HttpRequestException("No response from server.");
129 }
130
131 // HTTP/1.1 StatusCode ReasonPhrase
132 size_t iFirstSpace = sStatusLine.find(' ');
133 if (iFirstSpace == std::string::npos) {
134 throw HttpRequestException("Invalid response status line.");
135 }
136
137 size_t iSecondSpace = sStatusLine.find(' ', iFirstSpace + 1);
138 int iStatusCodeVal = 0;
139 std::string sReasonPhrase;
140 if (iSecondSpace == std::string::npos) {
141 iStatusCodeVal = std::atoi(sStatusLine.substr(iFirstSpace + 1).c_str());
142 } else {
143 iStatusCodeVal = std::atoi(sStatusLine.substr(iFirstSpace + 1, iSecondSpace - iFirstSpace - 1).c_str());
144 sReasonPhrase = sStatusLine.substr(iSecondSpace + 1);
145 }
146
147 auto spResponse = HttpResponseMessagePtr::NewShared(static_cast<HttpStatusCode>(iStatusCodeVal));
148 spResponse->SetReasonPhrase(String(sReasonPhrase.c_str()));
149 return spResponse;
150 }
151
152 void ParseHeaders(const SmartPointer<IO::Stream>& spStream, const HttpResponseMessagePtr& spResponse, bool& rbChunked, long& rlContentLength, String& rsContentType) {
153 auto& dictRespHeaders = spResponse->GetHeaders();
154 while (true) {
155 std::string sHeaderLine = ReadLine(spStream);
156 if (sHeaderLine.empty()) break;
157
158 size_t iColon = sHeaderLine.find(':');
159 if (iColon != std::string::npos) {
160 std::string sKey = sHeaderLine.substr(0, iColon);
161 std::string sVal = sHeaderLine.substr(iColon + 1);
162
163 // trim whitespace
164 while (!sKey.empty() && std::isspace(static_cast<unsigned char>(sKey.front()))) sKey.erase(sKey.begin());
165 while (!sKey.empty() && std::isspace(static_cast<unsigned char>(sKey.back()))) sKey.pop_back();
166 while (!sVal.empty() && std::isspace(static_cast<unsigned char>(sVal.front()))) sVal.erase(sVal.begin());
167 while (!sVal.empty() && std::isspace(static_cast<unsigned char>(sVal.back()))) sVal.pop_back();
168
169 String sKeyObj(sKey.c_str());
170 String sValObj(sVal.c_str());
171 dictRespHeaders[sKeyObj] = sValObj;
172
173 if (sKeyObj.ToLower() == "transfer-encoding" && sValObj.ToLower() == "chunked") {
174 rbChunked = true;
175 } else if (sKeyObj.ToLower() == "content-length") {
176 rlContentLength = std::atol(sVal.c_str());
177 } else if (sKeyObj.ToLower() == "content-type") {
178 rsContentType = sValObj;
179 }
180 }
181 }
182 }
183
184 Array<char> ReadResponseBody(const SmartPointer<IO::Stream>& spStream, bool bChunked, long lContentLength) {
185 std::vector<char> vecBodyData;
186 if (bChunked) {
187 while (true) {
188 std::string sSizeLine = ReadLine(spStream);
189 if (sSizeLine.empty()) break;
190
191 long lChunkSize = std::strtol(sSizeLine.c_str(), nullptr, 16);
192 if (lChunkSize <= 0) {
193 ReadLine(spStream); // read trailing CRLF of the final chunk
194 break;
195 }
196
197 std::vector<char> vecChunk(lChunkSize);
198 int iTotalRead = 0;
199 while (iTotalRead < lChunkSize) {
200 int iRead = spStream->Read(vecChunk.data() + iTotalRead, 0, static_cast<int>(lChunkSize - iTotalRead));
201 if (iRead <= 0) {
202 throw HttpRequestException("Connection closed prematurely while reading chunk data.");
203 }
204 iTotalRead += iRead;
205 }
206 vecBodyData.insert(vecBodyData.end(), vecChunk.begin(), vecChunk.end());
207
208 ReadLine(spStream); // read trailing CRLF of the chunk
209 }
210 } else if (lContentLength >= 0) {
211 vecBodyData.resize(lContentLength);
212 int iTotalRead = 0;
213 while (iTotalRead < lContentLength) {
214 int iRead = spStream->Read(vecBodyData.data() + iTotalRead, 0, static_cast<int>(lContentLength - iTotalRead));
215 if (iRead <= 0) {
216 throw HttpRequestException("Connection closed prematurely while reading content.");
217 }
218 iTotalRead += iRead;
219 }
220 } else {
221 // Read until EOF
222 char arrBuffer[4096];
223 int iBytesRead = 0;
224 while ((iBytesRead = spStream->Read(arrBuffer, 0, sizeof(arrBuffer))) > 0) {
225 vecBodyData.insert(vecBodyData.end(), arrBuffer, arrBuffer + iBytesRead);
226 }
227 }
228
229 Array<char> arrData(static_cast<int>(vecBodyData.size()));
230 if (!vecBodyData.empty()) {
231 std::memcpy(arrData.GetData(), vecBodyData.data(), vecBodyData.size());
232 }
233 return arrData;
234 }
235
236 SmartPointer<IO::Stream> ConnectStream(const Uri& uri, const String& scheme) {
237 int iPort = uri.GetPort() > 0 ? uri.GetPort() : ((scheme == "https") ? 443 : 80);
238 String sResolvedIp = ResolveHost(uri, iPort);
240 try { m_pLastTcpClient->Connect(sResolvedIp, iPort); }
241 catch (const Net::Sockets::SocketException& ex) { throw HttpRequestException(ex.What()); }
242 SmartPointer<IO::Stream> spStream = m_pLastTcpClient->GetStream();
243 if (scheme == "https") {
244 auto spSsl = SmartPointer<Net::Security::SslStream>::NewShared(spStream, false);
245 try { spSsl->AuthenticateAsClient(uri.GetHost()); }
246 catch (const SystemException& ex) { throw HttpRequestException(ex.What()); }
247 spStream = spSsl;
248 }
249 return spStream;
250 }
251
252 HttpResponseMessagePtr BuildStreamResponse(const SmartPointer<IO::Stream>& spStream) {
253 auto spResponse = ParseStatusLine(spStream);
254 bool bChunked = false; long lContentLength = -1; String sContentType = "text/plain";
255 ParseHeaders(spStream, spResponse, bChunked, lContentLength, sContentType);
256 auto spResponseContent = HttpContentPtr(new StreamContent(spStream), true);
257 spResponseContent->GetHeaders()["Content-Type"] = sContentType;
258 if (lContentLength >= 0) spResponseContent->GetHeaders()["Content-Length"] = Convert::ToString(static_cast<long long>(lContentLength));
259 spResponse->SetContent(spResponseContent);
260 return spResponse;
261 }
262
263 HttpResponseMessagePtr PrepareResponse(const SmartPointer<IO::Stream>& spStream) {
264 auto spResponse = ParseStatusLine(spStream);
265 bool bChunked = false;
266 long lContentLength = -1;
267 String sContentType = "text/plain";
268 ParseHeaders(spStream, spResponse, bChunked, lContentLength, sContentType);
269 Array<char> arrData = ReadResponseBody(spStream, bChunked, lContentLength);
270 auto spResponseContent = HttpContentPtr(new ByteArrayContent(arrData), true);
271 spResponseContent->GetHeaders()["Content-Type"] = sContentType;
272 spResponse->SetContent(spResponseContent);
273 return spResponse;
274 }
275 };
276
277 HttpClient::HttpClient() : m_pImpl(SmartPointer<Impl>::NewShared()) {
279 }
280
281 HttpClient::~HttpClient() = default;
282
285 return Get(Uri(requestUri));
286 }
287
290 auto request = HttpRequestMessagePtr::NewShared(HttpMethod::Get, requestUri);
291 return Send(request);
292 }
293
296 return Post(Uri(requestUri), content);
297 }
298
299 HttpResponseMessagePtr HttpClient::Post(const Uri& requestUri, const HttpContentPtr& content) {
301 auto request = HttpRequestMessagePtr::NewShared(HttpMethod::Post, requestUri);
302 request->SetContent(content);
303 return Send(request);
304 }
305
306 HttpResponseMessagePtr HttpClient::Put(const String& requestUri, const HttpContentPtr& content) {
308 return Put(Uri(requestUri), content);
309 }
310
311 HttpResponseMessagePtr HttpClient::Put(const Uri& requestUri, const HttpContentPtr& content) {
313 auto request = HttpRequestMessagePtr::NewShared(HttpMethod::Put, requestUri);
314 request->SetContent(content);
315 return Send(request);
316 }
317
320 return Delete(Uri(requestUri));
321 }
322
325 auto request = HttpRequestMessagePtr::NewShared(HttpMethod::Delete, requestUri);
326 return Send(request);
327 }
328
331 return GetString(Uri(requestUri));
332 }
333
334 String HttpClient::GetString(const Uri& requestUri) {
336 auto response = Get(requestUri);
337 response->EnsureSuccessStatusCode();
338 auto content = response->GetContent();
339 if (content.IsNull()) return String("");
340 return content->ReadAsString();
341 }
342
345 return GetByteArray(Uri(requestUri));
346 }
347
350 auto response = Get(requestUri);
351 response->EnsureSuccessStatusCode();
352 auto content = response->GetContent();
353 if (content.IsNull()) return Array<char>(0);
354 return content->ReadAsByteArray();
355 }
356
359 return m_pImpl->m_defaultRequestHeaders;
360 }
361
364 return m_pImpl->m_defaultRequestHeaders;
365 }
366
371
374 if (request.IsNull()) throw ArgumentNullException("request");
376 Uri uri = request->GetRequestUri();
377 String scheme = uri.GetScheme().ToLower();
378 if (scheme != "http" && scheme != "https") throw ArgumentException("Only 'http' and 'https' schemes are supported.");
380 auto spStream = m_pImpl->ConnectStream(uri, scheme);
382 m_pImpl->SendRequest(spStream, m_pImpl->PrepareHeaders(request, uri), request->GetContent());
384 if (completionOption == HttpCompletionOption::ResponseHeadersRead) return m_pImpl->BuildStreamResponse(spStream);
385 return m_pImpl->PrepareResponse(spStream);
386 }
387
388 }
389 }
390 }
391}
Defines the exception thrown when an invalid argument is provided to a method.
Defines the exception thrown when a null reference is passed to a method that does not accept it.
Converts a base data type to another base data type, and encodes/decodes Base64 data.
Provides simple domain name resolution functionality.
Provides a base class for sending HTTP requests and receiving HTTP responses mirroring ....
Exception thrown when an HTTP request or connection failure occurs.
Provides the underlying stream of data for network access.
Exception thrown when a network socket error occurs per RFC 793 / RFC 768.
Provides a stream that uses the Transport Layer Security (TLS) protocol to secure network communicati...
Provides client connections for TCP network services.
ArgumentException(const String &sMessage)
Initializes a new instance of the ArgumentException class with a specified error message.
ArgumentNullException(const String &sMessage)
Initializes a new instance of the ArgumentNullException class with a specified error message.
Provides methods for creating, manipulating, searching, and sorting arrays, thereby serving as the ba...
Definition Array.h:29
int GetLength() const
Gets the total number of elements in all dimensions of the Array.
Definition Array.h:142
Represents a collection of keys and values.
Definition Dictionary.h:66
static String ToString(bool value)
Definition Convert.cpp:253
static Array< String > GetHostAddresses(const String &hostName)
Returns the Internet Protocol (IP) addresses for the specified host.
Definition Dns.cpp:71
ByteArrayContent(const Array< char > &content)
Initializes a new instance of ByteArrayContent with full byte buffer.
HttpResponseMessagePtr Post(const String &requestUri, const HttpContentPtr &content)
Sends a POST request with content to the specified string Uri.
String GetString(const String &requestUri)
Sends a GET request to the specified URI and returns the response body as a string.
~HttpClient()
Releases unmanaged resources and destroys the HttpClient instance.
HttpClient()
Initializes a new instance of the HttpClient class.
HttpResponseMessagePtr Send(const HttpRequestMessagePtr &request)
Sends an HTTP request as an operation.
HttpResponseMessagePtr Delete(const String &requestUri)
Sends a DELETE request to the specified string Uri.
HttpResponseMessagePtr Get(const String &requestUri)
Sends a GET request to the specified string Uri.
Collections::Generic::Dictionary< String, String > & GetDefaultRequestHeaders()
Gets the headers which should be sent with each request.
Array< char > GetByteArray(const String &requestUri)
Sends a GET request to the specified URI and returns the response body as a byte array.
HttpResponseMessagePtr Put(const String &requestUri, const HttpContentPtr &content)
Sends a PUT request with content to the specified string Uri.
static const HttpMethod Get
Represents an HTTP GET protocol method.
Definition HttpMethod.h:49
static const HttpMethod Put
Represents an HTTP PUT protocol method.
Definition HttpMethod.h:53
static const HttpMethod Post
Represents an HTTP POST protocol method.
Definition HttpMethod.h:51
static const HttpMethod Delete
Represents an HTTP DELETE protocol method.
Definition HttpMethod.h:55
HttpRequestException(const String &sMessage)
Initializes a new instance of the HttpRequestException class with a specified error message.
StreamContent(const SmartPointer< IO::Stream > &stream)
Initializes a new instance of StreamContent wrapping an existing Stream.
Supports all classes in the DotNetDupe class hierarchy.
Definition Object.h:18
A unified smart pointer that supports both unique and shared ownership semantics.
static SmartPointer< HttpResponseMessage > NewShared()
bool IsNull() const noexcept
Checks if the SmartPointer is null.
Represents text as a sequence of UTF-8 code units with culture-invariant operations.
Definition String.h:74
String ToLower() const
Definition String.cpp:672
String()
Initializes a new instance of the String class to an empty string.
Definition String.cpp:64
Represents an object representation of a Uniform Resource Identifier (URI) and provides easy access t...
Definition Uri.h:21
String GetHost() const
Gets the host component of this instance.
Definition Uri.cpp:105
int GetPort() const
Gets the port number of this URI.
Definition Uri.cpp:106
String GetScheme() const
Gets the scheme name for this URI.
Definition Uri.cpp:102
Uri(const String &uriString)
Initializes a new instance of the Uri class with the specified URI string.
Definition Uri.cpp:9
static std::string ReadLine(const SmartPointer< IO::Stream > &stream)
SmartPointer< HttpResponseMessage > HttpResponseMessagePtr
Type alias for reference-counted SmartPointer to HttpResponseMessage.
HttpCompletionOption
Indicates if HttpClient operations should be considered completed as soon as headers are read,...
Definition HttpClient.h:27
@ ResponseContentRead
Operation should complete after reading the entire response including content.
Definition HttpClient.h:29
@ ResponseHeadersRead
Operation should complete as soon as a response is available and headers are read.
Definition HttpClient.h:31
SmartPointer< HttpContent > HttpContentPtr
Type alias for reference-counted SmartPointer to HttpContent.
Definition HttpContent.h:66
SmartPointer< HttpRequestMessage > HttpRequestMessagePtr
Type alias for reference-counted SmartPointer to HttpRequestMessage.
HttpStatusCode
Contains the values of status codes defined for HTTP in RFC 9110 and RFC 7231.