You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
7.5 KiB
193 lines
7.5 KiB
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} |
|
{-# LANGUAGE TupleSections #-} |
|
|
|
module Network.Xmpp.Stream where |
|
|
|
import Control.Applicative ((<$>), (<*>)) |
|
import qualified Control.Exception as Ex |
|
import Control.Monad.Error |
|
import Control.Monad.State.Strict |
|
|
|
import Data.Conduit |
|
import Data.Conduit.BufferedSource |
|
import Data.Conduit.List as CL |
|
import Data.Maybe (fromJust, isJust, isNothing) |
|
import Data.Text as T |
|
import Data.XML.Pickle |
|
import Data.XML.Types |
|
import Data.Void(Void) |
|
|
|
import Network.Xmpp.Monad |
|
import Network.Xmpp.Pickle |
|
import Network.Xmpp.Types |
|
|
|
import Text.XML.Stream.Elements |
|
import Text.XML.Stream.Parse as XP |
|
|
|
-- import Text.XML.Stream.Elements |
|
|
|
-- Unpickles and returns a stream element. Throws a StreamXMLError on failure. |
|
streamUnpickleElem :: PU [Node] a |
|
-> Element |
|
-> ErrorT StreamError (Pipe Event Void IO) a |
|
streamUnpickleElem p x = do |
|
case unpickleElem p x of |
|
Left l -> throwError $ StreamXMLError l |
|
Right r -> return r |
|
|
|
-- This is the conduit sink that handles the stream XML events. We extend it |
|
-- with ErrorT capabilities. |
|
type StreamSink a = ErrorT StreamError (Pipe Event Void IO) a |
|
|
|
-- Discards all events before the first EventBeginElement. |
|
throwOutJunk :: Monad m => Sink Event 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 |
|
lift throwOutJunk |
|
hd <- lift CL.head |
|
case hd of |
|
Just (EventBeginElement name attrs) -> return $ Element name attrs [] |
|
_ -> throwError $ StreamConnectionError |
|
|
|
-- Sends the initial stream:stream element and pulls the server features. |
|
xmppStartStream :: XmppConMonad (Either StreamError ()) |
|
xmppStartStream = runErrorT $ do |
|
hostname' <- gets sHostname |
|
lang <- gets sPreferredLang |
|
case hostname' of |
|
Nothing -> throwError StreamConnectionError |
|
Just hostname -> lift $ do |
|
pushXmlDecl |
|
pushOpenElement $ |
|
pickleElem pickleOutStream (Nothing, Just hostname, (pack . show) <$> lang) |
|
(lt, features) <- ErrorT . pullToSink $ runErrorT xmppStream |
|
modify (\s -> s {sFeatures = features, sStreamLang = Just lt}) |
|
return () |
|
|
|
-- Creates a new connection source (of Events) using the raw source (of bytes) |
|
-- and calls xmppStartStream. |
|
xmppRestartStream :: XmppConMonad (Either StreamError ()) |
|
xmppRestartStream = do |
|
raw <- gets sRawSrc |
|
newsrc <- liftIO . bufferSource $ raw $= XP.parseBytes def |
|
modify (\s -> s{sConSrc = newsrc}) |
|
xmppStartStream |
|
|
|
-- Reads the (partial) stream:stream and the server features from the stream. |
|
-- Throws an error the version number is not 1.0, or if the language tag is not |
|
-- set, or is invalid. |
|
xmppStream :: StreamSink (LangTag, ServerFeatures) |
|
xmppStream = do |
|
langTag <- xmppStreamHeader |
|
features <- xmppStreamFeatures |
|
return (langTag, features) |
|
where |
|
xmppStreamHeader :: StreamSink LangTag |
|
xmppStreamHeader = do |
|
lift throwOutJunk |
|
-- Get the stream:stream element (or whatever it is) from the server, |
|
-- and validate what we get. |
|
((Name lname ns prefix), (from, id, to, ver, lang, xns), ()) |
|
<- streamUnpickleElem pickleInStream =<< openElementFromEvents |
|
unless (lname == "stream") $ throwError $ StreamNotStreamElement lname |
|
unless ((ns == Just "jabber:client" && xns == Just "http://etherx.jabber.org/streams") || (ns == Just "http://etherx.jabber.org/streams" && (xns == Just"" || xns == Nothing))) $ throwError $ StreamInvalidStreamNamespace (ns, xns) |
|
unless (prefix == Just "stream") $ throwError $ StreamInvalidStreamPrefix prefix |
|
unless (ver == Just "1.0") $ throwError $ StreamWrongVersion ver |
|
-- TODO: Verify id, to, from, and stream:xmlns. |
|
liftIO $ print (from, id, to, ver, lang, xns) |
|
let lang_ = maybe Nothing langTag lang |
|
when (isNothing lang_) $ throwError $ StreamWrongLangTag lang |
|
return $ fromJust lang_ |
|
xmppStreamFeatures :: StreamSink ServerFeatures |
|
xmppStreamFeatures = do |
|
e <- lift $ elements =$ CL.head |
|
case e of |
|
Nothing -> liftIO $ Ex.throwIO StreamConnectionError |
|
Just r -> streamUnpickleElem pickleStreamFeatures r |
|
|
|
-- Pickler for the stream element to be sent to the server. Version "1.0" is |
|
-- assumed, and so is the "jabber:client" xmlns and |
|
-- "http://etherx.jabber.org/streams" xmlns:stream attributes. (We follow what |
|
-- RFC 6120 calls the "content-namespace-as-default-namespace".) |
|
pickleOutStream :: PU [Node] ( Maybe Text -- from |
|
, Maybe Text -- to |
|
, Maybe Text -- xml:lang |
|
) |
|
pickleOutStream = xpWrap |
|
(\(from, to, _ver, lang) -> (from, to, lang)) |
|
(\(from, to, lang) -> |
|
(from, to, "1.0", lang) |
|
) |
|
(xpElemAttrs |
|
(Name |
|
"stream" |
|
(Just "http://etherx.jabber.org/streams") |
|
(Just "stream") |
|
) |
|
(xp4Tuple |
|
(xpAttrImplied "from" xpId) |
|
(xpAttrImplied "to" xpId) |
|
(xpAttr "version" xpId) |
|
(xpAttrImplied (Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml")) xpId) |
|
) |
|
) |
|
|
|
-- Unpickler for the stream element to be received from the server. As this |
|
-- function puts no restrictions on the element received, the data need to be |
|
-- validated externally. |
|
pickleInStream :: PU [Node] ( Name |
|
, ( Maybe Text -- from |
|
, Maybe Text -- id |
|
, Maybe Text -- to |
|
, Maybe Text -- version |
|
, Maybe Text -- xml:lang |
|
, Maybe Text -- xmlns:stream |
|
) |
|
, () |
|
) |
|
pickleInStream = xpElemWithName |
|
(xp6Tuple |
|
(xpAttrImplied "from" xpId) |
|
(xpAttrImplied "id" xpId) |
|
(xpAttrImplied "to" xpId) |
|
(xpAttrImplied "version" xpId) |
|
(xpAttrImplied (Name "lang" (Just "http://www.w3.org/XML/1998/namespace") (Just "xml")) xpId) |
|
-- TODO: Actually fetch the xmlns:stream attribute. |
|
(xpAttrImplied (Name "stream" Nothing (Just "xmlns")) xpId) |
|
) |
|
xpUnit |
|
|
|
-- Pickler/Unpickler for the stream features - TLS, SASL, and the rest. |
|
pickleStreamFeatures :: PU [Node] ServerFeatures |
|
pickleStreamFeatures = xpWrap |
|
(\(tls, sasl, rest) -> SF tls (mbl sasl) rest) |
|
(\(SF tls sasl rest) -> (tls, lmb sasl, rest)) |
|
(xpElemNodes |
|
(Name |
|
"features" |
|
(Just "http://etherx.jabber.org/streams") |
|
(Just "stream") |
|
) |
|
(xpTriple |
|
(xpOption pickleTLSFeature) |
|
(xpOption pickleSaslFeature) |
|
(xpAll xpElemVerbatim) |
|
) |
|
) |
|
where |
|
pickleTLSFeature :: PU [Node] Bool |
|
pickleTLSFeature = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-tls}starttls" |
|
(xpElemExists "required") |
|
pickleSaslFeature :: PU [Node] [Text] |
|
pickleSaslFeature = xpElemNodes |
|
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms" |
|
(xpAll $ xpElemNodes |
|
"{urn:ietf:params:xml:ns:xmpp-sasl}mechanism" (xpContent xpId)) |