Author Topic: Torque Webserver V2  (Read 8042 times)

Some of you that frequent this section might remember me posting a webserver concept I had drafted up in a reference scripts topic I created awhile ago. I decided to rewrite that script today and add a ton of new features to it with hopes that it will get even more usage than the old one.

Click for beautiful syntax highlighting and monospace font:
http://pastebin.com/9q06sz62

Quote
Changelog:
- Changed $SERVER["REMOTE_ADDR"] to $_SERVER["REMOTE_ADDR"]
- Fixed a ?> detection issue when preceeded with a tab character
- Disconnects clients potentially attempting to exploit Torque file handling
- Added IP blacklist field at Webserver.blockIPs (Nice try, 78.69.143.58)
- Added HTTP authentication (.tqss), but don't count on it without testing

Note: I plan to constantly update this as needed. If you are interested in using this for a project (which users such as DrenDran, Chrono, and Zack0Wack0 have), stay tuned in this topic for the latest updates. Also, if you want to see a new feature added, just ask away and I can implement it.



I'm still calling it beta, so test out these features sometime:

  • Preferences for port, debug echos, local IP restriction, timeout in MS, root folder, default index page, and Torquescript tag prefixes and suffixes can be set in the Webserver creation.
  • Setting of variables familiar to PHP users such as  $_GET, $_POST, and $_SERVER. The $_SERVER variables are dynamically created in Webserver::finish based in on the header received.
  • Torquescript located in pages is evaluated before the data is sent to the client, allowing for very dynamic pages such as a real-time score display or even an external server management page.

From a more in depth point of view, it has all this and more:

  • Easily editable settings in object creation
  • Optional debug echos can be turned on
  • Accepts incoming connections on any port
  • Can filter to only local IP addresses
  • Connections can timeout after a delay
  • Lines parsed as received to allow for binary data
  • Binary data can be submitted (POST)
  • Lines parsed as a whole after all submitted
  • Temporary file used for reading binary data
  • Sets $_GET based on provided arguments
  • Sets $_SERVER based on provided header
  • Sets $_POST based on provided body data
  • Allows for defaulting to an index page
  • Sends a 404 error if page is not found
  • Support functions include, print, and puts



These two pages...





...are generated from...

derp.tqs
Code: [Select]
<?tqs
print($_GET["a"]    @ "<br/>\n");
print($_GET["dood"] @ "<br/>\n");
?>
<br/>
hi<br/>
<br/>
<?tqs include("/date.tqs"); ?><br/>
<br/>
<form name="input" action="submit.tqs" method="post">
Username: <input type="text" name="user" /><br/>
Fav Color: <input type="text" name="color" /><br/>
<input type="submit" value="Submit" />
</form>

date.tqs
Code: [Select]
Page displayed at:<br/>
<?tqs
print(getDateTime() @ "<br/>\n");
include("/author.tqs");
?>

author.tqs
Code: [Select]
By: <?tqs print("Truce"); ?>
submit.tqs
Code: [Select]
<?tqs
print($_POST["user"]  @ "<br/>\n");
print($_POST["color"] @ "<br/>\n");
?>



Sample output from debug being set to true (from console.log):

Quote
[Webserver] Connect request from IP 98.217.56.108 (6842)
[Webserver] > Client timeout in 1000 milliseconds.
[Webserver] Packet terminated from client 6842.
[Webserver] Parsing client 6842's GET args: a=31&dood=392
[Webserver] > Assigning 31 to a.
[Webserver] > Assigning 392 to dood.
[Webserver] Parsing client 6842's header: Host: trewse.us.to
Connection: keep-alive
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.642.2 Safari/534.16
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3


[Webserver] > Assigning trewse.us.to to HTTP_Host.
[Webserver] > Assigning keep-alive to HTTP_Connection.
[Webserver] > Assigning application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 to HTTP_Accept.
[Webserver] > Assigning Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.642.2 Safari/534.16 to HTTP_User_Agent.
[Webserver] > Assigning gzip,deflate,sdch to HTTP_Accept_Encoding.
[Webserver] > Assigning en-US,en;q=0.8 to HTTP_Accept_Language.
[Webserver] > Assigning ISO-8859-1,utf-8;q=0.7,*;q=0.3 to HTTP_Accept_Charset.
[Webserver] Parsing client 6842's POST args:
[Webserver] > No POST args found to parse!
[Webserver] Deploying file: /derp.tqs
[Webserver] > File found! Including all its contents.



For the HTTP authentication accounts file, format it like this example:
(username:password on a line, # are comments, blank lines ignored.)

Quote
# Friends
admin:password
truce:test

# Clan Members
blah:lol

The webserver objects points to config/accounts.dat by default.



Questions? Comments? Concerns? Just direct whatever feedback you may have to the reply button.
Again, everything I've tested (above) has worked so far, but I'm hoping someone will find a bug in it.
« Last Edit: March 28, 2011, 09:30:06 AM by Truce »

Cool. Who's going to make a webbrowser? :cookieMonster:

Even though, the only thing I noted (Except security issues, $SERVER["REMOTE_ADDR"] and not backwards compatible with HTTP/1.0) is that you should do a strupr on all parameters you get in on line 101. But that isn't necessary.
« Last Edit: January 25, 2011, 01:35:38 AM by mctwist »

Sweet you added include :D
Did you fix the post with chrome issue?

Cool. Who's going to make a webbrowser? :cookieMonster:

Even though, the only thing I noted (Except security issues, $SERVER["REMOTE_ADDR"] and not backwards compatible with HTTP/1.0) is that you should do a strupr on all parameters you get in on line 101. But that isn't necessary.

What kind of security issues are there? Also, fixed the missing underscore. Thanks.
As for the strupr, variables are case insensitive, so I didn't bother putting that in.

Sweet you added include :D
Did you fix the post with chrome issue?

It turns out the TCPObject had to be set to binary mode to capture the post args.
That took me much longer than it should have to debug, but I found it in the end.

What kind of security issues are there?
Code: [Select]
<?tqs
quit();
?>
Still, I doubt that you would do that nor allow anyone else.

Code: [Select]
<?tqs
quit();
?>
Still, I doubt that you would do that nor allow anyone else.

Considering only people with access to Blockland's folder can create that, I don't see a problem.

Considering only people with access to Blockland's folder can create that, I don't see a problem.
That reminds me of the exploit you found in RP Core. Nobody would abuse it unless some serverside script opened it up, which apparently it did for someone.

That reminds me of the exploit you found in RP Core. Nobody would abuse it unless some serverside script opened it up, which apparently it did for someone.

So any mod that exports vars to a .cs file and execs it to load them has flawed security, because another add-on can write "quit();" to that .cs file? No, because if that was the case, the add-on appending the code has a problem, not the original. Besides, from Iban's post on the RTB forums, it looked like you didn't need another mod to exploit RP Core, meaning yours had an issue.

Code: [Select]
<?tqs
quit();
?>
Still, I doubt that you would do that nor allow anyone else.
How is that any different from putting it in a CS file and having something execute it in-game?

Besides, from Iban's post on the RTB forums, it looked like you didn't need another mod to exploit RP Core, meaning yours had an issue.
Code: [Select]
// Changing a variable
function ChangeRPVariable(%var, %value)
{
// Invalid variable
if (%var $= "")
return false;
// Check for cheats
if (strstr(%var, ";") >= 0 || strstr(%value, ";") >= 0)
return false;
%t = $RP::setting::namei_[%var];
// Variable exist
if (!%t)
return false;

eval("$RP::pref::" @ %var @ " = \"" @ %value @ "\";");

$RP::setting::var[%t] = %value;
RP_PreparePrefList(%t);
//RP_PreparePrefTable();
/}
This is the code where the exploit was found. Note these lines:
Code: [Select]
%t = $RP::setting::namei_[%var];
// Variable exist
if (!%t)
return false;
So, if I'm throwing in his example in the function:
Code: [Select]
ChangeRPVariable("McTwist", "\"@quit()@\"");Check variable which is false:
Code: [Select]
if ("McTwist" $= "")
return false;
Continues with next, which is false:
Code: [Select]
if (strstr("McTwist", ";") >= 0 || strstr("\"@quit()@\"", ";") >= 0)
return false;
And now comes the interesting part:
Code: [Select]
"" = $RP::setting::namei_["McTwist"];
// Variable exist
if (!"")
return false;
What will happen there? Someone have to fake that variable to make this work. I admit that the exploit was a really idiotic thing, but still, it cannot be opened from outside.
Also, to clear things up, I heard you were one of the guys that hacked the victims server. Please tell if that information was wrong.

How is that any different from putting it in a CS file and having something execute it in-game?
Point. I take it back.

Whipped up a little page for testing stuff, check it out: http://trewse.us.to/

Refresh it a few times to see the different box layouts (completely randomized with an equally as random RGB color scheme). Clicking a box loads up a song from Youtube who's ID randomly picked from a file. Since I was lazy, I picked 16 song URLs out of recommended videos box so the genres might not be your thing, but give it a listen anywho. The songs will auto loop.

That site will usually be up and down since I host it from my laptop for testing.

loving.
Code: [Select]
function createWebServer(%name,%htdocs)
{
   if(isObject(nametoid("web_" @ %name)))
      return;
   %obj = new TCPObject()
   {
      class = webServerAPI_server;
      name = %name;
      htdocs = %htdocs;
      isWEBAPI = 1;
   };
   %obj.setName("web_" @ %name);
   return %obj;
}
function TCPObject::serve(%this,%port)
{
   %this.listen(%port);
}

function TCPObject::enableModule(%this,%mod)
{
%dir = filePath(expandFilename("./main.cs")) @ "/modules/" @ %mod @ "/";
echo(%dir);
if(!isFile(%dir @ "main.cs"))
return 0;
exec(%dir @ "main.cs");
if(strLen(%this.mods) < 1)
   %this.mods = 0;
%this.mod[%this.mods] = %mod;
%this.mods++;
}

package webServers
{
   function TCPObject::onConnectRequest(%this,%ip,%socket)
   {
      if(!%this.isWEBAPI)
      {
         Parent::onConnectRequest(%this,%ip,%socket);
         return;
      }
      if(isObject(%this.connection[%ip]))
      {
         if(%this.debug)
            echo("["@%this.name@"] duplicated_connect:" SPC %ip);
         %this.connection[%ip].disconnect();
         %this.connection[%ip].delete();
      }
      if(%this.debug)
         echo("["@%this.name@"] connect::" SPC %ip);
      %this.connection[%ip] = new TCPObject(webServerAPI_client,%socket)
      {
         ip = %ip;
         parent = %this;
      };
   }
};
activatePackage(webServers);

function webServerAPI_client::handleRequestOptions(%this,%data)
{
   deleteVariables("$_SERVER*");
   deleteVariables("$_ISSET*");
   deleteVariables("$_POST*");
   deleteVariables("$_GET*");
   $_SERVER["REMOTE_IP"] = %this.ip;
   $_SERVER["SERVING"] = %this;
   for(%i=0;%i<getLineCount(%data);%i++)
   {
      %line = getLine(%data,%i);
      if(firstWord(%line) $= "GET" || firstWord(%line) $= "POST")
      {
         //if(%this.parent.debug)
         //   echo("["@%this.parent.name@"]" SPC "Handling request: "@%line);
         %type = getWord(%line,0);
         %path = getWord(%line,1);
         %http = getWord(%line,2);
         $_SERVER["RQST_TYPE"] = %type;
         $_SERVER["FILE_PATH"] = %path;
         $_SERVER["HTTP_VERSION"] = %http;
      }
      else
      {
         //if(%this.parent.debug)
         //   echo("["@%this.parent.name@"]" SPC "Handling header: "@%line);
         %opt_t = getSubStr(%line,0,strPos(%line,": "));
         %opt_v = getSubStr(%line,strPos(%line,": ")+2,strLen(%line));
         $_SERVER[%opt_t] = %opt_v;
      }
   }
   if((%qPos = strPos(%path,"?")) >= 0)
   {
      %post_data = strReplace(getSubStr(%path,%qPos+1,strLen(%path)),"&","\t");
      //%post_data = strReplace(getSubStr(%path,strPos(%path,"?")+1,strLen(%path)),"&","\t");
      %path = getSubStr(%path,0,%qPos);
      for(%i=0;%i<getFieldCount(%post_data);%i++)
      {
         %ld = getField(%post_data,%i);
         %ps = strPos(%ld,"=");
         if(%ps < 0)
            $_ISSET[%ld] = true;
         else
         {
            %l_vr = getSubStr(%ld,0,%ps);
            %l_vl = getSubStr(%ld,%ps+1,strLen(%ld));
            $_ISSET[%l_vr] = true;
            $_GET[%l_vr] = %l_vl;
            $_POST[%l_vr] = %l_vl;
         }
      }
   }
   %this.handleRequest(%path);
}

function webServerAPI_client::handleRequest(%this,%file)
{
   %file = strReplace(%file,"..","");
   if(getSubStr(%file,0,1) $= "/")
      %file = %this.parent.htdocs @ %file;
   else
      %file = %this.parent.htdocs @ "/" @ %file;
   if(%this.parent.debug)
      echo("["@%this.parent.name@"]" SPC "Sending output: "@%file);
   if(!isFile(%file))
   {
      %oldFile = %file;
      if(isFile(%file @ "/index.html")) %file = %file @ "/index.html";
      if(isFile(%file @ "/index.htm")) %file = %file @ "/index.htm";
      if(isFile(%file @ "/index.tqs")) %file = %file @ "/index.tqs";
      if(isFile(%file @ "index.html")) %file = %file @ "index.html";
      if(isFile(%file @ "index.htm")) %file = %file @ "index.htm";
      if(isFile(%file @ "index.tqs")) %file = %file @ "index.tqs";
      if(%file !$= %oldFile && %this.parent.debug)
         echo("["@%this.parent.name@"]" SPC "File changed to: "@%file);
   }
   if(!isFile(%file))
   {
      %exceptionCaught = true;
      if(!isFile(%this.parent.htdocs @ %this.parent.page[404]))
         %errorOccured = true;
      else
         %file = %this.parent.htdocs @ %this.parent.page[404];
      %code = 404;
      %msg = "File Not Found";
      if(%this.parent.debug)
         echo("["@%this.parent.name@"]" SPC "404, solution: "@(%errorOccured ? "error" : "404 page"));
   }
   if(!%exceptionCaught && isFile(filePath(%file) @ "/.htaccess") && strLen($_SERVER["Authenticate"]) < 1)
   {
      %exceptionCaught = true;
      %errorOccured = true;
      %fo = new FileObject();
      %fo.openForRead(filePath(%file) @ "/.htaccess");
      %realm = %fo.readLine();
      %fo.close();
      %fo.delete();
      %code = 401;
      %msg = "Authorization Required";
      %headAdd = "WWW-Authenticate: Basic realm=\""@%realm@"\"";
   }
   if(!%exceptionCaught && isFile(filePath(%file) @ "/.htaccess") && strLen($_SERVER["Authenticate"]) > 0)
   {
      %authType = firstWord($_SERVER["Authenticate"]);
      %authData = restWords($_SERVER["Authenticate"]);
      if(%authType !$= "Basic")
      {
         %exceptionCaught = true;
         if(!isFile(%this.parent.htdocs @ %this.parent.page[403]))
            %errorOccured = true;
         else
            %file = %this.parent.htdocs @ %this.parent.page[403];
         %code = 403;
         %msg = "Permission Denied";
      }
      else
      {
         %isValid = false;
         %fo = new FileObject();
         %fo.openForRead(filePath(%file) @ "/.htaccess");
         while(!%fo.isEOF())
            if(%fo.readLine() $= %authData)
               %isValid = true;
         %fo.close();
         %fo.delete();
         if(!%isValid)
         {
            %exceptionCaught = true;
            if(!isFile(%this.parent.htdocs @ %this.parent.page[403]))
               %errorOccured = true;
            else
               %file = %this.parent.htdocs @ %this.parent.page[403];
            %code = 403;
            %msg = "Permission Denied";
         }
      }
   }
   if(!%errorOccured)
   {
      %fo = new FileObject();
      %fo.openForRead(%file);
      while(!%fo.isEOF())
         %filedat = %filedat @ %fo.readLine() NL "";
      %filedat = trim(%filedat);
      %fo.close();
      %fo.delete();
      if(!%exceptionCaught)
      {
         %code = 200;
         %msg = "OK";
      }
   }
   $_SERVER["FILEDATA"] = %filedat;
   for(%i=0;%i<%this.parent.mods;%i++)
   {
      %m = %this.parent.mod[%i] @ "_changeData";
      if(isFunction(%m))
         call(%m,%filedat);
   }
   if(strLen($_SERVER["FILEDATA"]) < 1)
      $_SERVER["FILEDATA"] = "No Content";
   %header = %header @ "HTTP/1.1" SPC %code SPC %msg @ "\n\r";
   %header = %header @ "Server: " @ %this.parent.name @ "\n\r";
   %header = %header @ "Date: " @ getDateTime() @ "\n\r";
   %header = %header @ (strLen(%headAdd) < 1 ? "" : %headAdd @ "\n\r");
   %header = %header @ "Content-Type: text/html" @ "\n\r";
   %header = %header @ "Content-Length: " @ strLen($_SERVER["FILEDATA"]) @ "\n\r";
   %data = %header @ "\n\r" @ $_SERVER["FILEDATA"];
   if(%this.parent.debug)
   {
      echo("["@%this.parent.name@"]" SPC "SERVER PROCESSING FINISHED, TRANSMITTING:");
      echo(%data);
   }
   %this.send(%data);
}

function fileDataCLEAR()
{
   $_SERVER["FILEDATA"] = "";
}
function fileDataAPPEND(%str)
{
   $_SERVER["FILEDATA"] = $_SERVER["FILEDATA"] @ %str NL "";
}

function webServerAPI_client::onLine(%this,%line)
{
   if(%this.parent.debug)
      echo("["@%this.parent.name@"]" SPC %this.ip SPC "sent: "@%line);
   if(strLen(%line) < 1)
      %this.handleRequestOptions(rtrim(%this.lineData));
   else
      %this.lineData = %this.lineData @ %line NL "";
   return;
   deleteVariables("$_SERVER*");
   $_SERVER[REMOTE_IP] = %this.ip;
   %cmd = firstWord(%line);
   %arg = restWords(%line);
   if(%this.parent.debug)
      echo("["@%this.parent.name@"] recieve: "@%line);
   switch$ (%cmd)
   {
      case GET:
         %file_path = %this.parent.htdocs @ strReplace(firstWord(%arg),"..","");
         %http_version = getWord(%arg,1);
         if(isFile(%file_path @ "index.tqs") && !isFile(%file_path))
            %file_path = %file_path @ "index.tqs";
         echo("["@%this.parent.name@"] "@%this.ip@" requested: "@%file_path);
         if(!isFile(%file_path))
         {
            %this.send("404 File Not Found");
            %this.disconnect();
            %this.schedule(10,delete);
            return;
         }
         %this.transmitFile(%file_path);
      case POST:
         deleteVariables("$_GET*");
         deleteVariables("$_POST*");
         deleteVariables("$_ARG*");
         %data = firstWord(%arg);
         %q_pos = strPos(%data,"?");
         if(%q_pos >= 0)
         {
            %args = strReplace(getSubStr(%data,%q_pos+1,strLen(%data)),"&","\t");
            for(%i=0;%i<getFieldCount(%args);%i++)
            {
               %tArg = getField(%args,%i);
               %l_pos = strPos(%tArg,"=");
               if(%l_pos < 0)
               {
                  $_ARG[%tArg] = true;
                  $_GET[%tArg] = true;
                  $_POST[%tArg] = true;
               }
               else
               {
                  %tArgN = getSubStr(%tArg,0,%l_pos);
                  %tArgD = getSubStr(%tArg,%l_pos+1,strLen(%tArg));
                  $_ARG[%tArgN] = true;
                  $_GET[%tArgN] = %tArgD;
                  $_POST[%tArgN] = %tArgD;
               }
            }
         }
         %file_path = %this.parent.htdocs @ strReplace(getSubStr(%data,0,(%q_pos < 0 ? strLen(%data) : %q_pos)),"..","");
         %http_version = getWord(%arg,1);
         if(isFile(%file_path @ "index.tqs") && !isFile(%file_path))
            %file_path = %file_path @ "index.tqs";
         echo("["@%this.parent.name@"] "@%this.ip@" requested: "@%file_path);
         if(!isFile(%file_path))
         {
            %this.send("404 File Not Found");
            %this.disconnect();
            %this.schedule(10,delete);
            return;
         }
         %this.transmitFile(%file_path);
   }
}

function webServerAPI_client::transmitFile(%this,%f)
{
   %read = new FileObject();
   %read.openForRead(%f);
   while(!%read.isEOF())
   {
      %line = %read.readLine();
      if(!%inTQS)
      {
         if(%line $= "<?tqs")
            %inTQS = true;
         else
            %this.send(%line @ "\n\r");
      }
      else
      {
         if(%line $= "?>")
         {
            %inTQS = false;
            %correct = false;
            eval(rtrim(%tqsCODE) SPC "%correct = true;");
            if(!%correct)
               %this.send("A syntax error was accountered in the server-sided script files.\n\r");
            %tqsCODE = "";
         }
         else
         {
            %dat = strReplace(%line,"transmit",%this @ ".send");
            %tqsCODE = %tqsCODE @ %dat NL "";
         }
      }
   }
   %read.close();
   %read.delete();
   %this.disconnect();
   %this.schedule(10,delete);
}

And yes, know that syntax is crappy. Did this long time ago.

loving.

And yes, know that syntax is crappy. Did this long time ago.

How long ago exactly?

How long ago exactly?
About a month. Unless he have kept it for himself for longer.

Bump

And 78.69.143.58 is probably DontCare4Free