diff options
Diffstat (limited to 'xmppServer.hs')
-rw-r--r-- | xmppServer.hs | 1113 |
1 files changed, 1113 insertions, 0 deletions
diff --git a/xmppServer.hs b/xmppServer.hs new file mode 100644 index 00000000..803b4324 --- /dev/null +++ b/xmppServer.hs | |||
@@ -0,0 +1,1113 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | ||
2 | {-# LANGUAGE TupleSections #-} | ||
3 | {-# LANGUAGE LambdaCase #-} | ||
4 | import System.Environment | ||
5 | import System.Posix.Signals | ||
6 | import Control.Concurrent (threadDelay,forkIO,forkOS,killThread,throwTo) | ||
7 | import Control.Concurrent.STM | ||
8 | import Control.Concurrent.STM.TMVar | ||
9 | import Control.Monad.Trans.Resource (runResourceT) | ||
10 | import Control.Monad.Trans | ||
11 | import Control.Monad.IO.Class (MonadIO, liftIO) | ||
12 | import Network.Socket ( SockAddr(..) ) | ||
13 | import System.Endian (fromBE32) | ||
14 | import Data.List (nub, (\\), intersect, groupBy, sort, sortBy ) | ||
15 | import Data.Ord (comparing ) | ||
16 | import Data.Monoid ( (<>), Sum(..), getSum ) | ||
17 | import qualified Data.Text as Text | ||
18 | import qualified Data.Text.IO as Text | ||
19 | import qualified Data.Text.Encoding as Text | ||
20 | import Control.Monad | ||
21 | import Control.Monad.Fix | ||
22 | import qualified Network.BSD as BSD | ||
23 | import qualified Data.Text as Text | ||
24 | import Data.Text (Text) | ||
25 | import qualified Data.Map as Map | ||
26 | import Data.Map (Map) | ||
27 | import Control.Exception ({-evaluate,-}handle,SomeException(..),bracketOnError,ErrorCall(..)) | ||
28 | import System.IO.Error (isDoesNotExistError) | ||
29 | import System.Posix.User (getUserEntryForID,userName) | ||
30 | import qualified Data.ByteString.Lazy.Char8 as L | ||
31 | import qualified ConfigFiles | ||
32 | import Data.Maybe (maybeToList,listToMaybe,mapMaybe) | ||
33 | import Data.Bits | ||
34 | import Data.Int (Int8) | ||
35 | import Data.XML.Types (Event) | ||
36 | import System.Posix.Types (UserID,CPid) | ||
37 | import Control.Applicative | ||
38 | |||
39 | import LockedChan (LockedChan) | ||
40 | import TraversableT | ||
41 | import UTmp (ProcessID,users) | ||
42 | import LocalPeerCred | ||
43 | import XMPPServer | ||
44 | import PeerResolve | ||
45 | import ConsoleWriter | ||
46 | import ClientState | ||
47 | |||
48 | type UserName = Text | ||
49 | type ResourceName = Text | ||
50 | |||
51 | unsplitJID :: (Maybe UserName,Text,Maybe ResourceName) -> Text | ||
52 | unsplitJID (n,h,r) = username <> h <> resource | ||
53 | where | ||
54 | username = maybe "" (<>"@") n | ||
55 | resource = maybe "" ("/"<>) r | ||
56 | |||
57 | splitJID :: Text -> (Maybe UserName,Text,Maybe ResourceName) | ||
58 | splitJID bjid = | ||
59 | let xs = splitAll '@' bjid | ||
60 | ys = splitAll '/' (last xs) | ||
61 | splitAll c bjid = take 1 xs0 ++ map (Text.drop 1) (drop 1 xs0) | ||
62 | where xs0 = Text.groupBy (\x y-> y/=c) bjid | ||
63 | server = head ys | ||
64 | name = case xs of | ||
65 | (n:s:_) -> Just n | ||
66 | (s:_) -> Nothing | ||
67 | rsrc = case ys of | ||
68 | (s:_:_) -> Just $ last ys | ||
69 | _ -> Nothing | ||
70 | in (name,server,rsrc) | ||
71 | |||
72 | isPeerKey :: ConnectionKey -> Bool | ||
73 | isPeerKey k = case k of { PeerKey {} -> True ; _ -> False } | ||
74 | |||
75 | isClientKey :: ConnectionKey -> Bool | ||
76 | isClientKey k = case k of { ClientKey {} -> True ; _ -> False } | ||
77 | |||
78 | textHostName :: IO Text | ||
79 | textHostName = fmap Text.pack BSD.getHostName | ||
80 | |||
81 | localJID :: Text -> Text -> IO Text | ||
82 | localJID user resource = do | ||
83 | hostname <- textHostName | ||
84 | return $ user <> "@" <> hostname <> "/" <> resource | ||
85 | |||
86 | |||
87 | data LocalPresence = LocalPresence | ||
88 | { networkClients :: Map ConnectionKey ClientState | ||
89 | -- TODO: loginClients | ||
90 | } | ||
91 | |||
92 | data RemotePresence = RemotePresence | ||
93 | { resources :: Map Text Stanza | ||
94 | -- , localSubscribers :: Map Text () | ||
95 | -- ^ subset of clientsByUser who should be | ||
96 | -- notified about this presence. | ||
97 | } | ||
98 | |||
99 | |||
100 | |||
101 | pcSingletonNetworkClient :: ConnectionKey | ||
102 | -> ClientState -> LocalPresence | ||
103 | pcSingletonNetworkClient key client = | ||
104 | LocalPresence | ||
105 | { networkClients = Map.singleton key client | ||
106 | } | ||
107 | |||
108 | pcInsertNetworkClient :: ConnectionKey -> ClientState -> LocalPresence -> LocalPresence | ||
109 | pcInsertNetworkClient key client pc = | ||
110 | pc { networkClients = Map.insert key client (networkClients pc) } | ||
111 | |||
112 | pcRemoveNewtworkClient :: ConnectionKey | ||
113 | -> LocalPresence -> Maybe LocalPresence | ||
114 | pcRemoveNewtworkClient key pc = if pcIsEmpty pc' then Nothing | ||
115 | else Just pc' | ||
116 | where | ||
117 | pc' = pc { networkClients = Map.delete key (networkClients pc) } | ||
118 | |||
119 | pcIsEmpty :: LocalPresence -> Bool | ||
120 | pcIsEmpty pc = Map.null (networkClients pc) | ||
121 | |||
122 | |||
123 | data PresenceState = PresenceState | ||
124 | { clients :: TVar (Map ConnectionKey ClientState) | ||
125 | , clientsByUser :: TVar (Map Text LocalPresence) | ||
126 | , remotesByPeer :: TVar (Map ConnectionKey | ||
127 | (Map UserName | ||
128 | RemotePresence)) | ||
129 | , associatedPeers :: TVar (Map SockAddr ()) | ||
130 | , server :: TMVar XMPPServer | ||
131 | , keyToChan :: TVar (Map ConnectionKey Conn) | ||
132 | , consoleWriter :: ConsoleWriter | ||
133 | } | ||
134 | |||
135 | |||
136 | |||
137 | getConsolePids :: PresenceState -> IO [(Text,ProcessID)] | ||
138 | getConsolePids state = do | ||
139 | us <- UTmp.users | ||
140 | return $ map (\(_,tty,pid)->(lazyByteStringToText tty,pid)) us | ||
141 | |||
142 | lazyByteStringToText :: L.ByteString -> Text | ||
143 | lazyByteStringToText = (foldr1 (<>) . map Text.decodeUtf8 . L.toChunks) | ||
144 | |||
145 | textToLazyByteString :: Text -> L.ByteString | ||
146 | textToLazyByteString s = L.fromChunks [Text.encodeUtf8 s] | ||
147 | |||
148 | identifyTTY' :: [(Text, ProcessID)] | ||
149 | -> System.Posix.Types.UserID | ||
150 | -> L.ByteString | ||
151 | -> IO (Maybe Text, Maybe System.Posix.Types.CPid) | ||
152 | identifyTTY' ttypids uid inode = ttypid | ||
153 | where ttypids' = map (\(tty,pid)->(L.fromChunks [Text.encodeUtf8 tty], pid)) ttypids | ||
154 | ttypid = fmap textify $ identifyTTY ttypids' uid inode | ||
155 | textify (tty,pid) = (fmap lazyByteStringToText tty, pid) | ||
156 | |||
157 | chooseResourceName :: PresenceState | ||
158 | -> ConnectionKey -> SockAddr -> t -> IO Text | ||
159 | chooseResourceName state k addr desired = do | ||
160 | muid <- getLocalPeerCred' addr | ||
161 | (mtty,pid) <- getTTYandPID muid | ||
162 | user <- getJabberUserForId muid | ||
163 | status <- atomically $ newTVar Nothing | ||
164 | flgs <- atomically $ newTVar 0 | ||
165 | let client = ClientState { clientResource = maybe "fallback" id mtty | ||
166 | , clientUser = user | ||
167 | , clientPid = pid | ||
168 | , clientStatus = status | ||
169 | , clientFlags = flgs } | ||
170 | |||
171 | do -- forward-lookup of the buddies so that it is cached for reversing. | ||
172 | buds <- configText ConfigFiles.getBuddies (clientUser client) | ||
173 | forM_ buds $ \bud -> do | ||
174 | let (_,h,_) = splitJID bud | ||
175 | forkIO $ void $ resolvePeer h | ||
176 | |||
177 | atomically $ do | ||
178 | modifyTVar' (clients state) $ Map.insert k client | ||
179 | modifyTVar' (clientsByUser state) $ flip Map.alter (clientUser client) | ||
180 | $ \mb -> Just $ maybe (pcSingletonNetworkClient k client) | ||
181 | (pcInsertNetworkClient k client) | ||
182 | mb | ||
183 | |||
184 | localJID (clientUser client) (clientResource client) | ||
185 | |||
186 | where | ||
187 | getTTYandPID muid = do | ||
188 | -- us <- fmap (map (second fst) . Map.toList) . readTVarIO $ activeUsers state | ||
189 | ttypids <- getConsolePids state | ||
190 | -- let tailOf3 ((_,a),b) = (a,b) | ||
191 | (t,pid) <- case muid of | ||
192 | Just (uid,inode) -> identifyTTY' ttypids uid inode | ||
193 | Nothing -> return (Nothing,Nothing) | ||
194 | let rsc = t `mplus` fmap ( ("pid."<>) . Text.pack . show ) pid | ||
195 | return (rsc,pid) | ||
196 | |||
197 | getJabberUserForId muid = | ||
198 | maybe (return "nobody") | ||
199 | (\(uid,_) -> | ||
200 | handle (\(SomeException _) -> | ||
201 | return . (<> "uid.") . Text.pack . show $ uid) | ||
202 | $ do | ||
203 | user <- fmap userName $ getUserEntryForID uid | ||
204 | return (Text.pack user) | ||
205 | ) | ||
206 | muid | ||
207 | |||
208 | forClient :: PresenceState | ||
209 | -> ConnectionKey -> IO b -> (ClientState -> IO b) -> IO b | ||
210 | forClient state k fallback f = do | ||
211 | mclient <- atomically $ do | ||
212 | cs <- readTVar (clients state) | ||
213 | return $ Map.lookup k cs | ||
214 | maybe fallback f mclient | ||
215 | |||
216 | tellClientHisName :: PresenceState -> ConnectionKey -> IO Text | ||
217 | tellClientHisName state k = forClient state k fallback go | ||
218 | where | ||
219 | fallback = localJID "nobody" "fallback" | ||
220 | go client = localJID (clientUser client) (clientResource client) | ||
221 | |||
222 | toMapUnit :: Ord k => [k] -> Map k () | ||
223 | toMapUnit xs = Map.fromList $ map (,()) xs | ||
224 | |||
225 | resolveAllPeers :: [Text] -> IO (Map SockAddr ()) | ||
226 | resolveAllPeers hosts = fmap (toMapUnit . concat) $ Prelude.mapM (fmap (take 1) . resolvePeer) hosts | ||
227 | |||
228 | |||
229 | rosterGetStuff | ||
230 | :: (L.ByteString -> IO [L.ByteString]) | ||
231 | -> PresenceState -> ConnectionKey -> IO [Text] | ||
232 | rosterGetStuff what state k = forClient state k (return []) | ||
233 | $ \client -> do | ||
234 | jids <- configText what (clientUser client) | ||
235 | let hosts = map ((\(_,h,_)->h) . splitJID) jids | ||
236 | addrs <- resolveAllPeers hosts | ||
237 | peers <- atomically $ readTVar (associatedPeers state) | ||
238 | addrs <- return $ addrs `Map.difference` peers | ||
239 | sv <- atomically $ takeTMVar $ server state | ||
240 | -- Grok peers to associate with from the roster: | ||
241 | forM_ (Map.keys addrs) $ \addr -> do | ||
242 | putStrLn $ "new addr: "++show addr | ||
243 | addPeer sv addr | ||
244 | -- Update local set of associated peers | ||
245 | atomically $ do | ||
246 | writeTVar (associatedPeers state) (addrs `Map.union` peers) | ||
247 | putTMVar (server state) sv | ||
248 | return jids | ||
249 | |||
250 | rosterGetBuddies :: PresenceState -> ConnectionKey -> IO [Text] | ||
251 | rosterGetBuddies state k = rosterGetStuff ConfigFiles.getBuddies state k | ||
252 | |||
253 | rosterGetSolicited :: PresenceState -> ConnectionKey -> IO [Text] | ||
254 | rosterGetSolicited = rosterGetStuff ConfigFiles.getSolicited | ||
255 | |||
256 | rosterGetOthers :: PresenceState -> ConnectionKey -> IO [Text] | ||
257 | rosterGetOthers = rosterGetStuff ConfigFiles.getOthers | ||
258 | |||
259 | rosterGetSubscribers :: PresenceState -> ConnectionKey -> IO [Text] | ||
260 | rosterGetSubscribers = rosterGetStuff ConfigFiles.getSubscribers | ||
261 | |||
262 | data Conn = Conn { connChan :: TChan Stanza | ||
263 | , auxAddr :: SockAddr } | ||
264 | |||
265 | configText :: Functor f => | ||
266 | (L.ByteString -> f [L.ByteString]) -> Text -> f [Text] | ||
267 | configText what u = fmap (map lazyByteStringToText) | ||
268 | $ what (textToLazyByteString u) | ||
269 | |||
270 | getBuddies' :: Text -> IO [Text] | ||
271 | getBuddies' = configText ConfigFiles.getBuddies | ||
272 | getSolicited' :: Text -> IO [Text] | ||
273 | getSolicited' = configText ConfigFiles.getSolicited | ||
274 | |||
275 | sendProbesAndSolicitations :: PresenceState | ||
276 | -> ConnectionKey -> SockAddr -> TChan Stanza -> IO () | ||
277 | sendProbesAndSolicitations state k laddr chan = do | ||
278 | -- get all buddies & solicited matching k for all users | ||
279 | xs <- runTraversableT $ do | ||
280 | cbu <- lift $ atomically $ readTVar $ clientsByUser state | ||
281 | user <- liftT $ Map.keys cbu | ||
282 | (isbud,getter) <- liftT [(True ,getBuddies' ) | ||
283 | ,(False,getSolicited')] | ||
284 | bud <- liftMT $ getter user | ||
285 | let (u,h,r) = splitJID bud | ||
286 | addr <- liftMT $ nub `fmap` resolvePeer h | ||
287 | liftT $ guard (PeerKey addr == k) | ||
288 | -- Note: Earlier I was tempted to do all the IO | ||
289 | -- within the TraversableT monad. That apparently | ||
290 | -- is a bad idea. Perhaps due to laziness and an | ||
291 | -- unforced list? Instead, we will return a list | ||
292 | -- of (Bool,Text) for processing outside. | ||
293 | return (isbud,u,if isbud then "" else user) | ||
294 | -- XXX: The following O(n²) nub may be a little | ||
295 | -- too onerous. | ||
296 | forM_ (nub xs) $ \(isbud,u,user) -> do | ||
297 | let make = if isbud then presenceProbe | ||
298 | else presenceSolicitation | ||
299 | toh = peerKeyToText k | ||
300 | jid = unsplitJID (u,toh,Nothing) | ||
301 | me = addrToText laddr | ||
302 | from = if isbud then me -- probe from server | ||
303 | else -- solicitation from particular user | ||
304 | unsplitJID (Just user,me,Nothing) | ||
305 | stanza <- make from jid | ||
306 | -- send probes for buddies, solicitations for solicited. | ||
307 | putStrLn $ "probing "++show k++" for: " ++ show (isbud,jid) | ||
308 | atomically $ writeTChan chan stanza | ||
309 | -- reverse xs `seq` return () | ||
310 | |||
311 | newConn :: PresenceState -> ConnectionKey -> SockAddr -> TChan Stanza -> IO () | ||
312 | newConn state k addr outchan = do | ||
313 | atomically $ modifyTVar' (keyToChan state) | ||
314 | $ Map.insert k Conn { connChan = outchan | ||
315 | , auxAddr = addr } | ||
316 | when (isPeerKey k) | ||
317 | $ sendProbesAndSolicitations state k addr outchan | ||
318 | |||
319 | delclient :: (Alternative m, Monad m) => | ||
320 | ConnectionKey -> m LocalPresence -> m LocalPresence | ||
321 | delclient k mlp = do | ||
322 | lp <- mlp | ||
323 | let nc = Map.delete k $ networkClients lp | ||
324 | guard $ not (Map.null nc) | ||
325 | return $ lp { networkClients = nc } | ||
326 | |||
327 | eofConn :: PresenceState -> ConnectionKey -> IO () | ||
328 | eofConn state k = do | ||
329 | atomically $ modifyTVar' (keyToChan state) $ Map.delete k | ||
330 | case k of | ||
331 | ClientKey {} -> do | ||
332 | forClient state k (return ()) $ \client -> do | ||
333 | stanza <- makePresenceStanza "jabber:server" Nothing Offline | ||
334 | informClientPresence state k stanza | ||
335 | atomically $ do | ||
336 | modifyTVar' (clientsByUser state) | ||
337 | $ Map.alter (delclient k) (clientUser client) | ||
338 | PeerKey {} -> do | ||
339 | let h = peerKeyToText k | ||
340 | jids <- atomically $ do | ||
341 | rbp <- readTVar (remotesByPeer state) | ||
342 | return $ do | ||
343 | umap <- maybeToList $ Map.lookup k rbp | ||
344 | (u,rp) <- Map.toList umap | ||
345 | r <- Map.keys (resources rp) | ||
346 | return $ unsplitJID (Just u, h, Just r) | ||
347 | forM_ jids $ \jid -> do | ||
348 | stanza <- makePresenceStanza "jabber:client" (Just jid) Offline | ||
349 | informPeerPresence state k stanza | ||
350 | |||
351 | {- | ||
352 | rewriteJIDForClient1:: Text -> IO (Maybe ((Maybe Text,Text,Maybe Text),SockAddr)) | ||
353 | rewriteJIDForClient1 jid = do | ||
354 | let (n,h,r) = splitJID jid | ||
355 | maddr <- fmap listToMaybe $ resolvePeer h | ||
356 | flip (maybe $ return Nothing) maddr $ \addr -> do | ||
357 | h' <- peerKeyToResolvedName (PeerKey addr) | ||
358 | return $ Just ((n,h',r), addr) | ||
359 | -} | ||
360 | |||
361 | -- | for example: 2001-db8-85a3-8d3-1319-8a2e-370-7348.ipv6-literal.net | ||
362 | ip6literal :: Text -> Text | ||
363 | ip6literal addr = Text.map dash addr <> ".ipv6-literal.net" | ||
364 | where | ||
365 | dash ':' = '-' | ||
366 | dash x = x | ||
367 | |||
368 | -- | The given address is taken to be the local address for the socket this JID | ||
369 | -- came in on. The returned JID parts are suitable for unsplitJID to create a | ||
370 | -- valid JID for communicating to a client. The returned Bool is True when the | ||
371 | -- host part refers to this local host (i.e. it equals the given SockAddr). | ||
372 | -- If there are multiple results, it will prefer one which is a member of the | ||
373 | -- given list in the last argument. | ||
374 | rewriteJIDForClient :: SockAddr -> Text -> [Text] -> IO (Bool,(Maybe Text,Text,Maybe Text)) | ||
375 | rewriteJIDForClient laddr jid buds = do | ||
376 | let (n,h,r) = splitJID jid | ||
377 | maddr <- parseAddress (strip_brackets h) | ||
378 | flip (maybe $ return (False,(n,ip6literal h,r))) maddr $ \addr -> do | ||
379 | let mine = laddr `withPort` 0 == addr `withPort` 0 | ||
380 | h' <- if mine then textHostName | ||
381 | else peerKeyToResolvedName buds (PeerKey addr) | ||
382 | return (mine,(n,h',r)) | ||
383 | |||
384 | sameAddress :: SockAddr -> SockAddr -> Bool | ||
385 | sameAddress laddr addr = laddr `withPort` 0 == addr `withPort` 0 | ||
386 | |||
387 | peerKeyToResolvedName :: [Text] -> ConnectionKey -> IO Text | ||
388 | peerKeyToResolvedName buds k@(ClientKey { localAddress=addr }) = return "ErrorClIeNt1" | ||
389 | peerKeyToResolvedName buds pk = do | ||
390 | ns <- peerKeyToResolvedNames pk | ||
391 | let hs = map (\jid -> let (_,h,_)=splitJID jid in h) buds | ||
392 | ns' = sortBy (comparing $ not . flip elem hs) ns | ||
393 | return $ maybe (peerKeyToText pk) id (listToMaybe ns') | ||
394 | |||
395 | |||
396 | multiplyJIDForClient :: SockAddr -> Text -> IO (Bool,[(Maybe Text,Text,Maybe Text)]) | ||
397 | multiplyJIDForClient laddr jid = do | ||
398 | let (n,h,r) = splitJID jid | ||
399 | maddr <- parseAddress (strip_brackets h) | ||
400 | flip (maybe $ return (False,[(n,ip6literal h,r)])) maddr $ \addr -> do | ||
401 | let mine = sameAddress laddr addr | ||
402 | names <- if mine then fmap (:[]) textHostName | ||
403 | else peerKeyToResolvedNames (PeerKey addr) | ||
404 | return (mine,map (\h' -> (n,h',r)) names) | ||
405 | |||
406 | |||
407 | addrTextToKey :: Text -> IO (Maybe ConnectionKey) | ||
408 | addrTextToKey h = do | ||
409 | maddr <- parseAddress (strip_brackets h) | ||
410 | return (fmap PeerKey maddr) | ||
411 | |||
412 | guardPortStrippedAddress :: Text -> SockAddr -> IO (Maybe ()) | ||
413 | guardPortStrippedAddress h laddr = do | ||
414 | maddr <- fmap (fmap (`withPort` 0)) $ parseAddress (strip_brackets h) | ||
415 | let laddr' = laddr `withPort` 0 | ||
416 | return $ maddr >>= guard . (==laddr') | ||
417 | |||
418 | |||
419 | -- | Accepts a textual representation of a domainname | ||
420 | -- JID suitable for client connections, and returns the | ||
421 | -- coresponding ipv6 address JID suitable for peers paired | ||
422 | -- with a SockAddr with the address part of that JID in | ||
423 | -- binary form. If no suitable address could be resolved | ||
424 | -- for the given name, Nothing is returned. | ||
425 | rewriteJIDForPeer :: Text -> IO (Maybe (Text,SockAddr)) | ||
426 | rewriteJIDForPeer jid = do | ||
427 | let (n,h,r) = splitJID jid | ||
428 | maddr <- fmap listToMaybe $ resolvePeer h | ||
429 | return $ flip fmap maddr $ \addr -> | ||
430 | let h' = addrToText addr | ||
431 | to' = unsplitJID (n,h',r) | ||
432 | in (to',addr) | ||
433 | |||
434 | deliverToConsole :: PresenceState -> IO () -> Stanza -> IO () | ||
435 | deliverToConsole state fail msg = do | ||
436 | putStrLn $ "TODO: deliver to console" | ||
437 | did1 <- writeActiveTTY (consoleWriter state) msg | ||
438 | did2 <- writeAllPty (consoleWriter state) msg | ||
439 | if not (did1 || did2) then fail else return () | ||
440 | |||
441 | -- | deliver <message/> or error stanza | ||
442 | deliverMessage :: PresenceState | ||
443 | -> IO () | ||
444 | -> StanzaWrap (LockedChan Event) | ||
445 | -> IO () | ||
446 | deliverMessage state fail msg = | ||
447 | case stanzaOrigin msg of | ||
448 | NetworkOrigin senderk@(ClientKey {}) _ -> do | ||
449 | -- Case 1. Client -> Peer | ||
450 | mto <- do | ||
451 | flip (maybe $ return Nothing) (stanzaTo msg) $ \to -> do | ||
452 | rewriteJIDForPeer to | ||
453 | flip (maybe fail {- reverse lookup failure -}) | ||
454 | mto | ||
455 | $ \(to',addr) -> do | ||
456 | let k = PeerKey addr | ||
457 | chans <- atomically $ readTVar (keyToChan state) | ||
458 | flip (maybe fail) (Map.lookup k chans) $ \(Conn { connChan=chan | ||
459 | , auxAddr=laddr }) -> do | ||
460 | (n,r) <- forClient state senderk (return (Nothing,Nothing)) | ||
461 | $ \c -> return (Just (clientUser c), Just (clientResource c)) | ||
462 | -- original 'from' address is discarded. | ||
463 | let from' = unsplitJID (n,addrToText laddr,r) | ||
464 | -- dup <- atomically $ cloneStanza (msg { stanzaTo=Just to', stanzaFrom=Just from' }) | ||
465 | let dup = (msg { stanzaTo=Just to', stanzaFrom=Just from' }) | ||
466 | sendModifiedStanzaToPeer dup chan | ||
467 | NetworkOrigin senderk@(PeerKey {}) _ -> do | ||
468 | key_to_chan <- atomically $ readTVar (keyToChan state) | ||
469 | flip (maybe fail) (Map.lookup senderk key_to_chan) | ||
470 | $ \(Conn { connChan=sender_chan | ||
471 | , auxAddr=laddr }) -> do | ||
472 | flip (maybe fail) (stanzaTo msg) $ \to -> do | ||
473 | (mine,(n,h,r)) <- rewriteJIDForClient laddr to [] | ||
474 | if not mine then fail else do | ||
475 | let to' = unsplitJID (n,h,r) | ||
476 | cmap <- atomically . readTVar $ clientsByUser state | ||
477 | (from',chans,ks) <- do | ||
478 | flip (maybe $ return (Nothing,[],[])) n $ \n -> do | ||
479 | buds <- configText ConfigFiles.getBuddies n | ||
480 | from' <- do | ||
481 | flip (maybe $ return Nothing) (stanzaFrom msg) $ \from -> do | ||
482 | (_,trip) <- rewriteJIDForClient laddr from buds | ||
483 | return . Just $ unsplitJID trip | ||
484 | let nope = return (from',[],[]) | ||
485 | flip (maybe nope) (Map.lookup n cmap) $ \presence_container -> do | ||
486 | let ks = Map.keys (networkClients presence_container) | ||
487 | chans = mapMaybe (flip Map.lookup key_to_chan) ks | ||
488 | return (from',chans,ks) | ||
489 | putStrLn $ "chan count: " ++ show (length chans) | ||
490 | let msg' = msg { stanzaTo=Just to' | ||
491 | , stanzaFrom=from' } | ||
492 | if null chans then deliverToConsole state fail msg' else do | ||
493 | forM_ chans $ \Conn { connChan=chan} -> do | ||
494 | putStrLn $ "sending "++show (stanzaId msg)++" to clients "++show ks | ||
495 | -- TODO: Cloning isn't really neccessary unless there are multiple | ||
496 | -- destinations and we should probably transition to minimal cloning, | ||
497 | -- or else we should distinguish between announcable stanzas and | ||
498 | -- consumable stanzas and announcables use write-only broadcast | ||
499 | -- channels that must be cloned in order to be consumed. | ||
500 | -- For now, we are doing redundant cloning. | ||
501 | dup <- cloneStanza msg' | ||
502 | sendModifiedStanzaToClient dup | ||
503 | chan | ||
504 | |||
505 | |||
506 | setClientFlag :: PresenceState -> ConnectionKey -> Int8 -> IO () | ||
507 | setClientFlag state k flag = | ||
508 | atomically $ do | ||
509 | cmap <- readTVar (clients state) | ||
510 | flip (maybe $ return ()) (Map.lookup k cmap) $ \client -> do | ||
511 | setClientFlag0 client flag | ||
512 | |||
513 | setClientFlag0 :: ClientState -> Int8 -> STM () | ||
514 | setClientFlag0 client flag = | ||
515 | modifyTVar' (clientFlags client) (\flgs -> flgs .|. flag) | ||
516 | |||
517 | informSentRoster :: PresenceState -> ConnectionKey -> IO () | ||
518 | informSentRoster state k = do | ||
519 | setClientFlag state k cf_interested | ||
520 | |||
521 | |||
522 | subscribedPeers :: Text -> IO [SockAddr] | ||
523 | subscribedPeers user = do | ||
524 | jids <- configText ConfigFiles.getSubscribers user | ||
525 | let hosts = map ((\(_,h,_)->h) . splitJID) jids | ||
526 | fmap Map.keys $ resolveAllPeers hosts | ||
527 | |||
528 | -- | this JID is suitable for peers, not clients. | ||
529 | clientJID :: Conn -> ClientState -> Text | ||
530 | clientJID con client = unsplitJID ( Just $ clientUser client | ||
531 | , addrToText $ auxAddr con | ||
532 | , Just $ clientResource client) | ||
533 | |||
534 | -- | Send presence notification to subscribed peers. | ||
535 | -- Note that a full JID from address will be added to the | ||
536 | -- stanza if it is not present. | ||
537 | informClientPresence :: PresenceState | ||
538 | -> ConnectionKey -> StanzaWrap (LockedChan Event) -> IO () | ||
539 | informClientPresence state k stanza = do | ||
540 | forClient state k (return ()) $ \client -> do | ||
541 | informClientPresence0 state (Just k) client stanza | ||
542 | |||
543 | informClientPresence0 :: PresenceState | ||
544 | -> Maybe ConnectionKey | ||
545 | -> ClientState | ||
546 | -> StanzaWrap (LockedChan Event) | ||
547 | -> IO () | ||
548 | informClientPresence0 state mbk client stanza = do | ||
549 | dup <- cloneStanza stanza | ||
550 | atomically $ writeTVar (clientStatus client) $ Just dup | ||
551 | is_avail <- atomically $ clientIsAvailable client | ||
552 | when (not is_avail) $ do | ||
553 | atomically $ setClientFlag0 client cf_available | ||
554 | maybe (return ()) (sendCachedPresence state) mbk | ||
555 | addrs <- subscribedPeers (clientUser client) | ||
556 | ktc <- atomically $ readTVar (keyToChan state) | ||
557 | let connected = mapMaybe (flip Map.lookup ktc . PeerKey) addrs | ||
558 | forM_ connected $ \con -> do | ||
559 | let from' = clientJID con client | ||
560 | mto <- runTraversableT $ do | ||
561 | to <- liftT $ stanzaTo stanza | ||
562 | (to',_) <- liftMT $ rewriteJIDForPeer to | ||
563 | return to' | ||
564 | dup <- cloneStanza stanza | ||
565 | sendModifiedStanzaToPeer dup { stanzaFrom = Just from' | ||
566 | , stanzaTo = mto } | ||
567 | (connChan con) | ||
568 | |||
569 | informPeerPresence :: PresenceState | ||
570 | -> ConnectionKey | ||
571 | -> StanzaWrap (LockedChan Event) | ||
572 | -> IO () | ||
573 | informPeerPresence state k stanza = do | ||
574 | -- Presence must indicate full JID with resource... | ||
575 | putStrLn $ "xmppInformPeerPresence checking from address..." | ||
576 | flip (maybe $ return ()) (stanzaFrom stanza) $ \from -> do | ||
577 | let (muser,h,mresource) = splitJID from | ||
578 | putStrLn $ "xmppInformPeerPresence from = " ++ show from | ||
579 | -- flip (maybe $ return ()) mresource $ \resource -> do | ||
580 | flip (maybe $ return ()) muser $ \user -> do | ||
581 | |||
582 | clients <- atomically $ do | ||
583 | |||
584 | -- Update remotesByPeer... | ||
585 | rbp <- readTVar (remotesByPeer state) | ||
586 | let umap = maybe Map.empty id $ Map.lookup k rbp | ||
587 | rp = case (presenceShow $ stanzaType stanza) of | ||
588 | Offline -> | ||
589 | maybe Map.empty | ||
590 | (\resource -> | ||
591 | maybe (Map.empty) | ||
592 | (Map.delete resource . resources) | ||
593 | $ Map.lookup user umap) | ||
594 | mresource | ||
595 | |||
596 | _ ->maybe Map.empty | ||
597 | (\resource -> | ||
598 | maybe (Map.singleton resource stanza) | ||
599 | (Map.insert resource stanza . resources ) | ||
600 | $ Map.lookup user umap) | ||
601 | mresource | ||
602 | umap' = Map.insert user (RemotePresence rp) umap | ||
603 | |||
604 | flip (maybe $ return []) (case presenceShow $ stanzaType stanza of | ||
605 | Offline -> Just () | ||
606 | _ -> mresource >> Just ()) | ||
607 | $ \_ -> do | ||
608 | writeTVar (remotesByPeer state) $ Map.insert k umap' rbp | ||
609 | -- TODO: Store or delete the stanza (remotesByPeer) | ||
610 | |||
611 | -- all clients, we'll filter available/authorized later | ||
612 | |||
613 | ktc <- readTVar (keyToChan state) | ||
614 | runTraversableT $ do | ||
615 | (ck,client) <- liftMT $ fmap Map.toList $ readTVar (clients state) | ||
616 | con <- liftMaybe $ Map.lookup ck ktc | ||
617 | return (ck,con,client) | ||
618 | putStrLn $ "xmppInformPeerPresence (length clients="++show (length clients)++")" | ||
619 | forM_ clients $ \(ck,con,client) -> do | ||
620 | -- (TODO: appropriately authorized clients only.) | ||
621 | -- For now, all "available" clients (available = sent initial presence) | ||
622 | is_avail <- atomically $ clientIsAvailable client | ||
623 | when is_avail $ do | ||
624 | putStrLn $ "reversing for client: " ++ show from | ||
625 | froms <- do -- flip (maybe $ return [from]) k . const $ do | ||
626 | let ClientKey laddr = ck | ||
627 | (_,trip) <- multiplyJIDForClient laddr from | ||
628 | return (map unsplitJID trip) | ||
629 | |||
630 | putStrLn $ "sending to client: " ++ show (stanzaType stanza,froms) | ||
631 | forM_ froms $ \from' -> do | ||
632 | dup <- cloneStanza stanza | ||
633 | sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) | ||
634 | (connChan con) | ||
635 | |||
636 | answerProbe :: PresenceState | ||
637 | -> Maybe Text -> ConnectionKey -> TChan Stanza -> IO () | ||
638 | answerProbe state mto k chan = do | ||
639 | -- putStrLn $ "answerProbe! " ++ show (stanzaType stanza) | ||
640 | ktc <- atomically $ readTVar (keyToChan state) | ||
641 | muser <- runTraversableT $ do | ||
642 | to <- liftT $ mto | ||
643 | conn <- liftT $ Map.lookup k ktc | ||
644 | let (mu,h,_) = splitJID to -- TODO: currently resource-id is ignored on presence | ||
645 | -- probes. Is this correct? Check the spec. | ||
646 | liftMT $ guardPortStrippedAddress h (auxAddr conn) | ||
647 | u <- liftT mu | ||
648 | let ch = addrToText (auxAddr conn) | ||
649 | return (u,conn,ch) | ||
650 | |||
651 | flip (maybe $ return ()) muser $ \(u,conn,ch) -> do | ||
652 | |||
653 | resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers u | ||
654 | let gaddrs = groupBy (\a b -> snd a == snd b) (sort resolved_subs) | ||
655 | whitelist = do | ||
656 | xs <- gaddrs | ||
657 | x <- take 1 xs | ||
658 | guard $ snd x==k | ||
659 | mapMaybe fst xs | ||
660 | |||
661 | -- -- only subscribed peers should get probe replies | ||
662 | -- addrs <- subscribedPeers u | ||
663 | |||
664 | -- TODO: notify remote peer that they are unsubscribed? | ||
665 | -- reply <- makeInformSubscription "jabber:server" to from False | ||
666 | when (not $ null whitelist) $ do | ||
667 | |||
668 | replies <- runTraversableT $ do | ||
669 | cbu <- lift . atomically $ readTVar (clientsByUser state) | ||
670 | let lpres = maybeToList $ Map.lookup u cbu | ||
671 | cw <- lift . atomically $ readTVar (cwClients $ consoleWriter state) | ||
672 | clientState <- liftT $ (lpres >>= Map.elems . networkClients) | ||
673 | ++ Map.elems cw | ||
674 | stanza <- liftIOMaybe $ atomically (readTVar (clientStatus clientState)) | ||
675 | stanza <- lift $ cloneStanza stanza | ||
676 | let jid = unsplitJID (Just $ clientUser clientState | ||
677 | , ch | ||
678 | ,Just $ clientResource clientState) | ||
679 | return stanza { stanzaFrom = Just jid | ||
680 | , stanzaType = (stanzaType stanza) | ||
681 | { presenceWhiteList = whitelist } | ||
682 | } | ||
683 | |||
684 | forM_ replies $ \reply -> do | ||
685 | sendModifiedStanzaToPeer reply chan | ||
686 | |||
687 | -- if no presence, send offline message | ||
688 | when (null replies) $ do | ||
689 | let jid = unsplitJID (Just u,ch,Nothing) | ||
690 | pstanza <- makePresenceStanza "jabber:server" (Just jid) Offline | ||
691 | atomically $ writeTChan (connChan conn) pstanza | ||
692 | |||
693 | sendCachedPresence :: PresenceState -> ConnectionKey -> IO () | ||
694 | sendCachedPresence state k = do | ||
695 | forClient state k (return ()) $ \client -> do | ||
696 | rbp <- atomically $ readTVar (remotesByPeer state) | ||
697 | jids <- configText ConfigFiles.getBuddies (clientUser client) | ||
698 | let hosts = map ((\(_,h,_)->h) . splitJID) jids | ||
699 | addrs <- resolveAllPeers hosts | ||
700 | let onlines = rbp `Map.intersection` Map.mapKeys PeerKey addrs | ||
701 | ClientKey laddr = k | ||
702 | mcon <- atomically $ do ktc <- readTVar (keyToChan state) | ||
703 | return $ Map.lookup k ktc | ||
704 | flip (maybe $ return ()) mcon $ \con -> do | ||
705 | -- me <- textHostName | ||
706 | forM_ (Map.toList onlines) $ \(pk, umap) -> do | ||
707 | forM_ (Map.toList umap) $ \(user,rp) -> do | ||
708 | let h = peerKeyToText pk | ||
709 | forM_ (Map.toList $ resources rp) $ \(resource,stanza) -> do | ||
710 | let jid = unsplitJID (Just user,h,Just resource) | ||
711 | (mine,js) <- multiplyJIDForClient laddr jid | ||
712 | forM_ js $ \jid -> do | ||
713 | let from' = unsplitJID jid | ||
714 | dup <- cloneStanza stanza | ||
715 | sendModifiedStanzaToClient (dup { stanzaFrom=Just from' }) | ||
716 | (connChan con) | ||
717 | |||
718 | pending <- configText ConfigFiles.getPending (clientUser client) | ||
719 | hostname <- textHostName | ||
720 | forM_ pending $ \pending_jid -> do | ||
721 | let cjid = unsplitJID ( Just $ clientUser client | ||
722 | , hostname | ||
723 | , Nothing ) | ||
724 | ask <- presenceSolicitation pending_jid cjid | ||
725 | sendModifiedStanzaToClient ask (connChan con) | ||
726 | |||
727 | -- Note: relying on self peer connection to send | ||
728 | -- send local buddies. | ||
729 | return () | ||
730 | |||
731 | addToRosterFile :: (MonadPlus t, Traversable t) => | ||
732 | (L.ByteString -> (L.ByteString -> IO (t L.ByteString)) | ||
733 | -> Maybe L.ByteString | ||
734 | -> t1) | ||
735 | -> Text -> Text -> [SockAddr] -> t1 | ||
736 | addToRosterFile doit whose to addrs = | ||
737 | modifyRosterFile doit whose to addrs True | ||
738 | |||
739 | removeFromRosterFile :: (MonadPlus t, Traversable t) => | ||
740 | (L.ByteString -> (L.ByteString -> IO (t L.ByteString)) | ||
741 | -> Maybe L.ByteString | ||
742 | -> t1) | ||
743 | -> Text -> Text -> [SockAddr] -> t1 | ||
744 | removeFromRosterFile doit whose to addrs = | ||
745 | modifyRosterFile doit whose to addrs False | ||
746 | |||
747 | modifyRosterFile :: (Traversable t, MonadPlus t) => | ||
748 | (L.ByteString -> (L.ByteString -> IO (t L.ByteString)) | ||
749 | -> Maybe L.ByteString | ||
750 | -> t1) | ||
751 | -> Text -> Text -> [SockAddr] -> Bool -> t1 | ||
752 | modifyRosterFile doit whose to addrs bAdd = do | ||
753 | let (mu,_,_) = splitJID to | ||
754 | cmp jid = runTraversableT $ do | ||
755 | let (msu,stored_h,mr) = splitJID (lazyByteStringToText jid) | ||
756 | -- Delete from file if a resource is present in file | ||
757 | (\f -> maybe f (const mzero) mr) $ do | ||
758 | -- Delete from file if no user is present in file | ||
759 | flip (maybe mzero) msu $ \stored_u -> do | ||
760 | -- do not delete anything if no user was specified | ||
761 | flip (maybe $ return jid) mu $ \u -> do | ||
762 | -- do not delete if stored user is same as specified | ||
763 | if stored_u /= u then return jid else do | ||
764 | stored_addrs <- lift $ resolvePeer stored_h | ||
765 | -- do not delete if failed to resolve | ||
766 | if null stored_addrs then return jid else do | ||
767 | -- delete if specified address matches stored | ||
768 | if null (stored_addrs \\ addrs) then mzero else do | ||
769 | -- keep | ||
770 | return jid | ||
771 | doit (textToLazyByteString whose) | ||
772 | cmp | ||
773 | (guard bAdd >> Just (textToLazyByteString to)) | ||
774 | |||
775 | clientSubscriptionRequest :: PresenceState -> IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () | ||
776 | clientSubscriptionRequest state fail k stanza chan = do | ||
777 | forClient state k fail $ \client -> do | ||
778 | flip (maybe fail) (stanzaTo stanza) $ \to -> do | ||
779 | putStrLn $ "Forwarding solictation to peer" | ||
780 | let (mu,h,_) = splitJID to | ||
781 | to <- return $ unsplitJID (mu,h,Nothing) -- delete resource | ||
782 | flip (maybe fail) mu $ \u -> do | ||
783 | addrs <- resolvePeer h | ||
784 | if null addrs then fail else do | ||
785 | -- add to-address to from's solicited | ||
786 | addToRosterFile ConfigFiles.modifySolicited (clientUser client) to addrs | ||
787 | removeFromRosterFile ConfigFiles.modifyBuddies (clientUser client) to addrs | ||
788 | resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers (clientUser client) | ||
789 | let is_subscribed = not . null $ intersect (map ((mu,).PeerKey) addrs) resolved_subs | ||
790 | -- subscribers: "from" | ||
791 | -- buddies: "to" | ||
792 | |||
793 | (ktc,ap) <- atomically $ | ||
794 | liftM2 (,) (readTVar $ keyToChan state) | ||
795 | (readTVar $ associatedPeers state) | ||
796 | |||
797 | case stanzaType stanza of | ||
798 | PresenceRequestSubscription True -> do | ||
799 | hostname <- textHostName | ||
800 | let cjid = unsplitJID (Just $ clientUser client, hostname,Nothing) | ||
801 | chans <- clientCons state ktc (clientUser client) | ||
802 | forM_ chans $ \( Conn { connChan=chan }, client ) -> do | ||
803 | -- roster update ask="subscribe" | ||
804 | update <- makeRosterUpdate cjid to | ||
805 | [ ("ask","subscribe") | ||
806 | , if is_subscribed then ("subscription","from") | ||
807 | else ("subscription","none") | ||
808 | ] | ||
809 | sendModifiedStanzaToClient update chan | ||
810 | _ -> return () | ||
811 | |||
812 | let dsts = Map.fromList $ map ((,()) . PeerKey) addrs | ||
813 | cdsts = ktc `Map.intersection` dsts | ||
814 | forM_ (Map.toList cdsts) $ \(pk,con) -> do | ||
815 | -- if already connected, send solicitation ... | ||
816 | -- let from = clientJID con client | ||
817 | let from = unsplitJID ( Just $ clientUser client | ||
818 | , addrToText $ auxAddr con | ||
819 | , Nothing ) | ||
820 | mb <- rewriteJIDForPeer to | ||
821 | flip (maybe $ return ()) mb $ \(to',addr) -> do | ||
822 | dup <- cloneStanza stanza | ||
823 | sendModifiedStanzaToPeer (dup { stanzaTo = Just to' | ||
824 | , stanzaFrom = Just from }) | ||
825 | (connChan con) | ||
826 | let addrm = Map.fromList (map (,()) addrs) | ||
827 | when (not . Map.null $ addrm Map.\\ ap) $ do | ||
828 | -- Add peer if we are not already associated ... | ||
829 | sv <- atomically $ takeTMVar $ server state | ||
830 | addPeer sv (head addrs) | ||
831 | atomically $ putTMVar (server state) sv | ||
832 | |||
833 | |||
834 | resolvedFromRoster | ||
835 | :: (L.ByteString -> IO [L.ByteString]) | ||
836 | -> UserName -> IO [(Maybe UserName, ConnectionKey)] | ||
837 | resolvedFromRoster doit u = do | ||
838 | subs <- configText doit u | ||
839 | runTraversableT $ do | ||
840 | (mu,h,_) <- liftT $ splitJID `fmap` subs | ||
841 | addr <- liftMT $ fmap nub $ resolvePeer h | ||
842 | return (mu,PeerKey addr) | ||
843 | |||
844 | clientCons :: PresenceState | ||
845 | -> Map ConnectionKey t -> Text -> IO [(t, ClientState)] | ||
846 | clientCons state ktc u = do | ||
847 | mlp <- atomically $ do | ||
848 | cmap <- readTVar $ clientsByUser state | ||
849 | return $ Map.lookup u cmap | ||
850 | let ks = do lp <- maybeToList mlp | ||
851 | Map.toList (networkClients lp) | ||
852 | doit (k,client) = do | ||
853 | con <- Map.lookup k ktc | ||
854 | return (con,client) | ||
855 | return $ mapMaybe doit ks | ||
856 | |||
857 | peerSubscriptionRequest :: PresenceState -> IO () -> ConnectionKey -> Stanza -> TChan Stanza -> IO () | ||
858 | peerSubscriptionRequest state fail k stanza chan = do | ||
859 | putStrLn $ "Handling pending subscription from remote" | ||
860 | flip (maybe fail) (stanzaFrom stanza) $ \from -> do | ||
861 | flip (maybe fail) (stanzaTo stanza) $ \to -> do | ||
862 | let (mto_u,h,_) = splitJID to | ||
863 | (mfrom_u,from_h,_) = splitJID from | ||
864 | to <- return $ unsplitJID (mto_u,h,Nothing) -- delete resource | ||
865 | from <- return $ unsplitJID (mfrom_u,from_h,Nothing) -- delete resource | ||
866 | ktc <- atomically . readTVar $ keyToChan state | ||
867 | flip (maybe fail) (Map.lookup k ktc) | ||
868 | $ \Conn { auxAddr=laddr } -> do | ||
869 | (mine,totup) <- rewriteJIDForClient laddr to [] | ||
870 | if not mine then fail else do | ||
871 | (_,fromtup) <- rewriteJIDForClient laddr from [] | ||
872 | flip (maybe fail) mto_u $ \u -> do | ||
873 | flip (maybe fail) mfrom_u $ \from_u -> do | ||
874 | resolved_subs <- resolvedFromRoster ConfigFiles.getSubscribers u | ||
875 | let already_subscribed = elem (mfrom_u,k) resolved_subs | ||
876 | is_wanted = case stanzaType stanza of | ||
877 | PresenceRequestSubscription b -> b | ||
878 | _ -> False -- Shouldn't happen. | ||
879 | -- Section 8 says (for presence of type "subscribe", the server MUST | ||
880 | -- adhere to the rules defined under Section 3 and summarized under | ||
881 | -- see Appendix A. (pariticularly Appendex A.3.1) | ||
882 | if already_subscribed == is_wanted | ||
883 | then do | ||
884 | -- contact ∈ subscribers --> SHOULD NOT, already handled | ||
885 | -- already subscribed, reply and quit | ||
886 | -- (note: swapping to and from for reply) | ||
887 | reply <- makeInformSubscription "jabber:server" to from is_wanted | ||
888 | sendModifiedStanzaToPeer reply chan | ||
889 | answerProbe state (Just to) k chan | ||
890 | else do | ||
891 | |||
892 | -- TODO: if peer-connection is to self, then auto-approve local user. | ||
893 | |||
894 | -- add from-address to to's pending | ||
895 | addrs <- resolvePeer from_h | ||
896 | |||
897 | -- Catch exception in case the user does not exist | ||
898 | if null addrs then fail else do | ||
899 | |||
900 | let from' = unsplitJID fromtup | ||
901 | |||
902 | already_pending <- | ||
903 | if is_wanted then | ||
904 | addToRosterFile ConfigFiles.modifyPending u from' addrs | ||
905 | else do | ||
906 | removeFromRosterFile ConfigFiles.modifySubscribers u from' addrs | ||
907 | reply <- makeInformSubscription "jabber:server" to from is_wanted | ||
908 | sendModifiedStanzaToPeer reply chan | ||
909 | return False | ||
910 | |||
911 | -- contact ∉ subscribers & contact ∈ pending --> SHOULD NOT | ||
912 | when (not already_pending) $ do | ||
913 | -- contact ∉ subscribers & contact ∉ pending --> MUST | ||
914 | |||
915 | chans <- clientCons state ktc u | ||
916 | forM_ chans $ \( Conn { connChan=chan }, client ) -> do | ||
917 | -- send to clients | ||
918 | -- TODO: interested/available clients only? | ||
919 | dup <- cloneStanza stanza | ||
920 | sendModifiedStanzaToClient dup { stanzaFrom = Just $ from' | ||
921 | , stanzaTo = Just $ unsplitJID totup } | ||
922 | chan | ||
923 | |||
924 | |||
925 | clientInformSubscription :: PresenceState | ||
926 | -> IO () | ||
927 | -> ConnectionKey | ||
928 | -> StanzaWrap (LockedChan Event) | ||
929 | -> IO () | ||
930 | clientInformSubscription state fail k stanza = do | ||
931 | forClient state k fail $ \client -> do | ||
932 | flip (maybe fail) (stanzaTo stanza) $ \to -> do | ||
933 | putStrLn $ "clientInformSubscription" | ||
934 | let (mu,h,mr) = splitJID to | ||
935 | addrs <- resolvePeer h | ||
936 | -- remove from pending | ||
937 | buds <- resolvedFromRoster ConfigFiles.getBuddies (clientUser client) | ||
938 | let is_buddy = not . null $ map ((mu,) . PeerKey) addrs `intersect` buds | ||
939 | removeFromRosterFile ConfigFiles.modifyPending (clientUser client) to addrs | ||
940 | let (relationship,addf,remf) = | ||
941 | case stanzaType stanza of | ||
942 | PresenceInformSubscription True -> | ||
943 | ( ("subscription", if is_buddy then "both" | ||
944 | else "from" ) | ||
945 | , ConfigFiles.modifySubscribers | ||
946 | , ConfigFiles.modifyOthers ) | ||
947 | _ -> ( ("subscription", if is_buddy then "to" | ||
948 | else "none" ) | ||
949 | , ConfigFiles.modifyOthers | ||
950 | , ConfigFiles.modifySubscribers ) | ||
951 | addToRosterFile addf (clientUser client) to addrs | ||
952 | removeFromRosterFile remf (clientUser client) to addrs | ||
953 | |||
954 | do | ||
955 | cbu <- atomically $ readTVar (clientsByUser state) | ||
956 | putStrLn $ "cbu = " ++ show (fmap (fmap clientPid . networkClients) cbu) | ||
957 | |||
958 | -- send roster update to clients | ||
959 | (clients,ktc) <- atomically $ do | ||
960 | cbu <- readTVar (clientsByUser state) | ||
961 | let mlp = Map.lookup (clientUser client) cbu | ||
962 | let cs = maybe [] (Map.toList . networkClients) mlp | ||
963 | ktc <- readTVar (keyToChan state) | ||
964 | return (cs,ktc) | ||
965 | forM_ clients $ \(ck, client) -> do | ||
966 | is_intereseted <- atomically $ clientIsInterested client | ||
967 | putStrLn $ "clientIsInterested: "++show is_intereseted | ||
968 | is_intereseted <- atomically $ clientIsInterested client | ||
969 | when is_intereseted $ do | ||
970 | flip (maybe $ return ()) (Map.lookup ck ktc) $ \con -> do | ||
971 | hostname <- textHostName | ||
972 | -- TODO: Should cjid include the resource? | ||
973 | let cjid = unsplitJID (mu, hostname, Nothing) | ||
974 | update <- makeRosterUpdate cjid to [relationship] | ||
975 | sendModifiedStanzaToClient update (connChan con) | ||
976 | |||
977 | -- notify peer | ||
978 | let dsts = Map.fromList $ map ((,()) . PeerKey) addrs | ||
979 | cdsts = ktc `Map.intersection` dsts | ||
980 | forM_ (Map.toList cdsts) $ \(pk,con) -> do | ||
981 | let from = clientJID con client | ||
982 | to' = unsplitJID (mu, peerKeyToText pk, Nothing) | ||
983 | dup <- cloneStanza stanza | ||
984 | sendModifiedStanzaToPeer (dup { stanzaTo = Just $ to' | ||
985 | , stanzaFrom = Just from }) | ||
986 | (connChan con) | ||
987 | answerProbe state (Just from) pk (connChan con) | ||
988 | |||
989 | peerInformSubscription :: PresenceState | ||
990 | -> IO () | ||
991 | -> ConnectionKey | ||
992 | -> StanzaWrap (LockedChan Event) | ||
993 | -> IO () | ||
994 | peerInformSubscription state fail k stanza = do | ||
995 | putStrLn $ "TODO: peerInformSubscription" | ||
996 | -- remove from solicited | ||
997 | flip (maybe fail) (stanzaFrom stanza) $ \from -> do | ||
998 | ktc <- atomically $ readTVar (keyToChan state) | ||
999 | flip (maybe fail) (Map.lookup k ktc) | ||
1000 | $ \(Conn { connChan=sender_chan | ||
1001 | , auxAddr=laddr }) -> do | ||
1002 | (_,(from_u,from_h,_)) <- rewriteJIDForClient laddr from [] | ||
1003 | let from'' = unsplitJID (from_u,from_h,Nothing) | ||
1004 | muser = do | ||
1005 | to <- stanzaTo stanza | ||
1006 | let (mu,to_h,to_r) = splitJID to | ||
1007 | mu | ||
1008 | -- TODO muser = Nothing when wanted=False | ||
1009 | -- should probably mean unsubscribed for all users. | ||
1010 | -- This would allow us to answer anonymous probes with 'unsubscribed'. | ||
1011 | flip (maybe fail) muser $ \user -> do | ||
1012 | addrs <- resolvePeer from_h | ||
1013 | was_solicited <- removeFromRosterFile ConfigFiles.modifySolicited user from'' addrs | ||
1014 | subs <- resolvedFromRoster ConfigFiles.getSubscribers user | ||
1015 | let is_sub = not . null $ map ((from_u,) . PeerKey) addrs `intersect` subs | ||
1016 | let (relationship,addf,remf) = | ||
1017 | case stanzaType stanza of | ||
1018 | PresenceInformSubscription True -> | ||
1019 | ( ("subscription", if is_sub then "both" | ||
1020 | else "to" ) | ||
1021 | , ConfigFiles.modifyBuddies | ||
1022 | , ConfigFiles.modifyOthers ) | ||
1023 | _ -> ( ("subscription", if is_sub then "from" | ||
1024 | else "none") | ||
1025 | , ConfigFiles.modifyOthers | ||
1026 | , ConfigFiles.modifyBuddies ) | ||
1027 | addToRosterFile addf user from'' addrs | ||
1028 | removeFromRosterFile remf user from'' addrs | ||
1029 | |||
1030 | hostname <- textHostName | ||
1031 | let to' = unsplitJID (Just user, hostname, Nothing) | ||
1032 | chans <- clientCons state ktc user | ||
1033 | forM_ chans $ \(Conn { connChan=chan }, client) -> do | ||
1034 | update <- makeRosterUpdate to' from'' [relationship] | ||
1035 | is_intereseted <- atomically $ clientIsInterested client | ||
1036 | when is_intereseted $ do | ||
1037 | sendModifiedStanzaToClient update chan | ||
1038 | -- TODO: interested/availabe clients only? | ||
1039 | dup <- cloneStanza stanza | ||
1040 | sendModifiedStanzaToClient dup { stanzaFrom = Just $ from'' | ||
1041 | , stanzaTo = Just to' } | ||
1042 | chan | ||
1043 | |||
1044 | main :: IO () | ||
1045 | main = runResourceT $ do | ||
1046 | args <- liftIO getArgs | ||
1047 | let verbosity = getSum $ flip foldMap args $ \case | ||
1048 | ('-':xs) -> Sum $ length (filter (=='-') xs) | ||
1049 | _ -> mempty | ||
1050 | cw <- liftIO newConsoleWriter | ||
1051 | state <- liftIO . atomically $ do | ||
1052 | clients <- newTVar Map.empty | ||
1053 | clientsByUser <- newTVar Map.empty | ||
1054 | remotesByPeer <- newTVar Map.empty | ||
1055 | associatedPeers <- newTVar Map.empty | ||
1056 | xmpp <- newEmptyTMVar | ||
1057 | keyToChan <- newTVar Map.empty | ||
1058 | return PresenceState | ||
1059 | { clients = clients | ||
1060 | , clientsByUser = clientsByUser | ||
1061 | , remotesByPeer = remotesByPeer | ||
1062 | , associatedPeers = associatedPeers | ||
1063 | , keyToChan = keyToChan | ||
1064 | , server = xmpp | ||
1065 | , consoleWriter = cw | ||
1066 | } | ||
1067 | sv <- xmppServer | ||
1068 | XMPPServerParameters | ||
1069 | { xmppChooseResourceName = chooseResourceName state | ||
1070 | , xmppTellClientHisName = tellClientHisName state | ||
1071 | , xmppTellMyNameToClient = textHostName | ||
1072 | , xmppTellMyNameToPeer = \addr -> return $ addrToText addr | ||
1073 | , xmppTellPeerHisName = return . peerKeyToText | ||
1074 | , xmppTellClientNameOfPeer = flip peerKeyToResolvedName | ||
1075 | , xmppNewConnection = newConn state | ||
1076 | , xmppEOF = eofConn state | ||
1077 | , xmppRosterBuddies = rosterGetBuddies state | ||
1078 | , xmppRosterSubscribers = rosterGetSubscribers state | ||
1079 | , xmppRosterSolicited = rosterGetSolicited state | ||
1080 | , xmppRosterOthers = rosterGetOthers state | ||
1081 | , xmppSubscribeToRoster = informSentRoster state | ||
1082 | , xmppDeliverMessage = deliverMessage state | ||
1083 | , xmppInformClientPresence = informClientPresence state | ||
1084 | , xmppInformPeerPresence = informPeerPresence state | ||
1085 | , xmppAnswerProbe = \k stanza chan -> answerProbe state (stanzaTo stanza) k chan | ||
1086 | , xmppClientSubscriptionRequest = clientSubscriptionRequest state | ||
1087 | , xmppPeerSubscriptionRequest = peerSubscriptionRequest state | ||
1088 | , xmppClientInformSubscription = clientInformSubscription state | ||
1089 | , xmppPeerInformSubscription = peerInformSubscription state | ||
1090 | , xmppVerbosity = return verbosity | ||
1091 | } | ||
1092 | liftIO $ do | ||
1093 | atomically $ putTMVar (server state) sv | ||
1094 | |||
1095 | quitVar <- newEmptyTMVarIO | ||
1096 | installHandler sigTERM (CatchOnce (atomically $ putTMVar quitVar True)) Nothing | ||
1097 | installHandler sigINT (CatchOnce (atomically $ putTMVar quitVar True)) Nothing | ||
1098 | |||
1099 | forkIO $ do | ||
1100 | let console = cwPresenceChan $ consoleWriter state | ||
1101 | fix $ \loop -> do | ||
1102 | what <- atomically | ||
1103 | $ orElse (do (client,stanza) <- takeTMVar console | ||
1104 | return $ do informClientPresence0 state Nothing client stanza | ||
1105 | loop) | ||
1106 | (do readTMVar quitVar | ||
1107 | return $ return ()) | ||
1108 | what | ||
1109 | |||
1110 | quitMessage <- atomically $ takeTMVar quitVar | ||
1111 | |||
1112 | putStrLn "goodbye." | ||
1113 | return () | ||