Browse Source

UNFINISHED Merge remote-tracking branch 'nejla/master'

Conflicts:
	source/Network/Xmpp/Concurrent/Types.hs
	source/Network/Xmpp/Connection_.hs
	source/Network/Xmpp/Internal.hs
	source/Network/Xmpp/Pickle.hs
	source/Network/Xmpp/Session.hs
	source/Network/Xmpp/Stream.hs
	source/Network/Xmpp/Types.hs
	source/Network/Xmpp/Xep/InbandRegistration.hs
master
Philipp Balzarek 13 years ago
parent
commit
aebbabcef1
  1. BIN
      import_visualisation-new-full.png
  2. BIN
      import_visualisation-new.png
  3. 13
      pontarius-xmpp.cabal
  4. 81
      source/Data/Conduit/Tls.hs
  5. 10
      source/Network/Xmpp.hs
  6. 57
      source/Network/Xmpp/Bind.hs
  7. 49
      source/Network/Xmpp/Concurrent.hs
  8. 4
      source/Network/Xmpp/Concurrent/Monad.hs
  9. 14
      source/Network/Xmpp/Concurrent/Threads.hs
  10. 8
      source/Network/Xmpp/Concurrent/Types.hs
  11. 2
      source/Network/Xmpp/IM/Message.hs
  12. 33
      source/Network/Xmpp/Internal.hs
  13. 205
      source/Network/Xmpp/Jid.hs
  14. 73
      source/Network/Xmpp/Marshal.hs
  15. 36
      source/Network/Xmpp/Message.hs
  16. 10
      source/Network/Xmpp/Presence.hs
  17. 102
      source/Network/Xmpp/Sasl.hs
  18. 26
      source/Network/Xmpp/Sasl/Common.hs
  19. 10
      source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs
  20. 4
      source/Network/Xmpp/Sasl/Mechanisms/Plain.hs
  21. 8
      source/Network/Xmpp/Sasl/Mechanisms/Scram.hs
  22. 12
      source/Network/Xmpp/Sasl/Types.hs
  23. 454
      source/Network/Xmpp/Stream.hs
  24. 115
      source/Network/Xmpp/Tls.hs
  25. 345
      source/Network/Xmpp/Types.hs
  26. 82
      source/Network/Xmpp/Utilities.hs
  27. 43
      source/Network/Xmpp/Xep/InbandRegistration.hs
  28. 34
      source/Network/Xmpp/Xep/ServiceDiscovery.hs
  29. 108
      source/Text/Xml/Stream/Elements.hs

BIN
import_visualisation-new-full.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
import_visualisation-new.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

13
pontarius-xmpp.cabal

@ -55,11 +55,9 @@ Library
, stringprep >=0.1.3 , stringprep >=0.1.3
, hslogger >=1.1.0 , hslogger >=1.1.0
Exposed-modules: Network.Xmpp Exposed-modules: Network.Xmpp
, Network.Xmpp.Connection , Network.Xmpp.Internal
, Network.Xmpp.IM , Network.Xmpp.IM
Other-modules: Data.Conduit.Tls Other-modules: Network.Xmpp.Concurrent
, Network.Xmpp.Bind
, Network.Xmpp.Concurrent
, Network.Xmpp.Concurrent.Types , Network.Xmpp.Concurrent.Types
, Network.Xmpp.Concurrent.Basic , Network.Xmpp.Concurrent.Basic
, Network.Xmpp.Concurrent.IQ , Network.Xmpp.Concurrent.IQ
@ -67,14 +65,9 @@ Library
, Network.Xmpp.Concurrent.Presence , Network.Xmpp.Concurrent.Presence
, Network.Xmpp.Concurrent.Threads , Network.Xmpp.Concurrent.Threads
, Network.Xmpp.Concurrent.Monad , Network.Xmpp.Concurrent.Monad
, Network.Xmpp.Connection_
, Network.Xmpp.IM.Message , Network.Xmpp.IM.Message
, Network.Xmpp.IM.Presence , Network.Xmpp.IM.Presence
, Network.Xmpp.Jid
, Network.Xmpp.Marshal , Network.Xmpp.Marshal
, Network.Xmpp.Message
, Network.Xmpp.Pickle
, Network.Xmpp.Presence
, Network.Xmpp.Sasl , Network.Xmpp.Sasl
, Network.Xmpp.Sasl.Common , Network.Xmpp.Sasl.Common
, Network.Xmpp.Sasl.Mechanisms , Network.Xmpp.Sasl.Mechanisms
@ -83,12 +76,10 @@ Library
, Network.Xmpp.Sasl.Mechanisms.Scram , Network.Xmpp.Sasl.Mechanisms.Scram
, Network.Xmpp.Sasl.StringPrep , Network.Xmpp.Sasl.StringPrep
, Network.Xmpp.Sasl.Types , Network.Xmpp.Sasl.Types
, Network.Xmpp.Session
, Network.Xmpp.Stream , Network.Xmpp.Stream
, Network.Xmpp.Tls , Network.Xmpp.Tls
, Network.Xmpp.Types , Network.Xmpp.Types
, Network.Xmpp.Xep.ServiceDiscovery , Network.Xmpp.Xep.ServiceDiscovery
, Text.Xml.Stream.Elements
GHC-Options: -Wall GHC-Options: -Wall
Source-Repository head Source-Repository head

81
source/Data/Conduit/Tls.hs

@ -1,81 +0,0 @@
{-# Language NoMonomorphismRestriction #-}
{-# OPTIONS_HADDOCK hide #-}
module Data.Conduit.Tls
( tlsinit
-- , conduitStdout
, module TLS
, module TLSExtra
)
where
import Control.Monad
import Control.Monad (liftM, when)
import Control.Monad.IO.Class
import Crypto.Random
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Conduit
import qualified Data.Conduit.Binary as CB
import Data.IORef
import Network.TLS as TLS
import Crypto.Random.API
import Network.TLS.Extra as TLSExtra
import System.IO (Handle)
client params gen backend = do
contextNew backend params gen
defaultParams = defaultParamsClient
tlsinit :: (MonadIO m, MonadIO m1) =>
Bool
-> TLSParams
-> Backend
-> m ( Source m1 BS.ByteString
, Sink BS.ByteString m1 ()
, BS.ByteString -> IO ()
, Int -> m1 BS.ByteString
, Context
)
tlsinit debug tlsParams backend = do
when debug . liftIO $ putStrLn "TLS with debug mode enabled"
gen <- liftIO $ getSystemRandomGen -- TODO: Find better random source?
con <- client tlsParams gen backend
handshake con
let src = forever $ do
dt <- liftIO $ recvData con
when debug (liftIO $ putStr "in: " >> BS.putStrLn dt)
yield dt
let snk = do
d <- await
case d of
Nothing -> return ()
Just x -> do
sendData con (BL.fromChunks [x])
when debug (liftIO $ putStr "out: " >> BS.putStrLn x)
snk
read <- liftIO $ mkReadBuffer (recvData con)
return ( src
, snk
, \s -> do
when debug (liftIO $ BS.putStrLn s)
sendData con $ BL.fromChunks [s]
, liftIO . read
, con
)
mkReadBuffer :: IO BS.ByteString -> IO (Int -> IO BS.ByteString)
mkReadBuffer read = do
buffer <- newIORef BS.empty
let read' n = do
nc <- readIORef buffer
bs <- if BS.null nc then read
else return nc
let (result, rest) = BS.splitAt n bs
writeIORef buffer rest
return result
return read'

10
source/Network/Xmpp.hs

@ -18,7 +18,7 @@
-- of XMPP (RFC 6120): setup and teardown of XML streams, channel encryption, -- of XMPP (RFC 6120): setup and teardown of XML streams, channel encryption,
-- authentication, error handling, and communication primitives for messaging. -- 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. -- module.
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} {-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
@ -96,7 +96,7 @@ module Network.Xmpp
, PresenceType(..) , PresenceType(..)
, PresenceError(..) , PresenceError(..)
-- *** Creating -- *** Creating
, module Network.Xmpp.Presence , presTo
-- *** Sending -- *** Sending
-- | Sends a presence stanza. In general, the presence stanza should have no -- | 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 -- 'to' attribute, in which case the server to which the client is connected
@ -145,7 +145,7 @@ module Network.Xmpp
, AuthFailure( AuthXmlFailure -- Does not export AuthStreamFailure , AuthFailure( AuthXmlFailure -- Does not export AuthStreamFailure
, AuthNoAcceptableMechanism , AuthNoAcceptableMechanism
, AuthChallengeFailure , AuthChallengeFailure
, AuthNoConnection , AuthNoStream
, AuthFailure , AuthFailure
, AuthSaslFailure , AuthSaslFailure
, AuthStringPrepFailure ) , AuthStringPrepFailure )
@ -154,10 +154,8 @@ module Network.Xmpp
import Network import Network
import Network.Xmpp.Concurrent import Network.Xmpp.Concurrent
import Network.Xmpp.Message import Network.Xmpp.Utilities
import Network.Xmpp.Presence
import Network.Xmpp.Sasl import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Session
import Network.Xmpp.Tls import Network.Xmpp.Tls
import Network.Xmpp.Types import Network.Xmpp.Types

57
source/Network/Xmpp/Bind.hs

@ -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
-- "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
-- element, with a possible "<resource>[JID]</resource>"
-- 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

49
source/Network/Xmpp/Concurrent.hs

@ -11,6 +11,7 @@ module Network.Xmpp.Concurrent
, toChans , toChans
, newSession , newSession
, writeWorker , writeWorker
, session
) where ) where
import Network.Xmpp.Concurrent.Monad import Network.Xmpp.Concurrent.Monad
@ -31,9 +32,17 @@ import Network.Xmpp.Concurrent.Presence
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Concurrent.Threads import Network.Xmpp.Concurrent.Threads
import Network.Xmpp.Marshal import Network.Xmpp.Marshal
import Network.Xmpp.Pickle
import Network.Xmpp.Types 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 Network.Xmpp.Utilities
import Control.Monad.Error import Control.Monad.Error
@ -74,14 +83,14 @@ toChans stanzaC iqHands sta = atomically $ do
-- | Creates and initializes a new Xmpp context. -- | Creates and initializes a new Xmpp context.
newSession :: TMVar Connection -> IO (Either XmppFailure Session) newSession :: TMVar Stream -> IO (Either XmppFailure Session)
newSession con = runErrorT $ do newSession stream = runErrorT $ do
outC <- lift newTChanIO outC <- lift newTChanIO
stanzaChan <- lift newTChanIO stanzaChan <- lift newTChanIO
iqHandlers <- lift $ newTVarIO (Map.empty, Map.empty) iqHandlers <- lift $ newTVarIO (Map.empty, Map.empty)
eh <- lift $ newTVarIO $ EventHandlers { connectionClosedHandler = \_ -> return () } eh <- lift $ newTVarIO $ EventHandlers { connectionClosedHandler = \_ -> return () }
let stanzaHandler = toChans stanzaChan iqHandlers let stanzaHandler = toChans stanzaChan iqHandlers
(kill, wLock, conState, readerThread) <- ErrorT $ startThreadsWith stanzaHandler eh con (kill, wLock, streamState, readerThread) <- ErrorT $ startThreadsWith stanzaHandler eh stream
writer <- lift $ forkIO $ writeWorker outC wLock writer <- lift $ forkIO $ writeWorker outC wLock
idRef <- lift $ newTVarIO 1 idRef <- lift $ newTVarIO 1
let getId = atomically $ do let getId = atomically $ do
@ -94,7 +103,7 @@ newSession con = runErrorT $ do
, writeRef = wLock , writeRef = wLock
, readerThread = readerThread , readerThread = readerThread
, idGenerator = getId , idGenerator = getId
, conRef = conState , streamRef = streamState
, eventHandlers = eh , eventHandlers = eh
, stopThreads = kill >> killThread writer , stopThreads = kill >> killThread writer
} }
@ -111,3 +120,31 @@ writeWorker stCh writeR = forever $ do
atomically $ unGetTChan stCh next -- If the writing failed, the atomically $ unGetTChan stCh next -- If the writing failed, the
-- connection is dead. -- connection is dead.
threadDelay 250000 -- Avoid free spinning. 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 $ openStream 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)

4
source/Network/Xmpp/Concurrent/Monad.hs

@ -9,7 +9,7 @@ import qualified Control.Exception.Lifted as Ex
import Control.Monad.Reader import Control.Monad.Reader
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection_ import Network.Xmpp.Stream
@ -94,6 +94,6 @@ closeConnection :: Session -> IO ()
closeConnection session = Ex.mask_ $ do closeConnection session = Ex.mask_ $ do
(_send, connection) <- atomically $ liftM2 (,) (_send, connection) <- atomically $ liftM2 (,)
(takeTMVar $ writeRef session) (takeTMVar $ writeRef session)
(takeTMVar $ conRef session) (takeTMVar $ streamRef session)
_ <- closeStreams connection _ <- closeStreams connection
return () return ()

14
source/Network/Xmpp/Concurrent/Threads.hs

@ -16,7 +16,7 @@ import Control.Monad.State.Strict
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection_ import Network.Xmpp.Stream
import Control.Concurrent.STM.TMVar import Control.Concurrent.STM.TMVar
@ -28,7 +28,7 @@ import Control.Monad.Error
-- all listener threads. -- all listener threads.
readWorker :: (Stanza -> IO ()) readWorker :: (Stanza -> IO ())
-> (XmppFailure -> IO ()) -> (XmppFailure -> IO ())
-> TMVar (TMVar Connection) -> TMVar (TMVar Stream)
-> IO a -> IO a
readWorker onStanza onConnectionClosed stateRef = readWorker onStanza onConnectionClosed stateRef =
Ex.mask_ . forever $ do Ex.mask_ . forever $ do
@ -37,8 +37,8 @@ readWorker onStanza onConnectionClosed stateRef =
-- necessarily be interruptible -- necessarily be interruptible
s <- atomically $ do s <- atomically $ do
con <- readTMVar stateRef con <- readTMVar stateRef
state <- cState <$> readTMVar con state <- streamState <$> readTMVar con
when (state == ConnectionClosed) when (state == Closed)
retry retry
return con return con
allowInterrupt allowInterrupt
@ -77,13 +77,13 @@ readWorker onStanza onConnectionClosed stateRef =
-- connection. -- connection.
startThreadsWith :: (Stanza -> IO ()) startThreadsWith :: (Stanza -> IO ())
-> TVar EventHandlers -> TVar EventHandlers
-> TMVar Connection -> TMVar Stream
-> IO (Either XmppFailure (IO (), -> IO (Either XmppFailure (IO (),
TMVar (BS.ByteString -> IO Bool), TMVar (BS.ByteString -> IO Bool),
TMVar (TMVar Connection), TMVar (TMVar Stream),
ThreadId)) ThreadId))
startThreadsWith stanzaHandler eh con = do startThreadsWith stanzaHandler eh con = do
read <- withConnection' (gets $ cSend . cHandle >>= \d -> return $ Right d) con read <- withStream' (gets $ streamSend . streamHandle >>= \d -> return $ Right d) con
case read of case read of
Left e -> return $ Left e Left e -> return $ Left e
Right read' -> do Right read' -> do

8
source/Network/Xmpp/Concurrent/Types.hs

@ -41,10 +41,10 @@ data Session = Session
-- Fields below are from Context. -- Fields below are from Context.
, writeRef :: TMVar (BS.ByteString -> IO Bool) , writeRef :: TMVar (BS.ByteString -> IO Bool)
, readerThread :: ThreadId , readerThread :: ThreadId
, idGenerator :: IO StanzaID , idGenerator :: IO StanzaId
-- | Lock (used by withConnection) to make sure that a maximum of one -- | Lock (used by withStream) to make sure that a maximum of one
-- XmppConMonad action is executed at any given time. -- Stream action is executed at any given time.
, conRef :: TMVar (TMVar Connection) , streamRef :: TMVar (TMVar Stream)
, eventHandlers :: TVar EventHandlers , eventHandlers :: TVar EventHandlers
, stopThreads :: IO () , stopThreads :: IO ()
} }

2
source/Network/Xmpp/IM/Message.hs

@ -11,8 +11,8 @@ import Data.Text (Text)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Marshal
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Pickle
data MessageBody = MessageBody { bodyLang :: (Maybe LangTag) data MessageBody = MessageBody { bodyLang :: (Maybe LangTag)
, bodyContent :: Text , bodyContent :: Text

33
source/Network/Xmpp/Connection.hs → source/Network/Xmpp/Internal.hs

@ -8,34 +8,37 @@
-- This module allows for low-level access to Pontarius XMPP. Generally, the -- This module allows for low-level access to Pontarius XMPP. Generally, the
-- "Network.Xmpp" module should be used instead. -- "Network.Xmpp" module should be used instead.
-- --
-- The 'Connection' object provides the most low-level access to the XMPP -- The 'Stream' object provides the most low-level access to the XMPP
-- stream: a simple and single-threaded interface which exposes the conduit -- stream: a simple and single-threaded interface which exposes the conduit
-- 'Event' source, as well as the input and output byte streams. Custom stateful -- 'Event' source, as well as the input and output byte streams. Custom stateful
-- 'Connection' functions can be executed using 'withConnection'. -- 'Stream' functions can be executed using 'withStream'.
-- --
-- The TLS, SASL, and 'Session' functionalities of Pontarius XMPP are built on -- The TLS, SASL, and 'Session' functionalities of Pontarius XMPP are built on
-- top of this API. -- top of this API.
module Network.Xmpp.Connection module Network.Xmpp.Internal
( Connection(..) ( Stream(..)
, ConnectionState(..) , StreamState(..)
, ConnectionHandle(..) , StreamHandle(..)
, ServerFeatures(..) , StreamFeatures(..)
, connect , openStream
, withConnection , withStream
, startTls , startTls
, simpleAuth
, auth , auth
, pushStanza , pushStanza
, pullStanza , pullStanza
, closeConnection , pushIQ
, newSession , SaslHandler(..)
, StanzaId(..)
) )
where where
import Network.Xmpp.Connection_ import Network.Xmpp.Stream
import Network.Xmpp.Session import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types
import Network.Xmpp.Tls import Network.Xmpp.Tls
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Concurrent import Network.Xmpp.Stream
import Network.Xmpp.Marshal

205
source/Network/Xmpp/Jid.hs

@ -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
}

73
source/Network/Xmpp/Marshal.hs

@ -11,7 +11,8 @@ module Network.Xmpp.Marshal where
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Pickle import Data.Text
import Network.Xmpp.Types import Network.Xmpp.Types
xpStreamStanza :: PU [Node] (Either StreamErrorInfo Stanza) xpStreamStanza :: PU [Node] (Either StreamErrorInfo Stanza)
@ -207,3 +208,73 @@ xpStreamError = ("xpStreamError" , "") <?+> xpWrap
(xpOption xpElemVerbatim) -- Application specific error conditions (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 -> Prelude.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
xpStream :: PU [Node] (Text, Maybe Jid, Maybe Jid, Maybe Text, Maybe LangTag)
xpStream = xpElemAttrs
(Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream"))
(xp5Tuple
(xpAttr "version" xpId)
(xpAttrImplied "from" xpPrim)
(xpAttrImplied "to" xpPrim)
(xpAttrImplied "id" xpId)
xpLangTag
)
-- Pickler/Unpickler for the stream features - TLS, SASL, and the rest.
xpStreamFeatures :: PU [Node] StreamFeatures
xpStreamFeatures = xpWrap
(\(tls, sasl, rest) -> StreamFeatures tls (mbl sasl) rest)
(\(StreamFeatures tls sasl rest) -> (tls, lmb sasl, rest))
(xpElemNodes
(Name
"features"
(Just "http://etherx.jabber.org/streams")
(Just "stream")
)
(xpTriple
(xpOption pickleTlsFeature)
(xpOption pickleSaslFeature)
(xpAll xpElemVerbatim)
)
)
where
pickleTlsFeature :: PU [Node] Bool
pickleTlsFeature = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-tls}starttls"
(xpElemExists "required")
pickleSaslFeature :: PU [Node] [Text]
pickleSaslFeature = xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms"
(xpAll $ xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanism" (xpContent xpId))

36
source/Network/Xmpp/Message.hs

@ -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

10
source/Network/Xmpp/Presence.hs

@ -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}

102
source/Network/Xmpp/Sasl.hs

@ -1,11 +1,17 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} {-# 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 module Network.Xmpp.Sasl
( xmppSasl ( xmppSasl
, digestMd5 , digestMd5
, scramSha1 , scramSha1
, plain , plain
, auth
) where ) where
import Control.Applicative import Control.Applicative
@ -29,7 +35,6 @@ import qualified Data.Text as Text
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Network.Xmpp.Connection_
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
@ -40,24 +45,38 @@ import Network.Xmpp.Sasl.Mechanisms
import Control.Concurrent.STM.TMVar 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 -- | Uses the first supported mechanism to authenticate, if any. Updates the
-- state with non-password credentials and restarts the stream upon -- state with non-password credentials and restarts the stream upon
-- success. Returns `Nothing' on success, an `AuthFailure' if -- success. Returns `Nothing' on success, an `AuthFailure' if
-- authentication fails, or an `XmppFailure' if anything else fails. -- authentication fails, or an `XmppFailure' if anything else fails.
xmppSasl :: [SaslHandler] -- ^ Acceptable authentication mechanisms and their xmppSasl :: [SaslHandler] -- ^ Acceptable authentication mechanisms and their
-- corresponding handlers -- corresponding handlers
-> TMVar Connection -> TMVar Stream
-> IO (Either XmppFailure (Maybe AuthFailure)) -> IO (Either XmppFailure (Maybe AuthFailure))
xmppSasl handlers = withConnection $ do xmppSasl handlers = withStream $ do
-- Chooses the first mechanism that is acceptable by both the client and the -- Chooses the first mechanism that is acceptable by both the client and the
-- server. -- server.
mechanisms <- gets $ saslMechanisms . cFeatures mechanisms <- gets $ streamSaslMechanisms . streamFeatures
case (filter (\(name, _) -> name `elem` mechanisms)) handlers of case (filter (\(name, _) -> name `elem` mechanisms)) handlers of
[] -> return $ Right $ Just $ AuthNoAcceptableMechanism mechanisms [] -> return $ Right $ Just $ AuthNoAcceptableMechanism mechanisms
(_name, handler):_ -> do (_name, handler):_ -> do
cs <- gets cState cs <- gets streamState
case cs of case cs of
ConnectionClosed -> return . Right $ Just AuthNoConnection Closed -> return . Right $ Just AuthNoStream
_ -> do _ -> do
r <- runErrorT handler r <- runErrorT handler
case r of case r of
@ -65,3 +84,74 @@ xmppSasl handlers = withConnection $ do
Right a -> do Right a -> do
_ <- runErrorT $ ErrorT restartStream _ <- runErrorT $ ErrorT restartStream
return $ Right $ Nothing return $ Right $ Nothing
-- | Authenticate to the server using the first matching method and bind a
-- resource.
auth :: [SaslHandler]
-> Maybe Text
-> TMVar Stream
-> 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
-- Produces a `bind' element, optionally wrapping a resource.
bindBody :: Maybe Text -> Element
bindBody = pickleElem $
-- Pickler to produce a
-- "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
-- element, with a possible "<resource>[JID]</resource>"
-- 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 Stream -> 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 $ withStream (do
modify $ \s -> s{streamJid = 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 Stream -> IO ()
startSession con = do
answer <- pushIQ "session" Nothing Set Nothing sessionXml con
case answer of
Left e -> error $ show e
Right _ -> return ()

26
source/Network/Xmpp/Sasl/Common.hs

@ -22,14 +22,16 @@ import Data.Word (Word8)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection_ import Network.Xmpp.Stream
import Network.Xmpp.Pickle
import Network.Xmpp.Sasl.StringPrep import Network.Xmpp.Sasl.StringPrep
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Marshal
import qualified System.Random as Random import qualified System.Random as Random
--makeNonce :: SaslM BS.ByteString import Control.Monad.State.Strict
--makeNonce :: ErrorT AuthFailure (StateT Stream IO) BS.ByteString
makeNonce :: IO BS.ByteString makeNonce :: IO BS.ByteString
makeNonce = do makeNonce = do
g <- liftIO Random.newStdGen g <- liftIO Random.newStdGen
@ -106,7 +108,7 @@ xpSaslElement = xpAlt saslSel
quote :: BS.ByteString -> BS.ByteString quote :: BS.ByteString -> BS.ByteString
quote x = BS.concat ["\"",x,"\""] quote x = BS.concat ["\"",x,"\""]
saslInit :: Text.Text -> Maybe BS.ByteString -> SaslM Bool saslInit :: Text.Text -> Maybe BS.ByteString -> ErrorT AuthFailure (StateT Stream IO) Bool
saslInit mechanism payload = do saslInit mechanism payload = do
r <- lift . pushElement . saslInitE mechanism $ r <- lift . pushElement . saslInitE mechanism $
Text.decodeUtf8 . B64.encode <$> payload Text.decodeUtf8 . B64.encode <$> payload
@ -115,7 +117,7 @@ saslInit mechanism payload = do
Right b -> return b Right b -> return b
-- | Pull the next element. -- | Pull the next element.
pullSaslElement :: SaslM SaslElement pullSaslElement :: ErrorT AuthFailure (StateT Stream IO) SaslElement
pullSaslElement = do pullSaslElement = do
r <- lift $ pullUnpickle (xpEither xpFailure xpSaslElement) r <- lift $ pullUnpickle (xpEither xpFailure xpSaslElement)
case r of case r of
@ -124,7 +126,7 @@ pullSaslElement = do
Right (Right r) -> return r Right (Right r) -> return r
-- | Pull the next element, checking that it is a challenge. -- | Pull the next element, checking that it is a challenge.
pullChallenge :: SaslM (Maybe BS.ByteString) pullChallenge :: ErrorT AuthFailure (StateT Stream IO) (Maybe BS.ByteString)
pullChallenge = do pullChallenge = do
e <- pullSaslElement e <- pullSaslElement
case e of case e of
@ -135,12 +137,12 @@ pullChallenge = do
_ -> throwError AuthChallengeFailure _ -> throwError AuthChallengeFailure
-- | Extract value from Just, failing with AuthChallengeFailure on Nothing. -- | Extract value from Just, failing with AuthChallengeFailure on Nothing.
saslFromJust :: Maybe a -> SaslM a saslFromJust :: Maybe a -> ErrorT AuthFailure (StateT Stream IO) a
saslFromJust Nothing = throwError $ AuthChallengeFailure saslFromJust Nothing = throwError $ AuthChallengeFailure
saslFromJust (Just d) = return d saslFromJust (Just d) = return d
-- | Pull the next element and check that it is success. -- | Pull the next element and check that it is success.
pullSuccess :: SaslM (Maybe Text.Text) pullSuccess :: ErrorT AuthFailure (StateT Stream IO) (Maybe Text.Text)
pullSuccess = do pullSuccess = do
e <- pullSaslElement e <- pullSaslElement
case e of case e of
@ -149,7 +151,7 @@ pullSuccess = do
-- | Pull the next element. When it's success, return it's payload. -- | Pull the next element. When it's success, return it's payload.
-- If it's a challenge, send an empty response and pull success. -- If it's a challenge, send an empty response and pull success.
pullFinalMessage :: SaslM (Maybe BS.ByteString) pullFinalMessage :: ErrorT AuthFailure (StateT Stream IO) (Maybe BS.ByteString)
pullFinalMessage = do pullFinalMessage = do
challenge2 <- pullSaslElement challenge2 <- pullSaslElement
case challenge2 of case challenge2 of
@ -165,13 +167,13 @@ pullFinalMessage = do
Right x -> return $ Just x Right x -> return $ Just x
-- | Extract p=q pairs from a challenge. -- | Extract p=q pairs from a challenge.
toPairs :: BS.ByteString -> SaslM Pairs toPairs :: BS.ByteString -> ErrorT AuthFailure (StateT Stream IO) Pairs
toPairs ctext = case pairs ctext of toPairs ctext = case pairs ctext of
Left _e -> throwError AuthChallengeFailure Left _e -> throwError AuthChallengeFailure
Right r -> return r Right r -> return r
-- | Send a SASL response element. The content will be base64-encoded. -- | Send a SASL response element. The content will be base64-encoded.
respond :: Maybe BS.ByteString -> SaslM Bool respond :: Maybe BS.ByteString -> ErrorT AuthFailure (StateT Stream IO) Bool
respond m = do respond m = do
r <- lift . pushElement . saslResponseE . fmap (Text.decodeUtf8 . B64.encode) $ m r <- lift . pushElement . saslResponseE . fmap (Text.decodeUtf8 . B64.encode) $ m
case r of case r of
@ -182,7 +184,7 @@ respond m = do
-- | Run the appropriate stringprep profiles on the credentials. -- | Run the appropriate stringprep profiles on the credentials.
-- May fail with 'AuthStringPrepFailure' -- May fail with 'AuthStringPrepFailure'
prepCredentials :: Text.Text -> Maybe Text.Text -> Text.Text prepCredentials :: Text.Text -> Maybe Text.Text -> Text.Text
-> SaslM (Text.Text, Maybe Text.Text, Text.Text) -> ErrorT AuthFailure (StateT Stream IO) (Text.Text, Maybe Text.Text, Text.Text)
prepCredentials authcid authzid password = case credentials of prepCredentials authcid authzid password = case credentials of
Nothing -> throwError $ AuthStringPrepFailure Nothing -> throwError $ AuthStringPrepFailure
Just creds -> return creds Just creds -> return creds

10
source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs

@ -31,8 +31,6 @@ import qualified Data.ByteString as BS
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection_
import Network.Xmpp.Pickle
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Sasl.Common import Network.Xmpp.Sasl.Common
@ -44,15 +42,15 @@ import Network.Xmpp.Sasl.Types
xmppDigestMd5 :: Text -- ^ Authentication identity (authzid or username) xmppDigestMd5 :: Text -- ^ Authentication identity (authzid or username)
-> Maybe Text -- ^ Authorization identity (authcid) -> Maybe Text -- ^ Authorization identity (authcid)
-> Text -- ^ Password (authzid) -> Text -- ^ Password (authzid)
-> SaslM () -> ErrorT AuthFailure (StateT Stream IO) ()
xmppDigestMd5 authcid authzid password = do xmppDigestMd5 authcid authzid password = do
(ac, az, pw) <- prepCredentials authcid authzid password (ac, az, pw) <- prepCredentials authcid authzid password
hn <- gets cHostName hn <- gets streamHostname
xmppDigestMd5' (fromJust hn) ac az pw xmppDigestMd5' (fromJust hn) ac az pw
where where
xmppDigestMd5' :: Text -> Text -> Maybe Text -> Text -> SaslM () xmppDigestMd5' :: Text -> Text -> Maybe Text -> Text -> ErrorT AuthFailure (StateT Stream IO) ()
xmppDigestMd5' hostname authcid authzid password = do xmppDigestMd5' hostname authcid authzid password = do
-- Push element and receive the challenge (in SaslM). -- Push element and receive the challenge.
_ <- saslInit "DIGEST-MD5" Nothing -- TODO: Check boolean? _ <- saslInit "DIGEST-MD5" Nothing -- TODO: Check boolean?
pairs <- toPairs =<< saslFromJust =<< pullChallenge pairs <- toPairs =<< saslFromJust =<< pullChallenge
cnonce <- liftIO $ makeNonce cnonce <- liftIO $ makeNonce

4
source/Network/Xmpp/Sasl/Mechanisms/Plain.hs

@ -35,10 +35,8 @@ import qualified Data.ByteString as BS
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection_
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Pickle
import qualified System.Random as Random import qualified System.Random as Random
@ -52,7 +50,7 @@ import Network.Xmpp.Sasl.Types
xmppPlain :: Text.Text -- ^ Password xmppPlain :: Text.Text -- ^ Password
-> Maybe Text.Text -- ^ Authorization identity (authzid) -> Maybe Text.Text -- ^ Authorization identity (authzid)
-> Text.Text -- ^ Authentication identity (authcid) -> Text.Text -- ^ Authentication identity (authcid)
-> SaslM () -> ErrorT AuthFailure (StateT Stream IO) ()
xmppPlain authcid authzid password = do xmppPlain authcid authzid password = do
(ac, az, pw) <- prepCredentials authcid authzid password (ac, az, pw) <- prepCredentials authcid authzid password
_ <- saslInit "PLAIN" ( Just $ plainMessage ac az pw) _ <- saslInit "PLAIN" ( Just $ plainMessage ac az pw)

8
source/Network/Xmpp/Sasl/Mechanisms/Scram.hs

@ -29,6 +29,10 @@ import Data.Word(Word8)
import Network.Xmpp.Sasl.Common import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.StringPrep import Network.Xmpp.Sasl.StringPrep
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Types
import Control.Monad.State.Strict
-- | A nicer name for undefined, for use as a dummy token to determin -- | A nicer name for undefined, for use as a dummy token to determin
-- the hash function to use -- the hash function to use
@ -45,7 +49,7 @@ scram :: (Crypto.Hash ctx hash)
-> Text.Text -- ^ Authentication ID (user name) -> Text.Text -- ^ Authentication ID (user name)
-> Maybe Text.Text -- ^ Authorization ID -> Maybe Text.Text -- ^ Authorization ID
-> Text.Text -- ^ Password -> Text.Text -- ^ Password
-> SaslM () -> ErrorT AuthFailure (StateT Stream IO) ()
scram hashToken authcid authzid password = do scram hashToken authcid authzid password = do
(ac, az, pw) <- prepCredentials authcid authzid password (ac, az, pw) <- prepCredentials authcid authzid password
scramhelper hashToken ac az pw scramhelper hashToken ac az pw
@ -94,7 +98,7 @@ scram hashToken authcid authzid password = do
fromPairs :: Pairs fromPairs :: Pairs
-> BS.ByteString -> BS.ByteString
-> SaslM (BS.ByteString, BS.ByteString, Integer) -> ErrorT AuthFailure (StateT Stream IO) (BS.ByteString, BS.ByteString, Integer)
fromPairs pairs cnonce | Just nonce <- lookup "r" pairs fromPairs pairs cnonce | Just nonce <- lookup "r" pairs
, cnonce `BS.isPrefixOf` nonce , cnonce `BS.isPrefixOf` nonce
, Just salt' <- lookup "s" pairs , Just salt' <- lookup "s" pairs

12
source/Network/Xmpp/Sasl/Types.hs

@ -15,7 +15,7 @@ data AuthFailure = AuthXmlFailure
-- itself -- itself
| AuthStreamFailure XmppFailure -- ^ Stream error on stream restart | AuthStreamFailure XmppFailure -- ^ Stream error on stream restart
-- TODO: Rename AuthConnectionFailure? -- TODO: Rename AuthConnectionFailure?
| AuthNoConnection | AuthNoStream
| AuthFailure -- General instance used for the Error instance | AuthFailure -- General instance used for the Error instance
| AuthSaslFailure SaslFailure -- ^ Defined SASL error condition | AuthSaslFailure SaslFailure -- ^ Defined SASL error condition
| AuthStringPrepFailure -- ^ StringPrep failed | AuthStringPrepFailure -- ^ StringPrep failed
@ -27,11 +27,9 @@ instance Error AuthFailure where
data SaslElement = SaslSuccess (Maybe Text.Text) data SaslElement = SaslSuccess (Maybe Text.Text)
| SaslChallenge (Maybe Text.Text) | SaslChallenge (Maybe Text.Text)
-- | SASL mechanism XmppConnection computation, with the possibility of throwing
-- an authentication error.
type SaslM a = ErrorT AuthFailure (StateT Connection IO) a
type Pairs = [(ByteString, ByteString)] type Pairs = [(ByteString, ByteString)]
-- | Tuple defining the SASL Handler's name, and a SASL mechanism computation -- | Tuple defining the SASL Handler's name, and a SASL mechanism computation.
type SaslHandler = (Text.Text, SaslM ()) -- The SASL mechanism is a stateful @Stream@ computation, which has the
-- possibility of resulting in an authentication error.
type SaslHandler = (Text.Text, ErrorT AuthFailure (StateT Stream IO) ())

454
source/Network/Xmpp/Stream.hs

@ -1,11 +1,14 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} {-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Network.Xmpp.Stream where module Network.Xmpp.Stream where
import Control.Applicative ((<$>), (<*>)) import Control.Applicative ((<$>), (<*>))
import qualified Control.Exception as Ex import qualified Control.Exception as Ex
import Control.Exception.Base
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.Reader import Control.Monad.Reader
import Control.Monad.State.Strict import Control.Monad.State.Strict
@ -20,13 +23,35 @@ import Data.Void (Void)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection_
import Network.Xmpp.Pickle
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Marshal import Network.Xmpp.Marshal
import Text.Xml.Stream.Elements
import Text.XML.Stream.Parse as XP import Text.XML.Stream.Parse as XP
import Control.Concurrent (forkIO, threadDelay)
import Network
import Control.Concurrent.STM
import Data.ByteString as BS
import Data.ByteString.Base64
import System.Log.Logger
import qualified GHC.IO.Exception as GIE
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import System.IO.Error (tryIOError)
import System.IO
import Data.Conduit
import Data.Conduit.Binary as CB
import Data.Conduit.Internal as DCI
import qualified Data.Conduit.List as CL
import qualified Data.Text as T
import Data.ByteString.Char8 as BSC8
import Text.XML.Unresolved(InvalidEventStream(..))
import qualified Control.Exception.Lifted as ExL
import Control.Monad.Trans.Resource as R
import Network.Xmpp.Utilities
-- import Text.XML.Stream.Elements -- import Text.XML.Stream.Elements
@ -73,17 +98,17 @@ openElementFromEvents = do
-- server responds in a way that is invalid, an appropriate stream error will be -- server responds in a way that is invalid, an appropriate stream error will be
-- generated, the connection to the server will be closed, and a XmppFailure -- generated, the connection to the server will be closed, and a XmppFailure
-- will be produced. -- will be produced.
startStream :: StateT Connection IO (Either XmppFailure ()) startStream :: StateT Stream IO (Either XmppFailure ())
startStream = runErrorT $ do startStream = runErrorT $ do
state <- lift $ get state <- lift $ get
con <- liftIO $ mkConnection state stream <- liftIO $ mkStream state
-- Set the `from' (which is also the expected to) attribute depending on the -- Set the `from' (which is also the expected to) attribute depending on the
-- state of the connection. -- state of the stream.
let expectedTo = case cState state of let expectedTo = case streamState state of
ConnectionPlain -> if cJidWhenPlain state Plain -> if includeJidWhenPlain state
then cJid state else Nothing then toJid state else Nothing
ConnectionSecured -> cJid state Secured -> toJid state
case cHostName state of case streamHostname state of
Nothing -> throwError XmppOtherFailure -- TODO: When does this happen? Nothing -> throwError XmppOtherFailure -- TODO: When does this happen?
Just hostname -> lift $ do Just hostname -> lift $ do
pushXmlDecl pushXmlDecl
@ -92,62 +117,62 @@ startStream = runErrorT $ do
, expectedTo , expectedTo
, Just (Jid Nothing hostname Nothing) , Just (Jid Nothing hostname Nothing)
, Nothing , Nothing
, cPreferredLang state , preferredLang state
) )
response <- ErrorT $ runEventsSink $ runErrorT $ streamS expectedTo response <- ErrorT $ runEventsSink $ runErrorT $ streamS expectedTo
case response of case response of
Left e -> throwError e Left e -> throwError e
-- Successful unpickling of stream element. -- Successful unpickling of stream element.
Right (Right (ver, from, to, id, lt, features)) Right (Right (ver, from, to, id, lt, features))
| (unpack ver) /= "1.0" -> | (T.unpack ver) /= "1.0" ->
closeStreamWithError con StreamUnsupportedVersion Nothing closeStreamWithError stream StreamUnsupportedVersion Nothing
| lt == Nothing -> | lt == Nothing ->
closeStreamWithError con StreamInvalidXml Nothing closeStreamWithError stream StreamInvalidXml Nothing
-- If `from' is set, we verify that it's the correct one. TODO: Should we check against the realm instead? -- If `from' is set, we verify that it's the correct one. TODO: Should we check against the realm instead?
| isJust from && (from /= Just (Jid Nothing (fromJust $ cHostName state) Nothing)) -> | isJust from && (from /= Just (Jid Nothing (fromJust $ streamHostname state) Nothing)) ->
closeStreamWithError con StreamInvalidFrom Nothing closeStreamWithError stream StreamInvalidFrom Nothing
| to /= expectedTo -> | to /= expectedTo ->
closeStreamWithError con StreamUndefinedCondition (Just $ Element "invalid-to" [] []) -- TODO: Suitable? closeStreamWithError stream StreamUndefinedCondition (Just $ Element "invalid-to" [] []) -- TODO: Suitable?
| otherwise -> do | otherwise -> do
modify (\s -> s{ cFeatures = features modify (\s -> s{ streamFeatures = features
, cStreamLang = lt , streamLang = lt
, cStreamId = id , streamId = id
, cFrom = from , streamFrom = from
} ) } )
return () return ()
-- Unpickling failed - we investigate the element. -- Unpickling failed - we investigate the element.
Right (Left (Element name attrs children)) Right (Left (Element name attrs children))
| (nameLocalName name /= "stream") -> | (nameLocalName name /= "stream") ->
closeStreamWithError con StreamInvalidXml Nothing closeStreamWithError stream StreamInvalidXml Nothing
| (nameNamespace name /= Just "http://etherx.jabber.org/streams") -> | (nameNamespace name /= Just "http://etherx.jabber.org/streams") ->
closeStreamWithError con StreamInvalidNamespace Nothing closeStreamWithError stream StreamInvalidNamespace Nothing
| (isJust $ namePrefix name) && (fromJust (namePrefix name) /= "stream") -> | (isJust $ namePrefix name) && (fromJust (namePrefix name) /= "stream") ->
closeStreamWithError con StreamBadNamespacePrefix Nothing closeStreamWithError stream StreamBadNamespacePrefix Nothing
| otherwise -> ErrorT $ checkchildren con (flattenAttrs attrs) | otherwise -> ErrorT $ checkchildren stream (flattenAttrs attrs)
where where
-- closeStreamWithError :: MonadIO m => TMVar Connection -> StreamErrorCondition -> -- closeStreamWithError :: MonadIO m => TMVar Stream -> StreamErrorCondition ->
-- Maybe Element -> ErrorT XmppFailure m () -- Maybe Element -> ErrorT XmppFailure m ()
closeStreamWithError con sec el = do closeStreamWithError stream sec el = do
liftIO $ do liftIO $ do
withConnection (pushElement . pickleElem xpStreamError $ withStream (pushElement . pickleElem xpStreamError $
StreamErrorInfo sec Nothing el) con StreamErrorInfo sec Nothing el) stream
closeStreams con closeStreams stream
throwError XmppOtherFailure throwError XmppOtherFailure
checkchildren con children = checkchildren stream children =
let to' = lookup "to" children let to' = lookup "to" children
ver' = lookup "version" children ver' = lookup "version" children
xl = lookup xmlLang children xl = lookup xmlLang children
in case () of () | Just (Nothing :: Maybe Jid) == (safeRead <$> to') -> in case () of () | Just (Nothing :: Maybe Jid) == (safeRead <$> to') ->
runErrorT $ closeStreamWithError con runErrorT $ closeStreamWithError stream
StreamBadNamespacePrefix Nothing StreamBadNamespacePrefix Nothing
| Nothing == ver' -> | Nothing == ver' ->
runErrorT $ closeStreamWithError con runErrorT $ closeStreamWithError stream
StreamUnsupportedVersion Nothing StreamUnsupportedVersion Nothing
| Just (Nothing :: Maybe LangTag) == (safeRead <$> xl) -> | Just (Nothing :: Maybe LangTag) == (safeRead <$> xl) ->
runErrorT $ closeStreamWithError con runErrorT $ closeStreamWithError stream
StreamInvalidXml Nothing StreamInvalidXml Nothing
| otherwise -> | otherwise ->
runErrorT $ closeStreamWithError con runErrorT $ closeStreamWithError stream
StreamBadFormat Nothing StreamBadFormat Nothing
safeRead x = case reads $ Text.unpack x of safeRead x = case reads $ Text.unpack x of
[] -> Nothing [] -> Nothing
@ -165,12 +190,12 @@ flattenAttrs attrs = Prelude.map (\(name, content) ->
-- Sets a new Event source using the raw source (of bytes) -- Sets a new Event source using the raw source (of bytes)
-- and calls xmppStartStream. -- and calls xmppStartStream.
restartStream :: StateT Connection IO (Either XmppFailure ()) restartStream :: StateT Stream IO (Either XmppFailure ())
restartStream = do restartStream = do
raw <- gets (cRecv . cHandle) raw <- gets (streamReceive . streamHandle)
let newSource = DCI.ResumableSource (loopRead raw $= XP.parseBytes def) let newSource = DCI.ResumableSource (loopRead raw $= XP.parseBytes def)
(return ()) (return ())
modify (\s -> s{cEventSource = newSource }) modify (\s -> s{streamEventSource = newSource })
startStream startStream
where where
loopRead read = do loopRead read = do
@ -190,7 +215,7 @@ streamS :: Maybe Jid -> StreamSink (Either Element ( Text
, Maybe Jid , Maybe Jid
, Maybe Text , Maybe Text
, Maybe LangTag , Maybe LangTag
, ServerFeatures )) , StreamFeatures ))
streamS expectedTo = do streamS expectedTo = do
header <- xmppStreamHeader header <- xmppStreamHeader
case header of case header of
@ -209,48 +234,327 @@ streamS expectedTo = do
case unpickleElem xpStream el of case unpickleElem xpStream el of
Left _ -> return $ Left el Left _ -> return $ Left el
Right r -> return $ Right r Right r -> return $ Right r
xmppStreamFeatures :: StreamSink ServerFeatures xmppStreamFeatures :: StreamSink StreamFeatures
xmppStreamFeatures = do xmppStreamFeatures = do
e <- lift $ elements =$ CL.head e <- lift $ elements =$ CL.head
case e of case e of
Nothing -> throwError XmppOtherFailure Nothing -> throwError XmppOtherFailure
Just r -> streamUnpickleElem xpStreamFeatures r Just r -> streamUnpickleElem xpStreamFeatures r
-- | Connects to the XMPP server and opens the XMPP stream against the given
-- host name, port, and realm.
openStream :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Stream))
openStream address port hostname = do
stream <- connectTcp address port hostname
case stream of
Right stream' -> do
result <- withStream startStream stream'
return $ Right stream'
Left e -> do
return $ Left e
xpStream :: PU [Node] (Text, Maybe Jid, Maybe Jid, Maybe Text, Maybe LangTag) -- | Send "</stream:stream>" and wait for the server to finish processing and to
xpStream = ("xpStream","") <?+> xpElemAttrs -- close the connection. Any remaining elements from the server are returned.
(Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream")) -- Surpresses StreamEndFailure exceptions, but may throw a StreamCloseError.
(xp5Tuple closeStreams :: TMVar Stream -> IO (Either XmppFailure [Element])
(xpAttr "version" xpId) closeStreams = withStream $ do
(xpAttrImplied "from" xpPrim) send <- gets (streamSend . streamHandle)
(xpAttrImplied "to" xpPrim) cc <- gets (streamClose . streamHandle)
(xpAttrImplied "id" xpId) liftIO $ send "</stream:stream>"
xpLangTag 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 Stream 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)
-- 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 Stream 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 Stream IO (Either XmppFailure Bool)
pushElement x = do
send <- gets (streamSend . streamHandle)
wrapIOException $ send $ renderElement x
-- | Encode and send stanza
pushStanza :: Stanza -> TMVar Stream -> IO (Either XmppFailure Bool)
pushStanza s = withStream' . 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 Stream IO (Either XmppFailure Bool)
pushXmlDecl = do
con <- gets streamHandle
wrapIOException $ (streamSend con) "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
pushOpenElement :: Element -> StateT Stream IO (Either XmppFailure Bool)
pushOpenElement e = do
sink <- gets (streamSend . streamHandle)
wrapIOException $ sink $ renderOpenElement e
-- `Connect-and-resumes' the given sink to the stream source, and pulls a
-- `b' value.
runEventsSink :: Sink Event IO b -> StateT Stream IO (Either XmppFailure b)
runEventsSink snk = do -- TODO: Wrap exceptions?
source <- gets streamEventSource
(src', r) <- lift $ source $$++ snk
modify (\s -> s{streamEventSource = src'})
return $ Right r
pullElement :: StateT Stream IO (Either XmppFailure Element)
pullElement = do
ExL.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
)
[ ExL.Handler (\StreamEnd -> return $ Left StreamEndFailure)
, ExL.Handler (\(InvalidXmppXml s) -- Invalid XML `Event' encountered, or missing element close tag
-> return $ Left XmppOtherFailure) -- TODO: Log: s
, ExL.Handler $ \(e :: InvalidEventStream) -- xml-conduit exception
-> return $ Left XmppOtherFailure -- TODO: Log: (show e)
]
-- Pickler/Unpickler for the stream features - TLS, SASL, and the rest. -- Pulls an element and unpickles it.
xpStreamFeatures :: PU [Node] ServerFeatures pullUnpickle :: PU [Node] a -> StateT Stream IO (Either XmppFailure a)
xpStreamFeatures = ("xpStreamFeatures", "") <?+> xpWrap pullUnpickle p = do
(\(tls, sasl, rest) -> SF tls (mbl sasl) rest) elem <- pullElement
(\(SF tls sasl rest) -> (tls, lmb sasl, rest)) case elem of
(xpElemNodes Left e -> return $ Left e
(Name Right elem' -> do
"features" let res = unpickleElem p elem'
(Just "http://etherx.jabber.org/streams") case res of
(Just "stream") Left e -> return $ Left XmppOtherFailure -- TODO: Log
) Right r -> return $ Right r
(xpTriple
(xpOption pickleTlsFeature) -- | Pulls a stanza (or stream error) from the stream.
(xpOption pickleSaslFeature) pullStanza :: TMVar Stream -> IO (Either XmppFailure Stanza)
(xpAll xpElemVerbatim) pullStanza = withStream' $ 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 = ExL.catch
(p >> return True)
(\e -> case GIE.ioe_type e of
GIE.ResourceVanished -> return False
GIE.IllegalOperation -> return False
_ -> ExL.throwIO e
) )
-- Stream state used when there is no connection.
xmppNoStream :: Stream
xmppNoStream = Stream {
streamState = Closed
, streamHandle = StreamHandle { streamSend = \_ -> return False
, streamReceive = \_ -> ExL.throwIO
XmppOtherFailure
, streamFlush = return ()
, streamClose = return ()
}
, streamEventSource = DCI.ResumableSource zeroSource (return ())
, streamFeatures = StreamFeatures Nothing [] []
, streamHostname = Nothing
, streamFrom = Nothing
, streamId = Nothing
, streamLang = Nothing
, streamJid = Nothing
, preferredLang = Nothing
, toJid = Nothing
, includeJidWhenPlain = False
}
where
zeroSource :: Source IO output
zeroSource = liftIO . ExL.throwIO $ XmppOtherFailure
connectTcp :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Stream))
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 = StreamHandle { streamSend = \d -> do
let d64 = encode d
debugM "Pontarius.Xmpp" $
"Sending TCP data: " ++ (BSC8.unpack d64)
++ "."
catchPush $ BS.hPut h d
, streamReceive = \n -> do
d <- BS.hGetSome h n
let d64 = encode d
debugM "Pontarius.Xmpp" $
"Received TCP data: " ++
(BSC8.unpack d64) ++ "."
return d
, streamFlush = hFlush h
, streamClose = hClose h
}
let stream = Stream
{ streamState = Plain
, streamHandle = hand
, streamEventSource = eSource
, streamFeatures = StreamFeatures Nothing [] []
, streamHostname = (Just hostname)
, streamFrom = Nothing
, streamId = Nothing
, streamLang = Nothing
, streamJid = Nothing
, preferredLang = Nothing -- TODO: Allow user to set
, toJid = Nothing -- TODO: Allow user to set
, includeJidWhenPlain = False -- TODO: Allow user to set
}
stream' <- mkStream stream
return $ Right stream'
where where
pickleTlsFeature :: PU [Node] Bool logConduit :: Conduit ByteString IO ByteString
pickleTlsFeature = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-tls}starttls" logConduit = CL.mapM $ \d -> do
(xpElemExists "required") let d64 = encode d
pickleSaslFeature :: PU [Node] [Text] debugM "Pontarius.Xmpp" $ "Received TCP data: " ++ (BSC8.unpack d64) ++
pickleSaslFeature = xpElemNodes "."
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms" return d
(xpAll $ xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanism" (xpContent xpId))
-- Closes the connection and updates the XmppConMonad Stream state.
-- killStream :: TMVar Stream -> IO (Either ExL.SomeException ())
killStream :: TMVar Stream -> IO (Either XmppFailure ())
killStream = withStream $ do
cc <- gets (streamClose . streamHandle)
err <- wrapIOException cc
-- (ExL.try cc :: IO (Either ExL.SomeException ()))
put xmppNoStream
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 Stream
-> IO (Either XmppFailure (Either IQError IQResult))
pushIQ iqID to tp lang body stream = do
pushStanza (IQRequestS $ IQRequest iqID Nothing to lang tp body) stream
res <- pullStanza stream
case res of
Left e -> return $ Left e
Right (IQErrorS e) -> return $ Right $ Left e
Right (IQResultS r) -> do
unless
(iqID == iqResultID r) . liftIO . ExL.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 "
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 ()
elements :: R.MonadThrow m => Conduit Event m Element
elements = do
x <- await
case x of
Just (EventBeginElement n as) -> do
goE n as >>= yield
elements
Just (EventEndElement streamName) -> lift $ R.monadThrow StreamEnd
Nothing -> return ()
_ -> lift $ R.monadThrow $ InvalidXmppXml $ "not an element: " ++ show x
where
many' f =
go id
where
go front = do
x <- f
case x of
Left x -> return $ (x, front [])
Right y -> go (front . (:) y)
goE n as = do
(y, ns) <- many' goN
if y == Just (EventEndElement n)
then return $ Element n as $ compressNodes ns
else lift $ R.monadThrow $ InvalidXmppXml $
"Missing close tag: " ++ show n
goN = do
x <- await
case x of
Just (EventBeginElement n as) -> (Right . NodeElement) <$> goE n as
Just (EventInstruction i) -> return $ Right $ NodeInstruction i
Just (EventContent c) -> return $ Right $ NodeContent c
Just (EventComment t) -> return $ Right $ NodeComment t
Just (EventCDATA t) -> return $ Right $ NodeContent $ ContentText t
_ -> return $ Left x
compressNodes :: [Node] -> [Node]
compressNodes [] = []
compressNodes [x] = [x]
compressNodes (NodeContent (ContentText x) : NodeContent (ContentText y) : z) =
compressNodes $ NodeContent (ContentText $ x `Text.append` y) : z
compressNodes (x:xs) = x : compressNodes xs
streamName :: Name
streamName = (Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream"))
withStream :: StateT Stream IO (Either XmppFailure c) -> TMVar Stream -> IO (Either XmppFailure c)
withStream action stream = bracketOnError
(atomically $ takeTMVar stream)
(atomically . putTMVar stream)
(\s -> do
(r, s') <- runStateT action s
atomically $ putTMVar stream s'
return r
)
-- nonblocking version. Changes to the connection are ignored!
withStream' :: StateT Stream IO (Either XmppFailure b) -> TMVar Stream -> IO (Either XmppFailure b)
withStream' action stream = do
stream_ <- atomically $ readTMVar stream
(r, _) <- runStateT action stream_
return r
mkStream :: Stream -> IO (TMVar Stream)
mkStream con = {- Stream `fmap` -} (atomically $ newTMVar con)

115
source/Network/Xmpp/Tls.hs

@ -13,20 +13,23 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import Data.Conduit import Data.Conduit
import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Binary as CB
import Data.Conduit.Tls as TLS
import Data.Typeable import Data.Typeable
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection_
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import Control.Concurrent.STM.TMVar import Control.Concurrent.STM.TMVar
mkBackend con = Backend { backendSend = \bs -> void (cSend con bs) import Data.IORef
, backendRecv = cRecv con import Crypto.Random.API
, backendFlush = cFlush con import Network.TLS
, backendClose = cClose con import Network.TLS.Extra
mkBackend con = Backend { backendSend = \bs -> void (streamSend con bs)
, backendRecv = streamReceive con
, backendFlush = streamFlush con
, backendClose = streamClose con
} }
where where
cutBytes n = do cutBytes n = do
@ -62,44 +65,98 @@ cutBytes n = do
starttlsE :: Element starttlsE :: Element
starttlsE = Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] [] starttlsE = Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] []
exampleParams :: TLS.TLSParams exampleParams :: TLSParams
exampleParams = TLS.defaultParamsClient exampleParams = defaultParamsClient
{ pConnectVersion = TLS.TLS10 { pConnectVersion = TLS10
, pAllowedVersions = [TLS.SSL3, TLS.TLS10, TLS.TLS11] , pAllowedVersions = [SSL3, TLS10, TLS11]
, pCiphers = [TLS.cipher_AES128_SHA1] , pCiphers = [cipher_AES128_SHA1]
, pCompressions = [TLS.nullCompression] , pCompressions = [nullCompression]
, pUseSecureRenegotiation = False -- No renegotiation , pUseSecureRenegotiation = False -- No renegotiation
, onCertificatesRecv = \_certificate -> , onCertificatesRecv = \_certificate ->
return TLS.CertificateUsageAccept return CertificateUsageAccept
} }
-- Pushes "<starttls/>, waits for "<proceed/>", performs the TLS handshake, and -- Pushes "<starttls/>, waits for "<proceed/>", performs the TLS handshake, and
-- restarts the stream. -- restarts the stream.
startTls :: TLS.TLSParams -> TMVar Connection -> IO (Either XmppFailure ()) startTls :: TLSParams -> TMVar Stream -> IO (Either XmppFailure ())
startTls params con = Ex.handle (return . Left . TlsError) startTls params con = Ex.handle (return . Left . TlsError)
. flip withConnection con . flip withStream con
. runErrorT $ do . runErrorT $ do
features <- lift $ gets cFeatures features <- lift $ gets streamFeatures
state <- gets cState state <- gets streamState
case state of case state of
ConnectionPlain -> return () Plain -> return ()
ConnectionClosed -> throwError XmppNoConnection Closed -> throwError XmppNoStream
ConnectionSecured -> throwError TlsConnectionSecured Secured -> throwError TlsStreamSecured
con <- lift $ gets cHandle con <- lift $ gets streamHandle
when (stls features == Nothing) $ throwError TlsNoServerSupport when (streamTls features == Nothing) $ throwError TlsNoServerSupport
lift $ pushElement starttlsE lift $ pushElement starttlsE
answer <- lift $ pullElement answer <- lift $ pullElement
case answer of case answer of
Left e -> return $ Left e Left e -> return $ Left e
Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] []) -> return $ Right () Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] []) -> return $ Right ()
Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _) -> return $ Left XmppOtherFailure Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _) -> return $ Left XmppOtherFailure
(raw, _snk, psh, read, ctx) <- lift $ TLS.tlsinit debug params (mkBackend con) (raw, _snk, psh, read, ctx) <- lift $ tlsinit debug params (mkBackend con)
let newHand = ConnectionHandle { cSend = catchPush . psh let newHand = StreamHandle { streamSend = catchPush . psh
, cRecv = read , streamReceive = read
, cFlush = contextFlush ctx , streamFlush = contextFlush ctx
, cClose = bye ctx >> cClose con , streamClose = bye ctx >> streamClose con
} }
lift $ modify ( \x -> x {cHandle = newHand}) lift $ modify ( \x -> x {streamHandle = newHand})
either (lift . Ex.throwIO) return =<< lift restartStream either (lift . Ex.throwIO) return =<< lift restartStream
modify (\s -> s{cState = ConnectionSecured}) modify (\s -> s{streamState = Secured})
return () return ()
client params gen backend = do
contextNew backend params gen
defaultParams = defaultParamsClient
tlsinit :: (MonadIO m, MonadIO m1) =>
Bool
-> TLSParams
-> Backend
-> m ( Source m1 BS.ByteString
, Sink BS.ByteString m1 ()
, BS.ByteString -> IO ()
, Int -> m1 BS.ByteString
, Context
)
tlsinit debug tlsParams backend = do
when debug . liftIO $ putStrLn "TLS with debug mode enabled"
gen <- liftIO $ getSystemRandomGen -- TODO: Find better random source?
con <- client tlsParams gen backend
handshake con
let src = forever $ do
dt <- liftIO $ recvData con
when debug (liftIO $ putStr "in: " >> BS.putStrLn dt)
yield dt
let snk = do
d <- await
case d of
Nothing -> return ()
Just x -> do
sendData con (BL.fromChunks [x])
when debug (liftIO $ putStr "out: " >> BS.putStrLn x)
snk
read <- liftIO $ mkReadBuffer (recvData con)
return ( src
, snk
, \s -> do
when debug (liftIO $ BS.putStrLn s)
sendData con $ BL.fromChunks [s]
, liftIO . read
, con
)
mkReadBuffer :: IO BS.ByteString -> IO (Int -> IO BS.ByteString)
mkReadBuffer read = do
buffer <- newIORef BS.empty
let read' n = do
nc <- readIORef buffer
bs <- if BS.null nc then read
else return nc
let (result, rest) = BS.splitAt n bs
writeIORef buffer rest
return result
return read'

345
source/Network/Xmpp/Types.hs

@ -22,7 +22,7 @@ module Network.Xmpp.Types
, PresenceType(..) , PresenceType(..)
, SaslError(..) , SaslError(..)
, SaslFailure(..) , SaslFailure(..)
, ServerFeatures(..) , StreamFeatures(..)
, Stanza(..) , Stanza(..)
, StanzaError(..) , StanzaError(..)
, StanzaErrorCondition(..) , StanzaErrorCondition(..)
@ -31,19 +31,20 @@ module Network.Xmpp.Types
, XmppFailure(..) , XmppFailure(..)
, StreamErrorCondition(..) , StreamErrorCondition(..)
, Version(..) , Version(..)
, ConnectionHandle(..) , StreamHandle(..)
, Connection(..) , Stream(..)
, withConnection , StreamState(..)
, withConnection'
, mkConnection
, ConnectionState(..)
, StreamErrorInfo(..) , StreamErrorInfo(..)
, langTag , langTag
, module Network.Xmpp.Jid , Jid(..)
, isBare
, isFull
, fromString
, StreamEnd(..)
, InvalidXmppXml(..)
) )
where where
import Control.Applicative ((<$>), many)
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Exception import Control.Exception
import Control.Monad.Error import Control.Monad.Error
@ -65,24 +66,30 @@ import qualified Network.TLS as TLS
import qualified Network as N import qualified Network as N
import Network.Xmpp.Jid
import System.IO 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 -- Wraps a string of random characters that, when using an appropriate
-- @IDGenerator@, is guaranteed to be unique for the Xmpp session. -- @IdGenerator@, is guaranteed to be unique for the Xmpp session.
data StanzaID = SI !Text deriving (Eq, Ord) data StanzaId = StanzaId !Text deriving (Eq, Ord)
instance Show StanzaID where instance Show StanzaId where
show (SI s) = Text.unpack s show (StanzaId s) = Text.unpack s
instance Read StanzaID where instance Read StanzaId where
readsPrec _ x = [(SI $ Text.pack x, "")] readsPrec _ x = [(StanzaId $ Text.pack x, "")]
instance IsString StanzaID where instance IsString StanzaId where
fromString = SI . Text.pack fromString = StanzaId . Text.pack
-- | The Xmpp communication primities (Message, Presence and Info/Query) are -- | The Xmpp communication primities (Message, Presence and Info/Query) are
-- called stanzas. -- called stanzas.
@ -644,8 +651,8 @@ data XmppFailure = StreamErrorFailure StreamErrorInfo -- ^ An error XML stream
-- far. -- far.
| TlsError TLS.TLSError | TlsError TLS.TLSError
| TlsNoServerSupport | TlsNoServerSupport
| XmppNoConnection | XmppNoStream
| TlsConnectionSecured -- ^ Connection already secured | TlsStreamSecured -- ^ Connection already secured
| XmppOtherFailure -- ^ Undefined condition. More | XmppOtherFailure -- ^ Undefined condition. More
-- information should be available -- information should be available
-- in the log. -- in the log.
@ -747,71 +754,253 @@ langTagParser = do
tagChars :: [Char] tagChars :: [Char]
tagChars = ['a'..'z'] ++ ['A'..'Z'] tagChars = ['a'..'z'] ++ ['A'..'Z']
data ServerFeatures = SF data StreamFeatures = StreamFeatures
{ stls :: !(Maybe Bool) { streamTls :: !(Maybe Bool)
, saslMechanisms :: ![Text.Text] , streamSaslMechanisms :: ![Text.Text]
, other :: ![Element] , streamOtherFeatures :: ![Element] -- TODO: All feature elements instead?
} deriving Show } deriving Show
-- | Signals the state of the connection. -- | Signals the state of the stream connection.
data ConnectionState data StreamState
= ConnectionClosed -- ^ No connection at this point. = Closed -- ^ No stream has been established
| ConnectionPlain -- ^ Connection established, but not secured. | Plain -- ^ Stream established, but not secured via TLS
| ConnectionSecured -- ^ Connection established and secured via TLS. | Secured -- ^ Stream established and secured via TLS
deriving (Show, Eq, Typeable) deriving (Show, Eq, Typeable)
-- | Defines operations for sending, receiving, flushing, and closing on a -- | Defines operations for sending, receiving, flushing, and closing on a
-- connection. -- stream.
data ConnectionHandle = data StreamHandle =
ConnectionHandle { cSend :: BS.ByteString -> IO Bool StreamHandle { streamSend :: BS.ByteString -> IO Bool
, cRecv :: Int -> IO BS.ByteString , streamReceive :: Int -> IO BS.ByteString
-- This is to hold the state of the XML parser (otherwise -- This is to hold the state of the XML parser (otherwise we
-- we will receive EventBeginDocument events and forget -- will receive EventBeginDocument events and forget about
-- about name prefixes). -- name prefixes). (TODO: Clarify)
, cFlush :: IO () , streamFlush :: IO ()
, cClose :: IO () , streamClose :: IO ()
} }
data Connection = Connection data Stream = Stream
{ cState :: !ConnectionState -- ^ State of connection { -- | State of the stream - 'Closed', 'Plain', or 'Secured'
, cHandle :: ConnectionHandle -- ^ Handle to send, receive, flush, and close streamState :: !StreamState -- ^ State of connection
-- on the connection. -- | Functions to send, receive, flush, and close on the stream
, cEventSource :: ResumableSource IO Event -- ^ Event conduit source, and , streamHandle :: StreamHandle
-- its associated finalizer -- | Event conduit source, and its associated finalizer
, cFeatures :: !ServerFeatures -- ^ Features as advertised by the server , streamEventSource :: ResumableSource IO Event
, cHostName :: !(Maybe Text) -- ^ Hostname of the server -- | Stream features advertised by the server
, cJid :: !(Maybe Jid) -- ^ Our JID , streamFeatures :: !StreamFeatures -- TODO: Maybe?
, cPreferredLang :: !(Maybe LangTag) -- ^ Default language when no explicit -- | The hostname we specified for the connection
, streamHostname :: !(Maybe Text)
-- | The hostname specified in the server's stream element's
-- `from' attribute
, streamFrom :: !(Maybe Jid)
-- | The identifier specified in the server's stream element's
-- `id' attribute
, streamId :: !(Maybe Text)
-- | The language tag value specified in the server's stream
-- element's `langtag' attribute; will be a `Just' value once
-- connected to the server
-- TODO: Verify
, streamLang :: !(Maybe LangTag)
-- | Our JID as assigned by the server
, streamJid :: !(Maybe Jid)
-- TODO: Move the below fields to a configuration record
, preferredLang :: !(Maybe LangTag) -- ^ Default language when no explicit
-- language tag is set -- language tag is set
, cStreamLang :: !(Maybe LangTag) -- ^ Will be a `Just' value once connected , toJid :: !(Maybe Jid) -- ^ JID to include in the stream element's `to'
-- to the server.
, cStreamId :: !(Maybe Text) -- ^ Stream ID as specified by the server.
, cToJid :: !(Maybe Jid) -- ^ JID to include in the stream element's `to'
-- attribute when the connection is secured. See -- attribute when the connection is secured. See
-- also below. -- also below.
, cJidWhenPlain :: !Bool -- ^ Whether or not to also include the Jid when , includeJidWhenPlain :: !Bool -- ^ Whether or not to also include the Jid when
-- the connection is plain. -- the connection is plain.
, cFrom :: !(Maybe Jid) -- ^ From as specified by the server in the stream
-- element's `from' attribute.
} }
withConnection :: StateT Connection IO (Either XmppFailure c) -> TMVar Connection -> IO (Either XmppFailure c) ---------------
withConnection action con = bracketOnError -- JID
(atomically $ takeTMVar con) ---------------
(atomically . putTMVar con )
(\c -> do -- | A JID is XMPP\'s native format for addressing entities in the network. It
(r, c') <- runStateT action c -- is somewhat similar to an e-mail address but contains three parts instead of
atomically $ putTMVar con c' -- two.
return r 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
-- nonblocking version. Changes to the connection are ignored! -- the entity requesting and using network access provided by a
withConnection' :: StateT Connection IO (Either XmppFailure b) -> TMVar Connection -> IO (Either XmppFailure b) -- server (i.e., a local account), although it can also
withConnection' action con = do -- represent other kinds of entities (e.g., a chat room
con_ <- atomically $ readTMVar con -- associated with a multi-user chat service). The entity
(r, _) <- runStateT action con_ -- represented by an XMPP localpart is addressed within the
return r -- context of a specific domain (i.e.,
-- @localpart\@domainpart@).
localpart :: !(Maybe Text)
mkConnection :: Connection -> IO (TMVar Connection)
mkConnection con = {- Connection `fmap` -} (atomically $ newTMVar con) -- | 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
}
data StreamEnd = StreamEnd deriving (Typeable, Show)
instance Exception StreamEnd
data InvalidXmppXml = InvalidXmppXml String deriving (Show, Typeable)
instance Exception InvalidXmppXml

82
source/Network/Xmpp/Utilities.hs

@ -1,8 +1,9 @@
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Network.Xmpp.Utilities (idGenerator) where {-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Utilities (presTo, message, answerMessage, openElementToEvents, renderOpenElement, renderElement) where
import Network.Xmpp.Types import Network.Xmpp.Types
@ -10,10 +11,29 @@ import Control.Monad.STM
import Control.Concurrent.STM.TVar import Control.Concurrent.STM.TVar
import Prelude import Prelude
import Data.XML.Types
import qualified Data.Attoparsec.Text as AP import qualified Data.Attoparsec.Text as AP
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.ByteString as BS
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import System.IO.Unsafe(unsafePerformIO)
import Data.Conduit.List as CL
-- import Data.Typeable
import Control.Applicative ((<$>))
import Control.Exception
import Control.Monad.Trans.Class
import Data.Conduit as C
import Data.XML.Types
import qualified Text.XML.Stream.Render as TXSR
import Text.XML.Unresolved as TXU
-- TODO: Not used, and should probably be removed.
-- | Creates a new @IdGenerator@. Internally, it will maintain an infinite list -- | Creates a new @IdGenerator@. Internally, it will maintain an infinite list
-- of IDs ('[\'a\', \'b\', \'c\'...]'). The argument is a prefix to prepend the -- of IDs ('[\'a\', \'b\', \'c\'...]'). The argument is a prefix to prepend the
-- IDs with. Calling the function will extract an ID and update the generator's -- IDs with. Calling the function will extract an ID and update the generator's
@ -36,11 +56,11 @@ idGenerator prefix = atomically $ do
-- Generates an infinite and predictable list of IDs, all beginning with the -- Generates an infinite and predictable list of IDs, all beginning with the
-- provided prefix. Adds the prefix to all combinations of IDs (ids'). -- provided prefix. Adds the prefix to all combinations of IDs (ids').
ids :: Text.Text -> [Text.Text] ids :: Text.Text -> [Text.Text]
ids p = map (\ id -> Text.append p id) ids' ids p = Prelude.map (\ id -> Text.append p id) ids'
where where
-- Generate all combinations of IDs, with increasing length. -- Generate all combinations of IDs, with increasing length.
ids' :: [Text.Text] ids' :: [Text.Text]
ids' = map Text.pack $ concatMap ids'' [1..] ids' = Prelude.map Text.pack $ Prelude.concatMap ids'' [1..]
-- Generates all combinations of IDs with the given length. -- Generates all combinations of IDs with the given length.
ids'' :: Integer -> [String] ids'' :: Integer -> [String]
ids'' 0 = [""] ids'' 0 = [""]
@ -52,3 +72,55 @@ idGenerator prefix = atomically $ do
-- Constructs a "Version" based on the major and minor version numbers. -- Constructs a "Version" based on the major and minor version numbers.
versionFromNumbers :: Integer -> Integer -> Version versionFromNumbers :: Integer -> Integer -> Version
versionFromNumbers major minor = Version major minor 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
openElementToEvents :: Element -> [Event]
openElementToEvents (Element name as ns) = EventBeginElement name as : goN ns []
where
goE (Element name' as' ns') =
(EventBeginElement name' as' :)
. goN ns'
. (EventEndElement name' :)
goN [] = id
goN [x] = goN' x
goN (x:xs) = goN' x . goN xs
goN' (NodeElement e) = goE e
goN' (NodeInstruction i) = (EventInstruction i :)
goN' (NodeContent c) = (EventContent c :)
goN' (NodeComment t) = (EventComment t :)
renderOpenElement :: Element -> BS.ByteString
renderOpenElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO
$ CL.sourceList (openElementToEvents e) $$ TXSR.renderText def =$ CL.consume
renderElement :: Element -> BS.ByteString
renderElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO
$ CL.sourceList (elementToEvents e) $$ TXSR.renderText def =$ CL.consume
where
elementToEvents :: Element -> [Event]
elementToEvents e@(Element name _ _) = openElementToEvents e ++ [EventEndElement name]

43
source/Network/Xmpp/Xep/InbandRegistration.hs

@ -19,11 +19,7 @@ import qualified Data.Text as Text
import Data.XML.Pickle import Data.XML.Pickle
import qualified Data.XML.Types as XML import qualified Data.XML.Types as XML
import Network.Xmpp.Connection_ import Network.Xmpp.Internal
import Network.Xmpp.Pickle
import Network.Xmpp.Types
import Network.Xmpp.Basic
import Network.Xmpp
import Network.Xmpp.Xep.ServiceDiscovery import Network.Xmpp.Xep.ServiceDiscovery
@ -34,7 +30,7 @@ ibrns = "jabber:iq:register"
ibrName x = (XML.Name x (Just ibrns) Nothing) ibrName x = (XML.Name x (Just ibrns) Nothing)
data IbrError = IbrNotSupported data IbrError = IbrNotSupported
| IbrNoConnection | IbrNoStream
| IbrIQError IQError | IbrIQError IQError
| IbrTimeout | IbrTimeout
@ -50,9 +46,33 @@ data Query = Query { instructions :: Maybe Text.Text
emptyQuery = Query Nothing False False [] emptyQuery = Query Nothing False False []
query :: IQRequestType -> Query -> TMVar Connection -> IO (Either IbrError Query) -- supported :: XmppConMonad (Either IbrError Bool)
-- supported = runErrorT $ fromFeatures <+> fromDisco
-- where
-- fromFeatures = do
-- fs <- other <$> gets sFeatures
-- let fe = XML.Element
-- "{http://jabber.org/features/iq-register}register"
-- []
-- []
-- return $ fe `elem` fs
-- fromDisco = do
-- hn' <- gets sHostname
-- hn <- case hn' of
-- Just h -> return (Jid Nothing h Nothing)
-- Nothing -> throwError IbrNoStream
-- qi <- lift $ xmppQueryInfo Nothing Nothing
-- case qi of
-- Left e -> return False
-- Right qir -> return $ "jabber:iq:register" `elem` qiFeatures qir
-- f <+> g = do
-- r <- f
-- if r then return True else g
query :: IQRequestType -> Query -> TMVar Stream -> IO (Either IbrError Query)
query queryType x con = do query queryType x con = do
answer <- pushIQ' "ibr" Nothing queryType Nothing (pickleElem xpQuery x) con answer <- pushIQ "ibr" Nothing queryType Nothing (pickleElem xpQuery x) con
case answer of case answer of
Right IQResult{iqResultPayload = Just b} -> Right IQResult{iqResultPayload = Just b} ->
case unpickleElem xpQuery b of case unpickleElem xpQuery b of
@ -93,7 +113,7 @@ mapError f = mapErrorT (liftM $ left f)
-- | Retrieve the necessary fields and fill them in to register an account with -- | Retrieve the necessary fields and fill them in to register an account with
-- the server. -- the server.
registerWith :: [(Field, Text.Text)] registerWith :: [(Field, Text.Text)]
-> TMVar Connection -> TMVar Stream
-> IO (Either RegisterError Query) -> IO (Either RegisterError Query)
registerWith givenFields con = runErrorT $ do registerWith givenFields con = runErrorT $ do
fs <- mapError IbrError . ErrorT $ requestFields con fs <- mapError IbrError . ErrorT $ requestFields con
@ -125,7 +145,7 @@ deleteAccount host hostname port username password = do
-- | Terminate your account on the server. You have to be logged in for this to -- | Terminate your account on the server. You have to be logged in for this to
-- work. You connection will most likely be terminated after unregistering. -- work. You connection will most likely be terminated after unregistering.
unregister :: TMVar Connection -> IO (Either IbrError Query) unregister :: TMVar Stream -> IO (Either IbrError Query)
unregister = query Set $ emptyQuery {remove = True} unregister = query Set $ emptyQuery {remove = True}
unregister' :: Session -> IO (Either IbrError Query) unregister' :: Session -> IO (Either IbrError Query)
@ -216,3 +236,6 @@ instance Read Field where
-- Registered -- Registered
-- Instructions -- Instructions
ppElement :: Element -> String
ppElement = Text.unpack . Text.decodeUtf8 . renderElement

34
source/Network/Xmpp/Xep/ServiceDiscovery.hs

@ -25,11 +25,7 @@ import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp import Network.Xmpp
import Network.Xmpp.Concurrent import Network.Xmpp.Internal
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection_
import Network.Xmpp.Pickle
import Network.Xmpp.Types
import Control.Concurrent.STM.TMVar import Control.Concurrent.STM.TMVar
data DiscoError = DiscoNoQueryElement data DiscoError = DiscoNoQueryElement
@ -105,10 +101,10 @@ queryInfo to node context = do
xmppQueryInfo :: Maybe Jid xmppQueryInfo :: Maybe Jid
-> Maybe Text.Text -> Maybe Text.Text
-> TMVar Connection -> TMVar Stream
-> IO (Either DiscoError QueryInfoResult) -> IO (Either DiscoError QueryInfoResult)
xmppQueryInfo to node con = do xmppQueryInfo to node con = do
res <- pushIQ' "info" to Get Nothing queryBody con res <- pushIQ "info" to Get Nothing queryBody con
return $ case res of return $ case res of
Left e -> Left $ DiscoIQError Nothing Left e -> Left $ DiscoIQError Nothing
Right res' -> case res' of Right res' -> case res' of
@ -167,3 +163,27 @@ queryItems to node session = do
Right r -> Right r Right r -> Right r
where where
queryBody = pickleElem xpQueryItems (node, []) queryBody = pickleElem xpQueryItems (node, [])
-- 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 -> Prelude.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))
}
xpLangTag :: PU [Attribute] (Maybe LangTag)
xpLangTag = xpAttrImplied xmlLang xpPrim
xmlLang :: Name
xmlLang = Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml")

108
source/Text/Xml/Stream/Elements.hs

@ -1,108 +0,0 @@
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Xml.Stream.Elements where
import Control.Applicative ((<$>))
import Control.Exception
import Control.Monad.Trans.Class
import Control.Monad.Trans.Resource as R
import qualified Data.ByteString as BS
import Data.Conduit as C
import Data.Conduit.List as CL
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Typeable
import Data.XML.Types
import System.IO.Unsafe(unsafePerformIO)
import qualified Text.XML.Stream.Render as TXSR
import Text.XML.Unresolved as TXU
compressNodes :: [Node] -> [Node]
compressNodes [] = []
compressNodes [x] = [x]
compressNodes (NodeContent (ContentText x) : NodeContent (ContentText y) : z) =
compressNodes $ NodeContent (ContentText $ x `Text.append` y) : z
compressNodes (x:xs) = x : compressNodes xs
streamName :: Name
streamName =
(Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream"))
data StreamEnd = StreamEnd deriving (Typeable, Show)
instance Exception StreamEnd
data InvalidXmppXml = InvalidXmppXml String deriving (Show, Typeable)
instance Exception InvalidXmppXml
parseElement txt = documentRoot $ TXU.parseText_ TXU.def txt
elements :: R.MonadThrow m => C.Conduit Event m Element
elements = do
x <- C.await
case x of
Just (EventBeginElement n as) -> do
goE n as >>= C.yield
elements
Just (EventEndElement streamName) -> lift $ R.monadThrow StreamEnd
Nothing -> return ()
_ -> lift $ R.monadThrow $ InvalidXmppXml $ "not an element: " ++ show x
where
many' f =
go id
where
go front = do
x <- f
case x of
Left x -> return $ (x, front [])
Right y -> go (front . (:) y)
goE n as = do
(y, ns) <- many' goN
if y == Just (EventEndElement n)
then return $ Element n as $ compressNodes ns
else lift $ R.monadThrow $ InvalidXmppXml $
"Missing close tag: " ++ show n
goN = do
x <- await
case x of
Just (EventBeginElement n as) -> (Right . NodeElement) <$> goE n as
Just (EventInstruction i) -> return $ Right $ NodeInstruction i
Just (EventContent c) -> return $ Right $ NodeContent c
Just (EventComment t) -> return $ Right $ NodeComment t
Just (EventCDATA t) -> return $ Right $ NodeContent $ ContentText t
_ -> return $ Left x
openElementToEvents :: Element -> [Event]
openElementToEvents (Element name as ns) = EventBeginElement name as : goN ns []
where
goE (Element name' as' ns') =
(EventBeginElement name' as' :)
. goN ns'
. (EventEndElement name' :)
goN [] = id
goN [x] = goN' x
goN (x:xs) = goN' x . goN xs
goN' (NodeElement e) = goE e
goN' (NodeInstruction i) = (EventInstruction i :)
goN' (NodeContent c) = (EventContent c :)
goN' (NodeComment t) = (EventComment t :)
elementToEvents :: Element -> [Event]
elementToEvents e@(Element name _ _) = openElementToEvents e ++ [EventEndElement name]
renderOpenElement :: Element -> BS.ByteString
renderOpenElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO
$ CL.sourceList (openElementToEvents e) $$ TXSR.renderText def =$ CL.consume
renderElement :: Element -> BS.ByteString
renderElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO
$ CL.sourceList (elementToEvents e) $$ TXSR.renderText def =$ CL.consume
ppElement :: Element -> String
ppElement = Text.unpack . Text.decodeUtf8 . renderElement
Loading…
Cancel
Save