Browse Source

Merge pull request #5 from Philonous/master

incremental changes
master
Jon Kristensen 14 years ago
parent
commit
b8a5df3faa
  1. 1
      .gitignore
  2. 24
      pontarius.cabal
  3. 20
      src/Data/Conduit/BufferedSource.hs
  4. 9
      src/Data/Conduit/TLS.hs
  5. 154
      src/Network/XMPP.hs
  6. 11
      src/Network/XMPP/Bind.hs
  7. 17
      src/Network/XMPP/Concurrent.hs
  8. 8
      src/Network/XMPP/Concurrent/IQ.hs
  9. 131
      src/Network/XMPP/Concurrent/Monad.hs
  10. 112
      src/Network/XMPP/Concurrent/Threads.hs
  11. 30
      src/Network/XMPP/Concurrent/Types.hs
  12. 292
      src/Network/XMPP/JID.hs
  13. 38
      src/Network/XMPP/Marshal.hs
  14. 18
      src/Network/XMPP/Message.hs
  15. 97
      src/Network/XMPP/Monad.hs
  16. 27
      src/Network/XMPP/Pickle.hs
  17. 15
      src/Network/XMPP/Presence.hs
  18. 116
      src/Network/XMPP/SASL.hs
  19. 24
      src/Network/XMPP/Session.hs
  20. 10
      src/Network/XMPP/Stream.hs
  21. 36
      src/Network/XMPP/TLS.hs
  22. 384
      src/Network/XMPP/Types.hs
  23. 149
      src/Network/XMPP/Utilities.hs
  24. 45
      src/Tests.hs
  25. 1
      src/Text/XML/Stream/Elements.hs
  26. 17
      tests/Stanzas.hs

1
.gitignore vendored

@ -1,5 +1,6 @@
dist/ dist/
cabal-dev/ cabal-dev/
wiki/
*.o *.o
*.hi *.hi
*~ *~

24
pontarius.cabal

@ -12,7 +12,7 @@ Stability: alpha
Bug-Reports: mailto:jon.kristensen@nejla.com Bug-Reports: mailto:jon.kristensen@nejla.com
-- Package-URL: -- Package-URL:
Synopsis: An incomplete implementation of RFC 6120 (XMPP: Core) Synopsis: An incomplete implementation of RFC 6120 (XMPP: Core)
Description: Pontarius is a work in progress of an implementation of Description: Pontarius is a work in progress implementation of
RFC 6120 (XMPP: Core). RFC 6120 (XMPP: Core).
Category: Network Category: Network
Tested-With: GHC == 7.4.1 Tested-With: GHC == 7.4.1
@ -51,17 +51,25 @@ Library
, data-default -any , data-default -any
, stringprep >= 0.1.5 , stringprep >= 0.1.5
Exposed-modules: Network.XMPP Exposed-modules: Network.XMPP
, Network.XMPP.Types , Network.XMPP.Bind
, Network.XMPP.SASL , Network.XMPP.Concurrent
, Network.XMPP.Stream
, Network.XMPP.Pickle
, Network.XMPP.Marshal , Network.XMPP.Marshal
, Network.XMPP.Monad , Network.XMPP.Monad
, Network.XMPP.Concurrent , Network.XMPP.Message
, Network.XMPP.TLS , Network.XMPP.Pickle
, Network.XMPP.Bind , Network.XMPP.Presence
, Network.XMPP.SASL
, Network.XMPP.Session , Network.XMPP.Session
, Network.XMPP.Stream
, Network.XMPP.TLS
, Network.XMPP.Types
Other-modules: Network.XMPP.JID
, Network.XMPP.Concurrent.Types
, Network.XMPP.Concurrent.IQ
, Network.XMPP.Concurrent.Threads
, Network.XMPP.Concurrent.Monad
, Text.XML.Stream.Elements , Text.XML.Stream.Elements
, Data.Conduit.BufferedSource
, Data.Conduit.TLS , Data.Conduit.TLS
GHC-Options: -Wall GHC-Options: -Wall

20
src/Data/Conduit/BufferedSource.hs

@ -0,0 +1,20 @@
module Data.Conduit.BufferedSource where
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Data.IORef
import Data.Conduit
import qualified Data.Conduit.List as CL
-- | Buffered source from conduit 0.3
bufferSource :: MonadIO m => Source m o -> IO (Source m o)
bufferSource s = do
srcRef <- newIORef s
return $ do
src <- liftIO $ readIORef srcRef
let go src = do
(src', res) <- lift $ src $$+ CL.head
case res of
Nothing -> return ()
Just x -> liftIO (writeIORef srcRef src') >> yield x >> go src'
in go src

9
src/Data/Conduit/TLS.hs

@ -1,4 +1,5 @@
{-# Language NoMonomorphismRestriction #-} {-# Language NoMonomorphismRestriction #-}
{-# OPTIONS_HADDOCK hide #-}
module Data.Conduit.TLS module Data.Conduit.TLS
( tlsinit ( tlsinit
-- , conduitStdout -- , conduitStdout
@ -26,7 +27,9 @@ tlsinit
TLSParams TLSParams
-> Handle -> m ( Source m1 BS.ByteString -> Handle -> m ( Source m1 BS.ByteString
, Sink BS.ByteString m1 () , Sink BS.ByteString m1 ()
, BS.ByteString -> IO ()) , BS.ByteString -> IO ()
, TLSCtx Handle
)
tlsinit tlsParams handle = do tlsinit tlsParams handle = do
gen <- liftIO $ (newGenIO :: IO SystemRandom) -- TODO: Find better random source? gen <- liftIO $ (newGenIO :: IO SystemRandom) -- TODO: Find better random source?
clientContext <- client tlsParams gen handle clientContext <- client tlsParams gen handle
@ -41,5 +44,7 @@ tlsinit tlsParams handle = do
(\_ -> return ()) (\_ -> return ())
return ( src return ( src
, snk , snk
, \s -> sendData clientContext $ BL.fromChunks [s] ) , \s -> sendData clientContext $ BL.fromChunks [s]
, clientContext
)

154
src/Network/XMPP.hs

@ -13,9 +13,12 @@
-- Stability: unstable -- Stability: unstable
-- Portability: portable -- Portability: portable
-- --
-- XMPP is an open standard, extendable, and secure communications -- The Extensible Messaging and Presence Protocol (XMPP) is an open technology for
-- protocol designed on top of XML, TLS, and SASL. Pontarius XMPP is -- real-time communication, which powers a wide range of applications including
-- an XMPP client library, implementing the core capabilities of XMPP -- instant messaging, presence, multi-party chat, voice and video calls,
-- collaboration, lightweight middleware, content syndication, and generalized
-- routing of XML data.
-- Pontarius an XMPP client library, implementing the core capabilities of XMPP
-- (RFC 6120). -- (RFC 6120).
-- --
-- Developers using this library are assumed to understand how XMPP -- Developers using this library are assumed to understand how XMPP
@ -30,36 +33,147 @@
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} {-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
module Network.XMPP module Network.XMPP
( module Network.XMPP.Bind ( -- * Session management
, module Network.XMPP.Concurrent withNewSession
, module Network.XMPP.Monad , withSession
, module Network.XMPP.SASL , newSession
, module Network.XMPP.Session , withConnection
, module Network.XMPP.Stream , connect
, module Network.XMPP.TLS , startTLS
, module Network.XMPP.Types , auth
, endSession
, setSessionEndHandler
-- * JID
-- | A JID (historically: Jabber ID) is XMPPs native format
-- for addressing entities in the network. It is somewhat similar to an
-- email-address but contains three parts instead of two:
, JID(..)
-- * Stanzas
-- | @Stanzas@ are the the smallest unit of communication in @XMPP@. They
-- come in 3 flavors:
--
-- * @'Message'@, for traditional IM-style message passing between peers
--
-- * @'Presence'@, for communicating status updates
--
-- * IQ (info/query), with a request-response semantics
--
-- All stanza types have the following attributes in common:
--
-- * The /id/ attribute is used by the originating entity to track
-- any response or error stanza that it might receive in relation to
-- the generated stanza from another entity (such as an intermediate
-- server or the intended recipient). It is up to the originating
-- entity whether the value of the 'id' attribute is unique only
-- within its current stream or unique globally.
--
-- * The /from/ attribute specifies the JID of the sender.
--
-- * The /to/ attribute specifies the JID of the intended recipient
-- for the stanza.
--
-- * The /type/ attribute specifies the purpose or context of the
-- message, presence, or IQ stanza. The particular allowable values
-- for the 'type' attribute vary depending on whether the stanza is
-- a message, presence, or IQ stanza.
-- ** Messages
-- | The /message/ stanza is a /push/ mechanism whereby one entity pushes
-- information to another entity, similar to the communications that occur in
-- a system such as email.
--
-- <http://xmpp.org/rfcs/rfc6120.html#stanzas-semantics-message>
, Message
, MessageError
, MessageType(..)
-- *** creating
, simpleMessage
, answerMessage
-- *** sending
, sendMessage
-- *** receiving
, pullMessage
, waitForMessage
, waitForMessageError
, filterMessages
-- ** Presence
-- | The /presence/ stanza is a specialized /broadcast/
-- or /publish-subscribe/ mechanism, whereby multiple entities
-- receive information about an entity to which they have
-- subscribed.
--
-- <http://xmpp.org/rfcs/rfc6120.html#stanzas-semantics-presence>
, Presence(..)
, PresenceError(..)
, ShowType(..)
-- *** creating
, module Network.XMPP.Presence , module Network.XMPP.Presence
, module Network.XMPP.Message -- *** sending
, xmppConnect , sendPresence
, xmppNewSession -- *** receiving
, pullPresence
, waitForPresence
-- ** IQ
-- | Info\/Query, or IQ, is a /request-response/ mechanism, similar in some
-- ways to the Hypertext Transfer Protocol @HTTP@. The semantics of IQ enable
-- an entity to make a request of, and receive a response from, another
-- entity. The data content and precise semantics of the request and response
-- is defined by the schema or other structural definition associated with the
-- XML namespace that
-- qualifies the direct child element of the IQ element. IQ interactions
-- follow a common pattern of structured data
-- exchange such as get/result or set/result (although an error can be returned
-- in reply to a request if appropriate)
--
-- <http://xmpp.org/rfcs/rfc6120.html#stanzas-semantics-iq>
, IQRequest(..)
, IQRequestType(..)
, IQResult(..)
, IQError(..)
, sendIQ
, sendIQ'
, answerIQ
, listenIQChan
, iqRequestPayload
, iqResultPayload
-- * Threads
, XMPP
, fork
, forkSession
-- * Misc
, exampleParams
) where ) where
import Data.Text as Text import Data.Text as Text
import Network import Network
import qualified Network.TLS as TLS
import Network.XMPP.Bind import Network.XMPP.Bind
import Network.XMPP.Concurrent import Network.XMPP.Concurrent
import Network.XMPP.Message import Network.XMPP.Message hiding (message)
import Network.XMPP.Monad import Network.XMPP.Monad
import Network.XMPP.Presence import Network.XMPP.Presence hiding (presence)
import Network.XMPP.SASL import Network.XMPP.SASL
import Network.XMPP.Session import Network.XMPP.Session
import Network.XMPP.Stream import Network.XMPP.Stream
import Network.XMPP.TLS import Network.XMPP.TLS
import Network.XMPP.Types import Network.XMPP.Types
xmppConnect :: HostName -> Text -> XMPPConMonad (Either StreamError ()) import Control.Monad.Error
xmppConnect address hostname = xmppRawConnect address hostname >> xmppStartStream
-- | Connect to host with given address.
connect :: HostName -> Text -> XMPPConMonad (Either StreamError ())
connect address hostname = xmppRawConnect address hostname >> xmppStartStream
xmppNewSession :: XMPPThread a -> IO (a, XMPPConState) -- | Authenticate to the server with the given username and password
xmppNewSession = withNewSession . runThreaded -- and bind a resource
auth :: Text.Text -- ^ The username
-> Text.Text -- ^ The password
-> Maybe Text -- ^ The desired resource or 'Nothing' to let the server
-- assign one
-> XMPPConMonad (Either AuthError Text.Text)
auth username passwd resource = runErrorT $ do
ErrorT $ xmppSASL username passwd
res <- lift $ xmppBind resource
lift $ xmppStartSession
return res

11
src/Network/XMPP/Bind.hs

@ -11,7 +11,7 @@ import Data.XML.Types
import Network.XMPP.Types import Network.XMPP.Types
import Network.XMPP.Pickle import Network.XMPP.Pickle
import Network.XMPP.Concurrent import Network.XMPP.Monad
-- A `bind' element. -- A `bind' element.
@ -29,7 +29,6 @@ bindBody rsrc = (pickleElem
rsrc rsrc
) )
-- Extracts the character data in the `jid' element. -- Extracts the character data in the `jid' element.
jidP :: PU [Node] JID jidP :: PU [Node] JID
@ -39,10 +38,10 @@ jidP = bindP $ xpElemNodes "jid" (xpContent xpPrim)
-- Sends a (synchronous) IQ set request for a (`Just') given or -- Sends a (synchronous) IQ set request for a (`Just') given or
-- server-generated resource and extract the JID from the non-error -- server-generated resource and extract the JID from the non-error
-- response. -- response.
xmppBind :: Maybe Text -> XMPPConMonad Text
xmppThreadedBind :: Maybe Text -> XMPPThread Text xmppBind rsrc = do
xmppThreadedBind rsrc = do answer <- xmppSendIQ' "bind" Nothing Set Nothing (bindBody rsrc)
answer <- sendIQ' Nothing Set Nothing (bindBody rsrc)
let (Right IQResult{iqResultPayload = Just b}) = answer -- TODO: Error handling let (Right IQResult{iqResultPayload = Just b}) = answer -- TODO: Error handling
let Right (JID _n _d (Just r)) = unpickleElem jidP b let Right (JID _n _d (Just r)) = unpickleElem jidP b
return r return r

17
src/Network/XMPP/Concurrent.hs

@ -1,18 +1,13 @@
module Network.XMPP.Concurrent module Network.XMPP.Concurrent
( module Network.XMPP.Concurrent.Types ( Session
, module Network.XMPP.Concurrent.Monad , XMPP
, module Network.XMPP.Concurrent.Threads , module Network.XMPP.Concurrent.Monad
, module Network.XMPP.Concurrent.IQ , module Network.XMPP.Concurrent.Threads
) where , module Network.XMPP.Concurrent.IQ
) where
import Network.XMPP.Concurrent.Types import Network.XMPP.Concurrent.Types
import Network.XMPP.Concurrent.Monad import Network.XMPP.Concurrent.Monad
import Network.XMPP.Concurrent.Threads import Network.XMPP.Concurrent.Threads
import Network.XMPP.Concurrent.IQ import Network.XMPP.Concurrent.IQ

8
src/Network/XMPP/Concurrent/IQ.hs

@ -17,7 +17,7 @@ sendIQ :: Maybe JID -- ^ Recipient (to)
-> IQRequestType -- ^ IQ type (Get or Set) -> IQRequestType -- ^ IQ type (Get or Set)
-> Maybe LangTag -- ^ Language tag of the payload (Nothing for default) -> Maybe LangTag -- ^ Language tag of the payload (Nothing for default)
-> Element -- ^ The iq body (there has to be exactly one) -> Element -- ^ The iq body (there has to be exactly one)
-> XMPPThread (TMVar IQResponse) -> XMPP (TMVar IQResponse)
sendIQ to tp lang body = do -- TODO: add timeout sendIQ to tp lang body = do -- TODO: add timeout
newId <- liftIO =<< asks idGenerator newId <- liftIO =<< asks idGenerator
handlers <- asks iqHandlers handlers <- asks iqHandlers
@ -27,7 +27,7 @@ sendIQ to tp lang body = do -- TODO: add timeout
writeTVar handlers (byNS, Map.insert newId resRef byId) writeTVar handlers (byNS, Map.insert newId resRef byId)
-- TODO: Check for id collisions (shouldn't happen?) -- TODO: Check for id collisions (shouldn't happen?)
return resRef return resRef
sendS . IQRequestS $ IQRequest newId Nothing to lang tp body sendStanza . IQRequestS $ IQRequest newId Nothing to lang tp body
return ref return ref
-- | like 'sendIQ', but waits for the answer IQ -- | like 'sendIQ', but waits for the answer IQ
@ -35,14 +35,14 @@ sendIQ' :: Maybe JID
-> IQRequestType -> IQRequestType
-> Maybe LangTag -> Maybe LangTag
-> Element -> Element
-> XMPPThread IQResponse -> XMPP IQResponse
sendIQ' to tp lang body = do sendIQ' to tp lang body = do
ref <- sendIQ to tp lang body ref <- sendIQ to tp lang body
liftIO . atomically $ takeTMVar ref liftIO . atomically $ takeTMVar ref
answerIQ :: (IQRequest, TVar Bool) answerIQ :: (IQRequest, TVar Bool)
-> Either StanzaError (Maybe Element) -> Either StanzaError (Maybe Element)
-> XMPPThread Bool -> XMPP Bool
answerIQ ((IQRequest iqid from _to lang _tp bd), sentRef) answer = do answerIQ ((IQRequest iqid from _to lang _tp bd), sentRef) answer = do
out <- asks outCh out <- asks outCh
let response = case answer of let response = case answer of

131
src/Network/XMPP/Concurrent/Monad.hs

@ -14,28 +14,33 @@ import qualified Data.Map as Map
import Data.Text(Text) import Data.Text(Text)
import Network.XMPP.Concurrent.Types import Network.XMPP.Concurrent.Types
import Network.XMPP.Monad
-- | Register a new IQ listener. IQ requests matching the type and namespace will -- | Register a new IQ listener. IQ requests matching the type and namespace will
-- be put in the channel. -- be put in the channel.
--
-- Return the new channel or Nothing if this namespace/'IQRequestType'
-- combination was alread handled
listenIQChan :: IQRequestType -- ^ type of IQs to receive (Get / Set) listenIQChan :: IQRequestType -- ^ type of IQs to receive (Get / Set)
-> Text -- ^ namespace of the child element -> Text -- ^ namespace of the child element
-> XMPPThread (Bool, TChan (IQRequest, TVar Bool)) -> XMPP (Maybe ( TChan (IQRequest, TVar Bool)))
listenIQChan tp ns = do listenIQChan tp ns = do
handlers <- asks iqHandlers handlers <- asks iqHandlers
liftIO . atomically $ do liftIO . atomically $ do
(byNS, byID) <- readTVar handlers (byNS, byID) <- readTVar handlers
iqCh <- newTChan iqCh <- newTChan
let (present, byNS') = Map.insertLookupWithKey' (\_ new _ -> new) let (present, byNS') = Map.insertLookupWithKey' (\_ _ old -> old)
(tp,ns) iqCh byNS (tp,ns) iqCh byNS
writeTVar handlers (byNS', byID) writeTVar handlers (byNS', byID)
return $ case present of return $ case present of
Nothing -> (True, iqCh) Nothing -> Just iqCh
Just iqCh' -> (False, iqCh') Just _iqCh' -> Nothing
-- | get the inbound stanza channel, duplicates from master if necessary -- | get the inbound stanza channel, duplicates from master if necessary
-- please note that once duplicated it will keep filling up, call -- please note that once duplicated it will keep filling up, call
-- 'dropMessageChan' to allow it to be garbage collected -- 'dropMessageChan' to allow it to be garbage collected
getMessageChan :: XMPPThread (TChan (Either MessageError Message)) getMessageChan :: XMPP (TChan (Either MessageError Message))
getMessageChan = do getMessageChan = do
mChR <- asks messagesRef mChR <- asks messagesRef
mCh <- liftIO $ readIORef mChR mCh <- liftIO $ readIORef mChR
@ -48,7 +53,7 @@ getMessageChan = do
Just mCh' -> return mCh' Just mCh' -> return mCh'
-- | see 'getMessageChan' -- | see 'getMessageChan'
getPresenceChan :: XMPPThread (TChan (Either PresenceError Presence)) getPresenceChan :: XMPP (TChan (Either PresenceError Presence))
getPresenceChan = do getPresenceChan = do
pChR <- asks presenceRef pChR <- asks presenceRef
pCh <- liftIO $ readIORef pChR pCh <- liftIO $ readIORef pChR
@ -62,51 +67,55 @@ getPresenceChan = do
-- | Drop the local end of the inbound stanza channel -- | Drop the local end of the inbound stanza channel
-- from our context so it can be GC-ed -- from our context so it can be GC-ed
dropMessageChan :: XMPPThread () dropMessageChan :: XMPP ()
dropMessageChan = do dropMessageChan = do
r <- asks messagesRef r <- asks messagesRef
liftIO $ writeIORef r Nothing liftIO $ writeIORef r Nothing
-- | see 'dropMessageChan' -- | see 'dropMessageChan'
dropPresenceChan :: XMPPThread () dropPresenceChan :: XMPP ()
dropPresenceChan = do dropPresenceChan = do
r <- asks presenceRef r <- asks presenceRef
liftIO $ writeIORef r Nothing liftIO $ writeIORef r Nothing
-- | Read an element from the inbound stanza channel, acquiring a copy -- | Read an element from the inbound stanza channel, acquiring a copy
-- of the channel as necessary -- of the channel as necessary
pullMessage :: XMPPThread (Either MessageError Message) pullMessage :: XMPP (Either MessageError Message)
pullMessage = do pullMessage = do
c <- getMessageChan c <- getMessageChan
liftIO $ atomically $ readTChan c liftIO $ atomically $ readTChan c
-- | Read an element from the inbound stanza channel, acquiring a copy -- | Read an element from the inbound stanza channel, acquiring a copy
-- of the channel as necessary -- of the channel as necessary
pullPresence :: XMPPThread (Either PresenceError Presence) pullPresence :: XMPP (Either PresenceError Presence)
pullPresence = do pullPresence = do
c <- getPresenceChan c <- getPresenceChan
liftIO $ atomically $ readTChan c liftIO $ atomically $ readTChan c
-- | Send a stanza to the server -- | Send a stanza to the server
sendS :: Stanza -> XMPPThread () sendStanza :: Stanza -> XMPP ()
sendS a = do sendStanza a = do
out <- asks outCh out <- asks outCh
liftIO . atomically $ writeTChan out a liftIO . atomically $ writeTChan out a
return () return ()
-- | Create a forked session object without forking a thread
forkSession :: Session -> IO Session
forkSession sess = do
mCH' <- newIORef Nothing
pCH' <- newIORef Nothing
return $ sess {messagesRef = mCH' ,presenceRef = pCH'}
-- | Fork a new thread -- | Fork a new thread
forkXMPP :: XMPPThread () -> XMPPThread ThreadId fork :: XMPP () -> XMPP ThreadId
forkXMPP a = do fork a = do
thread <- ask sess <- ask
mCH' <- liftIO $ newIORef Nothing sess' <- liftIO $ forkSession sess
pCH' <- liftIO $ newIORef Nothing liftIO $ forkIO $ runReaderT a sess'
liftIO $ forkIO $ runReaderT a (thread {messagesRef = mCH'
,presenceRef = pCH'
})
filterMessages :: (MessageError -> Bool) filterMessages :: (MessageError -> Bool)
-> (Message -> Bool) -> (Message -> Bool)
-> XMPPThread (Either MessageError Message) -> XMPP (Either MessageError Message)
filterMessages f g = do filterMessages f g = do
s <- pullMessage s <- pullMessage
case s of case s of
@ -115,7 +124,7 @@ filterMessages f g = do
Right m | g m -> return $ Right m Right m | g m -> return $ Right m
| otherwise -> filterMessages f g | otherwise -> filterMessages f g
waitForMessage :: (Message -> Bool) -> XMPPThread Message waitForMessage :: (Message -> Bool) -> XMPP Message
waitForMessage f = do waitForMessage f = do
s <- pullMessage s <- pullMessage
case s of case s of
@ -123,7 +132,7 @@ waitForMessage f = do
Right m | f m -> return m Right m | f m -> return m
| otherwise -> waitForMessage f | otherwise -> waitForMessage f
waitForMessageError :: (MessageError -> Bool) -> XMPPThread MessageError waitForMessageError :: (MessageError -> Bool) -> XMPP MessageError
waitForMessageError f = do waitForMessageError f = do
s <- pullMessage s <- pullMessage
case s of case s of
@ -131,7 +140,7 @@ waitForMessageError f = do
Left m | f m -> return m Left m | f m -> return m
| otherwise -> waitForMessageError f | otherwise -> waitForMessageError f
waitForPresence :: (Presence -> Bool) -> XMPPThread Presence waitForPresence :: (Presence -> Bool) -> XMPP Presence
waitForPresence f = do waitForPresence f = do
s <- pullPresence s <- pullPresence
case s of case s of
@ -143,27 +152,69 @@ waitForPresence f = do
-- Reader and writer workers will be temporarily stopped -- Reader and writer workers will be temporarily stopped
-- and resumed with the new session details once the action returns. -- and resumed with the new session details once the action returns.
-- The Action will run in the calling thread/ -- The Action will run in the calling thread/
-- NB: This will /not/ catch any exceptions. If you action dies, deadlocks -- Any uncaught exceptions will be interpreted as connection failure
-- or otherwisely exits abnormaly the XMPP session will be dead. withConnection :: XMPPConMonad a -> XMPP a
withConnection :: XMPPConMonad a -> XMPPThread a
withConnection a = do withConnection a = do
readerId <- asks readerThread readerId <- asks readerThread
stateRef <- asks conStateRef stateRef <- asks conStateRef
write <- asks writeRef write <- asks writeRef
wait <- liftIO $ newEmptyTMVarIO wait <- liftIO $ newEmptyTMVarIO
liftIO . throwTo readerId $ Interrupt wait liftIO . Ex.mask_ $ do
s <- liftIO . atomically $ do throwTo readerId $ Interrupt wait
s <- Ex.catch ( atomically $ do
_ <- takeTMVar write
s <- takeTMVar stateRef
putTMVar wait () putTMVar wait ()
takeTMVar write return s
takeTMVar stateRef )
(res, s') <- liftIO $ runStateT a s (\e -> atomically (putTMVar wait ())
liftIO . atomically $ do >> Ex.throwIO (e :: Ex.SomeException)
putTMVar write (sConPushBS s') -- No MVar taken
putTMVar stateRef s' )
Ex.catch ( do
(res, s') <- runStateT a s
atomically $ do
_ <- tryPutTMVar write (sConPushBS s')
_ <- tryPutTMVar stateRef s'
return ()
return res return res
)
-- we treat all Exceptions as fatal
(\e -> runStateT xmppKillConnection s
>> Ex.throwIO (e :: Ex.SomeException)
)
-- | Send a presence Stanza
sendPresence :: Presence -> XMPP ()
sendPresence = sendStanza . PresenceS
-- | Send a Message Stanza
sendMessage :: Message -> XMPP ()
sendMessage = sendStanza . MessageS
modifyHandlers :: (EventHandlers -> EventHandlers) -> XMPP ()
modifyHandlers f = do
eh <- asks eventHandlers
liftIO . atomically $ writeTVar eh . f =<< readTVar eh
setSessionEndHandler :: XMPP () -> XMPP ()
setSessionEndHandler eh = modifyHandlers (\s -> s{sessionEndHandler = eh})
-- | run an event handler
runHandler :: (EventHandlers -> XMPP a) -> XMPP a
runHandler h = do
eh <- liftIO . atomically . readTVar =<< asks eventHandlers
h eh
-- | End the current xmpp session
endSession :: XMPP ()
endSession = do -- TODO: This has to be idempotent (is it?)
withConnection xmppKillConnection
liftIO =<< asks stopThreads
runHandler sessionEndHandler
-- | Close the connection to the server
closeConnection :: XMPP ()
closeConnection = withConnection xmppKillConnection
sendPresence :: Presence -> XMPPThread ()
sendPresence = sendS . PresenceS
sendMessage :: Message -> XMPPThread ()
sendMessage = sendS . MessageS

112
src/Network/XMPP/Concurrent/Threads.hs

@ -10,18 +10,13 @@ import Control.Concurrent.STM
import qualified Control.Exception.Lifted as Ex import qualified Control.Exception.Lifted as Ex
import Control.Monad import Control.Monad
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans
import Control.Monad.Reader import Control.Monad.Reader
import Control.Monad.State.Strict import Control.Monad.State.Strict
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.Conduit
import qualified Data.Conduit.List as CL
import Data.Default (def)
import Data.IORef import Data.IORef
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Maybe import Data.Maybe
import qualified Data.Text as Text
import Data.XML.Types import Data.XML.Types
@ -31,38 +26,46 @@ import Network.XMPP.Pickle
import Network.XMPP.Concurrent.Types import Network.XMPP.Concurrent.Types
import Text.XML.Stream.Elements import Text.XML.Stream.Elements
import qualified Text.XML.Stream.Render as XR
import GHC.IO (unsafeUnmask)
-- While waiting for the first semaphore(s) to flip we might receive
-- another interrupt. When that happens we add it's semaphore to the
-- list and retry waiting
handleInterrupts :: [TMVar ()] -> IO [()]
handleInterrupts ts =
Ex.catch (atomically $ forM ts takeTMVar)
( \(Interrupt t) -> handleInterrupts (t:ts))
readWorker :: TChan (Either MessageError Message) readWorker :: TChan (Either MessageError Message)
-> TChan (Either PresenceError Presence) -> TChan (Either PresenceError Presence)
-> TVar IQHandlers -> TVar IQHandlers
-> TMVar XMPPConState -> TMVar XmppConnection
-> IO () -> IO ()
readWorker messageC presenceC handlers stateRef = readWorker messageC presenceC handlers stateRef =
Ex.mask_ . forever $ do Ex.mask_ . forever $ do
s <- liftIO . atomically $ takeTMVar stateRef res <- liftIO $ Ex.catch ( do
(sta', s') <- flip runStateT s $ Ex.catch ( do -- we don't know whether pull will
-- we don't know whether pull will necessarily be interruptible -- necessarily be interruptible
liftIO $ Ex.allowInterrupt s <- liftIO . atomically $ readTMVar stateRef
Just <$> pull allowInterrupt
Just <$> runStateT pullStanza s
) )
(\(Interrupt t) -> do (\(Interrupt t) -> do
liftIO . atomically $ void $ handleInterrupts [t]
putTMVar stateRef s
liftIO . atomically $ takeTMVar t
return Nothing return Nothing
) )
liftIO . atomically $ do liftIO . atomically $ do
case sta' of case res of
Nothing -> return () Nothing -> return ()
Just sta -> do Just (sta, _s) -> do
putTMVar stateRef s'
case sta of case sta of
MessageS m -> do writeTChan messageC $ Right m MessageS m -> do writeTChan messageC $ Right m
_ <- readTChan messageC -- Sic! _ <- readTChan messageC -- Sic!
return () return ()
-- this may seem ridiculous, but to prevent -- this may seem ridiculous, but to prevent
-- the channel from filling up we immedtiately remove the -- the channel from filling up we
-- immedtiately remove the
-- Stanza we just put in. It will still be -- Stanza we just put in. It will still be
-- available in duplicates. -- available in duplicates.
MessageErrorS m -> do writeTChan messageC $ Left m MessageErrorS m -> do writeTChan messageC $ Left m
@ -80,8 +83,13 @@ readWorker messageC presenceC handlers stateRef =
IQRequestS i -> handleIQRequest handlers i IQRequestS i -> handleIQRequest handlers i
IQResultS i -> handleIQResponse handlers (Right i) IQResultS i -> handleIQResponse handlers (Right i)
IQErrorS i -> handleIQResponse handlers (Left i) IQErrorS i -> handleIQResponse handlers (Left i)
where
-- Defining an Control.Exception.allowInterrupt equivalent for
-- GHC 7 compatibility.
allowInterrupt :: IO ()
allowInterrupt = unsafeUnmask $ return ()
handleIQRequest :: TVar IQHandlers -> IQRequest -> STM ()
handleIQRequest handlers iq = do handleIQRequest handlers iq = do
(byNS, _) <- readTVar handlers (byNS, _) <- readTVar handlers
let iqNS = fromMaybe "" (nameNamespace . elementName $ iqRequestPayload iq) let iqNS = fromMaybe "" (nameNamespace . elementName $ iqRequestPayload iq)
@ -91,6 +99,7 @@ handleIQRequest handlers iq = do
sent <- newTVar False sent <- newTVar False
writeTChan ch (iq, sent) writeTChan ch (iq, sent)
handleIQResponse :: TVar IQHandlers -> Either IQError IQResult -> STM ()
handleIQResponse handlers iq = do handleIQResponse handlers iq = do
(byNS, byID) <- readTVar handlers (byNS, byID) <- readTVar handlers
case Map.updateLookupWithKey (\_ _ -> Nothing) (iqID iq) byID of case Map.updateLookupWithKey (\_ _ -> Nothing) (iqID iq) byID of
@ -101,14 +110,14 @@ handleIQResponse handlers iq = do
writeTVar handlers (byNS, byID') writeTVar handlers (byNS, byID')
where where
iqID (Left err) = iqErrorID err iqID (Left err) = iqErrorID err
iqID (Right iq) = iqResultID iq iqID (Right iq') = iqResultID iq'
writeWorker :: TChan Stanza -> TMVar (BS.ByteString -> IO ()) -> IO () writeWorker :: TChan Stanza -> TMVar (BS.ByteString -> IO ()) -> IO ()
writeWorker stCh writeR = forever $ do writeWorker stCh writeR = forever $ do
(write, next) <- atomically $ (,) <$> (write, next) <- atomically $ (,) <$>
takeTMVar writeR <*> takeTMVar writeR <*>
readTChan stCh readTChan stCh
_ <- write $ renderElement (pickleElem stanzaP next) _ <- write $ renderElement (pickleElem xpStanza next)
atomically $ putTMVar writeR write atomically $ putTMVar writeR write
-- Two streams: input and output. Threads read from input stream and write to output stream. -- Two streams: input and output. Threads read from input stream and write to output stream.
@ -116,56 +125,58 @@ writeWorker stCh writeR = forever $ do
-- returns channel of incoming and outgoing stances, respectively -- returns channel of incoming and outgoing stances, respectively
-- and an Action to stop the Threads and close the connection -- and an Action to stop the Threads and close the connection
startThreads startThreads
:: XMPPConMonad ( TChan (Either MessageError Message) :: IO ( TChan (Either MessageError Message)
, TChan (Either PresenceError Presence) , TChan (Either PresenceError Presence)
, TVar IQHandlers , TVar IQHandlers
, TChan Stanza , TChan Stanza
, IO () , IO ()
, TMVar (BS.ByteString -> IO ()) , TMVar (BS.ByteString -> IO ())
, TMVar XMPPConState , TMVar XmppConnection
, ThreadId , ThreadId
, TVar EventHandlers
) )
startThreads = do startThreads = do
writeLock <- liftIO . newTMVarIO =<< gets sConPushBS writeLock <- newTMVarIO (\_ -> return ())
messageC <- liftIO newTChanIO messageC <- newTChanIO
presenceC <- liftIO newTChanIO presenceC <- newTChanIO
iqC <- liftIO newTChanIO outC <- newTChanIO
outC <- liftIO newTChanIO handlers <- newTVarIO ( Map.empty, Map.empty)
handlers <- liftIO $ newTVarIO ( Map.empty, Map.empty) eh <- newTVarIO zeroEventHandlers
conS <- liftIO . newTMVarIO =<< get conS <- newTMVarIO xmppNoConnection
lw <- liftIO . forkIO $ writeWorker outC writeLock lw <- forkIO $ writeWorker outC writeLock
cp <- liftIO . forkIO $ connPersist writeLock cp <- forkIO $ connPersist writeLock
s <- get rd <- forkIO $ readWorker messageC presenceC handlers conS
rd <- liftIO . forkIO $ readWorker messageC presenceC handlers conS
return (messageC, presenceC, handlers, outC return (messageC, presenceC, handlers, outC
, killConnection writeLock [lw, rd, cp] , killConnection writeLock [lw, rd, cp]
, writeLock, conS ,rd) , writeLock, conS ,rd, eh)
where where
killConnection writeLock threads = liftIO $ do killConnection writeLock threads = liftIO $ do
_ <- atomically $ takeTMVar writeLock -- Should we put it back? _ <- atomically $ takeTMVar writeLock -- Should we put it back?
_ <- forM threads killThread _ <- forM threads killThread
return() return()
-- | Start worker threads and run action. The supplied action will run -- | Creates and initializes a new XMPP session.
-- in the calling thread. use 'forkXMPP' to start another thread. newSession :: IO Session
runThreaded :: XMPPThread a newSession = do
-> XMPPConMonad a (mC, pC, hand, outC, stopThreads', writeR, conS, rdr, eh) <- startThreads
runThreaded a = do workermCh <- newIORef $ Nothing
liftIO . putStrLn $ "starting threads" workerpCh <- newIORef $ Nothing
(mC, pC, hand, outC, _stopThreads, writeR, conS, rdr ) <- startThreads idRef <- newTVarIO 1
liftIO . putStrLn $ "threads running"
workermCh <- liftIO . newIORef $ Nothing
workerpCh <- liftIO . newIORef $ Nothing
idRef <- liftIO $ newTVarIO 1
let getId = atomically $ do let getId = atomically $ do
curId <- readTVar idRef curId <- readTVar idRef
writeTVar idRef (curId + 1 :: Integer) writeTVar idRef (curId + 1 :: Integer)
return . read. show $ curId return . read. show $ curId
s <- get return (Session workermCh workerpCh mC pC outC hand writeR rdr getId conS eh stopThreads')
liftIO . putStrLn $ "starting application"
liftIO $ runReaderT a (Thread workermCh workerpCh mC pC outC hand writeR rdr getId conS) withNewSession :: XMPP b -> IO (Session, b)
withNewSession a = do
sess <- newSession
ret <- runReaderT a sess
return (sess, ret)
withSession :: Session -> XMPP a -> IO a
withSession = flip runReaderT
-- | Sends a blank space every 30 seconds to keep the connection alive -- | Sends a blank space every 30 seconds to keep the connection alive
connPersist :: TMVar (BS.ByteString -> IO ()) -> IO () connPersist :: TMVar (BS.ByteString -> IO ()) -> IO ()
@ -173,5 +184,4 @@ connPersist lock = forever $ do
pushBS <- atomically $ takeTMVar lock pushBS <- atomically $ takeTMVar lock
pushBS " " pushBS " "
atomically $ putTMVar lock pushBS atomically $ putTMVar lock pushBS
-- putStrLn "<space added>"
threadDelay 30000000 threadDelay 30000000

30
src/Network/XMPP/Concurrent/Types.hs

@ -1,3 +1,4 @@
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveDataTypeable #-}
module Network.XMPP.Concurrent.Types where module Network.XMPP.Concurrent.Types where
@ -21,27 +22,40 @@ type IQHandlers = (Map.Map (IQRequestType, Text) (TChan (IQRequest, TVar Bool))
, Map.Map StanzaId (TMVar IQResponse) , Map.Map StanzaId (TMVar IQResponse)
) )
data Thread = Thread { messagesRef :: IORef (Maybe ( TChan (Either data EventHandlers = EventHandlers
{ sessionEndHandler :: XMPP ()
, connectionClosedHandler :: XMPP ()
}
zeroEventHandlers :: EventHandlers
zeroEventHandlers = EventHandlers
{ sessionEndHandler = return ()
, connectionClosedHandler = return ()
}
data Session = Session { messagesRef :: IORef (Maybe ( TChan (Either
MessageError MessageError
Message Message
))) )))
, presenceRef :: IORef (Maybe (TChan (Either , presenceRef :: IORef (Maybe (TChan (Either
PresenceError PresenceError Presence )))
Presence
)))
, mShadow :: TChan (Either MessageError , mShadow :: TChan (Either MessageError
Message) -- the original chan Message)
-- the original chan
, pShadow :: TChan (Either PresenceError , pShadow :: TChan (Either PresenceError
Presence) -- the original chan Presence)
-- the original chan
, outCh :: TChan Stanza , outCh :: TChan Stanza
, iqHandlers :: TVar IQHandlers , iqHandlers :: TVar IQHandlers
, writeRef :: TMVar (BS.ByteString -> IO () ) , writeRef :: TMVar (BS.ByteString -> IO () )
, readerThread :: ThreadId , readerThread :: ThreadId
, idGenerator :: IO StanzaId , idGenerator :: IO StanzaId
, conStateRef :: TMVar XMPPConState , conStateRef :: TMVar XmppConnection
, eventHandlers :: TVar EventHandlers
, stopThreads :: IO ()
} }
type XMPPThread a = ReaderT Thread IO a type XMPP a = ReaderT Session IO a
data Interrupt = Interrupt (TMVar ()) deriving Typeable data Interrupt = Interrupt (TMVar ()) deriving Typeable
instance Show Interrupt where show _ = "<Interrupt>" instance Show Interrupt where show _ = "<Interrupt>"

292
src/Network/XMPP/JID.hs

@ -15,183 +15,213 @@
-- --
-- This module does not internationalize hostnames. -- This module does not internationalize hostnames.
module Network.XMPP.JID
module Network.XMPP.JID (fromString, fromStrings, isBare, isFull) where ( JID(..)
, fromText
import Network.XMPP.Types , fromStrings
, isBare
import Data.Maybe (fromJust, isJust) , isFull) where
import Text.Parsec ((<|>), anyToken, char, eof, many, noneOf, parse)
import Text.Parsec.ByteString (GenParser) import Control.Applicative ((<$>),(<|>))
import Control.Monad(guard)
import Text.StringPrep (StringPrepProfile (..), a1, b1, b2, c11, c12, c21, c22,
c3, c4, c5, c6, c7, c8, c9, runStringPrep) import qualified Data.Attoparsec.Text as AP
import Text.NamePrep (namePrepProfile) import Data.Maybe(fromJust)
import qualified Data.Set as Set
import Network.URI (isIPv4address, isIPv6address) import Data.String (IsString(..))
import Data.Text (Text)
import qualified Data.ByteString.Char8 as DBC (pack) import qualified Data.Text as Text
import qualified Data.Text as DT (pack, unpack)
-- import Network.URI (isIPv4address, isIPv6address)
-- | import qualified Text.NamePrep as SP
-- Converts a string to a JID. import qualified Text.StringPrep as SP
fromString :: String -> Maybe JID data JID = JID {
-- | The @localpart@ of a JID is an optional identifier
fromString s = fromStrings localpart domainpart resourcepart -- 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)
}
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 where
Right (localpart, domainpart, resourcepart) = eitherToMaybe = either (const Nothing) Just
parse jidParts "" (DBC.pack s)
-- |
-- Converts localpart, domainpart, and resourcepart strings to a JID.
-- | Converts localpart, domainpart, and resourcepart strings to a JID.
-- Runs the appropriate stringprep profiles and validates the parts. -- Runs the appropriate stringprep profiles and validates the parts.
fromStrings :: Maybe Text -> Text -> Maybe Text -> Maybe JID
fromStrings :: Maybe String -> String -> Maybe String -> Maybe JID fromStrings l d r = do
localPart <- case l of
fromStrings l s r Nothing -> return Nothing
| domainpart == Nothing = Nothing Just l'-> do
| otherwise = if validateNonDomainpart localpart && l'' <- SP.runStringPrep nodeprepProfile l'
isJust domainpart' && guard $ validPartLength l''
validateNonDomainpart resourcepart let prohibMap = Set.fromList nodeprepExtraProhibitedCharacters
then Just (JID localpart (fromJust domainpart') resourcepart) guard $ Text.all (`Set.notMember` prohibMap) l''
else Nothing 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 where
-- Applies the nodeprep profile on the localpart string, if any.
localpart :: Maybe String
localpart = case l of
Just l' -> case runStringPrep nodeprepProfile (DT.pack l') of
Just l'' -> Just $ DT.unpack l''
Nothing -> Nothing
Nothing -> Nothing
-- Applies the nameprep profile on the domainpart string.
-- TODO: Allow unassigned?
domainpart :: Maybe String
domainpart = case runStringPrep (namePrepProfile False) (DT.pack s) of
Just s' -> Just $ DT.unpack s'
Nothing -> Nothing
-- Applies the resourceprep profile on the resourcepart string, if
-- any.
resourcepart :: Maybe String
resourcepart = case r of
Just r' -> case runStringPrep resourceprepProfile (DT.pack r') of
Just r'' -> Just $ DT.unpack r''
Nothing -> Nothing
Nothing -> Nothing
-- Returns the domainpart if it was a valid IP or if the toASCII -- Returns the domainpart if it was a valid IP or if the toASCII
-- function was successful, or Nothing otherwise. -- function was successful, or Nothing otherwise.
domainpart' :: Maybe String validDomainPart _s = True -- TODO
domainpart' | isIPv4address s || isIPv6address s = Just s -- isIPv4address s || isIPv6address s || validHostname s
| validHostname s = Just s
| otherwise = Nothing
-- Validates that non-domainpart strings have an appropriate
-- length.
validateNonDomainpart :: Maybe String -> Bool
validateNonDomainpart Nothing = True
validateNonDomainpart (Just l) = validPartLength l
where
validPartLength :: String -> Bool
validPartLength p = length p > 0 && length p < 1024
validPartLength :: Text -> Bool
validPartLength p = Text.length p > 0 && Text.length p < 1024
-- Validates a host name -- Validates a host name
validHostname :: String -> Bool -- validHostname :: Text -> Bool
validHostname _ = True -- TODO -- validHostname _ = True -- TODO
-- | Returns True if the JID is `bare', and False otherwise.
-- | Returns True if the JID is /bare/, and False otherwise.
isBare :: JID -> Bool isBare :: JID -> Bool
isBare j | resourcepart j == Nothing = True isBare j | resourcepart j == Nothing = True
| otherwise = False | otherwise = False
-- | Returns True if the JID is `full', and False otherwise. -- | Returns True if the JID is `full', and False otherwise.
isFull :: JID -> Bool isFull :: JID -> Bool
isFull jid = not $ isBare jid isFull jid = not $ isBare jid
-- Parses an JID string and returns its three parts. It performs no -- Parses an JID string and returns its three parts. It performs no
-- validation or transformations. We are using Parsec to parse the -- validation or transformations. We are using Parsec to parse the
-- JIDs. There is no input for which 'jidParts' fails. -- JIDs. There is no input for which 'jidParts' fails.
jidParts :: GenParser Char st (Maybe String, String, Maybe String)
jidParts = do jidParts = do
-- Read until we reach an '@', a '/', or EOF. -- Read until we reach an '@', a '/', or EOF.
a <- many $ noneOf ['@', '/'] a <- AP.takeWhile1 (AP.notInClass ['@', '/'])
-- Case 1: We found an '@', and thus the localpart. At least the -- Case 1: We found an '@', and thus the localpart. At least the
-- domainpart is remaining. Read the '@' and until a '/' or EOF. -- domainpart is remaining. Read the '@' and until a '/' or EOF.
do do
char '@' b <- domainPartP
b <- many $ noneOf ['/']
-- Case 1A: We found a '/' and thus have all the JID parts. Read -- Case 1A: We found a '/' and thus have all the JID parts. Read
-- the '/' and until EOF. -- the '/' and until EOF.
do do
char '/' -- Resourcepart remaining c <- resourcePartP -- Parse resourcepart
c <- many $ anyToken -- Parse resourcepart
eof
return (Just a, b, Just c) return (Just a, b, Just c)
-- Case 1B: We have reached EOF; the JID is in the form -- Case 1B: We have reached EOF; the JID is in the form
-- localpart@domainpart. -- localpart@domainpart.
<|> do <|> do
eof AP.endOfInput
return (Just a, b, Nothing) return (Just a, b, Nothing)
-- Case 2: We found a '/'; the JID is in the form -- Case 2: We found a '/'; the JID is in the form
-- domainpart/resourcepart. -- domainpart/resourcepart.
<|> do <|> do
char '/' b <- resourcePartP
b <- many $ anyToken AP.endOfInput
eof
return (Nothing, a, Just b) return (Nothing, a, Just b)
-- Case 3: We have reached EOF; we have an JID consisting of only -- Case 3: We have reached EOF; we have an JID consisting of only
-- a domainpart. -- a domainpart.
<|> do <|> do
eof AP.endOfInput
return (Nothing, a, Nothing) return (Nothing, a, Nothing)
where
domainPartP = do
nodeprepProfile :: StringPrepProfile _ <- AP.char '@'
AP.takeWhile1 (/= '/')
nodeprepProfile = Profile { maps = [b1, b2] resourcePartP = do
, shouldNormalize = True _ <- AP.char '/'
, prohibited = [a1] ++ [c11, c12, c21, c22, AP.takeText
c3, c4, c5, c6, c7,
c8, c9]
, shouldCheckBidi = True } 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 needs to be checked for after normalization. We could also -- These needs to be checked for after normalization. We could also
-- look up the Unicode mappings and include a list of characters in -- look up the Unicode mappings and include a list of characters in
-- the prohibited field above. Let's defer that until we know that we -- the prohibited field above. Let's defer that until we know that we
-- are going to use stringprep. -- are going to use stringprep.
nodeprepExtraProhibitedCharacters :: [Char]
nodeprepExtraProhibitedCharacters = ['\x22', '\x26', '\x27', '\x2F', nodeprepExtraProhibitedCharacters = ['\x22', '\x26', '\x27', '\x2F',
'\x3A', '\x3C', '\x3E', '\x40'] '\x3A', '\x3C', '\x3E', '\x40']
resourceprepProfile :: SP.StringPrepProfile
resourceprepProfile = SP.Profile
resourceprepProfile :: StringPrepProfile { SP.maps = [SP.b1]
, SP.shouldNormalize = True
resourceprepProfile = Profile { maps = [b1] , SP.prohibited = [ SP.a1
, shouldNormalize = True , SP.c12
, prohibited = [a1] ++ [c12, c21, c22, , SP.c21
c3, c4, c5, c6, , SP.c22
c7, c8, c9] , SP.c3
, shouldCheckBidi = True } , SP.c4
, SP.c5
, SP.c6
, SP.c7
, SP.c8
, SP.c9
]
, SP.shouldCheckBidi = True
}

38
src/Network/XMPP/Marshal.hs

@ -5,8 +5,12 @@ 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 Network.XMPP.Types import Network.XMPP.Types
xpStreamEntity :: PU [Node] (Either XmppStreamError Stanza)
xpStreamEntity = xpEither xpStreamError xpStanza
stanzaSel :: Stanza -> Int stanzaSel :: Stanza -> Int
stanzaSel (IQRequestS _) = 0 stanzaSel (IQRequestS _) = 0
stanzaSel (IQResultS _) = 1 stanzaSel (IQResultS _) = 1
@ -16,8 +20,8 @@ stanzaSel (MessageErrorS _) = 4
stanzaSel (PresenceS _) = 5 stanzaSel (PresenceS _) = 5
stanzaSel (PresenceErrorS _) = 6 stanzaSel (PresenceErrorS _) = 6
stanzaP :: PU [Node] Stanza xpStanza :: PU [Node] Stanza
stanzaP = xpAlt stanzaSel xpStanza = xpAlt stanzaSel
[ xpWrap IQRequestS (\(IQRequestS x) -> x) xpIQRequest [ xpWrap IQRequestS (\(IQRequestS x) -> x) xpIQRequest
, xpWrap IQResultS (\(IQResultS x) -> x) xpIQResult , xpWrap IQResultS (\(IQResultS x) -> x) xpIQResult
, xpWrap IQErrorS (\(IQErrorS x) -> x) xpIQError , xpWrap IQErrorS (\(IQErrorS x) -> x) xpIQError
@ -27,12 +31,6 @@ stanzaP = xpAlt stanzaSel
, xpWrap PresenceErrorS (\(PresenceErrorS x) -> x) xpPresenceError , xpWrap PresenceErrorS (\(PresenceErrorS x) -> x) xpPresenceError
] ]
xmlLang :: Name
xmlLang = Name "lang" Nothing (Just "xml")
xpLangTag :: PU [Attribute] (Maybe LangTag)
xpLangTag = xpAttrImplied xmlLang xpPrim
xpMessage :: PU [Node] (Message) xpMessage :: PU [Node] (Message)
xpMessage = xpWrap (\((tp, qid, from, to, lang), (sub, body, thr, ext)) xpMessage = xpWrap (\((tp, qid, from, to, lang), (sub, body, thr, ext))
-> Message qid from to lang tp sub thr body ext) -> Message qid from to lang tp sub thr body ext)
@ -193,3 +191,27 @@ xpIQError = xpWrap (\((qid, from, to, lang, _tp),(err, body))
(xpOption xpElemVerbatim) (xpOption xpElemVerbatim)
) )
xpStreamError :: PU [Node] XmppStreamError
xpStreamError = xpWrap
(\((cond,() ,()), txt, el) -> XmppStreamError cond txt el)
(\(XmppStreamError cond txt el) ->((cond,() ,()), txt, el))
(xpElemNodes
(Name "error"
(Just "http://etherx.jabber.org/streams")
(Just "stream")
) $ xp3Tuple
(xpElemByNamespace
"urn:ietf:params:xml:ns:xmpp-streams" xpPrim
xpUnit
xpUnit
)
(xpOption $ xpElem
"{urn:ietf:params:xml:ns:xmpp-streams}text"
xpLangTag
(xpContent xpId))
( xpOption xpElemVerbatim
-- application specific error conditions
)
)

18
src/Network/XMPP/Message.hs

@ -1,11 +1,21 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module Network.XMPP.Message where -- | Message handling
module Network.XMPP.Message
( Message(..)
, MessageType(..)
, MessageError(..)
, message
, simpleMessage
, answerMessage
)
where
import Data.Text(Text) import Data.Text(Text)
import Data.XML.Types import Data.XML.Types
import Network.XMPP.Types import Network.XMPP.Types
-- The empty message
message :: Message message :: Message
message = Message { messageID = Nothing message = Message { messageID = Nothing
, messageFrom = Nothing , messageFrom = Nothing
@ -18,7 +28,11 @@ message = Message { messageID = Nothing
, messagePayload = [] , messagePayload = []
} }
simpleMessage :: JID -> Text -> Message
-- | Create simple message, containing nothing but a body text
simpleMessage :: JID -- ^ Recipient
-> Text -- ^ Myssage body
-> Message
simpleMessage to txt = message { messageTo = Just to simpleMessage to txt = message { messageTo = Just to
, messageBody = Just txt , messageBody = Just txt
} }

97
src/Network/XMPP/Monad.hs

@ -1,3 +1,4 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Network.XMPP.Monad where module Network.XMPP.Monad where
@ -7,13 +8,13 @@ import Control.Monad
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Class import Control.Monad.Trans.Class
--import Control.Monad.Trans.Resource --import Control.Monad.Trans.Resource
import Control.Concurrent import qualified Control.Exception as Ex
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Data.ByteString as BS import Data.ByteString as BS
import Data.Conduit import Data.Conduit
import Data.Conduit.BufferedSource
import Data.Conduit.Binary as CB import Data.Conduit.Binary as CB
import Data.Conduit.List as CL
import Data.Text(Text) import Data.Text(Text)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
@ -27,8 +28,6 @@ import System.IO
import Text.XML.Stream.Elements import Text.XML.Stream.Elements
import Text.XML.Stream.Parse as XP import Text.XML.Stream.Parse as XP
import Text.XML.Stream.Render as XR
pushN :: Element -> XMPPConMonad () pushN :: Element -> XMPPConMonad ()
pushN x = do pushN x = do
@ -36,7 +35,7 @@ pushN x = do
liftIO . sink $ renderElement x liftIO . sink $ renderElement x
push :: Stanza -> XMPPConMonad () push :: Stanza -> XMPPConMonad ()
push = pushN . pickleElem stanzaP push = pushN . pickleElem xpStanza
pushOpen :: Element -> XMPPConMonad () pushOpen :: Element -> XMPPConMonad ()
pushOpen e = do pushOpen e = do
@ -44,58 +43,65 @@ pushOpen e = do
liftIO . sink $ renderOpenElement e liftIO . sink $ renderOpenElement e
return () return ()
pulls :: Sink Event IO b -> XMPPConMonad b pullSink :: Sink Event IO b -> XMPPConMonad b
pulls snk = do pullSink snk = do
source <- gets sConSrc source <- gets sConSrc
(src', r) <- lift $ source $$+ snk (_, r) <- lift $ source $$+ snk
modify $ (\s -> s {sConSrc = src'})
return r return r
pullE :: XMPPConMonad Element pullElement :: XMPPConMonad Element
pullE = pulls elementFromEvents pullElement = pullSink elementFromEvents
pullPickle :: PU [Node] a -> XMPPConMonad a pullPickle :: PU [Node] a -> XMPPConMonad a
pullPickle p = unpickleElem' p <$> pullE pullPickle p = do
res <- unpickleElem p <$> pullElement
pull :: XMPPConMonad Stanza case res of
pull = pullPickle stanzaP Left e -> liftIO . Ex.throwIO $ StreamXMLError e
Right r -> return r
pullStanza :: XMPPConMonad Stanza
pullStanza = do
res <- pullPickle xpStreamEntity
case res of
Left e -> liftIO . Ex.throwIO $ StreamError e
Right r -> return r
xmppFromHandle :: Handle xmppFromHandle :: Handle
-> Text -> Text
-> Text
-> Maybe Text
-> XMPPConMonad a -> XMPPConMonad a
-> IO (a, XMPPConState) -> IO (a, XmppConnection)
xmppFromHandle handle hostname username res f = do xmppFromHandle handle hostname f = do
liftIO $ hSetBuffering handle NoBuffering liftIO $ hSetBuffering handle NoBuffering
let raw = sourceHandle handle let raw = sourceHandle handle
let src = raw $= XP.parseBytes def let src = raw $= XP.parseBytes def
let st = XMPPConState let st = XmppConnection
src src
(raw) (raw)
(BS.hPut handle) (BS.hPut handle)
(Just handle) (Just handle)
(SF Nothing [] []) (SF Nothing [] [])
False XmppConnectionPlain
(Just hostname) (Just hostname)
(Just username) Nothing
res Nothing
(hClose handle)
runStateT f st runStateT f st
zeroSource :: Source IO output zeroSource :: Source IO output
zeroSource = sourceState () (\_ -> forever $ threadDelay 10000000) zeroSource = liftIO . Ex.throwIO $ XmppNoConnection
xmppZeroConState :: XMPPConState xmppNoConnection :: XmppConnection
xmppZeroConState = XMPPConState xmppNoConnection = XmppConnection
{ sConSrc = zeroSource { sConSrc = zeroSource
, sRawSrc = zeroSource , sRawSrc = zeroSource
, sConPushBS = (\_ -> return ()) , sConPushBS = \_ -> Ex.throwIO $ XmppNoConnection
, sConHandle = Nothing , sConHandle = Nothing
, sFeatures = SF Nothing [] [] , sFeatures = SF Nothing [] []
, sHaveTLS = False , sConnectionState = XmppConnectionClosed
, sHostname = Nothing , sHostname = Nothing
, sUsername = Nothing , sUsername = Nothing
, sResource = Nothing , sResource = Nothing
, sCloseConnection = return ()
} }
xmppRawConnect :: HostName -> Text -> XMPPConMonad () xmppRawConnect :: HostName -> Text -> XMPPConMonad ()
@ -106,19 +112,42 @@ xmppRawConnect host hostname = do
hSetBuffering con NoBuffering hSetBuffering con NoBuffering
return con return con
let raw = sourceHandle con let raw = sourceHandle con
let src = raw $= XP.parseBytes def src <- liftIO . bufferSource $ raw $= XP.parseBytes def
let st = XMPPConState let st = XmppConnection
src src
(raw) (raw)
(BS.hPut con) (BS.hPut con)
(Just con) (Just con)
(SF Nothing [] []) (SF Nothing [] [])
False XmppConnectionPlain
(Just hostname) (Just hostname)
uname uname
Nothing Nothing
(hClose con)
put st put st
withNewSession :: XMPPConMonad a -> IO (a, XMPPConState) xmppNewSession :: XMPPConMonad a -> IO (a, XmppConnection)
withNewSession action = do xmppNewSession action = do
runStateT action xmppZeroConState runStateT action xmppNoConnection
xmppKillConnection :: XMPPConMonad ()
xmppKillConnection = do
cc <- gets sCloseConnection
void . liftIO $ (Ex.try cc :: IO (Either Ex.SomeException ()))
put xmppNoConnection
xmppSendIQ' :: StanzaId -> Maybe JID -> IQRequestType
-> Maybe LangTag -> Element
-> XMPPConMonad (Either IQError IQResult)
xmppSendIQ' iqID to tp lang body = do
push . IQRequestS $ IQRequest iqID Nothing to lang tp body
res <- pullPickle $ xpEither xpIQError xpIQResult
case res of
Left e -> return $ Left e
Right iq' -> do
unless (iqID == iqResultID iq') . liftIO . Ex.throwIO $
StreamXMLError
("In xmppSendIQ' IDs don't match: " ++ show iqID ++
" /= " ++ show (iqResultID iq') ++ " .")
return $ Right iq'

27
src/Network/XMPP/Pickle.hs

@ -5,11 +5,27 @@
-- Marshalling between XML and Native Types -- Marshalling between XML and Native Types
module Network.XMPP.Pickle where module Network.XMPP.Pickle
( mbToBool
, xpElemEmpty
, xmlLang
, xpLangTag
, xpNodeElem
, ignoreAttrs
, mbl
, lmb
, right
, unpickleElem'
, unpickleElem
, pickleElem
, ppElement
) where
import Data.XML.Types import Data.XML.Types
import Data.XML.Pickle import Data.XML.Pickle
import Network.XMPP.Types
import Text.XML.Stream.Elements import Text.XML.Stream.Elements
mbToBool :: Maybe t -> Bool mbToBool :: Maybe t -> Bool
@ -21,11 +37,11 @@ xpElemEmpty name = xpWrap (\((),()) -> ())
(\() -> ((),())) $ (\() -> ((),())) $
xpElem name xpUnit xpUnit xpElem name xpUnit xpUnit
-- xpElemExists :: Name -> PU [Node] Bool xmlLang :: Name
-- xpElemExists name = xpWrap (\x -> mbToBool x) xmlLang = Name "lang" Nothing (Just "xml")
-- (\x -> if x then Just () else Nothing) $
-- xpOption (xpElemEmpty name)
xpLangTag :: PU [Attribute] (Maybe LangTag)
xpLangTag = xpAttrImplied xmlLang xpPrim
xpNodeElem :: PU [Node] a -> PU Element a xpNodeElem :: PU [Node] a -> PU Element a
xpNodeElem xp = PU { pickleTree = \x -> head $ (pickleTree xp x) >>= \y -> xpNodeElem xp = PU { pickleTree = \x -> head $ (pickleTree xp x) >>= \y ->
@ -63,3 +79,4 @@ unpickleElem p x = unpickle (xpNodeElem p) x
pickleElem :: PU [Node] a -> a -> Element pickleElem :: PU [Node] a -> a -> Element
pickleElem p = pickle $ xpNodeElem p pickleElem p = pickle $ xpNodeElem p

15
src/Network/XMPP/Presence.hs

@ -1,9 +1,10 @@
{-# OPTIONS_HADDOCK hide #-}
module Network.XMPP.Presence where module Network.XMPP.Presence where
import Data.Text(Text) import Data.Text(Text)
import Network.XMPP.Types import Network.XMPP.Types
-- | The empty presence.
presence :: Presence presence :: Presence
presence = Presence { presenceID = Nothing presence = Presence { presenceID = Nothing
, presenceFrom = Nothing , presenceFrom = Nothing
@ -16,6 +17,7 @@ presence = Presence { presenceID = Nothing
, presencePayload = [] , presencePayload = []
} }
-- | Request subscription with an entity
presenceSubscribe :: JID -> Presence presenceSubscribe :: JID -> Presence
presenceSubscribe to = presence { presenceTo = Just to presenceSubscribe to = presence { presenceTo = Just to
, presenceType = Just Subscribe , presenceType = Just Subscribe
@ -45,14 +47,15 @@ presenceUnsubscribe to = presence { presenceTo = Just to
isPresenceUnsubscribe :: Presence -> Bool isPresenceUnsubscribe :: Presence -> Bool
isPresenceUnsubscribe pres = presenceType pres == (Just Unsubscribe) isPresenceUnsubscribe pres = presenceType pres == (Just Unsubscribe)
-- | Signals to the server that the client is available for communication -- | Signal to the server that the client is available for communication
presenceOnline :: Presence presenceOnline :: Presence
presenceOnline = presence presenceOnline = presence
-- | Signals to the server that the client is no longer available for communication. -- | Signal to the server that the client is no longer available for communication.
presenceOffline :: Presence presenceOffline :: Presence
presenceOffline = presence {presenceType = Just Unavailable} presenceOffline = presence {presenceType = Just Unavailable}
-- Change your status
status status
:: Maybe Text -- ^ Status message :: Maybe Text -- ^ Status message
-> Maybe ShowType -- ^ Status Type -> Maybe ShowType -- ^ Status Type
@ -63,16 +66,16 @@ status txt showType prio = presence { presenceShowType = showType
, presenceStatus = txt , presenceStatus = txt
} }
-- | Sets the current availability status. This implicitly sets the clients -- | Set the current availability status. This implicitly sets the clients
-- status online -- status online
presenceAvail :: ShowType -> Presence presenceAvail :: ShowType -> Presence
presenceAvail showType = status Nothing (Just showType) Nothing presenceAvail showType = status Nothing (Just showType) Nothing
-- | Sets the current status message. This implicitly sets the clients -- | Set the current status message. This implicitly sets the clients
-- status online -- status online
presenceMessage :: Text -> Presence presenceMessage :: Text -> Presence
presenceMessage txt = status (Just txt) Nothing Nothing presenceMessage txt = status (Just txt) Nothing Nothing
-- | Adds a recipient to a presence notification -- | Add a recipient to a presence notification
presTo :: Presence -> JID -> Presence presTo :: Presence -> JID -> Presence
presTo pres to = pres{presenceTo = Just to} presTo pres to = pres{presenceTo = Just to}

116
src/Network/XMPP/SASL.hs

@ -2,8 +2,9 @@
module Network.XMPP.SASL where module Network.XMPP.SASL where
import Control.Applicative import Control.Applicative
import Control.Arrow (left)
import Control.Monad import Control.Monad
import Control.Monad.IO.Class import Control.Monad.Error
import Control.Monad.State.Strict import Control.Monad.State.Strict
import qualified Crypto.Classes as CC import qualified Crypto.Classes as CC
@ -16,6 +17,7 @@ import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.Digest.Pure.MD5 as MD5 import qualified Data.Digest.Pure.MD5 as MD5
import qualified Data.List as L import qualified Data.List as L
import Data.Word (Word8)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
@ -26,6 +28,7 @@ import qualified Data.Text.Encoding as Text
import Network.XMPP.Monad import Network.XMPP.Monad
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
@ -48,35 +51,55 @@ saslResponse2E =
[] []
[] []
xmppSASL:: Text -> Text -> XMPPConMonad (Either String Text) data AuthError = AuthXmlError
xmppSASL uname passwd = do | AuthMechanismError [Text]
| AuthChallengeError
| AuthStreamError StreamError
| AuthConnectionError
deriving Show
instance Error AuthError where
noMsg = AuthXmlError
xmppSASL:: Text -> Text -> XMPPConMonad (Either AuthError Text)
xmppSASL uname passwd = runErrorT $ do
realm <- gets sHostname realm <- gets sHostname
case realm of case realm of
Just realm' -> do Just realm' -> do
xmppStartSASL realm' uname passwd ErrorT $ xmppStartSASL realm' uname passwd
modify (\s -> s{sUsername = Just uname}) modify (\s -> s{sUsername = Just uname})
return $ Right uname return uname
Nothing -> return $ Left "No connection found" Nothing -> throwError AuthConnectionError
xmppStartSASL :: Text xmppStartSASL :: Text
-> Text -> Text
-> Text -> Text
-> XMPPConMonad () -> XMPPConMonad (Either AuthError ())
xmppStartSASL realm username passwd = do xmppStartSASL realm username passwd = runErrorT $ do
mechanisms <- gets $ saslMechanisms . sFeatures mechanisms <- gets $ saslMechanisms . sFeatures
unless ("DIGEST-MD5" `elem` mechanisms) . error $ "No usable auth mechanism: " ++ show mechanisms unless ("DIGEST-MD5" `elem` mechanisms)
pushN $ saslInitE "DIGEST-MD5" . throwError $ AuthMechanismError mechanisms
Right challenge <- B64.decode . Text.encodeUtf8<$> pullPickle challengePickle lift . pushN $ saslInitE "DIGEST-MD5"
let Right pairs = toPairs challenge challenge' <- lift $ B64.decode . Text.encodeUtf8
<$> pullPickle challengePickle
challenge <- case challenge' of
Left _e -> throwError AuthChallengeError
Right r -> return r
pairs <- case toPairs challenge of
Left _ -> throwError AuthChallengeError
Right p -> return p
g <- liftIO $ Random.newStdGen g <- liftIO $ Random.newStdGen
pushN . saslResponseE $ createResponse g realm username passwd pairs lift . pushN . saslResponseE $ createResponse g realm username passwd pairs
challenge2 <- pullPickle (xpEither failurePickle challengePickle) challenge2 <- lift $ pullPickle (xpEither failurePickle challengePickle)
case challenge2 of case challenge2 of
Left x -> error $ show x Left _x -> throwError $ AuthXmlError
Right _ -> return () Right _ -> return ()
pushN saslResponse2E lift $ pushN saslResponse2E
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] <- pullE e <- lift pullElement
xmppRestartStream case e of
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] -> return ()
_ -> throwError AuthXmlError -- TODO: investigate
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream
return () return ()
createResponse :: Random.RandomGen g createResponse :: Random.RandomGen g
@ -91,14 +114,15 @@ createResponse g hostname username passwd' pairs = let
Just nonce = L.lookup "nonce" pairs Just nonce = L.lookup "nonce" pairs
uname = Text.encodeUtf8 username uname = Text.encodeUtf8 username
passwd = Text.encodeUtf8 passwd' passwd = Text.encodeUtf8 passwd'
realm = Text.encodeUtf8 hostname -- Using Int instead of Word8 for random 1.0.0.0 (GHC 7)
-- compatibility.
cnonce = BS.tail . BS.init . cnonce = BS.tail . BS.init .
B64.encode . BS.pack . take 8 $ Random.randoms g B64.encode . BS.pack . map toWord8 . take 8 $ Random.randoms g
nc = "00000001" nc = "00000001"
digestURI = ("xmpp/" `BS.append` realm) digestURI = ("xmpp/" `BS.append` (Text.encodeUtf8 hostname))
digest = md5Digest digest = md5Digest
uname uname
realm (lookup "realm" pairs)
passwd passwd
digestURI digestURI
nc nc
@ -106,19 +130,23 @@ createResponse g hostname username passwd' pairs = let
nonce nonce
cnonce cnonce
response = BS.intercalate"," . map (BS.intercalate "=") $ response = BS.intercalate"," . map (BS.intercalate "=") $
[["username" , quote uname ] [ ["username" , quote uname ]]
,["realm" , quote realm ] ++ case L.lookup "realm" pairs of
,["nonce" , quote nonce ] Just realm -> [["realm" , quote realm ]]
,["cnonce" , quote cnonce ] Nothing -> []
,["nc" , nc ] ++
,["qop" , qop ] [ ["nonce" , quote nonce ]
,["digest-uri", quote digestURI ] , ["cnonce" , quote cnonce ]
,["response" , digest ] , ["nc" , nc ]
,["charset" , "utf-8" ] , ["qop" , qop ]
, ["digest-uri", quote digestURI ]
, ["response" , digest ]
, ["charset" , "utf-8" ]
] ]
in Text.decodeUtf8 $ B64.encode response in Text.decodeUtf8 $ B64.encode response
where where
quote x = BS.concat ["\"",x,"\""] quote x = BS.concat ["\"",x,"\""]
toWord8 x = fromIntegral (x :: Int) :: Word8
toPairs :: BS.ByteString -> Either String [(BS.ByteString, BS.ByteString)] toPairs :: BS.ByteString -> Either String [(BS.ByteString, BS.ByteString)]
toPairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do toPairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do
@ -138,13 +166,14 @@ hashRaw :: [BS8.ByteString] -> BS8.ByteString
hashRaw = toStrict . Binary.encode hashRaw = toStrict . Binary.encode
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") . (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":")
toStrict :: BL.ByteString -> BS8.ByteString toStrict :: BL.ByteString -> BS8.ByteString
toStrict = BS.concat . BL.toChunks toStrict = BS.concat . BL.toChunks
-- TODO: this only handles MD5-sess -- TODO: this only handles MD5-sess
md5Digest :: BS8.ByteString md5Digest :: BS8.ByteString
-> BS8.ByteString -> Maybe BS8.ByteString
-> BS8.ByteString -> BS8.ByteString
-> BS8.ByteString -> BS8.ByteString
-> BS8.ByteString -> BS8.ByteString
@ -153,16 +182,29 @@ md5Digest :: BS8.ByteString
-> BS8.ByteString -> BS8.ByteString
-> BS8.ByteString -> BS8.ByteString
md5Digest uname realm password digestURI nc qop nonce cnonce= md5Digest uname realm password digestURI nc qop nonce cnonce=
let ha1 = hash [hashRaw [uname,realm,password], nonce, cnonce] let ha1 = hash [hashRaw [uname, maybe "" id realm, password], nonce, cnonce]
ha2 = hash ["AUTHENTICATE", digestURI] ha2 = hash ["AUTHENTICATE", digestURI]
in hash [ha1,nonce, nc, cnonce,qop,ha2] in hash [ha1,nonce, nc, cnonce,qop,ha2]
-- Pickling -- Pickling
failurePickle :: PU [Node] (SaslFailure)
failurePickle = xpWrap (\(txt,(failure,_,_))
-> SaslFailure failure txt)
(\(SaslFailure failure txt)
-> (txt,(failure,(),())))
(xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}failure"
(xp2Tuple
(xpOption $ xpElem
"{urn:ietf:params:xml:ns:xmpp-sasl}text"
xpLangTag
(xpContent xpId))
(xpElemByNamespace
"urn:ietf:params:xml:ns:xmpp-sasl"
xpPrim
(xpUnit)
(xpUnit))))
failurePickle :: PU [Node] (Element)
failurePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}failure"
(xpIsolate xpElemVerbatim)
challengePickle :: PU [Node] Text.Text challengePickle :: PU [Node] Text.Text
challengePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge" challengePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge"

24
src/Network/XMPP/Session.hs

@ -8,15 +8,14 @@ import Data.XML.Types(Element)
import Network.XMPP.Monad import Network.XMPP.Monad
import Network.XMPP.Pickle import Network.XMPP.Pickle
import Network.XMPP.Types import Network.XMPP.Types
import Network.XMPP.Concurrent
sessionXML :: Element sessionXML :: Element
sessionXML = pickleElem sessionXML = pickleElem
(xpElemBlank "{urn:ietf:params:xml:ns:xmpp-session}session" ) (xpElemBlank "{urn:ietf:params:xml:ns:xmpp-session}session" )
() ()
sessionIQ :: Stanza sessionIQ :: Stanza
sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess" sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess"
, iqRequestFrom = Nothing , iqRequestFrom = Nothing
@ -26,10 +25,17 @@ sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess"
, iqRequestPayload = sessionXML , iqRequestPayload = sessionXML
} }
xmppSession :: XMPPConMonad () xmppStartSession :: XMPPConMonad ()
xmppSession = do xmppStartSession = do
push $ sessionIQ answer <- xmppSendIQ' "session" Nothing Set Nothing sessionXML
answer <- pull case answer of
let IQResultS (IQResult "sess" Nothing Nothing _lang _body) = answer Left e -> error $ show e
return () Right _ -> return ()
startSession :: XMPP ()
startSession = do
answer <- sendIQ' Nothing Set Nothing sessionXML
case answer of
Left e -> error $ show e
Right _ -> return ()

10
src/Network/XMPP/Stream.hs

@ -3,13 +3,11 @@
module Network.XMPP.Stream where module Network.XMPP.Stream where
import Control.Applicative((<$>))
import Control.Exception(throwIO)
import Control.Monad(unless)
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Data.Conduit import Data.Conduit
import Data.Conduit.BufferedSource
import Data.Conduit.List as CL import Data.Conduit.List as CL
import Data.Text as T import Data.Text as T
import Data.XML.Pickle import Data.XML.Pickle
@ -30,7 +28,7 @@ streamUnpickleElem :: PU [Node] a
-> ErrorT StreamError (Pipe Event Void IO) a -> ErrorT StreamError (Pipe Event Void IO) a
streamUnpickleElem p x = do streamUnpickleElem p x = do
case unpickleElem p x of case unpickleElem p x of
Left l -> throwError $ StreamUnpickleError l Left l -> throwError $ StreamXMLError l
Right r -> return r Right r -> return r
type StreamSink a = ErrorT StreamError (Pipe Event Void IO) a type StreamSink a = ErrorT StreamError (Pipe Event Void IO) a
@ -58,14 +56,14 @@ xmppStartStream = runErrorT $ do
Nothing -> throwError StreamConnectionError Nothing -> throwError StreamConnectionError
Just hostname -> lift . pushOpen $ Just hostname -> lift . pushOpen $
pickleElem pickleStream ("1.0",Nothing, Just hostname) pickleElem pickleStream ("1.0",Nothing, Just hostname)
features <- ErrorT . pulls $ runErrorT xmppStream features <- ErrorT . pullSink $ runErrorT xmppStream
modify (\s -> s {sFeatures = features}) modify (\s -> s {sFeatures = features})
return () return ()
xmppRestartStream :: XMPPConMonad (Either StreamError ()) xmppRestartStream :: XMPPConMonad (Either StreamError ())
xmppRestartStream = do xmppRestartStream = do
raw <- gets sRawSrc raw <- gets sRawSrc
let newsrc = raw $= XP.parseBytes def newsrc <- liftIO . bufferSource $ raw $= XP.parseBytes def
modify (\s -> s{sConSrc = newsrc}) modify (\s -> s{sConSrc = newsrc})
xmppStartStream xmppStartStream

36
src/Network/XMPP/TLS.hs

@ -3,30 +3,20 @@
module Network.XMPP.TLS where module Network.XMPP.TLS where
import Control.Applicative((<$>))
import Control.Arrow(left)
import qualified Control.Exception.Lifted as Ex import qualified Control.Exception.Lifted as Ex
import Control.Monad import Control.Monad
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Control.Monad.Trans
import Data.Conduit
import Data.Conduit.List as CL
import Data.Conduit.TLS as TLS import Data.Conduit.TLS as TLS
import Data.Default
import Data.Typeable import Data.Typeable
import Data.XML.Types import Data.XML.Types
import qualified Network.TLS as TLS
import qualified Network.TLS.Extra as TLS
import Network.XMPP.Monad import Network.XMPP.Monad
import Network.XMPP.Pickle(ppElement)
import Network.XMPP.Stream import Network.XMPP.Stream
import Network.XMPP.Types import Network.XMPP.Types
import qualified Text.XML.Stream.Render as XR
starttlsE :: Element starttlsE :: Element
starttlsE = starttlsE =
Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] [] Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] []
@ -41,10 +31,11 @@ exampleParams = TLS.defaultParams
, pUseSecureRenegotiation = False -- No renegotiation , pUseSecureRenegotiation = False -- No renegotiation
, pCertificates = [] -- TODO , pCertificates = [] -- TODO
, pLogging = TLS.defaultLogging -- TODO , pLogging = TLS.defaultLogging -- TODO
, onCertificatesRecv = \ certificate -> , onCertificatesRecv = \ _certificate ->
return TLS.CertificateUsageAccept return TLS.CertificateUsageAccept
} }
-- | Error conditions that may arise during TLS negotiation.
data XMPPTLSError = TLSError TLSError data XMPPTLSError = TLSError TLSError
| TLSNoServerSupport | TLSNoServerSupport
| TLSNoConnection | TLSNoConnection
@ -53,29 +44,32 @@ data XMPPTLSError = TLSError TLSError
instance Error XMPPTLSError where instance Error XMPPTLSError where
noMsg = TLSNoConnection -- TODO: What should we choose here? noMsg = TLSNoConnection -- TODO: What should we choose here?
instance Ex.Exception XMPPTLSError
xmppStartTLS :: TLS.TLSParams -> XMPPConMonad (Either XMPPTLSError ()) startTLS :: TLS.TLSParams -> XMPPConMonad (Either XMPPTLSError ())
xmppStartTLS params = Ex.handle (return . Left . TLSError) startTLS params = Ex.handle (return . Left . TLSError)
. runErrorT $ do . runErrorT $ do
features <- lift $ gets sFeatures features <- lift $ gets sFeatures
handle' <- lift $ gets sConHandle handle' <- lift $ gets sConHandle
handle <- maybe (throwError TLSNoConnection) return handle' handle <- maybe (throwError TLSNoConnection) return handle'
when (stls features == Nothing) $ throwError TLSNoServerSupport when (stls features == Nothing) $ throwError TLSNoServerSupport
lift $ pushN starttlsE lift $ pushN starttlsE
answer <- lift $ pullE answer <- lift $ pullElement
case answer of case answer of
Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] [] -> return () Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] [] -> return ()
_ -> throwError $ TLSStreamError StreamXMLError Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _
(raw, snk, psh) <- lift $ TLS.tlsinit params handle -> lift . Ex.throwIO $ StreamConnectionError
-- TODO: find something more suitable
e -> lift . Ex.throwIO . StreamXMLError
$ "Unexpected element: " ++ ppElement e
(raw, _snk, psh, ctx) <- lift $ TLS.tlsinit params handle
lift $ modify (\x -> x lift $ modify (\x -> x
{ sRawSrc = raw { sRawSrc = raw
-- , sConSrc = -- Note: this momentarily leaves us in an -- , sConSrc = -- Note: this momentarily leaves us in an
-- inconsistent state -- inconsistent state
, sConPushBS = psh , sConPushBS = psh
, sCloseConnection = TLS.bye ctx >> sCloseConnection x
}) })
ErrorT $ (left TLSStreamError) <$> xmppRestartStream either (lift . Ex.throwIO) return =<< lift xmppRestartStream
modify (\s -> s{sHaveTLS = True}) modify (\s -> s{sConnectionState = XmppConnectionSecured})
return () return ()

384
src/Network/XMPP/Types.hs

@ -11,7 +11,42 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Network.XMPP.Types where module Network.XMPP.Types
( IQError(..)
, IQRequest(..)
, IQRequestType(..)
, IQResponse
, IQResult(..)
, IdGenerator(..)
, LangTag (..)
, Message(..)
, MessageError(..)
, MessageType(..)
, Presence(..)
, PresenceError(..)
, PresenceType(..)
, SaslError(..)
, SaslFailure(..)
, ServerAddress(..)
, ServerFeatures(..)
, ShowType(..)
, Stanza(..)
, StanzaError(..)
, StanzaErrorCondition(..)
, StanzaErrorType(..)
, StanzaId(..)
, StreamError(..)
, Version(..)
, XMPPConMonad
, XmppConnection(..)
, XmppConnectionState(..)
, XmppNoConnection(..)
, XMPPT(..)
, XmppStreamError(..)
, parseLangTag
, module Network.XMPP.JID
)
where
-- import Network.XMPP.Utilities (idGenerator) -- import Network.XMPP.Utilities (idGenerator)
@ -24,7 +59,6 @@ import Control.Monad.Error
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.Conduit import Data.Conduit
import Data.List.Split as L
import Data.String(IsString(..)) import Data.String(IsString(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
@ -33,16 +67,9 @@ import Data.XML.Types
import qualified Network as N import qualified Network as N
import System.IO import Network.XMPP.JID
-- | The string prefix MUST be
data SessionSettings =
SessionSettings { ssIdPrefix :: String
, ssIdGenerator :: IdGenerator
, ssStreamLang :: LangTag }
import System.IO
-- ============================================================================= -- =============================================================================
-- STANZA TYPES -- STANZA TYPES
@ -74,38 +101,6 @@ instance Read StanzaId where
instance IsString StanzaId where instance IsString StanzaId where
fromString = SI . Text.pack fromString = SI . Text.pack
-- |
-- @From@ is a readability type synonym for @Address@.
-- | Jabber ID (JID) datatype
data JID = JID { localpart :: !(Maybe Text)
-- ^ Account name
, domainpart :: !Text
-- ^ Server adress
, resourcepart :: !(Maybe Text)
-- ^ Resource name
}
instance Show JID where
show (JID nd dmn res) =
maybe "" ((++ "@") . Text.unpack) nd ++
(Text.unpack dmn) ++
maybe "" (('/' :) . Text.unpack) res
parseJID :: [Char] -> [JID]
parseJID jid = do
(jid', rst) <- case L.splitOn "@" jid of
[rest] -> [(JID Nothing, rest)]
[nd,rest] -> [(JID (Just (Text.pack nd)), rest)]
_ -> []
case L.splitOn "/" rst of
[dmn] -> [jid' (Text.pack dmn) Nothing]
[dmn, rsrc] -> [jid' (Text.pack dmn) (Just (Text.pack rsrc))]
_ -> []
instance Read JID where
readsPrec _ x = (,"") <$> parseJID x
-- An Info/Query (IQ) stanza is either of the type "request" ("get" or -- An Info/Query (IQ) stanza is either of the type "request" ("get" or
-- "set") or "response" ("result" or "error"). The @IQ@ type wraps -- "set") or "response" ("result" or "error"). The @IQ@ type wraps
-- these two sub-types. -- these two sub-types.
@ -120,14 +115,11 @@ data Stanza = IQRequestS IQRequest
| MessageErrorS MessageError | MessageErrorS MessageError
| PresenceS Presence | PresenceS Presence
| PresenceErrorS PresenceError | PresenceErrorS PresenceError
deriving Show
-- | -- |
-- A "request" Info/Query (IQ) stanza is one with either "get" or -- A "request" Info/Query (IQ) stanza is one with either "get" or
-- "set" as type. They are guaranteed to always contain a payload. -- "set" as type. They are guaranteed to always contain a payload.
--
-- Objects of this type cannot be generated by Pontarius applications,
-- but are only created internally.
data IQRequest = IQRequest { iqRequestID :: StanzaId data IQRequest = IQRequest { iqRequestID :: StanzaId
, iqRequestFrom :: Maybe JID , iqRequestFrom :: Maybe JID
, iqRequestTo :: Maybe JID , iqRequestTo :: Maybe JID
@ -137,7 +129,7 @@ data IQRequest = IQRequest { iqRequestID :: StanzaId
} }
deriving (Show) deriving (Show)
-- | The type of request that is made
data IQRequestType = Get | Set deriving (Eq, Ord) data IQRequestType = Get | Set deriving (Eq, Ord)
instance Show IQRequestType where instance Show IQRequestType where
@ -149,21 +141,12 @@ instance Read IQRequestType where
readsPrec _ "set" = [(Set, "")] readsPrec _ "set" = [(Set, "")]
readsPrec _ _ = [] readsPrec _ _ = []
-- | A "response" Info/Query (IQ) stanza is eitheran 'IQError' or an IQ stanza
-- | -- with the type "result" ('IQResult')
-- A "response" Info/Query (IQ) stanza is one with either "result" or
-- "error" as type. We have devided IQ responses into two types.
--
-- Objects of this type cannot be generated by Pontarius applications,
-- but are only created internally.
type IQResponse = Either IQError IQResult type IQResponse = Either IQError IQResult
-- | The answer to an IQ request
-- |
-- Objects of this type cannot be generated by Pontarius applications,
-- but are only created internally.
data IQResult = IQResult { iqResultID :: StanzaId data IQResult = IQResult { iqResultID :: StanzaId
, iqResultFrom :: Maybe JID , iqResultFrom :: Maybe JID
, iqResultTo :: Maybe JID , iqResultTo :: Maybe JID
@ -171,11 +154,7 @@ data IQResult = IQResult { iqResultID :: StanzaId
, iqResultPayload :: Maybe Element } , iqResultPayload :: Maybe Element }
deriving (Show) deriving (Show)
-- | The answer to an IQ request that generated an error
-- |
-- Objects of this type cannot be generated by Pontarius applications,
-- but are only created internally.
data IQError = IQError { iqErrorID :: StanzaId data IQError = IQError { iqErrorID :: StanzaId
, iqErrorFrom :: Maybe JID , iqErrorFrom :: Maybe JID
, iqErrorTo :: Maybe JID , iqErrorTo :: Maybe JID
@ -185,12 +164,7 @@ data IQError = IQError { iqErrorID :: StanzaId
} }
deriving (Show) deriving (Show)
-- | -- | The message stanza. Used for /push/ type communication
-- A non-error message stanza.
--
-- Objects of this type cannot be generated by Pontarius applications,
-- but are only created internally.
data Message = Message { messageID :: Maybe StanzaId data Message = Message { messageID :: Maybe StanzaId
, messageFrom :: Maybe JID , messageFrom :: Maybe JID
, messageTo :: Maybe JID , messageTo :: Maybe JID
@ -203,13 +177,7 @@ data Message = Message { messageID :: Maybe StanzaId
} }
deriving (Show) deriving (Show)
-- | An error stanza generated in response to a 'Message'
-- |
-- An error message stanza.
--
-- Objects of this type cannot be generated by Pontarius applications,
-- but are only created internally.
data MessageError = MessageError { messageErrorID :: Maybe StanzaId data MessageError = MessageError { messageErrorID :: Maybe StanzaId
, messageErrorFrom :: Maybe JID , messageErrorFrom :: Maybe JID
, messageErrorTo :: Maybe JID , messageErrorTo :: Maybe JID
@ -220,15 +188,47 @@ data MessageError = MessageError { messageErrorID :: Maybe StanzaId
deriving (Show) deriving (Show)
-- | -- | The type of a Message being sent
-- @MessageType@ holds XMPP message types as defined in XMPP-IM. The -- (<http://xmpp.org/rfcs/rfc6121.html#message-syntax-type>)
-- "error" message type is left out as errors are wrapped in data MessageType = -- | The message is sent in the context of a one-to-one chat
-- @MessageError@. -- session. Typically an interactive client will present a
-- message of type /chat/ in an interface that enables
data MessageType = Chat | -- ^ -- one-to-one chat between the two parties, including an
GroupChat | -- ^ -- appropriate conversation history.
Headline | -- ^ Chat
Normal -- ^ The default message type -- | The message is sent in the context of a
-- multi-user chat environment (similar to that of
-- @IRC@). Typically a receiving client will
-- present a message of type /groupchat/ in an
-- interface that enables many-to-many chat
-- between the parties, including a roster of
-- parties in the chatroom and an appropriate
-- conversation history.
| GroupChat
-- | The message provides an alert, a
-- notification, or other transient information to
-- which no reply is expected (e.g., news
-- headlines, sports updates, near-real-time
-- market data, or syndicated content). Because no
-- reply to the message is expected, typically a
-- receiving client will present a message of type
-- /headline/ in an interface that appropriately
-- differentiates the message from standalone
-- messages, chat messages, and groupchat messages
-- (e.g., by not providing the recipient with the
-- ability to reply).
| Headline
-- | The message is a standalone message that is
-- sent outside the context of a one-to-one
-- conversation or groupchat, and to which it is
-- expected that the recipient will
-- reply. Typically a receiving client will
-- present a message of type /normal/ in an
-- interface that enables the recipient to reply,
-- but without a conversation history.
--
-- This is the /default/ value
| Normal
deriving (Eq) deriving (Eq)
@ -341,7 +341,6 @@ instance Read ShowType where
-- wrapped in the @StanzaError@ type. -- wrapped in the @StanzaError@ type.
-- TODO: Sender XML is (optional and is) not included. -- TODO: Sender XML is (optional and is) not included.
data StanzaError = StanzaError { stanzaErrorType :: StanzaErrorType data StanzaError = StanzaError { stanzaErrorType :: StanzaErrorType
, stanzaErrorCondition :: StanzaErrorCondition , stanzaErrorCondition :: StanzaErrorCondition
, stanzaErrorText :: Maybe (Maybe LangTag, Text) , stanzaErrorText :: Maybe (Maybe LangTag, Text)
@ -471,81 +470,172 @@ instance Read StanzaErrorCondition where
-- OTHER STUFF -- OTHER STUFF
-- ============================================================================= -- =============================================================================
data SASLFailure = SASLFailure { saslFailureCondition :: SASLError data SaslFailure = SaslFailure { saslFailureCondition :: SaslError
, saslFailureText :: Maybe Text } -- TODO: XMLLang , saslFailureText :: Maybe ( Maybe LangTag
, Text
)
} deriving Show
data SASLError = -- SASLAborted | -- Client aborted - should not happen data SaslError = SaslAborted -- ^ Client aborted
SASLAccountDisabled | -- ^ The account has been temporarily | SaslAccountDisabled -- ^ The account has been temporarily
-- disabled -- disabled
SASLCredentialsExpired | -- ^ The authentication failed because | SaslCredentialsExpired -- ^ The authentication failed because
-- the credentials have expired -- the credentials have expired
SASLEncryptionRequired | -- ^ The mechanism requested cannot be | SaslEncryptionRequired -- ^ The mechanism requested cannot be
-- used the confidentiality and -- used the confidentiality and
-- integrity of the underlying -- integrity of the underlying
-- stream is protected (typically -- stream is protected (typically
-- with TLS) -- with TLS)
-- SASLIncorrectEncoding | -- The base64 encoding is incorrect | SaslIncorrectEncoding -- ^ The base64 encoding is incorrect
-- - should not happen | SaslInvalidAuthzid -- ^ The authzid has an incorrect
-- SASLInvalidAuthzid | -- The authzid has an incorrect format, -- format or the initiating entity does
-- or the initiating entity does not -- not have the appropriate permissions
-- have the appropriate permissions to -- to authorize that ID
-- authorize that ID | SaslInvalidMechanism -- ^ The mechanism is not supported by
SASLInvalidMechanism | -- ^ The mechanism is not supported by
-- the receiving entity -- the receiving entity
-- SASLMalformedRequest | -- Invalid syntax - should not happen | SaslMalformedRequest -- ^ Invalid syntax
SASLMechanismTooWeak | -- ^ The receiving entity policy | SaslMechanismTooWeak -- ^ The receiving entity policy
-- requires a stronger mechanism -- requires a stronger mechanism
SASLNotAuthorized (Maybe Text) | -- ^ Invalid credentials | SaslNotAuthorized -- ^ Invalid credentials
-- provided, or some -- provided, or some
-- generic authentication -- generic authentication
-- failure has occurred -- failure has occurred
SASLTemporaryAuthFailure -- ^ There receiving entity reported a | SaslTemporaryAuthFailure -- ^ There receiving entity reported a
-- temporary error condition; the -- temporary error condition; the
-- initiating entity is recommended -- initiating entity is recommended
-- to try again later -- to try again later
instance Show SaslError where
show SaslAborted = "aborted"
show SaslAccountDisabled = "account-disabled"
show SaslCredentialsExpired = "credentials-expired"
show SaslEncryptionRequired = "encryption-required"
show SaslIncorrectEncoding = "incorrect-encoding"
show SaslInvalidAuthzid = "invalid-authzid"
show SaslInvalidMechanism = "invalid-mechanism"
show SaslMalformedRequest = "malformed-request"
show SaslMechanismTooWeak = "mechanism-too-weak"
show SaslNotAuthorized = "not-authorized"
show SaslTemporaryAuthFailure = "temporary-auth-failure"
instance Read SaslError where
readsPrec _ "aborted" = [(SaslAborted , "")]
readsPrec _ "account-disabled" = [(SaslAccountDisabled , "")]
readsPrec _ "credentials-expired" = [(SaslCredentialsExpired , "")]
readsPrec _ "encryption-required" = [(SaslEncryptionRequired , "")]
readsPrec _ "incorrect-encoding" = [(SaslIncorrectEncoding , "")]
readsPrec _ "invalid-authzid" = [(SaslInvalidAuthzid , "")]
readsPrec _ "invalid-mechanism" = [(SaslInvalidMechanism , "")]
readsPrec _ "malformed-request" = [(SaslMalformedRequest , "")]
readsPrec _ "mechanism-too-weak" = [(SaslMechanismTooWeak , "")]
readsPrec _ "not-authorized" = [(SaslNotAuthorized , "")]
readsPrec _ "temporary-auth-failure" = [(SaslTemporaryAuthFailure , "")]
readsPrec _ _ = []
-- | Readability type for host name Texts. -- | Readability type for host name Texts.
-- type HostName = Text -- This is defined in Network as well -- type HostName = Text -- This is defined in Network as well
-- | Readability type for port number Integers.
type PortNumber = Integer -- We use N(etwork).PortID (PortNumber) internally
-- | Readability type for user name Texts.
type UserName = Text
-- | Readability type for password Texts.
type Password = Text
-- | Readability type for (Address) resource identifier Texts.
type Resource = Text
type StreamID = Text
data ServerAddress = ServerAddress N.HostName N.PortNumber deriving (Eq) data ServerAddress = ServerAddress N.HostName N.PortNumber deriving (Eq)
type Timeout = Int -- TODO: document the error cases
data StreamErrorCondition = StreamBadFormat
| StreamBadNamespacePrefix
| StreamConflict
| StreamConnectionTimeout
| StreamHostGone
| StreamHostUnknown
| StreamImproperAddressing
| StreamInternalServerError
| StreamInvalidFrom
| StreamInvalidNamespace
| StreamInvalidXml
| StreamNotAuthorized
| StreamNotWellFormed
| StreamPolicyViolation
| StreamRemoteConnectionFailed
| StreamReset
| StreamResourceConstraint
| StreamRestrictedXml
| StreamSeeOtherHost
| StreamSystemShutdown
| StreamUndefinedCondition
| StreamUnsupportedEncoding
| StreamUnsupportedFeature
| StreamUnsupportedStanzaType
| StreamUnsupportedVersion
deriving Eq
data StreamError = StreamError String instance Show StreamErrorCondition where
show StreamBadFormat = "bad-format"
show StreamBadNamespacePrefix = "bad-namespace-prefix"
show StreamConflict = "conflict"
show StreamConnectionTimeout = "connection-timeout"
show StreamHostGone = "host-gone"
show StreamHostUnknown = "host-unknown"
show StreamImproperAddressing = "improper-addressing"
show StreamInternalServerError = "internal-server-error"
show StreamInvalidFrom = "invalid-from"
show StreamInvalidNamespace = "invalid-namespace"
show StreamInvalidXml = "invalid-xml"
show StreamNotAuthorized = "not-authorized"
show StreamNotWellFormed = "not-well-formed"
show StreamPolicyViolation = "policy-violation"
show StreamRemoteConnectionFailed = "remote-connection-failed"
show StreamReset = "reset"
show StreamResourceConstraint = "resource-constraint"
show StreamRestrictedXml = "restricted-xml"
show StreamSeeOtherHost = "see-other-host"
show StreamSystemShutdown = "system-shutdown"
show StreamUndefinedCondition = "undefined-condition"
show StreamUnsupportedEncoding = "unsupported-encoding"
show StreamUnsupportedFeature = "unsupported-feature"
show StreamUnsupportedStanzaType = "unsupported-stanza-type"
show StreamUnsupportedVersion = "unsupported-version"
instance Read StreamErrorCondition where
readsPrec _ "bad-format" = [(StreamBadFormat , "")]
readsPrec _ "bad-namespace-prefix" = [(StreamBadNamespacePrefix , "")]
readsPrec _ "conflict" = [(StreamConflict , "")]
readsPrec _ "connection-timeout" = [(StreamConnectionTimeout , "")]
readsPrec _ "host-gone" = [(StreamHostGone , "")]
readsPrec _ "host-unknown" = [(StreamHostUnknown , "")]
readsPrec _ "improper-addressing" = [(StreamImproperAddressing , "")]
readsPrec _ "internal-server-error" = [(StreamInternalServerError , "")]
readsPrec _ "invalid-from" = [(StreamInvalidFrom , "")]
readsPrec _ "invalid-namespace" = [(StreamInvalidNamespace , "")]
readsPrec _ "invalid-xml" = [(StreamInvalidXml , "")]
readsPrec _ "not-authorized" = [(StreamNotAuthorized , "")]
readsPrec _ "not-well-formed" = [(StreamNotWellFormed , "")]
readsPrec _ "policy-violation" = [(StreamPolicyViolation , "")]
readsPrec _ "remote-connection-failed" = [(StreamRemoteConnectionFailed , "")]
readsPrec _ "reset" = [(StreamReset , "")]
readsPrec _ "resource-constraint" = [(StreamResourceConstraint , "")]
readsPrec _ "restricted-xml" = [(StreamRestrictedXml , "")]
readsPrec _ "see-other-host" = [(StreamSeeOtherHost , "")]
readsPrec _ "system-shutdown" = [(StreamSystemShutdown , "")]
readsPrec _ "undefined-condition" = [(StreamUndefinedCondition , "")]
readsPrec _ "unsupported-encoding" = [(StreamUnsupportedEncoding , "")]
readsPrec _ "unsupported-feature" = [(StreamUnsupportedFeature , "")]
readsPrec _ "unsupported-stanza-type" = [(StreamUnsupportedStanzaType , "")]
readsPrec _ "unsupported-version" = [(StreamUnsupportedVersion , "")]
readsPrec _ _ = [(StreamUndefinedCondition , "")]
data XmppStreamError = XmppStreamError
{ errorCondition :: StreamErrorCondition
, errorText :: Maybe (Maybe LangTag, Text)
, errorXML :: Maybe Element
} deriving (Show, Eq)
data StreamError = StreamError XmppStreamError
| StreamWrongVersion Text | StreamWrongVersion Text
| StreamXMLError | StreamXMLError String
| StreamUnpickleError String
| StreamConnectionError | StreamConnectionError
deriving (Show, Eq, Typeable) deriving (Show, Eq, Typeable)
instance Exception StreamError instance Exception StreamError
instance Error StreamError where strMsg = StreamError instance Error StreamError where noMsg = StreamConnectionError
-- ============================================================================= -- =============================================================================
-- XML TYPES -- XML TYPES
@ -610,24 +700,32 @@ instance Read LangTag where
-- all (\ (a, b) -> map toLower a == map toLower b) $ zip as bs -- all (\ (a, b) -> map toLower a == map toLower b) $ zip as bs
-- | otherwise = False -- | otherwise = False
data ServerFeatures = SF data ServerFeatures = SF
{ stls :: Maybe Bool { stls :: Maybe Bool
, saslMechanisms :: [Text.Text] , saslMechanisms :: [Text.Text]
, other :: [Element] , other :: [Element]
} deriving Show } deriving Show
data XMPPConState = XMPPConState data XmppConnectionState = XmppConnectionClosed -- ^ No connection at
-- this point
| XmppConnectionPlain -- ^ Connection
-- established, but
-- not secured
| XmppConnectionSecured -- ^ Connection
-- established and
-- secured via TLS
data XmppConnection = XmppConnection
{ sConSrc :: Source IO Event { sConSrc :: Source IO Event
, sRawSrc :: Source IO BS.ByteString , sRawSrc :: Source IO BS.ByteString
, sConPushBS :: BS.ByteString -> IO () , sConPushBS :: BS.ByteString -> IO ()
, sConHandle :: Maybe Handle , sConHandle :: Maybe Handle
, sFeatures :: ServerFeatures , sFeatures :: ServerFeatures
, sHaveTLS :: Bool , sConnectionState :: XmppConnectionState
, sHostname :: Maybe Text , sHostname :: Maybe Text
, sUsername :: Maybe Text , sUsername :: Maybe Text
, sResource :: Maybe Text , sResource :: Maybe Text
, sCloseConnection :: IO ()
-- TODO: add default Language
} }
-- | -- |
@ -635,14 +733,14 @@ data XMPPConState = XMPPConState
-- work with Pontarius. Pontarius clients needs to operate in this -- work with Pontarius. Pontarius clients needs to operate in this
-- context. -- context.
newtype XMPPT m a = XMPPT { runXMPPT :: StateT XMPPConState m a } deriving (Monad, MonadIO) newtype XMPPT m a = XMPPT { runXMPPT :: StateT XmppConnection m a } deriving (Monad, MonadIO)
type XMPPConMonad a = StateT XMPPConState IO a type XMPPConMonad a = StateT XmppConnection IO a
-- Make XMPPT derive the Monad and MonadIO instances. -- Make XMPPT derive the Monad and MonadIO instances.
deriving instance (Monad m, MonadIO m) => MonadState (XMPPConState) (XMPPT m) deriving instance (Monad m, MonadIO m) => MonadState (XmppConnection) (XMPPT m)
data XmppNoConnection = XmppNoConnection deriving (Show, Typeable)
instance Exception XmppNoConnection
-- We need a channel because multiple threads needs to append events,
-- and we need to wait for events when there are none.

149
src/Network/XMPP/Utilities.hs

@ -1,64 +1,62 @@
-- Copyright © 2010-2012 Jon Kristensen. See the LICENSE file in the -- Copyright © 2010-2012 Jon Kristensen. See the LICENSE file in the Pontarius
-- Pontarius distribution for more details. -- distribution for more details.
-- This module currently converts XML elements to strings. -- TODO: More efficient to use Text instead of Strings for ID generation?
-- TODO: Use -fno-cse? http://cvs.haskell.org/Hugs/pages/libraries/base/System-IO-Unsafe.html
-- TODO: Remove elementsToString?
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Network.XMPP.Utilities ( idGenerator module Network.XMPP.Utilities (idGenerator) where
, nextId
-- elementToString
-- , elementsToString ) where
) where
import Network.XMPP.Types import Network.XMPP.Types
import Prelude hiding (concat) import Control.Monad.STM
import Control.Concurrent.STM.TVar
import Data.ByteString (ByteString, concat) import Prelude
import Data.ByteString.Char8 (unpack) import Control.Applicative (many)
import Data.Enumerator (($$), Stream (Chunks), Enumerator, Step (Continue), joinI, run_, returnI)
import Data.Enumerator.List (consume)
import Data.XML.Types (Document (..), Element (..), Event (..), Name (..), Prologue (..))
import Data.IORef (atomicModifyIORef, newIORef)
import qualified Data.Attoparsec.Text as AP
-- import Text.XML.Enumerator.Render (renderBytes) import qualified Data.Text as Text
-- import Text.XML.Enumerator.Document (toEvents)
import System.IO.Unsafe (unsafePerformIO)
-- | -- |
-- Creates a new stanza "IdGenerator". Internally, it will maintain an infinite -- Creates a new @IdGenerator@. Internally, it will maintain an infinite list of
-- list of stanza IDs ('[\'a\', \'b\', \'c\'...]'). -- 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
-- internal state so that the same ID will not be generated again.
idGenerator :: String -> IO IdGenerator idGenerator :: Text.Text -> IO IdGenerator
idGenerator p = newIORef (ids p) >>= \ ioRef -> return $ IdGenerator ioRef idGenerator prefix = atomically $ do
tvar <- newTVar $ ids prefix
return $ IdGenerator $ next tvar
where where
-- Generates an infinite and predictable list of IDs, all -- Transactionally extract the next ID from the infinite list of IDs.
-- beginning with the provided prefix.
next :: TVar [Text.Text] -> IO Text.Text
next tvar = atomically $ do
list <- readTVar tvar
case list of
[] -> error "empty list in Utilities.hs"
(x:xs) -> do
writeTVar tvar xs
return x
-- Generates an infinite and predictable list of IDs, all beginning with the
-- provided prefix.
ids :: String -> [String] ids :: Text.Text -> [Text.Text]
-- Adds the prefix to all combinations of IDs (ids'). -- Adds the prefix to all combinations of IDs (ids').
ids p = map (\ id -> p ++ id) ids' ids p = 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' :: [String] ids' :: [Text.Text]
ids' = concatMap ids'' [1..] ids' = map Text.pack $ 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]
@ -70,46 +68,53 @@ idGenerator p = newIORef (ids p) >>= \ ioRef -> return $ IdGenerator ioRef
repertoire = ['a'..'z'] repertoire = ['a'..'z']
-- Converts a "<major>.<minor>" numeric version number to a @Version@ object.
-- | versionFromString :: Text.Text -> Maybe Version
-- Extracts an ID from the "IDGenerator", and updates the generators internal versionFromString s = case AP.parseOnly versionParser s of
-- state so that the same ID will not be generated again. Right version -> Just version
Left _ -> Nothing
nextId :: IdGenerator -> IO String
nextId g = let IdGenerator ioRef = g
in atomicModifyIORef ioRef (\ (i:is) -> (is, i))
-- Converts the Element objects to a document, converts it into Events, strips -- Constructs a "Version" based on the major and minor version numbers.
-- the DocumentBegin event, generates a ByteString, and converts it into a versionFromNumbers :: Integer -> Integer -> Version
-- String, aggregates the results and returns a string. versionFromNumbers major minor = Version major minor
-- elementsToString :: [Element] -> String
-- elementsToString [] = "" -- Read numbers, a dot, more numbers, and end-of-file.
-- elementsToString (e:es) = (elementToString (Just e)) ++ (elementsToString es) versionParser :: AP.Parser Version
versionParser = do
major <- AP.many1 AP.digit
AP.skip (== '.')
minor <- AP.many1 AP.digit
AP.endOfInput
return $ Version (read major) (read minor)
-- Converts the Element object to a document, converts it into Events, strips -- | Parses, validates, and possibly constructs a "LangTag" object.
-- the DocumentBegin event, generates a ByteString, and converts it into a langTag :: Text.Text -> Maybe LangTag
-- String. langTag s = case AP.parseOnly langTagParser s of
Right tag -> Just tag
Left _ -> Nothing
-- {-# NOINLINE elementToString #-}
-- elementToString :: Maybe Element -> String -- Parses a language tag as defined by RFC 1766 and constructs a LangTag object.
langTagParser :: AP.Parser LangTag
-- elementToString Nothing = "" langTagParser = do
-- elementToString (Just elem) = unpack $ concat $ unsafePerformIO $ do -- Read until we reach a '-' character, or EOF. This is the `primary tag'.
-- r <- run_ $ events $$ (joinI $ renderBytes $$ consume) primTag <- tag
-- return r -- Read zero or more subtags.
-- where subTags <- many subtag
AP.endOfInput
-- Enumerator that "produces" the events to convert to the document return $ LangTag primTag subTags
-- events :: Enumerator Event IO [ByteString] where
-- events (Continue more) = more $ Chunks (tail $ toEvents $ dummyDoc elem) tag :: AP.Parser Text.Text
-- events step = returnI step tag = do
t <- AP.takeWhile1 $ AP.inClass tagChars
-- dummyDoc :: Element -> Document return t
-- dummyDoc e = Document (Prologue [] Nothing []) elem [] subtag :: AP.Parser Text.Text
subtag = do
AP.skip (== '-')
subtag <- tag
return subtag
tagChars :: [Char]
tagChars = ['a'..'z'] ++ ['A'..'Z']

45
src/Tests.hs

@ -28,13 +28,17 @@ supervisor :: JID
supervisor = read "uart14@species64739.dyndns.org" supervisor = read "uart14@species64739.dyndns.org"
attXmpp :: STM a -> XMPPThread a attXmpp :: STM a -> XMPP a
attXmpp = liftIO . atomically attXmpp = liftIO . atomically
testNS :: Text testNS :: Text
testNS = "xmpp:library:test" testNS = "xmpp:library:test"
data Payload = Payload Int Bool Text deriving (Eq, Show) data Payload = Payload
{ payloadCounter ::Int
, payloadFlag :: Bool
, payloadText :: Text
} deriving (Eq, Show)
payloadP = xpWrap (\((counter,flag) , message) -> Payload counter flag message) payloadP = xpWrap (\((counter,flag) , message) -> Payload counter flag message)
(\(Payload counter flag message) ->((counter,flag) , message)) $ (\(Payload counter flag message) ->((counter,flag) , message)) $
@ -49,17 +53,20 @@ payloadP = xpWrap (\((counter,flag) , message) -> Payload counter flag message)
invertPayload (Payload count flag message) = Payload (count + 1) (not flag) (Text.reverse message) invertPayload (Payload count flag message) = Payload (count + 1) (not flag) (Text.reverse message)
iqResponder = do iqResponder = do
(free, chan) <- listenIQChan Get testNS chan' <- listenIQChan Get testNS
unless free $ liftIO $ putStrLn "Channel was already taken" chan <- case chan' of
Nothing -> liftIO $ putStrLn "Channel was already taken"
>> error "hanging up" >> error "hanging up"
Just c -> return c
forever $ do forever $ do
next@(iq,_) <- liftIO . atomically $ readTChan chan next@(iq,_) <- liftIO . atomically $ readTChan chan
let Right payload = unpickleElem payloadP $ iqRequestPayload iq let Right payload = unpickleElem payloadP $ iqRequestPayload iq
let answerPayload = invertPayload payload let answerPayload = invertPayload payload
let answerBody = pickleElem payloadP answerPayload let answerBody = pickleElem payloadP answerPayload
answerIQ next (Right $ Just answerBody) answerIQ next (Right $ Just answerBody)
when (payloadCounter payload == 10) endSession
autoAccept :: XMPPThread () autoAccept :: XMPP ()
autoAccept = forever $ do autoAccept = forever $ do
st <- waitForPresence isPresenceSubscribe st <- waitForPresence isPresenceSubscribe
sendPresence $ presenceSubscribed (fromJust $ presenceFrom st) sendPresence $ presenceSubscribed (fromJust $ presenceFrom st)
@ -84,33 +91,39 @@ runMain debug number = do
_ -> error "Need either 1 or 2" _ -> error "Need either 1 or 2"
let debug' = liftIO . atomically . let debug' = liftIO . atomically .
debug . (("Thread " ++ show number ++ ":") ++) debug . (("Thread " ++ show number ++ ":") ++)
xmppNewSession $ do wait <- newEmptyTMVarIO
withNewSession $ do
setSessionEndHandler (liftIO . atomically $ putTMVar wait ())
debug' "running" debug' "running"
withConnection $ do withConnection $ do
xmppConnect "localhost" "species64739.dyndns.org" connect "localhost" "species64739.dyndns.org"
xmppStartTLS exampleParams startTLS exampleParams
saslResponse <- xmppSASL (fromJust $ localpart we) "pwd" saslResponse <- auth (fromJust $ localpart we) "pwd" (resourcepart we)
case saslResponse of case saslResponse of
Right _ -> return () Right _ -> return ()
Left e -> error e Left e -> error $ show e
xmppThreadedBind (resourcepart we)
withConnection $ xmppSession
debug' "session standing" debug' "session standing"
sendPresence presenceOnline sendPresence presenceOnline
forkXMPP autoAccept fork autoAccept
forkXMPP iqResponder sendPresence $ presenceSubscribe them
when active . void . forkXMPP $ do fork iqResponder
when active $ do
liftIO $ threadDelay 1000000 -- Wait for the other thread to go online
void . fork $ do
forM [1..10] $ \count -> do forM [1..10] $ \count -> do
let message = Text.pack . show $ localpart we let message = Text.pack . show $ localpart we
let payload = Payload count (even count) (Text.pack $ show count) let payload = Payload count (even count) (Text.pack $ show count)
let body = pickleElem payloadP payload let body = pickleElem payloadP payload
debug' "sending"
Right answer <- sendIQ' (Just them) Get Nothing body Right answer <- sendIQ' (Just them) Get Nothing body
debug' "received"
let Right answerPayload = unpickleElem payloadP let Right answerPayload = unpickleElem payloadP
(fromJust $ iqResultPayload answer) (fromJust $ iqResultPayload answer)
expect debug' (invertPayload payload) answerPayload expect debug' (invertPayload payload) answerPayload
liftIO $ threadDelay 100000 liftIO $ threadDelay 100000
sendUser "All tests done" sendUser "All tests done"
liftIO . forever $ threadDelay 10000000 endSession
liftIO . atomically $ takeTMVar wait
return () return ()
return () return ()

1
src/Text/XML/Stream/Elements.hs

@ -1,3 +1,4 @@
{-# OPTIONS_HADDOCK hide #-}
module Text.XML.Stream.Elements where module Text.XML.Stream.Elements where
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))

17
tests/Stanzas.hs

@ -0,0 +1,17 @@
{-# LANGUAGE OverloadedStrings #-}
module Tests.Stanzas where
import Data.Either
import Data.XML.Types
import Network.XMPP
import Network.XMPP.Marshal
import Network.XMPP.Pickle
import Network.XMPP.Types
xml1 = Element {elementName = Name {nameLocalName = "iq", nameNamespace = Just "jabber:client", namePrefix = Nothing}, elementAttributes = [(Name {nameLocalName = "id", nameNamespace = Nothing, namePrefix = Nothing},[ContentText "2"]), (Name {nameLocalName = "type", nameNamespace = Nothing, namePrefix = Nothing},[ContentText "error"]),(Name {nameLocalName = "to", nameNamespace = Nothing, namePrefix = Nothing},[ContentText "testuser1@species64739.dyndns.org/bot1"]),(Name {nameLocalName = "from", nameNamespace = Nothing, namePrefix = Nothing},[ContentText "testuser2@species64739.dyndns.org/bot2"])], elementNodes = [NodeElement (Element {elementName = Name {nameLocalName = "error", nameNamespace = Just "jabber:client", namePrefix = Nothing}, elementAttributes = [(Name {nameLocalName = "type", nameNamespace = Nothing, namePrefix = Nothing},[ContentText "cancel"])], elementNodes = [NodeElement (Element {elementName = Name {nameLocalName = "service-unavailable", nameNamespace = Just "urn:ietf:params:xml:ns:xmpp-stanzas", namePrefix = Nothing}, elementAttributes = [], elementNodes = []})]})]}
isRight (Right _) = True
isRight _ = False
testXML1 = isRight $ unpickleElem stanzaP xml1
Loading…
Cancel
Save