{-# LANGUAGE CPP #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} module Presence where import System.Directory import System.Environment import System.IO.Error import System.Posix.Signals import Control.Concurrent (threadDelay,forkIO,forkOS,killThread,throwTo) import Control.Concurrent.STM import Control.Concurrent.STM.TMVar import Control.Monad.Trans import Control.Monad.IO.Class (MonadIO, liftIO) import Network.Socket ( SockAddr(..), PortNumber ) import System.Endian (fromBE32) import Data.Char import Data.List (nub, (\\), intersect, groupBy, sort, sortBy ) import Data.Ord (comparing ) import Data.Monoid ( (<>), Sum(..), getSum ) import qualified Data.Text as Text import qualified Data.Text.IO as Text import qualified Data.Text.Encoding as Text import Control.Monad import Control.Monad.Fix import qualified Network.BSD as BSD import qualified Data.Text as Text import Data.Text (Text) import qualified Data.Map as Map import Data.Map (Map) import Control.Exception ({-evaluate,-}handle,SomeException(..),bracketOnError,ErrorCall(..)) import System.IO.Error (isDoesNotExistError) import System.Posix.User (getUserEntryForID,userName) import qualified Data.ByteString.Lazy.Char8 as L import qualified ConfigFiles import Data.Maybe import Data.Bits import Data.Int (Int8) import Data.XML.Types (Event) import System.Posix.Types (UserID,CPid) import Control.Applicative #ifdef CRYPTONITE_BACKPORT import Crypto.Error.Types (CryptoFailable (..)) #else import Crypto.Error #endif import Crypto.PubKey.Curve25519 (SecretKey,toPublic) import Text.Read (readMaybe) import System.IO import LockedChan (LockedChan) import TraversableT import UTmp (ProcessID,users) import LocalPeerCred import XMPPServer import PeerResolve import ConsoleWriter import ClientState import Util import qualified Connection import Network.Tox.NodeId (id2key,key2id) import Crypto.Tox (decodeSecret) isPeerKey :: ConnectionKey -> Bool isPeerKey k = case k of { PeerKey {} -> True ; _ -> False } isClientKey :: ConnectionKey -> Bool isClientKey k = case k of { ClientKey {} -> True ; _ -> False } localJID :: Text -> Text -> Text -> IO Text localJID user "." resource = do hostname <- textHostName return $ user <> "@" <> hostname <> "/" <> resource localJID user profile resource = return $ user <> "@" <> profile <> "/" <> resource -- | These hooks will be invoked in order to connect to *.tox hosts in the -- user's roster. -- -- The parameter k is a lookup key corresponding to an XMPP client. Each -- unique value should be able to hold a reference to the ToxID identity which -- should stay online until all interested keys have run 'deactivateAccount'. data ToxManager k = ToxManager -- | Put the given ToxID online. { activateAccount :: k -> Text -> SecretKey -> IO () -- | Take the given ToxID offline (assuming no other /k/ has a claim). , deactivateAccount :: k -> Text -> IO () -- | Try to connect to the remote peer (or not). -- -- The arguments are our public key (in hostname format) followed by -- their public key (in hostname format) and the Policy to set for this -- link. , setToxConnectionPolicy :: Text -> Text -> Connection.Policy -> IO () } data PresenceState = forall status. PresenceState { clients :: TVar (Map ConnectionKey ClientState) , clientsByUser :: TVar (Map Text LocalPresence) , remotesByPeer :: TVar (Map ConnectionKey (Map UserName RemotePresence)) , server :: TMVar (XMPPServer, Connection.Manager status Text) , keyToChan :: TVar (Map ConnectionKey Conn) , consoleWriter :: Maybe ConsoleWriter , toxManager :: Maybe (ToxManager ConnectionKey) } newPresenceState :: Maybe ConsoleWriter -> Maybe (PresenceState -> ToxManager ConnectionKey) -> TMVar (XMPPServer, Connection.Manager status Text) -> IO PresenceState newPresenceState cw toxman xmpp = atomically $ do clients <- newTVar Map.empty clientsByUser <- newTVar Map.empty remotesByPeer <- newTVar Map.empty keyToChan <- newTVar Map.empty let st = PresenceState { clients = clients , clientsByUser = clientsByUser , remotesByPeer = remotesByPeer , keyToChan = keyToChan , server = xmpp , consoleWriter = cw , toxManager = Nothing } return $ st { toxManager = fmap ($ st) toxman } nameForClient :: PresenceState -> ConnectionKey -> IO Text nameForClient state k = do mc <- atomically $ do cmap <- readTVar (clients state) return $ Map.lookup k cmap case mc of Nothing -> textHostName Just client -> case clientProfile client of "." -> textHostName profile -> return profile presenceHooks :: PresenceState -> Int -> Maybe SockAddr -- ^ client-to-server bind address -> Maybe SockAddr -- ^ server-to-server bind address -> XMPPServerParameters presenceHooks state verbosity mclient mpeer = XMPPServerParameters { xmppChooseResourceName = chooseResourceName state , xmppTellClientHisName = tellClientHisName state , xmppTellMyNameToClient = nameForClient state , xmppTellMyNameToPeer = \addr -> return $ addrToText addr , xmppTellPeerHisName = return . peerKeyToText , xmppTellClientNameOfPeer = flip peerKeyToResolvedName , xmppNewConnection = newConn state , xmppEOF = eofConn state , xmppRosterBuddies = rosterGetBuddies state , xmppRosterSubscribers = rosterGetSubscribers state , xmppRosterSolicited = rosterGetSolicited state , xmppRosterOthers = rosterGetOthers state , xmppSubscribeToRoster = informSentRoster state , xmppDeliverMessage = deliverMessage state , xmppInformClientPresence = informClientPresence state , xmppInformPeerPresence = informPeerPresence state , xmppAnswerProbe = \k stanza chan -> answerProbe state (stanzaTo stanza) k chan , xmppClientSubscriptionRequest = clientSubscriptionRequest state , xmppPeerSubscriptionRequest = peerSubscriptionRequest state , xmppClientInformSubscription = clientInformSubscription state , xmppPeerInformSubscription = peerInformSubscription state , xmppVerbosity = return verbosity , xmppClientBind = mclient , xmppPeerBind = mpeer } data LocalPresence = LocalPresence { networkClients :: Map ConnectionKey ClientState -- TODO: loginClients } data RemotePresence = RemotePresence { resources :: Map Text Stanza -- , localSubscribers :: Map Text () -- ^ subset of clientsByUser who should be -- notified about this presence. } pcSingletonNetworkClient :: ConnectionKey -> ClientState -> LocalPresence pcSingletonNetworkClient key client = LocalPresence { networkClients = Map.singleton key client } pcInsertNetworkClient :: ConnectionKey -> ClientState -> LocalPresence -> LocalPresence pcInsertNetworkClient key client pc = pc { networkClients = Map.insert key client (networkClients pc) } pcRemoveNewtworkClient :: ConnectionKey -> LocalPresence -> Maybe LocalPresence pcRemoveNewtworkClient key pc = if pcIsEmpty pc' then Nothing else Just pc' where pc' = pc { networkClients = Map.delete key (networkClients pc) } pcIsEmpty :: LocalPresence -> Bool pcIsEmpty pc = Map.null (networkClients pc) getConsolePids :: PresenceState -> IO [(Text,ProcessID)] getConsolePids state = do us <- UTmp.users return $ map (\(_,tty,pid)->(lazyByteStringToText tty,pid)) us identifyTTY' :: [(Text, ProcessID)] -> System.Posix.Types.UserID -> L.ByteString -> IO (Maybe Text, Maybe System.Posix.Types.CPid) identifyTTY' ttypids uid inode = ttypid where ttypids' = map (\(tty,pid)->(L.fromChunks [Text.encodeUtf8 tty], pid)) ttypids ttypid = fmap textify $ identifyTTY ttypids' uid inode textify (tty,pid) = (fmap lazyByteStringToText tty, pid) chooseResourceName :: PresenceState -> ConnectionKey -> SockAddr -> Maybe Text -> Maybe Text -> IO Text chooseResourceName state k addr clientsNameForMe desired = do muid <- getLocalPeerCred' addr (mtty,pid) <- getTTYandPID muid user <- getJabberUserForId muid status <- atomically $ newTVar Nothing flgs <- atomically $ newTVar 0 profile <- fmap (fromMaybe ".") $ forM ((,) <$> clientsNameForMe <*> toxManager state) $ \(wanted_profile0,toxman) -> case Text.splitAt 43 wanted_profile0 of (pub,".tox") -> do cdir <- ConfigFiles.configPath (L.fromChunks [Text.encodeUtf8 user]) "." "" #if !MIN_VERSION_directory(1,2,5) let listDirectory path = filter (`notElem` [".",".."]) <$> getDirectoryContents path #endif cfs <- map Text.pack <$> listDirectory cdir `catchIOError` (\e -> return []) let profiles = filter (\f -> Text.toLower f == Text.toLower wanted_profile0) cfs -- hPutStrLn stderr $ "Toxmpp profile " ++ show (user,wanted_profile0,profiles,cfs) let wanted_profile = head $ profiles ++ [wanted_profile0] secs <- configText ConfigFiles.getSecrets user wanted_profile case secs of sec:_ | Just s <- decodeSecret (Text.encodeUtf8 sec) , map toLower (show $ key2id $ toPublic s) == map toLower (Text.unpack pub) -> do activateAccount toxman k wanted_profile s hPutStrLn stderr $ "loaded tox secret " ++ show sec return wanted_profile _ -> do -- XXX: We should probably fail to connect when an -- invalid Tox profile is used. For now, we'll -- fall back to the Unix account login. hPutStrLn stderr "failed to find tox secret" return "." ("*.tox","") -> do hPutStrLn stderr $ "TODO: Match single tox key profile or generate first." -- TODO: Match single tox key profile or generate first. _todo _ -> return "." let client = ClientState { clientResource = maybe "fallback" id mtty , clientUser = user , clientProfile = profile , clientPid = pid , clientStatus = status , clientFlags = flgs } do -- forward-lookup of the buddies so that it is cached for reversing. buds <- configText ConfigFiles.getBuddies (clientUser client) (clientProfile client) forM_ buds $ \bud -> do let (_,h,_) = splitJID bud forkIO $ void $ resolvePeer h atomically $ do modifyTVar' (clients state) $ Map.insert k client modifyTVar' (clientsByUser state) $ flip Map.alter (clientUser client) $ \mb -> Just $ maybe (pcSingletonNetworkClient k client) (pcInsertNetworkClient k client) mb localJID (clientUser client) (clientProfile client) (clientResource client) where getTTYandPID muid = do -- us <- fmap (map (second fst) . Map.toList) . readTVarIO $ activeUsers state ttypids <- getConsolePids state -- let tailOf3 ((_,a),b) = (a,b) (t,pid) <- case muid of Just (uid,inode) -> identifyTTY' ttypids uid inode Nothing -> return (Nothing,Nothing) let rsc = t `mplus` fmap ( ("pid."<>) . Text.pack . show ) pid return (rsc,pid) getJabberUserForId muid = maybe (return "nobody") (\(uid,_) -> handle (\(SomeException _) -> return . (<> "uid.") . Text.pack . show $ uid) $ do user <- fmap userName $ getUserEntryForID uid return (Text.pack user) ) muid -- Perform action with 'ClientState' associated with the given 'ConnectionKey'. -- If there is no associated 'ClientState', then perform the supplied fallback -- action. forClient :: PresenceState -> ConnectionKey -> IO b -> (ClientState -> IO b) -> IO b forClient state k fallback f = do mclient <- atomically $ do cs <- readTVar (clients state) return $ Map.lookup k cs maybe fallback f mclient tellClientHisName :: PresenceState -> ConnectionKey -> IO Text tellClientHisName state k = forClient state k fallback go where fallback = localJID "nobody" "." "fallback" go client = localJID (clientUser client) (clientProfile client) (clientResource client) toMapUnit :: Ord k => [k] -> Map k () toMapUnit xs = Map.fromList $ map (,()) xs resolveAllPeers :: [Text] -> IO (Map SockAddr ()) resolveAllPeers hosts = fmap (toMapUnit . concat) $ Prelude.mapM (fmap (take 1) . resolvePeer) hosts -- Read a roster file and start trying to connect to all relevent peers. rosterGetStuff :: (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) -> PresenceState -> ConnectionKey -> IO [Text] rosterGetStuff what state k = forClient state k (return []) $ \client -> do jids <- configText what (clientUser client) (clientProfile client) let hosts = map ((\(_,h,_)->h) . splitJID) jids -- Using case to bring 'status' type variable to Connection.Manager into scope. case state of PresenceState { server = svVar } -> do (sv,conns) <- atomically $ takeTMVar svVar -- Grok peers to associate with from the roster: forM_ hosts $ \host -> do -- We need either conns :: Connection.Manager TCPStatus Text -- or toxman :: ToxManager ConnectionKey -- It is decided by checking hostnames for .tox ending. let policySetter = fromMaybe (Connection.setPolicy conns host) $ do toxman <- toxManager state (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) (them, ".tox") <- Just $ Text.splitAt 43 host Just $ setToxConnectionPolicy toxman (clientProfile client) host policySetter Connection.TryingToConnect atomically $ putTMVar svVar (sv,conns) return jids rosterGetBuddies :: PresenceState -> ConnectionKey -> IO [Text] rosterGetBuddies state k = rosterGetStuff ConfigFiles.getBuddies state k rosterGetSolicited :: PresenceState -> ConnectionKey -> IO [Text] rosterGetSolicited = rosterGetStuff ConfigFiles.getSolicited -- XXX: Should we be connecting to these peers? rosterGetOthers :: PresenceState -> ConnectionKey -> IO [Text] rosterGetOthers = rosterGetStuff ConfigFiles.getOthers rosterGetSubscribers :: PresenceState -> ConnectionKey -> IO [Text] rosterGetSubscribers = rosterGetStuff ConfigFiles.getSubscribers data Conn = Conn { connChan :: TChan Stanza , auxAddr :: SockAddr } -- Read config file as Text content rather than UTF8 bytestrings. configText :: Functor f => (ConfigFiles.User -> ConfigFiles.Profile -> f [L.ByteString]) -> Text -- user -> Text -- profile -> f [Text] -- items configText what u p = fmap (map lazyByteStringToText) $ what (textToLazyByteString u) (Text.unpack p) getBuddies' :: Text -> Text -> IO [Text] getBuddies' = configText ConfigFiles.getBuddies getSolicited' :: Text -> Text -> IO [Text] getSolicited' = configText ConfigFiles.getSolicited sendProbesAndSolicitations :: PresenceState -> ConnectionKey -> SockAddr -> TChan Stanza -> IO () sendProbesAndSolicitations state k laddr chan = do -- get all buddies & solicited matching k for all users xs <- runTraversableT $ do cbu <- lift $ atomically $ readTVar $ clientsByUser state (user,LocalPresence cmap) <- liftT $ Map.toList cbu profile <- liftT $ nub $ map clientProfile $ Map.elems cmap (isbud,getter) <- liftT [(True ,getBuddies' ) ,(False,getSolicited')] bud <- liftMT $ getter user profile let (u,h,r) = splitJID bud addr <- liftMT $ nub `fmap` resolvePeer h liftT $ guard (PeerKey addr == k) -- Note: Earlier I was tempted to do all the IO -- within the TraversableT monad. That apparently -- is a bad idea. Perhaps due to laziness and an -- unforced list? Instead, we will return a list -- of (Bool,Text) for processing outside. return (isbud,u,user,profile) -- XXX: The following O(n²) nub may be a little -- too onerous. forM_ (nub xs) $ \(isbud,u,user,profile) -> do let make = if isbud then presenceProbe else presenceSolicitation toh = peerKeyToText k jid = unsplitJID (u,toh,Nothing) me = addrToText laddr from = if isbud then me -- probe from server else -- solicitation from particular user unsplitJID (Just user,me,Nothing) stanza <- make from jid -- send probes for buddies, solicitations for solicited. putStrLn $ "probing "++show k++" for: " ++ show (isbud,jid) atomically $ writeTChan chan stanza -- reverse xs `seq` return () newConn :: PresenceState -> ConnectionKey -> SockAddr -> TChan Stanza -> IO () newConn state k addr outchan = do atomically $ modifyTVar' (keyToChan state) $ Map.insert k Conn { connChan = outchan , auxAddr = addr } when (isPeerKey k) $ sendProbesAndSolicitations state k addr outchan delclient :: (Alternative m, Monad m) => ConnectionKey -> m LocalPresence -> m LocalPresence delclient k mlp = do lp <- mlp let nc = Map.delete k $ networkClients lp guard $ not (Map.null nc) return $ lp { networkClients = nc } eofConn :: PresenceState -> ConnectionKey -> IO () eofConn state k = do atomically $ modifyTVar' (keyToChan state) $ Map.delete k case k of ClientKey {} -> do forClient state k (return ()) $ \client -> do forM_ (toxManager state) $ \toxman -> do case Text.splitAt 43 (clientProfile client) of (pub,".tox") -> deactivateAccount toxman k (clientProfile client) _ -> return () stanza <- makePresenceStanza "jabber:server" Nothing Offline informClientPresence state k stanza atomically $ do modifyTVar' (clientsByUser state) $ Map.alter (delclient k) (clientUser client) PeerKey {} -> do let h = peerKeyToText k jids <- atomically $ do rbp <- readTVar (remotesByPeer state) return $ do umap <- maybeToList $ Map.lookup k rbp (u,rp) <- Map.toList umap r <- Map.keys (resources rp) return $ unsplitJID (Just u, h, Just r) forM_ jids $ \jid -> do stanza <- makePresenceStanza "jabber:client" (Just jid) Offline informPeerPresence state k stanza {- rewriteJIDForClient1:: Text -> IO (Maybe ((Maybe Text,Text,Maybe Text),SockAddr)) rewriteJIDForClient1 jid = do let (n,h,r) = splitJID jid maddr <- fmap listToMaybe $ resolvePeer h flip (maybe $ return Nothing) maddr $ \addr -> do h' <- peerKeyToResolvedName (PeerKey addr) return $ Just ((n,h',r), addr) -} -- | The given address is taken to be the local address for the socket this JID -- came in on. The returned JID parts are suitable for unsplitJID to create a -- valid JID for communicating to a client. The returned Bool is True when the -- host part refers to this local host (i.e. it equals the given SockAddr). -- If there are multiple results, it will prefer one which is a member of the -- given list in the last argument. rewriteJIDForClient :: SockAddr -> Text -> [Text] -> IO (Bool,(Maybe Text,Text,Maybe Text)) rewriteJIDForClient laddr jid buds = do let (n,h,r) = splitJID jid maddr <- parseAddress (strip_brackets h) flip (maybe $ return (False,(n,ip6literal h,r))) maddr $ \addr -> do let mine = laddr `withPort` 0 == addr `withPort` 0 h' <- if mine then textHostName else peerKeyToResolvedName buds (PeerKey addr) return (mine,(n,h',r)) -- This attempts to reverse resolve a peers address to give the human-friendly -- domain name as it appears in the roster. It prefers host names that occur -- in the given list of JIDs, but will fall back to any reverse-resolved name -- and if it was unable to reverse the address, it will yield an ip address. peerKeyToResolvedName :: [Text] -> ConnectionKey -> IO Text peerKeyToResolvedName buds k@(ClientKey { localAddress=addr }) = return "ErrorClIeNt1" peerKeyToResolvedName buds pk = do ns <- peerKeyToResolvedNames pk let hs = map (\jid -> let (_,h,_)=splitJID jid in h) buds ns' = sortBy (comparing $ not . flip elem hs) ns return $ fromMaybe (peerKeyToText pk) (listToMaybe ns') -- Given a local address and an IP-address JID, we return True if the JID is -- local, False otherwise. Additionally, a list of equivalent hostname JIDS -- are returned. multiplyJIDForClient :: SockAddr -> Text -> IO (Bool,[(Maybe Text,Text,Maybe Text)]) multiplyJIDForClient laddr jid = do let (n,h,r) = splitJID jid maddr <- parseAddress (strip_brackets h) flip (maybe $ return (False,[(n,ip6literal h,r)])) maddr $ \addr -> do let mine = sameAddress laddr addr names <- if mine then fmap (:[]) textHostName else peerKeyToResolvedNames (PeerKey addr) return (mine,map (\h' -> (n,h',r)) names) addrTextToKey :: Text -> IO (Maybe ConnectionKey) addrTextToKey h = do maddr <- parseAddress (strip_brackets h) return (fmap PeerKey maddr) guardPortStrippedAddress :: Text -> SockAddr -> IO (Maybe ()) guardPortStrippedAddress h laddr = do maddr <- fmap (fmap (`withPort` 0)) $ parseAddress (strip_brackets h) let laddr' = laddr `withPort` 0 return $ maddr >>= guard . (==laddr') -- | Accepts a textual representation of a domainname -- JID suitable for client connections, and returns the -- coresponding ipv6 address JID suitable for peers paired -- with a SockAddr with the address part of that JID in -- binary form. If no suitable address could be resolved -- for the given name, Nothing is returned. rewriteJIDForPeer :: Text -> IO (Maybe (Text,SockAddr)) rewriteJIDForPeer jid = do let (n,h,r) = splitJID jid maddr <- fmap listToMaybe $ resolvePeer h return $ flip fmap maddr $ \addr -> let h' = addrToText addr to' = unsplitJID (n,h',r) in (to',addr) deliverToConsole :: PresenceState -> IO () -> Stanza -> IO () deliverToConsole PresenceState{ consoleWriter = Just cw } fail msg = do did1 <- writeActiveTTY cw msg did2 <- writeAllPty cw msg if not (did1 || did2) then fail else return () deliverToConsole _ fail _ = fail -- | deliver or error stanza deliverMessage :: PresenceState -> IO () -> StanzaWrap (LockedChan Event) -> IO () deliverMessage state fail msg = case stanzaOrigin msg of NetworkOrigin senderk@(ClientKey {}) _ -> do -- Case 1. Client -> Peer mto <- do flip (maybe $ return Nothing) (stanzaTo msg) $ \to -> do rewriteJIDForPeer to flip (maybe fail {- reverse lookup failure -}) mto $ \(to',addr) -> do let k = PeerKey addr chans <- atomically $ readTVar (keyToChan state) flip (maybe fail) (Map.lookup k chans) $ \(Conn { connChan=chan , auxAddr=laddr }) -> do (n,r) <- forClient state senderk (return (Nothing,Nothing)) $ \c -> return (Just (clientUser c), Just (clientResource c)) -- original 'from' address is discarded. let from' = unsplitJID (n,addrToText laddr,r) -- dup <- atomically $ cloneStanza (msg { stanzaTo=Just to', stanzaFrom=Just from' }) let dup = (msg { stanzaTo=Just to', stanzaFrom=Just from' }) sendModifiedStanzaToPeer dup chan NetworkOrigin senderk@(PeerKey {}) _ -> do key_to_chan <- atomically $ readTVar (keyToChan state) flip (maybe fail) (Map.lookup senderk key_to_chan) $ \(Conn { connChan=sender_chan , auxAddr=laddr }) -> do flip (maybe fail) (stanzaTo msg) $ \to -> do (mine,(n,h,r)) <- rewriteJIDForClient laddr to [] if not mine then fail else do let to' = unsplitJID (n,h,r) cmap <- atomically . readTVar $ clientsByUser state chans <- fmap (fromMaybe []) $ do forM (n >>= flip Map.lookup cmap) $ \presence_container -> do let ks = Map.keys (networkClients presence_container) chans = do (k,client) <- Map.toList $ networkClients presence_container chan <- maybeToList $ Map.lookup k key_to_chan return (clientProfile client, chan) forM chans $ \(profile,chan) -> do buds <- configText ConfigFiles.getBuddies (fromJust n) profile from' <- do flip (maybe $ return Nothing) (stanzaFrom msg) $ \from -> do (_,trip) <- rewriteJIDForClient laddr from buds return . Just $ unsplitJID trip return (from',chan) putStrLn $ "chan count: " ++ show (length chans) if null chans then do forM_ (stanzaFrom msg) $ \from -> do from' <- do -- Fallback to "." profile when no clients. buds <- maybe (return []) (\n -> configText ConfigFiles.getBuddies n ".") n (_,trip) <- rewriteJIDForClient laddr from buds return . Just $ unsplitJID trip let msg' = msg { stanzaTo=Just to' , stanzaFrom=from' } deliverToConsole state fail msg' else do forM_ chans $ \(from',Conn { connChan=chan}) -> do -- TODO: Cloning isn't really neccessary unless there are multiple -- destinations and we should probably transition to minimal cloning, -- or else we should distinguish between announcable stanzas and -- consumable stanzas and announcables use write-only broadcast -- channels that must be cloned in order to be consumed. -- For now, we are doing redundant cloning. let msg' = msg { stanzaTo=Just to' , stanzaFrom=from' } dup <- cloneStanza msg' sendModifiedStanzaToClient dup chan setClientFlag :: PresenceState -> ConnectionKey -> Int8 -> IO () setClientFlag state k flag = atomically $ do cmap <- readTVar (clients state) flip (maybe $ return ()) (Map.lookup k cmap) $ \client -> do setClientFlag0 client flag setClientFlag0 :: ClientState -> Int8 -> STM () setClientFlag0 client flag = modifyTVar' (clientFlags client) (\flgs -> flgs .|. flag) informSentRoster :: PresenceState -> ConnectionKey -> IO () informSentRoster state k = do setClientFlag state k cf_interested subscribedPeers :: Text -> Text -> IO [SockAddr] subscribedPeers user profile = do jids <- configText ConfigFiles.getSubscribers user profile let hosts = map ((\(_,h,_)->h) . splitJID) jids fmap Map.keys $ resolveAllPeers hosts -- | this JID is suitable for peers, not clients. clientJID :: Conn -> ClientState -> Text clientJID con client = unsplitJID ( Just $ clientUser client , addrToText $ auxAddr con , Just $ clientResource client) -- | Send presence notification to subscribed peers. -- Note that a full JID from address will be added to the -- stanza if it is not present. informClientPresence :: PresenceState -> ConnectionKey -> StanzaWrap (LockedChan Event) -> IO () informClientPresence state k stanza = do forClient state k (return ()) $ \client -> do informClientPresence0 state (Just k) client stanza informClientPresence0 :: PresenceState -> Maybe ConnectionKey -> ClientState -> StanzaWrap (LockedChan Event) -> IO () informClientPresence0 state mbk client stanza = do dup <- cloneStanza stanza atomically $ writeTVar (clientStatus client) $ Just dup is_avail <- atomically $ clientIsAvailable client when (not is_avail) $ do atomically $ setClientFlag0 client cf_available maybe (return ()) (sendCachedPresence state) mbk addrs <- subscribedPeers (clientUser client) (clientProfile client) ktc <- atomically $ readTVar (keyToChan state) let connected = mapMaybe (flip Map.lookup ktc . PeerKey) addrs forM_ connected $ \con -> do let from' = clientJID con client mto <- runTraversableT $ do to <- liftT $ stanzaTo stanza (to',_) <- liftMT $ rewriteJIDForPeer to return to' dup <- cloneStanza stanza sendModifiedStanzaToPeer dup { stanzaFrom = Just from' , stanzaTo = mto } (connChan con) informPeerPresence :: PresenceState -> ConnectionKey -> StanzaWrap (LockedChan Event) -> IO () informPeerPresence state k stanza = do -- Presence must indicate full JID with resource... putStrLn $ "xmppInformPeerPresence checking from address..." flip (maybe $ return ()) (stanzaFrom stanza) $ \from -> do let (muser,h,mresource) = splitJID from putStrLn $ "xmppInformPeerPresence from = " ++ show from -- flip (maybe $ return ()) mresource $ \resource -> do flip (maybe $ return ()) muser $ \user -> do clients <- atomically $ do -- Update remotesByPeer... rbp <- readTVar (remotesByPeer state) let umap = maybe Map.empty id $ Map.lookup k rbp rp = case (presenceShow $ stanzaType stanza) of Offline -> maybe Map.empty (\resource -> maybe (Map.empty) (Map.delete resource . resources) $ Map.lookup user umap) mresource _ ->maybe Map.empty (\resource -> maybe (Map.singleton resource stanza) (Map.insert resource stanza . resources ) $ Map.lookup user umap) mresource umap' = Map.insert user (RemotePresence rp) umap flip (maybe $ return []) (case presenceShow $ stanzaType stanza of Offline -> Just () _ -> mresource >> Just ()) $ \_ -> do writeTVar (remotesByPeer state) $ Map.insert k umap' rbp -- TODO: Store or delete the stanza (remotesByPeer) -- all clients, we'll filter available/authorized later ktc <- readTVar (keyToChan state) runTraversableT $ do (ck,client) <- liftMT $ fmap Map.toList $ readTVar (clients state) con <- liftMaybe $ Map.lookup ck ktc return (ck,con,client) putStrLn $ "xmppInformPeerPresence (length clients="++show (length clients)++")" forM_ clients $ \(ck,con,client) -> do -- (TODO: appropriately authorized clients only.) -- For now, all "available" clients (available = sent initial presence) is_avail <- atomically $ clientIsAvailable client when is_avail $ do putStrLn $ "reversing for client: " ++ show from froms <- do -- flip (maybe $ return [from]) k . const $ do let ClientKey laddr = ck (_,trip) <- multiplyJIDForClient laddr from return (map unsplitJID trip) putStrLn $ "sending to client: " ++ show (stanzaType stanza,froms) forM_ froms $ \from' -> do dup <- cloneStanza stanza sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) (connChan con) consoleClients :: PresenceState -> STM (Map Text ClientState) consoleClients PresenceState{ consoleWriter = Just cw } = readTVar (cwClients cw) consoleClients _ = return Map.empty answerProbe :: PresenceState -> Maybe Text -> ConnectionKey -> TChan Stanza -> IO () answerProbe state mto k chan = do -- putStrLn $ "answerProbe! " ++ show (stanzaType stanza) (ktc,cmap) <- atomically $ (,) <$> readTVar (keyToChan state) <*> readTVar (clients state) muser <- runTraversableT $ do to <- liftT $ mto conn <- liftT $ Map.lookup k ktc let (mu,h,_) = splitJID to -- TODO: currently resource-id is ignored on presence -- probes. Is this correct? Check the spec. liftMT $ guardPortStrippedAddress h (auxAddr conn) u <- liftT mu let ch = addrToText (auxAddr conn) profile = fromMaybe "." $ clientProfile <$> Map.lookup k cmap return (u,profile,conn,ch) flip (maybe $ return ()) muser $ \(u,profile,conn,ch) -> do resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers u profile let gaddrs = groupBy (\a b -> snd a == snd b) (sort resolved_subs) whitelist = do xs <- gaddrs x <- take 1 xs guard $ snd x==k mapMaybe fst xs -- -- only subscribed peers should get probe replies -- addrs <- subscribedPeers u -- TODO: notify remote peer that they are unsubscribed? -- reply <- makeInformSubscription "jabber:server" to from False when (not $ null whitelist) $ do replies <- runTraversableT $ do cbu <- lift . atomically $ readTVar (clientsByUser state) let lpres = maybeToList $ Map.lookup u cbu cw <- lift . atomically $ consoleClients state clientState <- liftT $ (lpres >>= Map.elems . networkClients) ++ Map.elems cw stanza <- liftIOMaybe $ atomically (readTVar (clientStatus clientState)) stanza <- lift $ cloneStanza stanza let jid = unsplitJID (Just $ clientUser clientState , ch ,Just $ clientResource clientState) return stanza { stanzaFrom = Just jid , stanzaType = (stanzaType stanza) { presenceWhiteList = whitelist } } forM_ replies $ \reply -> do sendModifiedStanzaToPeer reply chan -- if no presence, send offline message when (null replies) $ do let jid = unsplitJID (Just u,ch,Nothing) pstanza <- makePresenceStanza "jabber:server" (Just jid) Offline atomically $ writeTChan (connChan conn) pstanza -- Send friend requests and remote presences stored in remotesByPeer to XMPP -- clients. sendCachedPresence :: PresenceState -> ConnectionKey -> IO () sendCachedPresence state k = do forClient state k (return ()) $ \client -> do rbp <- atomically $ readTVar (remotesByPeer state) jids <- configText ConfigFiles.getBuddies (clientUser client) (clientProfile client) let hosts = map ((\(_,h,_)->h) . splitJID) jids addrs <- resolveAllPeers hosts let onlines = rbp `Map.intersection` Map.mapKeys PeerKey addrs ClientKey laddr = k mcon <- atomically $ do ktc <- readTVar (keyToChan state) return $ Map.lookup k ktc flip (maybe $ return ()) mcon $ \con -> do forM_ (Map.toList onlines) $ \(pk, umap) -> do forM_ (Map.toList umap) $ \(user,rp) -> do let h = peerKeyToText pk forM_ (Map.toList $ resources rp) $ \(resource,stanza) -> do let jid = unsplitJID (Just user,h,Just resource) (mine,js) <- multiplyJIDForClient laddr jid forM_ js $ \jid -> do let from' = unsplitJID jid dup <- cloneStanza stanza sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) (connChan con) pending <- configText ConfigFiles.getPending (clientUser client) (clientProfile client) hostname <- nameForClient state k forM_ pending $ \pending_jid -> do let cjid = unsplitJID ( Just $ clientUser client , hostname , Nothing ) ask <- presenceSolicitation pending_jid cjid sendModifiedStanzaToClient ask (connChan con) -- Note: relying on self peer connection to send -- send local buddies. return () addToRosterFile :: (MonadPlus t, Traversable t) => (ConfigFiles.User -> ConfigFiles.Profile -> (L.ByteString -> IO (t L.ByteString)) -> Maybe L.ByteString -> t1) -> Text -- user -> Text -- profile -> Text -> [SockAddr] -> t1 addToRosterFile doit whose profile to addrs = modifyRosterFile doit whose profile to addrs True removeFromRosterFile :: (MonadPlus t, Traversable t) => (ConfigFiles.User -> ConfigFiles.Profile -> (L.ByteString -> IO (t L.ByteString)) -> Maybe L.ByteString -> t1) -> Text -- user -> Text -- profile -> Text -> [SockAddr] -> t1 removeFromRosterFile doit whose profile to addrs = modifyRosterFile doit whose profile to addrs False modifyRosterFile :: (Traversable t, MonadPlus t) => (ConfigFiles.User -> ConfigFiles.Profile -> (L.ByteString -> IO (t L.ByteString)) -> Maybe L.ByteString -> t1) -> Text -- user -> Text -- profile -> Text -> [SockAddr] -- If a JID reverse-resolves to this address, consider it same. -> Bool -> t1 modifyRosterFile doit whose profile to addrs bAdd = do let (mu,_,_) = splitJID to cmp jid = runTraversableT $ do let (msu,stored_h,mr) = splitJID (lazyByteStringToText jid) -- Delete from file if a resource is present in file (\f -> maybe f (const mzero) mr) $ do -- Delete from file if no user is present in file flip (maybe mzero) msu $ \stored_u -> do -- do not delete anything if no user was specified flip (maybe $ return jid) mu $ \u -> do -- do not delete if stored user is same as specified if stored_u /= u then return jid else do stored_addrs <- lift $ resolvePeer stored_h -- do not delete if failed to resolve if null stored_addrs then return jid else do -- delete if specified address matches stored if null (stored_addrs \\ addrs) then mzero else do -- keep return jid doit (textToLazyByteString whose) (Text.unpack profile) cmp (guard bAdd >> Just (textToLazyByteString to)) clientSubscriptionRequest :: PresenceState -> IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () clientSubscriptionRequest state fail k stanza chan = do forClient state k fail $ \client -> do flip (maybe fail) (stanzaTo stanza) $ \to -> do putStrLn $ "Forwarding solictation to peer" let (mu,h,_) = splitJID to to <- return $ unsplitJID (mu,h,Nothing) -- delete resource flip (maybe fail) mu $ \u -> do -- add to-address to from's solicited addrs <- resolvePeer h addToRosterFile ConfigFiles.modifySolicited (clientUser client) (clientProfile client) to addrs removeFromRosterFile ConfigFiles.modifyBuddies (clientUser client) (clientProfile client) to addrs resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers (clientUser client) (clientProfile client) let is_subscribed = not . null $ intersect (map ((mu,).PeerKey) addrs) resolved_subs -- subscribers: "from" -- buddies: "to" case state of PresenceState { server = svVar } -> do (ktc,(sv,conns)) <- atomically $ liftM2 (,) (readTVar $ keyToChan state) (takeTMVar svVar) case stanzaType stanza of PresenceRequestSubscription True -> do hostname <- nameForClient state k let cjid = unsplitJID (Just $ clientUser client, hostname,Nothing) chans <- clientCons state ktc (clientUser client) forM_ chans $ \( Conn { connChan=chan }, client ) -> do -- roster update ask="subscribe" update <- makeRosterUpdate cjid to [ ("ask","subscribe") , if is_subscribed then ("subscription","from") else ("subscription","none") ] sendModifiedStanzaToClient update chan _ -> return () let dsts = Map.fromList $ map ((,()) . PeerKey) addrs cdsts = ktc `Map.intersection` dsts forM_ (Map.toList cdsts) $ \(pk,con) -> do -- if already connected, send solicitation ... -- let from = clientJID con client let from = unsplitJID ( Just $ clientUser client , addrToText $ auxAddr con , Nothing ) mb <- rewriteJIDForPeer to flip (maybe $ return ()) mb $ \(to',addr) -> do dup <- cloneStanza stanza sendModifiedStanzaToPeer (dup { stanzaTo = Just to' , stanzaFrom = Just from }) (connChan con) let addrm = Map.fromList (map (,()) addrs) policySetter = fromMaybe (Connection.setPolicy conns h) $ do toxman <- toxManager state (me , ".tox") <- Just $ Text.splitAt 43 (clientProfile client) (them, ".tox") <- Just $ Text.splitAt 43 h Just $ setToxConnectionPolicy toxman (clientProfile client) h -- Add peer if we are not already associated ... policySetter Connection.TryingToConnect atomically $ putTMVar svVar (sv,conns) resolvedFromRoster :: (ConfigFiles.User -> ConfigFiles.Profile -> IO [L.ByteString]) -> UserName -> Text -> IO [(Maybe UserName, ConnectionKey)] resolvedFromRoster doit u profile = do subs <- configText doit u profile runTraversableT $ do (mu,h,_) <- liftT $ splitJID `fmap` subs addr <- liftMT $ fmap nub $ resolvePeer h return (mu,PeerKey addr) clientCons :: PresenceState -> Map ConnectionKey t -> Text -> IO [(t, ClientState)] clientCons state ktc u = map snd <$> clientCons' state ktc u clientCons' :: PresenceState -> Map ConnectionKey t -> Text -> IO [(ConnectionKey,(t, ClientState))] clientCons' state ktc u = do mlp <- atomically $ do cmap <- readTVar $ clientsByUser state return $ Map.lookup u cmap let ks = do lp <- maybeToList mlp Map.toList (networkClients lp) doit (k,client) = do con <- Map.lookup k ktc return (k,(con,client)) return $ mapMaybe doit ks peerSubscriptionRequest :: PresenceState -> IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () peerSubscriptionRequest state fail k stanza chan = do putStrLn $ "Handling pending subscription from remote" flip (maybe fail) (stanzaFrom stanza) $ \from -> do flip (maybe fail) (stanzaTo stanza) $ \to -> do let (mto_u,h,_) = splitJID to (mfrom_u,from_h,_) = splitJID from to <- return $ unsplitJID (mto_u,h,Nothing) -- delete resource from <- return $ unsplitJID (mfrom_u,from_h,Nothing) -- delete resource (ktc,cmap) <- atomically $ (,) <$> readTVar (keyToChan state) <*> readTVar (clients state) flip (maybe fail) (Map.lookup k ktc) $ \Conn { auxAddr=laddr } -> do (mine,totup) <- rewriteJIDForClient laddr to [] if not mine then fail else do (_,fromtup) <- rewriteJIDForClient laddr from [] flip (maybe fail) mto_u $ \u -> do flip (maybe fail) mfrom_u $ \from_u -> do let profile = fromMaybe "." $ clientProfile <$> Map.lookup k cmap -- XXX profile is wrong. -- TODO Likely the problem is that k is a peer ConnectionKey and of course -- will have no entry in the cmap. Thus giving "." even though it ought -- to be using a tox profile. -- -- Solution 1: Only .tox peers go in a tox profile. -- Solution 2: Duplicate non .tox peers in all profiles. -- Solution 3: Only one profile is active at a time. resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers u profile let already_subscribed = elem (mfrom_u,k) resolved_subs is_wanted = case stanzaType stanza of PresenceRequestSubscription b -> b _ -> False -- Shouldn't happen. -- Section 8 says (for presence of type "subscribe", the server MUST -- adhere to the rules defined under Section 3 and summarized under -- see Appendix A. (pariticularly Appendex A.3.1) if already_subscribed == is_wanted then do -- contact ∈ subscribers --> SHOULD NOT, already handled -- already subscribed, reply and quit -- (note: swapping to and from for reply) reply <- makeInformSubscription "jabber:server" to from is_wanted sendModifiedStanzaToPeer reply chan answerProbe state (Just to) k chan else do -- TODO: if peer-connection is to self, then auto-approve local user. -- add from-address to to's pending addrs <- resolvePeer from_h -- Catch exception in case the user does not exist if null addrs then fail else do let from' = unsplitJID fromtup -- Update roster files (subscribe: add to pending, unsubscribe: remove from subscribers). already_pending <- if is_wanted then addToRosterFile ConfigFiles.modifyPending u profile from' addrs else do removeFromRosterFile ConfigFiles.modifySubscribers u profile from' addrs reply <- makeInformSubscription "jabber:server" to from is_wanted sendModifiedStanzaToPeer reply chan return False -- contact ∉ subscribers & contact ∈ pending --> SHOULD NOT when (not already_pending) $ do -- contact ∉ subscribers & contact ∉ pending --> MUST chans <- clientCons state ktc u forM_ chans $ \( Conn { connChan=chan }, client ) -> do -- send to clients -- TODO: interested/available clients only? dup <- cloneStanza stanza sendModifiedStanzaToClient dup { stanzaFrom = Just $ from' , stanzaTo = Just $ unsplitJID totup } chan clientInformSubscription :: PresenceState -> IO () -> ConnectionKey -> StanzaWrap (LockedChan Event) -> IO () clientInformSubscription state fail k stanza = do forClient state k fail $ \client -> do flip (maybe fail) (stanzaTo stanza) $ \to -> do putStrLn $ "clientInformSubscription" let (mu,h,mr) = splitJID to addrs <- resolvePeer h -- remove from pending buds <- resolvedFromRoster ConfigFiles.getBuddies (clientUser client) (clientProfile client) let is_buddy = not . null $ map ((mu,) . PeerKey) addrs `intersect` buds removeFromRosterFile ConfigFiles.modifyPending (clientUser client) (clientProfile client) to addrs let (relationship,addf,remf) = case stanzaType stanza of PresenceInformSubscription True -> ( ("subscription", if is_buddy then "both" else "from" ) , ConfigFiles.modifySubscribers , ConfigFiles.modifyOthers ) _ -> ( ("subscription", if is_buddy then "to" else "none" ) , ConfigFiles.modifyOthers , ConfigFiles.modifySubscribers ) addToRosterFile addf (clientUser client) (clientProfile client) to addrs removeFromRosterFile remf (clientUser client) (clientProfile client) to addrs do cbu <- atomically $ readTVar (clientsByUser state) putStrLn $ "cbu = " ++ show (fmap (fmap clientPid . networkClients) cbu) -- send roster update to clients (clients,ktc) <- atomically $ do cbu <- readTVar (clientsByUser state) let mlp = Map.lookup (clientUser client) cbu let cs = maybe [] (Map.toList . networkClients) mlp ktc <- readTVar (keyToChan state) return (cs,ktc) forM_ clients $ \(ck, client) -> do is_intereseted <- atomically $ clientIsInterested client putStrLn $ "clientIsInterested: "++show is_intereseted is_intereseted <- atomically $ clientIsInterested client when is_intereseted $ do flip (maybe $ return ()) (Map.lookup ck ktc) $ \con -> do hostname <- nameForClient state ck -- TODO: Should cjid include the resource? let cjid = unsplitJID (mu, hostname, Nothing) update <- makeRosterUpdate cjid to [relationship] sendModifiedStanzaToClient update (connChan con) -- notify peer let dsts = Map.fromList $ map ((,()) . PeerKey) addrs cdsts = ktc `Map.intersection` dsts forM_ (Map.toList cdsts) $ \(pk,con) -> do let from = clientJID con client to' = unsplitJID (mu, peerKeyToText pk, Nothing) dup <- cloneStanza stanza sendModifiedStanzaToPeer (dup { stanzaTo = Just $ to' , stanzaFrom = Just from }) (connChan con) answerProbe state (Just from) pk (connChan con) peerInformSubscription :: PresenceState -> IO () -> ConnectionKey -> StanzaWrap (LockedChan Event) -> IO () peerInformSubscription state fail k stanza = do putStrLn $ "TODO: peerInformSubscription" -- remove from solicited flip (maybe fail) (stanzaFrom stanza) $ \from -> do (ktc,cmap) <- atomically $ (,) <$> readTVar (keyToChan state) <*> readTVar (clients state) flip (maybe fail) (Map.lookup k ktc) $ \(Conn { connChan=sender_chan , auxAddr=laddr }) -> do (_,(from_u,from_h,_)) <- rewriteJIDForClient laddr from [] let from'' = unsplitJID (from_u,from_h,Nothing) muser = do to <- stanzaTo stanza let (mu,to_h,to_r) = splitJID to mu -- TODO muser = Nothing when wanted=False -- should probably mean unsubscribed for all users. -- This would allow us to answer anonymous probes with 'unsubscribed'. flip (maybe fail) muser $ \user -> do addrs <- resolvePeer from_h let profile = fromMaybe "." $ clientProfile <$> Map.lookup k cmap was_solicited <- removeFromRosterFile ConfigFiles.modifySolicited user profile from'' addrs subs <- resolvedFromRoster ConfigFiles.getSubscribers user profile let is_sub = not . null $ map ((from_u,) . PeerKey) addrs `intersect` subs putStrLn $ "DEBUG peerInformSubscription (is_sub,typ)=" ++ show (is_sub,stanzaType stanza) let (relationship,addf,remf) = case stanzaType stanza of PresenceInformSubscription True -> ( ("subscription", if is_sub then "both" else "to" ) , ConfigFiles.modifyBuddies , ConfigFiles.modifyOthers ) _ -> ( ("subscription", if is_sub then "from" else "none") , ConfigFiles.modifyOthers , ConfigFiles.modifyBuddies ) addToRosterFile addf user profile from'' addrs removeFromRosterFile remf user profile from'' addrs chans <- clientCons' state ktc user forM_ chans $ \(ckey,(Conn { connChan=chan }, client)) -> do hostname <- nameForClient state ckey let to' = unsplitJID (Just user, hostname, Nothing) update <- makeRosterUpdate to' from'' [relationship] is_intereseted <- atomically $ clientIsInterested client when is_intereseted $ do sendModifiedStanzaToClient update chan -- TODO: interested/availabe clients only? dup <- cloneStanza stanza sendModifiedStanzaToClient dup { stanzaFrom = Just $ from'' , stanzaTo = Just to' } chan