Browse Source

UNFINISHED Merge remote-tracking branch 'nejla/master'

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

BIN
import_visualisation-new-full.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
import_visualisation-new.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

13
pontarius-xmpp.cabal

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

81
source/Data/Conduit/Tls.hs

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

10
source/Network/Xmpp.hs

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

57
source/Network/Xmpp/Bind.hs

@ -1,57 +0,0 @@ @@ -1,57 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Bind where
import Control.Exception
import Data.Text as Text
import Data.XML.Pickle
import Data.XML.Types
import Network.Xmpp.Connection_
import Network.Xmpp.Pickle
import Network.Xmpp.Types
import Control.Monad.State(modify)
import Control.Concurrent.STM.TMVar
import Control.Monad.Error
-- Produces a `bind' element, optionally wrapping a resource.
bindBody :: Maybe Text -> Element
bindBody = pickleElem $
-- Pickler to produce a
-- "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
-- element, with a possible "<resource>[JID]</resource>"
-- child.
xpBind . xpOption $ xpElemNodes "resource" (xpContent xpId)
-- Sends a (synchronous) IQ set request for a (`Just') given or server-generated
-- resource and extract the JID from the non-error response.
xmppBind :: Maybe Text -> TMVar Connection -> IO (Either XmppFailure Jid)
xmppBind rsrc c = runErrorT $ do
answer <- ErrorT $ pushIQ' "bind" Nothing Set Nothing (bindBody rsrc) c
case answer of
Right IQResult{iqResultPayload = Just b} -> do
let jid = unpickleElem xpJid b
case jid of
Right jid' -> do
ErrorT $ withConnection (do
modify $ \s -> s{cJid = Just jid'}
return $ Right jid') c -- not pretty
return jid'
otherwise -> throwError XmppOtherFailure
-- TODO: Log: ("Bind couldn't unpickle JID from " ++ show answer)
otherwise -> throwError XmppOtherFailure
where
-- Extracts the character data in the `jid' element.
xpJid :: PU [Node] Jid
xpJid = xpBind $ xpElemNodes jidName (xpContent xpPrim)
jidName = "{urn:ietf:params:xml:ns:xmpp-bind}jid"
-- A `bind' element pickler.
xpBind :: PU [Node] b -> PU [Node] b
xpBind c = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-bind}bind" c

49
source/Network/Xmpp/Concurrent.hs

@ -11,6 +11,7 @@ module Network.Xmpp.Concurrent @@ -11,6 +11,7 @@ module Network.Xmpp.Concurrent
, toChans
, newSession
, writeWorker
, session
) where
import Network.Xmpp.Concurrent.Monad
@ -31,9 +32,17 @@ import Network.Xmpp.Concurrent.Presence @@ -31,9 +32,17 @@ import Network.Xmpp.Concurrent.Presence
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Concurrent.Threads
import Network.Xmpp.Marshal
import Network.Xmpp.Pickle
import Network.Xmpp.Types
import Text.Xml.Stream.Elements
import Network
import Data.Text as Text
import Network.Xmpp.Tls
import qualified Network.TLS as TLS
import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Mechanisms
import Network.Xmpp.Sasl.Types
import Data.Maybe
import Network.Xmpp.Stream
import Network.Xmpp.Utilities
import Control.Monad.Error
@ -74,14 +83,14 @@ toChans stanzaC iqHands sta = atomically $ do @@ -74,14 +83,14 @@ toChans stanzaC iqHands sta = atomically $ do
-- | Creates and initializes a new Xmpp context.
newSession :: TMVar Connection -> IO (Either XmppFailure Session)
newSession con = runErrorT $ do
newSession :: TMVar Stream -> IO (Either XmppFailure Session)
newSession stream = runErrorT $ do
outC <- lift newTChanIO
stanzaChan <- lift newTChanIO
iqHandlers <- lift $ newTVarIO (Map.empty, Map.empty)
eh <- lift $ newTVarIO $ EventHandlers { connectionClosedHandler = \_ -> return () }
let stanzaHandler = toChans stanzaChan iqHandlers
(kill, wLock, conState, readerThread) <- ErrorT $ startThreadsWith stanzaHandler eh con
(kill, wLock, streamState, readerThread) <- ErrorT $ startThreadsWith stanzaHandler eh stream
writer <- lift $ forkIO $ writeWorker outC wLock
idRef <- lift $ newTVarIO 1
let getId = atomically $ do
@ -94,7 +103,7 @@ newSession con = runErrorT $ do @@ -94,7 +103,7 @@ newSession con = runErrorT $ do
, writeRef = wLock
, readerThread = readerThread
, idGenerator = getId
, conRef = conState
, streamRef = streamState
, eventHandlers = eh
, stopThreads = kill >> killThread writer
}
@ -111,3 +120,31 @@ writeWorker stCh writeR = forever $ do @@ -111,3 +120,31 @@ writeWorker stCh writeR = forever $ do
atomically $ unGetTChan stCh next -- If the writing failed, the
-- connection is dead.
threadDelay 250000 -- Avoid free spinning.
-- | Creates a 'Session' object by setting up a connection with an XMPP server.
--
-- Will connect to the specified host. If the fourth parameters is a 'Just'
-- value, @session@ will attempt to secure the connection with TLS. If the fifth
-- parameters is a 'Just' value, @session@ will attempt to authenticate and
-- acquire an XMPP resource.
session :: HostName -- ^ Host to connect to
-> Text -- ^ The realm host name (to
-- distinguish the XMPP service)
-> PortID -- ^ Port to connect to
-> Maybe TLS.TLSParams -- ^ TLS settings, if securing the
-- connection to the server is
-- desired
-> Maybe ([SaslHandler], Maybe Text) -- ^ SASL handlers and the desired
-- JID resource (or Nothing to let
-- the server decide)
-> IO (Either XmppFailure (Session, Maybe AuthFailure))
session hostname realm port tls sasl = runErrorT $ do
con <- ErrorT $ openStream hostname port realm
if isJust tls
then ErrorT $ startTls (fromJust tls) con
else return ()
aut <- if isJust sasl
then ErrorT $ auth (fst $ fromJust sasl) (snd $ fromJust sasl) con
else return Nothing
ses <- ErrorT $ newSession con
return (ses, aut)

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

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

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

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

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

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

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

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

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

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

205
source/Network/Xmpp/Jid.hs

@ -1,205 +0,0 @@ @@ -1,205 +0,0 @@
{-# OPTIONS_HADDOCK hide #-}
-- This module deals with JIDs, also known as XMPP addresses. For more
-- information on JIDs, see RFC 6122: XMPP: Address Format.
module Network.Xmpp.Jid
( Jid(..)
, fromText
, fromStrings
, isBare
, isFull
) where
import Control.Applicative ((<$>),(<|>))
import Control.Monad(guard)
import qualified Data.Attoparsec.Text as AP
import Data.Maybe(fromJust)
import qualified Data.Set as Set
import Data.String (IsString(..))
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Text.NamePrep as SP
import qualified Text.StringPrep as SP
-- | A JID is XMPP\'s native format for addressing entities in the network. It
-- is somewhat similar to an e-mail address but contains three parts instead of
-- two.
data Jid = Jid { -- | The @localpart@ of a JID is an optional identifier placed
-- before the domainpart and separated from the latter by a
-- \'\@\' character. Typically a localpart uniquely identifies
-- the entity requesting and using network access provided by a
-- server (i.e., a local account), although it can also
-- represent other kinds of entities (e.g., a chat room
-- associated with a multi-user chat service). The entity
-- represented by an XMPP localpart is addressed within the
-- context of a specific domain (i.e.,
-- @localpart\@domainpart@).
localpart :: !(Maybe Text)
-- | The domainpart typically identifies the /home/ server to
-- which clients connect for XML routing and data management
-- functionality. However, it is not necessary for an XMPP
-- domainpart to identify an entity that provides core XMPP
-- server functionality (e.g., a domainpart can identify an
-- entity such as a multi-user chat service, a
-- publish-subscribe service, or a user directory).
, domainpart :: !Text
-- | The resourcepart of a JID is an optional identifier placed
-- after the domainpart and separated from the latter by the
-- \'\/\' character. A resourcepart can modify either a
-- @localpart\@domainpart@ address or a mere @domainpart@
-- address. Typically a resourcepart uniquely identifies a
-- specific connection (e.g., a device or location) or object
-- (e.g., an occupant in a multi-user chat room) belonging to
-- the entity associated with an XMPP localpart at a domain
-- (i.e., @localpart\@domainpart/resourcepart@).
, resourcepart :: !(Maybe Text)
} deriving Eq
instance Show Jid where
show (Jid nd dmn res) =
maybe "" ((++ "@") . Text.unpack) nd ++ Text.unpack dmn ++
maybe "" (('/' :) . Text.unpack) res
instance Read Jid where
readsPrec _ x = case fromText (Text.pack x) of
Nothing -> []
Just j -> [(j,"")]
instance IsString Jid where
fromString = fromJust . fromText . Text.pack
-- | Converts a Text to a JID.
fromText :: Text -> Maybe Jid
fromText t = do
(l, d, r) <- eitherToMaybe $ AP.parseOnly jidParts t
fromStrings l d r
where
eitherToMaybe = either (const Nothing) Just
-- | Converts localpart, domainpart, and resourcepart strings to a JID. Runs the
-- appropriate stringprep profiles and validates the parts.
fromStrings :: Maybe Text -> Text -> Maybe Text -> Maybe Jid
fromStrings l d r = do
localPart <- case l of
Nothing -> return Nothing
Just l'-> do
l'' <- SP.runStringPrep nodeprepProfile l'
guard $ validPartLength l''
let prohibMap = Set.fromList nodeprepExtraProhibitedCharacters
guard $ Text.all (`Set.notMember` prohibMap) l''
return $ Just l''
domainPart <- SP.runStringPrep (SP.namePrepProfile False) d
guard $ validDomainPart domainPart
resourcePart <- case r of
Nothing -> return Nothing
Just r' -> do
r'' <- SP.runStringPrep resourceprepProfile r'
guard $ validPartLength r''
return $ Just r''
return $ Jid localPart domainPart resourcePart
where
validDomainPart :: Text -> Bool
validDomainPart _s = True -- TODO
validPartLength :: Text -> Bool
validPartLength p = Text.length p > 0 && Text.length p < 1024
-- | Returns 'True' if the JID is /bare/, and 'False' otherwise.
isBare :: Jid -> Bool
isBare j | resourcepart j == Nothing = True
| otherwise = False
-- | Returns 'True' if the JID is /full/, and 'False' otherwise.
isFull :: Jid -> Bool
isFull = not . isBare
-- Parses an JID string and returns its three parts. It performs no validation
-- or transformations.
jidParts :: AP.Parser (Maybe Text, Text, Maybe Text)
jidParts = do
-- Read until we reach an '@', a '/', or EOF.
a <- AP.takeWhile1 (AP.notInClass ['@', '/'])
-- Case 1: We found an '@', and thus the localpart. At least the domainpart
-- is remaining. Read the '@' and until a '/' or EOF.
do
b <- domainPartP
-- Case 1A: We found a '/' and thus have all the JID parts. Read the '/'
-- and until EOF.
do
c <- resourcePartP -- Parse resourcepart
return (Just a, b, Just c)
-- Case 1B: We have reached EOF; the JID is in the form
-- localpart@domainpart.
<|> do
AP.endOfInput
return (Just a, b, Nothing)
-- Case 2: We found a '/'; the JID is in the form
-- domainpart/resourcepart.
<|> do
b <- resourcePartP
AP.endOfInput
return (Nothing, a, Just b)
-- Case 3: We have reached EOF; we have an JID consisting of only a
-- domainpart.
<|> do
AP.endOfInput
return (Nothing, a, Nothing)
where
-- Read an '@' and everything until a '/'.
domainPartP :: AP.Parser Text
domainPartP = do
_ <- AP.char '@'
AP.takeWhile1 (/= '/')
-- Read everything until a '/'.
resourcePartP :: AP.Parser Text
resourcePartP = do
_ <- AP.char '/'
AP.takeText
-- The `nodeprep' StringPrep profile.
nodeprepProfile :: SP.StringPrepProfile
nodeprepProfile = SP.Profile { SP.maps = [SP.b1, SP.b2]
, SP.shouldNormalize = True
, SP.prohibited = [SP.a1
, SP.c11
, SP.c12
, SP.c21
, SP.c22
, SP.c3
, SP.c4
, SP.c5
, SP.c6
, SP.c7
, SP.c8
, SP.c9
]
, SP.shouldCheckBidi = True
}
-- These characters needs to be checked for after normalization.
nodeprepExtraProhibitedCharacters :: [Char]
nodeprepExtraProhibitedCharacters = ['\x22', '\x26', '\x27', '\x2F', '\x3A',
'\x3C', '\x3E', '\x40']
-- The `resourceprep' StringPrep profile.
resourceprepProfile :: SP.StringPrepProfile
resourceprepProfile = SP.Profile { SP.maps = [SP.b1]
, SP.shouldNormalize = True
, SP.prohibited = [ SP.a1
, SP.c12
, SP.c21
, SP.c22
, SP.c3
, SP.c4
, SP.c5
, SP.c6
, SP.c7
, SP.c8
, SP.c9
]
, SP.shouldCheckBidi = True
}

73
source/Network/Xmpp/Marshal.hs

@ -11,7 +11,8 @@ module Network.Xmpp.Marshal where @@ -11,7 +11,8 @@ module Network.Xmpp.Marshal where
import Data.XML.Pickle
import Data.XML.Types
import Network.Xmpp.Pickle
import Data.Text
import Network.Xmpp.Types
xpStreamStanza :: PU [Node] (Either StreamErrorInfo Stanza)
@ -207,3 +208,73 @@ xpStreamError = ("xpStreamError" , "") <?+> xpWrap @@ -207,3 +208,73 @@ xpStreamError = ("xpStreamError" , "") <?+> xpWrap
(xpOption xpElemVerbatim) -- Application specific error conditions
)
)
xpLangTag :: PU [Attribute] (Maybe LangTag)
xpLangTag = xpAttrImplied xmlLang xpPrim
xmlLang :: Name
xmlLang = Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml")
-- Given a pickler and an object, produces an Element.
pickleElem :: PU [Node] a -> a -> Element
pickleElem p = pickle $ xpNodeElem p
-- Given a pickler and an element, produces an object.
unpickleElem :: PU [Node] a -> Element -> Either UnpickleError a
unpickleElem p x = unpickle (xpNodeElem p) x
xpNodeElem :: PU [Node] a -> PU Element a
xpNodeElem xp = PU { pickleTree = \x -> Prelude.head $ (pickleTree xp x) >>= \y ->
case y of
NodeElement e -> [e]
_ -> []
, unpickleTree = \x -> case unpickleTree xp $ [NodeElement x] of
Left l -> Left l
Right (a,(_,c)) -> Right (a,(Nothing,c))
}
mbl :: Maybe [a] -> [a]
mbl (Just l) = l
mbl Nothing = []
lmb :: [t] -> Maybe [t]
lmb [] = Nothing
lmb x = Just x
xpStream :: PU [Node] (Text, Maybe Jid, Maybe Jid, Maybe Text, Maybe LangTag)
xpStream = xpElemAttrs
(Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream"))
(xp5Tuple
(xpAttr "version" xpId)
(xpAttrImplied "from" xpPrim)
(xpAttrImplied "to" xpPrim)
(xpAttrImplied "id" xpId)
xpLangTag
)
-- Pickler/Unpickler for the stream features - TLS, SASL, and the rest.
xpStreamFeatures :: PU [Node] StreamFeatures
xpStreamFeatures = xpWrap
(\(tls, sasl, rest) -> StreamFeatures tls (mbl sasl) rest)
(\(StreamFeatures tls sasl rest) -> (tls, lmb sasl, rest))
(xpElemNodes
(Name
"features"
(Just "http://etherx.jabber.org/streams")
(Just "stream")
)
(xpTriple
(xpOption pickleTlsFeature)
(xpOption pickleSaslFeature)
(xpAll xpElemVerbatim)
)
)
where
pickleTlsFeature :: PU [Node] Bool
pickleTlsFeature = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-tls}starttls"
(xpElemExists "required")
pickleSaslFeature :: PU [Node] [Text]
pickleSaslFeature = xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms"
(xpAll $ xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanism" (xpContent xpId))

36
source/Network/Xmpp/Message.hs

@ -1,36 +0,0 @@ @@ -1,36 +0,0 @@
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Message
( Message(..)
, MessageError(..)
, MessageType(..)
, answerMessage
, message
) where
import Data.XML.Types
import Network.Xmpp.Types
-- | An empty message.
message :: Message
message = Message { messageID = Nothing
, messageFrom = Nothing
, messageTo = Nothing
, messageLangTag = Nothing
, messageType = Normal
, messagePayload = []
}
-- Produce an answer message with the given payload, switching the "from" and
-- "to" attributes in the original message.
answerMessage :: Message -> [Element] -> Maybe Message
answerMessage Message{messageFrom = Just frm, ..} payload =
Just Message{ messageFrom = messageTo
, messageID = Nothing
, messageTo = Just frm
, messagePayload = payload
, ..
}
answerMessage _ _ = Nothing

10
source/Network/Xmpp/Presence.hs

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
{-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Presence where
import Data.Text(Text)
import Network.Xmpp.Types
-- | Add a recipient to a presence notification.
presTo :: Presence -> Jid -> Presence
presTo pres to = pres{presenceTo = Just to}

102
source/Network/Xmpp/Sasl.hs

@ -1,11 +1,17 @@ @@ -1,11 +1,17 @@
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-}
-- Submodule for functionality related to SASL negotation:
-- authentication functions, SASL functionality, bind functionality,
-- and the legacy `{urn:ietf:params:xml:ns:xmpp-session}session'
-- functionality.
module Network.Xmpp.Sasl
( xmppSasl
, digestMd5
, scramSha1
, plain
, auth
) where
import Control.Applicative
@ -29,7 +35,6 @@ import qualified Data.Text as Text @@ -29,7 +35,6 @@ import qualified Data.Text as Text
import Data.Text (Text)
import qualified Data.Text.Encoding as Text
import Network.Xmpp.Connection_
import Network.Xmpp.Stream
import Network.Xmpp.Types
@ -40,24 +45,38 @@ import Network.Xmpp.Sasl.Mechanisms @@ -40,24 +45,38 @@ import Network.Xmpp.Sasl.Mechanisms
import Control.Concurrent.STM.TMVar
import Control.Exception
import Data.XML.Pickle
import Data.XML.Types
import Network.Xmpp.Types
import Network.Xmpp.Marshal
import Control.Monad.State(modify)
import Control.Concurrent.STM.TMVar
import Control.Monad.Error
-- | Uses the first supported mechanism to authenticate, if any. Updates the
-- state with non-password credentials and restarts the stream upon
-- success. Returns `Nothing' on success, an `AuthFailure' if
-- authentication fails, or an `XmppFailure' if anything else fails.
xmppSasl :: [SaslHandler] -- ^ Acceptable authentication mechanisms and their
-- corresponding handlers
-> TMVar Connection
-> TMVar Stream
-> IO (Either XmppFailure (Maybe AuthFailure))
xmppSasl handlers = withConnection $ do
xmppSasl handlers = withStream $ do
-- Chooses the first mechanism that is acceptable by both the client and the
-- server.
mechanisms <- gets $ saslMechanisms . cFeatures
mechanisms <- gets $ streamSaslMechanisms . streamFeatures
case (filter (\(name, _) -> name `elem` mechanisms)) handlers of
[] -> return $ Right $ Just $ AuthNoAcceptableMechanism mechanisms
(_name, handler):_ -> do
cs <- gets cState
cs <- gets streamState
case cs of
ConnectionClosed -> return . Right $ Just AuthNoConnection
Closed -> return . Right $ Just AuthNoStream
_ -> do
r <- runErrorT handler
case r of
@ -65,3 +84,74 @@ xmppSasl handlers = withConnection $ do @@ -65,3 +84,74 @@ xmppSasl handlers = withConnection $ do
Right a -> do
_ <- runErrorT $ ErrorT restartStream
return $ Right $ Nothing
-- | Authenticate to the server using the first matching method and bind a
-- resource.
auth :: [SaslHandler]
-> Maybe Text
-> TMVar Stream
-> IO (Either XmppFailure (Maybe AuthFailure))
auth mechanisms resource con = runErrorT $ do
ErrorT $ xmppSasl mechanisms con
jid <- lift $ xmppBind resource con
lift $ startSession con
return Nothing
-- Produces a `bind' element, optionally wrapping a resource.
bindBody :: Maybe Text -> Element
bindBody = pickleElem $
-- Pickler to produce a
-- "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
-- element, with a possible "<resource>[JID]</resource>"
-- child.
xpBind . xpOption $ xpElemNodes "resource" (xpContent xpId)
-- Sends a (synchronous) IQ set request for a (`Just') given or server-generated
-- resource and extract the JID from the non-error response.
xmppBind :: Maybe Text -> TMVar Stream -> IO (Either XmppFailure Jid)
xmppBind rsrc c = runErrorT $ do
answer <- ErrorT $ pushIQ "bind" Nothing Set Nothing (bindBody rsrc) c
case answer of
Right IQResult{iqResultPayload = Just b} -> do
let jid = unpickleElem xpJid b
case jid of
Right jid' -> do
ErrorT $ withStream (do
modify $ \s -> s{streamJid = Just jid'}
return $ Right jid') c -- not pretty
return jid'
otherwise -> throwError XmppOtherFailure
-- TODO: Log: ("Bind couldn't unpickle JID from " ++ show answer)
otherwise -> throwError XmppOtherFailure
where
-- Extracts the character data in the `jid' element.
xpJid :: PU [Node] Jid
xpJid = xpBind $ xpElemNodes jidName (xpContent xpPrim)
jidName = "{urn:ietf:params:xml:ns:xmpp-bind}jid"
-- A `bind' element pickler.
xpBind :: PU [Node] b -> PU [Node] b
xpBind c = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-bind}bind" c
sessionXml :: Element
sessionXml = pickleElem
(xpElemBlank "{urn:ietf:params:xml:ns:xmpp-session}session")
()
sessionIQ :: Stanza
sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess"
, iqRequestFrom = Nothing
, iqRequestTo = Nothing
, iqRequestLangTag = Nothing
, iqRequestType = Set
, iqRequestPayload = sessionXml
}
-- Sends the session IQ set element and waits for an answer. Throws an error if
-- if an IQ error stanza is returned from the server.
startSession :: TMVar Stream -> IO ()
startSession con = do
answer <- pushIQ "session" Nothing Set Nothing sessionXml con
case answer of
Left e -> error $ show e
Right _ -> return ()

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

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

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

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

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

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

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

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

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

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

454
source/Network/Xmpp/Stream.hs

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

115
source/Network/Xmpp/Tls.hs

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

345
source/Network/Xmpp/Types.hs

@ -22,7 +22,7 @@ module Network.Xmpp.Types @@ -22,7 +22,7 @@ module Network.Xmpp.Types
, PresenceType(..)
, SaslError(..)
, SaslFailure(..)
, ServerFeatures(..)
, StreamFeatures(..)
, Stanza(..)
, StanzaError(..)
, StanzaErrorCondition(..)
@ -31,19 +31,20 @@ module Network.Xmpp.Types @@ -31,19 +31,20 @@ module Network.Xmpp.Types
, XmppFailure(..)
, StreamErrorCondition(..)
, Version(..)
, ConnectionHandle(..)
, Connection(..)
, withConnection
, withConnection'
, mkConnection
, ConnectionState(..)
, StreamHandle(..)
, Stream(..)
, StreamState(..)
, StreamErrorInfo(..)
, langTag
, module Network.Xmpp.Jid
, Jid(..)
, isBare
, isFull
, fromString
, StreamEnd(..)
, InvalidXmppXml(..)
)
where
import Control.Applicative ((<$>), many)
import Control.Concurrent.STM
import Control.Exception
import Control.Monad.Error
@ -65,24 +66,30 @@ import qualified Network.TLS as TLS @@ -65,24 +66,30 @@ import qualified Network.TLS as TLS
import qualified Network as N
import Network.Xmpp.Jid
import System.IO
import Control.Applicative ((<$>), (<|>), many)
import Control.Monad(guard)
import qualified Data.Set as Set
import Data.String (IsString(..))
import qualified Text.NamePrep as SP
import qualified Text.StringPrep as SP
-- |
-- Wraps a string of random characters that, when using an appropriate
-- @IDGenerator@, is guaranteed to be unique for the Xmpp session.
-- @IdGenerator@, is guaranteed to be unique for the Xmpp session.
data StanzaID = SI !Text deriving (Eq, Ord)
data StanzaId = StanzaId !Text deriving (Eq, Ord)
instance Show StanzaID where
show (SI s) = Text.unpack s
instance Show StanzaId where
show (StanzaId s) = Text.unpack s
instance Read StanzaID where
readsPrec _ x = [(SI $ Text.pack x, "")]
instance Read StanzaId where
readsPrec _ x = [(StanzaId $ Text.pack x, "")]
instance IsString StanzaID where
fromString = SI . Text.pack
instance IsString StanzaId where
fromString = StanzaId . Text.pack
-- | The Xmpp communication primities (Message, Presence and Info/Query) are
-- called stanzas.
@ -644,8 +651,8 @@ data XmppFailure = StreamErrorFailure StreamErrorInfo -- ^ An error XML stream @@ -644,8 +651,8 @@ data XmppFailure = StreamErrorFailure StreamErrorInfo -- ^ An error XML stream
-- far.
| TlsError TLS.TLSError
| TlsNoServerSupport
| XmppNoConnection
| TlsConnectionSecured -- ^ Connection already secured
| XmppNoStream
| TlsStreamSecured -- ^ Connection already secured
| XmppOtherFailure -- ^ Undefined condition. More
-- information should be available
-- in the log.
@ -747,71 +754,253 @@ langTagParser = do @@ -747,71 +754,253 @@ langTagParser = do
tagChars :: [Char]
tagChars = ['a'..'z'] ++ ['A'..'Z']
data ServerFeatures = SF
{ stls :: !(Maybe Bool)
, saslMechanisms :: ![Text.Text]
, other :: ![Element]
data StreamFeatures = StreamFeatures
{ streamTls :: !(Maybe Bool)
, streamSaslMechanisms :: ![Text.Text]
, streamOtherFeatures :: ![Element] -- TODO: All feature elements instead?
} deriving Show
-- | Signals the state of the connection.
data ConnectionState
= ConnectionClosed -- ^ No connection at this point.
| ConnectionPlain -- ^ Connection established, but not secured.
| ConnectionSecured -- ^ Connection established and secured via TLS.
-- | Signals the state of the stream connection.
data StreamState
= Closed -- ^ No stream has been established
| Plain -- ^ Stream established, but not secured via TLS
| Secured -- ^ Stream established and secured via TLS
deriving (Show, Eq, Typeable)
-- | Defines operations for sending, receiving, flushing, and closing on a
-- connection.
data ConnectionHandle =
ConnectionHandle { cSend :: BS.ByteString -> IO Bool
, cRecv :: Int -> IO BS.ByteString
-- This is to hold the state of the XML parser (otherwise
-- we will receive EventBeginDocument events and forget
-- about name prefixes).
, cFlush :: IO ()
, cClose :: IO ()
}
data Connection = Connection
{ cState :: !ConnectionState -- ^ State of connection
, cHandle :: ConnectionHandle -- ^ Handle to send, receive, flush, and close
-- on the connection.
, cEventSource :: ResumableSource IO Event -- ^ Event conduit source, and
-- its associated finalizer
, cFeatures :: !ServerFeatures -- ^ Features as advertised by the server
, cHostName :: !(Maybe Text) -- ^ Hostname of the server
, cJid :: !(Maybe Jid) -- ^ Our JID
, cPreferredLang :: !(Maybe LangTag) -- ^ Default language when no explicit
-- stream.
data StreamHandle =
StreamHandle { streamSend :: BS.ByteString -> IO Bool
, streamReceive :: Int -> IO BS.ByteString
-- This is to hold the state of the XML parser (otherwise we
-- will receive EventBeginDocument events and forget about
-- name prefixes). (TODO: Clarify)
, streamFlush :: IO ()
, streamClose :: IO ()
}
data Stream = Stream
{ -- | State of the stream - 'Closed', 'Plain', or 'Secured'
streamState :: !StreamState -- ^ State of connection
-- | Functions to send, receive, flush, and close on the stream
, streamHandle :: StreamHandle
-- | Event conduit source, and its associated finalizer
, streamEventSource :: ResumableSource IO Event
-- | Stream features advertised by the server
, streamFeatures :: !StreamFeatures -- TODO: Maybe?
-- | The hostname we specified for the connection
, streamHostname :: !(Maybe Text)
-- | The hostname specified in the server's stream element's
-- `from' attribute
, streamFrom :: !(Maybe Jid)
-- | The identifier specified in the server's stream element's
-- `id' attribute
, streamId :: !(Maybe Text)
-- | The language tag value specified in the server's stream
-- element's `langtag' attribute; will be a `Just' value once
-- connected to the server
-- TODO: Verify
, streamLang :: !(Maybe LangTag)
-- | Our JID as assigned by the server
, streamJid :: !(Maybe Jid)
-- TODO: Move the below fields to a configuration record
, preferredLang :: !(Maybe LangTag) -- ^ Default language when no explicit
-- language tag is set
, cStreamLang :: !(Maybe LangTag) -- ^ Will be a `Just' value once connected
-- to the server.
, cStreamId :: !(Maybe Text) -- ^ Stream ID as specified by the server.
, cToJid :: !(Maybe Jid) -- ^ JID to include in the stream element's `to'
, toJid :: !(Maybe Jid) -- ^ JID to include in the stream element's `to'
-- attribute when the connection is secured. See
-- also below.
, cJidWhenPlain :: !Bool -- ^ Whether or not to also include the Jid when
, includeJidWhenPlain :: !Bool -- ^ Whether or not to also include the Jid when
-- the connection is plain.
, cFrom :: !(Maybe Jid) -- ^ From as specified by the server in the stream
-- element's `from' attribute.
}
withConnection :: StateT Connection IO (Either XmppFailure c) -> TMVar Connection -> IO (Either XmppFailure c)
withConnection action con = bracketOnError
(atomically $ takeTMVar con)
(atomically . putTMVar con )
(\c -> do
(r, c') <- runStateT action c
atomically $ putTMVar con c'
return r
)
-- nonblocking version. Changes to the connection are ignored!
withConnection' :: StateT Connection IO (Either XmppFailure b) -> TMVar Connection -> IO (Either XmppFailure b)
withConnection' action con = do
con_ <- atomically $ readTMVar con
(r, _) <- runStateT action con_
return r
mkConnection :: Connection -> IO (TMVar Connection)
mkConnection con = {- Connection `fmap` -} (atomically $ newTMVar con)
---------------
-- JID
---------------
-- | A JID is XMPP\'s native format for addressing entities in the network. It
-- is somewhat similar to an e-mail address but contains three parts instead of
-- two.
data Jid = Jid { -- | The @localpart@ of a JID is an optional identifier placed
-- before the domainpart and separated from the latter by a
-- \'\@\' character. Typically a localpart uniquely identifies
-- the entity requesting and using network access provided by a
-- server (i.e., a local account), although it can also
-- represent other kinds of entities (e.g., a chat room
-- associated with a multi-user chat service). The entity
-- represented by an XMPP localpart is addressed within the
-- context of a specific domain (i.e.,
-- @localpart\@domainpart@).
localpart :: !(Maybe Text)
-- | The domainpart typically identifies the /home/ server to
-- which clients connect for XML routing and data management
-- functionality. However, it is not necessary for an XMPP
-- domainpart to identify an entity that provides core XMPP
-- server functionality (e.g., a domainpart can identify an
-- entity such as a multi-user chat service, a
-- publish-subscribe service, or a user directory).
, domainpart :: !Text
-- | The resourcepart of a JID is an optional identifier placed
-- after the domainpart and separated from the latter by the
-- \'\/\' character. A resourcepart can modify either a
-- @localpart\@domainpart@ address or a mere @domainpart@
-- address. Typically a resourcepart uniquely identifies a
-- specific connection (e.g., a device or location) or object
-- (e.g., an occupant in a multi-user chat room) belonging to
-- the entity associated with an XMPP localpart at a domain
-- (i.e., @localpart\@domainpart/resourcepart@).
, resourcepart :: !(Maybe Text)
} deriving Eq
instance Show Jid where
show (Jid nd dmn res) =
maybe "" ((++ "@") . Text.unpack) nd ++ Text.unpack dmn ++
maybe "" (('/' :) . Text.unpack) res
instance Read Jid where
readsPrec _ x = case fromText (Text.pack x) of
Nothing -> []
Just j -> [(j,"")]
instance IsString Jid where
fromString = fromJust . fromText . Text.pack
-- | Converts a Text to a JID.
fromText :: Text -> Maybe Jid
fromText t = do
(l, d, r) <- eitherToMaybe $ AP.parseOnly jidParts t
fromStrings l d r
where
eitherToMaybe = either (const Nothing) Just
-- | Converts localpart, domainpart, and resourcepart strings to a JID. Runs the
-- appropriate stringprep profiles and validates the parts.
fromStrings :: Maybe Text -> Text -> Maybe Text -> Maybe Jid
fromStrings l d r = do
localPart <- case l of
Nothing -> return Nothing
Just l'-> do
l'' <- SP.runStringPrep nodeprepProfile l'
guard $ validPartLength l''
let prohibMap = Set.fromList nodeprepExtraProhibitedCharacters
guard $ Text.all (`Set.notMember` prohibMap) l''
return $ Just l''
domainPart <- SP.runStringPrep (SP.namePrepProfile False) d
guard $ validDomainPart domainPart
resourcePart <- case r of
Nothing -> return Nothing
Just r' -> do
r'' <- SP.runStringPrep resourceprepProfile r'
guard $ validPartLength r''
return $ Just r''
return $ Jid localPart domainPart resourcePart
where
validDomainPart :: Text -> Bool
validDomainPart _s = True -- TODO
validPartLength :: Text -> Bool
validPartLength p = Text.length p > 0 && Text.length p < 1024
-- | Returns 'True' if the JID is /bare/, and 'False' otherwise.
isBare :: Jid -> Bool
isBare j | resourcepart j == Nothing = True
| otherwise = False
-- | Returns 'True' if the JID is /full/, and 'False' otherwise.
isFull :: Jid -> Bool
isFull = not . isBare
-- Parses an JID string and returns its three parts. It performs no validation
-- or transformations.
jidParts :: AP.Parser (Maybe Text, Text, Maybe Text)
jidParts = do
-- Read until we reach an '@', a '/', or EOF.
a <- AP.takeWhile1 (AP.notInClass ['@', '/'])
-- Case 1: We found an '@', and thus the localpart. At least the domainpart
-- is remaining. Read the '@' and until a '/' or EOF.
do
b <- domainPartP
-- Case 1A: We found a '/' and thus have all the JID parts. Read the '/'
-- and until EOF.
do
c <- resourcePartP -- Parse resourcepart
return (Just a, b, Just c)
-- Case 1B: We have reached EOF; the JID is in the form
-- localpart@domainpart.
<|> do
AP.endOfInput
return (Just a, b, Nothing)
-- Case 2: We found a '/'; the JID is in the form
-- domainpart/resourcepart.
<|> do
b <- resourcePartP
AP.endOfInput
return (Nothing, a, Just b)
-- Case 3: We have reached EOF; we have an JID consisting of only a
-- domainpart.
<|> do
AP.endOfInput
return (Nothing, a, Nothing)
where
-- Read an '@' and everything until a '/'.
domainPartP :: AP.Parser Text
domainPartP = do
_ <- AP.char '@'
AP.takeWhile1 (/= '/')
-- Read everything until a '/'.
resourcePartP :: AP.Parser Text
resourcePartP = do
_ <- AP.char '/'
AP.takeText
-- The `nodeprep' StringPrep profile.
nodeprepProfile :: SP.StringPrepProfile
nodeprepProfile = SP.Profile { SP.maps = [SP.b1, SP.b2]
, SP.shouldNormalize = True
, SP.prohibited = [SP.a1
, SP.c11
, SP.c12
, SP.c21
, SP.c22
, SP.c3
, SP.c4
, SP.c5
, SP.c6
, SP.c7
, SP.c8
, SP.c9
]
, SP.shouldCheckBidi = True
}
-- These characters needs to be checked for after normalization.
nodeprepExtraProhibitedCharacters :: [Char]
nodeprepExtraProhibitedCharacters = ['\x22', '\x26', '\x27', '\x2F', '\x3A',
'\x3C', '\x3E', '\x40']
-- The `resourceprep' StringPrep profile.
resourceprepProfile :: SP.StringPrepProfile
resourceprepProfile = SP.Profile { SP.maps = [SP.b1]
, SP.shouldNormalize = True
, SP.prohibited = [ SP.a1
, SP.c12
, SP.c21
, SP.c22
, SP.c3
, SP.c4
, SP.c5
, SP.c6
, SP.c7
, SP.c8
, SP.c9
]
, SP.shouldCheckBidi = True
}
data StreamEnd = StreamEnd deriving (Typeable, Show)
instance Exception StreamEnd
data InvalidXmppXml = InvalidXmppXml String deriving (Show, Typeable)
instance Exception InvalidXmppXml

82
source/Network/Xmpp/Utilities.hs

@ -1,8 +1,9 @@ @@ -1,8 +1,9 @@
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Network.Xmpp.Utilities (idGenerator) where
{-# OPTIONS_HADDOCK hide #-}
module Network.Xmpp.Utilities (presTo, message, answerMessage, openElementToEvents, renderOpenElement, renderElement) where
import Network.Xmpp.Types
@ -10,10 +11,29 @@ import Control.Monad.STM @@ -10,10 +11,29 @@ import Control.Monad.STM
import Control.Concurrent.STM.TVar
import Prelude
import Data.XML.Types
import qualified Data.Attoparsec.Text as AP
import qualified Data.Text as Text
import qualified Data.ByteString as BS
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import System.IO.Unsafe(unsafePerformIO)
import Data.Conduit.List as CL
-- import Data.Typeable
import Control.Applicative ((<$>))
import Control.Exception
import Control.Monad.Trans.Class
import Data.Conduit as C
import Data.XML.Types
import qualified Text.XML.Stream.Render as TXSR
import Text.XML.Unresolved as TXU
-- TODO: Not used, and should probably be removed.
-- | Creates a new @IdGenerator@. Internally, it will maintain an infinite list
-- 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
@ -36,11 +56,11 @@ idGenerator prefix = atomically $ do @@ -36,11 +56,11 @@ idGenerator prefix = atomically $ do
-- 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 = map (\ id -> Text.append p id) ids'
ids p = Prelude.map (\ id -> Text.append p id) ids'
where
-- Generate all combinations of IDs, with increasing length.
ids' :: [Text.Text]
ids' = map Text.pack $ concatMap ids'' [1..]
ids' = Prelude.map Text.pack $ Prelude.concatMap ids'' [1..]
-- Generates all combinations of IDs with the given length.
ids'' :: Integer -> [String]
ids'' 0 = [""]
@ -52,3 +72,55 @@ idGenerator prefix = atomically $ do @@ -52,3 +72,55 @@ idGenerator prefix = atomically $ do
-- Constructs a "Version" based on the major and minor version numbers.
versionFromNumbers :: Integer -> Integer -> Version
versionFromNumbers major minor = Version major minor
-- | Add a recipient to a presence notification.
presTo :: Presence -> Jid -> Presence
presTo pres to = pres{presenceTo = Just to}
-- | An empty message.
message :: Message
message = Message { messageID = Nothing
, messageFrom = Nothing
, messageTo = Nothing
, messageLangTag = Nothing
, messageType = Normal
, messagePayload = []
}
-- Produce an answer message with the given payload, switching the "from" and
-- "to" attributes in the original message.
answerMessage :: Message -> [Element] -> Maybe Message
answerMessage Message{messageFrom = Just frm, ..} payload =
Just Message{ messageFrom = messageTo
, messageID = Nothing
, messageTo = Just frm
, messagePayload = payload
, ..
}
answerMessage _ _ = Nothing
openElementToEvents :: Element -> [Event]
openElementToEvents (Element name as ns) = EventBeginElement name as : goN ns []
where
goE (Element name' as' ns') =
(EventBeginElement name' as' :)
. goN ns'
. (EventEndElement name' :)
goN [] = id
goN [x] = goN' x
goN (x:xs) = goN' x . goN xs
goN' (NodeElement e) = goE e
goN' (NodeInstruction i) = (EventInstruction i :)
goN' (NodeContent c) = (EventContent c :)
goN' (NodeComment t) = (EventComment t :)
renderOpenElement :: Element -> BS.ByteString
renderOpenElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO
$ CL.sourceList (openElementToEvents e) $$ TXSR.renderText def =$ CL.consume
renderElement :: Element -> BS.ByteString
renderElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO
$ CL.sourceList (elementToEvents e) $$ TXSR.renderText def =$ CL.consume
where
elementToEvents :: Element -> [Event]
elementToEvents e@(Element name _ _) = openElementToEvents e ++ [EventEndElement name]

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

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

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

@ -25,11 +25,7 @@ import Data.XML.Pickle @@ -25,11 +25,7 @@ import Data.XML.Pickle
import Data.XML.Types
import Network.Xmpp
import Network.Xmpp.Concurrent
import Network.Xmpp.Concurrent.Types
import Network.Xmpp.Connection_
import Network.Xmpp.Pickle
import Network.Xmpp.Types
import Network.Xmpp.Internal
import Control.Concurrent.STM.TMVar
data DiscoError = DiscoNoQueryElement
@ -105,10 +101,10 @@ queryInfo to node context = do @@ -105,10 +101,10 @@ queryInfo to node context = do
xmppQueryInfo :: Maybe Jid
-> Maybe Text.Text
-> TMVar Connection
-> TMVar Stream
-> IO (Either DiscoError QueryInfoResult)
xmppQueryInfo to node con = do
res <- pushIQ' "info" to Get Nothing queryBody con
res <- pushIQ "info" to Get Nothing queryBody con
return $ case res of
Left e -> Left $ DiscoIQError Nothing
Right res' -> case res' of
@ -167,3 +163,27 @@ queryItems to node session = do @@ -167,3 +163,27 @@ queryItems to node session = do
Right r -> Right r
where
queryBody = pickleElem xpQueryItems (node, [])
-- Given a pickler and an object, produces an Element.
pickleElem :: PU [Node] a -> a -> Element
pickleElem p = pickle $ xpNodeElem p
-- Given a pickler and an element, produces an object.
unpickleElem :: PU [Node] a -> Element -> Either UnpickleError a
unpickleElem p x = unpickle (xpNodeElem p) x
xpNodeElem :: PU [Node] a -> PU Element a
xpNodeElem xp = PU { pickleTree = \x -> Prelude.head $ (pickleTree xp x) >>= \y ->
case y of
NodeElement e -> [e]
_ -> []
, unpickleTree = \x -> case unpickleTree xp $ [NodeElement x] of
Left l -> Left l
Right (a,(_,c)) -> Right (a,(Nothing,c))
}
xpLangTag :: PU [Attribute] (Maybe LangTag)
xpLangTag = xpAttrImplied xmlLang xpPrim
xmlLang :: Name
xmlLang = Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml")

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

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