diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) Sebastiaan Visser 2008
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. 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.
+3. Neither the name of the author nor the names of his contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 AUTHORS 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.
+
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,6 @@
+#! /usr/bin/env runhaskell
+
+>import Distribution.Simple
+
+>main = defaultMain
+
diff --git a/salvia.cabal b/salvia.cabal
new file mode 100644
--- /dev/null
+++ b/salvia.cabal
@@ -0,0 +1,71 @@
+Name:             salvia
+Version:          0.0.4
+Description:      Lightweight Haskell Web Server Framework
+Synopsis:         Lightweight Haskell Web Server Framework 
+Category:         Network, Web
+License:          BSD3
+License-file:     LICENSE
+Author:           Sebastiaan Visser
+Maintainer:       sfvisser@cs.uu.nl
+Build-Type:       Simple
+Build-Depends:    base,
+                  bimap,
+                  bytestring,
+                  clevercss,
+                  containers,
+                  directory,
+                  encoding,
+                  filepath,
+                  hscolour,
+                  mtl ==1.1.0.2,
+                  network,
+                  old-locale,
+                  parsec,
+                  process,
+                  random,
+                  stm,
+                  time,
+                  utf8-string
+GHC-Options:      -threaded -Wall -fno-warn-orphans
+Extensions:       CPP
+HS-Source-Dirs:   src
+Other-modules:    Misc.Misc,
+                  Misc.Terminal
+Exposed-modules:  Network.Protocol.Cookie,
+                  Network.Protocol.Http,
+                  Network.Protocol.Mime,
+                  Network.Protocol.Uri,
+                  Network.Salvia.Httpd,
+                  Network.Salvia.Core.Config,
+                  Network.Salvia.Core.Handler,
+                  Network.Salvia.Core.IO,
+                  Network.Salvia.Core.Main,
+                  Network.Salvia.Core.Network,
+                  Network.Salvia.Handlers.Banner,
+                  Network.Salvia.Handlers.CGI,
+                  Network.Salvia.Handlers.Cookie,
+                  Network.Salvia.Handlers.Counter,
+                  Network.Salvia.Handlers.Default,
+                  Network.Salvia.Handlers.Directory,
+                  Network.Salvia.Handlers.Dispatching,
+                  Network.Salvia.Handlers.Error,
+                  Network.Salvia.Handlers.ExtensionDispatcher,
+                  Network.Salvia.Handlers.Fallback,
+                  Network.Salvia.Handlers.File,
+                  Network.Salvia.Handlers.FileSystem,
+                  Network.Salvia.Handlers.Head,
+                  Network.Salvia.Handlers.Log,
+                  Network.Salvia.Handlers.Login,
+                  Network.Salvia.Handlers.MethodRouter,
+                  Network.Salvia.Handlers.Parser,
+                  Network.Salvia.Handlers.PathRouter,
+                  Network.Salvia.Handlers.Printer,
+                  Network.Salvia.Handlers.Put,
+                  Network.Salvia.Handlers.Redirect,
+                  Network.Salvia.Handlers.Rewrite,
+                  Network.Salvia.Handlers.Session,
+                  Network.Salvia.Handlers.VirtualHosting,
+                  Network.Salvia.Advanced.CleverCSS,
+                  Network.Salvia.Advanced.ExtendedFileSystem,
+                  Network.Salvia.Advanced.HsColour
+
diff --git a/src/Misc/Misc.hs b/src/Misc/Misc.hs
new file mode 100644
--- /dev/null
+++ b/src/Misc/Misc.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+
+module Misc.Misc (
+    eitherToMaybe
+  , bool
+  , pMaybe
+  , (@@)
+  , safeLast
+  , guardMaybe
+  , ifM
+  , split
+  , withReverse
+  , intersperseS
+  , trim
+  , normalCase
+  , intRead
+  , now
+  , later
+  , safeRead
+  , safeHead
+  , atomModTVar
+  , atomReadTVar
+  , atomWithTVar
+  ) where
+
+import Control.Applicative 
+import Control.Concurrent.STM
+import Control.Monad.Identity
+import Data.Char
+import Data.Time.Clock
+import Data.Time.LocalTime
+import Text.ParserCombinators.Parsec hiding (many, optional, (<|>))
+
+-------[ common utilities ]----------------------------------------------------
+
+bool :: a -> a -> Bool -> a
+bool a b c = if c then a else b
+
+safeRead :: Read a => String -> Maybe a
+safeRead s = case reads s of
+  [(x, "")] -> Just x
+  _         -> Nothing
+
+intRead :: String -> Maybe Int
+intRead = safeRead
+
+-- Conversions.
+
+guardMaybe :: Bool -> Maybe ()
+guardMaybe = bool (Just ()) Nothing
+
+eitherToMaybe :: Either a b -> Maybe b
+eitherToMaybe = either (const Nothing) Just
+
+-------[ list utilities ]------------------------------------------------------
+
+safeList :: ([a] -> b) -> [a] -> Maybe b
+safeList _ [] = Nothing
+safeList f xs = Just $ f xs
+
+safeHead :: [a] -> Maybe a
+safeHead = safeList head
+
+safeLast :: [a] -> Maybe a
+safeLast = safeList last
+
+withReverse :: ([a] -> [b]) -> [a] -> [b]
+withReverse f = reverse . f . reverse
+
+trimWith :: (a -> Bool) -> [a] -> [a]
+trimWith f = withReverse (dropWhile f) . dropWhile f
+
+split :: Eq a => a -> [a] -> [[a]]
+split c = splitWith (==c)
+
+splitWith :: (a -> Bool) -> [a] -> [[a]]
+splitWith f xs =
+  case span (not . f) (dropWhile f xs) of
+   ([], []) -> []
+   (a,  []) -> [a]
+   (a,  b)  -> a : splitWith f b
+
+-------[ text manipulation ]---------------------------------------------------
+
+normalCase :: String -> String
+normalCase ""     = ""
+normalCase (x:xs) = toUpper x : map toLower xs
+
+-- Trim all heading and trailing whitespace.
+trim :: String -> String
+trim = trimWith (`elem` " \t\n\r")
+
+-- ShowS functions.
+
+intersperseS :: ShowS -> [ShowS] -> ShowS
+intersperseS _ []  = id
+intersperseS _ [s] = s
+intersperseS c xs  = foldl1 (\a -> ((a.c).)) xs
+
+-------[ second arity functor ]------------------------------------------------
+
+class Functor2 f where
+  fmap2 :: (a -> c) -> (b -> d) -> f a b -> f c d
+
+instance Functor2 (,) where
+  fmap2 f g (a, b) = (f a, g b)
+
+instance Functor2 Either where
+  fmap2 f _ (Left  a) = Left  (f a)
+  fmap2 _ g (Right b) = Right (g b)
+
+-------[ monadic expressions ]-------------------------------------------------
+
+ifM :: Monad m => m Bool -> (a -> a) -> a -> m a
+ifM t f g = t >>= (\b -> return $ if b then f g else g)
+
+-------[ parsec extensions ]---------------------------------------------------
+
+-- Helper function to quickly apply a parser.
+(@@) :: GenParser Char () a -> String -> Maybe a
+(@@) p b = either (const Nothing) Just $ parse (p <* eof) "" b
+
+-- Option parser with maybe result.
+pMaybe :: GenParser a b c -> GenParser a b (Maybe c)
+pMaybe = option Nothing . liftM Just
+
+-- Make parsec both applicative and alternative.
+instance Applicative (GenParser s a) where
+  pure  = return
+  (<*>) = ap
+
+instance Alternative (GenParser s a) where
+  empty = mzero
+  (<|>) = mplus
+
+-------[ time utils ]----------------------------------------------------------
+
+later :: Integer -> IO LocalTime
+later howlong = do
+  zone <- getCurrentTimeZone
+  time <- liftM (addUTCTime $ fromInteger howlong) getCurrentTime
+  return $ utcToLocalTime zone time
+
+now :: IO LocalTime
+now = later 0
+
+-------[ concurrency utils ]---------------------------------------------------
+
+atomModTVar :: (a -> a) -> TVar a -> IO a
+atomModTVar f v = atomically $ do
+  t <- readTVar v
+  writeTVar v (f t)
+  return (f t)
+
+atomReadTVar :: TVar a -> IO a
+atomReadTVar = atomically . readTVar
+
+atomWithTVar :: (a -> b) -> TVar a -> IO b
+atomWithTVar f v = atomically
+  $ liftM f (readTVar v)
+  
diff --git a/src/Misc/Terminal.hs b/src/Misc/Terminal.hs
new file mode 100644
--- /dev/null
+++ b/src/Misc/Terminal.hs
@@ -0,0 +1,211 @@
+module Misc.Terminal (
+
+    esc
+
+  , clearAll
+  , clearEol
+  , clear   
+
+  , move
+  , moveUp     
+  , moveDown   
+  , moveBack   
+  , moveForward
+
+  , save
+  , load
+
+  , clr
+  , fg
+  , bg
+
+  , normal   
+  , bold     
+  , faint    
+  , standout 
+  , underline
+  , blink    
+  , inverse  
+  , invisible
+
+  , Color (..)
+
+  , reset
+  , black  
+  , red    
+  , green  
+  , yellow 
+  , blue   
+  , magenta
+  , cyan   
+  , white  
+
+  , blackBold  
+  , redBold    
+  , greenBold  
+  , yellowBold 
+  , blueBold   
+  , magentaBold
+  , cyanBold   
+  , whiteBold  
+
+  , blackBg  
+  , redBg    
+  , greenBg  
+  , yellowBg 
+  , blueBg   
+  , magentaBg
+  , cyanBg   
+  , whiteBg  
+  , resetBg  
+
+  , width
+  , height
+  , geometry
+  ) where
+
+import Control.Applicative
+import Data.List (intercalate)
+import System.Environment (getEnvironment)
+
+--------[ ansi escape sequence generation ]------------------------------------
+
+-- Generic function for producing ANSI escape sequences.
+esc :: String -> [String] -> String -> String
+esc a args b = concat ["\ESC[", a, intercalate ";" $ args, b]
+
+-- Clear screen and end-of-line
+clearAll, clearEol, clear :: String
+clearAll = esc "2J" [] ""
+clearEol = esc "K"  [] "" 
+clear    = clearAll ++ move 1 1
+
+-- Move the cursor to the specified row and column.
+move :: Int -> Int -> String
+move row col = esc "" [show col, show row] "H"
+
+-- Relative cursor movements.
+moveUp, moveDown, moveBack, moveForward :: Int -> String
+
+moveUp      rs = esc "" [show rs] "A"
+moveDown    rs = esc "" [show rs] "B"
+moveBack    cs = esc "" [show cs] "D"
+moveForward cs = esc "" [show cs] "C"
+
+-- Load and store the current cursor position.
+save :: String
+save = esc "s" [] ""
+
+load :: String
+load = esc "u" [] ""
+
+-- Generic function for creating (foreground) color sequences.
+clr :: [String] -> String
+clr codes = esc "" codes "m"
+
+-- Create foreground and background colors.
+fg :: Color -> [String]
+fg c = [show ((num c :: Int) + 30)]
+
+bg :: Color -> [String]
+bg c = [show ((num c :: Int) + 40)]
+
+-- Style modifiers.
+normal, bold, faint, standout, underline, blink, inverse, invisible
+  :: [String] -> [String]
+
+normal    = ("0":)
+bold      = ("1":)
+faint     = ("2":)
+standout  = ("3":)
+underline = ("4":)
+blink     = ("5":)
+inverse   = ("7":)
+invisible = ("8":)
+
+--------[ ansi color listing ]-------------------------------------------------
+
+data Color =
+    Black
+  | Red
+  | Green
+  | Yellow
+  | Blue
+  | Magenta
+  | Cyan
+  | White
+  | Reset
+  deriving (Show, Eq)
+
+-- Ansi codes offsets for color values.
+num :: Num a => Color -> a
+num Black   = 0
+num Red     = 1
+num Green   = 2
+num Yellow  = 3
+num Blue    = 4
+num Magenta = 5
+num Cyan    = 6
+num White   = 7
+num Reset   = 9
+
+--------[ shortcut functions for common actions ]------------------------------
+
+-- Reset all color and style information.
+reset :: String
+reset = esc "" ["0", "39", "49"] "m"
+
+-- Shortcut for setting foreground colors.
+black, red, green, yellow, blue,
+  magenta, cyan, white :: String
+
+black   = clr $ fg Black
+red     = clr $ fg Red
+green   = clr $ fg Green
+yellow  = clr $ fg Yellow
+blue    = clr $ fg Blue
+magenta = clr $ fg Magenta
+cyan    = clr $ fg Cyan
+white   = clr $ fg White
+
+-- Shortcut for setting bold foreground colors.
+blackBold, redBold, greenBold, yellowBold, blueBold,
+  magentaBold, cyanBold, whiteBold :: String
+
+blackBold   = clr $ bold $ fg Black
+redBold     = clr $ bold $ fg Red
+greenBold   = clr $ bold $ fg Green
+yellowBold  = clr $ bold $ fg Yellow
+blueBold    = clr $ bold $ fg Blue
+magentaBold = clr $ bold $ fg Magenta
+cyanBold    = clr $ bold $ fg Cyan
+whiteBold   = clr $ bold $ fg White
+
+-- Shortcut for setting background colors.
+blackBg, redBg, greenBg, yellowBg, blueBg,
+  magentaBg, cyanBg, whiteBg, resetBg :: String
+
+blackBg   = clr $ bg Black
+redBg     = clr $ bg Red
+greenBg   = clr $ bg Green
+yellowBg  = clr $ bg Yellow
+blueBg    = clr $ bg Blue
+magentaBg = clr $ bg Magenta
+cyanBg    = clr $ bg Cyan
+whiteBg   = clr $ bg White
+resetBg   = clr $ bg Reset
+
+--------[ terminal geometry ]--------------------------------------------------
+
+-- Try to read terminal width from environment variable.
+width :: IO Int
+width = (maybe 80 read . lookup "COLUMNS") <$> getEnvironment
+
+-- Try to read terminal height from environment variable.
+height :: IO Int
+height = (maybe 24 read . lookup "LINES") <$> getEnvironment
+
+-- Try to read terminal width and height from environment variables.
+geometry :: IO (Int, Int)
+geometry = (,) <$> width <*> height
+
diff --git a/src/Network/Protocol/Cookie.hs b/src/Network/Protocol/Cookie.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Protocol/Cookie.hs
@@ -0,0 +1,165 @@
+module Network.Protocol.Cookie (
+    Cookie (..)
+  , empty
+
+  , Cookies
+  , cookies
+  , cookie
+  , showCookies
+
+  , parseCookies
+  ) where
+
+import Control.Monad ()
+import Control.Applicative hiding (empty)
+import Data.Char (toLower)
+import Data.List (intercalate)
+import Text.ParserCombinators.Parsec hiding (many, optional, (<|>))
+import qualified Data.Map as M
+
+import Misc.Misc (intersperseS, (@@), trim)
+import Network.Protocol.Uri (URI)
+
+-- For more information:
+-- http://www.ietf.org/rfc/rfc2109.txt
+
+-------[ HTTP cookie data type ]-----------------------------------------------
+
+data Cookie =
+  Cookie {
+    name       :: String
+  , value      :: String
+  , comment    :: Maybe String
+  , commentURL :: Maybe URI
+  , discard    :: Bool
+  , domain     :: Maybe String
+  , maxAge     :: Maybe Int
+  , expires    :: Maybe String
+  , path       :: Maybe String
+  , port       :: [Int]
+  , secure     :: Bool
+  , version    :: Int
+  }
+
+-- Create an empty cookie.
+empty :: Cookie
+empty = Cookie {
+    name       = ""
+  , value      = ""
+  , comment    = Nothing
+  , commentURL = Nothing
+  , discard    = False
+  , domain     = Nothing
+  , maxAge     = Nothing
+  , expires    = Nothing
+  , path       = Nothing
+  , port       = []
+  , secure     = False
+  , version    = 0
+  }
+
+-- A set of multiple cookies. These can all be set in one single HTTP
+-- Set-Cookie header field.
+
+type Cookies = M.Map String Cookie
+
+-- Convert a list of cookies into a cookie mapping. The name will be used as
+-- the key, the cookie itself as the value.
+cookies :: [Cookie] -> Cookies
+cookies = M.fromList . map (\a -> (name a, a))
+
+cookie :: String -> Cookies -> Maybe Cookie
+cookie n = M.lookup (map toLower n)
+
+-------[ cookie show instance ]------------------------------------------------
+
+instance Show Cookie where
+  show = flip showsCookie ""
+
+-- Show a semicolon separated list of attribute/value pairs. Only meta pairs
+-- with significant values will be pretty printed.
+showsCookie :: Cookie -> ShowS
+showsCookie c =
+    pair     (name c)     (value c)
+  . opt      "comment"    (comment c)
+  . opt      "commentURL" (fmap show $ commentURL c)
+  . bool     "discard"    (discard c)
+  . opt      "domain"     (domain c)
+  . opt      "maxAge"     (fmap show $ maxAge c)
+  . opt      "expires"    (expires c)
+  . opt      "path"       (path c)
+  . list     "port"       (map show $ port c)
+  . bool     "secure"     (secure c)
+  . opt      "version"    (optval $ version c)
+  where
+    attr a       = showString a
+    val v        = showString ("=" ++ v)
+    end          = showString "; "
+    single a     = attr a . end
+    pair a v     = attr a . val v . end
+    opt a        = maybe id (pair a)
+    list _ []    = id
+    list a xs    = pair a $ intercalate "," xs
+    bool _ False = id
+    bool a True  = single a
+    optval 0     = Nothing
+    optval i     = Just (show i)
+
+-- Show multiple cookies, pretty printed using a comma separator.
+showCookies :: Cookies -> String
+showCookies = ($"")
+  . intersperseS (showString ", ")
+  . map (shows . snd)
+  . M.toList
+
+-------[ cookie parser ]-------------------------------------------------------
+
+-- The top-level cookie parser. This parser will return just a mapping of
+-- cookies or nothing on failure.
+parseCookies :: String -> Maybe Cookies
+parseCookies = fmap cookies . (pCookie @@)
+
+{-
+Parse a set of cookie values and turn this into a collection of real cookies.
+As the specification states, only the name, value, domain, path and port will
+be recognized.
+-}
+
+pCookie :: GenParser Char st [Cookie]
+pCookie = map ck <$> pCookieValues
+  where
+    ck ((n, v), m) =
+      empty {
+        name   = n
+      , value  = v
+      , domain = "$domain" `M.lookup` m
+      , path   = "$path"   `M.lookup` m
+      , port   = maybe [] pPorts $
+                 "$port"   `M.lookup` m
+      }
+
+-- Parse a list of comma separated portnumbers. Returns a list of integers or
+-- an empty list on failure.
+pPorts :: String -> [Int]
+pPorts = either (const []) id . parse p ""
+   where
+    p = char '"'
+     *> sepBy1 (read <$> many1 digit) (char ',')
+     <* char '"'
+
+-- Parse a collection of cookie name/value pairs.
+pCookieValues :: GenParser Char st [((String, String), M.Map String String)]
+pCookieValues =
+      flip sepBy1 sep ((,)
+  <$> pair key
+  <*> (M.fromList
+  <$> filter (not . null . fst)
+  <$> many (try (sep *> pair skey))))
+  where
+    f      = trim . map toLower
+    key    = f <$> many1 (noneOf "=;")
+    skey   = f <$> ((:) <$> char '$' <*> many (noneOf "=;"))
+    val    = trim <$> many (noneOf ";")
+    pair k = (,) <$> k <*> (char '=' *> val)
+    sep    = char ';' *> many (oneOf " \t\r\n")
+
diff --git a/src/Network/Protocol/Http.hs b/src/Network/Protocol/Http.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Protocol/Http.hs
@@ -0,0 +1,388 @@
+{-# LANGUAGE FlexibleInstances #-}
+
+module Network.Protocol.Http(
+
+    Status (..)
+  , Method (..)
+  , methods
+  , Version (..)
+  , Headers
+  , Direction (..)
+  , Message (..)
+
+  , utf8
+  , http10
+  , http11
+  , emptyRequest
+  , emptyResponse
+
+  , method
+  , setMethod
+  , uri
+  , setUri
+  , modUri
+  , status
+  , setStatus
+  , setVersion
+  , setBody
+  , modBody
+  , header
+  , setHeader
+  , modHeader
+  , getLocation
+  , getHost
+  , getContentLength
+  , getKeepAlive
+  , getCookie
+  , setContentType
+  , setContentLength
+  , setLocation
+  , setDate
+  , setServer
+  , setCookie
+  , normalizeHeader
+
+  , pRequest
+  , pResponse
+
+  , showVersion
+  , showHeaders
+  , showMessageHeader
+
+  , statusCodes
+  , statusFailure
+  , statusFromCode
+  , codeFromStatus
+  ) where
+
+import Control.Applicative hiding (empty)
+import Data.Char
+import Data.List (intercalate)
+import Data.Maybe (fromMaybe)
+import Text.ParserCombinators.Parsec hiding (many, optional, (<|>))
+import qualified Data.Bimap as Bm
+import qualified Data.Map as M
+
+import Misc.Misc
+import Network.Protocol.Uri (URI, mkURI, pUriReference)
+import Network.Protocol.Cookie ()
+
+-------- HTTP message data-types ----------------------------------------------
+
+data Status =
+    Continue                      | SwitchingProtocols
+  | OK                            | Created
+  | Accepted                      | NonAuthoritativeInformation
+  | NoContent                     | ResetContent
+  | PartialContent                | MultipleChoices
+  | MovedPermanently              | Found
+  | SeeOther                      | NotModified
+  | UseProxy                      | TemporaryRedirect
+  | BadRequest                    | Unauthorized
+  | PaymentRequired               | Forbidden
+  | NotFound                      | MethodNotAllowed
+  | NotAcceptable                 | ProxyAuthenticationRequired
+  | RequestTimeOut                | Conflict
+  | Gone                          | LengthRequired
+  | PreconditionFailed            | RequestEntityTooLarge
+  | RequestURITooLarge            | UnsupportedMediaType
+  | RequestedRangeNotSatisfiable  | ExpectationFailed
+  | InternalServerError           | NotImplemented
+  | BadGateway                    | ServiceUnavailable
+  | GatewayTimeOut                | HTTPVersionNotSupported
+  | CustomStatus Int
+  deriving (Eq, Ord)
+
+data Method =
+    OPTIONS
+  | GET
+  | HEAD
+  | POST
+  | PUT
+  | DELETE
+  | TRACE
+  | CONNECT
+  deriving (Show, Eq)
+
+methods :: [Method]
+methods = [OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT]
+
+data Version = Version {major :: Int, minor :: Int}
+type Headers = M.Map String String
+
+data Direction =
+    Request  {_method :: Method, _uri :: URI}
+  | Response {_status :: Status}
+
+data Message =
+  Message {
+    direction :: Direction
+  , version :: Version
+  , headers :: Headers
+  , body :: String
+  }
+
+utf8 :: String
+utf8 = "utf-8"
+
+-------- HTTP message creation and alteration ---------------------------------
+
+-- Create HTTP versions.
+http10, http11 :: Version
+http10 = Version 1 0
+http11 = Version 1 1
+
+emptyRequest :: Message
+emptyRequest = Message (Request GET (mkURI)) http11 M.empty ""
+
+emptyResponse :: Message
+emptyResponse = Message (Response OK) http11 M.empty ""
+
+method :: Message -> Method
+method = _method . direction
+
+setMethod :: Method -> Message -> Message
+setMethod e m = m { direction = Request e (uri m) }
+
+uri :: Message -> URI
+uri = _uri . direction
+
+setUri :: URI -> Message -> Message
+setUri u m = m { direction = Request (method m) u }
+
+modUri :: (URI -> URI) -> Message -> Message
+modUri f m = setUri (f $ uri m) m
+
+status :: Message -> Status
+status = _status . direction
+
+setStatus :: Status -> Message -> Message
+setStatus s m = m { direction = Response s }
+
+setVersion :: Version -> Message -> Message
+setVersion v m = m { version = v }
+
+setBody :: String -> Message -> Message
+setBody b m = m { body = b }
+
+modBody :: (String -> String) -> Message -> Message
+modBody f m = setBody (f $ body m) m
+
+header :: String -> Message -> String
+header k = maybe "" id . M.lookup (normalizeHeader k) . headers
+
+setHeader :: String -> String -> Message -> Message
+setHeader k a (Message r v h b) = Message r v (M.insert (normalizeHeader k) a h) b
+
+modHeader :: String -> (String -> String) -> Message -> Message
+modHeader k f m = setHeader k (f $ header k m) m
+
+getLocation :: Message -> String
+getLocation = header "Location"
+
+getHost :: Message -> String
+getHost = header "Host"
+
+getContentLength :: Message -> Maybe Int
+getContentLength = intRead . header "Content-Length"
+
+getKeepAlive :: Message -> Maybe Int
+getKeepAlive = intRead . header "Keep-Alive"
+
+getCookie :: Message -> String
+getCookie = header "Cookie"
+
+setContentType :: String -> Maybe String -> Message -> Message
+setContentType t c = setHeader "Content-Type" (t ++ maybe "" ("; charset="++) c)
+
+setContentLength :: Num a => a -> Message -> Message
+setContentLength l = setHeader "Content-Length" (show l)
+
+setLocation :: URI -> Message -> Message
+setLocation l = setHeader "Location" (show l)
+
+setDate :: String -> Message -> Message
+setDate d = setHeader "Date" d
+
+setServer :: String -> Message -> Message
+setServer n  = setHeader "Server" n
+
+setCookie :: String -> Message -> Message
+setCookie c = setHeader "Set-Cookie" c
+
+normalizeHeader :: String -> String
+normalizeHeader = (intercalate "-") . (map normalCase) . (Misc.Misc.split '-')
+
+-------- HTTP message parsing -------------------------------------------------
+
+-- todo: cleanup ugly code
+
+lf, ws, ls :: String
+lf = "\r\n"
+ws = " \t\r\n"
+ls = " \t"
+
+pLf :: GenParser Char st Char
+pLf = (char '\r' <* pMaybe (char '\n')) <|> char '\n'
+
+pVersion :: GenParser Char st Version
+pVersion = 
+      (\h l -> Version (ord h - ord '0') (ord l  - ord '0'))
+  <$> (string "HTTP/" *> digit)
+  <*> (char '.'       *> digit)
+
+pHeaders :: GenParser Char st Headers
+pHeaders = M.insert
+  <$> many1 (noneOf (':':ws)) <* string ":"
+  <*> (intercalate ws <$> (many $ many1 (oneOf ls) *> many1 (noneOf lf) <* pLf))
+  <*> option M.empty pHeaders
+
+pMethod :: GenParser Char st Method
+pMethod = choice $ map (\a -> a <$ (try $ string $ show a)) methods
+
+pRequest :: GenParser Char st Message
+pRequest = (\m u v h b -> Message (Request m u) v h b)
+  <$> (pMethod <* many1 (oneOf ls))
+  <*> (pUriReference <* many1 (oneOf ls))
+  <*> (pVersion <* pLf)
+  <*> (pHeaders <* pLf)
+  <*> (many anyToken)
+
+pResponse :: GenParser Char st Message
+pResponse = (\v s h b -> Message (Response (statusFromCode $ read s)) v h b)
+  <$> (pVersion <* many1 (oneOf ls))
+  <*> (many1 digit <* many1 (oneOf ls) <* many1 (noneOf lf) <* pLf)
+  <*> (pHeaders <* pLf)
+  <*> (many anyToken)
+
+-------- HTTP message pretty printing -----------------------------------------
+
+showVersion :: Version -> String
+showVersion (Version a b) = concat ["HTTP/", show a, ".", show b]
+
+showHeaders :: Headers -> String
+showHeaders =
+    intercalate lf
+  . M.elems
+  . M.mapWithKey (\k a -> k ++ ": " ++ a)
+
+showMessageHeader :: Message -> String
+showMessageHeader (Message (Response s) v hs _) =
+  concat [
+    showVersion v, " "
+  , maybe "Unknown status" show
+  $ Bm.lookupR s statusCodes, " "
+  , show s, lf
+  , showHeaders hs, lf, lf
+  ]
+showMessageHeader (Message (Request m u) v hs _) =
+  concat [show m, " ", show u, " ", showVersion v, lf, showHeaders hs, lf, lf]
+
+instance Show Message where
+  show m = concat [showMessageHeader m, body m]
+
+-------- status code mappings -------------------------------------------------
+
+-- rfc2616 sec6.1.1 Status Code and Reason Phrase
+statusCodes :: Bm.Bimap Int Status
+statusCodes = Bm.fromList [
+    (100, Continue)
+  , (101, SwitchingProtocols)
+  , (200, OK)
+  , (201, Created)
+  , (202, Accepted)
+  , (203, NonAuthoritativeInformation)
+  , (204, NoContent)
+  , (205, ResetContent)
+  , (206, PartialContent)
+  , (300, MultipleChoices)
+  , (301, MovedPermanently)
+  , (302, Found)
+  , (303, SeeOther)
+  , (304, NotModified)
+  , (305, UseProxy)
+  , (307, TemporaryRedirect)
+  , (400, BadRequest)
+  , (401, Unauthorized)
+  , (402, PaymentRequired)
+  , (403, Forbidden)
+  , (404, NotFound)
+  , (405, MethodNotAllowed)
+  , (406, NotAcceptable)
+  , (407, ProxyAuthenticationRequired)
+  , (408, RequestTimeOut)
+  , (409, Conflict)
+  , (410, Gone)
+  , (411, LengthRequired)
+  , (412, PreconditionFailed)
+  , (413, RequestEntityTooLarge)
+  , (414, RequestURITooLarge)
+  , (415, UnsupportedMediaType)
+  , (416, RequestedRangeNotSatisfiable)
+  , (417, ExpectationFailed)
+  , (500, InternalServerError)
+  , (501, NotImplemented)
+  , (502, BadGateway)
+  , (503, ServiceUnavailable)
+  , (504, GatewayTimeOut)
+  , (505, HTTPVersionNotSupported)
+  ]
+
+-- rfc2616 sec6.1.1 Status Code and Reason Phrase
+
+instance Show Status where
+  show Continue                     = "Continue"
+  show SwitchingProtocols           = "Switching Protocols"
+  show OK                           = "OK"
+  show Created                      = "Created"
+  show Accepted                     = "Accepted"
+  show NonAuthoritativeInformation  = "Non-Authoritative Information"
+  show NoContent                    = "No Content"
+  show ResetContent                 = "Reset Content"
+  show PartialContent               = "Partial Content"
+  show MultipleChoices              = "Multiple Choices"
+  show MovedPermanently             = "Moved Permanently"
+  show Found                        = "Found"
+  show SeeOther                     = "See Other"
+  show NotModified                  = "Not Modified"
+  show UseProxy                     = "Use Proxy"
+  show TemporaryRedirect            = "Temporary Redirect"
+  show BadRequest                   = "Bad Request"
+  show Unauthorized                 = "Unauthorized"
+  show PaymentRequired              = "Payment Required"
+  show Forbidden                    = "Forbidden"
+  show NotFound                     = "Not Found"
+  show MethodNotAllowed             = "Method Not Allowed"
+  show NotAcceptable                = "Not Acceptable"
+  show ProxyAuthenticationRequired  = "Proxy Authentication Required"
+  show RequestTimeOut               = "Request Time-out"
+  show Conflict                     = "Conflict"
+  show Gone                         = "Gone"
+  show LengthRequired               = "Length Required"
+  show PreconditionFailed           = "Precondition Failed"
+  show RequestEntityTooLarge        = "Request Entity Too Large"
+  show RequestURITooLarge           = "Request-URI Too Large"
+  show UnsupportedMediaType         = "Unsupported Media Type"
+  show RequestedRangeNotSatisfiable = "Requested range not satisfiable"
+  show ExpectationFailed            = "Expectation Failed"
+  show InternalServerError          = "Internal Server Error"
+  show NotImplemented               = "Not Implemented"
+  show BadGateway                   = "Bad Gateway"
+  show ServiceUnavailable           = "Service Unavailable"
+  show GatewayTimeOut               = "Gateway Time-out"
+  show HTTPVersionNotSupported      = "HTTP Version not supported"
+  show (CustomStatus _)             = "Unknown Status"
+
+statusFailure :: Status -> Bool
+statusFailure st = codeFromStatus st >= 400
+
+statusFromCode :: Int -> Status
+statusFromCode num =
+    fromMaybe (CustomStatus num)
+  $ Bm.lookup num statusCodes
+
+codeFromStatus :: Status -> Int
+codeFromStatus st =
+    fromMaybe 0 -- total, should not happen
+  $ Bm.lookupR st statusCodes
+
diff --git a/src/Network/Protocol/Mime.hs b/src/Network/Protocol/Mime.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Protocol/Mime.hs
@@ -0,0 +1,653 @@
+module Network.Protocol.Mime where
+
+import Data.Map
+
+-------- file extension to mime type mapping ----------------------------------
+
+mime :: String -> Maybe String
+mime ext = Data.Map.lookup ext extensionToMime
+
+defaultMime :: String
+defaultMime = "text/plain"
+
+extensionToMime :: Map String String
+extensionToMime = fromList [
+    ("123",       "application/vnd.lotus-1-2-3")
+  , ("3dml",      "text/vnd.in3d.3dml")
+  , ("3g2",       "video/3gpp2")
+  , ("3gp",       "video/3gpp")
+  , ("ace",       "application/x-ace-compressed")
+  , ("acu",       "application/vnd.acucobol")
+  , ("acutc",     "application/vnd.acucorp")
+  , ("aep",       "application/vnd.audiograph")
+  , ("afp",       "application/vnd.ibm.modcap")
+  , ("ai",        "application/postscript")
+  , ("aif",       "audio/x-aiff")
+  , ("aifc",      "audio/x-aiff")
+  , ("aiff",      "audio/x-aiff")
+  , ("ami",       "application/vnd.amiga.ami")
+  , ("apr",       "application/vnd.lotus-approach")
+  , ("asc",       "application/pgp-signature")
+  , ("asf",       "application/vnd.ms-asf")
+  , ("asf",       "video/x-ms-asf")
+  , ("asm",       "text/x-asm")
+  , ("aso",       "application/vnd.accpac.simply.aso")
+  , ("asx",       "video/x-ms-asf")
+  , ("atc",       "application/vnd.acucorp")
+  , ("atom",      "application/atom+xml")
+  , ("atomcat",   "application/atomcat+xml")
+  , ("atomsvc",   "application/atomsvc+xml")
+  , ("atx",       "application/vnd.antix.game-component")
+  , ("au",        "audio/basic")
+  , ("bat",       "application/x-msdownload")
+  , ("bcpio",     "application/x-bcpio")
+  , ("bdm",       "application/vnd.syncml.dm+wbxml")
+  , ("bh2",       "application/vnd.fujitsu.oasysprs")
+  , ("bin",       "application/octet-stream")
+  , ("bmi",       "application/vnd.bmi")
+  , ("bmp",       "image/bmp")
+  , ("box",       "application/vnd.previewsystems.box")
+  , ("boz",       "application/x-bzip2")
+  , ("bpk",       "application/octet-stream")
+  , ("btif",      "image/prs.btif")
+  , ("bz",        "application/x-bzip")
+  , ("bz2",       "application/x-bzip2")
+  , ("c",         "text/x-c")
+  , ("c4d",       "application/vnd.clonk.c4group")
+  , ("c4f",       "application/vnd.clonk.c4group")
+  , ("c4g",       "application/vnd.clonk.c4group")
+  , ("c4p",       "application/vnd.clonk.c4group")
+  , ("c4u",       "application/vnd.clonk.c4group")
+  , ("cab",       "application/vnd.ms-cab-compressed")
+  , ("cc",        "text/x-c")
+  , ("ccxml",     "application/ccxml+xml")
+  , ("cdbcmsg",   "application/vnd.contact.cmsg")
+  , ("cdf",       "application/x-netcdf")
+  , ("cdkey",     "application/vnd.mediastation.cdkey")
+  , ("cdx",       "chemical/x-cdx")
+  , ("cdxml",     "application/vnd.chemdraw+xml")
+  , ("cdy",       "application/vnd.cinderella")
+  , ("cer",       "application/pkix-cert")
+  , ("cgm",       "image/cgm")
+  , ("chat",      "application/x-chat")
+  , ("chm",       "application/vnd.ms-htmlhelp")
+  , ("chrt",      "application/vnd.kde.kchart")
+  , ("cif",       "chemical/x-cif")
+  , ("cii",       "application/vnd.anser-web-certificate-issue-initiation")
+  , ("cil",       "application/vnd.ms-artgalry")
+  , ("cla",       "application/vnd.claymore")
+  , ("class",     "application/octet-stream")
+  , ("clkk",      "application/vnd.crick.clicker.keyboard")
+  , ("clkp",      "application/vnd.crick.clicker.palette")
+  , ("clkt",      "application/vnd.crick.clicker.template")
+  , ("clkw",      "application/vnd.crick.clicker.wordbank")
+  , ("clkx",      "application/vnd.crick.clicker")
+  , ("clp",       "application/x-msclip")
+  , ("cmc",       "application/vnd.cosmocaller")
+  , ("cmdf",      "chemical/x-cmdf")
+  , ("cml",       "chemical/x-cml")
+  , ("cmp",       "application/vnd.yellowriver-custom-menu")
+  , ("cmx",       "image/x-cmx")
+  , ("com",       "application/x-msdownload")
+  , ("conf",      "text/plain")
+  , ("cpio",      "application/x-cpio")
+  , ("cpp",       "text/x-c")
+  , ("cpt",       "application/mac-compactpro")
+  , ("crd",       "application/x-mscardfile")
+  , ("crl",       "application/pkix-crl")
+  , ("crt",       "application/x-x509-ca-cert")
+  , ("csh",       "application/x-csh")
+  , ("csml",      "chemical/x-csml")
+  , ("csp",       "application/vnd.commonspace")
+  , ("css",       "text/css")
+  , ("cst",       "application/vnd.commonspace")
+  , ("csv",       "text/csv")
+  , ("curl",      "application/vnd.curl")
+  , ("cww",       "application/prs.cww")
+  , ("cxx",       "text/x-c")
+  , ("daf",       "application/vnd.mobius.daf")
+  , ("davmount",  "application/davmount+xml")
+  , ("dcr",       "application/x-director")
+  , ("dd2",       "application/vnd.oma.dd2+xml")
+  , ("ddd",       "application/vnd.fujixerox.ddd")
+  , ("def",       "text/plain")
+  , ("der",       "application/x-x509-ca-cert")
+  , ("dfac",      "application/vnd.dreamfactory")
+  , ("dic",       "text/x-c")
+  , ("dir",       "application/x-director")
+  , ("dis",       "application/vnd.mobius.dis")
+  , ("dist",      "application/octet-stream")
+  , ("distz",     "application/octet-stream")
+  , ("djv",       "image/vnd.djvu")
+  , ("djvu",      "image/vnd.djvu")
+  , ("dll",       "application/x-msdownload")
+  , ("dmg",       "application/octet-stream")
+  , ("dms",       "application/octet-stream")
+  , ("dna",       "application/vnd.dna")
+  , ("doc",       "application/msword")
+  , ("dot",       "application/msword")
+  , ("dp",        "application/vnd.osgi.dp")
+  , ("dpg",       "application/vnd.dpgraph")
+  , ("dsc",       "text/prs.lines.tag")
+  , ("dtd",       "application/xml-dtd")
+  , ("dump",      "application/octet-stream")
+  , ("dvi",       "application/x-dvi")
+  , ("dwf",       "model/vnd.dwf")
+  , ("dwg",       "image/vnd.dwg")
+  , ("dxf",       "image/vnd.dxf")
+  , ("dxp",       "application/vnd.spotfire.dxp")
+  , ("dxr",       "application/x-director")
+  , ("ecelp4800", "audio/vnd.nuera.ecelp4800")
+  , ("ecelp7470", "audio/vnd.nuera.ecelp7470")
+  , ("ecelp9600", "audio/vnd.nuera.ecelp9600")
+  , ("ecma",      "application/ecmascript")
+  , ("edm",       "application/vnd.novadigm.edm")
+  , ("edx",       "application/vnd.novadigm.edx")
+  , ("efif",      "application/vnd.picsel")
+  , ("ei6",       "application/vnd.pg.osasli")
+  , ("elc",       "application/octet-stream")
+  , ("eml",       "message/rfc822")
+  , ("eol",       "audio/vnd.digital-winds")
+  , ("eot",       "application/vnd.ms-fontobject")
+  , ("eps",       "application/postscript")
+  , ("es3",       "application/vnd.eszigno3+xml")
+  , ("esf",       "application/vnd.epson.esf")
+  , ("et3",       "application/vnd.eszigno3+xml")
+  , ("etx",       "text/x-setext")
+  , ("exe",       "application/x-msdownload")
+  , ("ext",       "application/vnd.novadigm.ext")
+  , ("ez",        "application/andrew-inset")
+  , ("ez2",       "application/vnd.ezpix-album")
+  , ("ez3",       "application/vnd.ezpix-package")
+  , ("f",         "text/x-fortran")
+  , ("f77",       "text/x-fortran")
+  , ("f90",       "text/x-fortran")
+  , ("fbs",       "image/vnd.fastbidsheet")
+  , ("fdf",       "application/vnd.fdf")
+  , ("fe_launch", "application/vnd.denovo.fcselayout-link")
+  , ("fg5",       "application/vnd.fujitsu.oasysgp")
+  , ("fgd",       "application/x-director")
+  , ("fli",       "video/x-fli")
+  , ("flo",       "application/vnd.micrografx.flo")
+  , ("flw",       "application/vnd.kde.kivio")
+  , ("flx",       "text/vnd.fmi.flexstor")
+  , ("fly",       "text/vnd.fly")
+  , ("fm",        "application/vnd.framemaker")
+  , ("fnc",       "application/vnd.frogans.fnc")
+  , ("for",       "text/x-fortran")
+  , ("fpx",       "image/vnd.fpx")
+  , ("frame",     "application/vnd.framemaker")
+  , ("fsc",       "application/vnd.fsc.weblaunch")
+  , ("fst",       "image/vnd.fst")
+  , ("ftc",       "application/vnd.fluxtime.clip")
+  , ("fti",       "application/vnd.anser-web-funds-transfer-initiation")
+  , ("fvt",       "video/vnd.fvt")
+  , ("fzs",       "application/vnd.fuzzysheet")
+  , ("g3",        "image/g3fax")
+  , ("gac",       "application/vnd.groove-account")
+  , ("gdl",       "model/vnd.gdl")
+  , ("ghf",       "application/vnd.groove-help")
+  , ("gif",       "image/gif")
+  , ("gim",       "application/vnd.groove-identity-message")
+  , ("gph",       "application/vnd.flographit")
+  , ("gqf",       "application/vnd.grafeq")
+  , ("gqs",       "application/vnd.grafeq")
+  , ("gram",      "application/srgs")
+  , ("grv",       "application/vnd.groove-injector")
+  , ("grxml",     "application/srgs+xml")
+  , ("gtar",      "application/x-gtar")
+  , ("gtm",       "application/vnd.groove-tool-message")
+  , ("gtw",       "model/vnd.gtw")
+  , ("h",         "text/x-c")
+  , ("h261",      "video/h261")
+  , ("h263",      "video/h263")
+  , ("h264",      "video/h264")
+  , ("hbci",      "application/vnd.hbci")
+  , ("hdf",       "application/x-hdf")
+  , ("hh",        "text/x-c")
+  , ("hlp",       "application/winhlp")
+  , ("hpgl",      "application/vnd.hp-hpgl")
+  , ("hpid",      "application/vnd.hp-hpid")
+  , ("hps",       "application/vnd.hp-hps")
+  , ("hqx",       "application/mac-binhex40")
+  , ("htke",      "application/vnd.kenameaapp")
+  , ("htm",       "text/html")
+  , ("html",      "text/html")
+  , ("hvd",       "application/vnd.yamaha.hv-dic")
+  , ("hvp",       "application/vnd.yamaha.hv-voice")
+  , ("hvs",       "application/vnd.yamaha.hv-script")
+  , ("ico",       "image/vnd.microsoft.icon")
+  , ("ics",       "text/calendar")
+  , ("ief",       "image/ief")
+  , ("ifb",       "text/calendar")
+  , ("ifm",       "application/vnd.shana.informed.formdata")
+  , ("iges",      "model/iges")
+  , ("igl",       "application/vnd.igloader")
+  , ("igs",       "model/iges")
+  , ("igx",       "application/vnd.micrografx.igx")
+  , ("iif",       "application/vnd.shana.informed.interchange")
+  , ("imp",       "application/vnd.accpac.simply.imp")
+  , ("ims",       "application/vnd.ms-ims")
+  , ("in",        "text/plain")
+  , ("ipk",       "application/vnd.shana.informed.package")
+  , ("irm",       "application/vnd.ibm.rights-management")
+  , ("irp",       "application/vnd.irepository.package+xml")
+  , ("iso",       "application/octet-stream")
+  , ("itp",       "application/vnd.shana.informed.formtemplate")
+  , ("ivp",       "application/vnd.immervision-ivp")
+  , ("ivu",       "application/vnd.immervision-ivu")
+  , ("jad",       "text/vnd.sun.j2me.app-descriptor")
+  , ("jam",       "application/vnd.jam")
+  , ("java",      "text/x-java-source")
+  , ("jisp",      "application/vnd.jisp")
+  , ("jlt",       "application/vnd.hp-jlyt")
+  , ("jpe",       "image/jpeg")
+  , ("jpeg",      "image/jpeg")
+  , ("jpg",       "image/jpeg")
+  , ("jpgm",      "video/jpm")
+  , ("jpgv",      "video/jpeg")
+  , ("jpm",       "video/jpm")
+  , ("js",        "application/javascript")
+  , ("json",      "application/json")
+  , ("kar",       "audio/midi")
+  , ("karbon",    "application/vnd.kde.karbon")
+  , ("kfo",       "application/vnd.kde.kformula")
+  , ("kia",       "application/vnd.kidspiration")
+  , ("kml",       "application/vnd.google-earth.kml+xml")
+  , ("kmz",       "application/vnd.google-earth.kmz")
+  , ("kne",       "application/vnd.kinar")
+  , ("knp",       "application/vnd.kinar")
+  , ("kon",       "application/vnd.kde.kontour")
+  , ("kpr",       "application/vnd.kde.kpresenter")
+  , ("kpt",       "application/vnd.kde.kpresenter")
+  , ("ksp",       "application/vnd.kde.kspread")
+  , ("ktr",       "application/vnd.kahootz")
+  , ("ktz",       "application/vnd.kahootz")
+  , ("kwd",       "application/vnd.kde.kword")
+  , ("kwt",       "application/vnd.kde.kword")
+  , ("latex",     "application/x-latex")
+  , ("lbd",       "application/vnd.llamagraphics.life-balance.desktop")
+  , ("lbe",       "application/vnd.llamagraphics.life-balance.exchange+xml")
+  , ("les",       "application/vnd.hhe.lesson-player")
+  , ("lha",       "application/octet-stream")
+  , ("list",      "text/plain")
+  , ("list3820",  "application/vnd.ibm.modcap")
+  , ("listafp",   "application/vnd.ibm.modcap")
+  , ("log",       "text/plain")
+  , ("lrm",       "application/vnd.ms-lrm")
+  , ("ltf",       "application/vnd.frogans.ltf")
+  , ("lvp",       "audio/vnd.lucent.voice")
+  , ("lwp",       "application/vnd.lotus-wordpro")
+  , ("lzh",       "application/octet-stream")
+  , ("m13",       "application/x-msmediaview")
+  , ("m14",       "application/x-msmediaview")
+  , ("m1v",       "video/mpeg")
+  , ("m2a",       "audio/mpeg")
+  , ("m2v",       "video/mpeg")
+  , ("m3a",       "audio/mpeg")
+  , ("m3u",       "audio/x-mpegurl")
+  , ("m4u",       "video/vnd.mpegurl")
+  , ("ma",        "application/mathematica")
+  , ("mag",       "application/vnd.ecowin.chart")
+  , ("maker",     "application/vnd.framemaker")
+  , ("man",       "text/troff")
+  , ("mathml",    "application/mathml+xml")
+  , ("mb",        "application/mathematica")
+  , ("mbk",       "application/vnd.mobius.mbk")
+  , ("mbox",      "application/mbox")
+  , ("mc1",       "application/vnd.medcalcdata")
+  , ("mcd",       "application/vnd.mcd")
+  , ("mdb",       "application/x-msaccess")
+  , ("mdi",       "image/vnd.ms-modi")
+  , ("me",        "text/troff")
+  , ("mesh",      "model/mesh")
+  , ("mfm",       "application/vnd.mfmp")
+  , ("mgz",       "application/vnd.proteus.magazine")
+  , ("mid",       "audio/midi")
+  , ("midi",      "audio/midi")
+  , ("mif",       "application/vnd.mif")
+  , ("mime",      "message/rfc822")
+  , ("mj2",       "video/mj2")
+  , ("mjp2",      "video/mj2")
+  , ("mlp",       "application/vnd.dolby.mlp")
+  , ("mmd",       "application/vnd.chipnuts.karaoke-mmd")
+  , ("mmf",       "application/vnd.smaf")
+  , ("mmr",       "image/vnd.fujixerox.edmics-mmr")
+  , ("mny",       "application/x-msmoney")
+  , ("mov",       "video/quicktime")
+  , ("mp2",       "audio/mpeg")
+  , ("mp2a",      "audio/mpeg")
+  , ("mp3",       "audio/mpeg")
+  , ("mp4",       "video/mp4")
+  , ("mp4a",      "audio/mp4")
+  , ("mp4s",      "application/mp4")
+  , ("mp4v",      "video/mp4")
+  , ("mpc",       "application/vnd.mophun.certificate")
+  , ("mpe",       "video/mpeg")
+  , ("mpeg",      "video/mpeg")
+  , ("mpg",       "video/mpeg")
+  , ("mpg4",      "video/mp4")
+  , ("mpga",      "audio/mpeg")
+  , ("mpkg",      "application/vnd.apple.installer+xml")
+  , ("mpm",       "application/vnd.blueice.multipass")
+  , ("mpn",       "application/vnd.mophun.application")
+  , ("mpp",       "application/vnd.ms-project")
+  , ("mpt",       "application/vnd.ms-project")
+  , ("mpy",       "application/vnd.ibm.minipay")
+  , ("mqy",       "application/vnd.mobius.mqy")
+  , ("mrc",       "application/marc")
+  , ("ms",        "text/troff")
+  , ("mscml",     "application/mediaservercontrol+xml")
+  , ("mseq",      "application/vnd.mseq")
+  , ("msf",       "application/vnd.epson.msf")
+  , ("msh",       "model/mesh")
+  , ("msi",       "application/x-msdownload")
+  , ("msl",       "application/vnd.mobius.msl")
+  , ("mts",       "model/vnd.mts")
+  , ("mus",       "application/vnd.musician")
+  , ("mvb",       "application/x-msmediaview")
+  , ("mwf",       "application/vnd.mfer")
+  , ("mxf",       "application/mxf")
+  , ("mxl",       "application/vnd.recordare.musicxml")
+  , ("mxml",      "application/xv+xml")
+  , ("mxs",       "application/vnd.triscape.mxs")
+  , ("mxu",       "video/vnd.mpegurl")
+  , ("n-gage",    "application/vnd.nokia.n-gage.symbian.install")
+  , ("nb",        "application/mathematica")
+  , ("nc",        "application/x-netcdf")
+  , ("ngdat",     "application/vnd.nokia.n-gage.data")
+  , ("nlu",       "application/vnd.neurolanguage.nlu")
+  , ("nml",       "application/vnd.enliven")
+  , ("nnd",       "application/vnd.noblenet-directory")
+  , ("nns",       "application/vnd.noblenet-sealer")
+  , ("nnw",       "application/vnd.noblenet-web")
+  , ("npx",       "image/vnd.net-fpx")
+  , ("nsf",       "application/vnd.lotus-notes")
+  , ("oa2",       "application/vnd.fujitsu.oasys2")
+  , ("oa3",       "application/vnd.fujitsu.oasys3")
+  , ("oas",       "application/vnd.fujitsu.oasys")
+  , ("obd",       "application/x-msbinder")
+  , ("oda",       "application/oda")
+  , ("odc",       "application/vnd.oasis.opendocument.chart")
+  , ("odf",       "application/vnd.oasis.opendocument.formula")
+  , ("odg",       "application/vnd.oasis.opendocument.graphics")
+  , ("odi",       "application/vnd.oasis.opendocument.image")
+  , ("odp",       "application/vnd.oasis.opendocument.presentation")
+  , ("ods",       "application/vnd.oasis.opendocument.spreadsheet")
+  , ("odt",       "application/vnd.oasis.opendocument.text")
+  , ("ogg",       "application/ogg")
+  , ("oprc",      "application/vnd.palm")
+  , ("org",       "application/vnd.lotus-organizer")
+  , ("otc",       "application/vnd.oasis.opendocument.chart-template")
+  , ("otf",       "application/vnd.oasis.opendocument.formula-template")
+  , ("otg",       "application/vnd.oasis.opendocument.graphics-template")
+  , ("oth",       "application/vnd.oasis.opendocument.text-web")
+  , ("oti",       "application/vnd.oasis.opendocument.image-template")
+  , ("otm",       "application/vnd.oasis.opendocument.text-master")
+  , ("otp",       "application/vnd.oasis.opendocument.presentation-template")
+  , ("ots",       "application/vnd.oasis.opendocument.spreadsheet-template")
+  , ("ott",       "application/vnd.oasis.opendocument.text-template")
+  , ("oxt",       "application/vnd.openofficeorg.extension")
+  , ("p",         "text/x-pascal")
+  , ("p10",       "application/pkcs10")
+  , ("p12",       "application/x-pkcs12")
+  , ("p7b",       "application/x-pkcs7-certificates")
+  , ("p7c",       "application/pkcs7-mime")
+  , ("p7m",       "application/pkcs7-mime")
+  , ("p7r",       "application/x-pkcs7-certreqresp")
+  , ("p7s",       "application/pkcs7-signature")
+  , ("pas",       "text/x-pascal")
+  , ("pbd",       "application/vnd.powerbuilder6")
+  , ("pbm",       "image/x-portable-bitmap")
+  , ("pcl",       "application/vnd.hp-pcl")
+  , ("pclxl",     "application/vnd.hp-pclxl")
+  , ("pct",       "image/x-pict")
+  , ("pcx",       "image/x-pcx")
+  , ("pdb",       "application/vnd.palm")
+  , ("pdb",       "chemical/x-pdb")
+  , ("pdf",       "application/pdf")
+  , ("pfr",       "application/font-tdpfr")
+  , ("pfx",       "application/x-pkcs12")
+  , ("pgm",       "image/x-portable-graymap")
+  , ("pgn",       "application/x-chess-pgn")
+  , ("pgp",       "application/pgp-encrypted")
+  , ("pic",       "image/x-pict")
+  , ("pkg",       "application/octet-stream")
+  , ("pki",       "application/pkixcmp")
+  , ("pkipath",   "application/pkix-pkipath")
+  , ("plb",       "application/vnd.3gpp.pic-bw-large")
+  , ("plc",       "application/vnd.mobius.plc")
+  , ("plf",       "application/vnd.pocketlearn")
+  , ("pls",       "application/pls+xml")
+  , ("pml",       "application/vnd.ctc-posml")
+  , ("png",       "image/png")
+  , ("pnm",       "image/x-portable-anymap")
+  , ("portpkg",   "application/vnd.macports.portpkg")
+  , ("pot",       "application/vnd.ms-powerpoint")
+  , ("ppd",       "application/vnd.cups-ppd")
+  , ("ppm",       "image/x-portable-pixmap")
+  , ("pps",       "application/vnd.ms-powerpoint")
+  , ("ppt",       "application/vnd.ms-powerpoint")
+  , ("pqa",       "application/vnd.palm")
+  , ("prc",       "application/vnd.palm")
+  , ("pre",       "application/vnd.lotus-freelance")
+  , ("prf",       "application/pics-rules")
+  , ("ps",        "application/postscript")
+  , ("psb",       "application/vnd.3gpp.pic-bw-small")
+  , ("psd",       "image/vnd.adobe.photoshop")
+  , ("ptid",      "application/vnd.pvi.ptid1")
+  , ("pub",       "application/x-mspublisher")
+  , ("pvb",       "application/vnd.3gpp.pic-bw-var")
+  , ("pwn",       "application/vnd.3m.post-it-notes")
+  , ("qam",       "application/vnd.epson.quickanime")
+  , ("qbo",       "application/vnd.intu.qbo")
+  , ("qfx",       "application/vnd.intu.qfx")
+  , ("qps",       "application/vnd.publishare-delta-tree")
+  , ("qt",        "video/quicktime")
+  , ("qwd",       "application/vnd.quark.quarkxpress")
+  , ("qwt",       "application/vnd.quark.quarkxpress")
+  , ("qxb",       "application/vnd.quark.quarkxpress")
+  , ("qxd",       "application/vnd.quark.quarkxpress")
+  , ("qxl",       "application/vnd.quark.quarkxpress")
+  , ("qxt",       "application/vnd.quark.quarkxpress")
+  , ("ra",        "audio/x-pn-realaudio")
+  , ("ram",       "audio/x-pn-realaudio")
+  , ("rar",       "application/x-rar-compressed")
+  , ("ras",       "image/x-cmu-raster")
+  , ("rcprofile", "application/vnd.ipunplugged.rcprofile")
+  , ("rdf",       "application/rdf+xml")
+  , ("rdz",       "application/vnd.data-vision.rdz")
+  , ("rep",       "application/vnd.businessobjects")
+  , ("rgb",       "image/x-rgb")
+  , ("rif",       "application/reginfo+xml")
+  , ("rl",        "application/resource-lists+xml")
+  , ("rlc",       "image/vnd.fujixerox.edmics-rlc")
+  , ("rm",        "application/vnd.rn-realmedia")
+  , ("rmi",       "audio/midi")
+  , ("rmp",       "audio/x-pn-realaudio-plugin")
+  , ("rms",       "application/vnd.jcp.javame.midlet-rms")
+  , ("rnc",       "application/relax-ng-compact-syntax")
+  , ("roff",      "text/troff")
+  , ("rpss",      "application/vnd.nokia.radio-presets")
+  , ("rpst",      "application/vnd.nokia.radio-preset")
+  , ("rs",        "application/rls-services+xml")
+  , ("rsd",       "application/rsd+xml")
+  , ("rss",       "application/rss+xml")
+  , ("rtf",       "application/rtf")
+  , ("rtx",       "text/richtext")
+  , ("s",         "text/x-asm")
+  , ("saf",       "application/vnd.yamaha.smaf-audio")
+  , ("sbml",      "application/sbml+xml")
+  , ("sc",        "application/vnd.ibm.secure-container")
+  , ("scd",       "application/x-msschedule")
+  , ("scm",       "application/vnd.lotus-screencam")
+  , ("sdkd",      "application/vnd.solent.sdkm+xml")
+  , ("sdkm",      "application/vnd.solent.sdkm+xml")
+  , ("sdp",       "application/sdp")
+  , ("see",       "application/vnd.seemail")
+  , ("sema",      "application/vnd.sema")
+  , ("semd",      "application/vnd.semd")
+  , ("semf",      "application/vnd.semf")
+  , ("setpay",    "application/set-payment-initiation")
+  , ("setreg",    "application/set-registration-initiation")
+  , ("sfs",       "application/vnd.spotfire.sfs")
+  , ("sgm",       "text/sgml")
+  , ("sgml",      "text/sgml")
+  , ("sh",        "application/x-sh")
+  , ("shar",      "application/x-shar")
+  , ("shf",       "application/shf+xml")
+  , ("sig",       "application/pgp-signature")
+  , ("silo",      "model/mesh")
+  , ("sit",       "application/x-stuffit")
+  , ("sitx",      "application/x-stuffitx")
+  , ("skd",       "application/vnd.koan")
+  , ("skm",       "application/vnd.koan")
+  , ("skp",       "application/vnd.koan")
+  , ("skt",       "application/vnd.koan")
+  , ("slt",       "application/vnd.epson.salt")
+  , ("smi",       "application/smil+xml")
+  , ("smil",      "application/smil+xml")
+  , ("snd",       "audio/basic")
+  , ("so",        "application/octet-stream")
+  , ("spc",       "application/x-pkcs7-certificates")
+  , ("spf",       "application/vnd.yamaha.smaf-phrase")
+  , ("spl",       "application/x-futuresplash")
+  , ("spot",      "text/vnd.in3d.spot")
+  , ("src",       "application/x-wais-source")
+  , ("ssf",       "application/vnd.epson.ssf")
+  , ("ssml",      "application/ssml+xml")
+  , ("stf",       "application/vnd.wt.stf")
+  , ("stk",       "application/hyperstudio")
+  , ("str",       "application/vnd.pg.format")
+  , ("sus",       "application/vnd.sus-calendar")
+  , ("susp",      "application/vnd.sus-calendar")
+  , ("sv4cpio",   "application/x-sv4cpio")
+  , ("sv4crc",    "application/x-sv4crc")
+  , ("svd",       "application/vnd.svd")
+  , ("svg",       "image/svg+xml")
+  , ("svgz",      "image/svg+xml")
+  , ("swf",       "application/x-shockwave-flash")
+  , ("t",         "text/troff")
+  , ("tao",       "application/vnd.tao.intent-module-archive")
+  , ("tar",       "application/x-tar")
+  , ("tcl",       "application/x-tcl")
+  , ("tex",       "application/x-tex")
+  , ("texi",      "application/x-texinfo")
+  , ("texinfo",   "application/x-texinfo")
+  , ("text",      "text/plain")
+  , ("tif",       "image/tiff")
+  , ("tiff",      "image/tiff")
+  , ("tmo",       "application/vnd.tmobile-livetv")
+  , ("torrent",   "application/x-bittorrent")
+  , ("tpl",       "application/vnd.groove-tool-template")
+  , ("tpt",       "application/vnd.trid.tpt")
+  , ("tr",        "text/troff")
+  , ("tra",       "application/vnd.trueapp")
+  , ("trm",       "application/x-msterminal")
+  , ("tsv",       "text/tab-separated-values")
+  , ("twd",       "application/vnd.simtech-mindmapper")
+  , ("twds",      "application/vnd.simtech-mindmapper")
+  , ("txd",       "application/vnd.genomatix.tuxedo")
+  , ("txf",       "application/vnd.mobius.txf")
+  , ("txt",       "text/plain")
+  , ("ufd",       "application/vnd.ufdl")
+  , ("ufdl",      "application/vnd.ufdl")
+  , ("umj",       "application/vnd.umajin")
+  , ("unityweb",  "application/vnd.unity")
+  , ("uoml",      "application/vnd.uoml+xml")
+  , ("uri",       "text/uri-list")
+  , ("uris",      "text/uri-list")
+  , ("urls",      "text/uri-list")
+  , ("ustar",     "application/x-ustar")
+  , ("utz",       "application/vnd.uiq.theme")
+  , ("uu",        "text/x-uuencode")
+  , ("vcd",       "application/x-cdlink")
+  , ("vcf",       "text/x-vcard")
+  , ("vcg",       "application/vnd.groove-vcard")
+  , ("vcs",       "text/x-vcalendar")
+  , ("vcx",       "application/vnd.vcx")
+  , ("vis",       "application/vnd.visionary")
+  , ("viv",       "video/vnd.vivo")
+  , ("vrml",      "model/vrml")
+  , ("vsd",       "application/vnd.visio")
+  , ("vsf",       "application/vnd.vsf")
+  , ("vss",       "application/vnd.visio")
+  , ("vst",       "application/vnd.visio")
+  , ("vsw",       "application/vnd.visio")
+  , ("vtu",       "model/vnd.vtu")
+  , ("vxml",      "application/voicexml+xml")
+  , ("wav",       "audio/wav")
+  , ("wav",       "audio/x-wav")
+  , ("wax",       "audio/x-ms-wax")
+  , ("wbmp",      "image/vnd.wap.wbmp")
+  , ("wbs",       "application/vnd.criticaltools.wbs+xml")
+  , ("wbxml",     "application/vnd.wap.wbxml")
+  , ("wcm",       "application/vnd.ms-works")
+  , ("wdb",       "application/vnd.ms-works")
+  , ("wks",       "application/vnd.ms-works")
+  , ("wm",        "video/x-ms-wm")
+  , ("wma",       "audio/x-ms-wma")
+  , ("wmd",       "application/x-ms-wmd")
+  , ("wmf",       "application/x-msmetafile")
+  , ("wml",       "text/vnd.wap.wml")
+  , ("wmlc",      "application/vnd.wap.wmlc")
+  , ("wmls",      "text/vnd.wap.wmlscript")
+  , ("wmlsc",     "application/vnd.wap.wmlscriptc")
+  , ("wmv",       "video/x-ms-wmv")
+  , ("wmx",       "video/x-ms-wmx")
+  , ("wmz",       "application/x-ms-wmz")
+  , ("wpd",       "application/vnd.wordperfect")
+  , ("wpl",       "application/vnd.ms-wpl")
+  , ("wps",       "application/vnd.ms-works")
+  , ("wqd",       "application/vnd.wqd")
+  , ("wri",       "application/x-mswrite")
+  , ("wrl",       "model/vrml")
+  , ("wsdl",      "application/wsdl+xml")
+  , ("wspolicy",  "application/wspolicy+xml")
+  , ("wtb",       "application/vnd.webturbo")
+  , ("wvx",       "video/x-ms-wvx")
+  , ("x3d",       "application/vnd.hzn-3d-crossword")
+  , ("xar",       "application/vnd.xara")
+  , ("xbd",       "application/vnd.fujixerox.docuworks.binder")
+  , ("xbm",       "image/x-xbitmap")
+  , ("xdm",       "application/vnd.syncml.dm+xml")
+  , ("xdp",       "application/vnd.adobe.xdp+xml")
+  , ("xdw",       "application/vnd.fujixerox.docuworks")
+  , ("xenc",      "application/xenc+xml")
+  , ("xfdf",      "application/vnd.adobe.xfdf")
+  , ("xfdl",      "application/vnd.xfdl")
+  , ("xht",       "application/xhtml+xml")
+  , ("xhtml",     "application/xhtml+xml")
+  , ("xhvml",     "application/xv+xml")
+  , ("xif",       "image/vnd.xiff")
+  , ("xla",       "application/vnd.ms-excel")
+  , ("xlc",       "application/vnd.ms-excel")
+  , ("xlm",       "application/vnd.ms-excel")
+  , ("xls",       "application/vnd.ms-excel")
+  , ("xlt",       "application/vnd.ms-excel")
+  , ("xlw",       "application/vnd.ms-excel")
+  , ("xml",       "application/xml")
+  , ("xo",        "application/vnd.olpc-sugar")
+  , ("xop",       "application/xop+xml")
+  , ("xpm",       "image/x-xpixmap")
+  , ("xpr",       "application/vnd.is-xpr")
+  , ("xps",       "application/vnd.ms-xpsdocument")
+  , ("xpw",       "application/vnd.intercon.formnet")
+  , ("xpx",       "application/vnd.intercon.formnet")
+  , ("xsl",       "application/xml")
+  , ("xslt",      "application/xslt+xml")
+  , ("xsm",       "application/vnd.syncml+xml")
+  , ("xspf",      "application/xspf+xml")
+  , ("xul",       "application/vnd.mozilla.xul+xml")
+  , ("xvm",       "application/xv+xml")
+  , ("xvml",      "application/xv+xml")
+  , ("xwd",       "image/x-xwindowdump")
+  , ("xyz",       "chemical/x-xyz")
+  , ("zaz",       "application/vnd.zzazz.deck+xml")
+  , ("zip",       "application/zip")
+  , ("zmm",       "application/vnd.handheld-entertainment+xml")
+  , ("avi",       "video/x-msvideo")
+  , ("movie",     "video/x-sgi-movie")
+  , ("ice",       "x-conference/x-cooltalk")
+  ]
+
diff --git a/src/Network/Protocol/Uri.hs b/src/Network/Protocol/Uri.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Protocol/Uri.hs
@@ -0,0 +1,614 @@
+module Network.Protocol.Uri (
+
+    URI (..)
+  , Scheme
+  , IPv4
+  , Label
+  , Domain
+  , RegName
+  , Port
+  , Query
+  , Fragment
+  , Hash
+  , UserInfo
+  , PathSegment
+  , Parameters
+  , Path (..)
+  , Host (..)
+  , Authority (..)
+
+  , encode
+  , decode
+
+  , mkURI
+  , mkScheme
+  , mkPath
+  , mkAuthority
+  , mkQuery
+  , mkFragment
+  , mkUserinfo
+  , mkHost
+  , mkPort
+
+  , scheme
+  , authority
+  , path
+  , query
+  , fragment
+  , userinfo
+  , host
+  , port
+  , domains
+  , pathSegments
+
+  , setScheme
+  , setAuthority
+  , setPath
+  , setQuery
+  , setFragment
+  , setUserinfo
+  , setHost
+  , setPort
+
+  , modScheme
+  , modAuthority
+  , modPath
+  , modQuery
+  , modUserinfo
+  , modHost
+  , modPort
+
+  , parseURI
+  , parseAbsoluteURI
+  , parseAuthority
+  , parsePath
+  , parseHost
+
+  , pUriReference
+  , pAbsoluteURI
+  , pAuthority
+  , pPath
+  , pHost
+
+  , parseQueryParams
+  , queryParams
+
+  , extension
+  , setExtension
+  , modExtension
+
+  , relative
+  , mimetype
+  , normalize
+  , jail
+  , (/+)
+
+  ) where
+
+import Control.Applicative
+import Data.Bits
+import Data.Char (ord, chr, isDigit, isAlphaNum, intToDigit, isHexDigit)
+import Data.List (intercalate, isPrefixOf)
+import Data.Maybe (mapMaybe, fromJust)
+import Text.ParserCombinators.Parsec hiding (many, optional, (<|>))
+import System.FilePath.Posix
+
+import Network.Protocol.Mime
+import Misc.Misc
+
+-------[ data type definition of URIs ]----------------------------------------
+
+type Scheme      = String
+type IPv4        = [Int] -- actually 4-tupel
+type Label       = String
+type Domain      = [Label]
+type RegName     = String
+type Port        = Int
+type Query       = String
+type Fragment    = String
+type Hash        = String
+type UserInfo    = String
+type PathSegment = String
+type Parameters  = [(String, Maybe String)]
+
+data Path = Path {
+    _absolute :: Bool
+  , _segments :: [PathSegment]
+  }
+  deriving Eq
+
+data Host =
+    Hostname { _domain  :: Domain }
+  | RegName  { _regname :: String }
+  | IPv4     { _ipv4    :: IPv4   }
+  deriving Eq
+
+data Authority = Authority {
+    _userinfo :: UserInfo
+  , _host     :: Host
+  , _port     :: Port
+  }
+  deriving Eq
+
+data URI = URI {
+    _relative  :: Bool
+  , _scheme    :: Scheme
+  , _authority :: Authority
+  , _path      :: Path
+  , _query     :: Query
+  , _fragment  :: Fragment
+  }
+  deriving Eq
+
+{-
+testUri0 :: URI
+testUri0 = fromJust $ parseURI "http://sebas@hs.spugium.net:8080/wiki/first%20section/chapter-2;pdf?additional=vars"
+
+testUri1 :: URI
+testUri1 = fromJust $ parseURI "http://cs.uu.nl/docs/../docs/vakken/../vakken/"
+-}
+
+-------[ creating, selection and modifying URIs ]------------------------------
+
+-- pseudo constructors for `empty values'
+
+mkURI :: URI
+mkURI = URI False mkScheme mkAuthority mkPath mkQuery mkFragment
+
+mkScheme :: Scheme
+mkScheme = ""
+
+mkPath :: Path
+mkPath = Path False []
+
+mkAuthority :: Authority
+mkAuthority = Authority "" mkHost mkPort
+
+mkQuery :: Query
+mkQuery = ""
+
+mkFragment :: Fragment
+mkFragment = ""
+
+mkUserinfo :: UserInfo
+mkUserinfo = ""
+
+mkHost :: Host
+mkHost = Hostname []
+
+mkPort :: Port
+mkPort = (-1)
+
+-- getter functions
+scheme :: URI -> Scheme
+scheme = _scheme
+
+authority :: URI -> String
+authority = show . _authority
+
+path :: URI -> FilePath
+path = decode . show . _path
+
+query :: URI -> Query
+query = decode . _query
+
+fragment :: URI -> Fragment
+fragment = _fragment
+
+userinfo :: URI -> UserInfo
+userinfo = _userinfo . _authority
+
+host :: URI -> String
+host = show . _host . _authority
+
+port :: URI -> Port
+port = _port . _authority
+
+domains :: URI -> Domain
+domains = _domain . _host . _authority
+
+pathSegments :: URI -> [PathSegment]
+pathSegments = _segments . _path 
+
+-- setter functions
+setScheme :: Scheme -> URI -> URI
+setScheme s u = u { _scheme = s }
+
+setAuthority :: String -> URI -> URI
+setAuthority a u = u { _authority = fromJust $ parseAuthority a }
+
+setPath :: String -> URI -> URI
+setPath p u = u { _path = fromJust $ parsePath p }
+
+setQuery :: Query -> URI -> URI
+setQuery q u = u { _query = q }
+
+setFragment :: Fragment -> URI -> URI
+setFragment q u = u { _fragment  = q }
+
+setUserinfo :: UserInfo -> URI -> URI
+setUserinfo i u = u { _authority = (_authority u) { _userinfo = i }}
+
+setHost :: String -> URI -> URI
+setHost h u = u { _authority = (_authority u) { _host = fromJust $ parseHost h }}
+
+setPort :: Port -> URI -> URI
+setPort p u = u { _authority = (_authority u) { _port = p }}
+
+-- modifier functions
+modScheme :: (Scheme -> Scheme) -> URI -> URI
+modScheme f u = setScheme (f $ scheme u) u
+
+modAuthority :: (String -> String) -> URI -> URI
+modAuthority f u = setAuthority (f $ authority u) u
+
+modPath :: (String -> String) -> URI -> URI
+modPath f u = setPath (f $ encode $ path u) u
+
+modQuery :: (Query -> Query) -> URI -> URI
+modQuery f u = setQuery (f $ query u) u
+
+modUserinfo :: (UserInfo -> UserInfo) -> URI -> URI
+modUserinfo f u = setUserinfo  (f $ userinfo u) u
+
+modHost :: (String -> String) -> URI -> URI
+modHost f u = setHost (f $ host u) u
+
+modPort :: (Port -> Port) -> URI -> URI
+modPort f u = setPort (f $ port u) u
+
+-------[ path encoding and decoding ]------------------------------------------
+
+encode :: String -> String
+encode = concatMap encodeChr
+  where
+    encodeChr c
+      | unreserved c || genDelims c || subDelims c = [c]
+      | otherwise = '%' :
+          intToDigit (shiftR (ord c) 4) :
+          intToDigit ((ord c) .&. 0x0F) : []
+
+decode :: String -> String
+decode [] = []
+decode ('%':d:e:ds) | isHexDigit d && isHexDigit e = f d e : decode ds
+  where f a b = chr $ (ord a-ord '0') * 16 + (ord b - ord '0')
+decode (d:ds) = d : decode ds
+
+-------[ show instance for URIs ]----------------------------------------------
+
+-- TODO: cleanup and test this mess: QuickCheck property for testing:
+-- parse $ show u == u /\ show $ parse u' = u'
+
+-- TODO: ShowS
+
+instance Show Path where
+  show (Path a s) =
+       (if a then "/" else "")
+    ++ (intercalate "/" s)
+
+instance Show Host where
+  show (Hostname d) = if null d then "" else intercalate "." d
+  show (IPv4     a) = intercalate "." $ map show a
+  show (RegName  r) = r
+
+instance Show Authority where
+  show (Authority u h p) =
+       (if hst h then "//" else "")
+    ++ (if null u then "" else u ++ "@")
+    ++  show h
+    ++ (if p == -1 then "" else ":" ++ show p)
+    where hst (Hostname d) = not $ null d
+          hst (RegName  d) = not $ null d
+          hst _            = True
+
+instance Show URI where
+  show (URI r s a p q f) =
+       (if not r then (if null s then "" else s ++ ":") else "")
+    ++ (show a)
+    ++ (show p)
+    ++ (if null q then "" else "?" ++ q)
+    ++ (if null f then "" else "#" ++ f)
+
+-------[ global URI parse interface ]------------------------------------------
+
+-- Top level parsers.
+parseURI :: String -> Maybe URI
+parseURI = eitherToMaybe . parse pUriReference ""
+
+parseAbsoluteURI :: String -> Maybe URI
+parseAbsoluteURI = eitherToMaybe . parse pAbsoluteURI ""
+
+parseAuthority :: String -> Maybe Authority
+parseAuthority = eitherToMaybe . parse pAuthority ""
+
+parsePath :: String -> Maybe Path
+parsePath = eitherToMaybe . parse pPath ""
+
+parseHost :: String -> Maybe Host
+parseHost = eitherToMaybe . parse pHost ""
+
+-------[ parsing URIs according to rfc2396 ]-----------------------------------
+
+-- D.2.  Modifications
+pAlpha, pDigit, pAlphanum :: GenParser Char st Char
+
+pAlpha    = letter
+pDigit    = digit
+pAlphanum = alphaNum
+
+-- 2.3.  Unreserved Characters
+unreserved :: Char -> Bool
+unreserved c = isAlphaNum c || elem c "-._~"
+
+pUnreserved :: GenParser Char st Char
+pUnreserved  = pAlphanum <|> oneOf "-._~"
+
+-- 2.2.  Reserved Characters
+genDelims :: Char -> Bool
+genDelims = flip elem ":/?#[]@"
+
+subDelims :: Char -> Bool
+subDelims = flip elem "!$&'()*+,;="
+
+{-
+pReserved :: GenParser Char st Char
+pReserved  = pGenDelims <|> pSubDelims
+pGenDelims :: GenParser Char st Char
+pGenDelims = oneOf ":/?#[]@"
+-}
+
+pSubDelims :: GenParser Char st Char
+pSubDelims = oneOf "!$&'()*+,;="
+
+-- 2.1.  Percent-Encoding
+pPctEncoded :: GenParser Char st String
+pPctEncoded = (:) <$> char '%' <*> pHex
+
+pHex :: GenParser Char st String
+pHex = (\a b -> a:b:[])
+    <$> hexDigit
+    <*> hexDigit
+
+-- 3.  Syntax Components
+
+-- With the hier-part integrated.
+pUri :: GenParser Char st URI
+pUri = (\a (b,c) d e -> URI False a b c d e)
+  <$> (pScheme <* string ":")
+  <*> (ap <|> p)
+  <*> option "" (string "?" *> pQuery)
+  <*> option "" (string "#" *> pFragment)
+  where
+    ap = (,) <$> (string "//" *> pAuthority) <*> pPathAbempty
+    p  = ((,) mkAuthority) <$> (pPathAbsolute <|> pPathRootless {-<|> pPathEmpty-})
+
+-- 3.1.  Scheme
+pScheme :: GenParser Char st Scheme
+pScheme = (:) <$> pAlpha <*> many (pAlphanum <|> oneOf "+_.")
+
+-- 3.2.  Authority
+pAuthority :: GenParser Char st Authority
+pAuthority = Authority
+  <$> option mkUserinfo (try (pUserinfo <* string "@"))
+  <*> pHost
+  <*> option mkPort (string ":" *> pPort)
+
+-- 3.2.1.  User Information
+pUserinfo :: GenParser Char st String
+pUserinfo = concat <$> many (
+      (pure <$> pUnreserved)
+  <|> (         pPctEncoded)
+  <|> (pure <$> pSubDelims)
+  <|> (pure <$> oneOf ":")
+  )
+
+-- 3.2.2.  Host
+pHost :: GenParser Char st Host
+pHost = diff <$> pRegName -- <|> RegName <$> pRegName
+  where
+    diff  a = maybe (RegName a) sep (pHostname @@ a)
+    sep   a = bool (Hostname a) (ipreg a) $ hst a
+    ipreg a = bool (IPv4 $ map read a) (RegName $ intercalate "." a) $ ip a
+    hst     = not . all isDigit . head . dropWhile null . reverse
+    ip    a = length a == 4 && length (mapMaybe (pDecOctet @@) a) == 4
+
+{-
+pfff, ipv6 is sooo not gonna make it..
+
+pIPLiteral = "[" ( IPv6address <|> IPvFuture  ) "]"
+
+pIPvFuture = "v" 1*HEXDIG "." 1*( unreserved <|> subDelims <|> ":" )
+
+pIPv6address =
+                                 6( h16 ":" ) ls32
+  <|>                       "::" 5( h16 ":" ) ls32
+  <|> [               h16 ] "::" 4( h16 ":" ) ls32
+  <|> [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
+  <|> [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
+  <|> [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
+  <|> [ *4( h16 ":" ) h16 ] "::"              ls32
+  <|> [ *5( h16 ":" ) h16 ] "::"              h16
+  <|> [ *6( h16 ":" ) h16 ] "::"
+
+pH16           = 1*4HEXDIG
+pLs32          = ( h16 ":" h16 ) <|> IPv4address
+-}
+
+{-
+pIPv4address :: GenParser Char st [Int]
+pIPv4address = (:) <$> pDecOctet <*> (count 3 $ char '.' *> pDecOctet)
+-}
+
+pDecOctet :: GenParser Char st Int
+pDecOctet = read <$> choice [
+    try ((\a b c -> [a,b,c]) <$> char '2' <*> char '5'      <*> oneOf "012345")
+  , try ((\a b c -> [a,b,c]) <$> char '2' <*> oneOf "01234" <*> digit)
+  , try ((\a b c -> [a,b,c]) <$> char '1' <*> digit         <*> digit)
+  , try ((\a b   -> [a,b])   <$>              digit         <*> digit)
+  ,     (pure                <$>                                digit)
+  ]
+
+pRegName :: GenParser Char st Label
+pRegName = concat <$> many1 (
+      (pure <$> pUnreserved)
+  <|>           pPctEncoded
+  <|> (pure <$> pSubDelims))
+
+-- Not actually part of the rfc3986, but comptability with the rfc2396.
+-- This information can be useful, so why throw away.
+pHostname :: GenParser Char st Domain
+pHostname = sepBy (option "" pDomainlabel) (string ".")
+
+pDomainlabel :: GenParser Char st Label
+pDomainlabel = intercalate "-" <$> sepBy1 (some pAlphanum) (string "-")
+
+-- 3.2.3.  Port
+pPort :: GenParser Char st Port
+pPort = read <$> some pDigit
+
+-- 3.4.  Query
+pQuery :: GenParser Char st String
+pQuery = concat <$> many (pPchar <|> pure <$> oneOf "/?")
+
+-- 3.5.  Fragment
+pFragment :: GenParser Char st String
+pFragment = concat <$> many (pPchar <|> pure <$> oneOf "/?" )
+
+-- 3.3.  Path
+
+pPath, pPathAbempty, pPathAbsolute, pPathNoscheme, pPathRootless, pPathEmpty
+  :: GenParser Char st Path
+
+pPath =
+      try pPathAbsolute -- begins with "/" but not "//"
+  <|> try pPathNoscheme -- begins with a nonColon segment
+  <|> try pPathRootless -- begins with a segment
+  <|> pPathEmpty        -- zero characters
+
+pPathAbempty  = (Path True) <$> _pSlashSegments
+pPathAbsolute = (char '/' *>) $ (Path True) <$> (option [] $ (:) <$> pSegmentNz <*> _pSlashSegments)
+pPathNoscheme = (Path False) <$> ((:) <$> pSegmentNzNc <*> _pSlashSegments)
+pPathRootless = (Path False) <$> ((:) <$> pSegmentNz    <*> _pSlashSegments)
+pPathEmpty    = (Path False []) <$ string ""
+
+pSegment, pSegmentNz, pSegmentNzNc :: GenParser Char st String
+pSegment     = concat <$> many pPchar
+pSegmentNz   = concat <$> some pPchar
+pSegmentNzNc = concat <$> some (
+      (pure <$> pUnreserved)
+  <|>           pPctEncoded
+  <|> (pure <$> pSubDelims)
+  <|> (pure <$> oneOf "@" ))
+
+_pSlashSegments :: GenParser Char st [PathSegment]
+_pSlashSegments = (many $ (:) <$> char '/' *> pSegment)
+
+pPchar :: GenParser Char st String
+pPchar = choice [
+    pure <$> pUnreserved
+  , pPctEncoded
+  , pure <$> pSubDelims
+  , pure <$> oneOf ":@"
+  ]
+
+-- 4.1.  URI Reference
+
+pUriReference :: GenParser Char st URI
+pUriReference = try pAbsoluteURI <|> pRelativeRef
+
+-- 4.2.  Relative Reference
+
+-- With the relative-part integrated.
+pRelativeRef :: GenParser Char st URI
+pRelativeRef = ($)
+  <$> (try pRelativePart
+  <|> ((URI True mkScheme mkAuthority)
+  <$> (pPathAbsolute <|> pPathRootless <|> pPathEmpty)))
+  <*> option "" (string "?" *> pQuery)
+  <*> option "" (string "#" *> pFragment)
+
+pRelativePart :: GenParser Char st (Query -> Fragment -> URI)
+pRelativePart = (URI True mkScheme) <$> (string "//" *> pAuthority) <*> pPathAbempty
+
+-- 4.3.  Absolute URI
+
+pAbsoluteURI :: GenParser Char st URI
+pAbsoluteURI = pUri
+
+-------[ parsing query parameters ]--------------------------------------------
+
+-- Parse a pre-decoded query string into key value pairs parameters.
+
+pQueryParams :: GenParser Char st Parameters
+pQueryParams = 
+      filter (not . null . fst)
+  <$> sepBy ((,)
+  <$> many (noneOf "=&")
+  <*> pMaybe (char '='
+   *> many (noneOf "&")))
+      (char '&')
+
+parseQueryParams :: String -> Maybe Parameters
+parseQueryParams = eitherToMaybe . parse pQueryParams  ""
+
+queryParams :: URI -> Parameters
+queryParams = maybe [] id . parseQueryParams . query
+
+-------[ filename path utilities ]---------------------------------------------
+
+extension :: URI -> Maybe String
+extension u = do
+  lst <- safeLast . _segments . _path $ u
+  guardMaybe (elem '.' lst)
+  return $ withReverse (takeWhile (/='.')) lst
+
+setExtension :: Maybe String -> URI -> URI
+setExtension e u =
+  let ex = maybe "" ('.':) e in
+  (\f -> modPath (\p -> f p ++ ex) u)
+  $ maybe id (const $ withReverse (tail . dropWhile (/='.')))
+    (extension u)
+
+modExtension :: (String -> String) -> URI -> URI
+modExtension f u = setExtension (f `fmap` extension u) u
+
+relative :: URI -> URI
+relative = modPath (dropWhile (=='/'))
+
+-- Try to guess the 'correct' mime type for the input file based on the file
+-- extension.
+
+mimetype :: URI -> Maybe String
+mimetype u = extension u >>= mime
+
+-- Normalize a path by removing or merging all dot or dot-dot segments and
+-- double slashes. Todo: cleanup, remove fixp.
+
+normalize :: FilePath -> FilePath
+normalize [] = []
+normalize p = fixAbs absolute $ intercalate "/" $ norm
+  where
+    fixAbs True          = ('/':)
+    fixAbs False         = id
+    absolute             = head p == '/'
+    norm                 = fixp False $ split '/' p
+    fixp False xs        = let ys = merge xs in fixp (xs == ys) ys
+    fixp True  xs        = xs
+    merge ("":xs)        = merge xs
+    merge (".":xs)       = merge xs
+    merge ("..":"..":xs) = ".." : ".." : merge xs
+    merge (_:"..":xs)    = merge xs
+    merge (x:xs)         = x : merge xs
+    merge xs             = xs
+
+jail :: FilePath -> FilePath -> Maybe FilePath
+jail jailDir p =
+  let nj = normalize jailDir
+      np = normalize p in
+  if nj `isPrefixOf` np -- && not (".." `isPrefixOf` np)
+    then Just np
+    else Nothing
+
+(/+) :: FilePath -> FilePath -> FilePath
+a /+ b = normalize (a ++ "/" ++ b)
+
diff --git a/src/Network/Salvia/Advanced/CleverCSS.hs b/src/Network/Salvia/Advanced/CleverCSS.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Advanced/CleverCSS.hs
@@ -0,0 +1,32 @@
+module Network.Salvia.Advanced.CleverCSS (
+    hFilterCSS
+  , hCleverCSS
+  , hParametrizedCleverCSS
+  ) where
+
+import Text.CSS.CleverCSS (cleverCSSConvert)
+
+import Network.Salvia.Handlers.ExtensionDispatcher (hExtension)
+import Network.Salvia.Handlers.File (hFile, hFileFilter)
+import Network.Salvia.Handlers.Rewrite (hRewriteExt)
+import Network.Salvia.Handlers.PathRouter (hParameters)
+import Network.Salvia.Handlers.Fallback (hOr)
+import Network.Protocol.Uri (Parameters)
+import Network.Salvia.Httpd (Handler)
+
+hFilterCSS :: Handler () -> Handler () -> Handler ()
+hFilterCSS cssfilter handler = do
+  hExtension (Just "css")
+    (hFile `hOr` cssfilter)
+    handler
+
+hCleverCSS :: Handler ()
+hCleverCSS = do
+  params <- hParameters
+  hParametrizedCleverCSS params
+
+hParametrizedCleverCSS :: Parameters -> Handler ()
+hParametrizedCleverCSS p = do
+  hRewriteExt ('c':) (hFileFilter convert)
+  where convert = either id id . flip (cleverCSSConvert "") (map (fmap $ maybe "" id) p)
+
diff --git a/src/Network/Salvia/Advanced/ExtendedFileSystem.hs b/src/Network/Salvia/Advanced/ExtendedFileSystem.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Advanced/ExtendedFileSystem.hs
@@ -0,0 +1,17 @@
+module Network.Salvia.Advanced.ExtendedFileSystem (hExtendedFileSystem) where
+
+import Network.Salvia.Handlers.File (hFile)
+import Network.Salvia.Handlers.Directory (hDirectory)
+import Network.Salvia.Handlers.FileSystem (hFileTypeDispatcher)
+import Network.Salvia.Httpd (Handler)
+import Network.Salvia.Advanced.HsColour (hHighlightHaskell, hHsColour)
+import Network.Salvia.Advanced.CleverCSS (hFilterCSS, hCleverCSS)
+
+hExtendedFileSystem :: FilePath -> Handler ()
+hExtendedFileSystem dir =
+  hFileTypeDispatcher dir
+    hDirectory
+    (hHighlightHaskell hHsColour
+      $ hFilterCSS hCleverCSS
+      $ hFile)
+
diff --git a/src/Network/Salvia/Advanced/HsColour.hs b/src/Network/Salvia/Advanced/HsColour.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Advanced/HsColour.hs
@@ -0,0 +1,55 @@
+module Network.Salvia.Advanced.HsColour (
+    hHighlightHaskell
+  , hHsColour
+  , hHsColourCustomStyle
+
+  , defaultStyleSheet
+  ) where
+
+import Language.Haskell.HsColour.CSS
+
+import Network.Salvia.Handlers.ExtensionDispatcher
+import Network.Salvia.Handlers.File
+import Network.Protocol.Http (setContentType, utf8)
+import Network.Salvia.Httpd
+
+hHighlightHaskell :: Handler () -> Handler () -> Handler ()
+hHighlightHaskell highlighter = 
+  hExtensionRouter [
+    (Just "hs",  highlighter)
+  , (Just "lhs", highlighter)
+  , (Just "ag",  highlighter)
+  ] 
+
+hHsColour :: Handler ()
+hHsColour = hHsColourCustomStyle (Left defaultStyleSheet)
+
+-- Left means direct inclusion of stylesheet, right means link to external
+-- stylesheet.
+
+hHsColourCustomStyle :: Either String String -> Handler ()
+hHsColourCustomStyle style = do
+  sendStr (either id makeStyleLink style)
+  hFileFilter (hscolour False True "")
+  modResponse
+    $ setContentType ("text/html") (Just utf8)
+
+makeStyleLink :: String -> String
+makeStyleLink css = "<link rel=\"stylesheet\" type=\"text/css\" href=\"" ++ css ++ "\"></link>"
+
+defaultStyleSheet :: String
+defaultStyleSheet = filter (/=' ') $ concat [
+    "<style>"
+  , ".varop      { color : #960; font-weight : normal; }"
+  , ".keyglyph   { color : #960; font-weight : normal; }"
+  , ".definition { color : #005; font-weight : bold;   }"
+  , ".varid      { color : #444; font-weight : normal; }"
+  , ".keyword    { color : #000; font-weight : bold;   }"
+  , ".comment    { color : #44f; font-weight : normal; }"
+  , ".conid      { color : #000; font-weight : normal; }"
+  , ".num        { color : #00a; font-weight : normal; }"
+  , ".str        { color : #a00; font-weight : normal; }"
+  , "</style>"
+  , ""
+  ]
+
diff --git a/src/Network/Salvia/Core/Config.hs b/src/Network/Salvia/Core/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Core/Config.hs
@@ -0,0 +1,33 @@
+module Network.Salvia.Core.Config (
+    HttpdConfig (..)
+  , defaultConfig
+  ) where
+
+import Network.Socket hiding (send, listen)
+import System.IO
+
+-------- server configuration -------------------------------------------------
+
+data HttpdConfig =
+   HttpdConfig {
+     hostname   :: String       -- Server hostname.
+   , email      :: String       -- Server admin email address.
+   , listenAddr :: HostAddress  -- Addres to bind to.
+   , listenPort :: PortNumber   -- Port to listen on.
+   , backlog    :: Int          -- TCP backlog.
+   , bufferSize :: Int          -- Serve chunck with size.
+   }
+
+defaultConfig :: IO HttpdConfig
+defaultConfig = do
+  addr <- inet_addr "0.0.0.0"
+  return
+    $ HttpdConfig {
+      hostname   = "hostname"
+    , email      = "www@hostname"
+    , listenAddr = addr
+    , listenPort = 80
+    , backlog    = 4
+    , bufferSize = 64 * 1024
+    }
+
diff --git a/src/Network/Salvia/Core/Handler.hs b/src/Network/Salvia/Core/Handler.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Core/Handler.hs
@@ -0,0 +1,101 @@
+{-# LANGUAGE FlexibleInstances #-}
+
+module Network.Salvia.Core.Handler (
+    SendAction
+  , SendQueue
+  , Context (..)
+  , makeContext
+
+  , Handler
+  , ResourceHandler
+  , UriHandler
+  , putRequest, putResponse, putQueue
+  , modRequest, modResponse, modQueue
+  , withRequest, withResponse
+  ) where
+
+import Control.Applicative (Applicative, pure, (<*>))
+import Control.Monad.State (StateT, ap, modify, gets)
+import Network.Socket (SockAddr)
+import System.IO
+
+import Network.Salvia.Core.Config
+import Network.Protocol.Http (Message, emptyRequest, emptyResponse)
+import Network.Protocol.Uri (URI)
+
+-------- single request/response context --------------------------------------
+
+type SendAction = Handle -> IO ()
+type SendQueue  = [SendAction]
+
+data Context =
+  Context {
+  -- | The HTTP server configuration.
+    config   :: HttpdConfig
+  -- | The HTTP request header.
+  , request  :: Message
+  -- | The HTTP response header.
+  , response :: Message
+  -- | The socket handle for the connection with the client.
+  , sock     :: Handle
+  -- | The client addres.
+  , address  :: SockAddr
+  -- | The queue of send actions.
+  , queue    :: SendQueue
+  }
+
+-- Create and empty server context.
+makeContext :: HttpdConfig -> SockAddr -> Handle -> Context
+makeContext c a s = Context {
+    config   = c
+  , request  = emptyRequest
+  , response = emptyResponse  -- 200 OK, by default.
+  , sock     = s
+  , address  = a
+  , queue    = []
+  }
+
+-------- request handlers -----------------------------------------------------
+
+-- HTTP handler types.
+type Handler         a = StateT Context IO a
+type ResourceHandler a = FilePath -> Handler a
+type UriHandler      a = URI -> Handler a
+
+-- Make handlers applicative.
+instance Applicative (StateT Context IO) where
+  pure  = return
+  (<*>) = ap
+
+-- Domain specific setters and modifiers.
+
+putRequest :: Message -> Handler ()
+putRequest  r = modify (\m -> m { request  = r })
+
+modRequest :: (Message -> Message) -> Handler ()
+modRequest  f = gets request  >>= putRequest  . f
+
+putResponse :: Message -> Handler ()
+putResponse r = modify (\m -> m { response = r })
+
+modResponse :: (Message -> Message) -> Handler ()
+modResponse f = gets response >>= putResponse . f
+
+putQueue :: SendQueue -> Handler ()
+putQueue    q = modify (\m -> m { queue = q })
+
+modQueue :: (SendQueue -> SendQueue) -> Handler ()
+modQueue    f = gets queue >>= putQueue . f
+
+-- Execute handlers in a modified environment, but restore the original state.
+-- This is comparable to the `local' function from the Reader monad.
+withModified :: (Monad m) => m d -> (t -> m a) -> (d -> m b) -> t -> m c -> m c
+withModified get m put f h =
+  do { o <- get ; m f ; r <- h ; put o ; return r }
+
+withRequest :: (Message -> Message) -> Handler a -> Handler a
+withRequest = withModified (gets request) modRequest putRequest
+
+withResponse :: (Message -> Message) -> Handler a -> Handler a
+withResponse = withModified (gets response) modResponse putResponse
+
diff --git a/src/Network/Salvia/Core/IO.hs b/src/Network/Salvia/Core/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Core/IO.hs
@@ -0,0 +1,134 @@
+module Network.Salvia.Core.IO (
+
+    sendHeaders
+
+  , send
+  , sendStr
+  , sendStrLn
+  , sendBs
+
+  , spool
+  , spoolBs
+  , spoolAll
+  , spoolN
+
+  , emptyQueue
+  , reset
+
+  , contents
+  , contentsUtf8
+  , uriEncodedPostParamsUTF8
+
+  ) where
+
+-- TODO: we are mixing two encoding libs. fix.
+
+import Control.Monad.State
+import Data.Encoding (decodeLazy)
+import Data.Encoding.UTF8
+import System.IO
+import qualified Data.ByteString.Lazy as B
+import qualified System.IO.UTF8 as U
+
+import Network.Protocol.Http
+import Network.Protocol.Uri (Parameters, parseQueryParams, decode)
+import Network.Salvia.Core.Handler
+
+-------------------------------------------------------------------------------
+
+-- Send the response header to the socket.
+sendHeaders :: Handler ()
+sendHeaders = do
+  r <- gets response
+  s <- gets sock 
+  lift $ hPutStr s (showMessageHeader r)
+
+-- Queue a potential send action in the send queue.
+send :: SendAction -> Handler ()
+send f = modify (\m -> m { queue = (queue m) ++ [f] })
+
+-- Queue a String for sending.
+sendStr, sendStrLn :: String -> Handler ()
+sendStr s   = send (flip U.hPutStr s)
+sendStrLn s = send (flip U.hPutStr (s ++ "\n"))
+
+-- Queue a ByteString for sending.
+sendBs :: B.ByteString -> Handler ()
+sendBs bs = send (flip B.hPutStr bs)
+
+{-
+Queue spooling the entire contents of a stream to the socket using a String
+based filter.
+-}
+spool :: (String -> String) -> Handle -> Handler ()
+spool f fd = send (\s -> U.hGetContents fd >>= \d -> U.hPutStr s (f d))
+
+{-
+Queue spooling the entire contents of a stream to the socket using a ByteString
+based filter.
+-}
+spoolBs :: (B.ByteString -> B.ByteString) -> Handle -> Handler ()
+spoolBs f fd = send (\s -> B.hGetContents fd >>= \d -> B.hPut s (f d))
+
+{-
+Spool the entire contents from one stream to another stream, after this the
+handle will be closed.
+-}
+spoolAll :: Handle -> Handle -> Handler ()
+spoolAll f t = lift $ do
+  B.hGetContents f >>= B.hPut t
+  hClose t
+
+{-
+Spool a fixed number of bytes from one stream to another stream, after this the
+handle will be closed.
+-}
+spoolN :: Handle -> Handle -> Int -> Handler ()
+spoolN f t n = lift $ do
+  B.hGet f n >>= B.hPut t
+  hClose t
+
+-- Reset the send queue.
+emptyQueue :: Handler ()
+emptyQueue = putQueue []
+
+-- Reset both the send queue and the generated response.
+reset :: Handler ()
+reset = do
+  putResponse emptyResponse
+  emptyQueue
+
+{- |
+First naive handler to retreive the request payload as a ByteString. This
+probably does not handle all the quirks that the HTTP protocol specifies, but
+it does the job for now. When a 'ContentLength' header field is available only
+this fixed number of bytes will read from the socket. When neither the
+'KeepAlive' and 'ContentLength' header fields are available the entire payload
+of the request will be read from the socket. This method is probably only
+useful in the case of 'PUT' request, because no decoding of 'POST' data is
+handled.
+-}
+
+contents :: Handler (Maybe B.ByteString)
+contents = do
+  r <- gets request
+  s <- gets sock
+  let len = getContentLength r
+      kpa = getKeepAlive     r
+  lift $
+    case (kpa, len) of
+      (_,       Just n)  -> liftM Just (B.hGet s n)
+      (Nothing, Nothing) -> liftM Just (B.hGetContents s)
+      _                  -> return Nothing
+
+{- |
+Like the `contents' function but decodes the data as UTF8. Soon, time will come
+that decoding will be based upon the requested encoding.
+-}
+
+contentsUtf8 :: Handler (Maybe String)
+contentsUtf8 = (fmap $ decodeLazy UTF8) `liftM` contents
+
+uriEncodedPostParamsUTF8 :: Handler (Maybe Parameters)
+uriEncodedPostParamsUTF8 = liftM (>>= parseQueryParams . decode) contentsUtf8
+
diff --git a/src/Network/Salvia/Core/Main.hs b/src/Network/Salvia/Core/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Core/Main.hs
@@ -0,0 +1,27 @@
+module Network.Salvia.Core.Main (start) where
+
+import Control.Monad.State
+
+import Network.Salvia.Core.Config (listenAddr, listenPort, backlog, HttpdConfig)
+import Network.Salvia.Core.Handler (Handler, makeContext)
+import Network.Salvia.Core.Network (server)
+
+-------- HTTP deamon implementation -------------------------------------------
+
+{-
+Given a server configuration and a handler to invoke when receiving client
+request the server starts and keep running forever.
+-}
+
+start :: HttpdConfig -> Handler () -> IO ()
+start config httpHandler =
+  server
+    (listenAddr config)
+    (listenPort config)
+    (backlog config)
+    tcpHandler
+  where
+    tcpHandler handle addr = 
+      fst `liftM` runStateT httpHandler
+        (makeContext config addr handle)
+
diff --git a/src/Network/Salvia/Core/Network.hs b/src/Network/Salvia/Core/Network.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Core/Network.hs
@@ -0,0 +1,53 @@
+module Network.Salvia.Core.Network where
+
+import Control.Concurrent (forkIO)
+-- import Control.Exception (try)
+import Control.Monad
+import Network.Socket
+import System.IO
+
+-------- application independent networking -----------------------------------
+
+type Server a =
+     Handle
+  -> SockAddr
+  -> IO a
+
+{-
+Start a listening TCP server on the specified address/port combination and
+handle every connection with a custom handler.
+-}
+
+server :: HostAddress -> PortNumber -> Int -> Server a -> IO ()
+server addr port backlog handler = do
+  sock <- socket AF_INET Stream 0
+  setSocketOption sock ReuseAddr 1
+  bindSocket sock $ SockAddrInet port addr
+  listen sock backlog
+  acceptLoop sock handler
+
+{-
+Accept connections on the listening socket and pass execution to the
+application specific connection handler.
+-}
+
+acceptLoop :: Socket -> Server s -> IO ()
+acceptLoop sock handler = do
+  forever $ do
+    (sock', addr) <- accept sock
+    forkIO $ do
+      handle <- socketToHandle sock' ReadWriteMode
+      -- TODO: Using NoBuffering here may crash the entire program (GHC
+      -- runtime?) when processing more requests than just a few:
+      hSetBuffering handle (BlockBuffering (Just (64*1024)))
+      handler handle addr
+      -- We can probably just ignore exceptions from hClose, but this for the
+      -- moment I am interested whether this even happens.
+      hClose handle
+--       e <- try (hClose handle)
+--       case e of
+--         Left ex -> putStrLn ("Failure during hClose: " ++ show (ex :: IOException))
+--         Right _ -> return ()
+  putStrLn "quiting"
+
+
diff --git a/src/Network/Salvia/Handlers/Banner.hs b/src/Network/Salvia/Handlers/Banner.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Banner.hs
@@ -0,0 +1,26 @@
+module Network.Salvia.Handlers.Banner (hBanner) where
+
+import Control.Monad.State
+import Data.Time.Clock (getCurrentTime)
+import Data.Time.LocalTime (getCurrentTimeZone, utcToLocalTime)
+import Data.Time.Format (formatTime)
+import System.Locale (defaultTimeLocale)
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+-- | The 'hBanner' handler adds the current date-/timestamp and a custom server
+-- name to the response headers.
+
+hBanner ::
+     String     -- ^ The HTTP server name.
+  -> Handler ()
+hBanner server = do
+  date <- lift $ do
+    zone <- getCurrentTimeZone
+    time <- liftM (utcToLocalTime zone) getCurrentTime
+    return $ formatTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S %Z" time
+  modResponse
+    $ setDate   date
+    . setServer server
+
diff --git a/src/Network/Salvia/Handlers/CGI.hs b/src/Network/Salvia/Handlers/CGI.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/CGI.hs
@@ -0,0 +1,19 @@
+module Network.Salvia.Handlers.CGI (hCGI) where
+
+import Control.Monad.State
+import System.IO
+import System.Process (runProcess, waitForProcess)
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+hCGI :: FilePath -> Handler ()
+hCGI name = do
+  modResponse
+    $ setStatus OK
+  h <- gets sock
+  lift $ do
+    p <- runProcess name [] Nothing Nothing (Just h) (Just h) (Just stderr)
+    waitForProcess p
+    return ()
+
diff --git a/src/Network/Salvia/Handlers/Cookie.hs b/src/Network/Salvia/Handlers/Cookie.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Cookie.hs
@@ -0,0 +1,37 @@
+module Network.Salvia.Handlers.Cookie (
+    hSetCookies
+  , hGetCookies
+
+  , newCookie
+  ) where
+
+import Control.Applicative hiding (empty)
+import Control.Monad.State
+import Data.Time.Format
+import Data.Time.LocalTime
+import System.Locale (defaultTimeLocale)
+
+import Network.Salvia.Httpd
+import Network.Protocol.Cookie
+import Network.Protocol.Http (getCookie, setCookie)
+
+hSetCookies :: Cookies -> Handler ()
+hSetCookies = modResponse . setCookie . showCookies
+
+hGetCookies :: Handler (Maybe Cookies)
+hGetCookies = parseCookies <$> getCookie <$> gets request
+
+-- Convenient method for creating cookies that expire in the near future and
+-- are bound to the domain and port this server runs on. The path will be
+-- locked to root.
+newCookie :: LocalTime -> Handler Cookie
+newCookie expire = do
+  httpd <- gets config
+  return $ empty {
+      path    = Just "/"
+--  , domain  = Just $ '.' : hostname httpd
+    , port    = [fromEnum $ listenPort httpd]
+    , expires = Just $ formatTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S %Z" expire
+    }
+
+
diff --git a/src/Network/Salvia/Handlers/Counter.hs b/src/Network/Salvia/Handlers/Counter.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Counter.hs
@@ -0,0 +1,11 @@
+module Network.Salvia.Handlers.Counter (hCounter) where
+
+import Control.Monad.State
+import Control.Concurrent.STM
+
+import Network.Salvia.Httpd
+
+hCounter :: TVar Int -> Handler ()
+hCounter c = lift $ atomically
+  $ readTVar c >>= writeTVar c . (+1)
+
diff --git a/src/Network/Salvia/Handlers/Default.hs b/src/Network/Salvia/Handlers/Default.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Default.hs
@@ -0,0 +1,32 @@
+module Network.Salvia.Handlers.Default (hDefault) where
+
+import System.IO
+import Control.Concurrent.STM
+
+import Network.Salvia.Handlers.Banner
+import Network.Salvia.Handlers.Counter
+import Network.Salvia.Handlers.Error
+import Network.Salvia.Handlers.Head
+import Network.Salvia.Handlers.Log
+import Network.Salvia.Handlers.Parser
+import Network.Salvia.Handlers.Printer
+import Network.Salvia.Handlers.Session
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+hDefault :: TVar Int -> Sessions a -> SessionHandler a () -> Handler ()
+hDefault count sessions handler = do
+  hBanner "salvia-httpd"
+  hParser onerror
+    $ hHead $ do
+      session <- hSession sessions 300
+      handler session
+  hPrinter
+  hLog count stdout
+  hCounter count
+  where
+    onerror err = do
+      hError BadRequest
+      sendStrLn []
+      sendStrLn err
+
diff --git a/src/Network/Salvia/Handlers/Directory.hs b/src/Network/Salvia/Handlers/Directory.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Directory.hs
@@ -0,0 +1,55 @@
+module Network.Salvia.Handlers.Directory (
+    hDirectory
+  , hDirectoryResource
+  ) where
+
+import Control.Monad.State
+import Data.List (sort)
+import System.Directory (doesDirectoryExist, getDirectoryContents)
+
+import Network.Salvia.Httpd
+import Network.Salvia.Handlers.Redirect
+import Network.Salvia.Handlers.File (hResource)
+import Network.Protocol.Http
+import Network.Protocol.Uri (path, modPath)
+import Misc.Misc (bool)
+
+hDirectory :: Handler ()
+hDirectory = hResource hDirectoryResource
+
+hDirectoryResource :: ResourceHandler ()
+hDirectoryResource dirName = do
+  req <- gets request
+  if (null $ path $ uri req) || (last $ path $ uri req) /= '/'
+   then hRedirect (modPath (++"/") $ uri req)
+   else dirHandler dirName
+
+dirHandler:: ResourceHandler ()
+dirHandler dirName = do
+  req <- gets request
+  filenames <- lift $ getDirectoryContents dirName
+  processed <- lift $ mapM (processFilename dirName) (sort filenames)
+  let b = listing (path $ uri req) processed
+  modResponse
+    $ setContentType   "text/html" Nothing
+    . setContentLength (length b)
+    . setStatus OK
+  sendStr b
+
+-- Add trailing slash to a directory name.
+processFilename :: FilePath -> FilePath -> IO FilePath
+processFilename d f = bool (f ++ "/") f `liftM` doesDirectoryExist (d ++ f)
+
+-- Turn a list of filenames into HTML directory listing.
+listing :: FilePath -> [FilePath] -> String
+listing dirName fileNames =
+  concat [
+    "<html><head><title>Index of "
+  , dirName
+  , "</title></head><body><h1>Index of "
+  , dirName
+  , "</h1><ul>"
+  , fileNames >>= \f -> concat ["<li><a href='", f, "'>", f, "</a></li>"]
+  , "</ul></body></html>"
+  ]
+
diff --git a/src/Network/Salvia/Handlers/Dispatching.hs b/src/Network/Salvia/Handlers/Dispatching.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Dispatching.hs
@@ -0,0 +1,24 @@
+module Network.Salvia.Handlers.Dispatching (
+    Dispatcher
+  , ListDispatcher
+  , hDispatch
+  , hListDispatch
+  ) where
+
+import Control.Monad.State
+
+import Network.Salvia.Httpd
+
+type Dispatcher     a b = a -> Handler b   -> Handler b -> Handler b
+type ListDispatcher a b = [(a, Handler b)] -> Handler b -> Handler b
+
+hDispatch :: (Show c, Show a) => (Context -> c) -> (a -> c -> Bool) -> Dispatcher a b
+hDispatch f match a handler _default = do
+  ctx <- gets f
+  if a `match` ctx
+    then handler
+    else _default
+
+hListDispatch :: Dispatcher a b -> ListDispatcher a b
+hListDispatch disp = flip $ foldr $ uncurry disp
+
diff --git a/src/Network/Salvia/Handlers/Error.hs b/src/Network/Salvia/Handlers/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Error.hs
@@ -0,0 +1,47 @@
+module Network.Salvia.Handlers.Error (
+    hError
+  , hCustomError
+  , hIOError
+  , safeIO
+  ) where
+
+import System.IO.Error
+
+import Control.Monad.State
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+{- |
+The 'hError' handler enables the creation of a default style of error responses
+for the specified HTTP status code.
+-}
+
+hError ::
+     Status     -- ^ The HTTP status code.
+  -> Handler ()
+hError e = hCustomError e
+  (concat ["[", show (codeFromStatus e), "] ", show e, "\n"])
+
+hCustomError ::
+     Status     -- ^ The HTTP status code.
+  -> String     -- ^ Custom error message.
+  -> Handler ()
+hCustomError e m = do
+  modResponse
+    $ setStatus e
+    . setContentType "text/plain" Nothing
+  sendStr m
+
+-- | Map IO errors to a default style error response.
+hIOError :: IOError -> Handler ()
+hIOError e
+  | isDoesNotExistError e = hError NotFound
+  | isAlreadyInUseError e = hError ServiceUnavailable
+  | isPermissionError   e = hError Forbidden
+  | True                  = hError InternalServerError
+
+-- | Execute an handler with the result of an IO action. When the IO actions
+-- fails a default error handler will be executed.
+safeIO :: IO a -> (a -> Handler ()) -> Handler ()
+safeIO io h = lift (try io) >>= either hIOError h
+
diff --git a/src/Network/Salvia/Handlers/ExtensionDispatcher.hs b/src/Network/Salvia/Handlers/ExtensionDispatcher.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/ExtensionDispatcher.hs
@@ -0,0 +1,16 @@
+module Network.Salvia.Handlers.ExtensionDispatcher (
+    hExtension
+  , hExtensionRouter
+  ) where
+
+import Network.Salvia.Httpd (request)
+import Network.Protocol.Http (uri)
+import Network.Protocol.Uri (extension)
+import Network.Salvia.Handlers.Dispatching
+
+hExtension :: Dispatcher (Maybe String) a
+hExtension = hDispatch (extension . uri . request) (==)
+
+hExtensionRouter :: ListDispatcher (Maybe String) a
+hExtensionRouter = hListDispatch hExtension
+
diff --git a/src/Network/Salvia/Handlers/Fallback.hs b/src/Network/Salvia/Handlers/Fallback.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Fallback.hs
@@ -0,0 +1,24 @@
+module Network.Salvia.Handlers.Fallback (hOr, hEither) where
+
+import Control.Applicative
+import Control.Monad.State
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+{- |
+Until I figure out how to do this correctly, this handler somewow implements
+the MonadPlus/Alternative instance using a custom function. When the first
+handler fails the response will be reset and the second handler is executed.
+-}
+
+hOr :: Handler a -> Handler a -> Handler a
+hOr h0 h1 = do
+  a  <- h0
+  st <- gets (status . response)
+  if statusFailure st
+    then reset >> h1
+    else return a
+
+hEither :: Handler a -> Handler b -> Handler (Either a b)
+hEither h0 h1 = (Left <$> h0) `hOr` (Right <$> h1)
+
diff --git a/src/Network/Salvia/Handlers/File.hs b/src/Network/Salvia/Handlers/File.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/File.hs
@@ -0,0 +1,69 @@
+module Network.Salvia.Handlers.File (
+    hFile
+  , hFileResource
+
+  , hFileFilter
+  , hFileResourceFilter
+
+  , hResource
+  , hUri
+  ) where
+
+import Control.Monad.State
+import System.IO
+
+import Network.Salvia.Httpd
+import Network.Salvia.Handlers.Error
+import Network.Protocol.Http
+import Network.Protocol.Uri (mimetype, path, parseURI)
+import Network.Protocol.Mime
+
+hFile :: Handler ()
+hFile = hResource hFileResource
+
+hFileFilter :: (String -> String) -> Handler ()
+hFileFilter = hResource . hFileResourceFilter
+
+{-
+Turn a resource handler into a regular handler that utilizes the path part
+of the request URI as the resource identifier.
+-}
+
+hResource :: ResourceHandler a -> Handler a
+hResource rh = gets request >>= rh . path . uri
+
+{-
+Turn a URI handler into a regular handler that utilizes the request URI as the
+resource identifier.
+-}
+
+hUri :: UriHandler a -> Handler a
+hUri rh = gets request >>= rh . uri
+
+-------- HTTP deamon implementation -------------------------------------------
+
+-- Create a response message containing the file contents.
+-- TODO: what to do with encoding?
+hFileResource :: ResourceHandler ()
+hFileResource file = do
+  let m = maybe defaultMime id $ (parseURI file >>= mimetype)
+  safeIO (openBinaryFile file ReadMode)
+    $ \fd -> do
+      fs <- lift $ hFileSize fd
+      modResponse
+        $ setContentType m (Just "utf-8")
+        . setContentLength fs
+        . setStatus OK
+      spoolBs id fd
+
+-- TODO: what to do with encoding?
+hFileResourceFilter :: (String -> String) -> ResourceHandler ()
+hFileResourceFilter fFilter file = do  -- TODO... this should be a more general hFilter
+  let m = maybe defaultMime id $ (parseURI file >>= mimetype)
+  safeIO (openBinaryFile file ReadMode)
+    $ \fd -> do
+      modResponse
+        $ setContentType m (Just "utf-8")
+        . setStatus OK
+      spool fFilter fd
+
diff --git a/src/Network/Salvia/Handlers/FileSystem.hs b/src/Network/Salvia/Handlers/FileSystem.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/FileSystem.hs
@@ -0,0 +1,41 @@
+module Network.Salvia.Handlers.FileSystem (
+    hFileSystem
+  , hFileSystemNoIndexes
+  , hFileTypeDispatcher
+  ) where
+
+import Control.Monad.State
+import System.Directory (doesDirectoryExist)
+
+import Network.Salvia.Httpd
+import Network.Salvia.Handlers.Error
+import Network.Salvia.Handlers.File
+import Network.Salvia.Handlers.Directory
+import Network.Salvia.Handlers.Rewrite
+import Network.Protocol.Http
+import Network.Protocol.Uri (jail)
+import Misc.Misc (bool)
+
+-- Show file contents and show directory indexes.
+hFileSystem :: ResourceHandler ()
+hFileSystem dir = hFileTypeDispatcher dir hDirectory hFile
+
+-- Show file contents, do not show directory indexes.
+hFileSystemNoIndexes :: ResourceHandler ()
+hFileSystemNoIndexes dir = hFileTypeDispatcher dir (hError Forbidden) hFile
+
+-- Dispatch based on file type, regular files or directories.
+hFileTypeDispatcher :: FilePath -> Handler () -> Handler () -> Handler () 
+hFileTypeDispatcher dir hdir hfile =
+  hWithDir dir
+    $ hResource
+    $ hJailedDispatch dir hdir hfile
+
+hJailedDispatch :: FilePath -> Handler () -> Handler () -> ResourceHandler () 
+hJailedDispatch dir hdir hfile file = do
+  case jail dir file of
+    Nothing -> hError Forbidden
+    Just f  -> do
+      isDir <- lift (doesDirectoryExist f)
+      bool hdir hfile isDir
+
diff --git a/src/Network/Salvia/Handlers/Head.hs b/src/Network/Salvia/Handlers/Head.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Head.hs
@@ -0,0 +1,23 @@
+module Network.Salvia.Handlers.Head (hHead) where
+
+import Control.Applicative
+import Control.Monad.State
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+{- |
+The 'hHead' handler makes sure no response body is sent to the client when the
+request is an HTTP 'HEAD' request. In the case of a 'HEAD' request the
+specified sub handler will be executed under the assumption that the request
+was a 'GET' request, otherwise this handler will act as the identify function.
+-}
+
+hHead :: Handler a -> Handler a
+hHead handler = do
+  m <- gets (method . request)
+  case m of
+    HEAD -> withRequest (setMethod GET) handler
+         <* emptyQueue
+    _    -> handler
+
diff --git a/src/Network/Salvia/Handlers/Log.hs b/src/Network/Salvia/Handlers/Log.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Log.hs
@@ -0,0 +1,31 @@
+module Network.Salvia.Handlers.Log (hLog) where
+
+import Control.Concurrent.STM
+import Control.Monad.State
+import System.IO
+
+import Network.Salvia.Httpd  (Handler, request, response, address)
+import Network.Protocol.Http
+import Misc.Terminal (red, green, reset)
+
+-- TODO: handle should be tvar as well
+hLog :: TVar Int -> Handle -> Handler Handle
+hLog count handle = do
+  c    <- lift $ atomically $ readTVar count
+  req  <- gets request
+  res  <- gets response
+  addr <- gets address
+  let st   = status res
+      code = codeFromStatus st
+      clr  = if code >= 400 then red else green
+  lift $ hPutStrLn handle $ concat [
+      concat ["[", show addr, "] ", show c, "\t"]
+    , show $ method req, "\t"
+    , show $ uri req , " -> "
+    , clr
+    , show code, " "
+    , show st
+    , reset
+    ]
+  return handle
+
diff --git a/src/Network/Salvia/Handlers/Login.hs b/src/Network/Salvia/Handlers/Login.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Login.hs
@@ -0,0 +1,242 @@
+module Network.Salvia.Handlers.Login (
+
+  -- * Basic types.
+    Username
+  , Password
+  , Action
+  , Actions
+  , User (..)
+  , Users
+  , UserDatabase
+  , TUserDatabase
+
+  -- * User Sessions.
+  , UserPayload (..)
+  , UserSession
+  , TUserSession
+  , UserSessionHandler
+
+  -- * Handlers.
+  , hSignup
+  , hLogin
+  , hLogout
+  , hLoginfo
+  , hAuthorized
+
+  -- * Helper functions.
+  , readUserDatabase
+
+  ) where
+
+import Data.List (intercalate)
+import Data.Maybe (catMaybes)
+import Control.Concurrent.STM (TVar, atomically, readTVar, writeTVar, newTVar)
+import Control.Monad.State (lift, liftM)
+
+import Network.Salvia.Handlers.Error (hCustomError, hError)
+import Network.Salvia.Handlers.Session
+import Network.Salvia.Httpd hiding (start)
+import Network.Protocol.Http (Status (Unauthorized, OK), setStatus)
+import Misc.Misc (atomModTVar, safeHead)
+import qualified Network.Protocol.Uri as URI
+
+-------------------------------------------------------------------------------
+
+type Username = String
+type Password = String
+type Action   = String
+type Actions  = [Action]
+
+{- |
+User containg a username, password and a list of actions this user is allowed
+to perform within the system.
+-}
+
+data User = User {
+    username :: Username
+  , password :: Password
+  , actions  :: Actions
+  } deriving (Eq, Show)
+
+type Users = [User]
+
+{- |
+A user database containing a list of users, a list of default actions the guest
+or `no-user' user is allowed to perform and a polymorphic reference to the
+place the database originates from. This source field cab be used by update
+functions synchronizing changes back to the database.
+-}
+
+data UserDatabase src =
+  UserDatabase {
+    dbUsers  :: Users
+  , dbGuest  :: Actions
+  , dbSource :: src
+  }
+
+type TUserDatabase src = TVar (UserDatabase src)
+
+{- |
+A user payload instance contains user related session information and can be
+used as the payload for regular sessions. It contains a reference to the user
+it is bound to, a flag to indicate whether the user is logged in or not and a
+possible user specific session payload.
+-}
+
+data UserPayload a = UserPayload {
+    upUser     :: User
+  , upLoggedIn :: Bool
+  , upPayload  :: Maybe a
+  } deriving (Eq, Show)
+
+type UserSession  a = Session  (UserPayload a)
+type TUserSession a = TSession (UserPayload a)
+
+-- A handler that requires a session with a user specific payload.
+type UserSessionHandler a b = SessionHandler (UserPayload a) b
+
+-------------------------------------------------------------------------------
+
+-- Read user data file.
+-- Format: username password action*
+
+readUserDatabase :: FilePath -> IO (TUserDatabase FilePath)
+readUserDatabase file = do
+
+  -- First line contains the default `guest` actions, tail lines contain users.
+  gst:ls <- lines `liftM` readFile file
+
+  atomically $ newTVar $ UserDatabase
+    (catMaybes $ map parseUserLine ls)
+    (words gst)
+    file
+  where
+    parseUserLine line =
+      case words line of
+        user:pass:acts -> Just (User user pass acts)
+        _              -> Nothing
+
+printUserLine :: User -> String
+printUserLine u = intercalate " " ([username u, password u] ++ actions u)
+
+{- |
+The signup handler is used to create a new entry in the user database. It reads
+a new username and password from the HTTP POST parameters and adds a new entry
+in the database when no user with such name exists. The user gets the specified
+initial set of actions assigned. On failure an `Unauthorized' error will be
+produced.
+-}
+
+hSignup :: TUserDatabase FilePath -> Actions -> Handler ()
+hSignup tdb acts = do
+  db <- lift . atomically $ readTVar tdb
+  params <- uriEncodedPostParamsUTF8
+  case freshUserPass params (dbUsers db) acts of
+    Nothing -> hCustomError Unauthorized "signup failed"
+    Just u  -> do
+      lift $ do
+        atomically
+          $ writeTVar tdb
+          $ UserDatabase (u : dbUsers db) (dbGuest db) (dbSource db)
+        appendFile (dbSource db) (printUserLine u)
+
+freshUserPass :: Maybe URI.Parameters -> Users -> Actions -> Maybe User
+freshUserPass params us acts = do
+  p <- params
+  user <- "username" `lookup` p >>= id
+  pass <- "password" `lookup` p >>= id
+  case safeHead $ filter ((==user).username) us of
+    Nothing -> return $ User user pass acts
+    Just _  -> Nothing
+
+-------------------------------------------------------------------------------
+
+{- |
+The login handler. Read the username and password values from the post data and
+use that to authenticate the user. When the user can be found in the database
+the user is logged in and stored in the session payload. Otherwise a
+`Unauthorized' response will be sent and the user has not logged in.
+-}
+
+hLogin :: UserDatabase b -> UserSessionHandler a ()
+hLogin db session = do
+  params <- uriEncodedPostParamsUTF8
+  maybe
+    (hCustomError Unauthorized "login failed")
+    (loginSuccessful session)
+    (authenticate params db)
+
+authenticate :: Maybe URI.Parameters -> UserDatabase a -> Maybe User
+authenticate params db = do
+  p <- params
+  user <- "username" `lookup` p >>= id
+  pass <- "password" `lookup` p >>= id
+  case safeHead $ filter ((==user).username) (dbUsers db) of
+    Nothing -> Nothing
+    Just u  ->
+      if password u == pass
+      then return u
+      else Nothing
+
+-- Login user and create `Ok' response on successful user.
+loginSuccessful :: TUserSession a -> User -> Handler ()
+loginSuccessful session user = do
+  lift $ atomModTVar (\s -> s {payload = Just (UserPayload user True Nothing)}) session
+  modResponse $ setStatus OK
+  sendStrLn "login successful"
+
+-------------------------------------------------------------------------------
+
+hLogout :: TUserSession a -> Handler ()
+hLogout session = do
+  lift $ atomModTVar (\s -> s {payload = Nothing}) session
+  return ()
+
+-------------------------------------------------------------------------------
+
+{- |
+The `loginfo' handler exposes the current user session to the world using a
+simple text based file. The file contains information about the current session
+identifier, session start and expiration date and the possible user payload
+that is included.
+-}
+
+hLoginfo :: Show a => UserSessionHandler a ()
+hLoginfo s = do
+  s' <- lift $ atomically $ readTVar s
+
+  sendStrLn $ "sID="     ++ show (sID     s')
+  sendStrLn $ "start="   ++ show (start   s')
+  sendStrLn $ "expire="  ++ show (expire  s')
+
+  case payload s' of
+    Nothing -> return ()
+    Just (UserPayload (User uname _ acts) _ _) -> do
+      sendStrLn $ "username=" ++ uname
+      sendStrLn $ "actions="  ++ intercalate " " acts
+
+-------------------------------------------------------------------------------
+
+{- |
+Execute a handler only when the user for the current session is authorized to
+do so. The user must have the specified action contained in its actions list in
+order to be authorized. Otherwise an `Unauthorized' error will be produced.
+When no user can be found in the current session or this user is not logged in
+the guest account from the user database is used for authorization.
+-}
+
+hAuthorized ::
+     UserDatabase b              -- ^ The user database to read guest account from.
+  -> Action                      -- ^ The actions that should be authorized.
+  -> (Maybe User -> Handler ())  -- ^ The handler to perform when authorized.
+  -> UserSessionHandler a ()     -- ^ This handler requires a user session.
+
+hAuthorized db action handler session = do
+  load <- liftM payload (lift $ atomically $ readTVar session)
+  case load of
+    Just (UserPayload user _ _)
+      | action `elem` actions user -> handler (Just user)
+    Nothing
+      | action `elem` dbGuest db   -> handler Nothing
+    _                              -> hError Unauthorized
+
diff --git a/src/Network/Salvia/Handlers/MethodRouter.hs b/src/Network/Salvia/Handlers/MethodRouter.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/MethodRouter.hs
@@ -0,0 +1,41 @@
+module Network.Salvia.Handlers.MethodRouter (
+    hMethod
+  , hMethodRouter
+
+  , hOPTIONS
+  , hGET
+  , hHEAD
+  , hPOST
+  , hPUT
+  , hDELETE
+  , hTRACE
+  , hCONNECT
+  ) where
+
+import Network.Salvia.Handlers.Dispatching
+import Network.Salvia.Handlers.Error
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+hMethod' :: Dispatcher Method a
+hMethod' = hDispatch (method . request) (==)
+
+hMethodRouter :: ListDispatcher Method a
+hMethodRouter = hListDispatch hMethod'
+
+hMethod :: Method -> Handler () -> Handler ()
+hMethod m k = hMethod' m k (hError NotFound)
+
+-- Shortcut handlers that only work for specific method types or fail.
+hOPTIONS, hGET, hHEAD, hPOST, hPUT, hDELETE, hTRACE, hCONNECT
+  :: Handler () -> Handler ()
+
+hOPTIONS = hMethod OPTIONS
+hGET     = hMethod GET
+hHEAD    = hMethod HEAD
+hPOST    = hMethod POST
+hPUT     = hMethod PUT
+hDELETE  = hMethod DELETE
+hTRACE   = hMethod TRACE
+hCONNECT = hMethod CONNECT
+
diff --git a/src/Network/Salvia/Handlers/Parser.hs b/src/Network/Salvia/Handlers/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Parser.hs
@@ -0,0 +1,41 @@
+module Network.Salvia.Handlers.Parser (hParser) where
+
+import System.IO
+import Control.Monad.State
+import Text.ParserCombinators.Parsec (parse)
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+
+{- |
+The 'hParser' handler is used to parse the raw request message into the
+'Message' data type. This handler is generally used as (one of) the first
+handlers in a configuration. The first handler argument is executed when the
+request is invalid, possibly due to parser errors. The second handler argument
+is executed when the request is valid.
+-}
+
+hParser ::
+     (String -> Handler a) -- ^ The fail handler.
+  -> Handler a             -- ^ The succeed handler.
+  -> Handler a
+
+hParser onfail onsuccess = do
+  h <- gets sock
+  -- TODO use try and fail with bad request or reject silently.
+  msg <- lift $ readHeader h `catch` error "AAAAp"
+  case parse pRequest "" (msg "") of
+    Left err -> onfail (show err)
+    Right x -> do
+      putRequest x
+      onsuccess
+
+-- Read all lines until the first empty line.
+readHeader :: Handle -> IO (String -> String)
+readHeader h = do
+  l <- hGetLine h
+  let lf = showChar '\n'
+  if l `elem` ["", "\r"]
+    then return lf
+    else liftM ((showString l . lf) .) (readHeader h)
+
diff --git a/src/Network/Salvia/Handlers/PathRouter.hs b/src/Network/Salvia/Handlers/PathRouter.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/PathRouter.hs
@@ -0,0 +1,37 @@
+module Network.Salvia.Handlers.PathRouter (
+    hPath
+  , hPathRouter
+  , hPrefix
+  , hPrefixRouter
+
+  , hParameters
+  ) where
+
+import Control.Monad.State
+import Data.List (isPrefixOf)
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+import Network.Protocol.Uri (modPath, path, queryParams, Parameters)
+import Network.Salvia.Handlers.Dispatching
+
+chop :: String -> Handler a -> Handler a
+chop a = withRequest $ modUri $ modPath $ drop $ length a
+
+hPath :: Dispatcher String a
+hPath p h d = hDispatch (path . uri . request) (==) p (chop p h) d
+
+hPathRouter :: ListDispatcher String b
+hPathRouter = hListDispatch hPath
+
+hPrefix :: Dispatcher String a
+hPrefix p h d = hDispatch (path . uri . request) isPrefixOf p (chop p h) d
+
+hPrefixRouter :: ListDispatcher String b
+hPrefixRouter = hListDispatch hPrefix
+
+-- Path related utilities.
+
+hParameters :: Handler Parameters
+hParameters = gets (queryParams . uri . request)
+
diff --git a/src/Network/Salvia/Handlers/Printer.hs b/src/Network/Salvia/Handlers/Printer.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Printer.hs
@@ -0,0 +1,23 @@
+module Network.Salvia.Handlers.Printer (hPrinter) where
+
+import Control.Monad.State
+
+import Network.Salvia.Httpd
+
+{- |
+The 'hPrinter' handler print the entire response including the headers to the
+client. This handler is generally used as (one of) the last handler in a
+configuration.
+-}
+
+hPrinter :: Handler ()
+hPrinter = do
+
+  -- Send the entire HTTP response header.
+  sendHeaders
+
+  -- Process all send actions in queue.
+  s <- gets sock
+  q <- gets queue
+  lift $ mapM_ ($ s) q
+
diff --git a/src/Network/Salvia/Handlers/Put.hs b/src/Network/Salvia/Handlers/Put.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Put.hs
@@ -0,0 +1,27 @@
+module Network.Salvia.Handlers.Put (hPut) where
+
+import Control.Monad.State
+import System.IO
+import qualified Data.ByteString.Lazy as B
+
+import Network.Salvia.Httpd
+import Network.Salvia.Handlers.Error
+import Network.Protocol.Http
+
+{- |
+First naive handler for the PUT request. This probably does not handle all the
+quirks that the HTTP protocol specifies, but it does the job for now. When a
+'ContentLength' header field is available only this fixed number of bytes will
+be spooled from socket to the resource. When both the KeepAlive' and
+'ContentLength' header fields are not available the entire payload of the
+request is spooled to the resource.
+--}
+
+hPut :: ResourceHandler ()
+hPut name =
+  safeIO (openBinaryFile name WriteMode)
+    $ (contents >>=) . maybe putError . putOk
+  where
+    putError   = hError NotImplemented
+    putOk fd c = lift (B.hPut fd c >> hClose fd)
+
diff --git a/src/Network/Salvia/Handlers/Redirect.hs b/src/Network/Salvia/Handlers/Redirect.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Redirect.hs
@@ -0,0 +1,12 @@
+module Network.Salvia.Handlers.Redirect (hRedirect) where
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+import Network.Protocol.Uri (URI)
+
+hRedirect :: URI -> Handler ()
+hRedirect u = do
+  modResponse
+    $ setLocation u
+    . setStatus MovedPermanently
+
diff --git a/src/Network/Salvia/Handlers/Rewrite.hs b/src/Network/Salvia/Handlers/Rewrite.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Rewrite.hs
@@ -0,0 +1,38 @@
+module Network.Salvia.Handlers.Rewrite (
+    hRewrite
+  , hRewritePath
+  , hRewriteHost
+  , hRewriteExt
+  , hWithDir
+  , hWithoutDir
+  ) where
+
+import Data.List (isPrefixOf)
+import Control.Monad.State
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+import Network.Protocol.Uri (URI, modPath, modHost, path, modExtension)
+
+hRewrite :: (URI -> URI) -> Handler a -> Handler a
+hRewrite f h = withRequest (modUri f) h
+
+hRewriteHost :: (String -> String) -> Handler a -> Handler a
+hRewriteHost f h = withRequest (modUri $ modHost f) h
+
+hRewritePath :: (String -> String) -> Handler a -> Handler a
+hRewritePath f h = withRequest (modUri $ modPath f) h
+
+hRewriteExt :: (String -> String) -> Handler a -> Handler a
+hRewriteExt f = hRewrite (modExtension f)
+
+hWithDir :: String -> Handler a -> Handler a
+hWithDir d = hRewritePath (d++)
+
+hWithoutDir :: String -> Handler a -> Handler a
+hWithoutDir d h = do
+  req <- gets request
+  if (d `isPrefixOf` (path $ uri $ req))
+    then hRewritePath (drop (length d)) h
+    else h
+
diff --git a/src/Network/Salvia/Handlers/Session.hs b/src/Network/Salvia/Handlers/Session.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/Session.hs
@@ -0,0 +1,181 @@
+module Network.Salvia.Handlers.Session (
+    hSession
+
+  , SessionID
+  , Session (..)
+  , TSession
+  , SessionHandler
+  , Sessions
+
+  , mkSessions
+  ) where
+
+import Control.Applicative hiding (empty)
+import Control.Concurrent.STM
+import Control.Monad.State
+import Data.Time.LocalTime
+import Prelude hiding (lookup)
+import System.Random
+import qualified Data.Map as M
+
+import Network.Salvia.Handlers.Cookie
+import Network.Salvia.Httpd hiding (start)
+import Network.Protocol.Cookie hiding (empty)
+import Misc.Misc (safeRead, atomModTVar, atomReadTVar, now, later)
+
+-------------------------------------------------------------------------------
+
+-- A session identifier. Should be unique for every session.
+newtype SessionID = SID Integer
+  deriving (Eq, Ord)
+
+-- The session data type with polymorph payload.
+data Session a = Session {
+    sID     :: SessionID
+  , start   :: LocalTime
+  , expire  :: LocalTime
+  , payload :: Maybe a
+  }
+
+-- A shared session.
+type TSession a = TVar (Session a)
+
+-- A handler that expects a session.
+type SessionHandler a b = TSession a -> Handler b
+
+-- Create a new, empty, shared session.
+mkSession :: SessionID -> LocalTime -> IO (TSession a)
+mkSession sid e = do
+  s <- now
+  atomically $ newTVar $ Session sid s e Nothing
+
+-------------------------------------------------------------------------------
+
+type Sessions a = TVar (M.Map SessionID (TSession a))
+
+instance Show SessionID where
+  show (SID sid) = show sid
+
+mkSessions :: IO (Sessions a)
+mkSessions = atomically $ newTVar M.empty
+
+-------------------------------------------------------------------------------
+
+{-
+The session handler. This handler will try to return an existing session from
+the sessions map based on a session identifier found in the HTTP cookie. When
+such a session can be found the expiration date will be updated to a number of
+seconds in the future. When no session can be found a new one will be created.
+A cookie will be set that informs the client of the current session.
+-}
+
+hSession :: Sessions a -> Integer -> Handler (TSession a)
+hSession smap expiration = do
+
+  -- Get the session identifier from an existing cookie or create a new one.
+  prev <- getSessionID <$> hGetCookies
+
+  -- Compute current time and expiration time.
+  (n, ex) <- lift $ liftM2 (,) now (later expiration)
+
+  -- Either create a new session or try to reuse current one.
+  tsession <- 
+    maybe
+    (newSession smap ex)
+    (existingSession smap ex n)
+    prev
+
+  setSessionCookie tsession ex
+  return tsession
+
+-------------------------------------------------------------------------------
+
+{-
+Given the (possible wrong) request cookie, try to recover the existing --
+session identifier.
+-}
+
+getSessionID :: Maybe Cookies -> Maybe SessionID
+getSessionID prev = do
+  ck  <- prev
+  sid <- cookie "sid" ck
+  sid' <- safeRead $ value sid
+  return (SID sid')
+  
+{-
+Generate a fresh, random session identifier using the default system random
+generator.
+-}
+
+genSessionID :: IO SessionID
+genSessionID = do
+  g <- getStdGen
+  let (sid, g') = random g
+  setStdGen g'
+  return (SID (abs sid))
+
+
+{-
+This handler sets the HTTP cookie for the specified session. It will use a
+default cookie with an additional `sid' attribute with the session identifier
+as value. The session expiration date will be used as the cookie expire field.
+-}
+
+setSessionCookie :: TSession a -> LocalTime -> Handler ()
+setSessionCookie tsession ex = do
+  ck <- newCookie ex
+  sid <- lift $ liftM sID $ atomReadTVar tsession
+  hSetCookies $
+    cookies [ck {
+      name  = "sid"
+    , value = show sid
+    }]
+
+{-
+Handler when no (valid) session is available. Create a new session with a
+specified expiration date. The session will be stored in the session map.
+-}
+
+newSession :: Sessions a -> LocalTime -> Handler (TSession a)
+newSession sessions ex = lift $ do
+
+  -- Fresh session identifier.
+  sid <- genSessionID
+
+  -- Fresh session.
+  session <- mkSession sid ex
+
+  -- Place in session mapping usinf session identifier as key.
+  atomModTVar (M.insert sid session) sessions
+  return session
+
+{-
+Handler for existing sessions. Given an existing session identifier lookup a
+session from the session map. When no session is available, or the session is
+expired, create a new one using the `newSession' function. Otherwise the
+expiration date of the existing session is updated.
+-}
+
+existingSession ::
+     Sessions a -> LocalTime -> LocalTime
+  -> SessionID -> Handler (TSession a)
+existingSession sessions ex n sid = do
+
+  -- Lookup the session in the session map given the session identifier.
+  mtsession <- lift $ liftM (M.lookup sid) (atomReadTVar sessions)
+  case mtsession of
+
+    -- Unrecognized session identifiers are penalised by a fresh session.
+    Nothing -> newSession sessions ex
+    Just tsession -> do
+      expd <- lift $ liftM expire (atomReadTVar tsession)
+      if expd < n
+
+        -- Session is expired, create a new one.
+        then newSession sessions ex
+
+        -- Existing session, update expiration date.
+        else lift $ do
+          atomModTVar (\s -> s {expire = ex}) tsession
+          return tsession
+
diff --git a/src/Network/Salvia/Handlers/VirtualHosting.hs b/src/Network/Salvia/Handlers/VirtualHosting.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Handlers/VirtualHosting.hs
@@ -0,0 +1,16 @@
+module Network.Salvia.Handlers.VirtualHosting (hVirtualHosting) where
+
+import Data.List (isPrefixOf)
+
+import Network.Salvia.Httpd
+import Network.Protocol.Http
+import Network.Salvia.Handlers.Dispatching
+
+-- isSuffixOf?
+
+hVirtualHosting1 :: Dispatcher String a
+hVirtualHosting1 = hDispatch (getHost . request) isPrefixOf
+
+hVirtualHosting :: ListDispatcher String a
+hVirtualHosting = hListDispatch hVirtualHosting1
+
diff --git a/src/Network/Salvia/Httpd.hs b/src/Network/Salvia/Httpd.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Salvia/Httpd.hs
@@ -0,0 +1,44 @@
+module Network.Salvia.Httpd (
+
+  -- Httpd.Core.Config
+    HttpdConfig (..)
+  , defaultConfig
+
+  -- Httpd.Core.Handler
+  , Context (..)
+  , makeContext
+
+  , Handler
+  , ResourceHandler
+  , UriHandler
+  , putRequest, putResponse, putQueue
+  , modRequest, modResponse, modQueue
+  , withRequest, withResponse
+
+  -- Httpd.Core.IO
+  , sendHeaders
+  , send
+  , sendStr
+  , sendStrLn
+  , sendBs
+  , spool
+  , spoolBs
+  , spoolAll
+  , spoolN
+  , emptyQueue
+  , reset
+
+  , contents
+  , contentsUtf8
+  , uriEncodedPostParamsUTF8
+
+  -- Httpd.Core.Main
+  , start
+
+  ) where
+
+import Network.Salvia.Core.Config
+import Network.Salvia.Core.Handler
+import Network.Salvia.Core.IO
+import Network.Salvia.Core.Main
+
