{-# LANGUAGE ScopedTypeVariables #-}
-- | A library for programming custom proxy servers.
module Network.HTTP.Proxy.Server (proxyMain
                                 ,Settings (..)
                                 ,Cache (..)
                                 ,Default(..)) where

import Network.HTTP hiding (port)
import Network.HTTP.Server hiding (Response, Request)
import Network.HTTP.Server.Logger
import Data.Default.Class
import Network.HostName
import Control.Monad.Reader

-- | The proxy monad: Reader (for settings) over IO
type Proxy s a = ReaderT (Settings s) IO a

type ProxyResponse s = Proxy s (Response s)

-- | Proxy entry-point. Spawns a new proxy server.
proxyMain :: forall s. HStream s => Settings s -> IO ()
proxyMain settings = (`runReaderT` settings) $
  do mhname <- asks hostname
     hname <- case mhname of
       Nothing -> lift getHostName
       Just hostn -> return hostn
     log <- asks logger
     port <- asks portnum
     let config = defaultConfig {srvPort = fromInteger port
                                ,srvHost = hname
                                ,srvLog  = log}
     myLogInfo $ "Proxy server started on port " ++ (show port)
     lift $ serverWith config (proxyHandler settings)

myLogInfo :: String -> Proxy s ()
myLogInfo s = asks logger >>= \l -> lift (logInfo l 0 s)

myLogWarning :: String -> Proxy s ()
myLogWarning s = asks logger >>= \l -> lift (logWarning l s)

myLogError :: String -> Proxy s ()
myLogError s = asks logger >>= \l -> lift (logError l s)

proxyHandler :: HStream s => Settings s -> Handler s
proxyHandler settings _ _ request = (`runReaderT` settings) $ do
  -- check that the request is authorized
  myLogInfo "Checking request authorization"
  authorized <- lift $ isAuthorized settings request
  if authorized then processRequest settings request
                else do myLogWarning $ "Rejecting an unauthorized request: "
                           ++ (show request)
                        errorProxyUnauthorized

-- | Processes the request; this is the main proxy procedure
processRequest :: HStream s => Settings s -> Request s -> ProxyResponse s
processRequest settings request = do
  -- modify the request
  myLogInfo "Modifying the request"
  modRequest <- lift $ requestModifier settings request
  -- check the cache
  myLogInfo "Querying cache"
  mCachedResponse <- lift $ queryCache (cache settings) modRequest
  case mCachedResponse of
    -- found in cache: return
    Just response -> do
      myLogInfo "Cache hit: returning cached response"
      return response
    -- not found: fetch it from a remote server, invoke the
    -- 'responseModifier' hook, record in cache and return
    Nothing       -> do
      myLogInfo "Cache miss: forwarding the request"
      response <- fetch modRequest
      myLogInfo "Modifying the response"
      modResponse <- lift $ responseModifier settings request response
      myLogInfo "Caching the modified response"
      lift $ recordInCache (cache settings) request modResponse
      return modResponse

fetch :: HStream s => Request s -> ProxyResponse s
fetch request = do
  result <- lift $ simpleHTTP request
  case result of
    Left err  -> do myLogError $
                      "Connection error while fetching an external resource: "
                      ++ show err
                    lift errorInternalServerError
    Right rsp -> return rsp

-- | Proxy server settings
data Settings s =
  Settings {requestModifier  :: Request s -> IO (Request s)
            -- ^ A function for modifying requests. Will be called for
            -- each request received; the modified request will be
            -- forwarded to the target server. Defaults to an identity
            -- function.
           ,responseModifier :: Request s -> Response s -> IO (Response s)
            -- ^ A function for modifying responses. Will be called
            -- for each response received; the modified response will
            -- be forwarded to the client. Defaults to an identity
            -- function.
           ,cache            :: Cache s
            -- ^ The cache. Use 'def' for no cache.
           ,isAuthorized     :: Request s -> IO Bool
            -- ^ Authorization function. Allows denying certain
            -- requests. Defaults to allowing all requests
           ,logger           :: Logger
            -- ^ A logging function. The default is 'stdLogger' from
            -- http-server.
           ,portnum          :: Integer
            -- ^ Proxy server port number; default is 3128
           ,hostname         :: Maybe String
            -- ^ The server host name. Defaults to the result of
            -- 'getHostName'
           }

instance Default (Settings s) where
  def = Settings {requestModifier  = return
                 ,responseModifier = \_ -> return
                 ,cache            = def
                 ,isAuthorized     = return . const True
                 ,logger           = stdLogger
                 ,portnum          = 3128
                 ,hostname         = Nothing}

-- | The cache.
data Cache s = Cache {queryCache :: Request s -> IO (Maybe (Response s))
                      -- ^ Retreive the response to a request from the
                      -- cache.
                     ,recordInCache :: Request s -> Response s -> IO ()
                      -- ^ Record the response to a request in the
                      -- cache.
                     }

instance Default (Cache s) where
  def = Cache {queryCache = return . const Nothing
              ,recordInCache = \_ -> return . const ()}

-- |A generic 500 response
errorInternalServerError :: HStream s => IO (Response s)
errorInternalServerError = return $ err_response InternalServerError

-- |A generic 407 response. TODO: RFC 2068 requres to send
-- Proxy-Authenticate header with this response code
errorProxyUnauthorized :: HStream s => ProxyResponse s
errorProxyUnauthorized = return $ err_response ProxyAuthenticationRequired

-- |A generic 400 response
errorBadRequest :: HStream s => IO (Response s)
errorBadRequest = return $ err_response BadRequest