/*
 * uHTTPD (Micro HTTP server)
 *
 * Copyright 2009 Francesco Saverio Giudice <info / at / fsgiudice.com>
 * Copyright 2008 Mindaugas Kavaliauskas (dbtopas at dbtopas.lt)
 *
 * Credits:
 *    Based on first version posted from Mindaugas Kavaliauskas on
 *    developers NG on 2008-12-15 whom give my thanks to have
 *    shared initial work.
 *                                                          Francesco.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file LICENSE.txt.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA (or visit https://www.gnu.org/licenses/).
 *
 * As a special exception, the Harbour Project gives permission for
 * additional uses of the text contained in its release of Harbour.
 *
 * The exception is that, if you link the Harbour libraries with other
 * files to produce an executable, this does not by itself cause the
 * resulting executable to be covered by the GNU General Public License.
 * Your use of that executable is in no way restricted on account of
 * linking the Harbour library code into it.
 *
 * This exception does not however invalidate any other reasons why
 * the executable file might be covered by the GNU General Public License.
 *
 * This exception applies only to the code released by the Harbour
 * Project under the name Harbour.  If you copy code from other
 * Harbour Project or Free Software Foundation releases into a copy of
 * Harbour, as the General Public License permits, the exception does
 * not apply to the code that you add in this way.  To avoid misleading
 * anyone as to the status of such modified files, you must delete
 * this exception notice from them.
 *
 * If you write modifications of your own for Harbour, it is your choice
 * whether to permit this exception to apply to your modifications.
 * If you do not wish that, delete this exception notice.
 *
 */

/*
 * A simple HTTP server.
 *
 * More description to come.
 *
 */

/*
  TODO:
      - security check
        verify to launch .hrb and .exe *only* from cgi-bin
      - optimize code
      - add SSL support
      - fix dynamic threads (now locked to fixed number)
      - add full mime type handling
      - add cgi exec on linux
      - add .htaccess support
      - fix binding to address

*/

#require "hbnf"
#require "hbwin"
#require "hbgd"

// remove comment to activate hb_ToOutDebug()
// #define DEBUG_ACTIVE

#define FIXED_THREADS         // This force application to use fixed number of running threads and no service threads

#include "fileio.ch"
#include "inkey.ch"
#include "error.ch"
#include "hbmemory.ch"
#include "hbgtinfo.ch"

REQUEST __HB_EXTERN__

#include "hbsocket.ch"

#if defined( HBMK_HAS_HBGD )
// adding GD support
REQUEST GDChart
REQUEST GDImage
REQUEST gdImageChar
   #define APP_GD_SUPPORT "_GD"
   #stdout "Lib GD support enabled"
#else
   #define APP_GD_SUPPORT ""
   #stdout "Lib GD support disabled"
#endif

#ifdef FIXED_THREADS
   #define APP_DT_SUPPORT "_FIXED_THREADS"
   #stdout "Fixed # of threads"
#else
   #define APP_DT_SUPPORT ""
   #stdout "Dynamic # of threads"
#endif

#define APP_NAME      "uhttpd"
#define APP_VER_NUM   "0.4.4"
#define APP_VERSION   APP_VER_NUM + APP_GD_SUPPORT + APP_DT_SUPPORT

// default values - they can changes using line command switch or ini file

#define START_RUNNING_THREADS   6             // Start threads to serve connections
#define MAX_RUNNING_THREADS    20             // Max running threads

#define START_SERVICE_THREADS   0             // Initial number for service connections
#define MAX_SERVICE_THREADS     3             // Max running threads

#define LISTEN_PORT             8082          // differs from standard 80 port for tests in case
// anyone has a apache/IIS installed
#define FILE_STOP               ".uhttpd.stop"
#define FILE_ACCESS_LOG         "logs" + hb_ps() + "access.log"
#define FILE_ERROR_LOG          "logs" + hb_ps() + "error.log"
#define DIRECTORYINDEX_ARRAY    { "index.html", "index.htm" }

#define PAGE_STATUS_REFRESH   5
#define THREAD_MAX_WAIT     ( 30 ) // How much time thread has to wait a new connection before finish - IN SECONDS
#define CGI_MAX_EXEC_TIME     30   // in seconds

// TOCHECK: Caching of HRB modules (Is this faster than loading HRBBody from file where OS will cache ?)
#define HRB_ACTIVATE_CACHE   .F.   // if .T. caching of HRB modules will be enabled. (NOTE: changes of files will not be loaded until server is active)

#define CR_LF    ( Chr( 13 ) + Chr( 10 ) )

#define THREAD_GT hb_gtVersion()

STATIC s_lQuitRequest := .F.

STATIC s_hmtxQueue, s_hmtxServiceThreads, s_hmtxRunningThreads, s_hmtxLog, s_hmtxConsole, s_hmtxBusy
STATIC s_hmtxHRB

STATIC s_hfileLogAccess, s_hfileLogError, s_cApplicationRoot, s_cDocumentRoot, s_lIndexes, s_lConsole, s_nPort
STATIC s_cSessionPath
STATIC s_nThreads, s_nStartThreads, s_nMaxThreads
STATIC s_nServiceThreads, s_nStartServiceThreads, s_nMaxServiceThreads
STATIC s_nConnections, s_nMaxConnections, s_nTotConnections
STATIC s_nServiceConnections, s_nMaxServiceConnections, s_nTotServiceConnections
STATIC s_aRunningThreads := {}
STATIC s_aServiceThreads := {}
STATIC s_hHRBModules     := { => }
STATIC s_aDirectoryIndex

STATIC s_hActions := { ;
   /* "default-handler" => @Handler_Default(), */;    // default handler
   /* "send-as-is"      => @Handler_SendAsIs(), */;
   "cgi-script"      => @Handler_CgiScript(), ;
   "hrb-script"      => @Handler_HrbScript(), ;
   /* "server-info"     => @Handler_ServerInfo(), */;
   "server-status"   => @Handler_ServerStatus() }

STATIC s_hHandlers := { ;
   "hrb"            => "hrb-script", ;
   "exe"            => "cgi-script", ;
   "/serverstatus"  => "server-status" }

// STATIC s_lAcceptPathInfo   := .T.

// SCRIPTALIASES: now read from ini file
// STATIC s_hScriptAliases    := { "/info" => "/cgi-bin/info.hrb" }
STATIC s_hScriptAliases    := { => }
STATIC s_hAliases          := { => }

THREAD STATIC t_cResult, t_nStatusCode, /*t_aHeader,*/ t_cErrorMsg, t_oSession

MEMVAR _SERVER, _GET, _POST, _COOKIE, _SESSION, _REQUEST, _HTTP_REQUEST, _HTTP_RESPONSE, m_cPost

ANNOUNCE ERRORSYS


// ----------------------------------------
//
//               M A I N
//
// ----------------------------------------

PROCEDURE Main( ... )

   LOCAL nPort, hListen, hSocket, aRemote, cI, xVal
   LOCAL aThreads, nStartThreads, nMaxThreads, nStartServiceThreads
   LOCAL i, cPar, lStop
   LOCAL cGT, cApplicationRoot, cDocumentRoot, lIndexes, cConfig
   LOCAL lConsole, lScriptAliasMixedCase, aDirectoryIndex
   LOCAL nProgress := 0
   LOCAL hDefault, cLogAccess, cLogError, cSessionPath
   LOCAL cCmdPort, cCmdApplicationRoot, cCmdDocumentRoot, lCmdIndexes, nCmdStartThreads, nCmdMaxThreads
   LOCAL nConsoleRows, nConsoleCols
   LOCAL nCmdConsoleRows, nCmdConsoleCols

#if defined( __HBSCRIPT__HBSHELL )
   #if defined( __PLATFORM__WINDOWS )
      hbshell_gtSelect( "GTWVT" )
   #endif
#endif

   IF ! hb_mtvm()
      ? "I need multhread support. Please, recompile me!"
      WAIT
      ErrorLevel( 2 )
      RETURN
   ENDIF

   // ----------------------- Initializations ---------------------------------

   SysSettings()

   ErrorBlock( {| oError | uhttpd_DefError( oError ) } )

   // ----------------------- Parameters defaults -----------------------------

   // defaults not changeble via ini file
   lStop                := .F.
   cConfig              := hb_DirBase() + APP_NAME + ".ini"
   lConsole             := .T.
   nStartServiceThreads := START_SERVICE_THREADS

   // Check GT version - if I have started app with //GT:NUL then I have to disable
   // console and application will start in hidden way.
   cGT := hb_gtVersion()
   IF cGT == "NUL"
      lConsole := .F.
   ENDIF

   // TOCHECK: now not force case insensitive
   // hb_HCaseMatch( s_hScriptAliases, .F. )

   // ----------------- Line command parameters checking ----------------------

   i := 1
   DO WHILE i <= PCount()

      cPar := hb_PValue( i++ )

      DO CASE
      CASE cPar == "--port"             .OR. cPar == "-p"
         cCmdPort    := hb_PValue( i++ )

      CASE cPar == "--approot"          .OR. cPar == "-a"
         cCmdApplicationRoot := hb_PValue( i++ )

      CASE cPar == "--docroot"          .OR. cPar == "-d"
         cCmdDocumentRoot := hb_PValue( i++ )

      CASE cPar == "--indexes"          .OR. cPar == "-i"
         lCmdIndexes := .T.

      CASE cPar == "--stop"             .OR. cPar == "-s"
         lStop    := .T.

      CASE cPar == "--config"           .OR. cPar == "-c"
         cConfig     := hb_PValue( i++ )

      CASE cPar == "--start-threads"    .OR. cPar == "-ts"
         nCmdStartThreads := Val( hb_PValue( i++ ) )

      CASE cPar == "--max-threads"      .OR. cPar == "-tm"
         nCmdMaxThreads := Val( hb_PValue( i++ ) )

      CASE cPar == "--console-rows"     .OR. cPar == "-cr"
         nCmdConsoleRows := Val( hb_PValue( i++ ) )

      CASE cPar == "--console-cols"     .OR. cPar == "-cc"
         nCmdConsoleCols := Val( hb_PValue( i++ ) )

      CASE cPar == "--help"             .OR. Lower( cPar ) == "-h" .OR. cPar == "-?"
         Help()
         RETURN

      OTHERWISE
         Help()
         RETURN
      ENDCASE
   ENDDO

   // -------------------- checking STOP request -------------------------------

   IF lStop
      hb_MemoWrit( FILE_STOP, "" )
      RETURN
   ELSE
      FErase( FILE_STOP )
   ENDIF

   // ----------------- Parse ini file ----------------------------------------

   // hb_ToOutDebug( "cConfig = %s\n\r", cConfig )

   hDefault := ParseIni( cConfig )

   // ------------------- Parameters changeable from ini file ----------------

   // All key values MUST be in uppercase
   nPort                 := hDefault[ "MAIN" ][ "PORT" ]
   cApplicationRoot      := hDefault[ "MAIN" ][ "APPLICATION_ROOT" ]
   cDocumentRoot         := hDefault[ "MAIN" ][ "DOCUMENT_ROOT" ]
   lIndexes              := hDefault[ "MAIN" ][ "SHOW_INDEXES" ]
   lScriptAliasMixedCase := hDefault[ "MAIN" ][ "SCRIPTALIASMIXEDCASE" ]
   cSessionPath          := hDefault[ "MAIN" ][ "SESSIONPATH" ]
   aDirectoryIndex       := hDefault[ "MAIN" ][ "DIRECTORYINDEX" ]
   nConsoleRows          := hDefault[ "MAIN" ][ "CONSOLE-ROWS" ]
   nConsoleCols          := hDefault[ "MAIN" ][ "CONSOLE-COLS" ]

   cLogAccess            := hDefault[ "LOGFILES" ][ "ACCESS" ]
   cLogError             := hDefault[ "LOGFILES" ][ "ERROR" ]

   nStartThreads         := hDefault[ "THREADS" ][ "START_NUM" ]
   nMaxThreads           := hDefault[ "THREADS" ][ "MAX_NUM" ]

   // ATTENTION: script aliases can be in mixed case
   // i.e. we can have /info or /Info that will be different unless lScriptAliasMixedCase will be .F.
   FOR EACH xVal IN hDefault[ "SCRIPTALIASES" ]
      IF HB_ISSTRING( xVal )
         s_hScriptAliases[ iif( lScriptAliasMixedCase, xVal:__enumKey(), Upper( xVal:__enumKey() ) ) ] := xVal
      ENDIF
   NEXT

   // ATTENTION: path aliases cannnot be in mixed case
   // i.e. we can have /info or /Info that will be different
   FOR EACH xVal IN hDefault[ "ALIASES" ]
      IF HB_ISSTRING( xVal )
         s_hAliases[ xVal:__enumKey() ] := xVal
      ENDIF
   NEXT

   // hb_ToOutDebug( "cLogAccess = %s, cLogError = %s\n\r", cLogAccess, cLogError )
   // hb_ToOutDebug( "hDefault = %s\n\r", hb_ValToExp( hDefault ) )
   // hb_ToOutDebug( "s_hScriptAliases = %s\n\r", hb_ValToExp( s_hScriptAliases ) )
   // hb_ToOutDebug( "s_hAliases = %s\n\r", hb_ValToExp( s_hAliases ) )

   // ------------------- Parameters forced from command line ----------------

   IF cCmdPort != NIL
      nPort := Val( cCmdPort )
   ENDIF

   IF cCmdApplicationRoot != NIL
      cApplicationRoot := cCmdApplicationRoot
   ENDIF

   IF cCmdDocumentRoot != NIL
      cDocumentRoot := cCmdDocumentRoot
   ENDIF

   IF lCmdIndexes != NIL
      lIndexes := lCmdIndexes
   ENDIF

   IF nCmdStartThreads != NIL
      nStartThreads := nCmdStartThreads
   ENDIF

   IF nCmdMaxThreads != NIL
      nMaxThreads := nCmdMaxThreads
   ENDIF

   IF nCmdConsoleRows != NIL
      nConsoleRows := nCmdConsoleRows
   ENDIF

   IF nCmdConsoleCols != NIL
      nConsoleCols := nCmdConsoleCols
   ENDIF

   // -------------------- adjusting MACROS values ----------------------------

   // cApplicationRoot can be only ExePath() or a correct full path
   cDocumentRoot         := StrTran( cDocumentRoot, "$(APP_DIR)", cApplicationRoot )
   cSessionPath          := StrTran( cSessionPath, "$(APP_DIR)", cApplicationRoot )
   cLogAccess            := StrTran( cLogAccess, "$(APP_DIR)", cApplicationRoot )
   cLogError             := StrTran( cLogError, "$(APP_DIR)", cApplicationRoot )

   // -------------------- checking starting values ----------------------------

   IF nPort <= 0 .OR. nPort > 65535
      ? "Invalid port number:", nPort
      WAIT
      ErrorLevel( 1 )
      RETURN
   ENDIF

   IF HB_ISSTRING( cApplicationRoot )
      IF Empty( cApplicationRoot )
         cApplicationRoot := "." + hb_ps()
      ENDIF
      cI := cApplicationRoot
      IF hb_DirExists( cI )
         IF Right( cI, 1 ) == "/" .AND. Len( cI ) > 2 .AND. !( SubStr( cI, Len( cI ) - 2, 1 ) == ":" )
            s_cApplicationRoot := Left( cI, Len( cI ) - 1 )
         ELSE
            s_cApplicationRoot := cI
         ENDIF
      ELSE
         ? "Invalid application root:", cI
         WAIT
         ErrorLevel( 3 )
         RETURN
      ENDIF
   ELSE
      ? "Invalid application root"
      WAIT
      ErrorLevel( 3 )
      RETURN
   ENDIF

#ifdef DEBUG_ACTIVE
   hb_ToOutDebug( "s_cDocumentRoot = %s, cDocumentRoot = %s\n\r", s_cDocumentRoot, cDocumentRoot )
#endif

   IF HB_ISSTRING( cDocumentRoot )
      // cI := StrTran( SubStr( cDocumentRoot, 2 ), "\", "/" )
      cI := cDocumentRoot
      IF hb_DirExists( cI )
         IF Right( cI, 1 ) == "/" .AND. Len( cI ) > 2 .AND. !( SubStr( cI, Len( cI ) - 2, 1 ) == ":" )
            s_cDocumentRoot := Left( cI, Len( cI ) - 1 )
         ELSE
            s_cDocumentRoot := cI
         ENDIF
      ELSE
         ? "Invalid document root:", cI
         WAIT
         ErrorLevel( 3 )
         RETURN
      ENDIF
   ELSE
      ? "Invalid document root"
      WAIT
      ErrorLevel( 3 )
      RETURN
   ENDIF

#ifdef DEBUG_ACTIVE
   hb_ToOutDebug( "s_cDocumentRoot = %s, cDocumentRoot = %s\n\r", s_cDocumentRoot, cDocumentRoot )
#endif

   IF nMaxThreads <= 0
      nMaxThreads := MAX_RUNNING_THREADS
   ENDIF

   IF nStartThreads < 0
      nStartThreads := 0
   ELSEIF nStartThreads > nMaxThreads
      nStartThreads := nMaxThreads
   ENDIF

   IF nConsoleRows < 1 // .OR. nConsoleRows > MaxRow() + 1
      nConsoleRows := MaxRow()
   ENDIF

   IF nConsoleCols < 1 // .OR. nConsoleCols > MaxCol() + 1
      nConsoleCols := MaxCol()
   ENDIF

   // -------------------- assign STATIC values --------------------------------

   s_lIndexes               := lIndexes
   s_lConsole               := lConsole
   s_nPort                  := nPort
   s_nThreads               := 0
   s_nStartThreads          := nStartThreads
   s_nMaxThreads            := nMaxThreads
   s_nServiceThreads        := 0
   s_nStartServiceThreads   := nStartServiceThreads
   s_nMaxServiceThreads     := MAX_SERVICE_THREADS
   s_nConnections           := 0
   s_nMaxConnections        := 0
   s_nTotConnections        := 0
   s_nServiceConnections    := 0
   s_nMaxServiceConnections := 0
   s_nTotServiceConnections := 0
   s_cSessionPath           := cSessionPath
   s_aDirectoryIndex        := aDirectoryIndex

   // --------------------- Open log files -------------------------------------

   IF ( s_hfileLogAccess := FOpen( cLogAccess, FO_CREAT + FO_WRITE ) ) == F_ERROR
      ? "Can't open access log file"
      WAIT
      ErrorLevel( 1 )
      RETURN
   ENDIF
   FSeek( s_hfileLogAccess, 0, FS_END )

   IF ( s_hfileLogError := FOpen( cLogError, FO_CREAT + FO_WRITE ) ) == F_ERROR
      ? "Can't open error log file"
      WAIT
      ErrorLevel( 1 )
      RETURN
   ENDIF
   FSeek( s_hfileLogError, 0, FS_END )

   // --------------------- MAIN PART ------------------------------------------

   IF s_lConsole
      SET CURSOR OFF
      SetMode( nConsoleRows, nConsoleCols )
      // hb_ToOutDebug( "nConsoleRows = %s, nConsoleCols = %s", nConsoleRows, nConsoleCols )
      // hb_ToOutDebug( "nCmdConsoleRows = %s, nCmdConsoleCols = %s", nCmdConsoleRows, nCmdConsoleCols )
   ENDIF

   // --------------------- define mutexes -------------------------------------

   s_hmtxQueue          := hb_mutexCreate()
   s_hmtxLog            := hb_mutexCreate()
   s_hmtxConsole        := hb_mutexCreate()
   s_hmtxBusy           := hb_mutexCreate()
   s_hmtxRunningThreads := hb_mutexCreate()
   s_hmtxServiceThreads := hb_mutexCreate()
   s_hmtxHRB            := hb_mutexCreate()

   WriteToConsole( "--- Starting " + APP_NAME + " ---" )

   // --------------------------------------------------------------------------
   // SOCKET CREATION
   // --------------------------------------------------------------------------

   hListen := hb_socketOpen()
   IF ! hb_socketBind( hListen, { HB_SOCKET_AF_INET, "0.0.0.0", nPort } )
      ? "bind() error", hb_socketGetError()
   ELSEIF ! hb_socketListen( hListen )
      ? "listen() error", hb_socketGetError()
   ELSE
      // --------------------------------------------------------------------------------- //
      // Starting Accept connection thread
      // --------------------------------------------------------------------------------- //

      WriteToConsole( "Starting AcceptConnection Thread" )
      aThreads := {}
      AAdd( aThreads, hb_threadStart( @AcceptConnections() ) )
#ifdef DEBUG_ACTIVE
      hb_ToOutDebug( "Len( aThreads ) = %i\n\r", Len( aThreads ) )
#endif

      // --------------------------------------------------------------------------------- //
      // main loop
      // --------------------------------------------------------------------------------- //

      WriteToConsole( "Starting main loop" )

      IF s_lConsole
         hb_DispOutAt( 1, 5, APP_NAME + " - web server - v. " + APP_VERSION )
         hb_DispOutAt( 4, 5, "Server listening (Port: " + hb_ntos( nPort ) + ") : ..." )
         hb_DispOutAt( 10, 9, "Waiting." )
      ENDIF

      DO WHILE .T.

#ifdef __PLATFORM__WINDOWS
         // windows resource releasing - 1 millisecond wait
         IF win_SysRefresh( 1 ) != 0
            EXIT
         ENDIF
#endif

         IF Inkey( , HB_INKEY_GTEVENT ) == HB_K_CLOSE
            GT_notifier( HB_K_CLOSE )
         ENDIF

         IF hb_mutexLock( s_hmtxBusy )
            IF s_lQuitRequest
               hb_mutexUnlock( s_hmtxBusy )
               EXIT
            ENDIF
            hb_mutexUnlock( s_hmtxBusy )
         ENDIF

         IF s_lConsole

            // Show application infos
            IF hb_mutexLock( s_hmtxBusy )
               hb_DispOutAt(  5,  5, "Threads           : " + Transform( s_nThreads, "9999999999" ) )
               hb_DispOutAt(  6,  5, "Connections       : " + Transform( s_nConnections, "9999999999" ) )
               hb_DispOutAt(  7,  5, "Max Connections   : " + Transform( s_nMaxConnections, "9999999999" ) )
               hb_DispOutAt(  8,  5, "Total Connections : " + Transform( s_nTotConnections, "9999999999" ) )

#ifndef FIXED_THREADS
               hb_DispOutAt(  5, 37, "ServiceThreads    : " + Transform( s_nServiceThreads, "9999999999" ) )
               hb_DispOutAt(  6, 37, "Connections       : " + Transform( s_nServiceConnections, "9999999999" ) )
               hb_DispOutAt(  7, 37, "Max Connections   : " + Transform( s_nMaxServiceConnections, "9999999999" ) )
               hb_DispOutAt(  8, 37, "Total Connections : " + Transform( s_nTotServiceConnections, "9999999999" ) )
#endif // FIXED_THREADS
               hb_DispOutAt( 10, 40, "Memory: " + hb_ntos( Memory( HB_MEM_USED ) ) )
               hb_mutexUnlock( s_hmtxBusy )
            ENDIF

            // Show progress
            Progress( @nProgress )
         ENDIF

         // Wait a connection
         IF Empty( hSocket := hb_socketAccept( hListen, @aRemote, 50 ) )
            IF hb_socketGetError() == HB_SOCKET_ERR_TIMEOUT
               // Checking if I have to quit
               IF hb_FileExists( FILE_STOP )
                  FErase( FILE_STOP )
                  EXIT
               ENDIF
            ELSE
               WriteToConsole( hb_StrFormat( "accept() error: %s", hb_socketGetError() ) )
            ENDIF
         ELSE
            // Send accepted connection to AcceptConnections() thread
            hb_mutexNotify( s_hmtxQueue, hSocket )
         ENDIF

         // Memory release
         // hb_gcAll( .T. )

      ENDDO

      WriteToConsole( "Waiting threads" )
      // Send to threads that they have to stop
      AEval( aThreads, {|| hb_mutexNotify( s_hmtxQueue, NIL ) } )
      // Wait threads to end
      AEval( aThreads, {| h | hb_threadJoin( h ) } )

   ENDIF

   WriteToConsole( "--- Quitting " + APP_NAME + " ---" )

   // Close socket
   hb_socketClose( hListen )

   // Close log files
   FClose( s_hfileLogAccess )
   FClose( s_hfileLogError )

   SET CURSOR ON

   RETURN

// --------------------------------------------------------------------------------- //
// THREAD FUNCTIONS
// --------------------------------------------------------------------------------- //

STATIC FUNCTION AcceptConnections()

   LOCAL hSocket
   LOCAL n
#ifndef FIXED_THREADS
   LOCAL nConnections, nThreads, nMaxThreads
   LOCAL nServiceConnections, nServiceThreads, nMaxServiceThreads
   LOCAL lCanNotify
#endif
   LOCAL pThread
   LOCAL lQuitRequest := .F.

   ErrorBlock( {| oError | uhttpd_DefError( oError ) } )

   WriteToConsole( "Starting AcceptConnections()" )

   IF hb_mutexLock( s_hmtxBusy )
      // Starting initial running threads
      FOR n := 1 TO s_nStartThreads
         pThread := hb_threadStart( @ProcessConnection() )
         AAdd( s_aRunningThreads, pThread )
      NEXT

      // Starting initial service threads
      FOR n := 1 TO s_nStartServiceThreads
         pThread := hb_threadStart( @ServiceConnection() )
         AAdd( s_aServiceThreads, pThread )
      NEXT
      hb_mutexUnlock( s_hmtxBusy )
   ENDIF

   // Main AcceptConnections loop
   DO WHILE .T.

      // reset socket
      hSocket := NIL

#ifdef __PLATFORM__WINDOWS
      // releasing resources
      IF win_SysRefresh( 1 ) != 0
         lQuitRequest := .T.
      ENDIF
#endif

      IF hb_mutexLock( s_hmtxBusy )
         IF s_lQuitRequest
            hb_mutexUnlock( s_hmtxBusy )
            lQuitRequest := .T.
         ENDIF
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      // Waiting a connection from main application loop
      IF ! lQuitRequest
         hb_mutexSubscribe( s_hmtxQueue,, @hSocket )
      ENDIF

      // I have a QUIT request
      IF hSocket == NIL .OR. lQuitRequest

         // Requesting to Running threads to quit (using -1 value)
         AEval( s_aRunningThreads, {|| hb_mutexNotify( s_hmtxRunningThreads, -1 ) } )
#ifndef FIXED_THREADS
         // Requesting to Service threads to quit (using -1 value)
         AEval( s_aServiceThreads, {|| hb_mutexNotify( s_hmtxServiceThreads, -1 ) } )
#endif
         // waiting running threads to quit
         AEval( s_aRunningThreads, {| h | hb_threadJoin( h ) } )
#ifndef FIXED_THREADS
         // waiting service threads to quit
         AEval( s_aServiceThreads, {| h | hb_threadJoin( h ) } )
#endif
         IF hb_mutexLock( s_hmtxBusy )
            // hb_ToOutDebug( "Len( s_aRunningThreads ) = %i\n\r", Len( s_aRunningThreads ) )
            ASize( s_aRunningThreads, 0 )
#ifndef FIXED_THREADS
            ASize( s_aServiceThreads, 0 )
#endif
            hb_mutexUnlock( s_hmtxBusy )
         ENDIF

         EXIT
      ENDIF

#ifndef FIXED_THREADS
      // Load current state
      IF hb_mutexLock( s_hmtxBusy )
         nConnections       := s_nConnections
         nThreads           := s_nThreads
         nMaxThreads        := s_nMaxThreads
         nServiceConnections := s_nServiceConnections
         nServiceThreads    := s_nServiceThreads
         nMaxServiceThreads := s_nMaxServiceThreads
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      lCanNotify := .F.

      // If I have no more running threads to use ...
      IF nConnections > nMaxThreads

         // If I have no more of service threads to use ... (DOS attack ?)
         IF nServiceConnections > nMaxServiceThreads
            // DROP connection
            hb_socketShutdown( hSocket )
            hb_socketClose( hSocket )

            // If I have no service threads in use ...
         ELSEIF nServiceConnections >= nServiceThreads
            // Add one more
            IF hb_mutexLock( s_hmtxBusy )
               pThread := hb_threadStart( @ServiceConnection() )
               AAdd( s_aServiceThreads, pThread )
               lCanNotify := .T.
               hb_mutexUnlock( s_hmtxBusy )
            ENDIF
         ENDIF

         // Otherwise I send connection to current service thread queue
         IF lCanNotify
            hb_mutexNotify( s_hmtxServiceThreads, hSocket )
         ENDIF

         LOOP

         // If I have no free running threads to use ...
      ELSEIF nConnections >= nThreads
         // Add one more
         IF hb_mutexLock( s_hmtxBusy )
            pThread := hb_threadStart( @ProcessConnection() )
            AAdd( s_aRunningThreads, pThread )
            lCanNotify := .T.
            hb_mutexUnlock( s_hmtxBusy )
         ENDIF
      ELSE
         lCanNotify := .T.
      ENDIF
      // Otherwise I send connection to running thread queue
      // hb_ToOutDebug( "Len( s_aRunningThreads ) = %i\n\r", Len( s_aRunningThreads ) )
      IF lCanNotify
#endif // FIXED_THREADS
         hb_mutexNotify( s_hmtxRunningThreads, hSocket )
#ifndef FIXED_THREADS
      ENDIF
#endif // FIXED_THREADS

   ENDDO

   WriteToConsole( "Quitting AcceptConnections()" )

   RETURN 0

// --------------------------------------------------------------------------------- //
// CONNECTIONS
// --------------------------------------------------------------------------------- //
STATIC FUNCTION ProcessConnection()

   LOCAL hSocket, nLen, cRequest, cSend
   LOCAL nMsecs, nParseTime, nPos, nThreadID
   LOCAL lQuitRequest := .F.

   PRIVATE _SERVER
   PRIVATE _GET
   PRIVATE _POST
   PRIVATE _COOKIE
   PRIVATE _SESSION
   PRIVATE _REQUEST
   PRIVATE _HTTP_REQUEST
   PRIVATE _HTTP_RESPONSE
   PRIVATE m_cPost

   nThreadId    := hb_threadID()

#ifdef DEBUG_ACTIVE
   hb_ToOutDebug( "nThreadId = %s\r\n", nThreadId )
#endif

   ErrorBlock( {| oError | uhttpd_DefError( oError ) } )

   WriteToConsole( "Starting ProcessConnections() " + hb_CStr( nThreadID ) )

   IF hb_mutexLock( s_hmtxBusy )
      s_nThreads++
      hb_mutexUnlock( s_hmtxBusy )
   ENDIF

   // ProcessConnection Loop
   DO WHILE .T.

      // Reset socket
      hSocket := NIL

#ifdef __PLATFORM__WINDOWS
      // releasing resources
      IF win_SysRefresh( 1 ) != 0
         lQuitRequest := .T.
         EXIT
      ENDIF
#endif

      IF hb_mutexLock( s_hmtxBusy )
         IF s_lQuitRequest
            hb_mutexUnlock( s_hmtxBusy )
            lQuitRequest := .T.
            EXIT
         ENDIF
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      // Waiting a connection from AcceptConnections() but up to defined time
      hb_mutexSubscribe( s_hmtxRunningThreads, THREAD_MAX_WAIT, @hSocket )

      // received a -1 value, I have to quit
      IF HB_ISNUMERIC( hSocket )
         lQuitRequest := .T.
         EXIT

      ELSEIF hSocket == NIL   // no socket received, thread can graceful quit, but ...
#ifndef FIXED_THREADS
         IF hb_mutexLock( s_hmtxBusy )
            // .. not if under minimal number of starting threads
            IF s_nThreads <= s_nStartThreads
               hb_mutexUnlock( s_hmtxBusy )
               LOOP
            ENDIF
            hb_mutexUnlock( s_hmtxBusy )
         ENDIF
         EXIT
#else  // FIXED_THREADS
         LOOP
#endif // FIXED_THREADS
      ENDIF

      // Connection accepted
      IF hb_mutexLock( s_hmtxBusy )
         s_nConnections++
         s_nTotConnections++
         s_nMaxConnections := Max( s_nConnections, s_nMaxConnections )
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      // Save initial time
      nMsecs := hb_MilliSeconds()

      BEGIN SEQUENCE

         cRequest := NIL

         /* receive query */
         nLen := readRequest( hSocket, @cRequest )

#ifdef DEBUG_ACTIVE
         hb_ToOutDebug( "cRequest -- BEGIN --\n\r%s\n\rcRequest -- END --\n\r", cRequest )
#endif

         IF nLen == -1
            ? "recv() error:", hb_socketGetError()

         ELSEIF nLen == 0 /* connection closed */
         ELSE

            // hb_ToOutDebug( "cRequest -- BEGIN --\n\r%s\n\rcRequest -- END --\n\r", cRequest )

            _SERVER := HB_HASHI(); _GET := HB_HASHI(); _POST := HB_HASHI(); _COOKIE := HB_HASHI()
            _SESSION := HB_HASHI(); _REQUEST := HB_HASHI(); _HTTP_REQUEST := HB_HASHI(); _HTTP_RESPONSE := HB_HASHI()
            m_cPost := NIL
            t_cResult     := ""
            // t_aHeader     := {}
            t_nStatusCode := 200
            t_cErrorMsg   := ""

            defineServer( hSocket )

            IF ParseRequest( cRequest )
               // hb_ToOutDebug( "_SERVER = %s,\n\r _GET = %s,\n\r _POST = %s,\n\r _REQUEST = %s,\n\r _HTTP_REQUEST = %s,\n\r _HTTP_RESPONSE = %s\n\r", hb_ValToExp( _SERVER ), hb_ValToExp( _GET ), hb_ValToExp( _POST ), hb_ValToExp( _REQUEST ), hb_ValToExp( _HTTP_REQUEST ), hb_ValToExp( _HTTP_RESPONSE ) )
               cSend := uproc_default()
            ELSE
               // uhttpd_SetStatusCode( 400 )
               cSend := MakeResponse()
            ENDIF

#ifdef DEBUG_ACTIVE
            hb_ToOutDebug( "cSend = %s\n\r", cSend )
#endif

            sendReply( hSocket, cSend )

            WriteToLog( cRequest )

            // Destroy PRIVATE VARIABLES
            _SERVER := _GET := _POST := _COOKIE := _SESSION := _REQUEST := _HTTP_REQUEST := _HTTP_RESPONSE := m_cPost := NIL

         ENDIF

      END SEQUENCE

      nParseTime := hb_MilliSeconds() - nMsecs
      WriteToConsole( "Page served in : " + Str( nParseTime / 1000, 7, 4 ) + " seconds" )

      hb_socketShutdown( hSocket )
      hb_socketClose( hSocket )

      IF hb_mutexLock( s_hmtxBusy )
         s_nConnections--
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      // Memory release
      hb_gcAll( .T. )

   ENDDO

   WriteToConsole( "Quitting ProcessConnections() " + hb_CStr( nThreadId ) )

   // Here I remove this thread from thread queue as it is unnecessary, but only if there is not
   // an external quit request. In this case application is quitting and I cannot resize array
   // here to avoid race condition
   IF ! lQuitRequest .AND. hb_mutexLock( s_hmtxBusy )
      // hb_ToOutDebug( "Len( s_aRunningThreads ) = %i\n\r", Len( s_aRunningThreads ) )
      IF ( nPos := AScan( s_aRunningThreads, hb_threadSelf() ) > 0 )
         hb_ADel( s_aRunningThreads, nPos, .T. )
         s_nThreads := Len( s_aRunningThreads )
      ENDIF
      hb_mutexUnlock( s_hmtxBusy )
   ENDIF

   RETURN 0

STATIC FUNCTION ServiceConnection()

   LOCAL hSocket, nLen, cRequest, cSend
   LOCAL nMsecs, nParseTime, nPos, nThreadId
   LOCAL nError := 500013
   LOCAL lQuitRequest := .F.

   PRIVATE _SERVER, _GET, _POST, _COOKIE, _SESSION, _REQUEST, _HTTP_REQUEST, _HTTP_RESPONSE, m_cPost

   ErrorBlock( {| oError | uhttpd_DefError( oError ) } )

   nThreadId := hb_threadID()

   WriteToConsole( "Starting ServiceConnections() " + hb_CStr( nThreadId ) )

   IF hb_mutexLock( s_hmtxBusy )
      s_nServiceThreads++
      hb_mutexUnlock( s_hmtxBusy )
   ENDIF

   DO WHILE .T.

      // Reset socket
      hSocket := NIL

#ifdef __PLATFORM__WINDOWS
      // releasing resources
      IF win_SysRefresh( 1 ) != 0
         lQuitRequest := .T.
         EXIT
      ENDIF
#endif

      IF hb_mutexLock( s_hmtxBusy )
         IF s_lQuitRequest
            hb_mutexUnlock( s_hmtxBusy )
            lQuitRequest := .T.
            EXIT
         ENDIF
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      // Waiting a connection from AcceptConnections() but up to defined time
      hb_mutexSubscribe( s_hmtxServiceThreads, THREAD_MAX_WAIT, @hSocket )

      // received a -1 value, I have to quit
      IF HB_ISNUMERIC( hSocket )
         lQuitRequest := .T.
         EXIT
      ELSEIF hSocket == NIL   // no socket received, thread can graceful quit, but ...
         IF hb_mutexLock( s_hmtxBusy )
            // .. not if under minimal number of starting threads
            IF s_nServiceThreads <= s_nStartServiceThreads
               hb_mutexUnlock( s_hmtxBusy )
               LOOP
            ENDIF
            hb_mutexUnlock( s_hmtxBusy )
         ENDIF
         EXIT
      ENDIF

      // Connection accepted
      IF hb_mutexLock( s_hmtxBusy )
         s_nServiceConnections++
         s_nTotServiceConnections++
         s_nMaxServiceConnections := Max( s_nServiceConnections, s_nMaxServiceConnections )
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      // Save initial time
      nMsecs := hb_MilliSeconds()

      BEGIN SEQUENCE

         /* receive query */
         nLen := readRequest( hSocket, @cRequest )

         IF nLen == -1
            ? "recv() error:", hb_socketGetError()
         ELSEIF nLen == 0 /* connection closed */
         ELSE

            // hb_ToOutDebug( "cRequest -- INIZIO --\n\r%s\n\rcRequest -- FINE --\n\r", cRequest )

            _SERVER := HB_HASHI(); _GET := HB_HASHI(); _POST := HB_HASHI(); _COOKIE := HB_HASHI()
            _SESSION := HB_HASHI(); _REQUEST := HB_HASHI(); _HTTP_REQUEST := HB_HASHI(); _HTTP_RESPONSE := HB_HASHI()
            m_cPost := NIL
            t_cResult     := ""
            // t_aHeader     := {}
            t_nStatusCode := 200
            t_cErrorMsg   := ""

            defineServer( hSocket )

            IF ParseRequest( cRequest )
               // hb_ToOutDebug( "_SERVER = %s,\n\r _GET = %s,\n\r _POST = %s,\n\r _REQUEST = %s,\n\r _HTTP_REQUEST = %s,\n\r _HTTP_RESPONSE = %s\n\r", hb_ValToExp( _SERVER ), hb_ValToExp( _GET ), hb_ValToExp( _POST ), hb_ValToExp( _REQUEST ), hb_ValToExp( _HTTP_REQUEST ), hb_ValToExp( _HTTP_RESPONSE ) )
               define_Env( _SERVER )
            ENDIF
            // Error page served
            uhttpd_SetStatusCode( nError )
            cSend := MakeResponse()

            sendReply( hSocket, cSend )

            WriteToLog( cRequest )

            // Destroy PRIVATE VARIABLES
            _SERVER := _GET := _POST := _COOKIE := _SESSION := _REQUEST := _HTTP_REQUEST := _HTTP_RESPONSE := m_cPost := NIL

         ENDIF

      END SEQUENCE

      nParseTime := hb_MilliSeconds() - nMsecs
      WriteToConsole( "Page served in : " + Str( nParseTime / 1000, 7, 4 ) + " seconds" )

      hb_socketShutdown( hSocket )
      hb_socketClose( hSocket )

      IF hb_mutexLock( s_hmtxBusy )
         s_nServiceConnections--
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF

      // Memory release
      hb_gcAll( .T. )

   ENDDO

   WriteToConsole( "Quitting ServiceConnections() " + hb_CStr( nThreadId ) )

   // Here I remove this thread from thread queue as it is unnecessary, but only if there is not
   // an external quit request. In this case application is quitting and I cannot resize array
   // here to avoid race condition
   IF ! lQuitRequest .AND. hb_mutexLock( s_hmtxBusy )
      IF ( nPos := AScan( s_aServiceThreads, hb_threadSelf() ) > 0 )
         hb_ADel( s_aServiceThreads, nPos, .T. )
         s_nServiceThreads := Len( s_aServiceThreads )
      ENDIF
      hb_mutexUnlock( s_hmtxBusy )
   ENDIF

   RETURN 0

STATIC FUNCTION ParseRequest( cRequest )

   LOCAL aRequest, aLine, nI, nJ, cI
   LOCAL cReq, aVal, cFields, hVars
   LOCAL hUrl

   // RFC2616
   aRequest := uhttpd_split( CR_LF, cRequest )

#ifdef DEBUG_ACTIVE
   hb_ToOutDebug( "aRequest = %s\n\r", hb_ValToExp( aRequest ) )
#endif

   WriteToConsole( aRequest[ 1 ] )
   aLine := uhttpd_split( " ", aRequest[ 1 ] )
   IF Len( aLine ) != 3 .OR. ;
      ( !( Left( aLine[ 1 ], 3 ) == "GET" ) .AND. ;
        !( Left( aLine[ 1 ], 4 ) == "POST" ) ) .OR. ; // Sorry, we support GET and POST only
        !( Left( aLine[ 3 ], 5 ) == "HTTP/" )
      // Set status code
      t_nStatusCode := 501 // Not Implemented
      RETURN .F.
   ENDIF

   // define _SERVER var
   _SERVER[ "REQUEST_METHOD"  ] := aLine[ 1 ]
   _SERVER[ "REQUEST_URI"     ] := aLine[ 2 ]
   _SERVER[ "SERVER_PROTOCOL" ] := aLine[ 3 ]

   hUrl := uhttpd_SplitUrl( _SERVER[ "REQUEST_URI" ] )

   _SERVER[ "SCRIPT_NAME"  ] := hUrl[ "URI" ]
   _SERVER[ "QUERY_STRING" ] := hUrl[ "QUERY" ]

#if 0
   IF ( nI := At( "?", _SERVER[ "REQUEST_URI" ] ) ) > 0
      _SERVER[ "SCRIPT_NAME"  ] := Left( _SERVER[ "REQUEST_URI" ], nI - 1 )
      _SERVER[ "QUERY_STRING" ] := SubStr( _SERVER[ "REQUEST_URI" ], nI + 1 )
   ELSE
      _SERVER[ "SCRIPT_NAME"  ] := _SERVER[ "REQUEST_URI" ]
      _SERVER[ "QUERY_STRING" ] := ""
   ENDIF
#endif

   FOR nI := 2 TO Len( aRequest )
      IF aRequest[ nI ] == "";  EXIT
      ELSEIF ( nJ := At( ":", aRequest[ nI ] ) ) > 0
         cI := LTrim( SubStr( aRequest[ nI ], nJ + 1 ) )
         SWITCH Upper( Left( aRequest[ nI ], nJ - 1 ) )
         CASE "ACCEPT"
         CASE "ACCEPT-CHARSET"
         CASE "ACCEPT-ENCODING"
         CASE "ACCEPT-LANGUAGE"
         CASE "CACHE-CONTROL"
         CASE "CONNECTION"
         CASE "COOKIE"
         CASE "KEEP-ALIVE"
         CASE "REFERER"
         CASE "USER-AGENT"
            _SERVER[ "HTTP_" + StrTran( Upper( Left( aRequest[ nI ], nJ - 1 ) ), "-", "_" ) ] := cI
            EXIT
         CASE "HOST"
            // aVal := uhttpd_split( ":", aRequest[ nI ] )
            // _SERVER[ "HTTP_" + StrTran( Upper( aVal[ 1 ] ), "-", "_")] := AllTrim( aVal[ 2 ] )
            _SERVER[ "HTTP_" + StrTran( Upper( Left( aRequest[ nI ], nJ - 1 ) ), "-", "_" ) ] := cI
            EXIT
         CASE "CONTENT-TYPE"
         CASE "CONTENT-LENGTH"
            _SERVER[ StrTran( Upper( Left( aRequest[ nI ], nJ - 1 ) ), "-", "_" ) ] := cI
            EXIT
         ENDSWITCH
      ENDIF
   NEXT

   // Load _HTTP_REQUEST
   FOR EACH cReq IN aRequest
      IF cReq:__enumIndex() == 1 // GET request
         _HTTP_REQUEST[ "HTTP Request" ] := cReq
      ELSEIF Empty( cReq )
         EXIT
      ELSE
         aVal := uhttpd_split( ":", cReq, 1 )
         _HTTP_REQUEST[ aVal[ 1 ] ] := iif( Len( aVal ) == 2, AllTrim( aVal[ 2 ] ), NIL )
      ENDIF
   NEXT

   // check if Host field is provided
   IF hb_HPos( _HTTP_REQUEST, "Host" ) == 0

      // Try to determine Host name
      IF ! Empty( hUrl[ "HOST" ] )
         _HTTP_REQUEST[ "Host" ] := hUrl[ "HOST" ]
      ELSE
         _HTTP_REQUEST[ "Host" ] := ""
         // Set status code
         t_nStatusCode := 400 // Bad Request
         RETURN .F.
      ENDIF

   ENDIF

   // hb_ToOutDebug( "_HTTP_REQUEST: aRequest = %s, _HTTP_REQUEST = %s\n\r", hb_ValToExp( aRequest ), hb_ValToExp( _HTTP_REQUEST ) )

   // GET
   cFields := _SERVER[ "QUERY_STRING" ]
   IF ! Empty( cFields )
      hVars := uhttpd_GetVars( cFields )
      hb_HMerge( _GET, hVars )
      hb_HMerge( _REQUEST, hVars )
   ENDIF

   // hb_ToOutDebug( "GET: cFields = %s, hVars = %s, _GET = %s, _REQUEST = %s\n\r", cFields, hb_ValToExp( hVars ), hb_ValToExp( _GET ), hb_ValToExp( _REQUEST ) )

   // POST
   IF "POST" $ Upper( _SERVER[ "REQUEST_METHOD" ] )
      cFields := ATail( aRequest )
      IF ! Empty( cFields )
         hVars := uhttpd_GetVars( cFields )
         hb_HMerge( _POST, hVars )
         hb_HMerge( _REQUEST, hVars )
      ENDIF
      m_cPost := cFields  // TOFIX: Who needs this ?
   ENDIF

   // hb_ToOutDebug( "POST: cFields = %s, hVars = %s, _POST = %s, _REQUEST = %s\n\r", cFields, hb_ValToExp( hVars ), hb_ValToExp( _POST ), hb_ValToExp( _REQUEST ) )

   // COOKIES
   cFields := _SERVER[ "HTTP_COOKIE" ]
   IF ! Empty( cFields )
      hVars := uhttpd_GetVars( cFields, ";" )
      hb_HMerge( _COOKIE, hVars )
      hb_HMerge( _REQUEST, hVars )
   ENDIF
   // hb_ToOutDebug( "COOKIE: cFields = %s, hVars = %s, _COOKIE = %s, _REQUEST = %s\n\r", cFields, hb_ValToExp( hVars ), hb_ValToExp( _COOKIE ), hb_ValToExp( _REQUEST ) )


   // define _HTTP_RESPONSE
   _HTTP_RESPONSE[ "X-Powered-By"      ] := Version()
   _HTTP_RESPONSE[ "Connection"        ] := "Close"
   _HTTP_RESPONSE[ "Content-Type"      ] := "text/html; charset=UTF-8"
   _HTTP_RESPONSE[ "Server"            ] := APP_NAME + " " + APP_VERSION
   // _HTTP_RESPONSE[ "Transfer-Encoding" ] := "chunked"

   // Complete _SERVER
   _SERVER[ "SERVER_NAME"       ] := uhttpd_split( ":", _HTTP_REQUEST[ "HOST" ], 1 )[ 1 ]
   _SERVER[ "SCRIPT_FILENAME"   ] := StrTran( StrTran( _SERVER[ "DOCUMENT_ROOT" ] + _SERVER[ "SCRIPT_NAME" ], "//", "/" ), "\", "/" )
   _SERVER[ "SCRIPT_URL"        ] := _SERVER[ "SCRIPT_NAME" ]
   _SERVER[ "SCRIPT_URI"        ] := "http://" + _HTTP_REQUEST[ "HOST" ] + _SERVER[ "SCRIPT_NAME" ]

#ifdef DEBUG_ACTIVE
   hb_ToOutDebug( "_SERVER = %s\n\r", hb_ValToExp( _SERVER ) )
   hb_ToOutDebug( "_GET = %s\n\r", hb_ValToExp( _GET ) )
   hb_ToOutDebug( "_POST = %s\n\r", hb_ValToExp( _POST ) )
   hb_ToOutDebug( "_COOKIE = %s\n\r", hb_ValToExp( _COOKIE ) )
   hb_ToOutDebug( "_SESSION = %s\n\r", hb_ValToExp( _SESSION ) )
   hb_ToOutDebug( "_HTTP_REQUEST = %s\n\r", hb_ValToExp( _HTTP_REQUEST ) )
   hb_ToOutDebug( "_HTTP_RESPONSE = %s\n\r", hb_ValToExp( _HTTP_RESPONSE ) )
#endif

   // After defined all SERVER vars we can define a session
   // SESSION - sessions ID is stored as a cookie value, normally as SESSIONID var name (this can be user defined)
   t_oSession := uhttpd_SessionNew( "UHTTPD-SESSION", s_cSessionPath )
   t_oSession:Start()

   RETURN .T.


STATIC FUNCTION MakeResponse()

   LOCAL cRet, cReturnCode, v

   // uhttpd_SetHeader( "X-Powered-By", Version() )

   // uhttpd_SetHeader( "Connection", "close" )

   IF uhttpd_GetHeader( "Location" ) != NIL
      t_nStatusCode := 301
   ENDIF
   IF uhttpd_GetHeader( "Content-Type" ) == NIL
      uhttpd_SetHeader( "Content-Type", "text/html" )
   ENDIF

   cRet := "HTTP/1.1 "
   cReturnCode := DecodeStatusCode()

   SWITCH t_nStatusCode
   CASE 200
      EXIT

   CASE 301
   CASE 400
   CASE 401
   CASE 402
   CASE 403
   CASE 404
   CASE 405
   CASE 500
   CASE 501
   CASE 502
   CASE 503
   CASE 504
   CASE 505
      t_cResult := "<html><body><h1>" + cReturnCode + "</h1></body></html>"
      EXIT

      // extended error messages - from Microsoft IIS Server
   CASE 500013 // error: 500-13 Server too busy
      uhttpd_SetHeader( "Retry-After", "60" )  // retry after 60 seconds
      t_cResult := "<html><body><h1>500 Server Too Busy</h1></body></html>"
      EXIT

   CASE 500100 // error: 500-100 Undeclared Variable

   OTHERWISE
      cReturnCode := "403 Forbidden"
      t_cResult := "<html><body><h1>" + cReturnCode + "</h1></body></html>"
   ENDSWITCH

   // hb_ToOutDebug( "_SESSION = %s\n\r", hb_ValToExp( _SESSION ) )

   // Close session - Autodestructor will NOT close it, because t_oSession is destroyed only at end of Thread

   IF HB_ISOBJECT( t_oSession )
      t_oSession:Close()
   ENDIF
   // t_oSession := NIL

   WriteToConsole( cReturnCode )
   cRet += cReturnCode + CR_LF

   FOR EACH v IN _HTTP_RESPONSE
      cRet += v:__enumKey() + ": " + v + CR_LF
   NEXT

   // AEval( t_aHeader, {| x | cRet += x[1] + ": " + x[2] + CR_LF } )
   cRet += CR_LF
   cRet += t_cResult

   // hb_ToOutDebug( "_HTTP_RESPONSE = %s\n\rcRet = %s\n\r", hb_ValToExp( _HTTP_RESPONSE ), cRet )

   RETURN cRet

STATIC FUNCTION DecodeStatusCode()

   LOCAL cReturnCode

   SWITCH t_nStatusCode
   CASE 200
      cReturnCode := "200 OK"
      EXIT
   CASE 301
      cReturnCode := "301 Moved Permanently"
      EXIT
   CASE 400
      cReturnCode := "400 Bad Request"
      EXIT
   CASE 401
      cReturnCode := "401 Unauthorized"
      EXIT
   CASE 402
      cReturnCode := "402 Payment Required"
      EXIT
   CASE 403
      cReturnCode := "403 Forbidden"
      EXIT
   CASE 404
      cReturnCode := "404 Not Found"
      EXIT
   CASE 405
      cReturnCode := "405 Method Not Allowed"
      EXIT
   CASE 500
      cReturnCode := "500 Internal Server Error"
      EXIT
   CASE 501
      cReturnCode := "501 Not Implemented"
      EXIT
   CASE 502
      cReturnCode := "502 Bad Gateway"
      EXIT
   CASE 503
      cReturnCode := "503 Service Unavailable"
      EXIT
   CASE 504
      cReturnCode := "504 Gateway Timeout"
      EXIT
   CASE 505
      cReturnCode := "505 HTTP Version Not Supported"
      EXIT

      // extended error messages - from Microsoft IIS Server
   CASE 500013 // error: 500-13 Server too busy
      cReturnCode := "500-13 Server Too Busy"
      EXIT

   CASE 500100 // error: 500-100 Undeclared Variable

   OTHERWISE
      cReturnCode := "403 Forbidden"
   ENDSWITCH

   RETURN cReturnCode

STATIC PROCEDURE WriteToLog( cRequest )

   LOCAL cTime, cDate
   LOCAL aDays   := { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }
   LOCAL aMonths := { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }
   LOCAL cAccess, cError, nDoW, dDate, nDay, nMonth, nYear, nSize, cBias
   LOCAL cErrorMsg
   LOCAL cReferer

   IF hb_mutexLock( s_hmtxLog )

      // hb_ToOutDebug( "tip_TimeStamp() = %s \n\r", tip_TimeStamp() )

      cTime    := Time()
      dDate    := Date()
      cDate    := DToS( dDate )
      nSize    := Len( t_cResult )
      cReferer := _SERVER[ "HTTP_REFERER" ]
      cBias    := uhttpd_UTCOffset()

      cAccess := _SERVER[ "REMOTE_ADDR" ] + " - - [" + Right( cDate, 2 ) + "/" + ;
         aMonths[ Val( SubStr( cDate, 5, 2 ) ) ] + ;
         "/" + Left( cDate, 4 ) + ":" + cTime + " " + cBias + '] "' + ;
         Left( cRequest, At( CR_LF, cRequest ) - 1 ) + '" ' + ;
         hb_ntos( t_nStatusCode ) + " " + iif( nSize == 0, "-", hb_ntos( nSize ) ) + ;
         ' "' + iif( Empty( cReferer ), "-", cReferer ) + '" "' + _SERVER[ "HTTP_USER_AGENT" ] + ;
         '"' + hb_eol()

      // hb_ToOutDebug( "AccessLog = %s \n\r", cAccess )

      FWrite( s_hfileLogAccess, cAccess )

      IF !( t_nStatusCode == 200 ) // ok

         nDoW   := DoW( dDate )
         nDay   := Day( dDate )
         nMonth := Month( dDate )
         nYear  := Year( dDate )
         cErrorMsg := t_cErrorMsg

         cError := "[" + Left( aDays[ nDoW ], 3 ) + " " + aMonths[ nMonth ] + " " + StrZero( nDay, 2 ) + " " + ;
            PadL( LTrim( cTime ), 8, "0" ) + " " + StrZero( nYear, 4 ) + "] [error] [client " + _SERVER[ "REMOTE_ADDR" ] + "] " + ;
            cErrorMsg + hb_eol()

         // hb_ToOutDebug( "ErrorLog = %s \n\r", cError )

         FWrite( s_hfileLogError, cError )
      ENDIF

      hb_mutexUnlock( s_hmtxLog )
   ENDIF

   RETURN

STATIC FUNCTION CGIExec( cProc, /*@*/ cOutPut )

   LOCAL hIn, hOut
   LOCAL cData, nLen, cSend, v
   LOCAL nErrorLevel := 0, nKillExit := 0
   LOCAL pThread
   LOCAL hProc
   LOCAL hmtxCGIKill := hb_mutexCreate()
   LOCAL cCurPath

   // LOCAL cError


   IF HB_ISSTRING( cProc )

      // hb_ToOutDebug( "Launching process: %s\n\r", cProc )
      // No hIn, hErr == hOut

      // save current directory
      cCurPath := hb_CurDrive() + hb_osDriveSeparator() + hb_ps() + CurDir()

      // hb_ToOutDebug( "cCurPath: %s\n\r", cCurPath )

      // Change dir to document root
      DirChange( s_cDocumentRoot )

      // hb_ToOutDebug( "New Path: %s\n\r", hb_CurDrive() + hb_osDriveSeparator() + hb_ps() + CurDir() )

      hProc := hb_processOpen( cProc, @hIn, @hOut, @hOut, .T. ) // .T. = Detached Process (Hide Window)

      // return to original folder
      DirChange( cCurPath )

      // hb_ToOutDebug( "New 2 Path: %s\n\r", hb_CurDrive() + hb_osDriveSeparator() + hb_ps() + CurDir() )

      IF hProc != F_ERROR
         // hb_ToOutDebug( "Process handler: %s\n\r", hProc )
         // hb_ToOutDebug( "Error: %s\n\r", FError() )

         pThread := hb_threadStart( @CGIKill(), hProc, hmtxCGIKill )

         // Sending POST variables to CGI via STD_IN
         cSend := ""
         FOR EACH v IN _POST
            cSend += v:__enumKey() + "=" + LTrim( hb_CStr( v ) ) + iif( v:__enumIndex() < Len( _POST ), "&", "" )
         NEXT
         FWrite( hIn, cSend )
         // hb_ToOutDebug( "Sending: %s\n\r", cSend )

         hb_mutexNotify( hmtxCGIKill, { hProc, .T. } )

         // hb_ToOutDebug( "Reading output\n\r" )
         cData := Space( 1000 )
         cOutPut := ""
         DO WHILE ( nLen := FRead( hOut, @cData, hb_BLen( cData ) ) ) > 0
            cOutPut += hb_BLeft( cData, nLen )
            cData := Space( 1000 )
         ENDDO

         #if 0
         cData := Space( 1000 )
         cError := ""
         DO WHILE ( nLen := FRead( hErr, @cData, hb_BLen( cData ) ) ) > 0
            cError += hb_BLeft( cData, nLen )
            cData := Space( 1000 )
         ENDDO

         cOutPut += cError
         #endif

         // hb_ToOutDebug( "Received: cOutPut = %s\n\r", cOutPut )

         // ? "Waiting for process termination"
         // Return value
         nErrorLevel := hb_processValue( hProc )

         // hb_ToOutDebug( "CGIExec HB_ProcessValue nErrorLevel = %s\n\r", nErrorLevel )

         // Notify to CGIKill to terminate
         hb_mutexNotify( hmtxCGIKill, { hProc, .F. } )
         hb_threadJoin( pThread, @nKillExit )

         // hb_ToOutDebug( "CGIExec quitting CGI, nErrorLevel = %s\n\r", nKillExit )
         IF nKillExit != 0
            // retrieving last command from
            nErrorLevel := nKillExit
         ENDIF

         FClose( hIn )
         FClose( hOut )
         // FClose( hErr )

         // hb_ToOutDebug( "CGIExec closed handles\n\r" )

      ENDIF

   ELSE

      nErrorLevel := -1 // Error: cProc is not a valid string

   ENDIF

   hmtxCGIKill := NIL

   RETURN nErrorLevel

STATIC FUNCTION CGIKill( hProc, hmtxCGIKill )

   LOCAL lWait
   LOCAL nStartTime := hb_MilliSeconds()
   LOCAL nErrorLevel := 0
   LOCAL aValue, hRecProc
   LOCAL hCurProc := hProc

   // hb_ToOutDebug( "CGIKill() Started. nStartTime = %s\n\r", nStartTime )

   // Kill process after MAX_PROCESS_EXEC_TIME
   DO WHILE .T.

      aValue := NIL
      lWait := NIL

      hb_mutexSubscribe( hmtxCGIKill, 1, @aValue ) // 10 seconds

      IF HB_ISARRAY( aValue )
         hRecProc := aValue[ 1 ]
         lWait    := aValue[ 2 ]
         // if Process requested is different from this, sending request again in the queue
         IF !( hRecProc == hCurProc )
            lWait := NIL
         ENDIF
      ENDIF

      // hb_ToOutDebug( "CGIKill() lWait = %s, time := %s\n\r", lWait, hb_MilliSeconds() - nStartTime )

      IF HB_ISLOGICAL( lWait )
         IF lWait
            nStartTime := hb_MilliSeconds()
         ELSE
            EXIT
         ENDIF
      ENDIF

      IF ( hb_MilliSeconds() - nStartTime ) > CGI_MAX_EXEC_TIME * 1000

         // hb_ToOutDebug( "CGIKill() Killing Process hCurProc = %s\n\r", hCurProc )

         // Killing process if still exists
         IF hCurProc != NIL
            hb_processClose( hCurProc )
            nErrorLevel := 1
         ENDIF
         EXIT
      ENDIF
   ENDDO

   RETURN nErrorLevel


/********************************************************************
  Public helper functions
********************************************************************/

FUNCTION uhttpd_OSFileName( cFileName )

   IF hb_ps() == "/"
      RETURN cFileName
   ENDIF

   RETURN StrTran( cFileName, "/", hb_ps() )

PROCEDURE uhttpd_SetStatusCode( nStatusCode )

   t_nStatusCode := nStatusCode

   RETURN


PROCEDURE uhttpd_SetHeader( cType, cValue )

   // LOCAL nI
   // // Needed from SetCookie()
   // __defaultNIL( @lReplace, .T. )

   _HTTP_RESPONSE[ cType ] := cValue

#if 0
   IF lReplace .AND. ( nI := AScan( t_aHeader, {| x | Upper( x[ 1 ] ) == Upper( cType ) } ) ) > 0
      t_aHeader[ nI, 2 ] := cValue
   ELSE
      AAdd( t_aHeader, { cType, cValue } )
   ENDIF
#endif

   RETURN


FUNCTION uhttpd_GetHeader( cType )
   RETURN uhttpd_HGetValue( _HTTP_RESPONSE, cType )

PROCEDURE uhttpd_DelHeader( cType )

   LOCAL nPos := hb_HPos( _HTTP_RESPONSE, cType )

   IF nPos > 0
      hb_HDelAt( _HTTP_RESPONSE, nPos )
   ENDIF

   RETURN

PROCEDURE uhttpd_Write( cString )

   t_cResult += cString

   RETURN

/********************************************************************
  Internal helper functions
********************************************************************/

STATIC FUNCTION readRequest( hSocket, /* @ */ cRequest )

   LOCAL cBuf, nLen, nPos

   /* receive query */
   cRequest := ""
   DO WHILE .T.
      cBuf := Space( 4096 )
      nLen := hb_socketRecv( hSocket, @cBuf )
      IF nLen <= 0
         EXIT
      ENDIF
      cRequest += Left( cBuf, nLen )
      IF CR_LF + CR_LF $ cRequest
         EXIT
      ENDIF
   ENDDO

   /* receive CONTENT-LENGTH data */
   IF nLen > 0
      nPos := hb_AtI( CR_LF + "CONTENT-LENGTH:", cRequest )
      IF nPos > 0
         nPos := Val( SubStr( cRequest, nPos + 17, 10 ) )
         IF nPos > 0
            /* we have to decrease number of bytes to read by already read
             * data after CR_LF + CR_LF
             */
            nPos -= Len( cRequest ) - At( CR_LF + CR_LF, cRequest ) - 3
            WHILE nPos > 0
               cBuf := Space( nPos )
               nLen := hb_socketRecv( hSocket, @cBuf, nPos )
               IF nLen <= 0
                  EXIT
               ENDIF
               cRequest += Left( cBuf, nPos )
               nPos -= nLen
            ENDDO
         ENDIF
      ENDIF
   ENDIF

#ifdef DEBUG_ACTIVE
   hb_ToOutDebug( "readRequest(): nLen = %i, cRequest = %s \n\r", nLen, cRequest )
#endif

   RETURN nLen

STATIC FUNCTION sendReply( hSocket, cSend )

   LOCAL nError := 0
   LOCAL nLen

   DO WHILE Len( cSend ) > 0
      IF ( nLen := hb_socketSend( hSocket, cSend ) ) == -1
         ? "send() error:", hb_socketGetError()
         WriteToConsole( hb_StrFormat( "ServiceConnection() - send() error: %s, cSend = %s, hSocket = %s", hb_socketGetError(), cSend, hSocket ) )
         EXIT
      ELSEIF nLen > 0
         cSend := SubStr( cSend, nLen + 1 )
      ENDIF
   ENDDO

   RETURN nError

STATIC PROCEDURE defineServer( hSocket )

   LOCAL aI

   // define _SERVER vars (address part)
   IF ! Empty( aI := hb_socketGetPeerName( hSocket ) )
      _SERVER[ "REMOTE_ADDR" ] := aI[ HB_SOCKET_ADINFO_ADDRESS ]
      _SERVER[ "REMOTE_HOST" ] := _SERVER[ "REMOTE_ADDR" ]  // no reverse DNS
      _SERVER[ "REMOTE_PORT" ] := aI[ HB_SOCKET_ADINFO_PORT ]
   ENDIF

   IF ! Empty( aI := hb_socketGetSockName( hSocket ) )
      _SERVER[ "SERVER_ADDR" ] := aI[ HB_SOCKET_ADINFO_ADDRESS ]
      _SERVER[ "SERVER_PORT" ] := hb_ntos( aI[ HB_SOCKET_ADINFO_PORT ] )
   ENDIF

   // add other _SERVER vars
   _SERVER[ "REQUEST_METHOD"       ] := NIL
   _SERVER[ "REQUEST_URI"          ] := NIL
   _SERVER[ "SERVER_PROTOCOL"      ] := NIL
   _SERVER[ "SCRIPT_NAME"          ] := NIL
   _SERVER[ "QUERY_STRING"         ] := NIL
   _SERVER[ "HTTP_ACCEPT"          ] := NIL
   _SERVER[ "HTTP_ACCEPT_CHARSET"  ] := NIL
   _SERVER[ "HTTP_ACCEPT_ENCODING" ] := NIL
   _SERVER[ "HTTP_ACCEPT_LANGUAGE" ] := NIL
   _SERVER[ "HTTP_CONNECTION"      ] := NIL
   _SERVER[ "HTTP_HOST"            ] := NIL
   _SERVER[ "HTTP_KEEP_ALIVE"      ] := NIL
   _SERVER[ "HTTP_REFERER"         ] := ""
   _SERVER[ "HTTP_USER_AGENT"      ] := ""
   _SERVER[ "HTTP_CACHE_CONTROL"   ] := NIL
   _SERVER[ "HTTP_COOKIE"          ] := NIL
   _SERVER[ "SERVER_NAME"          ] := ""
   _SERVER[ "SERVER_SOFTWARE"      ] := APP_NAME + " " + APP_VERSION + " (" + OS() + ")"
   _SERVER[ "SERVER_SIGNATURE"     ] := "<address>" + _SERVER[ "SERVER_SOFTWARE" ] + " Server at " + _SERVER[ "SERVER_NAME" ] + " Port " + _SERVER[ "SERVER_PORT" ] + "</address>"
   _SERVER[ "DOCUMENT_ROOT"        ] := s_cDocumentRoot
   _SERVER[ "SERVER_ADMIN"         ] := hb_UserName() + "@" + NetName()
   _SERVER[ "SCRIPT_FILENAME"      ] := NIL
   _SERVER[ "GATEWAY_INTERFACE"    ] := "CGI/1.1"
   _SERVER[ "SCRIPT_URL"           ] := NIL
   _SERVER[ "SCRIPT_URI"           ] := NIL
   _SERVER[ "PATH_INFO"            ] := NIL
   _SERVER[ "PATH_TRANSLATED"      ] := NIL

   RETURN

FUNCTION uhttpd_split( cSeparator, cString, nMax )

   LOCAL aRet := {}, nI
   LOCAL nIter := 0

   __defaultNIL( @nMax, 0 )

   DO WHILE ( nI := At( cSeparator, cString ) ) > 0
      AAdd( aRet, Left( cString, nI - 1 ) )
      cString := SubStr( cString, nI + Len( cSeparator ) )
      IF nMax > 0 .AND. ++nIter >= nMax
         EXIT
      ENDIF
   ENDDO
   AAdd( aRet, cString )

   RETURN aRet

FUNCTION uhttpd_join( cSeparator, aData )

   LOCAL cRet := "", nI

   FOR nI := 1 TO Len( aData )
      IF nI > 1
         cRet += cSeparator
      ENDIF
      SWITCH ValType( aData[ nI ] )
      CASE "C"
      CASE "M" ; cRet += aData[ nI ]; EXIT
      CASE "N" ; cRet += hb_ntos( aData[ nI ] ); EXIT
      CASE "D" ; cRet += iif( ! Empty( aData[ nI ] ), DToC( aData[ nI ] ), "" ); EXIT
      ENDSWITCH
   NEXT

   RETURN cRet

STATIC FUNCTION uproc_default()

   LOCAL cScript
   LOCAL cFileName, nI
   LOCAL cExt, cHandler, xAction, nPos
   LOCAL cBaseFile
   LOCAL cPathInfo, lFound, cFile

   // Starting from Script Name request
   cScript := _SERVER[ "SCRIPT_NAME" ]

   // cFileName := StrTran(cRoot + _SERVER["SCRIPT_NAME"], "//", "/")
   cFileName := NIL
   cPathInfo := ""

   DO WHILE .T.

#ifdef DEBUG_ACTIVE
      // hb_ToOutDebug( "cFileName = %s, cScript = %s\n\r", cFileName, cScript )
#endif

      IF cFileName == NIL

         // Special script names
         IF Upper( cScript ) == "/SERVERSTATUS"
            cFileName := "/serverstatus"
            cExt      := "/serverstatus" // special extension
         ENDIF

      ENDIF

      IF cFileName == NIL

         cFileName := FileUnAlias( cScript )

      ENDIF

      // if filename is still NIL I set it
      IF cFileName == NIL
         cFileName := _SERVER[ "SCRIPT_FILENAME" ]
      ENDIF

#ifdef DEBUG_ACTIVE
      // hb_ToOutDebug( "cFileName = %s, uhttpd_OSFileName( cFileName ) = %s,\n\r", cFileName, uhttpd_OSFileName( cFileName ) )
#endif

      // Security
      IF ".." $ cFileName
         uhttpd_SetStatusCode( 403 )
         t_cErrorMsg := "Characters not allowed"
         RETURN MakeResponse()
      ENDIF

      // hb_ToOutDebug( "cFileName = %s, uhttpd_OSFileName( cFileName ) = %s,\n\r s_hScriptAliases = %s\n\r", cFileName, uhttpd_OSFileName( cFileName ), hb_ValToExp( s_hScriptAliases ) )

      // checking extension
      IF cExt == NIL

         // checking if file exists
         IF hb_FileExists( uhttpd_OSFileName( cFileName ) )

            // extract extension
            IF ( nI := RAt( ".", cFileName ) ) > 0
               cExt := Lower( SubStr( cFileName, nI + 1 ) )
            ENDIF

            // is it a directory ?
         ELSEIF hb_DirExists( uhttpd_OSFileName( cFileName ) )

            // if it exists as folder and it is missing trailing slash I add it and redirect to it
            IF !( Right( cFileName, 1 ) == "/" )
               uhttpd_SetHeader( "Location", "http://" + _SERVER[ "HTTP_HOST" ] + _SERVER[ "SCRIPT_NAME" ] + "/" )
               RETURN MakeResponse()
            ENDIF

            // Search for directory index file, i.e.: index.html
            IF AScan( s_aDirectoryIndex, ;
                  {| x | iif( hb_FileExists( uhttpd_OSFileName( cFileName + X ) ), ( cFileName += X, .T. ), .F. ) } ) > 0

               // I have to check filename again (behaviour changes on extension file name)
               // resetting extension
               cExt := NIL
               LOOP

            ENDIF

         ELSE

            // Check for PATH_INFO: I will search if there is a physical file removing parts from right
            cBaseFile := cScript
            lFound := .F.
            DO WHILE ! Empty( cBaseFile )

               // hb_ToOutDebug( "cBaseFile = %s, cPathInfo = %s\n\r", cBaseFile, cPathInfo )

               IF ( nPos := RAt( "/", cBaseFile ) ) > 0
                  cPathInfo := SubStr( cBaseFile, nPos ) + cPathInfo
                  cBaseFile := Left( cBaseFile, nPos - 1 )
               ELSE
                  EXIT
               ENDIF

               IF hb_FileExists( uhttpd_OSFileName( _SERVER[ "DOCUMENT_ROOT" ] + cBaseFile ) )
                  cFileName := uhttpd_OSFileName( _SERVER[ "DOCUMENT_ROOT" ] + cBaseFile )
                  lFound := .T.
                  EXIT
               ENDIF

               cFile := FileUnAlias( cBaseFile )
               IF cFile != NIL .AND. hb_FileExists( uhttpd_OSFileName( cFile ) )
                  cFileName := uhttpd_OSFileName( cFile )
                  lFound := .T.
                  EXIT
               ENDIF
            ENDDO

            // hb_ToOutDebug( "Uscita: cBaseFile = %s, cPathInfo = %s\n\r", cBaseFile, cPathInfo )

            // Found a script file name
            IF lFound .AND. ! Empty( cPathInfo )
               // Store PATH_INFO
               _SERVER[ "PATH_INFO" ]       := cPathInfo
               _SERVER[ "PATH_TRANSLATED" ] := cFileName
               // Restart
               LOOP
            ENDIF

         ENDIF

      ENDIF

      // Ok, now I have to see what action I have to take

      // hb_ToOutDebug( "cExt = %s\n\r", cExt )

      // Begin to search Handlers
      IF cExt != NIL
         cHandler := uhttpd_HGetValue( s_hHandlers, cExt )
      ENDIF

      // hb_ToOutDebug( "cHandler = %s\n\r", cHandler )

      IF cHandler != NIL
         xAction := uhttpd_HGetValue( s_hActions, cHandler )
      ENDIF

      // hb_ToOutDebug( "xAction = %s\n\r", xAction )

      IF xAction == NIL
         xAction := @Handler_Default()
      ENDIF

      // hb_ToOutDebug( "xAction = %s\n\r", xAction )

      // Setting CGI vars
      define_Env( _SERVER )

      // Eval Action
      RETURN hb_ExecFromArray( xAction, { cFileName } )

   ENDDO

   RETURN MakeResponse()

// Define environment variables
STATIC PROCEDURE Define_Env( hmServer )

   LOCAL v

   FOR EACH v IN hmServer
      hb_SetEnv( v:__enumKey(), v )
   NEXT

   RETURN

// ------------------------------- DEFAULT PAGES -----------------------------------

#if 0
STATIC PROCEDURE ShowServerStatus()

   LOCAL cThreads

   uhttpd_SetHeader( "Content-Type", "text/html" )
   uhttpd_Write( '<html><head>' )
   uhttpd_Write( '<META HTTP-EQUIV="Refresh" CONTENT="' + hb_ntos( PAGE_STATUS_REFRESH ) + ';URL=/ServerStatus">' )
   uhttpd_Write( '<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">' )
   uhttpd_Write( '<title>Server Status</title><body><h1>Server Status</h1><pre>' )
   // uhttpd_Write( '<table border="0">')

   uhttpd_Write( 'SERVER: ' + _SERVER[ "SERVER_SOFTWARE" ] + " Server at " + _SERVER[ "SERVER_NAME" ] + " Port " + _SERVER[ "SERVER_PORT" ] )
   uhttpd_Write( '<br />' )
   IF hb_mutexLock( s_hmtxBusy )
      uhttpd_Write( '<br />Thread: ' + Str( s_nThreads ) )
      uhttpd_Write( '<br />Connections: ' + Str( s_nConnections ) )
      uhttpd_Write( '<br />Max Connections: ' + Str( s_nMaxConnections ) )
      uhttpd_Write( '<br />Total Connections: ' + Str( s_nTotConnections ) )
      cThreads := ""
      AEval( s_aRunningThreads, {| e | cThreads += hb_ntos( hb_threadID( e ) ) + "," } )
      cThreads := "{ " + iif( ! Empty( cThreads ), Left( cThreads, Len( cThreads ) - 1 ), "<empty>" ) + " }"
      uhttpd_Write( '<br />Running Threads: ' + cThreads )

#ifndef FIXED_THREADS
      uhttpd_Write( '<br />Service Thread: ' + Str( s_nServiceThreads ) )
      uhttpd_Write( '<br />Service Connections: ' + Str( s_nServiceConnections ) )
      uhttpd_Write( '<br />Max Service Connections: ' + Str( s_nMaxServiceConnections ) )
      uhttpd_Write( '<br />Total Service Connections: ' + Str( s_nTotServiceConnections ) )
      cThreads := ""
      AEval( s_aServiceThreads, {| e | cThreads += hb_ntos( hb_threadID( e ) ) + "," } )
      cThreads := "{ " + iif( ! Empty( cThreads ), Left( cThreads, Len( cThreads ) - 1 ), "<empty>" ) + " }"
      uhttpd_Write( '<br />Service Threads: ' + cThreads )
#endif // FIXED_THREADS

      hb_mutexUnlock( s_hmtxBusy )
   ENDIF
   uhttpd_Write( '<br />Time: ' + Time() )

   // uhttpd_Write( '</table>')
   uhttpd_Write( "<hr></pre></body></html>" )

   RETURN
#endif

STATIC PROCEDURE ShowFolder( cDir )

   LOCAL aDir, aF
   LOCAL cParentDir, nPos

   uhttpd_SetHeader( "Content-Type", "text/html" )

   aDir := Directory( uhttpd_OSFileName( cDir ), "D" )
   IF hb_HHasKey( _GET, "s" )
      IF _GET[ "s" ] == "s"
         ASort( aDir,,, {| X, Y | iif( X[ 5 ] == "D", iif( Y[ 5 ] == "D", X[ 1 ] < Y[ 1 ], .T. ), ;
            iif( Y[ 5 ] == "D", .F., X[ 2 ] < Y[ 2 ] ) ) } )
      ELSEIF _GET[ "s" ] == "m"
         ASort( aDir,,, {| X, Y | iif( X[ 5 ] == "D", iif( Y[ 5 ] == "D", X[ 1 ] < Y[ 1 ], .T. ), ;
            iif( Y[ 5 ] == "D", .F., DToS( X[ 3 ] ) + X[ 4 ] < DToS( Y[ 3 ] ) + Y[ 4 ] ) ) } )
      ELSE
         ASort( aDir,,, {| X, Y | iif( X[ 5 ] == "D", iif( Y[ 5 ] == "D", X[ 1 ] < Y[ 1 ], .T. ), ;
            iif( Y[ 5 ] == "D", .F., X[ 1 ] < Y[ 1 ] ) ) } )
      ENDIF
   ELSE
      ASort( aDir,,, {| X, Y | iif( X[ 5 ] == "D", iif( Y[ 5 ] == "D", X[ 1 ] < Y[ 1 ], .T. ), ;
         iif( Y[ 5 ] == "D", .F., X[ 1 ] < Y[ 1 ] ) ) } )
   ENDIF

   uhttpd_Write( '<html><body><h1>Index of ' + _SERVER[ "SCRIPT_NAME" ] + '</h1><pre>      ' )
   uhttpd_Write( '<a href="?s=n">Name</a>                                                  ' )
   uhttpd_Write( '<a href="?s=m">Modified</a>             ' )
   uhttpd_Write( '<a href="?s=s">Size</a>' + CR_LF + '<hr>' )

   // Adding Upper Directory
   nPos := RAt( "/", SubStr( cDir, 1, Len( cDir ) - 1 ) )
   cParentDir := SubStr( cDir, 1, nPos )
   cParentDir := SubStr( cParentDir, Len( _SERVER[ "DOCUMENT_ROOT" ] ) + 1 )

   // hb_ToOutDebug( "cDir = %s, nPos = %i, cParentDir = %s\n\r", cDir, nPos, cParentDir )

   IF ! Empty( cParentDir )
      // Add parent directory
      hb_AIns( aDir, 1, { "<parent>", 0, "", "", "D" }, .T. )
   ENDIF

   FOR EACH aF IN aDir
      IF aF[ 1 ] == "<parent>"
         uhttpd_Write( '[DIR] <a href="' + cParentDir + '">..</a>' + ;
            CR_LF )
      ELSEIF Left( aF[ 1 ], 1 ) == "."
      ELSEIF "D" $ aF[ 5 ]
         uhttpd_Write( '[DIR] <a href="' + aF[ 1 ] + '/">' + aF[ 1 ] + '</a>' + Space( 50 - Len( aF[ 1 ] ) ) + ;
            DToC( aF[ 3 ] ) + ' ' + aF[ 4 ] + CR_LF )
      ELSE
         uhttpd_Write( '      <a href="' + aF[ 1 ] + '">' + aF[ 1 ] + '</a>' + Space( 50 - Len( aF[ 1 ] ) ) + ;
            DToC( aF[ 3 ] ) + ' ' + aF[ 4 ] + Str( aF[ 2 ], 12 ) + CR_LF )
      ENDIF
   NEXT
   uhttpd_Write( "<hr></pre></body></html>" )

   RETURN

// ------------------------------- Utility functions --------------------------------

// from Przemek's example, useful to use encrypted HRB module files
#if 0
STATIC FUNCTION HRB_LoadFromFileEncrypted( cFile, cKey )

   LOCAL cHrbBody

   cHrbBody := hb_MemoRead( cFile )
   cHrbBody := sx_Decrypt( cHrbBody, cKey )
   cHrbBody := hb_ZUncompress( cHrbBody )

   RETURN cHrbBody

// Reverse function to save is:
PROCEDURE HRB_SaveToFileEncrypted( cHrbBody, cKey, cEncFileName )
   LOCAL cFile
   IF ! Empty( cHrbBody )
      cHrbBody := hb_ZCompress( cHrbBody )
      cHrbBody := sx_Encrypt( cHrbBody, cKey )
      hb_MemoWrit( cEncFileName, cHrbBody )
   ENDIF
   RETURN
#endif

STATIC FUNCTION HRB_LoadFromFile( cFile )
   RETURN hb_MemoRead( cFile )

STATIC PROCEDURE Help()

#if 0
   LOCAL cPrg := hb_argv( 0 )
   LOCAL nPos := RAt( "\", cPrg )

   __OutDebug( hb_argv( 0 ) )

   IF nPos > 0
      cPrg := SubStr( cPrg, nPos + 1 )
   ENDIF
#endif

   ?
   ? "(C) 2009 Francesco Saverio Giudice <info@fsgiudice.com>"
   ?
   ? APP_NAME + " - web server - v. " + APP_VERSION
   ? "Based on original work of Mindaugas Kavaliauskas <dbtopas@dbtopas.lt>"
   ?
   ? "Parameters: (all optionals)"
   ?
   ? "-p       | --port           webserver tcp port         (default: " + hb_ntos( LISTEN_PORT ) + ")"
   ? "-c       | --config         Configuration file         (default: " + APP_NAME + ".ini)"
   ? "                            It is possibile to define file path"
   ? "-a       | --approot        Application root directory (default: <curdir>)"
   ? "-d       | --docroot        Document root directory    (default: <curdir>/home)"
   ? "-i       | --indexes        Allow directory view       (default: no)"
   ? "-s       | --stop           Stop webserver"
   ? "-ts      | --start-threads  Define starting threads    (default: " + hb_ntos( START_RUNNING_THREADS ) + ")"
   ? "-tm      | --max-threads    Define max threads         (default: " + hb_ntos( MAX_RUNNING_THREADS ) + ")"
   ? "-cr      | --console-rows   Console rows               (default: " + hb_ntos( MaxRow() + 1 ) + ")"
   ? "-cc      | --console-cols   Console cols               (default: " + hb_ntos( MaxCol() + 1 ) + ")"
   ? "-h | -?  | --help           This help message"
   ?
   WAIT

   RETURN

STATIC PROCEDURE SysSettings()

   SET SCOREBOARD OFF
   SET CENTURY     ON
   SET DATE      ANSI
   SET BELL       OFF
   SET DELETED     ON
   SET CONFIRM     ON
   SET ESCAPE      ON
   SET WRAP        ON
   // rddSetDefault( "DBFCDX" )

   RETURN

STATIC PROCEDURE Progress( /*@*/ nProgress )

   LOCAL cString := "["

   DO CASE
   CASE nProgress == 0
      cString += "-"
   CASE nProgress == 1
      cString += "\"
   CASE nProgress == 2
      cString += "|"
   CASE nProgress == 3
      cString += "/"
   ENDCASE

   cString += "]"

   nProgress++

   IF nProgress == 4
      nProgress := 0
   ENDIF

   // using hb_DispOutAt() to avoid MT screen updates problem
   hb_DispOutAt( 10,  5, cString )
   hb_DispOutAt(  0, 60, "Time: " + Time() )

   RETURN

// Show messages in console
#define CONSOLE_FIRSTROW   12
#define CONSOLE_LASTROW    MaxRow()
STATIC PROCEDURE WriteToConsole( ... )

   LOCAL cMsg

   IF hb_mutexLock( s_hmtxConsole )
      IF s_lConsole

         FOR EACH cMsg IN hb_AParams()

            hb_Scroll( CONSOLE_FIRSTROW, 0, CONSOLE_LASTROW, MaxCol(), -1 )
            hb_DispOutAt( CONSOLE_FIRSTROW, 0, PadR( "> " + hb_CStr( cMsg ), MaxCol() ) )

#ifdef DEBUG_ACTIVE
            hb_ToOutDebug( ">>> %s\n\r", cMsg )
#endif

         NEXT

      ENDIF
      hb_mutexUnlock( s_hmtxConsole )
   ENDIF

   RETURN

STATIC FUNCTION ParseIni( cConfig )

   LOCAL hIni := hb_iniRead( cConfig, .T. ) // .T. = load all keys in MixedCase, redundant as it is default, but to remember
   LOCAL cSection, hSect, cKey, xVal, cVal, nPos
   LOCAL hDefault

   // hb_ToOutDebug( "cConfig = %s,\n\rhIni = %s\n\r", cConfig, hb_ValToExp( hIni ) )

   // Define here what attributes we can have in ini config file and their defaults
   // Please add all keys in uppercase. hDefaults is Case Insensitive
   hDefault := { ;
      "MAIN"           => { "PORT"                 => LISTEN_PORT               , ;
                            "APPLICATION_ROOT"     => hb_DirBase()              , ;
                            "DOCUMENT_ROOT"        => hb_DirBase() + "home"     , ;
                            "SHOW_INDEXES"         => .F.                       , ;
                            "SCRIPTALIASMIXEDCASE" => .T.                       , ;
                            "SESSIONPATH"          => hb_DirBase() + "sessions" , ;
                            "DIRECTORYINDEX"       => DIRECTORYINDEX_ARRAY      , ;
                            "CONSOLE-ROWS"         => MaxRow() + 1              , ;
                            "CONSOLE-COLS"         => MaxCol() + 1             }, ;
      "LOGFILES"       => { "ACCESS"               => FILE_ACCESS_LOG           , ;
                            "ERROR"                => FILE_ERROR_LOG           }, ;
      "THREADS"        => { "MAX_WAIT"             => THREAD_MAX_WAIT           , ;
                            "START_NUM"            => START_RUNNING_THREADS     , ;
                            "MAX_NUM"              => MAX_RUNNING_THREADS      }, ;
      "SCRIPTALIASES"  => { => } , ;
      "ALIASES"        => { => } }

   hb_HCaseMatch( hDefault, .F. )

   // hb_ToOutDebug( "hDefault = %s\n\r", hb_ValToExp( hDefault ) )

   // Now read changes from ini file and modify only admited keys
   IF ! Empty( hIni )
      FOR EACH cSection IN hIni:Keys

         cSection := Upper( cSection )

         // hb_ToOutDebug( "cSection = %s\n\r", cSection )

         IF cSection $ hDefault

            hSect := hIni[ cSection ]

            // hb_ToOutDebug( "hSect = %s\n\r", hb_ValToExp( hSect ) )

            IF HB_ISHASH( hSect )
               FOR EACH cKey IN hSect:Keys

                  // Please, below check values MUST be uppercase

                  // hb_ToOutDebug( "cKey = %s\n\r", cKey )

                  IF cSection == "SCRIPTALIASES"
                     xVal := hSect[ cKey ]
                     IF xVal != NIL
                        hDefault[ cSection ][ cKey ] := xVal
                     ENDIF

                  ELSEIF cSection == "ALIASES"
                     xVal := hSect[ cKey ]
                     IF xVal != NIL
                        hDefault[ cSection ][ cKey ] := xVal
                     ENDIF

                  ELSEIF ( cKey := Upper( cKey ) ) $ hDefault[ cSection ]  // force cKey to be uppercase

                     IF ( nPos := hb_HScan( hSect, {| k | Upper( k ) == cKey } ) ) > 0
                        cVal := hb_HValueAt( hSect, nPos )

                        // hb_ToOutDebug( "cVal = %s\n\r", cVal )

                        DO CASE
                        CASE cSection == "MAIN"

                           DO CASE
                           CASE cKey == "PORT"
                              xVal := Val( cVal )
                           CASE cKey == "CONSOLE-ROWS"
                              xVal := Val( cVal )
                           CASE cKey == "CONSOLE-COLS"
                              xVal := Val( cVal )
                           CASE cKey == "APPLICATION_ROOT"
                              IF ! Empty( cVal )
                                 // Change APP_DIR macro with current exe path
                                 xVal := cVal
                              ENDIF
                           CASE cKey == "DOCUMENT_ROOT"
                              IF ! Empty( cVal )
                                 // After will change APP_DIR macro with application dir
                                 // xVal := StrTran( cVal, "$(APP_DIR)", hb_DirBase() )
                                 xVal := cVal
                              ENDIF
                           CASE cKey == "SCRIPTALIASMIXEDCASE"
                              xVal := cVal
                           CASE cKey == "SESSIONPATH"
                              IF ! Empty( cVal )
                                 // Change APP_DIR macro with current exe path
                                 // xVal := StrTran( cVal, "$(APP_DIR)", hb_DirBase() )
                                 xVal := cVal
                              ENDIF
                           CASE cKey == "DIRECTORYINDEX"
                              IF ! Empty( cVal )
                                 xVal := uhttpd_split( " ", AllTrim( cVal ) )
                              ENDIF
                           ENDCASE

                        CASE cSection == "LOGFILES"

                           DO CASE
                           CASE cKey == "ACCESS"
                              xVal := cVal
                           CASE cKey == "ERROR"
                              xVal := cVal
                           ENDCASE

                        CASE cSection == "THREADS"

                           DO CASE
                           CASE cKey == "MAX_WAIT"
                              xVal := Val( cVal )
                           CASE cKey == "START_NUM"
                              xVal := Val( cVal )
                           CASE cKey == "MAX_NUM"
                              xVal := Val( cVal )
                           ENDCASE

                        ENDCASE

                        IF xVal != NIL
                           hDefault[ cSection ][ cKey ] := xVal
                        ENDIF
                     ENDIF

                  ENDIF
               NEXT
            ENDIF
         ENDIF
      NEXT
   ENDIF

   RETURN hDefault

STATIC FUNCTION FileUnAlias( cScript )

   LOCAL cFileName, x

   // Checking if the request contains a Script Alias
   IF hb_HHasKey( s_hScriptAliases, cScript )
      // in this case I have to substitute the alias with the real file name
      cFileName := s_hScriptAliases[ cScript ]

      // substitute macros
      cFileName := StrTran( cFileName, "$(DOCROOT_DIR)", _SERVER[ "DOCUMENT_ROOT" ] )
      cFileName := StrTran( cFileName, "$(APP_DIR)", s_cApplicationRoot )
   ENDIF

   IF cFileName == NIL

      // Checking if the request contains an alias
      FOR EACH x IN s_hAliases
         IF x:__enumKey() == Left( cScript, Len( x:__enumKey() ) )
            cFileName := x + SubStr( cScript, Len( x:__enumKey() ) + 1 )

            // substitute macros
            cFileName := StrTran( cFileName, "$(DOCROOT_DIR)", _SERVER[ "DOCUMENT_ROOT" ] )
            cFileName := StrTran( cFileName, "$(APP_DIR)", s_cApplicationRoot )
            EXIT
         ENDIF
      NEXT

   ENDIF

   RETURN cFileName

STATIC FUNCTION uhttpd_DefError( oError )

   LOCAL cMessage
   LOCAL cCallstack
   LOCAL cDOSError

   LOCAL aOptions
   LOCAL nChoice

   LOCAL n
   LOCAL cDateTime, cString
   LOCAL cNewLine := hb_eol()

   // By default, division by zero results in zero
   IF oError:genCode == EG_ZERODIV .AND. ;
      oError:canSubstitute
      RETURN 0
   ENDIF

   // By default, retry on RDD lock error failure */
   IF oError:genCode == EG_LOCK .AND. ;
      oError:canRetry
      // oError:tries++
      RETURN .T.
   ENDIF

   // Set NetErr() of there was a database open error
   IF oError:genCode == EG_OPEN .AND. ;
      oError:osCode == 32 .AND. ;
      oError:canDefault
      NetErr( .T. )
      RETURN .F.
   ENDIF

   // Set NetErr() if there was a lock error on dbAppend()
   IF oError:genCode == EG_APPENDLOCK .AND. ;
      oError:canDefault
      NetErr( .T. )
      RETURN .F.
   ENDIF

   cMessage := ErrorMessage( oError )
   IF ! Empty( oError:osCode )
      cDOSError := "(OS Error " + hb_ntos( oError:osCode ) + ")"
   ENDIF

   // ;

   cCallstack := ""
   n := 1
   DO WHILE ! Empty( ProcName( ++n ) )
      cCallstack += "Called from " + ProcName( n ) + "(" + hb_ntos( ProcLine( n ) ) + ")  ;"
   ENDDO

   // Build buttons

   aOptions := {}

   AAdd( aOptions, "Quit" )

   IF oError:canRetry
      AAdd( aOptions, "Retry" )
   ENDIF

   IF oError:canDefault
      AAdd( aOptions, "Default" )
   ENDIF

   // Show alert box

#ifdef DEBUG_ACTIVE
   hb_ToOutDebug( "ERROR: %s\n\r", cMessage + " " + cCallstack )
#endif

   nChoice := 0
   DO WHILE nChoice == 0

      IF cDOSError == NIL
         nChoice := Alert( cMessage + ";" + cCallstack, aOptions )
      ELSE
         nChoice := Alert( cMessage + " " + cDOSError + ";" + cCallstack, aOptions )
      ENDIF

   ENDDO

   IF ! Empty( nChoice )
      DO CASE
      CASE aOptions[ nChoice ] == "Break"
         Break( oError )
      CASE aOptions[ nChoice ] == "Retry"
         RETURN .T.
      CASE aOptions[ nChoice ] == "Default"
         RETURN .F.
      ENDCASE
   ENDIF

   // "Quit" selected

   IF cDOSError != NIL
      cMessage += " " + cDOSError
   ENDIF

   OutErr( cNewLine )
   OutErr( cMessage )
   OutErr( cNewLine )
   OutErr( cCallstack )

   // Write to errorlog
   cDateTime := hb_TToC( hb_DateTime() )
   cString   := ;
      Replicate( "*", 70 ) + cNewLine + ;
      cDateTime            + cNewLine + ;
      Replicate( "*", 70 ) + cNewLine + ;
      cMessage             + cNewLine + ;
      cCallstack           + cNewLine + ;
      Replicate( "*", 70 ) + cNewLine

   uhttpd_WriteToLogFile( cString, hb_DirBase() + "error.log" )

   ErrorLevel( 1 )
   QUIT

   RETURN .F.

STATIC FUNCTION ErrorMessage( oError )

   // start error message
   LOCAL cMessage := iif( oError:severity > ES_WARNING, "Error", "Warning" ) + " "

   // add subsystem name if available
   IF HB_ISSTRING( oError:subsystem )
      cMessage += oError:subsystem()
   ELSE
      cMessage += "???"
   ENDIF

   // add subsystem's error code if available
   IF HB_ISNUMERIC( oError:subCode )
      cMessage += "/" + hb_ntos( oError:subCode )
   ELSE
      cMessage += "/???"
   ENDIF

   // add error description if available
   IF HB_ISSTRING( oError:description )
      cMessage += "  " + oError:description
   ENDIF

   // add either filename or operation
   DO CASE
   CASE ! Empty( oError:filename )
      cMessage += ": " + oError:filename
   CASE ! Empty( oError:operation )
      cMessage += ": " + oError:operation
   ENDCASE

   RETURN cMessage

// ----------------------------------------------------------------------------------
// HANDLERS
// ----------------------------------------------------------------------------------

// This handler handle static files
STATIC FUNCTION Handler_Default( cFileName )

   LOCAL cMime
   LOCAL cExt, nI
   LOCAL hMimeTypes := LoadMimeTypes()

   // If file exists
   IF hb_FileExists( uhttpd_OSFileName( cFileName ) )
      IF ( nI := RAt( ".", cFileName ) ) > 0
         cExt  := Lower( SubStr( cFileName, nI + 1 ) )
         cMime := uhttpd_HGetValue( hMimeTypes, cExt )
      ENDIF
      IF cMime == NIL
         // Unknown file type
         cMime := "application/octet-stream"
      ENDIF

      uhttpd_SetHeader( "Content-Type", cMime )
      uhttpd_Write( hb_MemoRead( uhttpd_OSFileName( cFileName ) ) )

      // Directory content request
   ELSEIF hb_DirExists( uhttpd_OSFileName( cFileName ) )

      // If I'm here it's means that I have no page, so, if it is defined, I will display content folder
      IF ! s_lIndexes
         uhttpd_SetStatusCode( 403 )
         t_cErrorMsg := "Display file list not allowed"
      ELSE
         // ----------------------- display folder content -------------------------------------
         ShowFolder( cFileName )
      ENDIF

   ELSE

      // We cannot handle request
      uhttpd_SetStatusCode( 404 )
      t_cErrorMsg := "File does not exist: " + cFileName

   ENDIF

   RETURN MakeResponse()

// This handler handle server status
STATIC FUNCTION Handler_ServerStatus()

   LOCAL cThreads

   uhttpd_SetHeader( "Content-Type", "text/html" )
   uhttpd_Write( '<html><head>' )
   uhttpd_Write( '<META HTTP-EQUIV="Refresh" CONTENT="' + hb_ntos( PAGE_STATUS_REFRESH ) + ';URL=/ServerStatus">' )
   uhttpd_Write( '<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">' )
   uhttpd_Write( '<title>Server Status</title><body><h1>Server Status</h1><pre>' )
   // uhttpd_Write( '<table border="0">')

   uhttpd_Write( 'SERVER: ' + _SERVER[ "SERVER_SOFTWARE" ] + " Server at " + _SERVER[ "SERVER_NAME" ] + " Port " + _SERVER[ "SERVER_PORT" ] )
   uhttpd_Write( '<br />' )
   IF hb_mutexLock( s_hmtxBusy )
      uhttpd_Write( '<br />Thread: ' + Str( s_nThreads ) )
      uhttpd_Write( '<br />Connections: ' + Str( s_nConnections ) )
      uhttpd_Write( '<br />Max Connections: ' + Str( s_nMaxConnections ) )
      uhttpd_Write( '<br />Total Connections: ' + Str( s_nTotConnections ) )
      cThreads := ""
      AEval( s_aRunningThreads, {| e | cThreads += hb_ntos( hb_threadID( e ) ) + "," } )
      cThreads := "{ " + iif( ! Empty( cThreads ), Left( cThreads, Len( cThreads ) - 1 ), "<empty>" ) + " }"
      uhttpd_Write( '<br />Running Threads: ' + cThreads )

#ifndef FIXED_THREADS
      uhttpd_Write( '<br />Service Thread: ' + Str( s_nServiceThreads ) )
      uhttpd_Write( '<br />Service Connections: ' + Str( s_nServiceConnections ) )
      uhttpd_Write( '<br />Max Service Connections: ' + Str( s_nMaxServiceConnections ) )
      uhttpd_Write( '<br />Total Service Connections: ' + Str( s_nTotServiceConnections ) )
      cThreads := ""
      AEval( s_aServiceThreads, {| e | cThreads += hb_ntos( hb_threadID( e ) ) + "," } )
      cThreads := "{ " + iif( ! Empty( cThreads ), Left( cThreads, Len( cThreads ) - 1 ), "<empty>" ) + " }"
      uhttpd_Write( '<br />Service Threads: ' + cThreads )
#endif // FIXED_THREADS

      hb_mutexUnlock( s_hmtxBusy )
   ENDIF
   uhttpd_Write( '<br />Time: ' + Time() )

   // uhttpd_Write( '</table>')
   uhttpd_Write( "<hr></pre></body></html>" )

   RETURN MakeResponse()

STATIC FUNCTION Handler_HrbScript( cFileName )

   LOCAL xResult
   LOCAL cHRBBody, pHRB, oError
   LOCAL cCurPath

   BEGIN SEQUENCE WITH {| oErr | Break( oErr ) }
      // Lock HRB to avoid MT race conditions
      IF ! HRB_ACTIVATE_CACHE
         cHRBBody := HRB_LoadFromFile( uhttpd_OSFileName( cFileName ) )
      ENDIF
      IF hb_mutexLock( s_hmtxHRB )
         BEGIN SEQUENCE
            IF HRB_ACTIVATE_CACHE
               // caching modules
               IF ! hb_HHasKey( s_hHRBModules, cFileName )
                  s_hHRBModules[ cFileName ] := HRB_LoadFromFile( uhttpd_OSFileName( cFileName ) )
               ENDIF
               cHRBBody := s_hHRBModules[ cFileName ]
            ENDIF
            WriteToConsole( "Executing: " + cFileName )
            IF ! Empty( pHRB := hb_hrbLoad( cHRBBody ) )

               // save current directory
               cCurPath := hb_CurDrive() + hb_osDriveSeparator() + hb_ps() + CurDir()
               // Change dir to document root
               DirChange( s_cDocumentRoot )

               xResult := hb_hrbDo( pHRB )

#ifdef DEBUG_ACTIVE
               hb_ToOutDebug( "Handler_HrbScript(): cFileName = %s,\n\rcCurPath = %s,\n\rs_cDocumentRoot = %s,\n\rpHRB = %s,\n\rxResult = %s\n\r", ;
                  cFileName, cCurPath, s_cDocumentRoot, pHRB, xResult )
#endif

               // return to original folder
               DirChange( cCurPath )

               hb_hrbUnload( pHRB )
            ELSE
               uhttpd_SetStatusCode( 404 )
               t_cErrorMsg := "File does not exist: " + cFileName
            ENDIF
         ALWAYS
            hb_mutexUnlock( s_hmtxHRB )
         END SEQUENCE

      ENDIF

      IF HB_ISSTRING( xResult )
         uhttpd_SetHeader( "Content-Type", "text/html" )
         uhttpd_Write( xResult )
      ELSE
         // Application in HRB module is responsible to send HTML content
      ENDIF

   RECOVER USING oError

      WriteToConsole( "Error!" )

      uhttpd_SetHeader( "Content-Type", "text/html" )
      uhttpd_Write( "Error" )
      uhttpd_Write( "<br />Description: " + hb_CStr( oError:Description ) )
      uhttpd_Write( "<br />Filename: "    + hb_CStr( oError:filename ) )
      uhttpd_Write( "<br />Operation: "   + hb_CStr( oError:operation ) )
      uhttpd_Write( "<br />OsCode: "      + hb_CStr( oError:osCode ) )
      uhttpd_Write( "<br />GenCode: "     + hb_CStr( oError:genCode ) )
      uhttpd_Write( "<br />SubCode: "     + hb_CStr( oError:subCode ) )
      uhttpd_Write( "<br />SubSystem: "   + hb_CStr( oError:subSystem ) )
      uhttpd_Write( "<br />Args: "        + hb_CStr( hb_ValToExp( oError:args ) ) )
      uhttpd_Write( "<br />ProcName: "    + hb_CStr( ProcName( 0 ) ) )
      uhttpd_Write( "<br />ProcLine: "    + hb_CStr( ProcLine( 0 ) ) )

   END SEQUENCE

   RETURN MakeResponse()

STATIC FUNCTION Handler_CgiScript( cFileName )

   LOCAL xResult

   WriteToConsole( "Executing: " + cFileName )

   IF CGIExec( uhttpd_OSFileName( cFileName ), @xResult ) == 0

      // uhttpd_SetHeader( "Content-Type", cI )
      // uhttpd_Write( xResult )
      RETURN "HTTP/1.1 200 OK " + CR_LF + xResult

   ELSE

      uhttpd_SetHeader( "Content-Type", "text/html" )
      IF ! Empty( xResult )
         uhttpd_Write( xResult )
      ELSE
         uhttpd_Write( "CGI Error" )
      ENDIF

   ENDIF

   RETURN MakeResponse()

STATIC FUNCTION LoadMimeTypes()

   // TODO: load mime types from file

   RETURN { ;
      "css"  => "text/css", ;
      "htm"  => "text/html", ;
      "html" => "text/html", ;
      "txt"  => "text/plain", ;
      "text" => "text/plain", ;
      "asc"  => "text/plain", ;
      "c"    => "text/plain", ;
      "h"    => "text/plain", ;
      "cpp"  => "text/plain", ;
      "hpp"  => "text/plain", ;
      "log"  => "text/plain", ;
      "rtf"  => "text/rtf", ;
      "xml"  => "text/xml", ;
      "xsl"  => "text/xsl", ;
      "bmp"  => "image/bmp", ;
      "gif"  => "image/gif", ;
      "jpg"  => "image/jpeg", ;
      "jpe"  => "image/jpeg", ;
      "jpeg" => "image/jpeg", ;
      "png"  => "image/png", ;
      "tif"  => "image/tiff", ;
      "tiff" => "image/tiff", ;
      "djv"  => "image/vnd.djvu", ;
      "djvu" => "image/vnd.djvu", ;
      "ico"  => "image/x-icon", ;
      "xls"  => "application/excel", ;
      "doc"  => "application/msword", ;
      "pdf"  => "application/pdf", ;
      "ps"   => "application/postscript", ;
      "eps"  => "application/postscript", ;
      "ppt"  => "application/powerpoint", ;
      "bz2"  => "application/x-bzip2", ;
      "gz"   => "application/x-gzip", ;
      "tgz"  => "application/x-gtar", ;
      "js"   => "application/x-javascript", ;
      "tar"  => "application/x-tar", ;
      "tex"  => "application/x-tex", ;
      "zip"  => "application/zip", ;
      "midi" => "audio/midi", ;
      "mp3"  => "audio/mpeg", ;
      "wav"  => "audio/x-wav", ;
      "qt"   => "video/quicktime", ;
      "mov"  => "video/quicktime", ;
      "avi"  => "video/x-msvideo" }

STATIC FUNCTION GT_notifier( nEvent, xParams )

   LOCAL nReturn := 0

   DO CASE
   CASE nEvent == HB_K_CLOSE
      IF hb_mutexLock( s_hmtxBusy )
         s_lQuitRequest := .T.
         nReturn := 1
         hb_mutexUnlock( s_hmtxBusy )
      ENDIF
   ENDCASE

   HB_SYMBOL_UNUSED( xParams )

   RETURN nReturn

STATIC FUNCTION UHTTPD_UTCOFFSET()

   LOCAL nOffset := hb_UTCOffset()

   RETURN iif( nOffset < 0, "-", "+" ) + ;
      StrZero( nOffset / 3600, 2, 0 ) + ;
      StrZero( ( nOffset % 3600 ) / 60, 2, 0 )

STATIC FUNCTION HB_HASHI()

   LOCAL h := { => }

   hb_HCaseMatch( h, .F. )

   RETURN h

#if defined( __HBSCRIPT__HBSHELL )
SET PROCEDURE TO "_cgifunc.prg"
SET PROCEDURE TO "_cookie.prg"
SET PROCEDURE TO "_session.prg"
#endif
