-- Copyright (C) 2010 John Millikin -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see . {-# LANGUAGE OverloadedStrings #-} module Network.Protocol.XMPP.Client ( Client , clientJID , connectClient , bindClient ) where import Network (connectTo) import Text.XML.HXT.Arrow ((>>>)) import qualified Text.XML.HXT.Arrow as A import qualified Text.XML.HXT.DOM.Interface as DOM import qualified Text.XML.HXT.DOM.XmlNode as XN import qualified System.IO as IO import Data.ByteString (ByteString) import qualified Data.Text as T import qualified Text.XML.LibXML.SAX as SAX import qualified Network.Protocol.XMPP.Internal.Authentication as A import qualified Network.Protocol.XMPP.Internal.Connections as C import qualified Network.Protocol.XMPP.Internal.Features as F import qualified Network.Protocol.XMPP.Internal.Handle as H import qualified Network.Protocol.XMPP.Internal.Stream as S import qualified Network.Protocol.XMPP.Stream as S import Network.Protocol.XMPP.Internal.XML ( getTree, putTree , element, qname , readEventsUntil ) import qualified Network.Protocol.XMPP.JID as J import Network.Protocol.XMPP.Stanza data Client = Client { clientJID :: J.JID , clientStream :: ClientStream } data ClientStream = ClientStream { streamJID :: J.JID , streamHandle :: H.Handle , streamFeatures :: [F.Feature] , streamParser :: SAX.Parser } instance S.Stream Client where streamNamespace _ = "jabber:client" getTree = S.getTree . clientStream putTree = S.putTree . clientStream instance S.Stream ClientStream where streamNamespace _ = "jabber:client" getTree s = getTree (streamHandle s) (streamParser s) putTree s = putTree (streamHandle s) connectClient :: C.Server -> J.JID -- ^ Client JID -> T.Text -- ^ Username -> T.Text -- ^ Password -> IO Client connectClient server jid username password = do -- Open a TCP connection let C.Server sjid host port = server rawHandle <- connectTo host port IO.hSetBuffering rawHandle IO.NoBuffering let handle = H.PlainHandle rawHandle -- Open the initial stream and authenticate stream <- beginStream sjid handle authedStream <- authenticate stream jid sjid username password return $ Client jid authedStream authenticate :: ClientStream -> J.JID -> J.JID -> T.Text -> T.Text -> IO ClientStream authenticate stream jid sjid username password = do let mechanisms = authenticationMechanisms stream result <- A.authenticate stream mechanisms jid sjid username password case result of -- TODO: throwIO some exception type? A.Failure -> error "Authentication failure" _ -> restartStream stream authenticationMechanisms :: ClientStream -> [ByteString] authenticationMechanisms = step . streamFeatures where step [] = [] step (f:fs) = case f of (F.FeatureSASL ms) -> ms _ -> step fs bindClient :: Client -> IO J.JID bindClient c = do -- Bind S.putStanza c $ bindStanza . J.jidResource . clientJID $ c bindResult <- S.getStanza c let jidArrow = A.deep (A.hasQName (qname "urn:ietf:params:xml:ns:xmpp-bind" "jid")) >>> A.getChildren >>> A.getText -- TODO: throwIO with exception let Just jid = do result <- bindResult iq <- case result of ReceivedIQ x -> Just x _ -> Nothing case A.runLA jidArrow (iqPayload iq) of [] -> Nothing (str:_) -> J.parseJID (T.pack str) -- Session S.putStanza c sessionStanza S.getStanza c S.putStanza c $ emptyPresence PresenceAvailable S.getStanza c return jid bindStanza :: Maybe J.Resource -> IQ bindStanza resource = emptyIQ IQSet payload where payload = element ("", "bind") [("", "xmlns", "urn:ietf:params:xml:ns:xmpp-bind")] requested requested = case fmap J.strResource resource of Nothing -> [] Just x -> [element ("", "resource") [] [XN.mkText (T.unpack x)]] sessionStanza :: IQ sessionStanza = emptyIQ IQSet $ element ("", "session") [("", "xmlns", "urn:ietf:params:xml:ns:xmpp-session")] [] beginStream :: J.JID -> H.Handle -> IO ClientStream beginStream jid handle = do plain <- newStream jid handle if streamSupportsTLS plain then do S.putTree plain xmlStartTLS S.getTree plain H.startTLS handle >>= newStream jid else return plain restartStream :: ClientStream -> IO ClientStream restartStream s = newStream (streamJID s) (streamHandle s) newStream :: J.JID -> H.Handle -> IO ClientStream newStream jid h = do parser <- SAX.mkParser H.hPutBytes h $ C.xmlHeader "jabber:client" jid readEventsUntil C.startOfStream h parser features <- F.parseFeatures `fmap` getTree h parser return $ ClientStream jid h features parser streamSupportsTLS :: ClientStream -> Bool streamSupportsTLS = any isStartTLS . streamFeatures where isStartTLS (F.FeatureStartTLS _) = True isStartTLS _ = False xmlStartTLS :: DOM.XmlTree xmlStartTLS = element ("", "starttls") [("", "xmlns", "urn:ietf:params:xml:ns:xmpp-tls")] []