DiscordCoreAPI
A Discord bot library written in C++, with custom asynchronous coroutines.
Loading...
Searching...
No Matches
HttpsClient.cpp
Go to the documentation of this file.
1/*
2 MIT License
3
4 DiscordCoreAPI, A bot library for Discord, written in C++, and featuring explicit multithreading through the usage of custom, asynchronous C++ CoRoutines.
5
6 Copyright 2022, 2023 Chris M. (RealTimeChris)
7
8 Permission is hereby granted, free of charge, to any person obtaining a copy
9 of this software and associated documentation files (the "Software"), to deal
10 in the Software without restriction, including without limitation the rights
11 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the Software is
13 furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be included in all
16 copies or substantial portions of the Software.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 SOFTWARE.
25*/
26/// HttpsClient.cpp - Source file for the https class.
27/// May 12, 2021
28/// https://discordcoreapi.com
29/// \file HttpsClient.cpp
30
34
35namespace discord_core_api {
36
37 namespace discord_core_internal {
38
39 https_connection::https_connection(const jsonifier::string& baseUrlNew, const uint16_t portNew)
40 : tcp_connection<https_connection>{ baseUrlNew, portNew } {
41 }
42
43 jsonifier::vector<jsonifier::string_view> tokenize(jsonifier::string_view in, const char* sep = "\r\n") {
44 jsonifier::string_view::size_type b = 0;
45 jsonifier::vector<jsonifier::string_view> result{};
46
47 while ((b = in.findFirstNotOf(sep, b)) != jsonifier::string_view::npos) {
48 auto e = in.find(sep, b);
49 if (b + (e - b) > in.size()) {
50 break;
51 }
52 result.emplace_back(in.substr(b, e - b));
53 b = e;
54 }
55 return result;
56 }
57
58 uint64_t parseCode(jsonifier::string_view string) {
59 uint64_t start = string.find(' ');
60 if (start == jsonifier::string_view::npos) {
61 return 0;
62 }
63
64 while (std::isspace(string[start])) {
65 start++;
66 }
67
68 uint64_t end = start;
69 while (std::isdigit(string[end])) {
70 end++;
71 }
72 jsonifier::string_view codeStr = string.substr(start, end - start);
73 uint64_t code = jsonifier::strToUint64(codeStr.data());
74 return code;
75 }
76
77 void https_connection::handleBuffer() {
78 do {
79 inputBufferReal += getInputBuffer();
80 switch (data.currentState) {
81 case https_state::Collecting_Headers: {
82 if (!parseHeaders()) {
83 return;
84 }
85 break;
86 }
87 case https_state::Collecting_Contents: {
88 if (!parseContents()) {
89 return;
90 }
91 break;
92 }
93 case https_state::Collecting_Chunked_Contents: {
94 if (!parseChunk()) {
95 return;
96 }
97 break;
98 }
99 case https_state::complete: {
100 inputBufferReal.clear();
101 return;
102 }
103 }
104 } while (inputBufferReal.size() > 0);
105 return;
106 }
107
108 https_client_core::https_client_core(jsonifier::string_view botTokenNew) {
109 botToken = botTokenNew;
110 }
111
112 void https_rnr_builder::updateRateLimitData(rate_limit_data& rateLimitData) {
113 auto connection{ static_cast<https_connection*>(this) };
114 if (connection->data.responseHeaders.contains("x-ratelimit-bucket")) {
115 rateLimitData.bucket = connection->data.responseHeaders.at("x-ratelimit-bucket");
116 }
117 if (connection->data.responseHeaders.contains("x-ratelimit-reset-after")) {
118 rateLimitData.sRemain.store(seconds{ static_cast<int64_t>(ceil(jsonifier::strToDouble(connection->data.responseHeaders.at("x-ratelimit-reset-after").data()))) },
119 std::memory_order_release);
120 }
121 if (connection->data.responseHeaders.contains("x-ratelimit-remaining")) {
122 rateLimitData.getsRemaining.store(static_cast<int64_t>(jsonifier::strToInt64(connection->data.responseHeaders.at("x-ratelimit-remaining").data())),
123 std::memory_order_release);
124 }
125 if (rateLimitData.getsRemaining.load(std::memory_order_acquire) <= 1 || rateLimitData.areWeASpecialBucket.load(std::memory_order_acquire)) {
126 rateLimitData.doWeWait.store(true, std::memory_order_release);
127 }
128 }
129
130 https_response_data https_rnr_builder::finalizeReturnValues(rate_limit_data& rateLimitData) {
131 auto connection{ static_cast<https_connection*>(this) };
132 if (connection->data.responseData.size() >= connection->data.contentLength && connection->data.contentLength > 0) {
133 connection->data.responseData = connection->data.responseData.substr(0, connection->data.contentLength);
134 } else {
135 auto pos1 = connection->data.responseData.findFirstOf('{');
136 auto pos2 = connection->data.responseData.findLastOf('}');
137 auto pos3 = connection->data.responseData.findFirstOf('[');
138 auto pos4 = connection->data.responseData.findLastOf(']');
139 if (pos1 != jsonifier::string_view::npos && pos2 != jsonifier::string_view::npos && pos1 < pos3) {
140 connection->data.responseData = connection->data.responseData.substr(pos1, pos2 + 1);
141 } else if (pos3 != jsonifier::string_view::npos && pos4 != jsonifier::string_view::npos) {
142 connection->data.responseData = connection->data.responseData.substr(pos3, pos4 + 1);
143 }
144 }
145 updateRateLimitData(rateLimitData);
146 return std::move(connection->data);
147 }
148
149 jsonifier::string https_rnr_builder::buildRequest(const https_workload_data& workload) {
150 jsonifier::string baseUrlNew{};
151 if (workload.baseUrl.find(".com") != jsonifier::string_view::npos) {
152 baseUrlNew = workload.baseUrl.substr(workload.baseUrl.find("https://") + jsonifier::string_view("https://").size(),
153 workload.baseUrl.find(".com") + jsonifier::string_view(".com").size() - jsonifier::string_view("https://").size());
154 } else if (workload.baseUrl.find(".org") != jsonifier::string_view::npos) {
155 baseUrlNew = workload.baseUrl.substr(workload.baseUrl.find("https://") + jsonifier::string_view("https://").size(),
156 workload.baseUrl.find(".org") + jsonifier::string_view(".org").size() - jsonifier::string_view("https://").size());
157 }
158 jsonifier::string returnString{};
159 if (workload.workloadClass == https_workload_class::Get || workload.workloadClass == https_workload_class::Delete) {
160 if (workload.workloadClass == https_workload_class::Get) {
161 returnString += "GET " + workload.baseUrl + workload.relativePath + " HTTP/1.1\r\n";
162 } else if (workload.workloadClass == https_workload_class::Delete) {
163 returnString += "DELETE " + workload.baseUrl + workload.relativePath + " HTTP/1.1\r\n";
164 }
165 for (auto& [key, value]: workload.headersToInsert) {
166 returnString += key + ": " + value + "\r\n";
167 }
168 returnString += "Pragma: no-cache\r\n";
169 returnString += "Connection: keep-alive\r\n";
170 returnString += "Host: " + baseUrlNew + "\r\n\r\n";
171 } else {
172 if (workload.workloadClass == https_workload_class::Patch) {
173 returnString += "PATCH " + workload.baseUrl + workload.relativePath + " HTTP/1.1\r\n";
174 } else if (workload.workloadClass == https_workload_class::Post) {
175 returnString += "POST " + workload.baseUrl + workload.relativePath + " HTTP/1.1\r\n";
176 } else if (workload.workloadClass == https_workload_class::Put) {
177 returnString = "PUT " + workload.baseUrl + workload.relativePath + " HTTP/1.1\r\n";
178 }
179 for (auto& [key, value]: workload.headersToInsert) {
180 returnString += key + ": " + value + "\r\n";
181 }
182 returnString += "Pragma: no-cache\r\n";
183 returnString += "Connection: keep-alive\r\n";
184 returnString += "Host: " + baseUrlNew + "\r\n";
185 returnString += "Content-Length: " + jsonifier::toString(workload.content.size()) + "\r\n\r\n";
186 returnString += workload.content;
187 }
188 return returnString;
189 }
190
191 bool https_rnr_builder::parseHeaders() {
192 auto connection{ static_cast<https_connection*>(this) };
193 jsonifier::string& stringViewNew = connection->inputBufferReal;
194 if (stringViewNew.find("\r\n\r\n") != jsonifier::string_view::npos) {
195 auto headers = tokenize(stringViewNew);
196 if (headers.size() && (headers.at(0).find("HTTP/1") != jsonifier::string_view::npos)) {
197 uint64_t parseCodeNew{};
198 try {
199 parseCodeNew = parseCode(headers.at(0));
200 } catch (const std::invalid_argument& error) {
201 message_printer::printError<print_message_type::https>(error.what());
202 connection->data.currentState = https_state::complete;
203 }
204 headers.erase(headers.begin());
205 if (headers.size() >= 3 && parseCodeNew) {
206 for (uint64_t x = 0; x < headers.size(); ++x) {
207 jsonifier::string_view::size_type sep = headers.at(x).find(": ");
208 if (sep != jsonifier::string_view::npos) {
209 jsonifier::string key = static_cast<jsonifier::string>(headers.at(x).substr(0, sep));
210 jsonifier::string_view value = headers.at(x).substr(sep + 2, headers.at(x).size());
211 for (auto& valueNew: key) {
212 valueNew = static_cast<char>(std::tolower(static_cast<int32_t>(valueNew)));
213 }
214 connection->data.responseHeaders.emplace(key, value);
215 }
216 }
217 if (connection->data.responseHeaders.contains("content-length")) {
218 connection->data.contentLength = jsonifier::strToUint64(connection->data.responseHeaders.at("content-length").data());
219 } else {
220 connection->data.contentLength = std::numeric_limits<uint32_t>::max();
221 connection->data.currentState = https_state::Collecting_Chunked_Contents;
222 }
223 connection->data.isItChunked = false;
224 if (connection->data.responseHeaders.contains("transfer-encoding")) {
225 if (connection->data.responseHeaders.at("transfer-encoding").find("chunked") != jsonifier::string_view::npos) {
226 connection->data.isItChunked = true;
227 connection->data.contentLength = 0;
228 connection->data.currentState = https_state::Collecting_Chunked_Contents;
229 }
230 }
231 connection->data.responseCode = parseCodeNew;
232 if (connection->data.responseCode == 302) {
233 connection->workload.baseUrl = connection->data.responseHeaders.at("location");
234 connection->disconnect();
235 return false;
236 }
237 if (connection->data.responseCode != 200 && connection->data.responseCode != 201 && connection->data.responseCode != std::numeric_limits<uint32_t>::max()) {
238 connection->inputBufferReal.erase(connection->inputBufferReal.begin() + static_cast<int64_t>(stringViewNew.find("\r\n\r\n")) + 4);
239 connection->data.currentState = https_state::complete;
240 return true;
241 } else if (!connection->data.isItChunked) {
242 connection->data.currentState = https_state::Collecting_Contents;
243 connection->inputBufferReal.erase(connection->inputBufferReal.begin() + static_cast<int64_t>(stringViewNew.find("\r\n\r\n")) + 4);
244 return true;
245 } else {
246 connection->inputBufferReal.erase(connection->inputBufferReal.begin() + static_cast<int64_t>(stringViewNew.find("\r\n\r\n")) + 4);
247 return true;
248 }
249 }
250 }
251 return true;
252 }
253 return false;
254 }
255
256 bool https_rnr_builder::parseChunk() {
257 auto connection{ static_cast<https_connection*>(this) };
258 jsonifier::string_view stringViewNew01{ connection->inputBufferReal };
259 if (auto finalPosition = stringViewNew01.find("\r\n0\r\n\r\n"); finalPosition != jsonifier::string_view::npos) {
260 uint64_t pos{ 0 };
261 while (pos < stringViewNew01.size()) {
262 uint64_t lineEnd = stringViewNew01.find("\r\n", pos);
263 if (lineEnd == jsonifier::string_view::npos) {
264 break;
265 }
266
267 jsonifier::string_view sizeLine{ stringViewNew01.data() + pos, lineEnd - pos };
268 uint64_t chunkSize = jsonifier::strToUint64<16>(static_cast<jsonifier::string>(sizeLine));
269
270 if (chunkSize == 0) {
271 break;
272 }
273
274 pos = lineEnd + 2;
275
276 jsonifier::string_view newString{ stringViewNew01.data() + pos, chunkSize };
277 connection->data.responseData += newString;
278 pos += chunkSize + 2;
279 }
280 connection->data.currentState = https_state::complete;
281 return true;
282 }
283 return false;
284 }
285
286 bool https_rnr_builder::parseContents() {
287 auto connection{ static_cast<https_connection*>(this) };
288 if (connection->inputBufferReal.size() >= connection->data.contentLength || !connection->data.contentLength) {
289 connection->data.responseData += jsonifier::string_view{ connection->inputBufferReal.data(), connection->data.contentLength };
290 connection->data.currentState = https_state::complete;
291 return true;
292 } else {
293 return false;
294 }
295 }
296
297 bool https_connection::areWeConnected() {
298 return tcp_connection::areWeStillConnected();
299 }
300
301 void https_connection::disconnect() {
302 tcp_connection::disconnect();
303 tcp_connection::reset();
304 }
305
306 void https_connection::resetValues(https_workload_data&& workloadDataNew, rate_limit_data* rateLimitDataNew) {
307 currentRateLimitData = rateLimitDataNew;
308 if (currentBaseUrl != workloadDataNew.baseUrl) {
309 tcp_connection::reset();
310 currentBaseUrl = workloadDataNew.baseUrl;
311 }
312 workload = std::move(workloadDataNew);
313 if (workload.baseUrl == "") {
314 workload.baseUrl = "https://discord.com/api/v10";
315 }
316 inputBufferReal.clear();
317 data = https_response_data{};
318 }
319
320 https_connection_manager::https_connection_manager(rate_limit_queue* rateLimitDataQueueNew) {
321 rateLimitQueue = rateLimitDataQueueNew;
322 }
323
324 rate_limit_queue& https_connection_manager::getRateLimitQueue() {
325 return *rateLimitQueue;
326 }
327
328 https_connection& https_connection_manager::getConnection(https_workload_type workloadType) {
329 std::unique_lock lock{ accessMutex };
330 if (!httpsConnections.contains(workloadType)) {
331 httpsConnections.emplace(workloadType, makeUnique<https_connection>());
332 }
333 httpsConnections.at(workloadType)->currentReconnectTries = 0;
334 return *httpsConnections.at(workloadType).get();
335 }
336
337 https_connection_stack_holder::https_connection_stack_holder(https_connection_manager& connectionManager, https_workload_data&& workload) {
338 connection = &connectionManager.getConnection(workload.getWorkloadType());
339 rateLimitQueue = &connectionManager.getRateLimitQueue();
340 auto rateLimitData = connectionManager.getRateLimitQueue().getEndpointAccess(workload.getWorkloadType());
341 if (!rateLimitData) {
342 throw dca_exception{ "Failed to gain endpoint access." };
343 }
344 connection->resetValues(std::move(workload), rateLimitData);
345 if (!connection->areWeConnected()) {
346 *static_cast<tcp_connection<https_connection>*>(connection) = https_connection{ connection->workload.baseUrl, static_cast<uint16_t>(443) };
347 }
348 }
349
350 https_connection_stack_holder::~https_connection_stack_holder() {
351 rateLimitQueue->releaseEndPointAccess(connection->workload.getWorkloadType());
352 }
353
354 https_connection& https_connection_stack_holder::getConnection() {
355 return *connection;
356 }
357
358 https_client::https_client(jsonifier::string_view botTokenNew) : https_client_core(botTokenNew), connectionManager(&rateLimitQueue) {
359 rateLimitQueue.initialize();
360 }
361
362 https_response_data https_client::httpsRequest(https_connection& connection) {
363 https_response_data resultData = executeByRateLimitData(connection);
364 return resultData;
365 }
366
367 https_response_data https_client_core::httpsRequestInternal(https_connection& connection) {
368 if (connection.workload.baseUrl == "https://discord.com/api/v10") {
369 connection.workload.headersToInsert.emplace("Authorization", "Bot " + botToken);
370 connection.workload.headersToInsert.emplace("User-Agent", "DiscordCoreAPI (https://discordcoreapi.com/1.0)");
371 if (connection.workload.payloadType == payload_type::Application_Json) {
372 connection.workload.headersToInsert.emplace("Content-Type", "application/json");
373 } else if (connection.workload.payloadType == payload_type::Multipart_Form) {
374 connection.workload.headersToInsert.emplace("Content-Type", "multipart/form-data; boundary=boundary25");
375 }
376 }
377 if (connection.currentReconnectTries >= connection.maxReconnectTries) {
378 connection.disconnect();
379 return https_response_data{};
380 }
381 if (!connection.areWeConnected()) {
382 connection.currentBaseUrl = connection.workload.baseUrl;
383 *static_cast<tcp_connection<https_connection>*>(&connection) = https_connection{ connection.workload.baseUrl, static_cast<uint16_t>(443) };
384 if (connection.currentStatus != connection_status::NO_Error || !connection.areWeConnected()) {
385 ++connection.currentReconnectTries;
386 connection.disconnect();
387 return httpsRequestInternal(connection);
388 }
389 }
390 auto request = connection.buildRequest(connection.workload);
391 if (connection.areWeConnected()) {
392 connection.writeData(static_cast<jsonifier::string_view>(request), true);
393 if (connection.currentStatus != connection_status::NO_Error || !connection.areWeConnected()) {
394 ++connection.currentReconnectTries;
395 connection.disconnect();
396 return httpsRequestInternal(connection);
397 }
398 auto result = getResponse(connection);
399 if (static_cast<int64_t>(result.responseCode) == -1 || !connection.areWeConnected()) {
400 ++connection.currentReconnectTries;
401 connection.disconnect();
402 return httpsRequestInternal(connection);
403 } else {
404 return result;
405 }
406 } else {
407 ++connection.currentReconnectTries;
408 connection.disconnect();
409 return httpsRequestInternal(connection);
410 }
411 }
412
413 https_response_data https_client::executeByRateLimitData(https_connection& connection) {
414 https_response_data returnData{};
415 milliseconds timeRemaining{};
416 milliseconds currentTime = std::chrono::duration_cast<milliseconds>(sys_clock::now().time_since_epoch());
417 if (connection.workload.workloadType == https_workload_type::Delete_Message_Old) {
418 connection.currentRateLimitData->sRemain.store(seconds{ 4 }, std::memory_order_release);
419 }
420 if (connection.workload.workloadType == https_workload_type::Post_Message || connection.workload.workloadType == https_workload_type::Patch_Message) {
421 connection.currentRateLimitData->areWeASpecialBucket.store(true, std::memory_order_release);
422 }
423 if (connection.currentRateLimitData->areWeASpecialBucket.load(std::memory_order_acquire)) {
424 connection.currentRateLimitData->sRemain.store(seconds{ static_cast<int64_t>(ceil(4.0f / 4.0f)) }, std::memory_order_release);
425 milliseconds targetTime{ connection.currentRateLimitData->sampledTimeInMs.load(std::memory_order_acquire) +
426 connection.currentRateLimitData->sRemain.load(std::memory_order_acquire) };
427 timeRemaining = targetTime - currentTime;
428 } else if (connection.currentRateLimitData->doWeWait.load(std::memory_order_acquire)) {
429 milliseconds targetTime{ connection.currentRateLimitData->sampledTimeInMs.load(std::memory_order_acquire) +
430 connection.currentRateLimitData->sRemain.load(std::memory_order_acquire) };
431 timeRemaining = targetTime - currentTime;
432 connection.currentRateLimitData->doWeWait.store(false, std::memory_order_release);
433 }
434 if (timeRemaining.count() > 0) {
435 message_printer::printSuccess<print_message_type::https>("we're waiting on rate-limit: " + jsonifier::toString(timeRemaining.count()));
436 milliseconds targetTime{ currentTime + timeRemaining };
437 while (targetTime > currentTime && targetTime.count() > 0 && currentTime.count() > 0 && timeRemaining.count() > 0) {
438 currentTime = std::chrono::duration_cast<milliseconds>(sys_clock::now().time_since_epoch());
439 timeRemaining = targetTime - currentTime;
440 if (timeRemaining.count() <= 20) {
441 continue;
442 } else {
443 std::this_thread::sleep_for(milliseconds{ static_cast<int64_t>(static_cast<double>(timeRemaining.count()) * 80.0f / 100.0f) });
444 }
445 }
446 }
447 returnData = https_client::httpsRequestInternal(connection);
448 connection.currentRateLimitData->sampledTimeInMs.store(std::chrono::duration_cast<std::chrono::duration<int64_t, std::milli>>(sys_clock::now().time_since_epoch()),
449 std::memory_order_release);
450
451 if (returnData.responseCode == 204 || returnData.responseCode == 201 || returnData.responseCode == 200) {
452 message_printer::printSuccess<print_message_type::https>(
453 connection.workload.callStack + " success: " + static_cast<jsonifier::string>(returnData.responseCode) + ": " + returnData.responseData);
454 } else if (returnData.responseCode == 429) {
455 if (connection.data.responseHeaders.contains("x-ratelimit-retry-after")) {
456 connection.currentRateLimitData->sRemain.store(seconds{ jsonifier::strToInt64(connection.data.responseHeaders.at("x-ratelimit-retry-after").data()) / 1000LL },
457 std::memory_order_release);
458 }
459 connection.currentRateLimitData->doWeWait.store(true, std::memory_order_release);
460 connection.currentRateLimitData->sampledTimeInMs.store(std::chrono::duration_cast<milliseconds>(sys_clock::now().time_since_epoch()), std::memory_order_release);
461 message_printer::printError<print_message_type::https>(connection.workload.callStack + "::httpsRequest(), we've hit rate limit! time remaining: " +
462 jsonifier::toString(connection.currentRateLimitData->sRemain.load(std::memory_order_acquire).count()));
463 connection.resetValues(std::move(connection.workload), connection.currentRateLimitData);
464 returnData = executeByRateLimitData(connection);
465 }
466 return returnData;
467 }
468
469 https_response_data https_client_core::recoverFromError(https_connection& connection) {
470 if (connection.currentReconnectTries >= connection.maxReconnectTries) {
471 connection.disconnect();
472 return connection.finalizeReturnValues(*connection.currentRateLimitData);
473 }
474 ++connection.currentReconnectTries;
475 connection.disconnect();
476 std::this_thread::sleep_for(150ms);
477 return httpsRequestInternal(connection);
478 }
479
480 https_response_data https_client_core::getResponse(https_connection& connection) {
481 stop_watch<milliseconds> stopWatch{ 10000ms };
482 stopWatch.reset();
483 while (connection.data.currentState != https_state::complete && !stopWatch.hasTimeElapsed()) {
484 if (connection.areWeConnected()) {
485 auto newState = connection.processIO(10);
486 switch (newState) {
487 case connection_status::NO_Error: {
488 continue;
489 }
490 case connection_status::CONNECTION_Error:
491 [[fallthrough]];
492 case connection_status::POLLERR_Error:
493 [[fallthrough]];
494 case connection_status::POLLHUP_Error:
495 [[fallthrough]];
496 case connection_status::POLLNVAL_Error:
497 [[fallthrough]];
498 case connection_status::READ_Error:
499 [[fallthrough]];
500 case connection_status::WRITE_Error:
501 [[fallthrough]];
502 case connection_status::SOCKET_Error:
503 [[fallthrough]];
504 default: {
505 return recoverFromError(connection);
506 }
507 }
508 } else {
509 return recoverFromError(connection);
510 }
511 }
512 return connection.finalizeReturnValues(*connection.currentRateLimitData);
513 }
514 }
515}
The main namespace for the forward-facing interfaces.