123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- //-----------------------------------------------------------------------------
- // Copyright (c) 2012 GarageGames, LLC
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to
- // deal in the Software without restriction, including without limitation the
- // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- // sell copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- // IN THE SOFTWARE.
- //-----------------------------------------------------------------------------
- //-----------------------------------------------------------------------------
- // Copyright (c) 2017 The Platinum Team
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to
- // deal in the Software without restriction, including without limitation the
- // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- // sell copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- // DEALINGS IN THE SOFTWARE.
- //-----------------------------------------------------------------------------
- #include "app/net/httpObject.h"
- #include "platform/platform.h"
- #include "core/stream/fileStream.h"
- #include "console/simBase.h"
- #include "console/consoleInternal.h"
- #include "console/engineAPI.h"
- #include <string.h>
- IMPLEMENT_CONOBJECT(HTTPObject);
- ConsoleDocClass(HTTPObject,
- "@brief Allows communications between the game and a server using HTTP.\n\n"
-
- "HTTPObject is derrived from TCPObject and makes use of the same callbacks for dealing with "
- "connections and received data. However, the way in which you use HTTPObject to connect "
- "with a server is different than TCPObject. Rather than opening a connection, sending data, "
- "waiting to receive data, and then closing the connection, you issue a get() or post() and "
- "handle the response. The connection is automatically created and destroyed for you.\n\n"
-
- "@tsexample\n"
- "// In this example we'll retrieve the weather in Las Vegas using\n"
- "// Google's API. The response is in XML which could be processed\n"
- "// and used by the game using SimXMLDocument, but we'll just output\n"
- "// the results to the console in this example.\n\n"
- "// Define callbacks for our specific HTTPObject using our instance's\n"
- "// name (WeatherFeed) as the namespace.\n\n"
- "// Handle an issue with resolving the server's name\n"
- "function WeatherFeed::onDNSFailed(%this)\n"
- "{\n"
- " // Store this state\n"
- " %this.lastState = \"DNSFailed\";\n\n"
- " // Handle DNS failure\n"
- "}\n\n"
- "function WeatherFeed::onConnectFailed(%this)\n"
- "{\n"
- " // Store this state\n"
- " %this.lastState = \"ConnectFailed\";\n\n"
- " // Handle connection failure\n"
- "}\n\n"
- "function WeatherFeed::onDNSResolved(%this)\n"
- "{\n"
- " // Store this state\n"
- " %this.lastState = \"DNSResolved\";\n\n"
- "}\n\n"
- "function WeatherFeed::onConnected(%this)\n"
- "{\n"
- " // Store this state\n"
- " %this.lastState = \"Connected\";\n\n"
- " // Clear our buffer\n"
- " %this.buffer = \"\";\n"
- "}\n\n"
- "function WeatherFeed::onDisconnect(%this)\n"
- "{\n"
- " // Store this state\n"
- " %this.lastState = \"Disconnected\";\n\n"
- " // Output the buffer to the console\n"
- " echo(\"Google Weather Results:\");\n"
- " echo(%this.buffer);\n"
- "}\n\n"
- "// Handle a line from the server\n"
- "function WeatherFeed::onLine(%this, %line)\n"
- "{\n"
- " // Store this line in out buffer\n"
- " %this.buffer = %this.buffer @ %line;\n"
- "}\n\n"
- "// Create the HTTPObject\n"
- "%feed = new HTTPObject(WeatherFeed);\n\n"
- "// Define a dynamic field to store the last connection state\n"
- "%feed.lastState = \"None\";\n\n"
- "// Send the GET command\n"
- "%feed.get(\"www.google.com:80\", \"/ig/api\", \"weather=Las-Vegas,US\");\n"
- "@endtsexample\n\n"
-
- "@see TCPObject\n"
- "@ingroup Networking\n"
- );
- CURLM *HTTPObject::gCurlMulti = nullptr;
- int HTTPObject::gCurlMultiTotal = 0;
- std::unordered_map<CURL *, HTTPObject *> HTTPObject::gCurlMap;
- size_t HTTPObject::writeCallback(char *buffer, size_t size, size_t nitems, HTTPObject *object) {
- return object->processData(buffer, size, nitems);
- }
- size_t HTTPObject::headerCallback(char *buffer, size_t size, size_t nitems, HTTPObject *object) {
- return object->processHeader(buffer, size, nitems);
- }
- //--------------------------------------
- HTTPObject::HTTPObject()
- : mCurl(nullptr),
- mBuffer(nullptr),
- mBufferSize(0),
- mBufferUsed(0),
- mDownload(false),
- mHeaders(nullptr)
- {
- CURL *request = curl_easy_init();
- curl_easy_setopt(request, CURLOPT_VERBOSE, false);
- curl_easy_setopt(request, CURLOPT_FOLLOWLOCATION, true);
- curl_easy_setopt(request, CURLOPT_TRANSFERTEXT, true);
- curl_easy_setopt(request, CURLOPT_USERAGENT, "Torque 1.0");
- curl_easy_setopt(request, CURLOPT_ENCODING, "ISO 8859-1");
- mCurl = request;
- gCurlMap[request] = this;
- curl_easy_setopt(request, CURLOPT_WRITEDATA, this);
- curl_easy_setopt(request, CURLOPT_WRITEFUNCTION, writeCallback);
- curl_easy_setopt(request, CURLOPT_HEADERDATA, this);
- curl_easy_setopt(request, CURLOPT_HEADERFUNCTION, headerCallback);
- }
- HTTPObject::~HTTPObject()
- {
- }
- //--------------------------------------
- bool HTTPObject::ensureBuffer(U32 length)
- {
- if (mBufferSize < length) {
- //CURL_MAX_WRITE_SIZE is the maximum packet size we'll be given. So round
- // off to that and we should not have to allocate too often.
- length = ((length / CURL_MAX_WRITE_SIZE) + 1) * CURL_MAX_WRITE_SIZE;
- void *alloced = dRealloc(mBuffer, length * sizeof(char));
- //Out of memory
- if (!alloced) {
- return false;
- }
- mBuffer = (U8 *)alloced;
- mBufferSize = length;
- }
- return true;
- }
- size_t HTTPObject::processData(char *buffer, size_t size, size_t nitems)
- {
- size_t writeSize = size * nitems + 1;
- if (!ensureBuffer(mBufferUsed + writeSize)) {
- //Error
- return 0;
- }
- memcpy(mBuffer + mBufferUsed, buffer, size * nitems);
- mBufferUsed += size * nitems;
- mBuffer[mBufferUsed] = 0;
- return size * nitems;
- }
- size_t HTTPObject::processHeader(char *buffer, size_t size, size_t nitems)
- {
- char *colon = strchr(buffer, ':');
- if (colon != NULL) {
- std::string key(buffer, colon - buffer);
- std::string value(colon + 2);
- if (value[value.length() - 1] == '\n')
- value.erase(value.length() - 1, 1);
- if (value[value.length() - 1] == '\r')
- value.erase(value.length() - 1, 1);
- mRecieveHeaders[key] = value;
- }
- return size * nitems;
- }
- void HTTPObject::start()
- {
- CURLMcode result = curl_multi_add_handle(gCurlMulti, mCurl);
- if (result != CURLM_OK) {
- Con::errorf("curl_easy_perform failed (%d): %s", result, curl_multi_strerror(result));
- return;
- }
- ++gCurlMultiTotal;
- }
- void HTTPObject::processLines()
- {
- if (mDownload) {
- const std::string &dlPath = mDownloadPath;
- int lastSlash = dlPath.find_last_of('/');
- const char *path;
- const char *file;
- if (lastSlash == std::string::npos) {
- //No
- return;
- } else {
- path = StringTable->insert(dlPath.c_str(), false);
- file = StringTable->insert(dlPath.substr(lastSlash + 1).c_str(), false);
- }
- //Don't download unless we get an OK
- long responseCode;
- curl_easy_getinfo(mCurl, CURLINFO_RESPONSE_CODE, &responseCode);
- if (responseCode != 200) {
- onDownloadFailed(path);
- return;
- }
- //Write to the output file
- FileStream *stream = new FileStream();
- if (!stream->open(path, Torque::FS::File::Read)) {
- Con::errorf("Could not download %s: error opening stream.");
- onDownloadFailed(path);
- return;
- }
- stream->write(mBufferUsed, mBuffer);
- stream->close();
- onDownload(path);
- delete stream;
- } else {
- //Pull all the lines out of mBuffer
- char *str = (char *)mBuffer;
- char *nextLine = str;
- while (str && nextLine) {
- nextLine = strchr(str, '\n');
- //Get how long the current line for allocating
- U32 lineSize = 0;
- if (nextLine == NULL) {
- lineSize = strlen(str);
- if (lineSize == 0) {
- break;
- }
- } else {
- lineSize = nextLine - str;
- }
- //Copy into a return buffer for the script
- char *line = Con::getReturnBuffer(lineSize + 1);
- memcpy(line, str, lineSize);
- line[lineSize] = 0;
- //Strip the \r from \r\n
- if (lineSize > 0 && line[lineSize - 1] == '\r') {
- line[lineSize - 1] = 0;
- }
- onLine(line);
- if (nextLine) {
- //Strip the \n
- str = nextLine + 1;
- }
- }
- }
- }
- void HTTPObject::finish(CURLcode errorCode)
- {
- bool status = (errorCode == CURLE_OK);
- Con::printf("Request %d finished with %s", getId(), (status ? "success" : "failure"));
- //Get HTTP response code
- long responseCode;
- curl_easy_getinfo(mCurl, CURLINFO_RESPONSE_CODE, &responseCode);
- Con::printf("HTTP Response code: %d", responseCode);
- if (status) {
- //We're done
- processLines();
- } else {
- Con::errorf("Error info: Code %d: %s", errorCode, curl_easy_strerror(errorCode));
- }
- //Clean up
- if (mBuffer) {
- dFree(mBuffer);
- }
- //Then delete the request
- curl_multi_remove_handle(gCurlMulti, mCurl);
- --gCurlMultiTotal;
- curl_easy_cleanup(mCurl);
- //Send a disconnect
- onDisconnect();
- }
- //--------------------------------------
- void HTTPObject::init()
- {
- gCurlMulti = curl_multi_init();
- }
- void HTTPObject::process()
- {
- int runningHandles = 0;
- CURLMcode code = curl_multi_perform(gCurlMulti, &runningHandles);
- if (code != CURLM_OK) {
- Con::errorf("curl_multi_perform failed (%d): %s", code, curl_multi_strerror(code));
- return;
- }
- if (runningHandles >= gCurlMultiTotal) {
- return;
- }
- while (true) {
- int queueSize = 0;
- CURLMsg *msg = curl_multi_info_read(gCurlMulti, &queueSize);
- if (!msg) {
- break;
- }
- if (msg->msg != CURLMSG_DONE) {
- continue;
- }
- auto it = gCurlMap.find(msg->easy_handle);
- if (it == gCurlMap.end()) {
- continue;
- }
- it->second->finish(msg->data.result);
- gCurlMap.erase(it);
- }
- }
- void HTTPObject::shutdown()
- {
- curl_multi_cleanup(gCurlMulti);
- gCurlMulti = nullptr;
- }
- //--------------------------------------
- void HTTPObject::setOption(const std::string &option, const std::string &value)
- {
- if (option == "verbose") { /* opt = new curlpp::options::Verbose(StringMath::scan<bool>(value)); */ }
- else if (option == "user-agent") { curl_easy_setopt(mCurl, CURLOPT_USERAGENT, value.c_str()); }
- else if (option == "cookie") { curl_easy_setopt(mCurl, CURLOPT_COOKIE, value.c_str()); }
- else if (option == "verify-peer") { curl_easy_setopt(mCurl, CURLOPT_SSL_VERIFYPEER, value == "true"); }
- else {
- Con::errorf("HTTPObject::setOption unknown option %s", option.c_str());
- }
- }
- void HTTPObject::setDownloadPath(const std::string &path)
- {
- char expanded[0x100];
- Con::expandScriptFilename(expanded, 0x100, path.c_str());
- mDownloadPath = std::string(expanded);
- }
- void HTTPObject::addHeader(const std::string &name, const std::string &value)
- {
- std::string header = name + ": " + value;
- //Formatting: Replace spaces with hyphens
- size_t nameLen = name.size();
- for (U32 i = 0; i < nameLen; i ++) {
- if (header[i] == ' ')
- header[i] = '-';
- }
- mHeaders = curl_slist_append(mHeaders, header.c_str());
- }
- void HTTPObject::get(const std::string &address, const std::string &uri, const std::string &query)
- {
- mUrl = address + uri + (query.empty() ? std::string("") : std::string("?") + query);
- curl_easy_setopt(mCurl, CURLOPT_URL, mUrl.c_str());
- start();
- }
- void HTTPObject::post(const std::string &address, const std::string &uri, const std::string &query, const std::string &data)
- {
- mUrl = address + uri + (query.empty() ? std::string("") : std::string("?") + query);
- curl_easy_setopt(mCurl, CURLOPT_URL, mUrl.c_str());
- mValues = data;
- curl_easy_setopt(mCurl, CURLOPT_POST, true);
- curl_easy_setopt(mCurl, CURLOPT_POSTFIELDS, mValues.c_str());
- start();
- }
- //--------------------------------------
- void HTTPObject::onConnected()
- {
- Con::executef(this, "onConnected");
- }
- void HTTPObject::onConnectFailed()
- {
- Con::executef(this, "onConnectFailed");
- }
- void HTTPObject::onLine(const std::string& line)
- {
- Con::executef(this, "onLine", line.c_str());
- }
- void HTTPObject::onDownload(const std::string& path)
- {
- Con::executef(this, "onDownload", path.c_str());
- }
- void HTTPObject::onDownloadFailed(const std::string& path)
- {
- Con::executef(this, "onDownloadFailed", path.c_str());
- }
- void HTTPObject::onDisconnect()
- {
- Con::executef(this, "onDisconncted");
- }
- //--------------------------------------
- DefineEngineMethod(HTTPObject, get, void, (const char* Address, const char* requirstURI, const char* query), (""),
- "@brief Send a GET command to a server to send or retrieve data.\n\n"
- "@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"
- "@param requirstURI Specific location on the server to access (IE: \"index.php\".)\n"
- "@param query Optional. Actual data to transmit to the server. Can be anything required providing it sticks with limitations of the HTTP protocol. "
- "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"
-
- "@tsexample\n"
- "// Create an HTTP object for communications\n"
- "%httpObj = new HTTPObject();\n\n"
- "// Specify a URL to transmit to\n"
- "%url = \"www.garagegames.com:80\";\n\n"
- "// Specify a URI to communicate with\n"
- "%URI = \"/index.php\";\n\n"
- "// Specify a query to send.\n"
- "%query = \"\";\n\n"
- "// Send the GET command to the server\n"
- "%httpObj.get(%url,%URI,%query);\n"
- "@endtsexample\n\n")
- {
- if(!query || !query[ 0 ])
- object->get(Address, requirstURI, "");
- else
- object->get(Address, requirstURI, query);
- }
- DefineEngineMethod(HTTPObject, post, void, (const char* Address, const char* requirstURI, const char* query, const char* post),,
- "@brief Send POST command to a server to send or retrieve data.\n\n"
- "@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"
- "@param requirstURI Specific location on the server to access (IE: \"index.php\".)\n"
- "@param query Actual data to transmit to the server. Can be anything required providing it sticks with limitations of the HTTP protocol. \n"
- "@param post Submission data to be processed.\n"
- "@tsexample\n"
- "// Create an HTTP object for communications\n"
- "%httpObj = new HTTPObject();\n\n"
- "// Specify a URL to transmit to\n"
- "%url = \"www.garagegames.com:80\";\n\n"
- "// Specify a URI to communicate with\n"
- "%URI = \"/index.php\";\n\n"
- "// Specify a query to send.\n"
- "%query = \"\";\n\n"
- "// Specify the submission data.\n"
- "%post = \"\";\n\n"
- "// Send the POST command to the server\n"
- "%httpObj.POST(%url,%URI,%query,%post);\n"
- "@endtsexample\n\n")
- {
- object->post(Address, requirstURI, query, post);
- }
- DefineEngineMethod(HTTPObject, setOption, void, (const char* option, const char* value),, "HTTPObject.setOption(option, value)")
- {
- object->setOption(option, value);
- }
- DefineEngineMethod(HTTPObject, setDownloadPath, void, (const char* path),, "HTTPObject.setDownloadPath(path)")
- {
- object->setDownloadPath(path);
- }
- DefineEngineMethod(HTTPObject, addHeader, void, (const char* name, const char* value),, "HTTPObject.addHeader(name, value)")
- {
- object->addHeader(name, value);
- }
|