packages feed

snaplet-recaptcha-0.1: src/Snap/Snaplet/ReCaptcha.hs

{-# LANGUAGE OverloadedStrings, FlexibleInstances, MultiParamTypeClasses, TemplateHaskell #-}
-- |
-- Module      : Snap.Snaplet.ReCaptcha
-- Copyright   : (c) Lars Petersen 2012
-- License     : BSD-style
--
-- Maintainer  : info@lars-petersen.net
-- Stability   : experimental
-- Portability : portable
-- 
-- This is a snaplet for google's ReCaptcha verification api. This library uses `http-conduit` and keeps connections alive (a maximum of 10 by now). This is an important point in order to avoid denial of service attacks.
-- 
-- Include it into your application like this:
--
-- > import Snap.Snaplet.ReCaptcha
-- >
-- > data MyApplication = MyApplication { _recaptcha :: Snaplet ReCaptcha, ... }
-- >
-- > $(makeLenses [''MyApplication])
-- >
-- > instance HasReCaptcha MyApplicaiton where
-- >   recaptchaLens = subSnaplet recaptcha
-- >
-- > myApplication :: SnapletInit MyApplication
-- > myApplication
-- >   = makeSnaplet
-- >       "MyApplication"
-- >       ""
-- >        Nothing
-- >        $ do r <- embedSnaplet "recaptcha" recaptcha $ initReCaptcha "YOUR_PRIVATE_KEY"
-- >             return $ MyApplication { _recaptcha = r, ... }
--
 
module Snap.Snaplet.ReCaptcha 
       ( -- * Snaplet and Initialization
         ReCaptcha ()
       , HasReCaptcha (..)
       , initReCaptcha
         -- * Handlers
       , verifyCaptcha
         -- * Types
       , PrivateKey
       , ReCaptchaResult (..)
       ) where

import Data.Lens.Common
import Data.Lens.Template

import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL

import Data.Monoid
import Control.Applicative
import Control.Failure
import Control.Exception (throw, try)
import Control.Monad.Trans.Resource (runResourceT)

import Network.HTTP.Types (renderSimpleQuery)
import Network.HTTP.Conduit as HTTP
import Snap

data ReCaptcha
  = ReCaptcha
    { privateKey        :: PrivateKey
    , connectionManager :: Manager 
    }

class HasReCaptcha b where
  recaptchaLens :: Lens (Snaplet b) (Snaplet ReCaptcha)

type PrivateKey = BS.ByteString

-- | The private key must be 40 characters long and encoded just like you get it from Google. 
initReCaptcha :: PrivateKey -> SnapletInit b ReCaptcha
initReCaptcha key
  = makeSnaplet
      "ReCaptcha"
      ""
      Nothing
      $ do man <- liftIO (newManager def)
           if BS.length key /= 40
             then fail "ReCaptcha: private key must be exactly 40 chars long"
             else return $ ReCaptcha key man

data ReCaptchaResult
  = Success
  -- Taken from the official ReCaptcha Api documentation
  | Failure BSL.ByteString
  | ConnectionError HttpException
  -- Misc
  | MissingArguments

instance Failure HttpException (Handler b ReCaptcha) where
  failure = throw

-- | You may use captcha verification troughout your whole application like this:
--
-- > myHandler :: (HasReCaptcha b) => Handler b SomeSubSnaplet ()
-- > myHandler
-- >   = do c <- verifyCaptcha
-- >        case c of
-- >          Success   -> .. -- captcha successfully solved
-- >          _         -> .. -- something went wrong
--
-- You don't need to extract the challenge and response paramters from the request. This is all handled by the snaplet. Just make sure the parameters `recaptcha_challenge_field` and `recaptcha_response_field` are contained in the user's request. Otherwise the `verifyCaptcha` call will return `MissingArguments`.
--
verifyCaptcha :: (HasReCaptcha b) => Handler b c ReCaptchaResult 
verifyCaptcha
  = do mchallenge <- getParam "recaptcha_challenge_field"
       mresponse  <- getParam "recaptcha_response_field"
       case (mchallenge, mresponse) of
         (Just challenge, Just response)
           -> do man         <- withTop' recaptchaLens (gets connectionManager)
                 privatekey  <- withTop' recaptchaLens (gets privateKey)
                 rq          <- withTop' recaptchaLens (parseUrl "http://www.google.com/recaptcha/api/verify")
                 remoteip    <- rqRemoteAddr <$> getRequest
                 let request = rq
                               { HTTP.method = "POST"
                               , queryString = renderSimpleQuery False
                                                 [ ("privatekey", privatekey)
                                                 , ("remoteip",   remoteip)
                                                 , ("challenge",  challenge)
                                                 , ("response",   response)
                                                 ]
                               }
                 result <- liftIO $ Control.Exception.try $ runResourceT $ httpLbs request man
                 case result of
                   Left e   -> do return (ConnectionError (e :: HttpException))
                   Right r  -> do let ls = BSL.split 0x0a $ responseBody r
                                  let ua = return (Failure $ responseBody r)
                                  if length ls < 2
                                    then ua
                                    else case ls !! 0 of
                                           "true"  -> return $ Success
                                           "false" -> return $ Failure (ls !! 1) 
                                           _       -> ua
         _ -> return MissingArguments