pontarius-xmpp 0.0.3.0 → 0.5.7.2
raw patch · 67 files changed
Files
- ChangeLog.md +15/−0
- Examples/EchoClient.hs +0/−73
- LICENSE +0/−28
- LICENSE.md +35/−0
- Network/XMPP.hs +0/−66
- Network/XMPP/JID.hs +0/−133
- Network/XMPP/SASL.hs +0/−194
- Network/XMPP/Session.hs +0/−906
- Network/XMPP/Stanza.hs +0/−237
- Network/XMPP/Utilities.hs +0/−109
- README +0/−23
- README.md +112/−0
- Setup.hs +0/−1
- XTLS.hs +0/−75
- benchmarks/Bench.hs +12/−0
- examples/echoclient/LICENSE.md +2/−0
- examples/echoclient/Main.hs +43/−0
- examples/echoclient/README.md +6/−0
- examples/echoclient/Setup.hs +2/−0
- examples/echoclient/echoclient.cabal +23/−0
- install.sh +0/−5
- pontarius-xmpp.cabal +205/−87
- source/Network/Xmpp.hs +255/−0
- source/Network/Xmpp/Concurrent.hs +386/−0
- source/Network/Xmpp/Concurrent/Basic.hs +87/−0
- source/Network/Xmpp/Concurrent/IQ.hs +213/−0
- source/Network/Xmpp/Concurrent/Message.hs +96/−0
- source/Network/Xmpp/Concurrent/Monad.hs +118/−0
- source/Network/Xmpp/Concurrent/Presence.hs +54/−0
- source/Network/Xmpp/Concurrent/Threads.hs +131/−0
- source/Network/Xmpp/Concurrent/Types.hs +197/−0
- source/Network/Xmpp/IM.hs +42/−0
- source/Network/Xmpp/IM/Message.hs +116/−0
- source/Network/Xmpp/IM/Presence.hs +77/−0
- source/Network/Xmpp/IM/PresenceTracker.hs +111/−0
- source/Network/Xmpp/IM/PresenceTracker/Types.hs +20/−0
- source/Network/Xmpp/IM/Roster.hs +254/−0
- source/Network/Xmpp/IM/Roster/Types.hs +52/−0
- source/Network/Xmpp/Internal.hs +73/−0
- source/Network/Xmpp/Lens.hs +740/−0
- source/Network/Xmpp/Marshal.hs +536/−0
- source/Network/Xmpp/Sasl.hs +155/−0
- source/Network/Xmpp/Sasl/Common.hs +238/−0
- source/Network/Xmpp/Sasl/Mechanisms.hs +10/−0
- source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs +122/−0
- source/Network/Xmpp/Sasl/Mechanisms/Plain.hs +59/−0
- source/Network/Xmpp/Sasl/Mechanisms/Scram.hs +163/−0
- source/Network/Xmpp/Sasl/StringPrep.hs +37/−0
- source/Network/Xmpp/Sasl/Types.hs +21/−0
- source/Network/Xmpp/Stanza.hs +124/−0
- source/Network/Xmpp/Stream.hs +917/−0
- source/Network/Xmpp/Tls.hs +204/−0
- source/Network/Xmpp/Types.hs +1311/−0
- source/Network/Xmpp/Utilities.hs +93/−0
- tests/Main.hs +13/−0
- tests/Run.hs +14/−0
- tests/Run/Config.hs +36/−0
- tests/Run/Google.hs +47/−0
- tests/Run/Payload.hs +117/−0
- tests/Run/SendReceive.hs +98/−0
- tests/Tests/Arbitrary.hs +6/−0
- tests/Tests/Arbitrary/Common.hs +27/−0
- tests/Tests/Arbitrary/Xml.hs +85/−0
- tests/Tests/Arbitrary/Xmpp.hs +155/−0
- tests/Tests/Parsers.hs +63/−0
- tests/Tests/Picklers.hs +89/−0
- tests/Tests/Stream.hs +46/−0
+ ChangeLog.md view
@@ -0,0 +1,15 @@+# 0.5.1 to 0.6.0+* Changed roster update callback to take RosterUpdate type+* Added onrosterPushL lens+* Removed legacy `crypto-random` (this caused issues when compiling with GHC 9.2.5)++# 0.5.0 to 0.5.1+* Fixed input logger choking on long non-ascii messages++# 0.4.5 to 0.5.0+* Support for the session element is now determined from stream features, the+ establishSession option was removed+* The parser can now handle nonzas. Nonzas can be handled with a plugin+* An initial roster can now be set with the initialRoster session configuration+ option+* The JID parser can now handle "/" and "@" characters in the resource part
− Examples/EchoClient.hs
@@ -1,73 +0,0 @@-{---This file (EchoClient.hs) illustrates how to connect, authenticate and set a-presence with Pontarius XMPP. The contents of this file may be used freely, as-if it is in the public domain.---}---import Network.XMPP--import Control.Concurrent.Chan (Chan, readChan, writeChan)-import Data.Maybe-import Data.XML.Types-import qualified Data.Text as DT----- Account and server details.--host = "test.pontarius.org"-userName = "sprint3-xmpp"-server = "test.pontarius.org"-port = 5222-resource = "echo-client"-password = ""----- Creates a Pontarius XMPP session and requests to connect.--main :: IO ()-main =- do (inEvents, outEvents) <- createSession- writeChan outEvents $ XOEConnect host port- loop inEvents outEvents----- All "in" events (incoming events to this program, events from Pontarius XMPP)--- are being processed in this loop. A state variable could be added here.--loop :: Chan XMPPInEvent -> Chan XMPPOutEvent -> IO ()-loop c c_ =- do e <- readChan c- -- putStrLn $ "XMPPInEvent received: " ++ (show e)- processEvent e- loop c c_- where- processEvent :: XMPPInEvent -> IO ()- - -- When connected, authenticate- processEvent XIEConnectionSucceeded =- writeChan c_ $ XOEAuthenticate userName password resource- - -- When authenticated, set presence- processEvent XIEAuthenticationSucceeded =- writeChan c_ $ XOEPresence $- presence Nothing Nothing Nothing Nothing Available []- - -- Auto-accept subscriptions when asked and ask for subscription- processEvent (XIEPresence (Presence { presenceStanza = stanza- , presenceType = Subscribe })) = do- let jid = stanzaFrom stanza- writeChan c_ $ XOEPresence $ presence Nothing Nothing jid Nothing- Subscribed []- writeChan c_ $ XOEPresence $ presence Nothing Nothing jid Nothing- Subscribe []- return ()- - -- Echo messages- processEvent (XIEMessage m) =- writeChan c_ $ XOEMessage $- message Nothing Nothing (stanzaFrom $ messageStanza m) Nothing- (messageType m) (messagePayload m)- processEvent _ = return ()
− LICENSE
@@ -1,28 +0,0 @@-Copyright © 2010-2011, Jon Kristensen--All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:-- * Redistributions of source code must retain the above copyright notice,- this list of conditions and the following disclaimer.-- * Redistributions in binary form must reproduce the above copyright notice,- this list of conditions and the following disclaimer in the documentation- and/or other materials provided with the distribution.-- * Neither the name of Pontarius, Pontarius XMPP nor the names of its- contributors may be used to endorse or promote products derived from this- software without specific prior written permission.--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE-DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,-INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY-OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,-EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ LICENSE.md view
@@ -0,0 +1,35 @@+Copyright © 2005-2011 Dmitry Astapov +Copyright © 2005-2011 Pierre Kovalev +Copyright © 2010-2011 Mahdi Abdinejadi +Copyright © 2010-2013 Jon Kristensen +Copyright © 2011 IETF Trust +Copyright © 2012-2013 Philipp Balzarek++All rights reserved.++Pontarius XMPP is licensed under the three-clause BSD license.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++* Neither the name of the Pontarius project nor the names of its contributors+ may be used to endorse or promote products derived from this software without+ specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR THE PONTARIUS PROJECT BE+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
− Network/XMPP.hs
@@ -1,66 +0,0 @@--- | Module: $Header$--- Description: A minimalistic and easy-to-use XMPP library--- Copyright: Copyright © 2010-2011 Jon Kristensen--- License: BSD-3--- --- Maintainer: info@pontarius.org--- Stability: unstable--- Portability: portable---- | Pontarius XMPP aims to be a secure, concurrent/event-based and easy-to-use--- XMPP library for Haskell. It is being actively developed.--- --- Note that we are not recommending anyone to use Pontarius XMPP at this time--- as it's still in an experimental stage and will have its API and data types--- modified frequently. See the project's web site at--- <http://www.pontarius.org/projects/pontarius-xmpp/> for more information.--- --- This module will be documented soon.--module Network.XMPP ( -- Network.XMPP.JID- JID (..)- , jid- , jidIsFull- , jidIsBare- , stringToJID- , jidToString- - -- Network.XMPP.SASL- , replyToChallenge1- - -- Network.XMPP.Session- , XMPPInEvent (..)- , XMPPOutEvent (..)- , createSession- - -- Network.XMPP.Stanza- , StanzaID (SID)- , From- , To- , XMLLang- , Stanza (..)- , MessageType (..)- , Message (..)- , message- , PresenceType (..)- , Presence (..)- , presence- , IQ (..)- , iqGet- , iqSet- , iqResult- , iqStanza- , iqPayloadNamespace- , iqPayload- - -- Network.XMPP.Utilities- , elementToString- , elementsToString- , getID- , getID_ ) where--import Network.XMPP.JID-import Network.XMPP.SASL-import Network.XMPP.Session-import Network.XMPP.Stanza-import Network.XMPP.Utilities
− Network/XMPP/JID.hs
@@ -1,133 +0,0 @@--- | Module: $Header$--- Description: JabberID (JID) data type and utility functions--- Copyright: Copyright © 2010-2011 Jon Kristensen--- License: BSD-3--- --- Maintainer: info@pontarius.org--- Stability: unstable--- Portability: portable--- --- JIDs are written in the format of `node@server/resource'. An example of a--- JID is `jonkri@jabber.org'.--- --- The node identifier is the part before the `@' character in Jabber IDs.--- Node names are optional. The server identifier is the part after the `@'--- character in Jabber IDs (and before the `/' character). The server--- identifier is the only required field of a JID. The server identifier MAY--- be an IP address but SHOULD be a fully qualified domain name. The resource--- identifier is the part after the `/' character in Jabber IDs. Like with--- node names, the resource identifier is optional.--- --- A JID without a resource identifier (i.e. a JID in the form of--- `node@server') is called `bare JID'. A JID with a resource identifier is--- called `full JID'.---- Node identifiers MUST be formatted in such a way so that the Nodeprep profile--- (see RFC 3920: XMPP Core, Appendix A) of RFC 3454: Preparation of--- Internationalized Strings (`stringprep') can be applied without failing.--- Servers MUST and clients SHOULD apply the Nodeprep profile to node names--- prior to comparing them. Node identifiers MUST NOT be more than 1023 bytes in--- length.------ If it is a domain name it MUST be an `internationalized domain name' as--- defined in RFC 3490: Internationalizing Domain Names in Applications (IDNA),--- to which RFC 3491: Nameprep: A Stringprep Profile for Internationalized--- Domain Names (IDN) can be applied without failing. Servers MUST and clients--- SHOULD first apply the Nameprep profile to the domain names prior to--- comparing them. Like with node names, server identifiers MUST NOT be more--- than 1023 bytes in length.------ A resource identifier has to be formatted in such a way so that the--- Resourceprep profile of RFC 3454: Preparation of Internationalized Strings--- (`stringprep') can be applied without failing. Servers MUST and clients--- SHOULD first apply the Resourceprep profile (see RFC 3920: XMPP Core,--- Appendix B) to resource names prior to comparing them. The resource--- identifier MUST MOT be more than 1023 bytes in length.------ Given the length of the `@' and `/' characters as well as the restrictions--- imposed on the node, server and resource identifiers, a JID will never be--- longer than 3071 bytes.---- TODO: Make the regular expression only match valid JIDs.--- TODO: Use Perl regular expressions to use non-capturing groups with "(?:"?--- TODO: Validate the input in the jid and stringToJID functions.--module Network.XMPP.JID ( JID (jidNode, jidServer, jidResource)- , jid- , jidIsFull- , jidIsBare- , stringToJID- , jidToString ) where--import Text.Regex.Posix ((=~))----- JID is a data type that has to be constructed in this module using either jid--- or stringToJID.--data JID = JID { jidNode :: Maybe String- , jidServer :: String- , jidResource :: Maybe String } deriving (Eq, Show)----- | Simple function to construct a JID. We will add validation to this function--- in a later release.--jid :: Maybe String -> String -> Maybe String -> JID-jid n s r = JID { jidNode = n, jidServer = s, jidResource = r }----- | Converts a (JID) String to a JID record.--stringToJID :: String -> Maybe JID--stringToJID string = matchToJID $ string =~ "^(([^@]+)@)?([^/]+)(/(.+))?$"- where- matchToJID [[_, _, "", server, _, ""]] =- Just JID { jidNode = Nothing, jidServer = server, jidResource = Nothing }- matchToJID [[_, _, node, server, _, ""]] =- Just JID { jidNode = Just node- , jidServer = server- , jidResource = Nothing }- matchToJID [[_, _, "", server, _, resource]] =- Just JID { jidNode = Nothing- , jidServer = server- , jidResource = Just resource }- matchToJID [[_, _, node, server, _, resource]] =- Just JID { jidNode = Just node- , jidServer = server- , jidResource = Just resource }- matchToJID _ = Nothing----- | Converts a JID to a String.--jidToString :: JID -> String--jidToString JID { jidNode = n, jidServer = s, jidResource = r }- | n == Nothing && r == Nothing = s- | r == Nothing = let Just n' = n in n' ++ "@" ++ s- | n == Nothing = let Just r' = r in s ++ "/" ++ r'- | otherwise = let Just n' = n; Just r' = r- in n' ++ "@" ++ s ++ "/" ++ r'----- | JIDs are written in the format of `node@server/resource'. A JID without a--- resource identifier (i.e. a JID in the form of `server' or `node@server')--- is called a `bare JID'. A JID with a resource identifier is called `full--- JID'. This function returns True if the JID is `bare' and False otherwise.--jidIsBare :: JID -> Bool-jidIsBare j | jidResource j == Nothing = True- | otherwise = False----- | JIDs are written in the format of `node@server/resource'. A JID without a--- resource identifier (i.e. a JID in the form of `server' or `node@server')--- is called a `bare JID'. A JID with a resource identifier is called `full--- JID'. This function returns True if the JID is `full' and False otherwise.---- This function is defined in terms of (not) jidIsBare.--jidIsFull :: JID -> Bool-jidIsFull jid = not $ jidIsBare jid
− Network/XMPP/SASL.hs
@@ -1,194 +0,0 @@--- TODO: Make it possible to include host.--- TODO: Host is assumed to be ISO 8859-1; make list of assumptions.--- TODO: Can it contain newline characters?--module Network.XMPP.SASL (replyToChallenge1) where--import Data.ByteString.Internal (c2w)-import Data.Char (isLatin1)-import Data.Digest.Pure.MD5-import qualified Data.Binary as DBi (Binary, encode)-import qualified Data.ByteString.Lazy as DBL (ByteString, append, pack,- fromChunks, toChunks, null)-import qualified Data.ByteString.Lazy.Char8 as DBLC (append, pack, unpack)-import qualified Data.List as DL---data Challenge1Error = C1MultipleCriticalAttributes |- C1NotAllParametersPresent |- C1SomeParamtersPresentMoreThanOnce |- C1WrongRealm |- C1UnsupportedAlgorithm |- C1UnsupportedCharset |- C1UnsupportedQOP- deriving Show----- Will produce a list of key-value pairs given a string in the format of--- realm="somerealm",nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8...-stringToList :: String -> [(String, String)]-stringToList "" = []-stringToList s' = let (next, rest) = break' s' ','- in break' next '=' : stringToList rest- where- -- Like break, but will remove the first char of the continuation, if- -- present.- break' :: String -> Char -> (String, String)- break' s' c = let (first, second) = break ((==) c) s'- in (first, removeCharIfPresent second c)- - -- Removes the first character, if present; "=hello" with '=' becomes- -- "hello".- removeCharIfPresent :: String -> Char -> String- removeCharIfPresent [] _ = []- removeCharIfPresent (c:t) c' | c == c' = t- removeCharIfPresent s' c = s'---- Counts the number of directives in the pair list.-countDirectives :: String -> [(String, String)] -> Int-countDirectives v l = DL.length $ filter (isEntry v) l- where- isEntry :: String -> (String, String) -> Bool- isEntry name (name', _) | name == name' = True- | otherwise = False----- Returns the given directive in the list of pairs, or Nothing.-lookupDirective :: String -> [(String, String)] -> Maybe String-lookupDirective d [] = Nothing-lookupDirective d ((d', v):t) | d == d' = Just v- | otherwise = lookupDirective d t----- Returns the given directive in the list of pairs, or the default value--- otherwise.-lookupDirectiveWithDefault :: String -> [(String, String)] -> String -> String-lookupDirectiveWithDefault di l de- | lookup == Nothing = de- | otherwise = let Just r = lookup in r- where- lookup = lookupDirective di l----- Takes a challenge string (which is not Base64-encoded), the host name of the--- Jabber server, the Jabber user name (JID), the password and a random and--- unique "cnonce" value and generates either an error or a response to that--- challenge.---- We have broken replyToChallenge1 for non-TLS authentication. In order to--- change it back, just uncomment the lines relevant to the realm and match it--- in the C1NotAllParametersSet case.--replyToChallenge1 :: String -> String -> String -> String -> String ->- Either String Challenge1Error-replyToChallenge1 s h u p c =- -- Remove all new line characters.- let list = stringToList $ filter (/= '\n') s- in -- Count that there are no more than one nonce or algorithm directives.- case countDirectives "nonce" list <= 1 &&- countDirectives "algorithm" list <= 1 of- True ->- let -- realm = lookupDirective "realm" list- nonce = lookupDirective "nonce" list- qop = lookupDirectiveWithDefault "qop" list "auth"- charset = lookupDirectiveWithDefault "charset" list "utf-8"- algorithm = lookupDirective "algorithm" list- - -- Verify that all necessary directives has been set.- in case (nonce, qop, charset, algorithm) of- (Just nonce', qop', charset', Just algorithm') ->- - -- Strip quotations of the directives that need it.- let -- realm'' = stripQuotations realm'- nonce'' = stripQuotations nonce'- qop'' = stripQuotations qop' -- It seems ejabberd gives us an errorous "auth" instead of auth- in- -- -- Verify that the realm is the same as the Jabber host.- -- case realm'' == h of- -- True ->- - -- Verify that QOP is "auth", charset is "utf-8" and that- -- the algorithm is "md5-sess".- case qop'' == "auth" of- True ->- case charset' == "utf-8" of- True ->- case algorithm' == "md5-sess" of- True ->- - -- All data is valid; generate the reply.- Left (reply nonce'' qop'')- - -- Errors are caught and reported below.- False -> Right C1UnsupportedAlgorithm- False -> Right C1UnsupportedCharset- False -> Right C1UnsupportedQOP- -- False -> Right C1WrongRealm- _ -> Right C1NotAllParametersPresent- where- reply n q =- let -- We start with what's in RFC 2831 is referred to as "A1", a 16 octet- -- MD5 hash.- - -- If the username or password values are in ISO-8859-1, we convert- -- them to ISO-8859-1 strings.- username = case all isLatin1 u of- True -> DBL.pack $ map c2w u- False -> DBLC.pack $ u- password = case all isLatin1 p of- True -> DBL.pack $ map c2w p- False -> DBLC.pack p- - nc = "00000001"- digestUri = "xmpp/" ++ h- - -- Build the "{ username-value, ":", realm-value, ":", passwd }"- -- bytestring, the rest of the bytestring and then join them.- a1a = DBi.encode $ md5 $ DBLC.append- (DBLC.append username (DBLC.pack (":" ++ h ++ ":")))- password- a1aDebug = "DBi.encode $ md5 $ " ++ (DBLC.unpack $ DBLC.append- (DBLC.append username (DBLC.pack (":" ++ h ++ ":")))- password)- a1b = DBLC.pack (":" ++ n ++ ":" ++ c)- a1 = DBLC.append a1a a1b- - -- Generate the "A2" value.- a2 = DBLC.pack ("AUTHENTICATE:" ++ digestUri)- - -- Produce the responseValue.- k = DBLC.pack (show $ md5 a1)- colon = DBLC.pack ":"- s0 = DBLC.pack (n ++ ":" ++ nc ++ ":" ++ c ++ ":" ++- q ++ ":")- s1 = DBLC.pack $ show $ md5 a2- - s_ = DBLC.append s0 s1- -- append k:d and 16 octet hash it- kd = md5 (DBLC.append k (DBLC.append colon s_))- - lol0 = DBLC.unpack s_- lol1 = show kd- - response = show kd- in "username=\"" ++ u ++ "\",realm=\"" ++ h ++ "\",nonce=\"" ++ n ++- "\",cnonce=\"" ++ c ++ "\",nc=" ++ nc ++ ",digest-uri=\"" ++- digestUri ++ "\",qop=auth,response=" ++ response ++ ",charset=utf-8"- -- "\n\n" ++- -- "a1aDebug: " ++ a1aDebug ++ "\n" ++- -- "a1b: " ++ (DBLC.unpack a1b) ++ "\n" ++- -- "a1: " ++ (DBLC.unpack a1) ++ "\n" ++- -- "a2: " ++ (DBLC.unpack a2) ++ "\n" ++- -- "k: " ++ (DBLC.unpack k) ++ "\n" ++- -- "colon: " ++ (DBLC.unpack colon) ++ "\n" ++- -- "s0: " ++ (DBLC.unpack s0) ++ "\n" ++- -- "s1: " ++ (DBLC.unpack s1) ++ "\n" ++- -- "s_: " ++ (DBLC.unpack s_) ++ "\n"----- Stripts the quotations around a string, if any; "\"hello\"" becomes "hello".--stripQuotations :: String -> String-stripQuotations "" = ""-stripQuotations s | (head s == '"') && (last s == '"') = tail $ init s- | otherwise = s
− Network/XMPP/Session.hs
@@ -1,906 +0,0 @@--- | Module: $Header$--- Description: XMPP client session management module--- Copyright: Copyright © 2010-2011 Jon Kristensen--- License: BSD-3--- --- Maintainer: info@pontarius.org--- Stability: unstable--- Portability: portable--- --- This module will be documented soon.---- TODO: Send white-space characters with regular intervals to keep the--- connection alive.--- TODO: Add namespace support for stream/features?--- TODO: Add support for `ver optional' support for features?--- TODO: Presence priority?--- TODO: Stop the logger--- TODO: Catch errors--module Network.XMPP.Session ( XMPPInEvent (..)- , XMPPOutEvent (..)- , createSession ) where---import Network.XMPP.JID-import Network.XMPP.SASL-import Network.XMPP.Stanza-import Network.XMPP.Utilities--import Codec.Binary.UTF8.String-import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)-import Control.Concurrent (forkIO, threadDelay)-import Control.Monad.IO.Class (liftIO, MonadIO)-import Data.Enumerator (($$), Iteratee, continue, joinI,- run_, yield)-import Data.Enumerator.Binary (enumHandle, enumFile)-import Data.Maybe-import Data.String-import Data.XML.Types-import GHC.IO.Handle (Handle, hPutStr, hFlush, hSetBuffering, hWaitForInput)-import Network-import Network.TLS-import Network.TLS.Cipher-import System.IO (BufferMode, BufferMode(NoBuffering))-import System.Log.HLogger-import System.Log.SimpleHLogger-import Text.XML.Enumerator.Parse (parseBytes, decodeEntities)-import Text.XML.Enumerator.Document (fromEvents)-import qualified Codec.Binary.Base64.String as CBBS-import qualified Data.ByteString as DB-import qualified Data.ByteString.Lazy as DBL (ByteString, append, pack, fromChunks, toChunks, null)-import qualified Data.ByteString.Lazy.Char8 as DBLC (append, pack, unpack)-import qualified Data.Enumerator as E-import qualified Data.Enumerator.List as EL-import qualified Data.List as DL-import qualified Data.Text as DT-import qualified Data.Text.Lazy as DTL---type Server = String-type Port = Integer--type UserName = String-type Password = String-type Resource = String---data XMPPInEvent = XIEConnectionSucceeded |- XIEConnectionFailed |- XIEAuthenticationSucceeded |- XIEAuthenticationFailed |- XIEMessage Message |- XIEPresence Presence |- XIEIQ IQ |- -- XIEIQJingle | -- Added for Jingle sessions- -- XIEIQXTLS | -- Added for XTLS connection- XIEDisconnect deriving (Eq, Show)--data XMPPOutEvent = XOEConnect Server Port |- XOEAuthenticate UserName Password Resource |- XOEMessage Message |- XOEPresence Presence |- XOEIQ IQ |- -- XOEIQJingle | -- Added for Jingle sessions- -- XOEIQXTLS | -- Added for XTLS connection- XOEDisconnect deriving (Eq, Show)---data SecurityEvent = TLSSucceeded TLSCtx | TLSFailed---instance Show SecurityEvent where- show (TLSSucceeded _) = "TLSSucceeded Ctx"- show TLSFailed = "TLSFailed"---data ConnectionState = Disconnected |- ConnectedNotTLSSecured |- ConnectedTLSSecured TLSCtx--instance Eq ConnectionState where- (==) Disconnected Disconnected = True- (==) ConnectedNotTLSSecured ConnectedNotTLSSecured = True- (==) (ConnectedTLSSecured _) (ConnectedTLSSecured _) = True- (==) _ _ = False--data AuthenticationState = AuthNotRequested |- AuthRequested |- Challenge1Processed |- Authenticated deriving (Eq)--data State = State { stateConnectionState :: ConnectionState- , stateAuthenticationState :: AuthenticationState- , stateHandle :: Maybe Handle- , stateServer :: Maybe (Server, Port)- , stateUserName :: Maybe String- , statePassword :: Maybe String- , stateResource :: Maybe String- , stateStreamID :: Maybe String }--defaultState = State { stateConnectionState = Disconnected- , stateAuthenticationState = AuthNotRequested- , stateHandle = Nothing- , stateServer = Nothing- , stateUserName = Nothing- , statePassword = Nothing- , stateResource = Nothing- , stateStreamID = Nothing }----- An XMLEvent is a high-level XMPP event generated by our XML parsing code.--data XMLEvent = XEBeginStream Stream | XEFeatures Features |- XEChallenge Challenge | XESuccess Success |- XEEndStream | XEIQ IQ | XEPresence Presence |- XEMessage Message | XEOther String deriving (Show)----- Represents the top-level "<stream>" element.--data Stream = Stream { streamNamespace :: StreamNamespace- , streamID :: String- , streamVersion :: Float } deriving (Show)----- TODO: Do not make this assumption, but parse the element instead.--defaultStream id = Stream { streamNamespace = Client- , streamID = id- , streamVersion = 1.0 }----- The "<stream:features>" element.--data Features = Features { featuresStartTLS :: Bool- , featuresMechanisms :: [FeaturesMechanism]- , featuresCompressionMethods :: [CompressionMethod] } deriving (Show)----- TODO: Do not make this assumption, but parse the element instead.--featuresDefault = Features { featuresStartTLS = True,- featuresMechanisms = [DigestMD5],- featuresCompressionMethods = [] }----- TODO: Necessary?--data StreamNamespace = Client | Server deriving (Show)----- Authentication mechanisms. We only support DigestMD5 at this point.--data FeaturesMechanism = DigestMD5 | CramMD5 | Login | Plain | UnknownMechanism deriving (Show)--data UnknownMechanism = UM String deriving (Show)---data CompressionMethod = Zlib deriving (Show)----- Containers for information from SASL challenges and successes.--data Challenge = Chal String deriving (Show)--data Success = Succ String deriving (Show)---data InternalEvent = IEX XMLEvent |- IEO XMPPOutEvent |- IES SecurityEvent deriving (Show)----- | Creates a Pontarius XMPP session by setting up internal processes and state--- and creates the two (in and out) event channels that the XMPP client uses--- for communicating with Pontarius XMPP.--createSession :: IO (Chan XMPPInEvent, Chan XMPPOutEvent)--createSession = do- logger <- simpleLogger "PontariusXMPP"- loggerLog logger (Just ("Session", "createSession")) Info "Pontarius XMPP has started"- - -- Create client "in" and "out" channels (naming from the perspective of the- -- XMPP client) as well as the internal event channel used by the state loop- inChan <- newChan- outChan <- newChan- internalChan <- newChan- - -- Start to listen to client "out" events (client actions)- forkIO $ clientListener outChan internalChan logger- - -- Start the state loop, the main loop of Pontarius XMPP- forkIO $ stateLoop defaultState internalChan inChan logger- - return (inChan, outChan)----- Receives events from the XMPP client and forwards them to the state loop by--- using the internal event channel.--clientListener :: Chan XMPPOutEvent -> Chan InternalEvent -> Logger -> IO ()--clientListener c c_ l =- do event <- readChan c- loggerLog l (Just ("Session", "clientListener")) Debug $ "clientListener: Forwarding received client event: " ++ show (event)- writeChan c_ (IEO event)- clientListener c c_ l----- Processes internal events to possibly perform actions and update the session--- state. Note that the InternalEvent type wraps the external client events.--stateLoop :: State -> Chan InternalEvent -> Chan XMPPInEvent -> Logger -> IO ()--stateLoop s c c_ l =- do event <- readChan c- -- logDebug l $ "stateLoop: Received event: " ++ (show event)- -- TODO: Debug with state?- s' <- processEvent event- stateLoop s' c c_ l- where- connectionState = stateConnectionState s- authenticationState = stateAuthenticationState s- handle = stateHandle s- server = stateServer s- userName = stateUserName s- password = statePassword s- resource = stateResource s- streamID = stateStreamID s- Just (serverHost, serverPort) = stateServer s- tlsCtx = let ConnectedTLSSecured x = connectionState in x- - -- let xml = clientOutEventToXML clientEvent- -- logDebug l $ "processEvent: Sending XML: " ++ xml- -- sendData (fromJust tlsCtx) $ DBLC.pack $ encodeString $ xml- - processEvent (IEO clientOutEvent) = do- loggerLog l (Just ("Session", "processEvent")) Debug $ "Processing client out event " ++ (show clientOutEvent)- case clientOutEvent of- XOEConnect serverHost_ serverPort_ -> do- handle <- connectTo serverHost_ (PortNumber $ fromInteger serverPort_)- hSetBuffering handle NoBuffering- hPutStr handle $ encodeString $- "<?xml version='1.0'?><stream:stream to='" ++ serverHost_ ++- "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.or" ++- "g/streams' version='1.0'>"- hFlush handle- - -- Start XML enumerator, which will read from the handle to generate the- -- relevant internal events- forkIO $ xmlEnumerator c handle serverHost_ l- - return s { stateConnectionState = ConnectedNotTLSSecured- , stateHandle = Just handle- , stateServer = Just (serverHost_, serverPort_) }- - -- TODO: Function to verify certificate?- XOEAuthenticate userName_ password_ resource_ -> do- sendData tlsCtx $ DBLC.pack $ encodeString- "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>"- return s { stateAuthenticationState = AuthRequested- , stateUserName = Just userName_- , statePassword = Just password_- , stateResource = Just resource_ }- - XOEPresence presence -> do- presence' <- case stanzaID $ presenceStanza presence of- Nothing -> do- id <- getID- return $ presence { presenceStanza = (presenceStanza presence) { stanzaID = Just (SID id) } }- _ -> return presence- - let xml = presenceToXML presence'- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Will send presence XML: " ++ xml- sendData tlsCtx $ DBLC.pack $ encodeString $ xml- return s- - XOEMessage message -> do- message' <- case stanzaID $ messageStanza message of- Nothing -> do- id <- getID- return $ message { messageStanza = (messageStanza message) { stanzaID = Just (SID id) } }- _ -> return message- - let xml = messageToXML message'- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Will send message XML: " ++ xml- sendData tlsCtx $ DBLC.pack $ encodeString $ xml- return s- - XOEIQ iq -> do- iq' <- case stanzaID $ iqStanza iq of- Nothing -> do- id <- getID- return $ case iq of- IQGet {} -> do- iq { iqGetStanza = (iqStanza iq) { stanzaID = Just (SID id) } }- IQSet {} -> do- iq { iqSetStanza = (iqStanza iq) { stanzaID = Just (SID id) } }- IQResult {} -> do- iq { iqResultStanza = (iqStanza iq) { stanzaID = Just (SID id) } }- _ -> return iq- - let xml = iqToXML iq'- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Will send IQ XML: " ++ xml- sendData tlsCtx $ DBLC.pack $ encodeString $ xml- return s- - XOEDisconnect -> do- -- TODO: Close stream- return s- - -- TODO: processEvent (IES TLSFailed) =- - processEvent (IES (TLSSucceeded receivedTLSCtx)) =- return (s { stateConnectionState = ConnectedTLSSecured receivedTLSCtx })- - -- A <stream:stream> element has begun- -- TODO: Parse the XEStreamBegin object- processEvent (IEX (XEBeginStream _)) = do- loggerLog l (Just ("Session", "processEvent")) Debug "processEvent: A new stream has been opened"- return s- - -- We have received <features> on an insecure stream- -- TODO: Parse the XEFeatures object- processEvent (IEX (XEFeatures _))- | connectionState == ConnectedNotTLSSecured = do- loggerLog l (Just ("Session", "processEvent")) Debug $- "processEvent: Received features ([...]) on insecure stream; req" ++- "uesting \"starttls\""- hPutStr (fromJust handle) $- "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"- hFlush (fromJust handle)- return s- - -- We have received <features> on a secure and non-authenticated stream- processEvent (IEX (XEFeatures _))- | connectionState == ConnectedTLSSecured tlsCtx &&- authenticationState /= Authenticated = do- loggerLog l (Just ("Session", "processEvent")) Debug $- "processEvent: Received features ([...]) on an unauthenticated" ++- " secure stream"- writeChan c_ $ XIEConnectionSucceeded- return s- - -- We have received <features> on an authenticated secure stream; we are- -- now ready to start processing client events- processEvent (IEX (XEFeatures _))- | connectionState == ConnectedTLSSecured tlsCtx = do- loggerLog l (Just ("Session", "processEvent")) Debug $- "processEvent: Received features ([...]) on an authenticated sec" ++- "ure stream"- loggerLog l (Just ("Session", "processEvent")) Info $ "processEvent: User has successfully logged in"- case resource of- Nothing -> do- sendData tlsCtx $ DBLC.pack $ encodeString- "<iq type=\"set\" id=\"bind_1\"><bind xmlns=\"urn:ietf:param" ++- "s:xml:ns:xmpp-bind\"></bind></iq>"- return ()- _ -> do- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Setting resource: " ++- (fromJust resource)- sendData tlsCtx $ DBLC.pack $ encodeString- "<iq type=\"set\" id=\"bind_1\"><bind xmlns=\"urn:ietf:param" ++- "s:xml:ns:xmpp-bind\"><resource>" ++ fromJust resource ++- "</resource></bind></iq>"- return ()- r <- getID- sendData tlsCtx $ DBLC.pack $ encodeString $- "<iq type=\"set\" id=\"" ++ r ++- "\"><session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>" ++- "</iq>"- writeChan c_ $ XIEAuthenticationSucceeded- return (s { stateAuthenticationState = Authenticated })- - -- We have received a SASL challenge on a secure stream- processEvent (IEX (XEChallenge (Chal challenge)))- | connectionState == ConnectedTLSSecured tlsCtx = do- let challenge' = CBBS.decode challenge- case authenticationState of- AuthRequested -> do- -- This is the first challenge - we need to calculate the reply- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Received initial challenge: " ++- challenge ++ " (or " ++ challenge' ++ ")"- random <- getID -- TODO: Length and content.- case replyToChallenge1 challenge' serverHost (fromJust userName)- (fromJust password) random of- Left reply -> do- let reply' = (filter (/= '\n') (CBBS.encode reply))- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Sending challenge response: " ++- reply'- sendData tlsCtx $ DBLC.pack $ encodeString $- "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" ++- reply' ++ "</response>"- return (s { stateAuthenticationState = Challenge1Processed } )- Right error -> do- putStrLn $ show error- return s- Challenge1Processed -> do- -- This is not the first challenge; [...]- -- TODO: Can we assume "rspauth"?- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Received non-initial challenge: " ++- challenge ++ " (or " ++ challenge' ++ ")"- liftIO $ sendData tlsCtx $ DBLC.pack $ encodeString $- "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"- return s- - -- We have received a SASL "success" message over a secured connection- -- TODO: Parse the success message?- -- TODO: <?xml version='1.0'?>?- processEvent (IEX (XESuccess (Succ _)))- | connectionState == ConnectedTLSSecured tlsCtx = do- loggerLog l (Just ("Session", "processEvent")) Debug $- "processEvent: Received authentication success: [...]; restartin" ++- "g stream" -- TODO- sendData tlsCtx $ DBLC.pack $- encodeString "<?xml version='1.0'?><stream:stream to='" ++- serverHost ++- "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/" ++- "streams' version='1.0'>"- return s { stateAuthenticationState = Authenticated }- - -- Ignore id="bind_1" and session IQ result, otherwise create client event- processEvent (IEX (XEIQ iqEvent))- | authenticationState == Authenticated = do- case shouldIgnoreIQ iqEvent of- True ->- return s- False -> do- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Got IQ for client: " ++- (show iqEvent)- writeChan c_ $ XIEIQ iqEvent- return s- - processEvent (IEX (XEPresence presenceEvent))- | authenticationState == Authenticated = do- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Got presence for client: " ++- (show presenceEvent)- writeChan c_ $ XIEPresence presenceEvent- return s- - processEvent (IEX (XEMessage messageEvent)) = do- loggerLog l (Just ("Session", "processEvent")) Debug $ "processEvent: Got message for client: " ++- (show messageEvent)- writeChan c_ $ XIEMessage messageEvent- return s- - -- We received an XML element that we didn't parse- processEvent (IEX xmlEvent) = do- loggerLog l (Just ("Session", "processEvent")) Warning $ "processEvent: XML event slipped through: " ++- (show xmlEvent)- return s-- shouldIgnoreIQ :: IQ -> Bool- shouldIgnoreIQ i = case iqPayload i of- Nothing -> False- Just e -> case nameNamespace $ elementName e of- Just x | x == DT.pack "urn:ietf:params:xml:ns:xmpp-bind" -> True- Just x | x == DT.pack "urn:ietf:params:xml:ns:xmpp-session" -> True- Just _ -> False- Nothing -> False- -parseIQ :: Element -> IQ-parseIQ e | typeAttr == "get" = let (Just payloadMust) = payload- in iqGet idAttr fromAttr toAttr Nothing- payloadMust- | typeAttr == "set" = let (Just payloadMust) = payload- in iqSet idAttr fromAttr toAttr Nothing- payloadMust- | typeAttr == "result" = iqResult idAttr fromAttr toAttr Nothing- payload-- where- -- TODO: Many duplicate functions from parsePresence.- - payload :: Maybe Element- payload = case null (elementChildren e) of- True -> Nothing- False -> Just $ head $ elementChildren e- - typeAttr :: String- typeAttr = case attributeText typeName e of- -- Nothing -> Nothing- Just a -> DT.unpack a- - fromAttr :: Maybe JID- fromAttr = case attributeText fromName e of- Nothing -> Nothing- Just a -> stringToJID $ DT.unpack a- - toAttr :: Maybe JID- toAttr = case attributeText toName e of- Nothing -> Nothing- Just a -> stringToJID $ DT.unpack a- - idAttr :: Maybe StanzaID- idAttr = case attributeText idName e of- Nothing -> Nothing- Just a -> Just (SID (DT.unpack a))- - typeName :: Name- typeName = fromString "type"- - fromName :: Name- fromName = fromString "from"- - toName :: Name- toName = fromString "to"- - idName :: Name- idName = fromString "id"---- TODO: Parse xml:lang--parsePresence :: Element -> Presence-parsePresence e = presence idAttr fromAttr toAttr Nothing typeAttr (elementChildren e)- where- -- TODO: Many duplicate functions from parseIQ.- - typeAttr :: PresenceType- typeAttr = case attributeText typeName e of- Just t -> stringToPresenceType $ DT.unpack t- Nothing -> Available- - fromAttr :: Maybe JID- fromAttr = case attributeText fromName e of- Nothing -> Nothing- Just a -> stringToJID $ DT.unpack a- - toAttr :: Maybe JID- toAttr = case attributeText toName e of- Nothing -> Nothing- Just a -> stringToJID $ DT.unpack a- - idAttr :: Maybe StanzaID- idAttr = case attributeText idName e of- Nothing -> Nothing- Just a -> Just (SID (DT.unpack a))- - fromName :: Name- fromName = fromString "from"- - typeName :: Name- typeName = fromString "type"- - toName :: Name- toName = fromString "to"- - idName :: Name- idName = fromString "id"--parseMessage :: Element -> Message-parseMessage e = message idAttr fromAttr toAttr Nothing typeAttr (elementChildren e)- where- -- TODO: Many duplicate functions from parseIQ.- - typeAttr :: MessageType- typeAttr = case attributeText typeName e of- Just t -> stringToMessageType $ DT.unpack t- Nothing -> Normal- - fromAttr :: Maybe JID- fromAttr = case attributeText fromName e of- Nothing -> Nothing- Just a -> stringToJID $ DT.unpack a- - toAttr :: Maybe JID- toAttr = case attributeText toName e of- Nothing -> Nothing- Just a -> stringToJID $ DT.unpack a- - idAttr :: Maybe StanzaID- idAttr = case attributeText idName e of- Nothing -> Nothing- Just a -> Just (SID (DT.unpack a))- - fromName :: Name- fromName = fromString "from"- - typeName :: Name- typeName = fromString "type"- - toName :: Name- toName = fromString "to"- - idName :: Name- idName = fromString "id"---- stringToPresenceType "available" = Available--- stringToPresenceType "away" = Away--- stringToPresenceType "chat" = Chat--- stringToPresenceType "dnd" = DoNotDisturb--- stringToPresenceType "xa" = ExtendedAway--stringToPresenceType "probe" = Probe-stringToPresenceType "error" = PresenceError--stringToPresenceType "unavailable" = Unavailable-stringToPresenceType "subscribe" = Subscribe-stringToPresenceType "subscribed" = Subscribed-stringToPresenceType "unsubscribe" = Unsubscribe-stringToPresenceType "unsubscribed" = Unsubscribed---- presenceTypeToString Available = "available"---- presenceTypeToString Away = "away"--- presenceTypeToString Chat = "chat"--- presenceTypeToString DoNotDisturb = "dnd"--- presenceTypeToString ExtendedAway = "xa"--presenceTypeToString Unavailable = "unavailable"--presenceTypeToString Probe = "probe"-presenceTypeToString PresenceError = "error"--presenceTypeToString Subscribe = "subscribe"-presenceTypeToString Subscribed = "subscribed"-presenceTypeToString Unsubscribe = "unsubscribe"-presenceTypeToString Unsubscribed = "unsubscribed"--stringToMessageType "chat" = Chat-stringToMessageType "error" = MessageError-stringToMessageType "groupchat" = Groupchat-stringToMessageType "headline" = Headline-stringToMessageType "normal" = Normal-stringToMessageType s = OtherMessageType s--messageTypeToString Chat = "chat"-messageTypeToString MessageError = "error"-messageTypeToString Groupchat = "groupchat"-messageTypeToString Headline = "headline"-messageTypeToString Normal = "normal"-messageTypeToString (OtherMessageType s) = s--xmlEnumerator :: Chan InternalEvent -> Handle -> String -> Logger -> IO ()-xmlEnumerator c h s l = do- loggerLog l (Just ("Session", "processEvent")) Debug $ "xmlEnumerator: Starting to read insecure XML"- run_ $ enumHandle 1 h $$ joinI $ parseBytes decodeEntities- $$ xmlReader c [] 0- loggerLog l (Just ("Session", "processEvent")) Debug $ "xmlEnumerator: Unsecure stream ended - performing TLS handshake"- t <- handshake' h s- case t of- Just tlsctx -> do loggerLog l (Just ("Session", "processEvent")) Debug $ "xmlEnumerator: Handshake successful - st" ++- "arting to read secure XML"- writeChan c (IES (TLSSucceeded tlsctx))- run_ $ enumTLS tlsctx $$ joinI $ parseBytes decodeEntities- $$ xmlReader c [] 0- loggerLog l (Just ("Session", "processEvent")) Debug $- "xmlEnumerator: Secure stream ended, exiting"- return ()- Nothing -> loggerLog l (Just ("Session", "processEvent")) Debug $ "xmlEnumerator: TLS handshake failed" -- TODO: Event- return ()--enumTLS :: TLSCtx -> E.Enumerator DB.ByteString IO b-enumTLS c s = loop c s where- loop :: TLSCtx -> E.Step DB.ByteString IO b -> E.Iteratee DB.ByteString IO b- loop c (E.Continue k) =- do d <- recvData c- case DBL.null d of- True -> loop c (E.Continue k)- False -> k (E.Chunks $ DBL.toChunks d) E.>>== loop c- loop _ step = E.returnI step--hPutStr' :: Handle -> String -> IO ()-hPutStr' h s = do- hPutStr h $ encodeString "<?xml version='1.0'?><stream:stream to='" ++- s ++ "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/" ++- "streams' version='1.0'>"- hFlush h--getTLSParams :: TLSParams-getTLSParams = TLSParams { pConnectVersion = TLS10- , pAllowedVersions = [TLS10,TLS11]- , pCiphers = [cipher_AES256_SHA1] -- Check the rest- , pCompressions = [nullCompression]- , pWantClientCert = False- , pCertificates = []- , onCertificatesRecv = \_ -> return True } -- Verify cert chain--handshake' :: Handle -> String -> IO (Maybe TLSCtx)-handshake' h s = do- let t = getTLSParams- r <- makeSRandomGen- case r of- Right sr -> do- putStrLn $ show sr- c <- client t sr h- handshake c- sendData c $ DBLC.pack $ encodeString "<?xml version='1.0'?><stream:stream to='" ++- s ++ "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/" ++- "streams' version='1.0'>"- putStrLn ">>>>TLS data sended<<<<"- return (Just c)- Left ge -> do- putStrLn $ show ge- return Nothing----- TODO: Add logger--xmlReader :: Chan InternalEvent -> [Event] -> Int ->- Iteratee Event IO (Maybe Event)--xmlReader ch [EventBeginDocument] 0 = xmlReader ch [] 0---- TODO: Safe to start change level here? We are doing this since the stream can--- restart.--- TODO: l < 2?-xmlReader ch [EventBeginElement name attribs] l- | l < 3 && nameLocalName name == DT.pack "stream" &&- namePrefix name == Just (DT.pack "stream") = do- liftIO $ writeChan ch $ IEX $ XEBeginStream $ defaultStream "TODO"- xmlReader ch [] 1--xmlReader ch [EventEndElement name] 1- | namePrefix name == Just (DT.pack "stream") &&- nameLocalName name == DT.pack "stream" = do- liftIO $ writeChan ch $ IEX XEEndStream- return Nothing---- Check if counter is one to forward it to related function.--- Should replace "reverse ((EventEndElement n):es)" with es--- ...-xmlReader ch ((EventEndElement n):es) 1- | nameLocalName n == DT.pack "proceed" = E.yield Nothing (E.Chunks [])- | otherwise = do- -- liftIO $ putStrLn "Got an IEX Event..."- liftIO $ writeChan ch $ IEX (processEventList (DL.reverse ((EventEndElement n):es)))- xmlReader ch [] 1---- Normal condition, buffer the event to events list.-xmlReader ch es co = do- head <- EL.head- let co' = counter co head- liftIO $ putStrLn $ show co' ++ "\t" ++ show head -- for test- case head of- Just e -> xmlReader ch (e:es) co'- Nothing -> xmlReader ch es co'----- TODO: Generate real event.-processEventList :: [Event] -> XMLEvent-processEventList e- | namePrefix name == Just (DT.pack "stream") &&- nameLocalName name == DT.pack "features" = XEFeatures featuresDefault- | nameLocalName name == DT.pack "challenge" =- let EventContent (ContentText c) = head es in XEChallenge $ Chal $ DT.unpack c- | nameLocalName name == DT.pack "success" =- let EventContent (ContentText c) = head es in XESuccess $ Succ $ DT.unpack c- | nameLocalName name == DT.pack "iq" = XEIQ $ parseIQ $ eventsToElement e- | nameLocalName name == DT.pack "presence" = XEPresence $ parsePresence $ eventsToElement e- | nameLocalName name == DT.pack "message" = XEMessage $ parseMessage $ eventsToElement e- | otherwise = XEOther $ elementToString $ Just (eventsToElement e)- where- (EventBeginElement name attribs) = head e- es = tail e--eventsToElement :: [Event] -> Element-eventsToElement e = do- documentRoot $ fromJust (run_ $ enum e $$ fromEvents)- where- enum :: [Event] -> E.Enumerator Event Maybe Document- enum e_ (E.Continue k) = k $ E.Chunks e_- enum e_ step = E.returnI step--counter :: Int -> Maybe Event -> Int-counter c (Just (EventBeginElement _ _)) = (c + 1)-counter c (Just (EventEndElement _) ) = (c - 1)-counter c _ = c--presenceToXML :: Presence -> String-presenceToXML p = "<presence" ++ from ++ id' ++ to ++ type' ++ ">" ++- (elementsToString $ presencePayload p) ++ "</presence>"- where- s = presenceStanza p- - from :: String- from = case stanzaFrom $ presenceStanza p of- -- TODO: Lower-case- Just s -> " from='" ++ (jidToString s) ++ "'"- Nothing -> ""- - id' :: String- id' = case stanzaID s of- Just (SID s) -> " id='" ++ s ++ "'"- Nothing -> ""- - to :: String- to = case stanzaTo $ presenceStanza p of- -- TODO: Lower-case- Just s -> " to='" ++ (jidToString s) ++ "'"- Nothing -> ""- - type' :: String- type' = case presenceType p of- Available -> ""- t -> " type='" ++ (presenceTypeToString t) ++ "'"--iqToXML :: IQ -> String-iqToXML IQGet { iqGetStanza = s, iqGetPayload = p } =- let type' = " type='get'" in "<iq" ++ from ++ id' ++ to ++ type' ++ ">" ++ (elementToString (Just p)) ++ "</iq>"- where- from :: String- from = case stanzaFrom s of- -- TODO: Lower-case- Just s -> " from='" ++ (jidToString s) ++ "'"- Nothing -> ""- - id' :: String- id' = case stanzaID s of- Just (SID s) -> " id='" ++ s ++ "'"- Nothing -> ""- - to :: String- to = case stanzaTo s of- -- TODO: Lower-case- Just s -> " to='" ++ (jidToString s) ++ "'"- Nothing -> ""--iqToXML IQSet { iqSetStanza = s, iqSetPayload = p } =- let type' = " type='set'" in "<iq" ++ from ++ id' ++ to ++ type' ++ ">" ++ (elementToString (Just p)) ++ "</iq>"- where- from :: String- from = case stanzaFrom s of- -- TODO: Lower-case- Just s -> " from='" ++ (jidToString s) ++ "'"- Nothing -> ""- - id' :: String- id' = case stanzaID s of- Just (SID s) -> " id='" ++ s ++ "'"- Nothing -> ""- - to :: String- to = case stanzaTo s of- -- TODO: Lower-case- Just s -> " to='" ++ (jidToString s) ++ "'"- Nothing -> ""--iqToXML IQResult { iqResultStanza = s, iqResultPayload = p } =- let type' = " type='result'" in "<iq" ++ from ++ id' ++ to ++ type' ++ ">" ++ (elementToString p) ++ "</iq>"- where- from :: String- from = case stanzaFrom s of- -- TODO: Lower-case- Just s -> " from='" ++ (jidToString s) ++ "'"- Nothing -> ""- - id' :: String- id' = case stanzaID s of- Just (SID s) -> " id='" ++ s ++ "'"- Nothing -> ""- - to :: String- to = case stanzaTo s of- -- TODO: Lower-case- Just s -> " to='" ++ (jidToString s) ++ "'"- Nothing -> ""--messageToXML :: Message -> String-messageToXML m = "<message" ++ from ++ id' ++ to ++ type' ++ ">" ++- (elementsToString $ messagePayload m) ++ "</message>"- where- s = messageStanza m- - from :: String- from = case stanzaFrom $ messageStanza m of- -- TODO: Lower-case- Just s -> " from='" ++ (jidToString s) ++ "'"- Nothing -> ""- - id' :: String- id' = case stanzaID s of- Just (SID s) -> " id='" ++ s ++ "'"- Nothing -> ""- - to :: String- to = case stanzaTo $ messageStanza m of- -- TODO: Lower-case- Just s -> " to='" ++ (jidToString s) ++ "'"- Nothing -> ""- - type' :: String- type' = case messageType m of- Normal -> ""- t -> " type='" ++ (messageTypeToString t) ++ "'"
− Network/XMPP/Stanza.hs
@@ -1,237 +0,0 @@--- | Module: $Header$--- Description: XMPP stanza types and utility functions--- Copyright: Copyright © 2010-2011 Jon Kristensen--- License: BSD-3--- --- Maintainer: info@pontarius.org--- Stability: unstable--- Portability: portable--- --- This module will be documented soon.---- Received stanzas can be assumed to have their ID and to fields set.---- TODO: Handle error stanzas--module Network.XMPP.Stanza ( StanzaID (SID)- , From- , To- , XMLLang- , Stanza (..)- , MessageType (..)- , Message (..)- , message- , PresenceType (..)- , Presence (..)- , presence- , IQ (..)- , iqGet- , iqSet- , iqResult- , iqStanza- , iqPayloadNamespace- , iqPayload ) where--import Network.XMPP.JID--import Data.XML.Types-import qualified Data.Text as DT---data StanzaID = SID String deriving (Eq, Show)-type From = JID-type To = JID-type XMLLang = String -- Validate, protect---data Stanza = Stanza { stanzaID :: Maybe StanzaID- , stanzaFrom :: Maybe From- , stanzaTo :: Maybe To- , stanzaLang :: Maybe XMLLang } deriving (Eq, Show)---stanza :: Maybe StanzaID -> Maybe From -> Maybe To -> Maybe XMLLang -> Stanza--stanza i f t l = Stanza { stanzaID = i- , stanzaFrom = f- , stanzaTo = t- , stanzaLang = l }---data MessageType = Chat |- MessageError |- Groupchat |- Headline |- Normal | -- Default- OtherMessageType String deriving (Eq, Show)---data Message = Message { messageStanza :: Stanza- , messageType :: MessageType- , messagePayload :: [Element] } deriving (Eq, Show)---message :: Maybe StanzaID -> Maybe From -> Maybe To -> Maybe XMLLang ->- MessageType -> [Element] -> Message--message i f t l t_ p = Message { messageStanza = stanza i f t l- , messageType = t_- , messagePayload = p }---data PresenceType = Subscribe | -- ^ Sender wants to subscribe to presence- Subscribed | -- ^ Sender has approved the subscription- Unsubscribe | -- ^ Sender is unsubscribing from presence- Unsubscribed | -- ^ Sender has denied or cancelled a- -- subscription- Probe | -- ^ Sender requests current presence;- -- should only be used by servers- PresenceError | -- ^ Processing or delivery of previously- -- sent presence stanza failed- Available | -- Not part of type='' [...]- -- Away |- -- Chat |- -- DoNotDisturb |- -- ExtendedAway |- Unavailable- deriving (Eq, Show)----- | Presence stanzas are used to express an entity's network availability.--data Presence = Presence { presenceStanza :: Stanza- , presenceType :: PresenceType- , presencePayload :: [Element] } deriving (Eq, Show)---presence :: Maybe StanzaID -> Maybe From -> Maybe To -> Maybe XMLLang ->- PresenceType -> [Element] -> Presence--presence i f t l t_ p = Presence { presenceStanza = Stanza { stanzaID = i- , stanzaFrom = f- , stanzaTo = t- , stanzaLang = l }- , presenceType = t_- , presencePayload = p }---data IQ = IQGet { iqGetStanza :: Stanza, iqGetPayload :: Element } |- IQSet { iqSetStanza :: Stanza, iqSetPayload :: Element } |- IQResult { iqResultStanza :: Stanza- , iqResultPayload :: Maybe Element } deriving (Eq, Show)---iqGet :: Maybe StanzaID -> Maybe From -> Maybe To -> Maybe XMLLang -> Element ->- IQ--iqGet i f t l p = IQGet { iqGetStanza = Stanza { stanzaID = i- , stanzaFrom = f- , stanzaTo = t- , stanzaLang = l }- , iqGetPayload = p }---iqSet :: Maybe StanzaID -> Maybe From -> Maybe To -> Maybe XMLLang -> Element ->- IQ--iqSet i f t l p = IQSet { iqSetStanza = Stanza { stanzaID = i- , stanzaFrom = f- , stanzaTo = t- , stanzaLang = l }- , iqSetPayload = p }---iqResult :: Maybe StanzaID -> Maybe From -> Maybe To -> Maybe XMLLang ->- Maybe Element -> IQ--iqResult i f t l p = IQResult { iqResultStanza = Stanza { stanzaID = i- , stanzaFrom = f- , stanzaTo = t- , stanzaLang = l }- , iqResultPayload = p }---iqStanza :: IQ -> Stanza-iqStanza (IQGet { iqGetStanza = s }) = s-iqStanza (IQSet { iqSetStanza = s }) = s-iqStanza (IQResult { iqResultStanza = s }) = s--iqPayload :: IQ -> Maybe Element-iqPayload (IQGet {iqGetPayload = p}) = Just p-iqPayload (IQSet {iqSetPayload = p}) = Just p-iqPayload (IQResult {iqResultPayload = p}) = p--iqPayloadNamespace :: IQ -> Maybe String-iqPayloadNamespace i = case iqPayload i of- Nothing -> Nothing- Just p -> case nameNamespace $ elementName p of- Nothing -> Nothing- Just n -> Just (DT.unpack n)----- =============================================================================--- CODE NOT YET USED--- =============================================================================----- | All stanzas (IQ, message, presence) can cause errors, which looks like--- <stanza-kind to='sender' type='error'>. These errors are of one of the--- types listed below.--data StanzaErrorType = Cancel | -- ^ Error is unrecoverable - do not retry- Continue | -- ^ Conditition was a warning - proceed- Modify | -- ^ Change the data and retry- Auth | -- ^ Provide credentials and retry- Wait -- ^ Error is temporary - wait and retry- deriving (Eq, Show)----- | The stanza errors are accommodated with one of the error conditions listed--- below. The ones that are not self-explainatory should be documented below.--data StanzaErrorCondition = BadRequest | -- ^ Malformed XML- Conflict | -- ^ Resource or session- -- with name already- -- exists- FeatureNotImplemented |- Forbidden | -- ^ Insufficient- -- permissions- Gone | -- ^ Entity can no longer- -- be contacted at this- -- address- InternalServerError |- ItemNotFound |- JIDMalformed |- NotAcceptable | -- ^ Does not meet policy- -- criteria- NotAllowed | -- ^ No entity may perform- -- this action- NotAuthorized | -- ^ Must provide proper- -- credentials- PaymentRequired |- RecipientUnavailable | -- ^ Temporarily- -- unavailable- Redirect | -- ^ Redirecting to other- -- entity, usually- -- temporarily- RegistrationRequired |- RemoteServerNotFound |- RemoteServerTimeout |- ResourceConstraint | -- ^ Entity lacks the- -- necessary system- -- resources- ServiceUnavailable |- SubscriptionRequired |- UndefinedCondition | -- ^ Application-specific- -- condition- UnexpectedRequest -- ^ Badly timed request- deriving (Eq, Show)----- IM/RFC:--- data PresenceStatus = PS String deriving (Eq, Show)---- -- TODO: Validate input.---- presenceStatus :: String -> Maybe PresenceStatus--- presenceStatus s = Just (PS s)
− Network/XMPP/Utilities.hs
@@ -1,109 +0,0 @@--- | Module: $Header$--- Description: Utility functions for Pontarius XMPP; currently only random ID--- generation functions--- Copyright: Copyright © 2010-2011 Jon Kristensen--- License: BSD-3--- --- Maintainer: info@pontarius.org--- Stability: unstable--- Portability: portable--- --- This module will be documented soon.---- TODO: Document this module--- TODO: Make is possible to customize characters--- TODO: Make it possible to customize length--module Network.XMPP.Utilities ( elementToString- , elementsToString- , getID- , getID_) where--import Data.Word-import Data.XML.Types-import System.Crypto.Random-import System.Random-import qualified Data.ByteString as DB-import qualified Data.Map as DM-import qualified Data.Text as DT------ =============================================================================--- ID Generation--- =============================================================================---characters :: String-characters = "abcdef0123456789"--getID :: IO String-getID = getID_ 8--getID_ :: Int -> IO String-getID_ i =- do se <- getSeed- let st = mkStdGen se- return $ take i (idSequence st)- where- idSequence :: StdGen -> String- idSequence st = [characters !! (fst $ rand st)] ++ idSequence (snd $ rand st)- rand :: StdGen -> (Int, StdGen)- rand st = randomR (0, length characters - 1) st--getSeed :: IO Int-getSeed =- do h <- openHandle- b <- hGetEntropy h 8- let w = DB.unpack b- let s = seed w- closeHandle h- return s- where seed :: [Word8] -> Int- seed [] = 1- seed (x:xs) = ((fromIntegral x) + 1) * (seed xs)------ =============================================================================--- XML Utilities--- =============================================================================---elementsToString :: [Element] -> String-elementsToString [] = ""-elementsToString (e:es) = (elementToString $ Just e) ++ elementsToString es--elementToString :: Maybe Element -> String-elementToString Nothing = ""-elementToString (Just e) = "<" ++ nameToString (elementName e) ++ xmlns ++- attributes (DM.toList (elementAttributes e)) ++- ">" ++ (nodesToString $ elementNodes e) ++ "</" ++- nameToString (elementName e) ++ ">"- where- xmlns :: String- xmlns = case nameNamespace $ elementName e of- Nothing -> ""- Just t -> " xmlns='" ++ (DT.unpack t) ++ "'"- - nameToString :: Name -> String- nameToString Name { nameLocalName = n, namePrefix = Nothing } = DT.unpack n- nameToString Name { nameLocalName = n, namePrefix = Just p } =- (DT.unpack p) ++ ":" ++ (DT.unpack n)-- contentToString :: Content -> String- contentToString (ContentText t) = DT.unpack t- contentToString (ContentEntity t) = DT.unpack t- - attributes :: [(Name, [Content])] -> String- attributes [] = ""- attributes ((n, c):t) = (" " ++ (nameToString n) ++ "='" ++- concat (map contentToString c) ++ "'") ++- attributes t- - nodesToString :: [Node] -> String- nodesToString [] = ""- nodesToString ((NodeElement e):ns) = (elementToString $ Just e) ++- (nodesToString ns)- nodesToString ((NodeContent c):ns) = (contentToString c) ++- (nodesToString ns)
− README
@@ -1,23 +0,0 @@-Pontarius XMPP aims to be a secure and easy-to-use XMPP library for Haskell. We-have just released a third alpha version with the following features:-- * Client-to-server Transport Layer Security and DIGEST-MD5 SASL Authentication- * Concurrent, Flexible and Event-based API for XMPP Client Developers- * Support for Info/Query, Presence and Message Stanzas- * Interoperable XML Parsing (Using enumerator, xml-enumerator and xml-types)--Please note that we are not recommending anyone to use Pontarius XMPP at this-time as it’s still in an experimental stage and will have its API and data types-modified. However, if you are interested to use Pontarius XMPP anyway, feel free-to contact the Pontarius project and we will try to help you get started. You-can also see the Example directory for a usage example.--We are currently working on general improvements, securing the client-to-client-communication and having the library support all of RFC 3920: XMPP Core.--We will release the next version, 0.1 Alpha 4, on the 4th of May.--Look at the Pontarius web site <http://www.pontarius.org/>, the Pontarius XMPP-web page <http://www.pontarius.org/projects/pontarius-xmpp/> and the Pontarius-XMPP Hackage page <http://hackage.haskell.org/package/pontarius-xmpp/> for more-information.
+ README.md view
@@ -0,0 +1,112 @@+Pontarius XMPP+==============++Pontarius XMPP is a Haskell XMPP client library that implements the capabilities+of [RFC 6120 ("XMPP CORE")](http://tools.ietf.org/html/rfc6120), [RFC 6121+("XMPP IM")](http://tools.ietf.org/html/rfc6121), and [RFC 6122 ("XMPP+ADDR")](http://tools.ietf.org/html/rfc6122). Pontarius XMPP was part of the+Pontarius project, an effort to produce free and open source, uncentralized, and+privacy-aware software solutions.++While in alpha, Pontarius XMPP works quite well and fulfills most requirements+of the RFCs.++Prerequisites+-------------++Pontarius XMPP requires GHC 7.0, or later.++You will need the ICU Unicode library and it's header files in order to be able+to build Pontarius XMPP. On Debian, you will need to install the *libicu-dev*+package. In Fedora, the package is called *libicu-devel*.++_Note to users of GHC 7.0 and GHC 7.2:_ You will need *cabal-install*, version+*0.14.0* or higher, or the build will fail with an "unrecognized option:+--disable-benchmarks" error. The versions *1.16.0* and higher might not build on+your system; if so, install *0.14.0* with "cabal install cabal-install-0.14.0".++_Note to users of GHC 7.2.1:_ Due to a bug, recent versions of the *binary*+package wont build without running "ghc-pkg trust base".++_Note to users of GHC 7.0.1:_ You will want to configure your Cabal environment+(including *cabal-install*) for version *0.9.2.1* of *bytestring*.++Getting started+---------------++The latest release of Pontarius XMPP, as well as its module API pages, can+always be found at [the Pontarius XMPP Hackage+page](http://hackage.haskell.org/package/pontarius-xmpp/).++_Note:_ Pontarius XMPP is still in its Alpha phase. Pontarius XMPP is not yet+feature-complete, it may contain bugs, and its API may change between versions.++The first thing to do is to import the modules that we are going to use. We are+also using the OverloadedStrings LANGUAGE pragma in order to be able to type+<code>Text</code> values like strings.++ {-# LANGUAGE OverloadedStrings #-}++ import Network.Xmpp++ import Control.Monad+ import Data.Default+ import System.Log.Logger++Pontarius XMPP supports [hslogger](http://hackage.haskell.org/package/hslogger)+logging. Start by enabling console logging.++ updateGlobalLogger "Pontarius.Xmpp" $ setLevel DEBUG++When this is done, a <code>Session</code> object can be acquired by calling+<code>session</code>. This object will be used for interacting with the library.++ result <- session+ "example.com"+ (Just (\_ -> ( [scramSha1 "username" Nothing "password"])+ , Nothing))+ def++The three parameters above are the XMPP server realm, a SASL handler (for+authentication), and the session configuration settings (set to the default+settings). <code>session</code> will perform the necessary DNS queries to find+the address of the realm, connect, establish the XMPP stream, attempt to secure+the stream with TLS, authenticate, establish a concurrent interface for+interacting with the stream, and return the <code>Session</code> object.++The return type of <code>session</code> is <code>IO (Either XmppFailure+Session)</code>. As <code>XmppFailure</code> is an+<code>Control.Monad.Error</code> instance, you can utilize the+<code>ErrorT</code> monad transformer for error handling. A more simple way of+doing it could be doing something like this:++ sess <- case result of+ Right s -> return s+ Left e -> error $ "XmppFailure: " ++ (show e)++Next, let us set our status to Online.++ sendPresence def sess++Here, <code>def</code> refers to the default <code>Presence</code> value, which+is the same as <code>Presence Nothing Nothing Nothing Nothing Available+[]</code>.++Now, let's say that we want to receive all message stanzas, and echo the stanzas+back to the sender. This can be done like so:++ forever $ do+ msg <- getMessage sess+ case answerMessage msg (messagePayload msg) of+ Just answer -> sendMessage answer sess+ Nothing -> putStrLn "Received message with no sender."++You don't need to worry about escaping your <code>Text</code> values - Pontarius+XMPP (or rather, [xml-picklers](https://github.com/Philonous/xml-picklers)) will+take care of that for you.++Additional XMPP threads can be created using <code>dupSession</code> and+<code>forkIO</code>.++For a public domain example of a simple Pontarius XMPP (Cabal) project, refer to+the examples/echoclient directory.
Setup.hs view
@@ -1,3 +1,2 @@ import Distribution.Simple- main = defaultMain
− XTLS.hs
@@ -1,75 +0,0 @@-{-# LANGUAGE OverloadedStrings #-} ---module XTLS (start) where--import Network.XMPP-import Control.Concurrent (forkIO)-import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)--import Data.XML.Types-import Network.XMPP.Stanza-import Control.Monad ((>=>))-import Data.Maybe--data State = State {}--start :: IO ()-start = putStrLn "Test"--stateLoop :: State -> Chan XMPPInEvent -> Chan XMPPOutEvent -> IO ()-stateLoop s c c_ = do- e <- readChan c- s' <- processEvent e s c_- stateLoop s' c c_--processEvent :: XMPPInEvent -> State -> Chan XMPPOutEvent -> IO State--- processEvent (PATTERN_MATCHING) s c = do--- return s-processEvent (XIEIQ iq) s c = do- -- putStrLn- case xtlsOrJingle iq of- 'j' -> return s- 'x' -> return s- 'n' -> return s--xtlsOrJingle :: IQ -> Char-xtlsOrJingle iq =- case iqPayloadNamespace iq of- Just "urn:xmpp:jingle:1" ->- case isXTLS $ head $ getContent (fromJust $ iqPayload iq) of- [] -> 'j'- other -> 'x'- other -> 'n'- where- isXTLS :: Element -> [Element]- isXTLS = elementChildren >=> isNamed xtlsName-- getContent :: Element -> [Element]- getContent = elementChildren >=> isNamed "content"---xtlsName :: Name-xtlsName = ( "{urn:xmpp:jingle:security:xtls:0}security" :: Name )---- Jingle Event base on the XEP-166-data JingleEvent = ContentAccept | ContentAdd | ContentModify | ContentReject |- ContentRemove | DiscriptionInfo | TransportAccept |- TransportInfo | TransportReject | TransportReplace--data JingleSession = Initiated { sid :: String , candidate :: [Candidate] } |- Accepted { sid :: String , candidate :: [Candidate] } |- Media { sid :: String } |- Terminated { sid :: String }--data Candidate = Candidate { id :: Int ,- ip :: String ,- portNum :: Int ,- priority :: Int }--data Transport = IBB { transid :: String , blockSize :: Int} |- OBB { transid :: String } -- TODO : define the OBB---data XTLSSecurity = XTLSSecurity { fingerPrint :: String , methods :: [MethodName]}-data MethodName = SRP | X509
+ benchmarks/Bench.hs view
@@ -0,0 +1,12 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Criterion.Main+import Network.Xmpp.Types++bench_jidFromTexts = whnf (\(a,b,c) -> jidFromTexts a b c)+ ( Just "+\227\161[\\3\8260\&4"+ , "\242|8e3\EOTrf6\DLEp\\\a"+ , Just ")\211\226")++main = do defaultMain [bench "jidFromTexts 2" bench_jidFromTexts]
+ examples/echoclient/LICENSE.md view
@@ -0,0 +1,2 @@+The `Main.hs', `Setup.hs', and `echoclient.cabal' files in this directory are in+the public domain.
+ examples/echoclient/Main.hs view
@@ -0,0 +1,43 @@+{-++This directory defines a project that illustrates how to connect, authenticate,+set a simple presence, receive message stanzas, and echo them back to whoever is+sending them, using Pontarius XMPP. This file is in the public domain.++-}++{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.Monad+import Data.Default+import Lens.Family2+import Network.Xmpp+import Network.Xmpp.Internal (TlsBehaviour(..))+import System.Log.Logger+++main :: IO ()+main = do+ updateGlobalLogger "Pontarius.Xmpp" $ setLevel DEBUG+ result <- session+ "test.pontarius.org"+ (Just (\_ -> ( [scramSha1 "testuser1" Nothing "pwd1"])+ , Nothing))+ $ def & streamConfigurationL . tlsBehaviourL .~ PreferPlain+ & streamConfigurationL . connectionDetailsL .~+ UseHost "localhost" 5222+ & onConnectionClosedL .~ reconnectSession++ sess <- case result of+ Right s -> return s+ Left e -> error $ "XmppFailure: " ++ (show e)+ sendPresence def sess+ forever $ do+ msg <- getMessage sess+ case answerMessage msg (messagePayload msg) of+ Just answer -> sendMessage answer sess >> return ()+ Nothing -> putStrLn "Received message with no sender."+ where+ reconnectSession sess failure = reconnect' sess >> return ()
+ examples/echoclient/README.md view
@@ -0,0 +1,6 @@+This directory defines a project that illustrates how to connect, authenticate,+set a simple presence, receive message stanzas, and echo them back to whoever is+sending them, using Pontarius XMPP.++The `Main.hs', `Setup.hs', and `echoclient.cabal' files in this directory may be+used freely, as they are in the public domain.
+ examples/echoclient/Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/echoclient/echoclient.cabal view
@@ -0,0 +1,23 @@+-- This directory defines a project that illustrates how to connect,+-- authenticate, set a simple presence, receive message stanzas, and echo them+-- back to whoever is sending them, using Pontarius XMPP. This file is in the+-- public domain.++Name: echoclient+Version: 0.0.0.0+Cabal-Version: >= 1.6+Build-Type: Simple+License: OtherLicense+Copyright: Mahdi Abdinejadi, Jon Kristensen, Philipp Balzarek+Synopsis: Echo client test program for Pontarius XMPP++Executable echoclient+ Build-Depends: base+ , data-default+ , hslogger+ , lens-family+ , mtl+ , pontarius-xmpp+ , text+ , tls+ Main-Is: Main.hs
− install.sh
@@ -1,5 +0,0 @@-#!/bin/sh-runhaskell Setup.hs configure --user-runhaskell Setup.hs build-runhaskell Setup.hs install-
pontarius-xmpp.cabal view
@@ -1,95 +1,213 @@--- TODO: Set dependency versions.--- TODO: Highlighting bug: The last line of the description.+Cabal-Version: 2.2+Name: pontarius-xmpp+Version: 0.5.7.2+Build-Type: Simple+License: BSD-3-Clause+License-File: LICENSE.md+Copyright: Dmitry Astapov, Pierre Kovalev, Mahdi Abdinejadi, Jon Kristensen,+ IETF Trust, Philipp Balzarek, Sergey Alirzaev+Author: Philipp Balzarek+Maintainer: l29ah@cock.li+Stability: alpha+Homepage: https://github.com/l29ah/pontarius-xmpp/+Bug-Reports: https://github.com/l29ah/pontarius-xmpp/issues/+Synopsis: An XMPP client library+Description: Pontarius XMPP is a work in progress implementation of RFC 6120+ ("XMPP CORE"), RFC 6121 ("XMPP IM"), and RFC 6122 ("XMPP ADDR").+ While in alpha, Pontarius XMPP works quite well and fulfills most+ requirements of the RFCs.+Category: Network+Tested-With: GHC == 9.8.4 -Name: pontarius-xmpp-Version: 0.0.3.0-Cabal-Version: >= 1.6-Build-Type: Simple-License: BSD3-License-File: LICENSE-Copyright: Copyright © 2011, Jon Kristensen-Author: Jon Kristensen, Mahdi Abdinejadi-Maintainer: jon.kristensen@pontarius.org-Stability: alpha-Homepage: http://www.pontarius.org/projects/pontarius-xmpp/-Bug-Reports: mailto:info@pontarius.org-Package-URL: http://www.pontarius.org/releases/pontarius-xmpp-0.0.3.0.tar.gz-Synopsis: A (prototyped) secure and easy to use XMPP library-Description: A work in progress of an implementation of RFC 3920: XMPP- Core.-Category: Network-Tested-With: GHC ==6.12.3--- Data-Files:--- Data-Dir:--- Extra-Source-Files:--- Extra-Tmp-Files:+Extra-Source-Files: README.md+ , ChangeLog.md+ , examples/echoclient/echoclient.cabal+ , examples/echoclient/LICENSE.md+ , examples/echoclient/Main.hs+ , examples/echoclient/README.md+ , examples/echoclient/Setup.hs +Flag with-th {+ Description: Enable Template Haskell support+ Default: True+}++-- kludge for https://github.com/haskell/cabal/issues/2032+common stuff+ Build-Depends: attoparsec >=0.10.0.3+ , base >4 && <5+ , base64-bytestring >=0.1.0.0+ , binary >=0.4.1+ , conduit >=1.3 && < 1.4+ , containers >=0.4.0.0+ , crypto-api >=0.9+ , cryptohash >=0.6.1+ , cryptohash-cryptoapi >=0.1+ , data-default >=0.2+ , dns >=3.0+ , exceptions >=0.6+ , hslogger >=1.1.0+ , iproute >=1.2.4+ , lens+ , lens-family+ , lifted-base >=0.1.0.1+ , mtl >=2.0.0.0+ , network >=2.3.1.0+ , profunctors >= 4+ , pureMD5 >=2.1.2.1+ , random >=1.0.0.0+ , resourcet >=0.3.0+ , split >=0.1.2.3+ , stm >=2.4+ , stringprep >=1.0.0+ , text >=0.11.1.5+ , tls >=1.3.9+ , transformers >=0.3+ , unbounded-delays >=0.1+ , void >=0.5.5+ , crypton-x509-system >=1.4+ , xml-conduit >=1.1.0.7+ , xml-picklers >=0.3.3+ , xml-types >=0.3.1+ , bytestring >=0.9.1.9+ If flag(with-th) && impl(ghc >=7.6.1) {+ Build-Depends: template-haskell >=2.5+ }+ Library- Exposed-Modules: Network.XMPP, Network.XMPP.JID, Network.XMPP.SASL,- Network.XMPP.Session, Network.XMPP.Stanza,- Network.XMPP.Utilities- Exposed: True- Build-Depends: hlogger ==0.0.3.0, base >= 2 && < 5, enumerator,- crypto-api, base64-string, regex-posix, pureMD5,- utf8-string, network, xml-types, text, transformers,- bytestring, binary, random, xml-enumerator, tls ==0.4.1,- containers- -- Other-Modules:- -- HS-Source-Dirs:- -- Extensions:- -- Build-Tools:- -- Buildable:- -- GHC-Options:- -- GHC-Prof-Options:- -- Hugs-Options:- -- NHC98-Options:- -- Includes:- -- Install-Includes:- -- Include-Dirs:- -- C-Sources:- -- Extra-Libraries:- -- Extra-Lib-Dirs:- -- CC-Options:- -- LD-Options:- -- Pkgconfig-Depends:- -- Frameworks:+ import: stuff+ hs-source-dirs: source+ Exposed: True+ Exposed-modules: Network.Xmpp+ , Network.Xmpp.IM+ , Network.Xmpp.Internal+ , Network.Xmpp.Lens+ , Network.Xmpp.Concurrent+ , Network.Xmpp.Concurrent.Basic+ , Network.Xmpp.Concurrent.IQ+ , Network.Xmpp.Concurrent.Message+ , Network.Xmpp.Concurrent.Monad+ , Network.Xmpp.Concurrent.Presence+ , Network.Xmpp.Concurrent.Threads+ , Network.Xmpp.Concurrent.Types+ , Network.Xmpp.IM.Message+ , Network.Xmpp.IM.Presence+ , Network.Xmpp.IM.PresenceTracker+ , Network.Xmpp.IM.Roster+ , Network.Xmpp.IM.Roster.Types+ , Network.Xmpp.IM.PresenceTracker.Types+ , Network.Xmpp.Marshal+ , Network.Xmpp.Sasl+ , Network.Xmpp.Sasl.Common+ , Network.Xmpp.Sasl.Mechanisms+ , Network.Xmpp.Sasl.Mechanisms.DigestMd5+ , Network.Xmpp.Sasl.Mechanisms.Plain+ , Network.Xmpp.Sasl.Mechanisms.Scram+ , Network.Xmpp.Sasl.StringPrep+ , Network.Xmpp.Sasl.Types+ , Network.Xmpp.Stanza+ , Network.Xmpp.Stream+ , Network.Xmpp.Tls+ , Network.Xmpp.Types+ , Network.Xmpp.Utilities --- Executable pontarius-xmpp-test- -- Main-Is: Test.hs- -- Build-Depends: hlogger ==0.0.3.0, base >= 2 && < 5, enumerator,- -- crypto-api, base64-string, regex-posix, pureMD5,- -- utf8-string, network, xml-types, text, transformers,- -- bytestring, binary, random, xml-enumerator, tls ==0.4.1,- -- containers- -- Other-Modules:- -- HS-Source-Dirs:- -- Extensions:- -- Build-Tools:- -- Buildable:- -- GHC-Options:- -- GHC-Prof-Options:- -- Hugs-Options:- -- NHC98-Options:- -- Includes:- -- Install-Includes:- -- Include-Dirs:- -- C-Sources:- -- Extra-Libraries:- -- Extra-Lib-Dirs:- -- CC-Options:- -- LD-Options:- -- Pkgconfig-Depends:- -- Frameworks:+ if flag(with-th) && impl(ghc >= 7.6.1)+ CPP-Options: -DWITH_TEMPLATE_HASKELL+ default-language: Haskell2010 +Test-Suite tests+ import: stuff+ Type: exitcode-stdio-1.0+ main-is: Main.hs+ Build-Depends: Cabal+ , QuickCheck+ , async+ , async+ , base+ , conduit+ , containers+ , data-default+ , generic-arbitrary+ , hslogger+ , hspec+ , hspec-expectations+ , lens+ , mtl+ , network+ , pontarius-xmpp+ , quickcheck-instances+ , ranges+ , smallcheck+ , stm+ , stringprep >= 1.0.0+ , tasty+ , tasty-hspec+ , tasty-hunit+ , tasty-quickcheck+ , tasty-th+ , text+ , transformers+ , xml-picklers+ , xml-types+ HS-Source-Dirs: tests+ Other-modules: Tests.Arbitrary+ , Tests.Arbitrary.Common+ , Tests.Arbitrary.Xml+ , Tests.Arbitrary.Xmpp+ , Tests.Parsers+ , Tests.Picklers+ , Tests.Stream+ ghc-options: -O2 -fno-warn-orphans -fconstraint-solver-iterations=10+ default-language: Haskell2010++Test-Suite runtests+ import: stuff+ -- requires credentials to auth at a remote server+ Buildable: False+ Type: exitcode-stdio-1.0+ hs-source-dirs: tests+ main-is: Run.hs+ other-modules: Run.Payload+ , Run.SendReceive+ , Run.Google+ , Run.Config+ GHC-Options: -threaded+ Build-Depends: base+ , HUnit+ , configurator+ , directory+ , filepath+ , hslogger+ , hspec+ , hspec-expectations+ , mtl+ , network+ , pontarius-xmpp+ , stm+ , text+ , xml-picklers+ , xml-types+ , tasty+ , tasty-hunit+ , tls+ default-language: Haskell2010++benchmark benchmarks+ import: stuff+ type: exitcode-stdio-1.0+ build-depends: base+ , criterion+ , pontarius-xmpp+ hs-source-dirs: benchmarks+ main-is: Bench.hs+ ghc-options: -O2+ default-language: Haskell2010+ Source-Repository head- Type: darcs- -- Module:- Location: https://patch-tag.com/r/jonkri/pontarius-xmpp- -- Subdir:+ Type: git+ Location: https://github.com/l29ah/pontarius-xmpp.git Source-Repository this- Type: darcs- -- Module:- Location: https://patch-tag.com/r/jonkri/pontarius-xmpp- Tag: 0.0.3.0- -- Subdir:+ Type: git+ Location: https://github.com/l29ah/pontarius-xmpp.git+ Tag: 0.5.7.2
+ source/Network/Xmpp.hs view
@@ -0,0 +1,255 @@+-- |+-- Module: $Header$+--+-- Stability: unstable+-- Portability: portable+--+-- The Extensible Messaging and Presence Protocol (XMPP) is an open technology+-- for near-real-time communication, which powers a wide range of applications+-- including instant messaging, presence, multi-party chat, voice and video+-- calls, collaboration, lightweight middleware, content syndication, and+-- generalized routing of XML data. XMPP provides a technology for the+-- asynchronous, end-to-end exchange of structured data by means of direct,+-- persistent XML streams among a distributed network of globally addressable,+-- presence-aware clients and servers.+--+-- Pontarius XMPP is an XMPP client library, implementing the core capabilities+-- of XMPP (RFC 6120): setup and tear-down of XML streams, channel encryption,+-- authentication, error handling, and communication primitives for messaging.+--+-- For low-level access to Pontarius XMPP, see the "Network.Xmpp.Internal"+-- module.+--+-- Getting Started+--+-- We use 'session' to create a session object and connect to a server. Here we+-- use the default 'SessionConfiguration'.+--+-- @+-- sess <- session realm (simpleAuth \"myUsername\" \"mypassword\") def+-- @+--+-- Defining 'AuthData' can be a bit unwieldy, so 'simpleAuth' gives us a+-- reasonable default. Though, for improved security, we should consider+-- restricting the mechanisms to 'scramSha1' whenever we can.+--+-- Next we have to set the presence to online, otherwise we won't be able to+-- send or receive stanzas to/from other entities.+--+-- @+-- sendPresence presenceOnline sess+-- @+--+{-# LANGUAGE CPP, NoMonomorphismRestriction, OverloadedStrings #-}++module Network.Xmpp+ ( -- * Session management+ Session+ , session+ , setConnectionClosedHandler+ , reconnect+ , reconnect'+ , reconnectNow+ , StreamConfiguration(..)+ , SessionConfiguration(..)+ , ConnectionDetails(..)+ , ConnectionState(..)+ , closeConnection+ , endSession+ , waitForStream+ , streamState+ -- ** Feature+ , StreamFeatures(..)+ , getFeatures+ -- ** Authentication handlers+ -- | The use of 'scramSha1' is /recommended/, but 'digestMd5' might be+ -- useful for interaction with older implementations.+ , SaslHandler+ , AuthData+ , Username+ , Password+ , AuthZID+ , simpleAuth+ , scramSha1+ , plain+ , digestMd5+ -- * Addressing+ -- | A JID (historically: Jabber ID) is XMPPs native format+ -- for addressing entities in the network. It is somewhat similar to an e-mail+ -- address, but contains three parts instead of two.+ , Jid+#if WITH_TEMPLATE_HASKELL+ , jid+ , jidQ+#endif+ , isBare+ , isFull+ , jidFromText+ , jidFromTexts+ , jidToText+ , jidToTexts+ , toBare+ , localpart+ , domainpart+ , resourcepart+ , parseJid+ , getJid+ -- * Stanzas+ -- | The basic protocol data unit in XMPP is the XML stanza. The stanza is+ -- essentially a fragment of XML that is sent over a stream. @Stanzas@ come in+ -- 3 flavors:+ --+ -- * /Message/, for traditional push-style message passing between peers+ --+ -- * /Presence/, for communicating status updates+ --+ -- * /Info/\//Query/ (or /IQ/), for request-response semantics communication+ --+ -- All stanza types have the following attributes in common:+ --+ -- * The /id/ attribute is used by the originating entity to track any+ -- response or error stanza that it might receive in relation to the+ -- generated stanza from another entity (such as an intermediate server or+ -- the intended recipient). It is up to the originating entity whether the+ -- value of the 'id' attribute is unique only within its current stream or+ -- unique globally.+ --+ -- * The /from/ attribute specifies the JID of the sender.+ --+ -- * The /to/ attribute specifies the JID of the intended recipient for the+ -- stanza.+ --+ -- * The /type/ attribute specifies the purpose or context of the message,+ -- presence, or IQ stanza. The particular allowable values for the 'type'+ -- attribute vary depending on whether the stanza is a message, presence,+ -- or IQ stanza.+ , getStanza+ , getStanzaChan+ , newStanzaID+ -- ** Messages+ -- | The /message/ stanza is a /push/ mechanism whereby one entity+ -- pushes information to another entity, similar to the communications that+ -- occur in a system such as email. It is not to be confused with+ -- an 'InstantMessage'+ , Message(..)+ , message+ , MessageError(..)+ , MessageType(..)+ -- *** Creating+ , answerMessage+ -- *** Sending+ , sendMessage+ -- *** Receiving+ , pullMessage+ , getMessage+ , getMessageA+ , waitForMessage+ , waitForMessageA+ , waitForMessageError+ , waitForMessageErrorA+ , filterMessages+ , filterMessagesA+ -- ** Presence+ -- | XMPP includes the ability for an entity to advertise its network+ -- availability, or "presence", to other entities. In XMPP, this availability+ -- for communication is signaled end-to-end by means of a dedicated+ -- communication primitive: the presence stanza.+ , Presence(..)+ , PresenceType(..)+ , PresenceError(..)+ -- *** Creating+ , presence+ , presenceOffline+ , presenceOnline+ , presenceSubscribe+ , presenceSubscribed+ , presenceUnsubscribe+ , presenceUnsubscribed+ , 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+ -- will broadcast that stanza to all subscribed entities. However, a+ -- publishing client may also send a presence stanza with a 'to' attribute, in+ -- which case the server will route or deliver that stanza to the intended+ -- recipient.+ , sendPresence+ -- *** Receiving+ , pullPresence+ , waitForPresence+ -- ** IQ+ -- | Info\/Query, or IQ, is a /request-response/ mechanism, similar in some+ -- ways to the Hypertext Transfer Protocol @HTTP@. The semantics of IQ enable+ -- an entity to make a request of, and receive a response from, another+ -- entity. The data content and precise semantics of the request and response+ -- is defined by the schema or other structural definition associated with the+ -- XML namespace that qualifies the direct child element of the IQ element. IQ+ -- interactions follow a common pattern of structured data exchange such as+ -- get\/result or set\/result (although an error can be returned in reply to a+ -- request if appropriate)+ --+ -- <http://xmpp.org/rfcs/rfc6120.html#stanzas-semantics-iq>+ , IQRequest(..)+ , IQRequestTicket+ , iqRequestBody+ , IQRequestType(..)+ , IQResult(..)+ , IQError(..)+ , IQResponse(..)+ , IQRequestClass(..)+ , IQRequestHandler+ , sendIQ+ , sendIQ'+ , sendIQRequest+ , answerIQ+ , iqResult+ , listenIQ+ , runIQHandler+ , unlistenIQ+ -- * Errors+ , StanzaErrorType(..)+ , StanzaError(..)+ , associatedErrorType+ , mkStanzaError+ , StanzaErrorCondition(..)+ , SaslFailure(..)+ , IQSendError(..)+ , IQRequestError(..)+ -- * Threads+ , dupSession+ -- * Lenses+ -- | Network.Xmpp doesn't re-export the accessors to avoid name+ -- clashes. To use them import Network.Xmpp.Lens+ , module Network.Xmpp.Lens+ -- * Plugins+ -- Plugins modify incoming and outgoing stanzas. They can, for example, handle+ -- encryption, compression or other protocol extensions.+ , Annotated+ , Annotation(..)+ , Plugin+ , Plugin'(..)+ -- * LangTag+ , LangTag+ , langTagFromText+ , langTagToText+ , parseLangTag+ -- * Miscellaneous+ , XmppFailure(..)+ , StreamErrorInfo(..)+ , StreamErrorCondition(..)+ , AuthFailure( AuthNoAcceptableMechanism+ , AuthSaslFailure+ , AuthIllegalCredentials+ , AuthOtherFailure )+ , connectTls+ , def+ ) where++import Network.Xmpp.Concurrent+import Network.Xmpp.Sasl+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Stanza+import Network.Xmpp.Types+import Network.Xmpp.Tls+import Network.Xmpp.Lens hiding (view, set, modify)+import Data.Default (def)
+ source/Network/Xmpp/Concurrent.hs view
@@ -0,0 +1,386 @@+{-# LANGUAGE TupleSections #-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.Xmpp.Concurrent+ ( module Network.Xmpp.Concurrent.Monad+ , module Network.Xmpp.Concurrent.Threads+ , module Network.Xmpp.Concurrent.Basic+ , module Network.Xmpp.Concurrent.Types+ , module Network.Xmpp.Concurrent.Message+ , module Network.Xmpp.Concurrent.Presence+ , module Network.Xmpp.Concurrent.IQ+ , newSession+ , session+ , newStanzaID+ , reconnect+ , reconnect'+ , reconnectNow+ , simpleAuth+ ) where++import Control.Applicative ((<$>))+import Control.Arrow (second)+import Control.Concurrent (threadDelay)+import Control.Concurrent.STM+import qualified Control.Exception as Ex+import Control.Monad+import Control.Monad.Except+import qualified Data.List as List+import qualified Data.Map as Map+import Data.Maybe+import Data.Text as Text hiding (show)+import Data.XML.Types+import Network.Socket+import Network.Xmpp.Concurrent.Basic+import Network.Xmpp.Concurrent.IQ+import Network.Xmpp.Concurrent.Message+import Network.Xmpp.Concurrent.Monad+import Network.Xmpp.Concurrent.Presence+import Network.Xmpp.Concurrent.Threads+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.IM.Roster+import Network.Xmpp.IM.Roster.Types+import Network.Xmpp.IM.PresenceTracker+import Network.Xmpp.IM.PresenceTracker.Types+import Network.Xmpp.Sasl+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Stream+import Network.Xmpp.Tls+import Network.Xmpp.Types+import System.Log.Logger+import System.Random (randomRIO)++import Control.Monad.State.Strict+++runHandlers :: [ XmppElement+ -> [Annotation]+ -> IO [Annotated XmppElement]+ ]+ -> XmppElement+ -> IO ()+runHandlers [] sta = do+ errorM "Pontarius.Xmpp" $+ "No stanza handlers set, discarding stanza" ++ show sta+ return ()+runHandlers hs sta = go hs sta []+ where go [] _ _ = return ()+ go (h:hands) sta' as = do+ res <- h sta' as+ forM_ res $ \(sta'', as') -> go hands sta'' (as ++ as')++toChan :: TChan (Annotated Stanza) -> StanzaHandler+toChan stanzaC _ sta as = do+ case sta of+ XmppStanza s -> atomically $ writeTChan stanzaC (s, as)+ _ -> return ()+ return [(sta, [])]++handleIQ :: TVar IQHandlers+ -> StanzaHandler+handleIQ _ _ s@XmppNonza{} _ = return [(s, [])]+handleIQ iqHands out s@(XmppStanza sta) as = do+ case sta of+ IQRequestS i -> handleIQRequest iqHands i >> return []+ IQResultS i -> handleIQResponse iqHands (Right i) >> return []+ IQErrorS i -> handleIQResponse iqHands (Left i) >> return []+ _ -> return [(s, [])]+ where+ -- If the IQ request has a namespace, send it through the appropriate channel.+ handleIQRequest :: TVar IQHandlers -> IQRequest -> IO ()+ handleIQRequest handlers iq = do+ res <- atomically $ do+ (byNS, _) <- readTVar handlers+ let iqNS = fromMaybe "" (nameNamespace . elementName+ $ iqRequestPayload iq)+ case Map.lookup (iqRequestType iq, iqNS) byNS of+ Nothing -> return . Just $ serviceUnavailable iq+ Just ch -> do+ sentRef <- newTMVar False+ let answerT answer attrs = do+ let IQRequest iqid from _to lang _tp bd _attrs = iq+ response = case answer of+ Left er -> IQErrorS $ IQError iqid Nothing+ from lang er+ (Just bd) attrs+ Right res -> IQResultS $ IQResult iqid Nothing+ from lang res+ attrs+ Ex.bracketOnError (atomically $ takeTMVar sentRef)+ (atomically . tryPutTMVar sentRef)+ $ \wasSent -> do+ case wasSent of+ True -> do+ atomically $ putTMVar sentRef True+ return Nothing+ False -> do+ didSend <- out $ XmppStanza response+ case didSend of+ Right () -> do+ atomically $ putTMVar sentRef True+ return $ Just (Right ())+ er@Left{} -> do+ atomically $ putTMVar sentRef False+ return $ Just er+ writeTChan ch $ IQRequestTicket answerT iq as+ return Nothing+ maybe (return ()) (void . out . XmppStanza) res+ serviceUnavailable (IQRequest iqid from _to lang _tp bd _attrs) =+ IQErrorS $ IQError iqid Nothing from lang err (Just bd) []+ err = StanzaError Cancel ServiceUnavailable Nothing Nothing++ handleIQResponse :: TVar IQHandlers -> Either IQError IQResult -> IO ()+ handleIQResponse handlers iq = atomically $ do+ (byNS, byID) <- readTVar handlers+ case Map.updateLookupWithKey (\_ _ -> Nothing) (iqID iq) byID of+ (Nothing, _) -> return () -- The handler might be removed due to+ -- timeout+ (Just (expectedJid, tmvar), byID') -> do+ let expected = case expectedJid of+ -- IQ was sent to the server and we didn't have a bound JID+ -- We just accept any matching response+ Left Nothing -> True+ -- IQ was sent to the server and we had a bound JID. Valid+ -- responses might have no to attribute, the domain of the+ -- server, our bare JID or our full JID+ Left (Just j) -> case iqFrom iq of+ Nothing -> True+ Just jf -> jf <~ j+ -- IQ was sent to a (full) JID. The answer has to come from+ -- the same exact JID.+ Right j -> iqFrom iq == Just j+ case expected of+ True -> do+ let answer = Just (either IQResponseError+ IQResponseResult iq, as)+ _ <- tryPutTMVar tmvar answer -- Don't block.+ writeTVar handlers (byNS, byID')+ False -> return ()+ where+ iqID (Left err') = iqErrorID err'+ iqID (Right iq') = iqResultID iq'+ iqFrom (Left err') = iqErrorFrom err'+ iqFrom (Right iq') = iqResultFrom iq'++-- | Creates and initializes a new Xmpp context.+newSession :: Stream+ -> SessionConfiguration+ -> HostName+ -> Maybe (ConnectionState -> [SaslHandler] , Maybe Text)+ -> IO (Either XmppFailure Session)+newSession stream config realm mbSasl = runExceptT $ do+ write' <- liftIO $ withStream' (gets $ streamSend . streamHandle) stream+ writeSem <- liftIO $ newTMVarIO write'+ stanzaChan <- lift newTChanIO+ iqHands <- lift $ newTVarIO (Map.empty, Map.empty)+ eh <- lift $ newEmptyTMVarIO+ ros <- case enableRoster config of+ False -> return $ Roster Nothing Map.empty+ True -> do+ mbRos <- liftIO $ initialRoster config+ return $ case mbRos of+ Nothing -> Roster Nothing Map.empty+ Just r -> r+ rosRef <- liftIO $ newTVarIO ros+ peers <- liftIO . newTVarIO $ Peers Map.empty+ rew <- lift $ newTVarIO 60+ let out = writeXmppElem writeSem+ boundJid <- liftIO $ withStream' (gets streamJid) stream+ let rosterH = if (enableRoster config)+ then [handleRoster boundJid rosRef+ (fromMaybe (\_ _ -> return ()) $ onRosterPush config)+ (out)]+ else []+ let presenceH = if (enablePresenceTracking config)+ then [handlePresence (onPresenceChange config) peers out]+ else []+ (sXmppElement, ps) <- initPlugins out $ plugins config+ let stanzaHandler = runHandlers $ List.concat+ [ inHandler <$> ps+ , [ toChan stanzaChan sXmppElement]+ , presenceH+ , rosterH+ , [ handleIQ iqHands sXmppElement]+ ]+ (kill, sState, reader) <- ExceptT $ startThreadsWith writeSem stanzaHandler+ eh stream+ (keepAlive config)+ idGen <- liftIO $ sessionStanzaIDs config+ let sess = Session { stanzaCh = stanzaChan+ , iqHandlers = iqHands+ , writeSemaphore = writeSem+ , readerThread = reader+ , idGenerator = idGen+ , streamRef = sState+ , eventHandlers = eh+ , stopThreads = kill+ , conf = config+ , rosterRef = rosRef+ , presenceRef = peers+ , sendStanza' = sXmppElement . XmppStanza+ , sRealm = realm+ , sSaslCredentials = mbSasl+ , reconnectWait = rew+ }+ liftIO . atomically $ putTMVar eh $+ EventHandlers { connectionClosedHandler = onConnectionClosed config sess }+ -- Pass the new session to the plugins so they can "tie the knot"+ liftIO . forM_ ps $ \p -> onSessionUp p sess+ return sess+ where+ -- Pass the stanza out action to each plugin+ initPlugins out' = go out' []+ where+ go out ps' [] = return (out, ps')+ go out ps' (p:ps) = do+ p' <- p out+ go (outHandler p') (p' : ps') ps++connectStream :: HostName+ -> SessionConfiguration+ -> AuthData+ -> IO (Either XmppFailure Stream)+connectStream realm config mbSasl = do+ Ex.bracketOnError (openStream realm (sessionStreamConfiguration config))+ (\s -> case s of+ Left _ -> return ()+ Right stream -> closeStreams stream)++ (\stream' -> case stream' of+ Left e -> return $ Left e+ Right stream -> do+ res <- runExceptT $ do+ ExceptT $ tls stream+ cs <- liftIO $ withStream (gets streamConnectionState)+ stream+ mbAuthError <- case mbSasl of+ Nothing -> return Nothing+ Just (handlers, resource) -> ExceptT $ auth (handlers cs)+ resource stream+ case mbAuthError of+ Nothing -> return ()+ Just e -> throwError $ XmppAuthFailure e+ return stream+ case res of+ Left e -> do+ debugM "Pontarius.Xmpp" "Closing stream after error"+ closeStreams stream+ return (Left e)+ Right r -> return $ Right r+ )++-- | Creates a 'Session' object by setting up a connection with an XMPP server.+--+-- Will connect to the specified host with the provided configuration. If the+-- third parameter is a 'Just' value, @session@ will attempt to authenticate and+-- acquire an XMPP resource.+session :: HostName -- ^ The hostname / realm+ -> AuthData+ -> SessionConfiguration -- ^ configuration details+ -> IO (Either XmppFailure Session)+session realm mbSasl config = runExceptT $ do+ stream <- ExceptT $ connectStream realm config mbSasl+ ses <- ExceptT $ newSession stream config realm mbSasl+ liftIO $ when (enableRoster config) $ initRoster ses+ return ses++-- | Authenticate using, in order of preference, 'scramSha1', 'digestMd5' and+-- finally, if both of those are not support and the stream is 'Secured' with+-- TLS, try 'plain'+--+-- The resource will be decided by the server+simpleAuth :: Username -> Password -> AuthData+simpleAuth uname pwd = Just (\cstate ->+ [ scramSha1 uname Nothing pwd+ , digestMd5 uname Nothing pwd+ ] +++ if (cstate == Secured)+ then [plain uname Nothing pwd]+ else []+ , Nothing)++-- | Reconnect immediately with the stored settings. Returns @Just@ the error+-- when the reconnect attempt fails and Nothing when no failure was encountered.+--+-- This function does not set your presence to online, so you will have to do+-- this yourself.+reconnectNow :: Session -- ^ session to reconnect+ -> IO (Maybe XmppFailure)+reconnectNow sess@Session{conf = config, reconnectWait = rw} = do+ debugM "Pontarius.Xmpp" "reconnecting"+ res <- flip withConnection sess $ \oldStream -> do+ debugM "Pontarius.Xmpp" "reconnect: closing stream"+ closeStreams oldStream+ debugM "Pontarius.Xmpp" "reconnect: opening stream"+ s <- connectStream (sRealm sess) config (sSaslCredentials sess)+ case s of+ Left e -> do+ errorM "Pontarius.Xmpp" $ "reconnect failed" ++ show e+ return (Left e , oldStream )+ Right r -> return (Right () , r )+ case res of+ Left e -> return $ Just e+ Right (Left e) -> return $ Just e+ Right (Right ()) -> do+ atomically $ writeTVar rw 60+ when (enableRoster config) $ initRoster sess+ return Nothing++-- | Reconnect with the stored settings.+--+-- Waits a random amount of seconds (between 0 and 60 inclusive) before the+-- first attempt and an increasing amount after each attempt after that. Caps+-- out at 2-5 minutes.+--+-- This function does not set your presence to online, so you will have to do+-- this yourself.+reconnect :: Integer -- ^ Maximum number of retries (numbers of 1 or less will+ -- perform exactly one retry)+ -> Session -- ^ Session to reconnect+ -> IO (Bool, [XmppFailure]) -- ^ Whether or not the reconnect attempt+ -- was successful, and a list of failure+ -- modes encountered+reconnect maxTries sess = go maxTries+ where+ go t = do+ res <- doRetry sess+ case res of+ Nothing -> return (True, [])+ Just e -> if (t > 1) then (second (e:)) <$> go (t - 1)+ else return $ (False, [e])++-- | Reconnect with the stored settings with an unlimited number of retries.+--+-- Waits a random amount of seconds (between 0 and 60 inclusive) before the+-- first attempt and an increasing amount after each attempt after that. Caps+-- out at 2-5 minutes.+--+-- This function does not set your presence to online, so you will have to do+-- this yourself.+reconnect' :: Session -- ^ Session to reconnect+ -> IO Integer -- ^ Number of failed retries before connection could be+ -- established+reconnect' sess = go 0+ where+ go i = do+ res <- doRetry sess+ case res of+ Nothing -> return i+ Just _e -> go (i+1)++doRetry :: Session -> IO (Maybe XmppFailure)+doRetry sess@Session{reconnectWait = rw} = do+ wait <- atomically $ do+ wt <- readTVar rw+ writeTVar rw $ min 300 (2 * wt)+ return wt+ t <- randomRIO (wait `div` 2 - 30, max 60 wait)+ debugM "Pontarius.Xmpp" $+ "Waiting " ++ show t ++ " seconds before reconnecting"+ threadDelay $ t * 10^(6 :: Int)+ reconnectNow sess++-- | Generates a new stanza identifier based on the 'sessionStanzaIDs' field of+-- 'SessionConfiguration'.+newStanzaID :: Session -> IO Text+newStanzaID = idGenerator
+ source/Network/Xmpp/Concurrent/Basic.hs view
@@ -0,0 +1,87 @@+{-# OPTIONS_HADDOCK hide #-}+module Network.Xmpp.Concurrent.Basic where++import Control.Applicative+import Control.Concurrent.STM+import qualified Control.Exception as Ex+import Control.Monad.State.Strict+import qualified Data.ByteString as BS+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.Marshal+import Network.Xmpp.Stream+import Network.Xmpp.Types+import Network.Xmpp.Utilities++semWrite :: WriteSemaphore -> BS.ByteString -> IO (Either XmppFailure ())+semWrite sem bs = Ex.bracket (atomically $ takeTMVar sem)+ (atomically . putTMVar sem)+ ($ bs)++writeXmppElem :: WriteSemaphore -> XmppElement -> IO (Either XmppFailure ())+writeXmppElem sem a = do+ let el = case a of+ XmppStanza s -> pickleElem xpStanza s+ XmppNonza n -> n+ outData = renderElement $ nsHack el+ debugOut outData+ semWrite sem outData++writeStanza :: WriteSemaphore -> Stanza -> IO (Either XmppFailure ())+writeStanza sem a = do+ let outData = renderElement $ nsHack (pickleElem xpStanza a)+ debugOut outData+ semWrite sem outData+++-- | Send a stanza to the server without running plugins. (The stanza is sent as+-- is)+sendRawStanza :: Stanza -> Session -> IO (Either XmppFailure ())+sendRawStanza a session = writeStanza (writeSemaphore session) a++-- | Send a stanza to the server, managed by plugins+sendStanza :: Stanza -> Session -> IO (Either XmppFailure ())+sendStanza = flip sendStanza'++-- | Get the channel of incoming stanzas.+getStanzaChan :: Session -> TChan (Stanza, [Annotation])+getStanzaChan session = stanzaCh session++-- | Get the next incoming stanza+getStanza :: Session -> IO (Stanza, [Annotation])+getStanza session = atomically . readTChan $ stanzaCh session++-- | Duplicate the inbound channel of the session object. Most receiving+-- functions discard stanzas they are not interested in from the inbound+-- channel. Duplicating the channel ensures that those stanzas can aren't lost+-- and can still be handled somewhere else.+dupSession :: Session -> IO Session+dupSession session = do+ stanzaCh' <- atomically $ cloneTChan (stanzaCh session)+ return $ session {stanzaCh = stanzaCh'}++-- | Return the JID assigned to us by the server+getJid :: Session -> IO (Maybe Jid)+getJid Session{streamRef = st} = do+ s <- atomically $ readTMVar st+ withStream' (gets streamJid) s++-- | Return the stream features the server announced+getFeatures :: Session -> IO StreamFeatures+getFeatures Session{streamRef = st} = do+ s <- atomically $ readTMVar st+ withStream' (gets streamFeatures) s++-- | Wait until the connection of the stream is re-established+waitForStream :: Session -> IO ()+waitForStream Session{streamRef = sr} = atomically $ do+ s <- readTMVar sr+ ss <- readTMVar $ unStream s+ case streamConnectionState ss of+ Plain -> return ()+ Secured -> return ()+ _ -> retry++streamState :: Session -> STM ConnectionState+streamState Session{streamRef = sr} = do+ s <- readTMVar sr+ streamConnectionState <$> (readTMVar $ unStream s)
+ source/Network/Xmpp/Concurrent/IQ.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_HADDOCK hide #-}+module Network.Xmpp.Concurrent.IQ where++import Control.Applicative ((<$>))+import Control.Concurrent (forkIO)+import Control.Concurrent.STM+import Control.Concurrent.Thread.Delay (delay)+import Control.Monad+import Control.Monad.Except+import Control.Monad.Trans+import qualified Data.Map as Map+import qualified Data.Map.Strict as Map.Strict+import Data.Maybe+import Data.Text (Text)+import Data.XML.Pickle+import Data.XML.Types+import Lens.Family2 (toListOf, (&), (^.))+import Network.Xmpp.Concurrent.Basic+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.Lens+import Network.Xmpp.Stanza+import Network.Xmpp.Types+import System.Log.Logger++-- | Sends an IQ, returns an STM action that returns the first inbound IQ with a+-- matching ID that has type @result@ or @error@ or Nothing if the timeout was+-- reached.+--+-- When sending the action fails, an XmppFailure is returned.+sendIQ :: Maybe Integer -- ^ Timeout . When the timeout is reached the response+ -- TMVar will be filled with 'IQResponseTimeout' and the+ -- id is removed from the list of IQ handlers. 'Nothing'+ -- deactivates the timeout+ -> Maybe Jid -- ^ Recipient (to)+ -> IQRequestType -- ^ IQ type (@Get@ or @Set@)+ -> Maybe LangTag -- ^ Language tag of the payload (@Nothing@ for+ -- default)+ -> Element -- ^ The IQ body (there has to be exactly one)+ -> [ExtendedAttribute] -- ^ Additional stanza attributes+ -> Session+ -> IO (Either XmppFailure (STM (Maybe (Annotated IQResponse))))+sendIQ timeOut t tp lang body attrs session = do+ newId <- idGenerator session+ j <- case t of+ Just t -> return $ Right t+ Nothing -> Left <$> getJid session+ ref <- atomically $ do+ resRef <- newEmptyTMVar+ let value = (j, resRef)+ (byNS, byId) <- readTVar (iqHandlers session)+ writeTVar (iqHandlers session) (byNS, Map.insert newId value byId)+ return resRef+ res <- sendStanza (IQRequestS $ IQRequest newId Nothing t lang tp body attrs)+ session+ case res of+ Right () -> do+ case timeOut of+ Nothing -> return ()+ Just t -> void . forkIO $ do+ delay t+ doTimeOut (iqHandlers session) newId ref+ return . Right $ readTMVar ref+ Left e -> return $ Left e+ where+ doTimeOut handlers iqid var = atomically $ do+ p <- tryPutTMVar var Nothing+ when p $ do+ (byNS, byId) <- readTVar (iqHandlers session)+ writeTVar handlers (byNS, Map.delete iqid byId)+ return ()++-- | Like 'sendIQ', but waits for the answer IQ.+sendIQA' :: Maybe Integer+ -> Maybe Jid+ -> IQRequestType+ -> Maybe LangTag+ -> Element+ -> [ExtendedAttribute]+ -> Session+ -> IO (Either IQSendError (Annotated IQResponse))+sendIQA' timeout to tp lang body attrs session = do+ ref <- sendIQ timeout to tp lang body attrs session+ either (return . Left . IQSendError) (fmap (maybe (Left IQTimeOut) Right)+ . atomically) ref++-- | Like 'sendIQ', but waits for the answer IQ. Discards plugin Annotations+sendIQ' :: Maybe Integer+ -> Maybe Jid+ -> IQRequestType+ -> Maybe LangTag+ -> Element+ -> [ExtendedAttribute]+ -> Session+ -> IO (Either IQSendError IQResponse)+sendIQ' timeout to tp lang body attrs session =+ fmap fst <$> sendIQA' timeout to tp lang body attrs session++-- | Register your interest in inbound IQ stanzas of a specific type and+-- namespace. The returned STM action yields the received, matching IQ stanzas.+--+-- If a handler for IQ stanzas with the given type and namespace is already+-- registered, the producer will be wrapped in Left. In this case the returned+-- request tickets may already be processed elsewhere.+listenIQ :: IQRequestType -- ^ Type of IQs to receive ('Get' or 'Set')+ -> Text -- ^ Namespace of the child element+ -> Session+ -> IO (Either (STM IQRequestTicket) (STM IQRequestTicket))+listenIQ tp ns session = do+ let handlers = (iqHandlers session)+ atomically $ do+ (byNS, byID) <- readTVar handlers+ iqCh <- newTChan+ let (present, byNS') = Map.Strict.insertLookupWithKey+ (\_ _ old -> old)+ (tp, ns)+ iqCh+ byNS+ writeTVar handlers (byNS', byID)+ case present of+ Nothing -> return . Right $ readTChan iqCh+ Just iqCh' -> do+ clonedChan <- cloneTChan iqCh'+ return . Left $ readTChan clonedChan+++-- | Unregister a previously registered IQ handler. No more IQ stanzas will be+-- delivered to any of the returned producers.+unlistenIQ :: IQRequestType -- ^ Type of IQ ('Get' or 'Set')+ -> Text -- ^ Namespace of the child element+ -> Session+ -> IO ()+unlistenIQ tp ns session = do+ let handlers = (iqHandlers session)+ atomically $ do+ (byNS, byID) <- readTVar handlers+ let byNS' = Map.delete (tp, ns) byNS+ writeTVar handlers (byNS', byID)+ return ()++-- | Answer an IQ request. Only the first answer ist sent and Just True is+-- returned when the answer is sucessfully sent. If an error occured during+-- sending Just False is returned (and another attempt can be+-- undertaken). Subsequent answers after the first sucessful one are dropped and+-- (False is returned in that case)+answerIQ :: IQRequestTicket+ -> Either StanzaError (Maybe Element)+ -> [ExtendedAttribute]+ -> IO (Maybe (Either XmppFailure ()))+answerIQ = answerTicket+++-- Class++class IQRequestClass a where+ data IQResponseType a+ pickleRequest :: PU Element a+ pickleResponse :: PU [Element] (IQResponseType a)+ requestType :: a -> IQRequestType+ requestNamespace :: a -> Text++data IQRequestError = IQRequestSendError XmppFailure+ | IQRequestTimeout+ | IQRequestUnpickleError UnpickleError+ deriving Show++-- | Send an IQ request. May throw IQSendError, UnpickleError,++sendIQRequest :: (IQRequestClass a, MonadError IQRequestError m, MonadIO m) =>+ Maybe Integer+ -> Maybe Jid+ -> a+ -> Session+ -> m (Either IQError (IQResponseType a))+sendIQRequest timeout t req con = do+ mbRes <- liftIO $ sendIQ' timeout t (requestType req) Nothing+ (pickle pickleRequest req) [] con+ case mbRes of+ Left (IQTimeOut) -> throwError IQRequestTimeout+ Left (IQSendError e) -> throwError $ IQRequestSendError e+ Right (IQResponseError e) -> return $ Left e+ Right (IQResponseResult res) ->+ case unpickle pickleResponse (res & toListOf payloadT) of+ Left e -> throwError $ IQRequestUnpickleError e+ Right r -> return $ Right r++type IQRequestHandler a = a -> IO (Either StanzaError (IQResponseType a))++runIQHandler :: IQRequestClass a =>+ IQRequestHandler a+ -> Session+ -> IO ()+runIQHandler (handler :: a -> IO (Either StanzaError (IQResponseType a)))+ sess = do+ let prx = undefined :: a+ ns = (requestNamespace prx)+ mbChan <- listenIQ (requestType prx) ns sess+ case mbChan of+ Left _ -> warningM "Pontarius.Xmpp" $ "IQ namespace " ++ show ns+ ++ " is already handled"+ Right getNext -> forever $ do+ ticket <- atomically getNext+ case unpickle pickleRequest (iqRequestBody ticket ^. payload) of+ Left _ -> answerIQ ticket (Left $ mkStanzaError BadRequest) []+ Right req -> do+ res <- handler req+ case res of+ Left e -> answerIQ ticket (Left e) []+ Right r -> do+ let answer = (pickle pickleResponse r)+ answerIQ ticket (Right $ listToMaybe answer ) []
+ source/Network/Xmpp/Concurrent/Message.hs view
@@ -0,0 +1,96 @@+{-# OPTIONS_HADDOCK hide #-}+module Network.Xmpp.Concurrent.Message where++import Control.Applicative((<$>))+import Network.Xmpp.Concurrent.Types+import Control.Concurrent.STM+import Network.Xmpp.Types+import Network.Xmpp.Concurrent.Basic++-- | Draw and discard stanzas from the inbound channel until a message or+-- message error is found. Returns the message or message error with annotations.+pullMessageA :: Session -> IO (Either (Annotated MessageError) (Annotated Message))+pullMessageA session = do+ (stanza, as) <- atomically . readTChan $ stanzaCh session+ case stanza of+ MessageS m -> return $ Right (m, as)+ MessageErrorS e -> return $ Left (e, as)+ _ -> pullMessageA session++-- | Draw and discard stanzas from the inbound channel until a message or+-- message error is found. Returns the message or message error.+pullMessage :: Session -> IO (Either MessageError Message)+pullMessage s = either (Left . fst) (Right . fst) <$> pullMessageA s++-- | Draw and discard stanzas from the inbound channel until a message is+-- found. Returns the message with annotations.+getMessageA :: Session -> IO (Annotated Message)+getMessageA = waitForMessageA (const True)++-- | Draw and discard stanzas from the inbound channel until a message is+-- found. Returns the message.+getMessage :: Session -> IO Message+getMessage s = fst <$> getMessageA s++-- | Draw and discard stanzas from the inbound channel until a message matching+-- the given predicate is found. Returns the matching message with annotations.+waitForMessageA :: (Annotated Message -> Bool) -> Session -> IO (Annotated Message)+waitForMessageA f session = do+ s <- pullMessageA session+ case s of+ Left _ -> waitForMessageA f session+ Right m | f m -> return m+ | otherwise -> waitForMessageA f session++-- | Draw and discard stanzas from the inbound channel until a message matching+-- the given predicate is found. Returns the matching message.+waitForMessage :: (Message -> Bool) -> Session -> IO Message+waitForMessage f s = fst <$> waitForMessageA (f . fst) s++-- | Draw and discard stanzas from the inbound channel until a message error+-- matching the given predicate is found. Returns the matching message error with+-- annotations.+waitForMessageErrorA :: (Annotated MessageError -> Bool)+ -> Session+ -> IO (Annotated MessageError)+waitForMessageErrorA f session = do+ s <- pullMessageA session+ case s of+ Right _ -> waitForMessageErrorA f session+ Left m | f m -> return m+ | otherwise -> waitForMessageErrorA f session++-- | Draw and discard stanzas from the inbound channel until a message error+-- matching the given predicate is found. Returns the matching message error+waitForMessageError :: (MessageError -> Bool) -> Session -> IO MessageError+waitForMessageError f s = fst <$> waitForMessageErrorA (f . fst) s++-- | Draw and discard stanzas from the inbound channel until a message or+-- message error matching the given respective predicate is found. Returns the+-- matching message or message error with annotations+filterMessagesA :: (Annotated MessageError -> Bool)+ -> (Annotated Message -> Bool)+ -> Session -> IO (Either (Annotated MessageError)+ (Annotated Message))+filterMessagesA f g session = do+ s <- pullMessageA session+ case s of+ Left e | f e -> return $ Left e+ | otherwise -> filterMessagesA f g session+ Right m | g m -> return $ Right m+ | otherwise -> filterMessagesA f g session++-- | Draw and discard stanzas from the inbound channel until a message or+-- message error matching the given respective predicate is found. Returns the+-- matching message or message error.+filterMessages :: (MessageError -> Bool)+ -> (Message -> Bool)+ -> Session+ -> IO (Either MessageError Message)+filterMessages f g s = either (Left . fst) (Right . fst) <$>+ filterMessagesA (f . fst) (g . fst) s++-- | Send a message stanza. Returns @Left@ when the 'Message' could not be+-- sent.+sendMessage :: Message -> Session -> IO (Either XmppFailure ())+sendMessage m session = sendStanza (MessageS m) session
+ source/Network/Xmpp/Concurrent/Monad.hs view
@@ -0,0 +1,118 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.Xmpp.Concurrent.Monad where++import Control.Applicative ((<$>))+import Control.Concurrent+import Control.Concurrent.STM+import qualified Control.Exception.Lifted as Ex+import Control.Monad.State+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.Stream+import Network.Xmpp.Types++-- TODO: Wait for presence error?++-- | Run an XmppConMonad action in isolation. Reader and writer workers will be+-- temporarily stopped and resumed with the new session details once the action+-- returns. The action will run in the calling thread. Any uncaught exceptions+-- will be interpreted as connection failure.+-- withConnection :: XmppConMonad a -> Context -> IO (Either StreamError a)+withConnection :: (Stream -> IO (b, Stream))+ -> Session+ -> IO (Either XmppFailure b)+withConnection a session = do+ wait <- newEmptyTMVarIO+ Ex.mask_ $ do+ -- Suspends the reader until the lock (wait) is released (set to `()').+ throwTo (readerThread session) $ Interrupt wait+ -- We acquire the write and stateRef locks, to make sure that this is+ -- the only thread that can write to the stream and to perform a+ -- withConnection calculation. Afterwards, we release the lock and+ -- fetch an updated state.+ s <- Ex.catch+ (atomically $ do+ _ <- takeTMVar (writeSemaphore session)+ s <- takeTMVar (streamRef session)+ putTMVar wait ()+ return s+ )+ -- If we catch an exception, we have failed to take the MVars above.+ (\e -> atomically (putTMVar wait ()) >>+ Ex.throwIO (e :: Ex.SomeException)+ )+ -- Run the XmppMonad action, save the (possibly updated) states, release+ -- the locks, and return the result.+ Ex.catches+ (do+ (res, s') <- a s+ wl <- withStream' (gets $ streamSend . streamHandle) s'+ atomically $ do+ putTMVar (writeSemaphore session) wl+ putTMVar (streamRef session) s'+ return $ Right res+ ) -- TODO: DO we have to replace the MVars in case of ane exception?+ -- We treat all Exceptions as fatal. If we catch a StreamError, we+ -- return it. Otherwise, we throw an exception.+ [ Ex.Handler $ \e -> return $ Left (e :: XmppFailure)+ , Ex.Handler $ \e -> killStream s+ >> Ex.throwIO (e :: Ex.SomeException)+ ]++-- | Executes a function to update the event handlers.+modifyHandlers :: (EventHandlers -> EventHandlers) -> Session -> IO ()+modifyHandlers f session = atomically $ modifyTMVar_ (eventHandlers session) f+ where+ -- Borrowing modifyTVar from+ -- http://hackage.haskell.org/packages/archive/stm/2.4/doc/html/src/Control-Concurrent-STM-TVar.html+ -- as it's not available in GHC 7.0.+ modifyTMVar_ :: TMVar a -> (a -> a) -> STM ()+ modifyTMVar_ var g = do+ x <- takeTMVar var+ putTMVar var (g x)++-- | Changes the handler to be executed when the server connection is closed. To+-- avoid race conditions the initial value should be set in the configuration+-- when creating the session+setConnectionClosedHandler :: (XmppFailure -> Session -> IO ()) -> Session -> IO ()+setConnectionClosedHandler eh session = do+ modifyHandlers (\s -> s{connectionClosedHandler =+ \e -> eh e session}) session++runConnectionClosedHandler :: Session -> XmppFailure -> IO ()+runConnectionClosedHandler session e = do+ h <- connectionClosedHandler <$> atomically (readTMVar+ $ eventHandlers session)+ h e++-- | Run an event handler.+runHandler :: (EventHandlers -> IO a) -> Session -> IO a+runHandler h session = h =<< atomically (readTMVar $ eventHandlers session)+++-- | End the current XMPP session. Kills the associated threads and closes the+-- connection.+--+-- Note that XMPP clients (that have signalled availability) should send+-- \"Unavailable\" presence prior to disconnecting.+--+-- The connectionClosedHandler will not be called (to avoid possibly+-- reestablishing the connection).+endSession :: Session -> IO ()+endSession session = do -- TODO: This has to be idempotent (is it?)+ stopThreads session+ _ <- flip withConnection session $ \stream -> do+ _ <- closeStreams stream+ return ((), stream)+ return ()+++-- | Close the connection to the server. Closes the stream (by enforcing a+-- write lock and sending a \</stream:stream\> element), waits (blocks) for+-- three seconds, and then closes the connection.+closeConnection :: Session -> IO ()+closeConnection session = do+ _ <-flip withConnection session $ \stream -> do+ _ <- closeStreams stream+ return ((), stream)+ runConnectionClosedHandler session StreamEndFailure
+ source/Network/Xmpp/Concurrent/Presence.hs view
@@ -0,0 +1,54 @@+{-# OPTIONS_HADDOCK hide #-}+module Network.Xmpp.Concurrent.Presence where++import Control.Applicative ((<$>))+import Control.Concurrent.STM+import Control.Lens.Prism (_Just)+import Lens.Family2 hiding (to)+import Lens.Family2.Stock hiding (_Just)+import Network.Xmpp.Concurrent.Basic+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.Lens+import Network.Xmpp.Types++-- | Read a presence stanza from the inbound stanza channel, discards any other+-- stanzas. Returns the presence stanza with annotations.+pullPresenceA :: Session -> IO (Either (Annotated PresenceError)+ (Annotated Presence))+pullPresenceA session = do+ (stanza, as) <- atomically . readTChan $ stanzaCh session+ case stanza of+ PresenceS p -> return $ Right (p, as)+ PresenceErrorS e -> return $ Left (e, as)+ _ -> pullPresenceA session++-- | Read a presence stanza from the inbound stanza channel, discards any other+-- stanzas. Returns the presence stanza.+pullPresence :: Session -> IO (Either PresenceError Presence)+pullPresence s = either (Left . fst) (Right . fst) <$> pullPresenceA s++-- | Draw and discard stanzas from the inbound channel until a presence stanza matching the given predicate is found. Return the presence stanza with annotations.+waitForPresenceA :: (Annotated Presence -> Bool)+ -> Session+ -> IO (Annotated Presence)+waitForPresenceA f session = do+ s <- pullPresenceA session+ case s of+ Left _ -> waitForPresenceA f session+ Right m | f m -> return m+ | otherwise -> waitForPresenceA f session++-- | Draw and discard stanzas from the inbound channel until a presence stanza matching the given predicate is found. Return the presence stanza with annotations.+waitForPresence :: (Presence -> Bool) -> Session -> IO Presence+waitForPresence f s = fst <$> waitForPresenceA (f . fst) s++-- | Send a presence stanza.+sendPresence :: Presence -> Session -> IO (Either XmppFailure ())+sendPresence p session = sendStanza (PresenceS checkedP) session+ where+ -- | RFC 6121 §3.1.1: When a user sends a presence subscription request to a+ -- potential instant messaging and presence contact, the value of the 'to'+ -- attribute MUST be a bare JID rather than a full JID+ checkedP = case presenceType p of+ Subscribe -> p & to . _Just %~ toBare+ _ -> p
+ source/Network/Xmpp/Concurrent/Threads.hs view
@@ -0,0 +1,131 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Network.Xmpp.Concurrent.Threads where++import Control.Applicative((<$>))+import Control.Concurrent+import Control.Concurrent.STM+import qualified Control.Exception.Lifted as Ex+import Control.Monad+import Control.Monad.Except+import Control.Monad.IO.Class+import qualified Data.ByteString as BS+import GHC.IO (unsafeUnmask)+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.Stream+import Network.Xmpp.Types+import System.Log.Logger++-- Worker to read stanzas from the stream and concurrently distribute them to+-- all listener threads.+readWorker :: (XmppElement -> IO ())+ -> (XmppFailure -> IO ())+ -> TMVar Stream+ -> IO a+readWorker onElement onCClosed stateRef = forever . Ex.mask_ $ do+ s' <- Ex.catches ( do+ atomically $ do+ s@(Stream con) <- readTMVar stateRef+ scs <- streamConnectionState <$> readTMVar con+ when (stateIsClosed scs)+ retry+ return $ Just s+ )+ [ Ex.Handler $ \(Interrupt t) -> do+ void $ handleInterrupts [t]+ return Nothing++ ]+ case s' of -- Maybe Stream+ Nothing -> return ()+ Just s -> do -- Stream+ res <- Ex.catches (do+ -- we don't know whether pull will+ -- necessarily be interruptible+ allowInterrupt+ res <- pullXmppElement s+ case res of+ Left e -> do+ errorM "Pontarius.Xmpp" $ "Read error: "+ ++ show e+ _ <- closeStreams s+ onCClosed e+ return Nothing+ Right r -> return $ Just r+ )+ [ Ex.Handler $ \(Interrupt t) -> do+ void $ handleInterrupts [t]+ return Nothing+ ]+ case res of+ Nothing -> return () -- Caught an exception, nothing to+ -- do. TODO: Can this happen?+ Just sta -> void $ onElement sta+ where+ -- Defining an Control.Exception.allowInterrupt equivalent for GHC 7+ -- compatibility.+ allowInterrupt :: IO ()+ allowInterrupt = unsafeUnmask $ return ()+ -- While waiting for the first semaphore(s) to flip we might receive another+ -- interrupt. When that happens we add it's semaphore to the list and retry+ -- waiting.+ handleInterrupts :: [TMVar ()] -> IO [()]+ handleInterrupts ts =+ Ex.catch (atomically $ forM ts takeTMVar)+ (\(Interrupt t) -> handleInterrupts (t:ts))+ stateIsClosed Closed = True+ stateIsClosed Finished = True+ stateIsClosed _ = False++-- | Runs thread in XmppState monad. Returns channel of incoming and outgoing+-- stances, respectively, and an Action to stop the Threads and close the+-- connection.+startThreadsWith :: TMVar (BS.ByteString -> IO (Either XmppFailure ()))+ -> (XmppElement -> IO ())+ -> TMVar EventHandlers+ -> Stream+ -> Maybe Int+ -> IO (Either XmppFailure (IO (),+ TMVar Stream,+ ThreadId))+startThreadsWith writeSem stanzaHandler eh con keepAlive = do+ -- read' <- withStream' (gets $ streamSend . streamHandle) con+ -- writeSem <- newTMVarIO read'+ conS <- newTMVarIO con+ cp <- forkIO $ connPersist keepAlive writeSem+ let onConClosed failure = do+ stopWrites+ noCon eh failure+ rdw <- forkIO $ readWorker stanzaHandler onConClosed conS+ return $ Right ( killConnection [rdw, cp]+ , conS+ , rdw+ )+ where+ stopWrites = atomically $ do+ _ <- takeTMVar writeSem+ putTMVar writeSem $ \_ -> return $ Left XmppNoStream+ killConnection threads = liftIO $ do+ debugM "Pontarius.Xmpp" "killing connection"+ stopWrites+ debugM "Pontarius.Xmpp" "killing threads"+ _ <- forM threads killThread+ return ()+ -- Call the connection closed handlers.+ noCon :: TMVar EventHandlers -> XmppFailure -> IO ()+ noCon h e = do+ hands <- atomically $ readTMVar h+ _ <- forkIO $ connectionClosedHandler hands e+ return ()++-- Acquires the write lock, pushes a space, and releases the lock.+-- | Sends a blank space every <delay> seconds to keep the connection alive.+connPersist :: Maybe Int -> TMVar (BS.ByteString -> IO a) -> IO ()+connPersist (Just delay) sem = forever $ do+ pushBS <- atomically $ takeTMVar sem+ _ <- pushBS " "+ atomically $ putTMVar sem pushBS+ threadDelay (delay*1000000)+connPersist Nothing _ = return ()
+ source/Network/Xmpp/Concurrent/Types.hs view
@@ -0,0 +1,197 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Network.Xmpp.Concurrent.Types where++import Control.Concurrent+import Control.Concurrent.STM+import qualified Control.Exception.Lifted as Ex+import Control.Monad.Except+import qualified Data.ByteString as BS+import Data.Default+import qualified Data.Map as Map+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Typeable+import Data.XML.Types (Element)+import Network.Socket+import Network.Xmpp.IM.Roster.Types+import Network.Xmpp.IM.PresenceTracker.Types+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Types++type StanzaHandler = (XmppElement -> IO (Either XmppFailure ()) ) -- ^ outgoing+ -- stanza+ -> XmppElement -- ^ stanza to handle+ -> [Annotation] -- ^ annotations added by previous handlers+ -> IO [(XmppElement, [Annotation])] -- ^ modified stanzas and+ -- /additional/ annotations++type Resource = Text++-- | SASL handlers and the desired JID resource+--+-- Nothing to disable authentication+--+-- The allowed SASL mecahnism can depend on the connection state. For example,+-- 'plain' should be avoided unless the connection state is 'Secured'+--+-- It is recommended to leave the resource up to the server+type AuthData = Maybe (ConnectionState -> [SaslHandler] , Maybe Resource)++-- | Annotations are auxiliary data attached to received stanzas by 'Plugin's to+-- convey information regarding their operation. For example, a plugin for+-- encryption might attach information about whether a received stanza was+-- encrypted and which algorithm was used.+data Annotation = forall f.(Typeable f, Show f) => Annotation{fromAnnotation :: f}++instance Show Annotation where+ show (Annotation x) = "Annotation{ fromAnnotation = " ++ show x ++ "}"++type Annotated a = (a, [Annotation])++-- | Retrieve the first matching annotation+getAnnotation :: Typeable b => Annotated a -> Maybe b+getAnnotation = foldr (\(Annotation a) b -> maybe b Just $ cast a) Nothing . snd++data Plugin' = Plugin'+ { -- | Resulting stanzas and additional Annotations+ inHandler :: XmppElement+ -> [Annotation]+ -> IO [(XmppElement, [Annotation])]+ , outHandler :: XmppElement -> IO (Either XmppFailure ())+ -- | In order to allow plugins to tie the knot (Plugin / Session) we pass+ -- the plugin the completed Session once it exists+ , onSessionUp :: Session -> IO ()+ }++type Plugin = (XmppElement -> IO (Either XmppFailure ())) -- ^ pass stanza to+ -- next plugin+ -> ExceptT XmppFailure IO Plugin'++type RosterPushCallback = Roster -> RosterUpdate -> IO ()++-- | Configuration for the @Session@ object.+data SessionConfiguration = SessionConfiguration+ { -- | Configuration for the @Stream@ object.+ sessionStreamConfiguration :: StreamConfiguration+ -- | Handler to be run when the conection to the XMPP server is+ -- closed. See also 'reconnect' and 'reconnect\'' for easy+ -- reconnection. The default does nothing+ , onConnectionClosed :: Session -> XmppFailure -> IO ()+ -- | Function to generate new stanza identifiers.+ , sessionStanzaIDs :: IO (IO Text)+ -- | Plugins can modify incoming and outgoing stanzas, for example to en-+ -- and decrypt them, respectively+ , plugins :: [Plugin]+ -- | Enable roster handling according to rfc 6121. See 'getRoster' to+ -- acquire the current roster+ , enableRoster :: Bool+ -- | Initial Roster to user when versioned rosters are supported+ , initialRoster :: IO (Maybe Roster)+ -- | Callback called on a roster Push. The callback is called after the+ -- roster is updated+ , onRosterPush :: Maybe RosterPushCallback+ -- | Track incomming presence stancas.+ , enablePresenceTracking :: Bool+ -- | Callback that is invoked when the presence status of a peer changes,+ -- i.e. it comes online, goes offline or its IM presence changes. The+ -- arguments are the (full) JID of the peer, the old state and the new+ -- state. The function is called in a new thread to avoid blocking+ -- handling stanzas+ , onPresenceChange :: Maybe ( Jid+ -> PeerStatus+ -> PeerStatus+ -> IO ())+ -- | How often to send keep-alives+ -- 'Nothing' disables keep-alive+ , keepAlive :: Maybe Int+ }++instance Default SessionConfiguration where+ def = SessionConfiguration { sessionStreamConfiguration = def+ , onConnectionClosed = \_ _ -> return ()+ , sessionStanzaIDs = do+ idRef <- newTVarIO 1+ return . atomically $ do+ curId <- readTVar idRef+ writeTVar idRef (curId + 1 :: Integer)+ return . Text.pack . show $ curId+ , plugins = []+ , enableRoster = True+ , initialRoster = return Nothing+ , onRosterPush = Nothing+ , enablePresenceTracking = True+ , onPresenceChange = Nothing+ , keepAlive = Just 30+ }++-- | Handlers to be run when the Xmpp session ends and when the Xmpp connection is+-- closed.+data EventHandlers = EventHandlers+ { connectionClosedHandler :: XmppFailure -> IO ()+ }++-- | Interrupt is used to signal to the reader thread that it should stop. Th contained semphore signals the reader to resume it's work.+data Interrupt = Interrupt (TMVar ()) deriving Typeable+instance Show Interrupt where show _ = "<Interrupt>"++instance Ex.Exception Interrupt++type WriteSemaphore = TMVar (BS.ByteString -> IO (Either XmppFailure ()))++-- | The Session object represents a single session with an XMPP server. You can+-- use 'session' to establish a session+data Session = Session+ { stanzaCh :: TChan (Stanza, [Annotation]) -- All stanzas+ , iqHandlers :: TVar IQHandlers+ -- Writing lock, so that only one thread could write to the stream at any+ -- given time.+ -- Fields below are from Context.+ , writeSemaphore :: WriteSemaphore+ , readerThread :: ThreadId+ , idGenerator :: IO Text+ -- | Lock (used by withStream) to make sure that a maximum of one+ -- Stream action is executed at any given time.+ , streamRef :: TMVar Stream+ , eventHandlers :: TMVar EventHandlers+ , stopThreads :: IO ()+ , rosterRef :: TVar Roster+ , presenceRef :: TVar Peers+ , conf :: SessionConfiguration+ , sendStanza' :: Stanza -> IO (Either XmppFailure ())+ , sRealm :: HostName+ , sSaslCredentials :: Maybe (ConnectionState -> [SaslHandler] , Maybe Text)+ , reconnectWait :: TVar Int+ }++-- | IQHandlers holds the registered channels for incoming IQ requests and+-- TMVars of and TMVars for expected IQ responses (the second Text represent a+-- stanza identifier.+type IQHandlers = ( Map.Map (IQRequestType, Text) (TChan IQRequestTicket)+ , Map.Map Text (Either (Maybe Jid) Jid,+ TMVar (Maybe (Annotated IQResponse)))+ )++-- | A received and wrapped up IQ request. Prevents you from (illegally)+-- answering a single IQ request multiple times+data IQRequestTicket = IQRequestTicket+ { -- | Send an answer to an IQ request once. Subsequent calls will do+ -- nothing and return Nothing+ answerTicket :: Either StanzaError (Maybe Element)+ -> [ExtendedAttribute]+ -> IO (Maybe (Either XmppFailure ()))+ -- | The actual IQ request that created this ticket.+ , iqRequestBody :: IQRequest+ -- | Annotations set by plugins in receive+ , iqRequestAnnotations :: [Annotation]+ }++-- | Error that can occur during sendIQ'+data IQSendError = IQSendError XmppFailure -- There was an error sending the IQ+ -- stanza+ | IQTimeOut -- No answer was received during the allotted time+ deriving (Show, Eq, Typeable)++instance Ex.Exception IQSendError
+ source/Network/Xmpp/IM.hs view
@@ -0,0 +1,42 @@+-- | RFC 6121: Instant Messaging and Presence+--+module Network.Xmpp.IM+ ( -- * Instant Messages+ InstantMessage(..)+ , MessageBody(..)+ , MessageThread(..)+ , MessageSubject(..)+ , Subscription(None, To, From, Both)+ , instantMessage+ , simpleIM+ , getIM+ , withIM+ , answerIM+ -- * Presence+ , ShowStatus(..)+ , IMPresence(..)+ , imPresence+ , getIMPresence+ , withIMPresence+ -- * Roster+ , Roster(..)+ , Item(..)+ , RosterUpdate(..)+ , getRoster+ , getRosterSTM+ , rosterAdd+ , rosterRemove+ -- * presenceTracker+ , PeerStatus(..)+ , isPeerAvailable+ , getEntityStatus+ , getAvailablePeers+ , getPeerEntities+ ) where++import Network.Xmpp.IM.Message+import Network.Xmpp.IM.Presence+import Network.Xmpp.IM.Roster+import Network.Xmpp.IM.Roster.Types+import Network.Xmpp.IM.PresenceTracker+import Network.Xmpp.IM.PresenceTracker.Types
+ source/Network/Xmpp/IM/Message.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK hide #-}++module Network.Xmpp.IM.Message where++import Data.Default+import Data.Function+import Data.List+import Data.Text (Text)+import Data.XML.Pickle+import Data.XML.Types+import Network.Xmpp.Marshal+import Network.Xmpp.Types++data MessageBody = MessageBody { bodyLang :: Maybe LangTag+ , bodyContent :: Text+ }++data MessageThread = MessageThread { threadID :: Text+ , threadParent :: Maybe Text+ }++data MessageSubject = MessageSubject { subjectLang :: Maybe LangTag+ , subjectContent :: Text+ }++-- | The instant message (IM) specific part of a message.+data InstantMessage = InstantMessage { imThread :: Maybe MessageThread+ , imSubject :: [MessageSubject]+ , imBody :: [MessageBody]+ }++-- | Empty instant message.+instantMessage :: InstantMessage+instantMessage = InstantMessage { imThread = Nothing+ , imSubject = []+ , imBody = []+ }++instance Default InstantMessage where+ def = instantMessage++-- | Get the IM specific parts of a message. Returns 'Nothing' when the received+-- payload is not valid IM data.+getIM :: Message -> Maybe InstantMessage+getIM im = either (const Nothing) Just . unpickle xpIM $ messagePayload im++sanitizeIM :: InstantMessage -> InstantMessage+sanitizeIM im = im{imBody = nubBy ((==) `on` bodyLang) $ imBody im}++-- | Append IM data to a message. Additional IM bodies with the same Langtag are+-- discarded.+withIM :: Message -> InstantMessage -> Message+withIM m im = m{ messagePayload = messagePayload m+ ++ pickleTree xpIM (sanitizeIM im) }++imToElements :: InstantMessage -> [Element]+imToElements im = pickle xpIM (sanitizeIM im)++-- | Generate a simple message+simpleIM :: Jid -- ^ recipient+ -> Text -- ^ body+ -> Message+simpleIM to bd = withIM message{messageTo = Just to}+ instantMessage{imBody = [MessageBody Nothing bd]}++-- | Generate an answer from a received message. The recepient is+-- taken from the original sender, the sender is set to 'Nothing',+-- message ID, language tag, message type as well as subject and+-- thread are inherited.+--+-- Additional IM bodies with the same Langtag are discarded.+answerIM :: [MessageBody] -> Message -> Maybe Message+answerIM bd msg = case getIM msg of+ Nothing -> Nothing+ Just im -> Just $ flip withIM (im{imBody = bd}) $+ message { messageID = messageID msg+ , messageFrom = Nothing+ , messageTo = messageFrom msg+ , messageLangTag = messageLangTag msg+ , messageType = messageType msg+ }++--------------------------+-- Picklers --------------+--------------------------+xpIM :: PU [Element] InstantMessage+xpIM = xpWrap (\(t, s, b) -> InstantMessage t s b)+ (\(InstantMessage t s b) -> (t, s, b))+ . xpClean+ $ xp3Tuple+ xpMessageThread+ xpMessageSubject+ xpMessageBody+++xpMessageSubject :: PU [Element] [MessageSubject]+xpMessageSubject = xpUnliftElems .+ xpWrap (map $ \(l, s) -> MessageSubject l s)+ (map $ \(MessageSubject l s) -> (l,s))+ $ xpElems "{jabber:client}subject" xpLangTag $ xpContent xpId++xpMessageBody :: PU [Element] [MessageBody]+xpMessageBody = xpUnliftElems .+ xpWrap (map $ \(l, s) -> MessageBody l s)+ (map $ \(MessageBody l s) -> (l,s))+ $ xpElems "{jabber:client}body" xpLangTag $ xpContent xpId++xpMessageThread :: PU [Element] (Maybe MessageThread)+xpMessageThread = xpUnliftElems+ . xpOption+ . xpWrap (\(t, p) -> MessageThread p t)+ (\(MessageThread p t) -> (t,p))+ $ xpElem "{jabber:client}thread"+ (xpAttrImplied "parent" xpId)+ (xpContent xpId)
+ source/Network/Xmpp/IM/Presence.hs view
@@ -0,0 +1,77 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NoMonomorphismRestriction #-}++module Network.Xmpp.IM.Presence where++import Data.Default+import Data.Text (Text)+import Data.XML.Pickle+import Data.XML.Types+import Network.Xmpp.Types++data ShowStatus = StatusAway+ | StatusChat+ | StatusDnd+ | StatusXa deriving (Read, Show, Eq)++data IMPresence = IMP { showStatus :: Maybe ShowStatus+ , status :: Maybe Text+ , priority :: Maybe Int+ } deriving (Show, Eq)++imPresence :: IMPresence+imPresence = IMP { showStatus = Nothing+ , status = Nothing+ , priority = Nothing+ }++instance Default IMPresence where+ def = imPresence++-- | Try to extract RFC6121 IM presence information from presence stanza.+-- Returns Nothing when the data is malformed, (Just IMPresence) otherwise.+getIMPresence :: Presence -> Maybe IMPresence+getIMPresence pres = case unpickle xpIMPresence (presencePayload pres) of+ Left _ -> Nothing+ Right r -> Just r++withIMPresence :: IMPresence -> Presence -> Presence+withIMPresence imPres pres = pres{presencePayload = presencePayload pres+ ++ pickleTree xpIMPresence+ imPres}++--+-- Picklers+--++xpIMPresence :: PU [Element] IMPresence+xpIMPresence = xpUnliftElems .+ xpWrap (\(s, st, p) -> IMP s st p)+ (\(IMP s st p) -> (s, st, p)) .+ xpClean $+ xp3Tuple+ (xpOption $ xpElemNodes "{jabber:client}show"+ (xpContent xpShow))+ -- TODO: Multiple status elements with different lang tags+ (xpOption $ xpElemNodes "{jabber:client}status"+ (xpContent xpText))+ (xpOption $ xpElemNodes "{jabber:client}priority"+ (xpContent xpPrim))++xpShow :: PU Text ShowStatus+xpShow = ("xpShow", "") <?>+ xpPartial ( \input -> case showStatusFromText input of+ Nothing -> Left "Could not parse show status."+ Just j -> Right j)+ showStatusToText+ where+ showStatusFromText "away" = Just StatusAway+ showStatusFromText "chat" = Just StatusChat+ showStatusFromText "dnd" = Just StatusDnd+ showStatusFromText "xa" = Just StatusXa+ showStatusFromText _ = Nothing+ showStatusToText StatusAway = "away"+ showStatusToText StatusChat = "chat"+ showStatusToText StatusDnd = "dnd"+ showStatusToText StatusXa = "xa"
+ source/Network/Xmpp/IM/PresenceTracker.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE RankNTypes #-}+module Network.Xmpp.IM.PresenceTracker where++import Control.Applicative+import Control.Concurrent+import Control.Concurrent.STM+import Control.Lens.Prism (_Just)+import Control.Monad+import qualified Data.Foldable as Foldable+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe+import Lens.Family2 hiding (Prism)+import Lens.Family2.Stock hiding (Prism, _Just, from)+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.IM.Presence+import Network.Xmpp.Lens hiding (Lens, Traversal)+import Network.Xmpp.Types+import Prelude hiding (mapM)++import Network.Xmpp.IM.PresenceTracker.Types++_peers :: Iso Peers (Map Jid (Map Jid (Maybe IMPresence)))+_peers = mkIso unPeers Peers++_PeerAvailable :: Prism PeerStatus (Maybe IMPresence)+_PeerAvailable = prism' PeerAvailable fromPeerAvailable+ where+ fromPeerAvailable (PeerAvailable pa) = Just pa+ fromPeerAvailable _ = Nothing++_PeerUnavailable :: Prism PeerStatus ()+_PeerUnavailable = prism' (const PeerUnavailable) fromPeerUnavailable+ where+ fromPeerUnavailable PeerUnavailable = Just ()+ fromPeerUnavailable _ = Nothing++_PeerStatus :: Iso (Maybe (Maybe IMPresence)) PeerStatus+_PeerStatus = mkIso toPeerStatus fromPeerStatus+ where+ toPeerStatus (Nothing) = PeerUnavailable+ toPeerStatus (Just imp) = PeerAvailable imp+ fromPeerStatus PeerUnavailable = Nothing+ fromPeerStatus (PeerAvailable imp) = Just imp++maybeMap :: Iso (Maybe (Map a b)) (Map a b)+maybeMap = mkIso maybeToMap mapToMaybe+ where+ maybeToMap Nothing = Map.empty+ maybeToMap (Just m) = m+ mapToMaybe m | Map.null m = Nothing+ | otherwise = Just m+++-- | Status of give full JID+peerStatusL :: Jid -> Lens' Peers PeerStatus+peerStatusL j = _peers . at (toBare j) . maybeMap . at j . _PeerStatus++peerMapPeerAvailable :: Jid -> Peers -> Bool+peerMapPeerAvailable j | isFull j = not . nullOf (peerStatusL j . _PeerAvailable)+ | otherwise = not . nullOf (_peers . at j . _Just)++handlePresence :: Maybe (Jid -> PeerStatus -> PeerStatus -> IO ())+ -> TVar Peers+ -> StanzaHandler+handlePresence onChange peers _ st _ = do+ let mbPr = do+ pr <- st ^? _Stanza . _Presence -- Only act on presence stanzas+ fr <- pr ^? from . _Just . _isFull -- Only act on full JIDs+ return (pr, fr)+ Foldable.forM_ mbPr $ \(pr, fr) ->+ case presenceType pr of+ Available -> setStatus fr (PeerAvailable (getIMPresence pr))+ Unavailable -> setStatus fr PeerUnavailable+ _ -> return ()+ return [(st, [])]+ where+ setStatus fr newStatus = do+ os <- atomically $ do+ ps <- readTVar peers+ let oldStatus = ps ^. peerStatusL fr+ writeTVar peers $ ps & set (peerStatusL fr) newStatus+ return oldStatus+ unless (os == newStatus) $ case onChange of+ Nothing -> return ()+ Just oc -> void . forkIO $ oc fr os newStatus+ return ()++-- | Check whether a given jid is available+isPeerAvailable :: Jid -> Session -> STM Bool+isPeerAvailable j sess = peerMapPeerAvailable j <$> readTVar (presenceRef sess)++-- | Get status of given full JID+getEntityStatus :: Jid -> Session -> STM PeerStatus+getEntityStatus j sess = do+ peers <- readTVar (presenceRef sess)+ return $ peers ^. peerStatusL j++-- | Get list of (bare) Jids with available entities+getAvailablePeers :: Session -> STM [Jid]+getAvailablePeers sess = do+ Peers peers <- readTVar (presenceRef sess)+ return $ Map.keys peers++-- | Get all available full JIDs to the given JID+getPeerEntities :: Jid -> Session -> STM (Map Jid (Maybe IMPresence))+getPeerEntities j sess = do+ Peers peers <- readTVar (presenceRef sess)+ case Map.lookup (toBare j) peers of+ Nothing -> return Map.empty+ Just js -> return js
+ source/Network/Xmpp/IM/PresenceTracker/Types.hs view
@@ -0,0 +1,20 @@+module Network.Xmpp.IM.PresenceTracker.Types where++import Data.Map (Map)++import Network.Xmpp.Types+import Network.Xmpp.IM.Presence++-- Map from bare JIDs to a map of full JIDs to show maybe status.+--+-- Invariants:+-- * The outer map should not have entries for bare JIDs that have no+-- available resource, i.e. the inner map should never be empty+--+-- * The inner map keys' local and domain part coincide with the outer keys'+newtype Peers = Peers { unPeers :: Map Jid (Map Jid (Maybe IMPresence))}+ deriving (Show)++data PeerStatus = PeerAvailable (Maybe IMPresence)+ | PeerUnavailable+ deriving (Show, Eq)
+ source/Network/Xmpp/IM/Roster.hs view
@@ -0,0 +1,254 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Xmpp.IM.Roster where++import Control.Applicative ((<$>))+import Control.Concurrent.STM+import Control.Monad+import Data.List (nub)+#if MIN_VERSION_containers(0, 5, 0)+import qualified Data.Map.Strict as Map+#else+import qualified Data.Map as Map+#endif+import Data.Maybe (isJust, fromMaybe)+import Data.Text (Text)+import Data.XML.Pickle+import Data.XML.Types+import System.Log.Logger++import Network.Xmpp.Concurrent.Basic+import Network.Xmpp.Concurrent.IQ+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.IM.Roster.Types+import Network.Xmpp.Marshal+import Network.Xmpp.Types++-- | Timeout to use with IQ requests+timeout :: Maybe Integer+timeout = Just 3000000 -- 3 seconds++-- | Add or update an item to the roster.+--+-- To update the item just send the complete set of new data.+rosterSet :: Jid -- ^ JID of the item+ -> Maybe Text -- ^ Name alias+ -> [Text] -- ^ Groups (duplicates will be removed)+ -> Session+ -> IO (Either IQSendError (Annotated IQResponse))+rosterSet j n gs session = do+ let el = pickleElem xpQuery (Query Nothing+ [QueryItem { qiApproved = Nothing+ , qiAsk = False+ , qiJid = j+ , qiName = n+ , qiSubscription = Nothing+ , qiGroups = nub gs+ }])+ sendIQA' timeout Nothing Set Nothing el [] session++-- | Synonym to rosterSet+rosterAdd :: Jid+ -> Maybe Text+ -> [Text]+ -> Session+ -> IO (Either IQSendError (Annotated IQResponse))+rosterAdd = rosterSet++-- | Remove an item from the roster. Return 'True' when the item is sucessfully+-- removed or if it wasn't in the roster to begin with.+rosterRemove :: Jid -> Session -> IO Bool+rosterRemove j sess = do+ roster <- getRoster sess+ case Map.lookup j (items roster) of+ Nothing -> return True -- jid is not on the Roster+ Just _ -> do+ res <- rosterPush (Item False False j Nothing Remove []) sess+ case res of+ Right (IQResponseResult IQResult{}, _) -> return True+ _ -> return False+ where+ rosterPush :: Item+ -> Session+ -> IO (Either IQSendError (Annotated IQResponse))+ rosterPush item session = do+ let el = pickleElem xpQuery (Query Nothing [fromItem item])+ sendIQA' timeout Nothing Set Nothing el [] session++-- | Retrieve the current Roster state (STM version)+getRosterSTM :: Session -> STM Roster+getRosterSTM session = readTVar (rosterRef session)++-- | Retrieve the current Roster state+getRoster :: Session -> IO Roster+getRoster session = atomically $ getRosterSTM session++-- | Get the initial roster or refresh the roster. You don't need to call this+-- on your own.+initRoster :: Session -> IO ()+initRoster session = do+ oldRoster <- getRoster session+ mbRoster <- retrieveRoster (if isJust (ver oldRoster) then Just oldRoster+ else Nothing ) session+ case mbRoster of+ Nothing -> errorM "Pontarius.Xmpp"+ "Server did not return a roster: "+ Just roster -> atomically $ writeTVar (rosterRef session) roster++handleRoster :: Maybe Jid+ -> TVar Roster+ -> RosterPushCallback+ -> StanzaHandler+handleRoster mbBoundJid ref onUpdate out sta _ = do+ case sta of+ XmppStanza (IQRequestS (iqr@IQRequest{iqRequestPayload =+ iqb@Element{elementName = en}}))+ | nameNamespace en == Just "jabber:iq:roster" -> do+ let doHandle = case (iqRequestFrom iqr, mbBoundJid) of+ -- We don't need to check our own JID when the IQ+ -- request was sent without a from address+ (Nothing, _) -> True+ -- We don't have a Jid bound, so we can't verify that+ -- the from address matches our bare jid+ (Just _fr, Nothing) -> False+ -- Check that the from address matches our bare jid+ (Just fr, Just boundJid) | fr == toBare boundJid -> True+ | otherwise -> False+ if doHandle+ then case unpickleElem xpQuery iqb of+ Right Query{ queryVer = v+ , queryItems = [update]+ } -> do+ handleUpdate v update+ _ <- out . XmppStanza $ result iqr+ return []+ _ -> do+ errorM "Pontarius.Xmpp" "Invalid roster query"+ _ <- out . XmppStanza $ badRequest iqr+ return []+ -- Don't handle roster pushes from unauthorized sources+ else return [(sta, [])]+ _ -> return [(sta, [])]+ where+ handleUpdate v' update = do+ oldRoster <- atomically $ readTVar ref+ case qiSubscription update of+ Just Remove -> do+ let j = qiJid update+ onUpdate oldRoster $ RosterUpdateRemove j+ updateRoster (Map.delete j)+ _ -> do+ let i = (toItem update)+ onUpdate oldRoster $ RosterUpdateAdd i+ updateRoster $ Map.insert (qiJid update) i+ where+ updateRoster f = atomically . modifyTVar ref $+ \(Roster v is) -> Roster (v' `mplus` v) (f is)++ badRequest (IQRequest iqid from _to lang _tp bd _attrs) =+ IQErrorS $ IQError iqid Nothing from lang errBR (Just bd) []+ errBR = StanzaError Cancel BadRequest Nothing Nothing+ result (IQRequest iqid from _to lang _tp _bd _attrs) =+ IQResultS $ IQResult iqid Nothing from lang Nothing []++retrieveRoster :: Maybe Roster -> Session -> IO (Maybe Roster)+retrieveRoster mbOldRoster sess = do+ useVersioning <- isJust . streamFeaturesRosterVer <$> getFeatures sess+ let version = if useVersioning+ then case mbOldRoster of+ Nothing -> Just ""+ Just oldRoster -> ver oldRoster+ else Nothing+ res <- sendIQ' timeout Nothing Get Nothing+ (pickleElem xpQuery (Query version []))+ []+ sess+ case res of+ Left e -> do+ errorM "Pontarius.Xmpp.Roster" $ "getRoster: " ++ show e+ return Nothing+ Right (IQResponseResult IQResult{iqResultPayload = Just ros})+ -> case unpickleElem xpQuery ros of+ Left _e -> do+ errorM "Pontarius.Xmpp.Roster" "getRoster: invalid query element"+ return Nothing+ Right ros' -> return . Just $ toRoster ros'+ Right (IQResponseResult IQResult{iqResultPayload = Nothing}) -> do+ return mbOldRoster+ -- sever indicated that no roster updates are necessary+ Right (IQResponseError e) -> do+ errorM "Pontarius.Xmpp.Roster" $ "getRoster: server returned error"+ ++ show e+ return Nothing+ where+ toRoster (Query v is) = Roster v (Map.fromList+ $ map (\i -> (qiJid i, toItem i))+ is)++toItem :: QueryItem -> Item+toItem qi = Item { riApproved = fromMaybe False (qiApproved qi)+ , riAsk = qiAsk qi+ , riJid = qiJid qi+ , riName = qiName qi+ , riSubscription = fromSubscription (qiSubscription qi)+ , riGroups = nub $ qiGroups qi+ }+ where+ fromSubscription Nothing = None+ fromSubscription (Just s) | s `elem` [None, To, From, Both] = s+ | otherwise = None++fromItem :: Item -> QueryItem+fromItem i = QueryItem { qiApproved = Nothing+ , qiAsk = False+ , qiJid = riJid i+ , qiName = riName i+ , qiSubscription = case riSubscription i of+ Remove -> Just Remove+ _ -> Nothing+ , qiGroups = nub $ riGroups i+ }++xpItems :: PU [Node] [QueryItem]+xpItems = xpWrap (map (\((app_, ask_, jid_, name_, sub_), groups_) ->+ QueryItem app_ ask_ jid_ name_ sub_ groups_))+ (map (\(QueryItem app_ ask_ jid_ name_ sub_ groups_) ->+ ((app_, ask_, jid_, name_, sub_), groups_))) $+ xpElems "{jabber:iq:roster}item"+ (xp5Tuple+ (xpAttribute' "approved" xpBool)+ (xpWrap isJust+ (\p -> if p then Just () else Nothing) $+ xpOption $ xpAttribute_ "ask" "subscribe")+ (xpAttribute "jid" xpJid)+ (xpAttribute' "name" xpText)+ (xpAttribute' "subscription" xpSubscription)+ )+ (xpFindMatches $ xpElemText "{jabber:iq:roster}group")++xpQuery :: PU [Node] Query+xpQuery = xpWrap (\(ver_, items_) -> Query ver_ items_ )+ (\(Query ver_ items_) -> (ver_, items_)) $+ xpElem "{jabber:iq:roster}query"+ (xpAttribute' "ver" xpText)+ xpItems++xpSubscription :: PU Text Subscription+xpSubscription = ("xpSubscription", "") <?>+ xpIso subscriptionFromText+ subscriptionToText+ where+ subscriptionFromText "none" = None+ subscriptionFromText "to" = To+ subscriptionFromText "from" = From+ subscriptionFromText "both" = Both+ subscriptionFromText "remove" = Remove+ subscriptionFromText _ = None+ subscriptionToText None = "none"+ subscriptionToText To = "to"+ subscriptionToText From = "from"+ subscriptionToText Both = "both"+ subscriptionToText Remove = "remove"
+ source/Network/Xmpp/IM/Roster/Types.hs view
@@ -0,0 +1,52 @@+{-# OPTIONS_HADDOCK hide #-}+module Network.Xmpp.IM.Roster.Types where++import qualified Data.Map as Map+import Data.Text (Text)+import Network.Xmpp.Types++-- Note that `Remove' is not exported from IM.hs, as it will never be visible to+-- the user anyway.+data Subscription = None -- ^ the user does not have a subscription to the+ -- contact's presence information, and the contact does+ -- not have a subscription to the user's presence+ -- information+ | To -- ^ the user has a subscription to the contact's+ -- presence information, but the contact does not have a+ -- subscription to the user's presence information+ | From -- ^ the contact has a subscription to the user's+ -- presence information, but the user does not have a+ -- subscription to the contact's presence information+ | Both -- ^ both the user and the contact have subscriptions+ -- to each other's presence information+ | Remove+ deriving (Eq, Read, Show)++data Roster = Roster { ver :: Maybe Text+ , items :: Map.Map Jid Item+ } deriving Show++-- | Roster Items+data Item = Item { riApproved :: Bool+ , riAsk :: Bool+ , riJid :: Jid+ , riName :: Maybe Text+ , riSubscription :: Subscription+ , riGroups :: [Text]+ } deriving Show++data RosterUpdate = RosterUpdateRemove Jid+ | RosterUpdateAdd Item -- ^ New or updated item+ deriving Show++data QueryItem = QueryItem { qiApproved :: Maybe Bool+ , qiAsk :: Bool+ , qiJid :: Jid+ , qiName :: Maybe Text+ , qiSubscription :: Maybe Subscription+ , qiGroups :: [Text]+ } deriving Show++data Query = Query { queryVer :: Maybe Text+ , queryItems :: [QueryItem]+ } deriving Show
+ source/Network/Xmpp/Internal.hs view
@@ -0,0 +1,73 @@+-- |+-- Module: $Header$+--+-- Stability: unstable+-- Portability: portable+--+-- This module allows for low-level access to Pontarius XMPP. Generally, the+-- "Network.Xmpp" module should be used instead.+--+-- 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+-- '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.Internal+ ( module Network.Xmpp.Concurrent+ , module Network.Xmpp.Concurrent.Basic+ , module Network.Xmpp.Concurrent.IQ+ , module Network.Xmpp.Concurrent.Message+ , module Network.Xmpp.Concurrent.Monad+ , module Network.Xmpp.Concurrent.Presence+ , module Network.Xmpp.Concurrent.Threads+ , module Network.Xmpp.Concurrent.Types+ , module Network.Xmpp.IM.Message+ , module Network.Xmpp.IM.Presence+ , module Network.Xmpp.IM.Roster+ , module Network.Xmpp.IM.Roster.Types+ , module Network.Xmpp.Marshal+ , module Network.Xmpp.Sasl+ , module Network.Xmpp.Sasl.Common+ , module Network.Xmpp.Sasl.Mechanisms+ , module Network.Xmpp.Sasl.Mechanisms.DigestMd5+ , module Network.Xmpp.Sasl.Mechanisms.Plain+ , module Network.Xmpp.Sasl.Mechanisms.Scram+ , module Network.Xmpp.Sasl.StringPrep+ , module Network.Xmpp.Sasl.Types+ , module Network.Xmpp.Stanza+ , module Network.Xmpp.Stream+ , module Network.Xmpp.Tls+ , module Network.Xmpp.Types+ , module Network.Xmpp.Utilities+ ) where+++import Network.Xmpp.Concurrent+import Network.Xmpp.Concurrent.Basic+import Network.Xmpp.Concurrent.IQ+import Network.Xmpp.Concurrent.Message+import Network.Xmpp.Concurrent.Monad+import Network.Xmpp.Concurrent.Presence+import Network.Xmpp.Concurrent.Threads+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.IM.Message+import Network.Xmpp.IM.Presence+import Network.Xmpp.IM.Roster+import Network.Xmpp.IM.Roster.Types+import Network.Xmpp.Marshal+import Network.Xmpp.Sasl+import Network.Xmpp.Sasl.Common+import Network.Xmpp.Sasl.Mechanisms+import Network.Xmpp.Sasl.Mechanisms.DigestMd5+import Network.Xmpp.Sasl.Mechanisms.Plain+import Network.Xmpp.Sasl.Mechanisms.Scram+import Network.Xmpp.Sasl.StringPrep+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Stanza+import Network.Xmpp.Stream hiding (mbl, lmb)+import Network.Xmpp.Tls+import Network.Xmpp.Types+import Network.Xmpp.Utilities
+ source/Network/Xmpp/Lens.hs view
@@ -0,0 +1,740 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}++-- | (More than just) Van Laarhoven lenses for XMPP types. The accessors in here+-- are designed to work with an optics library like lens or lens-family. This+-- module also provides a few simple functions ('view', 'modify', 'set' and+-- 'getAll') so you don't need to pull in another library to get some use out+-- of them.+--+-- * The name of the lenses corresponds to the field name of the data types with+-- an upper-case L appended. For documentation of the fields refer to the documentation of the data types (linked in the section header)+--+-- * Same goes for Traversals, except they are suffixed with a \'T\'+--+-- * Prism generally start with an underscore+--+-- /NB/ you do not need to import this module to get access to the optics+-- defined herein. They are also exported from Network.Xmpp. You only need to+-- import this module if you want to use the complementary accessor functions+-- without using an optics library like lens or lens-family++module Network.Xmpp.Lens+ ( Lens+ , Traversal+ , Prism+ , Iso+ -- * Accessors+ -- | Reimplementation of the basic lens functions so you don't have to+ -- bring in a lens library to use the optics++ -- ** Lenses+ , LF.view+ , modify+ , LF.set+ -- * Traversals+ , getAll+ -- * Prisms++ -- ** Construction+ , prism'+ , mkLens+ , mkIso+ -- * Lenses++ -- ** JID+ , _JidText+ , _isFull+ , _isBare++ -- ** Stanzas and Nonzas+ , _Stanza+ , _Nonza+ , _IQRequest+ , _IQResult+ , _IQError+ , _Message+ , _MessageError+ , _Presence+ , _PresenceError+ , IsStanza(..)+ , HasStanzaPayload(..)+ , IsErrorStanza(..)+ , messageTypeL+ , presenceTypeL+ , iqRequestTypeL+ -- *** 'StanzaError'+ , stanzaErrorTypeL+ , stanzaErrorConditionL+ , stanzaErrorTextL+ , stanzaErrorApplL+ -- ** Stream++ -- ** Stream Features+ , featureTlsL+ , featureMechanismsL+ , featureRosterVerL+ , featurePreApprovalL+ , featuresOtherL+ -- *** 'StreamConfiguration'+ , preferredLangL+ , toJidL+ , connectionDetailsL+ , resolvConfL+ , tlsBehaviourL+ , tlsParamsL+ -- **** TLS parameters+ , clientServerIdentificationL+ , tlsServerIdentificationL+ , clientSupportedL+ , supportedCiphersL+ , supportedVersionsL+ , tlsSupportedCiphersL+ , tlsSupportedVersionsL+ , clientUseServerNameIndicationL+ , tlsUseNameIndicationL+ -- *** 'SessionConfiguration'+ , streamConfigurationL+ , onConnectionClosedL+ , sessionStanzaIDsL+ , ensableRosterL+ , onRosterPushL+ , pluginsL+ , onPresenceChangeL+ -- ** IM+ -- *** Roster+ -- **** 'Roster'+ , verL+ , itemsL+ -- **** 'Item'+ , riApprovedL+ , riAskL+ , riJidL+ , riNameL+ , riSubscriptionL+ , riGroupsL+ -- **** 'QueryItem'+ , qiApprovedL+ , qiAskL+ , qiJidL+ , qiNameL+ , qiSubscriptionL+ , qiGroupsL+ -- **** 'Query'+ , queryVerL+ , queryItemsL+ -- ** IM Message+ -- *** 'MessageBody'+ , bodyLangL+ , bodyContentL+ -- *** 'MessageThread'+ , threadIdL+ , threadParentL+ -- *** 'MessageSubject'+ , subjectLangL+ , subjectContentL+ -- *** 'InstantMessage'+ , imThreadL+ , imSubjectL+ , imBodyL+ -- ** 'IMPresence'+ , showStatusL+ , statusL+ , priorityL++ )+ where++import Control.Applicative+import qualified Data.ByteString as BS+import Data.Functor.Identity (Identity(..))+import qualified Data.Map as Map+import Data.Profunctor+import Data.Text (Text)+import qualified Data.Text as Text+import Data.XML.Types (Element)+import qualified Lens.Family2 as LF+import Network.DNS (ResolvConf)+import Network.TLS as TLS+import Network.Xmpp.Concurrent.Types+import Network.Xmpp.IM.Message+import Network.Xmpp.IM.Presence+import Network.Xmpp.IM.PresenceTracker.Types+import Network.Xmpp.IM.Roster.Types+import Network.Xmpp.Types++-- | Van-Laarhoven lenses.+{-# DEPRECATED Lens "Use Lens' from lens-family or lens" #-}+type Lens a b = forall f . Functor f => (b -> f b) -> a -> f a++{-# DEPRECATED Traversal "Use Traversal' from lens-family or lens" #-}+type Traversal a b = forall f . Applicative f => (b -> f b) -> a -> f a++type Prism a b = forall p f. (Choice p, Applicative f) => p b (f b) -> p a (f a)++type Iso a b = forall p f. (Profunctor p, Functor f) => p b (f b) -> p a (f a)++prism' :: (b -> s) -> (s -> Maybe b) -> Prism s b+prism' bs sma = dimap (\s -> maybe (Left s) Right (sma s))+ (either pure (fmap bs)) . right'++mkLens :: (a -> b) -> (b -> a -> a) -> Lens a b+mkLens get set = \inj x -> fmap (flip set x) (inj $ get x)++mkIso :: (a -> b) -> (b -> a) -> Iso a b+mkIso t f = dimap t (fmap f)++newtype Collect a b = Collect {getCollection :: [a]} deriving Functor++instance Applicative (Collect a) where+ pure _ = Collect []+ Collect xs <*> Collect ys = Collect $ xs ++ ys++{-# DEPRECATED getAll "use toListOf (lens-family), partsOf (lens) or similar" #-}+-- | Return all the values a Traversal is pointing to in a list+getAll :: Traversal a b -> a -> [b]+getAll t = getCollection . t (Collect . pure)++{-# DEPRECATED modify "use over (lens-family, lens)" #-}+modify :: Traversal a b -> (b -> b) -> a -> a+modify t f = runIdentity . t (Identity . f)++-- Xmpp Lenses+--------------------++_JidText :: Prism Text Jid+_JidText = prism' jidToText jidFromText++_isFull :: Prism Jid Jid+_isFull = prism' id (\j -> if isFull j then Just j else Nothing)++_isBare :: Prism Jid Jid+_isBare = prism' toBare (\j -> if isBare j then Just j else Nothing)++_Stanza :: Prism XmppElement Stanza+_Stanza = prism' XmppStanza (\v -> case v of+ XmppStanza s -> Just s+ _ -> Nothing)++_Nonza :: Prism XmppElement Element+_Nonza = prism' XmppNonza (\v -> case v of+ XmppNonza n -> Just n+ _ -> Nothing)+++class IsStanza s where+ -- | From-attribute of the stanza+ from :: Lens s (Maybe Jid)+ -- | To-attribute of the stanza+ to :: Lens s (Maybe Jid)+ -- | Langtag of the stanza+ lang :: Lens s (Maybe LangTag)+ -- | Stanza ID. Setting this to /Nothing/ for IQ* stanzas will set the id to+ -- the empty Text.+ sid :: Lens s (Maybe Text)+ -- | Traversal over the payload elements.+ payloadT :: Traversal s Element++traverseList :: Traversal [a] a+traverseList _inj [] = pure []+traverseList inj (x:xs) = (:) <$> inj x <*> traverseList inj xs++instance IsStanza Message where+ from inj m@(Message{messageFrom=f}) = (\f' -> m{messageFrom = f'}) <$> inj f+ to inj m@(Message{messageTo=t}) = (\t' -> m{messageTo = t'}) <$> inj t+ lang inj m@(Message{messageLangTag=t}) =+ (\t' -> m{messageLangTag = t'}) <$> inj t+ sid inj m@(Message{messageID = i}) =+ ((\i' -> m{messageID = i'}) <$> inj i)+ payloadT inj m@(Message{messagePayload=pl}) =+ (\pl' -> m{messagePayload=pl'}) <$> traverseList inj pl+++instance IsStanza MessageError where+ from inj m@(MessageError{messageErrorFrom=f}) =+ (\f' -> m{messageErrorFrom = f'}) <$> inj f+ to inj m@(MessageError{messageErrorTo=t}) =+ (\t' -> m{messageErrorTo = t'}) <$> inj t+ lang inj m@(MessageError{messageErrorLangTag=t}) =+ (\t' -> m{messageErrorLangTag = t'}) <$> inj t+ sid inj m@(MessageError{messageErrorID = i}) =+ ((\i' -> m{messageErrorID = i'}) <$> inj i)+ payloadT inj m@(MessageError{messageErrorPayload=pl}) =+ (\pl' -> m{messageErrorPayload=pl'}) <$> traverseList inj pl++instance IsStanza Presence where+ from inj m@(Presence{presenceFrom=f}) = (\f' -> m{presenceFrom = f'}) <$> inj f+ to inj m@(Presence{presenceTo=t}) = (\t' -> m{presenceTo = t'}) <$> inj t+ lang inj m@(Presence{presenceLangTag=t}) =+ (\t' -> m{presenceLangTag = t'}) <$> inj t+ sid inj m@(Presence{presenceID = i}) =+ ((\i' -> m{presenceID = i'}) <$> inj i)+ payloadT inj m@(Presence{presencePayload=pl}) =+ (\pl' -> m{presencePayload=pl'}) <$> traverseList inj pl++instance IsStanza PresenceError where+ from inj m@(PresenceError{presenceErrorFrom=f}) =+ (\f' -> m{presenceErrorFrom = f'}) <$> inj f+ to inj m@(PresenceError{presenceErrorTo=t}) =+ (\t' -> m{presenceErrorTo = t'}) <$> inj t+ lang inj m@(PresenceError{presenceErrorLangTag=t}) =+ (\t' -> m{presenceErrorLangTag = t'}) <$> inj t+ sid inj m@(PresenceError{presenceErrorID = i}) =+ ((\i' -> m{presenceErrorID = i'}) <$> inj i)+ payloadT inj m@(PresenceError{presenceErrorPayload=pl}) =+ (\pl' -> m{presenceErrorPayload=pl'}) <$> traverseList inj pl++instance IsStanza IQRequest where+ from inj m@(IQRequest{iqRequestFrom=f}) =+ (\f' -> m{iqRequestFrom = f'}) <$> inj f+ to inj m@(IQRequest{iqRequestTo=t}) =+ (\t' -> m{iqRequestTo = t'}) <$> inj t+ lang inj m@(IQRequest{iqRequestLangTag=t}) =+ (\t' -> m{iqRequestLangTag = t'}) <$> inj t+ sid inj m@(IQRequest{iqRequestID = i}) =+ ((\i' -> m{iqRequestID = i'}) <$> maybeNonempty inj i)+ payloadT inj m@(IQRequest{iqRequestPayload=pl}) =+ (\pl' -> m{iqRequestPayload=pl'}) <$> inj pl++instance IsStanza IQResult where+ from inj m@(IQResult{iqResultFrom=f}) =+ (\f' -> m{iqResultFrom = f'}) <$> inj f+ to inj m@(IQResult{iqResultTo=t}) =+ (\t' -> m{iqResultTo = t'}) <$> inj t+ lang inj m@(IQResult{iqResultLangTag=t}) =+ (\t' -> m{iqResultLangTag = t'}) <$> inj t+ sid inj m@(IQResult{iqResultID = i}) =+ ((\i' -> m{iqResultID = i'}) <$> maybeNonempty inj i)+ payloadT inj m@(IQResult{iqResultPayload=pl}) =+ (\pl' -> m{iqResultPayload=pl'}) <$> maybe (pure Nothing)+ (fmap Just . inj) pl++instance IsStanza IQError where+ from inj m@(IQError{iqErrorFrom=f}) =+ (\f' -> m{iqErrorFrom = f'}) <$> inj f+ to inj m@(IQError{iqErrorTo=t}) =+ (\t' -> m{iqErrorTo = t'}) <$> inj t+ lang inj m@(IQError{iqErrorLangTag=t}) =+ (\t' -> m{iqErrorLangTag = t'}) <$> inj t+ sid inj m@(IQError{iqErrorID = i}) =+ ((\i' -> m{iqErrorID = i'}) <$> maybeNonempty inj i)+ payloadT inj m@(IQError{iqErrorPayload=pl}) =+ (\pl' -> m{iqErrorPayload=pl'}) <$> maybe (pure Nothing)+ (fmap Just . inj) pl++liftLens :: (forall s. IsStanza s => Lens s a) -> Lens Stanza a+liftLens f inj (IQRequestS s) = IQRequestS <$> f inj s+liftLens f inj (IQResultS s) = IQResultS <$> f inj s+liftLens f inj (IQErrorS s) = IQErrorS <$> f inj s+liftLens f inj (MessageS s) = MessageS <$> f inj s+liftLens f inj (MessageErrorS s) = MessageErrorS <$> f inj s+liftLens f inj (PresenceS s) = PresenceS <$> f inj s+liftLens f inj (PresenceErrorS s) = PresenceErrorS <$> f inj s++liftTraversal :: (forall s. IsStanza s => Traversal s a) -> Traversal Stanza a+liftTraversal f inj (IQRequestS s) = IQRequestS <$> f inj s+liftTraversal f inj (IQResultS s) = IQResultS <$> f inj s+liftTraversal f inj (IQErrorS s) = IQErrorS <$> f inj s+liftTraversal f inj (MessageS s) = MessageS <$> f inj s+liftTraversal f inj (MessageErrorS s) = MessageErrorS <$> f inj s+liftTraversal f inj (PresenceS s) = PresenceS <$> f inj s+liftTraversal f inj (PresenceErrorS s) = PresenceErrorS <$> f inj s++instance IsStanza Stanza where+ from = liftLens from+ to = liftLens to+ lang = liftLens lang+ sid = liftLens sid+ payloadT = liftTraversal payloadT++maybeNonempty :: Lens Text (Maybe Text)+maybeNonempty inj x = (maybe Text.empty id)+ <$> inj (if Text.null x then Nothing else Just x)+++_IQRequest :: Prism Stanza IQRequest+_IQRequest = prism' IQRequestS fromIQRequestS+ where+ fromIQRequestS (IQRequestS s) = Just s+ fromIQRequestS _ = Nothing++_IQResult :: Prism Stanza IQResult+_IQResult = prism' IQResultS fromIQResultS+ where+ fromIQResultS (IQResultS s) = Just s+ fromIQResultS _ = Nothing++_IQError :: Prism Stanza IQError+_IQError = prism' IQErrorS fromIQErrorS+ where+ fromIQErrorS (IQErrorS s) = Just s+ fromIQErrorS _ = Nothing++_Message :: Prism Stanza Message+_Message = prism' MessageS fromMessageS+ where+ fromMessageS (MessageS s) = Just s+ fromMessageS _ = Nothing++_MessageError :: Prism Stanza MessageError+_MessageError = prism' MessageErrorS fromMessageErrorS+ where+ fromMessageErrorS (MessageErrorS s) = Just s+ fromMessageErrorS _ = Nothing++_Presence :: Prism Stanza Presence+_Presence = prism' PresenceS fromPresenceS+ where+ fromPresenceS (PresenceS s) = Just s+ fromPresenceS _ = Nothing++_PresenceError :: Prism Stanza PresenceError+_PresenceError = prism' PresenceErrorS fromPresenceErrorS+ where+ fromPresenceErrorS (PresenceErrorS s) = Just s+ fromPresenceErrorS _ = Nothing++class IsErrorStanza s where+ -- | Error element of the stanza+ stanzaError :: Lens s StanzaError++instance IsErrorStanza IQError where+ stanzaError inj m@IQError{iqErrorStanzaError = i} =+ (\i' -> m{iqErrorStanzaError = i'}) <$> inj i++instance IsErrorStanza MessageError where+ stanzaError inj m@MessageError{messageErrorStanzaError = i} =+ (\i' -> m{messageErrorStanzaError = i'}) <$> inj i++instance IsErrorStanza PresenceError where+ stanzaError inj m@PresenceError{presenceErrorStanzaError = i} =+ (\i' -> m{presenceErrorStanzaError = i'}) <$> inj i++class HasStanzaPayload s p | s -> p where+ -- | Payload element(s) of the stanza. Since the amount of elements possible+ -- in a stanza vary by type, this lens can't be used with a general+ -- 'Stanza'. There is, however, a more general Traversable that works with+ -- all stanzas (including 'Stanza'): 'payloadT'+ payload :: Lens s p++instance HasStanzaPayload IQRequest Element where+ payload inj m@IQRequest{iqRequestPayload = i} =+ (\i' -> m{iqRequestPayload = i'}) <$> inj i++instance HasStanzaPayload IQResult (Maybe Element) where+ payload inj m@IQResult{iqResultPayload = i} =+ (\i' -> m{iqResultPayload = i'}) <$> inj i++instance HasStanzaPayload IQError (Maybe Element) where+ payload inj m@IQError{iqErrorPayload = i} =+ (\i' -> m{iqErrorPayload = i'}) <$> inj i++instance HasStanzaPayload Message [Element] where+ payload inj m@Message{messagePayload = i} =+ (\i' -> m{messagePayload = i'}) <$> inj i++instance HasStanzaPayload MessageError [Element] where+ payload inj m@MessageError{messageErrorPayload = i} =+ (\i' -> m{messageErrorPayload = i'}) <$> inj i++instance HasStanzaPayload Presence [Element] where+ payload inj m@Presence{presencePayload = i} =+ (\i' -> m{presencePayload = i'}) <$> inj i++instance HasStanzaPayload PresenceError [Element] where+ payload inj m@PresenceError{presenceErrorPayload = i} =+ (\i' -> m{presenceErrorPayload = i'}) <$> inj i++iqRequestTypeL :: Lens IQRequest IQRequestType+iqRequestTypeL inj p@IQRequest{iqRequestType = tp} =+ (\tp' -> p{iqRequestType = tp'}) <$> inj tp+++messageTypeL :: Lens Message MessageType+messageTypeL inj p@Message{messageType = tp} =+ (\tp' -> p{messageType = tp'}) <$> inj tp++presenceTypeL :: Lens Presence PresenceType+presenceTypeL inj p@Presence{presenceType = tp} =+ (\tp' -> p{presenceType = tp'}) <$> inj tp+++-- StanzaError+-----------------------++stanzaErrorTypeL :: Lens StanzaError StanzaErrorType+stanzaErrorTypeL inj se@StanzaError{stanzaErrorType = x} =+ (\x' -> se{stanzaErrorType = x'}) <$> inj x++stanzaErrorConditionL :: Lens StanzaError StanzaErrorCondition+stanzaErrorConditionL inj se@StanzaError{stanzaErrorCondition = x} =+ (\x' -> se{stanzaErrorCondition = x'}) <$> inj x++stanzaErrorTextL :: Lens StanzaError (Maybe (Maybe LangTag, NonemptyText))+stanzaErrorTextL inj se@StanzaError{stanzaErrorText = x} =+ (\x' -> se{stanzaErrorText = x'}) <$> inj x++stanzaErrorApplL :: Lens StanzaError (Maybe Element)+stanzaErrorApplL inj se@StanzaError{stanzaErrorApplicationSpecificCondition = x} =+ (\x' -> se{stanzaErrorApplicationSpecificCondition = x'}) <$> inj x+++-- StreamConfiguration+-----------------------++preferredLangL :: Lens StreamConfiguration (Maybe LangTag)+preferredLangL inj sc@StreamConfiguration{preferredLang = x}+ = (\x' -> sc{preferredLang = x'}) <$> inj x++toJidL :: Lens StreamConfiguration (Maybe (Jid, Bool))+toJidL inj sc@StreamConfiguration{toJid = x}+ = (\x' -> sc{toJid = x'}) <$> inj x++connectionDetailsL :: Lens StreamConfiguration ConnectionDetails+connectionDetailsL inj sc@StreamConfiguration{connectionDetails = x}+ = (\x' -> sc{connectionDetails = x'}) <$> inj x++resolvConfL :: Lens StreamConfiguration ResolvConf+resolvConfL inj sc@StreamConfiguration{resolvConf = x}+ = (\x' -> sc{resolvConf = x'}) <$> inj x++tlsBehaviourL :: Lens StreamConfiguration TlsBehaviour+tlsBehaviourL inj sc@StreamConfiguration{tlsBehaviour = x}+ = (\x' -> sc{tlsBehaviour = x'}) <$> inj x+++tlsParamsL :: Lens StreamConfiguration ClientParams+tlsParamsL inj sc@StreamConfiguration{tlsParams = x}+ = (\x' -> sc{tlsParams = x'}) <$> inj x++-- TLS parameters+-----------------++clientServerIdentificationL :: Lens ClientParams (String, BS.ByteString)+clientServerIdentificationL inj cp+ = (\x' -> cp{clientServerIdentification = x'}) <$> inj (clientServerIdentification cp)++clientSupportedL :: Lens ClientParams Supported+clientSupportedL inj cp+ = (\x' -> cp{clientSupported = x'}) <$> inj (clientSupported cp)++clientUseServerNameIndicationL :: Lens ClientParams Bool+clientUseServerNameIndicationL inj cp+ = (\x' -> cp{clientUseServerNameIndication = x'}) <$> inj (clientUseServerNameIndication cp)++supportedCiphersL :: Lens Supported [Cipher]+supportedCiphersL inj s+ = (\x' -> s{supportedCiphers = x'}) <$> inj (supportedCiphers s)++supportedVersionsL :: Lens Supported [TLS.Version]+supportedVersionsL inj s+ = (\x' -> s{supportedVersions = x'}) <$> inj (supportedVersions s)++-- SessionConfiguration+-----------------------+streamConfigurationL :: Lens SessionConfiguration StreamConfiguration+streamConfigurationL inj sc@SessionConfiguration{sessionStreamConfiguration = x}+ = (\x' -> sc{sessionStreamConfiguration = x'}) <$> inj x++onConnectionClosedL :: Lens SessionConfiguration (Session -> XmppFailure -> IO ())+onConnectionClosedL inj sc@SessionConfiguration{onConnectionClosed = x}+ = (\x' -> sc{onConnectionClosed = x'}) <$> inj x++sessionStanzaIDsL :: Lens SessionConfiguration (IO (IO Text))+sessionStanzaIDsL inj sc@SessionConfiguration{sessionStanzaIDs = x}+ = (\x' -> sc{sessionStanzaIDs = x'}) <$> inj x++ensableRosterL :: Lens SessionConfiguration Bool+ensableRosterL inj sc@SessionConfiguration{enableRoster = x}+ = (\x' -> sc{enableRoster = x'}) <$> inj x++onRosterPushL :: Lens SessionConfiguration (Maybe RosterPushCallback)+onRosterPushL = mkLens onRosterPush (\orp x -> x{onRosterPush = orp})++pluginsL :: Lens SessionConfiguration [Plugin]+pluginsL inj sc@SessionConfiguration{plugins = x}+ = (\x' -> sc{plugins = x'}) <$> inj x++onPresenceChangeL :: Lens SessionConfiguration (Maybe ( Jid -> PeerStatus+ -> PeerStatus -> IO ()))+onPresenceChangeL inj sc@SessionConfiguration{onPresenceChange = x}+ = (\x' -> sc{onPresenceChange = x'}) <$> inj x++-- | Access clientServerIdentification inside tlsParams inside streamConfiguration+tlsServerIdentificationL :: Lens SessionConfiguration (String, BS.ByteString)+tlsServerIdentificationL = streamConfigurationL+ . tlsParamsL+ . clientServerIdentificationL++-- | Access clientUseServerNameIndication inside tlsParams+tlsUseNameIndicationL :: Lens SessionConfiguration Bool+tlsUseNameIndicationL = streamConfigurationL+ . tlsParamsL+ . clientUseServerNameIndicationL++-- | Access supportedCiphers inside clientSupported inside tlsParams+tlsSupportedCiphersL :: Lens SessionConfiguration [Cipher]+tlsSupportedCiphersL = streamConfigurationL+ . tlsParamsL . clientSupportedL . supportedCiphersL++-- | Access supportedVersions inside clientSupported inside tlsParams+tlsSupportedVersionsL :: Lens SessionConfiguration [TLS.Version]+tlsSupportedVersionsL = streamConfigurationL+ . tlsParamsL . clientSupportedL . supportedVersionsL+++-- Roster+------------------++verL :: Lens Roster (Maybe Text)+verL inj r@Roster{ver = x} = (\x' -> r{ver = x'}) <$> inj x++itemsL :: Lens Roster (Map.Map Jid Item)+itemsL inj r@Roster{items = x} = (\x' -> r{items = x'}) <$> inj x++-- Item+------------------++riApprovedL :: Lens Item Bool+riApprovedL inj i@Item{riApproved = x} = (\x' -> i{riApproved = x'}) <$> inj x++riAskL :: Lens Item Bool+riAskL inj i@Item{riAsk = x} = (\x' -> i{riAsk = x'}) <$> inj x++riJidL :: Lens Item Jid+riJidL inj i@Item{riJid = x} = (\x' -> i{riJid = x'}) <$> inj x++riNameL :: Lens Item (Maybe Text)+riNameL inj i@Item{riName = x} = (\x' -> i{riName = x'}) <$> inj x++riSubscriptionL :: Lens Item Subscription+riSubscriptionL inj i@Item{riSubscription = x} =+ (\x' -> i{riSubscription = x'}) <$> inj x++riGroupsL :: Lens Item [Text]+riGroupsL inj i@Item{riGroups = x} = (\x' -> i{riGroups = x'}) <$> inj x++-- Roster Update+-------------------++_RosterUpdateRemove :: Prism RosterUpdate Jid+_RosterUpdateRemove = prism' RosterUpdateRemove fromRosterUpdateRemove+ where+ fromRosterUpdateRemove (RosterUpdateRemove jid) = Just jid+ fromRosterUpdateRemove RosterUpdateAdd{} = Nothing++_RosterUpdateAdd :: Prism RosterUpdate Item+_RosterUpdateAdd = prism' RosterUpdateAdd fromRosterUpdateAdd+ where+ fromRosterUpdateAdd RosterUpdateRemove{} = Nothing+ fromRosterUpdateAdd (RosterUpdateAdd item) = Just item+++-- QueryItem+-------------------+qiApprovedL :: Lens QueryItem (Maybe Bool)+qiApprovedL inj i@QueryItem{qiApproved = x} =+ (\x' -> i{qiApproved = x'}) <$> inj x++qiAskL :: Lens QueryItem Bool+qiAskL inj i@QueryItem{qiAsk = x} = (\x' -> i{qiAsk = x'}) <$> inj x++qiJidL :: Lens QueryItem Jid+qiJidL inj i@QueryItem{qiJid = x} = (\x' -> i{qiJid = x'}) <$> inj x++qiNameL :: Lens QueryItem (Maybe Text)+qiNameL inj i@QueryItem{qiName = x} = (\x' -> i{qiName = x'}) <$> inj x++qiSubscriptionL :: Lens QueryItem (Maybe Subscription)+qiSubscriptionL inj i@QueryItem{qiSubscription = x} =+ (\x' -> i{qiSubscription = x'}) <$> inj x++qiGroupsL :: Lens QueryItem [Text]+qiGroupsL inj i@QueryItem{qiGroups = x} = (\x' -> i{qiGroups = x'}) <$> inj x++queryVerL :: Lens Query (Maybe Text)+queryVerL inj i@Query{queryVer = x} = (\x' -> i{queryVer = x'}) <$> inj x++queryItemsL :: Lens Query [QueryItem]+queryItemsL inj i@Query{queryItems = x} = (\x' -> i{queryItems = x'}) <$> inj x+++-- IM+-------------------+++bodyLangL :: Lens MessageBody (Maybe LangTag)+bodyLangL inj m@MessageBody{bodyLang = bl} = (\bl' -> m{bodyLang = bl'}) <$> inj bl++bodyContentL :: Lens MessageBody Text+bodyContentL inj m@MessageBody{bodyContent = bc} =+ (\bc' -> m{bodyContent = bc'}) <$> inj bc++threadIdL :: Lens MessageThread Text+threadIdL inj m@MessageThread{threadID = bc} =+ (\bc' -> m{threadID = bc'}) <$> inj bc++threadParentL :: Lens MessageThread (Maybe Text)+threadParentL inj m@MessageThread{threadParent = bc} =+ (\bc' -> m{threadParent = bc'}) <$> inj bc++subjectLangL :: Lens MessageSubject (Maybe LangTag)+subjectLangL inj m@MessageSubject{subjectLang = bc} =+ (\bc' -> m{subjectLang = bc'}) <$> inj bc++subjectContentL :: Lens MessageSubject Text+subjectContentL inj m@MessageSubject{subjectContent = bc} =+ (\bc' -> m{subjectContent = bc'}) <$> inj bc++imThreadL :: Lens InstantMessage (Maybe MessageThread)+imThreadL inj m@InstantMessage{imThread = bc} =+ (\bc' -> m{imThread = bc'}) <$> inj bc++imSubjectL :: Lens InstantMessage [MessageSubject]+imSubjectL inj m@InstantMessage{imSubject = bc} =+ (\bc' -> m{imSubject = bc'}) <$> inj bc++imBodyL :: Lens InstantMessage [MessageBody]+imBodyL inj m@InstantMessage{imBody = bc} =+ (\bc' -> m{imBody = bc'}) <$> inj bc++-- IM Presence+------------------++showStatusL :: Lens IMPresence (Maybe ShowStatus)+showStatusL inj m@IMP{showStatus = bc} =+ (\bc' -> m{showStatus = bc'}) <$> inj bc++statusL :: Lens IMPresence (Maybe Text)+statusL inj m@IMP{status = bc} =+ (\bc' -> m{status = bc'}) <$> inj bc++priorityL :: Lens IMPresence (Maybe Int)+priorityL inj m@IMP{priority = bc} =+ (\bc' -> m{priority = bc'}) <$> inj bc++-- StreamFeatures+-------------------++featureTlsL :: Lens StreamFeatures (Maybe Bool)+featureTlsL = mkLens streamFeaturesTls (\x sf -> sf{streamFeaturesTls = x})++featureMechanismsL :: Lens StreamFeatures [Text]+featureMechanismsL =+ mkLens streamFeaturesMechanisms (\x sf -> sf{streamFeaturesMechanisms = x})++featureRosterVerL :: Lens StreamFeatures (Maybe Bool)+featureRosterVerL =+ mkLens streamFeaturesRosterVer (\x sf -> sf{streamFeaturesRosterVer = x})++featurePreApprovalL :: Lens StreamFeatures Bool+featurePreApprovalL =+ mkLens streamFeaturesPreApproval (\x sf -> sf{streamFeaturesPreApproval = x})++featuresOtherL :: Lens StreamFeatures [Element]+featuresOtherL =+ mkLens streamFeaturesOther (\x sf -> sf{streamFeaturesOther = x})
+ source/Network/Xmpp/Marshal.hs view
@@ -0,0 +1,536 @@+-- Picklers and unpicklers convert Haskell data to XML and XML to Haskell data,+-- respectively. By convention, pickler/unpickler ("PU") function names start+-- with "xp".++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE NoMonomorphismRestriction #-}++{-# OPTIONS_HADDOCK hide #-}++module Network.Xmpp.Marshal where++import Data.XML.Pickle+import Data.XML.Types++import qualified Control.Exception as Ex+import Data.Text (Text)+import qualified Data.Text as Text++import Network.Xmpp.Types++xpNonemptyText :: PU Text NonemptyText+xpNonemptyText = ("xpNonemptyText" , "") <?+> xpWrap Nonempty fromNonempty xpText++xpStreamElement :: PU [Node] (Either StreamErrorInfo XmppElement)+xpStreamElement = xpEither xpStreamError $ xpAlt elemSel+ [ xpWrap XmppStanza (\(XmppStanza x) -> x) xpStanza+ , xpWrap XmppNonza (\(XmppNonza x) -> x) xpElemVerbatim+ ]+ where+ -- Selector for which pickler to execute above.+ elemSel :: XmppElement -> Int+ elemSel (XmppStanza _) = 0+ elemSel (XmppNonza _) = 1++xpStreamStanza :: PU [Node] (Either StreamErrorInfo Stanza)+xpStreamStanza = xpEither xpStreamError xpStanza++xpExtendedAttrs :: PU [Attribute] [ExtendedAttribute]+xpExtendedAttrs = ("xpAttrVerbatim" , "") <?+>+ xpIso (map (\(name, cs) -> (name, flattenContents cs)))+ (map (\(name, c) -> (name, [ContentText c])))+ where+ flattenContents = Text.concat . filterContentText+ filterContentText = map (\c -> case c of+ ContentText t -> t+ ContentEntity{} -> Ex.throw UnresolvedEntityException )++xpStanza :: PU [Node] Stanza+xpStanza = ("xpStanza" , "") <?+> xpAlt stanzaSel+ [ xpWrap IQRequestS (\(IQRequestS x) -> x) xpIQRequest+ , xpWrap IQResultS (\(IQResultS x) -> x) xpIQResult+ , xpWrap IQErrorS (\(IQErrorS x) -> x) xpIQError+ , xpWrap MessageErrorS (\(MessageErrorS x) -> x) xpMessageError+ , xpWrap MessageS (\(MessageS x) -> x) xpMessage+ , xpWrap PresenceErrorS (\(PresenceErrorS x) -> x) xpPresenceError+ , xpWrap PresenceS (\(PresenceS x) -> x) xpPresence+ ]+ where+ -- Selector for which pickler to execute above.+ stanzaSel :: Stanza -> Int+ stanzaSel (IQRequestS _) = 0+ stanzaSel (IQResultS _) = 1+ stanzaSel (IQErrorS _) = 2+ stanzaSel (MessageErrorS _) = 3+ stanzaSel (MessageS _) = 4+ stanzaSel (PresenceErrorS _) = 5+ stanzaSel (PresenceS _) = 6++xpMessage :: PU [Node] (Message)+xpMessage = ("xpMessage" , "") <?+> xpWrap+ (\((tp, qid, from, to, lang, attrs), ext) -> Message qid from to lang tp ext attrs)+ (\(Message qid from to lang tp ext attrs) -> ((tp, qid, from, to, lang, attrs), ext))+ (xpElem "{jabber:client}message"+ (xp6Tuple+ (xpDefault Normal $ xpAttr "type" xpMessageType)+ (xpAttrImplied "id" xpId)+ (xpAttrImplied "from" xpJid)+ (xpAttrImplied "to" xpJid)+ xpLangTag+ xpExtendedAttrs+ -- TODO: NS?+ )+ (xpAll xpElemVerbatim)+ )++xpPresence :: PU [Node] Presence+xpPresence = ("xpPresence" , "") <?+> xpWrap+ (\((qid, from, to, lang, tp, attr), ext)+ -> Presence qid from to lang tp ext attr)+ (\(Presence qid from to lang tp ext attr)+ -> ((qid, from, to, lang, tp, attr), ext))+ (xpElem "{jabber:client}presence"+ (xp6Tuple+ (xpAttrImplied "id" xpId)+ (xpAttrImplied "from" xpJid)+ (xpAttrImplied "to" xpJid)+ xpLangTag+ (xpDefault Available $ xpAttr "type" xpPresenceType)+ xpExtendedAttrs+ )+ (xpAll xpElemVerbatim)+ )++xpIQRequest :: PU [Node] IQRequest+xpIQRequest = ("xpIQRequest" , "") <?+> xpWrap+ (\((qid, from, to, lang, tp, attr),body)+ -> IQRequest qid from to lang tp body attr)+ (\(IQRequest qid from to lang tp body attr)+ -> ((qid, from, to, lang, tp, attr), body))+ (xpElem "{jabber:client}iq"+ (xp6Tuple+ (xpAttr "id" xpId)+ (xpAttrImplied "from" xpJid)+ (xpAttrImplied "to" xpJid)+ xpLangTag+ ((xpAttr "type" xpIQRequestType))+ xpExtendedAttrs+ )+ xpElemVerbatim+ )++xpIQResult :: PU [Node] IQResult+xpIQResult = ("xpIQResult" , "") <?+> xpWrap+ (\((qid, from, to, lang, _tp, attr),body)+ -> IQResult qid from to lang body attr)+ (\(IQResult qid from to lang body attr)+ -> ((qid, from, to, lang, (), attr ), body))+ (xpElem "{jabber:client}iq"+ (xp6Tuple+ (xpAttr "id" xpId)+ (xpAttrImplied "from" xpJid)+ (xpAttrImplied "to" xpJid)+ xpLangTag+ ((xpAttrFixed "type" "result"))+ xpExtendedAttrs+ )+ (xpOption xpElemVerbatim)+ )++----------------------------------------------------------+-- Errors+----------------------------------------------------------++xpStanzaErrorCondition :: PU [Node] StanzaErrorCondition+xpStanzaErrorCondition = ("xpErrorCondition" , "") <?+> xpWrapEither+ (\(cond, (),cont) -> case (cond, cont) of+ (Gone _, x) -> Right $ Gone x+ (Redirect _, x) -> Right $ Redirect x+ (x , Nothing) -> Right x+ _ -> Left+ ("Only Gone and Redirect may have character data"+ :: String)+ )+ (\x -> case x of+ (Gone t) -> (Gone Nothing, (), t)+ (Redirect t) -> (Redirect Nothing, () , t)+ c -> (c, (), Nothing))+ (xpElemByNamespace+ "urn:ietf:params:xml:ns:xmpp-stanzas"+ xpStanzaErrorConditionShape+ xpUnit+ (xpOption $ xpContent xpNonemptyText)+ )+ where+ -- Create the "shape" of the error condition. In case of Gone and Redirect+ -- the optional field is left empty and must be filled in by the caller+ xpStanzaErrorConditionShape :: PU Text StanzaErrorCondition+ xpStanzaErrorConditionShape = ("xpStanzaErrorCondition", "") <?>+ xpIso stanzaErrorConditionFromText+ stanzaErrorConditionToText+ stanzaErrorConditionToText BadRequest = "bad-request"+ stanzaErrorConditionToText Conflict = "conflict"+ stanzaErrorConditionToText FeatureNotImplemented = "feature-not-implemented"+ stanzaErrorConditionToText Forbidden = "forbidden"+ stanzaErrorConditionToText (Gone _) = "gone"+ stanzaErrorConditionToText InternalServerError = "internal-server-error"+ stanzaErrorConditionToText ItemNotFound = "item-not-found"+ stanzaErrorConditionToText JidMalformed = "jid-malformed"+ stanzaErrorConditionToText NotAcceptable = "not-acceptable"+ stanzaErrorConditionToText NotAllowed = "not-allowed"+ stanzaErrorConditionToText NotAuthorized = "not-authorized"+ stanzaErrorConditionToText PolicyViolation = "policy-violation"+ stanzaErrorConditionToText RecipientUnavailable = "recipient-unavailable"+ stanzaErrorConditionToText (Redirect _) = "redirect"+ stanzaErrorConditionToText RegistrationRequired = "registration-required"+ stanzaErrorConditionToText RemoteServerNotFound = "remote-server-not-found"+ stanzaErrorConditionToText RemoteServerTimeout = "remote-server-timeout"+ stanzaErrorConditionToText ResourceConstraint = "resource-constraint"+ stanzaErrorConditionToText ServiceUnavailable = "service-unavailable"+ stanzaErrorConditionToText SubscriptionRequired = "subscription-required"+ stanzaErrorConditionToText UndefinedCondition = "undefined-condition"+ stanzaErrorConditionToText UnexpectedRequest = "unexpected-request"+ stanzaErrorConditionFromText "bad-request" = BadRequest+ stanzaErrorConditionFromText "conflict" = Conflict+ stanzaErrorConditionFromText "feature-not-implemented" = FeatureNotImplemented+ stanzaErrorConditionFromText "forbidden" = Forbidden+ stanzaErrorConditionFromText "gone" = Gone Nothing+ stanzaErrorConditionFromText "internal-server-error" = InternalServerError+ stanzaErrorConditionFromText "item-not-found" = ItemNotFound+ stanzaErrorConditionFromText "jid-malformed" = JidMalformed+ stanzaErrorConditionFromText "not-acceptable" = NotAcceptable+ stanzaErrorConditionFromText "not-allowed" = NotAllowed+ stanzaErrorConditionFromText "not-authorized" = NotAuthorized+ stanzaErrorConditionFromText "policy-violation" = PolicyViolation+ stanzaErrorConditionFromText "recipient-unavailable" = RecipientUnavailable+ stanzaErrorConditionFromText "redirect" = Redirect Nothing+ stanzaErrorConditionFromText "registration-required" = RegistrationRequired+ stanzaErrorConditionFromText "remote-server-not-found" = RemoteServerNotFound+ stanzaErrorConditionFromText "remote-server-timeout" = RemoteServerTimeout+ stanzaErrorConditionFromText "resource-constraint" = ResourceConstraint+ stanzaErrorConditionFromText "service-unavailable" = ServiceUnavailable+ stanzaErrorConditionFromText "subscription-required" = SubscriptionRequired+ stanzaErrorConditionFromText "undefined-condition" = UndefinedCondition+ stanzaErrorConditionFromText "unexpected-request" = UnexpectedRequest+ stanzaErrorConditionFromText _ = UndefinedCondition++++xpStanzaError :: PU [Node] StanzaError+xpStanzaError = ("xpStanzaError" , "") <?+> xpWrap+ (\((tp, _code), (cond, txt, ext)) -> StanzaError tp cond txt ext)+ (\(StanzaError tp cond txt ext) -> ((tp, Nothing), (cond, txt, ext)))+ (xpElem "{jabber:client}error"+ (xp2Tuple+ (xpAttr "type" xpStanzaErrorType)+ (xpAttribute' "code" xpId))+ (xp3Tuple+ xpStanzaErrorCondition+ (xpOption $ xpElem "{urn:ietf:params:xml:ns:xmpp-stanzas}text"+ (xpAttrImplied xmlLang xpLang)+ (xpContent xpNonemptyText)+ )+ (xpOption xpElemVerbatim)+ )+ )++xpMessageError :: PU [Node] (MessageError)+xpMessageError = ("xpMessageError" , "") <?+> xpWrap+ (\((_, qid, from, to, lang, attr), (err, ext)) ->+ MessageError qid from to lang err ext attr)+ (\(MessageError qid from to lang err ext attr) ->+ (((), qid, from, to, lang, attr), (err, ext)))+ (xpElem "{jabber:client}message"+ (xp6Tuple+ (xpAttrFixed "type" "error")+ (xpAttrImplied "id" xpId)+ (xpAttrImplied "from" xpJid)+ (xpAttrImplied "to" xpJid)+ (xpAttrImplied xmlLang xpLang)+ xpExtendedAttrs+ )+ (xp2Tuple xpStanzaError (xpAll xpElemVerbatim))+ )++xpPresenceError :: PU [Node] PresenceError+xpPresenceError = ("xpPresenceError" , "") <?+> xpWrap+ (\((qid, from, to, lang, _, attr),(err, ext)) ->+ PresenceError qid from to lang err ext attr)+ (\(PresenceError qid from to lang err ext attr) ->+ ((qid, from, to, lang, (), attr), (err, ext)))+ (xpElem "{jabber:client}presence"+ (xp6Tuple+ (xpAttrImplied "id" xpId)+ (xpAttrImplied "from" xpJid)+ (xpAttrImplied "to" xpJid)+ xpLangTag+ (xpAttrFixed "type" "error")+ xpExtendedAttrs+ )+ (xp2Tuple xpStanzaError (xpAll xpElemVerbatim))+ )++xpIQError :: PU [Node] IQError+xpIQError = ("xpIQError" , "") <?+> xpWrap+ (\((qid, from, to, lang, _tp, attr),(err, body)) ->+ IQError qid from to lang err body attr)+ (\(IQError qid from to lang err body attr) ->+ ((qid, from, to, lang, (), attr), (err, body)))+ (xpElem "{jabber:client}iq"+ (xp6Tuple+ (xpAttr "id" xpId)+ (xpAttrImplied "from" xpJid)+ (xpAttrImplied "to" xpJid)+ xpLangTag+ ((xpAttrFixed "type" "error"))+ xpExtendedAttrs+ )+ (xp2Tuple xpStanzaError (xpOption xpElemVerbatim))+ )++xpStreamError :: PU [Node] StreamErrorInfo+xpStreamError = ("xpStreamError" , "") <?+> xpWrap+ (\((cond,() ,()), txt, el) -> StreamErrorInfo cond txt el)+ (\(StreamErrorInfo cond txt el) ->((cond,() ,()), txt, el))+ (xpElemNodes+ (Name+ "error"+ (Just "http://etherx.jabber.org/streams")+ (Just "stream")+ )+ (xp3Tuple+ (xpElemByNamespace+ "urn:ietf:params:xml:ns:xmpp-streams"+ xpStreamErrorCondition+ xpUnit+ xpUnit+ )+ (xpOption $ xpElem+ "{urn:ietf:params:xml:ns:xmpp-streams}text"+ xpLangTag+ (xpContent xpNonemptyText)+ )+ (xpOption xpElemVerbatim) -- Application specific error conditions+ )+ )++xpLangTag :: PU [Attribute] (Maybe LangTag)+xpLangTag = xpAttrImplied xmlLang xpLang++xpLang :: PU Text LangTag+xpLang = ("xpLang", "") <?>+ xpPartial ( \input -> case langTagFromText input of+ Nothing -> Left "Could not parse language tag."+ Just j -> Right j)+ langTagToText++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 = xpRoot . xpUnliftElems++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" xpJid)+ (xpAttrImplied "to" xpJid)+ (xpAttrImplied "id" xpId)+ xpLangTag+ )++-- Pickler/Unpickler for the stream features - TLS, SASL, and the rest.+xpStreamFeatures :: PU [Node] StreamFeatures+xpStreamFeatures = ("xpStreamFeatures","") <?> xpWrap+ (\(tls, sasl, ver, preAppr, session, rest)+ -> StreamFeatures tls (mbl sasl) ver preAppr session rest )+ (\(StreamFeatures tls sasl ver preAppr session rest)+ -> (tls, lmb sasl, ver, preAppr, session, rest))+ (xpElemNodes+ (Name+ "features"+ (Just "http://etherx.jabber.org/streams")+ (Just "stream")+ )+ (xp6Tuple+ (xpOption pickleTlsFeature)+ (xpOption pickleSaslFeature)+ (xpOption pickleRosterVer)+ picklePreApproval+ (xpOption pickleSessionFeature)+ (xpAll xpElemVerbatim)+ )+ )+ where+ pickleTlsFeature :: PU [Node] Bool+ pickleTlsFeature = ("pickleTlsFeature", "") <?>+ xpElemNodes "{urn:ietf:params:xml:ns:xmpp-tls}starttls"+ (xpElemExists "{urn:ietf:params:xml:ns:xmpp-tls}required")+ pickleSaslFeature :: PU [Node] [Text]+ pickleSaslFeature = ("pickleSaslFeature", "") <?>+ xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms"+ (xpAll $ xpElemNodes+ "{urn:ietf:params:xml:ns:xmpp-sasl}mechanism" (xpContent xpId))+ pickleRosterVer = xpElemNodes "{urn:xmpp:features:rosterver}ver" $+ xpElemExists "{urn:xmpp:features:rosterver}optional"+ picklePreApproval = xpElemExists "{urn:xmpp:features:pre-approval}sub"+ pickleSessionFeature :: PU [Node] Bool+ pickleSessionFeature = ("pickleSessionFeature", "") <?>+ xpElemNodes "{urn:ietf:params:xml:ns:xmpp-session}session"+ (xpElemExists "{urn:ietf:params:xml:ns:xmpp-session}optional")+++xpJid :: PU Text Jid+xpJid = ("xpJid", "") <?>+ xpPartial ( \input -> case jidFromText input of+ Nothing -> Left "Could not parse JID."+ Just j -> Right j)+ jidToText++xpIQRequestType :: PU Text IQRequestType+xpIQRequestType = ("xpIQRequestType", "") <?>+ xpPartial ( \input -> case iqRequestTypeFromText input of+ Nothing -> Left "Could not parse IQ request type."+ Just j -> Right j)+ iqRequestTypeToText+ where+ iqRequestTypeFromText "get" = Just Get+ iqRequestTypeFromText "set" = Just Set+ iqRequestTypeFromText _ = Nothing+ iqRequestTypeToText Get = "get"+ iqRequestTypeToText Set = "set"++xpMessageType :: PU Text MessageType+xpMessageType = ("xpMessageType", "") <?>+ xpIso messageTypeFromText+ messageTypeToText+ where+ messageTypeFromText "chat" = Chat+ messageTypeFromText "groupchat" = GroupChat+ messageTypeFromText "headline" = Headline+ messageTypeFromText "normal" = Normal+ messageTypeFromText _ = Normal+ messageTypeToText Chat = "chat"+ messageTypeToText GroupChat = "groupchat"+ messageTypeToText Headline = "headline"+ messageTypeToText Normal = "normal"++xpPresenceType :: PU Text PresenceType+xpPresenceType = ("xpPresenceType", "") <?>+ xpPartial ( \input -> case presenceTypeFromText input of+ Nothing -> Left "Could not parse presence type."+ Just j -> Right j)+ presenceTypeToText+ where+ presenceTypeFromText "" = Just Available+ presenceTypeFromText "available" = Just Available+ presenceTypeFromText "unavailable" = Just Unavailable+ presenceTypeFromText "subscribe" = Just Subscribe+ presenceTypeFromText "subscribed" = Just Subscribed+ presenceTypeFromText "unsubscribe" = Just Unsubscribe+ presenceTypeFromText "unsubscribed" = Just Unsubscribed+ presenceTypeFromText "probe" = Just Probe+ presenceTypeFromText _ = Nothing+ presenceTypeToText Available = "available"+ presenceTypeToText Unavailable = "unavailable"+ presenceTypeToText Subscribe = "subscribe"+ presenceTypeToText Subscribed = "subscribed"+ presenceTypeToText Unsubscribe = "unsubscribe"+ presenceTypeToText Unsubscribed = "unsubscribed"+ presenceTypeToText Probe = "probe"++xpStanzaErrorType :: PU Text StanzaErrorType+xpStanzaErrorType = ("xpStanzaErrorType", "") <?>+ xpPartial ( \input -> case stanzaErrorTypeFromText input of+ Nothing -> Left "Could not parse stanza error type."+ Just j -> Right j)+ stanzaErrorTypeToText+ where+ stanzaErrorTypeFromText "auth" = Just Auth+ stanzaErrorTypeFromText "cancel" = Just Cancel+ stanzaErrorTypeFromText "continue" = Just Continue+ stanzaErrorTypeFromText "modify" = Just Modify+ stanzaErrorTypeFromText "wait" = Just Wait+ stanzaErrorTypeFromText _ = Nothing+ stanzaErrorTypeToText Auth = "auth"+ stanzaErrorTypeToText Cancel = "cancel"+ stanzaErrorTypeToText Continue = "continue"+ stanzaErrorTypeToText Modify = "modify"+ stanzaErrorTypeToText Wait = "wait"+++xpStreamErrorCondition :: PU Text StreamErrorCondition+xpStreamErrorCondition = ("xpStreamErrorCondition", "") <?>+ xpIso streamErrorConditionFromText+ streamErrorConditionToText+ where+ streamErrorConditionToText StreamBadFormat = "bad-format"+ streamErrorConditionToText StreamBadNamespacePrefix = "bad-namespace-prefix"+ streamErrorConditionToText StreamConflict = "conflict"+ streamErrorConditionToText StreamConnectionTimeout = "connection-timeout"+ streamErrorConditionToText StreamHostGone = "host-gone"+ streamErrorConditionToText StreamHostUnknown = "host-unknown"+ streamErrorConditionToText StreamImproperAddressing = "improper-addressing"+ streamErrorConditionToText StreamInternalServerError = "internal-server-error"+ streamErrorConditionToText StreamInvalidFrom = "invalid-from"+ streamErrorConditionToText StreamInvalidNamespace = "invalid-namespace"+ streamErrorConditionToText StreamInvalidXml = "invalid-xml"+ streamErrorConditionToText StreamNotAuthorized = "not-authorized"+ streamErrorConditionToText StreamNotWellFormed = "not-well-formed"+ streamErrorConditionToText StreamPolicyViolation = "policy-violation"+ streamErrorConditionToText StreamRemoteConnectionFailed = "remote-connection-failed"+ streamErrorConditionToText StreamReset = "reset"+ streamErrorConditionToText StreamResourceConstraint = "resource-constraint"+ streamErrorConditionToText StreamRestrictedXml = "restricted-xml"+ streamErrorConditionToText StreamSeeOtherHost = "see-other-host"+ streamErrorConditionToText StreamSystemShutdown = "system-shutdown"+ streamErrorConditionToText StreamUndefinedCondition = "undefined-condition"+ streamErrorConditionToText StreamUnsupportedEncoding = "unsupported-encoding"+ streamErrorConditionToText StreamUnsupportedFeature = "unsupported-feature"+ streamErrorConditionToText StreamUnsupportedStanzaType = "unsupported-stanza-type"+ streamErrorConditionToText StreamUnsupportedVersion = "unsupported-version"+ streamErrorConditionFromText "bad-format" = StreamBadFormat+ streamErrorConditionFromText "bad-namespace-prefix" = StreamBadNamespacePrefix+ streamErrorConditionFromText "conflict" = StreamConflict+ streamErrorConditionFromText "connection-timeout" = StreamConnectionTimeout+ streamErrorConditionFromText "host-gone" = StreamHostGone+ streamErrorConditionFromText "host-unknown" = StreamHostUnknown+ streamErrorConditionFromText "improper-addressing" = StreamImproperAddressing+ streamErrorConditionFromText "internal-server-error" = StreamInternalServerError+ streamErrorConditionFromText "invalid-from" = StreamInvalidFrom+ streamErrorConditionFromText "invalid-namespace" = StreamInvalidNamespace+ streamErrorConditionFromText "invalid-xml" = StreamInvalidXml+ streamErrorConditionFromText "not-authorized" = StreamNotAuthorized+ streamErrorConditionFromText "not-well-formed" = StreamNotWellFormed+ streamErrorConditionFromText "policy-violation" = StreamPolicyViolation+ streamErrorConditionFromText "remote-connection-failed" = StreamRemoteConnectionFailed+ streamErrorConditionFromText "reset" = StreamReset+ streamErrorConditionFromText "resource-constraint" = StreamResourceConstraint+ streamErrorConditionFromText "restricted-xml" = StreamRestrictedXml+ streamErrorConditionFromText "see-other-host" = StreamSeeOtherHost+ streamErrorConditionFromText "system-shutdown" = StreamSystemShutdown+ streamErrorConditionFromText "undefined-condition" = StreamUndefinedCondition+ streamErrorConditionFromText "unsupported-encoding" = StreamUnsupportedEncoding+ streamErrorConditionFromText "unsupported-feature" = StreamUnsupportedFeature+ streamErrorConditionFromText "unsupported-stanza-type" = StreamUnsupportedStanzaType+ streamErrorConditionFromText "unsupported-version" = StreamUnsupportedVersion+ streamErrorConditionFromText _ = StreamUndefinedCondition -- §4.9.2
+ source/Network/Xmpp/Sasl.hs view
@@ -0,0 +1,155 @@+{-# 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.Monad.Except+import Control.Monad.State.Strict+import Data.Text (Text)+import Data.XML.Pickle+import Data.XML.Types+import Network.Xmpp.Marshal+import Network.Xmpp.Sasl.Mechanisms+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Stream+import Network.Xmpp.Types+import System.Log.Logger (debugM, errorM, infoM)++-- | 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+ -> Stream+ -> IO (Either XmppFailure (Maybe AuthFailure))+xmppSasl handlers stream = do+ debugM "Pontarius.Xmpp" "xmppSasl: Attempts to authenticate..."+ flip withStream stream $ do+ -- Chooses the first mechanism that is acceptable by both the client and the+ -- server.+ mechanisms <- gets $ streamFeaturesMechanisms . streamFeatures+ case (filter (\(name, _) -> name `elem` mechanisms)) handlers of+ [] -> return $ Right $ Just $ AuthNoAcceptableMechanism mechanisms+ (_name, handler):_ -> do+ cs <- gets streamConnectionState+ case cs of+ Closed -> do+ lift $ errorM "Pontarius.Xmpp" "xmppSasl: Stream state closed."+ return . Left $ XmppNoStream+ _ -> runExceptT $ do+ -- TODO: Log details about handler? SaslHandler "show" instance?+ lift $ lift $ debugM "Pontarius.Xmpp" "xmppSasl: Performing handler..."+ r <- ExceptT handler+ case r of+ Just ae -> do+ lift $ lift $ errorM "Pontarius.Xmpp" $+ "xmppSasl: AuthFailure encountered: " +++ show ae+ return $ Just ae+ Nothing -> do+ lift $ lift $ debugM "Pontarius.Xmpp" "xmppSasl: Authentication successful, restarting stream."+ _ <- ExceptT restartStream+ lift $ lift $ debugM "Pontarius.Xmpp" "xmppSasl: Stream restarted."+ return Nothing++-- | Authenticate to the server using the first matching method and bind a+-- resource.+auth :: [SaslHandler]+ -> Maybe Text+ -> Stream+ -> IO (Either XmppFailure (Maybe AuthFailure))+auth mechanisms resource con = runExceptT $ do+ mbAuthFail <- ExceptT $ xmppSasl mechanisms con+ case mbAuthFail of+ Nothing -> do+ _jid <- ExceptT $ xmppBind resource con+ ExceptT $ flip withStream' con $ do+ s <- get++ case sendStreamElement s of+ False -> return $ Right Nothing+ True -> do+ _ <- liftIO $ startSession con+ return $ Right Nothing+ f -> return f+ where+ sendStreamElement s =+ and [ -- Check that the stream feature is set and not optional+ streamFeaturesSession (streamFeatures s) == Just False+ ]+++-- 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 "{urn:ietf:params:xml:ns:xmpp-bind}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 -> Stream -> IO (Either XmppFailure Jid)+xmppBind rsrc c = runExceptT $ do+ lift $ debugM "Pontarius.Xmpp" "Attempts to bind..."+ answer <- ExceptT $ pushIQ "bind" Nothing Set Nothing (bindBody rsrc) c+ case answer of+ Right IQResult{iqResultPayload = Just b} -> do+ lift $ debugM "Pontarius.Xmpp" "xmppBind: IQ result received; unpickling JID..."+ let j = unpickleElem xpJid' b+ case j of+ Right jid' -> do+ lift $ infoM "Pontarius.Xmpp" $ "Bound JID: " ++ show jid'+ _ <- lift $ withStream ( do modify $ \s ->+ s{streamJid = Just jid'})+ c+ return jid'+ _ -> do+ lift $ errorM "Pontarius.Xmpp"+ $ "xmppBind: JID could not be unpickled from: "+ ++ show b+ throwError $ XmppOtherFailure+ _ -> do+ lift $ errorM "Pontarius.XMPP" "xmppBind: IQ error received."+ throwError XmppOtherFailure+ where+ -- Extracts the character data in the `jid' element.+ xpJid' :: PU [Node] Jid+ xpJid' = xpBind $ xpElemNodes jidName (xpContent xpJid)+ 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")+ ()++-- 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 :: Stream -> IO Bool+startSession con = do+ debugM "Pontarius.XMPP" "startSession: Pushing `session' IQ set stanza..."+ answer <- pushIQ "session" Nothing Set Nothing sessionXml con+ case answer of+ Left e -> do+ errorM "Pontarius.XMPP" $ "startSession: Error stanza received (" ++ (show e) ++ ")"+ return False+ Right _ -> do+ debugM "Pontarius.XMPP" "startSession: Result stanza received."+ return True
+ source/Network/Xmpp/Sasl/Common.hs view
@@ -0,0 +1,238 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts #-}++module Network.Xmpp.Sasl.Common where++import Control.Applicative ((<$>))+import Control.Monad+import Control.Monad.Except+import qualified Data.Attoparsec.ByteString.Char8 as AP+import Data.Bits+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as B64+import Data.Maybe (maybeToList)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import Data.Word (Word8)+import Data.XML.Pickle+import Data.XML.Types+import Network.Xmpp.Marshal+import Network.Xmpp.Sasl.StringPrep+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Stream+import Network.Xmpp.Types++import qualified System.Random as Random++import Control.Monad.State.Strict++--makeNonce :: ExceptT AuthFailure (StateT StreamState IO) BS.ByteString+makeNonce :: IO BS.ByteString+makeNonce = do+ g <- liftIO Random.newStdGen+ return $ B64.encode . BS.pack . map toWord8 . take 15 $ Random.randoms g+ where+ toWord8 :: Int -> Word8+ toWord8 x = fromIntegral x :: Word8++-- The <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> element, with an+-- optional round-trip value.+saslInitE :: Text.Text -> Maybe Text.Text -> Element+saslInitE mechanism rt =+ Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth"+ [("mechanism", [ContentText mechanism])]+ (maybeToList $ NodeContent . ContentText <$> rt)++-- SASL response with text payload.+saslResponseE :: Maybe Text.Text -> Element+saslResponseE resp =+ Element "{urn:ietf:params:xml:ns:xmpp-sasl}response"+ []+ (maybeToList $ NodeContent . ContentText <$> resp)++-- The <success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> element.+xpSuccess :: PU [Node] (Maybe Text.Text)+xpSuccess = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}success"+ (xpOption $ xpContent xpId)++-- Parses the incoming SASL data to a mapped list of pairs.+pairs :: BS.ByteString -> Either String Pairs+pairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do+ AP.skipSpace+ name <- AP.takeWhile1 (/= '=')+ _ <- AP.char '='+ qt <- ((AP.char '"' >> return True) `mplus` return False)+ content <- AP.takeWhile1 (AP.notInClass [',', '"'])+ when qt . void $ AP.char '"'+ return (name, content)++-- Failure element pickler.+xpFailure :: PU [Node] SaslFailure+xpFailure = xpWrap+ (\(txt, (failure, _, _)) -> SaslFailure failure txt)+ (\(SaslFailure failure txt) -> (txt,(failure,(),())))+ (xpElemNodes+ "{urn:ietf:params:xml:ns:xmpp-sasl}failure"+ (xp2Tuple+ (xpOption $ xpElem+ "{urn:ietf:params:xml:ns:xmpp-sasl}text"+ xpLangTag+ (xpContent xpId))+ (xpElemByNamespace+ "urn:ietf:params:xml:ns:xmpp-sasl"+ xpSaslError+ (xpUnit)+ (xpUnit))))++xpSaslError :: PU Text.Text SaslError+xpSaslError = ("xpSaslError", "") <?>+ xpIso saslErrorFromText saslErrorToText+ where+ saslErrorToText SaslAborted = "aborted"+ saslErrorToText SaslAccountDisabled = "account-disabled"+ saslErrorToText SaslCredentialsExpired = "credentials-expired"+ saslErrorToText SaslEncryptionRequired = "encryption-required"+ saslErrorToText SaslIncorrectEncoding = "incorrect-encoding"+ saslErrorToText SaslInvalidAuthzid = "invalid-authzid"+ saslErrorToText SaslInvalidMechanism = "invalid-mechanism"+ saslErrorToText SaslMalformedRequest = "malformed-request"+ saslErrorToText SaslMechanismTooWeak = "mechanism-too-weak"+ saslErrorToText SaslNotAuthorized = "not-authorized"+ saslErrorToText SaslTemporaryAuthFailure = "temporary-auth-failure"+ saslErrorFromText "aborted" = SaslAborted+ saslErrorFromText "account-disabled" = SaslAccountDisabled+ saslErrorFromText "credentials-expired" = SaslCredentialsExpired+ saslErrorFromText "encryption-required" = SaslEncryptionRequired+ saslErrorFromText "incorrect-encoding" = SaslIncorrectEncoding+ saslErrorFromText "invalid-authzid" = SaslInvalidAuthzid+ saslErrorFromText "invalid-mechanism" = SaslInvalidMechanism+ saslErrorFromText "malformed-request" = SaslMalformedRequest+ saslErrorFromText "mechanism-too-weak" = SaslMechanismTooWeak+ saslErrorFromText "not-authorized" = SaslNotAuthorized+ saslErrorFromText "temporary-auth-failure" = SaslTemporaryAuthFailure+ saslErrorFromText _ = SaslNotAuthorized++-- Challenge element pickler.+xpChallenge :: PU [Node] (Maybe Text.Text)+xpChallenge = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge"+ (xpOption $ xpContent xpId)++-- | Pickler for SaslElement.+xpSaslElement :: PU [Node] SaslElement+xpSaslElement = xpAlt saslSel+ [ xpWrap SaslSuccess (\(SaslSuccess x) -> x) xpSuccess+ , xpWrap SaslChallenge (\(SaslChallenge c) -> c) xpChallenge+ ]+ where+ saslSel (SaslSuccess _) = 0+ saslSel (SaslChallenge _) = 1++-- | Add quotationmarks around a byte string.+quote :: BS.ByteString -> BS.ByteString+quote x = BS.concat ["\"",x,"\""]++saslInit :: Text.Text -> Maybe BS.ByteString -> ExceptT AuthFailure (StateT StreamState IO) ()+saslInit mechanism payload = do+ r <- lift . pushElement . saslInitE mechanism $+ Text.decodeUtf8 . encodeEmpty . B64.encode <$> payload+ case r of+ Right () -> return ()+ Left e -> throwError $ AuthStreamFailure e+ where+ -- §6.4.2+ encodeEmpty "" = "="+ encodeEmpty x = x++-- | Pull the next element.+pullSaslElement :: ExceptT AuthFailure (StateT StreamState IO) SaslElement+pullSaslElement = do+ mbse <- lift $ pullUnpickle (xpEither xpFailure xpSaslElement)+ case mbse of+ Left e -> throwError $ AuthStreamFailure e+ Right (Left e) -> throwError $ AuthSaslFailure e+ Right (Right r) -> return r++-- | Pull the next element, checking that it is a challenge.+pullChallenge :: ExceptT AuthFailure (StateT StreamState IO) (Maybe BS.ByteString)+pullChallenge = do+ e <- pullSaslElement+ case e of+ SaslChallenge Nothing -> return Nothing+ SaslChallenge (Just scb64)+ | Right sc <- B64.decode . Text.encodeUtf8 $ scb64+ -> return $ Just sc+ _ -> throwError AuthOtherFailure -- TODO: Log++-- | Extract value from Just, failing with AuthOtherFailure on Nothing.+saslFromJust :: Maybe a -> ExceptT AuthFailure (StateT StreamState IO) a+saslFromJust Nothing = throwError $ AuthOtherFailure -- TODO: Log+saslFromJust (Just d) = return d++-- | Pull the next element and check that it is success.+pullSuccess :: ExceptT AuthFailure (StateT StreamState IO) (Maybe Text.Text)+pullSuccess = do+ e <- pullSaslElement+ case e of+ SaslSuccess x -> return x+ _ -> throwError $ AuthOtherFailure -- TODO: Log++-- | 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 :: ExceptT AuthFailure (StateT StreamState IO) (Maybe BS.ByteString)+pullFinalMessage = do+ challenge2 <- pullSaslElement+ case challenge2 of+ SaslSuccess x -> decode x+ SaslChallenge x -> do+ _b <- respond Nothing+ _s <- pullSuccess+ decode x+ where+ decode Nothing = return Nothing+ decode (Just d) = case B64.decode $ Text.encodeUtf8 d of+ Left _e -> throwError $ AuthOtherFailure -- TODO: Log+ Right x -> return $ Just x++-- | Extract p=q pairs from a challenge.+toPairs :: BS.ByteString -> ExceptT AuthFailure (StateT StreamState IO) Pairs+toPairs ctext = case pairs ctext of+ Left _e -> throwError AuthOtherFailure -- TODO: Log+ Right r -> return r++-- | Send a SASL response element. The content will be base64-encoded.+respond :: Maybe BS.ByteString -> ExceptT AuthFailure (StateT StreamState IO) ()+respond m = do+ r <- lift . pushElement . saslResponseE . fmap (Text.decodeUtf8 . B64.encode) $ m+ case r of+ Left e -> throwError $ AuthStreamFailure e+ Right () -> return ()++-- | Run the appropriate stringprep profiles on the credentials.+-- May fail with 'AuthStringPrepFailure'+prepCredentials :: Text.Text -> Maybe Text.Text -> Text.Text+ -> ExceptT AuthFailure (StateT StreamState IO) (Text.Text, Maybe Text.Text, Text.Text)+prepCredentials authcid authzid password = case credentials of+ Nothing -> throwError $ AuthIllegalCredentials+ Just creds -> return creds+ where+ credentials = do+ ac <- normalizeUsername authcid+ az <- case authzid of+ Nothing -> Just Nothing+ Just az' -> Just <$> normalizeUsername az'+ pw <- normalizePassword password+ return (ac, az, pw)++-- | Bit-wise xor of byte strings+xorBS :: BS.ByteString -> BS.ByteString -> BS.ByteString+xorBS x y = BS.pack $ BS.zipWith xor x y++-- | Join byte strings with ","+merge :: [BS.ByteString] -> BS.ByteString+merge = BS.intercalate ","++-- | Infix concatenation of byte strings+(+++) :: BS.ByteString -> BS.ByteString -> BS.ByteString+(+++) = BS.append
+ source/Network/Xmpp/Sasl/Mechanisms.hs view
@@ -0,0 +1,10 @@+{-# OPTIONS_HADDOCK hide #-}+module Network.Xmpp.Sasl.Mechanisms+ ( module Network.Xmpp.Sasl.Mechanisms.DigestMd5+ , scramSha1+ , module Network.Xmpp.Sasl.Mechanisms.Plain+ ) where++import Network.Xmpp.Sasl.Mechanisms.DigestMd5+import Network.Xmpp.Sasl.Mechanisms.Scram+import Network.Xmpp.Sasl.Mechanisms.Plain
+ source/Network/Xmpp/Sasl/Mechanisms/DigestMd5.hs view
@@ -0,0 +1,122 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Xmpp.Sasl.Mechanisms.DigestMd5+ ( digestMd5+ ) where++import Control.Monad.Except+import Control.Monad.State.Strict+import qualified Crypto.Classes as CC+import qualified Data.Binary as Binary+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Char8 as BS8+import qualified Data.ByteString.Lazy as BL+import qualified Data.Digest.Pure.MD5 as MD5+import qualified Data.List as L+import Data.Text (Text)+import qualified Data.Text.Encoding as Text+import Network.Xmpp.Sasl.Common+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Types++++xmppDigestMd5 :: Text -- ^ Authentication identity (authzid or username)+ -> Maybe Text -- ^ Authorization identity (authcid)+ -> Text -- ^ Password (authzid)+ -> ExceptT AuthFailure (StateT StreamState IO) ()+xmppDigestMd5 authcid' authzid' password' = do+ (ac, az, pw) <- prepCredentials authcid' authzid' password'+ Just address <- gets streamAddress+ xmppDigestMd5' address ac az pw+ where+ xmppDigestMd5' :: Text -> Text -> Maybe Text -> Text -> ExceptT AuthFailure (StateT StreamState IO) ()+ xmppDigestMd5' hostname authcid _authzid password = do -- TODO: use authzid?+ -- Push element and receive the challenge.+ _ <- saslInit "DIGEST-MD5" Nothing -- TODO: Check boolean?+ prs <- toPairs =<< saslFromJust =<< pullChallenge+ cnonce <- liftIO $ makeNonce+ _b <- respond . Just $ createResponse hostname prs cnonce+ _challenge2 <- pullFinalMessage+ return ()+ where+ -- Produce the response to the challenge.+ createResponse :: Text+ -> Pairs+ -> BS.ByteString -- nonce+ -> BS.ByteString+ createResponse hname prs cnonce = let+ Just qop = L.lookup "qop" prs -- TODO: proper handling+ Just nonce = L.lookup "nonce" prs+ uname_ = Text.encodeUtf8 authcid+ passwd_ = Text.encodeUtf8 password+ -- Using Int instead of Word8 for random 1.0.0.0 (GHC 7)+ -- compatibility.++ nc = "00000001"+ digestURI = "xmpp/" `BS.append` Text.encodeUtf8 hname+ digest = md5Digest+ uname_+ (lookup "realm" prs)+ passwd_+ digestURI+ nc+ qop+ nonce+ cnonce+ response = BS.intercalate "," . map (BS.intercalate "=") $+ [["username", quote uname_]] +++ case L.lookup "realm" prs of+ Just realm -> [["realm" , quote realm ]]+ Nothing -> [] +++ [ ["nonce" , quote nonce ]+ , ["cnonce" , quote cnonce ]+ , ["nc" , nc ]+ , ["qop" , qop ]+ , ["digest-uri", quote digestURI]+ , ["response" , digest ]+ , ["charset" , "utf-8" ]+ ]+ in B64.encode response+ hash :: [BS8.ByteString] -> BS8.ByteString+ hash = BS8.pack . show+ . (CC.hash' :: BS.ByteString -> MD5.MD5Digest)+ . BS.intercalate (":")+ hashRaw :: [BS8.ByteString] -> BS8.ByteString+ hashRaw = toStrict . Binary.encode .+ (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":")+ toStrict :: BL.ByteString -> BS8.ByteString+ toStrict = BS.concat . BL.toChunks+ -- TODO: this only handles MD5-sess+ md5Digest :: BS8.ByteString+ -> Maybe BS8.ByteString+ -> BS8.ByteString+ -> BS8.ByteString+ -> BS8.ByteString+ -> BS8.ByteString+ -> BS8.ByteString+ -> BS8.ByteString+ -> BS8.ByteString+ md5Digest uname realm pwd digestURI nc qop nonce cnonce =+ let ha1 = hash [ hashRaw [uname, maybe "" id realm, pwd]+ , nonce+ , cnonce+ ]+ ha2 = hash ["AUTHENTICATE", digestURI]+ in hash [ha1, nonce, nc, cnonce, qop, ha2]++digestMd5 :: Username -- ^ Authentication identity (authcid or username)+ -> Maybe AuthZID -- ^ Authorization identity (authzid)+ -> Password -- ^ Password+ -> SaslHandler+digestMd5 authcid authzid password =+ ( "DIGEST-MD5"+ , do+ r <- runExceptT $ xmppDigestMd5 authcid authzid password+ case r of+ Left (AuthStreamFailure e) -> return $ Left e+ Left e -> return $ Right $ Just e+ Right () -> return $ Right Nothing+ )
+ source/Network/Xmpp/Sasl/Mechanisms/Plain.hs view
@@ -0,0 +1,59 @@+{-# OPTIONS_HADDOCK hide #-}+-- Implementation of the PLAIN Simple Authentication and Security Layer (SASL)+-- Mechanism, http://tools.ietf.org/html/rfc4616.++{-# LANGUAGE OverloadedStrings #-}++module Network.Xmpp.Sasl.Mechanisms.Plain+ ( plain+ ) where++import Control.Monad.Except+import Control.Monad.State.Strict+import qualified Data.ByteString as BS+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import Network.Xmpp.Sasl.Common+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Types++-- TODO: stringprep+xmppPlain :: Text.Text -- ^ Password+ -> Maybe Text.Text -- ^ Authorization identity (authzid)+ -> Text.Text -- ^ Authentication identity (authcid)+ -> ExceptT AuthFailure (StateT StreamState IO) ()+xmppPlain authcid' authzid' password = do+ (ac, az, pw) <- prepCredentials authcid' authzid' password+ _ <- saslInit "PLAIN" ( Just $ plainMessage ac az pw)+ _ <- pullSuccess+ return ()+ where+ -- Converts an optional authorization identity, an authentication identity,+ -- and a password to a \NUL-separated PLAIN message.+ plainMessage :: Text.Text -- Authorization identity (authzid)+ -> Maybe Text.Text -- Authentication identity (authcid)+ -> Text.Text -- Password+ -> BS.ByteString -- The PLAIN message+ plainMessage authcid _authzid passwd = BS.concat $+ [ authzid''+ , "\NUL"+ , Text.encodeUtf8 $ authcid+ , "\NUL"+ , Text.encodeUtf8 $ passwd+ ]+ where+ authzid'' = maybe "" Text.encodeUtf8 authzid'++plain :: Username -- ^ authentication ID (username)+ -> Maybe AuthZID -- ^ authorization ID+ -> Password -- ^ password+ -> SaslHandler+plain authcid authzid passwd =+ ( "PLAIN"+ , do+ r <- runExceptT $ xmppPlain authcid authzid passwd+ case r of+ Left (AuthStreamFailure e) -> return $ Left e+ Left e -> return $ Right $ Just e+ Right () -> return $ Right Nothing+ )
+ source/Network/Xmpp/Sasl/Mechanisms/Scram.hs view
@@ -0,0 +1,163 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Xmpp.Sasl.Mechanisms.Scram+ where++import Control.Applicative ((<$>))+import Control.Monad+import Control.Monad.Except+import Control.Monad.State.Strict+import qualified Crypto.Classes as Crypto+import qualified Crypto.HMAC as Crypto+import qualified Crypto.Hash.CryptoAPI as Crypto+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as B64+import Data.ByteString.Char8 as BS8 (unpack)+import Data.List (foldl1', genericTake)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import Network.Xmpp.Sasl.Common+import Network.Xmpp.Sasl.Types+import Network.Xmpp.Types++-- | A nicer name for undefined, for use as a dummy token to determin+-- the hash function to use+hashToken :: (Crypto.Hash ctx hash) => hash+hashToken = undefined++-- | Salted Challenge Response Authentication Mechanism (SCRAM) SASL+-- mechanism according to RFC 5802.+--+-- This implementation is independent and polymorphic in the used hash function.+scram :: (Crypto.Hash ctx hash)+ => hash -- ^ Dummy argument to determine the hash to use; you+ -- can safely pass undefined or a 'hashToken' to it+ -> Text.Text -- ^ Authentication ID (user name)+ -> Maybe Text.Text -- ^ Authorization ID+ -> Text.Text -- ^ Password+ -> ExceptT AuthFailure (StateT StreamState IO) ()+scram hToken authcid authzid password = do+ (ac, az, pw) <- prepCredentials authcid authzid password+ scramhelper ac az pw+ where+ scramhelper authcid' authzid' pwd = do+ cnonce <- liftIO makeNonce+ _ <- saslInit "SCRAM-SHA-1" (Just $ cFirstMessage cnonce)+ sFirstMessage <- saslFromJust =<< pullChallenge+ prs <- toPairs sFirstMessage+ (nonce, salt, ic) <- fromPairs prs cnonce+ let (cfm, v) = cFinalMessageAndVerifier nonce salt ic sFirstMessage cnonce+ _ <- respond $ Just cfm+ finalPairs <- toPairs =<< saslFromJust =<< pullFinalMessage+ unless (lookup "v" finalPairs == Just v) $ throwError AuthOtherFailure -- TODO: Log+ return ()+ where+ -- We need to jump through some hoops to get a polymorphic solution+ encode :: Crypto.Hash ctx hash => hash -> hash -> BS.ByteString+ encode _hashtoken = Crypto.encode++ hash :: BS.ByteString -> BS.ByteString+ hash str = encode hToken $ Crypto.hash' str++ hmac :: BS.ByteString -> BS.ByteString -> BS.ByteString+ hmac key str = encode hToken $ Crypto.hmac' (Crypto.MacKey key) str++ authzid'' :: Maybe BS.ByteString+ authzid'' = (\z -> "a=" +++ Text.encodeUtf8 z) <$> authzid'++ gs2CbindFlag :: BS.ByteString+ gs2CbindFlag = "n" -- we don't support channel binding yet++ gs2Header :: BS.ByteString+ gs2Header = merge $ [ gs2CbindFlag+ , maybe "" id authzid''+ , ""+ ]+ -- cbindData :: BS.ByteString+ -- cbindData = "" -- we don't support channel binding yet++ cFirstMessageBare :: BS.ByteString -> BS.ByteString+ cFirstMessageBare cnonce = merge [ "n=" +++ Text.encodeUtf8 authcid'+ , "r=" +++ cnonce]+ cFirstMessage :: BS.ByteString -> BS.ByteString+ cFirstMessage cnonce = gs2Header +++ cFirstMessageBare cnonce++ fromPairs :: Pairs+ -> BS.ByteString+ -> ExceptT AuthFailure (StateT StreamState IO) (BS.ByteString, BS.ByteString, Integer)+ fromPairs prs cnonce | Just nonce <- lookup "r" prs+ , cnonce `BS.isPrefixOf` nonce+ , Just salt' <- lookup "s" prs+ , Right salt <- B64.decode salt'+ , Just ic <- lookup "i" prs+ , [(i,"")] <- reads $ BS8.unpack ic+ = return (nonce, salt, i)+ fromPairs _ _ = throwError $ AuthOtherFailure -- TODO: Log++ cFinalMessageAndVerifier :: BS.ByteString+ -> BS.ByteString+ -> Integer+ -> BS.ByteString+ -> BS.ByteString+ -> (BS.ByteString, BS.ByteString)+ cFinalMessageAndVerifier nonce salt ic sfm cnonce+ = (merge [ cFinalMessageWOProof+ , "p=" +++ B64.encode clientProof+ ]+ , B64.encode serverSignature+ )+ where+ cFinalMessageWOProof :: BS.ByteString+ cFinalMessageWOProof = merge [ "c=" +++ B64.encode gs2Header+ , "r=" +++ nonce]++ saltedPassword :: BS.ByteString+ saltedPassword = hi (Text.encodeUtf8 pwd) salt ic++ clientKey :: BS.ByteString+ clientKey = hmac saltedPassword "Client Key"++ storedKey :: BS.ByteString+ storedKey = hash clientKey++ authMessage :: BS.ByteString+ authMessage = merge [ cFirstMessageBare cnonce+ , sfm+ , cFinalMessageWOProof+ ]++ clientSignature :: BS.ByteString+ clientSignature = hmac storedKey authMessage++ clientProof :: BS.ByteString+ clientProof = clientKey `xorBS` clientSignature++ serverKey :: BS.ByteString+ serverKey = hmac saltedPassword "Server Key"++ serverSignature :: BS.ByteString+ serverSignature = hmac serverKey authMessage++ -- helper+ hi :: BS.ByteString -> BS.ByteString -> Integer -> BS.ByteString+ hi str slt ic' = foldl1' xorBS (genericTake ic' us)+ where+ u1 = hmac str (slt +++ (BS.pack [0,0,0,1]))+ us = iterate (hmac str) u1++scramSha1 :: Username -- ^ username+ -> Maybe AuthZID -- ^ authorization ID+ -> Password -- ^ password+ -> SaslHandler+scramSha1 authcid authzid passwd =+ ( "SCRAM-SHA-1"+ , do+ r <- runExceptT $ scram (hashToken :: Crypto.SHA1) authcid authzid passwd+ case r of+ Left (AuthStreamFailure e) -> return $ Left e+ Left e -> return $ Right $ Just e+ Right () -> return $ Right Nothing+ )
+ source/Network/Xmpp/Sasl/StringPrep.hs view
@@ -0,0 +1,37 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.Xmpp.Sasl.StringPrep where++import Text.StringPrep+import qualified Data.Set as Set+import Data.Text(Text, singleton)++nonAsciiSpaces :: Set.Set Char+nonAsciiSpaces = Set.fromList [ '\x00A0', '\x1680', '\x2000', '\x2001', '\x2002'+ , '\x2003', '\x2004', '\x2005', '\x2006', '\x2007'+ , '\x2008', '\x2009', '\x200A', '\x200B', '\x202F'+ , '\x205F', '\x3000'+ ]++toSpace :: Char -> Text+toSpace x = if x `Set.member` nonAsciiSpaces then " " else singleton x++saslPrepQuery :: StringPrepProfile+saslPrepQuery = Profile+ [b1, toSpace]+ True+ [c12, c21, c22, c3, c4, c5, c6, c7, c8, c9]+ True++saslPrepStore :: StringPrepProfile+saslPrepStore = Profile+ [b1, toSpace]+ True+ [a1, c12, c21, c22, c3, c4, c5, c6, c7, c8, c9]+ True++normalizePassword :: Text -> Maybe Text+normalizePassword = runStringPrep saslPrepStore++normalizeUsername :: Text -> Maybe Text+normalizeUsername = runStringPrep saslPrepQuery
+ source/Network/Xmpp/Sasl/Types.hs view
@@ -0,0 +1,21 @@+{-# OPTIONS_HADDOCK hide #-}+module Network.Xmpp.Sasl.Types where++import Control.Monad.State.Strict+import Data.ByteString(ByteString)+import qualified Data.Text as Text+import Network.Xmpp.Types++type Username = Text.Text+type Password = Text.Text+type AuthZID = Text.Text++data SaslElement = SaslSuccess (Maybe Text.Text)+ | SaslChallenge (Maybe Text.Text)++type Pairs = [(ByteString, ByteString)]++-- | 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, StateT StreamState IO (Either XmppFailure (Maybe AuthFailure)))
+ source/Network/Xmpp/Stanza.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE RecordWildCards #-}++{-# OPTIONS_HADDOCK hide #-}++-- | Stanza related functions and constants+--++module Network.Xmpp.Stanza where++import Data.XML.Types+import Network.Xmpp.Types+import Network.Xmpp.Lens++-- | Request subscription with an entity.+presenceSubscribe :: Jid -> Presence+presenceSubscribe to' = presence { presenceTo = Just to'+ , presenceType = Subscribe+ }++-- | Approve a subscripton of an entity.+presenceSubscribed :: Jid -> Presence+presenceSubscribed to' = presence { presenceTo = Just to'+ , presenceType = Subscribed+ }++-- | End a subscription with an entity.+presenceUnsubscribe :: Jid -> Presence+presenceUnsubscribe to' = presence { presenceTo = Just to'+ , presenceType = Unsubscribe+ }++-- | Deny a not-yet approved or terminate a previously approved subscription of+-- an entity+presenceUnsubscribed :: Jid -> Presence+presenceUnsubscribed to' = presence { presenceTo = Just to'+ , presenceType = Unsubscribed+ }++-- | Signal to the server that the client is available for communication.+presenceOnline :: Presence+presenceOnline = presence++-- | Signal to the server that the client is no longer available for+-- communication.+presenceOffline :: Presence+presenceOffline = presence {presenceType = Unavailable}++-- | Produce an answer message with the given payload, setting "from" to the+-- "to" attributes in the original message. Produces a 'Nothing' value of the+-- provided message message has no "from" attribute. Sets the "from" attribute+-- to 'Nothing' to let the server assign one.+answerMessage :: Message -> [Element] -> Maybe Message+answerMessage Message{messageFrom = Just frm, ..} payload' =+ Just Message{ messageFrom = Nothing+ , messageID = Nothing+ , messageTo = Just frm+ , messagePayload = payload'+ , ..+ }+answerMessage _ _ = Nothing++-- | Add a recipient to a presence notification.+presTo :: Presence -> Jid -> Presence+presTo pres to' = pres{presenceTo = Just to'}++-- | Create a StanzaError with @condition@ and the 'associatedErrorType'. Leave+-- the error text and the application specific condition empty+mkStanzaError :: StanzaErrorCondition -- ^ condition+ -> StanzaError+mkStanzaError condition = StanzaError (associatedErrorType condition)+ condition Nothing Nothing++-- | Create an IQ error response to an IQ request using the given condition. The+-- error type is derived from the condition using 'associatedErrorType' and+-- both text and the application specific condition are left empty+iqError :: StanzaErrorCondition -> IQRequest -> IQError+iqError condition (IQRequest iqid from' _to lang' _tp _bd _attr) =+ IQError iqid Nothing from' lang' (mkStanzaError condition) Nothing []+++-- | Create an IQ Result matching an IQ request+iqResult :: Maybe Element -> IQRequest -> IQResult+iqResult pl iqr = IQResult+ { iqResultID = iqRequestID iqr+ , iqResultFrom = Nothing+ , iqResultTo = view from iqr+ , iqResultLangTag = view lang iqr+ , iqResultPayload = pl+ , iqResultAttributes = []+ }++-- | The RECOMMENDED error type associated with an error condition. The+-- following conditions allow for multiple types+--+-- * 'FeatureNotImplemented': 'Cancel' or 'Modify' (returns 'Cancel')+--+-- * 'PolicyViolation': 'Modify' or 'Wait' ('Modify')+--+-- * 'RemoteServerTimeout': 'Wait' or unspecified other ('Wait')+--+-- * 'UndefinedCondition': Any condition ('Cancel')+associatedErrorType :: StanzaErrorCondition -> StanzaErrorType+associatedErrorType BadRequest = Modify+associatedErrorType Conflict = Cancel+associatedErrorType FeatureNotImplemented = Cancel -- Or Modify+associatedErrorType Forbidden = Auth+associatedErrorType Gone{} = Cancel+associatedErrorType InternalServerError = Cancel+associatedErrorType ItemNotFound = Cancel+associatedErrorType JidMalformed = Modify+associatedErrorType NotAcceptable = Modify+associatedErrorType NotAllowed = Cancel+associatedErrorType NotAuthorized = Auth+associatedErrorType PolicyViolation = Modify -- Or Wait+associatedErrorType RecipientUnavailable = Wait+associatedErrorType Redirect{} = Modify+associatedErrorType RegistrationRequired = Auth+associatedErrorType RemoteServerNotFound = Cancel+associatedErrorType RemoteServerTimeout = Wait -- Possibly Others+associatedErrorType ResourceConstraint = Wait+associatedErrorType ServiceUnavailable = Cancel+associatedErrorType SubscriptionRequired = Auth+associatedErrorType UndefinedCondition = Cancel -- This can be anything+associatedErrorType UnexpectedRequest = Modify
+ source/Network/Xmpp/Stream.hs view
@@ -0,0 +1,917 @@+{-# OPTIONS_HADDOCK hide #-}++{-# LANGUAGE CPP #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FlexibleContexts #-}++module Network.Xmpp.Stream where++import Control.Applicative ((<$>))+import Control.Concurrent (forkIO, threadDelay)+import Control.Concurrent.STM+import qualified Control.Exception as Ex+import qualified Control.Exception.Lifted as ExL+import Control.Monad+import Control.Monad.Except+import Control.Monad.State.Strict+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BSC8+import Data.Char (isSpace)+import Data.Conduit hiding (connect)+import qualified Data.Conduit.Internal as DCI+import qualified Data.Conduit.List as CL+import Data.IP+import Data.List+import Data.Maybe+import Data.Ord+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import qualified Data.Text.Encoding.Error as Text+import Data.Void (Void)+import Data.Word (Word16)+import Data.XML.Pickle+import Data.XML.Types+import qualified GHC.IO.Exception as GIE+import Network.Socket hiding (Closed, Stream, connect)+import Network.DNS hiding (encode, lookup)+import qualified Network.Socket as S+import Network.Socket (AddrInfo)+import Network.Xmpp.Marshal+import Network.Xmpp.Types+import System.IO+-- import System.IO.Error (tryIOError) <- Only available in base >=4.4+import System.Log.Logger+import System.Random (randomRIO)+import Text.XML.Stream.Parse as XP+import Lens.Family2 (over)++import Network.Xmpp.Utilities+import qualified Network.Xmpp.Lens as L++-- "readMaybe" definition, as readMaybe is not introduced in the `base' package+-- until version 4.6.+readMaybe_ :: (Read a) => String -> Maybe a+readMaybe_ string = case reads string of+ [(a, "")] -> Just a+ _ -> Nothing++-- import Text.XML.Stream.Elements++mbl :: Maybe [a] -> [a]+mbl (Just l) = l+mbl Nothing = []++lmb :: [t] -> Maybe [t]+lmb [] = Nothing+lmb x = Just x++-- Unpickles and returns a stream element.+streamUnpickleElem :: PU [Node] a+ -> Element+ -> StreamSink a+streamUnpickleElem p x = do+ case unpickleElem p x of+ Left l -> do+ liftIO $ warningM "Pontarius.Xmpp" $ "streamUnpickleElem: Unpickle error: " ++ ppUnpickleError l+ throwError $ XmppOtherFailure+ Right r -> return r++-- This is the conduit sink that handles the stream XML events. We extend it+-- with ExceptT capabilities.+type StreamSink a = ConduitM Event Void (ExceptT XmppFailure IO) a++-- Discards all events before the first EventBeginElement.+throwOutJunk :: Monad m => ConduitM Event a m ()+throwOutJunk = do+ next <- CL.peek+ case next of+ Nothing -> return () -- This will only happen if the stream is closed.+ Just (EventBeginElement _ _) -> return ()+ _ -> CL.drop 1 >> throwOutJunk++-- Returns an (empty) Element from a stream of XML events.+openElementFromEvents :: StreamSink Element+openElementFromEvents = do+ throwOutJunk+ hd <- await+ case hd of+ Just (EventBeginElement name attrs) -> return $ Element name attrs []+ _ -> do+ liftIO $ warningM "Pontarius.Xmpp" "openElementFromEvents: Stream ended."+ throwError XmppOtherFailure++-- Sends the initial stream:stream element and pulls the server features. If the+-- 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 StreamState IO (Either XmppFailure ())+startStream = runExceptT $ do+ lift $ lift $ debugM "Pontarius.Xmpp" "Starting stream..."+ st <- lift $ get+ -- Set the `from' (which is also the expected to) attribute depending on the+ -- state of the stream.+ let expectedTo = case ( streamConnectionState st+ , toJid $ streamConfiguration st) of+ (Plain , (Just (j, True))) -> Just j+ (Plain , _ ) -> Nothing+ (Secured , (Just (j, _ ))) -> Just j+ (Secured , Nothing ) -> Nothing+ (Closed , _ ) -> Nothing+ (Finished , _ ) -> Nothing+ case streamAddress st of+ Nothing -> do+ lift $ lift $ errorM "Pontarius.Xmpp" "Server sent no hostname."+ throwError XmppOtherFailure+ Just address -> do+ ExceptT $ pushXmlDecl+ ExceptT . pushOpenElement . streamNSHack $+ pickleElem xpStream ( "1.0"+ , expectedTo+ , Just (Jid Nothing (Nonempty address) Nothing)+ , Nothing+ , preferredLang $ streamConfiguration st+ )+ response <- ExceptT $ runEventsSink $ streamS expectedTo+ case response of+ Right (ver, from, to, sid, lt, features)+ | versionFromText ver == Nothing -> closeStreamWithError+ StreamUnsupportedVersion Nothing+ "Unspecified version"+ | let v = versionFromText ver+ in isJust v && majorVersion (fromJust v) >= 2 ->+ closeStreamWithError StreamUnsupportedVersion Nothing+ "Non-1.x version"+ -- HACK: We ignore MUST-strength requirement (section 4.7.4. of RFC+ -- 6120) for the sake of compatibility with jabber.org+ -- | lt == Nothing ->+ -- closeStreamWithError StreamInvalidXml Nothing+ -- "Stream has no language tag"++ -- If `from' is set, we verify that it's the correct one. TODO: Should we+ -- check against the realm instead?+ | isJust from && (from /= Just (Jid Nothing (Nonempty . fromJust $ streamAddress st) Nothing)) ->+ closeStreamWithError StreamInvalidFrom Nothing+ "Stream from is invalid"+ | to /= expectedTo ->+ closeStreamWithError StreamUndefinedCondition (Just $ Element "invalid-to" [] [])+ "Stream to invalid"-- TODO: Suitable?+ | otherwise -> do+ -- HACK: (ignore section 4.7.4. of RFC 6120), see above+ unless (isJust lt) $ liftIO $ warningM "Pontariusm.Xmpp"+ "Stream has no language tag"+ modify (\s -> s{ streamFeatures = features+ , streamLang = lt+ , streamId = sid+ , streamFrom = from+ } )+ return ()+ -- Unpickling failed - we investigate the element.+ Left (Element name attrs _children)+ | (nameLocalName name /= "stream") ->+ closeStreamWithError StreamInvalidXml Nothing+ "Root element is not stream"+ | (nameNamespace name /= Just "http://etherx.jabber.org/streams") ->+ closeStreamWithError StreamInvalidNamespace Nothing+ "Wrong root element name space"+ | (isJust $ namePrefix name) && (fromJust (namePrefix name) /= "stream") ->+ closeStreamWithError StreamBadNamespacePrefix Nothing+ "Root name prefix set and not stream"+ | otherwise -> ExceptT $ checkchildren (flattenAttrs attrs)+ where+ -- HACK: We include the default namespace to make isode's M-LINK server happy.+ streamNSHack e = e{elementAttributes = elementAttributes e+ ++ [( "xmlns"+ , [ContentText "jabber:client"])]}+ closeStreamWithError :: StreamErrorCondition -> Maybe Element -> String+ -> ExceptT XmppFailure (StateT StreamState IO) ()+ closeStreamWithError sec el msg = do+ void . lift . pushElement . pickleElem xpStreamError+ $ StreamErrorInfo sec Nothing el+ void . lift $ closeStreams'+ liftIO $ errorM "Pontarius.Xmpp" $ "closeStreamWithError: " ++ msg+ throwError XmppOtherFailure+ checkchildren children =+ let to' = lookup "to" children+ ver' = lookup "version" children+ xl = lookup xmlLang children+ in case () of () | Just Nothing == fmap jidFromText to' ->+ runExceptT $ closeStreamWithError+ StreamBadNamespacePrefix Nothing+ "stream to not a valid JID"+ | Nothing == ver' ->+ runExceptT $ closeStreamWithError+ StreamUnsupportedVersion Nothing+ "stream no version"+ | Just (Nothing :: Maybe LangTag) == (safeRead <$> xl) ->+ runExceptT $ closeStreamWithError+ StreamInvalidXml Nothing+ "stream no language tag"+ | otherwise ->+ runExceptT $ closeStreamWithError+ StreamBadFormat Nothing+ ""+ safeRead x = case reads $ Text.unpack x of+ [] -> Nothing+ ((y,_):_) -> Just y++flattenAttrs :: [(Name, [Content])] -> [(Name, Text.Text)]+flattenAttrs attrs = Prelude.map (\(name, cont) ->+ ( name+ , Text.concat $ Prelude.map uncontentify cont)+ )+ attrs+ where+ uncontentify (ContentText t) = t+ uncontentify _ = ""++-- Sets a new Event source using the raw source (of bytes)+-- and calls xmppStartStream.+restartStream :: StateT StreamState IO (Either XmppFailure ())+restartStream = do+ liftIO $ debugM "Pontarius.Xmpp" "Restarting stream..."+ raw <- gets streamHandle+ let newSource = sourceStreamHandle raw $= XP.parseBytes def+ buffered <- liftIO . bufferSrc $ newSource+ modify (\s -> s{streamEventSource = buffered })+ startStream+++-- Creates a conduit from a StreamHandle+sourceStreamHandleRaw :: (MonadIO m, MonadError XmppFailure m)+ => StreamHandle -> ConduitM i ByteString m ()+sourceStreamHandleRaw s = forever . read $ streamReceive s+ where+ read rd = do+ bs' <- liftIO (rd 4096)+ bs <- case bs' of+ Left e -> throwError e+ Right r -> return r+ yield bs++sourceStreamHandle :: (MonadIO m, MonadError XmppFailure m)+ => StreamHandle -> ConduitM i ByteString m ()+sourceStreamHandle sh = sourceStreamHandleRaw sh $= logInput++logInput :: MonadIO m => ConduitM ByteString ByteString m ()+logInput = go Nothing+ where+ go mbDec = do+ mbBs <- await+ case mbBs of+ Nothing -> return ()+ Just bs -> do+ let decode = case mbDec of+ Nothing -> Text.streamDecodeUtf8With Text.lenientDecode+ Just d -> d+ (Text.Some out leftover cont) = decode bs+ cont' = if BS.null leftover+ then Nothing+ else Just cont+ unless (Text.null out) $+ liftIO $ debugM "Pontarius.Xmpp"+ $ "in: " ++ Text.unpack out+ yield bs+ go cont'++-- We buffer sources because we don't want to lose data when multiple+-- xml-entities are sent with the same packet and we don't want to eternally+-- block the StreamState while waiting for data to arrive+bufferSrc :: ConduitT () o (ExceptT XmppFailure IO) ()+ -> IO (ConduitM i o (ExceptT XmppFailure IO) ())+bufferSrc src = do+ ref <- newTMVarIO $ DCI.sealConduitT src+ let go = do+ dt <- liftIO $ Ex.bracketOnError+ (atomically $ takeTMVar ref)+ (\_ -> atomically . putTMVar ref $ zeroResumableSource)+ (\s -> do+ res <- runExceptT (s $$++ await)+ case res of+ Left e -> do+ atomically $ putTMVar ref zeroResumableSource+ return $ Left e+ Right (s',b) -> do+ atomically $ putTMVar ref s'+ return $ Right b+ )+ case dt of+ Left e -> throwError e+ Right Nothing -> return ()+ Right (Just d) -> yield d >> go+ return go+ where+ zeroResumableSource = DCI.sealConduitT zeroSource++-- Reads the (partial) stream:stream and the server features from the stream.+-- Returns the (unvalidated) stream attributes, the unparsed element, or+-- throwError throws a `XmppOtherFailure' (if something other than an element+-- was encountered at first, or if something other than stream features was+-- encountered second).+-- TODO: from.+streamS :: Maybe Jid -> StreamSink (Either Element ( Text+ , Maybe Jid+ , Maybe Jid+ , Maybe Text+ , Maybe LangTag+ , StreamFeatures ))+streamS _expectedTo = do -- TODO: check expectedTo+ streamHeader <- xmppStreamHeader+ case streamHeader of+ Right (version, from, to, sid, lTag) -> do+ features <- xmppStreamFeatures+ return $ Right (version, from, to, sid, lTag, features)+ Left el -> return $ Left el+ where+ xmppStreamHeader :: StreamSink (Either Element (Text, Maybe Jid, Maybe Jid, Maybe Text.Text, Maybe LangTag))+ xmppStreamHeader = do+ throwOutJunk+ -- Get the stream:stream element (or whatever it is) from the server,+ -- and validate what we get.+ el <- openElementFromEvents -- May throw `XmppOtherFailure' if an+ -- element is not received+ case unpickleElem xpStream el of+ Left _ -> return $ Left el+ Right r -> return $ Right r+ xmppStreamFeatures :: StreamSink StreamFeatures+ xmppStreamFeatures = do+ e <- elements =$ await+ case e of+ Nothing -> do+ lift $ lift $ errorM "Pontarius.Xmpp" "streamS: Stream ended."+ throwError XmppOtherFailure+ Just r -> streamUnpickleElem xpStreamFeatures r++-- | Connects to the XMPP server and opens the XMPP stream against the given+-- realm.+openStream :: HostName -> StreamConfiguration -> IO (Either XmppFailure (Stream))+openStream realm config = runExceptT $ do+ lift $ debugM "Pontarius.Xmpp" "Opening stream..."+ stream' <- createStream realm config+ ExceptT . liftIO $ withStream startStream stream'+ return stream'++-- | 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 :: Stream -> IO ()+closeStreams = withStream closeStreams'++closeStreams' :: StateT StreamState IO ()+closeStreams' = do+ lift $ debugM "Pontarius.Xmpp" "Closing stream"+ send <- gets (streamSend . streamHandle)+ cc <- gets (streamClose . streamHandle)+ lift $ debugM "Pontarius.Xmpp" "Sending closing tag"+ void . liftIO $ send "</stream:stream>"+ lift $ debugM "Pontarius.Xmpp" "Waiting for stream to close"+ void $ liftIO $ forkIO $ do+ threadDelay 3000000 -- TODO: Configurable value+ void ((Ex.try cc) :: IO (Either Ex.SomeException ()))+ return ()+ put xmppNoStream{ streamConnectionState = Finished }+-- lift $ debugM "Pontarius.Xmpp" "Collecting remaining elements"+-- es <- collectElems []+ -- lift $ debugM "Pontarius.Xmpp" "Stream sucessfully closed"+ -- return es+ -- where+ -- -- Pulls elements from the stream until the stream ends, or an error is+ -- -- raised.+ -- collectElems :: [Element] -> StateT StreamState 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)++-- TODO: Can the TLS send/recv functions throw something other than an IO error?+debugOut :: MonadIO m => ByteString -> m ()+debugOut outData = liftIO $ debugM "Pontarius.Xmpp"+ ("Out: " ++ (Text.unpack . Text.decodeUtf8 $ outData))++wrapIOException :: MonadIO m =>+ String+ -> IO a+ -> m (Either XmppFailure a)+wrapIOException tag action = do+ r <- liftIO $ tryIOError action+ case r of+ Right b -> return $ Right b+ Left e -> do+ liftIO $ warningM "Pontarius.Xmpp" $ concat+ [ "wrapIOException ("+ , tag+ , ") : Exception wrapped: "+ , show e+ ]+ return $ Left $ XmppIOException e++pushElement :: Element -> StateT StreamState IO (Either XmppFailure ())+pushElement x = do+ send <- gets (streamSend . streamHandle)+ let outData = renderElement $ nsHack x+ debugOut outData+ lift $ send outData++-- HACK: We remove the "jabber:client" namespace because it is set as+-- default in the stream. This is to make isode's M-LINK server happy and+-- should be removed once jabber.org accepts prefix-free canonicalization+nsHack :: Element -> Element+nsHack e@(Element{elementName = n})+ | nameNamespace n == Just "jabber:client" =+ e{ elementName = Name (nameLocalName n) Nothing Nothing+ , elementNodes = map mapNSHack $ elementNodes e+ }+ | otherwise = e+ where+ mapNSHack :: Node -> Node+ mapNSHack (NodeElement el) = NodeElement $ nsHack el+ mapNSHack nd = nd++-- | Encode and send stanza+pushStanza :: Stanza -> Stream -> IO (Either XmppFailure ())+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 StreamState IO (Either XmppFailure ())+pushXmlDecl = do+ con <- gets streamHandle+ lift $ streamSend con "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"++pushOpenElement :: Element -> StateT StreamState IO (Either XmppFailure ())+pushOpenElement e = do+ send <- gets (streamSend . streamHandle)+ let outData = renderOpenElement e+ debugOut outData+ lift $ send outData++-- `Connect-and-resumes' the given sink to the stream source, and pulls a+-- `b' value.+runEventsSink :: ConduitT Event Void (ExceptT XmppFailure IO) b+ -> StateT StreamState IO (Either XmppFailure b)+runEventsSink snk = do -- TODO: Wrap exceptions?+ src <- gets streamEventSource+ lift . runExceptT $ src $$ snk++pullElement :: StateT StreamState IO (Either XmppFailure Element)+pullElement = do+ e <- runEventsSink (elements =$ await)+ case e of+ Left l -> do+ liftIO . errorM "Pontarius.Xmpp" $+ "Error while retrieving XML element: " ++ show l+ return $ Left l++ Right Nothing -> do+ liftIO $ errorM "Pontarius.Xmpp" "pullElement: Stream ended."+ return . Left $ XmppOtherFailure+ Right (Just r) -> return $ Right r++-- Pulls an element and unpickles it.+pullUnpickle :: PU [Node] a -> StateT StreamState IO (Either XmppFailure a)+pullUnpickle p = do+ el <- pullElement+ case el of+ Left e -> return $ Left e+ Right elem' -> do+ let res = unpickleElem p elem'+ case res of+ Left e -> do+ lift $ errorM "Pontarius.Xmpp" $ "pullUnpickle: Unpickle failed: " ++ (ppUnpickleError e)+ return . Left $ XmppOtherFailure+ Right r -> return $ Right r++-- | Pulls a stanza (or stream error) from the stream.+pullStanza :: 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++-- | Pulls a stanza, nonza or stream error from the stream.+pullXmppElement :: Stream -> IO (Either XmppFailure XmppElement)+pullXmppElement = withStream' $ do+ res <- pullUnpickle xpStreamElement+ 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, which it will return.+catchPush :: IO () -> IO (Either XmppFailure ())+catchPush p = ExL.catch+ (p >> return (Right ()))+ (\e -> case GIE.ioe_type e of+ GIE.ResourceVanished -> return . Left $ XmppIOException e+ GIE.IllegalOperation -> return . Left $ XmppIOException e+ _ -> ExL.throwIO e+ )++zeroHandle :: StreamHandle+zeroHandle = StreamHandle { streamSend = \_ -> return (Left XmppNoStream)+ , streamReceive = \_ -> do+ errorM "Pontarius.Xmpp"+ "xmppNoStream: Stream is closed."+ return $ Left XmppNoStream+ , streamFlush = return ()+ , streamClose = return ()+ }++-- Stream state used when there is no connection.+xmppNoStream :: StreamState+xmppNoStream = StreamState {+ streamConnectionState = Closed+ , streamHandle = zeroHandle+ , streamEventSource = zeroSource+ , streamFeatures = mempty+ , streamAddress = Nothing+ , streamFrom = Nothing+ , streamId = Nothing+ , streamLang = Nothing+ , streamJid = Nothing+ , streamConfiguration = def+ }++zeroSource :: ConduitT () a (ExceptT XmppFailure IO) ()+zeroSource = do+ liftIO $ debugM "Pontarius.Xmpp" "zeroSource"+ throwError XmppNoStream++handleToStreamHandle :: Handle -> StreamHandle+handleToStreamHandle h = StreamHandle { streamSend = \d ->+ wrapIOException "streamSend"+ $ BS.hPut h d+ , streamReceive = \n ->+ wrapIOException "streamReceive"+ $ BS.hGetSome h n+ , streamFlush = hFlush h+ , streamClose = hClose h+ }++createStream :: HostName -> StreamConfiguration -> ExceptT XmppFailure IO (Stream)+createStream realm config = do+ result <- connect realm config+ case result of+ Just hand -> ExceptT $ do+ debugM "Pontarius.Xmpp" "Acquired handle."+ debugM "Pontarius.Xmpp" "Setting NoBuffering mode on handle."+ eSource <- liftIO . bufferSrc $+ (sourceStreamHandle hand $= logConduit)+ $= XP.parseBytes def+ let stream = StreamState+ { streamConnectionState = Plain+ , streamHandle = hand+ , streamEventSource = eSource+ , streamFeatures = mempty+ , streamAddress = Just $ Text.pack realm+ , streamFrom = Nothing+ , streamId = Nothing+ , streamLang = Nothing+ , streamJid = Nothing+ , streamConfiguration = maybeSetTlsHost realm config+ }+ stream' <- mkStream stream+ return $ Right stream'+ Nothing -> do+ lift $ debugM "Pontarius.Xmpp" "Did not acquire handle."+ throwError TcpConnectionFailure+ where+ logConduit :: MonadIO m => ConduitT ByteString ByteString m ()+ logConduit = CL.mapM $ \d -> do+ liftIO . debugM "Pontarius.Xmpp" $ "In: " ++ (BSC8.unpack d) +++ "."+ return d+ tlsIdentL = L.tlsParamsL . L.clientServerIdentificationL+ updateHost host ("", _) = (host, "")+ updateHost _ hst = hst+ maybeSetTlsHost host = over tlsIdentL (updateHost host)++-- Connects using the specified method. Returns the Handle acquired, if any.+connect :: HostName -> StreamConfiguration -> ExceptT XmppFailure IO+ (Maybe StreamHandle)+connect realm config = do+ case connectionDetails config of+ UseHost host port -> lift $ do+ debugM "Pontarius.Xmpp" "Connecting to configured address."+ h <- resolveAndConnectTcp host port+ case h of+ Nothing -> return Nothing+ Just h' -> do+ liftIO $ hSetBuffering h' NoBuffering+ return . Just $ handleToStreamHandle h'+ UseSrv host -> do+ h <- connectSrv (resolvConf config) host+ case h of+ Nothing -> return Nothing+ Just h' -> do+ liftIO $ hSetBuffering h' NoBuffering+ return . Just $ handleToStreamHandle h'+ UseRealm -> do+ h <- connectSrv (resolvConf config) realm+ case h of+ Nothing -> return Nothing+ Just h' -> do+ liftIO $ hSetBuffering h' NoBuffering+ return . Just $ handleToStreamHandle h'+ UseConnection mkC -> Just <$> mkC++connectSrv :: ResolvConf -> String -> ExceptT XmppFailure IO (Maybe Handle)+connectSrv config host = do+ case checkHostName (Text.pack host) of+ Just host' -> do+ resolvSeed <- lift $ makeResolvSeed config+ lift $ debugM "Pontarius.Xmpp" "Performing SRV lookup..."+ srvRecords <- srvLookup host' resolvSeed+ case srvRecords of+ Nothing -> do+ lift $ debugM "Pontarius.Xmpp"+ "No SRV records, using fallback process."+ lift $ resolveAndConnectTcp host 5222+ Just [(".", _)] -> do+ liftIO $ infoM "Pontarius.Xmpp"+ "SRV lookup returned \".\"; service not available"+ throwError TcpConnectionFailure+ Just srvRecords' -> do+ lift $ debugM "Pontarius.Xmpp"+ "SRV records found, looking up host."+ lift $ resolvSrvsAndConnectTcp+ ( for srvRecords' $+ \(domain, port) -> ( BSC8.unpack domain+ , fromIntegral port))+ Nothing -> do+ lift $ errorM "Pontarius.Xmpp"+ "The hostname could not be validated."+ throwError XmppIllegalTcpDetails+ where for = flip fmap++connectHandle :: AddrInfo -> IO Handle+connectHandle addrInfo = do+ s <- S.socket (S.addrFamily addrInfo) S.Stream S.defaultProtocol+ S.connect s (S.addrAddress addrInfo)+ S.socketToHandle s ReadWriteMode++-- Connects to a list of addresses and ports. Surpresses any exceptions from+-- connectTcp.+connectTcp :: [AddrInfo] -> IO (Maybe Handle)+connectTcp [] = return Nothing+connectTcp (addrInfo:remainder) = do+ let addr = (show $ S.addrAddress addrInfo)+ result <- Ex.try $ (do+ debugM "Pontarius.Xmpp" $ "Connecting to " ++ addr+ connectHandle addrInfo) :: IO (Either Ex.IOException Handle)+ case result of+ Right handle -> do+ debugM "Pontarius.Xmpp" $ "Successfully connected to " ++ addr+ return $ Just handle+ Left _ -> do+ debugM "Pontarius.Xmpp" $+ "Connection to " ++ addr ++ " could not be established."+ connectTcp remainder++#if MIN_VERSION_dns(1, 0, 0)+fixDnsResult :: Either e a -> Maybe a+fixDnsResult = either (const Nothing) Just+#else+fixDnsResult :: Maybe a -> Maybe a+fixDnsResult = id+#endif++-- Makes an AAAA query to acquire a IPs, and tries to connect to all of them. If+-- a handle can not be acquired this way, an analogous A query is performed.+-- Surpresses all IO exceptions.+resolveAndConnectTcp :: HostName -> PortNumber -> IO (Maybe Handle)+resolveAndConnectTcp hostName port = do+ ais <- S.getAddrInfo Nothing (Just hostName) Nothing+ connectTcp $ setPort <$> ais+ where+ setPort ai = ai {S.addrAddress = setAddressPort port (S.addrAddress ai)}+ setAddressPort port (S.SockAddrInet _ addr) = S.SockAddrInet port addr+ setAddressPort port (S.SockAddrInet6 _ flow addr scope) =+ S.SockAddrInet6 port flow addr scope+ setAddressPort _ addr = addr++-- Tries `resolvAndConnectTcp' for every SRV record, stopping if a handle is+-- acquired.+resolvSrvsAndConnectTcp :: [(HostName, PortNumber)] -> IO (Maybe Handle)+resolvSrvsAndConnectTcp [] = return Nothing+resolvSrvsAndConnectTcp ((domain, port):remaining) = do+ result <- resolveAndConnectTcp domain port+ case result of+ Just handle -> return $ Just handle+ Nothing -> resolvSrvsAndConnectTcp remaining+++-- The DNS functions may make error calls. This function catches any such+-- exceptions and rethrows them as IOExceptions.+rethrowErrorCall :: IO a -> IO a+rethrowErrorCall action = do+ result <- Ex.try action+ case result of+ Right result' -> return result'+ Left (Ex.ErrorCall e) -> Ex.ioError $ userError+ $ "rethrowErrorCall: " ++ e++-- Provides a list of A(AAA) names and port numbers upon a successful+-- DNS-SRV request, or `Nothing' if the DNS-SRV request failed.+srvLookup :: Text -> ResolvSeed -> ExceptT XmppFailure IO (Maybe [(Domain, Word16)])+srvLookup realm resolvSeed = ExceptT $ do+ result <- Ex.try $ rethrowErrorCall $ withResolver resolvSeed+ $ \resolver -> do+ srvResult <- lookupSRV resolver $ BSC8.pack $ "_xmpp-client._tcp." ++ (Text.unpack realm) ++ "."+ case fixDnsResult srvResult of+ Just [] -> do+ debugM "Pontarius.Xmpp" "No SRV result returned."+ return Nothing+ Just [(_, _, _, ".")] -> do+ debugM "Pontarius.Xmpp" $ "\".\" SRV result returned."+ return $ Just []+ Just srvResult' -> do+ debugM "Pontarius.Xmpp" $ "SRV result: " ++ (show srvResult')+ -- Get [(Domain, PortNumber)] of SRV request, if any.+ orderedSrvResult <- orderSrvResult srvResult'+ return $ Just $ Prelude.map (\(_, _, port, domain) -> (domain, port)) orderedSrvResult+ -- The service is not available at this domain.+ -- Sorts the records based on the priority value.+ Nothing -> do+ debugM "Pontarius.Xmpp" "No SRV result returned."+ return Nothing+ case result of+ Right result' -> return $ Right result'+ Left e -> return $ Left $ XmppIOException e+ where+ -- This function orders the SRV result in accordance with RFC+ -- 2782. It sorts the SRV results in order of priority, and then+ -- uses a random process to order the records with the same+ -- priority based on their weight.+ orderSrvResult :: [(Word16, Word16, Word16, Domain)] -> IO [(Word16, Word16, Word16, Domain)]+ orderSrvResult srvResult = do+ -- Order the result set by priority.+ let srvResult' = sortBy (comparing (\(priority, _, _, _) -> priority)) srvResult+ -- Group elements in sublists based on their priority. The+ -- type is `[[(Word16, Word16, Word16, Domain)]]'.+ let srvResult'' = Data.List.groupBy (\(priority, _, _, _) (priority', _, _, _) -> priority == priority') srvResult' :: [[(Word16, Word16, Word16, Domain)]]+ -- For each sublist, put records with a weight of zero first.+ let srvResult''' = Data.List.map (\sublist -> let (a, b) = partition (\(_, weight, _, _) -> weight == 0) sublist in Data.List.concat [a, b]) srvResult''+ -- Order each sublist.+ srvResult'''' <- mapM orderSublist srvResult'''+ -- Concatinated the results.+ return $ Data.List.concat srvResult''''+ where+ orderSublist :: [(Word16, Word16, Word16, Domain)] -> IO [(Word16, Word16, Word16, Domain)]+ orderSublist [] = return []+ orderSublist sublist = do+ -- Compute the running sum, as well as the total sum of+ -- the sublist. Add the running sum to the SRV tuples.+ let (total, sublist') = Data.List.mapAccumL (\total' (priority, weight, port, domain) -> (total' + weight, (priority, weight, port, domain, total' + weight))) 0 sublist+ -- Choose a random number between 0 and the total sum+ -- (inclusive).+ randomNumber <- randomRIO (0, total)+ -- Select the first record with its running sum greater+ -- than or equal to the random number.+ let (beginning, ((priority, weight, port, domain, _):end)) = Data.List.break (\(_, _, _, _, running) -> randomNumber <= running) sublist'+ -- Remove the running total number from the remaining+ -- elements.+ let sublist'' = Data.List.map (\(priority', weight', port', domain', _) -> (priority', weight', port', domain')) (Data.List.concat [beginning, end])+ -- Repeat the ordering procedure on the remaining+ -- elements.+ rest <- orderSublist sublist''+ return $ ((priority, weight, port, domain):rest)++-- | Close the connection and updates the XmppConMonad Stream state. Does+-- not send the stream end tag.+killStream :: Stream -> IO (Either XmppFailure ())+killStream = withStream $ do+ cc <- gets (streamClose . streamHandle)+ err <- wrapIOException "killStream" cc+ -- (ExL.try cc :: IO (Either ExL.SomeException ()))+ put xmppNoStream{ streamConnectionState = Finished }+ 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 :: Text+ -> Maybe Jid+ -> IQRequestType+ -> Maybe LangTag+ -> Element+ -> Stream+ -> IO (Either XmppFailure (Either IQError IQResult))+pushIQ iqID to tp lang body stream = runExceptT $ do+ ExceptT $ pushStanza+ (IQRequestS $ IQRequest iqID Nothing to lang tp body []) stream+ res <- lift $ pullStanza stream+ case res of+ Left e -> throwError e+ Right (IQErrorS e) -> return $ Left e+ Right (IQResultS r) -> do+ unless+ (iqID == iqResultID r) $ liftIO $ do+ liftIO $ errorM "Pontarius.Xmpp" $ "pushIQ: ID mismatch (" ++ (show iqID) ++ " /= " ++ (show $ iqResultID r) ++ ")."+ liftIO $ ExL.throwIO XmppOtherFailure+ -- TODO: Log: ("In sendIQ' IDs don't match: " ++ show iqID +++ -- " /= " ++ show (iqResultID r) ++ " .")+ return $ Right r+ _ -> do+ liftIO $ errorM "Pontarius.Xmpp" $ "pushIQ: Unexpected stanza type."+ throwError XmppOtherFailure++debugConduit :: (Show o, MonadIO m) => ConduitM o o m b+debugConduit = forever $ do+ s' <- await+ case s' of+ Just s -> do+ liftIO $ debugM "Pontarius.Xmpp" $ "debugConduit: In: " ++ (show s)+ yield s+ Nothing -> return ()++elements :: MonadError XmppFailure m => ConduitT Event Element m ()+elements = do+ x <- await+ case x of+ Just (EventBeginElement n as) -> do+ goE n as >>= yield+ elements+ -- This might be an XML error if the end element tag is not+ -- "</stream>". TODO: We might want to check this at a later time+ Just EventEndElement{} -> throwError StreamEndFailure+ -- This happens when the connection to the server is closed without+ -- the stream being properly terminated+ Just EventEndDocument -> throwError StreamEndFailure+ Just (EventContent (ContentText ct)) | Text.all isSpace ct ->+ elements+ Nothing -> return ()+ _ -> throwError $ XmppInvalidXml $ "not an element: " ++ show x+ where+ many' f =+ go id+ where+ go front = do+ x <- f+ case x of+ Left l -> return $ (l, front [])+ Right r -> go (front . (:) r)+ goE n as = do+ (y, ns) <- many' goN+ if y == Just (EventEndElement n)+ then return $ Element n (map (id >< compressContents) as)+ (compressNodes ns)+ else throwError . XmppInvalidXml $ "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++ compressContents :: [Content] -> [Content]+ compressContents cs = [ContentText $ Text.concat (map unwrap cs)]+ where unwrap (ContentText t) = t+ unwrap (ContentEntity t) = t++ (><) f g (x, y) = (f x, g y)++withStream :: StateT StreamState IO a -> Stream -> IO a+withStream action (Stream stream) = Ex.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 StreamState IO a -> Stream -> IO a+withStream' action (Stream stream) = do+ stream_ <- atomically $ readTMVar stream+ (r, _) <- runStateT action stream_+ return r+++mkStream :: StreamState -> IO Stream+mkStream con = Stream `fmap` atomically (newTMVar con)++-- "Borrowed" from base-4.4 for compatibility with GHC 7.0.1.+tryIOError :: IO a -> IO (Either IOError a)+tryIOError f = Ex.catch (Right <$> f) (return . Left)
+ source/Network/Xmpp/Tls.hs view
@@ -0,0 +1,204 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE FlexibleContexts #-}++module Network.Xmpp.Tls where++import Control.Applicative ((<$>))+import qualified Control.Exception.Lifted as Ex+import Control.Monad+import Control.Monad.Except+import Control.Monad.State.Strict+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BSC8+import qualified Data.ByteString.Lazy as BL+import Data.Conduit+import Data.IORef+import Data.Monoid+import Data.XML.Types+import Network.DNS.Resolver (ResolvConf)+import Network.TLS+import Network.Xmpp.Stream+import Network.Xmpp.Types+import System.Log.Logger (debugM, errorM, infoM)+import System.X509++mkBackend :: StreamHandle -> Backend+mkBackend con = Backend { backendSend = \bs -> void (streamSend con bs)+ , backendRecv = bufferReceive (streamReceive con)+ , backendFlush = streamFlush con+ , backendClose = streamClose con+ }+ where+ bufferReceive _ 0 = return BS.empty+ bufferReceive recv n = BS.concat `liftM` (go n)+ where+ go m = do+ mbBs <- recv m+ bs <- case mbBs of+ Left e -> Ex.throwIO e+ Right r -> return r+ case BS.length bs of+ 0 -> return []+ l -> if l < m+ then (bs :) `liftM` go (m - l)+ else return [bs]++starttlsE :: Element+starttlsE = Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] []++-- | Checks for TLS support and run starttls procedure if applicable+tls :: Stream -> IO (Either XmppFailure ())+tls con = fmap join -- We can have Left values both from exceptions and the+ -- error monad. Join unifies them into one error layer+ . wrapExceptions+ . flip withStream con+ . runExceptT $ do+ conf <- gets streamConfiguration+ sState <- gets streamConnectionState+ case sState of+ Plain -> return ()+ Closed -> do+ liftIO $ errorM "Pontarius.Xmpp.Tls" "The stream is closed."+ throwError XmppNoStream+ Finished -> do+ liftIO $ errorM "Pontarius.Xmpp.Tls" "The stream is finished."+ throwError XmppNoStream+ Secured -> do+ liftIO $ errorM "Pontarius.Xmpp.Tls" "The stream is already secured."+ throwError TlsStreamSecured+ features <- lift $ gets streamFeatures+ case (tlsBehaviour conf, streamFeaturesTls features) of+ (RequireTls , Just _ ) -> startTls+ (RequireTls , Nothing ) -> throwError TlsNoServerSupport+ (PreferTls , Just _ ) -> startTls+ (PreferTls , Nothing ) -> skipTls+ (PreferPlain , Just True) -> startTls+ (PreferPlain , _ ) -> skipTls+ (RefuseTls , Just True) -> throwError XmppOtherFailure+ (RefuseTls , _ ) -> skipTls+ where+ skipTls = liftIO $ infoM "Pontarius.Xmpp.Tls" "Skipping TLS negotiation"+ startTls = do+ liftIO $ infoM "Pontarius.Xmpp.Tls" "Running StartTLS"+ params <- gets $ tlsParams . streamConfiguration+ ExceptT $ pushElement starttlsE+ answer <- lift $ pullElement+ case answer of+ Left e -> throwError e+ Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] []) ->+ return ()+ Right (Element "{urn:ietf:params:xml:ns:xmpp-tls}failure" _ _) -> do+ liftIO $ errorM "Pontarius.Xmpp" "startTls: TLS initiation failed."+ throwError XmppOtherFailure+ Right r ->+ liftIO $ errorM "Pontarius.Xmpp.Tls" $+ "Unexpected element: " ++ show r+ hand <- gets streamHandle+ (_raw, _snk, psh, recv, ctx) <- lift $ tlsinit params (mkBackend hand)+ let newHand = StreamHandle { streamSend = catchPush . psh+ , streamReceive = wrapExceptions . recv+ , streamFlush = contextFlush ctx+ , streamClose = bye ctx >> streamClose hand+ }+ lift $ modify ( \x -> x {streamHandle = newHand})+ liftIO $ infoM "Pontarius.Xmpp.Tls" "Stream Secured."+ either (lift . Ex.throwIO) return =<< lift restartStream+ modify (\s -> s{streamConnectionState = Secured})+ return ()++client :: MonadIO m => ClientParams -> Backend -> m Context+client params backend = contextNew backend params++tlsinit :: (MonadIO m, MonadIO m1) =>+ ClientParams+ -> Backend+ -> m ( ConduitT () BS.ByteString m1 ()+ , ConduitT BS.ByteString Void m1 ()+ , BS.ByteString -> IO ()+ , Int -> m1 BS.ByteString+ , Context+ )+tlsinit params backend = do+ liftIO $ debugM "Pontarius.Xmpp.Tls" "TLS with debug mode enabled."+ -- gen <- liftIO (cprgCreate <$> createEntropyPool :: IO SystemRNG)+ sysCStore <- liftIO getSystemCertificateStore+ let params' = params{clientShared =+ (clientShared params){ sharedCAStore =+ sysCStore <> sharedCAStore (clientShared params)}}+ con <- client params' backend+ handshake con+ let src = forever $ do+ dt <- liftIO $ recvData con+ liftIO $ debugM "Pontarius.Xmpp.Tls" ("In :" ++ BSC8.unpack dt)+ yield dt+ let snk = do+ d <- await+ case d of+ Nothing -> return ()+ Just x -> do+ sendData con (BL.fromChunks [x])+ snk+ readWithBuffer <- liftIO $ mkReadBuffer (recvData con)+ return ( src+ , snk+ -- Note: sendData already sends the data to the debug output+ , \s -> sendData con $ BL.fromChunks [s]+ , liftIO . readWithBuffer+ , con+ )++mkReadBuffer :: IO BS.ByteString -> IO (Int -> IO BS.ByteString)+mkReadBuffer recv = do+ buffer <- newIORef BS.empty+ let read' n = do+ nc <- readIORef buffer+ bs <- if BS.null nc then recv+ else return nc+ let (result, rest) = BS.splitAt n bs+ writeIORef buffer rest+ return result+ return read'++-- | Connect to an XMPP server and secure the connection with TLS before+-- starting the XMPP streams+--+-- /NB/ RFC 6120 does not specify this method, but some servers, notably GCS,+-- seem to use it.+connectTls :: ResolvConf -- ^ Resolv conf to use (try 'defaultResolvConf' as a+ -- default)+ -> ClientParams -- ^ TLS parameters to use when securing the connection+ -> String -- ^ Host to use when connecting (will be resolved+ -- using SRV records)+ -> ExceptT XmppFailure IO StreamHandle+connectTls config params host = do+ h <- connectSrv config host >>= \h' -> case h' of+ Nothing -> throwError TcpConnectionFailure+ Just h'' -> return h''+ let hand = handleToStreamHandle h+ let params' = params{clientServerIdentification+ = case clientServerIdentification params of+ ("", _) -> (host, "")+ csi -> csi+ }+ (_raw, _snk, psh, recv, ctx) <- tlsinit params' $ mkBackend hand+ return StreamHandle{ streamSend = catchPush . psh+ , streamReceive = wrapExceptions . recv+ , streamFlush = contextFlush ctx+ , streamClose = bye ctx >> streamClose hand+ }++wrapExceptions :: IO a -> IO (Either XmppFailure a)+wrapExceptions f = Ex.catches (liftM Right $ f)+ [ Ex.Handler $ return . Left . XmppIOException+#if !MIN_VERSION_tls(1,8,0)+ , Ex.Handler $ wrap . XmppTlsError+#endif+ , Ex.Handler $ wrap . XmppTlsException+ , Ex.Handler $ return . Left+ ]+ where+ wrap = return . Left . TlsError
+ source/Network/Xmpp/Types.hs view
@@ -0,0 +1,1311 @@+{-# LANGUAGE CPP #-}++#if WITH_TEMPLATE_HASKELL+{-# LANGUAGE TemplateHaskell #-}+#endif++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK hide #-}++module Network.Xmpp.Types+ ( NonemptyText(..)+ , nonEmpty+ , text+ , IQError(..)+ , IQRequest(..)+ , IQRequestType(..)+ , IQResponse(..)+ , IQResult(..)+ , LangTag (..)+#if WITH_TEMPLATE_HASKELL+ , langTagQ+#endif+ , langTagFromText+ , langTagToText+ , parseLangTag+ , ExtendedAttribute+ , Message(..)+ , message+ , MessageError(..)+ , messageError+ , MessageType(..)+ , Presence(..)+ , presence+ , PresenceError(..)+ , PresenceType(..)+ , SaslError(..)+ , SaslFailure(..)+ , StreamFeatures(..)+ , Stanza(..)+ , XmppElement(..)+ , messageS+ , messageErrorS+ , presenceS+ , StanzaError(..)+ , StanzaErrorCondition(..)+ , StanzaErrorType(..)+ , XmppFailure(..)+ , XmppTlsError(..)+ , StreamErrorCondition(..)+ , Version(..)+ , versionFromText+ , StreamHandle(..)+ , Stream(..)+ , StreamState(..)+ , ConnectionState(..)+ , StreamErrorInfo(..)+ , ConnectionDetails(..)+ , StreamConfiguration(..)+ , xmppDefaultParams+ , xmppDefaultParamsStrong+ , Jid(..)+#if WITH_TEMPLATE_HASKELL+ , jidQ+ , jid+#endif+ , isBare+ , isFull+ , jidFromText+ , jidFromTexts+ , (<~)+ , nodeprepProfile+ , resourceprepProfile+ , jidToText+ , jidToTexts+ , toBare+ , localpart+ , domainpart+ , resourcepart+ , parseJid+ , TlsBehaviour(..)+ , AuthFailure(..)+ ) where++import Control.Applicative ((<$>), (<|>), many)+import Control.Concurrent.STM+import Control.Exception+import Control.Monad+import Control.Monad.Except+import qualified Data.Attoparsec.Text as AP+import qualified Data.ByteString as BS+import Data.Char (isSpace)+import Data.Conduit+import Data.Default+import Data.Semigroup as Sem+import qualified Data.Set as Set+import Data.String (IsString, fromString)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Typeable(Typeable)+import Data.XML.Types as XML+import qualified Data.Text.Encoding as Text+import GHC.Generics+#if WITH_TEMPLATE_HASKELL+import Language.Haskell.TH+import Language.Haskell.TH.Quote+import qualified Language.Haskell.TH.Syntax as TH+#endif+import Network.Socket+import Network.DNS+import Network.TLS hiding (Version, HostName)+import Network.TLS.Extra+import qualified Text.StringPrep as SP+import qualified Text.StringPrep.Profiles as SP+++-- $setup+-- :set -itests+-- >>> :add tests/Tests/Arbitrary.hs+-- >>> import Network.Xmpp.Types+-- >>> import Control.Applicative((<$>))++-- | Type of Texts that contain at least on non-space character+newtype NonemptyText = Nonempty {fromNonempty :: Text}+ deriving (Show, Read, Eq, Ord)++instance IsString NonemptyText where+ fromString str = case nonEmpty (Text.pack str) of+ Nothing -> error $ "NonemptyText fromString called on empty or " +++ "all-whitespace string"+ Just r -> r++-- | Check that Text contains at least one non-space character and wrap it+nonEmpty :: Text -> Maybe NonemptyText+nonEmpty txt = if Text.all isSpace txt then Nothing else Just (Nonempty txt)++-- | Same as 'fromNonempty'+text :: NonemptyText -> Text+text (Nonempty txt) = txt++data XmppElement = XmppStanza !Stanza+ | XmppNonza !Element+ deriving (Eq, Show)++-- | The Xmpp communication primitives (Message, Presence and Info/Query) are+-- called stanzas.+data Stanza = IQRequestS !IQRequest+ | IQResultS !IQResult+ | IQErrorS !IQError+ | MessageS !Message+ | MessageErrorS !MessageError+ | PresenceS !Presence+ | PresenceErrorS !PresenceError+ deriving (Eq, Show, Generic)++type ExtendedAttribute = (XML.Name, Text)++-- | A "request" Info/Query (IQ) stanza is one with either "get" or "set" as+-- type. It always contains an xml payload.+data IQRequest = IQRequest { iqRequestID :: !Text+ , iqRequestFrom :: !(Maybe Jid)+ , iqRequestTo :: !(Maybe Jid)+ , iqRequestLangTag :: !(Maybe LangTag)+ , iqRequestType :: !IQRequestType+ , iqRequestPayload :: !Element+ , iqRequestAttributes :: ![ExtendedAttribute]+ } deriving (Eq, Show, Generic)++-- | The type of IQ request that is made.+data IQRequestType = Get | Set deriving (Eq, Ord, Read, Show, Generic)++-- | A "response" Info/Query (IQ) stanza is either an 'IQError', an IQ stanza+-- of type "result" ('IQResult')+data IQResponse = IQResponseError IQError+ | IQResponseResult IQResult+ deriving (Eq, Show)++-- | The (non-error) answer to an IQ request.+data IQResult = IQResult { iqResultID :: !Text+ , iqResultFrom :: !(Maybe Jid)+ , iqResultTo :: !(Maybe Jid)+ , iqResultLangTag :: !(Maybe LangTag)+ , iqResultPayload :: !(Maybe Element)+ , iqResultAttributes :: ![ExtendedAttribute]+ } deriving (Eq, Show, Generic)++-- | The answer to an IQ request that generated an error.+data IQError = IQError { iqErrorID :: !Text+ , iqErrorFrom :: !(Maybe Jid)+ , iqErrorTo :: !(Maybe Jid)+ , iqErrorLangTag :: !(Maybe LangTag)+ , iqErrorStanzaError :: !StanzaError+ , iqErrorPayload :: !(Maybe Element) -- should this be []?+ , iqErrorAttributes :: ![ExtendedAttribute]+ } deriving (Eq, Show, Generic)++-- | The message stanza. Used for /push/ type communication.+data Message = Message { messageID :: !(Maybe Text)+ , messageFrom :: !(Maybe Jid)+ , messageTo :: !(Maybe Jid)+ , messageLangTag :: !(Maybe LangTag)+ , messageType :: !MessageType+ , messagePayload :: ![Element]+ , messageAttributes :: ![ExtendedAttribute]+ } deriving (Eq, Show, Generic)++-- | An empty message+--+-- @+-- message = Message { messageID = Nothing+-- , messageFrom = Nothing+-- , messageTo = Nothing+-- , messageLangTag = Nothing+-- , messageType = Normal+-- , messagePayload = []+-- }+-- @+message :: Message+message = Message { messageID = Nothing+ , messageFrom = Nothing+ , messageTo = Nothing+ , messageLangTag = Nothing+ , messageType = Normal+ , messagePayload = []+ , messageAttributes = []+ }++-- | Empty message stanza+--+-- @messageS = 'MessageS' 'message'@+messageS :: Stanza+messageS = MessageS message++instance Default Message where+ def = message++-- | An error stanza generated in response to a 'Message'.+data MessageError = MessageError { messageErrorID :: !(Maybe Text)+ , messageErrorFrom :: !(Maybe Jid)+ , messageErrorTo :: !(Maybe Jid)+ , messageErrorLangTag :: !(Maybe LangTag)+ , messageErrorStanzaError :: !StanzaError+ , messageErrorPayload :: ![Element]+ , messageErrorAttributes :: ![ExtendedAttribute]+ } deriving (Eq, Show, Generic)++messageError :: MessageError+messageError = MessageError { messageErrorID = Nothing+ , messageErrorFrom = Nothing+ , messageErrorTo = Nothing+ , messageErrorLangTag = Nothing+ , messageErrorStanzaError =+ StanzaError { stanzaErrorType = Cancel+ , stanzaErrorCondition =+ ServiceUnavailable+ , stanzaErrorText = Nothing+ , stanzaErrorApplicationSpecificCondition = Nothing+ }+ , messageErrorPayload = []+ , messageErrorAttributes = []+ }++instance Default MessageError where+ def = messageError++messageErrorS :: Stanza+messageErrorS = MessageErrorS def++-- | The type of a Message being sent+-- (<http://xmpp.org/rfcs/rfc6121.html#message-syntax-type>)+data MessageType = -- | The message is sent in the context of a one-to-one chat+ -- session. Typically an interactive client will present a+ -- message of type /chat/ in an interface that enables+ -- one-to-one chat between the two parties, including an+ -- appropriate conversation history.+ Chat+ -- | The message is sent in the context of a multi-user chat+ -- environment (similar to that of @IRC@). Typically a+ -- receiving client will present a message of type+ -- /groupchat/ in an interface that enables many-to-many+ -- chat between the parties, including a roster of parties+ -- in the chatroom and an appropriate conversation history.+ | GroupChat+ -- | The message provides an alert, a notification, or other+ -- transient information to which no reply is expected+ -- (e.g., news headlines, sports updates, near-real-time+ -- market data, or syndicated content). Because no reply to+ -- the message is expected, typically a receiving client+ -- will present a message of type /headline/ in an interface+ -- that appropriately differentiates the message from+ -- standalone messages, chat messages, and groupchat+ -- messages (e.g., by not providing the recipient with the+ -- ability to reply).+ | Headline+ -- | The message is a standalone message that is sent outside+ -- the context of a one-to-one conversation or groupchat, and+ -- to which it is expected that the recipient will reply.+ -- Typically a receiving client will present a message of+ -- type /normal/ in an interface that enables the recipient+ -- to reply, but without a conversation history.+ --+ -- This is the /default/ value.+ | Normal+ deriving (Eq, Read, Show, Generic)++-- | The presence stanza. Used for communicating status updates.+data Presence = Presence { presenceID :: !(Maybe Text)+ , presenceFrom :: !(Maybe Jid)+ , presenceTo :: !(Maybe Jid)+ , presenceLangTag :: !(Maybe LangTag)+ , presenceType :: !PresenceType+ , presencePayload :: ![Element]+ , presenceAttributes :: ![ExtendedAttribute]+ } deriving (Eq, Show, Generic)++-- | An empty presence.+presence :: Presence+presence = Presence { presenceID = Nothing+ , presenceFrom = Nothing+ , presenceTo = Nothing+ , presenceLangTag = Nothing+ , presenceType = Available+ , presencePayload = []+ , presenceAttributes = []+ }++-- | Empty presence stanza+presenceS :: Stanza+presenceS = PresenceS presence++instance Default Presence where+ def = presence++-- | An error stanza generated in response to a 'Presence'.+data PresenceError = PresenceError { presenceErrorID :: !(Maybe Text)+ , presenceErrorFrom :: !(Maybe Jid)+ , presenceErrorTo :: !(Maybe Jid)+ , presenceErrorLangTag :: !(Maybe LangTag)+ , presenceErrorStanzaError :: !StanzaError+ , presenceErrorPayload :: ![Element]+ , presenceErrorAttributes :: ![ExtendedAttribute]+ } deriving (Eq, Show, Generic)++-- | @PresenceType@ holds Xmpp presence types. The "error" message type is left+-- out as errors are using @PresenceError@.+data PresenceType = Subscribe | -- ^ Sender wants to subscribe to presence+ Subscribed | -- ^ Sender has approved the subscription+ Unsubscribe | -- ^ Sender is unsubscribing from presence+ Unsubscribed | -- ^ Sender has denied or cancelled a+ -- subscription+ Probe | -- ^ Sender requests current presence;+ -- should only be used by servers+ Available | -- ^ Sender wants to express availability+ -- (no type attribute is defined)+ Unavailable deriving (Eq, Read, Show, Generic)++-- | All stanzas (IQ, message, presence) can cause errors, which in the Xmpp+-- stream looks like @\<stanza-kind to=\'sender\' type=\'error\'\>@ . These+-- errors are wrapped in the @StanzaError@ type. TODO: Sender XML is (optional+-- and is) not yet included.+data StanzaError = StanzaError+ { stanzaErrorType :: StanzaErrorType+ , stanzaErrorCondition :: StanzaErrorCondition+ , stanzaErrorText :: Maybe (Maybe LangTag, NonemptyText)+ , stanzaErrorApplicationSpecificCondition :: Maybe Element+ } deriving (Eq, Show, Generic)++-- | @StanzaError@s always have one of these types.+data StanzaErrorType = Cancel | -- ^ Error is unrecoverable - do not retry+ Continue | -- ^ Conditition was a warning - proceed+ Modify | -- ^ Change the data and retry+ Auth | -- ^ Provide credentials and retry+ Wait -- ^ Error is temporary - wait and retry+ deriving (Eq, Read, Show, Generic)++-- | Stanza errors are accommodated with one of the error conditions listed+-- below.+data StanzaErrorCondition = BadRequest -- ^ Malformed XML.+ | Conflict -- ^ Resource or session with+ -- name already exists.+ | FeatureNotImplemented+ | Forbidden -- ^ Insufficient permissions.+ | Gone (Maybe NonemptyText) -- ^ Entity can no longer+ -- be contacted at this+ -- address.+ | InternalServerError+ | ItemNotFound+ | JidMalformed+ | NotAcceptable -- ^ Does not meet policy+ -- criteria.+ | NotAllowed -- ^ No entity may perform+ -- this action.+ | NotAuthorized -- ^ Must provide proper+ -- credentials.+ | PolicyViolation -- ^ The entity has violated+ -- some local service policy+ -- (e.g., a message contains+ -- words that are prohibited+ -- by the service)+ | RecipientUnavailable -- ^ Temporarily unavailable.+ | Redirect (Maybe NonemptyText) -- ^ Redirecting to+ -- other entity,+ -- usually+ -- temporarily.+ | RegistrationRequired+ | RemoteServerNotFound+ | RemoteServerTimeout+ | ResourceConstraint -- ^ Entity lacks the+ -- necessary system+ -- resources.+ | ServiceUnavailable+ | SubscriptionRequired+ | UndefinedCondition -- ^ Application-specific+ -- condition.+ | UnexpectedRequest -- ^ Badly timed request.+ deriving (Eq, Read, Show, Generic)++-- =============================================================================+-- OTHER STUFF+-- =============================================================================++data SaslFailure = SaslFailure { saslFailureCondition :: SaslError+ , saslFailureText :: Maybe ( Maybe LangTag+ , Text+ )+ } deriving (Eq, Show, Generic)++data SaslError = SaslAborted -- ^ Client aborted.+ | SaslAccountDisabled -- ^ The account has been temporarily+ -- disabled.+ | SaslCredentialsExpired -- ^ The authentication failed because+ -- the credentials have expired.+ | SaslEncryptionRequired -- ^ The mechanism requested cannot be+ -- used the confidentiality and+ -- integrity of the underlying+ -- stream is protected (typically+ -- with TLS).+ | SaslIncorrectEncoding -- ^ The base64 encoding is incorrect.+ | SaslInvalidAuthzid -- ^ The authzid has an incorrect+ -- format or the initiating entity+ -- does not have the appropriate+ -- permissions to authorize that ID.+ | SaslInvalidMechanism -- ^ The mechanism is not supported by+ -- the receiving entity.+ | SaslMalformedRequest -- ^ Invalid syntax.+ | SaslMechanismTooWeak -- ^ The receiving entity policy+ -- requires a stronger mechanism.+ | SaslNotAuthorized -- ^ Invalid credentials provided, or+ -- some generic authentication+ -- failure has occurred.+ | SaslTemporaryAuthFailure -- ^ There receiving entity reported a+ -- temporary error condition; the+ -- initiating entity is recommended+ -- to try again later.+ deriving (Eq, Read, Show, Generic)++-- The documentation of StreamErrorConditions is copied from+-- http://xmpp.org/rfcs/rfc6120.html#streams-error-conditions+data StreamErrorCondition+ = StreamBadFormat -- ^ The entity has sent XML that cannot be processed.+ | StreamBadNamespacePrefix -- ^ The entity has sent a namespace prefix that+ -- is unsupported, or has sent no namespace+ -- prefix on an element that needs such a prefix+ | StreamConflict -- ^ The server either (1) is closing the existing stream+ -- for this entity because a new stream has been initiated+ -- that conflicts with the existing stream, or (2) is+ -- refusing a new stream for this entity because allowing+ -- the new stream would conflict with an existing stream+ -- (e.g., because the server allows only a certain number+ -- of connections from the same IP address or allows only+ -- one server-to-server stream for a given domain pair as a+ -- way of helping to ensure in-order processing+ | StreamConnectionTimeout -- ^ One party is closing the stream because it+ -- has reason to believe that the other party has+ -- permanently lost the ability to communicate+ -- over the stream.+ | StreamHostGone -- ^ The value of the 'to' attribute provided in the+ -- initial stream header corresponds to an FQDN that is no+ -- longer serviced by the receiving entity+ | StreamHostUnknown -- ^ The value of the 'to' attribute provided in the+ -- initial stream header does not correspond to an FQDN+ -- that is serviced by the receiving entity.+ | StreamImproperAddressing -- ^ A stanza sent between two servers lacks a+ -- 'to' or 'from' attribute, the 'from' or 'to'+ -- attribute has no value, or the value violates+ -- the rules for XMPP addresses+ | StreamInternalServerError -- ^ The server has experienced a+ -- misconfiguration or other internal error that+ -- prevents it from servicing the stream.+ | StreamInvalidFrom -- ^ The data provided in a 'from' attribute does not+ -- match an authorized JID or validated domain as+ -- negotiated (1) between two servers using SASL or+ -- Server Dialback, or (2) between a client and a server+ -- via SASL authentication and resource binding.+ | StreamInvalidNamespace -- ^ The stream namespace name is something other+ -- than \"http://etherx.jabber.org/streams\" (see+ -- Section 11.2) or the content namespace declared+ -- as the default namespace is not supported (e.g.,+ -- something other than \"jabber:client\" or+ -- \"jabber:server\").+ | StreamInvalidXml -- ^ The entity has sent invalid XML over the stream to a+ -- server that performs validation+ | StreamNotAuthorized -- ^ The entity has attempted to send XML stanzas or+ -- other outbound data before the stream has been+ -- authenticated, or otherwise is not authorized to+ -- perform an action related to stream negotiation;+ -- the receiving entity MUST NOT process the offending+ -- data before sending the stream error.+ | StreamNotWellFormed -- ^ The initiating entity has sent XML that violates+ -- the well-formedness rules of [XML] or [XML‑NAMES].+ | StreamPolicyViolation -- ^ The entity has violated some local service+ -- policy (e.g., a stanza exceeds a configured size+ -- limit); the server MAY choose to specify the+ -- policy in the \<text/\> element or in an+ -- application-specific condition element.+ | StreamRemoteConnectionFailed -- ^ The server is unable to properly connect+ -- to a remote entity that is needed for+ -- authentication or authorization (e.g., in+ -- certain scenarios related to Server+ -- Dialback [XEP‑0220]); this condition is+ -- not to be used when the cause of the error+ -- is within the administrative domain of the+ -- XMPP service provider, in which case the+ -- \<internal-server-error /\> condition is+ -- more appropriate.+ | StreamReset -- ^ The server is closing the stream because it has new+ -- (typically security-critical) features to offer, because+ -- the keys or certificates used to establish a secure context+ -- for the stream have expired or have been revoked during the+ -- life of the stream , because the TLS sequence number has+ -- wrapped, etc. The reset applies to the stream and to any+ -- security context established for that stream (e.g., via TLS+ -- and SASL), which means that encryption and authentication+ -- need to be negotiated again for the new stream (e.g., TLS+ -- session resumption cannot be used)+ | StreamResourceConstraint -- ^ The server lacks the system resources+ -- necessary to service the stream.+ | StreamRestrictedXml -- ^ he entity has attempted to send restricted XML+ -- features such as a comment, processing instruction,+ -- DTD subset, or XML entity reference+ | StreamSeeOtherHost -- ^ The server will not provide service to the+ -- initiating entity but is redirecting traffic to+ -- another host under the administrative control of the+ -- same service provider.+ | StreamSystemShutdown -- ^ The server is being shut down and all active+ -- streams are being closed.+ | StreamUndefinedCondition -- ^ The error condition is not one of those+ -- defined by the other conditions in this list+ | StreamUnsupportedEncoding -- ^ The initiating entity has encoded the+ -- stream in an encoding that is not supported+ -- by the server or has otherwise improperly+ -- encoded the stream (e.g., by violating the+ -- rules of the [UTF‑8] encoding).+ | StreamUnsupportedFeature -- ^ The receiving entity has advertised a+ -- mandatory-to-negotiate stream feature that the+ -- initiating entity does not support, and has+ -- offered no other mandatory-to-negotiate+ -- feature alongside the unsupported feature.+ | StreamUnsupportedStanzaType -- ^ The initiating entity has sent a+ -- first-level child of the stream that is not+ -- supported by the server, either because the+ -- receiving entity does not understand the+ -- namespace or because the receiving entity+ -- does not understand the element name for+ -- the applicable namespace (which might be+ -- the content namespace declared as the+ -- default namespace)+ | StreamUnsupportedVersion -- ^ The 'version' attribute provided by the+ -- initiating entity in the stream header+ -- specifies a version of XMPP that is not+ -- supported by the server.+ deriving (Eq, Read, Show, Generic)++-- | Encapsulates information about an XMPP stream error.+data StreamErrorInfo = StreamErrorInfo+ { errorCondition :: !StreamErrorCondition+ , errorText :: !(Maybe (Maybe LangTag, NonemptyText))+ , errorXml :: !(Maybe Element)+ } deriving (Show, Eq, Generic)++data XmppTlsError = XmppTlsError TLSError+ | XmppTlsException TLSException+ deriving (Show, Eq, Typeable, Generic)++-- | Signals an XMPP stream error or another unpredicted stream-related+-- situation. This error is fatal, and closes the XMPP stream.+data XmppFailure = StreamErrorFailure StreamErrorInfo -- ^ An error XML stream+ -- element has been+ -- encountered.+ | StreamEndFailure -- ^ The stream has been closed.+ -- This exception is caught by the+ -- concurrent implementation, and+ -- will thus not be visible+ -- through use of 'Session'.+ | StreamCloseError ([Element], XmppFailure) -- ^ When an XmppFailure+ -- is encountered in+ -- closeStreams, this+ -- constructor wraps the+ -- elements collected so+ -- far.+ | TcpConnectionFailure -- ^ All attempts to TCP+ -- connect to the server+ -- failed.+ | XmppIllegalTcpDetails -- ^ The TCP details provided did not+ -- validate.+ | TlsError XmppTlsError -- ^ An error occurred in the+ -- TLS layer+ | TlsNoServerSupport -- ^ The server does not support+ -- the use of TLS+ | XmppNoStream -- ^ An action that required an active+ -- stream were performed when the+ -- 'StreamState' was 'Closed'+ | XmppAuthFailure AuthFailure -- ^ Authentication with the+ -- server failed (unrecoverably)+ | TlsStreamSecured -- ^ Connection already secured+ | XmppOtherFailure -- ^ Undefined condition. More+ -- information should be available in+ -- the log.+ | XmppIOException IOException -- ^ An 'IOException'+ -- occurred+ | XmppInvalidXml String -- ^ Received data is not valid XML+ deriving (Show, Eq, Typeable, Generic)++instance Exception XmppFailure++-- | Signals a SASL authentication error condition.+data AuthFailure = -- | No mechanism offered by the server was matched+ -- by the provided acceptable mechanisms; wraps the+ -- mechanisms offered by the server+ AuthNoAcceptableMechanism [Text.Text]+ | AuthStreamFailure XmppFailure -- TODO: Remove+ -- | A SASL failure element was encountered+ | AuthSaslFailure SaslFailure+ -- | The credentials provided did not conform to+ -- the SASLprep Stringprep profile+ | AuthIllegalCredentials+ -- | Other failure; more information is available+ -- in the log+ | AuthOtherFailure+ deriving (Eq, Show, Generic)++-- =============================================================================+-- XML TYPES+-- =============================================================================++-- | XMPP version number. Displayed as "\<major\>.\<minor\>". 2.4 is lesser than+-- 2.13, which in turn is lesser than 12.3.++data Version = Version { majorVersion :: !Integer+ , minorVersion :: !Integer } deriving (Eq, Read, Show, Generic)++-- If the major version numbers are not equal, compare them. Otherwise, compare+-- the minor version numbers.+instance Ord Version where+ compare (Version amajor aminor) (Version bmajor bminor)+ | amajor /= bmajor = compare amajor bmajor+ | otherwise = compare aminor bminor++-- instance Read Version where+-- readsPrec _ txt = (,"") <$> maybeToList (versionFromText $ Text.pack txt)++-- instance Show Version where+-- show (Version major minor) = (show major) ++ "." ++ (show minor)++-- Converts a "<major>.<minor>" numeric version number to a @Version@ object.+versionFromText :: Text.Text -> Maybe Version+versionFromText s = case AP.parseOnly versionParser s of+ Right version -> Just version+ Left _ -> Nothing++-- Read numbers, a dot, more numbers, and end-of-file.+versionParser :: AP.Parser Version+versionParser = do+ major <- AP.many1 AP.digit+ AP.skip (== '.')+ minor <- AP.many1 AP.digit+ AP.endOfInput+ return $ Version (read major) (read minor)++-- | The language tag in accordance with RFC 5646 (in the form of "en-US"). It+-- has a primary tag and a number of subtags. Two language tags are considered+-- equal if and only if they contain the same tags (case-insensitive).+data LangTag = LangTag { primaryTag :: !Text+ , subtags :: ![Text] }++-- Equals for language tags is not case-sensitive.+instance Eq LangTag where+ LangTag p s == LangTag q t = Text.toLower p == Text.toLower q &&+ map Text.toLower s == map Text.toLower t++-- | Parses, validates, and possibly constructs a "LangTag" object.+langTagFromText :: Text.Text -> Maybe LangTag+langTagFromText s = case AP.parseOnly langTagParser s of+ Right tag -> Just tag+ Left _ -> Nothing++langTagToText :: LangTag -> Text.Text+langTagToText (LangTag p []) = p+langTagToText (LangTag p s) = Text.concat $ [p, "-", Text.intercalate "-" s]++-- Parses a language tag as defined by RFC 1766 and constructs a LangTag object.+langTagParser :: AP.Parser LangTag+langTagParser = do+ -- Read until we reach a '-' character, or EOF. This is the `primary tag'.+ primTag <- tag+ -- Read zero or more subtags.+ subTags <- many subtag+ AP.endOfInput+ return $ LangTag primTag subTags+ where+ tag :: AP.Parser Text.Text+ tag = do+ t <- AP.takeWhile1 $ AP.inClass tagChars+ return t+ subtag :: AP.Parser Text.Text+ subtag = do+ AP.skip (== '-')+ tag+ tagChars :: [Char]+ tagChars = ['a'..'z'] ++ ['A'..'Z']++data StreamFeatures = StreamFeatures+ { streamFeaturesTls :: !(Maybe Bool)+ , streamFeaturesMechanisms :: ![Text.Text]+ , streamFeaturesRosterVer :: !(Maybe Bool)+ -- ^ @Nothing@ for no roster versioning, @Just False@ for roster+ -- versioning and @Just True@ when the server sends the non-standard+ -- "optional" element (observed with prosody).+ , streamFeaturesPreApproval :: !Bool -- ^ Does the server support pre-approval+ , streamFeaturesSession :: !(Maybe Bool)+ -- ^ Does this server allow the stream elelemt? (See+ -- https://tools.ietf.org/html/draft-cridland-xmpp-session-01)+ , streamFeaturesOther :: ![Element]+ -- TODO: All feature elements instead?+ } deriving (Eq, Show)++instance Sem.Semigroup StreamFeatures where+ sf1 <> sf2 =+ StreamFeatures+ { streamFeaturesTls = mplusOn streamFeaturesTls+ , streamFeaturesMechanisms = mplusOn streamFeaturesMechanisms+ , streamFeaturesRosterVer = mplusOn streamFeaturesRosterVer+ , streamFeaturesPreApproval =+ streamFeaturesPreApproval sf1+ || streamFeaturesPreApproval sf2+ , streamFeaturesSession = mplusOn streamFeaturesSession+ , streamFeaturesOther = mplusOn streamFeaturesOther++ }+ where+ mplusOn f = f sf1 `mplus` f sf2++instance Monoid StreamFeatures where+ mempty = StreamFeatures+ { streamFeaturesTls = Nothing+ , streamFeaturesMechanisms = []+ , streamFeaturesRosterVer = Nothing+ , streamFeaturesPreApproval = False+ , streamFeaturesSession = Nothing+ , streamFeaturesOther = []+ }+ mappend = (<>)++-- | Signals the state of the stream connection.+data ConnectionState+ = Closed -- ^ Stream has not been established yet+ | Plain -- ^ Stream established, but not secured via TLS+ | Secured -- ^ Stream established and secured via TLS+ | Finished -- ^ Stream was closed+ deriving (Show, Eq, Typeable, Generic)++-- | Defines operations for sending, receiving, flushing, and closing on a+-- stream.+data StreamHandle =+ StreamHandle { streamSend :: BS.ByteString+ -> IO (Either XmppFailure ()) -- ^ Sends may not+ -- interleave+ , streamReceive :: Int -> IO (Either XmppFailure 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 StreamState = StreamState+ { -- | State of the stream - 'Closed', 'Plain', or 'Secured'+ streamConnectionState :: !ConnectionState+ -- | Functions to send, receive, flush, and close the stream+ , streamHandle :: StreamHandle+ -- | Event conduit source, and its associated finalizer+ , streamEventSource :: ConduitT () Event (ExceptT XmppFailure IO) ()+ -- | Stream features advertised by the server+ , streamFeatures :: !StreamFeatures -- TODO: Maybe?+ -- | The hostname or IP specified for the connection+ , streamAddress :: !(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)+ -- | Configuration settings for the stream+ , streamConfiguration :: StreamConfiguration+ }++newtype Stream = Stream { unStream :: TMVar StreamState }++---------------+-- 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: localpart, domainpart, and resourcepart.+--+-- 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@).+--+-- 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).+--+-- 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@).+--+-- For more details see RFC 6122 <http://xmpp.org/rfcs/rfc6122.html>++data Jid = Jid { localpart_ :: !(Maybe NonemptyText)+ , domainpart_ :: !NonemptyText+ , resourcepart_ :: !(Maybe NonemptyText)+ } deriving (Eq, Ord)++-- | Converts a JID to a Text.+jidToText :: Jid -> Text+jidToText (Jid nd dmn res) = Text.concat . concat $+ [ maybe [] (:["@"]) (text <$> nd)+ , [text dmn]+ , maybe [] (\r -> ["/",r]) (text <$> res)+ ]++-- | Converts a JID to up to three Text values: (the optional) localpart, the+-- domainpart, and (the optional) resourcepart.+--+-- >>> jidToTexts [jid|foo@bar/quux|]+-- (Just "foo","bar",Just "quux")+--+-- >>> jidToTexts [jid|bar/quux|]+-- (Nothing,"bar",Just "quux")+--+-- >>> jidToTexts [jid|foo@bar|]+-- (Just "foo","bar",Nothing)+--+-- prop> jidToTexts j == (localpart j, domainpart j, resourcepart j)+jidToTexts :: Jid -> (Maybe Text, Text, Maybe Text)+jidToTexts (Jid nd dmn res) = (text <$> nd, text dmn, text <$> res)++-- Produces a Jid value in the format "parseJid \"<jid>\"".+instance Show Jid where+ show j = "parseJid " ++ show (jidToText j)++-- The string must be in the format "parseJid \"<jid>\"".+-- TODO: This function should produce its error values in a uniform way.+-- TODO: Do we need to care about precedence here?+instance Read Jid where+ readsPrec _ s = do+ -- Verifies that the first word is "parseJid", parses the second word and+ -- the remainder, if any, and produces these two values or fails.+ case lex s of+ [("parseJid", r')] ->+ case lex r' of+ [(s', r'')] ->+ case (reads s') of+ ((jidTxt,_):_) ->+ case jidFromText (Text.pack jidTxt) of+ Nothing -> []+ Just jid' -> [(jid', r'')]+ _ -> []+ _ -> []+ _ -> []+++#if WITH_TEMPLATE_HASKELL++instance TH.Lift Jid where+ lift (Jid lp dp rp) = [| Jid $(mbTextE $ text <$> lp)+ $(textE $ text dp)+ $(mbTextE $ text <$> rp)+ |]+ where+ textE t = [| Nonempty $ Text.pack $(stringE $ Text.unpack t) |]+ mbTextE Nothing = [| Nothing |]+ mbTextE (Just s) = [| Just $(textE s) |]++-- | Constructs and validates a @Jid@ at compile time.+--+-- Syntax:+-- @+-- [jid|localpart\@domainpart/resourcepart|]+-- @+--+-- >>> [jid|foo@bar/quux|]+-- parseJid "foo@bar/quux"+--+-- >>> Just [jid|foo@bar/quux|] == jidFromTexts (Just "foo") "bar" (Just "quux")+-- True+--+-- >>> Just [jid|foo@bar/quux|] == jidFromText "foo@bar/quux"+-- True+--+-- See also 'jidFromText'+jid :: QuasiQuoter+jid = QuasiQuoter { quoteExp = \s -> do+ when (head s == ' ') . fail $ "Leading whitespaces in JID" ++ show s+ let t = Text.pack s+ when (Text.last t == ' ') . reportWarning $ "Trailing whitespace in JID " ++ show s+ case jidFromText t of+ Nothing -> fail $ "Could not parse JID " ++ s+ Just j -> TH.lift j+ , quotePat = error "Jid patterns aren't implemented"+ , quoteType = error "jid QQ can't be used in type context"+ , quoteDec = error "jid QQ can't be used in declaration context"+ }++-- | Synonym for 'jid'+jidQ :: QuasiQuoter+jidQ = jidQ+#endif++-- | The partial order of "definiteness". JID1 is less than or equal JID2 iff+-- the domain parts are equal and JID1's local part and resource part each are+-- either Nothing or equal to Jid2's+(<~) :: Jid -> Jid -> Bool+(Jid lp1 dp1 rp1) <~ (Jid lp2 dp2 rp2) =+ dp1 == dp2 &&+ lp1 ~<~ lp2 &&+ rp1 ~<~ rp2+ where+ Nothing ~<~ _ = True+ Just x ~<~ Just y = x == y+ _ ~<~ _ = False++-- Produces a LangTag value in the format "parseLangTag \"<jid>\"".+instance Show LangTag where+ show l = "parseLangTag " ++ show (langTagToText l)++-- The string must be in the format "parseLangTag \"<LangTag>\"". This is based+-- on parseJid, and suffers the same problems.+instance Read LangTag where+ readsPrec _ s = do+ let (s', r) = case lex s of+ [] -> error "Expected `parseLangTag \"<LangTag>\"'"+ [("parseLangTag", r')] -> case lex r' of+ [] -> error "Expected `parseLangTag \"<LangTag>\"'"+ [(s'', r'')] -> (s'', r'')+ _ -> error "Expected `parseLangTag \"<LangTag>\"'"+ _ -> error "Expected `parseLangTag \"<LangTag>\"'"+ [(parseLangTag (read s' :: String), r)]++parseLangTag :: String -> LangTag+parseLangTag s = case langTagFromText $ Text.pack s of+ Just l -> l+ Nothing -> error $ "Language tag value (" ++ s ++ ") did not validate"++#if WITH_TEMPLATE_HASKELL+langTagQ :: QuasiQuoter+langTagQ = QuasiQuoter {quoteExp = \s -> case langTagFromText $ Text.pack s of+ Nothing -> fail $ "Not a valid language tag: "+ ++ s+ Just lt -> [|LangTag $(textE $ primaryTag lt)+ $(listE $+ map textE (subtags lt))+ |]++ , quotePat = error $ "LanguageTag patterns aren't"+ ++ " implemented"+ , quoteType = error $ "LanguageTag QQ can't be used"+ ++ " in type context"+ , quoteDec = error $ "LanguageTag QQ can't be used"+ ++ " in declaration context"++ }+ where+ textE t = [| Text.pack $(stringE $ Text.unpack t) |]+#endif+-- | Parses a JID string.+--+-- Note: This function is only meant to be used to reverse @Jid@ Show+-- operations; it will produce an 'undefined' value if the JID does not+-- validate; please refer to @jidFromText@ for a safe equivalent.+parseJid :: String -> Jid+parseJid s = case jidFromText $ Text.pack s of+ Just j -> j+ Nothing -> error $ "Jid value (" ++ s ++ ") did not validate"++-- | Parse a JID+--+-- >>> localpart <$> jidFromText "foo@bar/quux"+-- Just (Just "foo")+--+-- >>> domainpart <$> jidFromText "foo@bar/quux"+-- Just "bar"+--+-- >>> resourcepart <$> jidFromText "foo@bar/quux"+-- Just (Just "quux")+--+-- @ and / can occur in the domain part+--+-- >>> jidFromText "foo/bar@quux/foo"+-- Just parseJid "foo/bar@quux/foo"+--+-- * Counterexamples+--+-- A JID must only have one \'\@\':+--+-- >>> jidFromText "foo@bar@quux"+-- Nothing+--+-- The domain part can\'t be empty:+--+-- >>> jidFromText "foo@/quux"+-- Nothing+--+-- Both the local part and the resource part can be omitted (but the+-- \'\@\' and \'\/\', must also be removed):+--+-- >>> jidToTexts <$> jidFromText "bar"+-- Just (Nothing,"bar",Nothing)+--+-- >>> jidToTexts <$> jidFromText "@bar"+-- Nothing+--+-- >>> jidToTexts <$> jidFromText "bar/"+-- Nothing+--+jidFromText :: Text -> Maybe Jid+jidFromText t = do+ (l, d, r) <- eitherToMaybe $ AP.parseOnly jidParts t+ jidFromTexts l d r+ where+ eitherToMaybe = either (const Nothing) Just++-- | Convert localpart, domainpart, and resourcepart to a JID. Runs the+-- appropriate stringprep profiles and validates the parts.+--+-- >>> jidFromTexts (Just "foo") "bar" (Just "baz") == jidFromText "foo@bar/baz"+-- True+--+-- prop> \j -> jidFromTexts (localpart j) (domainpart j) (resourcepart j) == Just j+jidFromTexts :: Maybe Text -> Text -> Maybe Text -> Maybe Jid+jidFromTexts 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''+ l''' <- nonEmpty l''+ return $ Just l'''+ -- strip dots again to handle stuff like "⒐" until we are rfc7622+ domainPart' <- forbidSeparators . stripSuffix =<< SP.runStringPrep (SP.namePrepProfile False) (stripSuffix d)+ guard $ validDomainPart domainPart'+ guard $ validPartLength domainPart'+ domainPart <- nonEmpty domainPart'+ resourcePart <- case r of+ Nothing -> return Nothing+ Just r' -> do+ r'' <- SP.runStringPrep resourceprepProfile r'+ guard $ validPartLength r''+ r''' <- nonEmpty r''+ return $ Just r'''+ return $ Jid localPart domainPart resourcePart+ where+ validDomainPart :: Text -> Bool+ validDomainPart s = not $ Text.null s -- TODO: implement more stringent+ -- checks++ validPartLength :: Text -> Bool+ validPartLength p = Text.length p > 0+ && BS.length (Text.encodeUtf8 p) < 1024+ -- RFC6122 §2.2; we strip ALL the dots to avoid looking for a fixed point and bailing out early+ stripSuffix = Text.dropWhileEnd (== '.')+ -- "/" might be a valid JID, but stringprep messes it up, so we use+ forbidSeparators t = if Nothing == Text.find (flip elem ['/', '@']) t then Just t else Nothing++-- | Returns 'True' if the JID is /bare/, that is, it doesn't have a resource+-- part, and 'False' otherwise.+--+-- >>> isBare [jid|foo@bar|]+-- True+--+-- >>> isBare [jid|foo@bar/quux|]+-- False+isBare :: Jid -> Bool+isBare j | resourcepart j == Nothing = True+ | otherwise = False++-- | Returns 'True' if the JID is /full/, and 'False' otherwise.+--+-- @isFull = not . isBare@+--+-- >>> isBare [jid|foo@bar|]+-- True+--+-- >>> isBare [jid|foo@bar/quux|]+-- False+isFull :: Jid -> Bool+isFull = not . isBare++-- | Returns the @Jid@ without the resourcepart (if any).+--+-- >>> toBare [jid|foo@bar/quux|] == [jid|foo@bar|]+-- True+toBare :: Jid -> Jid+toBare j = j{resourcepart_ = Nothing}++-- | Returns the localpart of the @Jid@ (if any).+--+-- >>> localpart [jid|foo@bar/quux|]+-- Just "foo"+localpart :: Jid -> Maybe Text+localpart = fmap text . localpart_++-- | Returns the domainpart of the @Jid@.+--+-- >>> domainpart [jid|foo@bar/quux|]+-- "bar"+domainpart :: Jid -> Text+domainpart = text . domainpart_++-- | Returns the resourcepart of the @Jid@ (if any).+--+-- >>> resourcepart [jid|foo@bar/quux|]+-- Just "quux"+resourcepart :: Jid -> Maybe Text+resourcepart = fmap text . resourcepart_++-- | Parse the parts of a JID. The parts need to be validated with stringprep+-- before the JID can be constructed+jidParts :: AP.Parser (Maybe Text, Text, Maybe Text)+jidParts = do+ maybeLocalPart <- Just <$> localPart <|> return Nothing+ domainPart <- AP.takeWhile1 (AP.notInClass ['@', '/'])+ maybeResourcePart <- Just <$> resourcePart <|> return Nothing+ AP.endOfInput+ return (maybeLocalPart, domainPart, maybeResourcePart)+ where+ localPart = do+ bytes <- AP.takeWhile1 (AP.notInClass ['@', '/'])+ _ <- AP.char '@'+ return bytes+ resourcePart = 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+ }+-- | Specify the method with which the connection is (re-)established+data ConnectionDetails = UseRealm -- ^ Use realm to resolv host. This is the+ -- default.+ | UseSrv HostName -- ^ Use this hostname for a SRV lookup+ | UseHost HostName PortNumber -- ^ Use specified host+ | UseConnection (ExceptT XmppFailure IO StreamHandle)+ -- ^ Use a custom method to create a StreamHandle. This+ -- will also be used by reconnect. For example, to+ -- establish TLS before starting the stream as done by+ -- GCM, see 'connectTls'. You can also return an+ -- already established connection. This method should+ -- also return a hostname that is used for TLS+ -- signature verification. If startTLS is not used it+ -- can be left empty++-- | Configuration settings related to the stream.+data StreamConfiguration =+ StreamConfiguration { -- | Default language when no language tag is set+ preferredLang :: !(Maybe LangTag)+ -- | JID to include in the stream element's `to'+ -- attribute when the connection is secured; if the+ -- boolean is set to 'True', then the JID is also+ -- included when the 'ConnectionState' is 'Plain'+ , toJid :: !(Maybe (Jid, Bool))+ -- | By settings this field, clients can specify the+ -- network interface to use, override the SRV lookup+ -- of the realm, as well as specify the use of a+ -- non-standard port when connecting by IP or+ -- connecting to a domain without SRV records.+ , connectionDetails :: ConnectionDetails+ -- | DNS resolver configuration+ , resolvConf :: ResolvConf+ -- | Whether or not to perform the legacy+ -- session bind as defined in the (outdated)+ -- RFC 3921 specification+ , tlsBehaviour :: TlsBehaviour+ -- | Settings to be used for TLS negotitation+ , tlsParams :: ClientParams+ }++-- | Default parameters for TLS restricted to strong ciphers+xmppDefaultParamsStrong :: ClientParams+xmppDefaultParamsStrong = (defaultParamsClient "" BS.empty)+ { clientSupported = def+ { supportedCiphers = ciphersuite_strong+#if !MIN_VERSION_tls(2,0,0)+ ++ [ cipher_AES256_SHA1+ , cipher_AES128_SHA1+ ]+#endif+ }+ }++-- | Default parameters for TLS+-- @ciphersuite_all@ can be used to allow insecure ciphers+xmppDefaultParams :: ClientParams+xmppDefaultParams = (defaultParamsClient "" BS.empty)+ { clientSupported = def+ { supportedCiphers = ciphersuite_default+ }+ }++instance Default StreamConfiguration where+ def = StreamConfiguration { preferredLang = Nothing+ , toJid = Nothing+ , connectionDetails = UseRealm+ , resolvConf = defaultResolvConf+ , tlsBehaviour = PreferTls+ , tlsParams = xmppDefaultParams+ }++-- | How the client should behave in regards to TLS.+data TlsBehaviour = RequireTls -- ^ Require the use of TLS; disconnect if it's+ -- not offered.+ | PreferTls -- ^ Negotitate TLS if it's available.+ | PreferPlain -- ^ Negotitate TLS only if the server requires+ -- it+ | RefuseTls -- ^ Never secure the stream with TLS.+ deriving (Eq, Show, Generic)
+ source/Network/Xmpp/Utilities.hs view
@@ -0,0 +1,93 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Network.Xmpp.Utilities+ ( openElementToEvents+ , renderOpenElement+ , renderElement+ , checkHostName+ , withTMVar+ )+ where++import Control.Applicative ((<|>))+import Control.Concurrent.STM+import Control.Exception+import Control.Monad.State.Strict+import qualified Data.Attoparsec.Text as AP+import qualified Data.ByteString as BS+import Data.Conduit as C+import Data.Conduit.List as CL+import qualified Data.Text as Text+import Data.Text(Text)+import qualified Data.Text.Encoding as Text+import Data.XML.Types+import Prelude+import System.IO.Unsafe(unsafePerformIO)+import qualified Text.XML.Stream.Render as TXSR+import Text.XML.Unresolved as TXU++-- | Apply f with the content of tv as state, restoring the original value when an+-- exception occurs+withTMVar :: TMVar a -> (a -> IO (c, a)) -> IO c+withTMVar tv f = bracketOnError (atomically $ takeTMVar tv)+ (atomically . putTMVar tv)+ (\s -> do+ (x, s') <- f s+ atomically $ putTMVar tv s'+ return 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 :)++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 el@(Element name _ _) = openElementToEvents el+ ++ [EventEndElement name]++-- | Validates the hostname string in accordance with RFC 1123.+checkHostName :: Text -> Maybe Text+checkHostName t =+ eitherToMaybeHostName $ AP.parseOnly hostnameP t+ where+ eitherToMaybeHostName = either (const Nothing) Just++-- Validation of RFC 1123 hostnames.+hostnameP :: AP.Parser Text+hostnameP = do+ -- Hostnames may not begin with a hyphen.+ h <- AP.satisfy $ AP.inClass $ ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9']+ t <- AP.takeWhile $ AP.inClass $ ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ ['-']+ let label = Text.concat [Text.pack [h], t]+ if Text.length label > 63+ then fail "Label too long."+ else do+ AP.endOfInput+ return label+ <|> do+ _ <- AP.satisfy (== '.')+ r <- hostnameP+ if Text.length label + 1 + Text.length r > 255+ then fail "Hostname too long."+ else return $ Text.concat [label, Text.pack ".", r]
+ tests/Main.hs view
@@ -0,0 +1,13 @@+module Main where++import Test.Tasty++import Tests.Parsers+import Tests.Picklers+import Tests.Stream++main :: IO ()+main = defaultMain $ testGroup "root" [ parserTests+ , picklerTests+ , streamTests+ ]
+ tests/Run.hs view
@@ -0,0 +1,14 @@+module Main where++import Test.Tasty.HUnit+import Test.Tasty++import qualified Run.SendReceive as SendReceive+import qualified Run.Google as Google++sendReceiveTest = testCase "send and receive" SendReceive.run+googleTest = testCase "connect to google service" Google.connectGoogle++main = defaultMain $ testGroup "connection tests" [ sendReceiveTest+ , googleTest+ ]
+ tests/Run/Config.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}++module Run.Config where++import qualified Data.Configurator as Conf+import qualified Data.Configurator.Types as Conf+import System.Directory+import System.FilePath+import System.Log.Logger+import qualified Data.Text as Text++-- | Load the configuration files+loadConfig :: IO Conf.Config+loadConfig = do+ appData <- getAppUserDataDirectory "pontarius-xmpp-tests"+ home <- getHomeDirectory+ Conf.load [ Conf.Optional $ appData </> "pontarius-xmpp-tests.conf"+ , Conf.Optional $ home </> ".pontarius-xmpp-tests.conf"+ ]++configuredLoglevel conf = do+ loglevel <- Conf.lookup conf "loglevel" >>= \case+ (Nothing :: Maybe Text.Text) -> return ERROR+ Just "debug" -> return DEBUG+ Just "info" -> return INFO+ Just "notice" -> return NOTICE+ Just "warning" -> return WARNING+ Just "error" -> return ERROR+ Just "critical" -> return CRITICAL+ Just "alert" -> return ALERT+ Just "emergency" -> return EMERGENCY+ Just e -> error $ "Log level " ++ (Text.unpack e) ++ " unknown"+ updateGlobalLogger "Pontarius.Xmpp" $ setLevel loglevel+ return loglevel
+ tests/Run/Google.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Test connecting to google services+module Run.Google where+++import qualified Data.Configurator as Conf+import Network.Xmpp+import Network.Xmpp.Lens+import System.Exit+import System.Log.Logger+import Test.HUnit+import Network.TLS++import Run.Config++xmppConf = set tlsServerIdentificationL ("talk.google.com", "") $ def++connectGoogle = do+ conf <- loadConfig+ _ <- configuredLoglevel conf+ infoM "Pontarius.Xmpp" "Trying to connect to google server"+ let realm = "google.com"+ user <- Conf.require conf "google.user"+ password <- Conf.require conf "google.password"+ mbSess <- session realm (simpleAuth user password) xmppConf+ sess <- case mbSess of+ Left e -> do+ assertFailure $ "google session could not be initialized" ++ show e+ exitFailure+ Right r -> return r+ infoM "Pontarius.Xmpp" "Done trying to connect to google server"++-- connectGoogleSCM = do+-- conf <- loadConfig+-- _ <- configuredLoglevel conf+-- infoM "Pontarius.Xmpp" "Trying to connect to google server"+-- let realm = "gcm.googleapis.com"+-- user <- Conf.require conf "google.user"+-- password <- Conf.require conf "google.password"+-- mbSess <- session realm (simpleAuth user password) xmppConf+-- sess <- case mbSess of+-- Left e -> do+-- assertFailure $ "google session could not be initialized" ++ show e+-- exitFailure+-- Right r -> return r+-- infoM "Pontarius.Xmpp" "Done trying to connect to google server"
+ tests/Run/Payload.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE LambdaCase #-}+++module Run.Payload where++import Control.Monad+import Control.Monad.STM+import qualified Data.Text as Text+import Data.XML.Pickle+import Data.XML.Types+import Network.Xmpp+import Network.Xmpp.Internal+import System.Log.Logger+import Test.HUnit hiding (Node)+import Test.Hspec.Expectations++data Payload = Payload+ { payloadCounter :: !Int+ , ignoreFlag :: !Bool+ , errorFlag :: !Bool+ , payloadText :: !Text.Text+ } deriving (Eq, Show)++testNS :: Text.Text+testNS = "xmpp:library:test"++payloadP :: PU [Node] Payload+payloadP = xpWrap (\((counter,iFlag, eFlag) , message)+ -> Payload counter iFlag eFlag message)+ (\(Payload counter iFlag eFlag message)+ ->((counter,iFlag, eFlag) , message)) $+ xpElem (Name "request" (Just testNS) Nothing)+ (xp3Tuple+ (xpAttr "counter" xpPrim)+ (xpAttr "ignoreFlag" xpPrim)+ (xpAttr "errorFlag" xpPrim)+ )+ (xpElemNodes (Name "message" (Just testNS) Nothing)+ (xpContent xpId))++invertPayload :: Payload -> Payload+invertPayload (Payload count _iFlag _eFlag message) =+ Payload (count + 1) False False (Text.reverse message)++iqResponder :: Session -> IO ()+iqResponder context = do+ chan' <- listenIQ Set testNS context+ chan <- case chan' of+ Left _ -> do+ assertFailure "Channel was already taken"+ undefined+ Right c -> return c+ forever $ do+ next <- atomically $ chan+ let Right payload = unpickleElem payloadP . iqRequestPayload $+ iqRequestBody next+ let answerPayload = invertPayload payload+ let answerBody = pickleElem payloadP answerPayload+ unless (ignoreFlag payload) . void $+ case errorFlag payload of+ False -> answerIQ next (Right $ Just answerBody) []+ True -> answerIQ next (Left $ mkStanzaError NotAcceptable) []++testString :: Text.Text+testString = "abc ÄÖ>"++testPayload :: Jid -> Session -> IO ()+testPayload them session = do+ infoM "Pontarius.Xmpp" "Testing IQ send/receive"+ let pl1 = Payload 1 False False testString+ body1 = pickleElem payloadP pl1+ resp <- sendIQ' (Just 3000000) (Just them) Set Nothing body1 [] session++ case resp of+ Left e -> assertFailure $ "Could not send pl1" ++ show e+ Right (IQResponseError e) ->+ assertFailure $ "Unexpected IQ error" ++ show e+ Right (IQResponseResult IQResult{iqResultPayload = Just pl}) -> do+ case unpickleElem payloadP pl of+ Left e -> assertFailure $ "Error unpickling response p1"+ ++ ppUnpickleError e+ Right r -> do+ payloadCounter r `shouldBe` 2+ payloadText r `shouldBe` Text.reverse testString+ Right (IQResponseResult _) ->+ assertFailure "IQ result didn't contain payload"+ infoM "Pontarius.Xmpp" "Done testing IQ send/receive"+ ----------------------+ -- Timeout test+ ----------------------+ let pl2 = Payload 2 True False testString+ body2 = pickleElem payloadP pl2+ infoM "Pontarius.Xmpp" "Testing timeout"+ resp <- sendIQ' (Just 1000000) (Just them) Set Nothing body2 [] session+ case resp of+ Left IQTimeOut -> return ()+ Left e -> assertFailure $ "Unexpected send error" ++ show e+ Right r -> assertFailure $ "Unexpected IQ answer" ++ show r+ infoM "Pontarius.Xmpp" "IQ timed out (as expected)"+ ----------------------+ -- Error test+ ----------------------+ infoM "Pontarius.Xmpp" "Testing IQ error"+ let pl3 = Payload 3 False True testString+ body3 = pickleElem payloadP pl3+ resp <- sendIQ' (Just 3000000) (Just them) Set Nothing body3 [] session+ case resp of+ Left e -> assertFailure $ "Unexpected send error" ++ show e+ Right (IQResponseError e) ->+ stanzaErrorCondition (iqErrorStanzaError e) `shouldBe` NotAcceptable++ Right r -> assertFailure $ "Received unexpected IQ response" ++ show r+ infoM "Pontarius.Xmpp" "Received expected error"
+ tests/Run/SendReceive.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE LambdaCase #-}++module Run.SendReceive where++import Control.Applicative+import Control.Concurrent+import Control.Monad+import qualified Data.Configurator as Conf+import qualified Data.Configurator.Types as Conf+import Data.Maybe+import qualified Data.Text as Text+import Network.Socket+import Network.Xmpp+import System.Exit+import System.Log.Logger+import System.Timeout+import Test.HUnit+import Test.Hspec.Expectations++import Run.Payload+import Run.Config++xmppConfig :: ConnectionDetails -> SessionConfiguration+xmppConfig det = def{sessionStreamConfiguration+ = def{connectionDetails = det}+ , onConnectionClosed = \sess _ -> do+ _ <- reconnect' sess+ _ <- sendPresence presenceOnline sess+ return ()+ }++-- | reflect messages to their origin+reflect :: Session -> IO b+reflect sess = forever $ do+ m <- getMessage sess+ case answerMessage m (messagePayload m) of+ Nothing -> return ()+ Just am ->+ void $ sendMessage am{messageAttributes = messageAttributes m} sess++testAttributes = [( "{org.pontarius.xmpp.test}testattr"+ , "testvalue 12321 åäü>"+ )]++run :: IO ()+run = void $ do+ conf <- loadConfig+ uname1 <- Conf.require conf "xmpp.user1"+ pwd1 <- Conf.require conf "xmpp.password1"+ uname2 <- Conf.require conf "xmpp.user1"+ pwd2 <- Conf.require conf "xmpp.password1"+ realm <- Conf.require conf "xmpp.realm"+ server <- Conf.lookup conf "xmpp.server"+ port <- Conf.lookup conf "xmpp.port" :: IO (Maybe Integer)+ let conDetails = case server of+ Nothing -> UseRealm+ Just srv -> case port of+ Nothing -> UseSrv srv+ Just p -> UseHost srv (fromIntegral p)+ _ <- configuredLoglevel conf+ mbSess1 <- session realm (simpleAuth uname1 pwd1)+ ((xmppConfig conDetails))+ sess1 <- case mbSess1 of+ Left e -> do+ assertFailure $ "session 1 could not be initialized" ++ show e+ exitFailure+ Right r -> return r+ mbSess2 <- session realm (simpleAuth uname2 pwd2)+ ((xmppConfig conDetails))+ sess2 <- case mbSess2 of+ Left e -> do+ assertFailure $ "session 2 could not be initialized" ++ show e+ exitFailure+ Right r -> return r+ Just jid1 <- getJid sess1+ Just jid2 <- getJid sess2+ _ <- sendPresence presenceOnline sess1+ _ <- forkIO $ reflect sess1+ forkIO $ iqResponder sess1+ _ <- sendPresence presenceOnline sess2+ -- check message responsiveness+ infoM "Pontarius.Xmpp" "Running message mirror"+ sendMessage message{ messageTo = Just jid1+ , messageAttributes = testAttributes+ } sess2+ resp <- timeout 3000000 $ waitForMessage (\m -> messageFrom m == Just jid1)+ sess2+ case resp of+ Nothing -> assertFailure "Did not receive message answer"+ Just am -> messageAttributes am `shouldBe` testAttributes+ infoM "Pontarius.Xmpp" "Done running message mirror"+ infoM "Pontarius.Xmpp" "Running IQ tests"+ testPayload jid1 sess2+ infoM "Pontarius.Xmpp" "Done running IQ tests"
+ tests/Tests/Arbitrary.hs view
@@ -0,0 +1,6 @@+module Tests.Arbitrary where++import Tests.Arbitrary.Xml ()+import Tests.Arbitrary.Xmpp ()++-- $derive makeArbitrary IQRequestType
+ tests/Tests/Arbitrary/Common.hs view
@@ -0,0 +1,27 @@+module Tests.Arbitrary.Common where++import Control.Applicative ((<$>))+import Data.Char+import qualified Data.Text as Text+import Test.QuickCheck+import Test.QuickCheck.Instances ()++shrinkText1 :: Text.Text -> [Text.Text]+shrinkText1 txt = filter (not . Text.null) $ shrink txt++shrinkTextMaybe :: Maybe Text.Text -> [Maybe Text.Text]+shrinkTextMaybe mbtxt = filter (\mb -> mb /= Just (Text.empty)) $ shrink mbtxt++genText1 :: Gen Text.Text+genText1 = Text.pack <$> string1+ where+ string1 = listOf1 arbitrary `suchThat` (not . all isSpace)++maybeGen :: Gen a -> Gen (Maybe a)+maybeGen g = oneof [ return Nothing+ , Just <$> g+ ]++shrinkMaybe :: (t -> [t]) -> Maybe t -> [Maybe t]+shrinkMaybe _s Nothing = []+shrinkMaybe s (Just x) = Nothing : map Just (s x)
+ tests/Tests/Arbitrary/Xml.hs view
@@ -0,0 +1,85 @@+module Tests.Arbitrary.Xml where++import Control.Applicative ((<$>), (<*>))+import Test.QuickCheck+import Test.QuickCheck.Instances()+-- import Data.DeriveTH+import qualified Data.Text as Text+import Data.XML.Types+import Tests.Arbitrary.Common+import Text.CharRanges+++selectFromRange :: Range -> Gen Char+selectFromRange (Single a) = return a+selectFromRange (Range a b) = choose (a, b)++nameStartChar :: [Range]+nameStartChar =+ [ -- Single ':'+ Single '_'+ , Range 'A' 'Z'+ , Range 'a' 'z'+ , Range '\xC0' '\xD6'+ , Range '\xD8' '\xF6'+ , Range '\xF8' '\x2FF'+ , Range '\x370' '\x37D'+ , Range '\x37F' '\x1FFF'+ , Range '\x200C' '\x200D'+ , Range '\x2070' '\x218F'+ , Range '\x2C00' '\x2FEF'+ , Range '\x3001' '\xD7FF'+ , Range '\xF900' '\xFDCF'+ , Range '\xFDF0' '\xFFFD'+ , Range '\x10000' '\xEFFFF'+ ]++nameChar :: [Range]+nameChar =+ Single '-'+ : Single '.'+ : Single '\xB7'+ : Range '0' '9'+ : Range '\x0300' '\x036F'+ : Range '\x203F' '\x2040'+ : nameStartChar+++genNCName :: Gen Text.Text+genNCName = do+ sc <- elements nameStartChar >>= selectFromRange+ ncs <- listOf $ elements nameChar >>= selectFromRange+ return . Text.pack $ sc:ncs++-- | Cap the size of child elements.+slow :: Gen a -> Gen a+slow g = sized $ \n -> resize (min 5 (n `div` 4)) g++instance Arbitrary Name where+ arbitrary = Name <$> genNCName <*> genMaybe genNCName <*> genMaybe genNCName+ where+ genMaybe g = oneof [return Nothing, Just <$> g]+ shrink (Name a b c) = [ Name a' b c | a' <- shrinkText1 a]+ ++[ Name a b' c | b' <- shrinkTextMaybe b]+ ++[ Name a b c' | c' <- shrinkTextMaybe c]++instance Arbitrary Content where+ arbitrary = ContentText <$> arbitrary+ shrink (ContentText txt) = ContentText <$> shrinkText1 txt+ shrink _ = []+++instance Arbitrary Node where+ arbitrary = oneof [ NodeElement <$> arbitrary+ , NodeContent <$> arbitrary+ ]+ shrink (NodeElement e) = NodeElement <$> shrink e+ shrink (NodeContent c) = NodeContent <$> shrink c+ shrink _ = []++instance Arbitrary Element where+ arbitrary = Element <$> arbitrary <*> slow arbitrary <*> slow arbitrary+ shrink (Element a b c) =+ [ Element a' b c | a' <- shrink a]+ ++[ Element a b' c | b' <- shrink b]+ ++[ Element a b c' | c' <- shrink c]
+ tests/Tests/Arbitrary/Xmpp.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+module Tests.Arbitrary.Xmpp where++import Control.Applicative ((<$>), (<*>))+import Data.Char+import Data.Maybe+import qualified Data.Text as Text+import Network.Xmpp.Internal hiding (elements)+import Test.QuickCheck+import Test.QuickCheck.Instances()+import Test.QuickCheck.Arbitrary.Generic+import qualified Text.CharRanges as Ranges+import qualified Text.StringPrep as SP+import qualified Text.StringPrep.Profiles as SP++import Tests.Arbitrary.Common+import Tests.Arbitrary.Xml ()+++instance Arbitrary NonemptyText where+ arbitrary = Nonempty . Text.pack <$> listOf1+ (arbitrary `suchThat` (not . isSpace))+ shrink (Nonempty txt) = map Nonempty+ . filter (not . Text.all isSpace) $ shrink txt++instance Arbitrary Jid where+ arbitrary = do+ ~(Just jid) <- tryJid `suchThat` isJust+ return jid+ where+ tryJid = jidFromTexts <$> maybeGen (genString nodeprepProfile False)+ <*> genString (SP.namePrepProfile False) False+ <*> maybeGen (genString resourceprepProfile True)++ genString profile node = Text.pack . take 1024 <$> listOf1 genChar+ where+ genChar = arbitrary `suchThat` (not . isProhibited)+ prohibited = Ranges.toSet $ concat (SP.prohibited profile)+ isProhibited x = Ranges.member x prohibited+ || if node+ then False+ else x `elem` ['@','/', '@', '/']++ shrink (Jid lp dp rp) = [ Jid lp' dp rp | lp' <- shrinkMaybe shrink lp]+ ++ [ Jid lp dp' rp | dp' <- shrink dp]+ ++ [ Jid lp dp rp' | rp' <- shrinkMaybe shrink rp]+++-- string :: SP.StringPrepProfile -> Gen [Char]+-- string profile = take 1024 <$> listOf1 genChar+-- where+-- genChar = arbitrary `suchThat` (not . isProhibited)+-- prohibited = Ranges.toSet $ concat (SP.prohibited profile)+-- isProhibited x = Ranges.member x prohibited+-- || x `elem` "@/"++instance Arbitrary LangTag where+ arbitrary = LangTag <$> genTag <*> listOf genTag+ where genTag = fmap Text.pack . listOf1 . elements $ ['a'..'z'] ++ ['A'..'Z']+ shrink (LangTag lt lts) = [LangTag lt' lts | lt' <- shrinkText1 lt] +++ [LangTag lt lts' | lts' <- filter (not . Text.null)+ <$> shrink lts]++instance Arbitrary XmppFailure where+ arbitrary = elements [StreamEndFailure, TcpConnectionFailure, XmppIllegalTcpDetails, TlsNoServerSupport, XmppNoStream, TlsStreamSecured, XmppOtherFailure]++-- Auto-derive trivial instances+instance Arbitrary StanzaErrorType where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary StanzaErrorCondition where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary StanzaError where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary StreamErrorInfo where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary IQRequestType where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary IQRequest where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary IQResult where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary IQError where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary MessageType where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary Message where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary MessageError where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary PresenceType where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary Presence where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary PresenceError where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary Stanza where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary SaslError where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary SaslFailure where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary StreamErrorCondition where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary AuthFailure where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary Version where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary ConnectionState where+ arbitrary = genericArbitrary+ shrink = genericShrink++instance Arbitrary TlsBehaviour where+ arbitrary = genericArbitrary+ shrink = genericShrink
+ tests/Tests/Parsers.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.Parsers where++import Control.Applicative ((<$>))+import Network.Xmpp.Internal+import Test.Hspec+import Test.Tasty.QuickCheck+import Test.Tasty+import Test.Tasty.TH+import Test.Tasty.HUnit++import Tests.Arbitrary ()++case_JidFromText :: Assertion+case_JidFromText = hspec . describe "jidFromText" $ do+ it "parses a full JID" $ jidFromText "foo@bar.com/quux"+ `shouldBe` Just (Jid (Just "foo")+ "bar.com"+ (Just "quux"))+ it "parses a bare JID" $ jidFromText "foo@bar.com"+ `shouldBe` Just (Jid (Just "foo")+ "bar.com"+ Nothing)+ it "parses a domain" $ jidFromText "bar.com"+ `shouldBe` Just (Jid Nothing+ "bar.com"+ Nothing)+ it "parses domain with resource" $ jidFromText "bar.com/quux"+ `shouldBe` Just (Jid Nothing+ "bar.com"+ (Just "quux"))+ it "rejects multiple '@'" $ shouldReject "foo@bar@baz"+ it "parses multiple '/'" $ jidFromText "foo/bar/baz"+ `shouldBe`+ (Just (Jid Nothing "foo" (Just "bar/baz")))+ it "parses multiple '/' after '@'" $ jidFromText "quux@foo/bar/baz"+ `shouldBe`+ (Just (Jid (Just "quux") "foo" (Just "bar/baz")))+ it "parses '@' after '/'" $ jidFromText "foo/bar@baz"+ `shouldBe`+ (Just (Jid Nothing "foo" (Just "bar@baz")))+ it "rejects empty local part" $ shouldReject "@bar/baz"+ it "rejects empty resource part" $ shouldReject "foo@bar/"+ it "rejects empty domain part" $ shouldReject "foo@/baz"+ where shouldReject jid = jidFromText jid `shouldBe` Nothing++prop_jidFromText_rightInverse :: Jid -> Bool+prop_jidFromText_rightInverse j = let jidText = jidToText j in+ (jidToText <$> jidFromText jidText) == Just jidText++prop_jidFromText_leftInverse :: Jid -> Bool+prop_jidFromText_leftInverse jid = (jidFromText $ jidToText jid) == Just jid+++case_LangTagParser :: Assertion+case_LangTagParser = hspec . describe "langTagFromText" $+ it "has some properties" $ pendingWith "Check requirements"++parserTests :: TestTree+parserTests = $testGroupGenerator
+ tests/Tests/Picklers.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+module Tests.Picklers where++import Tests.Arbitrary ()+import Data.XML.Pickle+import Network.Xmpp.Internal+import Test.Tasty+import Test.Tasty.TH+import Test.Tasty.QuickCheck+import Data.Text (Text)++import Data.XML.Types++-- | Test Pickler self-inverse: Check that unpickling after pickling gives the+-- original value+tpsi :: Eq a => PU t a -> a -> Bool+tpsi p = \x -> case unpickle p (pickle p x) of+ Left _ -> False+ Right x' -> x == x'++testPickler :: PU t a -> a -> IO ()+testPickler p x = case unpickle p (pickle p x) of+ Left e -> putStrLn $ ppUnpickleError e+ Right _ -> putStrLn "OK."++prop_xpStreamStanza_invertible :: Either StreamErrorInfo Stanza -> Bool+prop_xpStreamStanza_invertible = tpsi xpStreamStanza+prop_xpStanza_invertible :: Stanza -> Bool+prop_xpStanza_invertible = tpsi xpStanza+prop_xpMessage_invertible :: Message -> Bool+prop_xpMessage_invertible = tpsi xpMessage+prop_xpPresence_invertible = tpsi xpPresence+prop_xpPresence_invertible :: Presence -> Bool+prop_xpIQRequest_invertible = tpsi xpIQRequest+prop_xpIQRequest_invertible :: IQRequest -> Bool+prop_xpIQResult_invertible = tpsi xpIQResult+prop_xpIQResult_invertible :: IQResult -> Bool+prop_xpErrorCondition_invertible = tpsi xpStanzaErrorCondition+prop_xpErrorCondition_invertible :: StanzaErrorCondition -> Bool+prop_xpStanzaError_invertible = tpsi xpStanzaError+prop_xpStanzaError_invertible :: StanzaError -> Bool+prop_xpMessageError_invertible = tpsi xpMessageError+prop_xpMessageError_invertible :: MessageError -> Bool+prop_xpPresenceError_invertible = tpsi xpPresenceError+prop_xpPresenceError_invertible :: PresenceError -> Bool+prop_xpIQError_invertible = tpsi xpIQError+prop_xpIQError_invertible :: IQError -> Bool+prop_xpStreamError_invertible = tpsi xpStreamError+prop_xpStreamError_invertible :: StreamErrorInfo -> Bool+prop_xpLangTag_invertible = tpsi xpLangTag+prop_xpLangTag_invertible :: Maybe LangTag -> Bool+prop_xpLang_invertible = tpsi xpLang+prop_xpLang_invertible :: LangTag -> Bool+prop_xpStream_invertible = tpsi xpStream+prop_xpStream_invertible :: ( Text+ , Maybe Jid+ , Maybe Jid+ , Maybe Text+ , Maybe LangTag )+ -> Bool+prop_xpJid_invertible = tpsi xpJid+prop_xpJid_invertible :: Jid -> Bool+prop_xpIQRequestType_invertible = tpsi xpIQRequestType+prop_xpIQRequestType_invertible :: IQRequestType -> Bool+prop_xpMessageType_invertible = tpsi xpMessageType+prop_xpMessageType_invertible :: MessageType -> Bool+prop_xpPresenceType_invertible = tpsi xpPresenceType+prop_xpPresenceType_invertible :: PresenceType -> Bool+prop_xpStanzaErrorType_invertible = tpsi xpStanzaErrorType+prop_xpStanzaErrorType_invertible :: StanzaErrorType -> Bool+prop_xpStanzaErrorCondition_invertible = tpsi xpStanzaErrorCondition+prop_xpStanzaErrorCondition_invertible :: StanzaErrorCondition -> Bool+prop_xpStreamErrorCondition_invertible = tpsi xpStreamErrorCondition+prop_xpStreamErrorCondition_invertible :: StreamErrorCondition -> Bool+-- prop_xpStreamFeatures_invertible = testPicklerInvertible xpStreamFeatures++picklerTests :: TestTree+picklerTests = $testGroupGenerator++bad = StreamErrorInfo { errorCondition = StreamInvalidFrom+ , errorText = Just (Nothing,"")+ , errorXml = Just (+ Element { elementName =+ Name { nameLocalName = "\65044"+ , nameNamespace = Just "\14139"+ , namePrefix = Just "\651"}+ , elementAttributes = []+ , elementNodes = []})}
+ tests/Tests/Stream.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.Stream where++import Control.Monad.Trans+import Data.Conduit+import qualified Data.Conduit.List as CL+import Data.XML.Types+import Test.Hspec+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.TH++import Network.Xmpp.Internal++junk = [ EventBeginDocument+ , EventEndDocument+ , EventBeginDoctype "" Nothing+ , EventEndDoctype+ , EventInstruction $ Instruction "" ""+-- , EventBeginElement Name [(Name, [Content])]+ , EventEndElement "foo"+ , EventContent $ ContentText ""+ , EventComment ""+ , EventCDATA ""+ ]++beginElem = EventBeginElement "foo" []++case_ThrowOutJunk = hspec . describe "throwOutJunk" $ do+ it "drops everything but EvenBeginElement" $ do+ res <- CL.sourceList junk $$ throwOutJunk >> await+ res `shouldBe` Nothing+ it "keeps everything after (and including) EvenBeginElement" $ do+ res <- CL.sourceList (junk ++ [beginElem] ++ junk)+ $$ throwOutJunk >> CL.consume+ res `shouldBe` (beginElem : junk)++case_LogInput = hspec . describe "logInput" $ do+ it "Can handle split UTF8 codepoints" $ do+ res <- CL.sourceList ["\209","\136"] $= logInput $$ CL.consume+ res `shouldBe` ["\209","\136"]++streamTests :: TestTree+streamTests = $testGroupGenerator