/* * uHTTPD (Micro HTTP server) * * Copyright 2009 Francesco Saverio Giudice * 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 := "

" + cReturnCode + "

" 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 := "

500 Server Too Busy

" EXIT CASE 500100 // error: 500-100 Undeclared Variable OTHERWISE cReturnCode := "403 Forbidden" t_cResult := "

" + cReturnCode + "

" 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" ] := "
" + _SERVER[ "SERVER_SOFTWARE" ] + " Server at " + _SERVER[ "SERVER_NAME" ] + " Port " + _SERVER[ "SERVER_PORT" ] + "
" _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( '' ) uhttpd_Write( '' ) uhttpd_Write( '' ) uhttpd_Write( 'Server Status

Server Status

' )
   // uhttpd_Write( '')

   uhttpd_Write( 'SERVER: ' + _SERVER[ "SERVER_SOFTWARE" ] + " Server at " + _SERVER[ "SERVER_NAME" ] + " Port " + _SERVER[ "SERVER_PORT" ] )
   uhttpd_Write( '
' ) IF hb_mutexLock( s_hmtxBusy ) uhttpd_Write( '
Thread: ' + Str( s_nThreads ) ) uhttpd_Write( '
Connections: ' + Str( s_nConnections ) ) uhttpd_Write( '
Max Connections: ' + Str( s_nMaxConnections ) ) uhttpd_Write( '
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 ), "" ) + " }" uhttpd_Write( '
Running Threads: ' + cThreads ) #ifndef FIXED_THREADS uhttpd_Write( '
Service Thread: ' + Str( s_nServiceThreads ) ) uhttpd_Write( '
Service Connections: ' + Str( s_nServiceConnections ) ) uhttpd_Write( '
Max Service Connections: ' + Str( s_nMaxServiceConnections ) ) uhttpd_Write( '
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 ), "" ) + " }" uhttpd_Write( '
Service Threads: ' + cThreads ) #endif // FIXED_THREADS hb_mutexUnlock( s_hmtxBusy ) ENDIF uhttpd_Write( '
Time: ' + Time() ) // uhttpd_Write( '
') uhttpd_Write( "
" ) 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( '

Index of ' + _SERVER[ "SCRIPT_NAME" ] + '

      ' )
   uhttpd_Write( 'Name                                                  ' )
   uhttpd_Write( 'Modified             ' )
   uhttpd_Write( 'Size' + CR_LF + '
' ) // 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, { "", 0, "", "", "D" }, .T. ) ENDIF FOR EACH aF IN aDir IF aF[ 1 ] == "" uhttpd_Write( '[DIR] ..' + ; CR_LF ) ELSEIF Left( aF[ 1 ], 1 ) == "." ELSEIF "D" $ aF[ 5 ] uhttpd_Write( '[DIR] ' + aF[ 1 ] + '' + Space( 50 - Len( aF[ 1 ] ) ) + ; DToC( aF[ 3 ] ) + ' ' + aF[ 4 ] + CR_LF ) ELSE uhttpd_Write( ' ' + aF[ 1 ] + '' + Space( 50 - Len( aF[ 1 ] ) ) + ; DToC( aF[ 3 ] ) + ' ' + aF[ 4 ] + Str( aF[ 2 ], 12 ) + CR_LF ) ENDIF NEXT uhttpd_Write( "
" ) 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 " ? ? APP_NAME + " - web server - v. " + APP_VERSION ? "Based on original work of Mindaugas Kavaliauskas " ? ? "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: )" ? "-d | --docroot Document root directory (default: /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( '' ) uhttpd_Write( '' ) uhttpd_Write( '' ) uhttpd_Write( 'Server Status

Server Status

' )
   // uhttpd_Write( '')

   uhttpd_Write( 'SERVER: ' + _SERVER[ "SERVER_SOFTWARE" ] + " Server at " + _SERVER[ "SERVER_NAME" ] + " Port " + _SERVER[ "SERVER_PORT" ] )
   uhttpd_Write( '
' ) IF hb_mutexLock( s_hmtxBusy ) uhttpd_Write( '
Thread: ' + Str( s_nThreads ) ) uhttpd_Write( '
Connections: ' + Str( s_nConnections ) ) uhttpd_Write( '
Max Connections: ' + Str( s_nMaxConnections ) ) uhttpd_Write( '
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 ), "" ) + " }" uhttpd_Write( '
Running Threads: ' + cThreads ) #ifndef FIXED_THREADS uhttpd_Write( '
Service Thread: ' + Str( s_nServiceThreads ) ) uhttpd_Write( '
Service Connections: ' + Str( s_nServiceConnections ) ) uhttpd_Write( '
Max Service Connections: ' + Str( s_nMaxServiceConnections ) ) uhttpd_Write( '
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 ), "" ) + " }" uhttpd_Write( '
Service Threads: ' + cThreads ) #endif // FIXED_THREADS hb_mutexUnlock( s_hmtxBusy ) ENDIF uhttpd_Write( '
Time: ' + Time() ) // uhttpd_Write( '
') uhttpd_Write( "
" ) 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( "
Description: " + hb_CStr( oError:Description ) ) uhttpd_Write( "
Filename: " + hb_CStr( oError:filename ) ) uhttpd_Write( "
Operation: " + hb_CStr( oError:operation ) ) uhttpd_Write( "
OsCode: " + hb_CStr( oError:osCode ) ) uhttpd_Write( "
GenCode: " + hb_CStr( oError:genCode ) ) uhttpd_Write( "
SubCode: " + hb_CStr( oError:subCode ) ) uhttpd_Write( "
SubSystem: " + hb_CStr( oError:subSystem ) ) uhttpd_Write( "
Args: " + hb_CStr( hb_ValToExp( oError:args ) ) ) uhttpd_Write( "
ProcName: " + hb_CStr( ProcName( 0 ) ) ) uhttpd_Write( "
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