Web applications

InfoInfo
Search:    

next: Object Oriented Programming previous: SQL & ODBC top: Contents

This chapter introduces two [WWW]web application frameworks provided by Spark. A web application framework is a software framework that is designed to support the development of dynamic websites and Web services.

Using the built-in web server
FastCGI


Using the built-in web server

Spark comes with a tiny, easily configurable and dynamically extensible web server called Fermion. The following code snippet shows how to start an instance of the Fermion web server:


;; my-httpd.ss

(import (net) (http) (aura))


(define httpd (web-server (list 'port 8080)))

(web-server-start httpd (lambda () #t))
(web-server-stop httpd)

The web-server procedure creates a new instance of the server. It takes a list of key-value pairs that serves as the server configuration. Here we configure the 'port to 8080. The valid configurations are:

Fermion starts listening for client connections once the web-server-start procedure is called. It takes two arguments - the web server object and a zero argument procedure. This procedure is evaluated in the loop that waits for client connections. The web server will continue to run until this procedure returns true. The web-server-stop procedure should be called to release the system resources held by the web server object.

Start the web server by loading the script to Spark:


> (load "my-httpd.ss")

Create a simple HTML document in the folder where the server is running:


<!-- hello.html -->

<html>
    <body>hello, world</body>
<html>

Request this page from a browser using the URL: [WWW]http://localhost:8080/hello.html. If the page should display correctly in the browser window, you have your web server running!

Extending the web server with scripts

Fermion can be extended with Spark scripts so as to serve dynamic content. Data submitted by the user over HTTP are forwarded to these scripts. The scripts implement the business logic that process this data and generate appropriate HTML content to be send back to the user.
The easiest way to generate dynamic content is to embed Spark code snippets in HTML. Scripts are embedded within the special tags - <?spark ?>. The following sample shows how to get user input using HTML forms and process the data with an embedded script. First, the HTML that generates the form:

<!-- add.html -->

<html>
  <head>
    <title>Add two numbers</title>
  </head>
  <body>
    <form action="./add.sml" method="GET">
      Number 1: <input type="text" name="num1" /><br />
      Number 2: <input type="text" name="num2" /><br />
     <input type="submit" />
    </form>
  </body>
</html>

The data is submitted to a file called add.sml, which contains Spark code to add the two numbers and HTML to format its display:

<!-- add.sml -->

<html>
  <head>
    <title>Add two numbers</title>
  </head>
  <body>
  <?spark
    "Number1=$num1<br>"
    "Number2=$num2<br>"
    "<b>Result="
    (+ $num1 $num2)
    "</b>"
  ?>
  </body>
</html>

Each line of Spark code is evaluated and the result is embedded in the HTML document. We refer to form variables by prefixing them with a dollar ($) sign. The embedded script preprocessor replace them with corresponding values. It is possible to have a single script file to generate the initial form and do the data processing. The following code demonstrates this:


<!-- regis.sml -->
<!-- Displays a simple registration form and process the data. -->

<html>
  <head>
    <title>Registration</title>
  </head>
  <body>

<?spark
(define (show-form)
  (let ((out (open-output-string)))
    (fprintf out "<form action=\"./regis.sml\">")
    (fprintf out "First Name: <input type=\"text\" name=\"fname\" />")
    (get-output-string out)))

(define (handle-form)
  "<b>First Name=$fname</b>")

(if (= (string-length "$fname") 0)
    (show-form)
    (handle-form))
?>

  </body>
</html>

It is also possible to execute a standalone Spark script, given that it "exports" a procedure that takes a single argument. For instance, the form in add.html (listed above) can submit data to the following script and get back HTML that contains the computed result:


;; file: add.ss

;; The form variables are passed to this procedure as key-value pairs.
;; Returns a string that is send to the browser as the result HTML.
(define (add http)
  (let ((out (open-output-string))
        (num1 (string->number (http-value http "num1")))
        (num2 (string->number (http-value http "num2"))))
    (fprintf out "~a + ~a = ~a" num1 num2 (+ num1 num2))
    (get-output-string out)))

;; "Export" the entry point procedure as the last statement of the script.
add

Modify the form action in add.html to point to add.ss. The server loads add.ss and evaluates the procedure add by passing it the form data. The resulting string is returned to be displayed by the browser.

Server managed sessions

[Note: This feature is an experiment. It may get replaced by a continuations based framework. Currently I am studying this topic in detail. There are arguments for and against this approach. For instance, see these links: [WWW]"Will continutaions continue" and [WWW]"Interaction-Safe State for the Web". As of now, please use embedded scripting or FastCGI for creating real-world Spark web applications. In fact, if you use modern JavaScript libraries like [WWW]"qooxdoo", you will never need server managed sessions!]

The Fermion web framework completely isolates the programmer from the request-response flow of the underlying HTTP traffic. The programmer can model web applications, just like a command line programs that interact with users in a natural way. The web server also maintains a session for each user connection. The state created by a request is available for all subsequent requests in the session. The user can also move back and forth within the session and modify the state. Let us look at a small webapp now. It provides the user with a browser UI were he can add two numbers. It has the following execution flow from a UI perspective:

Present a form where the user can enter first number -> Get and store the number -> Show form for the second number -> Get the second number -> add it to the first and show the result.

This is not much different from a command line program written for the same purpose. The advantage of the Fermion web framework is that it makes writing webapps as easy and natural as writing console programs. Let us see how:


;; add.ss

;; Procedure to present form1.
;; The form's action attribute should point to new-uri.
;; state is a hash table that contains the current state of the session, as of now empty.
(define (get-num1 new-uri state)
  (let ((html (sgml `(html
                      (body
                       (form ((action ,new-uri))
                             "Enter number 1: "
                             (input ((type "text")
                                     (name "num1")))))))))
    ((html 'text))))

;; Procedure to present form1.
;; The form's action attribute should point to new-uri.
;; state is a hash table that contains the current state of the session and it
;; contains an entry 'num1 mapped to the value entered by the use in the first form.
(define (get-num2 new-uri state)
  (let ((html (sgml `(html
                      (body
                       (form ((action ,new-uri))
                             "Enter number 2: "
                             (input ((type "text")
                                     (name "num2")))))))))
    ((html 'text))))

;; Procedure that generates the result HTML.
;; Numbers are added by retrieving their values from the state.
(define (add new-uri state)
  (let* ((s1 (hash-table-get state "num1" null))
         (s2 (hash-table-get state "num2" null))
         (res (number->string (+ (string->number s1)
                                 (string->number s2))))
         (html (sgml `(html
                       (body
                        (b ,res))))))
    ((html 'text))))

;; The last thing a web script should do is to return a list that contains the
;; procedures in the order they should be executed.
(list get-num1 get-num2 add)

Each stage of the web application is encoded as a procedure that takes two arguments: the new uri for the procedure to send its request and the current state of the session. The state is a hash table. The last thing the script should do is to create a list that contains the procedures in the order they should be executed by the server. Copy this script to the folder where the web server is running. It is customary to create a sub-folder called scripts and keep all scripts there. You can execute the webapp by requesting the URL: [WWW]http://localhost:8080/scripts/add.ss.

There are two flags that you can use to control the behavior of a session. The first one is http-keep-alive, which decides if a session can be reused or not. This defaults to #f. You can change this flag by calling (http-keep-alive!) with the state object as the first argument and a boolean value as the second. The http-share-state flag can be used to turn-off sharing of state between two instances of the same session. This will happen if the user clones a browser window. This flag is #t by default. You can turn this off by calling (http-share-state!). These procedures can be called in any of the exported procedures, most desirably in the first one. (In the above example, you should call them in the get-num1 procedure)

Security

The Fermion web server has some basic security features that can be controlled using the configuration parameters. For instance, the size of requests and responses can be limited. Users can add there own security features using hooks. A hook is a procedure that will get executed at specific points of request handling. Fermion can be customized using hooks that will be executed before request processing and before response delivery. Hooks are set using the web-server-hook! procedure. This takes three arguments: the web server object, hook name and hook procedure. Hook names can be either 'before-handle-request or 'before-send-response.

The 'before-handle-request hook procedure will receive three arguments when it is executed before handling each request: the web server object, client connection as returned by socket-accept and an object that represents the parsed http request. This procedure can perform tasks like sending a custom response, closing the connection, caching, maintaining a timeout for the connection etc. It should return #t if it wishes the web servers own response handler to proceed with the request handling.

The following example shows how to maintain a timeout of 5 seconds for each client connection. If the request is not done within in 5 seconds, it is forcefully closed.


(import (net)
          (http)
          (aura))

(define httpd (web-server (list 'port 8080
                                'session-timeout (* 2 60))))

;; This is a 'before-handle-request hook
;; that will watch each client handler thread
;; for a timeout. On timeout, if the thread
;; is still running, it is killed and the socket
;; is closed.
(define (client-timeout-hook web-server-obj
                             client-connection
                             http-request)
  (let ((thrd (current-thread)))
    (thread (lambda ()
              ;; Keep an alarm for 5 seconds.
              (sync (alarm-evt (+ (current-inexact-milliseconds)
                                  (* 5 1000))))
              (cond
               ((thread-running? thrd)
                (write-log web-server-obj
                           (list "Terminating thread ~a on timeout."
                                 thrd))
                (kill-thread thrd)
                (try
                 (socket-close (connection-socket client-connection))
                 (catch (lambda (error)
                          (write-log web-server-obj
                                     '("Error: (socket-close): ~a."
                                       error)))))))))
     ;; The hook lets the web server to generate a response.
    #t))

(web-server-hook! httpd 'before-handle-request client-timeout-hook)
(web-server-start httpd)
(web-server-stop httpd)

The 'before-send-response hook procedure provides an opportunity to examine and customize the HTTP response. Its arguments are: the web server object, client connection and the HTTP response as plain text.


FastCGI

If you want to extend a third-party web server with Spark scripts, you can use the [WWW]FastCGI interface. It is assumed that you have already setup the web server (apache, lighttpd etc) to redirect all requests for ".ss" files to a running instance a FCGI server implemented in Spark. (Look at the documentation of the particular web server on how to configure FCGI). The FastCGI API consists of the following procedures:

(fcgi-recv socket) - Receives a FastCGI request from the web server. socket represents the connection with the web server.
(fcgi-send request response-string) - Sends a response back to the FastCGI request. The request object is the one returned by fcgi-recv.
(fcg-request-parameter request parameter-name) - Returns the value of a HTTP parameter from the request.
(fcgi-request->string request) - Returns the string representation of the request. Useful for debugging.

A FCGI script is a network server in itself. The web server will forward FCGI requests to the FCGI server over TCP/IP. The FCGI server will process the request, generate a dynamic HTML response and send it back to the web sever. The web server will forward this response to the client (browser). Let us rewrite the above add 2 numbers sample using the FCGI API:


(import
        (net)
       (reactor)
       (fcgi))

(define (on-client-connect acceptor client-conn)
  (printf "FCGI client connected.~n") (flush-output)
  (let ((client-socket (car client-conn)))
    (acceptor-add-watch acceptor client-socket 'for-read)))

(define (on-client-read acceptor client-socket)
  (printf "Incoming FCGI request ... ~n")
  (flush-output)
  (try
   (let ((req (fcgi-recv client-socket)))
     (printf "~a~n" (fcgi-request->string req))
     (let* ((num1 (string->number (fcgi-request-parameter req "num1")))
            (num2 (string->number (fcgi-request-parameter req "num2")))
            (res (+ num1 num2))
            (out (open-output-string)))
       (fprintf out "<html><body><b>~a + ~a = ~a</b></body></html>"
                num1 num2 res)
       (fcgi-send req (get-output-string out))))
   (catch (lambda (ex)
            (printf "Error: ~a~n" ex)))
   (finally (lambda ()
              (printf "Closing client resources.~n") (flush-output)
              (acceptor-remove-watch acceptor client-socket 'for-read)
              (socket-close client-socket)))))

(define (on-server-timeout acceptor)
  (printf "Waiting for client, timedout.~n")
  (flush-output))

(define fcgi-server (socket-acceptor))
(acceptor-port! fcgi-server 8888)
(acceptor-on-client-connect! fcgi-server on-client-connect)
(acceptor-on-client-read! fcgi-server on-client-read)
(acceptor-on-server-timeout! fcgi-server on-server-timeout)
(acceptor-open fcgi-server #t (list 3 0))
(define count 0)
(let loop ()
  (try
   (acceptor-watch fcgi-server)
   (if #t;;(< count 10)
       (begin
         (set! count (+ count 1))))
   (catch (lambda (ex) (printf "~a~n" ex))))
  (loop))

(printf "FCGI server exiting ...~n") (flush-output)
(acceptor-close fcgi-server)

We have defined the FCGI server using the reactor framework. It listens on port 8888. When a request arrives from the web server, the on-client-read callback is evaluated. There we use the FCGI API calls to extract and process data from the HTTP request and send back an appropriate response. (Note that we generate the response HTML as plain string. We can also use the aura library instead).

Here is a sample HTML form that can be used to invoke this script:


<html>
    <body>
       <b>Enter two numbers to add: </b>
       <form method="POST" action="./add.ss">
         <input type="text" name="num1"/> + <input type="text" name="num2"/>
         <input type="submit" value="Submit"/>
      </form>
   </body>
</html>

The samples/net/web folder contains a template configuration for the [WWW]Hiawatha web server.

This is a Wiki Spot wiki. Wiki Spot is a 501(c)3 non-profit organization that helps communities collaborate via wikis.