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