packages feed

seacat-1.0.0.0: src/Web/Seacat/RequestHandler/BanHammer.hs

{-# LANGUAGE OverloadedStrings #-}

-- |Higher-level handlers to do with banning people.
-- Each of the functions here is of the same basic form, they take a
-- `Tag`, possibly some parameter to determine if a ban or limit is in
-- effect, a handler to apply on the case of ban, and a handler to
-- apply on the case of success.
-- Tags are just a name used to identify a route, they may be shared
-- amongst routes.
module Web.Seacat.RequestHandler.BanHammer ( Tag
                                           , ipBan
                                           , floodProtect
                                           , rateLimit
                                           , rateLimit') where

import Control.Applicative ((<$>))
import Control.Monad.IO.Class (liftIO)
import Data.Bits ((.|.), shift)
import Data.Text (Text)
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, getCurrentTime)
import Data.Word ()
import Database.Persist
import Network.Socket (SockAddr(..))
import Network.Wai (remoteHost)
import Web.Routes.PathInfo (PathInfo)
import Web.Seacat.Database
import Web.Seacat.RequestHandler.Types (Handler, RequestProcessor, askReq)

-- |A handy name to identify a route.
type Tag = Text

-- |Rate limit a particular route. This only looks at individual IPs,
-- and not ranges.
rateLimit :: PathInfo r
          => Tag
          -> NominalDiffTime
            -- ^ How frequently an individual can access this route.
          -> (UTCTime -> Handler r)
            -- ^ Takes the limit expiration time as a parameter.
          -> Handler r
          -> Handler r
rateLimit tag freq onLimit handler = do
  limited <- isRateLimited tag
  now     <- liftIO getCurrentTime
  ip      <- show . remoteHost <$> askReq

  case limited of
    Just t -> onLimit t
    Nothing -> do
      let banUntil = addUTCTime freq now
      _ <- insert $ SeacatRateLimit tag banUntil ip
      handler

-- |Don't let someone past if they are rate limited, but don't set a
-- new limit if there isn't one already. The parameters are as in
-- `rateLimit`.
rateLimit' :: PathInfo r => Tag -> (UTCTime -> Handler r) -> Handler r -> Handler r
rateLimit' tag onLimit handler = do
  limited <- isRateLimited tag

  case limited of
    Just t -> onLimit t
    Nothing -> handler

-- | Check if an IP is rate limited. If it is, return when the limit
-- ends. This has the side-effect of clearing out all expired limits.
isRateLimited :: PathInfo r => Tag -> RequestProcessor r (Maybe UTCTime)
isRateLimited tag = do
  ip <- show . remoteHost <$> askReq

  now <- liftIO getCurrentTime

  deleteWhere [SeacatRateLimitExpires <. now]

  ban <- selectFirst [ SeacatRateLimitApplies ==. tag
                    , SeacatRateLimitTarget  ==. ip
                    ] []

  return $ (\(Entity _ b) -> seacatRateLimitExpires b) <$> ban

--------------------

-- |Check if someone is banned, and send them to the error handler if
-- so.
ipBan :: PathInfo r
      => Tag
      -> (UTCTime -> Text -> Handler r)
        -- ^ Takes the expiration time and reason of the ban as
        -- parameters.
      -> Handler r
      -> Handler r
ipBan tag onBan handler = do
  ip <- remoteHost <$> askReq

  now <- liftIO getCurrentTime

  deleteWhere [SeacatIPBanExpires <. now]

  ban <- selectFirst [ SeacatIPBanApplies ==. tag
                    , SeacatIPBanStart <=. ipToRational ip
                    , SeacatIPBanStop >=. ipToRational ip
                    ] []
  case ban of
    Just (Entity _ ipban) -> onBan (seacatIPBanExpires ipban) (seacatIPBanReason ipban)
    Nothing -> handler

-- |Convert an IP address into a Rational, the type I'm using in the
-- database to represent them.
ipToRational :: SockAddr -> Rational
ipToRational (SockAddrInet _ hostAddr) = fromIntegral hostAddr
ipToRational (SockAddrInet6 _ _ (a, b, c, d) _) = let a' = shift (fromIntegral a :: Integer) 96
                                                      b' = shift (fromIntegral b :: Integer) 64
                                                      c' = shift (fromIntegral c :: Integer) 32
                                                      d' = fromIntegral d
                                                  in fromIntegral $ a' .|. b' .|. c' .|. d'
ipToRational (SockAddrUnix _) = -1

--------------------

-- |Flood protect a particular route. This only looks at individual
-- IPs and not ranges. If multiple routes share this tag, they should
-- use the same time period.
floodProtect :: PathInfo r
             => Tag
             -> NominalDiffTime
               -- ^ The time period covered by the protection
             -> Int
               -- ^ How many times in the time period an IP can access
               -- this tag.
             -> Handler r
             -> Handler r
             -> Handler r
floodProtect tag time accesses onFlood handler = do
  ip <- show . remoteHost <$> askReq
  now <- liftIO getCurrentTime

  deleteWhere [SeacatAntiFloodExpires <. now]

  flood <- ((>=accesses) . length) <$> selectList [ SeacatAntiFloodApplies ==. tag
                                                , SeacatAntiFloodTarget  ==. ip
                                                ] []

  if flood
  then onFlood
  else do
    let expires = addUTCTime time now
    _ <- insert $ SeacatAntiFlood tag expires ip
    handler