diff --git a/import_visualisation-new-full.png b/import_visualisation-new-full.png new file mode 100644 index 0000000..75b6ba9 Binary files /dev/null and b/import_visualisation-new-full.png differ diff --git a/import_visualisation-new.png b/import_visualisation-new.png new file mode 100644 index 0000000..f8c7bdc Binary files /dev/null and b/import_visualisation-new.png differ diff --git a/pontarius-xmpp.cabal b/pontarius-xmpp.cabal index a2304a7..ce1cfa4 100644 --- a/pontarius-xmpp.cabal +++ b/pontarius-xmpp.cabal @@ -55,7 +55,7 @@ Library , stringprep >=0.1.3 , hslogger >=1.1.0 Exposed-modules: Network.Xmpp - , Network.Xmpp.Connection + , Network.Xmpp.Internal , Network.Xmpp.IM Other-modules: Data.Conduit.Tls , Network.Xmpp.Bind @@ -67,14 +67,10 @@ Library , Network.Xmpp.Concurrent.Presence , Network.Xmpp.Concurrent.Threads , Network.Xmpp.Concurrent.Monad - , Network.Xmpp.Connection_ + , Network.Xmpp.Connection , Network.Xmpp.IM.Message , Network.Xmpp.IM.Presence - , Network.Xmpp.Jid , Network.Xmpp.Marshal - , Network.Xmpp.Message - , Network.Xmpp.Pickle - , Network.Xmpp.Presence , Network.Xmpp.Sasl , Network.Xmpp.Sasl.Common , Network.Xmpp.Sasl.Mechanisms @@ -83,7 +79,6 @@ Library , Network.Xmpp.Sasl.Mechanisms.Scram , Network.Xmpp.Sasl.StringPrep , Network.Xmpp.Sasl.Types - , Network.Xmpp.Session , Network.Xmpp.Stream , Network.Xmpp.Tls , Network.Xmpp.Types diff --git a/source/Network/Xmpp.hs b/source/Network/Xmpp.hs index d0e2e9c..6f9da93 100644 --- a/source/Network/Xmpp.hs +++ b/source/Network/Xmpp.hs @@ -18,7 +18,7 @@ -- of XMPP (RFC 6120): setup and teardown of XML streams, channel encryption, -- authentication, error handling, and communication primitives for messaging. -- --- For low-level access to Pontarius XMPP, see the "Network.Xmpp.Connection" +-- For low-level access to Pontarius XMPP, see the "Network.Xmpp.Internal" -- module. {-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} @@ -96,7 +96,7 @@ module Network.Xmpp , PresenceType(..) , PresenceError(..) -- *** Creating - , module Network.Xmpp.Presence + , presTo -- *** Sending -- | Sends a presence stanza. In general, the presence stanza should have no -- 'to' attribute, in which case the server to which the client is connected @@ -154,10 +154,8 @@ module Network.Xmpp import Network import Network.Xmpp.Concurrent -import Network.Xmpp.Message -import Network.Xmpp.Presence +import Network.Xmpp.Utilities import Network.Xmpp.Sasl import Network.Xmpp.Sasl.Types -import Network.Xmpp.Session import Network.Xmpp.Tls import Network.Xmpp.Types diff --git a/source/Network/Xmpp/Bind.hs b/source/Network/Xmpp/Bind.hs deleted file mode 100644 index a3676e6..0000000 --- a/source/Network/Xmpp/Bind.hs +++ /dev/null @@ -1,57 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - -{-# OPTIONS_HADDOCK hide #-} - -module Network.Xmpp.Bind where - -import Control.Exception - -import Data.Text as Text -import Data.XML.Pickle -import Data.XML.Types - -import Network.Xmpp.Connection_ -import Network.Xmpp.Pickle -import Network.Xmpp.Types - -import Control.Monad.State(modify) - -import Control.Concurrent.STM.TMVar - -import Control.Monad.Error - --- 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 -> TMVar Connection -> IO (Either XmppFailure Jid) -xmppBind rsrc c = runErrorT $ do - answer <- ErrorT $ pushIQ' "bind" Nothing Set Nothing (bindBody rsrc) c - case answer of - Right IQResult{iqResultPayload = Just b} -> do - let jid = unpickleElem xpJid b - case jid of - Right jid' -> do - ErrorT $ withConnection (do - modify $ \s -> s{cJid = Just jid'} - return $ Right jid') c -- not pretty - return jid' - otherwise -> throwError XmppOtherFailure - -- TODO: Log: ("Bind couldn't unpickle JID from " ++ show answer) - otherwise -> 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 diff --git a/source/Network/Xmpp/Concurrent.hs b/source/Network/Xmpp/Concurrent.hs index fa94910..8e7c02b 100644 --- a/source/Network/Xmpp/Concurrent.hs +++ b/source/Network/Xmpp/Concurrent.hs @@ -11,6 +11,7 @@ module Network.Xmpp.Concurrent , toChans , newSession , writeWorker + , session ) where import Network.Xmpp.Concurrent.Monad @@ -31,9 +32,17 @@ import Network.Xmpp.Concurrent.Presence import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Threads import Network.Xmpp.Marshal -import Network.Xmpp.Pickle import Network.Xmpp.Types import Text.Xml.Stream.Elements +import Network +import Data.Text as Text +import Network.Xmpp.Tls +import qualified Network.TLS as TLS +import Network.Xmpp.Sasl +import Network.Xmpp.Sasl.Mechanisms +import Network.Xmpp.Sasl.Types +import Data.Maybe +import Network.Xmpp.Stream import Control.Monad.Error @@ -111,3 +120,31 @@ writeWorker stCh writeR = forever $ do atomically $ unGetTChan stCh next -- If the writing failed, the -- connection is dead. threadDelay 250000 -- Avoid free spinning. + +-- | Creates a 'Session' object by setting up a connection with an XMPP server. +-- +-- Will connect to the specified host. If the fourth parameters is a 'Just' +-- value, @session@ will attempt to secure the connection with TLS. If the fifth +-- parameters is a 'Just' value, @session@ will attempt to authenticate and +-- acquire an XMPP resource. +session :: HostName -- ^ Host to connect to + -> Text -- ^ The realm host name (to + -- distinguish the XMPP service) + -> PortID -- ^ Port to connect to + -> Maybe TLS.TLSParams -- ^ TLS settings, if securing the + -- connection to the server is + -- desired + -> Maybe ([SaslHandler], Maybe Text) -- ^ SASL handlers and the desired + -- JID resource (or Nothing to let + -- the server decide) + -> IO (Either XmppFailure (Session, Maybe AuthFailure)) +session hostname realm port tls sasl = runErrorT $ do + con <- ErrorT $ connect hostname port realm + if isJust tls + then ErrorT $ startTls (fromJust tls) con + else return () + aut <- if isJust sasl + then ErrorT $ auth (fst $ fromJust sasl) (snd $ fromJust sasl) con + else return Nothing + ses <- ErrorT $ newSession con + return (ses, aut) diff --git a/source/Network/Xmpp/Concurrent/Monad.hs b/source/Network/Xmpp/Concurrent/Monad.hs index ff0f07a..588c9e3 100644 --- a/source/Network/Xmpp/Concurrent/Monad.hs +++ b/source/Network/Xmpp/Concurrent/Monad.hs @@ -9,7 +9,7 @@ import qualified Control.Exception.Lifted as Ex import Control.Monad.Reader import Network.Xmpp.Concurrent.Types -import Network.Xmpp.Connection_ +import Network.Xmpp.Connection diff --git a/source/Network/Xmpp/Concurrent/Threads.hs b/source/Network/Xmpp/Concurrent/Threads.hs index c55fc16..7c78682 100644 --- a/source/Network/Xmpp/Concurrent/Threads.hs +++ b/source/Network/Xmpp/Concurrent/Threads.hs @@ -16,7 +16,7 @@ import Control.Monad.State.Strict import qualified Data.ByteString as BS import Network.Xmpp.Concurrent.Types -import Network.Xmpp.Connection_ +import Network.Xmpp.Connection import Control.Concurrent.STM.TMVar diff --git a/source/Network/Xmpp/Connection.hs b/source/Network/Xmpp/Connection.hs index 263b452..0006073 100644 --- a/source/Network/Xmpp/Connection.hs +++ b/source/Network/Xmpp/Connection.hs @@ -1,41 +1,284 @@ --- | --- Module: $Header$ --- --- Maintainer: info@jonkri.com --- Stability: unstable --- Portability: portable --- --- This module allows for low-level access to Pontarius XMPP. Generally, the --- "Network.Xmpp" module should be used instead. --- --- The 'Connection' object provides the most low-level access to the XMPP --- stream: a simple and single-threaded interface which exposes the conduit --- 'Event' source, as well as the input and output byte streams. Custom stateful --- 'Connection' functions can be executed using 'withConnection'. --- --- The TLS, SASL, and 'Session' functionalities of Pontarius XMPP are built on --- top of this API. - -module Network.Xmpp.Connection - ( Connection(..) - , ConnectionState(..) - , ConnectionHandle(..) - , ServerFeatures(..) - , connect - , withConnection - , startTls - , simpleAuth - , auth - , pushStanza - , pullStanza - , closeConnection - , newSession - ) - - where - -import Network.Xmpp.Connection_ -import Network.Xmpp.Session -import Network.Xmpp.Tls -import Network.Xmpp.Types -import Network.Xmpp.Concurrent +{-# OPTIONS_HADDOCK hide #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings #-} + +module Network.Xmpp.Connection where + +import Control.Applicative((<$>)) +import Control.Concurrent (forkIO, threadDelay) +import System.IO.Error (tryIOError) +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.Trans.Class +--import Control.Monad.Trans.Resource +import qualified Control.Exception.Lifted as Ex +import qualified GHC.IO.Exception as GIE +import Control.Monad.State.Strict + +import Data.ByteString as BS +import Data.ByteString.Char8 as BSC8 +import Data.Conduit +import Data.Conduit.Binary as CB +import Data.Conduit.Internal as DCI +import qualified Data.Conduit.List as CL +import Data.IORef +import Data.Text(Text) +import qualified Data.Text as T +import Data.XML.Pickle +import Data.XML.Types + +import Network +import Network.Xmpp.Types +import Network.Xmpp.Marshal + +import System.IO + +import Text.Xml.Stream.Elements +import Text.XML.Stream.Parse as XP +import Text.XML.Unresolved(InvalidEventStream(..)) + +import System.Log.Logger +import Data.ByteString.Base64 + +import Control.Concurrent.STM.TMVar +import Control.Monad.Error + +-- Enable/disable debug output +-- This will dump all incoming and outgoing network taffic to the console, +-- prefixed with "in: " and "out: " respectively +debug :: Bool +debug = False + +-- TODO: Can the TLS send/recv functions throw something other than an IO error? + +wrapIOException :: IO a -> StateT Connection IO (Either XmppFailure a) +wrapIOException action = do + r <- liftIO $ tryIOError action + case r of + Right b -> return $ Right b + Left e -> return $ Left $ XmppIOException e + +pushElement :: Element -> StateT Connection IO (Either XmppFailure Bool) +pushElement x = do + send <- gets (cSend . cHandle) + wrapIOException $ send $ renderElement x + +-- | Encode and send stanza +pushStanza :: Stanza -> TMVar Connection -> IO (Either XmppFailure Bool) +pushStanza s = withConnection' . pushElement $ pickleElem xpStanza s + +-- XML documents and XMPP streams SHOULD be preceeded by an XML declaration. +-- UTF-8 is the only supported XMPP encoding. The standalone document +-- declaration (matching "SDDecl" in the XML standard) MUST NOT be included in +-- XMPP streams. RFC 6120 defines XMPP only in terms of XML 1.0. +pushXmlDecl :: StateT Connection IO (Either XmppFailure Bool) +pushXmlDecl = do + con <- gets cHandle + wrapIOException $ (cSend con) "" + +pushOpenElement :: Element -> StateT Connection IO (Either XmppFailure Bool) +pushOpenElement e = do + sink <- gets (cSend . cHandle) + wrapIOException $ sink $ renderOpenElement e + +-- `Connect-and-resumes' the given sink to the connection source, and pulls a +-- `b' value. +runEventsSink :: Sink Event IO b -> StateT Connection IO (Either XmppFailure b) +runEventsSink snk = do -- TODO: Wrap exceptions? + source <- gets cEventSource + (src', r) <- lift $ source $$++ snk + modify (\s -> s{cEventSource = src'}) + return $ Right r + +pullElement :: StateT Connection IO (Either XmppFailure Element) +pullElement = do + Ex.catches (do + e <- runEventsSink (elements =$ await) + case e of + Left f -> return $ Left f + Right Nothing -> return $ Left XmppOtherFailure -- TODO + Right (Just r) -> return $ Right r + ) + [ Ex.Handler (\StreamEnd -> return $ Left StreamEndFailure) + , Ex.Handler (\(InvalidXmppXml s) -- Invalid XML `Event' encountered, or missing element close tag + -> return $ Left XmppOtherFailure) -- TODO: Log: s + , Ex.Handler $ \(e :: InvalidEventStream) -- xml-conduit exception + -> return $ Left XmppOtherFailure -- TODO: Log: (show e) + ] + +-- Pulls an element and unpickles it. +pullUnpickle :: PU [Node] a -> StateT Connection IO (Either XmppFailure a) +pullUnpickle p = do + elem <- pullElement + case elem of + Left e -> return $ Left e + Right elem' -> do + let res = unpickleElem p elem' + case res of + Left e -> return $ Left XmppOtherFailure -- TODO: Log + Right r -> return $ Right r + +-- | Pulls a stanza (or stream error) from the stream. +pullStanza :: TMVar Connection -> IO (Either XmppFailure Stanza) +pullStanza = withConnection' $ do + res <- pullUnpickle xpStreamStanza + case res of + Left e -> return $ Left e + Right (Left e) -> return $ Left $ StreamErrorFailure e + Right (Right r) -> return $ Right r + +-- Performs the given IO operation, catches any errors and re-throws everything +-- except 'ResourceVanished' and IllegalOperation, in which case it will return False instead +catchPush :: IO () -> IO Bool +catchPush p = Ex.catch + (p >> return True) + (\e -> case GIE.ioe_type e of + GIE.ResourceVanished -> return False + GIE.IllegalOperation -> return False + _ -> Ex.throwIO e + ) + +-- Connection state used when there is no connection. +xmppNoConnection :: Connection +xmppNoConnection = Connection + { cHandle = ConnectionHandle { cSend = \_ -> return False + , cRecv = \_ -> Ex.throwIO + XmppOtherFailure + , cFlush = return () + , cClose = return () + } + , cEventSource = DCI.ResumableSource zeroSource (return ()) + , cFeatures = SF Nothing [] [] + , cState = ConnectionClosed + , cHostName = Nothing + , cJid = Nothing + , cStreamLang = Nothing + , cStreamId = Nothing + , cPreferredLang = Nothing + , cToJid = Nothing + , cJidWhenPlain = False + , cFrom = Nothing + } + where + zeroSource :: Source IO output + zeroSource = liftIO . Ex.throwIO $ XmppOtherFailure + +connectTcp :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Connection)) +connectTcp host port hostname = do + let PortNumber portNumber = port + debugM "Pontarius.Xmpp" $ "Connecting to " ++ host ++ " on port " ++ + (show portNumber) ++ " through the realm " ++ (T.unpack hostname) ++ "." + h <- connectTo host port + debugM "Pontarius.Xmpp" "Setting NoBuffering mode on handle." + hSetBuffering h NoBuffering + let eSource = DCI.ResumableSource + ((sourceHandle h $= logConduit) $= XP.parseBytes def) + (return ()) + let hand = ConnectionHandle { cSend = \d -> do + let d64 = encode d + debugM "Pontarius.Xmpp" $ + "Sending TCP data: " ++ (BSC8.unpack d64) + ++ "." + catchPush $ BS.hPut h d + , cRecv = \n -> do + d <- BS.hGetSome h n + let d64 = encode d + debugM "Pontarius.Xmpp" $ + "Received TCP data: " ++ + (BSC8.unpack d64) ++ "." + return d + , cFlush = hFlush h + , cClose = hClose h + } + let con = Connection + { cHandle = hand + , cEventSource = eSource + , cFeatures = (SF Nothing [] []) + , cState = ConnectionPlain + , cHostName = (Just hostname) + , cJid = Nothing + , cPreferredLang = Nothing -- TODO: Allow user to set + , cStreamLang = Nothing + , cStreamId = Nothing + , cToJid = Nothing -- TODO: Allow user to set + , cJidWhenPlain = False -- TODO: Allow user to set + , cFrom = Nothing + } + con' <- mkConnection con + return $ Right con' + where + logConduit :: Conduit ByteString IO ByteString + logConduit = CL.mapM $ \d -> do + let d64 = encode d + debugM "Pontarius.Xmpp" $ "Received TCP data: " ++ (BSC8.unpack d64) ++ + "." + return d + + +-- Closes the connection and updates the XmppConMonad Connection state. +-- killConnection :: TMVar Connection -> IO (Either Ex.SomeException ()) +killConnection :: TMVar Connection -> IO (Either XmppFailure ()) +killConnection = withConnection $ do + cc <- gets (cClose . cHandle) + err <- wrapIOException cc + -- (Ex.try cc :: IO (Either Ex.SomeException ())) + put xmppNoConnection + return err + +-- Sends an IQ request and waits for the response. If the response ID does not +-- match the outgoing ID, an error is thrown. +pushIQ' :: StanzaId + -> Maybe Jid + -> IQRequestType + -> Maybe LangTag + -> Element + -> TMVar Connection + -> IO (Either XmppFailure (Either IQError IQResult)) +pushIQ' iqID to tp lang body con = do + pushStanza (IQRequestS $ IQRequest iqID Nothing to lang tp body) con + res <- pullStanza con + case res of + Left e -> return $ Left e + Right (IQErrorS e) -> return $ Right $ Left e + Right (IQResultS r) -> do + unless + (iqID == iqResultID r) . liftIO . Ex.throwIO $ + XmppOtherFailure + -- TODO: Log: ("In sendIQ' IDs don't match: " ++ show iqID ++ + -- " /= " ++ show (iqResultID r) ++ " .") + return $ Right $ Right r + _ -> return $ Left XmppOtherFailure + -- TODO: Log: "sendIQ': unexpected stanza type " + +-- | Send "" and wait for the server to finish processing and to +-- close the connection. Any remaining elements from the server are returned. +-- Surpresses StreamEndFailure exceptions, but may throw a StreamCloseError. +closeStreams :: TMVar Connection -> IO (Either XmppFailure [Element]) +closeStreams = withConnection $ do + send <- gets (cSend . cHandle) + cc <- gets (cClose . cHandle) + liftIO $ send "" + void $ liftIO $ forkIO $ do + threadDelay 3000000 -- TODO: Configurable value + (Ex.try cc) :: IO (Either Ex.SomeException ()) + return () + collectElems [] + where + -- Pulls elements from the stream until the stream ends, or an error is + -- raised. + collectElems :: [Element] -> StateT Connection IO (Either XmppFailure [Element]) + collectElems es = do + result <- pullElement + case result of + Left StreamEndFailure -> return $ Right es + Left e -> return $ Left $ StreamCloseError (es, e) + Right e -> collectElems (e:es) + +debugConduit :: Pipe l ByteString ByteString u IO b +debugConduit = forever $ do + s' <- await + case s' of + Just s -> do + liftIO $ BS.putStrLn (BS.append "in: " s) + yield s + Nothing -> return () diff --git a/source/Network/Xmpp/Connection_.hs b/source/Network/Xmpp/Connection_.hs deleted file mode 100644 index 38a7532..0000000 --- a/source/Network/Xmpp/Connection_.hs +++ /dev/null @@ -1,285 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE OverloadedStrings #-} - -module Network.Xmpp.Connection_ where - -import Control.Applicative((<$>)) -import Control.Concurrent (forkIO, threadDelay) -import System.IO.Error (tryIOError) -import Control.Monad -import Control.Monad.IO.Class -import Control.Monad.Trans.Class ---import Control.Monad.Trans.Resource -import qualified Control.Exception.Lifted as Ex -import qualified GHC.IO.Exception as GIE -import Control.Monad.State.Strict - -import Data.ByteString as BS -import Data.ByteString.Char8 as BSC8 -import Data.Conduit -import Data.Conduit.Binary as CB -import Data.Conduit.Internal as DCI -import qualified Data.Conduit.List as CL -import Data.IORef -import Data.Text(Text) -import qualified Data.Text as T -import Data.XML.Pickle -import Data.XML.Types - -import Network -import Network.Xmpp.Types -import Network.Xmpp.Marshal -import Network.Xmpp.Pickle - -import System.IO - -import Text.Xml.Stream.Elements -import Text.XML.Stream.Parse as XP -import Text.XML.Unresolved(InvalidEventStream(..)) - -import System.Log.Logger -import Data.ByteString.Base64 - -import Control.Concurrent.STM.TMVar -import Control.Monad.Error - --- Enable/disable debug output --- This will dump all incoming and outgoing network taffic to the console, --- prefixed with "in: " and "out: " respectively -debug :: Bool -debug = False - --- TODO: Can the TLS send/recv functions throw something other than an IO error? - -wrapIOException :: IO a -> StateT Connection IO (Either XmppFailure a) -wrapIOException action = do - r <- liftIO $ tryIOError action - case r of - Right b -> return $ Right b - Left e -> return $ Left $ XmppIOException e - -pushElement :: Element -> StateT Connection IO (Either XmppFailure Bool) -pushElement x = do - send <- gets (cSend . cHandle) - wrapIOException $ send $ renderElement x - --- | Encode and send stanza -pushStanza :: Stanza -> TMVar Connection -> IO (Either XmppFailure Bool) -pushStanza s = withConnection' . pushElement $ pickleElem xpStanza s - --- XML documents and XMPP streams SHOULD be preceeded by an XML declaration. --- UTF-8 is the only supported XMPP encoding. The standalone document --- declaration (matching "SDDecl" in the XML standard) MUST NOT be included in --- XMPP streams. RFC 6120 defines XMPP only in terms of XML 1.0. -pushXmlDecl :: StateT Connection IO (Either XmppFailure Bool) -pushXmlDecl = do - con <- gets cHandle - wrapIOException $ (cSend con) "" - -pushOpenElement :: Element -> StateT Connection IO (Either XmppFailure Bool) -pushOpenElement e = do - sink <- gets (cSend . cHandle) - wrapIOException $ sink $ renderOpenElement e - --- `Connect-and-resumes' the given sink to the connection source, and pulls a --- `b' value. -runEventsSink :: Sink Event IO b -> StateT Connection IO (Either XmppFailure b) -runEventsSink snk = do -- TODO: Wrap exceptions? - source <- gets cEventSource - (src', r) <- lift $ source $$++ snk - modify (\s -> s{cEventSource = src'}) - return $ Right r - -pullElement :: StateT Connection IO (Either XmppFailure Element) -pullElement = do - Ex.catches (do - e <- runEventsSink (elements =$ await) - case e of - Left f -> return $ Left f - Right Nothing -> return $ Left XmppOtherFailure -- TODO - Right (Just r) -> return $ Right r - ) - [ Ex.Handler (\StreamEnd -> return $ Left StreamEndFailure) - , Ex.Handler (\(InvalidXmppXml s) -- Invalid XML `Event' encountered, or missing element close tag - -> return $ Left XmppOtherFailure) -- TODO: Log: s - , Ex.Handler $ \(e :: InvalidEventStream) -- xml-conduit exception - -> return $ Left XmppOtherFailure -- TODO: Log: (show e) - ] - --- Pulls an element and unpickles it. -pullUnpickle :: PU [Node] a -> StateT Connection IO (Either XmppFailure a) -pullUnpickle p = do - elem <- pullElement - case elem of - Left e -> return $ Left e - Right elem' -> do - let res = unpickleElem p elem' - case res of - Left e -> return $ Left XmppOtherFailure -- TODO: Log - Right r -> return $ Right r - --- | Pulls a stanza (or stream error) from the stream. -pullStanza :: TMVar Connection -> IO (Either XmppFailure Stanza) -pullStanza = withConnection' $ do - res <- pullUnpickle xpStreamStanza - case res of - Left e -> return $ Left e - Right (Left e) -> return $ Left $ StreamErrorFailure e - Right (Right r) -> return $ Right r - --- Performs the given IO operation, catches any errors and re-throws everything --- except 'ResourceVanished' and IllegalOperation, in which case it will return False instead -catchPush :: IO () -> IO Bool -catchPush p = Ex.catch - (p >> return True) - (\e -> case GIE.ioe_type e of - GIE.ResourceVanished -> return False - GIE.IllegalOperation -> return False - _ -> Ex.throwIO e - ) - --- Connection state used when there is no connection. -xmppNoConnection :: Connection -xmppNoConnection = Connection - { cHandle = ConnectionHandle { cSend = \_ -> return False - , cRecv = \_ -> Ex.throwIO - XmppOtherFailure - , cFlush = return () - , cClose = return () - } - , cEventSource = DCI.ResumableSource zeroSource (return ()) - , cFeatures = SF Nothing [] [] - , cState = ConnectionClosed - , cHostName = Nothing - , cJid = Nothing - , cStreamLang = Nothing - , cStreamId = Nothing - , cPreferredLang = Nothing - , cToJid = Nothing - , cJidWhenPlain = False - , cFrom = Nothing - } - where - zeroSource :: Source IO output - zeroSource = liftIO . Ex.throwIO $ XmppOtherFailure - -connectTcp :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Connection)) -connectTcp host port hostname = do - let PortNumber portNumber = port - debugM "Pontarius.Xmpp" $ "Connecting to " ++ host ++ " on port " ++ - (show portNumber) ++ " through the realm " ++ (T.unpack hostname) ++ "." - h <- connectTo host port - debugM "Pontarius.Xmpp" "Setting NoBuffering mode on handle." - hSetBuffering h NoBuffering - let eSource = DCI.ResumableSource - ((sourceHandle h $= logConduit) $= XP.parseBytes def) - (return ()) - let hand = ConnectionHandle { cSend = \d -> do - let d64 = encode d - debugM "Pontarius.Xmpp" $ - "Sending TCP data: " ++ (BSC8.unpack d64) - ++ "." - catchPush $ BS.hPut h d - , cRecv = \n -> do - d <- BS.hGetSome h n - let d64 = encode d - debugM "Pontarius.Xmpp" $ - "Received TCP data: " ++ - (BSC8.unpack d64) ++ "." - return d - , cFlush = hFlush h - , cClose = hClose h - } - let con = Connection - { cHandle = hand - , cEventSource = eSource - , cFeatures = (SF Nothing [] []) - , cState = ConnectionPlain - , cHostName = (Just hostname) - , cJid = Nothing - , cPreferredLang = Nothing -- TODO: Allow user to set - , cStreamLang = Nothing - , cStreamId = Nothing - , cToJid = Nothing -- TODO: Allow user to set - , cJidWhenPlain = False -- TODO: Allow user to set - , cFrom = Nothing - } - con' <- mkConnection con - return $ Right con' - where - logConduit :: Conduit ByteString IO ByteString - logConduit = CL.mapM $ \d -> do - let d64 = encode d - debugM "Pontarius.Xmpp" $ "Received TCP data: " ++ (BSC8.unpack d64) ++ - "." - return d - - --- Closes the connection and updates the XmppConMonad Connection state. --- killConnection :: TMVar Connection -> IO (Either Ex.SomeException ()) -killConnection :: TMVar Connection -> IO (Either XmppFailure ()) -killConnection = withConnection $ do - cc <- gets (cClose . cHandle) - err <- wrapIOException cc - -- (Ex.try cc :: IO (Either Ex.SomeException ())) - put xmppNoConnection - return err - --- Sends an IQ request and waits for the response. If the response ID does not --- match the outgoing ID, an error is thrown. -pushIQ' :: StanzaId - -> Maybe Jid - -> IQRequestType - -> Maybe LangTag - -> Element - -> TMVar Connection - -> IO (Either XmppFailure (Either IQError IQResult)) -pushIQ' iqID to tp lang body con = do - pushStanza (IQRequestS $ IQRequest iqID Nothing to lang tp body) con - res <- pullStanza con - case res of - Left e -> return $ Left e - Right (IQErrorS e) -> return $ Right $ Left e - Right (IQResultS r) -> do - unless - (iqID == iqResultID r) . liftIO . Ex.throwIO $ - XmppOtherFailure - -- TODO: Log: ("In sendIQ' IDs don't match: " ++ show iqID ++ - -- " /= " ++ show (iqResultID r) ++ " .") - return $ Right $ Right r - _ -> return $ Left XmppOtherFailure - -- TODO: Log: "sendIQ': unexpected stanza type " - --- | Send "" and wait for the server to finish processing and to --- close the connection. Any remaining elements from the server are returned. --- Surpresses StreamEndFailure exceptions, but may throw a StreamCloseError. -closeStreams :: TMVar Connection -> IO (Either XmppFailure [Element]) -closeStreams = withConnection $ do - send <- gets (cSend . cHandle) - cc <- gets (cClose . cHandle) - liftIO $ send "" - void $ liftIO $ forkIO $ do - threadDelay 3000000 -- TODO: Configurable value - (Ex.try cc) :: IO (Either Ex.SomeException ()) - return () - collectElems [] - where - -- Pulls elements from the stream until the stream ends, or an error is - -- raised. - collectElems :: [Element] -> StateT Connection IO (Either XmppFailure [Element]) - collectElems es = do - result <- pullElement - case result of - Left StreamEndFailure -> return $ Right es - Left e -> return $ Left $ StreamCloseError (es, e) - Right e -> collectElems (e:es) - -debugConduit :: Pipe l ByteString ByteString u IO b -debugConduit = forever $ do - s' <- await - case s' of - Just s -> do - liftIO $ BS.putStrLn (BS.append "in: " s) - yield s - Nothing -> return () diff --git a/source/Network/Xmpp/IM/Message.hs b/source/Network/Xmpp/IM/Message.hs index fe43744..b9af8f4 100644 --- a/source/Network/Xmpp/IM/Message.hs +++ b/source/Network/Xmpp/IM/Message.hs @@ -11,8 +11,8 @@ import Data.Text (Text) import Data.XML.Pickle import Data.XML.Types +import Network.Xmpp.Marshal import Network.Xmpp.Types -import Network.Xmpp.Pickle data MessageBody = MessageBody { bodyLang :: (Maybe LangTag) , bodyContent :: Text diff --git a/source/Network/Xmpp/Internal.hs b/source/Network/Xmpp/Internal.hs new file mode 100644 index 0000000..d921bc4 --- /dev/null +++ b/source/Network/Xmpp/Internal.hs @@ -0,0 +1,39 @@ +-- | +-- Module: $Header$ +-- +-- Maintainer: info@jonkri.com +-- Stability: unstable +-- Portability: portable +-- +-- This module allows for low-level access to Pontarius XMPP. Generally, the +-- "Network.Xmpp" module should be used instead. +-- +-- The 'Connection' object provides the most low-level access to the XMPP +-- stream: a simple and single-threaded interface which exposes the conduit +-- 'Event' source, as well as the input and output byte streams. Custom stateful +-- 'Connection' functions can be executed using 'withConnection'. +-- +-- The TLS, SASL, and 'Session' functionalities of Pontarius XMPP are built on +-- top of this API. + +module Network.Xmpp.Internal + ( Connection(..) + , ConnectionState(..) + , ConnectionHandle(..) + , ServerFeatures(..) + , connect + , withConnection + , startTls + , simpleAuth + , auth + , pushStanza + , pullStanza + ) + + where + +import Network.Xmpp.Connection +import Network.Xmpp.Sasl +import Network.Xmpp.Tls +import Network.Xmpp.Types +import Network.Xmpp.Stream diff --git a/source/Network/Xmpp/Jid.hs b/source/Network/Xmpp/Jid.hs deleted file mode 100644 index bb80884..0000000 --- a/source/Network/Xmpp/Jid.hs +++ /dev/null @@ -1,205 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} - --- This module deals with JIDs, also known as XMPP addresses. For more --- information on JIDs, see RFC 6122: XMPP: Address Format. - -module Network.Xmpp.Jid - ( Jid(..) - , fromText - , fromStrings - , isBare - , isFull - ) where - -import Control.Applicative ((<$>),(<|>)) -import Control.Monad(guard) - -import qualified Data.Attoparsec.Text as AP -import Data.Maybe(fromJust) -import qualified Data.Set as Set -import Data.String (IsString(..)) -import Data.Text (Text) -import qualified Data.Text as Text -import qualified Text.NamePrep as SP -import qualified Text.StringPrep as SP - --- | A JID is XMPP\'s native format for addressing entities in the network. It --- is somewhat similar to an e-mail address but contains three parts instead of --- two. -data Jid = Jid { -- | The @localpart@ of a JID is an optional identifier placed - -- before the domainpart and separated from the latter by a - -- \'\@\' character. Typically a localpart uniquely identifies - -- the entity requesting and using network access provided by a - -- server (i.e., a local account), although it can also - -- represent other kinds of entities (e.g., a chat room - -- associated with a multi-user chat service). The entity - -- represented by an XMPP localpart is addressed within the - -- context of a specific domain (i.e., - -- @localpart\@domainpart@). - localpart :: !(Maybe Text) - - -- | The domainpart typically identifies the /home/ server to - -- which clients connect for XML routing and data management - -- functionality. However, it is not necessary for an XMPP - -- domainpart to identify an entity that provides core XMPP - -- server functionality (e.g., a domainpart can identify an - -- entity such as a multi-user chat service, a - -- publish-subscribe service, or a user directory). - , domainpart :: !Text - - -- | The resourcepart of a JID is an optional identifier placed - -- after the domainpart and separated from the latter by the - -- \'\/\' character. A resourcepart can modify either a - -- @localpart\@domainpart@ address or a mere @domainpart@ - -- address. Typically a resourcepart uniquely identifies a - -- specific connection (e.g., a device or location) or object - -- (e.g., an occupant in a multi-user chat room) belonging to - -- the entity associated with an XMPP localpart at a domain - -- (i.e., @localpart\@domainpart/resourcepart@). - , resourcepart :: !(Maybe Text) - } deriving Eq - -instance Show Jid where - show (Jid nd dmn res) = - maybe "" ((++ "@") . Text.unpack) nd ++ Text.unpack dmn ++ - maybe "" (('/' :) . Text.unpack) res - -instance Read Jid where - readsPrec _ x = case fromText (Text.pack x) of - Nothing -> [] - Just j -> [(j,"")] - -instance IsString Jid where - fromString = fromJust . fromText . Text.pack - --- | Converts a Text to a JID. -fromText :: Text -> Maybe Jid -fromText t = do - (l, d, r) <- eitherToMaybe $ AP.parseOnly jidParts t - fromStrings l d r - where - eitherToMaybe = either (const Nothing) Just - --- | Converts localpart, domainpart, and resourcepart strings to a JID. Runs the --- appropriate stringprep profiles and validates the parts. -fromStrings :: Maybe Text -> Text -> Maybe Text -> Maybe Jid -fromStrings l d r = do - localPart <- case l of - Nothing -> return Nothing - Just l'-> do - l'' <- SP.runStringPrep nodeprepProfile l' - guard $ validPartLength l'' - let prohibMap = Set.fromList nodeprepExtraProhibitedCharacters - guard $ Text.all (`Set.notMember` prohibMap) l'' - return $ Just l'' - domainPart <- SP.runStringPrep (SP.namePrepProfile False) d - guard $ validDomainPart domainPart - resourcePart <- case r of - Nothing -> return Nothing - Just r' -> do - r'' <- SP.runStringPrep resourceprepProfile r' - guard $ validPartLength r'' - return $ Just r'' - return $ Jid localPart domainPart resourcePart - where - validDomainPart :: Text -> Bool - validDomainPart _s = True -- TODO - - validPartLength :: Text -> Bool - validPartLength p = Text.length p > 0 && Text.length p < 1024 - --- | Returns 'True' if the JID is /bare/, and 'False' otherwise. -isBare :: Jid -> Bool -isBare j | resourcepart j == Nothing = True - | otherwise = False - --- | Returns 'True' if the JID is /full/, and 'False' otherwise. -isFull :: Jid -> Bool -isFull = not . isBare - --- Parses an JID string and returns its three parts. It performs no validation --- or transformations. -jidParts :: AP.Parser (Maybe Text, Text, Maybe Text) -jidParts = do - -- Read until we reach an '@', a '/', or EOF. - a <- AP.takeWhile1 (AP.notInClass ['@', '/']) - -- Case 1: We found an '@', and thus the localpart. At least the domainpart - -- is remaining. Read the '@' and until a '/' or EOF. - do - b <- domainPartP - -- Case 1A: We found a '/' and thus have all the JID parts. Read the '/' - -- and until EOF. - do - c <- resourcePartP -- Parse resourcepart - return (Just a, b, Just c) - -- Case 1B: We have reached EOF; the JID is in the form - -- localpart@domainpart. - <|> do - AP.endOfInput - return (Just a, b, Nothing) - -- Case 2: We found a '/'; the JID is in the form - -- domainpart/resourcepart. - <|> do - b <- resourcePartP - AP.endOfInput - return (Nothing, a, Just b) - -- Case 3: We have reached EOF; we have an JID consisting of only a - -- domainpart. - <|> do - AP.endOfInput - return (Nothing, a, Nothing) - where - -- Read an '@' and everything until a '/'. - domainPartP :: AP.Parser Text - domainPartP = do - _ <- AP.char '@' - AP.takeWhile1 (/= '/') - -- Read everything until a '/'. - resourcePartP :: AP.Parser Text - resourcePartP = do - _ <- AP.char '/' - AP.takeText - --- The `nodeprep' StringPrep profile. -nodeprepProfile :: SP.StringPrepProfile -nodeprepProfile = SP.Profile { SP.maps = [SP.b1, SP.b2] - , SP.shouldNormalize = True - , SP.prohibited = [SP.a1 - , SP.c11 - , SP.c12 - , SP.c21 - , SP.c22 - , SP.c3 - , SP.c4 - , SP.c5 - , SP.c6 - , SP.c7 - , SP.c8 - , SP.c9 - ] - , SP.shouldCheckBidi = True - } - --- These characters needs to be checked for after normalization. -nodeprepExtraProhibitedCharacters :: [Char] -nodeprepExtraProhibitedCharacters = ['\x22', '\x26', '\x27', '\x2F', '\x3A', - '\x3C', '\x3E', '\x40'] - --- The `resourceprep' StringPrep profile. -resourceprepProfile :: SP.StringPrepProfile -resourceprepProfile = SP.Profile { SP.maps = [SP.b1] - , SP.shouldNormalize = True - , SP.prohibited = [ SP.a1 - , SP.c12 - , SP.c21 - , SP.c22 - , SP.c3 - , SP.c4 - , SP.c5 - , SP.c6 - , SP.c7 - , SP.c8 - , SP.c9 - ] - , SP.shouldCheckBidi = True - } \ No newline at end of file diff --git a/source/Network/Xmpp/Marshal.hs b/source/Network/Xmpp/Marshal.hs index bf5e5fa..38e9b0e 100644 --- a/source/Network/Xmpp/Marshal.hs +++ b/source/Network/Xmpp/Marshal.hs @@ -11,7 +11,6 @@ module Network.Xmpp.Marshal where import Data.XML.Pickle import Data.XML.Types -import Network.Xmpp.Pickle import Network.Xmpp.Types xpStreamStanza :: PU [Node] (Either StreamErrorInfo Stanza) @@ -207,3 +206,35 @@ xpStreamError = xpWrap (xpOption xpElemVerbatim) -- Application specific error conditions ) ) + +xpLangTag :: PU [Attribute] (Maybe LangTag) +xpLangTag = xpAttrImplied xmlLang xpPrim + +xmlLang :: Name +xmlLang = Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml") + +-- Given a pickler and an object, produces an Element. +pickleElem :: PU [Node] a -> a -> Element +pickleElem p = pickle $ xpNodeElem p + +-- Given a pickler and an element, produces an object. +unpickleElem :: PU [Node] a -> Element -> Either UnpickleError a +unpickleElem p x = unpickle (xpNodeElem p) x + +xpNodeElem :: PU [Node] a -> PU Element a +xpNodeElem xp = PU { pickleTree = \x -> head $ (pickleTree xp x) >>= \y -> + case y of + NodeElement e -> [e] + _ -> [] + , unpickleTree = \x -> case unpickleTree xp $ [NodeElement x] of + Left l -> Left l + Right (a,(_,c)) -> Right (a,(Nothing,c)) + } + +mbl :: Maybe [a] -> [a] +mbl (Just l) = l +mbl Nothing = [] + +lmb :: [t] -> Maybe [t] +lmb [] = Nothing +lmb x = Just x diff --git a/source/Network/Xmpp/Message.hs b/source/Network/Xmpp/Message.hs deleted file mode 100644 index 875421f..0000000 --- a/source/Network/Xmpp/Message.hs +++ /dev/null @@ -1,36 +0,0 @@ -{-# LANGUAGE RecordWildCards #-} -{-# OPTIONS_HADDOCK hide #-} - -module Network.Xmpp.Message - ( Message(..) - , MessageError(..) - , MessageType(..) - , answerMessage - , message - ) where - -import Data.XML.Types - -import Network.Xmpp.Types - --- | An empty message. -message :: Message -message = Message { messageID = Nothing - , messageFrom = Nothing - , messageTo = Nothing - , messageLangTag = Nothing - , messageType = Normal - , messagePayload = [] - } - --- Produce an answer message with the given payload, switching the "from" and --- "to" attributes in the original message. -answerMessage :: Message -> [Element] -> Maybe Message -answerMessage Message{messageFrom = Just frm, ..} payload = - Just Message{ messageFrom = messageTo - , messageID = Nothing - , messageTo = Just frm - , messagePayload = payload - , .. - } -answerMessage _ _ = Nothing \ No newline at end of file diff --git a/source/Network/Xmpp/Pickle.hs b/source/Network/Xmpp/Pickle.hs deleted file mode 100644 index 10b72a9..0000000 --- a/source/Network/Xmpp/Pickle.hs +++ /dev/null @@ -1,78 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} - -{-# LANGUAGE NoMonomorphismRestriction #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TupleSections #-} - --- Marshalling between XML and Native Types - - -module Network.Xmpp.Pickle - ( mbToBool - , xmlLang - , xpLangTag - , xpNodeElem - , ignoreAttrs - , mbl - , lmb - , right - , unpickleElem' - , unpickleElem - , pickleElem - , ppElement - ) where - -import Data.XML.Types -import Data.XML.Pickle - -import Network.Xmpp.Types - -import Text.Xml.Stream.Elements - -mbToBool :: Maybe t -> Bool -mbToBool (Just _) = True -mbToBool _ = False - -xmlLang :: Name -xmlLang = Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml") - -xpLangTag :: PU [Attribute] (Maybe LangTag) -xpLangTag = xpAttrImplied xmlLang xpPrim - -xpNodeElem :: PU [Node] a -> PU Element a -xpNodeElem xp = PU { pickleTree = \x -> head $ (pickleTree xp x) >>= \y -> - case y of - NodeElement e -> [e] - _ -> [] - , unpickleTree = \x -> case unpickleTree xp $ [NodeElement x] of - Left l -> Left l - Right (a,(_,c)) -> Right (a,(Nothing,c)) - } - -ignoreAttrs :: PU t ((), b) -> PU t b -ignoreAttrs = xpWrap snd ((),) - -mbl :: Maybe [a] -> [a] -mbl (Just l) = l -mbl Nothing = [] - -lmb :: [t] -> Maybe [t] -lmb [] = Nothing -lmb x = Just x - -right :: Either [Char] t -> t -right (Left l) = error l -right (Right r) = r - -unpickleElem' :: PU [Node] c -> Element -> c -unpickleElem' p x = case unpickle (xpNodeElem p) x of - Left l -> error $ (show l) ++ "\n saw: " ++ ppElement x - Right r -> r - --- Given a pickler and an element, produces an object. -unpickleElem :: PU [Node] a -> Element -> Either UnpickleError a -unpickleElem p x = unpickle (xpNodeElem p) x - --- Given a pickler and an object, produces an Element. -pickleElem :: PU [Node] a -> a -> Element -pickleElem p = pickle $ xpNodeElem p diff --git a/source/Network/Xmpp/Presence.hs b/source/Network/Xmpp/Presence.hs deleted file mode 100644 index c859f14..0000000 --- a/source/Network/Xmpp/Presence.hs +++ /dev/null @@ -1,10 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} - -module Network.Xmpp.Presence where - -import Data.Text(Text) -import Network.Xmpp.Types - --- | Add a recipient to a presence notification. -presTo :: Presence -> Jid -> Presence -presTo pres to = pres{presenceTo = Just to} \ No newline at end of file diff --git a/source/Network/Xmpp/Sasl.hs b/source/Network/Xmpp/Sasl.hs index 2a61ae2..e272557 100644 --- a/source/Network/Xmpp/Sasl.hs +++ b/source/Network/Xmpp/Sasl.hs @@ -1,11 +1,18 @@ {-# 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 + , simpleAuth ) where import Control.Applicative @@ -29,7 +36,7 @@ import qualified Data.Text as Text import Data.Text (Text) import qualified Data.Text.Encoding as Text -import Network.Xmpp.Connection_ +import Network.Xmpp.Connection import Network.Xmpp.Stream import Network.Xmpp.Types @@ -40,6 +47,20 @@ 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 @@ -65,3 +86,90 @@ xmppSasl handlers = withConnection $ do Right a -> do _ <- runErrorT $ ErrorT restartStream return $ Right $ Nothing + +-- | Authenticate to the server using the first matching method and bind a +-- resource. +auth :: [SaslHandler] + -> Maybe Text + -> TMVar Connection + -> IO (Either XmppFailure (Maybe AuthFailure)) +auth mechanisms resource con = runErrorT $ do + ErrorT $ xmppSasl mechanisms con + jid <- lift $ xmppBind resource con + lift $ startSession con + return Nothing + +-- | Authenticate to the server with the given username and password +-- and bind a resource. +-- +-- Prefers SCRAM-SHA1 over DIGEST-MD5. +simpleAuth :: Text.Text -- ^ The username + -> Text.Text -- ^ The password + -> Maybe Text -- ^ The desired resource or 'Nothing' to let the + -- server assign one + -> TMVar Connection + -> IO (Either XmppFailure (Maybe AuthFailure)) +simpleAuth username passwd resource = flip auth resource $ + [ -- TODO: scramSha1Plus + scramSha1 username Nothing passwd + , digestMd5 username Nothing passwd + ] + +-- 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 -> TMVar Connection -> IO (Either XmppFailure Jid) +xmppBind rsrc c = runErrorT $ do + answer <- ErrorT $ pushIQ' "bind" Nothing Set Nothing (bindBody rsrc) c + case answer of + Right IQResult{iqResultPayload = Just b} -> do + let jid = unpickleElem xpJid b + case jid of + Right jid' -> do + ErrorT $ withConnection (do + modify $ \s -> s{cJid = Just jid'} + return $ Right jid') c -- not pretty + return jid' + otherwise -> throwError XmppOtherFailure + -- TODO: Log: ("Bind couldn't unpickle JID from " ++ show answer) + otherwise -> 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 :: TMVar Connection -> IO () +startSession con = do + answer <- pushIQ' "session" Nothing Set Nothing sessionXml con + case answer of + Left e -> error $ show e + Right _ -> return () diff --git a/source/Network/Xmpp/Sasl/Common.hs b/source/Network/Xmpp/Sasl/Common.hs index e3dcc5c..c38c843 100644 --- a/source/Network/Xmpp/Sasl/Common.hs +++ b/source/Network/Xmpp/Sasl/Common.hs @@ -22,10 +22,10 @@ import Data.Word (Word8) import Data.XML.Pickle import Data.XML.Types -import Network.Xmpp.Connection_ -import Network.Xmpp.Pickle +import Network.Xmpp.Connection import Network.Xmpp.Sasl.StringPrep import Network.Xmpp.Sasl.Types +import Network.Xmpp.Marshal import qualified System.Random as Random diff --git a/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs b/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs index f8fc03c..3cab7e4 100644 --- a/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs +++ b/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs @@ -31,8 +31,7 @@ import qualified Data.ByteString as BS import Data.XML.Types -import Network.Xmpp.Connection_ -import Network.Xmpp.Pickle +import Network.Xmpp.Connection import Network.Xmpp.Stream import Network.Xmpp.Types import Network.Xmpp.Sasl.Common diff --git a/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs b/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs index 6f1626e..1ca91fe 100644 --- a/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs +++ b/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs @@ -35,10 +35,9 @@ import qualified Data.ByteString as BS import Data.XML.Types -import Network.Xmpp.Connection_ +import Network.Xmpp.Connection import Network.Xmpp.Stream import Network.Xmpp.Types -import Network.Xmpp.Pickle import qualified System.Random as Random diff --git a/source/Network/Xmpp/Session.hs b/source/Network/Xmpp/Session.hs deleted file mode 100644 index 67cf882..0000000 --- a/source/Network/Xmpp/Session.hs +++ /dev/null @@ -1,116 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} -{-# LANGUAGE OverloadedStrings #-} -module Network.Xmpp.Session where - -import qualified Control.Exception as Ex -import Control.Monad.Error -import Data.Text as Text -import Data.XML.Pickle -import Data.XML.Types(Element) -import Network -import qualified Network.TLS as TLS -import Network.Xmpp.Bind -import Network.Xmpp.Concurrent.Types -import Network.Xmpp.Concurrent -import Network.Xmpp.Connection_ -import Network.Xmpp.Marshal -import Network.Xmpp.Pickle -import Network.Xmpp.Sasl -import Network.Xmpp.Sasl.Mechanisms -import Network.Xmpp.Sasl.Types -import Network.Xmpp.Stream -import Network.Xmpp.Tls -import Network.Xmpp.Types -import Control.Concurrent.STM.TMVar -import Data.Maybe - --- | Creates a 'Session' object by setting up a connection with an XMPP server. --- --- Will connect to the specified host. If the fourth parameters is a 'Just' --- value, @session@ will attempt to secure the connection with TLS. If the fifth --- parameters is a 'Just' value, @session@ will attempt to authenticate and --- acquire an XMPP resource. -session :: HostName -- ^ Host to connect to - -> Text -- ^ The realm host name (to - -- distinguish the XMPP service) - -> PortID -- ^ Port to connect to - -> Maybe TLS.TLSParams -- ^ TLS settings, if securing the - -- connection to the server is - -- desired - -> Maybe ([SaslHandler], Maybe Text) -- ^ SASL handlers and the desired - -- JID resource (or Nothing to let - -- the server decide) - -> IO (Either XmppFailure (Session, Maybe AuthFailure)) -session hostname realm port tls sasl = runErrorT $ do - con <- ErrorT $ connect hostname port realm - if isJust tls - then ErrorT $ startTls (fromJust tls) con - else return () - aut <- if isJust sasl - then ErrorT $ auth (fst $ fromJust sasl) (snd $ fromJust sasl) con - else return Nothing - ses <- ErrorT $ newSession con - return (ses, aut) - --- | Connects to the XMPP server and opens the XMPP stream against the given --- host name, port, and realm. -connect :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Connection)) -connect address port hostname = do - con <- connectTcp address port hostname - case con of - Right con' -> do - result <- withConnection startStream con' - return $ Right con' - Left e -> do - return $ Left e - -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 :: TMVar Connection -> IO () -startSession con = do - answer <- pushIQ' "session" Nothing Set Nothing sessionXml con - case answer of - Left e -> error $ show e - Right _ -> return () - --- | Authenticate to the server using the first matching method and bind a --- resource. -auth :: [SaslHandler] - -> Maybe Text - -> TMVar Connection - -> IO (Either XmppFailure (Maybe AuthFailure)) -auth mechanisms resource con = runErrorT $ do - ErrorT $ xmppSasl mechanisms con - jid <- lift $ xmppBind resource con - lift $ startSession con - return Nothing - --- | Authenticate to the server with the given username and password --- and bind a resource. --- --- Prefers SCRAM-SHA1 over DIGEST-MD5. -simpleAuth :: Text.Text -- ^ The username - -> Text.Text -- ^ The password - -> Maybe Text -- ^ The desired resource or 'Nothing' to let the - -- server assign one - -> TMVar Connection - -> IO (Either XmppFailure (Maybe AuthFailure)) -simpleAuth username passwd resource = flip auth resource $ - [ -- TODO: scramSha1Plus - scramSha1 username Nothing passwd - , digestMd5 username Nothing passwd - ] diff --git a/source/Network/Xmpp/Stream.hs b/source/Network/Xmpp/Stream.hs index a4ce39e..1b3d10c 100644 --- a/source/Network/Xmpp/Stream.hs +++ b/source/Network/Xmpp/Stream.hs @@ -20,14 +20,16 @@ import Data.Void (Void) import Data.XML.Pickle import Data.XML.Types -import Network.Xmpp.Connection_ -import Network.Xmpp.Pickle +import Network.Xmpp.Connection import Network.Xmpp.Types import Network.Xmpp.Marshal import Text.Xml.Stream.Elements import Text.XML.Stream.Parse as XP +import Network +import Control.Concurrent.STM + -- import Text.XML.Stream.Elements -- Unpickles and returns a stream element. @@ -246,3 +248,15 @@ xpStreamFeatures = xpWrap "{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms" (xpAll $ xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}mechanism" (xpContent xpId)) + +-- | Connects to the XMPP server and opens the XMPP stream against the given +-- host name, port, and realm. +connect :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Connection)) +connect address port hostname = do + con <- connectTcp address port hostname + case con of + Right con' -> do + result <- withConnection startStream con' + return $ Right con' + Left e -> do + return $ Left e diff --git a/source/Network/Xmpp/Tls.hs b/source/Network/Xmpp/Tls.hs index 0d5754e..e8c3c87 100644 --- a/source/Network/Xmpp/Tls.hs +++ b/source/Network/Xmpp/Tls.hs @@ -17,7 +17,7 @@ import Data.Conduit.Tls as TLS import Data.Typeable import Data.XML.Types -import Network.Xmpp.Connection_ +import Network.Xmpp.Connection import Network.Xmpp.Stream import Network.Xmpp.Types diff --git a/source/Network/Xmpp/Types.hs b/source/Network/Xmpp/Types.hs index 86dd602..13c6d5e 100644 --- a/source/Network/Xmpp/Types.hs +++ b/source/Network/Xmpp/Types.hs @@ -39,11 +39,13 @@ module Network.Xmpp.Types , ConnectionState(..) , StreamErrorInfo(..) , langTag - , module Network.Xmpp.Jid + , Jid(..) + , isBare + , isFull + , fromString ) where -import Control.Applicative ((<$>), many) import Control.Concurrent.STM import Control.Exception import Control.Monad.Error @@ -65,10 +67,16 @@ import qualified Network.TLS as TLS import qualified Network as N -import Network.Xmpp.Jid - import System.IO +import Control.Applicative ((<$>), (<|>), many) +import Control.Monad(guard) + +import qualified Data.Set as Set +import Data.String (IsString(..)) +import qualified Text.NamePrep as SP +import qualified Text.StringPrep as SP + -- | -- Wraps a string of random characters that, when using an appropriate -- @IDGenerator@, is guaranteed to be unique for the Xmpp session. @@ -815,3 +823,188 @@ withConnection' action con = do mkConnection :: Connection -> IO (TMVar Connection) mkConnection con = {- Connection `fmap` -} (atomically $ newTMVar con) + +--------------- +-- JID +--------------- + +-- | A JID is XMPP\'s native format for addressing entities in the network. It +-- is somewhat similar to an e-mail address but contains three parts instead of +-- two. +data Jid = Jid { -- | The @localpart@ of a JID is an optional identifier placed + -- before the domainpart and separated from the latter by a + -- \'\@\' character. Typically a localpart uniquely identifies + -- the entity requesting and using network access provided by a + -- server (i.e., a local account), although it can also + -- represent other kinds of entities (e.g., a chat room + -- associated with a multi-user chat service). The entity + -- represented by an XMPP localpart is addressed within the + -- context of a specific domain (i.e., + -- @localpart\@domainpart@). + localpart :: !(Maybe Text) + + -- | The domainpart typically identifies the /home/ server to + -- which clients connect for XML routing and data management + -- functionality. However, it is not necessary for an XMPP + -- domainpart to identify an entity that provides core XMPP + -- server functionality (e.g., a domainpart can identify an + -- entity such as a multi-user chat service, a + -- publish-subscribe service, or a user directory). + , domainpart :: !Text + + -- | The resourcepart of a JID is an optional identifier placed + -- after the domainpart and separated from the latter by the + -- \'\/\' character. A resourcepart can modify either a + -- @localpart\@domainpart@ address or a mere @domainpart@ + -- address. Typically a resourcepart uniquely identifies a + -- specific connection (e.g., a device or location) or object + -- (e.g., an occupant in a multi-user chat room) belonging to + -- the entity associated with an XMPP localpart at a domain + -- (i.e., @localpart\@domainpart/resourcepart@). + , resourcepart :: !(Maybe Text) + } deriving Eq + +instance Show Jid where + show (Jid nd dmn res) = + maybe "" ((++ "@") . Text.unpack) nd ++ Text.unpack dmn ++ + maybe "" (('/' :) . Text.unpack) res + +instance Read Jid where + readsPrec _ x = case fromText (Text.pack x) of + Nothing -> [] + Just j -> [(j,"")] + +instance IsString Jid where + fromString = fromJust . fromText . Text.pack + +-- | Converts a Text to a JID. +fromText :: Text -> Maybe Jid +fromText t = do + (l, d, r) <- eitherToMaybe $ AP.parseOnly jidParts t + fromStrings l d r + where + eitherToMaybe = either (const Nothing) Just + +-- | Converts localpart, domainpart, and resourcepart strings to a JID. Runs the +-- appropriate stringprep profiles and validates the parts. +fromStrings :: Maybe Text -> Text -> Maybe Text -> Maybe Jid +fromStrings l d r = do + localPart <- case l of + Nothing -> return Nothing + Just l'-> do + l'' <- SP.runStringPrep nodeprepProfile l' + guard $ validPartLength l'' + let prohibMap = Set.fromList nodeprepExtraProhibitedCharacters + guard $ Text.all (`Set.notMember` prohibMap) l'' + return $ Just l'' + domainPart <- SP.runStringPrep (SP.namePrepProfile False) d + guard $ validDomainPart domainPart + resourcePart <- case r of + Nothing -> return Nothing + Just r' -> do + r'' <- SP.runStringPrep resourceprepProfile r' + guard $ validPartLength r'' + return $ Just r'' + return $ Jid localPart domainPart resourcePart + where + validDomainPart :: Text -> Bool + validDomainPart _s = True -- TODO + + validPartLength :: Text -> Bool + validPartLength p = Text.length p > 0 && Text.length p < 1024 + +-- | Returns 'True' if the JID is /bare/, and 'False' otherwise. +isBare :: Jid -> Bool +isBare j | resourcepart j == Nothing = True + | otherwise = False + +-- | Returns 'True' if the JID is /full/, and 'False' otherwise. +isFull :: Jid -> Bool +isFull = not . isBare + +-- Parses an JID string and returns its three parts. It performs no validation +-- or transformations. +jidParts :: AP.Parser (Maybe Text, Text, Maybe Text) +jidParts = do + -- Read until we reach an '@', a '/', or EOF. + a <- AP.takeWhile1 (AP.notInClass ['@', '/']) + -- Case 1: We found an '@', and thus the localpart. At least the domainpart + -- is remaining. Read the '@' and until a '/' or EOF. + do + b <- domainPartP + -- Case 1A: We found a '/' and thus have all the JID parts. Read the '/' + -- and until EOF. + do + c <- resourcePartP -- Parse resourcepart + return (Just a, b, Just c) + -- Case 1B: We have reached EOF; the JID is in the form + -- localpart@domainpart. + <|> do + AP.endOfInput + return (Just a, b, Nothing) + -- Case 2: We found a '/'; the JID is in the form + -- domainpart/resourcepart. + <|> do + b <- resourcePartP + AP.endOfInput + return (Nothing, a, Just b) + -- Case 3: We have reached EOF; we have an JID consisting of only a + -- domainpart. + <|> do + AP.endOfInput + return (Nothing, a, Nothing) + where + -- Read an '@' and everything until a '/'. + domainPartP :: AP.Parser Text + domainPartP = do + _ <- AP.char '@' + AP.takeWhile1 (/= '/') + -- Read everything until a '/'. + resourcePartP :: AP.Parser Text + resourcePartP = do + _ <- AP.char '/' + AP.takeText + +-- The `nodeprep' StringPrep profile. +nodeprepProfile :: SP.StringPrepProfile +nodeprepProfile = SP.Profile { SP.maps = [SP.b1, SP.b2] + , SP.shouldNormalize = True + , SP.prohibited = [SP.a1 + , SP.c11 + , SP.c12 + , SP.c21 + , SP.c22 + , SP.c3 + , SP.c4 + , SP.c5 + , SP.c6 + , SP.c7 + , SP.c8 + , SP.c9 + ] + , SP.shouldCheckBidi = True + } + +-- These characters needs to be checked for after normalization. +nodeprepExtraProhibitedCharacters :: [Char] +nodeprepExtraProhibitedCharacters = ['\x22', '\x26', '\x27', '\x2F', '\x3A', + '\x3C', '\x3E', '\x40'] + +-- The `resourceprep' StringPrep profile. +resourceprepProfile :: SP.StringPrepProfile +resourceprepProfile = SP.Profile { SP.maps = [SP.b1] + , SP.shouldNormalize = True + , SP.prohibited = [ SP.a1 + , SP.c12 + , SP.c21 + , SP.c22 + , SP.c3 + , SP.c4 + , SP.c5 + , SP.c6 + , SP.c7 + , SP.c8 + , SP.c9 + ] + , SP.shouldCheckBidi = True + } diff --git a/source/Network/Xmpp/Utilities.hs b/source/Network/Xmpp/Utilities.hs index 11441a8..3eaa793 100644 --- a/source/Network/Xmpp/Utilities.hs +++ b/source/Network/Xmpp/Utilities.hs @@ -1,8 +1,9 @@ -{-# OPTIONS_HADDOCK hide #-} - {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +{-# OPTIONS_HADDOCK hide #-} -module Network.Xmpp.Utilities (idGenerator) where +module Network.Xmpp.Utilities (idGenerator, presTo, message, answerMessage) where import Network.Xmpp.Types @@ -10,6 +11,8 @@ import Control.Monad.STM import Control.Concurrent.STM.TVar import Prelude +import Data.XML.Types + import qualified Data.Attoparsec.Text as AP import qualified Data.Text as Text @@ -52,3 +55,29 @@ idGenerator prefix = atomically $ do -- Constructs a "Version" based on the major and minor version numbers. versionFromNumbers :: Integer -> Integer -> Version versionFromNumbers major minor = Version major minor + +-- | Add a recipient to a presence notification. +presTo :: Presence -> Jid -> Presence +presTo pres to = pres{presenceTo = Just to} + +-- | An empty message. +message :: Message +message = Message { messageID = Nothing + , messageFrom = Nothing + , messageTo = Nothing + , messageLangTag = Nothing + , messageType = Normal + , messagePayload = [] + } + +-- Produce an answer message with the given payload, switching the "from" and +-- "to" attributes in the original message. +answerMessage :: Message -> [Element] -> Maybe Message +answerMessage Message{messageFrom = Just frm, ..} payload = + Just Message{ messageFrom = messageTo + , messageID = Nothing + , messageTo = Just frm + , messagePayload = payload + , .. + } +answerMessage _ _ = Nothing diff --git a/source/Network/Xmpp/Xep/InbandRegistration.hs b/source/Network/Xmpp/Xep/InbandRegistration.hs index a2f6fe4..6e14447 100644 --- a/source/Network/Xmpp/Xep/InbandRegistration.hs +++ b/source/Network/Xmpp/Xep/InbandRegistration.hs @@ -19,7 +19,7 @@ import qualified Data.Text as Text import Data.XML.Pickle import qualified Data.XML.Types as XML -import Network.Xmpp.Connection_ +import Network.Xmpp.Connection import Network.Xmpp.Pickle import Network.Xmpp.Types import Network.Xmpp.Xep.ServiceDiscovery diff --git a/source/Network/Xmpp/Xep/ServiceDiscovery.hs b/source/Network/Xmpp/Xep/ServiceDiscovery.hs index d5325e0..2b695c4 100644 --- a/source/Network/Xmpp/Xep/ServiceDiscovery.hs +++ b/source/Network/Xmpp/Xep/ServiceDiscovery.hs @@ -27,8 +27,8 @@ import Data.XML.Types import Network.Xmpp import Network.Xmpp.Concurrent import Network.Xmpp.Concurrent.Types -import Network.Xmpp.Connection_ -import Network.Xmpp.Pickle +import Network.Xmpp.Connection +import Network.Xmpp.Marshal import Network.Xmpp.Types import Control.Concurrent.STM.TMVar