{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleInstances #-} -- instance for TChan Event {-# LANGUAGE DoAndIfThenElse #-} module XMPPServer ( xmppServer , ConnectionKey(..) , XMPPServerParameters(..) , XMPPServer , xmppConnections , xmppEventChannel , StanzaWrap(..) , Stanza(..) , StanzaType(..) , StanzaOrigin(..) , cloneStanza , LangSpecificMessage(..) , peerKeyToText , addrToText , sendModifiedStanzaToPeer , sendModifiedStanzaToClient , presenceProbe , presenceSolicitation , makePresenceStanza , makeInformSubscription , makeRosterUpdate , makeMessage , JabberShow(..) , Server , flushPassThrough ) where import ConnectionKey import qualified Control.Concurrent.STM.UpdateStream as Slotted import Nesting import Connection.Tcp import EventUtil import ControlMaybe import LockedChan import PeerResolve import Blaze.ByteString.Builder (Builder) import Debug.Trace import System.IO (hFlush,stdout) import Control.Monad.Trans.Resource import Control.Monad.Trans (lift) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Fix (fix) import Control.Monad #ifdef THREAD_DEBUG import Control.Concurrent.Lifted.Instrument (forkIO,myThreadId,labelThread) #else import Control.Concurrent.Lifted (forkIO,myThreadId) import GHC.Conc (labelThread) #endif import Control.Concurrent.STM -- import Control.Concurrent.STM.TChan import Network.SocketLike import Text.Printf import System.Posix.Signals import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as Strict8 -- import qualified Data.ByteString.Lazy.Char8 as Lazy8 import Data.Int (Int8) import Data.Conduit import qualified Data.Conduit.List as CL import qualified Data.Conduit.Binary as CB import Data.Conduit.Blaze (builderToByteStringFlush) import qualified Text.XML.Stream.Render as XML hiding (content) import qualified Text.XML.Stream.Parse as XML import Data.XML.Types as XML import Data.Maybe import Data.List (nub) import Data.Monoid ( (<>) ) import Data.Text (Text) import qualified Data.Text as Text (pack,unpack,words,intercalate) import Data.Char (toUpper,chr,ord) import Data.Map (Map) import qualified Data.Map as Map import Data.Set (Set, (\\) ) import qualified Data.Set as Set import Data.String ( IsString(..) ) import qualified System.Random import Data.Void (Void) import System.Endian (toBE32) import Control.Applicative import System.IO import qualified Connection peerport :: PortNumber peerport = 5269 clientport :: PortNumber clientport = 5222 my_uuid :: Text my_uuid = "154ae29f-98f2-4af4-826d-a40c8a188574" data JabberShow = Offline | ExtendedAway | Away | DoNotDisturb | Available | Chatty deriving (Show,Enum,Ord,Eq,Read) data MessageThread = MessageThread { msgThreadParent :: Maybe Text, msgThreadContent :: Text } deriving (Show,Eq) data LangSpecificMessage = LangSpecificMessage { msgBody :: Maybe Text , msgSubject :: Maybe Text } deriving (Show,Eq) data RosterEventType = RequestedSubscription | NewBuddy -- preceded by PresenceInformSubscription True | RemovedBuddy -- preceded by PresenceInformSubscription False | PendingSubscriber -- same as PresenceRequestSubscription | NewSubscriber | RejectSubscriber deriving (Show,Read,Ord,Eq,Enum) data ClientHack = SimulatedChatErrors deriving (Show,Read,Ord,Eq,Enum) data StanzaType = Unrecognized | Ping | Pong | RequestResource (Maybe Text) (Maybe Text) -- ^ Client's name for this host followed by client's requested resource id. | SetResource | SessionRequest | UnrecognizedQuery Name | RequestRoster | Roster | RosterEvent { rosterEventType :: RosterEventType , rosterUser :: Text , rosterContact :: Text } | Error StanzaError XML.Event | PresenceStatus { presenceShow :: JabberShow , presencePriority :: Maybe Int8 , presenceStatus :: [(Lang,Text)] , presenceWhiteList :: [Text] } | PresenceInformError | PresenceInformSubscription Bool | PresenceRequestStatus | PresenceRequestSubscription Bool | Message { msgThread :: Maybe MessageThread , msgLangMap :: [(Lang,LangSpecificMessage)] } | NotifyClientVersion { versionName :: Text , versionVersion :: Text } | InternalEnableHack ClientHack | InternalCacheId Text deriving (Show,Eq) data StanzaOrigin = LocalPeer | NetworkOrigin ConnectionKey (TChan Stanza) data StanzaWrap a = Stanza { stanzaType :: StanzaType , stanzaId :: Maybe Text , stanzaTo :: Maybe Text , stanzaFrom :: Maybe Text , stanzaChan :: a , stanzaClosers :: TVar (Maybe [XML.Event]) , stanzaInterrupt :: TMVar () , stanzaOrigin :: StanzaOrigin } type Stanza = StanzaWrap (LockedChan XML.Event) data XMPPServerParameters = XMPPServerParameters { -- | Called when a client requests a resource id. The first Maybe indicates -- the name the client referred to this server by. The second Maybe is the -- client's preferred resource name. xmppChooseResourceName :: ConnectionKey -> SockAddr -> Maybe Text -> Maybe Text -> IO Text , -- | This should indicate the server's hostname that all client's see. xmppTellMyNameToClient :: IO Text , xmppTellMyNameToPeer :: SockAddr -> IO Text , xmppTellClientHisName :: ConnectionKey -> IO Text , xmppTellPeerHisName :: ConnectionKey -> IO Text , xmppNewConnection :: ConnectionKey -> SockAddr -> TChan Stanza -> IO () , xmppEOF :: ConnectionKey -> IO () , xmppRosterBuddies :: ConnectionKey -> IO [Text] , xmppRosterSubscribers :: ConnectionKey -> IO [Text] , xmppRosterSolicited :: ConnectionKey -> IO [Text] , xmppRosterOthers :: ConnectionKey -> IO [Text] , -- | Called when after sending a roster to a client. Usually this means -- the client status should change from "available" to "interested". xmppSubscribeToRoster :: ConnectionKey -> IO () -- , xmppLookupClientJID :: ConnectionKey -> IO Text , xmppTellClientNameOfPeer :: ConnectionKey -> [Text] -> IO Text , xmppDeliverMessage :: (IO ()) -> Stanza -> IO () -- | Called whenever a local client's presence changes. , xmppInformClientPresence :: ConnectionKey -> Stanza -> IO () -- | Called whenever a remote peer's presence changes. , xmppInformPeerPresence :: ConnectionKey -> Stanza -> IO () , -- | Called when a remote peer requests our status. xmppAnswerProbe :: ConnectionKey -> Stanza -> TChan Stanza -> IO () , xmppClientSubscriptionRequest :: IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () , -- | Called when a remote peer sends subscription request. xmppPeerSubscriptionRequest :: IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () , xmppClientInformSubscription :: IO () -> ConnectionKey -> Stanza -> IO () , -- | Called when a remote peer informs us of our subscription status. xmppPeerInformSubscription :: IO () -> ConnectionKey -> Stanza -> IO () , xmppVerbosity :: IO Int } enableClientHacks :: forall t a. (Eq a, IsString a) => a -> t -> TChan Stanza -> IO () enableClientHacks "Pidgin" version replyto = do wlog "Enabling hack SimulatedChatErrors for client Pidgin" donevar <- atomically newEmptyTMVar sendReply donevar (InternalEnableHack SimulatedChatErrors) [] replyto enableClientHacks "irssi-xmpp" version replyto = do wlog "Enabling hack SimulatedChatErrors for client irssi-xmpp" donevar <- atomically newEmptyTMVar sendReply donevar (InternalEnableHack SimulatedChatErrors) [] replyto enableClientHacks _ _ _ = return () cacheMessageId :: Text -> TChan Stanza -> IO () cacheMessageId id' replyto = do wlog $ "Caching id " ++ Text.unpack id' donevar <- atomically newEmptyTMVar sendReply donevar (InternalCacheId id') [] replyto -- TODO: http://xmpp.org/rfcs/rfc6120.html#rules-remote-error -- client connection -- socat script to send stanza fragment -- copyToChannel can keep a stack of closers to append to finish-off a stanza -- the TMVar () from forkConnection can be passed and with a stanza to detect interruption addrToText :: SockAddr -> Text addrToText (addr@(SockAddrInet _ _)) = Text.pack $ stripColon (show addr) where stripColon s = pre where (pre,port) = break (==':') s addrToText (addr@(SockAddrInet6 _ _ _ _)) = Text.pack $ stripColon (show addr) where stripColon s = if null bracket then pre else pre ++ "]" where (pre,bracket) = break (==']') s peerKeyToText :: ConnectionKey -> Text peerKeyToText (PeerKey { callBackAddress=addr }) = addrToText addr peerKeyToText (ClientKey { localAddress=addr }) = "ErrorClIeNt0" wlog :: String -> IO () wlog s = putStrLn s >> hFlush stdout wlogb :: ByteString -> IO () wlogb s = Strict8.putStrLn s >> hFlush stdout flushPassThrough :: Monad m => Conduit a m b -> Conduit (Flush a) m (Flush b) flushPassThrough c = getZipConduit $ ZipConduit (onlyChunks =$= mapOutput Chunk c) <* ZipConduit onlyFlushes where onlyChunks :: Monad m => Conduit (Flush a) m a onlyFlushes :: Monad m => Conduit (Flush a) m (Flush b) onlyChunks = awaitForever yieldChunk onlyFlushes = awaitForever yieldFlush yieldFlush Flush = yield Flush yieldFlush _ = return () yieldChunk (Chunk x) = yield x yieldChunk _ = return () xmlStream :: ReadCommand -> WriteCommand -> ( Source IO XML.Event , Sink (Flush XML.Event) IO () ) xmlStream conread conwrite = (xsrc,xsnk) where xsrc = src $= XML.parseBytes XML.def xsnk :: Sink (Flush Event) IO () xsnk = -- XML.renderBytes XML.def =$ snk flushPassThrough (XML.renderBuilder XML.def) =$= builderToByteStringFlush =$= discardFlush =$ snk where discardFlush :: Monad m => ConduitM (Flush a) a m () discardFlush = awaitForever yieldChunk yieldChunk (Chunk x) = yield x yieldChunk _ = return () src = do v <- lift conread maybe (return ()) -- lift . wlog $ "conread: Nothing") (yield >=> const src) v snk = awaitForever $ liftIO . conwrite type FlagCommand = STM Bool type ReadCommand = IO (Maybe ByteString) type WriteCommand = ByteString -> IO Bool cloneStanza :: StanzaWrap (LockedChan a) -> IO (StanzaWrap (LockedChan a)) cloneStanza stanza = do dupped <- cloneLChan (stanzaChan stanza) return stanza { stanzaChan = dupped } copyToChannel :: MonadIO m => (Event -> a) -> LockedChan a -> TVar (Maybe [Event]) -> ConduitM Event Event m () copyToChannel f chan closer_stack = awaitForever copy where copy x = do liftIO . atomically $ writeLChan chan (f x) case x of EventBeginDocument {} -> do let clsr = closerFor x liftIO . atomically $ modifyTVar' closer_stack (fmap (clsr:)) EventEndDocument {} -> do liftIO . atomically $ modifyTVar' closer_stack (fmap (drop 1)) _ -> return () yield x prettyPrint :: ByteString -> ConduitM Event Void IO () prettyPrint prefix = XML.renderBytes (XML.def { XML.rsPretty=True }) =$= CB.lines =$ CL.mapM_ (wlogb . (prefix <>)) swapNamespace :: Monad m => Text -> Text -> ConduitM Event Event m () swapNamespace old new = awaitForever (yield . swapit old new) swapit :: Text -> Text -> Event -> Event swapit old new (EventBeginElement n as) | nameNamespace n==Just old = EventBeginElement (n { nameNamespace = Just new }) as swapit old new (EventEndElement n) | nameNamespace n==Just old = EventEndElement (n { nameNamespace = Just new }) swapit old new x = x fixHeaders :: Monad m => Stanza -> ConduitM Event Event m () fixHeaders Stanza { stanzaType=typ, stanzaTo=mto, stanzaFrom=mfrom } = do x <- await maybe (return ()) f x where f (EventBeginElement n as) = do yield $ EventBeginElement n (update n as) awaitForever yield f x = yield x >> awaitForever yield update n as = as3 where as' = maybe as (setAttrib "to" as) mto as'' = maybe as' (setAttrib "from" as') mfrom as3 = case typ of PresenceStatus {} | nameNamespace n == Just "jabber:client" -> delAttrib "whitelist" as'' PresenceStatus {} | otherwise -> case presenceWhiteList typ of [] -> delAttrib "whitelist" as'' ws -> setAttrib "whitelist" as'' (Text.intercalate " " ws) _ -> as'' setAttrib akey as aval = attr akey aval:filter ((/=akey) . fst) as delAttrib akey as = filter ((/=akey) . fst) as conduitToChan :: Conduit () IO Event -> IO (LockedChan Event, TVar (Maybe [Event]), TMVar a) conduitToChan c = do chan <- atomically newLockedChan clsrs <- atomically $ newTVar (Just []) quitvar <- atomically $ newEmptyTMVar forkIO $ do c =$= copyToChannel id chan clsrs $$ awaitForever (const $ return ()) atomically $ writeTVar clsrs Nothing return (chan,clsrs,quitvar) conduitToStanza :: StanzaType -> Maybe Text -- ^ id -> Maybe Text -- ^ from -> Maybe Text -- ^ to -> Conduit () IO Event -> IO Stanza conduitToStanza stype mid from to c = do (chan,clsrs,quitvar) <- conduitToChan c return Stanza { stanzaType = stype , stanzaId = mid , stanzaTo = to , stanzaFrom = from , stanzaChan = chan , stanzaClosers = clsrs , stanzaInterrupt = quitvar , stanzaOrigin = LocalPeer } ioWriteChan :: MonadIO m => TChan a -> a -> m () ioWriteChan c v = liftIO . atomically $ writeTChan c v stanzaToConduit :: MonadIO m => Stanza -> ConduitM i Event m () stanzaToConduit stanza = do let xchan = stanzaChan stanza xfin = stanzaClosers stanza rdone = stanzaInterrupt stanza loop = return () xchan <- liftIO $ unlockChan xchan fix $ \inner -> do what <- liftIO . atomically $ foldr1 orElse [readTChan xchan >>= \xml -> return $ do yield xml -- atomically $ Slotted.push slots Nothing xml inner ,do mb <- readTVar xfin cempty <- isEmptyTChan xchan if isNothing mb then if cempty then return loop else retry else do done <- tryReadTMVar rdone check (isJust done) trace "todo: send closers" retry ,do isEmptyTChan xchan >>= check readTMVar rdone return (return ())] what sendModifiedStanzaToPeer :: Stanza -> TChan Stanza -> IO () sendModifiedStanzaToPeer stanza chan = do (echan,clsrs,quitvar) <- conduitToChan c ioWriteChan chan stanza { stanzaChan = echan , stanzaClosers = clsrs , stanzaInterrupt = quitvar , stanzaType = processedType (stanzaType stanza) -- TODO id? origin? } where old = "jabber:client" new = "jabber:server" c = stanzaToConduit stanza =$= swapNamespace old new =$= fixHeaders stanza processedType (Error cond tag) = Error cond (swapit old new tag) processedType x = x sendModifiedStanzaToClient :: Stanza -> TChan Stanza -> IO () sendModifiedStanzaToClient stanza chan = do (echan,clsrs,quitvar) <- conduitToChan c -- wlog $ "send-to-client " ++ show (stanzaId stanza) ioWriteChan chan stanza { stanzaChan = echan , stanzaClosers = clsrs , stanzaInterrupt = quitvar , stanzaType = processedType (stanzaType stanza) -- TODO id? origin? } where old = "jabber:server" new = "jabber:client" c = stanzaToConduit stanza =$= swapNamespace old new =$= fixHeaders stanza processedType (Error cond tag) = Error cond (swapit old new tag) processedType x = x -- id,to, and from are taken as-is from reply list -- todo: this should probably be restricted to IO monad sendReply :: (Functor m, MonadIO m) => TMVar () -> StanzaType -> [Event] -> TChan Stanza -> m () sendReply donevar stype reply replychan = do let stanzaTag = listToMaybe reply mid = stanzaTag >>= lookupAttrib "id" . tagAttrs mfrom = stanzaTag >>= lookupAttrib "from" . tagAttrs mto = stanzaTag >>= lookupAttrib "to" . tagAttrs isInternal (InternalEnableHack {}) = True isInternal (InternalCacheId {}) = True isInternal _ = False flip (maybe $ return ()) (fmap (const ()) stanzaTag `mplus` guard (isInternal stype)) . const $ do replyStanza <- liftIO . atomically $ do replyChan <- newLockedChan replyClsrs <- newTVar (Just []) return Stanza { stanzaType = stype , stanzaId = mid , stanzaTo = mto -- as-is from reply list , stanzaFrom = mfrom -- as-is from reply list , stanzaChan = replyChan , stanzaClosers = replyClsrs , stanzaInterrupt = donevar , stanzaOrigin = LocalPeer } ioWriteChan replychan replyStanza void . liftIO . forkIO $ do mapM_ (liftIO . atomically . writeLChan (stanzaChan replyStanza)) reply liftIO . atomically $ writeTVar (stanzaClosers replyStanza) Nothing -- liftIO $ wlog "finished reply stanza" stanzaFromList :: StanzaType -> [Event] -> IO Stanza stanzaFromList stype reply = do let stanzaTag = listToMaybe reply mid = stanzaTag >>= lookupAttrib "id" . tagAttrs mfrom = stanzaTag >>= lookupAttrib "from" . tagAttrs mto = stanzaTag >>= lookupAttrib "to" . tagAttrs {- isInternal (InternalEnableHack {}) = True isInternal (InternalCacheId {}) = True isInternal _ = False -} (donevar,replyChan,replyClsrs) <- atomically $ do donevar <- newEmptyTMVar -- TMVar () replyChan <- newLockedChan replyClsrs <- newTVar (Just []) return (donevar,replyChan, replyClsrs) forkIO $ do forM_ reply $ atomically . writeLChan replyChan atomically $ do putTMVar donevar () writeTVar replyClsrs Nothing return Stanza { stanzaType = stype , stanzaId = mid , stanzaTo = mto -- as-is from reply list , stanzaFrom = mfrom -- as-is from reply list , stanzaChan = replyChan , stanzaClosers = replyClsrs , stanzaInterrupt = donevar , stanzaOrigin = LocalPeer } grokStanzaIQGet :: Monad m => XML.Event -> NestingXML o m (Maybe StanzaType) grokStanzaIQGet stanza = do mtag <- nextElement flip (maybe $ return Nothing) mtag $ \tag -> do case tagName tag of "{urn:xmpp:ping}ping" -> return $ Just Ping "{jabber:iq:roster}query" -> return $ Just RequestRoster name -> return . Just $ UnrecognizedQuery name parseClientVersion :: NestingXML o IO (Maybe StanzaType) parseClientVersion = parseit Nothing Nothing where reportit mname mver = return $ do name <- mname ver <- mver return NotifyClientVersion { versionName=name, versionVersion=ver } parseit :: Maybe Text -> Maybe Text -> NestingXML o IO (Maybe StanzaType) parseit mname mver = do mtag <- nextElement flip (maybe $ reportit mname mver) mtag $ \tag -> do case tagName tag of "{jabber:iq:version}name" -> do x <- XML.content parseit (Just x) mver "{jabber:iq:version}version" -> do x <- XML.content parseit mname (Just x) _ -> parseit mname mver grokStanzaIQResult :: XML.Event -> NestingXML o IO (Maybe StanzaType) grokStanzaIQResult stanza = do mtag <- nextElement flip (maybe $ return (Just Pong)) mtag $ \tag -> do case tagName tag of "{jabber:iq:version}query" | nameNamespace (tagName stanza)==Just "jabber:client" -> parseClientVersion _ -> return Nothing grokStanzaIQSet :: XML.Event -> NestingXML o IO (Maybe StanzaType) grokStanzaIQSet stanza = do mtag <- nextElement flip (maybe $ return Nothing) mtag $ \tag -> do case tagName tag of "{urn:ietf:params:xml:ns:xmpp-bind}bind" -> do mchild <- nextElement case fmap tagName mchild of Just "{urn:ietf:params:xml:ns:xmpp-bind}resource" -> do rsc <- XML.content -- TODO: MonadThrow??? return . Just $ RequestResource Nothing (Just rsc) Just _ -> return Nothing Nothing -> return . Just $ RequestResource Nothing Nothing "{urn:ietf:params:xml:ns:xmpp-session}session" -> do return $ Just SessionRequest _ -> return Nothing {- C->Unrecognized Unrecognized type="set" C->Unrecognized id="purpleae62d88f" C->Unrecognized xmlns="jabber:client"> C->Unrecognized C->Unrecognized -} chanContents :: TChan x -> IO [x] chanContents ch = do x <- atomically $ do bempty <- isEmptyTChan ch if bempty then return Nothing else fmap Just $ readTChan ch maybe (return []) (\x -> do xs <- chanContents ch return (x:xs)) x parsePresenceStatus :: ( MonadThrow m , MonadIO m ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) parsePresenceStatus ns stanzaTag = do let toStat "away" = Away toStat "xa" = ExtendedAway toStat "dnd" = DoNotDisturb toStat "chat" = Chatty showv <- liftIO . atomically $ newTVar Available priov <- liftIO . atomically $ newTVar Nothing statusv <- liftIO . atomically $ newTChan fix $ \loop -> do mtag <- nextElement flip (maybe $ return ()) mtag $ \tag -> do when (nameNamespace (tagName tag) == Just ns) $ do case nameLocalName (tagName tag) of "show" -> do t <- XML.content liftIO . atomically $ writeTVar showv (toStat t) "priority" -> do t <- XML.content liftIO . handleIO_ (return ()) $ do prio <- readIO (Text.unpack t) atomically $ writeTVar priov (Just prio) "status" -> do t <- XML.content lang <- xmlLang ioWriteChan statusv (maybe "" id lang,t) _ -> return () loop show <- liftIO . atomically $ readTVar showv prio <- liftIO . atomically $ readTVar priov status <- liftIO $ chanContents statusv -- Could use unsafeInterleaveIO to -- avoid multiple passes, but whatever. let wlist = do w <- maybeToList $ lookupAttrib "whitelist" (tagAttrs stanzaTag) Text.words w return . Just $ PresenceStatus { presenceShow = show , presencePriority = prio , presenceStatus = status , presenceWhiteList = wlist } grokPresence :: ( MonadThrow m , MonadIO m ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) grokPresence ns stanzaTag = do let typ = lookupAttrib "type" (tagAttrs stanzaTag) case typ of Nothing -> parsePresenceStatus ns stanzaTag Just "unavailable" -> fmap (fmap (\p -> p {presenceShow=Offline})) $ parsePresenceStatus ns stanzaTag Just "error" -> return . Just $ PresenceInformError Just "unsubscribed" -> return . Just $ PresenceInformSubscription False Just "subscribed" -> return . Just $ PresenceInformSubscription True Just "probe" -> return . Just $ PresenceRequestStatus Just "unsubscribe" -> return . Just $ PresenceRequestSubscription False Just "subscribe" -> return . Just $ PresenceRequestSubscription True _ -> return Nothing parseMessage :: ( MonadThrow m , MonadIO m ) => Text -> XML.Event -> NestingXML o m StanzaType parseMessage ns stanza = do let bodytag = Name { nameNamespace = Just ns , nameLocalName = "body" , namePrefix = Nothing } subjecttag = Name { nameNamespace = Just ns , nameLocalName = "subject" , namePrefix = Nothing } threadtag = Name { nameNamespace = Just ns , nameLocalName = "thread" , namePrefix = Nothing } let emptyMsg = LangSpecificMessage { msgBody=Nothing, msgSubject=Nothing } parseChildren (th,cmap) = do child <- nextElement lvl <- nesting xmllang <- xmlLang let lang = maybe "" id xmllang let c = maybe emptyMsg id (Map.lookup lang cmap) -- log $ " child: "<> bshow child case child of Just tag | tagName tag==bodytag -> do txt <- XML.content awaitCloser lvl parseChildren (th,Map.insert lang (c { msgBody=Just txt }) cmap) Just tag | tagName tag==subjecttag -> do txt <- XML.content awaitCloser lvl parseChildren (th,Map.insert lang (c { msgSubject=Just txt }) cmap) Just tag | tagName tag==threadtag -> do txt <- XML.content awaitCloser lvl parseChildren (th {msgThreadContent=txt},cmap) Just tag -> do -- let nm = tagName tag -- attrs = tagAttrs tag -- -- elems = msgElements c -- txt <- XML.content awaitCloser lvl parseChildren (th,Map.insert lang c cmap) Nothing -> return (th,cmap) (th,langmap) <- parseChildren ( MessageThread {msgThreadParent=Nothing, msgThreadContent=""} , Map.empty ) return Message { msgLangMap = Map.toList langmap, msgThread = if msgThreadContent th/="" then Just th else Nothing } findConditionTag :: Monad m => NestingXML o m (Maybe XML.Event) findConditionTag = do x <- nextElement flip (maybe $ return Nothing) x $ \x -> do case nameNamespace (tagName x) of Just "urn:ietf:params:xml:ns:xmpp-stanzas" -> return (Just x) _ -> findConditionTag conditionFromText :: Text -> Maybe StanzaError conditionFromText t = fmap fst $ listToMaybe ss where es = [BadRequest .. UnexpectedRequest] ts = map (\e->(e,errorTagLocalName e)) es ss = dropWhile ((/=t) . snd) ts findErrorTag :: Monad m => Text -> NestingXML o m (Maybe StanzaError) findErrorTag ns = do x <- nextElement flip (maybe $ return Nothing) x $ \x -> do case tagName x of n | nameNamespace n==Just ns && nameLocalName n=="error" -> do mtag <- findConditionTag return $ do tag <- {- trace ("mtag = "++show mtag) -} mtag let t = nameLocalName (tagName tag) conditionFromText t _ -> findErrorTag ns grokMessage :: ( MonadThrow m , MonadIO m ) => Text -> XML.Event -> NestingXML o m (Maybe StanzaType) grokMessage ns stanzaTag = do let typ = lookupAttrib "type" (tagAttrs stanzaTag) case typ of Just "error" -> do mb <- findErrorTag ns return $ do e <- mb return $ Error e stanzaTag _ -> do t <- parseMessage ns stanzaTag return $ Just t grokStanza :: Text -> XML.Event -> NestingXML o IO (Maybe StanzaType) grokStanza "jabber:server" stanzaTag = case () of _ | stanzaTag `isServerIQOf` "get" -> grokStanzaIQGet stanzaTag _ | stanzaTag `isServerIQOf` "result" -> grokStanzaIQResult stanzaTag _ | tagName stanzaTag == "{jabber:server}presence" -> grokPresence "jabber:server" stanzaTag _ | tagName stanzaTag == "{jabber:server}message" -> grokMessage "jabber:server" stanzaTag _ -> return $ Just Unrecognized grokStanza "jabber:client" stanzaTag = case () of _ | stanzaTag `isClientIQOf` "get" -> grokStanzaIQGet stanzaTag _ | stanzaTag `isClientIQOf` "set" -> grokStanzaIQSet stanzaTag _ | stanzaTag `isClientIQOf` "result" -> grokStanzaIQResult stanzaTag _ | tagName stanzaTag == "{jabber:client}presence" -> grokPresence "jabber:client" stanzaTag _ | tagName stanzaTag == "{jabber:client}message" -> grokMessage "jabber:client" stanzaTag _ -> return $ Just Unrecognized mkname :: Text -> Text -> XML.Name mkname namespace name = (Name name (Just namespace) Nothing) makeMessage :: Text -> Text -> Text -> Text -> IO Stanza makeMessage namespace from to bod = stanzaFromList typ $ [ EventBeginElement (mkname namespace "message") [ attr "from" from , attr "to" to ] , EventBeginElement (mkname namespace "body") [] , EventContent (ContentText bod) , EventEndElement (mkname namespace "body") , EventEndElement (mkname namespace "message") ] where typ = Message { msgThread = Nothing , msgLangMap = [("", lsm)] } lsm = LangSpecificMessage { msgBody = Just bod , msgSubject = Nothing } makeInformSubscription :: Text -> Text -> Text -> Bool -> IO Stanza makeInformSubscription namespace from to approved = stanzaFromList (PresenceInformSubscription approved) $ [ EventBeginElement (mkname namespace "presence") [ attr "from" from , attr "to" to , attr "type" $ if approved then "subscribed" else "unsubscribed" ] , EventEndElement (mkname namespace "presence")] makePresenceStanza :: Text -> Maybe Text -> JabberShow -> IO Stanza makePresenceStanza namespace mjid pstat = do stanzaFromList PresenceStatus { presenceShow = pstat , presencePriority = Nothing , presenceStatus = [] , presenceWhiteList = [] } $ [ EventBeginElement (mkname namespace "presence") (setFrom $ typ pstat) ] ++ (shw pstat >>= jabberShow) ++ [ EventEndElement (mkname namespace "presence")] where setFrom = maybe id (\jid -> (attr "from" jid :) ) mjid typ Offline = [attr "type" "unavailable"] typ _ = [] shw ExtendedAway = ["xa"] shw Chatty = ["chat"] shw Away = ["away"] shw DoNotDisturb = ["dnd"] shw _ = [] jabberShow stat = [ EventBeginElement "{jabber:client}show" [] , EventContent (ContentText stat) , EventEndElement "{jabber:client}show" ] makeRosterUpdate :: Text -> Text -> [(Name, Text)] -> IO Stanza makeRosterUpdate tojid contact as = do let attrs = map (uncurry attr) as stanzaFromList Unrecognized [ EventBeginElement "{jabber:client}iq" [ attr "to" tojid , attr "id" "someid" , attr "type" "set" ] , EventBeginElement "{jabber:iq:roster}query" [] , EventBeginElement "{jabber:iq:roster}item" (attr "jid" contact : attrs) , EventEndElement "{jabber:iq:roster}item" , EventEndElement "{jabber:iq:roster}query" , EventEndElement "{jabber:client}iq" ] makePong :: Text -> Maybe Text -> Text -> Text -> [XML.Event] makePong namespace mid to from = -- Note: similar to session reply [ EventBeginElement (mkname namespace "iq") $(case mid of Just c -> (("id",[ContentText c]):) _ -> id) [ attr "type" "result" , attr "to" to , attr "from" from ] , EventEndElement (mkname namespace "iq") ] xmppInbound :: Server ConnectionKey SockAddr ReleaseKey XML.Event -> XMPPServerParameters -> ConnectionKey -> SockAddr -> FlagCommand -- ^ action to check whether the connection needs a ping -> TChan Stanza -- ^ channel to announce incoming stanzas on -> TChan Stanza -- ^ channel used to send stanzas -> TMVar () -- ^ mvar that is filled when the connection quits -> Sink XML.Event IO () xmppInbound sv xmpp k laddr pingflag stanzas output donevar = doNestingXML $ do let (namespace,tellmyname,tellyourname) = case k of ClientKey {} -> ( "jabber:client" , xmppTellMyNameToClient xmpp , xmppTellClientHisName xmpp k ) PeerKey {} -> ( "jabber:server" , xmppTellMyNameToPeer xmpp laddr , xmppTellPeerHisName xmpp k ) me <- liftIO tellmyname withXML $ \begindoc -> do when (begindoc==EventBeginDocument) $ do whenJust nextElement $ \xml -> do withJust (elementAttrs "stream" xml) $ \stream_attrs -> do -- liftIO $ hPutStrLn stderr $ "STREAM ATTRS "++show stream_attrs let stream_name = lookupAttrib "to" stream_attrs -- xmpp_version = lookupAttrib "version" stream_attrs fix $ \loop -> do -- liftIO . wlog $ "waiting for stanza." (chan,clsrs) <- liftIO . atomically $ liftM2 (,) newLockedChan (newTVar (Just [])) whenJust nextElement $ \stanzaTag -> do stanza_lvl <- nesting liftIO . atomically $ do writeLChan chan stanzaTag modifyTVar' clsrs (fmap (closerFor stanzaTag:)) copyToChannel id chan clsrs =$= do let mid = lookupAttrib "id" $ tagAttrs stanzaTag mfrom = lookupAttrib "from" $ tagAttrs stanzaTag mto = lookupAttrib "to" $ tagAttrs stanzaTag dispatch <- grokStanza namespace stanzaTag let unrecog = do let stype = Unrecognized s <- liftIO . atomically $ do return Stanza { stanzaType = stype , stanzaId = mid , stanzaTo = mto , stanzaFrom = mfrom , stanzaChan = chan , stanzaClosers = clsrs , stanzaInterrupt = donevar , stanzaOrigin = NetworkOrigin k output } ioWriteChan stanzas s you <- liftIO tellyourname flip (maybe $ unrecog) dispatch $ \dispatch -> case dispatch of -- Checking that the to-address matches this server. -- Otherwise it could be a client-to-client ping or a -- client-to-server for some other server. -- For now, assuming its for the immediate connection. Ping | mto==Just me || mto==Nothing -> do let pongto = maybe you id mfrom pongfrom = maybe me id mto pong = makePong namespace mid pongto pongfrom sendReply donevar Pong pong output #ifdef PINGNOISE -- TODO: Remove this, it is only to generate a debug print ioWriteChan stanzas Stanza { stanzaType = Ping , stanzaId = mid , stanzaTo = mto , stanzaFrom = mfrom , stanzaChan = chan , stanzaClosers = clsrs , stanzaInterrupt = donevar , stanzaOrigin = NetworkOrigin k output } #endif stype -> ioWriteChan stanzas Stanza { stanzaType = case stype of RequestResource _ rsc -> RequestResource stream_name rsc , stanzaId = mid , stanzaTo = mto , stanzaFrom = mfrom , stanzaChan = chan , stanzaClosers = clsrs , stanzaInterrupt = donevar , stanzaOrigin = NetworkOrigin k output } awaitCloser stanza_lvl liftIO . atomically $ writeTVar clsrs Nothing loop while :: IO Bool -> IO a -> IO [a] while cond body = do b <- cond if b then do x <- body xs <- while cond body return (x:xs) else return [] readUntilNothing :: TChan (Maybe x) -> IO [x] readUntilNothing ch = do x <- atomically $ readTChan ch maybe (return []) (\x -> do xs <- readUntilNothing ch return (x:xs)) x streamFeatures :: Text -> [XML.Event] streamFeatures "jabber:client" = [ EventBeginElement (streamP "features") [] , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" [] , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" {- -- , " " , " " -- , " DIGEST-MD5" , " PLAIN" , " " -} , EventEndElement (streamP "features") ] streamFeatures "jabber:server" = [] greet' :: Text -> Text -> [XML.Event] greet' namespace host = EventBeginDocument : greet'' namespace host greet'' :: Text -> Text -> [Event] greet'' namespace host = [ EventBeginElement (streamP "stream") [("from",[ContentText host]) ,("id",[ContentText "someid"]) ,("xmlns",[ContentText namespace]) ,("xmlns:stream",[ContentText "http://etherx.jabber.org/streams"]) ,("version",[ContentText "1.0"]) ] ] ++ streamFeatures namespace consid :: Maybe Text -> [(Name, [Content])] -> [(Name, [Content])] consid Nothing = id consid (Just sid) = (("id",[ContentText sid]):) data XMPPState = PingSlot deriving (Eq,Ord) makePing :: Text -> Maybe Text -> Text -> Text -> [XML.Event] makePing namespace mid to from = [ EventBeginElement (mkname namespace "iq") $ (case mid of Just c -> (("id",[ContentText c]):) _ -> id ) [ ("type",[ContentText "get"]) , attr "to" to , attr "from" from ] , EventBeginElement "{urn:xmpp:ping}ping" [] , EventEndElement "{urn:xmpp:ping}ping" , EventEndElement $ mkname namespace "iq"] iq_bind_reply :: Maybe Text -> Text -> [XML.Event] iq_bind_reply mid jid = [ EventBeginElement "{jabber:client}iq" (consid mid [("type",[ContentText "result"])]) , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" [("xmlns",[ContentText "urn:ietf:params:xml:ns:xmpp-bind"])] , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-bind}jid" [] , EventContent (ContentText jid) , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}jid" , EventEndElement "{urn:ietf:params:xml:ns:xmpp-bind}bind" , EventEndElement "{jabber:client}iq" {- -- query for client version , EventBeginElement "{jabber:client}iq" [ attr "to" jid , attr "from" hostname , attr "type" "get" , attr "id" "version"] , EventBeginElement "{jabber:iq:version}query" [] , EventEndElement "{jabber:iq:version}query" , EventEndElement "{jabber:client}iq" -} ] iq_session_reply :: Maybe Text -> Text -> [XML.Event] iq_session_reply mid host = -- Note: similar to Pong [ EventBeginElement "{jabber:client}iq" (consid mid [("from",[ContentText host]) ,("type",[ContentText "result"]) ]) , EventEndElement "{jabber:client}iq" ] iq_service_unavailable :: Maybe Text -> Text -> XML.Name -> [XML.Event] iq_service_unavailable mid host {- mjid -} req = [ EventBeginElement "{jabber:client}iq" (consid mid [attr "type" "error" ,attr "from" host]) , EventBeginElement req [] , EventEndElement req , EventBeginElement "{jabber:client}error" [ attr "type" "cancel" , attr "code" "503" ] , EventBeginElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" [] , EventEndElement "{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable" , EventEndElement "{jabber:client}error" , EventEndElement "{jabber:client}iq" ] wrapStanzaList :: [XML.Event] -> STM [Either (StanzaWrap XML.Event) XML.Event] wrapStanzaList xs = do wrap <- do clsrs <- newTVar Nothing donev <- newTMVar () return $ \ x -> Stanza { stanzaType = Unrecognized , stanzaId = mid , stanzaTo = mto , stanzaFrom = mfrom , stanzaClosers = clsrs , stanzaInterrupt = donev , stanzaOrigin = LocalPeer , stanzaChan = x } return $ map (Left . wrap) (take 1 xs) ++ map Right (drop 1 xs) where m = listToMaybe xs mto = m >>= lookupAttrib "to" . tagAttrs mfrom = m >>= lookupAttrib "from" . tagAttrs mid = m >>= lookupAttrib "id" . tagAttrs wrapStanzaConduit :: Monad m => StanzaWrap a -> ConduitM Event (Either (StanzaWrap Event) Event) m () wrapStanzaConduit stanza = do mfirst <- await flip (maybe $ return ()) mfirst $ \first -> do yield . Left $ stanza { stanzaChan = first } awaitForever $ yield . Right {- greet namespace = [ EventBeginDocument , EventBeginElement (streamP "stream") [ attr "xmlns" namespace , attr "version" "1.0" ] ] -} goodbye :: [XML.Event] goodbye = [ EventEndElement (streamP "stream") , EventEndDocument ] simulateChatError :: StanzaError -> Maybe Text -> [Event] simulateChatError err mfrom = [ EventBeginElement "{jabber:client}message" ((maybe id (\t->(attr "from" t:)) mfrom) [attr "type" "normal" ]) , EventBeginElement "{jabber:client}body" [] , EventContent $ ContentText ("/me " <> errorText err) , EventEndElement "{jabber:client}body" , EventBeginElement "{http://jabber.org/protocol/xhtml-im}html" [] , EventBeginElement "{http://www.w3.org/1999/xhtml}body" [] , EventBeginElement "{http://www.w3.org/1999/xhtml}p" [ attr "style" "font-weight:bold; color:red" ] , EventContent $ ContentText ("/me " <> errorText err) , EventEndElement "{http://www.w3.org/1999/xhtml}p" , EventEndElement "{http://www.w3.org/1999/xhtml}body" , EventEndElement "{http://jabber.org/protocol/xhtml-im}html" , EventEndElement "{jabber:client}message" ] presenceSolicitation :: Text -> Text -> IO Stanza presenceSolicitation = presenceStanza (PresenceRequestSubscription True) "subscribe" presenceProbe :: Text -> Text -> IO Stanza presenceProbe = presenceStanza PresenceRequestStatus "probe" presenceStanza :: StanzaType -> Text -> Text -> Text -> IO Stanza presenceStanza stanza_type type_attr me jid = stanzaFromList stanza_type [ EventBeginElement "{jabber:server}presence" [ attr "to" jid , attr "from" me , attr "type" type_attr ] , EventEndElement "{jabber:server}presence" ] slotsToSource :: Slotted.UpdateStream XMPPState (Either (StanzaWrap XML.Event) XML.Event) -> TVar Int -> TVar (Maybe (StanzaWrap XML.Event)) -> TVar Bool -> TMVar () -> Source IO (Flush XML.Event) slotsToSource slots nesting lastStanza needsFlush rdone = fix $ \slot_src -> join . lift . atomically $ foldr1 orElse [Slotted.pull slots >>= \x -> do x <- case x of Left wrapped -> do writeTVar nesting 1 writeTVar lastStanza (Just wrapped) return $ stanzaChan wrapped Right x -> do when (isEventBeginElement x) $ modifyTVar' nesting (+1) when (isEventEndElement x) $ do n <- readTVar nesting when (n==1) $ writeTVar lastStanza Nothing modifyTVar' nesting (subtract 1) return x writeTVar needsFlush True return $ do -- liftIO $ wlog $ "yielding Chunk: " ++ show x yield (Chunk x) slot_src ,do Slotted.isEmpty slots >>= check readTVar needsFlush >>= check writeTVar needsFlush False return $ do -- liftIO $ wlog "yielding Flush" yield Flush slot_src ,readTMVar rdone >> return (return ()) ] forkConnection :: Server ConnectionKey SockAddr ReleaseKey XML.Event -> XMPPServerParameters -> ConnectionKey -> SockAddr -> FlagCommand -> Source IO XML.Event -> Sink (Flush XML.Event) IO () -> TChan Stanza -> IO (TChan Stanza) forkConnection sv xmpp k laddr pingflag src snk stanzas = do let (namespace,tellmyname) = case k of ClientKey {} -> ("jabber:client", xmppTellMyNameToClient xmpp) PeerKey {} -> ("jabber:server",xmppTellMyNameToPeer xmpp laddr) me <- tellmyname rdone <- atomically newEmptyTMVar let isStarter (Left _) = True isStarter (Right e) | isEventBeginElement e = True isStarter _ = False isStopper (Left _) = False isStopper (Right e) | isEventEndElement e = True isStopper _ = False slots <- atomically $ Slotted.new isStarter isStopper needsFlush <- atomically $ newTVar False lastStanza <- atomically $ newTVar Nothing nesting <- atomically $ newTVar 0 let _ = slots :: Slotted.UpdateStream XMPPState (Either (StanzaWrap XML.Event) XML.Event) let greet_src = do CL.sourceList (greet' namespace me) =$= CL.map Chunk yield Flush slot_src = slotsToSource slots nesting lastStanza needsFlush rdone forkIO $ do myThreadId >>= flip labelThread ("post-queue."++show k) (greet_src >> slot_src) $$ snk last <- atomically $ readTVar lastStanza es <- while (atomically . fmap not $ Slotted.isEmpty slots) (atomically . Slotted.pull $ slots) let es' = mapMaybe metadata es metadata (Left s) = Just s metadata _ = Nothing -- TODO: Issuing RecipientUnavailable for all errors is a presence leak -- and protocol violation -- TODO: IDMangler can be used for better targetted error delivery. let fail stanza = do wlog $ "failed delivery: " ++ show (stanzaId stanza) quitVar <- atomically newEmptyTMVar reply <- makeErrorStanza stanza tag <- stanzaFirstTag stanza -- sendReply quitVar (Error RecipientUnavailable tag) reply replyto replystanza <- stanzaFromList (Error RecipientUnavailable tag) reply xmppDeliverMessage xmpp (wlog $ "discarded error delivery fail") replystanza notError s = case stanzaType s of Error {} -> False _ -> True -- TODO: Probably some stanzas should be queued or saved for re-connect. mapM_ fail $ filter notError (maybeToList last ++ es') wlog $ "end post-queue fork: " ++ show k output <- atomically newTChan hacks <- atomically $ newTVar Map.empty msgids <- atomically $ newTVar [] forkIO $ do -- mapM_ (atomically . Slotted.push slots Nothing) greetPeer myThreadId >>= flip labelThread ("pre-queue."++show k) verbosity <- xmppVerbosity xmpp fix $ \loop -> do what <- atomically $ foldr1 orElse [readTChan output >>= \stanza -> return $ do let notping f | (verbosity==1) = case stanzaType stanza of Pong -> return () _ -> f | (verbosity>=2) = f | otherwise = return () -- isempty <- atomically $ isEmptyTChan (stanzaChan stanza) -- kwlog $ "queuing: "++show (isempty, stanzaId stanza) notping $ do dup <- cloneStanza stanza let typ = Strict8.pack $ c ++ "<-"++(concat . take 1 . words $ show (stanzaType dup))++" " c = case k of ClientKey {} -> "C" PeerKey {} -> "P" wlog "" stanzaToConduit dup $$ prettyPrint typ -- wlog $ "hacks: "++show (stanzaId stanza) case stanzaType stanza of InternalEnableHack hack -> do -- wlog $ "enable hack: " ++ show hack atomically $ modifyTVar' hacks (Map.insert hack ()) InternalCacheId x -> do -- wlog $ "cache id thread: " ++ show x atomically $ modifyTVar' msgids (take 3 . (x:)) _ -> return () stanzaToConduit stanza =$= wrapStanzaConduit stanza $$ awaitForever -- TODO: PresenceStatus stanzas should be pushed to appropriate slots $ liftIO . atomically . Slotted.push slots Nothing case stanzaType stanza of Error err tag | tagName tag=="{jabber:client}message" -> do wlog $ "handling Error hacks" b <- atomically $ do m <- readTVar hacks cached <- readTVar msgids flip (maybe $ return False) (stanzaId stanza) $ \id' -> do return $ Map.member SimulatedChatErrors m && elem id' cached ids <- atomically $ readTVar msgids wlog $ "ids = " ++ show (b,stanzaId stanza, ids) when b $ do let sim = simulateChatError err (stanzaFrom stanza) wlog $ "sending simulated chat for error message." CL.sourceList sim =$= wrapStanzaConduit stanza -- not quite right, but whatever $$ awaitForever $ liftIO . atomically . Slotted.push slots Nothing Error e _ -> do wlog $ "no hacks for error: " ++ show e _ -> return () loop ,do pingflag >>= check return $ do to <- xmppTellPeerHisName xmpp k -- addrToText (callBackAddress k) let from = me -- Look it up from Server object -- or pass it with Connection event. mid = Just "ping" ping0 = makePing namespace mid to from ping <- atomically $ wrapStanzaList ping0 mapM_ (atomically . Slotted.push slots (Just $ PingSlot)) ping #ifdef PINGNOISE wlog "" CL.sourceList ping0 $$ prettyPrint $ case k of ClientKey {} -> "C<-Ping" PeerKey {} -> "P<-Ping " #endif loop ,readTMVar rdone >> return (return ()) ] what wlog $ "end pre-queue fork: " ++ show k forkIO $ do myThreadId >>= flip labelThread ("reader."++show k) -- src $$ awaitForever (lift . putStrLn . takeWhile (/=' ') . show) src $$ xmppInbound sv xmpp k laddr pingflag stanzas output rdone atomically $ putTMVar rdone () wlog $ "end reader fork: " ++ show k return output {- data Peer = Peer { peerWanted :: TVar Bool -- ^ False when this peer is on a you-call-me basis , peerState :: TVar PeerState } data PeerState = PeerPendingConnect UTCTime | PeerPendingAccept UTCTime | PeerConnected (TChan Stanza) -} peerKey :: SocketLike sock => sock -> IO (ConnectionKey,SockAddr) peerKey sock = do addr <- getSocketName sock peer <- sIsConnected sock >>= \c -> if c then getPeerName sock -- addr is normally socketName else return addr -- Weird hack: addr is would-be peer name laddr <- getSocketName sock return $ (PeerKey (peer `withPort` fromIntegral peerport),laddr) clientKey :: SocketLike sock => sock -> IO (ConnectionKey,SockAddr) clientKey sock = do addr <- getSocketName sock paddr <- getPeerName sock return $ (ClientKey addr,paddr) xmlifyRosterItems :: Monad m => Set Text -> Text -> Set Text -> ConduitM i Event m () xmlifyRosterItems solicited stype set = mapM_ item (Set.toList set) where item jid = do yield $ EventBeginElement "{jabber:iq:roster}item" ([ attr "jid" jid , attr "subscription" stype ]++if Set.member jid solicited then [attr "ask" "subscribe"] else [] ) yield $ EventEndElement "{jabber:iq:roster}item" sendRoster :: StanzaWrap a -> XMPPServerParameters -> TChan Stanza -> IO () sendRoster query xmpp replyto = do let k = case stanzaOrigin query of NetworkOrigin k _ -> Just k LocalPeer -> Nothing -- local peer requested roster? flip (maybe $ return ()) k $ \k -> do hostname <- xmppTellMyNameToClient xmpp let getlist f = do bs <- f xmpp k return (Set.fromList bs) -- js) buddies <- getlist xmppRosterBuddies subscribers <- getlist xmppRosterSubscribers solicited <- getlist xmppRosterSolicited subnone0 <- getlist xmppRosterOthers jid <- case k of ClientKey {} -> xmppTellClientHisName xmpp k -- LookupClientJID xmpp k PeerKey {} -> xmppTellClientNameOfPeer xmpp k (Set.toList buddies) let subnone = Set.union solicited subnone0 \\ Set.union buddies subscribers let subto = buddies \\ subscribers let subfrom = subscribers \\ buddies let subboth = Set.intersection buddies subscribers let roster = do yield $ EventBeginElement "{jabber:client}iq" (consid (stanzaId query) [ attr "to" jid , attr "type" "result" ]) yield $ EventBeginElement "{jabber:iq:roster}query" [] -- todo: ver? xmlifyRosterItems solicited "to" subto xmlifyRosterItems solicited "from" subfrom xmlifyRosterItems solicited "both" subboth xmlifyRosterItems solicited "none" subnone yield $ EventEndElement "{jabber:iq:roster}query" yield $ EventEndElement "{jabber:client}iq" conduitToStanza Roster (stanzaId query) Nothing (Just jid) roster >>= ioWriteChan replyto {- let debugpresence = [ EventBeginElement "{jabber:client}presence" [ attr "from" "guest@oxio4inifatsetlx.onion" , attr "to" jid] , EventEndElement "{jabber:client}presence" ] quitvar <- atomically newEmptyTMVar sendReply quitvar Unrecognized debugpresence replyto -} socketFromKey :: Server ConnectionKey SockAddr ReleaseKey XML.Event -> ConnectionKey -> IO SockAddr socketFromKey sv k = do map <- atomically $ readTVar (conmap sv) let mcd = Map.lookup k map case mcd of Nothing -> case k of ClientKey addr -> return addr PeerKey addr -> return addr -- XXX: ? wrong address -- Shouldnt happen anyway. Just cd -> return $ cdata cd class StanzaFirstTag a where stanzaFirstTag :: StanzaWrap a -> IO XML.Event instance StanzaFirstTag (TChan XML.Event) where stanzaFirstTag stanza = do e <-atomically $ peekTChan (stanzaChan stanza) return e instance StanzaFirstTag (LockedChan XML.Event) where stanzaFirstTag stanza = do e <-atomically $ peekLChan (stanzaChan stanza) return e instance StanzaFirstTag XML.Event where stanzaFirstTag stanza = return (stanzaChan stanza) data StanzaError = BadRequest | Conflict | FeatureNotImplemented | Forbidden | Gone | InternalServerError | ItemNotFound | JidMalformed | NotAcceptable | NotAllowed | NotAuthorized | PaymentRequired | RecipientUnavailable | Redirect | RegistrationRequired | RemoteServerNotFound | RemoteServerTimeout | ResourceConstraint | ServiceUnavailable | SubscriptionRequired | UndefinedCondition | UnexpectedRequest deriving (Show,Enum,Ord,Eq) xep0086 :: forall t t1. (Num t1, IsString t) => StanzaError -> (t, t1) xep0086 e = case e of BadRequest -> ("modify", 400) Conflict -> ("cancel", 409) FeatureNotImplemented -> ("cancel", 501) Forbidden -> ("auth", 403) Gone -> ("modify", 302) InternalServerError -> ("wait", 500) ItemNotFound -> ("cancel", 404) JidMalformed -> ("modify", 400) NotAcceptable -> ("modify", 406) NotAllowed -> ("cancel", 405) NotAuthorized -> ("auth", 401) PaymentRequired -> ("auth", 402) RecipientUnavailable -> ("wait", 404) Redirect -> ("modify", 302) RegistrationRequired -> ("auth", 407) RemoteServerNotFound -> ("cancel", 404) RemoteServerTimeout -> ("wait", 504) ResourceConstraint -> ("wait", 500) ServiceUnavailable -> ("cancel", 503) SubscriptionRequired -> ("auth", 407) UndefinedCondition -> ("", 500) UnexpectedRequest -> ("wait", 400) errorText :: StanzaError -> Text errorText e = case e of BadRequest -> "Bad request" Conflict -> "Conflict" FeatureNotImplemented -> "This feature is not implemented" Forbidden -> "Forbidden" Gone -> "Recipient can no longer be contacted" InternalServerError -> "Internal server error" ItemNotFound -> "Item not found" JidMalformed -> "JID Malformed" NotAcceptable -> "Message was rejected" NotAllowed -> "Not allowed" NotAuthorized -> "Not authorized" PaymentRequired -> "Payment is required" RecipientUnavailable -> "Recipient is unavailable" Redirect -> "Redirect" RegistrationRequired -> "Registration required" RemoteServerNotFound -> "Recipient's server not found" RemoteServerTimeout -> "Remote server timeout" ResourceConstraint -> "The server is low on resources" ServiceUnavailable -> "The service is unavailable" SubscriptionRequired -> "A subscription is required" UndefinedCondition -> "Undefined condition" UnexpectedRequest -> "Unexpected request" eventContent :: Maybe [Content] -> Text eventContent cs = maybe "" (foldr1 (<>) . map content1) cs where content1 (ContentText t) = t content1 (ContentEntity t) = t errorTagLocalName :: forall a. Show a => a -> Text errorTagLocalName e = Text.pack . drop 1 $ do c <- show e if 'A' <= c && c <= 'Z' then [ '-', chr( ord c - ord 'A' + ord 'a') ] else return c makeErrorStanza :: StanzaFirstTag a => StanzaWrap a -> IO [XML.Event] makeErrorStanza stanza = do startTag <- stanzaFirstTag stanza let n = tagName startTag endTag = EventEndElement n amap0 = Map.fromList (tagAttrs startTag) mto = Map.lookup "to" amap0 mfrom = Map.lookup "from" amap0 mtype = Map.lookup "type" amap0 mid = Map.lookup "id" amap0 amap1 = Map.alter (const mto) "from" amap0 -- amap2 = Map.alter (const $ Just $ [ContentText "blackbird"]) {-mfrom)-} "to" amap1 amap2 = Map.alter (const mfrom) "to" amap1 amap3 = Map.insert "type" [XML.ContentText "error"] amap2 startTag' = EventBeginElement (tagName startTag) (Map.toList amap3) -- err = Gone -- FeatureNotImplemented -- UndefinedCondition -- RecipientUnavailable err = RecipientUnavailable errname = n { nameLocalName = "error" } -- errattrs = [attr "type" "wait"] -- "modify"] errorAttribs e xs = ys ++ xs -- todo replace instead of append where (typ,code) = xep0086 e ys = [attr "type" typ, attr "code" (Text.pack . show $ code)] errorTagName = Name { nameNamespace = Just "urn:ietf:params:xml:ns:xmpp-stanzas" , nameLocalName = errorTagLocalName err , namePrefix = Nothing } errattrs = errorAttribs err [] let wlogd v s = do wlog $ "error "++show (lookupAttrib "id" $ tagAttrs startTag)++" " ++ v ++ " = " ++ show s {- wlogd "amap0" amap0 wlogd "mto" mto wlogd "mfrom" mfrom wlogd "amap3" amap3 -} if eventContent mtype=="error" then return [] else do return [ startTag' , EventBeginElement errname errattrs , EventBeginElement errorTagName [] , EventEndElement errorTagName , EventEndElement errname {- , EventBeginElement "{jabber:client}body" [] , EventContent (ContentText "what?") , EventEndElement "{jabber:client}body" -} {- , EventBeginElement "{154ae29f-98f2-4af4-826d-a40c8a188574}dummy" [] , EventEndElement "{154ae29f-98f2-4af4-826d-a40c8a188574}dummy" -} , endTag ] monitor :: Server ConnectionKey SockAddr ReleaseKey XML.Event -> ConnectionParameters ConnectionKey SockAddr -> XMPPServerParameters -> IO b monitor sv params xmpp = do chan <- return $ serverEvent sv stanzas <- atomically newTChan quitVar <- atomically newEmptyTMVar fix $ \loop -> do action <- atomically $ foldr1 orElse [ readTChan chan >>= \((k,u),e) -> return $ do case e of Connection pingflag xsrc xsnk -> do wlog $ tomsg k "Connection" outs <- forkConnection sv xmpp k u pingflag xsrc xsnk stanzas xmppNewConnection xmpp k u outs ConnectFailure addr -> return () -- wlog $ tomsg k "ConnectFailure" EOF -> do wlog $ tomsg k "EOF" xmppEOF xmpp k HalfConnection In -> do wlog $ tomsg k "ReadOnly" control sv (Connect (callBackAddress k) params) HalfConnection Out -> wlog $ tomsg k "WriteOnly" RequiresPing -> return () -- wlog $ tomsg k "RequiresPing" , readTChan stanzas >>= \stanza -> return $ do {- dup <- case stanzaType stanza of -- Must dup anything that is going to be delivered... Message {} -> do dup <- cloneStanza stanza -- dupped so we can make debug print return dup Error {} -> do dup <- cloneStanza stanza -- dupped so we can make debug print return dup _ -> return stanza -} dup <- cloneStanza stanza forkIO $ do case stanzaOrigin stanza of NetworkOrigin k@(ClientKey {}) replyto -> case stanzaType stanza of RequestResource clientsNameForMe wanted -> do sockaddr <- socketFromKey sv k rsc <- xmppChooseResourceName xmpp k sockaddr clientsNameForMe wanted let reply = iq_bind_reply (stanzaId stanza) rsc -- sendReply quitVar SetResource reply replyto hostname <- xmppTellMyNameToClient xmpp let requestVersion :: Producer IO XML.Event requestVersion = do yield $ EventBeginElement "{jabber:client}iq" [ attr "to" rsc , attr "from" hostname , attr "type" "get" , attr "id" "version"] yield $ EventBeginElement "{jabber:iq:version}query" [] yield $ EventEndElement "{jabber:iq:version}query" yield $ EventEndElement "{jabber:client}iq" {- -- XXX Debug chat: yield $ EventBeginElement "{jabber:client}message" [ attr "from" $ eventContent (Just [ContentText rsc]) , attr "type" "normal" ] -- "blackbird" ] yield $ EventBeginElement "{jabber:client}body" [] yield $ EventContent $ ContentText ("hello?") yield $ EventEndElement "{jabber:client}body" yield $ EventEndElement "{jabber:client}message" -} sendReply quitVar SetResource reply replyto conduitToStanza (UnrecognizedQuery "{jabber:iq:version}query") Nothing -- id (Just hostname) -- from (Just rsc) -- to requestVersion >>= ioWriteChan replyto SessionRequest -> do me <- xmppTellMyNameToClient xmpp let reply = iq_session_reply (stanzaId stanza) me sendReply quitVar Pong reply replyto RequestRoster -> do sendRoster stanza xmpp replyto xmppSubscribeToRoster xmpp k PresenceStatus {} -> do xmppInformClientPresence xmpp k stanza PresenceRequestSubscription {} -> do let fail = return () -- todo xmppClientSubscriptionRequest xmpp fail k stanza replyto PresenceInformSubscription {} -> do let fail = return () -- todo xmppClientInformSubscription xmpp fail k stanza NotifyClientVersion name version -> do enableClientHacks name version replyto UnrecognizedQuery query -> do me <- xmppTellMyNameToClient xmpp let reply = iq_service_unavailable (stanzaId stanza) me query sendReply quitVar (Error ServiceUnavailable (head reply)) reply replyto Message {} -> do -- wlog $ "LANGMAP "++show (stanzaId stanza, msgLangMap (stanzaType stanza)) maybe (return ()) (flip cacheMessageId replyto) $ do guard . not . null . mapMaybe (msgBody . snd) $ msgLangMap (stanzaType stanza) stanzaId stanza _ -> return () NetworkOrigin k@(PeerKey {}) replyto -> case stanzaType stanza of PresenceRequestStatus {} -> do xmppAnswerProbe xmpp k stanza replyto PresenceStatus {} -> do xmppInformPeerPresence xmpp k stanza PresenceRequestSubscription {} -> do let fail = return () -- todo xmppPeerSubscriptionRequest xmpp fail k stanza replyto PresenceInformSubscription {} -> do let fail = return () -- todo xmppPeerInformSubscription xmpp fail k stanza _ -> return () _ -> return () let deliver replyto = do -- TODO: Issuing RecipientUnavailable for all errors is a presence leak -- and protocol violation let fail = do wlog $ "Failed delivery id="++show (stanzaId stanza) -- TODO reply <- makeErrorStanza stanza tag <- stanzaFirstTag stanza sendReply quitVar (Error RecipientUnavailable tag) reply replyto xmppDeliverMessage xmpp fail stanza -- -- bad idea: -- let newStream = greet'' "jabber:client" "blackbird" -- sendReply quitVar Error newStream replyto case stanzaType stanza of Message {} -> do case stanzaOrigin stanza of LocalPeer {} -> return () NetworkOrigin _ replyto -> deliver replyto Error {} -> do case stanzaOrigin stanza of LocalPeer {} -> return () NetworkOrigin k replyto -> do -- wlog $ "delivering error: " ++show (stanzaId stanza) -- wlog $ " from: " ++ show k deliver replyto _ -> return () -- We need to clone in the case the stanza is passed on as for Message. verbosity <- xmppVerbosity xmpp let notping f | (verbosity==1) = case stanzaType stanza of Pong -> return () _ -> f | (verbosity>=2) = f | otherwise = return () notping $ do let typ = Strict8.pack $ c ++ "->"++(concat . take 1 . words $ show (stanzaType stanza))++" " c = case stanzaOrigin stanza of LocalPeer -> "*" NetworkOrigin (ClientKey {}) _ -> "C" NetworkOrigin (PeerKey {}) _ -> "P" wlog "" stanzaToConduit dup $$ prettyPrint typ ] action loop where tomsg k str = printf "%12s %s" str (show k) where _ = str :: String data XMPPServer = XMPPServer { _xmpp_sv :: Server ConnectionKey SockAddr ReleaseKey XML.Event , _xmpp_peer_params :: ConnectionParameters ConnectionKey SockAddr } grokPeer :: XMPPServer -> ConnectionKey -> (SockAddr, ConnectionParameters ConnectionKey SockAddr, Miliseconds) grokPeer sv (PeerKey addr) = (addr, _xmpp_peer_params sv, 10000) xmppConnections :: XMPPServer -> IO (Connection.Manager TCPStatus Text) xmppConnections sv = tcpManager (grokPeer sv) (Just . Text.pack) resolvPeer (_xmpp_sv sv) where resolvPeer :: Text -> IO (Maybe ConnectionKey) resolvPeer str = fmap PeerKey <$> listToMaybe <$> resolvePeer str xmppEventChannel :: XMPPServer -> TChan ((ConnectionKey, SockAddr), ConnectionEvent Event) xmppEventChannel sv = serverEvent $ _xmpp_sv sv xmppServer :: ( MonadResource m , MonadIO m ) => XMPPServerParameters -> m XMPPServer xmppServer xmpp = do sv <- server allocate xmlStream -- some fuzz helps avoid simultaneity pingfuzz <- liftIO $ do gen <- System.Random.getStdGen let (r,gen') = System.Random.next gen return $ r `mod` 2000 -- maximum 2 seconds of fuzz liftIO . wlog $ "pingfuzz = " ++ show pingfuzz let peer_params = (connectionDefaults peerKey) { pingInterval = 15000 + pingfuzz , timeout = 2000 , duplex = False } client_params = (connectionDefaults clientKey) { pingInterval = 0 , timeout = 0 } liftIO $ do forkIO $ do myThreadId >>= flip labelThread ("XMPP.monitor") monitor sv peer_params xmpp hPutStrLn stderr $ "Starting peer listen" control sv (Listen peerport peer_params) hPutStrLn stderr $ "Starting client listen" control sv (Listen clientport client_params) return XMPPServer { _xmpp_sv = sv, _xmpp_peer_params = peer_params } #if MIN_VERSION_stm(2,4,0) #else -- |Clone a 'TChan': similar to dupTChan, but the cloned channel starts with the -- same content available as the original channel. -- -- Terrible inefficient implementation provided to build against older libraries. cloneTChan :: TChan a -> STM (TChan a) cloneTChan chan = do contents <- chanContents' chan chan2 <- dupTChan chan mapM_ (writeTChan chan) contents return chan2 where chanContents' chan = do b <- isEmptyTChan chan if b then return [] else do x <- readTChan chan xs <- chanContents' chan return (x:xs) #endif