WashNGo-2.12: WASH/CGI/RawCGIInternal.hs
-- © 2001, 2002 Peter Thiemann
-- |Low-level interface to CGI scripting.
module WASH.CGI.RawCGIInternal
(module WASH.CGI.RawCGITypes, module WASH.CGI.RawCGIInternal)
where
-- (CGIParameter, CGIParameters, start, assocParm, assoc)
import Array
import Char
import IO
import List
import Maybe
import Random
import System
import WASH.Utility.Auxiliary
import WASH.Utility.Unique
import qualified WASH.Utility.URLCoding as URLCoding
import qualified WASH.Utility.RFC2279 as RFC2279
import WASH.CGI.CGIConfig
import WASH.CGI.CGITypes
import WASH.CGI.HTTP
import WASH.CGI.RawCGITypes
import WASH.Utility.BulkIO (rawGetBytes)
import WASH.Utility.Hex
import qualified WASH.CGI.Debug as Debug
getGenericOption isOption defaultOption options =
fromJust (find isOption (reverse (defaultOption:options)))
-- | Decoding of 'CGIOption'.
isPortOption NoPort = True
isPortOption AutoPort = True
isPortOption (Port _) = True
isPortOption _ = False
getPortOption =
getGenericOption isPortOption AutoPort
-- | Decoding of 'CGIOption'.
isHttpsOption NoHttps = True
isHttpsOption AutoHttps = True
isHttpsOption _ = False
getHttpsOption =
getGenericOption isHttpsOption AutoHttps
-- | Decoding of 'CGIOption'.
isURLOption FullURL = True
isURLOption PartialURL = True
isURLOption _ = False
getURLOption =
getGenericOption isURLOption PartialURL
-- | Decoding of 'CGIOption'.
isSessionOption (SessionMode _) = True
isSessionOption _ = False
getSessionMode =
unSessionMode . getGenericOption isSessionOption (SessionMode LogOnly)
fieldName = fst
fieldContents = snd
-- |Initial and default content type of a link submission
contentTypeUrlencoded = "application/x-www-form-urlencoded"
-- |Construct a CGI environment from the input and output handle of the current
-- connection and the CGI variables in the environment.
initEnv :: Handle -> Handle -> IO CGIEnv
initEnv hIn hOut =
do server_name <- protectedGetEnv "SERVER_NAME" "localhost"
server_port <- protectedGetEnv "SERVER_PORT" "80"
server_software <- protectedGetEnv "SERVER_SOFTWARE" ""
server_protocol <- protectedGetEnv "SERVER_PROTOCOL" ""
gateway_interface <- protectedGetEnv "GATEWAY_INTERFACE" ""
script_name <- protectedGetEnv "SCRIPT_NAME" ""
request_method <- protectedGetEnv "REQUEST_METHOD" "GET"
content_length <- protectedGetEnv "CONTENT_LENGTH" "0"
content_type <- protectedGetEnv "CONTENT_TYPE" contentTypeUrlencoded
http_cookie <- protectedGetEnv "HTTP_COOKIE" ""
http_accept <- protectedGetEnv "HTTP_ACCEPT" ""
path_info <- protectedGetEnv "PATH_INFO" ""
path_translated <- protectedGetEnv "PATH_TRANSLATED" ""
remote_host <- protectedGetEnv "REMOTE_HOST" ""
remote_addr <- protectedGetEnv "REMOTE_ADDR" ""
remote_user <- protectedGetEnv "REMOTE_USER" ""
auth_type <- protectedGetEnv "AUTH_TYPE" ""
https <- protectedGetEnv "HTTPS" "off"
raw_args <- getArgs
let byteCount :: Int
byteCount = read ('0':content_length)
httpMethod :: Method
httpMethod = read request_method
raw_contents <- case httpMethod of
GET -> return ""
_ -> rawGetBytes hIn byteCount
return
CGIEnv { serverName = server_name
, serverPort = server_port
, serverSoftware= server_software
, serverProtocol = server_protocol
, scriptName = script_name
, gatewayInterface = gateway_interface
, requestMethod = httpMethod
, contentLength = content_length
, contentType = content_type
, httpCookie = http_cookie
, httpAccept = http_accept
, pathInfo = path_info
, pathTranslated = path_translated
, remoteHost = remote_host
, remoteAddr = remote_addr
, remoteUser = remote_user
, authType = auth_type
, rawContents = raw_contents
, rawArgs = raw_args
, handle = hOut
, httpsEnabled = (https == "on" || server_port == "443")
}
-- |Main entry point for low-level CGI scripts. Takes a list of 'CGIOption' and
-- a 'CGIProgram' and runs it as a CGI script.
start :: CGIOptions -> (CGIInfo -> CGIParameters -> IO ()) -> IO ()
start options f =
do env <- initEnv stdin stdout
Debug.logInput env
-- use from a CGI program forces standard replay implementation
startEnv env (SessionMode LogOnly : options) f
startEnv :: CGIEnv -> CGIOptions -> (CGIInfo -> CGIParameters -> IO ()) -> IO ()
startEnv env options f =
do let portString =
case getPortOption options of
AutoPort -> ':' : serverPort env
NoPort -> ""
Port num -> ':' : show num
schemeString =
case getHttpsOption options of
AutoHttps -> if httpsEnabled env then "https" else "http"
NoHttps -> "http"
myurl =
case getURLOption options of
FullURL ->
schemeString ++ "://" ++ serverName env ++ portString ++
scriptName env ++ pathInfo env
PartialURL ->
scriptName env ++ pathInfo env
sessionMode =
getSessionMode options
methodIsGet
= requestMethod env == GET
contentIsURLEncoded
= contentTypeUrlencoded == map toLower (contentType env)
contents = rawContents env
rawDecodedParameters
| methodIsGet = []
| contentIsURLEncoded = map decodeLine $ parameterLines contents
| otherwise = decodeMultiPart (contentType env) contents
parsed_cookies = parseCookies $ httpCookie env
info = CGIInfo { cgiUrl = URL myurl
, cgiPathInfo = pathInfo env
, cgiScriptName = scriptName env
, cgiContentType = contentType env
, cgiContents = contents
, cgiCookies = parsed_cookies
, cgiArgs = rawArgs env
, cgiHandle = handle env
, cgiSessionMode = sessionMode
}
decodedParameters <- resolveFiles rawDecodedParameters
-- appendDebugFile "/tmp/CGIDECODED" (show decodedParameters)
f info decodedParameters
resolveFiles :: [(String, CGIRawValue)] -> IO CGIParameters
resolveFiles =
let resolveOne (key, rv) =
case rv of
CGIRawString s ->
return (key, s)
CGIRawFile fileName contentType fileContents ->
do localName <- inventFilePath
writeFile localName fileContents
let fileRef = FileReference
{ fileReferenceName = localName
, fileReferenceContentType = contentType
, fileReferenceExternalName = fileName
}
return (key, show fileRef)
in mapM resolveOne
parseCookies :: String -> [(String, String)]
parseCookies str =
let s0 = dropWhile isSpace str in
if null s0 then [] else
let (item, rest) = span (/= ';') s0 in
case span (/= '=') item of
(key, '=':value) ->
(key, value) : parseCookies (dropWhile (== ';') rest)
_ ->
error ("Trying to parse cookie: " ++ str)
dropSpecialParameters :: [(String,a)] -> [(String,a)]
dropSpecialParameters = filter (f . fieldName)
where f ('=':_) = False
f _ = True
decodeMultiPart :: String -> String -> [(String, CGIRawValue)]
decodeMultiPart contentType contents
= let Just boundary = extractBoundary contentType
startBoundary = '-':'-':boundary
g source = case
(
advanceIC startBoundary source >>= \afterBoundary ->
case afterBoundary of
'-':_ -> Nothing
'\13':'\10':body ->
advanceIC "content-disposition: form-data; name=\"" body
>>= \fieldNameRest ->
let (fieldName, rest) = span (/= '\"') fieldNameRest in
advanceIC "\"" rest
>>= \mayBeFileNameRest ->
(case mayBeFileNameRest of
';':_ -> advanceIC "; filename=\"" mayBeFileNameRest
>>= \fileNameRest ->
let (fileName, rest) = span (/= '\"') fileNameRest in
advanceIC "\"" rest
>>= \mayBeContentTypeRest ->
return (Just fileName, mayBeContentTypeRest)
_ -> return (Nothing, mayBeFileNameRest))
>>= \ (mFileName, fileNameRest) ->
let (contentType, rest) =
case advanceIC "\ncontent-type: " fileNameRest of
Just contentTypeRest -> span (/= '\13') contentTypeRest
Nothing -> ("text/plain", fileNameRest) in
advanceIC "\n\n" rest
>>= \contentRest ->
extractContents startBoundary contentRest
>>= \ (fieldContents, rest) ->
-- fieldName must be rfc1522decoded (?)
let moreParms = g rest
rawvalue = case mFileName of
Nothing ->
CGIRawString $ RFC2279.decode fieldContents
Just fileName ->
CGIRawFile fileName contentType fieldContents
in return ((RFC2279.decode fieldName, rawvalue) : moreParms)
)
of
Just parameters -> parameters
Nothing -> []
in g contents
extractContents :: String -> String -> Maybe (String, String)
extractContents boundary source
= g "" source
where g rev "" = Nothing
g rev ('\r':'\n':xs) = case advanceIC boundary xs of
Just _ -> Just (reverse rev, xs)
Nothing -> g ('\n':'\r':rev) xs
g rev (x:xs) = g (x:rev) xs
extractBoundary :: String -> Maybe String
extractBoundary contentType = advanceIC "multipart/form-data; boundary=" contentType
advanceIC :: String -> String -> Maybe String
advanceIC [] ys = Just ys
advanceIC xs [] = Nothing
advanceIC (' ':xs) (y:ys)
| isSpace y = advanceIC xs (dropWhile isSpace ys)
advanceIC ('\n':xs) ('\13':'\10':ys)
= advanceIC xs ys
advanceIC (x:xs) (y:ys)
| toUpper x == toUpper y = advanceIC xs ys
| otherwise = Nothing
parameterLines :: String -> [String]
parameterLines "" = []
parameterLines xs = let (firstPar, restPar) = span (/= '&') xs in
case restPar of
'&' : moreParameters -> firstPar : parameterLines moreParameters
_ -> [firstPar]
decodeLine :: String -> (String, CGIRawValue)
decodeLine str =
let (name, '=':value) = span (/= '=') str in
( RFC2279.decode $ URLCoding.decode name
, CGIRawString $ RFC2279.decode $ URLCoding.decode value)
assocParm :: String -> CGIParameters -> Maybe CGIValue
assocParm key =
listToMaybe . assocParmL key
assocParmL :: String -> CGIParameters -> [CGIValue]
assocParmL key =
map fieldContents . assocParmR key
assocParmR :: String -> CGIParameters -> CGIParameters
assocParmR key =
filter (\parm -> fieldName parm == key)
assoc :: (Eq a) => a -> [(a,b)] -> Maybe b
assoc key alist =
ass alist
where ass ((a,b):rest) = if a == key then Just b else ass rest
ass [] = Nothing -- error ("assoc ("++show key++") "++show alist)
-- update :: [(a,b)] -> a -> b -> [(a,b)]
-- update alist a b = (a,b) : alist
fieldNames :: CGIParameters -> [String]
fieldNames = map fieldName
-- encryption
generateKey :: IO (Maybe (Integer, String, String))
generateKey =
try (openFile keyFile ReadMode) >>= g
where
g (Left ioerror) =
return Nothing
g (Right h) =
do size <- hFileSize h
let size2 = size `div` 2
pos <- randomRIO (0, size2)
if pos < 0
then return Nothing
else do hSeek h AbsoluteSeek pos
xs <- hGetContents h
g <- getStdGen
return (Just (pos
,randomRs (minBound, maxBound) g
,extendRandomly xs))
extendRandomly :: [Char] -> [Char]
extendRandomly xs = h 0 xs
where h n (x:xs) = x : h (n + ord x) xs
h n [] = randoms (mkStdGen n)
nrNonces :: Int
nrNonces = 16
makeEncoder :: Maybe (Integer, String, String) -> String -> String
makeEncoder Nothing xs = xs
makeEncoder (Just (i, nonces, keys)) xs =
show i ++ ';' : encrypt1 (take nrNonces nonces ++ xs) keys
decode :: String -> IO String
decode inp = g (reads inp)
where
g :: [(Integer, String)] -> IO String
g ((pos, ';':encrypted) : _) =
do h <- openFile keyFile ReadMode
hSeek h AbsoluteSeek pos
xs <- hGetContents h
return (drop nrNonces $ decrypt1 encrypted (extendRandomly xs))
g _ = return inp
encrypt1 inp keys = map chr (enc 0 (map ord inp) (map ord keys))
where enc acc [] okeys = []
enc acc (oinp : oinps) (okey : okeys) =
let out = (oinp + okey + acc) `mod` 256 in
out : enc ((acc + oinp) `mod` 256) oinps okeys
decrypt1 einp keys = map chr (dec 0 (map ord einp) (map ord keys))
where dec acc [] okeys = []
dec acc (oeinp : oeinps) (okey : okeys) =
let oinp = (512 + oeinp - okey - acc) `mod` 256 in
oinp : dec ((acc + oinp) `mod` 256) oeinps okeys
encrypt, decrypt :: String -> String -> String
encrypt = zipWith cadd
cadd c1 c2 = chr (ord c1 + ord c2 `mod` 256)
decrypt = zipWith csub
csub c1 c2 = chr ((ord c1 + 256 - ord c2) `mod` 256)
{--
import Bits -- ghc specific
encrypt = zipWith cxor
decrypt = zipWith cxor
cxor c1 c2 = chr (ord c1 `xor` ord c2)
--}