diff --git a/import_visualisation-new-full.png b/import_visualisation-new-full.png
new file mode 100644
index 0000000..75b6ba9
Binary files /dev/null and b/import_visualisation-new-full.png differ
diff --git a/import_visualisation-new.png b/import_visualisation-new.png
new file mode 100644
index 0000000..f8c7bdc
Binary files /dev/null and b/import_visualisation-new.png differ
diff --git a/pontarius-xmpp.cabal b/pontarius-xmpp.cabal
index 3b71b17..f34e821 100644
--- a/pontarius-xmpp.cabal
+++ b/pontarius-xmpp.cabal
@@ -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
, 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
, 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
diff --git a/source/Data/Conduit/Tls.hs b/source/Data/Conduit/Tls.hs
deleted file mode 100644
index 0842ae5..0000000
--- a/source/Data/Conduit/Tls.hs
+++ /dev/null
@@ -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'
diff --git a/source/Network/Xmpp.hs b/source/Network/Xmpp.hs
index d0e2e9c..f545cba 100644
--- a/source/Network/Xmpp.hs
+++ b/source/Network/Xmpp.hs
@@ -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
, 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
, AuthFailure( AuthXmlFailure -- Does not export AuthStreamFailure
, AuthNoAcceptableMechanism
, AuthChallengeFailure
- , AuthNoConnection
+ , AuthNoStream
, AuthFailure
, AuthSaslFailure
, AuthStringPrepFailure )
@@ -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
diff --git a/source/Network/Xmpp/Bind.hs b/source/Network/Xmpp/Bind.hs
deleted file mode 100644
index a3676e6..0000000
--- a/source/Network/Xmpp/Bind.hs
+++ /dev/null
@@ -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
- -- ""
- -- element, with a possible "[JID]"
- -- 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
diff --git a/source/Network/Xmpp/Concurrent.hs b/source/Network/Xmpp/Concurrent.hs
index fa94910..b6df58c 100644
--- a/source/Network/Xmpp/Concurrent.hs
+++ b/source/Network/Xmpp/Concurrent.hs
@@ -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
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
-- | 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
, 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
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)
diff --git a/source/Network/Xmpp/Concurrent/Monad.hs b/source/Network/Xmpp/Concurrent/Monad.hs
index ff0f07a..5a1d627 100644
--- a/source/Network/Xmpp/Concurrent/Monad.hs
+++ b/source/Network/Xmpp/Concurrent/Monad.hs
@@ -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 ()
closeConnection session = Ex.mask_ $ do
(_send, connection) <- atomically $ liftM2 (,)
(takeTMVar $ writeRef session)
- (takeTMVar $ conRef session)
+ (takeTMVar $ streamRef session)
_ <- closeStreams connection
return ()
diff --git a/source/Network/Xmpp/Concurrent/Threads.hs b/source/Network/Xmpp/Concurrent/Threads.hs
index c55fc16..452aa4c 100644
--- a/source/Network/Xmpp/Concurrent/Threads.hs
+++ b/source/Network/Xmpp/Concurrent/Threads.hs
@@ -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
-- 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 =
-- 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 =
-- 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
diff --git a/source/Network/Xmpp/Concurrent/Types.hs b/source/Network/Xmpp/Concurrent/Types.hs
index 0d61e93..e753f05 100644
--- a/source/Network/Xmpp/Concurrent/Types.hs
+++ b/source/Network/Xmpp/Concurrent/Types.hs
@@ -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 ()
}
diff --git a/source/Network/Xmpp/IM/Message.hs b/source/Network/Xmpp/IM/Message.hs
index 505a27e..e5aa830 100644
--- a/source/Network/Xmpp/IM/Message.hs
+++ b/source/Network/Xmpp/IM/Message.hs
@@ -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
diff --git a/source/Network/Xmpp/Connection.hs b/source/Network/Xmpp/Internal.hs
similarity index 56%
rename from source/Network/Xmpp/Connection.hs
rename to source/Network/Xmpp/Internal.hs
index d1dddd5..790deaa 100644
--- a/source/Network/Xmpp/Connection.hs
+++ b/source/Network/Xmpp/Internal.hs
@@ -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
diff --git a/source/Network/Xmpp/Jid.hs b/source/Network/Xmpp/Jid.hs
deleted file mode 100644
index bb80884..0000000
--- a/source/Network/Xmpp/Jid.hs
+++ /dev/null
@@ -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
- }
\ No newline at end of file
diff --git a/source/Network/Xmpp/Marshal.hs b/source/Network/Xmpp/Marshal.hs
index 9b78c4c..3e9ab5e 100644
--- a/source/Network/Xmpp/Marshal.hs
+++ b/source/Network/Xmpp/Marshal.hs
@@ -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
(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))
diff --git a/source/Network/Xmpp/Message.hs b/source/Network/Xmpp/Message.hs
deleted file mode 100644
index 875421f..0000000
--- a/source/Network/Xmpp/Message.hs
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/source/Network/Xmpp/Presence.hs b/source/Network/Xmpp/Presence.hs
deleted file mode 100644
index c859f14..0000000
--- a/source/Network/Xmpp/Presence.hs
+++ /dev/null
@@ -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}
\ No newline at end of file
diff --git a/source/Network/Xmpp/Sasl.hs b/source/Network/Xmpp/Sasl.hs
index 2a61ae2..d338c0c 100644
--- a/source/Network/Xmpp/Sasl.hs
+++ b/source/Network/Xmpp/Sasl.hs
@@ -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
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
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
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
+ -- ""
+ -- element, with a possible "[JID]"
+ -- 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 ()
diff --git a/source/Network/Xmpp/Sasl/Common.hs b/source/Network/Xmpp/Sasl/Common.hs
index e3dcc5c..eea0ce7 100644
--- a/source/Network/Xmpp/Sasl/Common.hs
+++ b/source/Network/Xmpp/Sasl/Common.hs
@@ -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
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
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
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
_ -> 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
-- | 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
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
-- | 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
diff --git a/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs b/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs
index f8fc03c..bca3ab5 100644
--- a/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs
+++ b/source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs
@@ -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
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
diff --git a/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs b/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs
index 6f1626e..3e85a50 100644
--- a/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs
+++ b/source/Network/Xmpp/Sasl/Mechanisms/Plain.hs
@@ -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
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)
diff --git a/source/Network/Xmpp/Sasl/Mechanisms/Scram.hs b/source/Network/Xmpp/Sasl/Mechanisms/Scram.hs
index e9cebc7..4262c63 100644
--- a/source/Network/Xmpp/Sasl/Mechanisms/Scram.hs
+++ b/source/Network/Xmpp/Sasl/Mechanisms/Scram.hs
@@ -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)
-> 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
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
diff --git a/source/Network/Xmpp/Sasl/Types.hs b/source/Network/Xmpp/Sasl/Types.hs
index 90f20da..c341585 100644
--- a/source/Network/Xmpp/Sasl/Types.hs
+++ b/source/Network/Xmpp/Sasl/Types.hs
@@ -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
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) ())
diff --git a/source/Network/Xmpp/Stream.hs b/source/Network/Xmpp/Stream.hs
index 5688dec..769955b 100644
--- a/source/Network/Xmpp/Stream.hs
+++ b/source/Network/Xmpp/Stream.hs
@@ -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)
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
-- 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
, 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) ->
-- 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
, Maybe Jid
, Maybe Text
, Maybe LangTag
- , ServerFeatures ))
+ , StreamFeatures ))
streamS expectedTo = do
header <- xmppStreamHeader
case header of
@@ -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 "" 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 ""
+ 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) ""
+
+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)
diff --git a/source/Network/Xmpp/Tls.hs b/source/Network/Xmpp/Tls.hs
index 0d5754e..88cf37e 100644
--- a/source/Network/Xmpp/Tls.hs
+++ b/source/Network/Xmpp/Tls.hs
@@ -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
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 ", waits for "", 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'
diff --git a/source/Network/Xmpp/Types.hs b/source/Network/Xmpp/Types.hs
index f36548d..182a47b 100644
--- a/source/Network/Xmpp/Types.hs
+++ b/source/Network/Xmpp/Types.hs
@@ -22,7 +22,7 @@ module Network.Xmpp.Types
, PresenceType(..)
, SaslError(..)
, SaslFailure(..)
- , ServerFeatures(..)
+ , StreamFeatures(..)
, Stanza(..)
, StanzaError(..)
, StanzaErrorCondition(..)
@@ -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
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
-- 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
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
diff --git a/source/Network/Xmpp/Utilities.hs b/source/Network/Xmpp/Utilities.hs
index 11441a8..8b4864f 100644
--- a/source/Network/Xmpp/Utilities.hs
+++ b/source/Network/Xmpp/Utilities.hs
@@ -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
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
-- 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
-- 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]
diff --git a/source/Network/Xmpp/Xep/InbandRegistration.hs b/source/Network/Xmpp/Xep/InbandRegistration.hs
index 27deeb8..bbbc1a5 100644
--- a/source/Network/Xmpp/Xep/InbandRegistration.hs
+++ b/source/Network/Xmpp/Xep/InbandRegistration.hs
@@ -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"
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
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)
-- | 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
-- | 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
-- Registered
-- Instructions
+
+ppElement :: Element -> String
+ppElement = Text.unpack . Text.decodeUtf8 . renderElement
diff --git a/source/Network/Xmpp/Xep/ServiceDiscovery.hs b/source/Network/Xmpp/Xep/ServiceDiscovery.hs
index d5325e0..be654ff 100644
--- a/source/Network/Xmpp/Xep/ServiceDiscovery.hs
+++ b/source/Network/Xmpp/Xep/ServiceDiscovery.hs
@@ -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
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
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")
diff --git a/source/Text/Xml/Stream/Elements.hs b/source/Text/Xml/Stream/Elements.hs
deleted file mode 100644
index a357607..0000000
--- a/source/Text/Xml/Stream/Elements.hs
+++ /dev/null
@@ -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
\ No newline at end of file