httpObject.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. //-----------------------------------------------------------------------------
  2. // Copyright (c) 2012 GarageGames, LLC
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to
  6. // deal in the Software without restriction, including without limitation the
  7. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  8. // sell copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  20. // IN THE SOFTWARE.
  21. //-----------------------------------------------------------------------------
  22. //-----------------------------------------------------------------------------
  23. // Copyright (c) 2017 The Platinum Team
  24. //
  25. // Permission is hereby granted, free of charge, to any person obtaining a copy
  26. // of this software and associated documentation files (the "Software"), to
  27. // deal in the Software without restriction, including without limitation the
  28. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  29. // sell copies of the Software, and to permit persons to whom the Software is
  30. // furnished to do so, subject to the following conditions:
  31. //
  32. // The above copyright notice and this permission notice shall be included in
  33. // all copies or substantial portions of the Software.
  34. //
  35. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  36. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  37. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  38. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  39. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  40. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  41. // DEALINGS IN THE SOFTWARE.
  42. //-----------------------------------------------------------------------------
  43. #include "app/net/httpObject.h"
  44. #include "platform/platform.h"
  45. #include "core/stream/fileStream.h"
  46. #include "console/simBase.h"
  47. #include "console/consoleInternal.h"
  48. #include "console/engineAPI.h"
  49. #include <string.h>
  50. IMPLEMENT_CONOBJECT(HTTPObject);
  51. ConsoleDocClass(HTTPObject,
  52. "@brief Allows communications between the game and a server using HTTP.\n\n"
  53. "HTTPObject is derrived from TCPObject and makes use of the same callbacks for dealing with "
  54. "connections and received data. However, the way in which you use HTTPObject to connect "
  55. "with a server is different than TCPObject. Rather than opening a connection, sending data, "
  56. "waiting to receive data, and then closing the connection, you issue a get() or post() and "
  57. "handle the response. The connection is automatically created and destroyed for you.\n\n"
  58. "@tsexample\n"
  59. "// In this example we'll retrieve the weather in Las Vegas using\n"
  60. "// Google's API. The response is in XML which could be processed\n"
  61. "// and used by the game using SimXMLDocument, but we'll just output\n"
  62. "// the results to the console in this example.\n\n"
  63. "// Define callbacks for our specific HTTPObject using our instance's\n"
  64. "// name (WeatherFeed) as the namespace.\n\n"
  65. "// Handle an issue with resolving the server's name\n"
  66. "function WeatherFeed::onDNSFailed(%this)\n"
  67. "{\n"
  68. " // Store this state\n"
  69. " %this.lastState = \"DNSFailed\";\n\n"
  70. " // Handle DNS failure\n"
  71. "}\n\n"
  72. "function WeatherFeed::onConnectFailed(%this)\n"
  73. "{\n"
  74. " // Store this state\n"
  75. " %this.lastState = \"ConnectFailed\";\n\n"
  76. " // Handle connection failure\n"
  77. "}\n\n"
  78. "function WeatherFeed::onDNSResolved(%this)\n"
  79. "{\n"
  80. " // Store this state\n"
  81. " %this.lastState = \"DNSResolved\";\n\n"
  82. "}\n\n"
  83. "function WeatherFeed::onConnected(%this)\n"
  84. "{\n"
  85. " // Store this state\n"
  86. " %this.lastState = \"Connected\";\n\n"
  87. " // Clear our buffer\n"
  88. " %this.buffer = \"\";\n"
  89. "}\n\n"
  90. "function WeatherFeed::onDisconnect(%this)\n"
  91. "{\n"
  92. " // Store this state\n"
  93. " %this.lastState = \"Disconnected\";\n\n"
  94. " // Output the buffer to the console\n"
  95. " echo(\"Google Weather Results:\");\n"
  96. " echo(%this.buffer);\n"
  97. "}\n\n"
  98. "// Handle a line from the server\n"
  99. "function WeatherFeed::onLine(%this, %line)\n"
  100. "{\n"
  101. " // Store this line in out buffer\n"
  102. " %this.buffer = %this.buffer @ %line;\n"
  103. "}\n\n"
  104. "// Create the HTTPObject\n"
  105. "%feed = new HTTPObject(WeatherFeed);\n\n"
  106. "// Define a dynamic field to store the last connection state\n"
  107. "%feed.lastState = \"None\";\n\n"
  108. "// Send the GET command\n"
  109. "%feed.get(\"www.google.com:80\", \"/ig/api\", \"weather=Las-Vegas,US\");\n"
  110. "@endtsexample\n\n"
  111. "@see TCPObject\n"
  112. "@ingroup Networking\n"
  113. );
  114. CURLM *HTTPObject::gCurlMulti = nullptr;
  115. int HTTPObject::gCurlMultiTotal = 0;
  116. std::unordered_map<CURL *, HTTPObject *> HTTPObject::gCurlMap;
  117. size_t HTTPObject::writeCallback(char *buffer, size_t size, size_t nitems, HTTPObject *object) {
  118. return object->processData(buffer, size, nitems);
  119. }
  120. size_t HTTPObject::headerCallback(char *buffer, size_t size, size_t nitems, HTTPObject *object) {
  121. return object->processHeader(buffer, size, nitems);
  122. }
  123. //--------------------------------------
  124. HTTPObject::HTTPObject()
  125. : mCurl(nullptr),
  126. mBuffer(nullptr),
  127. mBufferSize(0),
  128. mBufferUsed(0),
  129. mDownload(false),
  130. mHeaders(nullptr)
  131. {
  132. CURL *request = curl_easy_init();
  133. curl_easy_setopt(request, CURLOPT_VERBOSE, false);
  134. curl_easy_setopt(request, CURLOPT_FOLLOWLOCATION, true);
  135. curl_easy_setopt(request, CURLOPT_TRANSFERTEXT, true);
  136. curl_easy_setopt(request, CURLOPT_USERAGENT, "Torque 1.0");
  137. curl_easy_setopt(request, CURLOPT_ENCODING, "ISO 8859-1");
  138. mCurl = request;
  139. gCurlMap[request] = this;
  140. curl_easy_setopt(request, CURLOPT_WRITEDATA, this);
  141. curl_easy_setopt(request, CURLOPT_WRITEFUNCTION, writeCallback);
  142. curl_easy_setopt(request, CURLOPT_HEADERDATA, this);
  143. curl_easy_setopt(request, CURLOPT_HEADERFUNCTION, headerCallback);
  144. }
  145. HTTPObject::~HTTPObject()
  146. {
  147. }
  148. //--------------------------------------
  149. bool HTTPObject::ensureBuffer(U32 length)
  150. {
  151. if (mBufferSize < length) {
  152. //CURL_MAX_WRITE_SIZE is the maximum packet size we'll be given. So round
  153. // off to that and we should not have to allocate too often.
  154. length = ((length / CURL_MAX_WRITE_SIZE) + 1) * CURL_MAX_WRITE_SIZE;
  155. void *alloced = dRealloc(mBuffer, length * sizeof(char));
  156. //Out of memory
  157. if (!alloced) {
  158. return false;
  159. }
  160. mBuffer = (U8 *)alloced;
  161. mBufferSize = length;
  162. }
  163. return true;
  164. }
  165. size_t HTTPObject::processData(char *buffer, size_t size, size_t nitems)
  166. {
  167. size_t writeSize = size * nitems + 1;
  168. if (!ensureBuffer(mBufferUsed + writeSize)) {
  169. //Error
  170. return 0;
  171. }
  172. memcpy(mBuffer + mBufferUsed, buffer, size * nitems);
  173. mBufferUsed += size * nitems;
  174. mBuffer[mBufferUsed] = 0;
  175. return size * nitems;
  176. }
  177. size_t HTTPObject::processHeader(char *buffer, size_t size, size_t nitems)
  178. {
  179. char *colon = strchr(buffer, ':');
  180. if (colon != NULL) {
  181. std::string key(buffer, colon - buffer);
  182. std::string value(colon + 2);
  183. if (value[value.length() - 1] == '\n')
  184. value.erase(value.length() - 1, 1);
  185. if (value[value.length() - 1] == '\r')
  186. value.erase(value.length() - 1, 1);
  187. mRecieveHeaders[key] = value;
  188. }
  189. return size * nitems;
  190. }
  191. void HTTPObject::start()
  192. {
  193. CURLMcode result = curl_multi_add_handle(gCurlMulti, mCurl);
  194. if (result != CURLM_OK) {
  195. Con::errorf("curl_easy_perform failed (%d): %s", result, curl_multi_strerror(result));
  196. return;
  197. }
  198. ++gCurlMultiTotal;
  199. }
  200. void HTTPObject::processLines()
  201. {
  202. if (mDownload) {
  203. const std::string &dlPath = mDownloadPath;
  204. int lastSlash = dlPath.find_last_of('/');
  205. const char *path;
  206. const char *file;
  207. if (lastSlash == std::string::npos) {
  208. //No
  209. return;
  210. } else {
  211. path = StringTable->insert(dlPath.c_str(), false);
  212. file = StringTable->insert(dlPath.substr(lastSlash + 1).c_str(), false);
  213. }
  214. //Don't download unless we get an OK
  215. long responseCode;
  216. curl_easy_getinfo(mCurl, CURLINFO_RESPONSE_CODE, &responseCode);
  217. if (responseCode != 200) {
  218. onDownloadFailed(path);
  219. return;
  220. }
  221. //Write to the output file
  222. FileStream *stream = new FileStream();
  223. if (!stream->open(path, Torque::FS::File::Read)) {
  224. Con::errorf("Could not download %s: error opening stream.");
  225. onDownloadFailed(path);
  226. return;
  227. }
  228. stream->write(mBufferUsed, mBuffer);
  229. stream->close();
  230. onDownload(path);
  231. delete stream;
  232. } else {
  233. //Pull all the lines out of mBuffer
  234. char *str = (char *)mBuffer;
  235. char *nextLine = str;
  236. while (str && nextLine) {
  237. nextLine = strchr(str, '\n');
  238. //Get how long the current line for allocating
  239. U32 lineSize = 0;
  240. if (nextLine == NULL) {
  241. lineSize = strlen(str);
  242. if (lineSize == 0) {
  243. break;
  244. }
  245. } else {
  246. lineSize = nextLine - str;
  247. }
  248. //Copy into a return buffer for the script
  249. char *line = Con::getReturnBuffer(lineSize + 1);
  250. memcpy(line, str, lineSize);
  251. line[lineSize] = 0;
  252. //Strip the \r from \r\n
  253. if (lineSize > 0 && line[lineSize - 1] == '\r') {
  254. line[lineSize - 1] = 0;
  255. }
  256. onLine(line);
  257. if (nextLine) {
  258. //Strip the \n
  259. str = nextLine + 1;
  260. }
  261. }
  262. }
  263. }
  264. void HTTPObject::finish(CURLcode errorCode)
  265. {
  266. bool status = (errorCode == CURLE_OK);
  267. Con::printf("Request %d finished with %s", getId(), (status ? "success" : "failure"));
  268. //Get HTTP response code
  269. long responseCode;
  270. curl_easy_getinfo(mCurl, CURLINFO_RESPONSE_CODE, &responseCode);
  271. Con::printf("HTTP Response code: %d", responseCode);
  272. if (status) {
  273. //We're done
  274. processLines();
  275. } else {
  276. Con::errorf("Error info: Code %d: %s", errorCode, curl_easy_strerror(errorCode));
  277. }
  278. //Clean up
  279. if (mBuffer) {
  280. dFree(mBuffer);
  281. }
  282. //Then delete the request
  283. curl_multi_remove_handle(gCurlMulti, mCurl);
  284. --gCurlMultiTotal;
  285. curl_easy_cleanup(mCurl);
  286. //Send a disconnect
  287. onDisconnect();
  288. }
  289. //--------------------------------------
  290. void HTTPObject::init()
  291. {
  292. gCurlMulti = curl_multi_init();
  293. }
  294. void HTTPObject::process()
  295. {
  296. int runningHandles = 0;
  297. CURLMcode code = curl_multi_perform(gCurlMulti, &runningHandles);
  298. if (code != CURLM_OK) {
  299. Con::errorf("curl_multi_perform failed (%d): %s", code, curl_multi_strerror(code));
  300. return;
  301. }
  302. if (runningHandles >= gCurlMultiTotal) {
  303. return;
  304. }
  305. while (true) {
  306. int queueSize = 0;
  307. CURLMsg *msg = curl_multi_info_read(gCurlMulti, &queueSize);
  308. if (!msg) {
  309. break;
  310. }
  311. if (msg->msg != CURLMSG_DONE) {
  312. continue;
  313. }
  314. auto it = gCurlMap.find(msg->easy_handle);
  315. if (it == gCurlMap.end()) {
  316. continue;
  317. }
  318. it->second->finish(msg->data.result);
  319. gCurlMap.erase(it);
  320. }
  321. }
  322. void HTTPObject::shutdown()
  323. {
  324. curl_multi_cleanup(gCurlMulti);
  325. gCurlMulti = nullptr;
  326. }
  327. //--------------------------------------
  328. void HTTPObject::setOption(const std::string &option, const std::string &value)
  329. {
  330. if (option == "verbose") { /* opt = new curlpp::options::Verbose(StringMath::scan<bool>(value)); */ }
  331. else if (option == "user-agent") { curl_easy_setopt(mCurl, CURLOPT_USERAGENT, value.c_str()); }
  332. else if (option == "cookie") { curl_easy_setopt(mCurl, CURLOPT_COOKIE, value.c_str()); }
  333. else if (option == "verify-peer") { curl_easy_setopt(mCurl, CURLOPT_SSL_VERIFYPEER, value == "true"); }
  334. else {
  335. Con::errorf("HTTPObject::setOption unknown option %s", option.c_str());
  336. }
  337. }
  338. void HTTPObject::setDownloadPath(const std::string &path)
  339. {
  340. char expanded[0x100];
  341. Con::expandScriptFilename(expanded, 0x100, path.c_str());
  342. mDownloadPath = std::string(expanded);
  343. }
  344. void HTTPObject::addHeader(const std::string &name, const std::string &value)
  345. {
  346. std::string header = name + ": " + value;
  347. //Formatting: Replace spaces with hyphens
  348. size_t nameLen = name.size();
  349. for (U32 i = 0; i < nameLen; i ++) {
  350. if (header[i] == ' ')
  351. header[i] = '-';
  352. }
  353. mHeaders = curl_slist_append(mHeaders, header.c_str());
  354. }
  355. void HTTPObject::get(const std::string &address, const std::string &uri, const std::string &query)
  356. {
  357. mUrl = address + uri + (query.empty() ? std::string("") : std::string("?") + query);
  358. curl_easy_setopt(mCurl, CURLOPT_URL, mUrl.c_str());
  359. start();
  360. }
  361. void HTTPObject::post(const std::string &address, const std::string &uri, const std::string &query, const std::string &data)
  362. {
  363. mUrl = address + uri + (query.empty() ? std::string("") : std::string("?") + query);
  364. curl_easy_setopt(mCurl, CURLOPT_URL, mUrl.c_str());
  365. mValues = data;
  366. curl_easy_setopt(mCurl, CURLOPT_POST, true);
  367. curl_easy_setopt(mCurl, CURLOPT_POSTFIELDS, mValues.c_str());
  368. start();
  369. }
  370. //--------------------------------------
  371. void HTTPObject::onConnected()
  372. {
  373. Con::executef(this, "onConnected");
  374. }
  375. void HTTPObject::onConnectFailed()
  376. {
  377. Con::executef(this, "onConnectFailed");
  378. }
  379. void HTTPObject::onLine(const std::string& line)
  380. {
  381. Con::executef(this, "onLine", line.c_str());
  382. }
  383. void HTTPObject::onDownload(const std::string& path)
  384. {
  385. Con::executef(this, "onDownload", path.c_str());
  386. }
  387. void HTTPObject::onDownloadFailed(const std::string& path)
  388. {
  389. Con::executef(this, "onDownloadFailed", path.c_str());
  390. }
  391. void HTTPObject::onDisconnect()
  392. {
  393. Con::executef(this, "onDisconncted");
  394. }
  395. //--------------------------------------
  396. DefineEngineMethod(HTTPObject, get, void, (const char* Address, const char* requirstURI, const char* query), (""),
  397. "@brief Send a GET command to a server to send or retrieve data.\n\n"
  398. "@param Address HTTP web address to send this get call to. Be sure to include the port at the end (IE: \"www.garagegames.com:80\").\n"
  399. "@param requirstURI Specific location on the server to access (IE: \"index.php\".)\n"
  400. "@param query Optional. Actual data to transmit to the server. Can be anything required providing it sticks with limitations of the HTTP protocol. "
  401. "If you were building the URL manually, this is the text that follows the question mark. For example: http://www.google.com/ig/api?<b>weather=Las-Vegas,US</b>\n"
  402. "@tsexample\n"
  403. "// Create an HTTP object for communications\n"
  404. "%httpObj = new HTTPObject();\n\n"
  405. "// Specify a URL to transmit to\n"
  406. "%url = \"www.garagegames.com:80\";\n\n"
  407. "// Specify a URI to communicate with\n"
  408. "%URI = \"/index.php\";\n\n"
  409. "// Specify a query to send.\n"
  410. "%query = \"\";\n\n"
  411. "// Send the GET command to the server\n"
  412. "%httpObj.get(%url,%URI,%query);\n"
  413. "@endtsexample\n\n")
  414. {
  415. if(!query || !query[ 0 ])
  416. object->get(Address, requirstURI, "");
  417. else
  418. object->get(Address, requirstURI, query);
  419. }
  420. DefineEngineMethod(HTTPObject, post, void, (const char* Address, const char* requirstURI, const char* query, const char* post),,
  421. "@brief Send POST command to a server to send or retrieve data.\n\n"
  422. "@param Address HTTP web address to send this get call to. Be sure to include the port at the end (IE: \"www.garagegames.com:80\").\n"
  423. "@param requirstURI Specific location on the server to access (IE: \"index.php\".)\n"
  424. "@param query Actual data to transmit to the server. Can be anything required providing it sticks with limitations of the HTTP protocol. \n"
  425. "@param post Submission data to be processed.\n"
  426. "@tsexample\n"
  427. "// Create an HTTP object for communications\n"
  428. "%httpObj = new HTTPObject();\n\n"
  429. "// Specify a URL to transmit to\n"
  430. "%url = \"www.garagegames.com:80\";\n\n"
  431. "// Specify a URI to communicate with\n"
  432. "%URI = \"/index.php\";\n\n"
  433. "// Specify a query to send.\n"
  434. "%query = \"\";\n\n"
  435. "// Specify the submission data.\n"
  436. "%post = \"\";\n\n"
  437. "// Send the POST command to the server\n"
  438. "%httpObj.POST(%url,%URI,%query,%post);\n"
  439. "@endtsexample\n\n")
  440. {
  441. object->post(Address, requirstURI, query, post);
  442. }
  443. DefineEngineMethod(HTTPObject, setOption, void, (const char* option, const char* value),, "HTTPObject.setOption(option, value)")
  444. {
  445. object->setOption(option, value);
  446. }
  447. DefineEngineMethod(HTTPObject, setDownloadPath, void, (const char* path),, "HTTPObject.setDownloadPath(path)")
  448. {
  449. object->setDownloadPath(path);
  450. }
  451. DefineEngineMethod(HTTPObject, addHeader, void, (const char* name, const char* value),, "HTTPObject.addHeader(name, value)")
  452. {
  453. object->addHeader(name, value);
  454. }