Serving up HTML content and web files within the same From HTTPS channel

Here’s an easy way to write an “all-in-one” web server for your web application running from an Iguana “From HTTPS” channel. The idea is that a request coming in with the ‘resource’ parameter will be treated as a file request. A request coming in with an ‘ajax’ parameter will choose a request handler from a list of “ajax request handlers”. And any other request will just be handled by the root page handler (called “indexPage”, in this case). I like this approach because it keeps these resources separate from the main location (URL) of the channel. If desired, one can simply make all resources sub-locations of the main URL, and these requests will be handled by the same channel (as of Iguana 5.5.5).

Let’s say that the “URL path” for the From HTTPS component is “web_demo”. Then here are some example URLs:

  • http://<hostname>:<port>/web_demo – will return an HTML page, the contents of which are generated by indexPage().
  • http://<hostname>:<port>/web_demo?resource=my.css – will return my.css if and only if it is in the project’s “other” files (notice that serveWebDoc() looks for the requested file in the iguana.project.files() table). This is important for a safe web application. It prevents users from accessing any file from the web server. It also helps keep your web files (.css, .js, image files, etc.) as part of the project’s Milestone.
  • http://<hostname>:<port>/web_demo?ajax=getHello&<additional parameters> – will return the results of getHello(). Note that additional parameters can be passed along with the ‘ajax’ parameter if needed.
-- The main page that is loaded when you go to the URL for this channel.
local function indexPage()
   net.http.respond{body=
[[<html>
   <head>
      <title>Sample Web Application</title>
      <link rel="stylesheet" href="?resource=my.css"></link>
      <script type="text/javascript" src="?resource=jquery-1.10.1.min.js"></script>
      <script type="text/javascript"><!--
         /* In this script you can now use the jQuery library, since it's included above */
         $(document).ready(function(){
            $('#more').click(function(){
               jQuery.post('', {ajax:'getHello'}, function(data){
                  $(data).insertBefore($('#more')).slideDown('fast');
               });
            });
         });
      --></script>
   </head>
   <body>
      <p>Hello World</p>
      <button id="more">More...</button>
   </body>
</html>]]}
end

-- Server-side state
local ExtraHelloCount = 1

-- AJAX request handler
local function getHello(Params)
   net.http.respond{entity_type='text/plain',
      body=
[[<p class="moreHello">
   Hello Again (]]..ExtraHelloCount..[[)
</p>]]}
   ExtraHelloCount = ExtraHelloCount + 1
end

-- The functions accepts the HTTP request as a parameter.
local AjaxHandlers = {
   ['getHello']=getHello
}

local ContentTypeMap = {
   ['.js']  = 'application/x-javascript',
   ['.css'] = 'text/css',
   ['.gif'] = 'image/gif'
}

local function contentTypeFromFileName(FileName)
   local Ext = FileName:match('.*(%.%a+)$')
   if ContentTypeMap[Ext] then
      return ContentTypeMap[Ext]
   else
      return 'text/plain'
   end
end

-- Serve up web docs (.css, .js files) from the "other" files in this project.
local function serveWebDoc(Params)
   if Params.resource then
      local ResourcePath = 'other/'..Params.resource
      if iguana.project.files()[ResourcePath] then
         local F = io.open(iguana.project.files()[ResourcePath], 'rb')
         local WD = ((F and F:read('*a')) or nil)
         if F then F:close() end
         local T = contentTypeFromFileName(Params.resource)
         local HttpResponse = net.http.respond{entity_type=T, body=WD}
         iguana.logDebug(HttpResponse)
         return true
      else
         iguana.logError('Request resource "'..ResourcePath..'" was not found')
         pcall(function()
               net.http.respond{entity_type='text/plain', body=
                  'The resource requested was not found.', code=404}
             end)
      end
   end
   return false
end

-- The main entry point: all HTTP requests routed to this
-- channel come in here.
function main(Data)
   local R = net.http.parseRequest{data=Data}

   if serveWebDoc(R.params) then return end

   if AjaxHandlers[R.params.ajax] then
      local Handler = AjaxHandlers[R.params.ajax]
      if iguana.isTest() then
         Handler(R.params)
      else
         local Success, Err = pcall(Handler, R.params)
         if not Success then
            iguana.logError(tostring(Err))
            pcall(function()
                  net.http.respond{entity_type='text/json',
                     body=json.serialize{data={ErrorMessage=tostring(Err)}}}
               end)
         end
      end
      return
   end

   local Handler = indexPage
   if iguana.isTest() then
      Handler(R.params)
   else
      local Success, Err = pcall(Handler, R.params)
      if not Success then
         iguana.logError(tostring(Err))
         pcall(function()
               net.http.respond{entity_type='text/html', body=
                  '<html><head><title>Error</title></head><body>'..
                  '<div class="err">'..filter.html.enc(tostring(Err))..
                  '</div></body></html>', code=500}
            end)
      end
   end
end

Note that the Javascript embedded in the HTML could itself be stored in a referenced external Javascript file. Even the HTML itself could be loaded from a file, rather than embedded in your Lua script. Or one could consider generating HTML using the ltp Lua template processor, allowing php-like HTML generation (but with Lua as the server-side scripting language). The sky is the limit for how this content is generated, with the power of Lua and the Translator at your fingertips!

Here is the complete working sample demo (don’t expect it to actually do anything useful, but it is a good starting point for any web application).

Web_Content_Demo_From_HTTPS.zip

To use this zip:

  1. Create a new channel (“From HTTPS”, “To Channel”).
  2. Set the URL path to “web_demo” (in channel properties for the From HTTPS Source).
  3. Import this zip into the source’s Translator. The project is complete with sample data that is the input: HTTP requests. You can flip through each type of request in the sample data and see how the script processes it.
  4. Save your initial milestone.
  5. Start the channel.
  6. Click the hyperlink that appears in the Source Component configuration (http://localhost:6544/web_demo or similar). You should see your page has loaded, with its required external .css and .js files, and you will be able to make trivial AJAX requests by pressing the More… button.

There are some neat tricks you can use to test your output more rapidly. For example, if iguana.isTest(), then write out the HTML to a file, test.html, which will get recreated each time the script runs in the IDE. Open this file in your favourite browser, and refresh each time you want to see your changes; no need to re-save milestones and re-start the channel.

Happy serving!

Kevin Senn