1
1
{-# LANGUAGE OverloadedStrings #-}
2
{-# LANGUAGE RankNTypes #-}
3
-- | Backend for Common Gateway Interface. Almost all users should use the
2
5
module Network.Wai.Handler.CGI
10
import Network.Wai.Enumerator (fromResponseBody)
11
import Network.Wai.Handler.Helper
13
import Network.Socket (getAddrInfo, addrAddress)
12
14
import System.Environment (getEnvironment)
13
15
import Data.Maybe (fromMaybe)
14
16
import qualified Data.ByteString.Char8 as B
17
import qualified Data.ByteString.Lazy as L
15
18
import Control.Arrow ((***))
16
19
import Data.Char (toLower)
17
20
import qualified System.IO
18
import Data.String (fromString)
21
import qualified Data.String as String
22
import Data.Enumerator
23
( Enumerator, Step (..), Stream (..), continue, yield
24
, enumList, ($$), joinI, returnI, (>>==), run_
26
import Data.Monoid (mconcat)
27
import Blaze.ByteString.Builder (fromByteString, toLazyByteString)
28
import Blaze.ByteString.Builder.Char8 (fromChar, fromString)
29
import Blaze.ByteString.Builder.Enumerator (builderToByteString)
30
import Control.Monad.IO.Class (liftIO)
31
import Data.ByteString.Lazy.Internal (defaultChunkSize)
32
import System.IO (Handle)
33
import Network.HTTP.Types (Status (..))
34
import qualified Network.HTTP.Types as H
35
import qualified Data.CaseInsensitive as CI
20
37
safeRead :: Read a => a -> String -> a
26
43
lookup' :: String -> [(String, String)] -> String
27
44
lookup' key pairs = fromMaybe "" $ lookup key pairs
46
-- | Run an application using CGI.
29
47
run :: Application -> IO ()
31
49
vars <- getEnvironment
32
50
let input = requestBodyHandle System.IO.stdin
33
51
output = B.hPut System.IO.stdout
34
run'' vars input output Nothing app
52
runGeneric vars input output Nothing app
36
runSendfile :: String -- ^ sendfile header
54
-- | Some web servers provide an optimization for sending files via a sendfile
55
-- system call via a special header. To use this feature, provide that header
57
runSendfile :: B.ByteString -- ^ sendfile header
37
58
-> Application -> IO ()
38
59
runSendfile sf app = do
39
60
vars <- getEnvironment
40
61
let input = requestBodyHandle System.IO.stdin
41
62
output = B.hPut System.IO.stdout
42
run'' vars input output (Just sf) app
44
run' :: [(String, String)] -- ^ all variables
45
-> System.IO.Handle -- ^ responseBody of input
46
-> System.IO.Handle -- ^ destination for output
49
run' vars inputH outputH app = do
50
let input = requestBodyHandle inputH
51
output = B.hPut outputH
52
run'' vars input output Nothing app
54
run'' :: [(String, String)] -- ^ all variables
55
-> (Int -> Source) -- ^ responseBody of input
63
runGeneric vars input output (Just sf) app
65
-- | A generic CGI helper, which allows other backends (FastCGI and SCGI) to
66
-- use the same code as CGI. Most users will not need this function, and can
67
-- stick with 'run' or 'runSendfile'.
69
:: [(String, String)] -- ^ all variables
70
-> (forall a. Int -> Enumerator B.ByteString IO a) -- ^ responseBody of input
56
71
-> (B.ByteString -> IO ()) -- ^ destination for output
57
-> Maybe String -- ^ does the server support the X-Sendfile header?
72
-> Maybe B.ByteString -- ^ does the server support the X-Sendfile header?
60
run'' vars inputH outputH xsendfile app = do
75
runGeneric vars inputH outputH xsendfile app = do
61
76
let rmethod = B.pack $ lookup' "REQUEST_METHOD" vars
62
77
pinfo = lookup' "PATH_INFO" vars
63
78
qstring = lookup' "QUERY_STRING" vars
65
80
serverport = safeRead 80 $ lookup' "SERVER_PORT" vars
66
81
contentLength = safeRead 0 $ lookup' "CONTENT_LENGTH" vars
68
case lookup "REMOTE_HOST" vars of
83
case lookup "REMOTE_ADDR" vars of
71
case lookup "REMOTE_ADDR" vars of
86
case lookup "REMOTE_HOST" vars of
75
90
case map toLower $ lookup' "SERVER_PROTOCOL" vars of
93
addrs <- getAddrInfo Nothing (Just remoteHost') Nothing
97
[] -> error $ "Invalid REMOTE_ADDR or REMOTE_HOST: " ++ remoteHost'
79
99
{ requestMethod = rmethod
80
, pathInfo = B.pack pinfo
81
, queryString = B.pack qstring
100
, rawPathInfo = B.pack pinfo
101
, pathInfo = H.decodePathSegments $ B.pack pinfo
102
, rawQueryString = B.pack qstring
103
, queryString = H.parseQuery $ B.pack qstring
82
104
, serverName = B.pack servername
83
105
, serverPort = serverport
84
106
, requestHeaders = map (cleanupVarName *** B.pack) vars
85
107
, isSecure = isSecure'
86
, requestBody = inputH contentLength
87
, errorHandler = System.IO.hPutStr System.IO.stderr
88
, remoteHost = B.pack remoteHost'
89
, httpVersion = "1.1" -- FIXME
109
, httpVersion = H.http11 -- FIXME
92
let h = responseHeaders res
93
let h' = case lookup "Content-Type" h of
94
Nothing -> ("Content-Type", "text/html; charset=utf-8")
98
hPut $ B.pack $ "Status: " ++ (show $ statusCode $ status res) ++ " "
99
hPut $ statusMessage $ status res
100
hPut $ B.singleton '\n'
101
mapM_ (printHeader hPut) h'
102
case (xsendfile, responseBody res) of
103
(Just sf, ResponseFile fp) ->
104
hPut $ B.pack $ concat
113
hPut $ B.singleton '\n'
114
_ <- runEnumerator (fromResponseBody (responseBody res))
118
myPut :: (B.ByteString -> IO ()) -> () -> B.ByteString -> IO (Either () ())
119
myPut output _ bs = output bs >> return (Right ())
121
printHeader :: (B.ByteString -> IO ())
122
-> (ResponseHeader, B.ByteString)
124
printHeader f (x, y) = do
130
cleanupVarName :: String -> RequestHeader
131
cleanupVarName ('H':'T':'T':'P':'_':a:as) =
132
fromString $ a : helper' as
111
-- FIXME worry about exception?
112
res <- run_ $ inputH contentLength $$ app env
113
case (xsendfile, res) of
114
(Just sf, ResponseFile s hs fp Nothing) ->
115
mapM_ outputH $ L.toChunks $ toLazyByteString $ sfBuilder s hs sf fp
116
_ -> responseEnumerator res $ \s hs ->
117
joinI $ enumList 1 [headers s hs, fromChar '\n'] $$ builderIter
119
headers s hs = mconcat (map header $ status s : map header' (fixHeaders hs))
120
status (Status i m) = (fromByteString "Status", mconcat
121
[ fromString $ show i
125
header' (x, y) = (fromByteString $ CI.original x, fromByteString y)
126
header (x, y) = mconcat
128
, fromByteString ": "
132
sfBuilder s hs sf fp = mconcat
134
, header $ (fromByteString sf, fromString fp)
137
, fromByteString " not supported"
139
bsStep = Continue bsStep'
140
bsStep' EOF = yield () EOF
141
bsStep' (Chunks []) = continue bsStep'
142
bsStep' (Chunks bss) = liftIO (mapM_ outputH bss) >> continue bsStep'
143
builderIter = builderToByteString bsStep
145
case lookup "content-type" h of
146
Nothing -> ("Content-Type", "text/html; charset=utf-8") : h
149
cleanupVarName :: String -> CI.CI B.ByteString
150
cleanupVarName "CONTENT_TYPE" = "Content-Type"
151
cleanupVarName "CONTENT_LENGTH" = "Content-Length"
152
cleanupVarName "SCRIPT_NAME" = "CGI-Script-Name"
155
'H':'T':'T':'P':'_':a:as -> String.fromString $ a : helper' as
156
_ -> String.fromString s -- FIXME remove?
134
158
helper' ('_':x:rest) = '-' : x : helper' rest
135
159
helper' (x:rest) = toLower x : helper' rest
137
cleanupVarName "CONTENT_TYPE" = "Content-Type"
138
cleanupVarName "CONTENT_LENGTH" = "Content-Length"
139
cleanupVarName "SCRIPT_NAME" = "CGI-Script-Name"
140
cleanupVarName x = fromString x -- FIXME remove?
162
requestBodyHandle :: Handle -> Int -> Enumerator B.ByteString IO a
163
requestBodyHandle h =
166
go i = Just `fmap` B.hGet h (min i defaultChunkSize)
168
requestBodyFunc :: (Int -> IO (Maybe B.ByteString))
170
-> Enumerator B.ByteString IO a
171
requestBodyFunc _ 0 step = returnI step
172
requestBodyFunc h len (Continue k) = do
173
mbs <- liftIO $ h len
175
Nothing -> continue k
177
let newLen = len - B.length bs
178
k (Chunks [bs]) >>== requestBodyFunc h newLen
179
requestBodyFunc _ _ step = returnI step