{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
--
-- Submodule for functionality related to SASL negotation:
-- authentication functions, SASL functionality, bind functionality,
-- and the legacy `{urn:ietf:params:xml:ns:xmpp-session}session'
-- functionality.
module Network.Xmpp.Sasl
( xmppSasl
, digestMd5
, scramSha1
, plain
, auth
) where
import Control.Monad.Error
import Control.Monad.State.Strict
import Data.Text (Text)
import Data.XML.Pickle
import Data.XML.Types
import Network.Xmpp.Marshal
import Network.Xmpp.Sasl.Mechanisms
import Network.Xmpp.Sasl.Types
import Network.Xmpp.Stream
import Network.Xmpp.Types
import System.Log.Logger (debugM, errorM, infoM)
-- | Uses the first supported mechanism to authenticate, if any. Updates the
-- state with non-password credentials and restarts the stream upon
-- success. Returns `Nothing' on success, an `AuthFailure' if
-- authentication fails, or an `XmppFailure' if anything else fails.
xmppSasl :: [SaslHandler] -- ^ Acceptable authentication mechanisms and their
-- corresponding handlers
-> Stream
-> IO (Either XmppFailure (Maybe AuthFailure))
xmppSasl handlers stream = do
debugM "Pontarius.Xmpp" "xmppSasl: Attempts to authenticate..."
flip withStream stream $ do
-- Chooses the first mechanism that is acceptable by both the client and the
-- server.
mechanisms <- gets $ streamSaslMechanisms . streamFeatures
case (filter (\(name, _) -> name `elem` mechanisms)) handlers of
[] -> return $ Right $ Just $ AuthNoAcceptableMechanism mechanisms
(_name, handler):_ -> do
cs <- gets streamConnectionState
case cs of
Closed -> do
lift $ errorM "Pontarius.Xmpp" "xmppSasl: Stream state closed."
return . Left $ XmppNoStream
_ -> runErrorT $ do
-- TODO: Log details about handler? SaslHandler "show" instance?
lift $ lift $ debugM "Pontarius.Xmpp" "xmppSasl: Performing handler..."
r <- ErrorT handler
case r of
Just ae -> do
lift $ lift $ errorM "Pontarius.Xmpp" $
"xmppSasl: AuthFailure encountered: " ++
show ae
return $ Just ae
Nothing -> do
lift $ lift $ debugM "Pontarius.Xmpp" "xmppSasl: Authentication successful, restarting stream."
_ <- ErrorT restartStream
lift $ lift $ debugM "Pontarius.Xmpp" "xmppSasl: Stream restarted."
return Nothing
-- | Authenticate to the server using the first matching method and bind a
-- resource.
auth :: [SaslHandler]
-> Maybe Text
-> Stream
-> IO (Either XmppFailure (Maybe AuthFailure))
auth mechanisms resource con = runErrorT $ do
mbAuthFail <- ErrorT $ xmppSasl mechanisms con
case mbAuthFail of
Nothing -> do
_jid <- ErrorT $ xmppBind resource con
ErrorT $ flip withStream' con $ do
s <- get
case establishSession $ streamConfiguration s of
False -> return $ Right Nothing
True -> do
_ <-liftIO $ startSession con
return $ Right Nothing
f -> return f
-- Produces a `bind' element, optionally wrapping a resource.
bindBody :: Maybe Text -> Element
bindBody = pickleElem $
-- Pickler to produce a
-- ""
-- element, with a possible "[JID]"
-- child.
xpBind . xpOption $ xpElemNodes "resource" (xpContent xpId)
-- Sends a (synchronous) IQ set request for a (`Just') given or server-generated
-- resource and extract the JID from the non-error response.
xmppBind :: Maybe Text -> Stream -> IO (Either XmppFailure Jid)
xmppBind rsrc c = runErrorT $ do
lift $ debugM "Pontarius.Xmpp" "Attempts to bind..."
answer <- ErrorT $ pushIQ "bind" Nothing Set Nothing (bindBody rsrc) c
case answer of
Right IQResult{iqResultPayload = Just b} -> do
lift $ debugM "Pontarius.Xmpp" "xmppBind: IQ result received; unpickling JID..."
let j = unpickleElem xpJid' b
case j of
Right jid' -> do
lift $ infoM "Pontarius.Xmpp" $ "Bound JID: " ++ show jid'
_ <- lift $ withStream ( do modify $ \s ->
s{streamJid = Just jid'})
c
return jid'
_ -> do
lift $ errorM "Pontarius.Xmpp"
$ "xmppBind: JID could not be unpickled from: "
++ show b
throwError $ XmppOtherFailure
_ -> do
lift $ errorM "Pontarius.XMPP" "xmppBind: IQ error received."
throwError XmppOtherFailure
where
-- Extracts the character data in the `jid' element.
xpJid' :: PU [Node] Jid
xpJid' = xpBind $ xpElemNodes jidName (xpContent xpJid)
jidName = "{urn:ietf:params:xml:ns:xmpp-bind}jid"
-- A `bind' element pickler.
xpBind :: PU [Node] b -> PU [Node] b
xpBind c = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-bind}bind" c
sessionXml :: Element
sessionXml = pickleElem
(xpElemBlank "{urn:ietf:params:xml:ns:xmpp-session}session")
()
-- Sends the session IQ set element and waits for an answer. Throws an error if
-- if an IQ error stanza is returned from the server.
startSession :: Stream -> IO Bool
startSession con = do
debugM "Pontarius.XMPP" "startSession: Pushing `session' IQ set stanza..."
answer <- pushIQ "session" Nothing Set Nothing sessionXml con
case answer of
Left e -> do
errorM "Pontarius.XMPP" $ "startSession: Error stanza received (" ++ (show e) ++ ")"
return False
Right _ -> do
debugM "Pontarius.XMPP" "startSession: Result stanza received."
return True