packages feed

http-slim-1.2: Network/FastCGI.hs

{-# 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
     close conn

-- | 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]
                     }