/*

 BabbleMud - A MUD server

 Author:  Nick Gammon 
          http://www.gammon.com.au/ 

(C) Copyright Nick Gammon 2004. Permission to copy, use, modify, sell and
distribute this software is granted provided this copyright notice appears
in all copies. This software is provided "as is" without express or implied
warranty, and with no claim as to its suitability for any purpose.
 
*/

// standard library includes ...

#include <vector>
#include <stdexcept>
#include <iostream>
#include <fstream>
#include <iterator>
#include <iomanip>

#ifdef WIN32
#include <time.h>
#include <winsock.h>
#endif

#include <sys/stat.h>
using namespace std; 

#include "utils.h"
#include "constants.h"
#include "strings.h"
#include "globals.h"
#include "webclient.h"
#include "gameutils.h"

static char fbuf [512];    

WebClient::~WebClient () // dtor
  {
  try // exceptions in destructors are bad
    {
    ProcessWrite ();    // send outstanding text
    if (s_ != NO_SOCKET) /* close connection if active */
      close (s_);
    CloseFile ();

    } 
  catch (exception & e)
    {
    cerr << "Exception thrown in destructor for web client: " <<
             e.what () << endl;
    }
  };

void DoFooter (WebClient & w)
  {
  w << control ().gets ("html_footer") << "\n";
  w.connstate_ = eAwaitingFinish;  
  } // end of DoFooter

const string SendMessage (WebClient & w, int iWhich)
  {
  string sMessage;
  
  switch (iWhich)
    {
      
    // Informational 1xx
      
    case 100: sMessage = "Continue"; break;  
    case 101: sMessage = "Switching Protocols"; break;  
      
    // Successful 2xx

    case 200: sMessage = "OK"; break;  
    case 201: sMessage = "Created"; break;  
    case 202: sMessage = "Accepted"; break;  
    case 203: sMessage = "Non-Authoritative Information"; break;  
    case 204: sMessage = "No Content"; break;  
    case 205: sMessage = "Reset Content"; break;  
    case 206: sMessage = "Partial Content"; break;  
    
    // Redirection 3xx

    case 300: sMessage = "Multiple Choices"; break;  
    case 301: sMessage = "Moved Permanently"; break;  
    case 302: sMessage = "Found"; break;  
    case 303: sMessage = "See Other"; break;  
    case 304: sMessage = "Not Modified"; break;  
    case 305: sMessage = "Use Proxy"; break;  
    case 306: sMessage = "(Unused)"; break;  
    case 307: sMessage = "Temporary Redirect"; break;  
    
    // Client Errors 4xx
    
    case 400: sMessage = "Bad Request"; break;  
    case 401: sMessage = "Unauthorized"; break;  
    case 402: sMessage = "Payment Required"; break;  
    case 403: sMessage = "Forbidden"; break;  
    case 404: sMessage = "Not Found"; break;  
    case 405: sMessage = "Method Not Allowed"; break;  
    case 406: sMessage = "Not Acceptable"; break;  
    case 407: sMessage = "Proxy Authentication Required"; break;  
    case 408: sMessage = "Request Timeout"; break;  
    case 409: sMessage = "Conflict"; break;  
    case 410: sMessage = "Gone"; break;  
    case 411: sMessage = "Length Required"; break;  
    case 412: sMessage = "Precondition Failed"; break;  
    case 413: sMessage = "Request Entity Too Large"; break;  
    case 414: sMessage = "Request-URI Too Long"; break;  
    case 415: sMessage = "Unsupported Media Type"; break;  
    case 416: sMessage = "Requested Range Not Satisfiable"; break;  
    case 417: sMessage = "Expectation Failed"; break;  

    // Server Errors 5xx

    case 500: sMessage = "Internal Server Error"; break;  
    case 501: sMessage = "Not Implemented"; break;  
    case 502: sMessage = "Bad Gateway"; break;  
    case 503: sMessage = "Service Unavailable"; break;  
    case 504: sMessage = "Gateway Timeout"; break;  
    case 505: sMessage = "HTTP Version Not Supported"; break;  
      
    default:  iWhich = 500;
              sMessage = "Internal Server Error"; 
              break;
    } // end of switch
    
  w << MAKE_STRING (w.version_ << " " << setw (3)<< iWhich 
                    << " " << sMessage << "\n");
  
  return sMessage;
  
  } // end of WebClient::SendMessage    
  
const string GetBrowserTime (const long whichtime)
  {

   struct tm *gmt;
   
   long ltime = whichtime;

   if (!ltime)
    time( &ltime );

   /* Obtain coordinated universal time: */
   gmt = gmtime( &ltime );
   
   char buff [100];
   
   char * days [] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
   char * months [] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
                        "Aug", "Sep", "Oct", "Nov", "Dec"};
   // sanity checks
   if (gmt->tm_wday < 0 || gmt->tm_wday > 6)
     gmt->tm_wday = 0;
   
   if (gmt->tm_mon < 0 || gmt->tm_mon > 11)
     gmt->tm_mon = 0;
     
   // there seems to be some doubt as to whether the date should be
   //  15 Feb 2004 or 15-Feb-2004
   sprintf (buff, "%s, %02i %s %i %02i:%02i:%02i GMT",
            days [gmt->tm_wday],
            gmt->tm_mday,
            months [gmt->tm_mon],
            gmt->tm_year + 1900,
            gmt->tm_hour,
            gmt->tm_min,
            gmt->tm_sec);
            
  return buff;       
} // end of GetBrowserTime

void SendError (WebClient & w, const int iWhich)
  {
// interpret and send the message

  string sMessage = DoHeader (w, iWhich);
  
  w << "<h1>" << sMessage << "</h1>\n";
  w << "<hr/>\n";;
  w << "<p><font size=1>HTTP Error " << iWhich << " - " <<
            sMessage << "</font></p>\n";
  w << "<p>" << MUDNAME << " " << VERSION << "</p>\n";

  DoFooter (w);
  } // end of WebClient::SendError

void AssembleValue (const string & sItem, StringMap & data)
  {
  string s = trim (sItem);
  
  if (s.empty ())
    return;
    
  string::size_type i = sItem.find ('=');
  if (i != string::npos)
    data [URLDecode (sItem.substr (0, i))] = URLDecode (sItem.substr (i + 1, string::npos));
  } // end of AssembleValue

void ShowError (WebClient & w, const string & what)
  {
  w << "<p></p><table border=\"0\" cellpadding=\"5\" style=\"border:0;font-size:11pt;\">"
    << "<tr bgcolor=\"#A52A2A\">"
    << "<td><font color=\"#FFFFFF\" size=\"+1\">\n";
  w << FixHTML (what) << "\n";
  w << "</font></td></tr></table>\n";
  }   // end of ShowError


void ProcessPage (WebClient & w)
  {
static WebHandlerMap & webhandlermap = getWebHandlerMap ();
    
  // get rid of leading slash(es)
  while (!w.URL_.empty () && (w.URL_ [0] == '/' || w.URL_ [0] == '\\'))
    w.URL_.erase (0, 1);

    // default to index.htm
  if (w.URL_.empty ())
    w.URL_ = "index.htm";
 
 // try to work out file type
  string sType;  
  string sSuffix;
  
  string::size_type iLastDot = w.URL_.find_last_of ('.');
  
  if (iLastDot != string::npos && iLastDot != w.URL_.length () - 1)
    {
    sSuffix = tolower (w.URL_.substr (iLastDot + 1));
      
//    webserverlog ("Got suffix of \"" + sSuffix + "\"");
    
    if (sSuffix == "gif")  
      sType = "image/gif";
    else if (sSuffix == "htm" || sSuffix == "html" || sSuffix == "php")  
      sType = "text/html; charset=iso-8859-1";
    else if (sSuffix == "txt")  
      sType = "text/plain; charset=iso-8859-1";
    }  // end of having a suffix

  if (sType.empty ())
    {
    SendError (w, 404);   // only allow above suffixes
    return;  
    }

  // verification - test their cookie session code and see if we recognise it
  
  string token = w.cookieData_ ["token"];
  
  if (!token.empty ())
    {
    ObjectList & webUsers = control ().items_ ["web_users"];
    ObjectIter iUser = FindObject (webUsers, "token", token);
    if (iUser != webUsers.end ())
      {
      w.verified_ = true;
      w.user_ = *iUser;
      } // aha! found him/her/it
    } // end of having a token cookie
  
  if (sSuffix == "php")
    {
    
    // look up this page in our control data
    ObjectList & pages = control ().items_ ["web_pages"];
    ObjectIter iPage = FindObject (pages, "name", w.URL_ );
    
    // not there, they used the wrong name
    if (iPage == pages.end ())
      {
      SendError (w, 404);   // special page not found
      return;  
      }
    
    // now find its handler
    WebHandlerMap::const_iterator handlerIter = webhandlermap.find (iPage->gets ("handler"));
      
    // no handler? system error
    if (handlerIter == webhandlermap.end ())
      {
      cerr << "Web page handler " << iPage->gets ("handler") << " not found." << endl;
      SendError (w, 404);   // special page not found
      return;  
      }
    
    try
      {
      if (verifyWebUser (w, *iPage))     // check they can do this page
        handlerIter->second (w);    // call appropriate handler
      }
    catch (exception & e)
      {
      ShowError (w, e.what ());
      } // end of catch
    
    DoFooter (w);   // normal pages, and errors, require footers
    return;         // all done
    } // end of php file

  string filename = WEB_DIR + w.URL_;   // full name
  
  webserverlog ("opening file " + filename);

  // get length
  
  struct stat statbuff;
   
  // also, don't let them work out of the directory hierarchy with ../
  if (filename.find ("../") != string::npos ||
      filename.find ("..\"") != string::npos ||
      stat (filename.c_str (), &statbuff) != 0)
    {   
    SendError (w, 404);   // not found
    return;  
    }

  // open file
    
  w.file_ = fopen (filename.c_str (), "r");
  
  if (!w.file_)
    {   
    SendError (w, 404);   // not found
    return;  
    }
  
  string modifiedDateString = GetBrowserTime (statbuff.st_mtime);
  
  // if they said something like: If-Modified-Since: Sat, 14 Aug 2004 05:07:56 GMT
  // and that was the exact date we have for the file, send a 304 "not modififed" message
  // and no further data.

  if (w.ifModifiedSince == modifiedDateString)
    {
    DoHeader (w, 304, "", false); // not modified
    w << "\n";    // blank line ends header
    webserverlog ("File " + filename + " not modified since " + modifiedDateString);
    w.connstate_ = eAwaitingFinish;  
    w.CloseFile ();
    return;
    }
  
  DoHeader (w, 200, "", false);
  if (!sType.empty ())
    w << "Content-Type: " << sType << "\n";
  w << "Content-Length: " << static_cast<int> (statbuff.st_size) << "\n";
  w << "Last-Modified: " << modifiedDateString << "\n";
  w << "\n";

  // header only wanted, all done
  if (w.command_ == "HEAD")
    {
    w.CloseFile ();
    w.connstate_ = eAwaitingFinish; 
    return;    
    }
  
  size_t iRead = fread (fbuf, 1, sizeof fbuf, w.file_);

  w.connstate_ = eSendingResponse;
   
  if (iRead < sizeof fbuf)
    {
    if (ferror (w.file_))
      {
      perror ("reading file");
      w.CloseFile ();
      w.CloseClient ();  
      }
    w.CloseFile ();
    w.connstate_ = eAwaitingFinish;  
    }
  
  w << string (fbuf, iRead); // first block of data
  
  } // end of WebClient::ProcessGet
  
// process data (eg. POST data)
void ProcessData (WebClient & w)
  {
string::size_type i;
  
  do
    {
    i = w.inbuf_.find ('&');
    if (i != string::npos)
      {
      AssembleValue (w.inbuf_.substr (0, i), w.postData_);
      w.inbuf_.erase (0, i + 1);
      }  
    } while (i != string::npos);
       
  AssembleValue (w.inbuf_, w.postData_);

  w.inbuf_.clear ();
 
  ProcessPage (w);
  
  } // end of ProcessData

void ProcessCompleteRequest (WebClient & w)
  {
    
  webserverlog ("Processing request ... " + w.command_ + " " + w.URL_);
  
  if (w.command_ == "GET" || w.command_ == "HEAD")
    ProcessPage (w);
  else
    SendError (w, 501);       // not implemented

  } // end of ProcessCompleteRequest


void ProcessURI (WebClient & w, string sURI)
  {
  string::size_type i = sURI.find ('?');
  
  // nothing? then whole thing must be file name
  if (i == string::npos)
    {
    w.URL_ = URLDecode (sURI);
    return;
    }

  w.URL_ = URLDecode (sURI.substr (0, i));	/* extract first item */
  sURI.erase (0, i + 1);    // erase first item
      
  do
    {
    i = sURI.find ('&');
    if (i != string::npos)
      {
      AssembleValue (sURI.substr (0, i), w.getData_);
      sURI.erase (0, i + 1);
      }  
    } while (i != string::npos);
       
  AssembleValue (sURI, w.getData_);
  

  } // end of ProcessURI
   

/* process commands (GET/PUT/POST etc.) */

void ProcessCommand (WebClient & w, const string & sLine)
{
pair<string, string> s;

 // the initial line should look like:  COMMAND URI VERSION
 // eg. GET /images/mypicture.gif HTTP/1.0
 
  webserverlog ("URI = " + sLine);

  s = GetWord (sLine);
  w.command_ = s.first;
   
  // get URI
  s = GetWord (s.second);  
  string sURI = s.first;

  // extract out GET arguments (eg. "a=b&c=d")
  ProcessURI (w, sURI);
  
  s = GetWord (s.second);
  w.version_ = s.first;

  if (w.version_ == "")
    w.version_ = "HTTP/1.0";

  w.connstate_ = eAwaitingHeaders;	/* get the headers now */
          
}	/* end of ProcessCommand */

// see: http://www.cookiecentral.com/faq
     
// convert a cookie header into a cookie item
// we will assume the first part is name=value
/*
 The other cookie args are:

  expires=Mon, 01-Jan-2001 00:00:00 GMT 
  path=/promo 
  domain=www.myserver.com 
  secure=false

eg.

Set-Cookie: foo=bar; path=/; expires Mon, 09-Dec-2002 13:46:00 GMT

  However it appears these extra args are on the Set-Cookie parameter
  (server to cient), not the Cookie parameter (client to server).
  
*/

void AddCookie (WebClient & w, string sCookie)
  {   
  string::size_type i = sCookie.find (';');
  if (i != string::npos)
    sCookie.erase (i, string::npos);	// erase other stuff
      
  AssembleValue (sCookie, w.cookieData_);
  
  } // end of AddCookie
 

// process a header line, eg. Extension: Security/Remote-Passphrase

void ProcessHeader (WebClient & w, string & sLine)
  {
  
  // a blank line after the headers signifies start of the data portion
  if (sLine.empty ())   // end of headers
    {
    HeaderList::const_iterator listiter;
    bool sendContinue = false;
    
    // first, get the cookie data, and other important headers
    for (listiter = w.headers_.begin (); 
         listiter != w.headers_.end (); 
         listiter++)
      {
      if (listiter->first == "cookie")
        AddCookie (w, listiter->second);
      else if (listiter->first == "if-modified-since")
        w.ifModifiedSince = listiter->second;
      else if (listiter->first == "content-length")
        w.data_wanted_ = atoi (listiter->second.c_str ());
      else if (listiter->first == "expect")
        {
        if (tolower (listiter->second).find ("100-continue") != string::npos)
          sendContinue = true;
        }

      } // end of searching for cookies
      
    if (w.command_ != "POST")
      ProcessCompleteRequest (w);     // send the reply
    else
      {
      
      if (w.version_ == "HTTP/1.1" && sendContinue)
        {
        SendMessage (w, 100);  // tell them to continue
        w << "\n";  // need a blank line
        }
      
      w.connstate_ = eAwaitingData;   /* data follows the headers */
      
       // we may have some there now
      w.data_received_ = w.inbuf_.length ();
      
      }
    return;      
    }
        
  // a line starting with a space continues the previous header
  if (isspace (sLine [0]))
    {
    // can't append to a non-existent header
    if (w.headers_.empty ())
      {
      SendError (w, 400); // bad request
      return;          
      }
     
    pair<string, string> & header = w.headers_.back (); 
      
    header.second += " ";    // add to last header (space is included)
    header.second += trim (sLine); 
    return;
    }
  
   pair<string, string> s;
  
   // get header name 
   s = GetWord (sLine);
   string sName = s.first;
  
  // the header should end with :
  if (sName.length () <= 1 || sName [sName.length () - 1] != ':')
    {
    SendError (w, 400); // bad request
    return;  
    }

  // get rid of colon  
  sName = trim (string (&sName [0], sName.length () - 1));
  
  // add to list of headers for this client
  w.headers_.push_back (pair<string, string> (tolower (sName), s.second));  
              
  } // end of ProcessHeader
   
/* process client input - check connection state, and act accordingly */

void ProcessWebClientInput (WebClient & w, string & sLine)
{
  /* get rid of carriage-return, if present */

  string::size_type i = sLine.find ('\r');
  if (i != string::npos)
    sLine.erase (i, 1);
  
//  webserverlog (MAKE_STRING ("State " << w.connstate_ << " got line: " << sLine));
  
  switch (w.connstate_)
    {
    case eAwaitingCommand:
      ProcessCommand (w, trim (sLine));
      break;

    case eAwaitingHeaders:
      ProcessHeader (w, sLine);
      break;
          
    case eAwaitingFinish:
      break;
      
    /* whoops! */
    default:
      cerr << "Invalid connstate " <<  w.connstate_ << " for connection " << w.GetSocket () << endl;
      break;
    }
}	/* end of ProcessClientInput */

/* Here when there is outstanding data to be read for this web client */

void WebClient::ProcessRead ()
{
  
  if (closing_)
    return;   // once closed, don't handle any pending input
  
  // I make it static to save allocating a buffer each time.
  // Hopefully this function won't be called recursively.
  static vector<char> buf (1000);  // reserve 1000 bytes for reading into
  
  // keep reading until it blocks
  while (true)
    {
    
    // we have already reached end-of-file - don't read any more
    if (connstate_ == eSendingResponse)
      return;
    
    int nRead = read (s_, &buf [0], buf.size ());
    
    if (nRead == -1)
      {
      #ifdef WIN32
      if (errno != WSAEWOULDBLOCK)
      #else
      if (errno != EWOULDBLOCK)
      #endif
        {
        perror ("read from web client");
        CloseClient ();        
        }
      return;
      }
  
    // at end-of-file send the reply
    if (nRead == 0)
      {
//      webserverlog ("End of file");
      
      connstate_ = eSendingResponse;
      ProcessCompleteRequest (*this);     // send the reply
      return;
      }
  
    inbuf_ += string (&buf [0], nRead);    /* add to input buffer */
  
    if (connstate_ == eAwaitingData)
       data_received_ += nRead;  
    
    /* try to extract lines from the input buffer */
    while (!closing_)
      {
      
      if (connstate_ == eAwaitingData)
         {
         if (data_received_ >= data_wanted_)
            ProcessData (*this);
         else
           break;
         }
      
      string::size_type i = inbuf_.find ('\n');
      if (i == string::npos)
        break;  /* no more at present */
  
      string sLine = inbuf_.substr (0, i);  // extract first line 
      inbuf_.erase (0, i + 1);              // keep rest of string
  
      ProcessWebClientInput (*this, sLine);  /* now, do something with it */        
      }
   } // end of read loop
} /* end of WebClient::ProcessRead */

/* Here when we can send stuff to the web client. We are allowing for large
 volumes of output that might not be sent all at once, so whatever cannot
 go this time gets put into the list of outstanding strings for this web client. */

void WebClient::ProcessWrite ()
{
  /* we will loop attempting to write all in buffer, until write blocks */
  while (s_ != NO_SOCKET && !outbuf_.empty ())
    {

    // how much to send
    int iLength = outbuf_.size ();

    // send to web client
    int nWrite = write (s_, outbuf_.c_str (), iLength );
    
    // check for bad write
    if (nWrite < 0)
      {
      #ifdef WIN32
      if (errno != WSAEWOULDBLOCK)
      #else
      if (errno != EWOULDBLOCK)
      #endif
        {
        perror ("send to web client");  /* some other error? */
        CloseClient ();        
        }
      return;
      }

    // remove what we successfully sent from the buffer
    outbuf_.erase (0, nWrite);
      
    // if partial write, exit
    if (nWrite < iLength)
       break;

 // if we are sending stuff and the buffer empties, read more
    
    if (connstate_ == eSendingResponse && 
        file_ &&
        outbuf_.empty ())
      {

  //    printf ("..reading more..\n");
      
      size_t iRead = fread (fbuf, 1, sizeof fbuf, file_);
      outbuf_ = string (fbuf, iRead);
          
      // less than requested - error or end-of-file
      if (iRead < sizeof fbuf)
        {
        if (ferror (file_))
          {
          perror ("reading file");
          CloseClient ();  
          }
        CloseFile ();
        connstate_ = eAwaitingFinish;  
        }
      }
    
    } /* end of having write loop */

}   /* end of WebClient::ProcessWrite  */


void WebClient::ProcessException ()
{
  /* signals can cause exceptions, don't get too excited. :) */
  webserverlog ("Exception on socket " + s_);
} /* end of WebClient::ProcessException */

void WebClient::CloseFile ()
  {
  if (file_)
    {
    fclose (file_);
    file_ = NULL;
    }
  } // end of WebClient::CloseFile

struct fWebShowOneDebugItem
{
  WebClient & w_;
  int count_;

  // ctor
  fWebShowOneDebugItem (WebClient & w) : w_ (w), count_ (0) {}
  
  void operator() (const pair<string, string> & item)
    {
    w_ << "<li>";
    w_ << FixHTML (item.first) << " = " 
       << FixHTML (item.second) << "\n";
    ++count_; 
    }
  
  const int GetCount () { return count_; }

};  // end of fWebShowOneDebugItem

template <typename T>
void WebShowList (WebClient & w, const string & name, const T & which)
  {
  w << "<h2>" << name << " variables</h2>\n";
  w << "<ul>\n";
  int count = for_each (ISEQ (which), fWebShowOneDebugItem (w)).GetCount ();
  w << "</ul>\n";
  w << "<p>" << count << " " << name << " variable(s)</p>\n";    
  } // end of WebShowList

// debugging what we get from the client
void WebDebugVars (WebClient & w)
  {
  WebShowList (w, "Headers", w.headers_);
  WebShowList (w, "Get", w.getData_);
  WebShowList (w, "Post", w.postData_);
  WebShowList (w, "Cookie", w.cookieData_);
  } // end of DebugVars
