[Initial darcs import. bringert@cs.chalmers.se**20060828154609] { adddir ./examples addfile ./CGILight.hs addfile ./hello.hs move ./hello.hs ./examples/hello.hs hunk ./CGILight.hs 1 +{- | + Module : CGILight + Copyright : (c) The University of Glasgow 2001 + (c) Peter Thiemann 2001,2002 + (c) Bjorn Bringert 2004-2006 + License : BSD-style + + Maintainer : bjorn@bringert.net + Stability : experimental + Portability : portable + + + This is a simple CGI library for teaching purposes. + It does not support file uploads, custom error handling + ByteString output. + Use the cgi package when you need any of those. + + A simple example: + +@ +import CGILight + +cgiMain :: Environment -> Inputs -> IO (Headers,String) +cgiMain env input = return (hs, output) + where + hs = [("Content-type","text/html")] + output = "

Hello World!

" + +main :: IO () +main = runCGI cgiMain +@ + + This module may include code by + Erik Meijer , + Sven Panne , + Andy Gill , + and Peter Thiemann. + +-} +module CGILight ( + -- * Types + Environment, Inputs, Headers, + -- * Running CGI programs + runCGI, + -- * URL encoding + formEncode, urlEncode, formDecode, urlDecode, + ) where + +import Data.Char +import Data.List +import Data.Maybe +import Network.URI +import System.Environment +import System.IO +import Text.ParserCombinators.Parsec + + +type Environment = [(String,String)] +type Inputs = [(String,String)] + +type Header = (String, String) +type Headers = [Header] + +runCGI :: (Environment -> Inputs -> IO (Headers,String)) -> IO () +runCGI f = + do env <- getCgiVars + input <- hGetContents stdin + let inputs = decodeInput env input + (hs,output) <- f env inputs + mapM_ (hPutStrLn stdout . formatHeader) hs + hPutStrLn stdout "" + hPutStrLn stdout output + hFlush stdout + where formatHeader (n,v) = n ++ ": " ++ v + + + +-- This ought to be getEnvironment, but olders versions +-- of System.Environment don't have that. +getCgiVars :: IO [(String,String)] +getCgiVars = do vals <- mapM myGetEnv cgiVarNames + return (zip cgiVarNames vals) + +myGetEnv :: String -> IO String +myGetEnv v = Prelude.catch (getEnv v) (const (return "")) + +cgiVarNames :: [String] +cgiVarNames = + [ "DOCUMENT_ROOT" + , "AUTH_TYPE" + , "GATEWAY_INTERFACE" + , "SERVER_SOFTWARE" + , "SERVER_NAME" + , "REQUEST_METHOD" + , "SERVER_ADMIN" + , "SERVER_PORT" + , "QUERY_STRING" + , "CONTENT_LENGTH" + , "CONTENT_TYPE" + , "REMOTE_USER" + , "REMOTE_IDENT" + , "REMOTE_ADDR" + , "REMOTE_HOST" + , "TZ" + , "PATH" + , "PATH_INFO" + , "PATH_TRANSLATED" + , "SCRIPT_NAME" + , "SCRIPT_FILENAME" + , "HTTP_CONNECTION" + , "HTTP_ACCEPT_LANGUAGE" + , "HTTP_ACCEPT" + , "HTTP_HOST" + , "HTTP_UA_COLOR" + , "HTTP_UA_CPU" + , "HTTP_UA_OS" + , "HTTP_UA_PIXELS" + , "HTTP_USER_AGENT" + ] + + + + + + + +-- +-- * Inputs +-- + +-- | Get and decode the input according to the request +-- method and the content-type. +decodeInput :: [(String,String)] -- ^ CGI environment variables. + -> String -- ^ Request body. + -> Inputs -- ^ Input variables and values. +decodeInput env inp = queryInput env ++ bodyInput env inp + + +-- +-- * Query string +-- + +-- | Get inputs from the query string. +queryInput :: [(String,String)] -- ^ CGI environment variables. + -> Inputs -- ^ Input variables and values. +queryInput env = formInput $ lookupOrNil "QUERY_STRING" env + +-- | Decode application\/x-www-form-urlencoded inputs. +formInput :: String + -> Inputs -- ^ Input variables and values. +formInput qs = [(n, v) | (n,v) <- formDecode qs] + + +-- +-- * URL encoding +-- + +-- | Format name-value pairs as application\/x-www-form-urlencoded. +formEncode :: [(String,String)] -> String +formEncode xs = + concat $ intersperse "&" [urlEncode n ++ "=" ++ urlEncode v | (n,v) <- xs] + +-- | Convert a single value to the application\/x-www-form-urlencoded encoding. +urlEncode :: String -> String +urlEncode = replace ' ' '+' . escapeURIString okChar + where okChar c = c == ' ' || + (isUnescapedInURI c && c `notElem` "&=+") + +-- | Get the name-value pairs from application\/x-www-form-urlencoded data. +formDecode :: String -> [(String,String)] +formDecode "" = [] +formDecode s = (urlDecode n, urlDecode (drop 1 v)) : formDecode (drop 1 rs) + where (nv,rs) = break (=='&') s + (n,v) = break (=='=') nv + +-- | Convert a single value from the +-- application\/x-www-form-urlencoded encoding. +urlDecode :: String -> String +urlDecode = unEscapeString . replace '+' ' ' + + + +-- +-- * Request content +-- + +-- | Get input variables from the body, if any. +bodyInput :: [(String,String)] -- ^ CGI environment variables. + -> String -- ^ Request body. + -> Inputs -- ^ Input variables and values. +bodyInput env inp = + case lookup "REQUEST_METHOD" env of + Just "POST" -> + let ctype = lookup "CONTENT_TYPE" env >>= parseContentType + in decodeBody ctype $ takeInput env inp + _ -> [] + +-- | Decode a POST body. +decodeBody :: Maybe ContentType -- ^ Content-type, if any + -> String -- ^ Request body + -> Inputs -- ^ Input variables and values. +decodeBody ctype inp = + case ctype of + Just (ContentType "application" "x-www-form-urlencoded" _) + -> formInput inp + Just _ -> [] -- unknown content-type, the user will have to + -- deal with it by looking at the raw content + -- No content-type given, assume x-www-form-urlencoded + Nothing -> formInput inp + +-- | Take the right number of bytes from the input. +takeInput :: [(String,String)] -- ^ CGI environment variables. + -> String -- ^ Request body. + -> String -- ^ CONTENT_LENGTH bytes from the request + -- body, or the empty string if there is no + -- CONTENT_LENGTH. +takeInput env req = + case len of + Just l -> take l req + Nothing -> "" + where len = lookup "CONTENT_LENGTH" env >>= maybeRead + + + +-- +-- * Utilities +-- + +-- | Replace all instances of a value in a list by another value. +replace :: Eq a => + a -- ^ Value to look for + -> a -- ^ Value to replace it with + -> [a] -- ^ Input list + -> [a] -- ^ Output list +replace x y = map (\z -> if z == x then y else z) + +maybeRead :: Read a => String -> Maybe a +maybeRead = fmap fst . listToMaybe . reads + +-- | Same as 'lookup' specialized to strings, but +-- returns the empty string if lookup fails. +lookupOrNil :: String -> [(String,String)] -> String +lookupOrNil n = fromMaybe "" . lookup n + + + + +-- +-- * Parameters (for Content-type etc.) +-- + +p_parameter :: Parser (String,String) +p_parameter = + do lexeme $ char ';' + p_name <- lexeme $ p_token + lexeme $ char '=' + -- Workaround for seemingly standardized web browser bug + -- where nothing is escaped in the filename parameter + -- of the content-disposition header in multipart/form-data + let litStr = if p_name == "filename" + then buggyLiteralString + else literalString + p_value <- litStr <|> p_token + return (map toLower p_name, p_value) + +-- +-- * Content type +-- + +data ContentType = + ContentType String String [(String, String)] + deriving (Show, Read, Eq, Ord) + +pContentType :: Parser ContentType +pContentType = + do many ws1 + c_type <- p_token + lexeme $ char '/' + c_subtype <- lexeme $ p_token + c_parameters <- many p_parameter + return $ ContentType (map toLower c_type) (map toLower c_subtype) c_parameters + +parseContentType :: Monad m => String -> m ContentType +parseContentType = parseM pContentType "Content-type" + +-- +-- * Utilities +-- + +parseM :: Monad m => Parser a -> SourceName -> String -> m a +parseM p n inp = + case parse p n inp of + Left e -> fail (show e) + Right x -> return x + +-- +-- * Parsing utilities +-- + +-- | RFC 822 LWSP-char +ws1 :: Parser Char +ws1 = oneOf " \t" + +lexeme :: Parser a -> Parser a +lexeme p = do x <- p; many ws1; return x + +literalString :: Parser String +literalString = do char '\"' + str <- many (noneOf "\"\\" <|> quoted_pair) + char '\"' + return str + +-- No web browsers seem to implement RFC 2046 correctly, +-- since they do not escape double quotes and backslashes +-- in the filename parameter in multipart/form-data. +-- +-- Note that this eats everything until the last double quote on the line. +buggyLiteralString :: Parser String +buggyLiteralString = + do char '\"' + str <- manyTill anyChar (try lastQuote) + return str + where lastQuote = do char '\"' + notFollowedBy (try (many (noneOf "\"") >> char '\"')) + +especials, tokenchar :: [Char] +especials = "()<>@,;:\\\"/[]?.=" +tokenchar = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" \\ especials + +p_token :: Parser String +p_token = many1 (oneOf tokenchar) + +text_chars :: [Char] +text_chars = map chr ([1..9] ++ [11,12] ++ [14..127]) + +p_text :: Parser Char +p_text = oneOf text_chars + +quoted_pair :: Parser Char +quoted_pair = do char '\\' + p_text hunk ./examples/hello.hs 1 +#!/usr/bin/env runhugs + +import CGILight + +cgiMain :: Environment -> Inputs -> IO (Headers,String) +cgiMain env input = return (hs, output) + where + hs = [("Content-type","text/html")] + output = "

Hello World!

" + +main :: IO () +main = runCGI cgiMain }