TDME2  1.9.200
HTTPDownloadClient.cpp
Go to the documentation of this file.
2 
3 #include <iomanip>
4 #include <memory>
5 #include <filesystem>
6 #include <fstream>
7 #include <string>
8 #include <sstream>
9 #include <unordered_map>
10 #include <vector>
11 
12 #include <tdme/tdme.h>
13 #include <tdme/math/Math.h>
23 #include <tdme/utilities/Base64.h>
25 #include <tdme/utilities/Console.h>
27 #include <tdme/utilities/Integer.h>
30 
31 using std::hex;
32 using std::make_unique;
33 using std::nouppercase;
34 using std::ifstream;
35 using std::ios;
36 using std::ofstream;
37 using std::ostringstream;
38 using std::setw;
39 using std::string;
40 using std::to_string;
41 using std::unique_ptr;
42 using std::unordered_map;
43 using std::uppercase;
44 using std::vector;
45 
46 using tdme::math::Math;
63 
65 
66 HTTPDownloadClient::HTTPDownloadClient(): downloadThreadMutex("downloadthread-mutex") {
67 }
68 
69 string HTTPDownloadClient::urlEncode(const string &value) {
70  // TODO: put me into utilities
71  // see: https://stackoverflow.com/questions/154536/encode-decode-urls-in-c
72  ostringstream escaped;
73  escaped.fill('0');
74  escaped << hex;
75 
76  for (string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
77  string::value_type c = (*i);
78 
79  // Keep alphanumeric and other accepted characters intact
80  if (Character::isAlphaNumeric(c) == true || c == '-' || c == '_' || c == '.' || c == '~') {
81  escaped << c;
82  continue;
83  }
84 
85  // Any other characters are percent-encoded
86  escaped << uppercase;
87  escaped << '%' << setw(2) << int((unsigned char) c);
88  escaped << nouppercase;
89  }
90 
91  return escaped.str();
92 }
93 
94 string HTTPDownloadClient::createHTTPRequestHeaders(const string& hostName, const string& relativeUrl) {
95  string query;
96  for (const auto& [parameterName, parameterValue]: getParameters) {
97  if (query.empty() == true) query+= "?"; else query+="&";
98  query+= urlEncode(parameterName) + "=" + urlEncode(parameterValue);
99  }
100  auto request =
101  string("GET " + relativeUrl + query + " HTTP/1.1\r\n") +
102  string("User-Agent: tdme2-httpdownloadclient\r\n") +
103  string("Host: " + hostName + "\r\n") +
104  string("Connection: close\r\n");
105  if (username.empty() == false || password.empty() == false) {
106  string base64Pass;
107  Base64::encode(username + ":" + password, base64Pass);
108  request+= "Authorization: Basic " + base64Pass + "\r\n";
109  }
110  for (const auto& [headerName, headerValue]: headers) {
111  request+= headerName + ": " + headerValue + "\r\n";
112  }
113  request+=
114  string("\r\n");
115  return request;
116 }
117 
118 uint64_t HTTPDownloadClient::parseHTTPResponseHeaders(ifstream& rawResponse) {
119  responseHeaders.clear();
120  auto headerSize = 0ll;
121  auto returnHeaderSize = 0ll;
122  int headerIdx = 0;
123  string statusHeader;
124  string line;
125  char lastChar = -1;
126  char currentChar;
127  while (rawResponse.eof() == false) {
128  rawResponse.get(currentChar);
129  headerSize++;
130  if (lastChar == '\r' && currentChar == '\n') {
131  if (line.empty() == false) {
132  if (headerIdx == 0) {
133  statusHeader = line;
134  headerIdx++;
135  } else {
136  auto headerNameValueSeparator = StringTools::indexOf(line, ':');
137  responseHeaders[StringTools::trim(StringTools::substring(line, 0, headerNameValueSeparator))] =
138  StringTools::trim(StringTools::substring(line, headerNameValueSeparator + 1));
139  }
140  } else {
141  returnHeaderSize = headerSize;
142  break;
143  }
144  line.clear();
145  } else
146  if (currentChar != '\r' && currentChar != '\n') {
147  line+= currentChar;
148  }
149  lastChar = currentChar;
150  }
151  if (statusHeader.empty() == false) {
152  StringTokenizer t;
153  t.tokenize(statusHeader, " ");
154  for (auto i = 0; i < 3 && t.hasMoreTokens(); i++) {
155  auto token = t.nextToken();
156  if (i == 1) {
157  statusCode = Integer::parse(token);
158  }
159  }
160  }
161  //
162  return returnHeaderSize;
163 }
164 
166  url.clear();
167  file.clear();
168  headers.clear();
169  getParameters.clear();
170  statusCode = -1;
171  responseHeaders.clear();
172  //
173  haveHeaders = false;
174  haveContentSize = false;
175  headerSize = 0LL;
176  contentSize = 0LL;
177  finished = true;
178  progress = 0.0f;
179 }
180 
182  class DownloadThread: public Thread {
183  public:
184  DownloadThread(HTTPDownloadClient* downloadClient): Thread("download-thread"), downloadClient(downloadClient) {
185  }
186  private:
187  void run() {
188  downloadClient->finished = false;
189  downloadClient->progress = 0.0f;
190  unique_ptr<TCPSocket> socket;
191  try {
192  // TODO: we might need a class to determine protocol, hostname and port, yaaar
193  auto protocolSeparatorIdx = StringTools::indexOf(downloadClient->url, string("://"));
194  if (protocolSeparatorIdx == -1) throw HTTPClientException("Invalid URL");
195  auto relativeUrl = StringTools::substring(downloadClient->url, protocolSeparatorIdx + 3);
196  if (relativeUrl.empty() == true) throw HTTPClientException("No URL given");
197  auto slashIdx = relativeUrl.find('/');
198  auto hostname = relativeUrl;
199  if (slashIdx != -1) hostname = StringTools::substring(relativeUrl, 0, slashIdx);
200  relativeUrl = StringTools::substring(relativeUrl, hostname.size());
201  // socket
202  if (StringTools::startsWith(downloadClient->url, "http://") == true) {
203  //
204  auto ip = Network::getIpByHostname(hostname);
205  if (ip.empty() == true) {
206  Console::println("HTTPDownloadClient::execute(): failed");
207  throw HTTPClientException("Could not resolve host IP by hostname");
208  }
209  //
210  socket = make_unique<TCPSocket>();
211  socket->connect(ip, 80);
212  } else
213  if (StringTools::startsWith(downloadClient->url, "https://") == true) {
214  socket = make_unique<SecureTCPSocket>();
215  socket->connect(hostname, 443);
216  } else {
217  throw HTTPClientException("Invalid protocol");
218  }
219  //
220  auto request = downloadClient->createHTTPRequestHeaders(hostname, relativeUrl);
221  socket->write((void*)request.data(), request.length());
222 
223  {
224  // output file stream
225  ofstream ofs(std::filesystem::u8path(downloadClient->file + ".download"), ofstream::binary);
226  if (ofs.is_open() == false) {
227  throw HTTPClientException("Unable to open file for writing(" + to_string(errno) + "): " + (downloadClient->file + ".download"));
228  }
229 
230  // download
231  char rawResponseBuf[16384];
232  auto rawResponseBytesRead = 0;
233  uint64_t bytesRead = 0;
234  try {
235  for (;isStopRequested() == false;) {
236  auto rawResponseBytesRead = socket->read(rawResponseBuf, sizeof(rawResponseBuf));
237  ofs.write(rawResponseBuf, rawResponseBytesRead);
238  if (downloadClient->haveHeaders == false) {
239  // flush download file to disk
240  ofs.flush();
241  // input file stream
242  ifstream ifs(std::filesystem::u8path(downloadClient->file + ".download"), ofstream::binary);
243  if (ifs.is_open() == false) {
244  throw HTTPClientException("Unable to open file for reading(" + to_string(errno) + "): " + (downloadClient->file + ".download"));
245  }
246  // try to read headers
247  downloadClient->responseHeaders.clear();
248  if ((downloadClient->headerSize = downloadClient->parseHTTPResponseHeaders(ifs)) > 0) {
249  downloadClient->haveHeaders = true;
250  auto contentLengthHeaderIt = downloadClient->responseHeaders.find("Content-Length");
251  if (contentLengthHeaderIt != downloadClient->responseHeaders.end()) {
252  const auto& contentLengthHeader = contentLengthHeaderIt->second;
253  downloadClient->haveContentSize = true;
254  downloadClient->contentSize = Integer::parse(contentLengthHeader);
255  }
256  }
257  ifs.close();
258  }
259  bytesRead+= rawResponseBytesRead;
260  if (downloadClient->haveHeaders == true && downloadClient->haveContentSize == true) {
261  downloadClient->progress = static_cast<float>(bytesRead - downloadClient->headerSize) / static_cast<float>(downloadClient->contentSize);
262  }
263  };
264  } catch (NetworkSocketClosedException& sce) {
265  // end of stream
266  }
267 
268  // close download file
269  ofs.close();
270  }
271 
272  // transfer to real file
273  if (downloadClient->statusCode == 200 && isStopRequested() == false) {
274  // input file stream
275  ifstream ifs(std::filesystem::u8path(downloadClient->file + ".download"), ofstream::binary);
276  if (ifs.is_open() == false) {
277  throw HTTPClientException("Unable to open file for reading(" + to_string(errno) + "): " + (downloadClient->file + ".download"));
278  }
279 
280  ifs.seekg(downloadClient->headerSize, ios::beg);
281  auto ifsHeaderSize = ifs.tellg();
282  ifs.seekg(0, ios::end);
283  auto ifsSizeTotal = ifs.tellg();
284  auto ifsSize = ifsSizeTotal - ifsHeaderSize;
285  ifs.seekg(ifsHeaderSize, ios::beg);
286 
287  // output file stream
288  ofstream ofs(std::filesystem::u8path(downloadClient->file), ofstream::binary);
289  if (ofs.is_open() == false) {
290  throw HTTPClientException("Unable to open file for writing(" + to_string(errno) + "): " + downloadClient->file);
291  }
292 
293  //
294  char buf[16384];
295  auto ifsBytesToRead = 0;
296  auto ifsBytesRead = 0;
297  do {
298  auto ifsBytesToRead = Math::min(static_cast<int64_t>(ifsSize - ifsBytesRead), sizeof(buf));
299  ifs.read(buf, ifsBytesToRead);
300  ofs.write(buf, ifsBytesToRead);
301  ifsBytesRead+= ifsBytesToRead;
302  } while (ifsBytesRead < ifsSize);
303 
304  // close target file
305  ofs.close();
306 
307  // close download file
308  ifs.close();
309  }
310 
311  //
312  FileSystem::getStandardFileSystem()->removeFile(".", downloadClient->file + ".download");
313 
314  //
315  socket->shutdown();
316 
317  //
318  downloadClient->finished = true;
319  downloadClient->progress = 1.0f;
320  } catch (Exception& exception) {
321  socket->shutdown();
322  downloadClient->finished = true;
323  Console::println(string("HTTPDownloadClient::execute(): performed HTTP request: FAILED: ") + exception.what());
324  }
325  }
326  private:
327  HTTPDownloadClient* downloadClient;
328  };
330  finished = false;
331  this->downloadThread = make_unique<DownloadThread>(this);
332  this->downloadThread->start();
334 }
335 
338  if (downloadThread != nullptr) downloadThread->stop();
340 }
341 
344  if (downloadThread != nullptr) {
345  downloadThread->join();
346  this->downloadThread = nullptr;
347  }
349 }
Standard math functions.
Definition: Math.h:19
uint64_t parseHTTPResponseHeaders(ifstream &rawResponse)
Parse HTTP response headers.
void start()
Starts the HTTP download to file.
unordered_map< string, string > getParameters
void join()
Wait until underlying thread has finished.
unordered_map< string, string > responseHeaders
static string urlEncode(const string &value)
Returns a URL encoded representation of value.
string createHTTPRequestHeaders(const string &hostName, const string &relativeUrl)
Create HTTP request headers.
File system singleton class.
Definition: FileSystem.h:17
Network class.
Definition: Network.h:14
Class representing a secure TCP socket.
Class representing a TCP socket.
Definition: TCPSocket.h:15
Mutex implementation.
Definition: Mutex.h:19
void unlock()
Unlocks this mutex.
Definition: Mutex.h:54
void lock()
Locks the mutex, additionally mutex locks will block until other locks have been unlocked.
Definition: Mutex.h:47
Base class for threads.
Definition: Thread.h:20
Base64 encoding/decoding class.
Definition: Base64.h:16
Character class.
Definition: Character.h:17
Console class.
Definition: Console.h:29
Integer class.
Definition: Integer.h:25
String tokenizer class.
void tokenize(const string &str, const string &delimiters, bool emptyTokens=false)
Tokenize.
String tools class.
Definition: StringTools.h:22
std::exception Exception
Exception base class.
Definition: Exception.h:18