{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleInstances #-} -- instance for TChan Event {-# LANGUAGE DoAndIfThenElse #-} {-# LANGUAGE ExistentialQuantification #-} module XMPPServer ( xmppServer , forkXmpp , quitXmpp , ClientAddress , PeerAddress , Local(..) , Remote(..) , ConnectionData(..) , ConnectionType(..) , XMPPServerParameters(..) , XMPPServer , classifyConnection , addrToPeerKey , addrFromClientKey , xmppConnections , xmppEventChannel , StanzaWrap(..) , Stanza(..) , StanzaType(..) , StanzaOrigin(..) , cloneStanza , LangSpecificMessage(..) , peerKeyToText , addrToText , sendModifiedStanzaToPeer , sendModifiedStanzaToClient , presenceProbe , presenceSolicitation , makePresenceStanza , makeInformSubscription , makeRosterUpdate , makeMessage , JabberShow(..) , Server , flushPassThrough , greet' , (<&>) , grokStanza ) where import ConnectionKey import qualified Control.Concurrent.STM.UpdateStream as Slotted import Nesting import Connection.Tcp import EventUtil import ControlMaybe import LockedChan import Connection (PeerAddress(..)) import qualified Connection import Util import Network.Address (getBindAddress, sockAddrPort) import Debug.Trace 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,ThreadId,MVar,putMVar,takeMVar,newMVar) #else import Control.Concurrent.Lifted (forkIO,myThreadId,ThreadId) import GHC.Conc (labelThread) #endif import Control.Concurrent.STM -- import Control.Concurrent.STM.TChan import Network.SocketLike import Text.Printf 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 #if MIN_VERSION_conduit_extra(1,1,7) import Data.Conduit.ByteString.Builder (builderToByteStringFlush) #else import Data.Conduit.Blaze (builderToByteStringFlush) #endif import Control.Monad.Catch (MonadThrow) import DNSCache (withPort) 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.Monoid ( (<>) ) import Data.Text (Text) import qualified Data.Text as Text (pack,unpack,words,intercalate,drop) import Data.Char (chr,ord) 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 DPut import DebugTag -- 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 | PeerOrigin PeerAddress (TChan Stanza) | ClientOrigin ClientAddress (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) newtype Local a = Local a deriving (Eq,Ord,Show) newtype Remote a = Remote a deriving (Eq,Ord,Show) 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. -- -- Note: The returned domain will be discarded and replaced with the result of -- 'xmppTellMyNameToClient'. xmppChooseResourceName :: ClientAddress -> Remote SockAddr -> Maybe Text -> Maybe Text -> IO Text , -- | This should indicate the server's hostname that all client's see. xmppTellMyNameToClient :: ClientAddress -> IO Text , xmppTellMyNameToPeer :: Local SockAddr -> IO Text , xmppTellClientHisName :: ClientAddress -> IO Text , xmppTellPeerHisName :: PeerAddress -> IO Text , xmppNewConnection :: SockAddr -> ConnectionData -> TChan Stanza -> IO () , xmppEOF :: SockAddr -> ConnectionData -> IO () , xmppRosterBuddies :: ClientAddress -> IO [Text] , xmppRosterSubscribers :: ClientAddress -> IO [Text] , xmppRosterSolicited :: ClientAddress -> IO [Text] , xmppRosterOthers :: ClientAddress -> IO [Text] , -- | Called when after sending a roster to a client. Usually this means -- the client status should change from "available" to "interested". xmppSubscribeToRoster :: ClientAddress -> IO () -- , xmppLookupClientJID :: SockAddr -> IO Text , xmppDeliverMessage :: (IO ()) -> Stanza -> IO () -- | Called whenever a local client's presence changes. , xmppInformClientPresence :: ClientAddress -> Stanza -> IO () -- | Called whenever a remote peer's presence changes. , xmppInformPeerPresence :: PeerAddress -> Stanza -> IO () , -- | Called when a remote peer requests our status. xmppAnswerProbe :: PeerAddress -> Stanza -> TChan Stanza -> IO () , xmppClientSubscriptionRequest :: IO () -> ClientAddress -> Stanza -> TChan Stanza -> IO () , -- | Called when a remote peer sends subscription request. xmppPeerSubscriptionRequest :: IO () -> PeerAddress -> Stanza -> TChan Stanza -> IO () , xmppClientInformSubscription :: IO () -> ClientAddress -> Stanza -> IO () , -- | Called when a remote peer informs us of our subscription status. xmppPeerInformSubscription :: IO () -> PeerAddress -> Stanza -> IO () , xmppVerbosity :: IO Int , xmppClientBind :: Maybe SockAddr , xmppPeerBind :: Maybe SockAddr } 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,_) = 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 -- Shows (as Text) the IP address associated with the given SockAddr. peerKeyToText :: PeerAddress -> Text peerKeyToText (PeerAddress addr) = addrToText addr wlog :: String -> IO () wlog = dput XJabber wlogb :: ByteString -> IO () wlogb = wlog . Strict8.unpack flushPassThrough :: Monad m => ConduitT a b m () -> ConduitT (Flush a) (Flush b) m () flushPassThrough c = getZipConduit $ ZipConduit (onlyChunks .| mapOutput Chunk c) <* ZipConduit onlyFlushes where onlyChunks :: Monad m => ConduitT (Flush a) a m () onlyFlushes :: Monad m => ConduitT (Flush a) (Flush b) m () onlyChunks = awaitForever yieldChunk onlyFlushes = awaitForever yieldFlush yieldFlush Flush = yield Flush yieldFlush _ = return () yieldChunk (Chunk x) = yield x yieldChunk _ = return () xmlStream :: ReadCommand -> WriteCommand -> ( ConduitT () XML.Event IO () , ConduitT (Flush XML.Event) Void IO () ) xmlStream conread conwrite = (xsrc,xsnk) where xsrc = src .| XML.parseBytes XML.def xsnk :: ConduitT (Flush Event) Void 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 :: ConduitT () Event IO () -> IO (LockedChan Event, TVar (Maybe [Event]), TMVar a) conduitToChan c = do chan <- atomically newLockedChan clsrs <- atomically $ newTVar (Just []) quitvar <- atomically $ newEmptyTMVar forkIO $ do runConduit $ 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 -> ConduitT () Event IO () -> 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 -- Modifies a server-to-server stanza to send it to a client. This changes the -- namespace and also filters some non-supported attributes. Any other -- modifications need to be made by the caller. 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 forM_ (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 forM mtag $ \tag -> do case tagName tag of "{urn:xmpp:ping}ping" -> return Ping "{jabber:iq:roster}query" -> return RequestRoster name -> return $ 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 fromMaybe (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 fromMaybe (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 case tagName <$> mtag of Just "{urn:ietf:params:xml:ns:xmpp-bind}bind" -> do mchild <- nextElement case 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 Just "{urn:ietf:params:xml:ns:xmpp-session}session" -> 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 forM_ 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 mx <- nextElement fmap join $ forM mx $ \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 fmap join $ forM x $ \x -> 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 :: ConnectionData -> XMPPServerParameters -- ^ XXX: unused -> (Text, IO Text, IO Text, TChan Stanza -> StanzaOrigin) -> FlagCommand -- ^ action to check whether the connection needs a ping (XXX: unused) -> TChan Stanza -- ^ channel to announce incoming stanzas on -> TChan Stanza -- ^ channel used to send stanzas -> TMVar () -- ^ mvar that is filled when the connection quits -> ConduitM Event o IO () xmppInbound cdta xmpp (namespace,tellmyname,tellyourname,mkorigin) pingflag stanzas output donevar = doNestingXML $ do withXML $ \begindoc -> do when (begindoc==EventBeginDocument) $ do whenJust nextElement $ \xml -> do withJust (elementAttrs "stream" xml) $ \stream_attrs -> do -- liftIO $ dput XMisc $ "STREAM ATTRS "++show stream_attrs let stream_name = lookupAttrib "to" stream_attrs stream_remote = lookupAttrib "from" stream_attrs -- xmpp_version = lookupAttrib "version" stream_attrs liftIO $ atomically $ writeTVar (cdRemoteName cdta) stream_remote 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 = mkorigin output } ioWriteChan stanzas s you <- liftIO tellyourname me <- liftIO tellmyname fromMaybe 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 do -- 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 = mkorigin output } stype -> ioWriteChan stanzas Stanza { stanzaType = case stype of RequestResource _ rsc -> RequestResource stream_name rsc _ -> stype , stanzaId = mid , stanzaTo = mto , stanzaFrom = mfrom , stanzaChan = chan , stanzaClosers = clsrs , stanzaInterrupt = donevar , stanzaOrigin = mkorigin 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 forM_ 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" ] -- | Create a friend-request stanza. presenceSolicitation :: Text -- ^ JID of sender making request. -> Text -- ^ JID of recipient who needs to approve it. -> 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 () -> ConduitT () (Flush XML.Event) IO () 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 PeerAddress ConnectionData releaseKey XML.Event -> XMPPServerParameters -> PeerAddress -- SockAddr (XXX(what?): remote for peer, local for client) -> ConnectionData -> FlagCommand -> ConduitT () XML.Event IO () -> ConduitT (Flush XML.Event) Void IO () -> TChan Stanza -> MVar () -> IO (TChan Stanza) forkConnection sv xmpp saddr cdta pingflag src snk stanzas pp_mvar = do let auxAddr = cdAddr cdta clientOrServer@(namespace,tellmyname,telltheirname,_) = case auxAddr of Right _ -> ("jabber:client", xmppTellMyNameToClient xmpp (ClientAddress $ peerAddress saddr) , xmppTellClientHisName xmpp (ClientAddress $ peerAddress saddr) , ClientOrigin (ClientAddress $ peerAddress saddr)) Left laddr -> ("jabber:server", xmppTellMyNameToPeer xmpp laddr , xmppTellPeerHisName xmpp saddr , PeerOrigin saddr) 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 -- client.PeerAddress {peerAddress = [::1]:5222} let lbl n = concat [ n , Text.unpack (Text.drop 7 namespace) -- "client" or "server" , "." , case cdProfile cdta of _ | Right _ <- cdAddr cdta -> show saddr "." -> show saddr mytoxname -> show saddr {- TODO: remote tox peer name? -} ] forkIO $ do myThreadId >>= flip labelThread (lbl "xmpp-post.") -- This thread handles messages after they are pulled out of -- the slots-queue. Hence, xmpp-post, for post- slots-queue. -- Read all slots-queued XML events or stanzas and yield them -- upstream. This should continue until the connection is -- closed. runConduit $ (greet_src >> slot_src) .| snk -- Connection is now closed. Here we handle any unsent stanzas. last <- atomically $ readTVar lastStanza es <- while (atomically . fmap not $ Slotted.isEmpty slots) (atomically . Slotted.pull $ slots) let es' = mapMaybe metadata es -- We only care about full stanzas. 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 xmpp-post fork: " ++ (lbl "") output <- atomically newTChan hacks <- atomically $ newTVar Map.empty msgids <- atomically $ newTVar [] forkIO $ do -- Here is the pre- slots-queue thread which handles messages as they -- arrive and assigns slots to them if that is appropriate. -- mapM_ (atomically . Slotted.push slots Nothing) greetPeer myThreadId >>= flip labelThread (lbl "xmpp-pre.") verbosity <- xmppVerbosity xmpp fix $ \loop -> do what <- atomically $ foldr1 orElse [readTChan output >>= \stanza -> return $ do wantStanzas <- getVerbose XJabber let notping f | not wantStanzas = return () | (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 auxAddr of Right _ -> "C" Left _ -> "P" wlog "" liftIO $ takeMVar pp_mvar runConduit $ stanzaToConduit dup .| prettyPrint typ liftIO $ putMVar pp_mvar () -- 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 () runConduit $ 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 fromMaybe (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." runConduit $ 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 <- telltheirname 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 wlog "" runConduit $ CL.sourceList ping0 .| prettyPrint (case auxAddr of Right _ -> "C<-Ping" Left _ -> "P<-Ping ") loop ,readTMVar rdone >> return (return ()) ] what wlog $ "end xmpp-pre fork: " ++ show (lbl "") forkIO $ do myThreadId >>= flip labelThread (lbl "xmpp-reader.") -- src $$ awaitForever (lift . putStrLn . takeWhile (/=' ') . show) runConduit $ src .| xmppInbound cdta xmpp clientOrServer pingflag stanzas output rdone atomically $ putTMVar rdone () wlog $ "end reader fork: " ++ lbl "" 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 => Maybe SockAddr -> sock -> IO (PeerAddress,ConnectionData) peerKey bind_addr sock = do laddr <- getSocketName sock raddr <- sIsConnected sock >>= \c -> if c then getPeerName sock -- addr is normally socketName else return laddr -- Weird hack: addr is would-be peer name -- Assume remote peers are listening on the same port that we do. let peerport = fromIntegral $ fromMaybe 5269 $ do p <- bind_addr >>= sockAddrPort guard (p /= 0) -- Make sure we never use port 0 because it is used -- to distinguish fake address connection keys. return p rname <- atomically $ newTVar Nothing -- dput XMan $ "peerKey " ++ show (PeerAddress $ raddr `withPort` peerport,laddr) return $ ( PeerAddress $ raddr `withPort` peerport , ConnectionData { cdAddr = Left (Local laddr) , cdType = XMPP , cdProfile = "." , cdRemoteName = rname } ) clientKey :: SocketLike sock => sock -> IO (PeerAddress,ConnectionData) clientKey sock = do laddr <- getSocketName sock -- [::1]:5222 bind address, same for all clients raddr <- getPeerName sock -- [::1]:????? unique key when (Just 0 == sockAddrPort raddr) $ do dput XMan $ unwords [ "BUG: XMPP Client" , show (laddr,raddr) , "is using port zero. This could interfere" , "with Tox peer sessions." ] rname <- atomically $ newTVar Nothing -- dput XMan $ "clientKey " ++ show (PeerAddress laddr,raddr) return $ ( PeerAddress raddr -- Actually a ClientAddress, but _xmpp_sv conkey type is PeerAddress. , ConnectionData { cdAddr = Right (Remote raddr) -- FIXME: This is a bad way to detect client/peer. , cdType = XMPP , cdProfile = "." , cdRemoteName = rname } ) 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 -> ClientAddress -> TChan Stanza -> IO () sendRoster query xmpp clientKey replyto = do let maddr = case stanzaOrigin query of ClientOrigin addr _ -> Just addr PeerOrigin {} -> Nothing -- remote peer requested roster? LocalPeer -> Nothing -- local peer requested roster? forM_ maddr $ \k -> do hostname <- xmppTellMyNameToClient xmpp clientKey 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 <- xmppTellClientHisName xmpp k -- LookupClientJID xmpp k 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 PeerAddress ConnectionData releaseKey XML.Event -> ClientAddress -> IO (Remote SockAddr) socketFromKey sv (ClientAddress addr) = do map <- atomically $ readTVar (conmap sv) let mcd = Map.lookup (PeerAddress addr) map oops = Remote addr -- No connection data, so using incorrect address. case mcd of Nothing -> return oops Just cd -> return $ either (const oops) id $ cdAddr $ 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 :: StanzaError -> (Text, Int) 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 PeerAddress ConnectionData releaseKey XML.Event -> ConnectionParameters PeerAddress ConnectionData -> XMPPServerParameters -> IO b monitor sv params xmpp = do chan <- return $ serverEvent sv stanzas <- atomically newTChan quitVar <- atomically newEmptyTMVar pp_mvar <- newMVar () -- Lock for synchronous pretty-printing of stanzas in log. fix $ \loop -> do action <- atomically $ foldr1 orElse [ readTChan chan >>= \((addr,u),e) -> return $ do case e of Connection pingflag xsrc xsnk -> do wlog $ tomsg addr "Connection" outs <- forkConnection sv xmpp addr u pingflag xsrc xsnk stanzas pp_mvar -- /addr/ may be a peer or a client. So we'll strip off -- the PeerAddress constructor before exposing it. xmppNewConnection xmpp (peerAddress addr) u outs ConnectFailure addr -> do return () -- wlog $ tomsg k "ConnectFailure" EOF -> do wlog $ tomsg addr "EOF" -- /addr/ may be a peer or a client. So we'll strip off -- the PeerAddress constructor before exposing it. xmppEOF xmpp (peerAddress addr) u HalfConnection In -> do wlog $ tomsg addr "ReadOnly" case cdAddr u of Left (Local _) -> control sv (Connect (peerAddress addr) params) _ -> return () -- Don't call-back client connections. HalfConnection Out -> do wlog $ tomsg addr "WriteOnly" RequiresPing -> do 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 ClientOrigin k replyto -> case stanzaType stanza of RequestResource clientsNameForMe wanted -> do sockaddr <- socketFromKey sv k rsc0 <- xmppChooseResourceName xmpp k sockaddr clientsNameForMe wanted hostname <- xmppTellMyNameToClient xmpp k let rsc = unsplitJID (n,hostname,r) where (n,_,r) = splitJID rsc0 let reply = iq_bind_reply (stanzaId stanza) rsc -- sendReply quitVar SetResource reply replyto let requestVersion :: ConduitT i XML.Event IO () 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 k let reply = iq_session_reply (stanzaId stanza) me sendReply quitVar Pong reply replyto RequestRoster -> do sendRoster stanza xmpp k 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 k 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 () PeerOrigin k 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 () ClientOrigin _ replyto -> deliver replyto PeerOrigin _ replyto -> deliver replyto Error {} -> do case stanzaOrigin stanza of LocalPeer {} -> return () ClientOrigin _ replyto -> deliver replyto PeerOrigin _ replyto -> deliver replyto _ -> return () -- We need to clone in the case the stanza is passed on as for Message. wantStanzas <- getVerbose XJabber verbosity <- xmppVerbosity xmpp let notping f | not wantStanzas = return () | (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 -> "*" ClientOrigin {} -> "C" PeerOrigin {} -> "P" wlog "" liftIO $ takeMVar pp_mvar runConduit $ stanzaToConduit dup .| prettyPrint typ liftIO $ putMVar pp_mvar () ] action loop where tomsg k str = printf "%12s %s" str (show k) where _ = str :: String data ConnectionType = XMPP | Tox deriving (Eq,Ord,Enum,Show,Read) data ConnectionData = ConnectionData { cdAddr :: Either (Local SockAddr) -- Peer connection local address (Remote SockAddr) -- unused, todo:remove. (was client connection remote address). , cdType :: ConnectionType , cdProfile :: Text -- Currently ignored for clients. Instead, see -- 'clientProfile' field of 'ClientState'. -- -- For peers: "." for XMPP, otherwise the ".tox" hostname -- of this local node. -- Initially Nothing, when the remote end identifies itself by a given name, -- the result will be stored here. , cdRemoteName :: TVar (Maybe Text) } addrToPeerKey :: Remote SockAddr -> PeerAddress addrToPeerKey (Remote raddr) = PeerAddress raddr addrFromClientKey :: ClientAddress -> Local SockAddr addrFromClientKey (ClientAddress laddr) = Local laddr classifyConnection :: SockAddr -> ConnectionData -> Either (PeerAddress, Local SockAddr) (ClientAddress, Remote SockAddr) classifyConnection saddr dta = case cdAddr dta of Left laddr -> Left (PeerAddress saddr, laddr) Right raddr -> Right (ClientAddress saddr, raddr) data XMPPServer = forall releaseKey. XMPPServer { _xmpp_sv :: Server PeerAddress ConnectionData releaseKey XML.Event -- ^ Internally, we're using PeerAddress for both clients -- and peers. For the external interface, we mark client -- addresses as 'ClientAddress' and not 'PeerAddress'. , _xmpp_man :: Connection.Manager TCPStatus Text , _xmpp_peer_params :: ConnectionParameters PeerAddress ConnectionData , _xmpp_peer_bind :: SockAddr } xmppConnections :: XMPPServer -> IO (Connection.Manager TCPStatus Text) xmppConnections xsv@XMPPServer{_xmpp_man = m} = return m xmppEventChannel :: XMPPServer -> TChan ((PeerAddress, ConnectionData), ConnectionEvent Event) xmppEventChannel XMPPServer{_xmpp_sv=sv} = serverEvent sv quitXmpp :: XMPPServer -> IO () quitXmpp XMPPServer{_xmpp_sv=sv} = control sv Quit xmppServer :: MonadIO m => Allocate releaseKey m -> Maybe SockAddr -- ^ Listen address for server-to-server protocol. -> m XMPPServer xmppServer allocate bind_addr = do sv <- server allocate xmlStream liftIO $ do gen <- System.Random.getStdGen peer_bind <- maybe (getBindAddress "5269" True) return bind_addr let (r, _) = System.Random.next gen fuzz = r `mod` 2000 -- maximum 2 seconds of ping fuzz peer_params :: ConnectionParameters PeerAddress ConnectionData peer_params = (connectionDefaults $ peerKey $ Just peer_bind) { pingInterval = 15000 + fuzz , timeout = 2000 , duplex = False } tcp <- tcpManager (\(PeerAddress addr) -> (addr, peer_params, 10000)) sv return XMPPServer { _xmpp_sv = sv , _xmpp_man = tcp , _xmpp_peer_params = peer_params , _xmpp_peer_bind = peer_bind } forkXmpp :: MonadIO m => XMPPServer -> XMPPServerParameters -> m ThreadId forkXmpp XMPPServer { _xmpp_sv = sv , _xmpp_peer_params = peer_params , _xmpp_peer_bind = peer_bind } xmpp = liftIO $ do let client_params :: ConnectionParameters PeerAddress ConnectionData client_params = (connectionDefaults clientKey) { pingInterval = 0 , timeout = 0 } mt <- forkIO $ do myThreadId >>= flip labelThread ("XMPP.monitor") monitor sv peer_params xmpp dput XMisc $ "Starting peer listen" control sv (Listen peer_bind peer_params) dput XMisc $ "Starting client listen" client_bind <- maybe (getBindAddress "5222" True) return $ xmppClientBind xmpp control sv (Listen client_bind client_params) return mt #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