Blame Network/HTTP/Auth.hs

Packit acf257
{-# LANGUAGE CPP #-}
Packit acf257
-----------------------------------------------------------------------------
Packit acf257
-- |
Packit acf257
-- Module      :  Network.HTTP.Auth
Packit acf257
-- Copyright   :  See LICENSE file
Packit acf257
-- License     :  BSD
Packit acf257
-- 
Packit acf257
-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
Packit acf257
-- Stability   :  experimental
Packit acf257
-- Portability :  non-portable (not tested)
Packit acf257
--
Packit acf257
-- Representing HTTP Auth values in Haskell.
Packit acf257
-- Right now, it contains mostly functionality needed by 'Network.Browser'.
Packit acf257
-- 
Packit acf257
-----------------------------------------------------------------------------
Packit acf257
module Network.HTTP.Auth
Packit acf257
       ( Authority(..)
Packit acf257
       , Algorithm(..)
Packit acf257
       , Challenge(..)
Packit acf257
       , Qop(..)
Packit acf257
Packit acf257
       , headerToChallenge -- :: URI -> Header -> Maybe Challenge
Packit acf257
       , withAuthority     -- :: Authority -> Request ty -> String
Packit acf257
       ) where
Packit acf257
Packit acf257
import Network.URI
Packit acf257
import Network.HTTP.Base
Packit acf257
import Network.HTTP.Utils
Packit acf257
import Network.HTTP.Headers ( Header(..) )
Packit acf257
import qualified Network.HTTP.MD5Aux as MD5 (md5s, Str(Str))
Packit acf257
import qualified Network.HTTP.Base64 as Base64 (encode)
Packit acf257
import Text.ParserCombinators.Parsec
Packit acf257
   ( Parser, char, many, many1, satisfy, parse, spaces, sepBy1 )
Packit acf257
Packit acf257
import Data.Char
Packit acf257
import Data.Maybe
Packit acf257
import Data.Word ( Word8 )
Packit acf257
Packit acf257
-- | @Authority@ specifies the HTTP Authentication method to use for
Packit acf257
-- a given domain/realm; @Basic@ or @Digest@.
Packit acf257
data Authority 
Packit acf257
 = AuthBasic { auRealm    :: String
Packit acf257
             , auUsername :: String
Packit acf257
             , auPassword :: String
Packit acf257
             , auSite     :: URI
Packit acf257
             }
Packit acf257
 | AuthDigest{ auRealm     :: String
Packit acf257
             , auUsername  :: String
Packit acf257
             , auPassword  :: String
Packit acf257
             , auNonce     :: String
Packit acf257
             , auAlgorithm :: Maybe Algorithm
Packit acf257
             , auDomain    :: [URI]
Packit acf257
             , auOpaque    :: Maybe String
Packit acf257
             , auQop       :: [Qop]
Packit acf257
             }
Packit acf257
Packit acf257
Packit acf257
data Challenge 
Packit acf257
 = ChalBasic  { chRealm   :: String }
Packit acf257
 | ChalDigest { chRealm   :: String
Packit acf257
              , chDomain  :: [URI]
Packit acf257
              , chNonce   :: String
Packit acf257
              , chOpaque  :: Maybe String
Packit acf257
              , chStale   :: Bool
Packit acf257
              , chAlgorithm ::Maybe Algorithm
Packit acf257
              , chQop     :: [Qop]
Packit acf257
              }
Packit acf257
Packit acf257
-- | @Algorithm@ controls the digest algorithm to, @MD5@ or @MD5Session@.
Packit acf257
data Algorithm = AlgMD5 | AlgMD5sess
Packit acf257
    deriving(Eq)
Packit acf257
Packit acf257
instance Show Algorithm where
Packit acf257
    show AlgMD5 = "md5"
Packit acf257
    show AlgMD5sess = "md5-sess"
Packit acf257
Packit acf257
-- | 
Packit acf257
data Qop = QopAuth | QopAuthInt
Packit acf257
    deriving(Eq,Show)
Packit acf257
Packit acf257
-- | @withAuthority auth req@ generates a credentials value from the @auth@ 'Authority',
Packit acf257
-- in the context of the given request.
Packit acf257
-- 
Packit acf257
-- If a client nonce was to be used then this function might need to be of type ... -> BrowserAction String
Packit acf257
withAuthority :: Authority -> Request ty -> String
Packit acf257
withAuthority a rq = case a of
Packit acf257
        AuthBasic{}  -> "Basic " ++ base64encode (auUsername a ++ ':' : auPassword a)
Packit acf257
        AuthDigest{} ->
Packit acf257
            "Digest " ++
Packit acf257
             concat [ "username="  ++ quo (auUsername a)
Packit acf257
                    , ",realm="    ++ quo (auRealm a)
Packit acf257
                    , ",nonce="    ++ quo (auNonce a)
Packit acf257
                    , ",uri="      ++ quo digesturi
Packit acf257
                    , ",response=" ++ quo rspdigest
Packit acf257
                       -- plus optional stuff:
Packit acf257
                    , fromMaybe "" (fmap (\ alg -> ",algorithm=" ++ quo (show alg)) (auAlgorithm a))
Packit acf257
                    , fromMaybe "" (fmap (\ o   -> ",opaque=" ++ quo o) (auOpaque a))
Packit acf257
                    , if null (auQop a) then "" else ",qop=auth"
Packit acf257
                    ]
Packit acf257
    where
Packit acf257
        quo s = '"':s ++ "\""
Packit acf257
Packit acf257
        rspdigest = map toLower (kd (md5 a1) (noncevalue ++ ":" ++ md5 a2))
Packit acf257
Packit acf257
        a1, a2 :: String
Packit acf257
        a1 = auUsername a ++ ":" ++ auRealm a ++ ":" ++ auPassword a
Packit acf257
        
Packit acf257
        {-
Packit acf257
        If the "qop" directive's value is "auth" or is unspecified, then A2
Packit acf257
        is:
Packit acf257
           A2  = Method ":" digest-uri-value
Packit acf257
        If the "qop" value is "auth-int", then A2 is:
Packit acf257
           A2  = Method ":" digest-uri-value ":" H(entity-body)
Packit acf257
        -}
Packit acf257
        a2 = show (rqMethod rq) ++ ":" ++ digesturi
Packit acf257
Packit acf257
        digesturi = show (rqURI rq)
Packit acf257
        noncevalue = auNonce a
Packit acf257
Packit acf257
type Octet = Word8
Packit acf257
Packit acf257
-- FIXME: these probably only work right for latin-1 strings
Packit acf257
stringToOctets :: String -> [Octet]
Packit acf257
stringToOctets = map (fromIntegral . fromEnum)
Packit acf257
Packit acf257
base64encode :: String -> String
Packit acf257
base64encode = Base64.encode . stringToOctets
Packit acf257
Packit acf257
md5 :: String -> String
Packit acf257
md5 = MD5.md5s . MD5.Str
Packit acf257
Packit acf257
kd :: String -> String -> String
Packit acf257
kd a b = md5 (a ++ ":" ++ b)
Packit acf257
Packit acf257
Packit acf257
Packit acf257
Packit acf257
-- | @headerToChallenge base www_auth@ tries to convert the @WWW-Authenticate@ header 
Packit acf257
-- @www_auth@  into a 'Challenge' value.
Packit acf257
headerToChallenge :: URI -> Header -> Maybe Challenge
Packit acf257
headerToChallenge baseURI (Header _ str) =
Packit acf257
    case parse challenge "" str of
Packit acf257
        Left{} -> Nothing
Packit acf257
        Right (name,props) -> case name of
Packit acf257
            "basic"  -> mkBasic props
Packit acf257
            "digest" -> mkDigest props
Packit acf257
            _        -> Nothing
Packit acf257
    where
Packit acf257
        challenge :: Parser (String,[(String,String)])
Packit acf257
        challenge =
Packit acf257
            do { nme <- word
Packit acf257
               ; spaces
Packit acf257
               ; pps <- cprops
Packit acf257
               ; return (map toLower nme,pps)
Packit acf257
               }
Packit acf257
Packit acf257
        cprops = sepBy1 cprop comma
Packit acf257
Packit acf257
        comma = do { spaces ; _ <- char ',' ; spaces }
Packit acf257
Packit acf257
        cprop =
Packit acf257
            do { nm <- word
Packit acf257
               ; _ <- char '='
Packit acf257
               ; val <- quotedstring
Packit acf257
               ; return (map toLower nm,val)
Packit acf257
               }
Packit acf257
Packit acf257
        mkBasic, mkDigest :: [(String,String)] -> Maybe Challenge
Packit acf257
Packit acf257
        mkBasic params = fmap ChalBasic (lookup "realm" params)
Packit acf257
Packit acf257
        mkDigest params =
Packit acf257
            -- with Maybe monad
Packit acf257
            do { r <- lookup "realm" params
Packit acf257
               ; n <- lookup "nonce" params
Packit acf257
               ; return $ 
Packit acf257
                    ChalDigest { chRealm  = r
Packit acf257
                               , chDomain = (annotateURIs 
Packit acf257
                                            $ map parseURI
Packit acf257
                                            $ words 
Packit acf257
                                            $ fromMaybe [] 
Packit acf257
                                            $ lookup "domain" params)
Packit acf257
                               , chNonce  = n
Packit acf257
                               , chOpaque = lookup "opaque" params
Packit acf257
                               , chStale  = "true" == (map toLower
Packit acf257
                                           $ fromMaybe "" (lookup "stale" params))
Packit acf257
                               , chAlgorithm= readAlgorithm (fromMaybe "MD5" $ lookup "algorithm" params)
Packit acf257
                               , chQop    = readQop (fromMaybe "" $ lookup "qop" params)
Packit acf257
                               }
Packit acf257
               }
Packit acf257
Packit acf257
        annotateURIs :: [Maybe URI] -> [URI]
Packit acf257
#if MIN_VERSION_network(2,4,0)
Packit acf257
        annotateURIs = map (`relativeTo` baseURI) . catMaybes
Packit acf257
#else
Packit acf257
        annotateURIs = (map (\u -> fromMaybe u (u `relativeTo` baseURI))) . catMaybes
Packit acf257
#endif
Packit acf257
Packit acf257
        -- Change These:
Packit acf257
        readQop :: String -> [Qop]
Packit acf257
        readQop = catMaybes . (map strToQop) . (splitBy ',')
Packit acf257
Packit acf257
        strToQop qs = case map toLower (trim qs) of
Packit acf257
            "auth"     -> Just QopAuth
Packit acf257
            "auth-int" -> Just QopAuthInt
Packit acf257
            _          -> Nothing
Packit acf257
Packit acf257
        readAlgorithm astr = case map toLower (trim astr) of
Packit acf257
            "md5"      -> Just AlgMD5
Packit acf257
            "md5-sess" -> Just AlgMD5sess
Packit acf257
            _          -> Nothing
Packit acf257
Packit acf257
word, quotedstring :: Parser String
Packit acf257
quotedstring =
Packit acf257
    do { _ <- char '"'  -- "
Packit acf257
       ; str <- many (satisfy $ not . (=='"'))
Packit acf257
       ; _ <- char '"'
Packit acf257
       ; return str
Packit acf257
       }
Packit acf257
Packit acf257
word = many1 (satisfy (\x -> isAlphaNum x || x=='_' || x=='.' || x=='-' || x==':'))