module TCPConnection ( TCPConnection , openConnection , sendStanza , getStanzas , closeConnection ) where import XMLParse import Prelude hiding(read) import Network import Network.GnuTLS import Data.IORef import Control.Monad import System.Posix.Temp import System.IO import System.Random import Control.Concurrent import qualified Codec.Binary.Base64 as B64 import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC -- |An XMPP connection over TCP. -- The args are: tls session, network handle, unparsed incoming message, log handle, mutex. data TCPConnection = TCPConnection (Session Client) Handle (IORef String) Handle (MVar ()) -- |Open a TCP connection to the named server, port 5222, and send a -- stream header. Get the stream header that the server sent. This should really check SRV records. -- Finally, authenticate using SASL PLAIN mechanism. openConnection :: (String, String, String) -> IO TCPConnection openConnection (username, server, password) = let rawWrite h f s = (hPutStrLn f $ ">> " ++ s) >> (hPutStr h s) rawRead h f = do hWaitForInput h (-1) s <- getEverything hPutStrLn f $ "<< " ++ s return s where getEverything = do r <- hReady h if r then liftM2 (:) (hGetChar h) getEverything else return [] in do -- Initialization (logfile, logH) <- mkstemp "/tmp/log/XXXXXX" putStrLn $ "Log is " ++ logfile hSetBuffering logH LineBuffering h <- connectTo server (PortNumber 5222) hSetBuffering h NoBuffering buffer <- newIORef "" v <- newEmptyMVar -- Start the stream rawWrite h logH $ xmlToString False $ XML "stream:stream" [("to",server), ("xmlns","jabber:client"), ("xmlns:stream","http://etherx.jabber.org/streams"), ("version", "1.0")] [] -- TODO: check the response, which would specify if tls is required and what sasl mechanisms are supported rawRead h logH -- jabber.org now requires use of tls, maybe for a week only? rawWrite h logH $ xmlToString True $ XML "starttls" [("xmlns","urn:ietf:params:xml:ns:xmpp-tls")] [] -- TODO: check the response is rawRead h logH -- attempt TLS negotiation. Unfortunately hsgnutls is poorly documented :( s <- tlsClient [handle := h, -- Anonymous authentication KxAnonDh would not work. CrtOpenpgp is not supported yet. priorities := [CrtX509], -- anonymousClientCredentials would not work credentials := certificateCredentials ] handshake s let c = TCPConnection s h buffer logH v -- attempt SASL negotiation write c $ xmlToString False $ XML "stream:stream" [("to",server), ("xmlns","jabber:client"), ("xmlns:stream","http://etherx.jabber.org/streams"), ("version", "1.0")] [] -- TODO: check the response, which would specify what sasl mechanisms are supported read c -- TODO: assume plain mechanism is supported, coz there's no SASL lib for use -- the correct format is -- {base64 data here}, -- where the payload is |optional authzid authcid passwd| let auth = xmlToString True $ XML "auth" [("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"), ("mechanism", "PLAIN") ] [CData payload] payload = B64.encode $ concat [nul, authcid, nul, passwd] nul = [toEnum 0] authcid = B.unpack $ BC.pack username passwd = B.unpack $ BC.pack password write c auth [(XML response' _ _)] <- getStanzas c if "success" == response' then putStrLn "Authentication succeeded" else error "Authentication failed" -- Attempt resource binding write c $ xmlToString False $ XML "stream:stream" [("to",server), ("xmlns","jabber:client"), ("xmlns:stream","http://etherx.jabber.org/streams"), ("version", "1.0")] [] -- TODO: check if the resource binding feature is required read c sendIq c server "set" [XML "bind" [("xmlns","urn:ietf:params:xml:ns:xmpp-bind")] []] -- TODO: check the result of resource binding read c -- Attempt session establishment sendIq c server "set" [XML "session" [("xmlns","urn:ietf:params:xml:ns:xmpp-session")] []] -- TODO: check the result read c sendStanza c $ XML "presence" [] [] return c write :: TCPConnection -> String -> IO () write (TCPConnection h _ _ f v) s = do putMVar v () tlsSendString h s hPutStrLn f $ ">> " ++ s takeMVar v read :: TCPConnection -> IO String read (TCPConnection h network _ f _) = do hWaitForInput network (-1) s <- getEverything hPutStrLn f $ "<< " ++ s return s where getEverything = do r <- hReady network if r then liftM2 (++) (tlsRecvString h) getEverything else return [] -- |Get incoming stanzas from the connection. getStanzas :: TCPConnection -> IO [XMLElem] getStanzas c = parseBuffered c deepTags -- |Send a stanza on the connection. sendStanza :: TCPConnection -> XMLElem -> IO () sendStanza c x = let str = xmlToString True x in write c str -- |Close the connection. closeConnection :: TCPConnection -> IO () closeConnection (TCPConnection h network _ f _) = bye h ShutRdwr >> hClose network >> hClose f parseBuffered :: TCPConnection -> Parser a -> IO a parseBuffered c@(TCPConnection h _ bufvar f _) parser = do buffer <- readIORef bufvar input <- read c case parse (getRest parser) "" (buffer++input) of Right (result, rest) -> do writeIORef bufvar rest return result Left e -> do putStrLn $ "An error? Hopefully doesn't matter.\n"++(show e) parseBuffered c parser sendIq :: TCPConnection -> String -- ^JID of recipient -> String -- ^Type of IQ, either \"get\" or \"set\" -> [XMLElem] -- ^Payload elements -> IO () sendIq c to iqtype payload = do iqid <- randomIO :: IO Int sendStanza c $ XML "iq" [("to", to), ("type", iqtype), ("id", show iqid)] payload