36 changed files with 2084 additions and 1562 deletions
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
dist/ |
||||
cabal-dev/ |
||||
*.o |
||||
*.hi |
||||
*~ |
||||
*# |
||||
*.#* |
||||
*_flymake.hs |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
[submodule "xml-types-pickle"] |
||||
path = xml-types-pickle |
||||
url = git@github.com:Philonous/xml-types-pickle.git |
||||
@ -1,89 +0,0 @@
@@ -1,89 +0,0 @@
|
||||
-- Copyright © 2010-2012 Jon Kristensen. See the LICENSE file in the |
||||
-- Pontarius distribution for more details. |
||||
|
||||
-- | |
||||
-- Module: $Header$ |
||||
-- Description: Pontarius API |
||||
-- Copyright: Copyright © 2010-2012 Jon Kristensen |
||||
-- License: Apache License 2.0 |
||||
-- |
||||
-- Maintainer: jon.kristensen@nejla.com |
||||
-- Stability: unstable |
||||
-- Portability: portable |
||||
-- |
||||
-- XMPP is an open standard, extendable, and secure communications |
||||
-- protocol designed on top of XML, TLS, and SASL. Pontarius XMPP is |
||||
-- an XMPP client library, implementing the core capabilities of XMPP |
||||
-- (RFC 6120). |
||||
-- |
||||
-- Developers using this library are assumed to understand how XMPP |
||||
-- works. |
||||
-- |
||||
-- This module will be documented soon. |
||||
-- |
||||
-- Note that we are not recommending anyone to use Pontarius XMPP at |
||||
-- this time as it's still in an experimental stage and will have its |
||||
-- API and data types modified frequently. |
||||
|
||||
module Network.XMPP ( -- Network.XMPP.JID |
||||
Address (..) |
||||
, Localpart |
||||
, Domainpart |
||||
, Resourcepart |
||||
, isFull |
||||
, isBare |
||||
, fromString |
||||
, fromStrings |
||||
|
||||
-- Network.XMPP.Session |
||||
, runXMPPT |
||||
, hookStreamsOpenedEvent |
||||
, hookDisconnectedEvent |
||||
, destroy |
||||
, openStreams |
||||
, create |
||||
|
||||
-- , ClientHandler (..) |
||||
-- , ClientState (..) |
||||
-- , ConnectResult (..) |
||||
-- , HostName |
||||
-- , Password |
||||
-- , PortNumber |
||||
-- , Resource |
||||
-- , Session |
||||
-- , TerminationReason |
||||
-- , UserName |
||||
-- , sendIQ |
||||
-- , sendPresence |
||||
-- , sendMessage |
||||
-- , connect |
||||
-- , openStreams |
||||
-- , tlsSecureStreams |
||||
-- , authenticate |
||||
-- , session |
||||
-- , OpenStreamResult (..) |
||||
-- , SecureWithTLSResult (..) |
||||
-- , AuthenticateResult (..) |
||||
|
||||
-- Network.XMPP.Stanza |
||||
, StanzaID (SID) |
||||
, From |
||||
, To |
||||
, LangTag |
||||
, MessageType (..) |
||||
, Message (..) |
||||
, PresenceType (..) |
||||
, Presence (..) |
||||
, IQ (..) |
||||
, iqPayloadNamespace |
||||
, iqPayload ) where |
||||
|
||||
import Network.XMPP.Address |
||||
-- import Network.XMPP.SASL |
||||
import Network.XMPP.Session |
||||
import Network.XMPP.Stanza |
||||
import Network.XMPP.Utilities |
||||
import Network.XMPP.Types |
||||
-- import Network.XMPP.TLS |
||||
import Network.XMPP.Stream |
||||
|
||||
@ -1,172 +0,0 @@
@@ -1,172 +0,0 @@
|
||||
-- Copyright © 2010-2012 Jon Kristensen. See the LICENSE file in the |
||||
-- Pontarius distribution for more details. |
||||
|
||||
{-# OPTIONS_HADDOCK hide #-} |
||||
|
||||
-- TODO: Make it possible to include host. |
||||
-- TODO: Host is assumed to be ISO 8859-1; make list of assumptions. |
||||
-- TODO: Can it contain newline characters? |
||||
|
||||
module Network.XMPP.SASL (replyToChallenge, saltedPassword, clientKey, storedKey, authMessage, clientSignature, clientProof, serverKey, serverSignature) where |
||||
|
||||
import Prelude hiding (concat, zipWith) |
||||
import Data.ByteString.Internal (c2w) |
||||
import Data.Char (isLatin1) |
||||
import Data.Digest.Pure.MD5 |
||||
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.List as DL |
||||
import Data.Text (empty, singleton) |
||||
import Text.StringPrep (StringPrepProfile (..), a1, b1, c12, c21, c22, c3, c4, c5, c6, c7, c8, c9, runStringPrep) |
||||
import Data.Ranges (inRanges, ranges) |
||||
|
||||
import Crypto.HMAC (MacKey (MacKey), hmac) |
||||
import Crypto.Hash.SHA1 (SHA1, hash) |
||||
import Data.Bits (xor) |
||||
import Data.ByteString () |
||||
import Data.ByteString.Lazy (ByteString, concat, fromChunks, pack, toChunks, zipWith) |
||||
import Data.Serialize (Serialize, encodeLazy) |
||||
import Data.Serialize.Put (putWord32be, runPutLazy) |
||||
|
||||
import Data.Maybe (fromJust, isJust) |
||||
|
||||
import qualified Data.Text as DT |
||||
|
||||
import Text.StringPrep (runStringPrep) |
||||
|
||||
data Challenge1Error = C1MultipleCriticalAttributes | |
||||
C1NotAllParametersPresent | |
||||
C1SomeParamtersPresentMoreThanOnce | |
||||
C1WrongRealm | |
||||
C1UnsupportedAlgorithm | |
||||
C1UnsupportedCharset | |
||||
C1UnsupportedQOP |
||||
deriving Show |
||||
|
||||
|
||||
-- Will produce a list of key-value pairs given a string in the format of |
||||
-- realm="somerealm",nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8... |
||||
stringToList :: String -> [(String, String)] |
||||
stringToList "" = [] |
||||
stringToList s' = let (next, rest) = break' s' ',' |
||||
in break' next '=' : stringToList rest |
||||
where |
||||
-- Like break, but will remove the first char of the continuation, if |
||||
-- present. |
||||
break' :: String -> Char -> (String, String) |
||||
break' s' c = let (first, second) = break ((==) c) s' |
||||
in (first, removeCharIfPresent second c) |
||||
|
||||
-- Removes the first character, if present; "=hello" with '=' becomes |
||||
-- "hello". |
||||
removeCharIfPresent :: String -> Char -> String |
||||
removeCharIfPresent [] _ = [] |
||||
removeCharIfPresent (c:t) c' | c == c' = t |
||||
removeCharIfPresent s' c = s' |
||||
|
||||
-- Counts the number of directives in the pair list. |
||||
countDirectives :: String -> [(String, String)] -> Int |
||||
countDirectives v l = DL.length $ filter (isEntry v) l |
||||
where |
||||
isEntry :: String -> (String, String) -> Bool |
||||
isEntry name (name', _) | name == name' = True |
||||
| otherwise = False |
||||
|
||||
|
||||
-- Returns the given directive in the list of pairs, or Nothing. |
||||
lookupDirective :: String -> [(String, String)] -> Maybe String |
||||
lookupDirective d [] = Nothing |
||||
lookupDirective d ((d', v):t) | d == d' = Just v |
||||
| otherwise = lookupDirective d t |
||||
|
||||
|
||||
-- Returns the given directive in the list of pairs, or the default value |
||||
-- otherwise. |
||||
lookupDirectiveWithDefault :: String -> [(String, String)] -> String -> String |
||||
lookupDirectiveWithDefault di l de |
||||
| lookup == Nothing = de |
||||
| otherwise = let Just r = lookup in r |
||||
where |
||||
lookup = lookupDirective di l |
||||
|
||||
|
||||
-- Implementation of "Hi()" as specified in the Notation section of RFC 5802 |
||||
-- ("SCRAM"). It takes a string "str", a salt, and an interation count, and |
||||
-- returns an octet string. The iteration count must be greater than zero. |
||||
|
||||
hi :: ByteString -> ByteString -> Integer -> ByteString |
||||
|
||||
hi str salt i | i > 0 = xorUs $ us (concat [salt, runPutLazy $ putWord32be 1]) i |
||||
where |
||||
|
||||
-- Calculates the U's (U1 ... Ui) using the HMAC algorithm |
||||
us :: ByteString -> Integer -> [ByteString] |
||||
us a 1 = [encodeLazy (hmac (MacKey (head $ toChunks str)) a :: SHA1)] |
||||
us a x = [encodeLazy (hmac (MacKey (head $ toChunks str)) a :: SHA1)] ++ (us (encodeLazy (hmac (MacKey (head $ toChunks str)) a :: SHA1)) (x - 1)) |
||||
|
||||
-- XORs the ByteStrings: U1 XOR U2 XOR ... XOR Ui |
||||
xorUs :: [ByteString] -> ByteString |
||||
xorUs (b:bs) = foldl (\ x y -> pack $ zipWith xor x y) b bs |
||||
|
||||
|
||||
saltedPassword :: String -> ByteString -> Integer -> Maybe ByteString |
||||
|
||||
saltedPassword password salt i = if isJust password' then Just $ hi (DBLC.pack $ DT.unpack $ fromJust password') salt i else Nothing |
||||
where |
||||
password' = runStringPrep saslprepProfile (DT.pack password) |
||||
|
||||
clientKey :: ByteString -> ByteString |
||||
|
||||
clientKey sp = encodeLazy (hmac (MacKey (head $ toChunks sp)) (DBLC.pack "Client Key") :: SHA1) |
||||
|
||||
|
||||
storedKey :: ByteString -> ByteString |
||||
|
||||
storedKey ck = fromChunks [hash $ head $ toChunks ck] |
||||
|
||||
|
||||
authMessage :: String -> String -> String -> ByteString |
||||
|
||||
authMessage cfmb sfm cfmwp = DBLC.pack $ cfmb ++ "," ++ sfm ++ "," ++ cfmwp |
||||
|
||||
|
||||
clientSignature :: ByteString -> ByteString -> ByteString |
||||
|
||||
clientSignature sk am = encodeLazy (hmac (MacKey (head $ toChunks sk)) am :: SHA1) |
||||
|
||||
|
||||
clientProof :: ByteString -> ByteString -> ByteString |
||||
|
||||
clientProof ck cs = pack $ zipWith xor ck cs |
||||
|
||||
|
||||
serverKey :: ByteString -> ByteString |
||||
|
||||
serverKey sp = encodeLazy (hmac (MacKey (head $ toChunks sp)) (DBLC.pack "Server Key") :: SHA1) |
||||
|
||||
|
||||
serverSignature :: ByteString -> ByteString -> ByteString |
||||
|
||||
serverSignature servkey am = encodeLazy (hmac (MacKey (head $ toChunks servkey)) am :: SHA1) |
||||
|
||||
|
||||
-- TODO: Implement SCRAM. |
||||
|
||||
replyToChallenge = replyToChallenge |
||||
|
||||
|
||||
-- Stripts the quotations around a string, if any; "\"hello\"" becomes "hello". |
||||
|
||||
stripQuotations :: String -> String |
||||
stripQuotations "" = "" |
||||
stripQuotations s | (head s == '"') && (last s == '"') = tail $ init s |
||||
| otherwise = s |
||||
|
||||
|
||||
saslprepProfile :: StringPrepProfile |
||||
|
||||
saslprepProfile = Profile { maps = [\ char -> if char `inRanges` (ranges c12) then singleton '\x0020' else singleton char, b1] |
||||
, shouldNormalize = True |
||||
, prohibited = [a1] ++ [c12, c21, c22, c3, c4, c5, c6, c7, c8, c9] |
||||
, shouldCheckBidi = True } |
||||
@ -1,372 +0,0 @@
@@ -1,372 +0,0 @@
|
||||
-- Copyright © 2010-2012 Jon Kristensen. See the LICENSE file in the |
||||
-- Pontarius distribution for more details. |
||||
|
||||
|
||||
-- TODO: Predicates on callbacks? |
||||
-- TODO: . vs $ |
||||
-- TODO: type XMPP = XMPPT IO? + runXMPP |
||||
|
||||
|
||||
{-# LANGUAGE ExistentialQuantification #-} |
||||
{-# LANGUAGE MultiParamTypeClasses #-} |
||||
|
||||
|
||||
module Network.XMPP.Session ( |
||||
XMPPT (runXMPPT) |
||||
, hookStreamsOpenedEvent |
||||
, hookDisconnectedEvent |
||||
, destroy |
||||
, openStreams |
||||
, create |
||||
, DisconnectReason |
||||
) where |
||||
|
||||
import Network.XMPP.Stream |
||||
import Network.XMPP.Types |
||||
import Network.XMPP.Utilities |
||||
|
||||
import Control.Concurrent (Chan, forkIO, forkOS, newChan, readChan, writeChan) |
||||
import Control.Monad.IO.Class (MonadIO, liftIO) |
||||
import Data.Certificate.X509 (X509) |
||||
import Data.Dynamic (Dynamic) |
||||
-- import Control.Monad.Reader (MonadReader, ReaderT, ask) |
||||
import Control.Monad.State.Lazy (MonadState, StateT, get, put, execStateT) |
||||
|
||||
import qualified Control.Exception as CE |
||||
import qualified Network as N |
||||
import System.IO (BufferMode, BufferMode(NoBuffering)) |
||||
import GHC.IO.Handle (Handle, hPutStr, hFlush, hSetBuffering, hWaitForInput) |
||||
import Codec.Binary.UTF8.String |
||||
|
||||
|
||||
|
||||
create :: MonadIO m => XMPPT m () -> m () |
||||
|
||||
create main = do |
||||
chan <- liftIO $ newChan |
||||
idGen <- liftIO $ idGenerator "" |
||||
execStateT (runXMPPT init) (State chan idGen []) |
||||
return () |
||||
where |
||||
init = do |
||||
main |
||||
stateLoop |
||||
|
||||
|
||||
-- Internal events - events to be processed within Pontarius. |
||||
|
||||
-- data InternalEvent s m = IEC (ClientEvent s m) | IEE EnumeratorEvent | IET (TimeoutEvent s m) deriving (Show) |
||||
|
||||
|
||||
instance Show (InternalEvent m) where |
||||
show _ = "InternalEvent" |
||||
|
||||
-- | |
||||
-- Events that may be emitted from Pontarius. |
||||
|
||||
data Event = -- ConnectedEvent (Either IntFailureReason Resource) |
||||
{-|-} OpenedStreamsEvent (Maybe OpenStreamsFailureReason) |
||||
-- | TLSSecuredEvent (Maybe TLSSecuringFailureReason) |
||||
-- | AuthenticatedEvent (Either AuthenticationFailureReason Resource) |
||||
| DisconnectedEvent DisconnectReason |
||||
-- | MessageEvent (Either MessageError Message) |
||||
-- | PresenceEvent (Either PresenceError Presence) |
||||
-- | IQEvent (Either IQResult IQRequest) |
||||
-- | forall a. Dynamic a => DynamicEvent a |
||||
deriving (Show) |
||||
|
||||
-- data DynamicEvent = forall a. Dynamic a => DynamicEvent a |
||||
-- data DynamicEvent = DynamicEvent Dynamic |
||||
|
||||
|
||||
-- data ConnectedFailureReason |
||||
-- = COSFR OpenStreamsFailureReason |
||||
-- | CTSFR TLSSecureFailureReason |
||||
-- | CAFR AuthenticateFailureReason |
||||
|
||||
|
||||
-- The "hook modification" events have a higher priority than other events, and |
||||
-- are thus sent through a Chan of their own. The boolean returns value signals |
||||
-- whether or not the hook should be removed. |
||||
|
||||
-- data HookModification m |
||||
-- = MonadIO m => -- RegisterConnectedHook (ConnectedEvent -> XMPPT m Bool) (Maybe (ConnectedEvent -> Bool)) |
||||
-- | RegisterTLSSecuredHook (TLSSecuredEvent -> XMPPT m Bool) (Maybe (TLSSecuredEvent -> Bool)) |
||||
-- | RegisterAuthenticatedHook (AuthenticatedEvent -> XMPPT m Bool) (Maybe (AuthenticatedEvent -> Bool)) |
||||
-- -- | forall a. Dynamic a => RegisterDynamicHook (DynamicEvent a -> XMPPT m Bool) |
||||
-- | RegisterDynamicHook (DynamicEvent -> XMPPT m Bool) (Maybe (DynamicEvent -> Bool)) |
||||
|
||||
|
||||
-- Reads an event from the internal event channel, processes it, |
||||
-- performs the generated impure actions, and loops. |
||||
|
||||
stateLoop :: MonadIO m => XMPPT m () |
||||
|
||||
stateLoop = do |
||||
rs <- get |
||||
event <- liftIO $ readChan $ evtChan rs |
||||
liftIO $ putStrLn $ "Processing " ++ (show event) ++ "..." |
||||
processEvent event |
||||
-- sequence_ IO actions frmo procesEvent? |
||||
stateLoop |
||||
|
||||
|
||||
-- Processes an internal event and generates a list of impure actions. |
||||
|
||||
processEvent :: MonadIO m => InternalEvent m -> XMPPT m () |
||||
|
||||
processEvent (OpenStreamsEvent h p) = openStreamAction h p |
||||
where |
||||
openStreamAction :: MonadIO m => HostName -> PortNumber -> XMPPT m () |
||||
openStreamAction h p = let p' = fromIntegral p |
||||
computation chan = do -- chan ugly |
||||
-- threadID <- |
||||
handle <- N.connectTo h (N.PortNumber p') |
||||
hSetBuffering handle NoBuffering |
||||
forkIO $ conduit chan (Left handle) -- This must be done after hSetBuffering |
||||
hPutStr handle $ encodeString "<stream:stream to='" ++ h ++ "' version='1.0' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>" -- didn't work with <?xml version='1.0'> |
||||
hFlush handle |
||||
return () |
||||
in do |
||||
rs <- get |
||||
result <- liftIO $ CE.try (computation $ evtChan rs) |
||||
case result of |
||||
Right () -> do |
||||
return () |
||||
-- -- lift $ liftIO $ putMVar (stateThreadID state) threadID |
||||
Left (CE.SomeException e) -> do -- TODO: Safe to do this? |
||||
fireStreamsOpenedEvent $ Just OpenStreamsFailureReason |
||||
-- Left error -> do |
||||
-- -- let clientState = stateClientState state |
||||
-- -- ((), clientState') <- lift $ runStateT (callback OpenStreamFailure) clientState |
||||
-- -- put $ state { stateShouldExit = True } |
||||
-- -- return $ Just e |
||||
-- return $ Just error |
||||
|
||||
|
||||
-- hookConnectedEvent :: MonadIO m => (ConnectedEvent -> XMPPT m Bool) -> (Maybe (ConnectedEvent -> Bool)) -> XMPPT m () |
||||
|
||||
-- hookConnectedEvent cb pred = ask >>= \rs -> liftIO $ writeChan (hookModChan rs) (RegisterConnectedHook cb pred) |
||||
|
||||
|
||||
-- | Hook the provided callback and (optional) predicate to the |
||||
-- "Streams Opened" event. |
||||
-- |
||||
-- The "Streams Opened" event will be fired when the stream:features element has been successfully received or an error has occurred. |
||||
|
||||
hookStreamsOpenedEvent :: MonadIO m => (Maybe OpenStreamsFailureReason -> XMPPT m Bool) -> (Maybe (Maybe OpenStreamsFailureReason -> XMPPT m Bool)) -> XMPPT m HookId |
||||
|
||||
hookStreamsOpenedEvent cb pred = do |
||||
rs <- get |
||||
hookId <- liftIO $ nextId $ hookIdGenerator rs |
||||
put $ rs { hooks = (HookId hookId, StreamsOpenedHook pred cb):hooks rs } |
||||
return $ HookId hookId |
||||
|
||||
|
||||
hookDisconnectedEvent :: MonadIO m => (DisconnectReason -> XMPPT m Bool) -> (Maybe (DisconnectReason -> XMPPT m Bool)) -> XMPPT m HookId |
||||
hookDisconnectedEvent cb pred = do |
||||
rs <- get |
||||
hookId <- liftIO $ nextId $ hookIdGenerator rs |
||||
-- TODO: Actually hook it. |
||||
return $ HookId hookId |
||||
|
||||
|
||||
-- hookTLSSecuredEvent :: MonadIO m => (TLSSecuredEvent -> XMPPT m Bool) -> (Maybe (TLSSecuredEvent -> Bool)) -> XMPPT m () |
||||
|
||||
-- hookTLSSecuredEvent cb pred = ask >>= \rs -> liftIO $ writeChan (hookModChan rs) (RegisterTLSSecuredHook cb pred) |
||||
|
||||
|
||||
-- hookAuthenticatedEvent :: MonadIO m => (AuthenticatedEvent -> XMPPT m Bool) -> (Maybe (AuthenticatedEvent -> Bool)) -> XMPPT m () |
||||
|
||||
-- hookAuthenticatedEvent cb pred = ask >>= \rs -> liftIO $ writeChan (hookModChan rs) (RegisterAuthenticatedHook cb pred) |
||||
|
||||
|
||||
-- hookDynamicEvent :: MonadIO m => (DynamicEvent -> XMPPT m Bool) -> (Maybe (DynamicEvent -> Bool)) -> XMPPT m () |
||||
|
||||
-- hookDynamicEvent cb pred = ask >>= \rs -> liftIO $ writeChan (hookModChan rs) (RegisterDynamicHook cb pred) |
||||
|
||||
|
||||
-- | Asynchronously request to open a stream to an XMPP server on the |
||||
-- given host name and port. |
||||
|
||||
openStreams :: MonadIO m => HostName -> PortNumber -> XMPPT m () |
||||
|
||||
openStreams h p = get >>= \rs -> liftIO $ writeChan (evtChan rs) (OpenStreamsEvent h p) |
||||
|
||||
|
||||
-- Like any other fire*Event function, it queries the hooks, filters |
||||
-- out the ones that are relevant, prepares them to be used with |
||||
-- processHook, and processes them. |
||||
|
||||
fireStreamsOpenedEvent :: MonadIO m => Maybe OpenStreamsFailureReason -> XMPPT m () |
||||
|
||||
fireStreamsOpenedEvent r = do |
||||
rs <- get |
||||
let hooks' = filterStreamsOpenedHooks $ hooks rs |
||||
sequence_ $ map (\(hookId, pred, cb) -> processHook hookId pred cb) $ map prepareStreamsOpenedHooks hooks' |
||||
return () |
||||
where |
||||
prepareStreamsOpenedHooks :: MonadIO m => Hook m -> (HookId, Maybe (XMPPT m Bool), XMPPT m Bool) |
||||
prepareStreamsOpenedHooks (hookId, StreamsOpenedHook pred cb) = |
||||
let pred' = case pred of |
||||
Nothing -> Nothing |
||||
Just pred'' -> Just $ pred'' r |
||||
cb' = cb r in (hookId, pred', cb') |
||||
|
||||
|
||||
-- Takes an optional predicate and a callback function, and excecutes |
||||
-- the callback function if the predicate does not exist, or exists |
||||
-- and is true, and returns True if the hook should be removed. |
||||
|
||||
processHook :: MonadIO m => HookId -> Maybe (XMPPT m Bool) -> XMPPT m Bool -> XMPPT m () |
||||
|
||||
processHook id pred cb = do |
||||
remove <- processHook' |
||||
if remove then do |
||||
rs <- get |
||||
put $ rs { hooks = removeHook id (hooks rs) } |
||||
else return () |
||||
where |
||||
processHook' = case pred of |
||||
Just pred' -> do |
||||
result <- pred' |
||||
if result then cb else return False |
||||
Nothing -> cb |
||||
|
||||
|
||||
destroy = destroy |
||||
|
||||
|
||||
filterStreamsOpenedHooks :: MonadIO m => [Hook m] -> [Hook m] |
||||
|
||||
filterStreamsOpenedHooks h = filter pred h |
||||
where |
||||
pred (_, StreamsOpenedHook _ _) = True |
||||
pred _ = False |
||||
|
||||
|
||||
removeHook :: MonadIO m => HookId -> [Hook m] -> [Hook m] |
||||
|
||||
removeHook id h = filter (\(id', _) -> id' /= id) h |
||||
|
||||
|
||||
-- tlsSecure = tlsSecure |
||||
|
||||
-- authenticate = authenticate |
||||
|
||||
|
||||
-- fireConnectedEvent = fireConnectedEvent |
||||
|
||||
|
||||
-- | |
||||
-- connect is implemented using hookStreamOpenedEvent, hookTLSSecuredEvent, and |
||||
-- hookAuthenticatedEvent, and is offered as a convenience function for clients |
||||
-- that doesn't need to perform any XMPP actions in-between opening the streams |
||||
-- and TLS securing the stream and\/or authenticating, allowing them to listen |
||||
-- for and manage one event instead of up to three. Just-values in the third and |
||||
-- fourth parameters will make connect TLS secure the stream and authenticate, |
||||
-- respectively. Most clients will want to hook to the Connected event using |
||||
-- hookConnectedEvent prior to using this function. |
||||
-- |
||||
-- The ConnectedEvent and StreamOpenedEvent are guaranteed to be generated upon |
||||
-- calling this function. So will a subset of the TLSSecuredEvent and |
||||
-- AuthenticatedEvent, depending on whether their functionalities are requested |
||||
-- using Just-values in the third and fourth parameters. |
||||
-- |
||||
-- connect is designed with the assupmtion that openStreams, tlsSecure, and |
||||
-- authenticate will not be used by the client. Calling those functions may |
||||
-- generate events that can cause connect to behave incorrectly. |
||||
|
||||
-- connect :: MonadIO m => HostName -> PortNumber -> Maybe (Maybe [X509], ([X509] -> Bool)) -> Maybe (UserName, Password, Maybe Resource) -> XMPPT m () |
||||
-- |
||||
-- connect h p Nothing Nothing = do |
||||
-- hookStreamsOpenedEvent onStreamsOpenedEvent Nothing |
||||
-- openStreams h p |
||||
-- |
||||
-- where |
||||
-- |
||||
-- onStreamsOpenedEvent Nothing = do |
||||
-- fireConnectedEvent Nothing |
||||
-- return False |
||||
-- |
||||
-- onStreamsOpenedEvent (Just e) = do |
||||
-- fireConnectedEvent $ Left $ COSFR e |
||||
-- return False |
||||
-- |
||||
-- connect h p (Just t) Nothing = do |
||||
-- hookStreamsOpenedEvent onStreamsOpenedEvent Nothing |
||||
-- openStreams h p |
||||
-- |
||||
-- where |
||||
-- |
||||
-- onStreamsOpenedEvent Nothing = do |
||||
-- hookTLSSecuredEvent onTLSSecuredEvent Nothing |
||||
-- tlsSecure |
||||
-- return False |
||||
-- |
||||
-- onStreamsOpenedEvent (Just e) = do |
||||
-- fireConnectedEvent $ Left $ COSFR e |
||||
-- return False |
||||
-- |
||||
-- onTLSSecuredEvent Nothing = do |
||||
-- fireConnectedEvent Nothing |
||||
-- return False |
||||
-- |
||||
-- onTLSSecuredEvent (Just e) = do |
||||
-- fireConnectedEvent $ Left $ CTSFR e |
||||
-- return False |
||||
-- |
||||
-- connect h p Nothing (Just a) = do |
||||
-- hookStreamsOpenedEvent onStreamsOpenedEvent Nothing |
||||
-- openStreams h p |
||||
-- |
||||
-- where |
||||
-- |
||||
-- onStreamsOpenedEvent Nothing = do |
||||
-- hookAuthenticatedEvent onAuthenticatedEvent Nothing |
||||
-- authenticate |
||||
-- return False |
||||
-- |
||||
-- onStreamsOpenedEvent (Just e) = do |
||||
-- fireConnectedEvent $ Left $ COSFR e |
||||
-- return False |
||||
-- |
||||
-- onAuthenticatedEvent (Right r) = do |
||||
-- fireConnectedEvent $ Just r |
||||
-- return False |
||||
-- |
||||
-- onAuthenticated (Left e) = do |
||||
-- fireConnectedEvent $ Left $ CAFR e |
||||
-- return False |
||||
-- |
||||
-- connect h p (Just t) (Just a) = do |
||||
-- hookStreamsOpenedEvent onStreamsOpenedEvent Nothing |
||||
-- openStreams h p |
||||
-- |
||||
-- where |
||||
-- |
||||
-- onStreamsOpenedEvent Nothing = do |
||||
-- hookTLSSecuredEvent onTLSSecuredEvent Nothing |
||||
-- tlsSecure |
||||
-- return False |
||||
-- |
||||
-- onStreamsOpenedEvent (Just e) = do |
||||
-- fireConnectedEvent $ Left $ COSFR e |
||||
-- return False |
||||
-- |
||||
-- onTLSSecuredEvent Nothing = do |
||||
-- hookAuthenticatedEvent onAuthenticatedEvent Nothing |
||||
-- authenticate |
||||
-- return False |
||||
-- |
||||
-- onTLSSecuredEvent (Just e) = do |
||||
-- fireConnectedEvent $ Left $ CTSFR e |
||||
-- return False |
||||
-- |
||||
-- onAuthenticatedEvent (Right r) = do |
||||
-- fireConnectedEvent $ Just r |
||||
-- return False |
||||
-- |
||||
-- onAuthenticated (Left e) = do |
||||
-- fireConnectedEvent $ Left $ CAFR e |
||||
-- return False |
||||
@ -1,543 +0,0 @@
@@ -1,543 +0,0 @@
|
||||
-- Copyright © 2010-2012 Jon Kristensen. See the LICENSE file in the |
||||
-- Pontarius distribution for more details. |
||||
|
||||
{-# OPTIONS_HADDOCK hide #-} |
||||
|
||||
{-# LANGUAGE OverloadedStrings #-} |
||||
|
||||
module Network.XMPP.Stream ( |
||||
conduit, |
||||
presenceToXML, |
||||
iqToXML, |
||||
messageToXML, |
||||
parsePresence, |
||||
parseIQ, |
||||
parseMessage, |
||||
langTag, |
||||
versionFromString, |
||||
versionFromNumbers |
||||
) where |
||||
|
||||
import Network.XMPP.Types hiding (Continue) |
||||
|
||||
import Prelude hiding (null) |
||||
|
||||
import Control.Concurrent.Chan (Chan, writeChan) |
||||
import Control.Exception.Base (SomeException) |
||||
import Control.Monad.IO.Class (liftIO) |
||||
import Data.ByteString.Lazy (null, toChunks) |
||||
import Data.Conduit (($$), ($=), MonadResource, Sink (..), runResourceT) |
||||
import Data.Conduit.Binary (sourceHandle) |
||||
import Data.Maybe (fromJust, isJust) |
||||
import Data.Text (pack, unpack) |
||||
import Data.XML.Types (Content (..), Document (..), Element (..), Event (..), Name (..), Node (..)) |
||||
import GHC.IO.Handle (Handle) |
||||
import Network.TLS (TLSCtx, recvData) |
||||
import Text.Parsec (char, count, digit, eof, many, many1, oneOf, parse) |
||||
import Text.Parsec.ByteString (GenParser) |
||||
-- import Text.XML.Enumerator.Document (fromEvents) |
||||
import Text.XML.Stream.Parse (def, parseBytes) |
||||
import Text.XML.Unresolved (fromEvents) |
||||
|
||||
import Control.Monad.IO.Class (MonadIO, liftIO) |
||||
|
||||
|
||||
import qualified Data.ByteString as DB (ByteString) |
||||
import qualified Data.ByteString.Char8 as DBC (pack) |
||||
import qualified Data.Conduit.List as DEL (head) |
||||
import Data.Conduit.List (consume, sourceList) -- use lazy consume instead? |
||||
|
||||
|
||||
-- Reads from the provided handle or TLS context and sends the events |
||||
-- to the internal event channel. |
||||
|
||||
conduit :: MonadIO m => Chan (InternalEvent m) -> Either Handle (TLSCtx a) -> IO () |
||||
|
||||
conduit c s = do |
||||
enumeratorResult <- case s of |
||||
Left handle -> do |
||||
print <- runResourceT $ sourceHandle handle $= parseBytes def $$ DEL.head -- $$ DEL.head -- eventConsumer c [] 0 |
||||
return $ Right 0 -- TODO |
||||
Right tlsCtx -> -- run $ enumTLS tlsCtx $$ joinI $ |
||||
-- parseBytes decodeEntities $$ eventConsumer c [] 0 |
||||
return $ Left 0 -- TODO |
||||
case enumeratorResult of |
||||
Right _ -> return () -- writeChan c $ IEE EnumeratorDone |
||||
Left e -> return () -- writeChan c $ IEE (EnumeratorException e) |
||||
-- where |
||||
-- -- Behaves like enumHandle, but reads from the TLS context instead |
||||
-- -- TODO: Type? |
||||
-- enumTLS :: TLSCtx -> Enumerator DB.ByteString IO b |
||||
-- enumTLS c s = loop c s |
||||
|
||||
-- -- TODO: Type? |
||||
-- loop :: TLSCtx -> Step DB.ByteString IO b -> Iteratee DB.ByteString IO b |
||||
-- loop c (Continue k) = do |
||||
-- d <- recvData c |
||||
-- case null d of |
||||
-- True -> loop c (Continue k) |
||||
-- False -> k (Chunks $ toChunks d) >>== loop c |
||||
-- loop _ step = returnI step |
||||
|
||||
|
||||
-- Consumes XML events from the input stream, accumulating as |
||||
-- necessary, and sends the proper events through the channel. The |
||||
-- second parameter should be initialized to [] (no events) and the |
||||
-- third to 0 (zeroth XML level). |
||||
|
||||
eventConsumer :: (MonadResource r, MonadIO m) => |
||||
Chan (InternalEvent m) -> [Event] -> Int -> Sink Event r () |
||||
|
||||
-- <stream:stream> open event received. |
||||
|
||||
eventConsumer chan [EventBeginElement (Name localName namespace prefixName) attribs] 0 |
||||
| localName == pack "stream" && isJust prefixName && fromJust prefixName == pack "stream" = do |
||||
liftIO $ putStrLn "here?" |
||||
liftIO $ writeChan chan $ EnumeratorBeginStream from to id ver lang ns |
||||
eventConsumer chan [] 1 |
||||
where |
||||
from = case lookup "from" attribs of Nothing -> Nothing; Just fromAttrib -> Just $ show fromAttrib |
||||
to = case lookup "to" attribs of Nothing -> Nothing; Just toAttrib -> Just $ show toAttrib |
||||
id = case lookup "id" attribs of Nothing -> Nothing; Just idAttrib -> Just $ show idAttrib |
||||
ver = case lookup "version" attribs of Nothing -> Nothing; Just verAttrib -> Just $ show verAttrib |
||||
lang = case lookup "xml:lang" attribs of Nothing -> Nothing; Just langAttrib -> Just $ show langAttrib |
||||
ns = case namespace of Nothing -> Nothing; Just namespaceAttrib -> Just $ unpack namespaceAttrib |
||||
|
||||
-- <stream:stream> close event received. |
||||
|
||||
eventConsumer chan [EventEndElement name] 1 |
||||
| namePrefix name == Just (pack "stream") && nameLocalName name == pack "stream" = do |
||||
liftIO $ putStrLn "here!" |
||||
liftIO $ writeChan chan $ EnumeratorEndStream |
||||
return () |
||||
|
||||
-- Ignore EventDocumentBegin event. |
||||
|
||||
eventConsumer chan [EventBeginDocument] 0 = eventConsumer chan [] 0 |
||||
|
||||
-- We have received a complete first-level XML element. Process the accumulated |
||||
-- values into an first-level element event. |
||||
|
||||
eventConsumer chan ((EventEndElement e):es) 1 = do |
||||
liftIO $ putStrLn "here..." |
||||
element <- liftIO $ eventsToElement $ reverse ((EventEndElement e):es) |
||||
liftIO $ writeChan chan $ EnumeratorFirstLevelElement element |
||||
eventConsumer chan [] 1 |
||||
|
||||
-- Normal condition - accumulate the event. |
||||
|
||||
eventConsumer chan events level = do |
||||
liftIO $ putStrLn "listenering for XML event" |
||||
event <- DEL.head |
||||
liftIO $ putStrLn "got event" |
||||
case event of |
||||
Just event' -> let level' = case event' of |
||||
EventBeginElement _ _ -> level + 1 |
||||
EventEndElement _ -> level - 1 |
||||
_ -> level |
||||
in eventConsumer chan (event':events) level' |
||||
Nothing -> eventConsumer chan events level |
||||
|
||||
|
||||
eventsToElement :: [Event] -> IO Element -- Was: Either SomeException Element |
||||
|
||||
-- TODO: Exceptions. |
||||
|
||||
eventsToElement e = do |
||||
putStrLn "eventsToElement" |
||||
doc <- runResourceT $ sourceList e $$ fromEvents |
||||
return $ documentRoot doc |
||||
-- case r of Right doc -> Right $ documentRoot doc; Left ex -> Left ex |
||||
-- where |
||||
-- -- TODO: Type? |
||||
-- eventsEnum (Continue k) = k $ Chunks e |
||||
-- eventsEnum step = returnI step |
||||
|
||||
|
||||
-- Sending stanzas is done through functions, where LangTag is Maybe. |
||||
|
||||
|
||||
-- Generates an XML element for a message stanza. The language tag provided is |
||||
-- the default language of the stream. |
||||
|
||||
messageToXML :: InternalMessage -> LangTag -> Element |
||||
|
||||
-- Non-error message. |
||||
|
||||
messageToXML (Right m) streamLang = Element "message" attribs nodes |
||||
|
||||
where |
||||
|
||||
-- Has the stanza attributes and the message type. |
||||
attribs :: [(Name, [Content])] |
||||
attribs = stanzaAttribs (messageID m) (messageFrom m) (messageTo m) stanzaLang ++ |
||||
[("type", [ContentText $ pack $ show $ messageType m])] |
||||
|
||||
-- Has an arbitrary number of elements as children. |
||||
nodes :: [Node] |
||||
nodes = map (\ x -> NodeElement x) (messagePayload m) |
||||
|
||||
stanzaLang :: Maybe LangTag |
||||
stanzaLang = stanzaLang' streamLang $ messageLangTag m |
||||
|
||||
-- Presence error. |
||||
|
||||
messageToXML (Left m) streamLang = Element "message" attribs nodes |
||||
|
||||
where |
||||
|
||||
-- Has the stanza attributes and the "error" presence type. |
||||
attribs :: [(Name, [Content])] |
||||
attribs = stanzaAttribs (messageErrorID m) (messageErrorFrom m) (messageErrorTo m) |
||||
stanzaLang ++ [("type", [ContentText $ pack "error"])] |
||||
|
||||
-- Has the error element stanza as its child. |
||||
-- TODO: Include sender XML here? |
||||
nodes :: [Node] |
||||
nodes = [NodeElement $ errorElem streamLang stanzaLang $ messageErrorStanzaError m] |
||||
|
||||
-- The stanza language tag, if it's different from the stream language tag. |
||||
stanzaLang :: Maybe LangTag |
||||
stanzaLang = stanzaLang' streamLang $ messageErrorLangTag m |
||||
|
||||
|
||||
-- Generates an XML element for a presence stanza. The language tag provided is |
||||
-- the default language of the stream. |
||||
|
||||
presenceToXML :: InternalPresence -> LangTag -> Element |
||||
|
||||
-- Non-error presence. |
||||
|
||||
presenceToXML (Right p) streamLang = Element "presence" attribs nodes |
||||
|
||||
where |
||||
|
||||
-- Has the stanza attributes and the presence type. |
||||
attribs :: [(Name, [Content])] |
||||
attribs = stanzaAttribs (presenceID p) (presenceFrom p) (presenceTo p) stanzaLang ++ |
||||
typeAttrib |
||||
|
||||
-- Has an arbitrary number of elements as children. |
||||
nodes :: [Node] |
||||
nodes = map (\ x -> NodeElement x) (presencePayload p) |
||||
|
||||
stanzaLang :: Maybe LangTag |
||||
stanzaLang = stanzaLang' streamLang $ presenceLangTag p |
||||
|
||||
typeAttrib :: [(Name, [Content])] |
||||
typeAttrib = case presenceType p of Nothing -> []; Just presenceType' -> [("type", [ContentText $ pack $ show presenceType'])] |
||||
|
||||
-- Presence error. |
||||
|
||||
presenceToXML (Left p) streamLang = Element "presence" attribs nodes |
||||
|
||||
where |
||||
|
||||
-- Has the stanza attributes and the "error" presence type. |
||||
attribs :: [(Name, [Content])] |
||||
attribs = stanzaAttribs (presenceErrorID p) (presenceErrorFrom p) (presenceErrorTo p) |
||||
stanzaLang ++ [("type", [ContentText $ pack "error"])] |
||||
|
||||
-- Has the error element stanza as its child. |
||||
-- TODO: Include sender XML here? |
||||
nodes :: [Node] |
||||
nodes = [NodeElement $ errorElem streamLang stanzaLang $ presenceErrorStanzaError p] |
||||
|
||||
-- The stanza language tag, if it's different from the stream language tag. |
||||
stanzaLang :: Maybe LangTag |
||||
stanzaLang = stanzaLang' streamLang $ presenceErrorLangTag p |
||||
|
||||
|
||||
-- Generates an XML element for a presence stanza. The language tag provided is |
||||
-- the default language of the stream. |
||||
|
||||
iqToXML :: IQ -> LangTag -> Element |
||||
|
||||
-- Request IQ. |
||||
|
||||
iqToXML (Left i) streamLang = Element "iq" attribs nodes |
||||
|
||||
where |
||||
|
||||
-- Has the stanza attributes and the IQ request type (`get' or `set'). |
||||
attribs :: [(Name, [Content])] |
||||
attribs = stanzaAttribs (iqRequestID i) (iqRequestFrom i) (iqRequestTo i) |
||||
stanzaLang ++ typeAttrib |
||||
|
||||
-- Has exactly one payload child element. |
||||
nodes :: [Node] |
||||
nodes = [NodeElement $ iqRequestPayload i] |
||||
|
||||
-- The stanza language tag, if it's different from the stream language tag. |
||||
stanzaLang :: Maybe LangTag |
||||
stanzaLang = stanzaLang' streamLang $ iqRequestLangTag i |
||||
|
||||
-- The required type attribute. |
||||
typeAttrib :: [(Name, [Content])] |
||||
typeAttrib = [("type", [ContentText $ pack $ show $ iqRequestType i])] |
||||
|
||||
-- Response result IQ. |
||||
|
||||
iqToXML (Right (Right i)) streamLang = Element "iq" attribs nodes |
||||
|
||||
where |
||||
|
||||
-- Has the stanza attributes and the IQ `result' type. |
||||
attribs :: [(Name, [Content])] |
||||
attribs = stanzaAttribs (iqResultID i) (iqResultFrom i) (iqResultTo i) |
||||
stanzaLang ++ typeAttrib |
||||
|
||||
-- Has one or zero payload child elements. |
||||
nodes :: [Node] |
||||
nodes = case iqResultPayload i of Nothing -> []; Just payloadElem -> [NodeElement payloadElem] |
||||
|
||||
stanzaLang :: Maybe LangTag |
||||
stanzaLang = stanzaLang' streamLang $ iqResultLangTag i |
||||
|
||||
-- The required type attribute. |
||||
typeAttrib :: [(Name, [Content])] |
||||
typeAttrib = [("type", [ContentText $ pack "result"])] |
||||
|
||||
-- Response error IQ. |
||||
|
||||
iqToXML (Right (Left i)) streamLang = Element "iq" attribs nodes |
||||
|
||||
where |
||||
|
||||
-- Has the stanza attributes and the presence type. |
||||
attribs :: [(Name, [Content])] |
||||
attribs = stanzaAttribs (iqErrorID i) (iqErrorFrom i) (iqErrorTo i) stanzaLang ++ |
||||
typeAttrib |
||||
|
||||
-- Has an optional elements as child. |
||||
nodes :: [Node] |
||||
nodes = case iqErrorPayload i of Nothing -> []; Just payloadElem -> [NodeElement payloadElem] |
||||
|
||||
stanzaLang :: Maybe LangTag |
||||
stanzaLang = stanzaLang' streamLang $ iqErrorLangTag i |
||||
|
||||
typeAttrib :: [(Name, [Content])] |
||||
typeAttrib = [("type", [ContentText $ pack "error"])] |
||||
|
||||
|
||||
-- Creates the error element that is common for all stanzas. |
||||
|
||||
errorElem :: LangTag -> Maybe LangTag -> StanzaError -> Element |
||||
|
||||
errorElem streamLang stanzaLang stanzaError = Element "error" typeAttrib |
||||
([defCondElem] ++ textElem ++ appSpecCondElem) |
||||
|
||||
where |
||||
|
||||
-- The required stanza error type. |
||||
typeAttrib :: [(Name, [Content])] |
||||
typeAttrib = [("type", [ContentText $ pack $ show $ stanzaErrorType stanzaError])] |
||||
|
||||
-- The required defined condition element. |
||||
defCondElem :: Node |
||||
defCondElem = NodeElement $ Element (Name (pack $ show $ stanzaErrorCondition stanzaError) (Just $ pack "urn:ietf:params:xml:ns:xmpp-stanzas") Nothing) [] [] |
||||
|
||||
|
||||
-- The optional text element. |
||||
textElem :: [Node] |
||||
textElem = case stanzaErrorText stanzaError of |
||||
Nothing -> [] |
||||
Just (textLang, text) -> |
||||
[NodeElement $ Element "{urn:ietf:params:xml:ns:xmpp-stanzas}text" |
||||
(langTagAttrib $ childLang streamLang [stanzaLang, fst $ fromJust $ stanzaErrorText stanzaError]) |
||||
[NodeContent $ ContentText $ pack text]] |
||||
|
||||
-- The optional application specific condition element. |
||||
appSpecCondElem :: [Node] |
||||
appSpecCondElem = case stanzaErrorApplicationSpecificCondition stanzaError of |
||||
Nothing -> [] |
||||
Just elem -> [NodeElement elem] |
||||
|
||||
|
||||
-- Generates the element attribute for an optional language tag. |
||||
|
||||
langTagAttrib :: Maybe LangTag -> [(Name, [Content])] |
||||
|
||||
langTagAttrib lang = case lang of Nothing -> []; Just lang' -> [("xml:lang", [ContentText $ pack $ show lang'])] |
||||
|
||||
|
||||
stanzaLang' :: LangTag -> LangTag -> Maybe LangTag |
||||
|
||||
stanzaLang' streamLang stanzaLang | streamLang == stanzaLang = Nothing |
||||
| otherwise = Just stanzaLang |
||||
|
||||
|
||||
-- Finds the language tag to set on the current element, if any. Makes sure that |
||||
-- language tags are not repeated unnecessarily (like on a child element, when |
||||
-- the parent has it). The first parameter is the stream language tag, and the |
||||
-- list of optional language tags are ordered in their XML element child |
||||
-- sequence, parent first, starting with the stanza language tag. |
||||
|
||||
childLang :: LangTag -> [Maybe LangTag] -> Maybe LangTag |
||||
|
||||
childLang streamLang optLangTags |
||||
|
||||
-- The current element does not have a language tag - set nothing. |
||||
| (head $ reverse optLangTags) == Nothing = Nothing |
||||
|
||||
-- All optional language tags are Nothing - set nothing. |
||||
| length langTags == 1 = Nothing |
||||
|
||||
-- The language tag of this element is the same as the closest parent with a |
||||
-- language tag - set nothing. |
||||
| (head langTags) == (head $ tail langTags) = Nothing |
||||
|
||||
-- Set the language tag. |
||||
| otherwise = Just $ head langTags |
||||
|
||||
where |
||||
|
||||
-- Contains the chain of language tags in descending priority order. |
||||
-- Contains at least one element - the stream language tag. |
||||
langTags = reverse $ [streamLang] ++ (map fromJust $ filter (\ l -> isJust l) optLangTags) |
||||
|
||||
|
||||
-- Creates the attributes common for all stanzas. |
||||
|
||||
stanzaAttribs :: Maybe StanzaID -> Maybe From -> Maybe To -> Maybe LangTag -> [(Name, [Content])] |
||||
|
||||
stanzaAttribs i f t l = if isJust $ i then [("id", [ContentText $ pack $ show $ fromJust i])] else [] ++ |
||||
if isJust $ f then [("from", [ContentText $ pack $ show $ fromJust f])] else [] ++ |
||||
if isJust $ t then [("to", [ContentText $ pack $ show $ fromJust t])] else [] ++ |
||||
if isJust $ l then [("xml:lang", [ContentText $ pack $ show l])] else [] |
||||
|
||||
|
||||
parseIQ :: Element -> IQ |
||||
|
||||
parseIQ = parseIQ |
||||
|
||||
|
||||
parsePresence :: Element -> InternalPresence |
||||
|
||||
parsePresence = parsePresence |
||||
|
||||
|
||||
parseMessage :: Element -> InternalMessage |
||||
|
||||
parseMessage = parseMessage |
||||
|
||||
|
||||
-- Converts a string to a PresenceType. Nothing means convertion error, Just |
||||
-- Nothing means the presence error type, and Just $ Just is the PresenceType. |
||||
|
||||
stringToPresenceType :: String -> Maybe (Maybe PresenceType) |
||||
|
||||
stringToPresenceType "probe" = Just $ Just Probe |
||||
stringToPresenceType "unavailable" = Just $ Just Unavailable |
||||
stringToPresenceType "subscribe" = Just $ Just Subscribe |
||||
stringToPresenceType "subscribed" = Just $ Just Subscribed |
||||
stringToPresenceType "unsubscribe" = Just $ Just Unsubscribe |
||||
stringToPresenceType "unsubscribed" = Just $ Just Unsubscribed |
||||
stringToPresenceType "error" = Just Nothing |
||||
stringToPresenceType _ = Nothing |
||||
|
||||
|
||||
-- Converts a Maybe MessageType to a string. Nothing means "error". |
||||
|
||||
presenceTypeToString :: Maybe PresenceType -> String |
||||
|
||||
presenceTypeToString (Just Unavailable) = "unavailable" |
||||
presenceTypeToString (Just Probe) = "probe" |
||||
presenceTypeToString Nothing = "error" |
||||
presenceTypeToString (Just Subscribe) = "subscribe" |
||||
presenceTypeToString (Just Subscribed) = "subscribed" |
||||
presenceTypeToString (Just Unsubscribe) = "unsubscribe" |
||||
presenceTypeToString (Just Unsubscribed) = "unsubscribed" |
||||
|
||||
|
||||
-- Converts a string to a MessageType. Nothing means convertion error, Just |
||||
-- Nothing means the message error type, and Just $ Just is the MessageType. |
||||
|
||||
stringToMessageType :: String -> Maybe (Maybe MessageType) |
||||
|
||||
stringToMessageType "chat" = Just $ Just Chat |
||||
stringToMessageType "error" = Just $ Nothing |
||||
stringToMessageType "groupchat" = Just $ Just Groupchat |
||||
stringToMessageType "headline" = Just $ Just Headline |
||||
stringToMessageType "normal" = Just $ Just Normal |
||||
stringToMessageType _ = Nothing |
||||
|
||||
|
||||
-- Converts a Maybe MessageType to a string. Nothing means "error". |
||||
|
||||
messageTypeToString :: Maybe MessageType -> String |
||||
|
||||
messageTypeToString (Just Chat) = "chat" |
||||
messageTypeToString Nothing = "error" |
||||
messageTypeToString (Just Groupchat) = "groupchat" |
||||
messageTypeToString (Just Headline) = "headline" |
||||
messageTypeToString (Just Normal) = "normal" |
||||
|
||||
|
||||
-- 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) |
||||
|
||||
|
||||
-- | |
||||
-- 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'] |
||||
@ -1,30 +0,0 @@
@@ -1,30 +0,0 @@
|
||||
-- Copyright © 2010-2012 Jon Kristensen. See the LICENSE file in the |
||||
-- Pontarius distribution for more details. |
||||
|
||||
-- TODO: TLS12 when supported in tls; TODO: TLS11 results in a read error - bug? |
||||
-- TODO: cipher_AES128_SHA1 = TLS_RSA_WITH_AES_128_CBC_SHA? |
||||
-- TODO: Compression? |
||||
-- TODO: Validate certificate |
||||
|
||||
{-# OPTIONS_HADDOCK hide #-} |
||||
|
||||
module Network.XMPP.TLS (tlsParams) where |
||||
|
||||
import Network.TLS (TLSCertificateUsage (CertificateUsageAccept), |
||||
TLSParams (..), Version (SSL3, TLS10, TLS11), |
||||
defaultLogging, nullCompression) |
||||
import Network.TLS.Extra (cipher_AES128_SHA1) |
||||
|
||||
|
||||
tlsParams :: TLSParams |
||||
|
||||
tlsParams = TLSParams { pConnectVersion = TLS10 |
||||
, pAllowedVersions = [SSL3, TLS10,TLS11] |
||||
, pCiphers = [cipher_AES128_SHA1] |
||||
, pCompressions = [nullCompression] |
||||
, pWantClientCert = False -- Used for servers |
||||
, pUseSecureRenegotiation = False -- No renegotiation |
||||
, pCertificates = [] -- TODO |
||||
, pLogging = defaultLogging -- TODO |
||||
, onCertificatesRecv = \ certificate -> |
||||
return CertificateUsageAccept } |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
{-# Language NoMonomorphismRestriction #-} |
||||
module Data.Conduit.TLS |
||||
( tlsinit |
||||
-- , conduitStdout |
||||
, module TLS |
||||
, module TLSExtra |
||||
) |
||||
where |
||||
|
||||
import Control.Monad(liftM) |
||||
import Control.Monad.IO.Class |
||||
|
||||
import Crypto.Random |
||||
|
||||
import qualified Data.ByteString as BS |
||||
import qualified Data.ByteString.Lazy as BL |
||||
import Data.Conduit |
||||
|
||||
import Network.TLS as TLS |
||||
import Network.TLS.Extra as TLSExtra |
||||
|
||||
import System.IO(Handle) |
||||
|
||||
tlsinit |
||||
:: (MonadIO m, MonadIO m1) => |
||||
TLSParams |
||||
-> Handle -> m ( Source m1 BS.ByteString |
||||
, Sink BS.ByteString m1 () |
||||
, BS.ByteString -> IO ()) |
||||
tlsinit tlsParams handle = do |
||||
gen <- liftIO $ (newGenIO :: IO SystemRandom) -- TODO: Find better random source? |
||||
clientContext <- client tlsParams gen handle |
||||
handshake clientContext |
||||
let src = sourceState |
||||
clientContext |
||||
(\con -> StateOpen con `liftM` recvData con) |
||||
let snk = sinkState |
||||
clientContext |
||||
(\con bs -> sendData con (BL.fromChunks [bs]) |
||||
>> return (StateProcessing con)) |
||||
(\_ -> return ()) |
||||
return ( src |
||||
, snk |
||||
, \s -> sendData clientContext $ BL.fromChunks [s] ) |
||||
|
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
{-# LANGUAGE PackageImports, OverloadedStrings #-} |
||||
module Example where |
||||
|
||||
import Data.Text as T |
||||
|
||||
import Network.XMPP |
||||
import Control.Concurrent |
||||
import Control.Concurrent.STM |
||||
import Control.Monad |
||||
import Control.Monad.IO.Class |
||||
|
||||
philonous :: JID |
||||
philonous = read "uart14@species64739.dyndns.org" |
||||
|
||||
attXmpp :: STM a -> XMPPThread a |
||||
attXmpp = liftIO . atomically |
||||
|
||||
autoAccept :: XMPPThread () |
||||
autoAccept = forever $ do |
||||
st <- pullPresence |
||||
case st of |
||||
Presence from _ idq (Just Subscribe) _ _ _ _ -> |
||||
sendS . SPresence $ |
||||
Presence Nothing from idq (Just Subscribed) Nothing Nothing Nothing [] |
||||
_ -> return () |
||||
|
||||
mirror :: XMPPThread () |
||||
mirror = forever $ do |
||||
st <- pullMessage |
||||
case st of |
||||
Message (Just from) _ idq tp subject (Just bd) thr _ -> |
||||
sendS . SMessage $ |
||||
Message Nothing from idq tp subject |
||||
(Just $ "you wrote: " `T.append` bd) thr [] |
||||
_ -> return () |
||||
|
||||
|
||||
main :: IO () |
||||
main = do |
||||
sessionConnect "localhost" "species64739.dyndns.org" "bot" Nothing $ do |
||||
-- singleThreaded $ xmppStartTLS exampleParams |
||||
singleThreaded $ xmppSASL "pwd" |
||||
xmppThreadedBind (Just "botsi") |
||||
-- singleThreaded $ xmppBind (Just "botsi") |
||||
singleThreaded $ xmppSession |
||||
forkXMPP autoAccept |
||||
forkXMPP mirror |
||||
sendS . SPresence $ Presence Nothing Nothing Nothing Nothing |
||||
(Just Available) Nothing Nothing [] |
||||
sendS . SMessage $ Message Nothing philonous Nothing Nothing Nothing |
||||
(Just "bla") Nothing [] |
||||
liftIO . forever $ threadDelay 1000000 |
||||
return () |
||||
return () |
||||
|
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
-- Copyright © 2010-2012 Jon Kristensen. |
||||
-- Copyright 2012 Philipp Balzarek |
||||
-- See the LICENSE file in the |
||||
-- Pontarius distribution for more details. |
||||
|
||||
-- | |
||||
-- Module: $Header$ |
||||
-- Description: Pontarius API |
||||
-- Copyright: Copyright © 2010-2012 Jon Kristensen |
||||
-- License: Apache License 2.0 |
||||
-- |
||||
-- Maintainer: jon.kristensen@nejla.com |
||||
-- Stability: unstable |
||||
-- Portability: portable |
||||
-- |
||||
-- XMPP is an open standard, extendable, and secure communications |
||||
-- protocol designed on top of XML, TLS, and SASL. Pontarius XMPP is |
||||
-- an XMPP client library, implementing the core capabilities of XMPP |
||||
-- (RFC 6120). |
||||
-- |
||||
-- Developers using this library are assumed to understand how XMPP |
||||
-- works. |
||||
-- |
||||
-- This module will be documented soon. |
||||
-- |
||||
-- Note that we are not recommending anyone to use Pontarius XMPP at |
||||
-- this time as it's still in an experimental stage and will have its |
||||
-- API and data types modified frequently. |
||||
|
||||
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} |
||||
|
||||
module Network.XMPP |
||||
( module Network.XMPP.Bind |
||||
, module Network.XMPP.Concurrent |
||||
, module Network.XMPP.Monad |
||||
, module Network.XMPP.SASL |
||||
, module Network.XMPP.Session |
||||
, module Network.XMPP.Stream |
||||
, module Network.XMPP.TLS |
||||
, module Network.XMPP.Types |
||||
, module Network.XMPP.Presence |
||||
, module Network.XMPP.Message |
||||
, xmppConnect |
||||
, xmppNewSession |
||||
) where |
||||
|
||||
import Data.Text as Text |
||||
|
||||
import Network |
||||
import Network.XMPP.Bind |
||||
import Network.XMPP.Concurrent |
||||
import Network.XMPP.Message |
||||
import Network.XMPP.Monad |
||||
import Network.XMPP.Presence |
||||
import Network.XMPP.SASL |
||||
import Network.XMPP.Session |
||||
import Network.XMPP.Stream |
||||
import Network.XMPP.TLS |
||||
import Network.XMPP.Types |
||||
|
||||
xmppConnect :: HostName -> Text -> XMPPConMonad () |
||||
xmppConnect address hostname = xmppRawConnect address hostname >> xmppStartStream |
||||
|
||||
xmppNewSession :: XMPPThread a -> IO (a, XMPPConState) |
||||
xmppNewSession = withNewSession . runThreaded |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
{-# LANGUAGE OverloadedStrings #-} |
||||
|
||||
module Network.XMPP.Bind where |
||||
|
||||
import Data.Text as Text |
||||
|
||||
import Data.XML.Pickle |
||||
import Data.XML.Types |
||||
|
||||
import Network.XMPP.Types |
||||
import Network.XMPP.Pickle |
||||
import Network.XMPP.Concurrent |
||||
|
||||
bindP :: PU [Node] b -> PU [Node] b |
||||
bindP c = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-bind}bind" c |
||||
|
||||
bindBody :: Maybe Text -> Element |
||||
bindBody rsrc = (pickleElem |
||||
(bindP . xpOption $ xpElemNodes "resource" (xpContent xpId)) |
||||
rsrc |
||||
) |
||||
|
||||
jidP :: PU [Node] JID |
||||
jidP = bindP $ xpElemNodes "jid" (xpContent xpPrim) |
||||
|
||||
xmppThreadedBind :: Maybe Text -> XMPPThread Text |
||||
xmppThreadedBind rsrc = do |
||||
answer <- sendIQ' Nothing Set Nothing (bindBody rsrc) |
||||
let (Right IQResult{iqResultPayload = Just b}) = answer -- TODO: Error handling |
||||
let (JID _n _d (Just r)) = unpickleElem jidP b |
||||
return r |
||||
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
module Network.XMPP.Concurrent |
||||
( module Network.XMPP.Concurrent.Types |
||||
, module Network.XMPP.Concurrent.Monad |
||||
, module Network.XMPP.Concurrent.Threads |
||||
, module Network.XMPP.Concurrent.IQ |
||||
) where |
||||
|
||||
import Network.XMPP.Concurrent.Types |
||||
import Network.XMPP.Concurrent.Monad |
||||
import Network.XMPP.Concurrent.Threads |
||||
import Network.XMPP.Concurrent.IQ |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
module Network.XMPP.Concurrent.IQ where |
||||
|
||||
import Control.Concurrent.STM |
||||
import Control.Monad.IO.Class |
||||
import Control.Monad.Trans.Reader |
||||
|
||||
import Data.XML.Types |
||||
import qualified Data.Map as Map |
||||
|
||||
import Network.XMPP.Concurrent.Types |
||||
import Network.XMPP.Concurrent.Monad |
||||
import Network.XMPP.Types |
||||
|
||||
-- | Sends an IQ, returns a 'TMVar' that will be filled with the first inbound |
||||
-- IQ with a matching ID that has type @result@ or @error@ |
||||
sendIQ :: Maybe JID -- ^ Recipient (to) |
||||
-> IQRequestType -- ^ IQ type (Get or Set) |
||||
-> Maybe LangTag -- ^ Language tag of the payload (Nothing for default) |
||||
-> Element -- ^ The iq body (there has to be exactly one) |
||||
-> XMPPThread (TMVar IQResponse) |
||||
sendIQ to tp lang body = do -- TODO: add timeout |
||||
newId <- liftIO =<< asks idGenerator |
||||
handlers <- asks iqHandlers |
||||
ref <- liftIO . atomically $ do |
||||
resRef <- newEmptyTMVar |
||||
(byNS, byId) <- readTVar handlers |
||||
writeTVar handlers (byNS, Map.insert newId resRef byId) |
||||
-- TODO: Check for id collisions (shouldn't happen?) |
||||
return resRef |
||||
sendS . IQRequestS $ IQRequest newId Nothing to lang tp body |
||||
return ref |
||||
|
||||
-- | like 'sendIQ', but waits for the answer IQ |
||||
sendIQ' :: Maybe JID |
||||
-> IQRequestType |
||||
-> Maybe LangTag |
||||
-> Element |
||||
-> XMPPThread IQResponse |
||||
sendIQ' to tp lang body = do |
||||
ref <- sendIQ to tp lang body |
||||
liftIO . atomically $ takeTMVar ref |
||||
|
||||
answerIQ :: (IQRequest, TVar Bool) |
||||
-> Either StanzaError (Maybe Element) |
||||
-> XMPPThread Bool |
||||
answerIQ ((IQRequest iqid from _to lang _tp bd), sentRef) answer = do |
||||
out <- asks outCh |
||||
let response = case answer of |
||||
Left err -> IQErrorS $ IQError iqid Nothing from lang err (Just bd) |
||||
Right res -> IQResultS $ IQResult iqid Nothing from lang res |
||||
liftIO . atomically $ do |
||||
sent <- readTVar sentRef |
||||
case sent of |
||||
False -> do |
||||
writeTVar sentRef True |
||||
|
||||
writeTChan out response |
||||
return True |
||||
True -> return False |
||||
@ -0,0 +1,169 @@
@@ -0,0 +1,169 @@
|
||||
module Network.XMPP.Concurrent.Monad where |
||||
|
||||
import Network.XMPP.Types |
||||
|
||||
import Control.Concurrent |
||||
import Control.Concurrent.STM |
||||
import qualified Control.Exception.Lifted as Ex |
||||
import Control.Monad.IO.Class |
||||
import Control.Monad.Trans.Reader |
||||
import Control.Monad.Trans.State |
||||
|
||||
import Data.IORef |
||||
import qualified Data.Map as Map |
||||
import Data.Text(Text) |
||||
|
||||
import Network.XMPP.Concurrent.Types |
||||
|
||||
-- | Register a new IQ listener. IQ requests matching the type and namespace will |
||||
-- be put in the channel. |
||||
listenIQChan :: IQRequestType -- ^ type of IQs to receive (Get / Set) |
||||
-> Text -- ^ namespace of the child element |
||||
-> XMPPThread (Bool, TChan (IQRequest, TVar Bool)) |
||||
listenIQChan tp ns = do |
||||
handlers <- asks iqHandlers |
||||
liftIO . atomically $ do |
||||
(byNS, byID) <- readTVar handlers |
||||
iqCh <- newTChan |
||||
let (present, byNS') = Map.insertLookupWithKey' (\_ new _ -> new) |
||||
(tp,ns) iqCh byNS |
||||
writeTVar handlers (byNS', byID) |
||||
return $ case present of |
||||
Nothing -> (True, iqCh) |
||||
Just iqCh' -> (False, iqCh') |
||||
|
||||
-- | get the inbound stanza channel, duplicates from master if necessary |
||||
-- please note that once duplicated it will keep filling up, call |
||||
-- 'dropMessageChan' to allow it to be garbage collected |
||||
getMessageChan :: XMPPThread (TChan (Either MessageError Message)) |
||||
getMessageChan = do |
||||
mChR <- asks messagesRef |
||||
mCh <- liftIO $ readIORef mChR |
||||
case mCh of |
||||
Nothing -> do |
||||
shadow <- asks mShadow |
||||
mCh' <- liftIO $ atomically $ dupTChan shadow |
||||
liftIO $ writeIORef mChR (Just mCh') |
||||
return mCh' |
||||
Just mCh' -> return mCh' |
||||
|
||||
-- | see 'getMessageChan' |
||||
getPresenceChan :: XMPPThread (TChan (Either PresenceError Presence)) |
||||
getPresenceChan = do |
||||
pChR <- asks presenceRef |
||||
pCh <- liftIO $ readIORef pChR |
||||
case pCh of |
||||
Nothing -> do |
||||
shadow <- asks pShadow |
||||
pCh' <- liftIO $ atomically $ dupTChan shadow |
||||
liftIO $ writeIORef pChR (Just pCh') |
||||
return pCh' |
||||
Just pCh' -> return pCh' |
||||
|
||||
-- | Drop the local end of the inbound stanza channel |
||||
-- from our context so it can be GC-ed |
||||
dropMessageChan :: XMPPThread () |
||||
dropMessageChan = do |
||||
r <- asks messagesRef |
||||
liftIO $ writeIORef r Nothing |
||||
|
||||
-- | see 'dropMessageChan' |
||||
dropPresenceChan :: XMPPThread () |
||||
dropPresenceChan = do |
||||
r <- asks presenceRef |
||||
liftIO $ writeIORef r Nothing |
||||
|
||||
-- | Read an element from the inbound stanza channel, acquiring a copy |
||||
-- of the channel as necessary |
||||
pullMessage :: XMPPThread (Either MessageError Message) |
||||
pullMessage = do |
||||
c <- getMessageChan |
||||
liftIO $ atomically $ readTChan c |
||||
|
||||
-- | Read an element from the inbound stanza channel, acquiring a copy |
||||
-- of the channel as necessary |
||||
pullPresence :: XMPPThread (Either PresenceError Presence) |
||||
pullPresence = do |
||||
c <- getPresenceChan |
||||
liftIO $ atomically $ readTChan c |
||||
|
||||
-- | Send a stanza to the server |
||||
sendS :: Stanza -> XMPPThread () |
||||
sendS a = do |
||||
out <- asks outCh |
||||
liftIO . atomically $ writeTChan out a |
||||
return () |
||||
|
||||
-- | Fork a new thread |
||||
forkXMPP :: XMPPThread () -> XMPPThread ThreadId |
||||
forkXMPP a = do |
||||
thread <- ask |
||||
mCH' <- liftIO $ newIORef Nothing |
||||
pCH' <- liftIO $ newIORef Nothing |
||||
liftIO $ forkIO $ runReaderT a (thread {messagesRef = mCH' |
||||
,presenceRef = pCH' |
||||
}) |
||||
|
||||
filterMessages :: (MessageError -> Bool) |
||||
-> (Message -> Bool) |
||||
-> XMPPThread (Either MessageError Message) |
||||
filterMessages f g = do |
||||
s <- pullMessage |
||||
case s of |
||||
Left e | f e -> return $ Left e |
||||
| otherwise -> filterMessages f g |
||||
Right m | g m -> return $ Right m |
||||
| otherwise -> filterMessages f g |
||||
|
||||
waitForMessage :: (Message -> Bool) -> XMPPThread Message |
||||
waitForMessage f = do |
||||
s <- pullMessage |
||||
case s of |
||||
Left _ -> waitForMessage f |
||||
Right m | f m -> return m |
||||
| otherwise -> waitForMessage f |
||||
|
||||
waitForMessageError :: (MessageError -> Bool) -> XMPPThread MessageError |
||||
waitForMessageError f = do |
||||
s <- pullMessage |
||||
case s of |
||||
Right _ -> waitForMessageError f |
||||
Left m | f m -> return m |
||||
| otherwise -> waitForMessageError f |
||||
|
||||
waitForPresence :: (Presence -> Bool) -> XMPPThread Presence |
||||
waitForPresence f = do |
||||
s <- pullPresence |
||||
case s of |
||||
Left _ -> waitForPresence f |
||||
Right m | f m -> return m |
||||
| otherwise -> waitForPresence f |
||||
|
||||
-- | Run an XMPPMonad action in isolation. |
||||
-- Reader and writer workers will be temporarily stopped |
||||
-- and resumed with the new session details once the action returns. |
||||
-- The Action will run in the calling thread/ |
||||
-- NB: This will /not/ catch any exceptions. If you action dies, deadlocks |
||||
-- or otherwisely exits abnormaly the XMPP session will be dead. |
||||
withConnection :: XMPPConMonad a -> XMPPThread a |
||||
withConnection a = do |
||||
readerId <- asks readerThread |
||||
stateRef <- asks conStateRef |
||||
write <- asks writeRef |
||||
wait <- liftIO $ newEmptyTMVarIO |
||||
liftIO . throwTo readerId $ Interrupt wait |
||||
s <- liftIO . atomically $ do |
||||
putTMVar wait () |
||||
takeTMVar write |
||||
takeTMVar stateRef |
||||
(res, s') <- liftIO $ runStateT a s |
||||
liftIO . atomically $ do |
||||
putTMVar write (sConPushBS s') |
||||
putTMVar stateRef s' |
||||
return res |
||||
|
||||
sendPresence :: Presence -> XMPPThread () |
||||
sendPresence = sendS . PresenceS |
||||
|
||||
sendMessage :: Message -> XMPPThread () |
||||
sendMessage = sendS . MessageS |
||||
@ -0,0 +1,178 @@
@@ -0,0 +1,178 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-} |
||||
{-# LANGUAGE OverloadedStrings #-} |
||||
module Network.XMPP.Concurrent.Threads where |
||||
|
||||
import Network.XMPP.Types |
||||
|
||||
import Control.Applicative((<$>),(<*>)) |
||||
import Control.Concurrent |
||||
import Control.Concurrent.STM |
||||
import qualified Control.Exception.Lifted as Ex |
||||
import Control.Monad |
||||
import Control.Monad.IO.Class |
||||
import Control.Monad.Trans.Class |
||||
import Control.Monad.Trans.Reader |
||||
import Control.Monad.Trans.Resource |
||||
import Control.Monad.Trans.State |
||||
|
||||
import qualified Data.ByteString as BS |
||||
import Data.Conduit |
||||
import qualified Data.Conduit.List as CL |
||||
import Data.Default (def) |
||||
import Data.IORef |
||||
import qualified Data.Map as Map |
||||
import Data.Maybe |
||||
import qualified Data.Text as Text |
||||
|
||||
import Data.XML.Types |
||||
|
||||
import Network.XMPP.Monad |
||||
import Network.XMPP.Marshal |
||||
import Network.XMPP.Pickle |
||||
import Network.XMPP.Concurrent.Types |
||||
|
||||
import Text.XML.Stream.Elements |
||||
import qualified Text.XML.Stream.Render as XR |
||||
|
||||
readWorker :: TChan (Either MessageError Message) |
||||
-> TChan (Either PresenceError Presence) |
||||
-> TVar IQHandlers |
||||
-> TMVar XMPPConState |
||||
-> IO () |
||||
readWorker messageC presenceC handlers stateRef = |
||||
Ex.mask_ . forever $ do |
||||
s <- liftIO . atomically $ takeTMVar stateRef |
||||
(sta', s') <- flip runStateT s $ Ex.catch ( do |
||||
-- we don't know whether pull will necessarily be interruptible |
||||
liftIO $ Ex.allowInterrupt |
||||
Just <$> pull |
||||
) |
||||
(\(Interrupt t) -> do |
||||
liftIO . atomically $ |
||||
putTMVar stateRef s |
||||
liftIO . atomically $ takeTMVar t |
||||
return Nothing |
||||
) |
||||
liftIO . atomically $ do |
||||
case sta' of |
||||
Nothing -> return () |
||||
Just sta -> do |
||||
putTMVar stateRef s' |
||||
case sta of |
||||
MessageS m -> do writeTChan messageC $ Right m |
||||
_ <- readTChan messageC -- Sic! |
||||
return () |
||||
-- this may seem ridiculous, but to prevent |
||||
-- the channel from filling up we immedtiately remove the |
||||
-- Stanza we just put in. It will still be |
||||
-- available in duplicates. |
||||
MessageErrorS m -> do writeTChan messageC $ Left m |
||||
_ <- readTChan messageC |
||||
return () |
||||
PresenceS p -> do |
||||
writeTChan presenceC $ Right p |
||||
_ <- readTChan presenceC |
||||
return () |
||||
PresenceErrorS p -> do |
||||
writeTChan presenceC $ Left p |
||||
_ <- readTChan presenceC |
||||
return () |
||||
|
||||
IQRequestS i -> handleIQRequest handlers i |
||||
IQResultS i -> handleIQResponse handlers (Right i) |
||||
IQErrorS i -> handleIQResponse handlers (Left i) |
||||
|
||||
|
||||
handleIQRequest handlers iq = do |
||||
(byNS, _) <- readTVar handlers |
||||
let iqNS = fromMaybe "" (nameNamespace . elementName $ iqRequestPayload iq) |
||||
case Map.lookup (iqRequestType iq, iqNS) byNS of |
||||
Nothing -> return () -- TODO: send error stanza |
||||
Just ch -> do |
||||
sent <- newTVar False |
||||
writeTChan ch (iq, sent) |
||||
|
||||
handleIQResponse handlers iq = do |
||||
(byNS, byID) <- readTVar handlers |
||||
case Map.updateLookupWithKey (\_ _ -> Nothing) (iqID iq) byID of |
||||
(Nothing, _) -> return () -- we are not supposed |
||||
-- to send an error |
||||
(Just tmvar, byID') -> do |
||||
_ <- tryPutTMVar tmvar iq -- don't block |
||||
writeTVar handlers (byNS, byID') |
||||
where |
||||
iqID (Left err) = iqErrorID err |
||||
iqID (Right iq) = iqResultID iq |
||||
|
||||
writeWorker :: TChan Stanza -> TMVar (BS.ByteString -> IO ()) -> IO () |
||||
writeWorker stCh writeR = forever $ do |
||||
(write, next) <- atomically $ (,) <$> |
||||
takeTMVar writeR <*> |
||||
readTChan stCh |
||||
_ <- write $ renderElement (pickleElem stanzaP next) |
||||
atomically $ putTMVar writeR write |
||||
|
||||
-- Two streams: input and output. Threads read from input stream and write to output stream. |
||||
-- | Runs thread in XmppState monad |
||||
-- returns channel of incoming and outgoing stances, respectively |
||||
-- and an Action to stop the Threads and close the connection |
||||
startThreads |
||||
:: XMPPConMonad ( TChan (Either MessageError Message) |
||||
, TChan (Either PresenceError Presence) |
||||
, TVar IQHandlers |
||||
, TChan Stanza |
||||
, IO () |
||||
, TMVar (BS.ByteString -> IO ()) |
||||
, TMVar XMPPConState |
||||
, ThreadId |
||||
) |
||||
|
||||
startThreads = do |
||||
writeLock <- liftIO . newTMVarIO =<< gets sConPushBS |
||||
messageC <- liftIO newTChanIO |
||||
presenceC <- liftIO newTChanIO |
||||
iqC <- liftIO newTChanIO |
||||
outC <- liftIO newTChanIO |
||||
handlers <- liftIO $ newTVarIO ( Map.empty, Map.empty) |
||||
conS <- liftIO . newTMVarIO =<< get |
||||
lw <- liftIO . forkIO $ writeWorker outC writeLock |
||||
cp <- liftIO . forkIO $ connPersist writeLock |
||||
s <- get |
||||
rd <- liftIO . forkIO $ readWorker messageC presenceC handlers conS |
||||
return (messageC, presenceC, handlers, outC |
||||
, killConnection writeLock [lw, rd, cp] |
||||
, writeLock, conS ,rd) |
||||
where |
||||
killConnection writeLock threads = liftIO $ do |
||||
_ <- atomically $ takeTMVar writeLock -- Should we put it back? |
||||
_ <- forM threads killThread |
||||
return() |
||||
|
||||
-- | Start worker threads and run action. The supplied action will run |
||||
-- in the calling thread. use 'forkXMPP' to start another thread. |
||||
runThreaded :: XMPPThread a |
||||
-> XMPPConMonad a |
||||
runThreaded a = do |
||||
liftIO . putStrLn $ "starting threads" |
||||
(mC, pC, hand, outC, _stopThreads, writeR, conS, rdr ) <- startThreads |
||||
liftIO . putStrLn $ "threads running" |
||||
workermCh <- liftIO . newIORef $ Nothing |
||||
workerpCh <- liftIO . newIORef $ Nothing |
||||
idRef <- liftIO $ newTVarIO 1 |
||||
let getId = atomically $ do |
||||
curId <- readTVar idRef |
||||
writeTVar idRef (curId + 1 :: Integer) |
||||
return . read. show $ curId |
||||
s <- get |
||||
liftIO . putStrLn $ "starting application" |
||||
liftIO $ runReaderT a (Thread workermCh workerpCh mC pC outC hand writeR rdr getId conS) |
||||
|
||||
|
||||
-- | Sends a blank space every 30 seconds to keep the connection alive |
||||
connPersist :: TMVar (BS.ByteString -> IO ()) -> IO () |
||||
connPersist lock = forever $ do |
||||
pushBS <- atomically $ takeTMVar lock |
||||
pushBS " " |
||||
atomically $ putTMVar lock pushBS |
||||
-- putStrLn "<space added>" |
||||
threadDelay 30000000 |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
{-# LANGUAGE DeriveDataTypeable #-} |
||||
|
||||
module Network.XMPP.Concurrent.Types where |
||||
|
||||
import qualified Control.Exception.Lifted as Ex |
||||
import Control.Concurrent |
||||
import Control.Concurrent.STM |
||||
import Control.Monad.Trans.Reader |
||||
|
||||
import qualified Data.ByteString as BS |
||||
import Data.IORef |
||||
import qualified Data.Map as Map |
||||
import Data.Text(Text) |
||||
import Data.Typeable |
||||
|
||||
|
||||
import Network.XMPP.Types |
||||
|
||||
|
||||
type IQHandlers = (Map.Map (IQRequestType, Text) (TChan (IQRequest, TVar Bool)) |
||||
, Map.Map StanzaId (TMVar IQResponse) |
||||
) |
||||
|
||||
data Thread = Thread { messagesRef :: IORef (Maybe ( TChan (Either |
||||
MessageError |
||||
Message |
||||
))) |
||||
, presenceRef :: IORef (Maybe (TChan (Either |
||||
PresenceError |
||||
Presence |
||||
))) |
||||
, mShadow :: TChan (Either MessageError |
||||
Message) -- the original chan |
||||
, pShadow :: TChan (Either PresenceError |
||||
Presence) -- the original chan |
||||
, outCh :: TChan Stanza |
||||
, iqHandlers :: TVar IQHandlers |
||||
, writeRef :: TMVar (BS.ByteString -> IO () ) |
||||
, readerThread :: ThreadId |
||||
, idGenerator :: IO StanzaId |
||||
, conStateRef :: TMVar XMPPConState |
||||
} |
||||
|
||||
type XMPPThread a = ReaderT Thread IO a |
||||
|
||||
data Interrupt = Interrupt (TMVar ()) deriving Typeable |
||||
instance Show Interrupt where show _ = "<Interrupt>" |
||||
instance Ex.Exception Interrupt |
||||
@ -0,0 +1,195 @@
@@ -0,0 +1,195 @@
|
||||
{-# Language OverloadedStrings, ViewPatterns, NoMonomorphismRestriction #-} |
||||
|
||||
module Network.XMPP.Marshal where |
||||
|
||||
import Data.XML.Pickle |
||||
import Data.XML.Types |
||||
|
||||
import Network.XMPP.Types |
||||
|
||||
stanzaSel :: Stanza -> Int |
||||
stanzaSel (IQRequestS _) = 0 |
||||
stanzaSel (IQResultS _) = 1 |
||||
stanzaSel (IQErrorS _) = 2 |
||||
stanzaSel (MessageS _) = 3 |
||||
stanzaSel (MessageErrorS _) = 4 |
||||
stanzaSel (PresenceS _) = 5 |
||||
stanzaSel (PresenceErrorS _) = 6 |
||||
|
||||
stanzaP :: PU [Node] Stanza |
||||
stanzaP = xpAlt stanzaSel |
||||
[ xpWrap IQRequestS (\(IQRequestS x) -> x) xpIQRequest |
||||
, xpWrap IQResultS (\(IQResultS x) -> x) xpIQResult |
||||
, xpWrap IQErrorS (\(IQErrorS x) -> x) xpIQError |
||||
, xpWrap MessageS (\(MessageS x) -> x) xpMessage |
||||
, xpWrap MessageErrorS (\(MessageErrorS x) -> x) xpMessageError |
||||
, xpWrap PresenceS (\(PresenceS x) -> x) xpPresence |
||||
, xpWrap PresenceErrorS (\(PresenceErrorS x) -> x) xpPresenceError |
||||
] |
||||
|
||||
xmlLang :: Name |
||||
xmlLang = Name "lang" Nothing (Just "xml") |
||||
|
||||
xpLangTag :: PU [Attribute] (Maybe LangTag) |
||||
xpLangTag = xpAttrImplied xmlLang xpPrim |
||||
|
||||
xpMessage :: PU [Node] (Message) |
||||
xpMessage = xpWrap (\((tp, qid, from, to, lang), (sub, body, thr, ext)) |
||||
-> Message qid from to lang tp sub thr body ext) |
||||
(\(Message qid from to lang tp sub thr body ext) |
||||
-> ((tp, qid, from, to, lang), (sub, body, thr, ext))) |
||||
$ |
||||
xpElem "{jabber:client}message" |
||||
(xp5Tuple |
||||
(xpDefault Normal $ xpAttr "type" xpPrim) |
||||
(xpAttrImplied "id" xpPrim) |
||||
(xpAttrImplied "from" xpPrim) |
||||
(xpAttrImplied "to" xpPrim) |
||||
(xpAttrImplied xmlLang xpPrim) |
||||
-- TODO: NS? |
||||
) |
||||
(xp4Tuple |
||||
(xpOption . xpElemNodes "{jabber:client}subject" $ xpContent xpId) |
||||
(xpOption . xpElemNodes "{jabber:client}body" $ xpContent xpId) |
||||
(xpOption . xpElemNodes "{jabber:client}thread" $ xpContent xpId) |
||||
(xpAll xpElemVerbatim) |
||||
) |
||||
|
||||
|
||||
xpPresence :: PU [Node] Presence |
||||
xpPresence = xpWrap (\((qid, from, to, lang, tp),(shw, stat, prio, ext)) |
||||
-> Presence qid from to lang tp shw stat prio ext) |
||||
(\(Presence qid from to lang tp shw stat prio ext) |
||||
-> ((qid, from, to, lang, tp), (shw, stat, prio, ext))) |
||||
$ |
||||
xpElem "{jabber:client}presence" |
||||
(xp5Tuple |
||||
(xpAttrImplied "id" xpPrim) |
||||
(xpAttrImplied "from" xpPrim) |
||||
(xpAttrImplied "to" xpPrim) |
||||
xpLangTag |
||||
(xpAttrImplied "type" xpPrim) |
||||
) |
||||
(xp4Tuple |
||||
(xpOption . xpElemNodes "{jabber:client}show" $ xpContent xpPrim) |
||||
(xpOption . xpElemNodes "{jabber:client}status" $ xpContent xpId) |
||||
(xpOption . xpElemNodes "{jabber:client}priority" $ xpContent xpPrim) |
||||
(xpAll xpElemVerbatim) |
||||
) |
||||
|
||||
xpIQRequest :: PU [Node] IQRequest |
||||
xpIQRequest = xpWrap (\((qid, from, to, lang, tp),body) |
||||
-> IQRequest qid from to lang tp body) |
||||
(\(IQRequest qid from to lang tp body) |
||||
-> ((qid, from, to, lang, tp), body)) |
||||
$ |
||||
xpElem "{jabber:client}iq" |
||||
(xp5Tuple |
||||
(xpAttr "id" xpPrim) |
||||
(xpAttrImplied "from" xpPrim) |
||||
(xpAttrImplied "to" xpPrim) |
||||
xpLangTag |
||||
((xpAttr "type" xpPrim)) |
||||
) |
||||
(xpElemVerbatim) |
||||
|
||||
xpIQResult :: PU [Node] IQResult |
||||
xpIQResult = xpWrap (\((qid, from, to, lang, _tp),body) |
||||
-> IQResult qid from to lang body) |
||||
(\(IQResult qid from to lang body) |
||||
-> ((qid, from, to, lang, ()), body)) |
||||
$ |
||||
xpElem "{jabber:client}iq" |
||||
(xp5Tuple |
||||
(xpAttr "id" xpPrim) |
||||
(xpAttrImplied "from" xpPrim) |
||||
(xpAttrImplied "to" xpPrim) |
||||
xpLangTag |
||||
((xpAttrFixed "type" "result")) |
||||
) |
||||
(xpOption xpElemVerbatim) |
||||
|
||||
---------------------------------------------------------- |
||||
-- Errors |
||||
---------------------------------------------------------- |
||||
|
||||
xpErrorCondition :: PU [Node] StanzaErrorCondition |
||||
xpErrorCondition = xpWrap (\(cond, (), ()) -> cond) (\cond -> (cond, (), ())) $ |
||||
xpElemByNamespace |
||||
"urn:ietf:params:xml:ns:xmpp-stanzas" xpPrim |
||||
xpUnit |
||||
xpUnit |
||||
|
||||
xpStanzaError :: PU [Node] StanzaError |
||||
xpStanzaError = xpWrap |
||||
(\(tp, (cond, txt, ext)) -> StanzaError tp cond txt ext) |
||||
(\(StanzaError tp cond txt ext) -> (tp, (cond, txt, ext))) $ |
||||
xpElem "{jabber:client}error" |
||||
(xpAttr "type" xpPrim) |
||||
(xp3Tuple |
||||
xpErrorCondition |
||||
(xpOption $ xpElem "{jabber:client}text" |
||||
(xpAttrImplied xmlLang xpPrim) |
||||
(xpContent xpId) |
||||
) |
||||
(xpOption xpElemVerbatim) |
||||
) |
||||
|
||||
xpMessageError :: PU [Node] (MessageError) |
||||
xpMessageError = xpWrap (\((_, qid, from, to, lang), (err, ext)) |
||||
-> MessageError qid from to lang err ext) |
||||
(\(MessageError qid from to lang err ext) |
||||
-> (((), qid, from, to, lang), (err, ext))) |
||||
$ |
||||
xpElem "{jabber:client}message" |
||||
(xp5Tuple |
||||
(xpAttrFixed "type" "error") |
||||
(xpAttrImplied "id" xpPrim) |
||||
(xpAttrImplied "from" xpPrim) |
||||
(xpAttrImplied "to" xpPrim) |
||||
(xpAttrImplied xmlLang xpPrim) |
||||
-- TODO: NS? |
||||
) |
||||
(xp2Tuple |
||||
xpStanzaError |
||||
(xpAll xpElemVerbatim) |
||||
) |
||||
|
||||
xpPresenceError :: PU [Node] PresenceError |
||||
xpPresenceError = xpWrap (\((qid, from, to, lang, _),(err, ext)) |
||||
-> PresenceError qid from to lang err ext) |
||||
(\(PresenceError qid from to lang err ext) |
||||
-> ((qid, from, to, lang, ()), (err, ext))) |
||||
$ |
||||
xpElem "{jabber:client}presence" |
||||
(xp5Tuple |
||||
(xpAttrImplied "id" xpPrim) |
||||
(xpAttrImplied "from" xpPrim) |
||||
(xpAttrImplied "to" xpPrim) |
||||
xpLangTag |
||||
(xpAttrFixed "type" "error") |
||||
) |
||||
(xp2Tuple |
||||
xpStanzaError |
||||
(xpAll xpElemVerbatim) |
||||
) |
||||
|
||||
xpIQError :: PU [Node] IQError |
||||
xpIQError = xpWrap (\((qid, from, to, lang, _tp),(err, body)) |
||||
-> IQError qid from to lang err body) |
||||
(\(IQError qid from to lang err body) |
||||
-> ((qid, from, to, lang, ()), (err, body))) |
||||
$ |
||||
xpElem "{jabber:client}iq" |
||||
(xp5Tuple |
||||
(xpAttr "id" xpPrim) |
||||
(xpAttrImplied "from" xpPrim) |
||||
(xpAttrImplied "to" xpPrim) |
||||
xpLangTag |
||||
((xpAttrFixed "type" "error")) |
||||
) |
||||
(xp2Tuple |
||||
xpStanzaError |
||||
(xpOption xpElemVerbatim) |
||||
) |
||||
|
||||
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
{-# LANGUAGE RecordWildCards #-} |
||||
module Network.XMPP.Message where |
||||
|
||||
import Data.Text(Text) |
||||
import Data.XML.Types |
||||
|
||||
import Network.XMPP.Types |
||||
|
||||
message :: Message |
||||
message = Message { messageID = Nothing |
||||
, messageFrom = Nothing |
||||
, messageTo = Nothing |
||||
, messageLangTag = Nothing |
||||
, messageType = Normal |
||||
, messageSubject = Nothing |
||||
, messageThread = Nothing |
||||
, messageBody = Nothing |
||||
, messagePayload = [] |
||||
} |
||||
|
||||
simpleMessage :: JID -> Text -> Message |
||||
simpleMessage to txt = message { messageTo = Just to |
||||
, messageBody = Just txt |
||||
} |
||||
|
||||
answerMessage :: Message -> Text -> [Element] -> Maybe Message |
||||
answerMessage Message{messageFrom = Just frm, ..} txt payload = |
||||
Just $ Message{ messageFrom = messageTo |
||||
, messageID = Nothing |
||||
, messageTo = Just frm |
||||
, messageBody = Just txt |
||||
, messagePayload = payload |
||||
, .. |
||||
} |
||||
answerMessage _ _ _ = Nothing |
||||
|
||||
@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
|
||||
{-# LANGUAGE OverloadedStrings #-} |
||||
|
||||
module Network.XMPP.Monad where |
||||
|
||||
import Control.Applicative((<$>)) |
||||
import Control.Monad |
||||
import Control.Monad.IO.Class |
||||
import Control.Monad.Trans.Class |
||||
--import Control.Monad.Trans.Resource |
||||
import Control.Concurrent |
||||
import Control.Monad.Trans.State |
||||
|
||||
import Data.ByteString as BS |
||||
import Data.Conduit |
||||
import Data.Conduit.Binary as CB |
||||
import Data.Conduit.List as CL |
||||
import Data.Text(Text) |
||||
import Data.XML.Pickle |
||||
import Data.XML.Types |
||||
|
||||
import Network |
||||
import Network.XMPP.Types |
||||
import Network.XMPP.Marshal |
||||
import Network.XMPP.Pickle |
||||
|
||||
import System.IO |
||||
|
||||
import Text.XML.Stream.Elements |
||||
import Text.XML.Stream.Parse as XP |
||||
import Text.XML.Stream.Render as XR |
||||
|
||||
|
||||
pushN :: Element -> XMPPConMonad () |
||||
pushN x = do |
||||
sink <- gets sConPushBS |
||||
liftIO . sink $ renderElement x |
||||
|
||||
push :: Stanza -> XMPPConMonad () |
||||
push = pushN . pickleElem stanzaP |
||||
|
||||
pushOpen :: Element -> XMPPConMonad () |
||||
pushOpen e = do |
||||
sink <- gets sConPushBS |
||||
liftIO . sink $ renderOpenElement e |
||||
return () |
||||
|
||||
pulls :: Sink Event IO b -> XMPPConMonad b |
||||
pulls snk = do |
||||
source <- gets sConSrc |
||||
(src', r) <- lift $ source $$+ snk |
||||
modify $ (\s -> s {sConSrc = src'}) |
||||
return r |
||||
|
||||
pullE :: XMPPConMonad Element |
||||
pullE = pulls elementFromEvents |
||||
|
||||
pullPickle :: PU [Node] a -> XMPPConMonad a |
||||
pullPickle p = unpickleElem p <$> pullE |
||||
|
||||
pull :: XMPPConMonad Stanza |
||||
pull = pullPickle stanzaP |
||||
|
||||
xmppFromHandle :: Handle |
||||
-> Text |
||||
-> Text |
||||
-> Maybe Text |
||||
-> XMPPConMonad a |
||||
-> IO (a, XMPPConState) |
||||
xmppFromHandle handle hostname username res f = do |
||||
liftIO $ hSetBuffering handle NoBuffering |
||||
let raw = sourceHandle' handle |
||||
let src = raw $= XP.parseBytes def |
||||
let st = XMPPConState |
||||
src |
||||
(raw) |
||||
(BS.hPut handle) |
||||
(Just handle) |
||||
(SF Nothing [] []) |
||||
False |
||||
(Just hostname) |
||||
(Just username) |
||||
res |
||||
runStateT f st |
||||
|
||||
-- TODO: Once pullrequest has been merged, switch back to upstream |
||||
sourceHandle' :: MonadIO m => Handle -> Source m BS.ByteString |
||||
sourceHandle' h = |
||||
src |
||||
where |
||||
src = PipeM pull close |
||||
|
||||
pull = do |
||||
bs <- liftIO (BS.hGetSome h 4096) |
||||
if BS.null bs |
||||
then return $ Done Nothing () |
||||
else return $ HaveOutput src close bs |
||||
|
||||
close = return () |
||||
|
||||
sinkHandle' :: MonadIO m |
||||
=> Handle |
||||
-> Sink BS.ByteString m () |
||||
sinkHandle' h = |
||||
NeedInput push close |
||||
where |
||||
push input = PipeM |
||||
(liftIO (BS.hPut h input) >> return (NeedInput push close)) |
||||
(return ()) |
||||
close = return () |
||||
|
||||
zeroSource :: Source IO output |
||||
zeroSource = sourceState () (\_ -> forever $ threadDelay 10000000) |
||||
|
||||
xmppZeroConState :: XMPPConState |
||||
xmppZeroConState = XMPPConState |
||||
{ sConSrc = zeroSource |
||||
, sRawSrc = zeroSource |
||||
, sConPushBS = (\_ -> return ()) |
||||
, sConHandle = Nothing |
||||
, sFeatures = SF Nothing [] [] |
||||
, sHaveTLS = False |
||||
, sHostname = Nothing |
||||
, sUsername = Nothing |
||||
, sResource = Nothing |
||||
} |
||||
|
||||
xmppRawConnect :: HostName -> Text -> XMPPConMonad () |
||||
xmppRawConnect host hostname = do |
||||
uname <- gets sUsername |
||||
con <- liftIO $ do |
||||
con <- connectTo host (PortNumber 5222) |
||||
hSetBuffering con NoBuffering |
||||
return con |
||||
let raw = sourceHandle' con |
||||
let src = raw $= XP.parseBytes def |
||||
let st = XMPPConState |
||||
src |
||||
(raw) |
||||
(BS.hPut con) |
||||
(Just con) |
||||
(SF Nothing [] []) |
||||
False |
||||
(Just hostname) |
||||
uname |
||||
Nothing |
||||
put st |
||||
|
||||
withNewSession :: XMPPConMonad a -> IO (a, XMPPConState) |
||||
withNewSession action = do |
||||
runStateT action xmppZeroConState |
||||
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
{-# LANGUAGE NoMonomorphismRestriction #-} |
||||
{-# LANGUAGE OverloadedStrings #-} |
||||
{-# LANGUAGE TupleSections #-} |
||||
|
||||
-- Marshalling between XML and Native Types |
||||
|
||||
|
||||
module Network.XMPP.Pickle where |
||||
|
||||
import Data.XML.Types |
||||
import Data.XML.Pickle |
||||
|
||||
import Text.XML.Stream.Elements |
||||
|
||||
mbToBool :: Maybe t -> Bool |
||||
mbToBool (Just _) = True |
||||
mbToBool _ = False |
||||
|
||||
xpElemEmpty :: Name -> PU [Node] () |
||||
xpElemEmpty name = xpWrap (\((),()) -> ()) |
||||
(\() -> ((),())) $ |
||||
xpElem name xpUnit xpUnit |
||||
|
||||
-- xpElemExists :: Name -> PU [Node] Bool |
||||
-- xpElemExists name = xpWrap (\x -> mbToBool x) |
||||
-- (\x -> if x then Just () else Nothing) $ |
||||
-- xpOption (xpElemEmpty name) |
||||
|
||||
|
||||
xpNodeElem :: PU [Node] a -> PU Element a |
||||
xpNodeElem xp = PU { pickleTree = \x -> head $ (pickleTree xp x) >>= \y -> |
||||
case y of |
||||
NodeElement e -> [e] |
||||
_ -> [] |
||||
, unpickleTree = \x -> case unpickleTree xp $ [NodeElement x] of |
||||
Left l -> Left l |
||||
Right (a,(_,c)) -> Right (a,(Nothing,c)) |
||||
} |
||||
|
||||
ignoreAttrs :: PU t ((), b) -> PU t b |
||||
ignoreAttrs = xpWrap snd ((),) |
||||
|
||||
mbl :: Maybe [a] -> [a] |
||||
mbl (Just l) = l |
||||
mbl Nothing = [] |
||||
|
||||
lmb :: [t] -> Maybe [t] |
||||
lmb [] = Nothing |
||||
lmb x = Just x |
||||
|
||||
right :: Either [Char] t -> t |
||||
right (Left l) = error l |
||||
right (Right r) = r |
||||
|
||||
unpickleElem :: PU [Node] c -> Element -> c |
||||
unpickleElem p x = case unpickle (xpNodeElem p) x of |
||||
Left l -> error $ l ++ "\n saw: " ++ ppElement x |
||||
Right r -> r |
||||
|
||||
pickleElem :: PU [Node] a -> a -> Element |
||||
pickleElem p = pickle $ xpNodeElem p |
||||
|
||||
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
module Network.XMPP.Presence where |
||||
|
||||
import Data.Text(Text) |
||||
import Network.XMPP.Types |
||||
|
||||
|
||||
presence :: Presence |
||||
presence = Presence { presenceID = Nothing |
||||
, presenceFrom = Nothing |
||||
, presenceTo = Nothing |
||||
, presenceLangTag = Nothing |
||||
, presenceType = Nothing |
||||
, presenceShowType = Nothing |
||||
, presenceStatus = Nothing |
||||
, presencePriority = Nothing |
||||
, presencePayload = [] |
||||
} |
||||
|
||||
presenceSubscribe :: JID -> Presence |
||||
presenceSubscribe to = presence { presenceTo = Just to |
||||
, presenceType = Just Subscribe |
||||
} |
||||
|
||||
-- | Is presence a subscription request |
||||
isPresenceSubscribe :: Presence -> Bool |
||||
isPresenceSubscribe pres = presenceType pres == (Just Subscribe) |
||||
|
||||
-- | Approve a subscripton of an entity |
||||
presenceSubscribed :: JID -> Presence |
||||
presenceSubscribed to = presence { presenceTo = Just to |
||||
, presenceType = Just Subscribed |
||||
} |
||||
|
||||
-- | Is presence a subscription approval |
||||
isPresenceSubscribed :: Presence -> Bool |
||||
isPresenceSubscribed pres = presenceType pres == (Just Subscribed) |
||||
|
||||
-- | End a subscription with an entity |
||||
presenceUnsubscribe :: JID -> Presence |
||||
presenceUnsubscribe to = presence { presenceTo = Just to |
||||
, presenceType = Just Unsubscribed |
||||
} |
||||
|
||||
-- | Is presence an unsubscription request |
||||
isPresenceUnsubscribe :: Presence -> Bool |
||||
isPresenceUnsubscribe pres = presenceType pres == (Just Unsubscribe) |
||||
|
||||
-- | Signals to the server that the client is available for communication |
||||
presenceOnline :: Presence |
||||
presenceOnline = presence |
||||
|
||||
-- | Signals to the server that the client is no longer available for communication. |
||||
presenceOffline :: Presence |
||||
presenceOffline = presence {presenceType = Just Unavailable} |
||||
|
||||
status |
||||
:: Maybe Text -- ^ Status message |
||||
-> Maybe ShowType -- ^ Status Type |
||||
-> Maybe Int -- ^ Priority |
||||
-> Presence |
||||
status txt showType prio = presence { presenceShowType = showType |
||||
, presencePriority = prio |
||||
, presenceStatus = txt |
||||
} |
||||
|
||||
-- | Sets the current availability status. This implicitly sets the clients |
||||
-- status online |
||||
presenceAvail :: ShowType -> Presence |
||||
presenceAvail showType = status Nothing (Just showType) Nothing |
||||
|
||||
-- | Sets the current status message. This implicitly sets the clients |
||||
-- status online |
||||
presenceMessage :: Text -> Presence |
||||
presenceMessage txt = status (Just txt) Nothing Nothing |
||||
|
||||
-- | Adds a recipient to a presence notification |
||||
presTo :: Presence -> JID -> Presence |
||||
presTo pres to = pres{presenceTo = Just to} |
||||
@ -0,0 +1,167 @@
@@ -0,0 +1,167 @@
|
||||
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} |
||||
module Network.XMPP.SASL where |
||||
|
||||
import Control.Applicative |
||||
import Control.Monad |
||||
import Control.Monad.IO.Class |
||||
import Control.Monad.Trans.State |
||||
|
||||
import qualified Crypto.Classes as CC |
||||
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as AP |
||||
import qualified Data.Binary as Binary |
||||
import qualified Data.ByteString as BS |
||||
import qualified Data.ByteString.Base64 as B64 |
||||
import qualified Data.ByteString.Char8 as BS8 |
||||
import qualified Data.ByteString.Lazy as BL |
||||
import qualified Data.Digest.Pure.MD5 as MD5 |
||||
import qualified Data.List as L |
||||
import Data.XML.Pickle |
||||
import Data.XML.Types |
||||
|
||||
import qualified Data.Text as Text |
||||
import Data.Text (Text) |
||||
import qualified Data.Text.Encoding as Text |
||||
|
||||
import Network.XMPP.Monad |
||||
import Network.XMPP.Stream |
||||
import Network.XMPP.Types |
||||
|
||||
import qualified System.Random as Random |
||||
|
||||
|
||||
saslInitE :: Text -> Element |
||||
saslInitE mechanism = |
||||
Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth" |
||||
[ ("mechanism", [ContentText mechanism]) ] |
||||
[] |
||||
|
||||
saslResponseE :: Text -> Element |
||||
saslResponseE resp = |
||||
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response" |
||||
[] |
||||
[NodeContent $ ContentText resp] |
||||
|
||||
saslResponse2E :: Element |
||||
saslResponse2E = |
||||
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response" |
||||
[] |
||||
[] |
||||
|
||||
xmppSASL:: Text -> Text -> XMPPConMonad (Either String Text) |
||||
xmppSASL uname passwd = do |
||||
realm <- gets sHostname |
||||
case realm of |
||||
Just realm' -> do |
||||
xmppStartSASL realm' uname passwd |
||||
modify (\s -> s{sUsername = Just uname}) |
||||
return $ Right uname |
||||
Nothing -> return $ Left "No connection found" |
||||
|
||||
xmppStartSASL :: Text |
||||
-> Text |
||||
-> Text |
||||
-> XMPPConMonad () |
||||
xmppStartSASL realm username passwd = do |
||||
mechanisms <- gets $ saslMechanisms . sFeatures |
||||
unless ("DIGEST-MD5" `elem` mechanisms) . error $ "No usable auth mechanism: " ++ show mechanisms |
||||
pushN $ saslInitE "DIGEST-MD5" |
||||
Right challenge <- B64.decode . Text.encodeUtf8<$> pullPickle challengePickle |
||||
let Right pairs = toPairs challenge |
||||
pushN . saslResponseE =<< createResponse realm username passwd pairs |
||||
challenge2 <- pullPickle (xpEither failurePickle challengePickle) |
||||
case challenge2 of |
||||
Left x -> error $ show x |
||||
Right _ -> return () |
||||
pushN saslResponse2E |
||||
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] <- pullE |
||||
xmppRestartStream |
||||
return () |
||||
|
||||
createResponse :: Text |
||||
-> Text |
||||
-> Text |
||||
-> [(BS8.ByteString, BS8.ByteString)] |
||||
-> XMPPConMonad Text |
||||
createResponse hostname username passwd' pairs = do |
||||
let Just qop = L.lookup "qop" pairs |
||||
let Just nonce = L.lookup "nonce" pairs |
||||
let uname = Text.encodeUtf8 username |
||||
let passwd = Text.encodeUtf8 passwd' |
||||
let realm = Text.encodeUtf8 hostname |
||||
g <- liftIO $ Random.newStdGen |
||||
let cnonce = BS.tail . BS.init . |
||||
B64.encode . BS.pack . take 8 $ Random.randoms g |
||||
let nc = "00000001" |
||||
let digestURI = ("xmpp/" `BS.append` realm) |
||||
let digest = md5Digest |
||||
uname |
||||
realm |
||||
passwd |
||||
digestURI |
||||
nc |
||||
qop |
||||
nonce |
||||
cnonce |
||||
let response = BS.intercalate"," . map (BS.intercalate "=") $ |
||||
[["username" , quote uname ] |
||||
,["realm" , quote realm ] |
||||
,["nonce" , quote nonce ] |
||||
,["cnonce" , quote cnonce ] |
||||
,["nc" , nc ] |
||||
,["qop" , qop ] |
||||
,["digest-uri", quote digestURI ] |
||||
,["response" , digest ] |
||||
,["charset" , "utf-8" ] |
||||
] |
||||
return . Text.decodeUtf8 $ B64.encode response |
||||
where quote x = BS.concat ["\"",x,"\""] |
||||
|
||||
toPairs :: BS.ByteString -> Either String [(BS.ByteString, BS.ByteString)] |
||||
toPairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do |
||||
AP.skipSpace |
||||
name <- AP.takeWhile1 (/= '=') |
||||
_ <- AP.char '=' |
||||
quote <- ((AP.char '"' >> return True) `mplus` return False) |
||||
content <- AP.takeWhile1 (AP.notInClass ",\"" ) |
||||
when quote . void $ AP.char '"' |
||||
return (name,content) |
||||
|
||||
hash :: [BS8.ByteString] -> BS8.ByteString |
||||
hash = BS8.pack . show |
||||
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") |
||||
|
||||
hashRaw :: [BS8.ByteString] -> BS8.ByteString |
||||
hashRaw = toStrict . Binary.encode |
||||
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") |
||||
|
||||
toStrict :: BL.ByteString -> BS8.ByteString |
||||
toStrict = BS.concat . BL.toChunks |
||||
|
||||
-- TODO: this only handles MD5-sess |
||||
|
||||
md5Digest :: BS8.ByteString |
||||
-> BS8.ByteString |
||||
-> BS8.ByteString |
||||
-> BS8.ByteString |
||||
-> BS8.ByteString |
||||
-> BS8.ByteString |
||||
-> BS8.ByteString |
||||
-> BS8.ByteString |
||||
-> BS8.ByteString |
||||
md5Digest uname realm password digestURI nc qop nonce cnonce= |
||||
let ha1 = hash [hashRaw [uname,realm,password], nonce, cnonce] |
||||
ha2 = hash ["AUTHENTICATE", digestURI] |
||||
in hash [ha1,nonce, nc, cnonce,qop,ha2] |
||||
|
||||
|
||||
-- Pickling |
||||
|
||||
failurePickle :: PU [Node] (Element) |
||||
failurePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}failure" |
||||
(xpIsolate xpElemVerbatim) |
||||
|
||||
challengePickle :: PU [Node] Text.Text |
||||
challengePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge" |
||||
(xpIsolate $ xpContent xpId) |
||||
|
||||
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
{-# LANGUAGE OverloadedStrings #-} |
||||
|
||||
module Network.XMPP.Session where |
||||
|
||||
import Data.XML.Pickle |
||||
import Data.XML.Types(Element) |
||||
|
||||
import Network.XMPP.Monad |
||||
import Network.XMPP.Pickle |
||||
import Network.XMPP.Types |
||||
|
||||
sessionXML :: Element |
||||
sessionXML = pickleElem |
||||
(xpElemBlank "{urn:ietf:params:xml:ns:xmpp-session}session" ) |
||||
() |
||||
|
||||
|
||||
|
||||
|
||||
sessionIQ :: Stanza |
||||
sessionIQ = IQRequestS $ IQRequest { iqRequestID = "sess" |
||||
, iqRequestFrom = Nothing |
||||
, iqRequestTo = Nothing |
||||
, iqRequestLangTag = Nothing |
||||
, iqRequestType = Set |
||||
, iqRequestPayload = sessionXML |
||||
} |
||||
|
||||
xmppSession :: XMPPConMonad () |
||||
xmppSession = do |
||||
push $ sessionIQ |
||||
answer <- pull |
||||
let IQResultS (IQResult "sess" Nothing Nothing _lang _body) = answer |
||||
return () |
||||
|
||||
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings #-} |
||||
{-# LANGUAGE TupleSections #-} |
||||
|
||||
module Network.XMPP.Stream where |
||||
|
||||
import Control.Applicative((<$>)) |
||||
import Control.Monad(unless) |
||||
import Control.Monad.Trans.State |
||||
|
||||
import Data.Conduit |
||||
import Data.Conduit.List as CL |
||||
import Data.Text as T |
||||
import Data.XML.Pickle |
||||
import Data.XML.Types |
||||
|
||||
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 |
||||
|
||||
throwOutJunk :: Monad m => Sink Event m () |
||||
throwOutJunk = do |
||||
next <- CL.peek |
||||
case next of |
||||
Nothing -> return () |
||||
Just (EventBeginElement _ _) -> return () |
||||
_ -> CL.drop 1 >> throwOutJunk |
||||
|
||||
openElementFromEvents :: Monad m => Sink Event m Element |
||||
openElementFromEvents = do |
||||
throwOutJunk |
||||
Just (EventBeginElement name attrs) <- CL.head |
||||
return $ Element name attrs [] |
||||
|
||||
|
||||
xmppStartStream :: XMPPConMonad () |
||||
xmppStartStream = do |
||||
hostname <- gets sHostname |
||||
pushOpen $ pickleElem pickleStream ("1.0",Nothing, hostname) |
||||
features <- pulls xmppStream |
||||
modify (\s -> s {sFeatures = features}) |
||||
return () |
||||
|
||||
xmppRestartStream :: XMPPConMonad () |
||||
xmppRestartStream = do |
||||
raw <- gets sRawSrc |
||||
let newsrc = raw $= XP.parseBytes def |
||||
modify (\s -> s{sConSrc = newsrc}) |
||||
xmppStartStream |
||||
|
||||
|
||||
xmppStream :: Sink Event IO ServerFeatures |
||||
xmppStream = do |
||||
xmppStreamHeader |
||||
xmppStreamFeatures |
||||
|
||||
xmppStreamHeader :: Sink Event IO () |
||||
xmppStreamHeader = do |
||||
throwOutJunk |
||||
(ver, _, _) <- unpickleElem pickleStream <$> openElementFromEvents |
||||
unless (ver == "1.0") $ error "Not XMPP version 1.0 " |
||||
return() |
||||
|
||||
|
||||
xmppStreamFeatures :: Sink Event IO ServerFeatures |
||||
xmppStreamFeatures = unpickleElem pickleStreamFeatures <$> elementFromEvents |
||||
|
||||
|
||||
-- Pickling |
||||
|
||||
pickleStream :: PU [Node] (Text, Maybe Text, Maybe Text) |
||||
pickleStream = xpElemAttrs (Name "stream" (Just "http://etherx.jabber.org/streams") (Just "stream")) |
||||
(xpTriple |
||||
(xpAttr "version" xpId) |
||||
(xpOption $ xpAttr "from" xpId) |
||||
(xpOption $ xpAttr "to" xpId) |
||||
) |
||||
|
||||
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) ) |
||||
|
||||
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) |
||||
) |
||||
|
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
{-# LANGUAGE OverloadedStrings #-} |
||||
|
||||
module Network.XMPP.TLS where |
||||
|
||||
import Control.Monad |
||||
import Control.Monad.Trans.Class |
||||
import Control.Monad.Trans.State |
||||
|
||||
import Data.Conduit |
||||
import Data.Conduit.List as CL |
||||
import Data.Conduit.TLS as TLS |
||||
import Data.Default |
||||
import Data.XML.Types |
||||
|
||||
import qualified Network.TLS as TLS |
||||
import qualified Network.TLS.Extra as TLS |
||||
import Network.XMPP.Monad |
||||
import Network.XMPP.Stream |
||||
import Network.XMPP.Types |
||||
|
||||
import qualified Text.XML.Stream.Render as XR |
||||
|
||||
|
||||
starttlsE :: Element |
||||
starttlsE = |
||||
Element "{urn:ietf:params:xml:ns:xmpp-tls}starttls" [] [] |
||||
|
||||
exampleParams :: TLS.TLSParams |
||||
exampleParams = TLS.defaultParams |
||||
{pConnectVersion = TLS.TLS10 |
||||
, pAllowedVersions = [TLS.SSL3, TLS.TLS10, TLS.TLS11] |
||||
, pCiphers = [TLS.cipher_AES128_SHA1] |
||||
, pCompressions = [TLS.nullCompression] |
||||
, pWantClientCert = False -- Used for servers |
||||
, pUseSecureRenegotiation = False -- No renegotiation |
||||
, pCertificates = [] -- TODO |
||||
, pLogging = TLS.defaultLogging -- TODO |
||||
, onCertificatesRecv = \ certificate -> |
||||
return TLS.CertificateUsageAccept |
||||
} |
||||
|
||||
xmppStartTLS :: TLS.TLSParams -> XMPPConMonad () |
||||
xmppStartTLS params = do |
||||
features <- gets sFeatures |
||||
unless (stls features == Nothing) $ do |
||||
pushN starttlsE |
||||
Element "{urn:ietf:params:xml:ns:xmpp-tls}proceed" [] [] <- pullE |
||||
Just handle <- gets sConHandle |
||||
(raw, snk, psh) <- lift $ TLS.tlsinit params handle |
||||
modify (\x -> x |
||||
{ sRawSrc = raw |
||||
-- , sConSrc = -- Note: this momentarily leaves us in an |
||||
-- inconsistent state |
||||
, sConPushBS = psh |
||||
}) |
||||
xmppRestartStream |
||||
modify (\s -> s{sHaveTLS = True}) |
||||
return () |
||||
|
||||
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
{-# LANGUAGE PackageImports, OverloadedStrings, NoMonomorphismRestriction #-} |
||||
module Example where |
||||
|
||||
import Control.Concurrent |
||||
import Control.Concurrent.STM |
||||
import Control.Monad |
||||
import Control.Monad.IO.Class |
||||
|
||||
import Data.Maybe |
||||
import Data.Text (Text) |
||||
import qualified Data.Text as Text |
||||
import Data.XML.Pickle |
||||
import Data.XML.Types |
||||
|
||||
import Network.XMPP |
||||
import Network.XMPP.Pickle |
||||
|
||||
import System.Environment |
||||
import Text.XML.Stream.Elements |
||||
|
||||
testUser1 :: JID |
||||
testUser1 = read "testuser1@species64739.dyndns.org/bot1" |
||||
|
||||
testUser2 :: JID |
||||
testUser2 = read "testuser2@species64739.dyndns.org/bot2" |
||||
|
||||
supervisor :: JID |
||||
supervisor = read "uart14@species64739.dyndns.org" |
||||
|
||||
|
||||
attXmpp :: STM a -> XMPPThread a |
||||
attXmpp = liftIO . atomically |
||||
|
||||
testNS :: Text |
||||
testNS = "xmpp:library:test" |
||||
|
||||
data Payload = Payload Int Bool Text deriving (Eq, Show) |
||||
|
||||
payloadP = xpWrap (\((counter,flag) , message) -> Payload counter flag message) |
||||
(\(Payload counter flag message) ->((counter,flag) , message)) $ |
||||
xpElem (Name "request" (Just testNS) Nothing) |
||||
(xpPair |
||||
(xpAttr "counter" xpPrim) |
||||
(xpAttr "flag" xpPrim) |
||||
) |
||||
(xpElemNodes (Name "message" (Just testNS) Nothing) |
||||
(xpContent xpId)) |
||||
|
||||
invertPayload (Payload count flag message) = Payload (count + 1) (not flag) (Text.reverse message) |
||||
|
||||
iqResponder = do |
||||
(free, chan) <- listenIQChan Get testNS |
||||
unless free $ liftIO $ putStrLn "Channel was already taken" |
||||
>> error "hanging up" |
||||
forever $ do |
||||
next@(iq,_) <- liftIO . atomically $ readTChan chan |
||||
let payload = unpickleElem payloadP $ iqRequestPayload iq |
||||
let answerPayload = invertPayload payload |
||||
let answerBody = pickleElem payloadP answerPayload |
||||
answerIQ next (Right $ Just answerBody) |
||||
|
||||
autoAccept :: XMPPThread () |
||||
autoAccept = forever $ do |
||||
st <- waitForPresence isPresenceSubscribe |
||||
sendPresence $ presenceSubscribed (fromJust $ presenceFrom st) |
||||
|
||||
sendUser = sendMessage . simpleMessage supervisor . Text.pack |
||||
|
||||
expect debug x y | x == y = debug "Ok." |
||||
| otherwise = do |
||||
let failMSG = "failed" ++ show x ++ " /= " ++ show y |
||||
debug failMSG |
||||
sendUser failMSG |
||||
|
||||
|
||||
wait3 :: MonadIO m => m () |
||||
wait3 = liftIO $ threadDelay 1000000 |
||||
|
||||
runMain :: (String -> STM ()) -> Int -> IO () |
||||
runMain debug number = do |
||||
let (we, them, active) = case number of |
||||
1 -> (testUser1, testUser2,True) |
||||
2 -> (testUser2, testUser1,False) |
||||
_ -> error "Need either 1 or 2" |
||||
let debug' = liftIO . atomically . |
||||
debug . (("Thread " ++ show number ++ ":") ++) |
||||
xmppNewSession $ do |
||||
debug' "running" |
||||
withConnection $ do |
||||
xmppConnect "localhost" "species64739.dyndns.org" |
||||
xmppStartTLS exampleParams |
||||
saslResponse <- xmppSASL (fromJust $ node we) "pwd" |
||||
case saslResponse of |
||||
Right _ -> return () |
||||
Left e -> error e |
||||
xmppThreadedBind (resource we) |
||||
withConnection $ xmppSession |
||||
debug' "session standing" |
||||
sendPresence presenceOnline |
||||
forkXMPP autoAccept |
||||
forkXMPP iqResponder |
||||
when active . void . forkXMPP $ do |
||||
forM [1..10] $ \count -> do |
||||
let message = Text.pack . show $ node we |
||||
let payload = Payload count (even count) (Text.pack $ show count) |
||||
let body = pickleElem payloadP payload |
||||
Right answer <- sendIQ' (Just them) Get Nothing body |
||||
let answerPayload = unpickleElem payloadP |
||||
(fromJust $ iqResultPayload answer) |
||||
expect debug' (invertPayload payload) answerPayload |
||||
liftIO $ threadDelay 100000 |
||||
sendUser "All tests done" |
||||
liftIO . forever $ threadDelay 10000000 |
||||
return () |
||||
return () |
||||
|
||||
|
||||
main = do |
||||
out <- newTChanIO |
||||
forkIO . forever $ atomically (readTChan out) >>= putStrLn |
||||
let debugOut = writeTChan out |
||||
forkIO $ runMain debugOut 1 |
||||
runMain debugOut 2 |
||||
|
||||
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
module Text.XML.Stream.Elements where |
||||
|
||||
import Control.Applicative ((<$>)) |
||||
import Control.Monad.Trans.Class |
||||
import Control.Monad.Trans.Resource as R |
||||
|
||||
import qualified Data.ByteString as BS |
||||
import qualified Data.Text as Text |
||||
import qualified Data.Text.Encoding as Text |
||||
import Data.XML.Types |
||||
import qualified Text.XML.Stream.Render as TXSR |
||||
import Text.XML.Unresolved as TXU |
||||
|
||||
import Data.Conduit as C |
||||
import Data.Conduit.List as CL |
||||
|
||||
import System.IO.Unsafe(unsafePerformIO) |
||||
|
||||
compressNodes :: [Node] -> [Node] |
||||
compressNodes [] = [] |
||||
compressNodes [x] = [x] |
||||
compressNodes (NodeContent (ContentText x) : NodeContent (ContentText y) : z) = |
||||
compressNodes $ NodeContent (ContentText $ x `Text.append` y) : z |
||||
compressNodes (x:xs) = x : compressNodes xs |
||||
|
||||
elementFromEvents :: R.MonadThrow m => C.Sink Event m Element |
||||
elementFromEvents = do |
||||
x <- CL.peek |
||||
case x of |
||||
Just (EventBeginElement n as) -> goE n as |
||||
_ -> lift $ R.monadThrow $ InvalidEventStream $ "not an element: " ++ show x |
||||
where |
||||
many' f = |
||||
go id |
||||
where |
||||
go front = do |
||||
x <- f |
||||
case x of |
||||
Nothing -> return $ front [] |
||||
Just y -> go (front . (:) y) |
||||
dropReturn x = CL.drop 1 >> return x |
||||
goE n as = do |
||||
CL.drop 1 |
||||
ns <- many' goN |
||||
y <- CL.head |
||||
if y == Just (EventEndElement n) |
||||
then return $ Element n as $ compressNodes ns |
||||
else lift $ R.monadThrow $ InvalidEventStream $ "Missing end element for " ++ show n ++ ", got: " ++ show y |
||||
goN = do |
||||
x <- CL.peek |
||||
case x of |
||||
Just (EventBeginElement n as) -> (Just . NodeElement) <$> goE n as |
||||
Just (EventInstruction i) -> dropReturn $ Just $ NodeInstruction i |
||||
Just (EventContent c) -> dropReturn $ Just $ NodeContent c |
||||
Just (EventComment t) -> dropReturn $ Just $ NodeComment t |
||||
Just (EventCDATA t) -> dropReturn $ Just $ NodeContent $ ContentText t |
||||
_ -> return Nothing |
||||
|
||||
|
||||
openElementToEvents :: Element -> [Event] |
||||
openElementToEvents (Element name as ns) = EventBeginElement name as : goN ns [] |
||||
where |
||||
goE (Element name' as' ns') = |
||||
(EventBeginElement name' as' :) |
||||
. goN ns' |
||||
. (EventEndElement name' :) |
||||
goN [] = id |
||||
goN [x] = goN' x |
||||
goN (x:xs) = goN' x . goN xs |
||||
goN' (NodeElement e) = goE e |
||||
goN' (NodeInstruction i) = (EventInstruction i :) |
||||
goN' (NodeContent c) = (EventContent c :) |
||||
goN' (NodeComment t) = (EventComment t :) |
||||
|
||||
elementToEvents :: Element -> [Event] |
||||
elementToEvents e@(Element name _ _) = openElementToEvents e ++ [EventEndElement name] |
||||
|
||||
|
||||
renderOpenElement :: Element -> BS.ByteString |
||||
renderOpenElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO |
||||
$ CL.sourceList (openElementToEvents e) $$ TXSR.renderText def =$ CL.consume |
||||
|
||||
renderElement :: Element -> BS.ByteString |
||||
renderElement e = Text.encodeUtf8 . Text.concat . unsafePerformIO |
||||
$ CL.sourceList (elementToEvents e) $$ TXSR.renderText def =$ CL.consume |
||||
|
||||
ppElement :: Element -> String |
||||
ppElement = Text.unpack . Text.decodeUtf8 . renderElement |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
module Utils where |
||||
|
||||
whileJust f = do |
||||
f' <- f |
||||
case f' of |
||||
Just x -> x : whileJust f |
||||
Nothing -> [] |
||||
Loading…
Reference in new issue