Browse Source

Merge remote-tracking branch 'philonous/master'

Conflicts:
	source/Network/Xmpp/Concurrent.hs
	source/Network/Xmpp/IM/Roster.hs
	source/Network/Xmpp/Types.hs
master
Jon Kristensen 13 years ago
parent
commit
2fd4e9210e
  1. 15
      pontarius-xmpp.cabal
  2. 17
      source/Network/Xmpp.hs
  3. 58
      source/Network/Xmpp/Concurrent.hs
  4. 8
      source/Network/Xmpp/Concurrent/Basic.hs
  5. 44
      source/Network/Xmpp/Concurrent/IQ.hs
  6. 2
      source/Network/Xmpp/Concurrent/Message.hs
  7. 8
      source/Network/Xmpp/Concurrent/Monad.hs
  8. 1
      source/Network/Xmpp/Concurrent/Presence.hs
  9. 29
      source/Network/Xmpp/Concurrent/Threads.hs
  10. 14
      source/Network/Xmpp/Concurrent/Types.hs
  11. 29
      source/Network/Xmpp/IM.hs
  12. 172
      source/Network/Xmpp/IM/Message.hs
  13. 109
      source/Network/Xmpp/IM/Presence.hs
  14. 158
      source/Network/Xmpp/IM/Roster.hs
  15. 47
      source/Network/Xmpp/IM/Roster/Types.hs
  16. 5
      source/Network/Xmpp/Internal.hs
  17. 84
      source/Network/Xmpp/Sasl.hs
  18. 30
      source/Network/Xmpp/Sasl/Common.hs
  19. 48
      source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs
  20. 43
      source/Network/Xmpp/Sasl/Mechanisms/Plain.hs
  21. 58
      source/Network/Xmpp/Sasl/Mechanisms/Scram.hs
  22. 9
      source/Network/Xmpp/Sasl/StringPrep.hs
  23. 76
      source/Network/Xmpp/Stanza.hs
  24. 328
      source/Network/Xmpp/Stream.hs
  25. 75
      source/Network/Xmpp/Tls.hs
  26. 55
      source/Network/Xmpp/Types.hs
  27. 130
      source/Network/Xmpp/Utilities.hs
  28. 23
      source/Network/Xmpp/Xep/DataForms.hs

15
pontarius-xmpp.cabal

@ -33,11 +33,12 @@ Library
, base64-bytestring >=0.1.0.0 , base64-bytestring >=0.1.0.0
, binary >=0.4.1 , binary >=0.4.1
, bytestring >=0.9.1.9 , bytestring >=0.9.1.9
, conduit >=0.5 && <1.0 , conduit >=0.5
, containers >=0.4.0.0 , containers >=0.4.0.0
, crypto-api >=0.9 , crypto-api >=0.9
, crypto-random-api >=0.2 , crypto-random-api >=0.2
, cryptohash >=0.6.1 , cryptohash >=0.6.1
, cryptohash-cryptoapi >=0.1
, data-default >=0.2 , data-default >=0.2
, dns >=0.3.0 , dns >=0.3.0
, hslogger >=1.1.0 , hslogger >=1.1.0
@ -58,17 +59,22 @@ Library
, void >=0.5.5 , void >=0.5.5
, xml-types >=0.3.1 , xml-types >=0.3.1
, xml-conduit >=1.0 , xml-conduit >=1.0
, xml-picklers >=0.3 , xml-picklers >=0.3.2
Exposed-modules: Network.Xmpp Exposed-modules: Network.Xmpp
, Network.Xmpp.IM
, Network.Xmpp.Internal , Network.Xmpp.Internal
Other-modules: Network.Xmpp.Concurrent Other-modules: Network.Xmpp.Concurrent
, Network.Xmpp.Concurrent.Types
, Network.Xmpp.Concurrent.Basic , Network.Xmpp.Concurrent.Basic
, Network.Xmpp.Concurrent.IQ , Network.Xmpp.Concurrent.IQ
, Network.Xmpp.Concurrent.Message , Network.Xmpp.Concurrent.Message
, Network.Xmpp.Concurrent.Monad
, Network.Xmpp.Concurrent.Presence , Network.Xmpp.Concurrent.Presence
, Network.Xmpp.Concurrent.Threads , Network.Xmpp.Concurrent.Threads
, Network.Xmpp.Concurrent.Monad , Network.Xmpp.Concurrent.Types
, Network.Xmpp.IM.Message
, Network.Xmpp.IM.Presence
, Network.Xmpp.IM.Roster
, Network.Xmpp.IM.Roster.Types
, Network.Xmpp.Marshal , Network.Xmpp.Marshal
, Network.Xmpp.Sasl , Network.Xmpp.Sasl
, Network.Xmpp.Sasl.Common , Network.Xmpp.Sasl.Common
@ -78,6 +84,7 @@ Library
, Network.Xmpp.Sasl.Mechanisms.Scram , Network.Xmpp.Sasl.Mechanisms.Scram
, Network.Xmpp.Sasl.StringPrep , Network.Xmpp.Sasl.StringPrep
, Network.Xmpp.Sasl.Types , Network.Xmpp.Sasl.Types
, Network.Xmpp.Stanza
, Network.Xmpp.Stream , Network.Xmpp.Stream
, Network.Xmpp.Tls , Network.Xmpp.Tls
, Network.Xmpp.Types , Network.Xmpp.Types

17
source/Network/Xmpp.hs

@ -29,6 +29,7 @@ module Network.Xmpp
, session , session
, StreamConfiguration(..) , StreamConfiguration(..)
, SessionConfiguration(..) , SessionConfiguration(..)
, ConnectionDetails(..)
-- TODO: Close session, etc. -- TODO: Close session, etc.
-- ** Authentication handlers -- ** Authentication handlers
-- | The use of 'scramSha1' is /recommended/, but 'digestMd5' might be -- | The use of 'scramSha1' is /recommended/, but 'digestMd5' might be
@ -45,6 +46,7 @@ module Network.Xmpp
, isFull , isFull
, jidFromText , jidFromText
, jidFromTexts , jidFromTexts
, getJid
-- * Stanzas -- * Stanzas
-- | The basic protocol data unit in XMPP is the XML stanza. The stanza is -- | The basic protocol data unit in XMPP is the XML stanza. The stanza is
-- 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
@ -81,6 +83,7 @@ module Network.Xmpp
-- occur in a system such as email. It is not to be confused with -- occur in a system such as email. It is not to be confused with
-- /instant messaging/ which is handled in the 'Network.Xmpp.IM' module -- /instant messaging/ which is handled in the 'Network.Xmpp.IM' module
, Message(..) , Message(..)
, message
, MessageError(..) , MessageError(..)
, MessageType(..) , MessageType(..)
-- *** Creating -- *** Creating
@ -102,6 +105,12 @@ module Network.Xmpp
, PresenceType(..) , PresenceType(..)
, PresenceError(..) , PresenceError(..)
-- *** Creating -- *** Creating
, presence
, presenceOffline
, presenceOnline
, presenceSubscribe
, presenceSubscribed
, presenceUnsubscribe
, presTo , presTo
-- *** Sending -- *** Sending
-- | Sends a presence stanza. In general, the presence stanza should have no -- | Sends a presence stanza. In general, the presence stanza should have no
@ -137,8 +146,7 @@ module Network.Xmpp
, sendIQ' , sendIQ'
, answerIQ , answerIQ
, listenIQChan , listenIQChan
, iqRequestPayload , dropIQChan
, iqResultPayload
-- * Errors -- * Errors
, StanzaError(..) , StanzaError(..)
, StanzaErrorType(..) , StanzaErrorType(..)
@ -156,10 +164,9 @@ module Network.Xmpp
, AuthOtherFailure ) , AuthOtherFailure )
) where ) where
import Network
import Network.Xmpp.Concurrent import Network.Xmpp.Concurrent
import Network.Xmpp.Utilities
import Network.Xmpp.Sasl import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Tls import Network.Xmpp.Stanza
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Utilities

58
source/Network/Xmpp/Concurrent.hs

@ -18,37 +18,30 @@ import Control.Applicative((<$>),(<*>))
import Control.Concurrent import Control.Concurrent
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad import Control.Monad
import Control.Monad.Error
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.IORef
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Maybe import Data.Maybe
import Data.Maybe (fromMaybe)
import Data.Text as Text import Data.Text as Text
import Data.XML.Types import Data.XML.Types
import Network import Network
import qualified Network.TLS as TLS
import Network.Xmpp.Concurrent.Basic import Network.Xmpp.Concurrent.Basic
import Network.Xmpp.Concurrent.IQ import Network.Xmpp.Concurrent.IQ
import Network.Xmpp.Concurrent.Message import Network.Xmpp.Concurrent.Message
import Network.Xmpp.Concurrent.Monad import Network.Xmpp.Concurrent.Monad
import Network.Xmpp.Concurrent.Presence import Network.Xmpp.Concurrent.Presence
import Network.Xmpp.Concurrent.Threads import Network.Xmpp.Concurrent.Threads
import Network.Xmpp.Concurrent.Threads
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.IM.Roster.Types
import Network.Xmpp.IM.Roster
import Network.Xmpp.Marshal import Network.Xmpp.Marshal
import Network.Xmpp.Sasl import Network.Xmpp.Sasl
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 Network.Xmpp.Utilities import Network.Xmpp.Utilities
import Control.Monad.Error
import Data.Default
import System.Log.Logger
import Control.Monad.State.Strict
runHandlers :: (TChan Stanza) -> [StanzaHandler] -> Stanza -> IO () runHandlers :: (TChan Stanza) -> [StanzaHandler] -> Stanza -> IO ()
runHandlers _ [] _ = return () runHandlers _ [] _ = return ()
runHandlers outC (h:hands) sta = do runHandlers outC (h:hands) sta = do
@ -80,8 +73,24 @@ handleIQ iqHands outC sta = atomically $ do
case Map.lookup (iqRequestType iq, iqNS) byNS of case Map.lookup (iqRequestType iq, iqNS) byNS of
Nothing -> writeTChan outC $ serviceUnavailable iq Nothing -> writeTChan outC $ serviceUnavailable iq
Just ch -> do Just ch -> do
sent <- newTVar False sentRef <- newTVar False
writeTChan ch $ IQRequestTicket sent iq let answerT answer = do
let IQRequest iqid from _to lang _tp bd = iq
response = case answer of
Left er -> IQErrorS $ IQError iqid Nothing
from lang er
(Just bd)
Right res -> IQResultS $ IQResult iqid Nothing
from lang res
atomically $ do
sent <- readTVar sentRef
case sent of
False -> do
writeTVar sentRef True
writeTChan outC response
return True
True -> return False
writeTChan ch $ IQRequestTicket answerT iq
serviceUnavailable (IQRequest iqid from _to lang _tp bd) = serviceUnavailable (IQRequest iqid from _to lang _tp bd) =
IQErrorS $ IQError iqid Nothing from lang err (Just bd) IQErrorS $ IQError iqid Nothing from lang err (Just bd)
err = StanzaError Cancel ServiceUnavailable Nothing Nothing err = StanzaError Cancel ServiceUnavailable Nothing Nothing
@ -96,7 +105,7 @@ handleIQ iqHands outC sta = atomically $ do
_ <- tryPutTMVar tmvar answer -- Don't block. _ <- tryPutTMVar tmvar answer -- Don't block.
writeTVar handlers (byNS, byID') writeTVar handlers (byNS, byID')
where where
iqID (Left err) = iqErrorID err iqID (Left err') = iqErrorID err'
iqID (Right iq') = iqResultID iq' iqID (Right iq') = iqResultID iq'
-- | Creates and initializes a new Xmpp context. -- | Creates and initializes a new Xmpp context.
@ -104,26 +113,32 @@ newSession :: Stream -> SessionConfiguration -> IO (Either XmppFailure Session)
newSession stream config = runErrorT $ do newSession stream config = runErrorT $ do
outC <- lift newTChanIO outC <- lift newTChanIO
stanzaChan <- lift newTChanIO stanzaChan <- lift newTChanIO
iqHandlers <- lift $ newTVarIO (Map.empty, Map.empty) iqHands <- lift $ newTVarIO (Map.empty, Map.empty)
eh <- lift $ newTVarIO $ EventHandlers { connectionClosedHandler = sessionClosedHandler config } eh <- lift $ newTVarIO $ EventHandlers { connectionClosedHandler = sessionClosedHandler config }
let stanzaHandler = runHandlers outC $ Prelude.concat [ [toChan stanzaChan] ros <- liftIO . newTVarIO $ Roster Nothing Map.empty
let rosterH = if (enableRoster config) then handleRoster ros
else \ _ _ -> return True
let stanzaHandler = runHandlers outC $ Prelude.concat [ [ toChan stanzaChan ]
, extraStanzaHandlers , extraStanzaHandlers
config config
, [handleIQ iqHandlers] , [ handleIQ iqHands
, rosterH
]
] ]
(kill, wLock, streamState, readerThread) <- ErrorT $ startThreadsWith stanzaHandler eh stream (kill, wLock, streamState, reader) <- ErrorT $ startThreadsWith stanzaHandler eh stream
writer <- lift $ forkIO $ writeWorker outC wLock writer <- lift $ forkIO $ writeWorker outC wLock
idGen <- liftIO $ sessionStanzaIDs config idGen <- liftIO $ sessionStanzaIDs config
return $ Session { stanzaCh = stanzaChan return $ Session { stanzaCh = stanzaChan
, outCh = outC , outCh = outC
, iqHandlers = iqHandlers , iqHandlers = iqHands
, writeRef = wLock , writeRef = wLock
, readerThread = readerThread , readerThread = reader
, idGenerator = idGen , idGenerator = idGen
, streamRef = streamState , streamRef = streamState
, eventHandlers = eh , eventHandlers = eh
, stopThreads = kill >> killThread writer , stopThreads = kill >> killThread writer
, conf = config , conf = config
, rosterRef = ros
} }
-- Worker to write stanzas to the stream concurrently. -- Worker to write stanzas to the stream concurrently.
@ -145,12 +160,12 @@ writeWorker stCh writeR = forever $ do
-- third parameter is a 'Just' value, @session@ will attempt to authenticate and -- third parameter is a 'Just' value, @session@ will attempt to authenticate and
-- acquire an XMPP resource. -- acquire an XMPP resource.
session :: HostName -- ^ The hostname / realm session :: HostName -- ^ The hostname / realm
-> SessionConfiguration -- ^ configuration details
-> Maybe ([SaslHandler], Maybe Text) -- ^ SASL handlers and the desired -> Maybe ([SaslHandler], Maybe Text) -- ^ SASL handlers and the desired
-- JID resource (or Nothing to let -- JID resource (or Nothing to let
-- the server decide) -- the server decide)
-> SessionConfiguration -- ^ configuration details
-> IO (Either XmppFailure Session) -> IO (Either XmppFailure Session)
session realm config mbSasl = runErrorT $ do session realm mbSasl config = runErrorT $ do
stream <- ErrorT $ openStream realm (sessionStreamConfiguration config) stream <- ErrorT $ openStream realm (sessionStreamConfiguration config)
ErrorT $ tls stream ErrorT $ tls stream
mbAuthError <- case mbSasl of mbAuthError <- case mbSasl of
@ -160,4 +175,5 @@ session realm config mbSasl = runErrorT $ do
Nothing -> return () Nothing -> return ()
Just _ -> throwError XmppAuthFailure Just _ -> throwError XmppAuthFailure
ses <- ErrorT $ newSession stream config ses <- ErrorT $ newSession stream config
liftIO $ when (enableRoster config) $ initRoster ses
return ses return ses

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

@ -3,7 +3,9 @@ module Network.Xmpp.Concurrent.Basic where
import Control.Concurrent.STM import Control.Concurrent.STM
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import Control.Monad.State.Strict
-- | Send a stanza to the server. -- | Send a stanza to the server.
sendStanza :: Stanza -> Session -> IO () sendStanza :: Stanza -> Session -> IO ()
@ -14,3 +16,9 @@ dupSession :: Session -> IO Session
dupSession session = do dupSession session = do
stanzaCh' <- atomically $ dupTChan (stanzaCh session) stanzaCh' <- atomically $ dupTChan (stanzaCh session)
return $ session {stanzaCh = stanzaCh'} return $ session {stanzaCh = stanzaCh'}
-- | Return the JID assigned to us by the server
getJid :: Session -> IO (Maybe Jid)
getJid Session{streamRef = st} = do
s <- atomically $ readTMVar st
withStream' (gets streamJid) s

44
source/Network/Xmpp/Concurrent/IQ.hs

@ -4,8 +4,6 @@ module Network.Xmpp.Concurrent.IQ where
import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Text (Text) import Data.Text (Text)
@ -64,9 +62,14 @@ sendIQ' to tp lang body session = do
-- | Retrieves an IQ listener channel. If the namespace/'IQRequestType' is not -- | Retrieves an IQ listener channel. If the namespace/'IQRequestType' is not
-- already handled, a new 'TChan' is created and returned as a 'Right' value. -- already handled, a new 'TChan' is created and returned as a 'Right' value.
-- Otherwise, the already existing channel will be returned wrapped in a 'Left' -- Otherwise, the already existing channel will be returned wrapped in a 'Left'
-- value. Note that the 'Left' channel might need to be duplicated in order not -- value. The 'Left' channel might need to be duplicated in order not
-- to interfere with existing consumers. -- to interfere with existing consumers.
listenIQChan :: IQRequestType -- ^ Type of IQs to receive (@Get@ or @Set@) --
-- Note thet every 'IQRequest' must be answered exactly once. To insure this,
-- the incoming requests are wrapped in an 'IQRequestTicket' that prevents
-- multiple responses. Use 'iqRequestBody' to extract the corresponding request
-- and 'answerIQ' to send the response
listenIQChan :: IQRequestType -- ^ Type of IQs to receive ('Get' or 'Set')
-> Text -- ^ Namespace of the child element -> Text -- ^ Namespace of the child element
-> Session -> Session
-> IO (Either (TChan IQRequestTicket) (TChan IQRequestTicket)) -> IO (Either (TChan IQRequestTicket) (TChan IQRequestTicket))
@ -85,23 +88,22 @@ listenIQChan tp ns session = do
Nothing -> Right iqCh Nothing -> Right iqCh
Just iqCh' -> Left iqCh' Just iqCh' -> Left iqCh'
answerIQ :: IQRequestTicket -- | Unregister a previously acquired IQ channel. Please make sure that you
-> Either StanzaError (Maybe Element) -- where the one who acquired it in the first place as no check for ownership
-- can be made
dropIQChan :: IQRequestType -- ^ Type of IQ ('Get' or 'Set')
-> Text -- ^ Namespace of the child element
-> Session -> Session
-> IO Bool -> IO ()
answerIQ (IQRequestTicket dropIQChan tp ns session = do
sentRef let handlers = (iqHandlers session)
(IQRequest iqid from _to lang _tp bd))
answer session = do
let response = case answer of
Left err -> IQErrorS $ IQError iqid Nothing from lang err (Just bd)
Right res -> IQResultS $ IQResult iqid Nothing from lang res
atomically $ do atomically $ do
sent <- readTVar sentRef (byNS, byID) <- readTVar handlers
case sent of let byNS' = Map.delete (tp, ns) byNS
False -> do writeTVar handlers (byNS', byID)
writeTVar sentRef True return ()
writeTChan (outCh session) response -- | Answer an IQ request. Only the first answer ist sent and then True is
return True -- returned. Subsequent answers are dropped and (False is returned in that case)
True -> return False answerIQ :: IQRequestTicket -> Either StanzaError (Maybe Element) -> IO Bool
answerIQ ticket = answerTicket ticket

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

@ -3,9 +3,7 @@ module Network.Xmpp.Concurrent.Message where
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Control.Concurrent.STM import Control.Concurrent.STM
import Data.IORef
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Concurrent.Basic import Network.Xmpp.Concurrent.Basic
-- | Read an element from the inbound stanza channel, discardes any -- | Read an element from the inbound stanza channel, discardes any

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

@ -60,15 +60,15 @@ import Network.Xmpp.Stream
-- | Executes a function to update the event handlers. -- | Executes a function to update the event handlers.
modifyHandlers :: (EventHandlers -> EventHandlers) -> Session -> 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
-- http://hackage.haskell.org/packages/archive/stm/2.4/doc/html/src/Control-Concurrent-STM-TVar.html -- http://hackage.haskell.org/packages/archive/stm/2.4/doc/html/src/Control-Concurrent-STM-TVar.html
-- as it's not available in GHC 7.0. -- as it's not available in GHC 7.0.
modifyTVar :: TVar a -> (a -> a) -> STM () modifyTVar_ :: TVar a -> (a -> a) -> STM ()
modifyTVar var f = do modifyTVar_ var g = do
x <- readTVar var x <- readTVar var
writeTVar var (f x) writeTVar var (g 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_ :: (XmppFailure -> Session -> IO ()) -> Session -> IO () setConnectionClosedHandler_ :: (XmppFailure -> Session -> IO ()) -> Session -> IO ()

1
source/Network/Xmpp/Concurrent/Presence.hs

@ -2,7 +2,6 @@
module Network.Xmpp.Concurrent.Presence where module Network.Xmpp.Concurrent.Presence where
import Control.Concurrent.STM import Control.Concurrent.STM
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.Basic import Network.Xmpp.Concurrent.Basic

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

@ -4,25 +4,18 @@
module Network.Xmpp.Concurrent.Threads where module Network.Xmpp.Concurrent.Threads where
import Network.Xmpp.Types
import Control.Applicative((<$>)) import Control.Applicative((<$>))
import Control.Concurrent import Control.Concurrent
import Control.Concurrent.STM import Control.Concurrent.STM
import qualified Control.Exception.Lifted as Ex import qualified Control.Exception.Lifted as Ex
import Control.Monad import Control.Monad
import Control.Monad.IO.Class import Control.Monad.Error
import Control.Monad.State.Strict import Control.Monad.State.Strict
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import GHC.IO (unsafeUnmask)
import Network.Xmpp.Concurrent.Types import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types
import Control.Concurrent.STM.TMVar
import GHC.IO (unsafeUnmask)
import Control.Monad.Error
import System.Log.Logger import System.Log.Logger
-- Worker to read stanzas from the stream and concurrently distribute them to -- Worker to read stanzas from the stream and concurrently distribute them to
@ -38,8 +31,8 @@ readWorker onStanza onConnectionClosed stateRef =
-- necessarily be interruptible -- necessarily be interruptible
s <- atomically $ do s <- atomically $ do
s@(Stream con) <- readTMVar stateRef s@(Stream con) <- readTMVar stateRef
state <- streamConnectionState <$> readTMVar con scs <- streamConnectionState <$> readTMVar con
when (state == Closed) when (scs == Closed)
retry retry
return s return s
allowInterrupt allowInterrupt
@ -55,7 +48,7 @@ readWorker onStanza onConnectionClosed stateRef =
] ]
case res of case res of
Nothing -> return () -- Caught an exception, nothing to do. TODO: Can this happen? Nothing -> return () -- Caught an exception, nothing to do. TODO: Can this happen?
Just (Left e) -> return () Just (Left _) -> return ()
Just (Right sta) -> onStanza sta 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
@ -85,19 +78,19 @@ startThreadsWith :: (Stanza -> IO ())
TMVar Stream, TMVar Stream,
ThreadId)) ThreadId))
startThreadsWith stanzaHandler eh con = do startThreadsWith stanzaHandler eh con = do
read <- withStream' (gets $ streamSend . streamHandle >>= \d -> return $ Right d) con rd <- withStream' (gets $ streamSend . streamHandle >>= \d -> return $ Right d) con
case read of case rd of
Left e -> return $ Left e Left e -> return $ Left e
Right read' -> do Right read' -> do
writeLock <- newTMVarIO read' 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 rdw <- forkIO $ readWorker stanzaHandler (noCon eh) conS
return $ Right ( killConnection writeLock [rd, cp] return $ Right ( killConnection writeLock [rdw, cp]
, writeLock , writeLock
, conS , conS
, rd , rdw
) )
where where
killConnection writeLock threads = liftIO $ do killConnection writeLock threads = liftIO $ do

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

@ -3,19 +3,16 @@
module Network.Xmpp.Concurrent.Types where module Network.Xmpp.Concurrent.Types where
import qualified Control.Exception.Lifted as Ex
import Control.Concurrent import Control.Concurrent
import Control.Concurrent.STM import Control.Concurrent.STM
import qualified Control.Exception.Lifted as Ex
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.Typeable
import Network.Xmpp.Types
import Data.IORef
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Text (Text) import Data.Text (Text)
import Data.Typeable
import Data.XML.Types (Element)
import Network.Xmpp.IM.Roster.Types
import Network.Xmpp.Types 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
@ -47,6 +44,7 @@ data Session = Session
, streamRef :: TMVar (Stream) , streamRef :: TMVar (Stream)
, eventHandlers :: TVar EventHandlers , eventHandlers :: TVar EventHandlers
, stopThreads :: IO () , stopThreads :: IO ()
, rosterRef :: TVar Roster
, conf :: SessionConfiguration , conf :: SessionConfiguration
} }
@ -59,6 +57,6 @@ type IQHandlers = (Map.Map (IQRequestType, Text) (TChan IQRequestTicket)
-- | Contains whether or not a reply has been sent, and the IQ request body to -- | Contains whether or not a reply has been sent, and the IQ request body to
-- reply to. -- reply to.
data IQRequestTicket = IQRequestTicket data IQRequestTicket = IQRequestTicket
{ sentRef :: (TVar Bool) { answerTicket :: Either StanzaError (Maybe Element) -> IO Bool
, iqRequestBody :: IQRequest , iqRequestBody :: IQRequest
} }

29
source/Network/Xmpp/IM.hs

@ -1,15 +1,28 @@
-- | RFC 6121: Instant Messaging and Presence
--
module Network.Xmpp.IM module Network.Xmpp.IM
( -- * Instant Messages ( -- * Instant Messages
subject MessageBody(..)
, thread , MessageThread(..)
, body , MessageSubject(..)
, bodies , instantMessage
, newIM , getIM
, simpleIM , withIM
, answerIM
-- * Presence -- * Presence
, module Network.Xmpp.IM.Presence , ShowStatus(..)
, IMPresence(..)
, imPresence
, getIMPresence
, withIMPresence
-- * Roster
, Roster(..)
, Item(..)
, getRoster
, rosterAdd
, rosterRemove
) where ) where
import Network.Xmpp.IM.Message import Network.Xmpp.IM.Message
import Network.Xmpp.IM.Presence import Network.Xmpp.IM.Presence
import Network.Xmpp.IM.Roster
import Network.Xmpp.IM.Roster.Types

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

@ -1,119 +1,64 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.IM.Message module Network.Xmpp.IM.Message where
where
import Control.Applicative ((<$>))
import Data.Maybe (maybeToList, listToMaybe)
import Data.Text (Text) import Data.Text (Text)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Marshal import Network.Xmpp.Marshal
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Stanza
import Data.List
import Data.Function
data MessageBody = MessageBody { bodyLang :: (Maybe LangTag)
data MessageBody = MessageBody { bodyLang :: Maybe LangTag
, bodyContent :: Text , bodyContent :: Text
} }
data MessageThread = MessageThread { theadID :: Text data MessageThread = MessageThread { theadID :: Text
, threadParent :: (Maybe Text) , threadParent :: Maybe Text
} }
data MessageSubject = MessageSubject { subjectLang :: (Maybe LangTag) data MessageSubject = MessageSubject { subjectLang :: Maybe LangTag
, subjectContent :: Text , subjectContent :: Text
} }
xpMessageSubject :: PU [Element] MessageSubject -- | The instant message (IM) specific part of a message.
xpMessageSubject = xpUnliftElems . data InstantMessage = InstantMessage { imThread :: Maybe MessageThread
xpWrap (\(l, s) -> MessageSubject l s) , imSubject :: [MessageSubject]
(\(MessageSubject l s) -> (l,s)) , imBody :: [MessageBody]
$ xpElem "{jabber:client}subject" xpLangTag $ xpContent xpId }
xpMessageBody :: PU [Element] MessageBody instantMessage :: InstantMessage
xpMessageBody = xpUnliftElems . instantMessage = InstantMessage { imThread = Nothing
xpWrap (\(l, s) -> MessageBody l s) , imSubject = []
(\(MessageBody l s) -> (l,s)) , imBody = []
$ xpElem "{jabber:client}body" xpLangTag $ xpContent xpId }
xpMessageThread :: PU [Element] MessageThread -- | Get the IM specific parts of a message. Returns 'Nothing' when the received
xpMessageThread = xpUnliftElems -- payload is not valid IM data.
. xpWrap (\(t, p) -> MessageThread p t) getIM :: Message -> Maybe InstantMessage
(\(MessageThread p t) -> (t,p)) getIM im = either (const Nothing) Just . unpickle xpIM $ messagePayload im
$ xpElem "{jabber:client}thread"
(xpAttrImplied "parent" xpId)
(xpContent xpId)
-- | Get the subject elements of a message (if any). Messages may sanitizeIM :: InstantMessage -> InstantMessage
-- contain multiple subjects if each of them has a distinct xml:lang sanitizeIM im = im{imBody = nubBy ((==) `on` bodyLang) $ imBody im}
-- attribute
subject :: Message -> [MessageSubject] -- | Append IM data to a message
subject m = ms withIM :: Message -> InstantMessage -> Message
where withIM m im = m{ messagePayload = messagePayload m
-- xpFindMatches will _always_ return Right ++ pickleTree xpIM (sanitizeIM im) }
Right ms = unpickle (xpFindMatches xpMessageSubject) $ messagePayload m
imToElements :: InstantMessage -> [Element]
-- | Get the thread elements of a message (if any). The thread of a imToElements im = pickle xpIM (sanitizeIM im)
-- message is considered opaque, that is, no meaning, other than it's
-- literal identity, may be derived from it and it is not human
-- readable
thread :: Message -> Maybe MessageThread
thread m = ms
where
-- xpFindMatches will _always_ return Right
Right ms = unpickle (xpOption xpMessageThread) $ messagePayload m
-- | Get the body elements of a message (if any). Messages may contain
-- multiple bodies if each of them has a distinct xml:lang attribute
bodies :: Message -> [MessageBody]
bodies m = ms
where
-- xpFindMatches will _always_ return Right
Right ms = unpickle (xpFindMatches xpMessageBody) $ messagePayload m
-- | Return the first body element, regardless of it's language.
body :: Message -> Maybe Text
body m = bodyContent <$> listToMaybe (bodies m)
-- | Generate a new instant message
newIM
:: Jid
-> Maybe StanzaID
-> Maybe LangTag
-> MessageType
-> Maybe MessageSubject
-> Maybe MessageThread
-> Maybe MessageBody
-> [Element]
-> Message
newIM t i lang tp sbj thrd bdy payload = Message
{ messageID = i
, messageFrom = Nothing
, messageTo = Just t
, messageLangTag = lang
, messageType = tp
, messagePayload = concat $
maybeToList (pickle xpMessageSubject <$> sbj)
++ maybeToList (pickle xpMessageThread <$> thrd)
++ maybeToList (pickle xpMessageBody <$> bdy)
++ [payload]
}
-- | Generate a simple message -- | Generate a simple message
simpleIM :: Jid -- ^ recipient simpleIM :: Jid -- ^ recipient
-> Text -- ^ body -> Text -- ^ body
-> Message -> Message
simpleIM t bd = newIM simpleIM to bd = withIM message{messageTo = Just to}
t instantMessage{imBody = [MessageBody Nothing bd]}
Nothing
Nothing
Normal
Nothing
Nothing
(Just $ MessageBody Nothing bd)
[]
-- | Generate an answer from a received message. The recepient is -- | Generate an answer from a received message. The recepient is
-- taken from the original sender, the sender is set to Nothing, -- taken from the original sender, the sender is set to Nothing,
@ -121,17 +66,48 @@ simpleIM t bd = newIM
-- thread are inherited, the remaining payload is replaced by the -- thread are inherited, the remaining payload is replaced by the
-- given one. -- given one.
-- --
-- If multiple message bodies are given they must have different language tags -- If multiple message bodies are given they MUST have different language tags
answerIM :: [MessageBody] -> [Element] -> Message -> Message answerIM :: [MessageBody] -> Message -> Maybe Message
answerIM bd payload msg = Message answerIM bd msg = case getIM msg of
{ messageID = messageID msg Nothing -> Nothing
Just im -> Just $ flip withIM (im{imBody = bd}) $
message { messageID = messageID msg
, messageFrom = Nothing , messageFrom = Nothing
, messageTo = messageFrom msg , messageTo = messageFrom msg
, messageLangTag = messageLangTag msg , messageLangTag = messageLangTag msg
, messageType = messageType msg , messageType = messageType msg
, messagePayload = concat $
(pickle xpMessageSubject <$> subject msg)
++ maybeToList (pickle xpMessageThread <$> thread msg)
++ (pickle xpMessageBody <$> bd)
++ [payload]
} }
--------------------------
-- Picklers --------------
--------------------------
xpIM :: PU [Element] InstantMessage
xpIM = xpWrap (\(t, s, b) -> InstantMessage t s b)
(\(InstantMessage t s b) -> (t, s, b))
. xpClean
$ xp3Tuple
xpMessageThread
xpMessageSubject
xpMessageBody
xpMessageSubject :: PU [Element] [MessageSubject]
xpMessageSubject = xpUnliftElems .
xpWrap (map $ \(l, s) -> MessageSubject l s)
(map $ \(MessageSubject l s) -> (l,s))
$ xpElems "{jabber:client}subject" xpLangTag $ xpContent xpId
xpMessageBody :: PU [Element] [MessageBody]
xpMessageBody = xpUnliftElems .
xpWrap (map $ \(l, s) -> MessageBody l s)
(map $ \(MessageBody l s) -> (l,s))
$ xpElems "{jabber:client}body" xpLangTag $ xpContent xpId
xpMessageThread :: PU [Element] (Maybe MessageThread)
xpMessageThread = xpUnliftElems
. xpOption
. xpWrap (\(t, p) -> MessageThread p t)
(\(MessageThread p t) -> (t,p))
$ xpElem "{jabber:client}thread"
(xpAttrImplied "parent" xpId)
(xpContent xpId)

109
source/Network/Xmpp/IM/Presence.hs

@ -1,76 +1,67 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module Network.Xmpp.IM.Presence where module Network.Xmpp.IM.Presence where
import Data.Text(Text) import Data.Text (Text)
import Data.XML.Pickle
import Data.XML.Types
import Network.Xmpp.Types import Network.Xmpp.Types
-- | An empty presence. data ShowStatus = StatusAway
presence :: Presence | StatusChat
presence = Presence { presenceID = Nothing | StatusDnd
, presenceFrom = Nothing | StatusXa
, presenceTo = Nothing
, presenceLangTag = Nothing
, presenceType = Nothing
, presencePayload = []
}
-- | Request subscription with an entity. instance Show ShowStatus where
presenceSubscribe :: Jid -> Presence show StatusAway = "away"
presenceSubscribe to = presence { presenceTo = Just to show StatusChat = "chat"
, presenceType = Just Subscribe show StatusDnd = "dnd"
} show StatusXa = "xa"
-- | Is presence a subscription request? instance Read ShowStatus where
isPresenceSubscribe :: Presence -> Bool readsPrec _ "away" = [(StatusAway, "")]
isPresenceSubscribe pres = presenceType pres == (Just Subscribe) readsPrec _ "chat" = [(StatusChat, "")]
readsPrec _ "dnd" = [(StatusDnd , "")]
readsPrec _ "xa" = [(StatusXa , "")]
readsPrec _ _ = []
-- | Approve a subscripton of an entity. data IMPresence = IMP { showStatus :: Maybe ShowStatus
presenceSubscribed :: Jid -> Presence , status :: Maybe Text
presenceSubscribed to = presence { presenceTo = Just to , priority :: Maybe Int
, presenceType = Just Subscribed
} }
-- | Is presence a subscription approval? imPresence :: IMPresence
isPresenceSubscribed :: Presence -> Bool imPresence = IMP { showStatus = Nothing
isPresenceSubscribed pres = presenceType pres == (Just Subscribed) , status = Nothing
, priority = Nothing
-- | End a subscription with an entity.
presenceUnsubscribe :: Jid -> Presence
presenceUnsubscribe to = presence { presenceTo = Just to
, presenceType = Just Unsubscribed
} }
-- | Is presence an unsubscription request?
isPresenceUnsubscribe :: Presence -> Bool
isPresenceUnsubscribe pres = presenceType pres == (Just Unsubscribe)
-- | Signal to the server that the client is available for communication.
presenceOnline :: Presence
presenceOnline = presence
-- | Signal to the server that the client is no longer available for getIMPresence :: Presence -> Maybe IMPresence
-- communication. getIMPresence pres = case unpickle xpIMPresence (presencePayload pres) of
presenceOffline :: Presence Left _ -> Nothing
presenceOffline = presence {presenceType = Just Unavailable} Right r -> Just r
---- Change your status withIMPresence :: IMPresence -> Presence -> Presence
--status withIMPresence imPres pres = pres{presencePayload = presencePayload pres
-- :: Maybe Text -- ^ Status message ++ pickleTree xpIMPresence
-- -> Maybe ShowType -- ^ Status Type imPres}
-- -> Maybe Int -- ^ Priority
-- -> Presence
--status txt showType prio = presence { presenceShowType = showType
-- , presencePriority = prio
-- , presenceStatus = txt
-- }
-- | Set the current availability status. This implicitly sets the client's --
-- status online. -- Picklers
--presenceAvail :: ShowType -> Presence --
--presenceAvail showType = status Nothing (Just showType) Nothing
-- | Set the current status message. This implicitly sets the client's status xpIMPresence :: PU [Element] IMPresence
-- online. xpIMPresence = xpUnliftElems .
--presenceMessage :: Text -> Presence xpWrap (\(s, st, p) -> IMP s st p)
--presenceMessage txt = status (Just txt) Nothing Nothing (\(IMP s st p) -> (s, st, p)) .
xpClean $
xp3Tuple
(xpOption $ xpElemNodes "{jabber:client}show"
(xpContent xpPrim))
(xpOption $ xpElemNodes "{jabber:client}status"
(xpContent xpText))
(xpOption $ xpElemNodes "{jabber:client}priority"
(xpContent xpPrim))

158
source/Network/Xmpp/IM/Roster.hs

@ -1,84 +1,81 @@
{-# LANGUAGE PatternGuards #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.IM.Roster module Network.Xmpp.IM.Roster where
where
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad import Control.Monad
import Data.List (nub)
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust, fromMaybe)
import Data.Text (Text) import Data.Text (Text)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp
import Network.Xmpp.Marshal
import System.Log.Logger import System.Log.Logger
import qualified Data.Map.Strict as Map
import Network.Xmpp.IM.Roster.Types
import Network.Xmpp.Marshal
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Concurrent.IQ
data Subscription = None | To | From | Both | Remove deriving Eq
-- | Push a roster item to the server. The values for approved and ask are
instance Show Subscription where -- ignored and all values for subsciption except "remove" are ignored
show None = "none" rosterPush :: Item -> Session -> IO IQResponse
show To = "to" rosterPush item session = do
show From = "from" let el = pickleElem xpQuery (Query Nothing [fromItem item])
show Both = "both" sendIQ' Nothing Set Nothing el session
show Remove = "remove"
-- | Add or update an item to the roster.
instance Read Subscription where --
readsPrec _ "none" = [(None ,"")] -- To update the item just send the complete set of new data
readsPrec _ "to" = [(To ,"")] rosterAdd :: Jid -- ^ JID of the item
readsPrec _ "from" = [(From ,"")] -> Maybe Text -- ^ Name alias
readsPrec _ "both" = [(Both ,"")] -> [Text] -- ^ Groups (duplicates will be removed)
readsPrec _ "remove" = [(Remove ,"")] -> Session
readsPrec _ _ = [] -> IO IQResponse
rosterAdd j n gs session = do
data Roster = Roster { ver :: Maybe Text let el = pickleElem xpQuery (Query Nothing
, items :: Map.Map Jid Item } [QueryItem { qiApproved = Nothing
, qiAsk = False
, qiJid = j
data Item = Item { approved :: Bool , qiName = n
, ask :: Bool , qiSubscription = Nothing
, jid :: Jid , qiGroups = nub gs
, name :: Maybe Text }])
, subscription :: Subscription sendIQ' Nothing Set Nothing el session
, groups :: [Text]
} deriving Show -- | Remove an item from the roster. Return True when the item is sucessfully
-- removed or if it wasn't in the roster to begin with.
data QueryItem = QueryItem { qiApproved :: Maybe Bool rosterRemove :: Jid -> Session -> IO Bool
, qiAsk :: Bool rosterRemove j sess = do
, qiJid :: Jid roster <- getRoster sess
, qiName :: Maybe Text case Map.lookup j (items roster) of
, qiSubscription :: Maybe Subscription Nothing -> return True -- jid is not on the Roster
, qiGroups :: [Text] Just _ -> do
} deriving Show res <- rosterPush (Item False False j Nothing Remove []) sess
case res of
data Query = Query { queryVer :: Maybe Text IQResponseResult IQResult{} -> return True
, queryItems :: [QueryItem] _ -> return False
} deriving Show
-- | Retrieve the current Roster state
getRoster :: Session -> IO Roster
withRoster :: Maybe Roster getRoster session = atomically $ readTVar (rosterRef session)
-> SessionConfiguration
-> (SessionConfiguration -> IO (Either XmppFailure Session)) -- | Get the initial roster / refresh the roster. You don't need to call this on your own
-> IO (Either XmppFailure (TVar Roster, Session)) initRoster :: Session -> IO ()
withRoster oldRoster conf startSession = do initRoster session = do
rosterRef <- newTVarIO $ Roster Nothing Map.empty oldRoster <- getRoster session
mbSess <- startSession conf{extraStanzaHandlers = handleRoster rosterRef : mbRoster <- retrieveRoster (if isJust (ver oldRoster) then Just oldRoster
extraStanzaHandlers conf} else Nothing ) session
case mbSess of
Left e -> return $ Left e
Right sess -> do
mbRoster <- getRoster oldRoster sess
case mbRoster of case mbRoster of
Nothing -> errorM "Pontarius.Xmpp" "Server did not return a roster" Nothing -> errorM "Pontarius.Xmpp"
Just roster -> atomically $ writeTVar rosterRef roster "Server did not return a roster"
return $ Right (rosterRef, sess) Just roster -> atomically $ writeTVar (rosterRef session) roster
handleRoster :: TVar Roster -> TChan Stanza -> Stanza -> IO Bool handleRoster :: TVar Roster -> TChan Stanza -> Stanza -> IO Bool
handleRoster rosterRef outC sta = do handleRoster ref outC sta = case sta of
case sta of
IQRequestS (iqr@IQRequest{iqRequestPayload = IQRequestS (iqr@IQRequest{iqRequestPayload =
iqb@Element{elementName = en}}) iqb@Element{elementName = en}})
| nameNamespace en == Just "jabber:iq:roster" -> do | nameNamespace en == Just "jabber:iq:roster" -> do
@ -98,7 +95,7 @@ handleRoster rosterRef outC sta = do
return False return False
_ -> return True _ -> return True
where where
handleUpdate v' update = atomically $ modifyTVar rosterRef $ \(Roster v is) -> handleUpdate v' update = atomically $ modifyTVar ref $ \(Roster v is) ->
Roster (v' `mplus` v) $ case qiSubscription update of Roster (v' `mplus` v) $ case qiSubscription update of
Just Remove -> Map.delete (qiJid update) is Just Remove -> Map.delete (qiJid update) is
_ -> Map.insert (qiJid update) (toItem update) is _ -> Map.insert (qiJid update) (toItem update) is
@ -109,8 +106,8 @@ handleRoster rosterRef outC sta = do
result (IQRequest iqid from _to lang _tp _bd) = result (IQRequest iqid from _to lang _tp _bd) =
IQResultS $ IQResult iqid Nothing from lang Nothing IQResultS $ IQResult iqid Nothing from lang Nothing
getRoster :: Maybe Roster -> Session -> IO (Maybe Roster) retrieveRoster :: Maybe Roster -> Session -> IO (Maybe Roster)
getRoster oldRoster sess = do retrieveRoster oldRoster sess = do
res <- sendIQ' Nothing Get Nothing res <- sendIQ' Nothing Get Nothing
(pickleElem xpQuery (Query (ver =<< oldRoster) [])) (pickleElem xpQuery (Query (ver =<< oldRoster) []))
sess sess
@ -120,9 +117,9 @@ getRoster oldRoster sess = do
Left _e -> do Left _e -> do
errorM "Pontarius.Xmpp.Roster" "getRoster: invalid query element" errorM "Pontarius.Xmpp.Roster" "getRoster: invalid query element"
return Nothing return Nothing
Right roster -> return . Just $ toRoster roster Right ros' -> return . Just $ toRoster ros'
IQResponseResult (IQResult{iqResultPayload = Nothing}) -> do IQResponseResult (IQResult{iqResultPayload = Nothing}) -> do
return $ oldRoster return oldRoster
-- sever indicated that no roster updates are necessary -- sever indicated that no roster updates are necessary
IQResponseTimeout -> do IQResponseTimeout -> do
errorM "Pontarius.Xmpp.Roster" "getRoster: request timed out" errorM "Pontarius.Xmpp.Roster" "getRoster: request timed out"
@ -137,12 +134,23 @@ getRoster oldRoster sess = do
is) is)
toItem :: QueryItem -> Item toItem :: QueryItem -> Item
toItem qi = Item { approved = maybe False id (qiApproved qi) toItem qi = Item { approved = fromMaybe False (qiApproved qi)
, ask = qiAsk qi , ask = qiAsk qi
, jid = qiJid qi , jid = qiJid qi
, name = qiName qi , name = qiName qi
, subscription = maybe None id (qiSubscription qi) , subscription = fromMaybe None (qiSubscription qi)
, groups = qiGroups qi , groups = nub $ qiGroups qi
}
fromItem :: Item -> QueryItem
fromItem i = QueryItem { qiApproved = Nothing
, qiAsk = False
, qiJid = jid i
, qiName = name i
, qiSubscription = case subscription i of
Remove -> Just Remove
_ -> Nothing
, qiGroups = nub $ groups i
} }
xpItems :: PU [Node] [QueryItem] xpItems :: PU [Node] [QueryItem]
@ -153,7 +161,7 @@ xpItems = xpWrap (map (\((app_, ask_, jid_, name_, sub_), groups_) ->
xpElems "{jabber:iq:roster}item" xpElems "{jabber:iq:roster}item"
(xp5Tuple (xp5Tuple
(xpAttribute' "approved" xpBool) (xpAttribute' "approved" xpBool)
(xpWrap (maybe False (const True)) (xpWrap isJust
(\p -> if p then Just () else Nothing) $ (\p -> if p then Just () else Nothing) $
xpOption $ xpAttribute_ "ask" "subscribe") xpOption $ xpAttribute_ "ask" "subscribe")
(xpAttribute "jid" xpPrim) (xpAttribute "jid" xpPrim)

47
source/Network/Xmpp/IM/Roster/Types.hs

@ -0,0 +1,47 @@
module Network.Xmpp.IM.Roster.Types where
import qualified Data.Map as Map
import Data.Text (Text)
import Network.Xmpp.Types
data Subscription = None | To | From | Both | Remove deriving Eq
instance Show Subscription where
show None = "none"
show To = "to"
show From = "from"
show Both = "both"
show Remove = "remove"
instance Read Subscription where
readsPrec _ "none" = [(None ,"")]
readsPrec _ "to" = [(To ,"")]
readsPrec _ "from" = [(From ,"")]
readsPrec _ "both" = [(Both ,"")]
readsPrec _ "remove" = [(Remove ,"")]
readsPrec _ _ = []
data Roster = Roster { ver :: Maybe Text
, items :: Map.Map Jid Item } deriving Show
data Item = Item { approved :: Bool
, ask :: Bool
, jid :: Jid
, name :: Maybe Text
, subscription :: Subscription
, groups :: [Text]
} deriving Show
data QueryItem = QueryItem { qiApproved :: Maybe Bool
, qiAsk :: Bool
, qiJid :: Jid
, qiName :: Maybe Text
, qiSubscription :: Maybe Subscription
, qiGroups :: [Text]
} deriving Show
data Query = Query { queryVer :: Maybe Text
, queryItems :: [QueryItem]
} deriving Show

5
source/Network/Xmpp/Internal.hs

@ -29,7 +29,7 @@ module Network.Xmpp.Internal
, pushStanza , pushStanza
, pullStanza , pullStanza
, pushIQ , pushIQ
, SaslHandler(..) , SaslHandler
, StanzaID(..) , StanzaID(..)
) )
@ -37,9 +37,6 @@ module Network.Xmpp.Internal
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Sasl import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Tls import Network.Xmpp.Tls
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Stream
import Network.Xmpp.Marshal

84
source/Network/Xmpp/Sasl.hs

@ -1,6 +1,6 @@
{-# OPTIONS_HADDOCK hide #-} {-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} {-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
--
-- Submodule for functionality related to SASL negotation: -- Submodule for functionality related to SASL negotation:
-- authentication functions, SASL functionality, bind functionality, -- authentication functions, SASL functionality, bind functionality,
-- and the legacy `{urn:ietf:params:xml:ns:xmpp-session}session' -- and the legacy `{urn:ietf:params:xml:ns:xmpp-session}session'
@ -14,51 +14,17 @@ module Network.Xmpp.Sasl
, auth , auth
) where ) where
import Control.Applicative
import Control.Arrow (left)
import Control.Monad
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Data.Maybe (fromJust, isJust)
import qualified Crypto.Classes as CC
import qualified Data.Binary as Binary
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL
import qualified Data.Digest.Pure.MD5 as MD5
import qualified Data.List as L
import Data.Word (Word8)
import qualified Data.Text as Text
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Encoding as Text
import Network.Xmpp.Stream
import Network.Xmpp.Types
import System.Log.Logger (debugM, errorM)
import qualified System.Random as Random
import Network.Xmpp.Sasl.Types
import Network.Xmpp.Sasl.Mechanisms
import Control.Concurrent.STM.TMVar
import Control.Exception
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Types
import Network.Xmpp.Marshal import Network.Xmpp.Marshal
import Network.Xmpp.Sasl.Mechanisms
import Control.Monad.State(modify) import Network.Xmpp.Sasl.Types
import Network.Xmpp.Stream
import Control.Concurrent.STM.TMVar import Network.Xmpp.Types
import System.Log.Logger (debugM, errorM, infoM)
import Control.Monad.Error
-- | Uses the first supported mechanism to authenticate, if any. Updates the -- | Uses the first supported mechanism to authenticate, if any. Updates the
-- state with non-password credentials and restarts the stream upon -- state with non-password credentials and restarts the stream upon
@ -105,16 +71,18 @@ auth :: [SaslHandler]
-> Stream -> Stream
-> IO (Either XmppFailure (Maybe AuthFailure)) -> IO (Either XmppFailure (Maybe AuthFailure))
auth mechanisms resource con = runErrorT $ do auth mechanisms resource con = runErrorT $ do
ErrorT $ xmppSasl mechanisms con mbAuthFail <- ErrorT $ xmppSasl mechanisms con
jid <- ErrorT $ xmppBind resource con case mbAuthFail of
ErrorT $ flip withStream con $ do Nothing -> do
_jid <- ErrorT $ xmppBind resource con
ErrorT $ flip withStream' con $ do
s <- get s <- get
case establishSession $ streamConfiguration s of case establishSession $ streamConfiguration s of
False -> return $ Right Nothing False -> return $ Right Nothing
True -> do True -> do
_ <- lift $ startSession con _ <-liftIO $ startSession con
return $ Right Nothing return $ Right Nothing
return Nothing f -> return f
-- Produces a `bind' element, optionally wrapping a resource. -- Produces a `bind' element, optionally wrapping a resource.
bindBody :: Maybe Text -> Element bindBody :: Maybe Text -> Element
@ -133,20 +101,21 @@ xmppBind rsrc c = runErrorT $ do
answer <- ErrorT $ pushIQ "bind" Nothing Set Nothing (bindBody rsrc) c answer <- ErrorT $ pushIQ "bind" Nothing Set Nothing (bindBody rsrc) c
case answer of case answer of
Right IQResult{iqResultPayload = Just b} -> do Right IQResult{iqResultPayload = Just b} -> do
lift $ debugM "Pontarius.XMPP" "xmppBind: IQ result received; unpickling JID..." lift $ debugM "Pontarius.Xmpp" "xmppBind: IQ result received; unpickling JID..."
let jid = unpickleElem xpJid b let jid = unpickleElem xpJid b
case jid of case jid of
Right jid' -> do Right jid' -> do
lift $ debugM "Pontarius.XMPP" $ "xmppBind: JID unpickled: " ++ show jid' lift $ infoM "Pontarius.Xmpp" $ "Bound JID: " ++ show jid'
ErrorT $ withStream (do _ <- lift $ withStream ( do modify $ \s ->
modify $ \s -> s{streamJid = Just jid'} s{streamJid = Just jid'})
return $ Right jid') c -- not pretty c
return jid' return jid'
otherwise -> do _ -> do
lift $ errorM "Pontarius.XMPP" $ "xmppBind: JID could not be unpickled from: " lift $ errorM "Pontarius.Xmpp"
$ "xmppBind: JID could not be unpickled from: "
++ show b ++ show b
throwError $ XmppOtherFailure throwError $ XmppOtherFailure
otherwise -> do _ -> do
lift $ errorM "Pontarius.XMPP" "xmppBind: IQ error received." lift $ errorM "Pontarius.XMPP" "xmppBind: IQ error received."
throwError XmppOtherFailure throwError XmppOtherFailure
where where
@ -164,15 +133,6 @@ sessionXml = pickleElem
(xpElemBlank "{urn:ietf:params:xml:ns:xmpp-session}session") (xpElemBlank "{urn:ietf:params:xml:ns:xmpp-session}session")
() ()
sessionIQ :: Stanza
sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess"
, iqRequestFrom = Nothing
, iqRequestTo = Nothing
, iqRequestLangTag = Nothing
, iqRequestType = Set
, iqRequestPayload = sessionXml
}
-- Sends the session IQ set element and waits for an answer. Throws an error if -- 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 :: Stream -> IO Bool startSession :: Stream -> IO Bool

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

@ -4,28 +4,23 @@
module Network.Xmpp.Sasl.Common where module Network.Xmpp.Sasl.Common where
import Network.Xmpp.Types
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.State.Class
import qualified Data.Attoparsec.ByteString.Char8 as AP import qualified Data.Attoparsec.ByteString.Char8 as AP
import Data.Bits import Data.Bits
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import Data.Maybe (fromMaybe)
import Data.Maybe (maybeToList) import Data.Maybe (maybeToList)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Data.Word (Word8) import Data.Word (Word8)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.Xmpp.Marshal
import Network.Xmpp.Stream
import Network.Xmpp.Sasl.StringPrep import Network.Xmpp.Sasl.StringPrep
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Marshal import Network.Xmpp.Stream
import Network.Xmpp.Types
import qualified System.Random as Random import qualified System.Random as Random
@ -66,9 +61,9 @@ pairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do
AP.skipSpace AP.skipSpace
name <- AP.takeWhile1 (/= '=') name <- AP.takeWhile1 (/= '=')
_ <- AP.char '=' _ <- AP.char '='
quote <- ((AP.char '"' >> return True) `mplus` return False) qt <- ((AP.char '"' >> return True) `mplus` return False)
content <- AP.takeWhile1 (AP.notInClass [',', '"']) content <- AP.takeWhile1 (AP.notInClass [',', '"'])
when quote . void $ AP.char '"' when qt . void $ AP.char '"'
return (name, content) return (name, content)
-- Failure element pickler. -- Failure element pickler.
@ -108,19 +103,20 @@ xpSaslElement = xpAlt saslSel
quote :: BS.ByteString -> BS.ByteString quote :: BS.ByteString -> BS.ByteString
quote x = BS.concat ["\"",x,"\""] quote x = BS.concat ["\"",x,"\""]
saslInit :: Text.Text -> Maybe BS.ByteString -> ErrorT AuthFailure (StateT StreamState IO) Bool saslInit :: Text.Text -> Maybe BS.ByteString -> ErrorT AuthFailure (StateT StreamState IO) ()
saslInit mechanism payload = do saslInit mechanism payload = do
r <- lift . pushElement . saslInitE mechanism $ r <- lift . pushElement . saslInitE mechanism $
Text.decodeUtf8 . B64.encode <$> payload Text.decodeUtf8 . B64.encode <$> payload
case r of case r of
Right True -> return ()
Right False -> throwError $ AuthStreamFailure XmppNoStream
Left e -> throwError $ AuthStreamFailure e Left e -> throwError $ AuthStreamFailure e
Right b -> return b
-- | Pull the next element. -- | Pull the next element.
pullSaslElement :: ErrorT AuthFailure (StateT StreamState IO) SaslElement pullSaslElement :: ErrorT AuthFailure (StateT StreamState IO) SaslElement
pullSaslElement = do pullSaslElement = do
r <- lift $ pullUnpickle (xpEither xpFailure xpSaslElement) mbse <- lift $ pullUnpickle (xpEither xpFailure xpSaslElement)
case r of case mbse of
Left e -> throwError $ AuthStreamFailure e Left e -> throwError $ AuthStreamFailure e
Right (Left e) -> throwError $ AuthSaslFailure e Right (Left e) -> throwError $ AuthSaslFailure e
Right (Right r) -> return r Right (Right r) -> return r
@ -173,13 +169,13 @@ toPairs ctext = case pairs ctext of
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 -> ErrorT AuthFailure (StateT StreamState IO) Bool respond :: Maybe BS.ByteString -> ErrorT AuthFailure (StateT StreamState IO) ()
respond m = do respond m = do
r <- lift . pushElement . saslResponseE . fmap (Text.decodeUtf8 . B64.encode) $ m r <- lift . pushElement . saslResponseE . fmap (Text.decodeUtf8 . B64.encode) $ m
case r of case r of
Left e -> throwError $ AuthStreamFailure e Left e -> throwError $ AuthStreamFailure e
Right b -> return b Right False -> throwError $ AuthStreamFailure XmppNoStream
Right True -> return ()
-- | Run the appropriate stringprep profiles on the credentials. -- | Run the appropriate stringprep profiles on the credentials.
-- May fail with 'AuthStringPrepFailure' -- May fail with 'AuthStringPrepFailure'

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

@ -5,37 +5,21 @@ module Network.Xmpp.Sasl.Mechanisms.DigestMd5
( digestMd5 ( digestMd5
) where ) where
import Control.Applicative
import Control.Arrow (left)
import Control.Monad
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Data.Maybe (fromJust, isJust)
import qualified Crypto.Classes as CC import qualified Crypto.Classes as CC
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.Digest.Pure.MD5 as MD5 import qualified Data.Digest.Pure.MD5 as MD5
import qualified Data.List as L import qualified Data.List as L
import 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 Data.XML.Pickle
import qualified Data.ByteString as BS
import Data.XML.Types
import Network.Xmpp.Stream
import Network.Xmpp.Types
import Network.Xmpp.Sasl.Common import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.StringPrep
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Types
@ -43,19 +27,19 @@ xmppDigestMd5 :: Text -- ^ Authentication identity (authzid or username)
-> Maybe Text -- ^ Authorization identity (authcid) -> Maybe Text -- ^ Authorization identity (authcid)
-> Text -- ^ Password (authzid) -> Text -- ^ Password (authzid)
-> ErrorT AuthFailure (StateT StreamState IO) () -> ErrorT AuthFailure (StateT StreamState IO) ()
xmppDigestMd5 authcid authzid password = do xmppDigestMd5 authcid' authzid' password' = do
(ac, az, pw) <- prepCredentials authcid authzid password (ac, az, pw) <- prepCredentials authcid' authzid' password'
Just address <- gets streamAddress Just address <- gets streamAddress
xmppDigestMd5' address ac az pw xmppDigestMd5' address ac az pw
where where
xmppDigestMd5' :: Text -> Text -> Maybe Text -> Text -> ErrorT AuthFailure (StateT StreamState IO) () xmppDigestMd5' :: Text -> Text -> Maybe Text -> Text -> ErrorT AuthFailure (StateT StreamState IO) ()
xmppDigestMd5' hostname authcid authzid password = do xmppDigestMd5' hostname authcid _authzid password = do -- TODO: use authzid?
-- Push element and receive the challenge. -- Push element and receive the challenge.
_ <- saslInit "DIGEST-MD5" Nothing -- TODO: Check boolean? _ <- saslInit "DIGEST-MD5" Nothing -- TODO: Check boolean?
pairs <- toPairs =<< saslFromJust =<< pullChallenge prs <- toPairs =<< saslFromJust =<< pullChallenge
cnonce <- liftIO $ makeNonce cnonce <- liftIO $ makeNonce
_b <- respond . Just $ createResponse hostname pairs cnonce _b <- respond . Just $ createResponse hostname prs cnonce
challenge2 <- pullFinalMessage _challenge2 <- pullFinalMessage
return () return ()
where where
-- Produce the response to the challenge. -- Produce the response to the challenge.
@ -63,19 +47,19 @@ xmppDigestMd5 authcid authzid password = do
-> Pairs -> Pairs
-> BS.ByteString -- nonce -> BS.ByteString -- nonce
-> BS.ByteString -> BS.ByteString
createResponse hostname pairs cnonce = let createResponse hname prs cnonce = let
Just qop = L.lookup "qop" pairs -- TODO: proper handling Just qop = L.lookup "qop" prs -- TODO: proper handling
Just nonce = L.lookup "nonce" pairs Just nonce = L.lookup "nonce" prs
uname_ = Text.encodeUtf8 authcid uname_ = Text.encodeUtf8 authcid
passwd_ = Text.encodeUtf8 password passwd_ = Text.encodeUtf8 password
-- Using Int instead of Word8 for random 1.0.0.0 (GHC 7) -- Using Int instead of Word8 for random 1.0.0.0 (GHC 7)
-- compatibility. -- compatibility.
nc = "00000001" nc = "00000001"
digestURI = "xmpp/" `BS.append` Text.encodeUtf8 hostname digestURI = "xmpp/" `BS.append` Text.encodeUtf8 hname
digest = md5Digest digest = md5Digest
uname_ uname_
(lookup "realm" pairs) (lookup "realm" prs)
passwd_ passwd_
digestURI digestURI
nc nc
@ -84,7 +68,7 @@ xmppDigestMd5 authcid authzid password = do
cnonce cnonce
response = BS.intercalate "," . map (BS.intercalate "=") $ response = BS.intercalate "," . map (BS.intercalate "=") $
[["username", quote uname_]] ++ [["username", quote uname_]] ++
case L.lookup "realm" pairs of case L.lookup "realm" prs of
Just realm -> [["realm" , quote realm ]] Just realm -> [["realm" , quote realm ]]
Nothing -> [] ++ Nothing -> [] ++
[ ["nonce" , quote nonce ] [ ["nonce" , quote nonce ]
@ -115,8 +99,8 @@ xmppDigestMd5 authcid authzid password = do
-> BS8.ByteString -> BS8.ByteString
-> BS8.ByteString -> BS8.ByteString
-> BS8.ByteString -> BS8.ByteString
md5Digest uname realm password digestURI nc qop nonce cnonce = md5Digest uname realm pwd digestURI nc qop nonce cnonce =
let ha1 = hash [ hashRaw [uname, maybe "" id realm, password] let ha1 = hash [ hashRaw [uname, maybe "" id realm, pwd]
, nonce , nonce
, cnonce , cnonce
] ]

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

@ -8,51 +8,22 @@ module Network.Xmpp.Sasl.Mechanisms.Plain
( plain ( plain
) where ) where
import Control.Applicative
import Control.Arrow (left)
import Control.Monad
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Data.Maybe (fromJust, isJust)
import qualified Crypto.Classes as CC
import qualified Data.Binary as Binary
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL
import qualified Data.Digest.Pure.MD5 as MD5
import qualified Data.List as L
import Data.Word (Word8)
import qualified Data.Text as Text
import Data.Text (Text)
import qualified Data.Text.Encoding as Text
import Data.XML.Pickle
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.XML.Types
import Network.Xmpp.Stream
import Network.Xmpp.Types
import qualified System.Random as Random
import Data.Maybe (fromMaybe)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Network.Xmpp.Sasl.Common import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Types
-- TODO: stringprep -- TODO: stringprep
xmppPlain :: Text.Text -- ^ Password xmppPlain :: Text.Text -- ^ Password
-> Maybe Text.Text -- ^ Authorization identity (authzid) -> Maybe Text.Text -- ^ Authorization identity (authzid)
-> Text.Text -- ^ Authentication identity (authcid) -> Text.Text -- ^ Authentication identity (authcid)
-> ErrorT AuthFailure (StateT StreamState IO) () -> ErrorT AuthFailure (StateT StreamState IO) ()
xmppPlain authcid authzid password = do xmppPlain authcid' authzid' password = do
(ac, az, pw) <- prepCredentials authcid authzid password (ac, az, pw) <- prepCredentials authcid' authzid' password
_ <- saslInit "PLAIN" ( Just $ plainMessage ac az pw) _ <- saslInit "PLAIN" ( Just $ plainMessage ac az pw)
_ <- pullSuccess _ <- pullSuccess
return () return ()
@ -63,15 +34,15 @@ xmppPlain authcid authzid password = do
-> Maybe Text.Text -- Authentication identity (authcid) -> Maybe Text.Text -- Authentication identity (authcid)
-> Text.Text -- Password -> Text.Text -- Password
-> BS.ByteString -- The PLAIN message -> BS.ByteString -- The PLAIN message
plainMessage authcid authzid passwd = BS.concat $ plainMessage authcid _authzid passwd = BS.concat $
[ authzid' [ authzid''
, "\NUL" , "\NUL"
, Text.encodeUtf8 $ authcid , Text.encodeUtf8 $ authcid
, "\NUL" , "\NUL"
, Text.encodeUtf8 $ passwd , Text.encodeUtf8 $ passwd
] ]
where where
authzid' = maybe "" Text.encodeUtf8 authzid authzid'' = maybe "" Text.encodeUtf8 authzid'
plain :: Text.Text -- ^ authentication ID (username) plain :: Text.Text -- ^ authentication ID (username)
-> Maybe Text.Text -- ^ authorization ID -> Maybe Text.Text -- ^ authorization ID

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

@ -8,32 +8,20 @@ module Network.Xmpp.Sasl.Mechanisms.Scram
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.Trans (liftIO) import Control.Monad.State.Strict
import qualified Crypto.Classes as Crypto import qualified Crypto.Classes as Crypto
import qualified Crypto.HMAC as Crypto import qualified Crypto.HMAC as Crypto
import qualified Crypto.Hash.SHA1 as Crypto import qualified Crypto.Hash.CryptoAPI as Crypto
import Data.Binary(Binary,encode)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 as BS8 (unpack) import Data.ByteString.Char8 as BS8 (unpack)
import qualified Data.ByteString.Lazy as LBS
import Data.List (foldl1', genericTake) import Data.List (foldl1', genericTake)
import qualified Data.Binary.Builder as Build
import Data.Maybe (maybeToList)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Data.Word(Word8)
import Network.Xmpp.Sasl.Common import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.StringPrep
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Types import Network.Xmpp.Types
import Control.Monad.State.Strict
-- | A nicer name for undefined, for use as a dummy token to determin -- | A nicer name for undefined, for use as a dummy token to determin
-- the hash function to use -- the hash function to use
hashToken :: (Crypto.Hash ctx hash) => hash hashToken :: (Crypto.Hash ctx hash) => hash
@ -50,18 +38,18 @@ scram :: (Crypto.Hash ctx hash)
-> Maybe Text.Text -- ^ Authorization ID -> Maybe Text.Text -- ^ Authorization ID
-> Text.Text -- ^ Password -> Text.Text -- ^ Password
-> ErrorT AuthFailure (StateT StreamState IO) () -> ErrorT AuthFailure (StateT StreamState IO) ()
scram hashToken authcid authzid password = do scram hToken authcid authzid password = do
(ac, az, pw) <- prepCredentials authcid authzid password (ac, az, pw) <- prepCredentials authcid authzid password
scramhelper hashToken ac az pw scramhelper ac az pw
where where
scramhelper hashToken authcid authzid' password = do scramhelper authcid' authzid' pwd = do
cnonce <- liftIO $ makeNonce cnonce <- liftIO $ makeNonce
saslInit "SCRAM-SHA-1" (Just $ cFirstMessage cnonce) _ <- saslInit "SCRAM-SHA-1" (Just $ cFirstMessage cnonce)
sFirstMessage <- saslFromJust =<< pullChallenge sFirstMessage <- saslFromJust =<< pullChallenge
pairs <- toPairs sFirstMessage prs <- toPairs sFirstMessage
(nonce, salt, ic) <- fromPairs pairs cnonce (nonce, salt, ic) <- fromPairs prs cnonce
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 AuthOtherFailure -- TODO: Log unless (lookup "v" finalPairs == Just v) $ throwError AuthOtherFailure -- TODO: Log
return () return ()
@ -71,27 +59,27 @@ scram hashToken authcid authzid password = do
encode _hashtoken = Crypto.encode encode _hashtoken = Crypto.encode
hash :: BS.ByteString -> BS.ByteString hash :: BS.ByteString -> BS.ByteString
hash str = encode hashToken $ Crypto.hash' str hash str = encode hToken $ Crypto.hash' str
hmac :: BS.ByteString -> BS.ByteString -> BS.ByteString hmac :: BS.ByteString -> BS.ByteString -> BS.ByteString
hmac key str = encode hashToken $ Crypto.hmac' (Crypto.MacKey key) str hmac key str = encode hToken $ Crypto.hmac' (Crypto.MacKey key) str
authzid :: Maybe BS.ByteString authzid'' :: Maybe BS.ByteString
authzid = (\z -> "a=" +++ Text.encodeUtf8 z) <$> authzid' authzid'' = (\z -> "a=" +++ Text.encodeUtf8 z) <$> authzid'
gs2CbindFlag :: BS.ByteString gs2CbindFlag :: BS.ByteString
gs2CbindFlag = "n" -- we don't support channel binding yet gs2CbindFlag = "n" -- we don't support channel binding yet
gs2Header :: BS.ByteString gs2Header :: BS.ByteString
gs2Header = merge $ [ gs2CbindFlag gs2Header = merge $ [ gs2CbindFlag
, maybe "" id authzid , maybe "" id authzid''
, "" , ""
] ]
cbindData :: BS.ByteString -- cbindData :: BS.ByteString
cbindData = "" -- we don't support channel binding yet -- cbindData = "" -- we don't support channel binding yet
cFirstMessageBare :: BS.ByteString -> BS.ByteString cFirstMessageBare :: BS.ByteString -> BS.ByteString
cFirstMessageBare cnonce = merge [ "n=" +++ Text.encodeUtf8 authcid cFirstMessageBare cnonce = merge [ "n=" +++ Text.encodeUtf8 authcid'
, "r=" +++ cnonce] , "r=" +++ cnonce]
cFirstMessage :: BS.ByteString -> BS.ByteString cFirstMessage :: BS.ByteString -> BS.ByteString
cFirstMessage cnonce = gs2Header +++ cFirstMessageBare cnonce cFirstMessage cnonce = gs2Header +++ cFirstMessageBare cnonce
@ -99,11 +87,11 @@ scram hashToken authcid authzid password = do
fromPairs :: Pairs fromPairs :: Pairs
-> BS.ByteString -> BS.ByteString
-> ErrorT AuthFailure (StateT StreamState IO) (BS.ByteString, BS.ByteString, Integer) -> ErrorT AuthFailure (StateT StreamState IO) (BS.ByteString, BS.ByteString, Integer)
fromPairs pairs cnonce | Just nonce <- lookup "r" pairs fromPairs prs cnonce | Just nonce <- lookup "r" prs
, cnonce `BS.isPrefixOf` nonce , cnonce `BS.isPrefixOf` nonce
, Just salt' <- lookup "s" pairs , Just salt' <- lookup "s" prs
, Right salt <- B64.decode salt' , Right salt <- B64.decode salt'
, Just ic <- lookup "i" pairs , Just ic <- lookup "i" prs
, [(i,"")] <- reads $ BS8.unpack ic , [(i,"")] <- reads $ BS8.unpack ic
= return (nonce, salt, i) = return (nonce, salt, i)
fromPairs _ _ = throwError $ AuthOtherFailure -- TODO: Log fromPairs _ _ = throwError $ AuthOtherFailure -- TODO: Log
@ -126,7 +114,7 @@ scram hashToken authcid authzid password = do
, "r=" +++ nonce] , "r=" +++ nonce]
saltedPassword :: BS.ByteString saltedPassword :: BS.ByteString
saltedPassword = hi (Text.encodeUtf8 password) salt ic saltedPassword = hi (Text.encodeUtf8 pwd) salt ic
clientKey :: BS.ByteString clientKey :: BS.ByteString
clientKey = hmac saltedPassword "Client Key" clientKey = hmac saltedPassword "Client Key"
@ -154,9 +142,9 @@ scram hashToken authcid authzid password = do
-- helper -- helper
hi :: BS.ByteString -> BS.ByteString -> Integer -> BS.ByteString hi :: BS.ByteString -> BS.ByteString -> Integer -> BS.ByteString
hi str salt ic = foldl1' xorBS (genericTake ic us) hi str slt ic' = foldl1' xorBS (genericTake ic' us)
where where
u1 = hmac str (salt +++ (BS.pack [0,0,0,1])) u1 = hmac str (slt +++ (BS.pack [0,0,0,1]))
us = iterate (hmac str) u1 us = iterate (hmac str) u1
scramSha1 :: Text.Text -- ^ username scramSha1 :: Text.Text -- ^ username

9
source/Network/Xmpp/Sasl/StringPrep.hs

@ -4,27 +4,34 @@ module Network.Xmpp.Sasl.StringPrep where
import Text.StringPrep import Text.StringPrep
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Text(singleton) import Data.Text(Text, singleton)
nonAsciiSpaces :: Set.Set Char
nonAsciiSpaces = Set.fromList [ '\x00A0', '\x1680', '\x2000', '\x2001', '\x2002' nonAsciiSpaces = Set.fromList [ '\x00A0', '\x1680', '\x2000', '\x2001', '\x2002'
, '\x2003', '\x2004', '\x2005', '\x2006', '\x2007' , '\x2003', '\x2004', '\x2005', '\x2006', '\x2007'
, '\x2008', '\x2009', '\x200A', '\x200B', '\x202F' , '\x2008', '\x2009', '\x200A', '\x200B', '\x202F'
, '\x205F', '\x3000' , '\x205F', '\x3000'
] ]
toSpace :: Char -> Text
toSpace x = if x `Set.member` nonAsciiSpaces then " " else singleton x toSpace x = if x `Set.member` nonAsciiSpaces then " " else singleton x
saslPrepQuery :: StringPrepProfile
saslPrepQuery = Profile saslPrepQuery = Profile
[b1, toSpace] [b1, toSpace]
True True
[c12, c21, c22, c3, c4, c5, c6, c7, c8, c9] [c12, c21, c22, c3, c4, c5, c6, c7, c8, c9]
True True
saslPrepStore :: StringPrepProfile
saslPrepStore = Profile saslPrepStore = Profile
[b1, toSpace] [b1, toSpace]
True True
[a1, c12, c21, c22, c3, c4, c5, c6, c7, c8, c9] [a1, c12, c21, c22, c3, c4, c5, c6, c7, c8, c9]
True True
normalizePassword :: Text -> Maybe Text
normalizePassword = runStringPrep saslPrepStore normalizePassword = runStringPrep saslPrepStore
normalizeUsername :: Text -> Maybe Text
normalizeUsername = runStringPrep saslPrepQuery normalizeUsername = runStringPrep saslPrepQuery

76
source/Network/Xmpp/Stanza.hs

@ -0,0 +1,76 @@
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_HADDOCK hide #-}
-- | Stanza related functions and constants
--
module Network.Xmpp.Stanza where
import Data.XML.Types
import Network.Xmpp.Types
-- | An empty message
message :: Message
message = Message { messageID = Nothing
, messageFrom = Nothing
, messageTo = Nothing
, messageLangTag = Nothing
, messageType = Normal
, messagePayload = []
}
-- | An empty presence.
presence :: Presence
presence = Presence { presenceID = Nothing
, presenceFrom = Nothing
, presenceTo = Nothing
, presenceLangTag = Nothing
, presenceType = Nothing
, presencePayload = []
}
-- | Request subscription with an entity.
presenceSubscribe :: Jid -> Presence
presenceSubscribe to = presence { presenceTo = Just to
, presenceType = Just Subscribe
}
-- | Approve a subscripton of an entity.
presenceSubscribed :: Jid -> Presence
presenceSubscribed to = presence { presenceTo = Just to
, presenceType = Just Subscribed
}
-- | End a subscription with an entity.
presenceUnsubscribe :: Jid -> Presence
presenceUnsubscribe to = presence { presenceTo = Just to
, presenceType = Just Unsubscribed
}
-- | Signal to the server that the client is available for communication.
presenceOnline :: Presence
presenceOnline = presence
-- | Signal to the server that the client is no longer available for
-- communication.
presenceOffline :: Presence
presenceOffline = presence {presenceType = Just Unavailable}
-- | Produce an answer message with the given payload, switching the "from" and
-- "to" attributes in the original message. Produces a 'Nothing' value of the
-- provided message message has no from attribute.
answerMessage :: Message -> [Element] -> Maybe Message
answerMessage Message{messageFrom = Just frm, ..} payload =
Just Message{ messageFrom = messageTo
, messageID = Nothing
, messageTo = Just frm
, messagePayload = payload
, ..
}
answerMessage _ _ = Nothing
-- | Add a recipient to a presence notification.
presTo :: Presence -> Jid -> Presence
presTo pres to = pres{presenceTo = Just to}

328
source/Network/Xmpp/Stream.hs

@ -7,54 +7,46 @@
module Network.Xmpp.Stream where module Network.Xmpp.Stream where
import Control.Applicative ((<$>), (<*>)) import Control.Applicative ((<$>))
import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM import Control.Concurrent.STM
import qualified Control.Exception as Ex import qualified Control.Exception as Ex
import Control.Exception.Base
import qualified Control.Exception.Lifted as ExL import qualified Control.Exception.Lifted as ExL
import Control.Monad import Control.Monad
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.IO.Class
import Control.Monad.Reader
import Control.Monad.State.Strict import Control.Monad.State.Strict
import Control.Monad.Trans.Class import Control.Monad.Trans.Resource as R
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.ByteString.Base64 import qualified Data.ByteString.Char8 as BSC8
import Data.ByteString.Char8 as BSC8
import Data.Conduit import Data.Conduit
import Data.Conduit.Binary as CB import Data.Conduit.Binary as CB
import qualified Data.Conduit.Internal as DCI import qualified Data.Conduit.Internal as DCI
import qualified Data.Conduit.List as CL import qualified Data.Conduit.List as CL
import Data.Maybe (fromJust, isJust, isNothing) import Data.IP
import Data.List
import Data.Maybe
import Data.Ord
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Void (Void) import Data.Void (Void)
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import qualified GHC.IO.Exception as GIE import qualified GHC.IO.Exception as GIE
import Network import Network
import Network.DNS hiding (encode, lookup)
import Network.Xmpp.Marshal import Network.Xmpp.Marshal
import Network.Xmpp.Types import Network.Xmpp.Types
import System.IO import System.IO
import System.IO.Error (tryIOError) import System.IO.Error (tryIOError)
import System.Log.Logger import System.Log.Logger
import System.Random (randomRIO)
import Text.XML.Stream.Parse as XP import Text.XML.Stream.Parse as XP
import Text.XML.Unresolved(InvalidEventStream(..)) import Text.XML.Unresolved(InvalidEventStream(..))
import Control.Monad.Trans.Resource as R
import Network.Xmpp.Utilities import Network.Xmpp.Utilities
import Network.DNS hiding (encode, lookup)
import Data.Ord
import Data.Maybe
import Data.List
import Data.IP
import System.Random
import qualified Network.Socket as NS
-- "readMaybe" definition, as readMaybe is not introduced in the `base' package -- "readMaybe" definition, as readMaybe is not introduced in the `base' package
-- until version 4.6. -- until version 4.6.
readMaybe_ :: (Read a) => String -> Maybe a readMaybe_ :: (Read a) => String -> Maybe a
@ -72,6 +64,17 @@ lmb :: [t] -> Maybe [t]
lmb [] = Nothing lmb [] = Nothing
lmb x = Just x lmb x = Just x
pushing :: MonadIO m =>
m (Either XmppFailure Bool)
-> ErrorT XmppFailure m ()
pushing m = do
res <- ErrorT m
case res of
True -> return ()
False -> do
liftIO $ debugM "Pontarius.Xmpp" "Failed to send data."
throwError XmppOtherFailure
-- Unpickles and returns a stream element. -- Unpickles and returns a stream element.
streamUnpickleElem :: PU [Node] a streamUnpickleElem :: PU [Node] a
-> Element -> Element
@ -85,7 +88,7 @@ streamUnpickleElem p x = do
-- 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 XmppFailure (Pipe Event Event Void () IO) a type StreamSink a = ErrorT XmppFailure (ConduitM 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 ()
@ -114,55 +117,64 @@ openElementFromEvents = do
startStream :: StateT StreamState IO (Either XmppFailure ()) startStream :: StateT StreamState IO (Either XmppFailure ())
startStream = runErrorT $ do startStream = runErrorT $ do
lift $ lift $ debugM "Pontarius.Xmpp" "Starting stream..." lift $ lift $ debugM "Pontarius.Xmpp" "Starting stream..."
state <- lift $ get st <- lift $ get
-- Set the `from' (which is also the expected to) attribute depending on the -- Set the `from' (which is also the expected to) attribute depending on the
-- state of the stream. -- state of the stream.
let expectedTo = case ( streamConnectionState state let expectedTo = case ( streamConnectionState st
, toJid $ streamConfiguration state) of , toJid $ streamConfiguration st) of
(Plain, (Just (jid, True))) -> Just jid (Plain , (Just (jid, True))) -> Just jid
(Secured, (Just (jid, _))) -> Just jid (Plain , _ ) -> Nothing
(Plain, Nothing) -> Nothing (Secured, (Just (jid, _ ))) -> Just jid
(Secured, Nothing) -> Nothing (Secured, Nothing ) -> Nothing
case streamAddress state of (Closed , _ ) -> Nothing
case streamAddress st of
Nothing -> do Nothing -> do
lift $ lift $ errorM "Pontarius.XMPP" "Server sent no hostname." lift $ lift $ errorM "Pontarius.XMPP" "Server sent no hostname."
throwError XmppOtherFailure throwError XmppOtherFailure
Just address -> lift $ do Just address -> do
pushXmlDecl pushing pushXmlDecl
pushOpenElement $ pushing . pushOpenElement . streamNSHack $
pickleElem xpStream ( "1.0" pickleElem xpStream ( "1.0"
, expectedTo , expectedTo
, Just (Jid Nothing address Nothing) , Just (Jid Nothing address Nothing)
, Nothing , Nothing
, preferredLang $ streamConfiguration state , preferredLang $ streamConfiguration st
) )
response <- ErrorT $ runEventsSink $ runErrorT $ streamS expectedTo response <- ErrorT $ runEventsSink $ runErrorT $ streamS expectedTo
case response of case response of
Left e -> throwError e Left e -> throwError e
-- Successful unpickling of stream element. -- Successful unpickling of stream element.
Right (Right (ver, from, to, id, lt, features)) Right (Right (ver, from, to, sid, lt, features))
| (Text.unpack ver) /= "1.0" -> | (Text.unpack ver) /= "1.0" ->
closeStreamWithError StreamUnsupportedVersion Nothing closeStreamWithError StreamUnsupportedVersion Nothing
"Unknown version" "Unknown version"
| lt == Nothing ->
closeStreamWithError StreamInvalidXml Nothing -- HACK: We ignore MUST-strength requirement (section 4.7.4. of RFC
"Stream has no language tag" -- 6120) for the sake of compatibility with jabber.org
-- If `from' is set, we verify that it's the correct one. TODO: Should we check against the realm instead? -- | lt == Nothing ->
| isJust from && (from /= Just (Jid Nothing (fromJust $ streamAddress state) Nothing)) -> -- closeStreamWithError StreamInvalidXml Nothing
-- "Stream has no language tag"
-- 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 $ streamAddress st) Nothing)) ->
closeStreamWithError StreamInvalidFrom Nothing closeStreamWithError StreamInvalidFrom Nothing
"Stream from is invalid" "Stream from is invalid"
| to /= expectedTo -> | to /= expectedTo ->
closeStreamWithError StreamUndefinedCondition (Just $ Element "invalid-to" [] []) closeStreamWithError StreamUndefinedCondition (Just $ Element "invalid-to" [] [])
"Stream to invalid"-- TODO: Suitable? "Stream to invalid"-- TODO: Suitable?
| otherwise -> do | otherwise -> do
-- HACK: (ignore section 4.7.4. of RFC 6120), see above
unless (isJust lt) $ liftIO $ warningM "Pontariusm.Xmpp"
"Stream has no language tag"
modify (\s -> s{ streamFeatures = features modify (\s -> s{ streamFeatures = features
, streamLang = lt , streamLang = lt
, streamId = id , streamId = sid
, streamFrom = from , streamFrom = from
} ) } )
return () return ()
-- Unpickling failed - we investigate the element. -- Unpickling failed - we investigate the element.
Right (Left (Element name attrs children)) Right (Left (Element name attrs _children))
| (nameLocalName name /= "stream") -> | (nameLocalName name /= "stream") ->
closeStreamWithError StreamInvalidXml Nothing closeStreamWithError StreamInvalidXml Nothing
"Root element is not stream" "Root element is not stream"
@ -174,15 +186,17 @@ startStream = runErrorT $ do
"Root name prefix set and not stream" "Root name prefix set and not stream"
| otherwise -> ErrorT $ checkchildren (flattenAttrs attrs) | otherwise -> ErrorT $ checkchildren (flattenAttrs attrs)
where where
-- closeStreamWithError :: MonadIO m => Stream -> StreamErrorCondition -> -- HACK: We include the default namespace to make isode's M-LINK server happy.
-- Maybe Element -> ErrorT XmppFailure m () streamNSHack e = e{elementAttributes = elementAttributes e
++ [( "xmlns"
, [ContentText "jabber:client"])]}
closeStreamWithError :: StreamErrorCondition -> Maybe Element -> String closeStreamWithError :: StreamErrorCondition -> Maybe Element -> String
-> ErrorT XmppFailure (StateT StreamState IO) () -> ErrorT XmppFailure (StateT StreamState IO) ()
closeStreamWithError sec el msg = do closeStreamWithError sec el msg = do
lift . pushElement . pickleElem xpStreamError void . lift . pushElement . pickleElem xpStreamError
$ StreamErrorInfo sec Nothing el $ StreamErrorInfo sec Nothing el
lift $ closeStreams' void . lift $ closeStreams'
lift $ lift $ errorM "Pontarius.XMPP" $ "closeStreamWithError: " ++ msg liftIO $ errorM "Pontarius.XMPP" $ "closeStreamWithError: " ++ msg
throwError XmppOtherFailure throwError XmppOtherFailure
checkchildren children = checkchildren children =
let to' = lookup "to" children let to' = lookup "to" children
@ -206,12 +220,12 @@ startStream = runErrorT $ do
"" ""
safeRead x = case reads $ Text.unpack x of safeRead x = case reads $ Text.unpack x of
[] -> Nothing [] -> Nothing
[(y,_),_] -> Just y ((y,_):_) -> Just y
flattenAttrs :: [(Name, [Content])] -> [(Name, Text.Text)] flattenAttrs :: [(Name, [Content])] -> [(Name, Text.Text)]
flattenAttrs attrs = Prelude.map (\(name, content) -> flattenAttrs attrs = Prelude.map (\(name, cont) ->
( name ( name
, Text.concat $ Prelude.map uncontentify content) , Text.concat $ Prelude.map uncontentify cont)
) )
attrs attrs
where where
@ -229,11 +243,15 @@ restartStream = do
modify (\s -> s{streamEventSource = newSource }) modify (\s -> s{streamEventSource = newSource })
startStream startStream
where where
loopRead read = do loopRead rd = do
bs <- liftIO (read 4096) bs <- liftIO (rd 4096)
if BS.null bs if BS.null bs
then return () then return ()
else yield bs >> loopRead read else do
liftIO $ debugM "Pontarius.Xmpp" $ "in: " ++
(Text.unpack . Text.decodeUtf8 $ bs)
yield bs
loopRead rd
-- Reads the (partial) stream:stream and the server features from the stream. -- Reads the (partial) stream:stream and the server features from the stream.
-- Returns the (unvalidated) stream attributes, the unparsed element, or -- Returns the (unvalidated) stream attributes, the unparsed element, or
@ -247,12 +265,12 @@ streamS :: Maybe Jid -> StreamSink (Either Element ( Text
, Maybe Text , Maybe Text
, Maybe LangTag , Maybe LangTag
, StreamFeatures )) , StreamFeatures ))
streamS expectedTo = do streamS _expectedTo = do -- TODO: check expectedTo
header <- xmppStreamHeader streamHeader <- xmppStreamHeader
case header of case streamHeader of
Right (version, from, to, id, langTag) -> do Right (version, from, to, sid, lTag) -> do
features <- xmppStreamFeatures features <- xmppStreamFeatures
return $ Right (version, from, to, id, langTag, features) return $ Right (version, from, to, sid, lTag, features)
Left el -> return $ Left el Left el -> return $ Left el
where where
xmppStreamHeader :: StreamSink (Either Element (Text, Maybe Jid, Maybe Jid, Maybe Text.Text, Maybe LangTag)) xmppStreamHeader :: StreamSink (Either Element (Text, Maybe Jid, Maybe Jid, Maybe Text.Text, Maybe LangTag))
@ -280,7 +298,7 @@ openStream :: HostName -> StreamConfiguration -> IO (Either XmppFailure (Stream)
openStream realm config = runErrorT $ do openStream realm config = runErrorT $ do
lift $ debugM "Pontarius.XMPP" "Opening stream..." lift $ debugM "Pontarius.XMPP" "Opening stream..."
stream' <- createStream realm config stream' <- createStream realm config
result <- liftIO $ withStream startStream stream' ErrorT . liftIO $ withStream startStream stream'
return stream' return stream'
-- | Send "</stream:stream>" and wait for the server to finish processing and to -- | Send "</stream:stream>" and wait for the server to finish processing and to
@ -289,14 +307,15 @@ openStream realm config = runErrorT $ do
closeStreams :: Stream -> IO (Either XmppFailure [Element]) closeStreams :: Stream -> IO (Either XmppFailure [Element])
closeStreams = withStream closeStreams' closeStreams = withStream closeStreams'
closeStreams' :: StateT StreamState IO (Either XmppFailure [Element])
closeStreams' = do closeStreams' = do
lift $ debugM "Pontarius.XMPP" "Closing stream..." lift $ debugM "Pontarius.XMPP" "Closing stream..."
send <- gets (streamSend . streamHandle) send <- gets (streamSend . streamHandle)
cc <- gets (streamClose . streamHandle) cc <- gets (streamClose . streamHandle)
liftIO $ send "</stream:stream>" void . liftIO $ send "</stream:stream>"
void $ liftIO $ forkIO $ do void $ liftIO $ forkIO $ do
threadDelay 3000000 -- TODO: Configurable value threadDelay 3000000 -- TODO: Configurable value
(Ex.try cc) :: IO (Either Ex.SomeException ()) void ((Ex.try cc) :: IO (Either Ex.SomeException ()))
return () return ()
collectElems [] collectElems []
where where
@ -311,6 +330,9 @@ closeStreams' = do
Right e -> collectElems (e:es) Right e -> collectElems (e:es)
-- TODO: Can the TLS send/recv functions throw something other than an IO error? -- TODO: Can the TLS send/recv functions throw something other than an IO error?
debugOut :: MonadIO m => ByteString -> m ()
debugOut outData = liftIO $ debugM "Pontarius.Xmpp"
("Out: " ++ (Text.unpack . Text.decodeUtf8 $ outData))
wrapIOException :: IO a -> StateT StreamState IO (Either XmppFailure a) wrapIOException :: IO a -> StateT StreamState IO (Either XmppFailure a)
wrapIOException action = do wrapIOException action = do
@ -324,7 +346,21 @@ wrapIOException action = do
pushElement :: Element -> StateT StreamState IO (Either XmppFailure Bool) pushElement :: Element -> StateT StreamState IO (Either XmppFailure Bool)
pushElement x = do pushElement x = do
send <- gets (streamSend . streamHandle) send <- gets (streamSend . streamHandle)
wrapIOException $ send $ renderElement x let outData = renderElement $ nsHack x
debugOut outData
wrapIOException $ send outData
where
-- HACK: We remove the "jabber:client" namespace because it is set as
-- default in the stream. This is to make isode's M-LINK server happy and
-- should be removed once jabber.org accepts prefix-free canonicalization
nsHack e@(Element{elementName = n})
| nameNamespace n == Just "jabber:client" =
e{ elementName = Name (nameLocalName n) Nothing Nothing
, elementNodes = map mapNSHack $ elementNodes e
}
| otherwise = e
mapNSHack (NodeElement e) = NodeElement $ nsHack e
mapNSHack n = n
-- | Encode and send stanza -- | Encode and send stanza
pushStanza :: Stanza -> Stream -> IO (Either XmppFailure Bool) pushStanza :: Stanza -> Stream -> IO (Either XmppFailure Bool)
@ -341,8 +377,10 @@ pushXmlDecl = do
pushOpenElement :: Element -> StateT StreamState IO (Either XmppFailure Bool) pushOpenElement :: Element -> StateT StreamState IO (Either XmppFailure Bool)
pushOpenElement e = do pushOpenElement e = do
sink <- gets (streamSend . streamHandle) send <- gets (streamSend . streamHandle)
wrapIOException $ sink $ renderOpenElement e let outData = renderOpenElement e
debugOut outData
wrapIOException $ send outData
-- `Connect-and-resumes' the given sink to the stream source, and pulls a -- `Connect-and-resumes' the given sink to the stream source, and pulls a
-- `b' value. -- `b' value.
@ -378,8 +416,8 @@ pullElement = do
-- Pulls an element and unpickles it. -- Pulls an element and unpickles it.
pullUnpickle :: PU [Node] a -> StateT StreamState IO (Either XmppFailure a) pullUnpickle :: PU [Node] a -> StateT StreamState IO (Either XmppFailure a)
pullUnpickle p = do pullUnpickle p = do
elem <- pullElement el <- pullElement
case elem of case el of
Left e -> return $ Left e Left e -> return $ Left e
Right elem' -> do Right elem' -> do
let res = unpickleElem p elem' let res = unpickleElem p elem'
@ -433,7 +471,7 @@ xmppNoStream = StreamState {
where where
zeroSource :: Source IO output zeroSource :: Source IO output
zeroSource = liftIO $ do zeroSource = liftIO $ do
errorM "Pontarius.XMPP" "zeroSource utilized." errorM "Pontarius.Xmpp" "zeroSource utilized."
ExL.throwIO XmppOtherFailure ExL.throwIO XmppOtherFailure
createStream :: HostName -> StreamConfiguration -> ErrorT XmppFailure IO (Stream) createStream :: HostName -> StreamConfiguration -> ErrorT XmppFailure IO (Stream)
@ -472,7 +510,7 @@ createStream realm config = do
where where
logConduit :: Conduit ByteString IO ByteString logConduit :: Conduit ByteString IO ByteString
logConduit = CL.mapM $ \d -> do logConduit = CL.mapM $ \d -> do
debugM "Pontarius.Xmpp" $ "Received TCP data: " ++ (BSC8.unpack d) ++ debugM "Pontarius.Xmpp" $ "In: " ++ (BSC8.unpack d) ++
"." "."
return d return d
@ -483,79 +521,78 @@ createStream realm config = do
-- attempt has been made. Will return the Handle acquired, if any. -- attempt has been made. Will return the Handle acquired, if any.
connect :: HostName -> StreamConfiguration -> ErrorT XmppFailure IO (Maybe Handle) connect :: HostName -> StreamConfiguration -> ErrorT XmppFailure IO (Maybe Handle)
connect realm config = do connect realm config = do
case socketDetails config of case connectionDetails config of
-- Just (_, NS.SockAddrUnix _) -> do UseHost host port -> lift $ do
-- lift $ errorM "Pontarius.Xmpp" "SockAddrUnix address provided." debugM "Pontarius.Xmpp" "Connecting to configured address."
-- throwError XmppIllegalTcpDetails connectTcp $ [(host, port)]
Just socketDetails' -> lift $ do UseSrv host -> connectSrv host
debugM "Pontarius.Xmpp" "Connecting to configured SockAddr address..." UseRealm -> connectSrv realm
connectTcp $ Left socketDetails' where
Nothing -> do connectSrv host = do
case (readMaybe_ realm :: Maybe IPv6, readMaybe_ realm :: Maybe IPv4, hostname (Text.pack realm) :: Maybe Hostname) of case checkHostName (Text.pack host) of
(Just ipv6, Nothing, _) -> lift $ connectTcp $ Right [(show ipv6, 5222)] Just host' -> do
(Nothing, Just ipv4, _) -> lift $ connectTcp $ Right [(show ipv4, 5222)]
(Nothing, Nothing, Just (Hostname realm')) -> do
resolvSeed <- lift $ makeResolvSeed (resolvConf config) resolvSeed <- lift $ makeResolvSeed (resolvConf config)
lift $ debugM "Pontarius.Xmpp" "Performing SRV lookup..." lift $ debugM "Pontarius.Xmpp" "Performing SRV lookup..."
srvRecords <- srvLookup realm' resolvSeed srvRecords <- srvLookup host' resolvSeed
case srvRecords of case srvRecords of
-- No SRV records. Try fallback lookup.
Nothing -> do Nothing -> do
lift $ debugM "Pontarius.Xmpp" "No SRV records, using fallback process..." lift $ debugM "Pontarius.Xmpp"
lift $ resolvAndConnectTcp resolvSeed (BSC8.pack $ realm) 5222 "No SRV records, using fallback process."
lift $ resolvAndConnectTcp resolvSeed (BSC8.pack $ host)
5222
Just srvRecords' -> do Just srvRecords' -> do
lift $ debugM "Pontarius.Xmpp" "SRV records found, performing A/AAAA lookups..." lift $ debugM "Pontarius.Xmpp"
"SRV records found, performing A/AAAA lookups."
lift $ resolvSrvsAndConnectTcp resolvSeed srvRecords' lift $ resolvSrvsAndConnectTcp resolvSeed srvRecords'
(Nothing, Nothing, Nothing) -> do Nothing -> do
lift $ errorM "Pontarius.Xmpp" "The hostname could not be validated." lift $ errorM "Pontarius.Xmpp"
"The hostname could not be validated."
throwError XmppIllegalTcpDetails throwError XmppIllegalTcpDetails
-- Connects to a list of addresses and ports. Surpresses any exceptions from -- Connects to a list of addresses and ports. Surpresses any exceptions from
-- connectTcp. -- connectTcp.
connectTcp :: Either (NS.Socket, NS.SockAddr) [(HostName, Int)] -> IO (Maybe Handle) connectTcp :: [(HostName, PortID)] -> IO (Maybe Handle)
connectTcp (Right []) = return Nothing connectTcp [] = return Nothing
connectTcp (Right ((address, port):remainder)) = do connectTcp ((address, port):remainder) = do
result <- try $ (do result <- Ex.try $ (do
debugM "Pontarius.Xmpp" $ "Connecting to " ++ (address) ++ " on port " ++ debugM "Pontarius.Xmpp" $ "Connecting to " ++ address ++ " on port " ++
(show port) ++ "." (show port) ++ "."
connectTo address (PortNumber $ fromIntegral port)) :: IO (Either IOException Handle) connectTo address port) :: IO (Either Ex.IOException Handle)
case result of case result of
Right handle -> do Right handle -> do
debugM "Pontarius.Xmpp" "Successfully connected to HostName." debugM "Pontarius.Xmpp" "Successfully connected to HostName."
return $ Just handle return $ Just handle
Left _ -> do Left _ -> do
debugM "Pontarius.Xmpp" "Connection to HostName could not be established." debugM "Pontarius.Xmpp" "Connection to HostName could not be established."
connectTcp $ Right remainder connectTcp remainder
connectTcp (Left (sock, sockAddr)) = do
result <- try $ (do
NS.connect sock sockAddr
NS.socketToHandle sock ReadWriteMode) :: IO (Either IOException Handle)
case result of
Right handle -> do
debugM "Pontarius.Xmpp" "Successfully connected to SockAddr."
return $ Just handle
Left _ -> do
debugM "Pontarius.Xmpp" "Connection to SockAddr could not be established."
return Nothing
-- Makes an AAAA query to acquire a IPs, and tries to connect to all of them. If -- Makes an AAAA query to acquire a IPs, and tries to connect to all of them. If
-- a handle can not be acquired this way, an analogous A query is performed. -- a handle can not be acquired this way, an analogous A query is performed.
-- Surpresses all IO exceptions. -- Surpresses all IO exceptions.
resolvAndConnectTcp :: ResolvSeed -> Domain -> Int -> IO (Maybe Handle) resolvAndConnectTcp :: ResolvSeed -> Domain -> Int -> IO (Maybe Handle)
resolvAndConnectTcp resolvSeed domain port = do resolvAndConnectTcp resolvSeed domain port = do
aaaaResults <- (try $ rethrowErrorCall $ withResolver resolvSeed $ aaaaResults <- (Ex.try $ rethrowErrorCall $ withResolver resolvSeed $
\resolver -> lookupAAAA resolver domain) :: IO (Either IOException (Maybe [IPv6])) \resolver -> lookupAAAA resolver domain) :: IO (Either Ex.IOException (Maybe [IPv6]))
handle <- case aaaaResults of handle <- case aaaaResults of
Right Nothing -> return Nothing Right Nothing -> return Nothing
Right (Just ipv6s) -> connectTcp $ Right $ Data.List.map (\ipv6 -> (show ipv6, port)) ipv6s Right (Just ipv6s) -> connectTcp $
Left e -> return Nothing map (\ip -> ( show ip
, PortNumber $ fromIntegral port))
ipv6s
Left _e -> return Nothing
case handle of case handle of
Nothing -> do Nothing -> do
aResults <- (try $ rethrowErrorCall $ withResolver resolvSeed $ aResults <- (Ex.try $ rethrowErrorCall $ withResolver resolvSeed $
\resolver -> lookupA resolver domain) :: IO (Either IOException (Maybe [IPv4])) \resolver -> lookupA resolver domain) :: IO (Either Ex.IOException (Maybe [IPv4]))
handle' <- case aResults of handle' <- case aResults of
Left _ -> return Nothing
Right Nothing -> return Nothing Right Nothing -> return Nothing
Right (Just ipv4s) -> connectTcp $ Right $ Data.List.map (\ipv4 -> (show ipv4, port)) ipv4s
Right (Just ipv4s) -> connectTcp $
map (\ip -> (show ip
, PortNumber
$ fromIntegral port))
ipv4s
case handle' of case handle' of
Nothing -> return Nothing Nothing -> return Nothing
Just handle'' -> return $ Just handle'' Just handle'' -> return $ Just handle''
@ -576,29 +613,30 @@ resolvSrvsAndConnectTcp resolvSeed ((domain, port):remaining) = do
-- exceptions and rethrows them as IOExceptions. -- exceptions and rethrows them as IOExceptions.
rethrowErrorCall :: IO a -> IO a rethrowErrorCall :: IO a -> IO a
rethrowErrorCall action = do rethrowErrorCall action = do
result <- try action result <- Ex.try action
case result of case result of
Right result' -> return result' Right result' -> return result'
Left (ErrorCall e) -> ioError $ userError $ "rethrowErrorCall: " ++ e Left (Ex.ErrorCall e) -> Ex.ioError $ userError
Left e -> throwIO e $ "rethrowErrorCall: " ++ e
-- Provides a list of A(AAA) names and port numbers upon a successful -- Provides a list of A(AAA) names and port numbers upon a successful
-- DNS-SRV request, or `Nothing' if the DNS-SRV request failed. -- DNS-SRV request, or `Nothing' if the DNS-SRV request failed.
srvLookup :: Text -> ResolvSeed -> ErrorT XmppFailure IO (Maybe [(Domain, Int)]) srvLookup :: Text -> ResolvSeed -> ErrorT XmppFailure IO (Maybe [(Domain, Int)])
srvLookup realm resolvSeed = ErrorT $ do srvLookup realm resolvSeed = ErrorT $ do
result <- try $ rethrowErrorCall $ withResolver resolvSeed $ \resolver -> do result <- Ex.try $ rethrowErrorCall $ withResolver resolvSeed
$ \resolver -> do
srvResult <- lookupSRV resolver $ BSC8.pack $ "_xmpp-client._tcp." ++ (Text.unpack realm) ++ "." srvResult <- lookupSRV resolver $ BSC8.pack $ "_xmpp-client._tcp." ++ (Text.unpack realm) ++ "."
case srvResult of case srvResult of
Just srvResult -> do
debugM "Pontarius.Xmpp" $ "SRV result: " ++ (show srvResult)
-- Get [(Domain, PortNumber)] of SRV request, if any.
srvResult' <- orderSrvResult srvResult
return $ Just $ Prelude.map (\(_, _, port, domain) -> (domain, port)) srvResult'
-- The service is not available at this domain.
-- Sorts the records based on the priority value.
Just [(_, _, _, ".")] -> do Just [(_, _, _, ".")] -> do
debugM "Pontarius.Xmpp" $ "\".\" SRV result returned." debugM "Pontarius.Xmpp" $ "\".\" SRV result returned."
return $ Just [] return $ Just []
Just srvResult' -> do
debugM "Pontarius.Xmpp" $ "SRV result: " ++ (show srvResult')
-- Get [(Domain, PortNumber)] of SRV request, if any.
orderedSrvResult <- orderSrvResult srvResult'
return $ Just $ Prelude.map (\(_, _, port, domain) -> (domain, port)) orderedSrvResult
-- The service is not available at this domain.
-- Sorts the records based on the priority value.
Nothing -> do Nothing -> do
debugM "Pontarius.Xmpp" "No SRV result returned." debugM "Pontarius.Xmpp" "No SRV result returned."
return Nothing return Nothing
@ -629,7 +667,7 @@ srvLookup realm resolvSeed = ErrorT $ do
orderSublist sublist = do orderSublist sublist = do
-- Compute the running sum, as well as the total sum of -- Compute the running sum, as well as the total sum of
-- the sublist. Add the running sum to the SRV tuples. -- the sublist. Add the running sum to the SRV tuples.
let (total, sublist') = Data.List.mapAccumL (\total (priority, weight, port, domain) -> (total + weight, (priority, weight, port, domain, total + weight))) 0 sublist let (total, sublist') = Data.List.mapAccumL (\total' (priority, weight, port, domain) -> (total' + weight, (priority, weight, port, domain, total' + weight))) 0 sublist
-- Choose a random number between 0 and the total sum -- Choose a random number between 0 and the total sum
-- (inclusive). -- (inclusive).
randomNumber <- randomRIO (0, total) randomNumber <- randomRIO (0, total)
@ -638,11 +676,11 @@ srvLookup realm resolvSeed = ErrorT $ do
let (beginning, ((priority, weight, port, domain, _):end)) = Data.List.break (\(_, _, _, _, running) -> randomNumber <= running) sublist' let (beginning, ((priority, weight, port, domain, _):end)) = Data.List.break (\(_, _, _, _, running) -> randomNumber <= running) sublist'
-- Remove the running total number from the remaining -- Remove the running total number from the remaining
-- elements. -- elements.
let sublist'' = Data.List.map (\(priority, weight, port, domain, _) -> (priority, weight, port, domain)) (Data.List.concat [beginning, end]) let sublist'' = Data.List.map (\(priority', weight', port', domain', _) -> (priority', weight', port', domain')) (Data.List.concat [beginning, end])
-- Repeat the ordering procedure on the remaining -- Repeat the ordering procedure on the remaining
-- elements. -- elements.
tail <- orderSublist sublist'' rest <- orderSublist sublist''
return $ ((priority, weight, port, domain):tail) return $ ((priority, weight, port, domain):rest)
-- Closes the connection and updates the XmppConMonad Stream state. -- Closes the connection and updates the XmppConMonad Stream state.
-- killStream :: Stream -> IO (Either ExL.SomeException ()) -- killStream :: Stream -> IO (Either ExL.SomeException ())
@ -663,25 +701,26 @@ pushIQ :: StanzaID
-> Element -> Element
-> Stream -> Stream
-> IO (Either XmppFailure (Either IQError IQResult)) -> IO (Either XmppFailure (Either IQError IQResult))
pushIQ iqID to tp lang body stream = do pushIQ iqID to tp lang body stream = runErrorT $ do
pushStanza (IQRequestS $ IQRequest iqID Nothing to lang tp body) stream pushing $ pushStanza
res <- pullStanza stream (IQRequestS $ IQRequest iqID Nothing to lang tp body) stream
res <- lift $ pullStanza stream
case res of case res of
Left e -> return $ Left e Left e -> throwError e
Right (IQErrorS e) -> return $ Right $ Left e Right (IQErrorS e) -> return $ Left e
Right (IQResultS r) -> do Right (IQResultS r) -> do
unless unless
(iqID == iqResultID r) $ liftIO $ do (iqID == iqResultID r) $ liftIO $ do
errorM "Pontarius.XMPP" $ "pushIQ: ID mismatch (" ++ (show iqID) ++ " /= " ++ (show $ iqResultID r) ++ ")." liftIO $ errorM "Pontarius.XMPP" $ "pushIQ: ID mismatch (" ++ (show iqID) ++ " /= " ++ (show $ iqResultID r) ++ ")."
ExL.throwIO XmppOtherFailure liftIO $ ExL.throwIO XmppOtherFailure
-- TODO: Log: ("In sendIQ' IDs don't match: " ++ show iqID ++ -- TODO: Log: ("In sendIQ' IDs don't match: " ++ show iqID ++
-- " /= " ++ show (iqResultID r) ++ " .") -- " /= " ++ show (iqResultID r) ++ " .")
return $ Right $ Right r return $ Right r
_ -> do _ -> do
errorM "Pontarius.XMPP" $ "pushIQ: Unexpected stanza type." liftIO $ errorM "Pontarius.XMPP" $ "pushIQ: Unexpected stanza type."
return . Left $ XmppOtherFailure throwError XmppOtherFailure
debugConduit :: Pipe l ByteString ByteString u IO b debugConduit :: (Show o, MonadIO m) => ConduitM o o m b
debugConduit = forever $ do debugConduit = forever $ do
s' <- await s' <- await
case s' of case s' of
@ -697,7 +736,9 @@ elements = do
Just (EventBeginElement n as) -> do Just (EventBeginElement n as) -> do
goE n as >>= yield goE n as >>= yield
elements elements
Just (EventEndElement streamName) -> lift $ R.monadThrow StreamEnd -- This might be an XML error if the end element tag is not
-- "</stream>". TODO: We might want to check this at a later time
Just (EventEndElement _) -> lift $ R.monadThrow StreamEnd
Nothing -> return () Nothing -> return ()
_ -> lift $ R.monadThrow $ InvalidXmppXml $ "not an element: " ++ show x _ -> lift $ R.monadThrow $ InvalidXmppXml $ "not an element: " ++ show x
where where
@ -707,8 +748,8 @@ elements = do
go front = do go front = do
x <- f x <- f
case x of case x of
Left x -> return $ (x, front []) Left l -> return $ (l, front [])
Right y -> go (front . (:) y) Right r -> go (front . (:) r)
goE n as = do goE n as = do
(y, ns) <- many' goN (y, ns) <- many' goN
if y == Just (EventEndElement n) if y == Just (EventEndElement n)
@ -732,11 +773,8 @@ elements = do
compressNodes $ NodeContent (ContentText $ x `Text.append` y) : z compressNodes $ NodeContent (ContentText $ x `Text.append` y) : z
compressNodes (x:xs) = x : compressNodes xs compressNodes (x:xs) = x : compressNodes xs
streamName :: Name withStream :: StateT StreamState IO a -> Stream -> IO a
streamName = (Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream")) withStream action (Stream stream) = Ex.bracketOnError
withStream :: StateT StreamState IO (Either XmppFailure c) -> Stream -> IO (Either XmppFailure c)
withStream action (Stream stream) = bracketOnError
(atomically $ takeTMVar stream ) (atomically $ takeTMVar stream )
(atomically . putTMVar stream) (atomically . putTMVar stream)
(\s -> do (\s -> do
@ -746,7 +784,7 @@ withStream action (Stream stream) = bracketOnError
) )
-- nonblocking version. Changes to the connection are ignored! -- nonblocking version. Changes to the connection are ignored!
withStream' :: StateT StreamState IO (Either XmppFailure b) -> Stream -> IO (Either XmppFailure b) withStream' :: StateT StreamState IO a -> Stream -> IO a
withStream' action (Stream stream) = do withStream' action (Stream stream) = do
stream_ <- atomically $ readTMVar stream stream_ <- atomically $ readTMVar stream
(r, _) <- runStateT action stream_ (r, _) <- runStateT action stream_

75
source/Network/Xmpp/Tls.hs

@ -4,7 +4,6 @@
module Network.Xmpp.Tls where module Network.Xmpp.Tls where
import Control.Concurrent.STM.TMVar
import qualified Control.Exception.Lifted as Ex import qualified Control.Exception.Lifted as Ex
import Control.Monad import Control.Monad
import Control.Monad.Error import Control.Monad.Error
@ -14,21 +13,30 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC8 import qualified Data.ByteString.Char8 as BSC8
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 Data.IORef import Data.IORef
import Data.Typeable
import Data.XML.Types import Data.XML.Types
import Network.TLS import Network.TLS
import Network.TLS.Extra
import Network.Xmpp.Stream import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import System.Log.Logger (debugM, errorM) import System.Log.Logger (debugM, errorM, infoM)
mkBackend :: StreamHandle -> Backend
mkBackend con = Backend { backendSend = \bs -> void (streamSend con bs) mkBackend con = Backend { backendSend = \bs -> void (streamSend con bs)
, backendRecv = streamReceive con , backendRecv = bufferReceive (streamReceive con)
, backendFlush = streamFlush con , backendFlush = streamFlush con
, backendClose = streamClose con , backendClose = streamClose con
} }
where
bufferReceive _ 0 = return BS.empty
bufferReceive recv n = BS.concat `liftM` (go n)
where
go m = do
bs <- recv m
case BS.length bs of
0 -> return []
l -> if l < m
then (bs :) `liftM` go (m - l)
else return [bs]
starttlsE :: Element starttlsE :: Element
starttlsE = Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] [] starttlsE = Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] []
@ -43,49 +51,60 @@ tls con = Ex.handle (return . Left . TlsError)
case sState of case sState of
Plain -> return () Plain -> return ()
Closed -> do Closed -> do
liftIO $ errorM "Pontarius.XMPP" "startTls: The stream is closed." liftIO $ errorM "Pontarius.Xmpp" "startTls: The stream is closed."
throwError XmppNoStream throwError XmppNoStream
Secured -> do Secured -> do
liftIO $ errorM "Pontarius.XMPP" "startTls: The stream is already secured." liftIO $ errorM "Pontarius.Xmpp" "startTls: The stream is already secured."
throwError TlsStreamSecured throwError TlsStreamSecured
features <- lift $ gets streamFeatures features <- lift $ gets streamFeatures
case (tlsBehaviour conf, streamTls features) of case (tlsBehaviour conf, streamTls features) of
(RequireTls , Just _ ) -> startTls (RequireTls , Just _ ) -> startTls
(RequireTls , Nothing ) -> throwError TlsNoServerSupport (RequireTls , Nothing ) -> throwError TlsNoServerSupport
(PreferTls , Just _ ) -> startTls (PreferTls , Just _ ) -> startTls
(PreferTls , Nothing ) -> return () (PreferTls , Nothing ) -> skipTls
(PreferPlain , Just True) -> startTls (PreferPlain , Just True) -> startTls
(PreferPlain , _ ) -> return () (PreferPlain , _ ) -> skipTls
(RefuseTls , Just True) -> throwError XmppOtherFailure (RefuseTls , Just True) -> throwError XmppOtherFailure
(RefuseTls , _ ) -> return () (RefuseTls , _ ) -> skipTls
where where
skipTls = liftIO $ infoM "Pontarius.Xmpp" "Skipping TLS negotiation"
startTls = do startTls = do
liftIO $ infoM "Pontarius.Xmpp" "Running StartTLS"
params <- gets $ tlsParams . streamConfiguration params <- gets $ tlsParams . streamConfiguration
lift $ pushElement starttlsE sent <- ErrorT $ pushElement starttlsE
unless sent $ do
liftIO $ errorM "Pontarius.Xmpp" "startTls: Could not sent stanza."
throwError XmppOtherFailure
answer <- lift $ pullElement answer <- lift $ pullElement
case answer of case answer of
Left e -> return $ Left e Left e -> throwError e
Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] []) -> Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] []) ->
return $ Right () return ()
Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _) -> do Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _) -> do
liftIO $ errorM "Pontarius.XMPP" "startTls: TLS initiation failed." liftIO $ errorM "Pontarius.Xmpp" "startTls: TLS initiation failed."
return . Left $ XmppOtherFailure throwError XmppOtherFailure
Right r ->
liftIO $ errorM "Pontarius.Xmpp" $
"startTls: Unexpected element: " ++ show r
hand <- gets streamHandle hand <- gets streamHandle
(raw, _snk, psh, read, ctx) <- lift $ tlsinit params (mkBackend hand) (_raw, _snk, psh, recv, ctx) <- lift $ tlsinit params (mkBackend hand)
let newHand = StreamHandle { streamSend = catchPush . psh let newHand = StreamHandle { streamSend = catchPush . psh
, streamReceive = read , streamReceive = recv
, streamFlush = contextFlush ctx , streamFlush = contextFlush ctx
, streamClose = bye ctx >> streamClose hand , streamClose = bye ctx >> streamClose hand
} }
lift $ modify ( \x -> x {streamHandle = newHand}) lift $ modify ( \x -> x {streamHandle = newHand})
liftIO $ infoM "Pontarius.Xmpp" "Stream Secured."
either (lift . Ex.throwIO) return =<< lift restartStream either (lift . Ex.throwIO) return =<< lift restartStream
modify (\s -> s{streamConnectionState = Secured}) modify (\s -> s{streamConnectionState = Secured})
return () return ()
client :: (MonadIO m, CPRG rng) => Params -> rng -> Backend -> m Context
client params gen backend = do client params gen backend = do
contextNew backend params gen contextNew backend params gen
defaultParams = defaultParamsClient xmppDefaultParams :: Params
xmppDefaultParams = defaultParamsClient
tlsinit :: (MonadIO m, MonadIO m1) => tlsinit :: (MonadIO m, MonadIO m1) =>
TLSParams TLSParams
@ -96,10 +115,10 @@ tlsinit :: (MonadIO m, MonadIO m1) =>
, Int -> m1 BS.ByteString , Int -> m1 BS.ByteString
, Context , Context
) )
tlsinit tlsParams backend = do tlsinit params backend = do
liftIO $ debugM "Pontarius.Xmpp.TLS" "TLS with debug mode enabled." liftIO $ debugM "Pontarius.Xmpp.TLS" "TLS with debug mode enabled."
gen <- liftIO $ getSystemRandomGen -- TODO: Find better random source? gen <- liftIO $ getSystemRandomGen -- TODO: Find better random source?
con <- client tlsParams gen backend con <- client params gen backend
handshake con handshake con
let src = forever $ do let src = forever $ do
dt <- liftIO $ recvData con dt <- liftIO $ recvData con
@ -111,25 +130,21 @@ tlsinit tlsParams backend = do
Nothing -> return () Nothing -> return ()
Just x -> do Just x -> do
sendData con (BL.fromChunks [x]) sendData con (BL.fromChunks [x])
liftIO $ debugM "Pontarius.Xmpp.TLS"
("out :" ++ BSC8.unpack x)
snk snk
read <- liftIO $ mkReadBuffer (recvData con) readWithBuffer <- liftIO $ mkReadBuffer (recvData con)
return ( src return ( src
, snk , snk
, \s -> do , \s -> sendData con $ BL.fromChunks [s]
liftIO $ debugM "Pontarius.Xmpp.TLS" ("out :" ++ BSC8.unpack s) , liftIO . readWithBuffer
sendData con $ BL.fromChunks [s]
, liftIO . read
, con , con
) )
mkReadBuffer :: IO BS.ByteString -> IO (Int -> IO BS.ByteString) mkReadBuffer :: IO BS.ByteString -> IO (Int -> IO BS.ByteString)
mkReadBuffer read = do mkReadBuffer recv = do
buffer <- newIORef BS.empty buffer <- newIORef BS.empty
let read' n = do let read' n = do
nc <- readIORef buffer nc <- readIORef buffer
bs <- if BS.null nc then read bs <- if BS.null nc then recv
else return nc else return nc
let (result, rest) = BS.splitAt n bs let (result, rest) = BS.splitAt n bs
writeIORef buffer rest writeIORef buffer rest

55
source/Network/Xmpp/Types.hs

@ -37,6 +37,7 @@ module Network.Xmpp.Types
, ConnectionState(..) , ConnectionState(..)
, StreamErrorInfo(..) , StreamErrorInfo(..)
, StanzaHandler , StanzaHandler
, ConnectionDetails(..)
, StreamConfiguration(..) , StreamConfiguration(..)
, langTag , langTag
, Jid(..) , Jid(..)
@ -46,8 +47,6 @@ module Network.Xmpp.Types
, jidFromTexts , jidFromTexts
, StreamEnd(..) , StreamEnd(..)
, InvalidXmppXml(..) , InvalidXmppXml(..)
, Hostname(..)
, hostname
, SessionConfiguration(..) , SessionConfiguration(..)
, TlsBehaviour(..) , TlsBehaviour(..)
) )
@ -70,7 +69,6 @@ import Data.Typeable(Typeable)
import Data.XML.Types import Data.XML.Types
import Network import Network
import Network.DNS import Network.DNS
import Network.Socket
import Network.TLS hiding (Version) import Network.TLS hiding (Version)
import Network.TLS.Extra import Network.TLS.Extra
import qualified Text.NamePrep as SP import qualified Text.NamePrep as SP
@ -1012,6 +1010,10 @@ data InvalidXmppXml = InvalidXmppXml String deriving (Show, Typeable)
instance Exception InvalidXmppXml instance Exception InvalidXmppXml
data ConnectionDetails = UseRealm -- ^ Use realm to resolv host
| UseSrv HostName -- ^ Use this hostname for a SRC lookup
| UseHost HostName PortID -- ^ Use specified host
-- | Configuration settings related to the stream. -- | Configuration settings related to the stream.
data StreamConfiguration = data StreamConfiguration =
StreamConfiguration { -- | Default language when no language tag is set StreamConfiguration { -- | Default language when no language tag is set
@ -1026,7 +1028,7 @@ data StreamConfiguration =
-- of the realm, as well as specify the use of a -- of the realm, as well as specify the use of a
-- non-standard port when connecting by IP or -- non-standard port when connecting by IP or
-- connecting to a domain without SRV records. -- connecting to a domain without SRV records.
, socketDetails :: Maybe (Socket, SockAddr) , connectionDetails :: ConnectionDetails
-- | DNS resolver configuration -- | DNS resolver configuration
, resolvConf :: ResolvConf , resolvConf :: ResolvConf
-- | Whether or not to perform the legacy -- | Whether or not to perform the legacy
@ -1039,56 +1041,19 @@ data StreamConfiguration =
, tlsParams :: TLSParams , tlsParams :: TLSParams
} }
instance Default StreamConfiguration where instance Default StreamConfiguration where
def = StreamConfiguration { preferredLang = Nothing def = StreamConfiguration { preferredLang = Nothing
, toJid = Nothing , toJid = Nothing
, socketDetails = Nothing , connectionDetails = UseRealm
, resolvConf = defaultResolvConf , resolvConf = defaultResolvConf
, establishSession = True , establishSession = True
, tlsBehaviour = PreferTls , tlsBehaviour = PreferTls
, tlsParams = defaultParamsClient { pConnectVersion = TLS12 , tlsParams = defaultParamsClient { pConnectVersion = TLS10
, pAllowedVersions = [TLS12] , pAllowedVersions = [TLS10, TLS11, TLS12]
, pCiphers = ciphersuite_strong , pCiphers = ciphersuite_strong
} }
} }
data Hostname = Hostname Text deriving (Eq, Show)
instance Read Hostname where
readsPrec _ x = case hostname (Text.pack x) of
Nothing -> []
Just h -> [(h,"")]
instance IsString Hostname where
fromString = fromJust . hostname . Text.pack
-- | Validates the hostname string in accordance with RFC 1123.
hostname :: Text -> Maybe Hostname
hostname t = do
eitherToMaybeHostname $ AP.parseOnly hostnameP t
where
eitherToMaybeHostname = either (const Nothing) (Just . Hostname)
-- Validation of RFC 1123 hostnames.
hostnameP :: AP.Parser Text
hostnameP = do
-- Hostnames may not begin with a hyphen.
h <- AP.satisfy $ AP.inClass $ ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9']
t <- AP.takeWhile $ AP.inClass $ ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ ['-']
let label = Text.concat [Text.pack [h], t]
if Text.length label > 63
then fail "Label too long."
else do
AP.endOfInput
return label
<|> do
_ <- AP.satisfy (== '.')
r <- hostnameP
if (Text.length label) + 1 + (Text.length r) > 255
then fail "Hostname too long."
else return $ Text.concat [label, Text.pack ".", r]
type StanzaHandler = TChan Stanza -- ^ outgoing stanza type StanzaHandler = TChan Stanza -- ^ outgoing stanza
-> Stanza -- ^ stanza to handle -> Stanza -- ^ stanza to handle
-> IO Bool -- ^ True when processing should continue -> IO Bool -- ^ True when processing should continue
@ -1102,6 +1067,7 @@ data SessionConfiguration = SessionConfiguration
-- | Function to generate the stream of stanza identifiers. -- | Function to generate the stream of stanza identifiers.
, sessionStanzaIDs :: IO (IO StanzaID) , sessionStanzaIDs :: IO (IO StanzaID)
, extraStanzaHandlers :: [StanzaHandler] , extraStanzaHandlers :: [StanzaHandler]
, enableRoster :: Bool
} }
instance Default SessionConfiguration where instance Default SessionConfiguration where
@ -1114,6 +1080,7 @@ instance Default SessionConfiguration where
writeTVar idRef (curId + 1 :: Integer) writeTVar idRef (curId + 1 :: Integer)
return . StanzaID . Text.pack . show $ curId return . StanzaID . Text.pack . show $ curId
, extraStanzaHandlers = [] , extraStanzaHandlers = []
, enableRoster = True
} }
-- | How the client should behave in regards to TLS. -- | How the client should behave in regards to TLS.

130
source/Network/Xmpp/Utilities.hs

@ -1,105 +1,30 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Utilities (presTo, message, answerMessage, openElementToEvents, renderOpenElement, renderElement) where
import Network.Xmpp.Types module Network.Xmpp.Utilities
( openElementToEvents
import Control.Monad.STM , renderOpenElement
import Control.Concurrent.STM.TVar , renderElement
import Prelude , checkHostName
)
import Data.XML.Types where
import Control.Applicative ((<|>))
import qualified Data.Attoparsec.Text as AP import qualified Data.Attoparsec.Text as AP
import qualified Data.Text as Text
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.Conduit as C
import Data.Conduit.List as CL
import qualified Data.Text as Text import qualified Data.Text as Text
import Data.Text(Text)
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import System.IO.Unsafe(unsafePerformIO)
import Data.Conduit.List as CL
-- import Data.Typeable
import Control.Applicative ((<$>))
import Control.Exception
import Control.Monad.Trans.Class
import Data.Conduit as C
import Data.XML.Types import Data.XML.Types
import Prelude
import System.IO.Unsafe(unsafePerformIO)
import qualified Text.XML.Stream.Render as TXSR import qualified Text.XML.Stream.Render as TXSR
import Text.XML.Unresolved as TXU import Text.XML.Unresolved as TXU
-- TODO: Not used, and should probably be removed.
-- | Creates a new @IdGenerator@. Internally, it will maintain an infinite list
-- of IDs ('[\'a\', \'b\', \'c\'...]'). The argument is a prefix to prepend the
-- IDs with. Calling the function will extract an ID and update the generator's
-- internal state so that the same ID will not be generated again.
idGenerator :: Text.Text -> IO IdGenerator
idGenerator prefix = atomically $ do
tvar <- newTVar $ ids prefix
return $ IdGenerator $ next tvar
where
-- Transactionally extract the next ID from the infinite list of IDs.
next :: TVar [Text.Text] -> IO Text.Text
next tvar = atomically $ do
list <- readTVar tvar
case list of
[] -> error "empty list in Utilities.hs"
(x:xs) -> do
writeTVar tvar xs
return x
-- Generates an infinite and predictable list of IDs, all beginning with the
-- provided prefix. Adds the prefix to all combinations of IDs (ids').
ids :: Text.Text -> [Text.Text]
ids p = Prelude.map (\ id -> Text.append p id) ids'
where
-- Generate all combinations of IDs, with increasing length.
ids' :: [Text.Text]
ids' = Prelude.map Text.pack $ Prelude.concatMap ids'' [1..]
-- Generates all combinations of IDs with the given length.
ids'' :: Integer -> [String]
ids'' 0 = [""]
ids'' l = [x:xs | x <- repertoire, xs <- ids'' (l - 1)]
-- Characters allowed in IDs.
repertoire :: String
repertoire = ['a'..'z']
-- Constructs a "Version" based on the major and minor version numbers.
versionFromNumbers :: Integer -> Integer -> Version
versionFromNumbers major minor = Version major minor
-- | Add a recipient to a presence notification.
presTo :: Presence -> Jid -> Presence
presTo pres to = pres{presenceTo = Just to}
-- | An empty message.
message :: Message
message = Message { messageID = Nothing
, messageFrom = Nothing
, messageTo = Nothing
, messageLangTag = Nothing
, messageType = Normal
, messagePayload = []
}
-- | Produce an answer message with the given payload, switching the "from" and
-- "to" attributes in the original message. Produces a 'Nothing' value of the
-- provided message message has no from attribute.
answerMessage :: Message -> [Element] -> Maybe Message
answerMessage Message{messageFrom = Just frm, ..} payload =
Just Message{ messageFrom = messageTo
, messageID = Nothing
, messageTo = Just frm
, messagePayload = payload
, ..
}
answerMessage _ _ = Nothing
openElementToEvents :: Element -> [Event] openElementToEvents :: Element -> [Event]
openElementToEvents (Element name as ns) = EventBeginElement name as : goN ns [] openElementToEvents (Element name as ns) = EventBeginElement name as : goN ns []
where where
@ -124,4 +49,31 @@ renderElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO
$ CL.sourceList (elementToEvents e) $$ TXSR.renderText def =$ CL.consume $ CL.sourceList (elementToEvents e) $$ TXSR.renderText def =$ CL.consume
where where
elementToEvents :: Element -> [Event] elementToEvents :: Element -> [Event]
elementToEvents e@(Element name _ _) = openElementToEvents e ++ [EventEndElement name] elementToEvents el@(Element name _ _) = openElementToEvents el
++ [EventEndElement name]
-- | Validates the hostname string in accordance with RFC 1123.
checkHostName :: Text -> Maybe Text
checkHostName t =
eitherToMaybeHostName $ AP.parseOnly hostnameP t
where
eitherToMaybeHostName = either (const Nothing) Just
-- Validation of RFC 1123 hostnames.
hostnameP :: AP.Parser Text
hostnameP = do
-- Hostnames may not begin with a hyphen.
h <- AP.satisfy $ AP.inClass $ ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9']
t <- AP.takeWhile $ AP.inClass $ ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ ['-']
let label = Text.concat [Text.pack [h], t]
if Text.length label > 63
then fail "Label too long."
else do
AP.endOfInput
return label
<|> do
_ <- AP.satisfy (== '.')
r <- hostnameP
if Text.length label + 1 + Text.length r > 255
then fail "Hostname too long."
else return $ Text.concat [label, Text.pack ".", r]

23
source/Network/Xmpp/Xep/DataForms.hs

@ -7,12 +7,9 @@
module Network.Xmpp.Xep.DataForms where module Network.Xmpp.Xep.DataForms where
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.XML.Types as XML
import Data.XML.Pickle import Data.XML.Pickle
import qualified Data.Text as Text import qualified Data.XML.Types as XML
import qualified Text.XML.Stream.Parse as Parse
dataFormNs :: Text.Text dataFormNs :: Text.Text
dataFormNs = "jabber:x:data" dataFormNs = "jabber:x:data"
@ -95,12 +92,12 @@ instance Read FieldType where
xpForm :: PU [XML.Node] Form xpForm :: PU [XML.Node] Form
xpForm = xpWrap (\(tp , (title, instructions, fields, reported, items)) -> xpForm = xpWrap (\(tp , (ttl, ins, flds, rpd, its)) ->
Form tp title (map snd instructions) fields reported (map snd items)) Form tp ttl (map snd ins) flds rpd (map snd its))
(\(Form tp title instructions fields reported items) -> (\(Form tp ttl ins flds rpd its) ->
(tp , (tp ,
(title, map ((),) instructions (ttl, map ((),) ins
, fields, reported, map ((),) items))) , flds, rpd, map ((),) its)))
$ $
xpElem (dataFormName "x") xpElem (dataFormName "x")
@ -113,10 +110,10 @@ xpForm = xpWrap (\(tp , (title, instructions, fields, reported, items)) ->
(xpElems (dataFormName "item") xpUnit xpFields)) (xpElems (dataFormName "item") xpUnit xpFields))
xpFields :: PU [XML.Node] [Field] xpFields :: PU [XML.Node] [Field]
xpFields = xpWrap (map $ \((var, tp, label),(desc, req, vals, opts)) xpFields = xpWrap (map $ \((var, tp, lbl),(desc, req, vals, opts))
-> Field var label tp desc req vals opts) -> Field var lbl tp desc req vals opts)
(map $ \(Field var label tp desc req vals opts) (map $ \(Field var lbl tp desc req vals opts)
-> ((var, tp, label),(desc, req, vals, opts))) $ -> ((var, tp, lbl),(desc, req, vals, opts))) $
xpElems (dataFormName "field") xpElems (dataFormName "field")
(xp3Tuple (xp3Tuple
(xpAttrImplied "var" xpId ) (xpAttrImplied "var" xpId )

Loading…
Cancel
Save