listener.js

/*
    Brian Osborne - June 2011

    A general purpose http listener written in server-side JavaScript.

    Designed for eary node.js version 0.4.7 (released 27th April 2011)
     
    Listens on port 8080, although can easily change to any port by altering the final line of code.
     
    Will handle ANY request, whether it be a GET, a POST, loading a resource, or running a different script.
   
    All .js pages loaded in the URI must include an init() method, such as:
   
            File "processOrder.js"...
       
                    exports.init = function( $nodejs ) {
                        // process $nodejs.response object to output to browser
                    }
           
            All .js files called by $nodejs.executeScript() will have full access to the entire $nodejs object (including the
            request object, response object, postBody and queryString). Therefore, you have the capability to write very complex
            and nested node.js applications using this simple http listener application.
   
    Anywhere you see ACTION: it is indicating that the code needs to be changed to reflect your business logic.
   
    I would recommend making us of require() to load you own scripts and use the exports facility in your scripts.
   
    For example:
   
        var myHttpPostHandler = require( './post.js' );
        myHttpPostHandler.processField( 'name', $nodejs.getPostField( 'name' ) );
        myHttpPostHandler.processField( 'address', $nodejs.getPostField( 'address' ) );
       
    Where post.js contains this method:
   
        exports.processField = function( fieldName, fieldValue ) { // do something };
       
    TECHNICAL DOCUMENTATION:
   
    Public interface (constructor):
   
        generalRequestHandler.listenOnPort( int portNumber, boolean logActivity )
       
            Change the parameters to this method to set the port to listen on and to indicate if all activity should be logged.
            This is the method that runs when node loads the containing .js file
           
    Developer interface (local) METHODS:
   
        There is a module object called $nodejs that contains all the functionality that the developer (you) will need access to.
        If any other .js files are called from your listener, then this module object is passed to it, making everything accessible.
       
        void/boolean $nodejs.executeScript( filename )
       
            This method can run the exported init() method of a .js file, passing the entire $nodejs object to it (with details about the
            top-level request and response objects). Any .js file called by executeScript() must have an exported init( $nodejs ) method,
            which can optionally return true or false (to indicate if it completed successfully).
           
        boolean $nodejs.preHandleRequest()
       
            This method will fire as a callback whenever the listener picks up a request, but after any POST data has loaded.
            Therefore, you can be    sure that any code running in your preHandleRequest() method will have received all the necessary data.
            If this method returns true then the listener will continue to load the requested resource.
            If this method returns false, then it is assumed your code has set the response object and no further action will be taken.
           
        void $nodejs.takeDefaultAction()
       
            This method will be the last thing called if no resource is requested (i.e. the filename is "/"). If your $nodejs.preHandleRequest()
            returns false then this method will not be called.
            In this method you can specify a default action to take, such as maybe loading "index.html"
           
        void $nodejs.returnPage( int statusNum, string chunk, string chunkType [, string contentType ] )
       
            This method gives you a quick way to send a page back to the browser.
            Normally, statusNum would be set to 200 (OK).
            chunk is the content of the page.
            chunkType might be "utf-8" or "binary" (if a resource is being loaded)
            contentType might be "text/plain", "text/html" etc (or not passed if a binary file is being loaded).
           
        string $nodejs.getPostField( string fieldName )
       
            This method allows you to extract the value of any passed POST field by passing the field name.
            null is returned if the field does not exist.
           
        string $nodejs.getQueryParameter( string paramName )
       
            This method allows you to extract the value of any parameter passed in the query string by giving the parameter name.
            null is returned if the parameter does not exist.
           
        void $nodejs.handleResourcenotFound()
       
            This method displays the 404 Not Found page back to the user.

        void $nodejs.handleLoadError()
       
            This method displays the 500 Error page back to the user when a resource/page fails to load.
           
    Developer interface (local) PROPERTIES:
   
        These properties are constantly available to the developer, whether working in this .js file or and
        file called via the $nodejs.executeScript() method.
   
        $nodejs.url
       
            The shortcut for the request( 'url' ) object
       
        $nodejs.path
       
            The shortcut for the request( 'path' ) object
       
        $nodejs.response
       
            The response object that controls what is passed back to the browser. This can be updated from any .js
            file that your process calls.
       
        $nodejs.request
       
            The original request passed from the server. You might want to, for example, use this to examine the header.
       
        $nodejs.filename
       
            The full file path of the resource requested in the request. or "/" if no resource is specified.
       
        $nodejs.postBody
       
            The JSON object containing all post data value pairs.
       
        $nodejs.content
       
            The JSON object representing the url data
       
        $nodejs.isPost
       
            A boolean flag indicating if the request was a HTTP POST
       
        $nodejs.logToConsole
       
            A boolean indicating if activity should be logged to the console. This is initially set when the
            listener first starts, but you can programatically change its vaue at any time.
       
        $nodejs.queryString
       
            The JSON object containing all query string parameter value pairs.

*/
generalRequestHandler = (function() {

    var $nodejs = {

        // common dependencies
        url: require( 'url' ),
        path: require( 'path' ),
           
        // holds http data
        response: null,
        request: null,
        filename: null,
        postBody: {},
        content: null,
        isPost: false,
        logToConsole: false,
        queryString: null,
       
        //******************************** YOUR CODE IS NEEDED HERE *********************************************

        // this local method runs you code to interrogate post fields and query string parameters
        // ACTION: Put your code in this method and ensure it returns true or false.
        // if it returns true, then the default response action is performed
        // if it returns false, then it is assumed that this method is outputting the response (via $nodejs.response)
        preHandleRequest: function() {
            // if this method returns true then the request will try to load / handle the requested resource
            // you can write code here to examine query_string parameters or post fields.
            // use $nodejs.getPostField( fieldName ) to extract POST fields
            // use $nodejs.getQueryParameter( paramName ) to extract QueryString parameters
            // see $nodejs.filename for the name of the resource being requested
            // see $nodejs.isPost to determine if the request includes post data
            // see $nodejs.queryString to see the quesrString parameter object
            return true;
        },
       
        // this local method runs if your listener is activated without a resource being specified (i.e. $nodejs.filename = "/")
        // ACTION: Put your code in this method to take default action, such as displaying index.html
        takeDefaultAction: function() {
            // ACTION: This assumes a JavaScript file called default.js exists in the root directory and has an exported method called init()
            // ACTION: Remove or change the following line of code.
            $nodejs.executeScript( '/default.js' );
        },
       
        // **************************************** END OF YOUR CODE *******************************************
       
        // ========================== LOCAL METHODS (that your code may call) ===================================
       
        // this local method does a quick page write. If chunkType is 'binary' then exclude the contentType parameter.
        returnPage: function( statusNum, chunk, chunkType, contentType ) {
            $nodejs.response.writeHead( statusNum, ((contentType !== undefined)?{ "Content-Type": contentType }:contentType) );
            $nodejs.response.end( chunk, chunkType );
        },
       
        // this local method executes a JavaScript (.js) file
        // All scripts MUST have an export method called init() that will receive the entire $nodejs object as a parameter
        // Your script can (optionally) return true or false.
        executeScript: function( filename ) {
            try {
                return require( filename ).init( $nodejs );
            } catch (e) {
                $nodejs.handleLoadError( e );
                return false;
            }
        },
       
        // this local method returns a post field value (or null if field not passed in post)
        getPostField: function( fieldName ) {
            return $nodejs.getNamedValue( fieldName, $nodejs.postBody );
        },
       
        // this local method returns the value of a query-string parameter (or null if parameter was not passed)
        getQueryParameter: function( paramName ) {
            return $nodejs.getNamedValue( fieldName, $nodejs.queryString );
        },
       
        // this local method handles returning a page to say that a resource ($nodejs.filename) was not found on the server
        handleResourceNotFound: function() {
            if ($nodejs.logToConsole) { console.log( '404 Not Found' ); }
            // ACTION: You may wish to display a more user-friendly "Not Found" message
            $nodejs.returnPage( 404, '404 Not Found', 'utf-8', 'text/plain' );   
        },
       
        // this local method handles returning an error message if a page fails to load
        handleLoadError: function( error ) {
            if ($nodejs.logToConsole) { console.log( '500 '+error ); }
            // ACTION: You may wish to display a more user-friendly error message
            $nodejs.returnPage( 500, ''+error , 'utf-8', 'text/plain' );       
        },
       
        // this local method is used to return the value of a named JSON variable
        getNamedValue: function( itemName, itemContainer ) {       
            itemName = itemName.toLowerCase();
            var giveBack = null;
            for (var item in itemContainer) {
                if (item.toLowerCase() === itemName) {
                    giveBack = itemContainer[ item ];
                    break;
                }
            }           
            return giveBack;               
        }
       
    };

    // ========================== INTERNALS (don't change ) ============================================
    var $internal = {
       
        // this internal method will be running as the http request handler, dealing with any connections to your port
        init:function( port, logToConsole  ) {
            require('http').createServer( function (request, response) {

                // store http data for use in all local methods
                $nodejs.request = request;
                $nodejs.response = response;
                $nodejs.content = $nodejs.url.parse($nodejs.request.url,true);
                $nodejs.queryString = $nodejs.content.query;
                $nodejs.filename = $nodejs.path.join(process.cwd(),$nodejs.content.pathname);
                $nodejs.logToConsole = logToConsole;
               
                if ($nodejs.logToConsole) { console.log('Request from '+request.connection.remoteAddress+': '+$nodejs.filename); }
               
                // If handling a post, then ensure that we pull in all the post data before handling the request
                if ($nodejs.request.method === 'POST') {
               
                    var postBody = '';
                   
                    // As data arrives in the post add it to the local data container
                    $nodejs.request.on( 'data' , function( data ) { postBody += data; } );
                   
                    // When the post completes, create the post data object and then handle the request
                    $nodejs.request.on( 'end' , function() { $nodejs.postBody = require( 'querystring' ).parse( postBody ); $internal.handleRequest( true ); } );
                   
                // If no post, then we can handle the request immediately
                } else {
               
                    $internal.handleRequest( false );
                   
                }
               
            } ).listen(port);
           
            console.log('Server listening on port: '+port+'/');
           
        },

        // this internal method checks for the existence of a requested resource and then tries to load it.
        loadPage: function( exists ) {
       
            // if the resource does not exist then display a 404 error
            if (!exists) {
           
                $nodejs.handleResourceNotFound();
               
            // if a JavaScript (.js) page is being requested then we need to run its public exports.init( $nodejs.response ) method
            } else if ($nodejs.filename.indexOf( '.js' ) > 0) {
           
                // this runs a .js script, passing it the response object so it may output to the browser
                $nodejs.executeScript( $nodejs.filename );
           
            // we need to load the resource and directly send it to the browser
            } else {

                require('fs').readFile( $nodejs.filename, "binary",
                    function( error, binary ) {
                        if (error) {
                            $nodejs.handleLoadError( error );
                        } else {
                            $nodejs.returnPage( 200, binary, 'binary' );
                        }
                    }
                );
            }
        },

        // this internal method handles the request after any passed data has been loaded
        handleRequest: function( isPost ) {
       
            $nodejs.isPost = isPost;

            if ($nodejs.preHandleRequest) {
               
                // Are we loading a page or resource?
                if ($nodejs.filename.length > 2) {
               
                    $nodejs.path.exists( $nodejs.filename , $internal.loadPage );
                   
                // otherwise, take default listener action (test code)
                } else {
               
                    $nodejs.takeDefaultAction()
               
                }
               
            }
           
        }
       
    };
   
    // ======================= PUBLIC INTERFACE (don't change) =====================
    return {
       
        listenOnPort: function( port , logToConsole ) {
            $internal.init( port , logToConsole );
        }
       
    }
   
})();

// ================================= CONFIGURE / START-UP =========================
// ACTION: First parameter is the port to listen on. Second parameter indicates if each request should be logged.
generalRequestHandler.listenOnPort(8080, true);