tcpObject.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  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. #include "app/net/tcpObject.h"
  23. #include "platform/platform.h"
  24. #include "console/simBase.h"
  25. #include "console/consoleInternal.h"
  26. #include "core/strings/stringUnit.h"
  27. #include "console/engineAPI.h"
  28. #include "core/stream/fileStream.h"
  29. TCPObject *TCPObject::table[TCPObject::TableSize] = {0, };
  30. IMPLEMENT_CONOBJECT(TCPObject);
  31. ConsoleDocClass( TCPObject,
  32. "@brief Allows communications between the game and a server using TCP/IP protocols.\n\n"
  33. "To use TCPObject you set up a connection to a server, send data to the server, and handle "
  34. "each line of the server's response using a callback. Once you are done communicating with "
  35. "the server, you disconnect.\n\n"
  36. "TCPObject is intended to be used with text based protocols which means you'll need to "
  37. "delineate the server's response with an end-of-line character. i.e. the newline "
  38. "character @\\n. You may optionally include the carriage return character @\\r prior to the newline "
  39. "and TCPObject will strip it out before sending the line to the callback. If a newline "
  40. "character is not included in the server's output, the received data will not be "
  41. "processed until you disconnect from the server (which flushes the internal buffer).\n\n"
  42. "TCPObject may also be set up to listen to a specific port, making Torque into a TCP server. "
  43. "When used in this manner, a callback is received when a client connection is made. Following "
  44. "the outside connection, text may be sent and lines are processed in the usual manner.\n\n"
  45. "If you want to work with HTTP you may wish to use HTTPObject instead as it handles all of the "
  46. "HTTP header setup and parsing.\n\n"
  47. "@tsexample\n"
  48. "// In this example we'll retrieve the new forum threads RSS\n"
  49. "// feed from garagegames.com. As we're using TCPObject, the\n"
  50. "// raw text response will be received from the server, including\n"
  51. "// the HTTP header.\n\n"
  52. "// Define callbacks for our specific TCPObject using our instance's\n"
  53. "// name (RSSFeed) as the namespace.\n\n"
  54. "// Handle an issue with resolving the server's name\n"
  55. "function RSSFeed::onDNSFailed(%this)\n"
  56. "{\n"
  57. " // Store this state\n"
  58. " %this.lastState = \"DNSFailed\";\n\n"
  59. " // Handle DNS failure\n"
  60. "}\n\n"
  61. "function RSSFeed::onConnectFailed(%this)\n"
  62. "{\n"
  63. " // Store this state\n"
  64. " %this.lastState = \"ConnectFailed\";\n\n"
  65. " // Handle connection failure\n"
  66. "}\n\n"
  67. "function RSSFeed::onDNSResolved(%this)\n"
  68. "{\n"
  69. " // Store this state\n"
  70. " %this.lastState = \"DNSResolved\";\n\n"
  71. "}\n\n"
  72. "function RSSFeed::onConnected(%this)\n"
  73. "{\n"
  74. " // Store this state\n"
  75. " %this.lastState = \"Connected\";\n\n"
  76. "}\n\n"
  77. "function RSSFeed::onDisconnect(%this)\n"
  78. "{\n"
  79. " // Store this state\n"
  80. " %this.lastState = \"Disconnected\";\n\n"
  81. "}\n\n"
  82. "// Handle a line from the server\n"
  83. "function RSSFeed::onLine(%this, %line)\n"
  84. "{\n"
  85. " // Print the line to the console\n"
  86. " echo( %line );\n"
  87. "}\n\n"
  88. "// Create the TCPObject\n"
  89. "%rss = new TCPObject(RSSFeed);\n\n"
  90. "// Define a dynamic field to store the last connection state\n"
  91. "%rss.lastState = \"None\";\n\n"
  92. "// Connect to the server\n"
  93. "%rss.connect(\"www.garagegames.com:80\");\n\n"
  94. "// Send the RSS feed request to the server. Response will be\n"
  95. "// handled in onLine() callback above\n"
  96. "%rss.send(\"GET /feeds/rss/threads HTTP/1.1\\r\\nHost: www.garagegames.com\\r\\n\\r\\n\");\n"
  97. "@endtsexample\n\n"
  98. "@see HTTPObject\n"
  99. "@ingroup Networking\n"
  100. );
  101. IMPLEMENT_CALLBACK(TCPObject, onConnectionRequest, void, (const char* address, const char* ID), (address, ID),
  102. "@brief Called whenever a connection request is made.\n\n"
  103. "This callback is used when the TCPObject is listening to a port and a client is attempting to connect.\n"
  104. "@param address Server address connecting from.\n"
  105. "@param ID Connection ID\n"
  106. "@see listen()\n"
  107. );
  108. IMPLEMENT_CALLBACK(TCPObject, onLine, void, (const char* line), (line),
  109. "@brief Called whenever a line of data is sent to this TCPObject.\n\n"
  110. "This callback is called when the received data contains a newline @\\n character, or "
  111. "the connection has been disconnected and the TCPObject's buffer is flushed.\n"
  112. "@param line Data sent from the server.\n"
  113. );
  114. IMPLEMENT_CALLBACK(TCPObject, onPacket, bool, (const char* data), (data),
  115. "@brief Called when we get a packet with no newlines or nulls (probably websocket).\n\n"
  116. "@param data Data sent from the server.\n"
  117. "@return true if script handled the packet.\n"
  118. );
  119. IMPLEMENT_CALLBACK(TCPObject, onEndReceive, void, (), (),
  120. "@brief Called when we are done reading all lines.\n\n"
  121. );
  122. IMPLEMENT_CALLBACK(TCPObject, onDNSResolved, void, (),(),
  123. "Called whenever the DNS has been resolved.\n"
  124. );
  125. IMPLEMENT_CALLBACK(TCPObject, onDNSFailed, void, (),(),
  126. "Called whenever the DNS has failed to resolve.\n"
  127. );
  128. IMPLEMENT_CALLBACK(TCPObject, onConnected, void, (),(),
  129. "Called whenever a connection is established with a server.\n"
  130. );
  131. IMPLEMENT_CALLBACK(TCPObject, onConnectFailed, void, (),(),
  132. "Called whenever a connection has failed to be established with a server.\n"
  133. );
  134. IMPLEMENT_CALLBACK(TCPObject, onDisconnect, void, (),(),
  135. "Called whenever the TCPObject disconnects from whatever it is currently connected to.\n"
  136. );
  137. TCPObject *TCPObject::find(NetSocket tag)
  138. {
  139. for(TCPObject *walk = table[tag.getHash() & TableMask]; walk; walk = walk->mNext)
  140. if(walk->mTag.getHash() == tag.getHash())
  141. return walk;
  142. return NULL;
  143. }
  144. void TCPObject::addToTable(NetSocket newTag)
  145. {
  146. removeFromTable();
  147. mTag = newTag;
  148. mNext = table[mTag.getHash() & TableMask];
  149. table[mTag.getHash() & TableMask] = this;
  150. }
  151. void TCPObject::removeFromTable()
  152. {
  153. for(TCPObject **walk = &table[mTag.getHash() & TableMask]; *walk; walk = &((*walk)->mNext))
  154. {
  155. if(*walk == this)
  156. {
  157. *walk = mNext;
  158. return;
  159. }
  160. }
  161. }
  162. void processConnectedReceiveEvent(NetSocket sock, RawData incomingData);
  163. void processConnectedAcceptEvent(NetSocket listeningPort, NetSocket newConnection, NetAddress originatingAddress);
  164. void processConnectedNotifyEvent( NetSocket sock, U32 state );
  165. S32 gTCPCount = 0;
  166. TCPObject::TCPObject()
  167. {
  168. mBuffer = NULL;
  169. mBufferSize = 0;
  170. mPort = 0;
  171. mTag = NetSocket::INVALID;
  172. mNext = NULL;
  173. mState = Disconnected;
  174. gTCPCount++;
  175. if(gTCPCount == 1)
  176. {
  177. Net::getConnectionAcceptedEvent().notify(processConnectedAcceptEvent);
  178. Net::getConnectionReceiveEvent().notify(processConnectedReceiveEvent);
  179. Net::getConnectionNotifyEvent().notify(processConnectedNotifyEvent);
  180. }
  181. }
  182. TCPObject::~TCPObject()
  183. {
  184. disconnect();
  185. dFree(mBuffer);
  186. gTCPCount--;
  187. if(gTCPCount == 0)
  188. {
  189. Net::getConnectionAcceptedEvent().remove(processConnectedAcceptEvent);
  190. Net::getConnectionReceiveEvent().remove(processConnectedReceiveEvent);
  191. Net::getConnectionNotifyEvent().remove(processConnectedNotifyEvent);
  192. }
  193. }
  194. bool TCPObject::processArguments(S32 argc, ConsoleValue *argv)
  195. {
  196. if(argc == 0)
  197. return true;
  198. else if(argc == 1)
  199. {
  200. addToTable(NetSocket::fromHandle(argv[0].getInt()));
  201. return true;
  202. }
  203. return false;
  204. }
  205. bool TCPObject::onAdd()
  206. {
  207. if(!Parent::onAdd())
  208. return false;
  209. const char *name = getName();
  210. if(name && name[0] && getClassRep())
  211. {
  212. Namespace *parent = getClassRep()->getNameSpace();
  213. Con::linkNamespaces(parent->mName, name);
  214. mNameSpace = Con::lookupNamespace(name);
  215. }
  216. Sim::getTCPGroup()->addObject(this);
  217. return true;
  218. }
  219. U32 TCPObject::onReceive(U8 *buffer, U32 bufferLen)
  220. {
  221. // we got a raw buffer event
  222. // default action is to split the buffer into lines of text
  223. // and call processLine on each
  224. // for any incomplete lines we have mBuffer
  225. U32 start = 0;
  226. parseLine(buffer, &start, bufferLen);
  227. return start;
  228. }
  229. void TCPObject::parseLine(U8 *buffer, U32 *start, U32 bufferLen)
  230. {
  231. // find the first \n in buffer
  232. U32 i;
  233. U8 *line = buffer + *start;
  234. for(i = *start; i < bufferLen; i++)
  235. if(buffer[i] == '\n' || buffer[i] == 0)
  236. break;
  237. U32 len = i - *start;
  238. if(i == bufferLen || mBuffer)
  239. {
  240. // we've hit the end with no newline
  241. mBuffer = (U8 *) dRealloc(mBuffer, mBufferSize + len + 1);
  242. dMemcpy(mBuffer + mBufferSize, line, len);
  243. mBufferSize += len;
  244. *start = i;
  245. // process the line
  246. if(i != bufferLen)
  247. {
  248. mBuffer[mBufferSize] = 0;
  249. if(mBufferSize && mBuffer[mBufferSize-1] == '\r')
  250. mBuffer[mBufferSize - 1] = 0;
  251. U8 *temp = mBuffer;
  252. mBuffer = 0;
  253. mBufferSize = 0;
  254. processLine((UTF8*)temp);
  255. dFree(temp);
  256. }
  257. }
  258. else if(i != bufferLen)
  259. {
  260. line[len] = 0;
  261. if(len && line[len-1] == '\r')
  262. line[len-1] = 0;
  263. processLine((UTF8*)line);
  264. }
  265. if(i != bufferLen)
  266. *start = i + 1;
  267. }
  268. void TCPObject::onConnectionRequest(const NetAddress *addr, U32 connectId)
  269. {
  270. char idBuf[16];
  271. char addrBuf[256];
  272. Net::addressToString(addr, addrBuf);
  273. dSprintf(idBuf, sizeof(idBuf), "%d", connectId);
  274. onConnectionRequest_callback(addrBuf,idBuf);
  275. }
  276. bool TCPObject::processLine(UTF8 *line)
  277. {
  278. onLine_callback(line);
  279. return true;
  280. }
  281. void TCPObject::onDNSResolved()
  282. {
  283. mState = DNSResolved;
  284. onDNSResolved_callback();
  285. }
  286. void TCPObject::onDNSFailed()
  287. {
  288. mState = Disconnected;
  289. onDNSFailed_callback();
  290. }
  291. void TCPObject::onConnected()
  292. {
  293. mState = Connected;
  294. onConnected_callback();
  295. }
  296. void TCPObject::onConnectFailed()
  297. {
  298. mState = Disconnected;
  299. onConnectFailed_callback();
  300. }
  301. bool TCPObject::finishLastLine()
  302. {
  303. if(mBufferSize)
  304. {
  305. mBuffer[mBufferSize] = 0;
  306. processLine((UTF8*)mBuffer);
  307. dFree(mBuffer);
  308. mBuffer = 0;
  309. mBufferSize = 0;
  310. return true;
  311. }
  312. return false;
  313. }
  314. bool TCPObject::isBufferEmpty()
  315. {
  316. return (mBufferSize <= 0);
  317. }
  318. void TCPObject::emptyBuffer()
  319. {
  320. if(mBufferSize)
  321. {
  322. dFree(mBuffer);
  323. mBuffer = 0;
  324. mBufferSize = 0;
  325. }
  326. }
  327. void TCPObject::onDisconnect()
  328. {
  329. finishLastLine();
  330. mState = Disconnected;
  331. onDisconnect_callback();
  332. }
  333. void TCPObject::listen(U16 port)
  334. {
  335. mState = Listening;
  336. NetSocket newTag = Net::openListenPort(port);
  337. addToTable(newTag);
  338. }
  339. void TCPObject::connect(const char *address)
  340. {
  341. NetSocket newTag = Net::openConnectTo(address);
  342. addToTable(newTag);
  343. }
  344. void TCPObject::disconnect()
  345. {
  346. if( mTag != NetSocket::INVALID ) {
  347. Net::closeConnectTo(mTag);
  348. }
  349. removeFromTable();
  350. }
  351. void TCPObject::send(const U8 *buffer, U32 len)
  352. {
  353. Net::sendtoSocket(mTag, buffer, S32(len));
  354. }
  355. bool TCPObject::sendFile(const char* fileName)
  356. {
  357. //Open the file for reading
  358. FileStream readFile;
  359. if(!readFile.open(fileName, Torque::FS::File::Read))
  360. {
  361. return false;
  362. }
  363. //Read each byte into our buffer
  364. Vector<U8> buffer(readFile.getStreamSize());
  365. readFile.read(buffer.size(), &buffer);
  366. //Send the buffer
  367. send(buffer.address(), buffer.size());
  368. return true;
  369. }
  370. DefineEngineMethod(TCPObject, send, void, (const char *data),,
  371. "@brief Transmits the data string to the connected computer.\n\n"
  372. "This method is used to send text data to the connected computer regardless if we initiated the "
  373. "connection using connect(), or listening to a port using listen().\n"
  374. "@param data The data string to send.\n"
  375. "@tsexample\n"
  376. "// Set the command data\n"
  377. "%data = \"GET \" @ $RSSFeed::serverURL @ \" HTTP/1.0\\r\\n\";\n"
  378. "%data = %data @ \"Host: \" @ $RSSFeed::serverName @ \"\\r\\n\";\n"
  379. "%data = %data @ \"User-Agent: \" @ $RSSFeed::userAgent @ \"\\r\\n\\r\\n\"\n\n"
  380. "// Send the command to the connected server.\n"
  381. "%thisTCPObj.send(%data);\n"
  382. "@endtsexample\n")
  383. {
  384. object->send( (const U8*)data, dStrlen(data) );
  385. }
  386. DefineEngineMethod(TCPObject, sendFile, bool, (const char *fileName),,
  387. "@brief Transmits the file in binary to the connected computer.\n\n"
  388. "@param fileName The filename of the file to transfer.\n")
  389. {
  390. return object->sendFile(fileName);
  391. }
  392. DefineEngineMethod(TCPObject, finishLastLine, void, (),,
  393. "@brief Eat the rest of the lines.\n")
  394. {
  395. object->finishLastLine();
  396. }
  397. DefineEngineMethod(TCPObject, listen, void, (U32 port),,
  398. "@brief Start listening on the specified port for connections.\n\n"
  399. "This method starts a listener which looks for incoming TCP connections to a port. "
  400. "You must overload the onConnectionRequest callback to create a new TCPObject to "
  401. "read, write, or reject the new connection.\n\n"
  402. "@param port Port for this TCPObject to start listening for connections on.\n"
  403. "@tsexample\n"
  404. "// Create a listener on port 8080.\n"
  405. "new TCPObject( TCPListener );\n"
  406. "TCPListener.listen( 8080 );\n\n"
  407. "function TCPListener::onConnectionRequest( %this, %address, %id )\n"
  408. "{\n"
  409. " // Create a new object to manage the connection.\n"
  410. " new TCPObject( TCPClient, %id );\n"
  411. "}\n\n"
  412. "function TCPClient::onLine( %this, %line )\n"
  413. "{\n"
  414. " // Print the line of text from client.\n"
  415. " echo( %line );\n"
  416. "}\n"
  417. "@endtsexample\n")
  418. {
  419. object->listen(U32(port));
  420. }
  421. DefineEngineMethod(TCPObject, connect, void, (const char* address),,
  422. "@brief Connect to the given address.\n\n"
  423. "@param address Server address (including port) to connect to.\n"
  424. "@tsexample\n"
  425. "// Set the address.\n"
  426. "%address = \"www.garagegames.com:80\";\n\n"
  427. "// Inform this TCPObject to connect to the specified address.\n"
  428. "%thisTCPObj.connect(%address);\n"
  429. "@endtsexample\n")
  430. {
  431. object->connect(address);
  432. }
  433. DefineEngineMethod(TCPObject, disconnect, void, (),,
  434. "@brief Disconnect from whatever this TCPObject is currently connected to, if anything.\n\n"
  435. "@tsexample\n"
  436. "// Inform this TCPObject to disconnect from anything it is currently connected to.\n"
  437. "%thisTCPObj.disconnect();\n"
  438. "@endtsexample\n")
  439. {
  440. object->disconnect();
  441. }
  442. void processConnectedReceiveEvent(NetSocket sock, RawData incomingData)
  443. {
  444. TCPObject *tcpo = TCPObject::find(sock);
  445. if(!tcpo)
  446. {
  447. Con::printf("Got bad connected receive event.");
  448. return;
  449. }
  450. U32 size = incomingData.size;
  451. U8 *buffer = (U8*)incomingData.data;
  452. while(size)
  453. {
  454. U32 ret = tcpo->onReceive(buffer, size);
  455. AssertFatal(ret <= size, "Invalid return size");
  456. size -= ret;
  457. buffer += ret;
  458. }
  459. //If our buffer now has something in it then it's probably a web socket packet and lets handle it
  460. if(!tcpo->isBufferEmpty())
  461. {
  462. //Copy all the data into a string (may be a quicker way of doing this)
  463. U8 *data = (U8*)incomingData.data;
  464. String temp;
  465. for(S32 i = 0; i < incomingData.size; i++)
  466. {
  467. temp += data[i];
  468. }
  469. //Send the packet to script
  470. bool handled = tcpo->onPacket_callback(temp);
  471. //If the script did something with it, clear the buffer
  472. if(handled)
  473. {
  474. tcpo->emptyBuffer();
  475. }
  476. }
  477. tcpo->onEndReceive_callback();
  478. }
  479. void processConnectedAcceptEvent(NetSocket listeningPort, NetSocket newConnection, NetAddress originatingAddress)
  480. {
  481. TCPObject *tcpo = TCPObject::find(listeningPort);
  482. if(!tcpo)
  483. return;
  484. tcpo->onConnectionRequest(&originatingAddress, (U32)newConnection.getHandle());
  485. }
  486. void processConnectedNotifyEvent( NetSocket sock, U32 state )
  487. {
  488. TCPObject *tcpo = TCPObject::find(sock);
  489. if(!tcpo)
  490. return;
  491. switch(state)
  492. {
  493. case Net::DNSResolved:
  494. tcpo->onDNSResolved();
  495. break;
  496. case Net::DNSFailed:
  497. tcpo->onDNSFailed();
  498. break;
  499. case Net::Connected:
  500. tcpo->onConnected();
  501. break;
  502. case Net::ConnectFailed:
  503. tcpo->onConnectFailed();
  504. break;
  505. case Net::Disconnected:
  506. tcpo->onDisconnect();
  507. break;
  508. }
  509. }