You are looking at historical revision 10338 of this page. It may differ significantly from its current revision.

Spiffy

Description

A small web-server written in Chicken.

Author

Felix Winkelmann. Currently maintained by Peter Bex.

Requirements

Requires the http and sendfile extensions.

Download

spiffy.egg

Documentation

Spiffy is a simple web-server for the Chicken Scheme system. It's quite easy to set up and use and it can be customized in numerous ways.

Note that only very basic functionality is provided, there is no support for encoding styles.

(start-server location: [string: host name] init: [procedure])

Starts the web server. The arguments provided default as follows:

  (start-server location: (get-host-name)
                init: noop)

The location argument is directly passed on to http:make-server. The init argument should be a procedure of zero arguments. This procedure will be called after setting up the connection and changing the user ID. Any other configuration is done via standard SRFI-39 parameters. See the next section for configuration options.

Note that there is currently no way of terminating the server manually; you have to terminate the whole process.

Server configuration

The following SRFI-39 parameters determine configuration of Spiffy. To save you the trouble of reading SRFI-39, you read this by treating them as regular variables, and set them by treating them as functions of one argument, i.e. (spiffy-tcp-port "8080").

spiffy-tcp-port (integer)
The port (as in the --port option). Default: 8080
spiffy-debug-mode (boolean)
Whether debugging information should be printed. Default: #f
spiffy-access-log (string or port)
Filename (string) or port to append access log output to. Default: #f (disabled)
spiffy-error-log (string or port)
Filename (string) or port which error messages from evaluated code should be output. Default: (current-error-port)
spiffy-server-name (string)
The name of the server as given in HTTP responses. Default: (Silly)
spiffy-accept-incoming-host (string)
If given, restricts incoming requests to this hostname. Default: #f
spiffy-user-name (string)
If given, changes to given user after setting up listening port. Default: #f
spiffy-root-path (string)
The path containing the served files. Default: ./web
spiffy-access-denied? (lambda)
A procedure of two arguments that returns #t if access is to be denied. The arguments are the path and filename which is to be served. The procedure is invoked on every step along the way of going from spiffy-root-path up to the actual file. Defaults to a procedure which checks if the file is a dotfile, has the .sspx extension or the path includes .. or ~.
spiffy-vhost-map (alist of string<->lambda pairs)
Maps hosts to lambdas of virtual servers. See the section "Virtual hosts" below. Default: #f
spiffy-index-pages (list of strings)
List of filenames that should be tried in case a request references a directory. Default: '("index.html" "index.ssp" "index.ws")
spiffy-file-type-map (alist of string <-> symbol pairs)
Maps file-extensions to MIME types. Default:
 '(("txt" . text/plain)
   ("xml" . text/xml)
   ("xul" . application/vnd.mozilla.xul+xml)
   ("htm" . text/html)
   ("html" . text/html)
   ("pdf" . application/pdf)
   ("css" . text/css)
   ("bmp" . image/bmp)
   ("ico" . image/x-icon)
   ("gif" . image/gif)
   ("jpg" . image/jpeg)
   ("jpeg" . image/jpeg)
   ("png" . image/png))
spiffy-default-mime-type (symbol)
The default mime type used if the extension of the file was not found in spiffy-file-type-map. Default: application/octet-stream
spiffy-read-block-size (integer)
The size of chunks in which files should be loaded and transferred to the client. Default: 100000
spiffy-cache-limit (integer)
The maximum size of the cache for static file-based resources. Default: 0
spiffy-access-file (filename)
The name of directory-local access-permission files. Default: #f
spiffy-config-file (filename)
The name of the configuration file to load at startup. Default: #f
spiffy-handle-directory (lambda)
A procedure that accepts one parameter (the requested path), which gets invoked when a directory which has no index-file is requested. Default: a lambda which returns a 403 Forbidden status.
spiffy-not-found-handler (lambda)
A procedure of zero parameters that will handle all requests for files that were not found. Default: A lambda which calls the previous http:fallback-handler (returns a 404 status).
spiffy-error-css (string)
The CSS to be used on error pages.
spiffy-exception-handler (lambda)
A procedure of one argument (the exception) that gets invoked to produce an error page when an exception is raised. Defaults to a procedure which displays a default error page which uses spiffy-error-css and shows a backtrace. An error is always logged to the error log before calling this procedure.

Module support

Starting at version 3.0, Spiffy's functionality has become large enough to split out into modules. Currently it has modules for:

Web-scheme
Web-scheme, a functional web programming language on top of Scheme.
CGI
An implementation of the Common Gateway Interface, version 1.1. Use this to run legacy PHP scripts, if you have them.
SSP (Scheme Server Pages)
Spiffy provides a way to embed Scheme code in web pages the way it's usually done for PHP, Embedded Ruby or ASP. You just type your HTML with bits of code sprinkled through it delimited by special "code tags".
Simple-directory-handler
A directory handler that displays a rather simple directory listing, for use with spiffy-handle-directory.

You can run a module simply by adding a <code>use</code> statement to your program and then hooking a file extension handler for that module's handler. Here's an example on how to do this:

 (spiffy-file-ext-handlers `(("ssp" . ,ssp-handler)
                             ("ws" . ,web-scheme-handler)
                             ("php" . ,(cgi-handler* "/usr/pkg/libexec/cgi-bin/php"))))

During execution of file extension handlers (which modules basically are), the following parameters are available, which hold some useful data for the currently handled request:

(current-request)
Holds an object representing the current HTTP request. See HTTP for more information.
(current-urlencoded-arguments)
Holds an a-list containing pairs of strings, which contains URL-encoded arguments. So a request for /foo.html?bar=baz would result in this parameter containing (("bar" . "baz")).
(current-response-code)
A pair containing the status code and the status message sent in the reply. Defaults to (200 . "OK").
(current-response-headers)
An a-list holding the HTTP headers which are sent before the body of the current response. By default this parameter contains (("content-type" . "text/html")). You can modify the existing value, or assign a different one by calling the parameter with a new a-list.
(current-hostname)
The current vhost's hostname.
(current-pathinfo)
The pathinfo -- the part of the URL after a filename (Only available at directory-level, not at vhost level)
(current-path)
The complete URL (including pathinfo, but excluding URL parameters) Mostly useful for not-found-handlers.

Adding dynamic resources

You can add URL resources by invoking the procedure http:add-resource. Procedures can be registered that are invoked when a request refers to a given path. See the documentation for the HTTP extension library Chicken.

  (http:add-resource
    "/index.html"
    (lambda (request args)
      (current-request request)
      (let ([name (car (user-information (current-user-id)))])
        (let ([s (sprintf "<h1>Hello, ~A</h1>" name)])
          (set-header! "Content-type: text/html")
          (set-header! (sprintf "Content-length: ~a" (string-length s))) 
          (write-response-header)
          (printf s)))))

This example is contrived. It is not necessary to set the content-type to text/html because that is the default for resources. It is also not necessary to set the content-length header if you use write-fragment-response.

Content transporters

The content of a served file is read and transmitted using a content-transporter procedure. The default content-transporter reads in the data and forwards it to the client with appropriate Content-length and Content-type headers.

User-defined content-transporters allow customized retrieval and processing of resources, depending on file-type.

(content-transporter TYPE [PROC])
If called with a single argument, this procedure returns the content-transporter procedure for the given TYPE, which should be a symbol like text/html. If the optional argument PROC is given, then the content-transporter for the given type is set to PROC. PROC should accept two arguments: the type TYPE and the filename of the requested resource. Any output generated by PROC is transmitted to the client.

Access to the cache

(cache-read PATH)
Reads a string from the cache, stored under the string PATH and returns it if found, or #f otherwise.
(cache-write PATH STRING)
Writes a string into the cache, stored under PATH. If the cache-limit is not sufficient (see spiffy-cache-limit the data is not stored.

Note that the cache is by default not enabled (spiffy-cache-limit is 0).

(invalidate-cache [PATH])
If the optional argument PATH is given, remove the associated item from the cache. If no argument is given, clear the whole cache.

Logging

If logging is enabled by setting the parameter spiffy-access-log to the pathname of a log-file or a port, then each request will be logged by appending a line of the following format:

 CLIENT-IP DATE-AND-TIME REQUEST-LINE USER-AGENT

Virtual hosts

You can set up virtual hosting by defining a vhost-map like so:

 (spiffy-vhost-map `(("foo\\.bar\\.com" .
                         ,(lambda (continue)
			    (parameterize ((spiffy-file-ext-handlers `(("ssp" . ,ssp-handler) ("ws" . ,web-scheme-handler)))
                                           (spiffy-root-path "/var/www/domains/foo.bar.com"))
			      (continue))))
		     (,(glob->regexp "*.domain.com") .
 			 ,(lambda (continue)
			    (parameterize ((spiffy-file-ext-handlers `(("php" . ,(cgi-handler* "/usr/pkg/bin/php"))))
                                           (spiffy-root-path "/var/www/domains/domain.com"))
		              (continue))))))

In this example, if a client accesses foo.bar.com/mumble/blah.html, the file /var/www/domains/foo.bar.com/mumble/blah.html will be served. Any files ending in .ssp or .ws will be served by the corresponding file type handler. If there's any PHP file, its source will simply be displayed. In case of my.domain.com/something/bar.html, the file /var/www/domains/domain.com/something/bar.html will be served. If there's a .ssp or .ws file there, it will not be interpreted. Its source will be displayed instead. A .php file, on the other hand, will be passed via CGI to the program /usr/pkg/bin/php.

Domain names are mapped to a lambda that sets up any parameters it wants to override from the defaults. The host names are matched using string-match. If the host name is not yet a regexp, it will be converted to a case-insensitive regexp.

Virtual hosting is a HTTP/1.1 feature, so this will only work with HTTP/1.1 clients. Actually, it also works with HTTP/1.0 clients which insert an additional Host: line in the request headers since Spiffy ignores the HTTP version the client sends.

Access files

Fine-grained access-control can be implemented by using so-called access files. When a request for a specific file is made and a file with the name given in the spiffy-access-file parameter exists in any directory between the spiffy-root-dir of that vhost and the directory in which the file resides, then the access file is loaded as an s-expression containing a function and is evaluated with a single argument, the function that should be called to continue processing the request.

This works just like vhosting. The function that gets called can call parameterize to set additional constraints on the code that handles deeper directories.

For example, if we evaluate (spiffy-access-file ".access") before starting the server, and we put the following code in a file named .access into the root-directory, then all accesses from localhost to any file in the root-directory or any subdirectory will be denied:

 (lambda (continue)
   (if (string=? (http:request-ip (current-request)) "127.0.0.1")
       (continue)
       (http:write-error-response 403 "Forbidden")))

If we only want to deny access to files that start with an X, put this in the .access file:

 (lambda (continue)
   (parameterize ((spiffy-access-denied?
                    (lambda (path file) (string-prefix? "X" file))))
     (continue)))

Of course, access files can be used for much more than just access checks. One can put anything in them that could be put in vhost configuration or in top-level configuration.

They are very useful for making deployable web applications, so you can just drop a directory on your server which has its own configuration embedded in an access file in the root directory of the application, without having to edit the server's main configuration files.

Some useful procedures

(write-fragment-response . FRAGMENTS)
Generates a HTTP response and writes the elements of all the FRAGMENTS you pass to it as the contents of the body. A fragment may be a #f or the empty list (which are ignored), a string, character, number, symbol or a list of other fragments. Uses write-response-header internally, so uses the same parameters to determine headers and status code.
(send-static-file FILENAME)
Generates a HTTP response and writes the contents of the given file. Remember to set the Content-Type header if current-response-headers do not set a text/html content-type.
(redirect PATH)
Changes the current value of (current-response-code) and (current-response-headers) to return a redirection response. This is basically the same as redirect-page but is usable in .ssp pages.
(redirect-page PATH)
Generates a 303 See Other HTTP response that redirects the client to PATH. It also outputs a small note that the page was moved. This procedure should be used from vhost configurations or other places where there is no output to send (ie, as a replacement for a call to (continue)).
(spiffy-debug FORMATSTRING ARG ...)
When debugging output has been turned on (via the debug argument to start-server, or by (spiffy-debug-mode #t)), then this is equivalent to (fprintf (current-error-port) FORMATSTRING ARG ...).
(write-response-header)
Writes a HTTP response header to the current output port. This largely equivalent to http:write-response-header, but handles persistent connections properly by checking the HTTP version any Connection request headers and it obeys arguments current-response-code and current-response-headers instead of using keyword arguments.
(current-workdir)
A parameter that holds the current include path. File loading operations like load-once, load-ssp or include-ssp set this parameter, so that nested load/include operations always take place relative the the containing source file.
(htmlize STRING)
Makes STRING suitable for embedding in HTML by replacing <, >, & and " with &lt;, &gt;, &amp; and &quot;.
(htmlize-with-spaces STRING)
Makes STRING suitable for embedding in HTML similar to htmlize, but also replaces spaces with &nbsp; entities and newlines with <br />.
(strip-tags STRING)
Returns STRING with HTML tags removed.
(escape-chars STRING)
Transforms non-alphanumeric characters in STRING into %XX characters sequences.
(load-once FILENAME)
If no file with this name has been loaded, or if the file has been modified since the last time it has been loaded, then it is loaded and evaluated and #t is returned. Otherwise nothing is done and load-once returns #f.

Note that this works only reliable for source files, not for compiled code.

(get-header STRING [DEFAULT])
Retrieves the value of a HTTP header in the current request. STRING should name the header and will be compared (case-insensitive) with any headers passed in the current request object (as given in (current-request)). If no header with this name can be found DEFAULT will be returned (or #f if DEFAULT is not specified).
(set-header! STRING)
Sets a HTTP header in the current response by changing the value of the current-response-headers parameter. STRING should be a valid HTTP header of the form <header-name>: <header-value>. If the header was already set it will be overwritten except in the case of Set-Cookie. In this case, it will just output an additional Set-Cookie header.

Convenience macros

(syntax) (define-http-resource (URL ARGUMENT ...) BODY ...)
Defines a resource-handler (as with http:add-resource) for URL, which should be a string or symbol. the ARGUMENTs should be symbols, which will be bound to URL-encoded or POSTed arguments or they should be of the form (IDENTIFIER DEFAULT) to bind them to a default value, if not provided. The result of BODY ... will be returned as the response, it should be a string containing (X)HTML or XML.

To exit prematurly from the body, or to customize the returned response, the procedure respond will be lexically visible inside the body, and can be called, for example, like this:

  (parameterize ((current-response-code '(200 . "OK")))
    (respond "<html><body>Hello, world</body></html>"))

Calling (respond RESULT) is identical to returning RESULT in the body.

Modules

This section will describe what the various modules that come with Spiffy are and how they work.

ssp-handler

SSP, or Scheme Server Pages, are a way to embed Scheme in HTML pages. Files with an extension of .ssp are handled specifically, by replacing occurrences of certain reserved tags with Scheme code. There are two possible forms, either the long version, where all output is redirected to the HTTP response port, or the short, where the result of the embedded expression is displayed in the response. The tags default to <?scheme and <?, see Configuration for how to change them.

   <html><body>
   <ol><?scheme (for-each (lambda (i) (printf "<li>~S~%" i)) (iota 5))?></ol>
   <br />
   <b><?(call-with-values (lambda () (user-information (current-user-id))) (lambda (name . _) name))?><b>
   </body></html>

would generate for example something like this:

    1. 0
    2. 1
    3. 2
    4. 3
    5. 4 
 (felix x 500 100 /home/felix /bin/bash)

When a .ssp file is loaded the first time, or when it has been modified, then a translation takes place that generates a loadable Scheme source file (with the extension .sspx, in the same directory as the original file) from the original data, so in the above example something like this would be generated:

  (let ()
    (display "<html><body>\n<ol>")
    (for-each (lambda (i) (printf "<li>~S~%" i)) (iota 5))
    (display "</ol>\n<br />\n<b>")
    (display (call-with-values (lambda () (user-information (current-user-id))) (lambda (name . _) name)))
    (display "<b>\n</body></html>\n") )

Note that the body is evaluated in a (let () ...) form.

Note: each request runs in a separate thread, so code in .ssp pages should take care when using global variables.

Configuration

The SSP handler can be configured with the following options:

ssp-close-tag (regexp)
The closing tag for Scheme fragments in .ssp files. Default: ?>
ssp-short-open-tag (regexp)
The opening tag for short fragments. Default: <?
ssp-long-open-tag (regexp)
The opening tag for long fragments. Default: <?scheme
ssp-eval-environment (environment)
The environment passed to eval when evaluating Scheme code inside .ssp-pages. Default: interaction-environment
Procedures

The ssp-handler module adds the following procedures to the environment:

(include-ssp FILENAME)
Translates the file FILENAME into Scheme by replacing <?scheme ... ?> and <? ... ?> sequences (if needed) and writes the translated contents to the current output-port.
(ssp-stringize FILENAME)
Similar to include-ssp, but instead of writing the translated text, the text is returned as a string.
(exit-handler)
During execution of an ssp page, exit-handler is bound to a procedure that will finish the current page, ignoring any further content or code.

web-scheme-handler

Another way of executing Scheme code to produce content are .ws files: these should contain a Scheme expression that is expected to evaluate to a string which will be directly written as the response to the current request. This facility is intended for Scheme code that uses the web-scheme extension.

You can use the web-scheme-handler for any Scheme file which returns HTML as a string or which has a side-effect of outputting the HTML. If it's the latter, make sure the final statement in your file does not return a string or it will be appended to the output (just like in the csi REPL).

Tip This handler type is perfect not only for web-scheme but also for when you're using SRV:send-reply with SXML or for example a wiki-to-string translator.

Note: each request runs in a separate thread, so code in .ws pages should take care when using global variables.

Configuration

The Web-scheme handler can be configured with the following options:

web-scheme-eval-environment (environment)
The environment passed to eval when evaluating Scheme code inside .ws-pages. Default: interaction-environment

cgi-handler

Spiffy supports a subset of the CGI/1.1 spec. All request headers will be passed as environment variables to the CGI program, prefixed with "HTTP_", and converted to uppercase, with hyphens ("-") replaced by an underscore ("_"). The CGI program will receive the request body in unparsed form from stdin and should write a complete HTTP response to stdout. Any headers that are missing but required for HTTP will be added by Spiffy.

The following environment variables are currently not set during invocation of CGI subprocesses:

  AUTH_TYPE
  PATH_TRANSLATED
  REMOTE_HOST
  REMOTE_IDENT
  REMOTE_USER

The cgi-handler procedure will simply execute the cgi binary directly. This is useful if you have files with a special extension or in a directory on your server than can be run directly.

The cgi-handler* procedure is usually more useful. It allows you to define an interpreter to use for files and returns a file handler. See the example above for spiffy-file-ext-handlers.

Configuration

CGI handler can be configured with the following parameters:

spiffy-cgi-default-environment (alist of string->string)
The environment variables that should be in the default environnment of every CGI program. Variables like SCRIPT_NAME will be added dynamically to the end of this alist. Default:
(("SERVER_SOFTWARE" . ,(spiffy-server-name))
 ("GATEWAY_INTERFACE" . "CGI/1.1")
 ("REDIRECT_STATUS" . "200"))

simple-directory-handler

In order to get directory listings, you can use simple-directory-handler. Just assign the simple-directory-handler to spiffy-handle-directory and you're set.

Configuration

The simple directory handler has a few configuration options:

simple-directory-css (string)
The CSS which is included in every page to style it. Defaults to a simple CSS string that adds some padding to the table and gives the page a mustard-colored background.
simple-directory-dotfiles? (bool)
Determines if dotfiles should show up in the directory listings. Default: #f
simple-directory-display-file (lambda)
A lambda that accepts three arguments: the remote filename, the local filename and a boolean that says if the file is a directory. This lambda should output a table row with the desired information. Defaults to a lambda that prints the name, size and date when the file was last modified.

Changelog

License

 Copyright (c) 2005-2007, Felix L. Winkelmann and Peter Bex
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:
 
 Redistributions of source code must retain the above copyright
 notice, this list of conditions and the following disclaimer.
 
 Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.
 
 Neither the name of the author nor the names of its contributors may
 be used to endorse or promote products derived from this software
 without specific prior written permission.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 OF THE POSSIBILITY OF SUCH DAMAGE.