{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, DeriveDataTypeable, ScopedTypeVariables, CPP #-} module Network.FastCGI ( module Network.HTTP.Base, module Network.HTTP.Headers, -- * Accepting requests simpleFastCGI, -- * Environment Env, getDocumentRoot, getGatewayInterface, getPathInfo, getPathTranslated, getQueryString, getRedirectStatus, getRedirectURI, getRemoteAddress, getRemotePort, getRemoteHost, getRemoteIdent, getRemoteUser, getScriptFilename, getScriptName, getServerAddress, getServerName, getServerPort, getServerProtocol, getServerSoftware, getAuthenticationType, -- * Low-level API Connection, fastCGI, writeResponse, writeHeaders, writeString, writeByteString, close ) where import Control.Concurrent import Control.Exception ( bracket, catch ) import Data.Bits import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Char8 as BSC (unpack, pack) import Data.Char import Data.Maybe ( fromMaybe ) import qualified Data.Map as Map import qualified Network.Socket as Socket hiding (recv) import qualified Network.Socket.ByteString as Socket import Network.URI ( URI(..), URIAuth(..) #if MIN_VERSION_network_uri(2,6,2) , nullURI, nullURIAuth #endif , parseURIReference ) import Network.HTTP.Base import Network.HTTP.Headers import Network.HTTP.Utils ( parseInt, trim, encodeString, decodeString ) import System.IO ( TextEncoding, latin1 ) -- | An opaque type representing the state of a single connection from the web server. data FastCGIState = FastCGIState { fcgiSocket :: Socket.Socket, fcgiPeer :: Socket.SockAddr, fcgiRequests :: Map.Map Int PartialRequest } type Env = [(String,String)] data PartialRequest = PartialRequest { prqEnv :: Env, prqParamsBuf :: BS.ByteString, prqEncoding :: TextEncoding, prqBody :: [String], prqReq :: Request } data Record = Record { recordType :: RecordType, recordRequestID :: Int, recordContent :: BS.ByteString } deriving (Show) data RecordType = BeginRequestRecord | AbortRequestRecord | EndRequestRecord | ParamsRecord | StdinRecord | StdoutRecord | StderrRecord | DataRecord | GetValuesRecord | GetValuesResultRecord | UnknownTypeRecord | OtherRecord Int deriving (Eq, Show) instance Enum RecordType where toEnum 1 = BeginRequestRecord toEnum 2 = AbortRequestRecord toEnum 3 = EndRequestRecord toEnum 4 = ParamsRecord toEnum 5 = StdinRecord toEnum 6 = StdoutRecord toEnum 7 = StderrRecord toEnum 8 = DataRecord toEnum 9 = GetValuesRecord toEnum 10 = GetValuesResultRecord toEnum 11 = UnknownTypeRecord toEnum code = OtherRecord code fromEnum BeginRequestRecord = 1 fromEnum AbortRequestRecord = 2 fromEnum EndRequestRecord = 3 fromEnum ParamsRecord = 4 fromEnum StdinRecord = 5 fromEnum StdoutRecord = 6 fromEnum StderrRecord = 7 fromEnum DataRecord = 8 fromEnum GetValuesRecord = 9 fromEnum GetValuesResultRecord = 10 fromEnum UnknownTypeRecord = 11 fromEnum (OtherRecord code) = code -- | Takes a handler, and concurrently accepts requests from the web server -- by calling the handler. simpleFastCGI :: (Env -> Request -> IO Response) -- ^ A handler which is invoked once for each incoming connection. -> IO () -- ^ Never actually returns. simpleFastCGI handler = do fastCGI $ \env rq conn -> do rsp <- handleErrors (fLog conn) (handler env rq) writeResponse conn rsp -- | Takes a handler, and concurrently accepts requests from the web server -- by calling the handler. fastCGI :: (Env -> Request -> Connection -> IO ()) -- ^ A handler which is invoked once for each incoming connection. -> IO () -- ^ Never actually returns. fastCGI handler = do #if MIN_VERSION_network(3,0,0) listenSocket <- Socket.mkSocket 0 #else listenSocket <- Socket.mkSocket 0 Socket.AF_INET Socket.Stream Socket.defaultProtocol Socket.Listening #endif let acceptLoop' = do (socket, peer) <- Socket.accept listenSocket let state = FastCGIState { fcgiSocket = socket, fcgiPeer = peer, fcgiRequests = Map.empty } requestLoop state handler acceptLoop' acceptLoop' data Connection = Connection FastCGIState Int requestLoop :: FastCGIState -> (Env -> Request -> Connection -> IO ()) -> IO () requestLoop state handler = do maybeRecord <- recvRecord state case maybeRecord of Nothing -> do Socket.close (fcgiSocket state) Just record -> do let conn = Connection state (recordRequestID record) case recordType record of BeginRequestRecord -> do let req = Request { rqURI=nullURI , rqMethod = GET , rqHeaders = [] , rqBody = "" } prq = PartialRequest { prqEnv = [] , prqParamsBuf = BS.empty , prqEncoding = latin1 , prqBody = [] , prqReq = req } state' = state{fcgiRequests=Map.insert (recordRequestID record) prq (fcgiRequests state) } requestLoop state' handler GetValuesRecord -> do fLog conn ("Get values record: " ++ show record) requestLoop state handler ParamsRecord -> do let requestID = recordRequestID record case Map.lookup requestID (fcgiRequests state) of Nothing -> fLog conn ("Ignoring record for unknown request ID " ++ show requestID) Just prq | BS.length (recordContent record) == 0 -> do enc <- getEncoding (rqHeaders (prqReq prq)) let state' = state{fcgiRequests=Map.insert requestID prq{prqEncoding=enc} (fcgiRequests state)} requestLoop state' handler | otherwise -> do let takeUntilEmpty prq bufferTail = case takeNameValuePair bufferTail of Nothing -> return prq{prqParamsBuf=bufferTail} Just ((name, value), bufferTail) -> do let name' = BSC.unpack name value' = BSC.unpack value prq' = processRequestVariable name' value' prq takeUntilEmpty prq' bufferTail prq <- takeUntilEmpty prq (BS.append (prqParamsBuf prq) (recordContent record)) let state' = state{fcgiRequests=Map.insert requestID prq (fcgiRequests state)} requestLoop state' handler StdinRecord -> do let requestID = recordRequestID record case Map.lookup requestID (fcgiRequests state) of Nothing -> fLog conn ("Ignoring record for unknown request ID " ++ show requestID) Just prq | BS.length (recordContent record) == 0 -> do forkIO $ handler (prqEnv prq) (prqReq prq){rqBody=concat (reverse (prqBody prq))} conn let state' = state{fcgiRequests=Map.delete requestID (fcgiRequests state)} requestLoop state' handler | otherwise -> do s <- decodeString (prqEncoding prq) (recordContent record) let prq' = prq{prqBody=s:prqBody prq} state' = state{fcgiRequests=Map.insert requestID prq' (fcgiRequests state)} requestLoop state' handler OtherRecord unknownCode -> do sendRecord state $ Record { recordType = UnknownTypeRecord, recordRequestID = 0, recordContent = BS.pack [fromIntegral unknownCode, 0, 0, 0, 0, 0, 0, 0] } _ -> fLog conn ("Ignoring record of unexpected type "++show (recordType record)) {- sendRecord state $ Record { recordType = EndRequestRecord, recordRequestID = requestID, recordContent = BS.pack [0, 0, 0, 0, 0, 0, 0, 0] } -} #if MIN_VERSION_network_uri(2,6,2) #else -- |Blank URI nullURI :: URI nullURI = URI { uriScheme = "" , uriAuthority = Nothing , uriPath = "" , uriQuery = "" , uriFragment = "" } -- |Blank URIAuth. nullURIAuth :: URIAuth nullURIAuth = URIAuth { uriUserInfo = "" , uriRegName = "" , uriPort = "" } #endif processRequestVariable :: String -> String -> PartialRequest -> PartialRequest processRequestVariable "REQUEST_METHOD" value prq = prq{prqReq=(prqReq prq){rqMethod=parseRequestMethod value}} processRequestVariable "REQUEST_URI" value prq = let rq = prqReq prq uri = rqURI rq sch = uriScheme uri auth = uriAuthority uri in case parseURIReference value of Just uri -> prq{prqReq=rq{rqURI=uri{uriScheme=sch,uriAuthority=auth}}} Nothing -> prq processRequestVariable "SERVER_NAME" value prq = let rq = prqReq prq uri = rqURI rq auth = fromMaybe nullURIAuth (uriAuthority uri) in prq{prqReq=rq{rqURI=uri{uriAuthority=Just auth{uriRegName=value}}}} processRequestVariable "SERVER_PORT" value prq = let rq = prqReq prq uri = rqURI rq auth = fromMaybe nullURIAuth (uriAuthority uri) in case value of "80" -> prq{prqReq=rq{rqURI=uri{uriScheme="http:"}}} "443" -> prq{prqReq=rq{rqURI=uri{uriScheme="https:"}}} _ -> prq{prqReq=rq{rqURI=uri{uriAuthority=Just auth{uriPort=':':value}}}} processRequestVariable "QUERY_STRING" value prq = prq processRequestVariable name value prq = case variableToHeaderName name of Nothing -> prq{prqEnv=(name,value) : prqEnv prq} Just header -> prq{prqReq=insertHeader header value (prqReq prq)} recvRecord :: FastCGIState -> IO (Maybe Record) recvRecord state = do byteString <- recvAll (fcgiSocket state) 8 case BS.length byteString of 8 -> do let recordVersion = BS.index byteString 0 recordTypeCode = fromIntegral $ BS.index byteString 1 recordRequestIDB1 = BS.index byteString 2 recordRequestIDB0 = BS.index byteString 3 recordRequestID = (fromIntegral recordRequestIDB1) * 256 + (fromIntegral recordRequestIDB0) recordContentLengthB1 = BS.index byteString 4 recordContentLengthB0 = BS.index byteString 5 recordContentLength = (fromIntegral recordContentLengthB1) * 256 + (fromIntegral recordContentLengthB0) recordPaddingLength = BS.index byteString 6 if recordVersion /= 1 then return Nothing else do let recordType = toEnum recordTypeCode recordContent <- recvAll (fcgiSocket state) recordContentLength recvAll (fcgiSocket state) (fromIntegral recordPaddingLength) return (Just (Record { recordType = recordType, recordRequestID = recordRequestID, recordContent = recordContent })) _ -> return Nothing sendRecord :: FastCGIState -> Record -> IO () sendRecord state record = do let recordRequestIDB0 = fromIntegral $ recordRequestID record `mod` 256 recordRequestIDB1 = fromIntegral $ (recordRequestID record `div` 256) `mod` 256 recordContentLength = BS.length $ recordContent record recordContentLengthB0 = fromIntegral $ recordContentLength `mod` 256 recordContentLengthB1 = fromIntegral $ (recordContentLength `div` 256) `mod` 256 headerByteString = BS.pack [1, fromIntegral $ fromEnum $ recordType record, recordRequestIDB1, recordRequestIDB0, recordContentLengthB1, recordContentLengthB0, 0, 0] byteString = BS.append headerByteString $ recordContent record Socket.sendAll (fcgiSocket state) byteString recvAll :: Socket.Socket -> Int -> IO BS.ByteString recvAll socket totalSize = do if totalSize == 0 then return BS.empty else do byteString <- Socket.recv socket totalSize case BS.length byteString of 0 -> return byteString receivedSize | receivedSize == totalSize -> return byteString | otherwise -> do restByteString <- recvAll socket (totalSize - receivedSize) return (BS.append byteString restByteString) takeLength :: BS.ByteString -> Maybe (Int, BS.ByteString) takeLength byteString = if BS.length byteString < 1 then Nothing else let firstByte = BS.index byteString 0 threeMoreComing = (firstByte .&. 0x80) == 0x80 in if threeMoreComing then if BS.length byteString < 4 then Nothing else let secondByte = BS.index byteString 1 thirdByte = BS.index byteString 2 fourthByte = BS.index byteString 3 decoded = ((fromIntegral $ firstByte .&. 0x7F) `shiftL` 24) + (fromIntegral secondByte `shiftL` 16) + (fromIntegral thirdByte `shiftL` 8) + (fromIntegral fourthByte) in Just (decoded, BS.drop 4 byteString) else Just (fromIntegral firstByte, BS.drop 1 byteString) takeNameValuePair :: BS.ByteString -> Maybe ((BS.ByteString, BS.ByteString), BS.ByteString) takeNameValuePair byteString = let maybeNameLength = takeLength byteString in case maybeNameLength of Nothing -> Nothing Just (nameLength, byteString') -> let maybeValueLength = takeLength byteString' in case maybeValueLength of Nothing -> Nothing Just (valueLength, byteString'') -> let name = BS.take nameLength byteString'' byteString''' = BS.drop nameLength byteString'' value = BS.take valueLength byteString''' byteString'''' = BS.drop valueLength byteString''' in Just ((name, value), byteString'''') -- | Logs a message using the web server's logging facility. fLog :: Connection -> String -> IO () fLog (Connection state requestID) message | length message > 0 = sendRecord state $ Record { recordType = StderrRecord, recordRequestID = requestID, recordContent = BSC.pack message } | otherwise = return () -- | Return the document root, as provided by the web server, if it was provided. getDocumentRoot :: Env -> Maybe String getDocumentRoot = lookup "DOCUMENT_ROOT" -- | Return the gateway interface, as provided by the web server, if it was provided. getGatewayInterface :: Env -> Maybe String getGatewayInterface = lookup "GATEWAY_INTERFACE" -- | Return the path info, as provided by the web server, if it was provided. getPathInfo :: Env -> Maybe String getPathInfo = lookup "PATH_INFO" -- | Return the path-translated value, as provided by the web server, if it was provided. getPathTranslated :: Env -> Maybe String getPathTranslated = lookup "PATH_TRANSLATED" -- | Return the query string, as provided by the web server, if it was provided. getQueryString :: Env -> Maybe String getQueryString = lookup "QUERY_STRING" -- | Return the redirect status, as provided by the web server, if it was provided. getRedirectStatus :: Env -> Maybe Int getRedirectStatus env = lookup "REDIRECT_STATUS" env >>= parseInt -- | Return the redirect URI, as provided by the web server, if it was provided. getRedirectURI :: Env -> Maybe String getRedirectURI = lookup "REDIRECT_URI" -- | Return the remote address, as provided by the web server, if it was provided. getRemoteAddress :: Env -> Maybe String getRemoteAddress = lookup "REMOTE_ADDR" -- | Return the remote port, as provided by the web server, if it was provided. getRemotePort :: Env -> Maybe Int getRemotePort env = lookup "REMOTE_PORT" env >>= parseInt -- | Return the remote hostname, as provided by the web server, if it was provided. getRemoteHost :: Env -> Maybe String getRemoteHost = lookup "REMOTE_HOST" -- | Return the remote ident value, as provided by the web server, if it was provided. getRemoteIdent :: Env -> Maybe String getRemoteIdent = lookup "REMOTE_IDENT" -- | Return the remote user name, as provided by the web server, if it was provided. getRemoteUser :: Env -> Maybe String getRemoteUser = lookup "REMOTE_USER" -- | Return the script filename, as provided by the web server, if it was provided. getScriptFilename :: Env -> Maybe String getScriptFilename = lookup "SCRIPT_FILENAME" -- | Return the script name, as provided by the web server, if it was provided. getScriptName :: Env -> Maybe String getScriptName = lookup "SCRIPT_NAME" -- | Return the server address, as provided by the web server, if it was provided. getServerAddress :: Env -> Maybe String getServerAddress = lookup "SERVER_ADDR" -- | Return the server name, as provided by the web server, if it was provided. getServerName :: Env -> Maybe String getServerName = lookup "SERVER_NAME" -- | Return the server port, as provided by the web server, if it was provided. getServerPort :: Env -> Maybe Int getServerPort env = lookup "SERVER_PORT" env >>= parseInt -- | Return the server protocol, as provided by the web server, if it was provided. getServerProtocol :: Env -> Maybe String getServerProtocol = lookup "SERVER_PROTOCOL" -- | Return the server software name and version, as provided by the web server, if -- it was provided. getServerSoftware :: Env -> Maybe String getServerSoftware = lookup "SERVER_SOFTWARE" -- | Return the authentication type, as provided by the web server, if it was provided. getAuthenticationType :: Env -> Maybe String getAuthenticationType = lookup "AUTH_TYPE" sendBuffer :: FastCGIState -> Int -> BS.ByteString -> IO () sendBuffer state requestID buffer = do let len = BS.length buffer lengthThisRecord = min len 0xFFFF bufferThisRecord = BS.take lengthThisRecord buffer bufferRemaining = BS.drop lengthThisRecord buffer if lengthThisRecord > 0 then sendRecord state $ Record { recordType = StdoutRecord, recordRequestID = requestID, recordContent = bufferThisRecord } else return () if len > lengthThisRecord then sendBuffer state requestID bufferRemaining else return () writeResponse :: Connection -> Response -> IO () writeResponse conn rsp = do enc <- getEncoding (rspHeaders rsp) lbs <- encodeString enc (rspBody rsp) let rsp' = normalizeResponse (Just (LBS.length lbs)) rsp writeHeaders conn rsp' mapM_ (writeByteString conn) (LBS.toChunks lbs) writeHeaders :: Connection -> Response -> IO () writeHeaders (Connection state requestID) rsp = do let status = show (rspCode rsp) headers = rspHeaders rsp nameValuePairs = ("Status", status) : map (\(Header hdr val) -> (show hdr,val)) headers bytestrings = map (\(name, value) -> BSC.pack $ name ++ ": " ++ value ++ "\r\n") nameValuePairs ++ [BSC.pack "\r\n"] buffer = foldl BS.append BS.empty bytestrings sendBuffer state requestID buffer writeString :: Connection -> TextEncoding -> String -> IO () writeString conn enc s = do lbs <- encodeString enc s mapM_ (writeByteString conn) (LBS.toChunks lbs) writeByteString :: Connection -> BS.ByteString -> IO () writeByteString (Connection state requestID) bs = sendRecord state $ Record { recordType = StdoutRecord, recordRequestID = requestID, recordContent = bs } close :: Connection -> IO () close (Connection state requestID) = do sendRecord state $ Record { recordType = EndRequestRecord, recordRequestID = requestID, recordContent = BS.pack [0, 0, 0, 0, 0, 0, 0, 0] }