|
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==':'))
|