1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
{-# LANGUAGE OverloadedStrings, CPP, ScopedTypeVariables #-}
module Network.Wai.Application.Classic.CGI (
cgiApp
) where
import qualified Control.Exception as E (SomeException, IOException, try, catch, bracket)
import Control.Monad (when, (<=<))
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS (readInt, unpack, tail)
import Data.Conduit
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as CL
import Network.HTTP.Types
import Network.SockAddr
import Network.Wai
import Network.Wai.Conduit
import Network.Wai.Application.Classic.Conduit
import Network.Wai.Application.Classic.Field
import Network.Wai.Application.Classic.Header
import Network.Wai.Application.Classic.Path
import Network.Wai.Application.Classic.Types
import System.Environment
import System.IO
import System.Process
----------------------------------------------------------------
type ENVVARS = [(String,String)]
gatewayInterface :: String
gatewayInterface = "CGI/1.1"
----------------------------------------------------------------
{-|
Handle GET and POST for CGI.
The program to link this library must ignore SIGCHLD as follows:
> installHandler sigCHLD Ignore Nothing
-}
cgiApp :: ClassicAppSpec -> CgiAppSpec -> CgiRoute -> Application
cgiApp cspec spec cgii req respond = case method of
Right GET -> cgiApp' False cspec spec cgii req respond
Right POST -> cgiApp' True cspec spec cgii req respond
_ -> respond $ responseLBS methodNotAllowed405 textPlainHeader "Method Not Allowed\r\n" -- xxx
where
method = parseMethod $ requestMethod req
cgiApp' :: Bool -> ClassicAppSpec -> CgiAppSpec -> CgiRoute -> Application
cgiApp' body cspec spec cgii req respond = E.bracket setup teardown (respond <=< cgi)
where
setup = execProcess cspec spec cgii req
teardown (rhdl,whdl,pid) = do
terminateProcess pid -- SIGTERM
hClose rhdl
hClose whdl
cgi (rhdl,whdl,_) = do
when body $ toCGI whdl req
hClose whdl -- telling EOF
fromCGI rhdl
----------------------------------------------------------------
type TRYPATH = Either E.IOException String
toCGI :: Handle -> Request -> IO ()
#if MIN_VERSION_conduit(1,3,0)
toCGI whdl req = runConduit (sourceRequestBody req .| CB.sinkHandle whdl)
#else
toCGI whdl req = sourceRequestBody req $$ CB.sinkHandle whdl
#endif
fromCGI :: Handle -> IO Response
fromCGI rhdl = do
(src', hs) <- cgiHeader `E.catch` recover
let (st, hdr, hasBody) = case check hs of
Nothing -> (internalServerError500,[],False)
Just (s,h) -> (s,h,True)
let src | hasBody = src'
| otherwise = CL.sourceNull
return $ responseSource st hdr src
where
check hs = lookup hContentType hs >> case lookup hStatus hs of
Nothing -> Just (ok200, hs)
Just l -> toStatus l >>= \s -> Just (s,hs')
where
hs' = filter (\(k,_) -> k /= hStatus) hs
toStatus s = BS.readInt s >>= \x -> Just (Status (fst x) s)
emptyHeader = []
recover (_ :: E.SomeException) = return (CL.sourceNull, emptyHeader)
cgiHeader = do
(rsrc,hs) <- CB.sourceHandle rhdl $$+ parseHeader
src <- toResponseSource rsrc
return (src,hs)
----------------------------------------------------------------
execProcess :: ClassicAppSpec -> CgiAppSpec -> CgiRoute -> Request -> IO (Handle, Handle, ProcessHandle)
execProcess cspec spec cgii req = do
let naddr = showSockAddr . remoteHost $ req
epath <- E.try (getEnv "PATH") :: IO TRYPATH
(Just whdl,Just rhdl,_,pid) <- createProcess $ proSpec naddr epath
hSetEncoding rhdl latin1
hSetEncoding whdl latin1
return (rhdl, whdl, pid)
where
proSpec naddr epath = CreateProcess {
cmdspec = RawCommand prog []
, cwd = Nothing
, env = Just $ makeEnv req naddr scriptName pathinfo (softwareName cspec) epath
, std_in = CreatePipe
, std_out = CreatePipe
, std_err = Inherit
, close_fds = True
#if __GLASGOW_HASKELL__ >= 702
, create_group = True
#endif
#if __GLASGOW_HASKELL__ >= 707
, delegate_ctlc = False
#endif
#if __GLASGOW_HASKELL__ >= 800
, detach_console = False
, create_new_console = False
, new_session = False
, child_group = Nothing
, child_user = Nothing
#endif
#if __GLASGOW_HASKELL__ >= 802
, use_process_jobs = False
#endif
}
(prog, scriptName, pathinfo) =
pathinfoToCGI (cgiSrc cgii)
(cgiDst cgii)
(rawPathInfo req)
(indexCgi spec)
makeEnv :: Request -> String -> String -> String -> ByteString ->
TRYPATH -> ENVVARS
makeEnv req naddr scriptName pathinfo sname epath = addPath epath . addLen . addType . addCookie $ baseEnv
where
baseEnv = [
("GATEWAY_INTERFACE", gatewayInterface)
, ("SCRIPT_NAME", scriptName)
, ("REQUEST_METHOD", BS.unpack . requestMethod $ req)
, ("SERVER_NAME", BS.unpack host)
, ("SERVER_PORT", BS.unpack port)
, ("REMOTE_ADDR", naddr)
, ("SERVER_PROTOCOL", show . httpVersion $ req)
, ("SERVER_SOFTWARE", BS.unpack sname)
, ("PATH_INFO", pathinfo)
, ("QUERY_STRING", query req)
]
headers = requestHeaders req
addLen = addLength "CONTENT_LENGTH" $ requestBodyLength req
addType = addEnv "CONTENT_TYPE" $ lookup hContentType headers
addCookie = addEnv "HTTP_COOKIE" $ lookup hCookie headers
addPath (Left _) ev = ev
addPath (Right path) ev = ("PATH", path) : ev
query = BS.unpack . safeTail . rawQueryString
where
safeTail "" = ""
safeTail bs = BS.tail bs
(host, port) = hostPort req
addEnv :: String -> Maybe ByteString -> ENVVARS -> ENVVARS
addEnv _ Nothing envs = envs
addEnv key (Just val) envs = (key,BS.unpack val) : envs
addLength :: String -> RequestBodyLength -> ENVVARS -> ENVVARS
addLength _ ChunkedBody envs = envs
addLength key (KnownLength len) envs = (key, show len) : envs
{-|
>>> pathinfoToCGI "/cgi-bin/" "/User/cgi-bin/" "/cgi-bin/foo" "index.cgi"
("/User/cgi-bin/foo","/cgi-bin/foo","")
>>> pathinfoToCGI "/cgi-bin/" "/User/cgi-bin/" "/cgi-bin/foo/bar" "index.cgi"
("/User/cgi-bin/foo","/cgi-bin/foo","/bar")
>>> pathinfoToCGI "/cgi-bin/" "/User/cgi-bin/" "/cgi-bin/" "index.cgi"
("/User/cgi-bin/index.cgi","/cgi-bin/index.cgi","")
-}
pathinfoToCGI :: Path -> Path -> Path -> Path -> (FilePath, String, String)
pathinfoToCGI src dst path index = (prog, scriptName, pathinfo)
where
path' = path <\> src
(prog',pathinfo')
| src == path = (index, "")
| otherwise = breakAtSeparator path'
prog = pathString (dst </> prog')
scriptName = pathString (src </> prog')
pathinfo = pathString pathinfo'
|