Browse Source

Merge remote-tracking branch 'nejla/master'

Conflicts:
	pontarius-xmpp.cabal
	source/Network/Xmpp/Basic.hs
	source/Network/Xmpp/Concurrent/Channels/Types.hs
	source/Network/Xmpp/Connection.hs
	source/Network/Xmpp/Errors.hs
	source/Network/Xmpp/Marshal.hs
	source/Network/Xmpp/Pickle.hs
	source/Network/Xmpp/Session.hs
	source/Network/Xmpp/Stream.hs
	source/Network/Xmpp/Tls.hs
	source/Network/Xmpp/Types.hs
	source/Network/Xmpp/Xep/InbandRegistration.hs
master
Philipp Balzarek 13 years ago
parent
commit
5cf70fa7e2
  1. 4
      README
  2. 10
      examples/EchoClient.hs
  3. BIN
      import_visualisation.png
  4. 38
      pontarius-xmpp.cabal
  5. 5
      source/Data/Conduit/Tls.hs
  6. 59
      source/Network/Xmpp.hs
  7. 32
      source/Network/Xmpp/Bind.hs
  8. 111
      source/Network/Xmpp/Concurrent.hs
  9. 4
      source/Network/Xmpp/Concurrent/Basic.hs
  10. 112
      source/Network/Xmpp/Concurrent/Channels.hs
  11. 7
      source/Network/Xmpp/Concurrent/IQ.hs
  12. 6
      source/Network/Xmpp/Concurrent/Message.hs
  13. 14
      source/Network/Xmpp/Concurrent/Monad.hs
  14. 5
      source/Network/Xmpp/Concurrent/Presence.hs
  15. 43
      source/Network/Xmpp/Concurrent/Threads.hs
  16. 47
      source/Network/Xmpp/Concurrent/Types.hs
  17. 295
      source/Network/Xmpp/Connection.hs
  18. 285
      source/Network/Xmpp/Connection_.hs
  19. 8
      source/Network/Xmpp/Marshal.hs
  20. 2
      source/Network/Xmpp/Pickle.hs
  21. 30
      source/Network/Xmpp/Sasl.hs
  22. 40
      source/Network/Xmpp/Sasl/Common.hs
  23. 9
      source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs
  24. 2
      source/Network/Xmpp/Sasl/Mechanisms/Plain.hs
  25. 4
      source/Network/Xmpp/Sasl/Mechanisms/Scram.hs
  26. 22
      source/Network/Xmpp/Sasl/Types.hs
  27. 136
      source/Network/Xmpp/Session.hs
  28. 163
      source/Network/Xmpp/Stream.hs
  29. 54
      source/Network/Xmpp/Tls.hs
  30. 128
      source/Network/Xmpp/Types.hs
  31. 8
      source/Network/Xmpp/Xep/InbandRegistration.hs
  32. 24
      source/Network/Xmpp/Xep/ServiceDiscovery.hs
  33. 2
      source/Text/Xml/Stream/Elements.hs
  34. 7
      source/Utils.hs

4
README

@ -1,2 +1,2 @@
Pontarius is an active work in progress to build a Haskell XMPP library that Pontarius XMPP is an active work in progress to build a Haskell XMPP library
implements the client capabilities of RFC 6120 ("XMPP Core"). that implements the client capabilities of RFC 6120 ("XMPP Core").

10
examples/EchoClient.hs

@ -22,6 +22,11 @@ import Text.Printf
import Network.Xmpp import Network.Xmpp
import Network.Xmpp.IM import Network.Xmpp.IM
import System.Log.Formatter
import System.Log.Logger
import System.Log.Handler hiding (setLevel)
import System.Log.Handler.Simple
import System.IO (stderr)
-- Server and authentication details. -- Server and authentication details.
host = "localhost" host = "localhost"
@ -41,6 +46,11 @@ autoAccept session = forever $ do
main :: IO () main :: IO ()
main = do main = do
updateGlobalLogger "Pontarius.Xmpp" $ setLevel DEBUG
handler <- streamHandler stderr DEBUG >>= \h ->
return $ setFormatter h (simpleLogFormatter "$time - $loggername: $prio: $msg")
updateGlobalLogger "Pontarius.Xmpp" (addHandler handler)
sess <- simpleConnect sess <- simpleConnect
host host
port port

BIN
import_visualisation.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

38
pontarius-xmpp.cabal

@ -31,13 +31,14 @@ Library
, resourcet >=0.3.0 , resourcet >=0.3.0
, containers >=0.4.0.0 , containers >=0.4.0.0
, random >=1.0.0.0 , random >=1.0.0.0
, tls >=1.0.0 , tls >=1.1.0
, tls-extra >=0.5.0 , tls-extra >=0.5.0
, pureMD5 >=2.1.2.1 , pureMD5 >=2.1.2.1
, base64-bytestring >=0.1.0.0 , base64-bytestring >=0.1.0.0
, binary >=0.4.1 , binary >=0.4.1
, attoparsec >=0.10.0.3 , attoparsec >=0.10.0.3
, crypto-api >=0.9 , crypto-api >=0.9
, crypto-random-api >=0.2
, cryptohash >=0.6.1 , cryptohash >=0.6.1
, text >=0.11.1.5 , text >=0.11.1.5
, bytestring >=0.9.1.9 , bytestring >=0.9.1.9
@ -52,43 +53,42 @@ Library
, xml-picklers >=0.3 , xml-picklers >=0.3
, data-default >=0.2 , data-default >=0.2
, stringprep >=0.1.3 , stringprep >=0.1.3
, hslogger >=1.1.0
Exposed-modules: Network.Xmpp Exposed-modules: Network.Xmpp
, Network.Xmpp.Connection
, Network.Xmpp.IM , Network.Xmpp.IM
, Network.Xmpp.Basic Other-modules: Data.Conduit.Tls
, Network.Xmpp.Lens
-- Undocumented modules
, Network.Xmpp.Bind , Network.Xmpp.Bind
, Network.Xmpp.Concurrent , Network.Xmpp.Concurrent
, Network.Xmpp.Concurrent.Types
, Network.Xmpp.Concurrent.Basic
, Network.Xmpp.Concurrent.IQ
, Network.Xmpp.Concurrent.Message
, Network.Xmpp.Concurrent.Presence
, Network.Xmpp.Concurrent.Threads
, Network.Xmpp.Concurrent.Monad
, Network.Xmpp.Connection_
, Network.Xmpp.IM.Message , Network.Xmpp.IM.Message
, Network.Xmpp.IM.Presence , Network.Xmpp.IM.Presence
, Network.Xmpp.Jid
, Network.Xmpp.Marshal , Network.Xmpp.Marshal
, Network.Xmpp.Connection
, Network.Xmpp.Message , Network.Xmpp.Message
, Network.Xmpp.Pickle , Network.Xmpp.Pickle
, Network.Xmpp.Presence , Network.Xmpp.Presence
, Network.Xmpp.Sasl , Network.Xmpp.Sasl
, Network.Xmpp.Sasl.Common
, Network.Xmpp.Sasl.Mechanisms , Network.Xmpp.Sasl.Mechanisms
, Network.Xmpp.Sasl.Mechanisms.Plain
, Network.Xmpp.Sasl.Mechanisms.DigestMd5 , Network.Xmpp.Sasl.Mechanisms.DigestMd5
, Network.Xmpp.Sasl.Mechanisms.Plain
, Network.Xmpp.Sasl.Mechanisms.Scram , Network.Xmpp.Sasl.Mechanisms.Scram
, Network.Xmpp.Sasl.StringPrep
, Network.Xmpp.Sasl.Types , Network.Xmpp.Sasl.Types
, Network.Xmpp.Session , Network.Xmpp.Session
, Network.Xmpp.Stream , Network.Xmpp.Stream
, Network.Xmpp.TLS , Network.Xmpp.Tls
, Network.Xmpp.Types , Network.Xmpp.Types
, Network.Xmpp.Xep.ServiceDiscovery , Network.Xmpp.Xep.ServiceDiscovery
, Network.Xmpp.Jid , Text.Xml.Stream.Elements
, Network.Xmpp.Concurrent.Types
, Network.Xmpp.Concurrent.Channels.IQ
, Network.Xmpp.Concurrent.Channels
, Network.Xmpp.Concurrent.Channels.Types
, Network.Xmpp.Concurrent.Threads
, Network.Xmpp.Concurrent.Monad
, Text.XML.Stream.Elements
, Data.Conduit.TLS
, Network.Xmpp.Sasl.Common
, Network.Xmpp.Sasl.StringPrep
, Network.Xmpp.Errors
GHC-Options: -Wall GHC-Options: -Wall
Source-Repository head Source-Repository head

5
source/Data/Conduit/TLS.hs → source/Data/Conduit/Tls.hs

@ -1,6 +1,6 @@
{-# Language NoMonomorphismRestriction #-} {-# Language NoMonomorphismRestriction #-}
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
module Data.Conduit.TLS module Data.Conduit.Tls
( tlsinit ( tlsinit
-- , conduitStdout -- , conduitStdout
, module TLS , module TLS
@ -21,6 +21,7 @@ import qualified Data.Conduit.Binary as CB
import Data.IORef import Data.IORef
import Network.TLS as TLS import Network.TLS as TLS
import Crypto.Random.API
import Network.TLS.Extra as TLSExtra import Network.TLS.Extra as TLSExtra
import System.IO (Handle) import System.IO (Handle)
@ -42,7 +43,7 @@ tlsinit :: (MonadIO m, MonadIO m1) =>
) )
tlsinit debug tlsParams backend = do tlsinit debug tlsParams backend = do
when debug . liftIO $ putStrLn "TLS with debug mode enabled" when debug . liftIO $ putStrLn "TLS with debug mode enabled"
gen <- liftIO $ (newGenIO :: IO SystemRandom) -- TODO: Find better random source? gen <- liftIO $ getSystemRandomGen -- TODO: Find better random source?
con <- client tlsParams gen backend con <- client tlsParams gen backend
handshake con handshake con
let src = forever $ do let src = forever $ do

59
source/Network/Xmpp.hs

@ -1,7 +1,5 @@
-- | -- |
-- Module: $Header$ -- Module: $Header$
-- Description: RFC 6120 (XMPP: Core).
-- License: Apache License 2.0
-- --
-- Maintainer: info@jonkri.com -- Maintainer: info@jonkri.com
-- Stability: unstable -- Stability: unstable
@ -16,36 +14,28 @@
-- persistent XML streams among a distributed network of globally addressable, -- persistent XML streams among a distributed network of globally addressable,
-- presence-aware clients and servers. -- presence-aware clients and servers.
-- --
-- Pontarius is an XMPP client library, implementing the core capabilities of -- Pontarius XMPP is an XMPP client library, implementing the core capabilities
-- XMPP (RFC 6120): setup and teardown of XML streams, channel encryption, -- of XMPP (RFC 6120): setup and teardown of XML streams, channel encryption,
-- authentication, error handling, and communication primitives for messaging. -- authentication, error handling, and communication primitives for messaging.
-- --
-- Note that we are not recommending anyone to use Pontarius XMPP at this time -- For low-level access to Pontarius XMPP, see the "Network.Xmpp.Connection"
-- as it's still in an experimental stage and will have its API and data types -- module.
-- modified frequently.
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} {-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
module Network.Xmpp module Network.Xmpp
( -- * Session management ( -- * Session management
Session Session
, newSession , session
, withConnection -- TODO: Close session, etc.
, connectTcp -- ** Authentication handlers
, simpleConnect
, startTLS
, simpleAuth
, auth
, scramSha1 , scramSha1
, digestMd5
, plain , plain
, closeConnection , digestMd5
, endContext -- * Addressing
, setConnectionClosedHandler
-- * JID
-- | A JID (historically: Jabber ID) is XMPPs native format -- | A JID (historically: Jabber ID) is XMPPs native format
-- for addressing entities in the network. It is somewhat similar to an e-mail -- for addressing entities in the network. It is somewhat similar to an e-mail
-- address but contains three parts instead of two: -- address, but contains three parts instead of two.
, Jid(..) , Jid(..)
, isBare , isBare
, isFull , isFull
@ -54,11 +44,11 @@ module Network.Xmpp
-- essentially a fragment of XML that is sent over a stream. @Stanzas@ come in -- essentially a fragment of XML that is sent over a stream. @Stanzas@ come in
-- 3 flavors: -- 3 flavors:
-- --
-- * @'Message'@, for traditional push-style message passing between peers -- * /Message/, for traditional push-style message passing between peers
-- --
-- * @'Presence'@, for communicating status updates -- * /Presence/, for communicating status updates
-- --
-- * IQ (info/query), for request-response semantics communication -- * /Info/\//Query/ (or /IQ/), for request-response semantics communication
-- --
-- All stanza types have the following attributes in common: -- All stanza types have the following attributes in common:
-- --
@ -78,7 +68,7 @@ module Network.Xmpp
-- presence, or IQ stanza. The particular allowable values for the 'type' -- presence, or IQ stanza. The particular allowable values for the 'type'
-- attribute vary depending on whether the stanza is a message, presence, -- attribute vary depending on whether the stanza is a message, presence,
-- or IQ stanza. -- or IQ stanza.
--
-- ** Messages -- ** Messages
-- | The /message/ stanza is a /push/ mechanism whereby one entity -- | The /message/ stanza is a /push/ mechanism whereby one entity
-- pushes information to another entity, similar to the communications that -- pushes information to another entity, similar to the communications that
@ -149,22 +139,25 @@ module Network.Xmpp
, LangTag(..) , LangTag(..)
, exampleParams , exampleParams
, PortID(..) , PortID(..)
, XmppFailure(..)
, StreamErrorInfo(..)
, StreamErrorCondition(..)
, AuthFailure( AuthXmlFailure -- Does not export AuthStreamFailure
, AuthNoAcceptableMechanism
, AuthChallengeFailure
, AuthNoConnection
, AuthFailure
, AuthSaslFailure
, AuthStringPrepFailure )
) where ) where
import Data.XML.Types (Element)
import Network import Network
import Network.Xmpp.Bind
import Network.Xmpp.Concurrent import Network.Xmpp.Concurrent
import Network.Xmpp.Concurrent.Channels
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection
import Network.Xmpp.Marshal
import Network.Xmpp.Message import Network.Xmpp.Message
import Network.Xmpp.Presence import Network.Xmpp.Presence
import Network.Xmpp.Sasl import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Types
import Network.Xmpp.Session import Network.Xmpp.Session
import Network.Xmpp.Stream import Network.Xmpp.Tls
import Network.Xmpp.TLS
import Network.Xmpp.Types import Network.Xmpp.Types

32
source/Network/Xmpp/Bind.hs

@ -1,4 +1,3 @@
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
@ -11,12 +10,16 @@ import Data.Text as Text
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Types import Network.Xmpp.Types
import Control.Monad.State(modify) import Control.Monad.State(modify)
import Control.Concurrent.STM.TMVar
import Control.Monad.Error
-- Produces a `bind' element, optionally wrapping a resource. -- Produces a `bind' element, optionally wrapping a resource.
bindBody :: Maybe Text -> Element bindBody :: Maybe Text -> Element
bindBody = pickleElem $ bindBody = pickleElem $
@ -28,16 +31,21 @@ bindBody = pickleElem $
-- Sends a (synchronous) IQ set request for a (`Just') given or server-generated -- Sends a (synchronous) IQ set request for a (`Just') given or server-generated
-- resource and extract the JID from the non-error response. -- resource and extract the JID from the non-error response.
xmppBind :: Maybe Text -> Connection -> IO Jid xmppBind :: Maybe Text -> TMVar Connection -> IO (Either XmppFailure Jid)
xmppBind rsrc c = do xmppBind rsrc c = runErrorT $ do
answer <- pushIQ' "bind" Nothing Set Nothing (bindBody rsrc) c answer <- ErrorT $ pushIQ' "bind" Nothing Set Nothing (bindBody rsrc) c
jid <- case () of () | Right IQResult{iqResultPayload = Just b} <- answer case answer of
, Right jid <- unpickleElem xpJid b Right IQResult{iqResultPayload = Just b} -> do
-> return jid let jid = unpickleElem xpJid b
| otherwise -> throw $ StreamXMLError case jid of
("Bind couldn't unpickle JID from " ++ show answer) Right jid' -> do
withConnection (modify $ \s -> s{sJid = Just jid}) c ErrorT $ withConnection (do
return jid modify $ \s -> s{cJid = Just jid'}
return $ Right jid') c -- not pretty
return jid'
otherwise -> throwError XmppOtherFailure
-- TODO: Log: ("Bind couldn't unpickle JID from " ++ show answer)
otherwise -> throwError XmppOtherFailure
where where
-- Extracts the character data in the `jid' element. -- Extracts the character data in the `jid' element.
xpJid :: PU [Node] Jid xpJid :: PU [Node] Jid

111
source/Network/Xmpp/Concurrent.hs

@ -1,12 +1,113 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Concurrent module Network.Xmpp.Concurrent
( Context ( module Network.Xmpp.Concurrent.Monad
, module Network.Xmpp.Concurrent.Monad
, module Network.Xmpp.Concurrent.Threads , module Network.Xmpp.Concurrent.Threads
, module Network.Xmpp.Concurrent.Channels , module Network.Xmpp.Concurrent.Basic
, module Network.Xmpp.Concurrent.Types
, module Network.Xmpp.Concurrent.Message
, module Network.Xmpp.Concurrent.Presence
, module Network.Xmpp.Concurrent.IQ
, toChans
, newSession
, writeWorker
) where ) where
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.Channels import Control.Applicative((<$>),(<*>))
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad
import qualified Data.ByteString as BS
import Data.IORef
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.XML.Types
import Network.Xmpp.Concurrent.Basic
import Network.Xmpp.Concurrent.IQ
import Network.Xmpp.Concurrent.Message
import Network.Xmpp.Concurrent.Presence
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Concurrent.Threads
import Network.Xmpp.Marshal
import Network.Xmpp.Pickle
import Network.Xmpp.Types
import Text.Xml.Stream.Elements
import Control.Monad.Error
toChans :: TChan Stanza
-> TVar IQHandlers
-> Stanza
-> IO ()
toChans stanzaC iqHands sta = atomically $ do
writeTChan stanzaC sta
case sta of
IQRequestS i -> handleIQRequest iqHands i
IQResultS i -> handleIQResponse iqHands (Right i)
IQErrorS i -> handleIQResponse iqHands (Left i)
_ -> return ()
where
-- If the IQ request has a namespace, send it through the appropriate channel.
handleIQRequest :: TVar IQHandlers -> IQRequest -> STM ()
handleIQRequest handlers iq = do
(byNS, _) <- readTVar handlers
let iqNS = fromMaybe "" (nameNamespace . elementName $ iqRequestPayload iq)
case Map.lookup (iqRequestType iq, iqNS) byNS of
Nothing -> return () -- TODO: send error stanza
Just ch -> do
sent <- newTVar False
writeTChan ch $ IQRequestTicket sent iq
handleIQResponse :: TVar IQHandlers -> Either IQError IQResult -> STM ()
handleIQResponse handlers iq = do
(byNS, byID) <- readTVar handlers
case Map.updateLookupWithKey (\_ _ -> Nothing) (iqID iq) byID of
(Nothing, _) -> return () -- We are not supposed to send an error.
(Just tmvar, byID') -> do
let answer = either IQResponseError IQResponseResult iq
_ <- tryPutTMVar tmvar answer -- Don't block.
writeTVar handlers (byNS, byID')
where
iqID (Left err) = iqErrorID err
iqID (Right iq') = iqResultID iq'
-- | Creates and initializes a new Xmpp context.
newSession :: TMVar Connection -> IO (Either XmppFailure Session)
newSession con = runErrorT $ do
outC <- lift newTChanIO
stanzaChan <- lift newTChanIO
iqHandlers <- lift $ newTVarIO (Map.empty, Map.empty)
eh <- lift $ newTVarIO $ EventHandlers { connectionClosedHandler = \_ -> return () }
let stanzaHandler = toChans stanzaChan iqHandlers
(kill, wLock, conState, readerThread) <- ErrorT $ startThreadsWith stanzaHandler eh con
writer <- lift $ forkIO $ writeWorker outC wLock
idRef <- lift $ newTVarIO 1
let getId = atomically $ do
curId <- readTVar idRef
writeTVar idRef (curId + 1 :: Integer)
return . read. show $ curId
return $ Session { stanzaCh = stanzaChan
, outCh = outC
, iqHandlers = iqHandlers
, writeRef = wLock
, readerThread = readerThread
, idGenerator = getId
, conRef = conState
, eventHandlers = eh
, stopThreads = kill >> killThread writer
}
-- Worker to write stanzas to the stream concurrently.
writeWorker :: TChan Stanza -> TMVar (BS.ByteString -> IO Bool) -> IO ()
writeWorker stCh writeR = forever $ do
(write, next) <- atomically $ (,) <$>
takeTMVar writeR <*>
readTChan stCh
r <- write $ renderElement (pickleElem xpStanza next)
atomically $ putTMVar writeR write
unless r $ do
atomically $ unGetTChan stCh next -- If the writing failed, the
-- connection is dead.
threadDelay 250000 -- Avoid free spinning.

4
source/Network/Xmpp/Concurrent/Channels/Basic.hs → source/Network/Xmpp/Concurrent/Basic.hs

@ -1,8 +1,8 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Concurrent.Channels.Basic where module Network.Xmpp.Concurrent.Basic where
import Control.Concurrent.STM import Control.Concurrent.STM
import Network.Xmpp.Concurrent.Channels.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Types import Network.Xmpp.Types
-- | Send a stanza to the server. -- | Send a stanza to the server.

112
source/Network/Xmpp/Concurrent/Channels.hs

@ -1,112 +0,0 @@
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Concurrent.Channels
( module Network.Xmpp.Concurrent.Channels.Basic
, module Network.Xmpp.Concurrent.Channels.Types
, module Network.Xmpp.Concurrent.Channels.Message
, module Network.Xmpp.Concurrent.Channels.Presence
, module Network.Xmpp.Concurrent.Channels.IQ
, toChans
, newSession
, writeWorker
)
where
import Control.Applicative((<$>),(<*>))
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad
import qualified Data.ByteString as BS
import Data.IORef
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.XML.Types
import Network.Xmpp.Concurrent.Channels.Basic
import Network.Xmpp.Concurrent.Channels.IQ
import Network.Xmpp.Concurrent.Channels.Message
import Network.Xmpp.Concurrent.Channels.Presence
import Network.Xmpp.Concurrent.Channels.Types
import Network.Xmpp.Concurrent.Threads
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Marshal
import Network.Xmpp.Pickle
import Network.Xmpp.Types
import Text.XML.Stream.Elements
toChans :: TChan Stanza
-> TVar IQHandlers
-> Stanza
-> IO ()
toChans stanzaC iqHands sta = atomically $ do
writeTChan stanzaC sta
case sta of
IQRequestS i -> handleIQRequest iqHands i
IQResultS i -> handleIQResponse iqHands (Right i)
IQErrorS i -> handleIQResponse iqHands (Left i)
_ -> return ()
where
-- If the IQ request has a namespace, send it through the appropriate channel.
handleIQRequest :: TVar IQHandlers -> IQRequest -> STM ()
handleIQRequest handlers iq = do
(byNS, _) <- readTVar handlers
let iqNS = fromMaybe "" (nameNamespace . elementName $ iqRequestPayload iq)
case Map.lookup (iqRequestType iq, iqNS) byNS of
Nothing -> return () -- TODO: send error stanza
Just ch -> do
sent <- newTVar False
writeTChan ch $ IQRequestTicket sent iq
handleIQResponse :: TVar IQHandlers -> Either IQError IQResult -> STM ()
handleIQResponse handlers iq = do
(byNS, byID) <- readTVar handlers
case Map.updateLookupWithKey (\_ _ -> Nothing) (iqID iq) byID of
(Nothing, _) -> return () -- We are not supposed to send an error.
(Just tmvar, byID') -> do
let answer = either IQResponseError IQResponseResult iq
_ <- tryPutTMVar tmvar answer -- Don't block.
writeTVar handlers (byNS, byID')
where
iqID (Left err) = iqErrorID err
iqID (Right iq') = iqResultID iq'
-- | Creates and initializes a new Xmpp context.
newSession :: Connection -> IO Session
newSession con = do
outC <- newTChanIO
stanzaChan <- newTChanIO
iqHandlers <- newTVarIO (Map.empty, Map.empty)
eh <- newTVarIO $ EventHandlers { connectionClosedHandler = \_ -> return () }
let stanzaHandler = toChans stanzaChan iqHandlers
(kill, wLock, conState, readerThread) <- startThreadsWith stanzaHandler eh con
writer <- forkIO $ writeWorker outC wLock
idRef <- newTVarIO 1
let getId = atomically $ do
curId <- readTVar idRef
writeTVar idRef (curId + 1 :: Integer)
return . read. show $ curId
let cont = Context { writeRef = wLock
, readerThread = readerThread
, idGenerator = getId
, conRef = conState
, eventHandlers = eh
, stopThreads = kill >> killThread writer
}
return $ Session { context = cont
, stanzaCh = stanzaChan
, outCh = outC
, iqHandlers = iqHandlers
}
-- Worker to write stanzas to the stream concurrently.
writeWorker :: TChan Stanza -> TMVar (BS.ByteString -> IO Bool) -> IO ()
writeWorker stCh writeR = forever $ do
(write, next) <- atomically $ (,) <$>
takeTMVar writeR <*>
readTChan stCh
r <- write $ renderElement (pickleElem xpStanza next)
atomically $ putTMVar writeR write
unless r $ do
atomically $ unGetTChan stCh next -- If the writing failed, the
-- connection is dead.
threadDelay 250000 -- Avoid free spinning.

7
source/Network/Xmpp/Concurrent/Channels/IQ.hs → source/Network/Xmpp/Concurrent/IQ.hs

@ -1,5 +1,5 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Concurrent.Channels.IQ where module Network.Xmpp.Concurrent.IQ where
import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM import Control.Concurrent.STM
@ -11,8 +11,7 @@ import qualified Data.Map as Map
import Data.Text (Text) import Data.Text (Text)
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Concurrent.Channels.Basic import Network.Xmpp.Concurrent.Basic
import Network.Xmpp.Concurrent.Channels.Types
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Types import Network.Xmpp.Types
@ -27,7 +26,7 @@ sendIQ :: Maybe Int -- ^ Timeout
-> Session -> Session
-> IO (TMVar IQResponse) -> IO (TMVar IQResponse)
sendIQ timeOut to tp lang body session = do -- TODO: Add timeout sendIQ timeOut to tp lang body session = do -- TODO: Add timeout
newId <- idGenerator (context session) newId <- idGenerator session
ref <- atomically $ do ref <- atomically $ do
resRef <- newEmptyTMVar resRef <- newEmptyTMVar
(byNS, byId) <- readTVar (iqHandlers session) (byNS, byId) <- readTVar (iqHandlers session)

6
source/Network/Xmpp/Concurrent/Channels/Message.hs → source/Network/Xmpp/Concurrent/Message.hs

@ -1,12 +1,12 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Concurrent.Channels.Message where module Network.Xmpp.Concurrent.Message where
import Network.Xmpp.Concurrent.Channels.Types import Network.Xmpp.Concurrent.Types
import Control.Concurrent.STM import Control.Concurrent.STM
import Data.IORef import Data.IORef
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Concurrent.Channels.Basic import Network.Xmpp.Concurrent.Basic
-- | Read an element from the inbound stanza channel, acquiring a copy of the -- | Read an element from the inbound stanza channel, acquiring a copy of the
-- channel as necessary. -- channel as necessary.

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

@ -9,7 +9,7 @@ import qualified Control.Exception.Lifted as Ex
import Control.Monad.Reader import Control.Monad.Reader
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
@ -59,7 +59,7 @@ import Network.Xmpp.Connection
-- ] -- ]
-- | Executes a function to update the event handlers. -- | Executes a function to update the event handlers.
modifyHandlers :: (EventHandlers -> EventHandlers) -> Context -> IO () modifyHandlers :: (EventHandlers -> EventHandlers) -> Session -> IO ()
modifyHandlers f session = atomically $ modifyTVar (eventHandlers session) f modifyHandlers f session = atomically $ modifyTVar (eventHandlers session) f
where where
-- Borrowing modifyTVar from -- Borrowing modifyTVar from
@ -71,18 +71,18 @@ modifyHandlers f session = atomically $ modifyTVar (eventHandlers session) f
writeTVar var (f x) writeTVar var (f x)
-- | Sets the handler to be executed when the server connection is closed. -- | Sets the handler to be executed when the server connection is closed.
setConnectionClosedHandler :: (StreamError -> Context -> IO ()) -> Context -> IO () setConnectionClosedHandler_ :: (XmppFailure -> Session -> IO ()) -> Session -> IO ()
setConnectionClosedHandler eh session = do setConnectionClosedHandler_ eh session = do
modifyHandlers (\s -> s{connectionClosedHandler = modifyHandlers (\s -> s{connectionClosedHandler =
\e -> eh e session}) session \e -> eh e session}) session
-- | Run an event handler. -- | Run an event handler.
runHandler :: (EventHandlers -> IO a) -> Context -> IO a runHandler :: (EventHandlers -> IO a) -> Session -> IO a
runHandler h session = h =<< atomically (readTVar $ eventHandlers session) runHandler h session = h =<< atomically (readTVar $ eventHandlers session)
-- | End the current Xmpp session. -- | End the current Xmpp session.
endContext :: Context -> IO () endContext :: Session -> IO ()
endContext session = do -- TODO: This has to be idempotent (is it?) endContext session = do -- TODO: This has to be idempotent (is it?)
closeConnection session closeConnection session
stopThreads session stopThreads session
@ -90,7 +90,7 @@ endContext session = do -- TODO: This has to be idempotent (is it?)
-- | Close the connection to the server. Closes the stream (by enforcing a -- | Close the connection to the server. Closes the stream (by enforcing a
-- write lock and sending a </stream:stream> element), waits (blocks) for three -- write lock and sending a </stream:stream> element), waits (blocks) for three
-- seconds, and then closes the connection. -- seconds, and then closes the connection.
closeConnection :: Context -> IO () closeConnection :: Session -> IO ()
closeConnection session = Ex.mask_ $ do closeConnection session = Ex.mask_ $ do
(_send, connection) <- atomically $ liftM2 (,) (_send, connection) <- atomically $ liftM2 (,)
(takeTMVar $ writeRef session) (takeTMVar $ writeRef session)

5
source/Network/Xmpp/Concurrent/Channels/Presence.hs → source/Network/Xmpp/Concurrent/Presence.hs

@ -1,12 +1,11 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Concurrent.Channels.Presence where module Network.Xmpp.Concurrent.Presence where
import Network.Xmpp.Concurrent.Channels.Types
import Control.Concurrent.STM import Control.Concurrent.STM
import Data.IORef import Data.IORef
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Concurrent.Channels.Basic import Network.Xmpp.Concurrent.Basic
-- | Read an element from the inbound stanza channel, acquiring a copy of the -- | Read an element from the inbound stanza channel, acquiring a copy of the
-- channel as necessary. -- channel as necessary.

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

@ -16,15 +16,19 @@ import Control.Monad.State.Strict
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Control.Concurrent.STM.TMVar
import GHC.IO (unsafeUnmask) import GHC.IO (unsafeUnmask)
import Control.Monad.Error
-- Worker to read stanzas from the stream and concurrently distribute them to -- Worker to read stanzas from the stream and concurrently distribute them to
-- all listener threads. -- all listener threads.
readWorker :: (Stanza -> IO ()) readWorker :: (Stanza -> IO ())
-> (StreamError -> IO ()) -> (XmppFailure -> IO ())
-> TMVar Connection -> TMVar (TMVar Connection)
-> IO a -> IO a
readWorker onStanza onConnectionClosed stateRef = readWorker onStanza onConnectionClosed stateRef =
Ex.mask_ . forever $ do Ex.mask_ . forever $ do
@ -32,8 +36,8 @@ readWorker onStanza onConnectionClosed stateRef =
-- we don't know whether pull will -- we don't know whether pull will
-- necessarily be interruptible -- necessarily be interruptible
s <- atomically $ do s <- atomically $ do
con@(Connection con_) <- readTMVar stateRef con <- readTMVar stateRef
state <- sConnectionState <$> readTMVar con_ state <- cState <$> readTMVar con
when (state == ConnectionClosed) when (state == ConnectionClosed)
retry retry
return con return con
@ -43,13 +47,14 @@ readWorker onStanza onConnectionClosed stateRef =
[ Ex.Handler $ \(Interrupt t) -> do [ Ex.Handler $ \(Interrupt t) -> do
void $ handleInterrupts [t] void $ handleInterrupts [t]
return Nothing return Nothing
, Ex.Handler $ \(e :: StreamError) -> do , Ex.Handler $ \(e :: XmppFailure) -> do
onConnectionClosed e onConnectionClosed e
return Nothing return Nothing
] ]
case res of case res of
Nothing -> return () -- Caught an exception, nothing to do Nothing -> return () -- Caught an exception, nothing to do. TODO: Can this happen?
Just sta -> onStanza sta Just (Left e) -> return ()
Just (Right sta) -> onStanza sta
where where
-- Defining an Control.Exception.allowInterrupt equivalent for GHC 7 -- Defining an Control.Exception.allowInterrupt equivalent for GHC 7
-- compatibility. -- compatibility.
@ -72,20 +77,22 @@ readWorker onStanza onConnectionClosed stateRef =
-- connection. -- connection.
startThreadsWith :: (Stanza -> IO ()) startThreadsWith :: (Stanza -> IO ())
-> TVar EventHandlers -> TVar EventHandlers
-> Connection -> TMVar Connection
-> IO -> IO (Either XmppFailure (IO (),
(IO (),
TMVar (BS.ByteString -> IO Bool), TMVar (BS.ByteString -> IO Bool),
TMVar Connection, TMVar (TMVar Connection),
ThreadId) ThreadId))
startThreadsWith stanzaHandler eh con = do startThreadsWith stanzaHandler eh con = do
read <- withConnection' (gets $ cSend. cHand) con read <- withConnection' (gets $ cSend . cHandle >>= \d -> return $ Right d) con
writeLock <- newTMVarIO read case read of
Left e -> return $ Left e
Right read' -> do
writeLock <- newTMVarIO read'
conS <- newTMVarIO con conS <- newTMVarIO con
-- lw <- forkIO $ writeWorker outC writeLock -- lw <- forkIO $ writeWorker outC writeLock
cp <- forkIO $ connPersist writeLock cp <- forkIO $ connPersist writeLock
rd <- forkIO $ readWorker stanzaHandler (noCon eh) conS rd <- forkIO $ readWorker stanzaHandler (noCon eh) conS
return ( killConnection writeLock [rd, cp] return $ Right ( killConnection writeLock [rd, cp]
, writeLock , writeLock
, conS , conS
, rd , rd
@ -96,7 +103,7 @@ startThreadsWith stanzaHandler eh con = do
_ <- forM threads killThread _ <- forM threads killThread
return () return ()
-- Call the connection closed handlers. -- Call the connection closed handlers.
noCon :: TVar EventHandlers -> StreamError -> IO () noCon :: TVar EventHandlers -> XmppFailure -> IO ()
noCon h e = do noCon h e = do
hands <- atomically $ readTVar h hands <- atomically $ readTVar h
_ <- forkIO $ connectionClosedHandler hands e _ <- forkIO $ connectionClosedHandler hands e

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

@ -12,27 +12,52 @@ import Data.Typeable
import Network.Xmpp.Types import Network.Xmpp.Types
import Data.IORef
import qualified Data.Map as Map
import Data.Text (Text)
import Network.Xmpp.Types
-- | Handlers to be run when the Xmpp session ends and when the Xmpp connection is -- | Handlers to be run when the Xmpp session ends and when the Xmpp connection is
-- closed. -- closed.
data EventHandlers = EventHandlers data EventHandlers = EventHandlers
{ connectionClosedHandler :: StreamError -> IO () { connectionClosedHandler :: XmppFailure -> IO ()
} }
-- | Xmpp Context object -- | Interrupt is used to signal to the reader thread that it should stop. Th contained semphore signals the reader to resume it's work.
data Context = Context data Interrupt = Interrupt (TMVar ()) deriving Typeable
{ writeRef :: TMVar (BS.ByteString -> IO Bool) instance Show Interrupt where show _ = "<Interrupt>"
instance Ex.Exception Interrupt
-- | A concurrent interface to Pontarius XMPP.
data Session = Session
{ stanzaCh :: TChan Stanza -- All stanzas
, outCh :: TChan Stanza
, iqHandlers :: TVar IQHandlers
-- Writing lock, so that only one thread could write to the stream at any
-- given time.
-- Fields below are from Context.
, writeRef :: TMVar (BS.ByteString -> IO Bool)
, readerThread :: ThreadId , readerThread :: ThreadId
, idGenerator :: IO StanzaID , idGenerator :: IO StanzaID
-- | Lock (used by withConnection) to make sure that a maximum of one -- | Lock (used by withConnection) to make sure that a maximum of one
-- XmppConMonad action is executed at any given time. -- XmppConMonad action is executed at any given time.
, conRef :: TMVar Connection , conRef :: TMVar (TMVar Connection)
, eventHandlers :: TVar EventHandlers , eventHandlers :: TVar EventHandlers
, stopThreads :: IO () , stopThreads :: IO ()
} }
-- | IQHandlers holds the registered channels for incomming IQ requests and
-- | Interrupt is used to signal to the reader thread that it should stop. Th contained semphore signals the reader to resume it's work. -- TMVars of and TMVars for expected IQ responses
data Interrupt = Interrupt (TMVar ()) deriving Typeable type IQHandlers = (Map.Map (IQRequestType, Text) (TChan IQRequestTicket)
instance Show Interrupt where show _ = "<Interrupt>" , Map.Map StanzaID (TMVar IQResponse)
)
instance Ex.Exception Interrupt
-- | Contains whether or not a reply has been sent, and the IQ request body to
-- reply to.
data IQRequestTicket = IQRequestTicket
{ sentRef :: (TVar Bool)
, iqRequestBody :: IQRequest
}

295
source/Network/Xmpp/Connection.hs

@ -1,262 +1,41 @@
{-# OPTIONS_HADDOCK hide #-} -- |
{-# LANGUAGE ScopedTypeVariables #-} -- Module: $Header$
{-# LANGUAGE OverloadedStrings #-} --
-- Maintainer: info@jonkri.com
module Network.Xmpp.Connection where -- Stability: unstable
-- Portability: portable
import Control.Applicative((<$>)) --
import Control.Concurrent (forkIO, threadDelay) -- This module allows for low-level access to Pontarius XMPP. Generally, the
import Control.Monad -- "Network.Xmpp" module should be used instead.
import Control.Monad.IO.Class --
import Control.Monad.Trans.Class -- The 'Connection' object provides the most low-level access to the XMPP
--import Control.Monad.Trans.Resource -- stream: a simple and single-threaded interface which exposes the conduit
import qualified Control.Exception.Lifted as Ex -- 'Event' source, as well as the input and output byte streams. Custom stateful
import qualified GHC.IO.Exception as GIE -- 'Connection' functions can be executed using 'withConnection'.
import Control.Monad.State.Strict --
-- The TLS, SASL, and 'Session' functionalities of Pontarius XMPP are built on
import Data.ByteString as BS -- top of this API.
import Data.Conduit
import Data.Conduit.Binary as CB module Network.Xmpp.Connection
import Data.Conduit.Internal as DCI ( Connection(..)
import qualified Data.Conduit.List as CL , ConnectionState(..)
import Data.IORef , ConnectionHandle(..)
import Data.Text(Text) , ServerFeatures(..)
import Data.XML.Pickle , connect
import Data.XML.Types , withConnection
, startTls
import Network , simpleAuth
import Network.Xmpp.Types , auth
import Network.Xmpp.Marshal , pushStanza
import Network.Xmpp.Pickle , pullStanza
, closeConnection
import System.IO , newSession
import Text.XML.Stream.Elements
import Text.XML.Stream.Parse as XP
import Text.XML.Unresolved(InvalidEventStream(..))
-- Enable/disable debug output
-- This will dump all incoming and outgoing network taffic to the console,
-- prefixed with "in: " and "out: " respectively
debug :: Bool
debug = False
pushElement :: Element -> StateT Connection_ IO Bool
pushElement x = do
send <- gets (cSend . cHand)
liftIO . send $ renderElement x
-- | Encode and send stanza
pushStanza :: Stanza -> Connection -> IO Bool
pushStanza s = withConnection' . pushElement $ pickleElem xpStanza s
-- XML documents and XMPP streams SHOULD be preceeded by an XML declaration.
-- UTF-8 is the only supported XMPP encoding. The standalone document
-- declaration (matching "SDDecl" in the XML standard) MUST NOT be included in
-- XMPP streams. RFC 6120 defines XMPP only in terms of XML 1.0.
pushXmlDecl :: StateT Connection_ IO Bool
pushXmlDecl = do
con <- gets cHand
liftIO $ (cSend con) "<?xml version='1.0' encoding='UTF-8' ?>"
pushOpenElement :: Element -> StateT Connection_ IO Bool
pushOpenElement e = do
sink <- gets (cSend . cHand )
liftIO . sink $ renderOpenElement e
-- `Connect-and-resumes' the given sink to the connection source, and pulls a
-- `b' value.
runEventsSink :: Sink Event IO b -> StateT Connection_ IO b
runEventsSink snk = do
source <- gets cEventSource
(src', r) <- lift $ source $$++ snk
modify (\s -> s{cEventSource = src'})
return r
pullElement :: StateT Connection_ IO Element
pullElement = do
Ex.catches (do
e <- runEventsSink (elements =$ await)
case e of
Nothing -> liftIO $ Ex.throwIO StreamConnectionError
Just r -> return r
)
[ Ex.Handler (\StreamEnd -> Ex.throwIO StreamStreamEnd)
, Ex.Handler (\(InvalidXmppXml s)
-> liftIO . Ex.throwIO $ StreamXMLError s)
, Ex.Handler $ \(e :: InvalidEventStream)
-> liftIO . Ex.throwIO $ StreamXMLError (show e)
]
-- Pulls an element and unpickles it.
pullUnpickle :: PU [Node] a -> StateT Connection_ IO a
pullUnpickle p = do
res <- unpickleElem p <$> pullElement
case res of
Left e -> liftIO . Ex.throwIO $ StreamXMLError (show e)
Right r -> return r
-- | Pulls a stanza (or stream error) from the stream. Throws an error on a stream
-- error.
pullStanza :: Connection -> IO Stanza
pullStanza = withConnection' $ do
res <- pullUnpickle xpStreamStanza
case res of
Left e -> liftIO . Ex.throwIO $ StreamError e
Right r -> return r
-- Performs the given IO operation, catches any errors and re-throws everything
-- except 'ResourceVanished' and IllegalOperation, in which case it will return False instead
catchPush :: IO () -> IO Bool
catchPush p = Ex.catch
(p >> return True)
(\e -> case GIE.ioe_type e of
GIE.ResourceVanished -> return False
GIE.IllegalOperation -> return False
_ -> Ex.throwIO e
) )
-- -- Connection_ state used when there is no connection.
xmppNoConnection :: Connection_
xmppNoConnection = Connection_
{ cHand = Hand { cSend = \_ -> return False
, cRecv = \_ -> Ex.throwIO
$ StreamConnectionError
, cFlush = return ()
, cClose = return ()
}
, cEventSource = DCI.ResumableSource zeroSource (return ())
, sFeatures = SF Nothing [] []
, sConnectionState = ConnectionClosed
, sHostname = Nothing
, sJid = Nothing
, sStreamLang = Nothing
, sStreamId = Nothing
, sPreferredLang = Nothing
, sToJid = Nothing
, sJidWhenPlain = False
, sFrom = Nothing
}
where
zeroSource :: Source IO output
zeroSource = liftIO . Ex.throwIO $ StreamConnectionError
-- Connects to the given hostname on port 5222 (TODO: Make this dynamic) and
-- updates the XmppConMonad Connection_ state.
connectTcpRaw :: HostName -> PortID -> Text -> IO Connection
connectTcpRaw host port hostname = do
h <- connectTo host port
hSetBuffering h NoBuffering
let eSource = if debug then
DCI.ResumableSource (sourceHandle h
$= debugOut
$= XP.parseBytes def)
(return ())
else DCI.ResumableSource (sourceHandle h
$= XP.parseBytes def)
(return ())
let hand = Hand { cSend = if debug
then \d -> do
BS.putStrLn (BS.append "out: " d)
catchPush $ BS.hPut h d
else catchPush . BS.hPut h
, cRecv = if debug then
\n -> do
bs <- BS.hGetSome h n
Prelude.putStr "in: "
BS.putStrLn bs
return bs
else BS.hGetSome h
, cFlush = hFlush h
, cClose = hClose h
}
let con = Connection_
{ cHand = hand
, cEventSource = eSource
, sFeatures = (SF Nothing [] [])
, sConnectionState = ConnectionPlain
, sHostname = (Just hostname)
, sJid = Nothing
, sPreferredLang = Nothing -- TODO: Allow user to set
, sStreamLang = Nothing
, sStreamId = Nothing
, sToJid = Nothing -- TODO: Allow user to set
, sJidWhenPlain = False -- TODO: Allow user to set
, sFrom = Nothing
}
mkConnection con
where
debugOut = do
d <- await
case d of
Nothing -> return ()
Just bs -> do
liftIO $ BS.putStr "in: "
liftIO $ BS.putStrLn bs
yield bs
debugOut
-- Closes the connection and updates the XmppConMonad Connection_ state.
killConnection :: Connection -> IO (Either Ex.SomeException ())
killConnection = withConnection $ do
cc <- gets (cClose . cHand)
err <- liftIO $ (Ex.try cc :: IO (Either Ex.SomeException ()))
put xmppNoConnection
return err
-- Sends an IQ request and waits for the response. If the response ID does not
-- match the outgoing ID, an error is thrown.
pushIQ' :: StanzaID
-> Maybe Jid
-> IQRequestType
-> Maybe LangTag
-> Element
-> Connection
-> IO (Either IQError IQResult)
pushIQ' iqID to tp lang body con = do
pushStanza (IQRequestS $ IQRequest iqID Nothing to lang tp body) con
res <- pullStanza con
case res of
IQErrorS e -> return $ Left e
IQResultS r -> do
unless
(iqID == iqResultID r) . liftIO . Ex.throwIO $
StreamXMLError
("In sendIQ' IDs don't match: " ++ show iqID ++ " /= " ++
show (iqResultID r) ++ " .")
return $ Right r
_ -> liftIO . Ex.throwIO . StreamXMLError $
"sendIQ': unexpected stanza type "
-- | Send "</stream:stream>" and wait for the server to finish processing and to
-- close the connection. Any remaining elements from the server and whether or
-- not we received a </stream:stream> element from the server is returned.
closeStreams :: Connection -> IO ([Element], Bool)
closeStreams = withConnection $ do
send <- gets (cSend . cHand)
cc <- gets (cClose . cHand)
liftIO $ send "</stream:stream>"
void $ liftIO $ forkIO $ do
threadDelay 3000000
(Ex.try cc) :: IO (Either Ex.SomeException ())
return ()
collectElems []
where where
-- Pulls elements from the stream until the stream ends, or an error is
-- raised.
collectElems :: [Element] -> StateT Connection_ IO ([Element], Bool)
collectElems es = do
result <- Ex.try pullElement
case result of
Left StreamStreamEnd -> return (es, True)
Left _ -> return (es, False)
Right e -> collectElems (e:es)
debugConduit :: Pipe l ByteString ByteString u IO b import Network.Xmpp.Connection_
debugConduit = forever $ do import Network.Xmpp.Session
s' <- await import Network.Xmpp.Tls
case s' of import Network.Xmpp.Types
Just s -> do import Network.Xmpp.Concurrent
liftIO $ BS.putStrLn (BS.append "in: " s)
yield s
Nothing -> return ()

285
source/Network/Xmpp/Connection_.hs

@ -0,0 +1,285 @@
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Connection_ where
import Control.Applicative((<$>))
import Control.Concurrent (forkIO, threadDelay)
import System.IO.Error (tryIOError)
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
--import Control.Monad.Trans.Resource
import qualified Control.Exception.Lifted as Ex
import qualified GHC.IO.Exception as GIE
import Control.Monad.State.Strict
import Data.ByteString as BS
import Data.ByteString.Char8 as BSC8
import Data.Conduit
import Data.Conduit.Binary as CB
import Data.Conduit.Internal as DCI
import qualified Data.Conduit.List as CL
import Data.IORef
import Data.Text(Text)
import qualified Data.Text as T
import Data.XML.Pickle
import Data.XML.Types
import Network
import Network.Xmpp.Types
import Network.Xmpp.Marshal
import Network.Xmpp.Pickle
import System.IO
import Text.Xml.Stream.Elements
import Text.XML.Stream.Parse as XP
import Text.XML.Unresolved(InvalidEventStream(..))
import System.Log.Logger
import Data.ByteString.Base64
import Control.Concurrent.STM.TMVar
import Control.Monad.Error
-- Enable/disable debug output
-- This will dump all incoming and outgoing network taffic to the console,
-- prefixed with "in: " and "out: " respectively
debug :: Bool
debug = False
-- TODO: Can the TLS send/recv functions throw something other than an IO error?
wrapIOException :: IO a -> StateT Connection IO (Either XmppFailure a)
wrapIOException action = do
r <- liftIO $ tryIOError action
case r of
Right b -> return $ Right b
Left e -> return $ Left $ XmppIOException e
pushElement :: Element -> StateT Connection IO (Either XmppFailure Bool)
pushElement x = do
send <- gets (cSend . cHandle)
wrapIOException $ send $ renderElement x
-- | Encode and send stanza
pushStanza :: Stanza -> TMVar Connection -> IO (Either XmppFailure Bool)
pushStanza s = withConnection' . pushElement $ pickleElem xpStanza s
-- XML documents and XMPP streams SHOULD be preceeded by an XML declaration.
-- UTF-8 is the only supported XMPP encoding. The standalone document
-- declaration (matching "SDDecl" in the XML standard) MUST NOT be included in
-- XMPP streams. RFC 6120 defines XMPP only in terms of XML 1.0.
pushXmlDecl :: StateT Connection IO (Either XmppFailure Bool)
pushXmlDecl = do
con <- gets cHandle
wrapIOException $ (cSend con) "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
pushOpenElement :: Element -> StateT Connection IO (Either XmppFailure Bool)
pushOpenElement e = do
sink <- gets (cSend . cHandle)
wrapIOException $ sink $ renderOpenElement e
-- `Connect-and-resumes' the given sink to the connection source, and pulls a
-- `b' value.
runEventsSink :: Sink Event IO b -> StateT Connection IO (Either XmppFailure b)
runEventsSink snk = do -- TODO: Wrap exceptions?
source <- gets cEventSource
(src', r) <- lift $ source $$++ snk
modify (\s -> s{cEventSource = src'})
return $ Right r
pullElement :: StateT Connection IO (Either XmppFailure Element)
pullElement = do
Ex.catches (do
e <- runEventsSink (elements =$ await)
case e of
Left f -> return $ Left f
Right Nothing -> return $ Left XmppOtherFailure -- TODO
Right (Just r) -> return $ Right r
)
[ Ex.Handler (\StreamEnd -> return $ Left StreamEndFailure)
, Ex.Handler (\(InvalidXmppXml s) -- Invalid XML `Event' encountered, or missing element close tag
-> return $ Left XmppOtherFailure) -- TODO: Log: s
, Ex.Handler $ \(e :: InvalidEventStream) -- xml-conduit exception
-> return $ Left XmppOtherFailure -- TODO: Log: (show e)
]
-- Pulls an element and unpickles it.
pullUnpickle :: PU [Node] a -> StateT Connection IO (Either XmppFailure a)
pullUnpickle p = do
elem <- pullElement
case elem of
Left e -> return $ Left e
Right elem' -> do
let res = unpickleElem p elem'
case res of
Left e -> return $ Left XmppOtherFailure -- TODO: Log
Right r -> return $ Right r
-- | Pulls a stanza (or stream error) from the stream.
pullStanza :: TMVar Connection -> IO (Either XmppFailure Stanza)
pullStanza = withConnection' $ do
res <- pullUnpickle xpStreamStanza
case res of
Left e -> return $ Left e
Right (Left e) -> return $ Left $ StreamErrorFailure e
Right (Right r) -> return $ Right r
-- Performs the given IO operation, catches any errors and re-throws everything
-- except 'ResourceVanished' and IllegalOperation, in which case it will return False instead
catchPush :: IO () -> IO Bool
catchPush p = Ex.catch
(p >> return True)
(\e -> case GIE.ioe_type e of
GIE.ResourceVanished -> return False
GIE.IllegalOperation -> return False
_ -> Ex.throwIO e
)
-- Connection state used when there is no connection.
xmppNoConnection :: Connection
xmppNoConnection = Connection
{ cHandle = ConnectionHandle { cSend = \_ -> return False
, cRecv = \_ -> Ex.throwIO
XmppOtherFailure
, cFlush = return ()
, cClose = return ()
}
, cEventSource = DCI.ResumableSource zeroSource (return ())
, cFeatures = SF Nothing [] []
, cState = ConnectionClosed
, cHostName = Nothing
, cJid = Nothing
, cStreamLang = Nothing
, cStreamId = Nothing
, cPreferredLang = Nothing
, cToJid = Nothing
, cJidWhenPlain = False
, cFrom = Nothing
}
where
zeroSource :: Source IO output
zeroSource = liftIO . Ex.throwIO $ XmppOtherFailure
connectTcp :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Connection))
connectTcp host port hostname = do
let PortNumber portNumber = port
debugM "Pontarius.Xmpp" $ "Connecting to " ++ host ++ " on port " ++
(show portNumber) ++ " through the realm " ++ (T.unpack hostname) ++ "."
h <- connectTo host port
debugM "Pontarius.Xmpp" "Setting NoBuffering mode on handle."
hSetBuffering h NoBuffering
let eSource = DCI.ResumableSource
((sourceHandle h $= logConduit) $= XP.parseBytes def)
(return ())
let hand = ConnectionHandle { cSend = \d -> do
let d64 = encode d
debugM "Pontarius.Xmpp" $
"Sending TCP data: " ++ (BSC8.unpack d64)
++ "."
catchPush $ BS.hPut h d
, cRecv = \n -> do
d <- BS.hGetSome h n
let d64 = encode d
debugM "Pontarius.Xmpp" $
"Received TCP data: " ++
(BSC8.unpack d64) ++ "."
return d
, cFlush = hFlush h
, cClose = hClose h
}
let con = Connection
{ cHandle = hand
, cEventSource = eSource
, cFeatures = (SF Nothing [] [])
, cState = ConnectionPlain
, cHostName = (Just hostname)
, cJid = Nothing
, cPreferredLang = Nothing -- TODO: Allow user to set
, cStreamLang = Nothing
, cStreamId = Nothing
, cToJid = Nothing -- TODO: Allow user to set
, cJidWhenPlain = False -- TODO: Allow user to set
, cFrom = Nothing
}
con' <- mkConnection con
return $ Right con'
where
logConduit :: Conduit ByteString IO ByteString
logConduit = CL.mapM $ \d -> do
let d64 = encode d
debugM "Pontarius.Xmpp" $ "Received TCP data: " ++ (BSC8.unpack d64) ++
"."
return d
-- Closes the connection and updates the XmppConMonad Connection state.
-- killConnection :: TMVar Connection -> IO (Either Ex.SomeException ())
killConnection :: TMVar Connection -> IO (Either XmppFailure ())
killConnection = withConnection $ do
cc <- gets (cClose . cHandle)
err <- wrapIOException cc
-- (Ex.try cc :: IO (Either Ex.SomeException ()))
put xmppNoConnection
return err
-- Sends an IQ request and waits for the response. If the response ID does not
-- match the outgoing ID, an error is thrown.
pushIQ' :: StanzaID
-> Maybe Jid
-> IQRequestType
-> Maybe LangTag
-> Element
-> TMVar Connection
-> IO (Either XmppFailure (Either IQError IQResult))
pushIQ' iqID to tp lang body con = do
pushStanza (IQRequestS $ IQRequest iqID Nothing to lang tp body) con
res <- pullStanza con
case res of
Left e -> return $ Left e
Right (IQErrorS e) -> return $ Right $ Left e
Right (IQResultS r) -> do
unless
(iqID == iqResultID r) . liftIO . Ex.throwIO $
XmppOtherFailure
-- TODO: Log: ("In sendIQ' IDs don't match: " ++ show iqID ++
-- " /= " ++ show (iqResultID r) ++ " .")
return $ Right $ Right r
_ -> return $ Left XmppOtherFailure
-- TODO: Log: "sendIQ': unexpected stanza type "
-- | Send "</stream:stream>" and wait for the server to finish processing and to
-- close the connection. Any remaining elements from the server are returned.
-- Surpresses StreamEndFailure exceptions, but may throw a StreamCloseError.
closeStreams :: TMVar Connection -> IO (Either XmppFailure [Element])
closeStreams = withConnection $ do
send <- gets (cSend . cHandle)
cc <- gets (cClose . cHandle)
liftIO $ send "</stream:stream>"
void $ liftIO $ forkIO $ do
threadDelay 3000000 -- TODO: Configurable value
(Ex.try cc) :: IO (Either Ex.SomeException ())
return ()
collectElems []
where
-- Pulls elements from the stream until the stream ends, or an error is
-- raised.
collectElems :: [Element] -> StateT Connection IO (Either XmppFailure [Element])
collectElems es = do
result <- pullElement
case result of
Left StreamEndFailure -> return $ Right es
Left e -> return $ Left $ StreamCloseError (es, e)
Right e -> collectElems (e:es)
debugConduit :: Pipe l ByteString ByteString u IO b
debugConduit = forever $ do
s' <- await
case s' of
Just s -> do
liftIO $ BS.putStrLn (BS.append "in: " s)
yield s
Nothing -> return ()

8
source/Network/Xmpp/Marshal.hs

@ -14,7 +14,7 @@ import Data.XML.Types
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Types import Network.Xmpp.Types
xpStreamStanza :: PU [Node] (Either XmppStreamError Stanza) xpStreamStanza :: PU [Node] (Either StreamErrorInfo Stanza)
xpStreamStanza = xpEither xpStreamError xpStanza xpStreamStanza = xpEither xpStreamError xpStanza
xpStanza :: PU [Node] Stanza xpStanza :: PU [Node] Stanza
@ -182,10 +182,10 @@ xpIQError = ("xpIQError" , "") <?+> xpWrap
(xp2Tuple xpStanzaError (xpOption xpElemVerbatim)) (xp2Tuple xpStanzaError (xpOption xpElemVerbatim))
) )
xpStreamError :: PU [Node] XmppStreamError xpStreamError :: PU [Node] StreamErrorInfo
xpStreamError = ("xpStreamError" , "") <?+> xpWrap xpStreamError = ("xpStreamError" , "") <?+> xpWrap
(\((cond,() ,()), txt, el) -> XmppStreamError cond txt el) (\((cond,() ,()), txt, el) -> StreamErrorInfo cond txt el)
(\(XmppStreamError cond txt el) ->((cond,() ,()), txt, el)) (\(StreamErrorInfo cond txt el) ->((cond,() ,()), txt, el))
(xpElemNodes (xpElemNodes
(Name (Name
"error" "error"

2
source/Network/Xmpp/Pickle.hs

@ -21,7 +21,7 @@ import Data.XML.Pickle
import Network.Xmpp.Types import Network.Xmpp.Types
import Text.XML.Stream.Elements import Text.Xml.Stream.Elements
xmlLang :: Name xmlLang :: Name
xmlLang = Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml") xmlLang = Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml")

30
source/Network/Xmpp/Sasl.hs

@ -29,7 +29,7 @@ import qualified Data.Text as Text
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
@ -38,24 +38,30 @@ import qualified System.Random as Random
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Sasl.Mechanisms import Network.Xmpp.Sasl.Mechanisms
import Control.Concurrent.STM.TMVar
-- | Uses the first supported mechanism to authenticate, if any. Updates the -- | Uses the first supported mechanism to authenticate, if any. Updates the
-- state with non-password credentials and restarts the stream upon -- state with non-password credentials and restarts the stream upon
-- success. -- success. Returns `Nothing' on success, an `AuthFailure' if
-- authentication fails, or an `XmppFailure' if anything else fails.
xmppSasl :: [SaslHandler] -- ^ Acceptable authentication mechanisms and their xmppSasl :: [SaslHandler] -- ^ Acceptable authentication mechanisms and their
-- corresponding handlers -- corresponding handlers
-> Connection -> TMVar Connection
-> IO (Either AuthError ()) -> IO (Either XmppFailure (Maybe AuthFailure))
xmppSasl handlers = withConnection $ do xmppSasl handlers = withConnection $ do
-- Chooses the first mechanism that is acceptable by both the client and the -- Chooses the first mechanism that is acceptable by both the client and the
-- server. -- server.
mechanisms <- gets $ saslMechanisms . sFeatures mechanisms <- gets $ saslMechanisms . cFeatures
case (filter (\(name, _) -> name `elem` mechanisms)) handlers of case (filter (\(name, _) -> name `elem` mechanisms)) handlers of
[] -> return . Left $ AuthNoAcceptableMechanism mechanisms [] -> return $ Right $ Just $ AuthNoAcceptableMechanism mechanisms
(_name, handler):_ -> runErrorT $ do (_name, handler):_ -> do
cs <- gets sConnectionState cs <- gets cState
case cs of case cs of
ConnectionClosed -> throwError AuthConnectionError ConnectionClosed -> return . Right $ Just AuthNoConnection
_ -> do _ -> do
r <- handler r <- runErrorT handler
_ <- ErrorT $ left AuthStreamError <$> restartStream case r of
return r Left ae -> return $ Right $ Just ae
Right a -> do
_ <- runErrorT $ ErrorT restartStream
return $ Right $ Nothing

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

@ -22,7 +22,7 @@ import Data.Word (Word8)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Sasl.StringPrep import Network.Xmpp.Sasl.StringPrep
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
@ -107,16 +107,21 @@ quote :: BS.ByteString -> BS.ByteString
quote x = BS.concat ["\"",x,"\""] quote x = BS.concat ["\"",x,"\""]
saslInit :: Text.Text -> Maybe BS.ByteString -> SaslM Bool saslInit :: Text.Text -> Maybe BS.ByteString -> SaslM Bool
saslInit mechanism payload = lift . pushElement . saslInitE mechanism $ saslInit mechanism payload = do
r <- lift . pushElement . saslInitE mechanism $
Text.decodeUtf8 . B64.encode <$> payload Text.decodeUtf8 . B64.encode <$> payload
case r of
Left e -> throwError $ AuthStreamFailure e
Right b -> return b
-- | Pull the next element. -- | Pull the next element.
pullSaslElement :: SaslM SaslElement pullSaslElement :: SaslM SaslElement
pullSaslElement = do pullSaslElement = do
el <- lift $ pullUnpickle (xpEither xpFailure xpSaslElement) r <- lift $ pullUnpickle (xpEither xpFailure xpSaslElement)
case el of case r of
Left e ->throwError $ AuthSaslFailure e Left e -> throwError $ AuthStreamFailure e
Right r -> return r Right (Left e) -> throwError $ AuthSaslFailure e
Right (Right r) -> return r
-- | Pull the next element, checking that it is a challenge. -- | Pull the next element, checking that it is a challenge.
pullChallenge :: SaslM (Maybe BS.ByteString) pullChallenge :: SaslM (Maybe BS.ByteString)
@ -127,11 +132,11 @@ pullChallenge = do
SaslChallenge (Just scb64) SaslChallenge (Just scb64)
| Right sc <- B64.decode . Text.encodeUtf8 $ scb64 | Right sc <- B64.decode . Text.encodeUtf8 $ scb64
-> return $ Just sc -> return $ Just sc
_ -> throwError AuthChallengeError _ -> throwError AuthChallengeFailure
-- | Extract value from Just, failing with AuthChallengeError on Nothing. -- | Extract value from Just, failing with AuthChallengeFailure on Nothing.
saslFromJust :: Maybe a -> SaslM a saslFromJust :: Maybe a -> SaslM a
saslFromJust Nothing = throwError $ AuthChallengeError saslFromJust Nothing = throwError $ AuthChallengeFailure
saslFromJust (Just d) = return d saslFromJust (Just d) = return d
-- | Pull the next element and check that it is success. -- | Pull the next element and check that it is success.
@ -140,7 +145,7 @@ pullSuccess = do
e <- pullSaslElement e <- pullSaslElement
case e of case e of
SaslSuccess x -> return x SaslSuccess x -> return x
_ -> throwError $ AuthXmlError _ -> throwError $ AuthXmlFailure
-- | Pull the next element. When it's success, return it's payload. -- | Pull the next element. When it's success, return it's payload.
-- If it's a challenge, send an empty response and pull success. -- If it's a challenge, send an empty response and pull success.
@ -156,27 +161,30 @@ pullFinalMessage = do
where where
decode Nothing = return Nothing decode Nothing = return Nothing
decode (Just d) = case B64.decode $ Text.encodeUtf8 d of decode (Just d) = case B64.decode $ Text.encodeUtf8 d of
Left _e -> throwError $ AuthChallengeError Left _e -> throwError $ AuthChallengeFailure
Right x -> return $ Just x Right x -> return $ Just x
-- | Extract p=q pairs from a challenge. -- | Extract p=q pairs from a challenge.
toPairs :: BS.ByteString -> SaslM Pairs toPairs :: BS.ByteString -> SaslM Pairs
toPairs ctext = case pairs ctext of toPairs ctext = case pairs ctext of
Left _e -> throwError AuthChallengeError Left _e -> throwError AuthChallengeFailure
Right r -> return r Right r -> return r
-- | Send a SASL response element. The content will be base64-encoded. -- | Send a SASL response element. The content will be base64-encoded.
respond :: Maybe BS.ByteString -> SaslM Bool respond :: Maybe BS.ByteString -> SaslM Bool
respond = lift . pushElement . saslResponseE . respond m = do
fmap (Text.decodeUtf8 . B64.encode) r <- lift . pushElement . saslResponseE . fmap (Text.decodeUtf8 . B64.encode) $ m
case r of
Left e -> throwError $ AuthStreamFailure e
Right b -> return b
-- | Run the appropriate stringprep profiles on the credentials. -- | Run the appropriate stringprep profiles on the credentials.
-- May fail with 'AuthStringPrepError' -- May fail with 'AuthStringPrepFailure'
prepCredentials :: Text.Text -> Maybe Text.Text -> Text.Text prepCredentials :: Text.Text -> Maybe Text.Text -> Text.Text
-> SaslM (Text.Text, Maybe Text.Text, Text.Text) -> SaslM (Text.Text, Maybe Text.Text, Text.Text)
prepCredentials authcid authzid password = case credentials of prepCredentials authcid authzid password = case credentials of
Nothing -> throwError $ AuthStringPrepError Nothing -> throwError $ AuthStringPrepFailure
Just creds -> return creds Just creds -> return creds
where where
credentials = do credentials = do

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

@ -31,7 +31,7 @@ import qualified Data.ByteString as BS
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
@ -47,11 +47,8 @@ xmppDigestMd5 :: Text -- ^ Authentication identity (authzid or username)
-> SaslM () -> SaslM ()
xmppDigestMd5 authcid authzid password = do xmppDigestMd5 authcid authzid password = do
(ac, az, pw) <- prepCredentials authcid authzid password (ac, az, pw) <- prepCredentials authcid authzid password
hn <- gets sHostname hn <- gets cHostName
case hn of xmppDigestMd5' (fromJust hn) ac az pw
Just hn' -> do
xmppDigestMd5' hn' ac az pw
Nothing -> throwError AuthConnectionError
where where
xmppDigestMd5' :: Text -> Text -> Maybe Text -> Text -> SaslM () xmppDigestMd5' :: Text -> Text -> Maybe Text -> Text -> SaslM ()
xmppDigestMd5' hostname authcid authzid password = do xmppDigestMd5' hostname authcid authzid password = do

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

@ -35,7 +35,7 @@ import qualified Data.ByteString as BS
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Pickle import Network.Xmpp.Pickle

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

@ -59,7 +59,7 @@ scram hashToken authcid authzid password = do
let (cfm, v) = cFinalMessageAndVerifier nonce salt ic sFirstMessage cnonce let (cfm, v) = cFinalMessageAndVerifier nonce salt ic sFirstMessage cnonce
respond $ Just cfm respond $ Just cfm
finalPairs <- toPairs =<< saslFromJust =<< pullFinalMessage finalPairs <- toPairs =<< saslFromJust =<< pullFinalMessage
unless (lookup "v" finalPairs == Just v) $ throwError AuthServerAuthError unless (lookup "v" finalPairs == Just v) $ throwError AuthServerAuthFailure
return () return ()
where where
-- We need to jump through some hoops to get a polymorphic solution -- We need to jump through some hoops to get a polymorphic solution
@ -102,7 +102,7 @@ scram hashToken authcid authzid password = do
, Just ic <- lookup "i" pairs , Just ic <- lookup "i" pairs
, [(i,"")] <- reads $ BS8.unpack ic , [(i,"")] <- reads $ BS8.unpack ic
= return (nonce, salt, i) = return (nonce, salt, i)
fromPairs _ _ = throwError $ AuthChallengeError fromPairs _ _ = throwError $ AuthChallengeFailure
cFinalMessageAndVerifier :: BS.ByteString cFinalMessageAndVerifier :: BS.ByteString
-> BS.ByteString -> BS.ByteString

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

@ -7,29 +7,29 @@ import Data.ByteString(ByteString)
import qualified Data.Text as Text import qualified Data.Text as Text
import Network.Xmpp.Types import Network.Xmpp.Types
data AuthError = AuthXmlError data AuthFailure = AuthXmlFailure
| AuthNoAcceptableMechanism [Text.Text] -- ^ Wraps mechanisms | AuthNoAcceptableMechanism [Text.Text] -- ^ Wraps mechanisms
-- offered -- offered
| AuthChallengeError | AuthChallengeFailure
| AuthServerAuthError -- ^ The server failed to authenticate | AuthServerAuthFailure -- ^ The server failed to authenticate
-- itself -- itself
| AuthStreamError StreamError -- ^ Stream error on stream restart | AuthStreamFailure XmppFailure -- ^ Stream error on stream restart
-- TODO: Rename AuthConnectionError? -- TODO: Rename AuthConnectionFailure?
| AuthConnectionError -- ^ Connection is closed | AuthNoConnection
| AuthError -- General instance used for the Error instance | AuthFailure -- General instance used for the Error instance
| AuthSaslFailure SaslFailure -- ^ Defined SASL error condition | AuthSaslFailure SaslFailure -- ^ Defined SASL error condition
| AuthStringPrepError -- ^ StringPrep failed | AuthStringPrepFailure -- ^ StringPrep failed
deriving Show deriving Show
instance Error AuthError where instance Error AuthFailure where
noMsg = AuthError noMsg = AuthFailure
data SaslElement = SaslSuccess (Maybe Text.Text) data SaslElement = SaslSuccess (Maybe Text.Text)
| SaslChallenge (Maybe Text.Text) | SaslChallenge (Maybe Text.Text)
-- | SASL mechanism XmppConnection computation, with the possibility of throwing -- | SASL mechanism XmppConnection computation, with the possibility of throwing
-- an authentication error. -- an authentication error.
type SaslM a = ErrorT AuthError (StateT Connection_ IO) a type SaslM a = ErrorT AuthFailure (StateT Connection IO) a
type Pairs = [(ByteString, ByteString)] type Pairs = [(ByteString, ByteString)]

136
source/Network/Xmpp/Session.hs

@ -11,97 +11,61 @@ import Network
import qualified Network.TLS as TLS import qualified Network.TLS as TLS
import Network.Xmpp.Bind import Network.Xmpp.Bind
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Concurrent.Channels import Network.Xmpp.Concurrent
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Marshal import Network.Xmpp.Marshal
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Sasl import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Mechanisms import Network.Xmpp.Sasl.Mechanisms
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.TLS import Network.Xmpp.Tls
import Network.Xmpp.Types import Network.Xmpp.Types
import Control.Concurrent.STM.TMVar
import Data.Maybe
-- | The quick and easy way to set up a connection to an XMPP server -- | Creates a 'Session' object by setting up a connection with an XMPP server.
-- --
-- This will -- Will connect to the specified host. If the fourth parameters is a 'Just'
-- -- value, @session@ will attempt to secure the connection with TLS. If the fifth
-- * connect to the host -- parameters is a 'Just' value, @session@ will attempt to authenticate and
-- -- acquire an XMPP resource.
-- * secure the connection with TLS session :: HostName -- ^ Host to connect to
-- -> Text -- ^ The realm host name (to
-- * authenticate to the server using either SCRAM-SHA1 (preferred) or -- distinguish the XMPP service)
-- Digest-MD5 -> PortID -- ^ Port to connect to
-- -> Maybe TLS.TLSParams -- ^ TLS settings, if securing the
-- * bind a resource -- connection to the server is
-- -- desired
-- * return the full JID you have been assigned -> Maybe ([SaslHandler], Maybe Text) -- ^ SASL handlers and the desired
-- -- JID resource (or Nothing to let
-- Note that the server might assign a different resource even when we send -- the server decide)
-- a preference. -> IO (Either XmppFailure (Session, Maybe AuthFailure))
simpleConnect :: HostName -- ^ Host to connect to session hostname realm port tls sasl = runErrorT $ do
-> PortID -- ^ Port to connec to con <- ErrorT $ connect hostname port realm
-> Text -- ^ Hostname of the server (to distinguish the XMPP if isJust tls
-- service) then ErrorT $ startTls (fromJust tls) con
-> Text -- ^ User name (authcid) else return ()
-> Text -- ^ Password aut <- if isJust sasl
-> Maybe Text -- ^ Desired resource (or Nothing to let the server then ErrorT $ auth (fst $ fromJust sasl) (snd $ fromJust sasl) con
-- decide) else return Nothing
-> IO Session ses <- ErrorT $ newSession con
simpleConnect host port hostname username password resource = do return (ses, aut)
con' <- connectTcp host port hostname
con <- case con' of
Left e -> Ex.throwIO e
Right r -> return r
startTLS exampleParams con
saslResponse <- simpleAuth username password resource con
case saslResponse of
Right jid -> newSession con
Left e -> error $ show e
-- | Connect to host with given address. -- | Connects to the XMPP server and opens the XMPP stream against the given
connectTcp :: HostName -> PortID -> Text -> IO (Either StreamError Connection) -- host name, port, and realm.
connectTcp address port hostname = do connect :: HostName -> PortID -> Text -> IO (Either XmppFailure (TMVar Connection))
con <- connectTcpRaw address port hostname connect address port hostname = do
result <- withConnection startStream con con <- connectTcp address port hostname
case result of case con of
Right con' -> do
result <- withConnection startStream con'
return $ Right con'
Left e -> do Left e -> do
withConnection (pushElement . pickleElem xpStreamError $ toError e)
con
closeStreams con
return $ Left e return $ Left e
Right () -> return $ Right con
where
-- TODO: Descriptive texts in stream errors?
toError (StreamNotStreamElement _name) =
XmppStreamError StreamInvalidXml Nothing Nothing
toError (StreamInvalidStreamNamespace _ns) =
XmppStreamError StreamInvalidNamespace Nothing Nothing
toError (StreamInvalidStreamPrefix _prefix) =
XmppStreamError StreamBadNamespacePrefix Nothing Nothing
-- TODO: Catch remaining xmppStartStream errors.
toError (StreamWrongVersion _ver) =
XmppStreamError StreamUnsupportedVersion Nothing Nothing
toError (StreamWrongLangTag _) =
XmppStreamError StreamInvalidXml Nothing Nothing
toError StreamUnknownError =
XmppStreamError StreamBadFormat Nothing Nothing
toError (StreamWrongTo _) =
XmppStreamError StreamBadFormat Nothing Nothing
toError (StreamXMLError _) =
XmppStreamError StreamInvalidXml Nothing Nothing
toError StreamStreamEnd =
XmppStreamError StreamBadFormat Nothing Nothing
toError StreamConnectionError =
XmppStreamError StreamRemoteConnectionFailed Nothing Nothing
toError (StreamError _) =
XmppStreamError StreamUndefinedCondition Nothing Nothing
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")
() ()
@ -111,14 +75,14 @@ sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess"
, iqRequestTo = Nothing , iqRequestTo = Nothing
, iqRequestLangTag = Nothing , iqRequestLangTag = Nothing
, iqRequestType = Set , iqRequestType = Set
, iqRequestPayload = sessionXML , iqRequestPayload = sessionXml
} }
-- Sends the session IQ set element and waits for an answer. Throws an error if -- Sends the session IQ set element and waits for an answer. Throws an error if
-- if an IQ error stanza is returned from the server. -- if an IQ error stanza is returned from the server.
startSession :: Connection -> IO () startSession :: TMVar Connection -> IO ()
startSession con = do startSession con = do
answer <- pushIQ' "session" Nothing Set Nothing sessionXML con answer <- pushIQ' "session" Nothing Set Nothing sessionXml con
case answer of case answer of
Left e -> error $ show e Left e -> error $ show e
Right _ -> return () Right _ -> return ()
@ -127,13 +91,13 @@ startSession con = do
-- resource. -- resource.
auth :: [SaslHandler] auth :: [SaslHandler]
-> Maybe Text -> Maybe Text
-> Connection -> TMVar Connection
-> IO (Either AuthError Jid) -> IO (Either XmppFailure (Maybe AuthFailure))
auth mechanisms resource con = runErrorT $ do auth mechanisms resource con = runErrorT $ do
ErrorT $ xmppSasl mechanisms con ErrorT $ xmppSasl mechanisms con
jid <- lift $ xmppBind resource con jid <- lift $ xmppBind resource con
lift $ startSession con lift $ startSession con
return jid return Nothing
-- | Authenticate to the server with the given username and password -- | Authenticate to the server with the given username and password
-- and bind a resource. -- and bind a resource.
@ -143,8 +107,8 @@ simpleAuth :: Text.Text -- ^ The username
-> Text.Text -- ^ The password -> Text.Text -- ^ The password
-> Maybe Text -- ^ The desired resource or 'Nothing' to let the -> Maybe Text -- ^ The desired resource or 'Nothing' to let the
-- server assign one -- server assign one
-> Connection -> TMVar Connection
-> IO (Either AuthError Jid) -> IO (Either XmppFailure (Maybe AuthFailure))
simpleAuth username passwd resource = flip auth resource $ simpleAuth username passwd resource = flip auth resource $
[ -- TODO: scramSha1Plus [ -- TODO: scramSha1Plus
scramSha1 username Nothing passwd scramSha1 username Nothing passwd

163
source/Network/Xmpp/Stream.hs

@ -20,12 +20,12 @@ import Data.Void (Void)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Errors
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Marshal
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.Elements -- import Text.XML.Stream.Elements
@ -38,18 +38,18 @@ lmb :: [t] -> Maybe [t]
lmb [] = Nothing lmb [] = Nothing
lmb x = Just x lmb x = Just x
-- Unpickles and returns a stream element. Throws a StreamXMLError on failure. -- Unpickles and returns a stream element.
streamUnpickleElem :: PU [Node] a streamUnpickleElem :: PU [Node] a
-> Element -> Element
-> StreamSink a -> StreamSink a
streamUnpickleElem p x = do streamUnpickleElem p x = do
case unpickleElem p x of case unpickleElem p x of
Left l -> throwError $ StreamXMLError (show l) Left l -> throwError $ XmppOtherFailure -- TODO: Log: StreamXmlError (show l)
Right r -> return r Right r -> return r
-- This is the conduit sink that handles the stream XML events. We extend it -- This is the conduit sink that handles the stream XML events. We extend it
-- with ErrorT capabilities. -- with ErrorT capabilities.
type StreamSink a = ErrorT StreamError (Pipe Event Event Void () IO) a type StreamSink a = ErrorT XmppFailure (Pipe Event Event Void () IO) a
-- Discards all events before the first EventBeginElement. -- Discards all events before the first EventBeginElement.
throwOutJunk :: Monad m => Sink Event m () throwOutJunk :: Monad m => Sink Event m ()
@ -67,42 +67,107 @@ openElementFromEvents = do
hd <- lift CL.head hd <- lift CL.head
case hd of case hd of
Just (EventBeginElement name attrs) -> return $ Element name attrs [] Just (EventBeginElement name attrs) -> return $ Element name attrs []
_ -> throwError $ StreamConnectionError _ -> throwError $ XmppOtherFailure
-- Sends the initial stream:stream element and pulls the server features. -- Sends the initial stream:stream element and pulls the server features. If the
startStream :: StateT Connection_ IO (Either StreamError ()) -- server responds in a way that is invalid, an appropriate stream error will be
-- generated, the connection to the server will be closed, and a XmppFailure
-- will be produced.
startStream :: StateT Connection IO (Either XmppFailure ())
startStream = runErrorT $ do startStream = runErrorT $ do
state <- get state <- lift $ get
-- Set the `to' attribute depending on the state of the connection. con <- liftIO $ mkConnection state
let from = case sConnectionState state of -- Set the `from' (which is also the expected to) attribute depending on the
ConnectionPlain -> if sJidWhenPlain state -- state of the connection.
then sJid state else Nothing let expectedTo = case cState state of
ConnectionSecured -> sJid state ConnectionPlain -> if cJidWhenPlain state
case sHostname state of then cJid state else Nothing
Nothing -> throwError StreamConnectionError ConnectionSecured -> cJid state
case cHostName state of
Nothing -> throwError XmppOtherFailure -- TODO: When does this happen?
Just hostname -> lift $ do Just hostname -> lift $ do
pushXmlDecl pushXmlDecl
pushOpenElement $ pushOpenElement $
pickleElem xpStream ( "1.0" pickleElem xpStream ( "1.0"
, from , expectedTo
, Just (Jid Nothing hostname Nothing) , Just (Jid Nothing hostname Nothing)
, Nothing , Nothing
, sPreferredLang state , cPreferredLang state
) )
(lt, from, id, features) <- ErrorT . runEventsSink $ runErrorT $ response <- ErrorT $ runEventsSink $ runErrorT $ streamS expectedTo
streamS from case response of
modify (\s -> s{ sFeatures = features Left e -> throwError e
, sStreamLang = Just lt -- Successful unpickling of stream element.
, sStreamId = id Right (Right (ver, from, to, id, lt, features))
, sFrom = from | (unpack ver) /= "1.0" ->
closeStreamWithError con StreamUnsupportedVersion Nothing
| lt == Nothing ->
closeStreamWithError con StreamInvalidXml Nothing
-- If `from' is set, we verify that it's the correct one. TODO: Should we check against the realm instead?
| isJust from && (from /= Just (Jid Nothing (fromJust $ cHostName state) Nothing)) ->
closeStreamWithError con StreamInvalidFrom Nothing
| to /= expectedTo ->
closeStreamWithError con StreamUndefinedCondition (Just $ Element "invalid-to" [] []) -- TODO: Suitable?
| otherwise -> do
modify (\s -> s{ cFeatures = features
, cStreamLang = lt
, cStreamId = id
, cFrom = from
} ) } )
return () return ()
-- Unpickling failed - we investigate the element.
Right (Left (Element name attrs children))
| (nameLocalName name /= "stream") ->
closeStreamWithError con StreamInvalidXml Nothing
| (nameNamespace name /= Just "http://etherx.jabber.org/streams") ->
closeStreamWithError con StreamInvalidNamespace Nothing
| (isJust $ namePrefix name) && (fromJust (namePrefix name) /= "stream") ->
closeStreamWithError con StreamBadNamespacePrefix Nothing
| otherwise -> ErrorT $ checkchildren con (flattenAttrs attrs)
where
-- closeStreamWithError :: MonadIO m => TMVar Connection -> StreamErrorCondition ->
-- Maybe Element -> ErrorT XmppFailure m ()
closeStreamWithError con sec el = do
liftIO $ do
withConnection (pushElement . pickleElem xpStreamError $
StreamErrorInfo sec Nothing el) con
closeStreams con
throwError XmppOtherFailure
checkchildren con children =
let to' = lookup "to" children
ver' = lookup "version" children
xl = lookup xmlLang children
in case () of () | Just (Nothing :: Maybe Jid) == (safeRead <$> to') ->
runErrorT $ closeStreamWithError con
StreamBadNamespacePrefix Nothing
| Nothing == ver' ->
runErrorT $ closeStreamWithError con
StreamUnsupportedVersion Nothing
| Just (Nothing :: Maybe LangTag) == (safeRead <$> xl) ->
runErrorT $ closeStreamWithError con
StreamInvalidXml Nothing
| otherwise ->
runErrorT $ closeStreamWithError con
StreamBadFormat Nothing
safeRead x = case reads $ Text.unpack x of
[] -> Nothing
[(y,_),_] -> Just y
flattenAttrs :: [(Name, [Content])] -> [(Name, Text.Text)]
flattenAttrs attrs = Prelude.map (\(name, content) ->
( name
, Text.concat $ Prelude.map uncontentify content)
)
attrs
where
uncontentify (ContentText t) = t
uncontentify _ = ""
-- Sets a new Event source using the raw source (of bytes) -- Sets a new Event source using the raw source (of bytes)
-- and calls xmppStartStream. -- and calls xmppStartStream.
restartStream :: StateT Connection_ IO (Either StreamError ()) restartStream :: StateT Connection IO (Either XmppFailure ())
restartStream = do restartStream = do
raw <- gets (cRecv . cHand) raw <- gets (cRecv . cHandle)
let newSource = DCI.ResumableSource (loopRead raw $= XP.parseBytes def) let newSource = DCI.ResumableSource (loopRead raw $= XP.parseBytes def)
(return ()) (return ())
modify (\s -> s{cEventSource = newSource }) modify (\s -> s{cEventSource = newSource })
@ -115,43 +180,43 @@ restartStream = do
else yield bs >> loopRead read else yield bs >> loopRead read
-- Reads the (partial) stream:stream and the server features from the stream. -- Reads the (partial) stream:stream and the server features from the stream.
-- Also validates the stream element's attributes and throws an error if -- Returns the (unvalidated) stream attributes, the unparsed element, or
-- appropriate. -- throwError throws a `XmppOtherFailure' (if something other than an element
-- was encountered at first, or if something other than stream features was
-- encountered second).
-- TODO: from. -- TODO: from.
streamS :: Maybe Jid -> StreamSink ( LangTag streamS :: Maybe Jid -> StreamSink (Either Element ( Text
, Maybe Jid
, Maybe Jid , Maybe Jid
, Maybe Text , Maybe Text
, ServerFeatures) , Maybe LangTag
, ServerFeatures ))
streamS expectedTo = do streamS expectedTo = do
(from, to, id, langTag) <- xmppStreamHeader header <- xmppStreamHeader
case header of
Right (version, from, to, id, langTag) -> do
features <- xmppStreamFeatures features <- xmppStreamFeatures
return (langTag, from, id, features) return $ Right (version, from, to, id, langTag, features)
Left el -> return $ Left el
where where
xmppStreamHeader :: StreamSink (Maybe Jid, Maybe Jid, Maybe Text.Text, LangTag) xmppStreamHeader :: StreamSink (Either Element (Text, Maybe Jid, Maybe Jid, Maybe Text.Text, Maybe LangTag))
xmppStreamHeader = do xmppStreamHeader = do
lift throwOutJunk lift throwOutJunk
-- Get the stream:stream element (or whatever it is) from the server, -- Get the stream:stream element (or whatever it is) from the server,
-- and validate what we get. -- and validate what we get.
el <- openElementFromEvents el <- openElementFromEvents -- May throw `XmppOtherFailure' if an
liftIO . print $ unpickleElem xpStream el -- element is not received
case unpickleElem xpStream el of case unpickleElem xpStream el of
Left _ -> throwError $ findStreamErrors el Left _ -> return $ Left el
Right r -> validateData r Right r -> return $ Right r
validateData (_, _, _, _, Nothing) = throwError $ StreamWrongLangTag Nothing
validateData (ver, from, to, i, Just lang)
| ver /= "1.0" = throwError $ StreamWrongVersion (Just ver)
| isJust to && to /= expectedTo = throwError $ StreamWrongTo (Text.pack . show <$> to)
| otherwise = return (from, to, i, lang)
xmppStreamFeatures :: StreamSink ServerFeatures xmppStreamFeatures :: StreamSink ServerFeatures
xmppStreamFeatures = do xmppStreamFeatures = do
e <- lift $ elements =$ CL.head e <- lift $ elements =$ CL.head
case e of case e of
Nothing -> liftIO $ Ex.throwIO StreamConnectionError Nothing -> throwError XmppOtherFailure
Just r -> streamUnpickleElem xpStreamFeatures r Just r -> streamUnpickleElem xpStreamFeatures r
xpStream :: PU [Node] (Text, Maybe Jid, Maybe Jid, Maybe Text, Maybe LangTag) xpStream :: PU [Node] (Text, Maybe Jid, Maybe Jid, Maybe Text, Maybe LangTag)
xpStream = ("xpStream","") <?+> xpElemAttrs xpStream = ("xpStream","") <?+> xpElemAttrs
(Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream")) (Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream"))
@ -175,14 +240,14 @@ xpStreamFeatures = ("xpStreamFeatures", "") <?+> xpWrap
(Just "stream") (Just "stream")
) )
(xpTriple (xpTriple
(xpOption pickleTLSFeature) (xpOption pickleTlsFeature)
(xpOption pickleSaslFeature) (xpOption pickleSaslFeature)
(xpAll xpElemVerbatim) (xpAll xpElemVerbatim)
) )
) )
where where
pickleTLSFeature :: PU [Node] Bool pickleTlsFeature :: PU [Node] Bool
pickleTLSFeature = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-tls}starttls" pickleTlsFeature = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-tls}starttls"
(xpElemExists "required") (xpElemExists "required")
pickleSaslFeature :: PU [Node] [Text] pickleSaslFeature :: PU [Node] [Text]
pickleSaslFeature = xpElemNodes pickleSaslFeature = xpElemNodes

54
source/Network/Xmpp/TLS.hs → source/Network/Xmpp/Tls.hs

@ -2,7 +2,7 @@
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.TLS where module Network.Xmpp.Tls where
import qualified Control.Exception.Lifted as Ex import qualified Control.Exception.Lifted as Ex
import Control.Monad import Control.Monad
@ -13,15 +13,16 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import Data.Conduit import Data.Conduit
import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Binary as CB
import Data.Conduit.TLS as TLS import Data.Conduit.Tls as TLS
import Data.Typeable import Data.Typeable
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Text.XML.Stream.Elements(ppElement)
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import Control.Concurrent.STM.TMVar
mkBackend con = Backend { backendSend = \bs -> void (cSend con bs) mkBackend con = Backend { backendSend = \bs -> void (cSend con bs)
, backendRecv = cRecv con , backendRecv = cRecv con
, backendFlush = cFlush con , backendFlush = cFlush con
@ -72,48 +73,33 @@ exampleParams = TLS.defaultParamsClient
return TLS.CertificateUsageAccept return TLS.CertificateUsageAccept
} }
-- | Error conditions that may arise during TLS negotiation.
data XmppTLSError = TLSError TLSError
| TLSNoServerSupport
| TLSNoConnection
| TLSConnectionSecured -- ^ Connection already secured
| TLSStreamError StreamError
| XmppTLSError -- General instance used for the Error instance
deriving (Show, Eq, Typeable)
instance Error XmppTLSError where
noMsg = XmppTLSError
-- Pushes "<starttls/>, waits for "<proceed/>", performs the TLS handshake, and -- Pushes "<starttls/>, waits for "<proceed/>", performs the TLS handshake, and
-- restarts the stream. May throw errors. -- restarts the stream.
startTLS :: TLS.TLSParams -> Connection -> IO (Either XmppTLSError ()) startTls :: TLS.TLSParams -> TMVar Connection -> IO (Either XmppFailure ())
startTLS params con = Ex.handle (return . Left . TLSError) startTls params con = Ex.handle (return . Left . TlsError)
. flip withConnection con . flip withConnection con
. runErrorT $ do . runErrorT $ do
features <- lift $ gets sFeatures features <- lift $ gets cFeatures
state <- gets sConnectionState state <- gets cState
case state of case state of
ConnectionPlain -> return () ConnectionPlain -> return ()
ConnectionClosed -> throwError TLSNoConnection ConnectionClosed -> throwError XmppNoConnection
ConnectionSecured -> throwError TLSConnectionSecured ConnectionSecured -> throwError TlsConnectionSecured
con <- lift $ gets cHand con <- lift $ gets cHandle
when (stls features == Nothing) $ throwError TLSNoServerSupport when (stls features == Nothing) $ throwError TlsNoServerSupport
lift $ pushElement starttlsE lift $ pushElement starttlsE
answer <- lift $ pullElement answer <- lift $ pullElement
case answer of case answer of
Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] [] -> return () Left e -> return $ Left e
Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _ -> Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] []) -> return $ Right ()
lift . Ex.throwIO $ StreamConnectionError Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _) -> return $ Left XmppOtherFailure
-- TODO: find something more suitable
e -> lift . Ex.throwIO . StreamXMLError $
"Unexpected element: " ++ ppElement e
(raw, _snk, psh, read, ctx) <- lift $ TLS.tlsinit debug params (mkBackend con) (raw, _snk, psh, read, ctx) <- lift $ TLS.tlsinit debug params (mkBackend con)
let newHand = Hand { cSend = catchPush . psh let newHand = ConnectionHandle { cSend = catchPush . psh
, cRecv = read , cRecv = read
, cFlush = contextFlush ctx , cFlush = contextFlush ctx
, cClose = bye ctx >> cClose con , cClose = bye ctx >> cClose con
} }
lift $ modify ( \x -> x {cHand = newHand}) lift $ modify ( \x -> x {cHandle = newHand})
either (lift . Ex.throwIO) return =<< lift restartStream either (lift . Ex.throwIO) return =<< lift restartStream
modify (\s -> s{sConnectionState = ConnectionSecured}) modify (\s -> s{cState = ConnectionSecured})
return () return ()

128
source/Network/Xmpp/Types.hs

@ -28,17 +28,16 @@ module Network.Xmpp.Types
, StanzaErrorCondition(..) , StanzaErrorCondition(..)
, StanzaErrorType(..) , StanzaErrorType(..)
, StanzaID(..) , StanzaID(..)
, StreamError(..) , XmppFailure(..)
, StreamErrorCondition(..) , StreamErrorCondition(..)
, Version(..) , Version(..)
, HandleLike(..) , ConnectionHandle(..)
, Connection(..) , Connection(..)
, Connection_(..)
, withConnection , withConnection
, withConnection' , withConnection'
, mkConnection , mkConnection
, ConnectionState(..) , ConnectionState(..)
, XmppStreamError(..) , StreamErrorInfo(..)
, langTag , langTag
, module Network.Xmpp.Jid , module Network.Xmpp.Jid
) )
@ -62,6 +61,7 @@ import qualified Data.Text as Text
import Data.Typeable(Typeable) import Data.Typeable(Typeable)
import Data.XML.Types import Data.XML.Types
import qualified Network.TLS as TLS
import qualified Network as N import qualified Network as N
@ -619,28 +619,41 @@ instance Read StreamErrorCondition where
readsPrec _ "unsupported-version" = [(StreamUnsupportedVersion , "")] readsPrec _ "unsupported-version" = [(StreamUnsupportedVersion , "")]
readsPrec _ _ = [(StreamUndefinedCondition , "")] readsPrec _ _ = [(StreamUndefinedCondition , "")]
data XmppStreamError = XmppStreamError -- | Encapsulates information about an XMPP stream error.
data StreamErrorInfo = StreamErrorInfo
{ errorCondition :: !StreamErrorCondition { errorCondition :: !StreamErrorCondition
, errorText :: !(Maybe (Maybe LangTag, Text)) , errorText :: !(Maybe (Maybe LangTag, Text))
, errorXML :: !(Maybe Element) , errorXml :: !(Maybe Element)
} deriving (Show, Eq) } deriving (Show, Eq)
data StreamError = StreamError XmppStreamError -- | Signals an XMPP stream error or another unpredicted stream-related
| StreamUnknownError -- Something has gone wrong, but we don't -- situation.
-- know what data XmppFailure = StreamErrorFailure StreamErrorInfo -- ^ An error XML stream
| StreamNotStreamElement Text -- element has been
| StreamInvalidStreamNamespace (Maybe Text) -- encountered.
| StreamInvalidStreamPrefix (Maybe Text) | StreamEndFailure -- ^ The stream has been closed.
| StreamWrongTo (Maybe Text) -- This exception is caught by the
| StreamWrongVersion (Maybe Text) -- concurrent implementation, and
| StreamWrongLangTag (Maybe Text) -- will thus not be visible
| StreamXMLError String -- If stream pickling goes wrong. -- through use of 'Session'.
| StreamStreamEnd -- received closing stream tag | StreamCloseError ([Element], XmppFailure) -- ^ When an XmppFailure
| StreamConnectionError -- is encountered in
-- closeStreams, this
-- constructor wraps the
-- elements collected so
-- far.
| TlsError TLS.TLSError
| TlsNoServerSupport
| XmppNoConnection
| TlsConnectionSecured -- ^ Connection already secured
| XmppOtherFailure -- ^ Undefined condition. More
-- information should be available
-- in the log.
| XmppIOException IOException
deriving (Show, Eq, Typeable) deriving (Show, Eq, Typeable)
instance Exception StreamError instance Exception XmppFailure
instance Error StreamError where noMsg = StreamConnectionError instance Error XmppFailure where noMsg = XmppOtherFailure
-- ============================================================================= -- =============================================================================
-- XML TYPES -- XML TYPES
@ -740,59 +753,50 @@ data ServerFeatures = SF
, other :: ![Element] , other :: ![Element]
} deriving Show } deriving Show
-- | Signals the state of the connection.
data ConnectionState data ConnectionState
= ConnectionClosed -- ^ No connection at this point. = ConnectionClosed -- ^ No connection at this point.
| ConnectionPlain -- ^ Connection established, but not secured. | ConnectionPlain -- ^ Connection established, but not secured.
| ConnectionSecured -- ^ Connection established and secured via TLS. | ConnectionSecured -- ^ Connection established and secured via TLS.
deriving (Show, Eq, Typeable) deriving (Show, Eq, Typeable)
data HandleLike = Hand { cSend :: BS.ByteString -> IO Bool -- | Defines operations for sending, receiving, flushing, and closing on a
-- connection.
data ConnectionHandle =
ConnectionHandle { cSend :: BS.ByteString -> IO Bool
, cRecv :: Int -> IO BS.ByteString , cRecv :: Int -> IO BS.ByteString
-- This is to hold the state of the XML parser -- This is to hold the state of the XML parser (otherwise
-- (otherwise we will receive lot's of EvenBegin -- we will receive EventBeginDocument events and forget
-- Document and forger about name prefixes) -- about name prefixes).
, cFlush :: IO () , cFlush :: IO ()
, cClose :: IO () , cClose :: IO ()
} }
data Connection_ = Connection_ data Connection = Connection
{ sConnectionState :: !ConnectionState -- ^ State of { cState :: !ConnectionState -- ^ State of connection
-- connection , cHandle :: ConnectionHandle -- ^ Handle to send, receive, flush, and close
, cHand :: HandleLike -- on the connection.
, cEventSource :: ResumableSource IO Event , cEventSource :: ResumableSource IO Event -- ^ Event conduit source, and
, sFeatures :: !ServerFeatures -- ^ Features the server -- its associated finalizer
-- advertised , cFeatures :: !ServerFeatures -- ^ Features as advertised by the server
, sHostname :: !(Maybe Text) -- ^ Hostname of the , cHostName :: !(Maybe Text) -- ^ Hostname of the server
-- server , cJid :: !(Maybe Jid) -- ^ Our JID
, sJid :: !(Maybe Jid) -- ^ Our JID , cPreferredLang :: !(Maybe LangTag) -- ^ Default language when no explicit
, sPreferredLang :: !(Maybe LangTag) -- ^ Default language
-- when no explicit
-- language tag is set -- language tag is set
, sStreamLang :: !(Maybe LangTag) -- ^ Will be a `Just' , cStreamLang :: !(Maybe LangTag) -- ^ Will be a `Just' value once connected
-- value once connected
-- to the server. -- to the server.
, sStreamId :: !(Maybe Text) -- ^ Stream ID as , cStreamId :: !(Maybe Text) -- ^ Stream ID as specified by the server.
-- specified by the , cToJid :: !(Maybe Jid) -- ^ JID to include in the stream element's `to'
-- server. -- attribute when the connection is secured. See
, sToJid :: !(Maybe Jid) -- ^ JID to include in the -- also below.
-- stream element's `to' , cJidWhenPlain :: !Bool -- ^ Whether or not to also include the Jid when
-- attribute when the -- the connection is plain.
-- connection is , cFrom :: !(Maybe Jid) -- ^ From as specified by the server in the stream
-- secured. See also below. -- element's `from' attribute.
, sJidWhenPlain :: !Bool -- ^ Whether or not to also
-- include the Jid when the
-- connection is plain.
, sFrom :: !(Maybe Jid) -- ^ From as specified by
-- the server in the
-- stream element's `from'
-- attribute.
} }
withConnection :: StateT Connection IO (Either XmppFailure c) -> TMVar Connection -> IO (Either XmppFailure c)
newtype Connection = Connection {unConnection :: TMVar Connection_} withConnection action con = bracketOnError
withConnection :: StateT Connection_ IO c -> Connection -> IO c
withConnection action (Connection con) = bracketOnError
(atomically $ takeTMVar con) (atomically $ takeTMVar con)
(atomically . putTMVar con ) (atomically . putTMVar con )
(\c -> do (\c -> do
@ -802,12 +806,12 @@ withConnection action (Connection con) = bracketOnError
) )
-- nonblocking version. Changes to the connection are ignored! -- nonblocking version. Changes to the connection are ignored!
withConnection' :: StateT Connection_ IO b -> Connection -> IO b withConnection' :: StateT Connection IO (Either XmppFailure b) -> TMVar Connection -> IO (Either XmppFailure b)
withConnection' action (Connection con) = do withConnection' action con = do
con_ <- atomically $ readTMVar con con_ <- atomically $ readTMVar con
(r, _) <- runStateT action con_ (r, _) <- runStateT action con_
return r return r
mkConnection :: Connection_ -> IO Connection mkConnection :: Connection -> IO (TMVar Connection)
mkConnection con = Connection `fmap` (atomically $ newTMVar con) mkConnection con = {- Connection `fmap` -} (atomically $ newTMVar con)

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

@ -19,7 +19,7 @@ import qualified Data.Text as Text
import Data.XML.Pickle import Data.XML.Pickle
import qualified Data.XML.Types as XML import qualified Data.XML.Types as XML
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Basic import Network.Xmpp.Basic
@ -50,7 +50,7 @@ data Query = Query { instructions :: Maybe Text.Text
emptyQuery = Query Nothing False False [] emptyQuery = Query Nothing False False []
query :: IQRequestType -> Query -> Connection -> IO (Either IbrError Query) query :: IQRequestType -> Query -> TMVar Connection -> IO (Either IbrError Query)
query queryType x con = do query queryType x con = do
answer <- pushIQ' "ibr" Nothing queryType Nothing (pickleElem xpQuery x) con answer <- pushIQ' "ibr" Nothing queryType Nothing (pickleElem xpQuery x) con
case answer of case answer of
@ -93,7 +93,7 @@ mapError f = mapErrorT (liftM $ left f)
-- | Retrieve the necessary fields and fill them in to register an account with -- | Retrieve the necessary fields and fill them in to register an account with
-- the server. -- the server.
registerWith :: [(Field, Text.Text)] registerWith :: [(Field, Text.Text)]
-> Connection -> TMVar Connection
-> IO (Either RegisterError Query) -> IO (Either RegisterError Query)
registerWith givenFields con = runErrorT $ do registerWith givenFields con = runErrorT $ do
fs <- mapError IbrError . ErrorT $ requestFields con fs <- mapError IbrError . ErrorT $ requestFields con
@ -125,7 +125,7 @@ deleteAccount host hostname port username password = do
-- | Terminate your account on the server. You have to be logged in for this to -- | Terminate your account on the server. You have to be logged in for this to
-- work. You connection will most likely be terminated after unregistering. -- work. You connection will most likely be terminated after unregistering.
unregister :: Connection -> IO (Either IbrError Query) unregister :: TMVar Connection -> IO (Either IbrError Query)
unregister = query Set $ emptyQuery {remove = True} unregister = query Set $ emptyQuery {remove = True}
unregister' :: Session -> IO (Either IbrError Query) unregister' :: Session -> IO (Either IbrError Query)

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

@ -26,16 +26,16 @@ import Data.XML.Types
import Network.Xmpp import Network.Xmpp
import Network.Xmpp.Concurrent import Network.Xmpp.Concurrent
import Network.Xmpp.Concurrent.Channels
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection import Network.Xmpp.Connection_
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import Network.Xmpp.Types import Network.Xmpp.Types
import Control.Concurrent.STM.TMVar
data DiscoError = DiscoNoQueryElement data DiscoError = DiscoNoQueryElement
| DiscoIQError IQError | DiscoIQError (Maybe IQError)
| DiscoTimeout | DiscoTimeout
| DiscoXMLError Element UnpickleError | DiscoXmlError Element UnpickleError
deriving (Show) deriving (Show)
@ -92,12 +92,12 @@ queryInfo :: Jid -- ^ Entity to query
queryInfo to node context = do queryInfo to node context = do
res <- sendIQ' (Just to) Get Nothing queryBody context res <- sendIQ' (Just to) Get Nothing queryBody context
return $ case res of return $ case res of
IQResponseError e -> Left $ DiscoIQError e IQResponseError e -> Left $ DiscoIQError (Just e)
IQResponseTimeout -> Left $ DiscoTimeout IQResponseTimeout -> Left $ DiscoTimeout
IQResponseResult r -> case iqResultPayload r of IQResponseResult r -> case iqResultPayload r of
Nothing -> Left DiscoNoQueryElement Nothing -> Left DiscoNoQueryElement
Just p -> case unpickleElem xpQueryInfo p of Just p -> case unpickleElem xpQueryInfo p of
Left e -> Left $ DiscoXMLError p e Left e -> Left $ DiscoXmlError p e
Right r -> Right r Right r -> Right r
where where
queryBody = pickleElem xpQueryInfo (QIR node [] []) queryBody = pickleElem xpQueryInfo (QIR node [] [])
@ -105,16 +105,18 @@ queryInfo to node context = do
xmppQueryInfo :: Maybe Jid xmppQueryInfo :: Maybe Jid
-> Maybe Text.Text -> Maybe Text.Text
-> Connection -> TMVar Connection
-> IO (Either DiscoError QueryInfoResult) -> IO (Either DiscoError QueryInfoResult)
xmppQueryInfo to node con = do xmppQueryInfo to node con = do
res <- pushIQ' "info" to Get Nothing queryBody con res <- pushIQ' "info" to Get Nothing queryBody con
return $ case res of return $ case res of
Left e -> Left $ DiscoIQError e Left e -> Left $ DiscoIQError Nothing
Right res' -> case res' of
Left e -> Left $ DiscoIQError (Just e)
Right r -> case iqResultPayload r of Right r -> case iqResultPayload r of
Nothing -> Left DiscoNoQueryElement Nothing -> Left DiscoNoQueryElement
Just p -> case unpickleElem xpQueryInfo p of Just p -> case unpickleElem xpQueryInfo p of
Left e -> Left $ DiscoXMLError p e Left e -> Left $ DiscoXmlError p e
Right r -> Right r Right r -> Right r
where where
queryBody = pickleElem xpQueryInfo (QIR node [] []) queryBody = pickleElem xpQueryInfo (QIR node [] [])
@ -156,12 +158,12 @@ queryItems :: Jid -- ^ Entity to query
queryItems to node session = do queryItems to node session = do
res <- sendIQ' (Just to) Get Nothing queryBody session res <- sendIQ' (Just to) Get Nothing queryBody session
return $ case res of return $ case res of
IQResponseError e -> Left $ DiscoIQError e IQResponseError e -> Left $ DiscoIQError (Just e)
IQResponseTimeout -> Left $ DiscoTimeout IQResponseTimeout -> Left $ DiscoTimeout
IQResponseResult r -> case iqResultPayload r of IQResponseResult r -> case iqResultPayload r of
Nothing -> Left DiscoNoQueryElement Nothing -> Left DiscoNoQueryElement
Just p -> case unpickleElem xpQueryItems p of Just p -> case unpickleElem xpQueryItems p of
Left e -> Left $ DiscoXMLError p e Left e -> Left $ DiscoXmlError p e
Right r -> Right r Right r -> Right r
where where
queryBody = pickleElem xpQueryItems (node, []) queryBody = pickleElem xpQueryItems (node, [])

2
source/Text/XML/Stream/Elements.hs → source/Text/Xml/Stream/Elements.hs

@ -1,7 +1,7 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Text.XML.Stream.Elements where module Text.Xml.Stream.Elements where
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))
import Control.Exception import Control.Exception

7
source/Utils.hs

@ -1,7 +0,0 @@
module Utils where
whileJust f = do
f' <- f
case f' of
Just x -> x : whileJust f
Nothing -> []
Loading…
Cancel
Save