|
|
|
@ -1,4 +1,5 @@ |
|
|
|
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} |
|
|
|
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} |
|
|
|
|
|
|
|
|
|
|
|
module Network.XMPP.SASL where |
|
|
|
module Network.XMPP.SASL where |
|
|
|
|
|
|
|
|
|
|
|
import Control.Applicative |
|
|
|
import Control.Applicative |
|
|
|
@ -32,109 +33,123 @@ import Network.XMPP.Pickle |
|
|
|
|
|
|
|
|
|
|
|
import qualified System.Random as Random |
|
|
|
import qualified System.Random as Random |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
saslInitE :: Text -> Element |
|
|
|
|
|
|
|
saslInitE mechanism = |
|
|
|
|
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth" |
|
|
|
|
|
|
|
[ ("mechanism", [ContentText mechanism]) ] |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
saslResponseE :: Text -> Element |
|
|
|
|
|
|
|
saslResponseE resp = |
|
|
|
|
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response" |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
[NodeContent $ ContentText resp] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
saslResponse2E :: Element |
|
|
|
|
|
|
|
saslResponse2E = |
|
|
|
|
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response" |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data AuthError = AuthXmlError |
|
|
|
data AuthError = AuthXmlError |
|
|
|
| AuthMechanismError [Text] |
|
|
|
| AuthMechanismError [Text] -- ^ Wraps mechanisms offered |
|
|
|
| AuthChallengeError |
|
|
|
| AuthChallengeError |
|
|
|
| AuthStreamError StreamError |
|
|
|
| AuthStreamError StreamError -- ^ Stream error on stream restart |
|
|
|
| AuthConnectionError |
|
|
|
| AuthConnectionError -- ^ No host name set in state |
|
|
|
|
|
|
|
| AuthError -- General instance used for the Error instance |
|
|
|
deriving Show |
|
|
|
deriving Show |
|
|
|
|
|
|
|
|
|
|
|
instance Error AuthError where |
|
|
|
instance Error AuthError where |
|
|
|
noMsg = AuthXmlError |
|
|
|
noMsg = AuthError |
|
|
|
|
|
|
|
|
|
|
|
xmppSASL:: Text -> Text -> XMPPConMonad (Either AuthError Text) |
|
|
|
-- Uses the DIGEST-MD5 method (if available) to authenticate. Updates the |
|
|
|
|
|
|
|
-- sUsername XMPPConMonad field with a `Just' value and restarts the stream upon |
|
|
|
|
|
|
|
-- success. This computation wraps an ErrorT computation, which means that |
|
|
|
|
|
|
|
-- catchError can be used to catch any errors. |
|
|
|
|
|
|
|
xmppSASL :: Text -- ^ User name |
|
|
|
|
|
|
|
-> Text -- ^ Password |
|
|
|
|
|
|
|
-> XMPPConMonad (Either AuthError ()) |
|
|
|
xmppSASL uname passwd = runErrorT $ do |
|
|
|
xmppSASL uname passwd = runErrorT $ do |
|
|
|
realm <- gets sHostname |
|
|
|
realm <- gets sHostname |
|
|
|
case realm of |
|
|
|
case realm of |
|
|
|
Just realm' -> do |
|
|
|
Just realm' -> do |
|
|
|
ErrorT $ xmppStartSASL realm' uname passwd |
|
|
|
ErrorT $ xmppStartSASL realm' |
|
|
|
modify (\s -> s{sUsername = Just uname}) |
|
|
|
modify (\s -> s{sUsername = Just uname}) |
|
|
|
return uname |
|
|
|
|
|
|
|
Nothing -> throwError AuthConnectionError |
|
|
|
Nothing -> throwError AuthConnectionError |
|
|
|
|
|
|
|
where |
|
|
|
xmppStartSASL :: Text |
|
|
|
xmppStartSASL :: Text -- ^ SASL realm |
|
|
|
-> Text |
|
|
|
|
|
|
|
-> Text |
|
|
|
|
|
|
|
-> XMPPConMonad (Either AuthError ()) |
|
|
|
-> XMPPConMonad (Either AuthError ()) |
|
|
|
xmppStartSASL realm username passwd = runErrorT $ do |
|
|
|
xmppStartSASL realm = runErrorT $ do |
|
|
|
mechanisms <- gets $ saslMechanisms . sFeatures |
|
|
|
mechanisms <- gets $ saslMechanisms . sFeatures |
|
|
|
unless ("DIGEST-MD5" `elem` mechanisms) |
|
|
|
unless ("DIGEST-MD5" `elem` mechanisms) . |
|
|
|
. throwError $ AuthMechanismError mechanisms |
|
|
|
throwError $ AuthMechanismError mechanisms |
|
|
|
lift . pushN $ saslInitE "DIGEST-MD5" |
|
|
|
-- Push element and receive the challenge (in XMPPConMonad). |
|
|
|
challenge' <- lift $ B64.decode . Text.encodeUtf8 |
|
|
|
_ <- lift . pushN $ saslInitE "DIGEST-MD5" -- TODO: Check boolean? |
|
|
|
<$> pullPickle challengePickle |
|
|
|
challenge' <- lift $ B64.decode . Text.encodeUtf8 <$> |
|
|
|
|
|
|
|
pullPickle challengePickle |
|
|
|
challenge <- case challenge' of |
|
|
|
challenge <- case challenge' of |
|
|
|
Left _e -> throwError AuthChallengeError |
|
|
|
Left _e -> throwError AuthChallengeError |
|
|
|
Right r -> return r |
|
|
|
Right r -> return r |
|
|
|
pairs <- case toPairs challenge of |
|
|
|
pairs <- case toPairs challenge of |
|
|
|
Left _ -> throwError AuthChallengeError |
|
|
|
Left _ -> throwError AuthChallengeError |
|
|
|
Right p -> return p |
|
|
|
Right p -> return p |
|
|
|
g <- liftIO $ Random.newStdGen |
|
|
|
g <- liftIO Random.newStdGen |
|
|
|
lift . pushN . saslResponseE $ createResponse g realm username passwd pairs |
|
|
|
_ <- lift . pushN . -- TODO: Check boolean? |
|
|
|
|
|
|
|
saslResponseE $ createResponse g realm pairs |
|
|
|
challenge2 <- lift $ pullPickle (xpEither failurePickle challengePickle) |
|
|
|
challenge2 <- lift $ pullPickle (xpEither failurePickle challengePickle) |
|
|
|
case challenge2 of |
|
|
|
case challenge2 of |
|
|
|
Left _x -> throwError $ AuthXmlError |
|
|
|
Left _x -> throwError AuthXmlError |
|
|
|
Right _ -> return () |
|
|
|
Right _ -> return () |
|
|
|
lift $ pushN saslResponse2E |
|
|
|
lift $ pushN saslResponse2E |
|
|
|
e <- lift pullElement |
|
|
|
e <- lift pullElement |
|
|
|
case e of |
|
|
|
case e of |
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] -> return () |
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] -> |
|
|
|
|
|
|
|
return () |
|
|
|
_ -> throwError AuthXmlError -- TODO: investigate |
|
|
|
_ -> throwError AuthXmlError -- TODO: investigate |
|
|
|
|
|
|
|
-- The SASL authentication has succeeded; the stream is restarted. |
|
|
|
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream |
|
|
|
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream |
|
|
|
return () |
|
|
|
return () |
|
|
|
|
|
|
|
-- The <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> element. |
|
|
|
|
|
|
|
saslInitE :: Text -> Element |
|
|
|
|
|
|
|
saslInitE mechanism = |
|
|
|
|
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth" |
|
|
|
|
|
|
|
[("mechanism", [ContentText mechanism])] |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
-- SASL response with text payload. |
|
|
|
|
|
|
|
saslResponseE :: Text -> Element |
|
|
|
|
|
|
|
saslResponseE resp = |
|
|
|
|
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response" |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
[NodeContent $ ContentText resp] |
|
|
|
|
|
|
|
-- SASL response without payload. |
|
|
|
|
|
|
|
saslResponse2E :: Element |
|
|
|
|
|
|
|
saslResponse2E = |
|
|
|
|
|
|
|
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response" |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
[] |
|
|
|
|
|
|
|
-- Parses the incoming SASL data to a mapped list of pairs. |
|
|
|
|
|
|
|
toPairs :: BS.ByteString -> Either String [(BS.ByteString, BS.ByteString)] |
|
|
|
|
|
|
|
toPairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do |
|
|
|
|
|
|
|
AP.skipSpace |
|
|
|
|
|
|
|
name <- AP.takeWhile1 (/= '=') |
|
|
|
|
|
|
|
_ <- AP.char '=' |
|
|
|
|
|
|
|
quote <- ((AP.char '"' >> return True) `mplus` return False) |
|
|
|
|
|
|
|
content <- AP.takeWhile1 (AP.notInClass [',', '"']) |
|
|
|
|
|
|
|
when quote . void $ AP.char '"' |
|
|
|
|
|
|
|
return (name, content) |
|
|
|
|
|
|
|
-- Produce the response to the challenge. |
|
|
|
createResponse :: Random.RandomGen g |
|
|
|
createResponse :: Random.RandomGen g |
|
|
|
=> g |
|
|
|
=> g |
|
|
|
-> Text |
|
|
|
-> Text |
|
|
|
|
|
|
|
-> [(BS8.ByteString, BS8.ByteString)] -- Pairs |
|
|
|
-> Text |
|
|
|
-> Text |
|
|
|
-> Text |
|
|
|
createResponse g hostname pairs = let |
|
|
|
-> [(BS8.ByteString, BS8.ByteString)] |
|
|
|
|
|
|
|
-> Text |
|
|
|
|
|
|
|
createResponse g hostname username passwd' pairs = let |
|
|
|
|
|
|
|
Just qop = L.lookup "qop" pairs |
|
|
|
Just qop = L.lookup "qop" pairs |
|
|
|
Just nonce = L.lookup "nonce" pairs |
|
|
|
Just nonce = L.lookup "nonce" pairs |
|
|
|
uname = Text.encodeUtf8 username |
|
|
|
uname_ = Text.encodeUtf8 uname |
|
|
|
passwd = Text.encodeUtf8 passwd' |
|
|
|
passwd_ = Text.encodeUtf8 passwd |
|
|
|
-- Using Int instead of Word8 for random 1.0.0.0 (GHC 7) |
|
|
|
-- Using Int instead of Word8 for random 1.0.0.0 (GHC 7) compatibility. |
|
|
|
-- compatibility. |
|
|
|
|
|
|
|
cnonce = BS.tail . BS.init . |
|
|
|
cnonce = BS.tail . BS.init . |
|
|
|
B64.encode . BS.pack . map toWord8 . take 8 $ Random.randoms g |
|
|
|
B64.encode . BS.pack . map toWord8 . |
|
|
|
|
|
|
|
take 8 $ Random.randoms g |
|
|
|
nc = "00000001" |
|
|
|
nc = "00000001" |
|
|
|
digestURI = ("xmpp/" `BS.append` (Text.encodeUtf8 hostname)) |
|
|
|
digestURI = "xmpp/" `BS.append` Text.encodeUtf8 hostname |
|
|
|
digest = md5Digest |
|
|
|
digest = md5Digest |
|
|
|
uname |
|
|
|
uname_ |
|
|
|
(lookup "realm" pairs) |
|
|
|
(lookup "realm" pairs) |
|
|
|
passwd |
|
|
|
passwd_ |
|
|
|
digestURI |
|
|
|
digestURI |
|
|
|
nc |
|
|
|
nc |
|
|
|
qop |
|
|
|
qop |
|
|
|
nonce |
|
|
|
nonce |
|
|
|
cnonce |
|
|
|
cnonce |
|
|
|
response = BS.intercalate "," . map (BS.intercalate "=") $ |
|
|
|
response = BS.intercalate "," . map (BS.intercalate "=") $ |
|
|
|
[ ["username" , quote uname ]] |
|
|
|
[["username", quote uname_]] ++ |
|
|
|
++ case L.lookup "realm" pairs of |
|
|
|
case L.lookup "realm" pairs of |
|
|
|
Just realm -> [["realm" , quote realm ]] |
|
|
|
Just realm -> [["realm" , quote realm ]] |
|
|
|
Nothing -> [] |
|
|
|
Nothing -> [] ++ |
|
|
|
++ |
|
|
|
|
|
|
|
[ ["nonce" , quote nonce ] |
|
|
|
[ ["nonce" , quote nonce ] |
|
|
|
, ["cnonce" , quote cnonce ] |
|
|
|
, ["cnonce" , quote cnonce ] |
|
|
|
, ["nc" , nc ] |
|
|
|
, ["nc" , nc ] |
|
|
|
@ -144,34 +159,19 @@ createResponse g hostname username passwd' pairs = let |
|
|
|
, ["charset" , "utf-8" ] |
|
|
|
, ["charset" , "utf-8" ] |
|
|
|
] |
|
|
|
] |
|
|
|
in Text.decodeUtf8 $ B64.encode response |
|
|
|
in Text.decodeUtf8 $ B64.encode response |
|
|
|
where |
|
|
|
quote :: BS8.ByteString -> BS8.ByteString |
|
|
|
quote x = BS.concat ["\"",x,"\""] |
|
|
|
quote x = BS.concat ["\"",x,"\""] |
|
|
|
toWord8 x = fromIntegral (x :: Int) :: Word8 |
|
|
|
toWord8 :: Int -> Word8 |
|
|
|
|
|
|
|
toWord8 x = fromIntegral x :: Word8 |
|
|
|
toPairs :: BS.ByteString -> Either String [(BS.ByteString, BS.ByteString)] |
|
|
|
|
|
|
|
toPairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do |
|
|
|
|
|
|
|
AP.skipSpace |
|
|
|
|
|
|
|
name <- AP.takeWhile1 (/= '=') |
|
|
|
|
|
|
|
_ <- AP.char '=' |
|
|
|
|
|
|
|
quote <- ((AP.char '"' >> return True) `mplus` return False) |
|
|
|
|
|
|
|
content <- AP.takeWhile1 (AP.notInClass ",\"" ) |
|
|
|
|
|
|
|
when quote . void $ AP.char '"' |
|
|
|
|
|
|
|
return (name,content) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hash :: [BS8.ByteString] -> BS8.ByteString |
|
|
|
hash :: [BS8.ByteString] -> BS8.ByteString |
|
|
|
hash = BS8.pack . show |
|
|
|
hash = BS8.pack . show |
|
|
|
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") |
|
|
|
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") |
|
|
|
|
|
|
|
|
|
|
|
hashRaw :: [BS8.ByteString] -> BS8.ByteString |
|
|
|
hashRaw :: [BS8.ByteString] -> BS8.ByteString |
|
|
|
hashRaw = toStrict . Binary.encode |
|
|
|
hashRaw = toStrict . Binary.encode . |
|
|
|
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") |
|
|
|
(CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toStrict :: BL.ByteString -> BS8.ByteString |
|
|
|
toStrict :: BL.ByteString -> BS8.ByteString |
|
|
|
toStrict = BS.concat . BL.toChunks |
|
|
|
toStrict = BS.concat . BL.toChunks |
|
|
|
|
|
|
|
|
|
|
|
-- TODO: this only handles MD5-sess |
|
|
|
-- TODO: this only handles MD5-sess |
|
|
|
|
|
|
|
|
|
|
|
md5Digest :: BS8.ByteString |
|
|
|
md5Digest :: BS8.ByteString |
|
|
|
-> Maybe BS8.ByteString |
|
|
|
-> Maybe BS8.ByteString |
|
|
|
-> BS8.ByteString |
|
|
|
-> BS8.ByteString |
|
|
|
@ -182,16 +182,17 @@ md5Digest :: BS8.ByteString |
|
|
|
-> BS8.ByteString |
|
|
|
-> BS8.ByteString |
|
|
|
-> BS8.ByteString |
|
|
|
-> BS8.ByteString |
|
|
|
md5Digest uname realm password digestURI nc qop nonce cnonce = |
|
|
|
md5Digest uname realm password digestURI nc qop nonce cnonce = |
|
|
|
let ha1 = hash [hashRaw [uname, maybe "" id realm, password], nonce, cnonce] |
|
|
|
let ha1 = hash [ hashRaw [uname, maybe "" id realm, password] |
|
|
|
|
|
|
|
, nonce |
|
|
|
|
|
|
|
, cnonce |
|
|
|
|
|
|
|
] |
|
|
|
ha2 = hash ["AUTHENTICATE", digestURI] |
|
|
|
ha2 = hash ["AUTHENTICATE", digestURI] |
|
|
|
in hash [ha1, nonce, nc, cnonce, qop, ha2] |
|
|
|
in hash [ha1, nonce, nc, cnonce, qop, ha2] |
|
|
|
|
|
|
|
-- Failure element pickler. |
|
|
|
-- Pickling |
|
|
|
failurePickle :: PU [Node] SaslFailure |
|
|
|
failurePickle :: PU [Node] (SaslFailure) |
|
|
|
failurePickle = xpWrap |
|
|
|
failurePickle = xpWrap (\(txt,(failure,_,_)) |
|
|
|
(\(txt, (failure, _, _)) -> SaslFailure failure txt) |
|
|
|
-> SaslFailure failure txt) |
|
|
|
(\(SaslFailure failure txt) -> (txt,(failure,(),()))) |
|
|
|
(\(SaslFailure failure txt) |
|
|
|
|
|
|
|
-> (txt,(failure,(),()))) |
|
|
|
|
|
|
|
(xpElemNodes |
|
|
|
(xpElemNodes |
|
|
|
"{urn:ietf:params:xml:ns:xmpp-sasl}failure" |
|
|
|
"{urn:ietf:params:xml:ns:xmpp-sasl}failure" |
|
|
|
(xp2Tuple |
|
|
|
(xp2Tuple |
|
|
|
@ -204,9 +205,7 @@ failurePickle = xpWrap (\(txt,(failure,_,_)) |
|
|
|
xpPrim |
|
|
|
xpPrim |
|
|
|
(xpUnit) |
|
|
|
(xpUnit) |
|
|
|
(xpUnit)))) |
|
|
|
(xpUnit)))) |
|
|
|
|
|
|
|
-- Challenge element pickler. |
|
|
|
|
|
|
|
|
|
|
|
challengePickle :: PU [Node] Text.Text |
|
|
|
challengePickle :: PU [Node] Text.Text |
|
|
|
challengePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge" |
|
|
|
challengePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge" |
|
|
|
(xpIsolate $ xpContent xpId) |
|
|
|
(xpIsolate $ xpContent xpId) |
|
|
|
|
|
|
|
|
|
|
|
|