----------------------------------------------------------------------------- -- -- Module : Network.XMPP.Stream -- Copyright : Copyright © 2011, Jon Kristensen -- License : UnknownLicense "LGPL3" -- -- Maintainer : jon.kristensen@pontarius.org -- Stability : alpha -- Portability : -- -- | -- ----------------------------------------------------------------------------- module Network.XMPP.Stream ( isTLSSecured, xmlEnumerator, xmlReader, presenceToXML, iqToXML, messageToXML, parsePresence, parseIQ, parseMessage ) where import Network.XMPP.JID import Network.XMPP.Types import Network.XMPP.Utilities import Network.XMPP.TLS import Network.XMPP.Stanza import qualified Control.Exception as CE import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan) import GHC.IO.Handle (Handle, hPutStr, hFlush, hSetBuffering, hWaitForInput) import Network.TLS import Network.TLS.Cipher import Data.Enumerator (($$), Iteratee, continue, joinI, run, run_, yield) import Data.Enumerator.Binary (enumHandle, enumFile) import Text.XML.Enumerator.Parse (parseBytes, decodeEntities) import Text.XML.Enumerator.Document (fromEvents) 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 import Data.Maybe import Data.XML.Types import Control.Monad.IO.Class (liftIO, MonadIO) import Data.String (IsString(..)) isTLSSecured :: TLSState -> Bool isTLSSecured (PostHandshake _) = True isTLSSecured _ = False -- Reads from the provided handle or TLS context and sends the events to the -- internal event channel. xmlEnumerator :: Chan (InternalEvent s m) -> Either Handle TLSCtx -> IO () xmlEnumerator c s = do enumeratorResult <- case s of Left handle -> run $ enumHandle 1 handle $$ joinI $ parseBytes decodeEntities $$ xmlReader c Right tlsCtx -> run $ enumTLS tlsCtx $$ joinI $ parseBytes decodeEntities $$ xmlReader c case enumeratorResult of Right _ -> writeChan c $ IEE EnumeratorDone Left e -> writeChan c $ IEE (EnumeratorException e) where -- Behaves like enumHandle, but reads from the TLS context instead enumTLS :: TLSCtx -> E.Enumerator DB.ByteString IO b enumTLS c s = loop c s 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 xmlReader :: Chan (InternalEvent s m) -> Iteratee Event IO (Maybe Event) xmlReader c = xmlReader_ c [] 0 xmlReader_ :: Chan (InternalEvent s m) -> [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 $ IEE $ EnumeratorXML $ XEBeginStream $ "StreamTODO" xmlReader_ ch [] 1 xmlReader_ ch [EventEndElement name] 1 | namePrefix name == Just (DT.pack "stream") && nameLocalName name == DT.pack "stream" = do liftIO $ writeChan ch $ IEE $ EnumeratorXML $ 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" = do liftIO $ writeChan ch $ IEE $ EnumeratorXML $ XEProceed E.yield Nothing (E.Chunks []) | otherwise = do -- liftIO $ putStrLn "Got an IEX Event..." liftIO $ writeChan ch $ IEE $ EnumeratorXML $ (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 "FeaturesTODO" | 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 = "" ++ (elementsToString $ presencePayload p) ++ "" 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 "" ++ (elementToString (Just p)) ++ "" 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 "" ++ (elementToString (Just p)) ++ "" 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 "" ++ (elementToString p) ++ "" 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 = "" ++ (elementsToString $ messagePayload m) ++ "" 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) ++ "'" 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 -- TODO: Special case 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" -- TODO: Special case presenceTypeToString Subscribe = "subscribe" presenceTypeToString Subscribed = "subscribed" presenceTypeToString Unsubscribe = "unsubscribe" presenceTypeToString Unsubscribed = "unsubscribed" stringToMessageType "chat" = Chat stringToMessageType "error" = Error_ stringToMessageType "groupchat" = Groupchat stringToMessageType "headline" = Headline stringToMessageType "normal" = Normal stringToMessageType s = OtherMessageType s messageTypeToString Chat = "chat" messageTypeToString Error_ = "error" messageTypeToString Groupchat = "groupchat" messageTypeToString Headline = "headline" messageTypeToString Normal = "normal" messageTypeToString (OtherMessageType s) = s