{-# 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.Applicative import Control.Arrow (left) import Control.Monad import Control.Monad.Error import Control.Monad.State.Strict import Data.Maybe (fromJust, isJust) import qualified Crypto.Classes as CC import qualified Data.Binary as Binary import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as BL import qualified Data.Digest.Pure.MD5 as MD5 import qualified Data.List as L import Data.Word (Word8) import qualified Data.Text as Text import Data.Text (Text) import qualified Data.Text.Encoding as Text import Network.Xmpp.Stream import Network.Xmpp.Types import System.Log.Logger (debugM, errorM) import qualified System.Random as Random import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Mechanisms import Control.Concurrent.STM.TMVar import Control.Exception import Data.XML.Pickle import Data.XML.Types import Network.Xmpp.Types import Network.Xmpp.Marshal import Control.Monad.State(modify) import Control.Concurrent.STM.TMVar import Control.Monad.Error -- | 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 ErrorT $ xmppSasl mechanisms con jid <- ErrorT $ xmppBind resource con ErrorT $ flip withStream con $ do s <- get case establishSession $ streamConfiguration s of False -> return $ Right Nothing True -> do _ <- lift $ startSession con return $ Right Nothing return Nothing -- 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 jid = unpickleElem xpJid b case jid of Right jid' -> do lift $ debugM "Pontarius.XMPP" $ "xmppBind: JID unpickled: " ++ show jid' ErrorT $ withStream (do modify $ \s -> s{streamJid = Just jid'} return $ Right jid') c -- not pretty return jid' otherwise -> do lift $ errorM "Pontarius.XMPP" $ "xmppBind: JID could not be unpickled from: " ++ show b throwError $ XmppOtherFailure otherwise -> 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 xpPrim) 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") () sessionIQ :: Stanza sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess" , iqRequestFrom = Nothing , iqRequestTo = Nothing , iqRequestLangTag = Nothing , iqRequestType = Set , iqRequestPayload = sessionXml } -- 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