-- © 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)
--}