|
@@ -0,0 +1,273 @@
|
|
|
+
|
|
|
+
|
|
|
+#include "HTTPClient.h"
|
|
|
+#include "TCPSocket.h"
|
|
|
+
|
|
|
+using namespace bell;
|
|
|
+
|
|
|
+struct HTTPClient::HTTPResponse *HTTPClient::execute(const struct HTTPRequest &request) {
|
|
|
+ auto *response = new HTTPResponse();
|
|
|
+ auto *url = request.url.c_str();
|
|
|
+ HTTPClient::executeImpl(request, url, response);
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+void HTTPClient::executeImpl(const struct HTTPRequest &request, const char *url, struct HTTPResponse *&response) {
|
|
|
+ bool https = url[4] == 's';
|
|
|
+ uint16_t port = https ? 443 : 80;
|
|
|
+ auto *hostname = url + (https ? 8 : 7);
|
|
|
+ auto *hostnameEnd = strchr(hostname, ':');
|
|
|
+ auto *path = strchr(hostname, '/');
|
|
|
+ if (hostnameEnd == nullptr) {
|
|
|
+ hostnameEnd = path;
|
|
|
+ } else {
|
|
|
+ port = strtol(hostnameEnd + 1, nullptr, 10);
|
|
|
+ }
|
|
|
+ auto hostnameStr = std::string(hostname, (const char *)hostnameEnd);
|
|
|
+
|
|
|
+ if (https) {
|
|
|
+ response->socket = std::make_shared<TLSSocket>();
|
|
|
+ } else {
|
|
|
+ response->socket = std::make_shared<TCPSocket>();
|
|
|
+ }
|
|
|
+ response->socket->open(hostnameStr, port);
|
|
|
+
|
|
|
+ const char *endl = "\r\n";
|
|
|
+ std::stringstream stream;
|
|
|
+ switch (request.method) {
|
|
|
+ case HTTPMethod::GET:
|
|
|
+ stream << "GET ";
|
|
|
+ break;
|
|
|
+ case HTTPMethod::POST:
|
|
|
+ stream << "POST ";
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ stream << path << " HTTP/1.1" << endl;
|
|
|
+ stream << "Host: " << hostnameStr << ":" << port << endl;
|
|
|
+ stream << "Accept: */*" << endl;
|
|
|
+ if (!request.body.empty()) {
|
|
|
+ stream << "Content-Type: " << request.contentType << endl;
|
|
|
+ stream << "Content-Length: " << request.body.size() << endl;
|
|
|
+ }
|
|
|
+ for (const auto &header : request.headers) {
|
|
|
+ stream << header.first << ": " << header.second << endl;
|
|
|
+ }
|
|
|
+ stream << endl;
|
|
|
+ stream << request.body;
|
|
|
+ std::string data = stream.str();
|
|
|
+
|
|
|
+ size_t len = response->socket->write((uint8_t *)data.c_str(), data.size());
|
|
|
+ if (len != data.size()) {
|
|
|
+ response->close();
|
|
|
+ BELL_LOG(error, "http", "Writing failed: wrote %d of %d bytes", len, data.size());
|
|
|
+ free(response);
|
|
|
+ response = nullptr;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ response->readHeaders();
|
|
|
+
|
|
|
+ if (response->isRedirect && (request.maxRedirects < 0 || response->redirectCount < request.maxRedirects)) {
|
|
|
+ response->redirectCount++;
|
|
|
+ response->close();
|
|
|
+ HTTPClient::executeImpl(request, response->location.c_str(), response);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool HTTPClient::readHeader(const char *&header, const char *name) {
|
|
|
+ size_t len = strlen(name);
|
|
|
+ if (strncasecmp(header, name, len) == 0) {
|
|
|
+ header += len;
|
|
|
+ while (*header == ' ')
|
|
|
+ header++;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+size_t HTTPClient::HTTPResponse::readRaw(char *dst) {
|
|
|
+ size_t len = this->socket->read((uint8_t *)dst, BUF_SIZE);
|
|
|
+
|
|
|
+ this->bodyRead += len;
|
|
|
+ dst[len] = '\0';
|
|
|
+ return len;
|
|
|
+}
|
|
|
+
|
|
|
+void HTTPClient::HTTPResponse::readHeaders() {
|
|
|
+ size_t len;
|
|
|
+ char *line, *lineEnd;
|
|
|
+ bool complete = false;
|
|
|
+ std::string lineBuf;
|
|
|
+
|
|
|
+ if (this->buf == nullptr) {
|
|
|
+ this->buf = static_cast<char *>(malloc(BUF_SIZE + 1));
|
|
|
+ this->bufPtr = this->buf;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ this->statusCode = 0;
|
|
|
+ this->contentLength = 0;
|
|
|
+ this->isChunked = false;
|
|
|
+ this->isGzip = false;
|
|
|
+ this->isComplete = false;
|
|
|
+ this->isRedirect = false;
|
|
|
+ this->isStreaming = false;
|
|
|
+ do {
|
|
|
+ len = this->readRaw(this->buf);
|
|
|
+ line = this->buf;
|
|
|
+ do {
|
|
|
+ lineEnd = strstr(line, "\r\n");
|
|
|
+ if (!lineEnd) {
|
|
|
+ lineBuf += std::string(line, this->buf + len);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ lineBuf += std::string(line, lineEnd);
|
|
|
+ if (lineBuf.empty()) {
|
|
|
+ complete = true;
|
|
|
+
|
|
|
+ if (lineEnd + 2 < this->buf + len) {
|
|
|
+ this->bufPtr = lineEnd + 2;
|
|
|
+ this->bufRemaining = len - (this->bufPtr - this->buf);
|
|
|
+ this->bodyRead = this->bufRemaining;
|
|
|
+ this->isStreaming =
|
|
|
+ !this->isComplete && !this->contentLength && (len < BUF_SIZE || this->socket->poll() == 0);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto *header = lineBuf.c_str();
|
|
|
+ if (strncmp(header, "HTTP/", 5) == 0) {
|
|
|
+ header += 9;
|
|
|
+ this->statusCode = strtol(header, nullptr, 10);
|
|
|
+ } else if (readHeader(header, "content-type:")) {
|
|
|
+ this->contentType = std::string(header);
|
|
|
+ } else if (readHeader(header, "content-length:")) {
|
|
|
+ this->contentLength = strtol(header, nullptr, 10);
|
|
|
+ if (!this->contentLength)
|
|
|
+ this->isComplete = true;
|
|
|
+ } else if (readHeader(header, "transfer-encoding:")) {
|
|
|
+ this->isChunked = strncmp(header, "chunked", 7) == 0;
|
|
|
+ } else if (readHeader(header, "location:")) {
|
|
|
+ this->isRedirect = true;
|
|
|
+ this->location = std::string(header);
|
|
|
+ } else {
|
|
|
+ char *colonPtr = (char*) strchr(header, ':');
|
|
|
+ if (colonPtr) {
|
|
|
+ auto *valuePtr = colonPtr + 1;
|
|
|
+ while (*valuePtr == ' ')
|
|
|
+ valuePtr++;
|
|
|
+ *colonPtr = '\0';
|
|
|
+ for (auto *p = (char *)header; *p; ++p)
|
|
|
+ *p = (char)tolower(*p);
|
|
|
+ this->headers[std::string(header)] = std::string(valuePtr);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ lineBuf.clear();
|
|
|
+ line = lineEnd + 2;
|
|
|
+ } while (true);
|
|
|
+ } while (!complete);
|
|
|
+}
|
|
|
+
|
|
|
+bool HTTPClient::HTTPResponse::skip(size_t len, bool dontRead) {
|
|
|
+ size_t skip = 0;
|
|
|
+ if (len > bufRemaining) {
|
|
|
+ skip = len - bufRemaining;
|
|
|
+ len = bufRemaining;
|
|
|
+ }
|
|
|
+ bufRemaining -= len;
|
|
|
+ bufPtr += len;
|
|
|
+ if (!bufRemaining && !dontRead) {
|
|
|
+ if (isComplete || (contentLength && bodyRead >= contentLength && !chunkRemaining)) {
|
|
|
+ isComplete = true;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ bufRemaining = this->readRaw(this->buf);
|
|
|
+ if (!bufRemaining)
|
|
|
+ return false;
|
|
|
+ bufPtr = this->buf + skip;
|
|
|
+ bufRemaining -= skip;
|
|
|
+ if (!contentLength && bufRemaining < BUF_SIZE) {
|
|
|
+
|
|
|
+ isStreaming = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+size_t HTTPClient::HTTPResponse::read(char *dst, size_t toRead) {
|
|
|
+ if (isComplete) {
|
|
|
+
|
|
|
+ dst[0] = '\0';
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ auto *dstStart = dst;
|
|
|
+ size_t read = 0;
|
|
|
+ while (toRead) {
|
|
|
+ skip(0);
|
|
|
+ if (isChunked && !chunkRemaining) {
|
|
|
+ if (*bufPtr == '0') {
|
|
|
+ isComplete = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ auto *endPtr = bufPtr;
|
|
|
+ if (strchr(bufPtr, '\r') == nullptr) {
|
|
|
+ auto size = std::string(bufPtr, bufPtr + bufRemaining);
|
|
|
+ if (!skip(bufRemaining))
|
|
|
+ break;
|
|
|
+ endPtr = strchr(bufPtr, '\r');
|
|
|
+ if (endPtr == nullptr)
|
|
|
+ break;
|
|
|
+ size += std::string(bufPtr, endPtr);
|
|
|
+ chunkRemaining = std::stoul(size, nullptr, 16);
|
|
|
+ } else {
|
|
|
+ chunkRemaining = strtol(bufPtr, &endPtr, 16);
|
|
|
+ }
|
|
|
+ if (!skip(endPtr - bufPtr + 2))
|
|
|
+ break;
|
|
|
+ } else if (contentLength && !chunkRemaining) {
|
|
|
+ chunkRemaining = contentLength;
|
|
|
+ }
|
|
|
+
|
|
|
+ while (chunkRemaining && toRead) {
|
|
|
+ size_t count = std::min(toRead, std::min(bufRemaining, chunkRemaining));
|
|
|
+ strncpy(dst, bufPtr, count);
|
|
|
+ dst += count;
|
|
|
+ read += count;
|
|
|
+ chunkRemaining -= count;
|
|
|
+ toRead -= count;
|
|
|
+ if (!skip(count)) {
|
|
|
+ toRead = 0;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (isChunked && !chunkRemaining && !skip(2, isStreaming))
|
|
|
+ toRead = 0;
|
|
|
+ }
|
|
|
+ if (isStreaming && !bufRemaining) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!isChunked && contentLength && !chunkRemaining)
|
|
|
+ isComplete = true;
|
|
|
+
|
|
|
+ dstStart[read] = '\0';
|
|
|
+ return read;
|
|
|
+}
|
|
|
+
|
|
|
+std::string HTTPClient::HTTPResponse::readToString() {
|
|
|
+ if (this->contentLength) {
|
|
|
+ std::string result(this->contentLength, '\0');
|
|
|
+ this->read(result.data(), this->contentLength);
|
|
|
+ this->close();
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ std::string result;
|
|
|
+ char buffer[BUF_SIZE];
|
|
|
+ size_t len;
|
|
|
+ do {
|
|
|
+ len = this->read(buffer, BUF_SIZE);
|
|
|
+ result += std::string(buffer);
|
|
|
+ } while (len);
|
|
|
+ this->close();
|
|
|
+ return result;
|
|
|
+}
|