salvia (empty) → 0.0.4
raw patch · 42 files changed
+3906/−0 lines, 42 filesdep +basedep +bimapdep +bytestringsetup-changed
Dependencies added: base, bimap, bytestring, clevercss, containers, directory, encoding, filepath, hscolour, mtl, network, old-locale, parsec, process, random, stm, time, utf8-string
Files
- LICENSE +28/−0
- Setup.lhs +6/−0
- salvia.cabal +71/−0
- src/Misc/Misc.hs +161/−0
- src/Misc/Terminal.hs +211/−0
- src/Network/Protocol/Cookie.hs +165/−0
- src/Network/Protocol/Http.hs +388/−0
- src/Network/Protocol/Mime.hs +653/−0
- src/Network/Protocol/Uri.hs +614/−0
- src/Network/Salvia/Advanced/CleverCSS.hs +32/−0
- src/Network/Salvia/Advanced/ExtendedFileSystem.hs +17/−0
- src/Network/Salvia/Advanced/HsColour.hs +55/−0
- src/Network/Salvia/Core/Config.hs +33/−0
- src/Network/Salvia/Core/Handler.hs +101/−0
- src/Network/Salvia/Core/IO.hs +134/−0
- src/Network/Salvia/Core/Main.hs +27/−0
- src/Network/Salvia/Core/Network.hs +53/−0
- src/Network/Salvia/Handlers/Banner.hs +26/−0
- src/Network/Salvia/Handlers/CGI.hs +19/−0
- src/Network/Salvia/Handlers/Cookie.hs +37/−0
- src/Network/Salvia/Handlers/Counter.hs +11/−0
- src/Network/Salvia/Handlers/Default.hs +32/−0
- src/Network/Salvia/Handlers/Directory.hs +55/−0
- src/Network/Salvia/Handlers/Dispatching.hs +24/−0
- src/Network/Salvia/Handlers/Error.hs +47/−0
- src/Network/Salvia/Handlers/ExtensionDispatcher.hs +16/−0
- src/Network/Salvia/Handlers/Fallback.hs +24/−0
- src/Network/Salvia/Handlers/File.hs +69/−0
- src/Network/Salvia/Handlers/FileSystem.hs +41/−0
- src/Network/Salvia/Handlers/Head.hs +23/−0
- src/Network/Salvia/Handlers/Log.hs +31/−0
- src/Network/Salvia/Handlers/Login.hs +242/−0
- src/Network/Salvia/Handlers/MethodRouter.hs +41/−0
- src/Network/Salvia/Handlers/Parser.hs +41/−0
- src/Network/Salvia/Handlers/PathRouter.hs +37/−0
- src/Network/Salvia/Handlers/Printer.hs +23/−0
- src/Network/Salvia/Handlers/Put.hs +27/−0
- src/Network/Salvia/Handlers/Redirect.hs +12/−0
- src/Network/Salvia/Handlers/Rewrite.hs +38/−0
- src/Network/Salvia/Handlers/Session.hs +181/−0
- src/Network/Salvia/Handlers/VirtualHosting.hs +16/−0
- src/Network/Salvia/Httpd.hs +44/−0
+ LICENSE view
@@ -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.+
+ Setup.lhs view
@@ -0,0 +1,6 @@+#! /usr/bin/env runhaskell++>import Distribution.Simple++>main = defaultMain+
+ salvia.cabal view
@@ -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+
+ src/Misc/Misc.hs view
@@ -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)+
+ src/Misc/Terminal.hs view
@@ -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+
+ src/Network/Protocol/Cookie.hs view
@@ -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")+
+ src/Network/Protocol/Http.hs view
@@ -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+
+ src/Network/Protocol/Mime.hs view
@@ -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")+ ]+
+ src/Network/Protocol/Uri.hs view
@@ -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)+
+ src/Network/Salvia/Advanced/CleverCSS.hs view
@@ -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)+
+ src/Network/Salvia/Advanced/ExtendedFileSystem.hs view
@@ -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)+
+ src/Network/Salvia/Advanced/HsColour.hs view
@@ -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>"+ , ""+ ]+
+ src/Network/Salvia/Core/Config.hs view
@@ -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+ }+
+ src/Network/Salvia/Core/Handler.hs view
@@ -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+
+ src/Network/Salvia/Core/IO.hs view
@@ -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+
+ src/Network/Salvia/Core/Main.hs view
@@ -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)+
+ src/Network/Salvia/Core/Network.hs view
@@ -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"++
+ src/Network/Salvia/Handlers/Banner.hs view
@@ -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+
+ src/Network/Salvia/Handlers/CGI.hs view
@@ -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 ()+
+ src/Network/Salvia/Handlers/Cookie.hs view
@@ -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+ }++
+ src/Network/Salvia/Handlers/Counter.hs view
@@ -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)+
+ src/Network/Salvia/Handlers/Default.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Directory.hs view
@@ -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>"+ ]+
+ src/Network/Salvia/Handlers/Dispatching.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Error.hs view
@@ -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+
+ src/Network/Salvia/Handlers/ExtensionDispatcher.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Fallback.hs view
@@ -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)+
+ src/Network/Salvia/Handlers/File.hs view
@@ -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+
+ src/Network/Salvia/Handlers/FileSystem.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Head.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Log.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Login.hs view
@@ -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+
+ src/Network/Salvia/Handlers/MethodRouter.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Parser.hs view
@@ -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)+
+ src/Network/Salvia/Handlers/PathRouter.hs view
@@ -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)+
+ src/Network/Salvia/Handlers/Printer.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Put.hs view
@@ -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)+
+ src/Network/Salvia/Handlers/Redirect.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Rewrite.hs view
@@ -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+
+ src/Network/Salvia/Handlers/Session.hs view
@@ -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+
+ src/Network/Salvia/Handlers/VirtualHosting.hs view
@@ -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+
+ src/Network/Salvia/Httpd.hs view
@@ -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+