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.
567 lines
17 KiB
567 lines
17 KiB
-- Copyright © 2010-2011 Jon Kristensen. See the LICENSE file in the Pontarius |
|
-- XMPP distribution for more details. |
|
|
|
{-# OPTIONS_HADDOCK hide #-} |
|
|
|
module Network.XMPP.Stream ( |
|
isTLSSecured, |
|
xmlEnumerator, |
|
xmlReader, |
|
presenceToXML, |
|
iqToXML, |
|
messageToXML, |
|
parsePresence, |
|
parseIQ, |
|
parseMessage, |
|
langTag, |
|
versionFromString, |
|
versionFromNumbers |
|
) where |
|
|
|
import Network.XMPP.Address hiding (fromString) |
|
import qualified Network.XMPP.Address as X |
|
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 hiding (Version) |
|
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(..)) |
|
|
|
import Text.Parsec (char, count, digit, eof, many, many1, oneOf, parse) |
|
import Text.Parsec.ByteString (GenParser) |
|
|
|
import qualified Data.ByteString.Char8 as DBC (pack) |
|
|
|
import Data.List (intersperse) |
|
import Data.Char (toLower) |
|
|
|
|
|
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 = "<presence" ++ from ++ id' ++ to ++ type' ++ ">" ++ |
|
(elementsToString $ presencePayload p) ++ "</presence>" |
|
where |
|
from :: String |
|
from = case presenceFrom p of |
|
-- TODO: Lower-case |
|
Just s -> " from='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
id' :: String |
|
id' = case presenceID p of |
|
Just (SID s) -> " id='" ++ s ++ "'" |
|
Nothing -> "" |
|
|
|
to :: String |
|
to = case presenceTo p of |
|
-- TODO: Lower-case |
|
Just s -> " to='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
type' :: String |
|
type' = case presenceType p of |
|
Available -> "" |
|
t -> " type='" ++ (presenceTypeToString t) ++ "'" |
|
|
|
iqToXML :: IQ -> String |
|
iqToXML (IQReq (IQGet { iqRequestID = i, iqRequestPayload = p, iqRequestFrom = f, iqRequestTo = t })) = |
|
let type' = " type='get'" in "<iq" ++ from ++ id' ++ to ++ type' ++ ">" ++ (elementToString (Just p)) ++ "</iq>" |
|
where |
|
from :: String |
|
from = case f of |
|
-- TODO: Lower-case |
|
Just s -> " from='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
id' :: String |
|
id' = case i of |
|
Just (SID s) -> " id='" ++ s ++ "'" |
|
Nothing -> "" |
|
|
|
to :: String |
|
to = case t of |
|
-- TODO: Lower-case |
|
Just s -> " to='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
iqToXML (IQReq (IQSet { iqRequestID = i, iqRequestPayload = p, iqRequestFrom = f, iqRequestTo = t })) = |
|
let type' = " type='set'" in "<iq" ++ from ++ id' ++ to ++ type' ++ ">" ++ (elementToString (Just p)) ++ "</iq>" |
|
where |
|
from :: String |
|
from = case f of |
|
-- TODO: Lower-case |
|
Just s -> " from='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
id' :: String |
|
id' = case i of |
|
Just (SID s) -> " id='" ++ s ++ "'" |
|
Nothing -> "" |
|
|
|
to :: String |
|
to = case t of |
|
-- TODO: Lower-case |
|
Just s -> " to='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
iqToXML (IQRes (IQResult { iqResponseID = i, iqResponsePayload = p, iqResponseFrom = f, iqResponseTo = t })) = |
|
let type' = " type='result'" in "<iq" ++ from ++ id' ++ to ++ type' ++ ">" ++ (elementToString p) ++ "</iq>" |
|
where |
|
from :: String |
|
from = case f of |
|
-- TODO: Lower-case |
|
Just s -> " from='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
id' :: String |
|
id' = case i of |
|
Just (SID s) -> " id='" ++ s ++ "'" |
|
Nothing -> "" |
|
|
|
to :: String |
|
to = case t of |
|
-- TODO: Lower-case |
|
Just s -> " to='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
-- TODO: Turn message errors into XML. |
|
|
|
messageToXML :: Message -> String |
|
messageToXML Message { messageID = i, messageFrom = f, messageTo = t, messagePayload = p, messageType = ty } = "<message" ++ from ++ id' ++ to ++ type' ++ ">" ++ |
|
(elementsToString $ p) ++ "</message>" |
|
where |
|
from :: String |
|
from = case f of |
|
-- TODO: Lower-case |
|
Just s -> " from='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
id' :: String |
|
id' = case i of |
|
Just (SID s) -> " id='" ++ s ++ "'" |
|
Nothing -> "" |
|
|
|
to :: String |
|
to = case t of |
|
-- TODO: Lower-case |
|
Just s -> " to='" ++ (show s) ++ "'" |
|
Nothing -> "" |
|
|
|
type' :: String |
|
type' = case ty of |
|
Normal -> "" |
|
t -> " type='" ++ (messageTypeToString t) ++ "'" |
|
|
|
|
|
parseIQ :: Element -> IQ |
|
parseIQ e | typeAttr == "get" = let (Just payloadMust) = payload |
|
in IQReq (IQGet idAttr fromAttr toAttr Nothing |
|
payloadMust) |
|
| typeAttr == "set" = let (Just payloadMust) = payload |
|
in IQReq (IQSet idAttr fromAttr toAttr Nothing |
|
payloadMust) |
|
| typeAttr == "result" = IQRes (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 Address |
|
fromAttr = case attributeText fromName e of |
|
Nothing -> Nothing |
|
Just a -> X.fromString $ DT.unpack a |
|
|
|
toAttr :: Maybe Address |
|
toAttr = case attributeText toName e of |
|
Nothing -> Nothing |
|
Just a -> X.fromString $ 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 Address |
|
fromAttr = case attributeText fromName e of |
|
Nothing -> Nothing |
|
Just a -> X.fromString $ DT.unpack a |
|
|
|
toAttr :: Maybe Address |
|
toAttr = case attributeText toName e of |
|
Nothing -> Nothing |
|
Just a -> X.fromString $ 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 Address |
|
fromAttr = case attributeText fromName e of |
|
Nothing -> Nothing |
|
Just a -> X.fromString $ DT.unpack a |
|
|
|
toAttr :: Maybe Address |
|
toAttr = case attributeText toName e of |
|
Nothing -> Nothing |
|
Just a -> X.fromString $ 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 "available" = Available -- TODO: Some client sent this |
|
|
|
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 |
|
|
|
|
|
data Version = Version { majorVersion :: Integer |
|
, minorVersion :: Integer } deriving (Eq) |
|
|
|
|
|
-- Version numbers are displayed as "<major>.<minor>". |
|
|
|
instance Show Version where |
|
show (Version major minor) = (show major) ++ "." ++ (show minor) |
|
|
|
|
|
-- 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 |
|
|
|
|
|
-- Converts a "<major>.<minor>" numeric version number to a "Version" object. |
|
|
|
versionFromString :: String -> Maybe Version |
|
|
|
versionFromString s = case parse version "" (DBC.pack s) of |
|
Right version -> Just version |
|
Left _ -> Nothing |
|
|
|
|
|
-- Constructs a "Version" based on the major and minor version numbers. |
|
|
|
versionFromNumbers :: Integer -> Integer -> Version |
|
|
|
versionFromNumbers major minor = Version major minor |
|
|
|
|
|
version :: GenParser Char st Version |
|
|
|
version = do |
|
|
|
-- Read numbers, a dot, more numbers, and end-of-file. |
|
major <- many1 digit |
|
char '.' |
|
minor <- many1 digit |
|
eof |
|
return $ Version (read major) (read minor) |
|
|
|
|
|
data LangTag = LangTag { primaryTag :: String |
|
, subtags :: [String] } |
|
|
|
|
|
-- Displays the language tag in the form of "en-US". |
|
|
|
instance Show LangTag where |
|
show (LangTag p []) = p |
|
show (LangTag p s) = p ++ "-" ++ (concat $ intersperse "-" s) |
|
|
|
|
|
-- Two language tags are considered equal of they contain the same tags (case-insensitive). |
|
|
|
instance Eq LangTag where |
|
(LangTag ap as) == (LangTag bp bs) |
|
| length as == length bs && map toLower ap == map toLower bp = all (\ (a, b) -> map toLower a == map toLower b) $ zip as bs |
|
| otherwise = False |
|
|
|
|
|
-- | |
|
-- Parses, validates, and possibly constructs a "LangTag" object. |
|
|
|
langTag :: String -> Maybe LangTag |
|
|
|
langTag s = case parse languageTag "" (DBC.pack s) of |
|
Right tag -> Just tag |
|
Left _ -> Nothing |
|
|
|
|
|
-- Parses a language tag as defined by RFC 1766 and constructs a LangTag object. |
|
|
|
languageTag :: GenParser Char st LangTag |
|
|
|
languageTag = do |
|
|
|
-- Read until we reach a '-' character, or EOF. This is the `primary tag'. |
|
primTag <- tag |
|
|
|
-- Read zero or more subtags. |
|
subTags <- subtags |
|
eof |
|
|
|
return $ LangTag primTag subTags |
|
where |
|
|
|
subtags :: GenParser Char st [String] |
|
subtags = many $ do |
|
char '-' |
|
subtag <- tag |
|
return subtag |
|
|
|
tag :: GenParser Char st String |
|
tag = do |
|
a <- many1 $ oneOf tagChars |
|
return a |
|
|
|
tagChars :: [Char] |
|
tagChars = ['a'..'z'] ++ ['A'..'Z']
|
|
|