//----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- #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" 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" ); //-------------------------------------- HTTPObject::HTTPObject() { mHostName = 0; mPath = 0; mQuery = 0; mPost = 0; mBufferSave = 0; } HTTPObject::~HTTPObject() { dFree(mHostName); dFree(mPath); dFree(mQuery); dFree(mPost); mHostName = 0; mPath = 0; mQuery = 0; mPost = 0; dFree(mBufferSave); } //-------------------------------------- //-------------------------------------- void HTTPObject::get(const char *host, const char *path, const char *query) { if(mHostName) dFree(mHostName); if(mPath) dFree(mPath); if(mQuery) dFree(mQuery); if(mPost) dFree(mPost); if(mBufferSave) dFree(mBufferSave); mBufferSave = 0; mHostName = dStrdup(host); mPath = dStrdup(path); if(query) mQuery = dStrdup(query); else mQuery = NULL; mPost = NULL; connect(host); } void HTTPObject::post(const char *host, const char *path, const char *query, const char *post) { if(mHostName) dFree(mHostName); if(mPath) dFree(mPath); if(mQuery) dFree(mQuery); if(mPost) dFree(mPost); if(mBufferSave) dFree(mBufferSave); mBufferSave = 0; mHostName = dStrdup(host); mPath = dStrdup(path); if(query && query[0]) mQuery = dStrdup(query); else mQuery = NULL; mPost = dStrdup(post); connect(host); } static char getHex(char c) { if(c <= 9) return c + '0'; return c - 10 + 'A'; } static S32 getHexVal(char c) { if(c >= '0' && c <= '9') return c - '0'; else if(c >= 'A' && c <= 'Z') return c - 'A' + 10; else if(c >= 'a' && c <= 'z') return c - 'a' + 10; return -1; } void HTTPObject::expandPath(char *dest, const char *path, U32 destSize) { static bool asciiEscapeTableBuilt = false; static bool asciiEscapeTable[256]; if(!asciiEscapeTableBuilt) { asciiEscapeTableBuilt = true; U32 i; for(i = 0; i <= ' '; i++) asciiEscapeTable[i] = true; for(;i <= 0x7F; i++) asciiEscapeTable[i] = false; for(;i <= 0xFF; i++) asciiEscapeTable[i] = true; asciiEscapeTable[static_cast('\"')] = true; asciiEscapeTable[static_cast('_')] = true; asciiEscapeTable[static_cast('\'')] = true; asciiEscapeTable[static_cast('#')] = true; asciiEscapeTable[static_cast('$')] = true; asciiEscapeTable[static_cast('%')] = true; asciiEscapeTable[static_cast('&')] = false; asciiEscapeTable[static_cast('+')] = true; asciiEscapeTable[static_cast('-')] = true; asciiEscapeTable[static_cast('~')] = true; } U32 destIndex = 0; U32 srcIndex = 0; while(path[srcIndex] && destIndex < destSize - 3) { char c = path[srcIndex++]; if(asciiEscapeTable[static_cast(c)]) { dest[destIndex++] = '%'; dest[destIndex++] = getHex((c >> 4) & 0xF); dest[destIndex++] = getHex(c & 0xF); } else dest[destIndex++] = c; } dest[destIndex] = 0; } //-------------------------------------- void HTTPObject::onConnected() { Parent::onConnected(); char expPath[8192]; char buffer[8192]; if(mQuery) { dSprintf(buffer, sizeof(buffer), "%s?%s", mPath, mQuery); expandPath(expPath, buffer, sizeof(expPath)); } else expandPath(expPath, mPath, sizeof(expPath)); char *pt = dStrchr(mHostName, ':'); if(pt) *pt = 0; //If we want to do a get request if(mPost == NULL) { dSprintf(buffer, sizeof(buffer), "GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", expPath, mHostName); } //Else we want to do a post request else { dSprintf(buffer, sizeof(buffer), "POST %s HTTP/1.1\r\nHost: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: %i\r\n\r\n%s\r\n\r\n", expPath, mHostName, dStrlen(mPost), mPost); } if(pt) *pt = ':'; send((U8*)buffer, dStrlen(buffer)); mParseState = ParsingStatusLine; mChunkedEncoding = false; } void HTTPObject::onConnectFailed() { dFree(mHostName); dFree(mPath); dFree(mQuery); mHostName = 0; mPath = 0; mQuery = 0; Parent::onConnectFailed(); } void HTTPObject::onDisconnect() { dFree(mHostName); dFree(mPath); dFree(mQuery); mHostName = 0; mPath = 0; mQuery = 0; Parent::onDisconnect(); } bool HTTPObject::processLine(UTF8 *line) { if(mParseState == ParsingStatusLine) { mParseState = ParsingHeader; } else if(mParseState == ParsingHeader) { if(!dStricmp((char *) line, "transfer-encoding: chunked")) mChunkedEncoding = true; if(line[0] == 0) { if(mChunkedEncoding) mParseState = ParsingChunkHeader; else mParseState = ProcessingBody; return true; } } else if(mParseState == ParsingChunkHeader) { if(line[0]) // strip off the crlf if necessary { mChunkSize = 0; S32 hexVal; while((hexVal = getHexVal(*line++)) != -1) { mChunkSize *= 16; mChunkSize += hexVal; } if(mBufferSave) { mBuffer = mBufferSave; mBufferSize = mBufferSaveSize; mBufferSave = 0; } if(mChunkSize) mParseState = ProcessingBody; else { mParseState = ProcessingDone; finishLastLine(); } } } else { return Parent::processLine((UTF8*)line); } return true; } U32 HTTPObject::onDataReceive(U8 *buffer, U32 bufferLen) { U32 start = 0; parseLine(buffer, &start, bufferLen); return start; } //-------------------------------------- U32 HTTPObject::onReceive(U8 *buffer, U32 bufferLen) { if(mParseState == ProcessingBody) { if(mChunkedEncoding && bufferLen >= mChunkSize) { U32 ret = onDataReceive(buffer, mChunkSize); mChunkSize -= ret; if(mChunkSize == 0) { if(mBuffer) { mBufferSaveSize = mBufferSize; mBufferSave = mBuffer; mBuffer = 0; mBufferSize = 0; } mParseState = ParsingChunkHeader; } return ret; } else { U32 ret = onDataReceive(buffer, bufferLen); mChunkSize -= ret; return ret; } } else if(mParseState != ProcessingDone) { U32 start = 0; parseLine(buffer, &start, bufferLen); return start; } return bufferLen; } //-------------------------------------- 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?weather=Las-Vegas,US\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, NULL); 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); }