summaryrefslogtreecommitdiff
path: root/ToxToXMPP.hs
diff options
context:
space:
mode:
authorjoe <joe@jerkface.net>2018-06-25 17:56:14 -0400
committerjoe <joe@jerkface.net>2018-06-25 18:18:28 -0400
commit53b72dd253ce01a24430429cef400675401292dc (patch)
treecda8a4e081f14eb9148a402eb209cbd07b63b5c4 /ToxToXMPP.hs
parent214fd246d0d2c0240d6adbf43a0a68d772eb71b7 (diff)
Separated conduit parser and general tox-manager stuff.
Diffstat (limited to 'ToxToXMPP.hs')
-rw-r--r--ToxToXMPP.hs500
1 files changed, 11 insertions, 489 deletions
diff --git a/ToxToXMPP.hs b/ToxToXMPP.hs
index 9979526a..1493827a 100644
--- a/ToxToXMPP.hs
+++ b/ToxToXMPP.hs
@@ -3,80 +3,22 @@
3{-# LANGUAGE NamedFieldPuns #-} 3{-# LANGUAGE NamedFieldPuns #-}
4{-# LANGUAGE NondecreasingIndentation #-} 4{-# LANGUAGE NondecreasingIndentation #-}
5{-# LANGUAGE ViewPatterns #-} 5{-# LANGUAGE ViewPatterns #-}
6module ToxToXMPP 6module ToxToXMPP where
7 ( forkAccountWatcher
8 , JabberClients
9 , PerClient
10 , initPerClient
11 , toxQSearch
12 , toxAnnounceInterval
13 , xmppToTox
14 , toxToXmpp
15 , interweave
16 ) where
17 7
18import Control.Applicative 8import Crypto.Tox
19import Data.Conduit as C 9import Data.Conduit as C
20import qualified Data.Conduit.List as CL 10import qualified Data.Conduit.List as CL
21import Data.Monoid 11import Data.Monoid
22import Data.Text.Encoding as T 12import qualified Data.Text as T
23import Data.XML.Types as XML 13 ;import Data.Text (Text)
14import Data.Text.Encoding as T
15import Data.XML.Types as XML
24import EventUtil 16import EventUtil
25import Network.Tox.Crypto.Transport as Tox
26import Network.Tox.Handshake (HandshakeParams (..))
27import qualified Text.XML.Stream.Parse as XML
28import Util (unsplitJID)
29import XMPPServer as XMPP
30
31
32import Announcer
33import Announcer.Tox
34import Connection
35import Network.QueryResponse
36import Network.Tox.DHT.Handlers (nodeSearch, nodesOfInterest)
37-- import Control.Concurrent
38import Control.Concurrent.STM
39import Control.Monad
40import Crypto.Tox
41import qualified Data.HashMap.Strict as HashMap
42import Data.Maybe
43import qualified Data.Set as Set
44import qualified Data.Text as T
45import Data.Time.Clock.POSIX
46import Network.Address 17import Network.Address
47import Network.Kademlia.Search 18import Network.Tox.Crypto.Transport as Tox
48import qualified Network.Tox as Tox
49import Network.Tox.ContactInfo as Tox
50import qualified Network.Tox.Crypto.Handlers as Tox
51 ;import Network.Tox.Crypto.Handlers (UponCookie (..))
52-- import qualified Network.Tox.DHT.Handlers as Tox
53import ClientState
54import Data.Bits
55import Data.Function
56import qualified Data.Map as Map
57import Data.Text (Text)
58import Data.Word
59import qualified Network.Kademlia.Routing as R
60import Network.Tox
61import Network.Tox.DHT.Handlers
62import qualified Network.Tox.DHT.Transport as Tox
63 ;import Network.Tox.DHT.Transport (FriendRequest (..), dhtpk)
64import Network.Tox.NodeId 19import Network.Tox.NodeId
65import qualified Network.Tox.Onion.Handlers as Tox 20import Util (unsplitJID)
66import qualified Network.Tox.Onion.Transport as Tox 21import XMPPServer as XMPP
67 ;import Network.Tox.Onion.Transport (OnionData (..))
68import Presence
69import Text.Read
70import XMPPServer (ClientAddress)
71#ifdef THREAD_DEBUG
72import Control.Concurrent.Lifted.Instrument
73#else
74import Control.Concurrent.Lifted
75import GHC.Conc (labelThread)
76#endif
77import DPut
78import Nesting
79import XMPPToTox
80 22
81toxToXmpp :: Monad m => SockAddr -> PublicKey -> Text -> Conduit Tox.CryptoMessage m XML.Event 23toxToXmpp :: Monad m => SockAddr -> PublicKey -> Text -> Conduit Tox.CryptoMessage m XML.Event
82toxToXmpp laddr me theirhost = do 24toxToXmpp laddr me theirhost = do
@@ -126,423 +68,3 @@ xmppInstantMessage namespace mfrom mto style text = do
126 C.yield $ EventEndElement "{http://jabber.org/protocol/xhtml-im}html" 68 C.yield $ EventEndElement "{http://jabber.org/protocol/xhtml-im}html"
127 C.yield $ EventEndElement (ns "message") 69 C.yield $ EventEndElement (ns "message")
128 70
129key2jid :: Word32 -> PublicKey -> Text
130key2jid nospam key = T.pack $ show $ NoSpamId nsp key
131 where
132 nsp = NoSpam nospam (Just sum)
133 sum = nlo `xor` nhi `xor` xorsum key
134 nlo = fromIntegral (0x0FFFF .&. nospam) :: Word16
135 nhi = fromIntegral (0x0FFFF .&. (nospam `shiftR` 16)) :: Word16
136
137type JabberClients = Map.Map ClientAddress PerClient
138
139data PerClient = PerClient
140 { pcDeliveredFRs :: TVar (Set.Set Tox.FriendRequest)
141 }
142
143initPerClient :: STM PerClient
144initPerClient = do
145 frs <- newTVar Set.empty
146 return PerClient
147 { pcDeliveredFRs = frs
148 }
149
150data ToxToXMPP = ToxToXMPP
151 { txAnnouncer :: Announcer
152 , txAccount :: Account JabberClients
153 , txPresence :: PresenceState
154 , txTox :: Tox JabberClients
155 }
156
157default_nospam :: Word32
158default_nospam = 0x6a7a27fc -- big-endian base64: anon/A==
159
160nodeinfoStaleTime :: POSIXTime
161nodeinfoStaleTime = 600
162
163nodeinfoSearchInterval :: POSIXTime
164nodeinfoSearchInterval = 15
165
166data Awaiting v = Since POSIXTime
167data Acquired v = At POSIXTime v
168data Moot v = Moot
169
170data NNS a b c = NNS { -- NetcryptoNegotiationState
171 sessionDesired :: Bool,
172 theirPublicKey :: a Tox.DHTPublicKey,
173 theirAddress :: b NodeInfo,
174 theirCookie :: c (Tox.Cookie Encrypted),
175 sessionIsActive :: Bool
176}
177
178data NS
179 = Stage1 (NNS Moot Moot Moot)
180 | Stage2 (NNS Awaiting Moot Moot)
181 | Stage3 (NNS Acquired Awaiting Moot)
182 | Stage4 (NNS Acquired Acquired Awaiting)
183 | Stage5 (NNS Acquired Acquired Acquired)
184
185gotDhtPubkey :: Tox.DHTPublicKey -> ToxToXMPP -> PublicKey -> IO ()
186gotDhtPubkey theirDhtKey tx theirKey = do
187 contact <- atomically $ getContact theirKey (txAccount tx) >>= mapM (readTVar . contactLastSeenAddr)
188 forM_ contact $ \lastSeen -> do
189 case lastSeen of
190 Nothing -> doSearch
191 Just (tm, _) -> do
192 now <- getPOSIXTime
193 when (now - tm > nodeinfoStaleTime) doSearch
194 where
195 tox :: Tox JabberClients
196 tox = txTox tx
197
198 myPublicKey = toPublic $ userSecret (txAccount tx)
199 me = key2id myPublicKey
200
201 doSearch = do
202 let akey = akeyConnect (txAnnouncer tx) me theirKey
203 atomically $ registerNodeCallback (toxRouting tox) (nic akey)
204 scheduleSearch (txAnnouncer tx) akey meth theirDhtKey
205
206 target :: NodeId
207 target = key2id $ dhtpk theirDhtKey
208
209 meth :: SearchMethod Tox.DHTPublicKey
210 meth =
211 SearchMethod
212 { sSearch = nodeSearch (toxDHT tox) (nodesOfInterest $ toxRouting tox)
213 , sNearestNodes = nearNodes tox
214 , sTarget = target
215 , sInterval = nodeinfoSearchInterval
216 , sWithResult = \r sr -> return ()
217 }
218 nic akey =
219 NodeInfoCallback
220 { interestingNodeId = target
221 , listenerId = 2
222 , observedAddress = observe akey
223 , rumoredAddress = assume akey
224 }
225
226 assume :: AnnounceKey -> POSIXTime -> SockAddr -> NodeInfo -> STM ()
227 assume akey time addr ni =
228 tput XNodeinfoSearch $ show ("rumor", akey, time, addr, ni)
229
230 observe :: AnnounceKey -> POSIXTime -> NodeInfo -> STM ()
231 observe akey time ni@(nodeAddr -> addr) = do
232 tput XNodeinfoSearch $ show ("observation", akey, time, addr)
233 setContactAddr time theirKey ni (txAccount tx)
234
235gotAddr :: NodeInfo -> ToxToXMPP -> PublicKey -> IO ()
236gotAddr ni@(nodeAddr -> addr) tx theirKey = do
237 dhtkey <- (fmap.fmap) snd $
238 fmap join $
239 atomically $
240 traverse readTVar =<< fmap contactKeyPacket <$> getContact theirKey (txAccount tx)
241 forM_ dhtkey $ gotAddr' ni tx theirKey
242
243gotAddr' :: NodeInfo -> ToxToXMPP -> PublicKey -> Tox.DHTPublicKey -> IO ()
244gotAddr' ni@(nodeAddr -> addr) tx theirKey theirDhtKey = atomically blee
245
246 where
247 myPublicKey = toPublic $ userSecret (txAccount tx)
248 me = key2id myPublicKey
249 akey = akeyConnect (txAnnouncer tx) me theirKey
250
251 blee = do
252 scheduleImmediately (txAnnouncer tx) akey $
253 ScheduledItem $ getCookie ni (activeSesh addr) (getContact theirKey (txAccount tx))
254
255 tox :: Tox JabberClients
256 tox = txTox tx
257
258 byAddr :: TVar (Map.Map SockAddr Tox.NetCryptoSession)
259 byAddr = Tox.netCryptoSessions (toxCryptoSessions tox)
260
261 crypto = Tox.transportCrypto $ toxCryptoSessions tox
262
263 readNcVar :: (Tox.NetCryptoSession -> TVar b) -> SockAddr -> STM (Maybe b)
264 readNcVar v addr = traverse readTVar =<< fmap v . Map.lookup addr <$> readTVar byAddr
265
266 chillSesh :: SockAddr -> STM (Maybe (Status Tox.ToxProgress))
267 chillSesh = readNcVar Tox.ncState
268
269 activeSesh :: SockAddr -> STM Bool
270 activeSesh a = chillSesh a >>= return . \case
271 Just Established -> True
272 _ -> False
273
274 readCookie :: SockAddr -> STM (Maybe (UponCookie (Tox.Cookie Encrypted)))
275 readCookie = readNcVar Tox.ncCookie
276 readCookie' :: SockAddr -> STM (Maybe (Tox.Cookie Encrypted))
277 readCookie' = fmap join . (fmap.fmap) Tox.toMaybe . readCookie
278
279 client :: Network.Tox.DHT.Handlers.Client
280 client = toxDHT tox
281
282 getCookie
283 :: NodeInfo
284 -> STM Bool
285 -> STM (Maybe Contact)
286 -> Announcer
287 -> AnnounceKey
288 -> POSIXTime
289 -> STM (IO ())
290 getCookie ni isActive getC ann akey now = getCookieAgain
291 where
292 getCookieAgain = do
293 tput XNodeinfoSearch $ show ("getCookieAgain", akey)
294 mbContact <- getC
295 case mbContact of
296 Nothing -> return $ return ()
297 Just contact -> do
298 active <- isActive
299 return $ when (not active) getCookieIO
300
301 callRealShakeHands = realShakeHands (userSecret (txAccount tx)) theirKey (dhtpk theirDhtKey) (toxCryptoSessions tox) (nodeAddr ni)
302
303 reschedule n f = scheduleRel ann akey f n
304 reschedule' n f = reschedule n (ScheduledItem $ \_ _ now -> f now)
305
306 cookieMaxAge = 60 * 5
307
308 getCookieIO :: IO ()
309 getCookieIO = do
310 dput XUnused "getCookieIO - entered"
311 cookieRequest crypto client myPublicKey ni >>= \case
312 Nothing -> atomically $ reschedule' 5 (const getCookieAgain)
313 Just cookie -> do
314 void $ callRealShakeHands cookie
315 cookieCreationStamp <- getPOSIXTime
316 let shaker :: POSIXTime -> STM (IO ())
317 shaker now = do
318 active <- isActive
319 if (active)
320 then return $ return ()
321 else if (now > cookieCreationStamp + cookieMaxAge)
322 then return $
323 dput XUnused "getCookieIO/shaker - cookie expired" >>
324 getCookieIO
325 else do
326 reschedule' 5 shaker
327 return . void $ callRealShakeHands cookie
328 atomically $ reschedule' 5 shaker
329
330realShakeHands :: SecretKey -> PublicKey -> PublicKey -> Tox.NetCryptoSessions -> SockAddr -> Tox.Cookie Encrypted -> IO Bool
331realShakeHands myseckey theirpubkey theirDhtKey allsessions saddr cookie = do
332 dput XUnused "realShakeHands"
333 let hp =
334 HParam
335 { hpOtherCookie = cookie
336 , hpMySecretKey = myseckey
337 , hpCookieRemotePubkey = theirpubkey
338 , hpCookieRemoteDhtkey = theirDhtKey
339 , hpTheirBaseNonce = Nothing
340 , hpTheirSessionKeyPublic = Nothing
341 }
342 newsession <- generateSecretKey
343 timestamp <- getPOSIXTime
344 (myhandshake, ioAction) <-
345 atomically $
346 Tox.freshCryptoSession allsessions saddr newsession timestamp hp
347 ioAction
348 -- send handshake
349 isJust <$> forM myhandshake (Tox.sendHandshake allsessions saddr)
350
351dispatch :: ToxToXMPP -> ContactEvent -> IO ()
352dispatch tx (SessionEstablished theirKey) = stopConnecting tx theirKey "established"
353dispatch tx (SessionTerminated theirKey) = startConnecting tx theirKey "terminated"
354dispatch tx (AddrChange theirKey saddr) = gotAddr saddr tx theirKey
355dispatch tx (PolicyChange theirkey TryingToConnect ) = startConnecting tx theirkey "policy"
356dispatch tx (PolicyChange theirkey policy ) = stopConnecting tx theirkey "policy"
357dispatch tx (OnionRouted theirKey (OnionDHTPublicKey pkey)) = gotDhtPubkey pkey tx theirKey
358dispatch tx (OnionRouted theirkey (OnionFriendRequest fr) ) = do
359 let ToxToXMPP { txAnnouncer = acr
360 , txAccount = acnt
361 , txPresence = st } = tx
362 k2c <- atomically $ do
363 refs <- readTVar (accountExtra acnt)
364 k2c <- Map.intersectionWith (,) refs <$> readTVar (ckeyToChan st)
365 clients <- readTVar (clients st)
366 return $ Map.intersectionWith (,) k2c clients
367 -- TODO: Below we're using a hard coded default as their jabber user id.
368 -- This isn't the right thing, but we don't know their user-id. Perhaps
369 -- there should be a way to parse it out of the friend request text. Maybe
370 -- after a zero-termination, or as visible text (nospam:...).
371 let theirjid = key2jid default_nospam theirkey
372 forM_ k2c $ \((PerClient{pcDeliveredFRs},conn),client) -> do
373 alreadyDelivered <- atomically $ do
374 frs <- readTVar pcDeliveredFRs
375 writeTVar pcDeliveredFRs $ Set.insert fr frs
376 return $ Set.member fr frs
377 when (not alreadyDelivered) $ do
378 self <- localJID (clientUser client) (clientProfile client) (clientResource client)
379 ask <- presenceSolicitation theirjid self
380 -- TODO Send friend-request text as an instant message or at least
381 -- embed it in the stanza as a <status> element.
382 sendModifiedStanzaToClient ask (connChan conn)
383
384interweave :: [a] -> [a] -> [a]
385interweave [] ys = ys
386interweave (x:xs) ys = x : interweave ys xs
387
388akeyDHTKeyShare :: Announcer -> NodeId -> PublicKey -> AnnounceKey
389akeyDHTKeyShare announcer me them =
390 packAnnounceKey announcer $ "dhtkey(" ++ (take 8 $ show me) ++ ")" ++ show (key2id them)
391
392akeyConnect :: Announcer -> NodeId -> PublicKey -> AnnounceKey
393akeyConnect announcer me them =
394 packAnnounceKey announcer $ "connect(" ++ (take 8 $ show me) ++ ")" ++ show (key2id them)
395
396
397-- | Returns a list of nospam values to use for friend requests to send to a
398-- remote peer. This list is non-empty only when it is desirable to send
399-- friend requests.
400checkSoliciting :: PresenceState -> PublicKey -> PublicKey -> Contact -> IO [NoSpam]
401checkSoliciting presence me them contact = do
402 let theirhost = T.pack $ show (key2id them) ++ ".tox"
403 myhost = T.pack $ show (key2id me) ++ ".tox"
404 xs <- getBuddiesAndSolicited presence myhost $ \h -> do
405 return $ T.toLower h == T.toLower theirhost
406 return $ do
407 (is_buddy,their_u,my_uid,xmpp_client_profile) <- xs
408 guard $ xmpp_client_profile == myhost
409 NoSpamId nospam _ <- case fmap T.unpack $ their_u of
410 Just ('$':_) -> maybeToList $ readMaybe $ T.unpack $ unsplitJID (their_u,theirhost,Nothing)
411 Just ('0':'x':_) -> maybeToList $ readMaybe $ T.unpack $ unsplitJID (their_u,theirhost,Nothing)
412 _ -> maybeToList $ readMaybe $ T.unpack $ key2jid default_nospam them
413 return nospam
414
415nearNodes :: Tox extra -> NodeId -> STM [NodeInfo]
416nearNodes tox nid = do
417 bkts4 <- readTVar $ routing4 $ toxRouting tox
418 bkts6 <- readTVar $ routing6 $ toxRouting tox
419 let nss =
420 map
421 (R.kclosest (searchSpace (toxQSearch tox)) searchK nid)
422 [bkts4, bkts6]
423 return $ foldr interweave [] nss
424
425startConnecting0 :: ToxToXMPP -> PublicKey -> Contact -> String -> IO ()
426startConnecting0 tx them contact reason = do
427 dput XMan $ "START CONNECTING " ++ show (key2id them) ++ "("++reason++")"
428 -- TODO When a connection is already established, this function should
429 -- be a no-op. This occurs when an XMPP client disconnects and
430 -- reconnects while a session is established.
431 let ToxToXMPP { txTox = tox
432 , txAnnouncer = announcer
433 , txAccount = acnt } = tx
434 let nearNodes nid = do
435 bkts4 <- readTVar $ routing4 $ toxRouting tox
436 bkts6 <- readTVar $ routing6 $ toxRouting tox
437 let nss = map (R.kclosest (searchSpace (toxQSearch tox)) searchK nid)
438 [bkts4,bkts6]
439 return $ foldr interweave [] nss
440 wanted <- atomically $ (==Just TryingToConnect) <$> readTVar (contactPolicy contact)
441 let mypub = toPublic $ userSecret acnt
442 me = key2id mypub
443 soliciting <- checkSoliciting (txPresence tx) mypub them contact
444 when wanted $ do
445 akey <- return $ akeyDHTKeyShare announcer me them
446 -- We send this packet every 30 seconds if there is more
447 -- than one peer (in the 8) that says they our friend is
448 -- announced on them. This packet can also be sent through
449 -- the DHT module as a DHT request packet (see DHT) if we
450 -- know the DHT public key of the friend and are looking
451 -- for them in the DHT but have not connected to them yet.
452 -- 30 second is a reasonable timeout to not flood the
453 -- network with too many packets while making sure the
454 -- other will eventually receive the packet. Since packets
455 -- are sent through every peer that knows the friend,
456 -- resending it right away without waiting has a high
457 -- likelihood of failure as the chances of packet loss
458 -- happening to all (up to to 8) packets sent is low.
459 --
460 let meth = SearchMethod (toxQSearch tox) onResult nearNodes (key2id them) 30
461 where
462 onResult theirkey rendezvous = do
463 dkey <- Tox.getContactInfo tox
464 let tr = Tox.toxToRoute tox
465 route = Tox.AnnouncedRendezvous theirkey rendezvous
466 dput XMan $ unwords [ take 8 (show $ key2id mypub) ++ ":"
467 , "Sending my DHT-key"
468 , show (key2id $ Tox.dhtpk dkey)
469 , "to"
470 , show (key2id theirkey)
471 , "via"
472 , show (Tox.rendezvousNode rendezvous)
473 ]
474 sendMessage tr route (mypub,Tox.OnionDHTPublicKey dkey)
475 forM_ soliciting $ \cksum@(NoSpam nospam _)-> do
476 dput XMan $ unwords [ take 8 (show $ key2id mypub) ++ ":"
477 , "Sending friend-request"
478 , "with nospam"
479 , "(" ++ nospam64 cksum ++ "," ++nospam16 cksum ++ ")"
480 , "to"
481 , show (key2id theirkey)
482 , "via"
483 , show (Tox.rendezvousNode rendezvous)
484 ]
485 let fr = FriendRequest
486 { friendNoSpam = nospam
487 , friendRequestText = "XMPP friend request"
488 }
489 sendMessage tr route (mypub,Tox.OnionFriendRequest fr)
490 scheduleSearch announcer akey meth them
491
492startConnecting :: ToxToXMPP -> PublicKey -> String -> IO ()
493startConnecting tx them reason = do
494 mc <- atomically $ HashMap.lookup (key2id them)
495 <$> readTVar (contacts $ txAccount tx)
496 forM_ mc $ flip (startConnecting0 tx them) reason
497
498
499stopConnecting :: ToxToXMPP -> PublicKey -> String -> IO ()
500stopConnecting ToxToXMPP{txAnnouncer=announcer,txAccount=acnt} them reason = do
501 dput XMan $ "STOP CONNECTING " ++ show (key2id them) ++ "("++reason++")"
502 let pub = toPublic $ userSecret acnt
503 me = key2id pub
504 akeyC = akeyConnect announcer me them
505 akeyD = akeyDHTKeyShare announcer me them
506 cancel announcer akeyC
507 cancel announcer akeyD
508
509forkAccountWatcher :: Account JabberClients -> Tox JabberClients -> PresenceState -> Announcer -> IO ThreadId
510forkAccountWatcher acc tox st announcer = forkIO $ do
511 myThreadId >>= flip labelThread ("tox-xmpp:"
512 ++ show (key2id $ toPublic $ userSecret acc))
513 (chan,cs) <- atomically $ do
514 chan <- dupTChan $ eventChan acc -- duplicate broadcast channel for reading.
515 contacts <- readTVar (contacts acc)
516 return (chan,contacts)
517 let tx = ToxToXMPP { txAnnouncer = announcer
518 , txAccount = acc
519 , txPresence = st
520 , txTox = tox
521 }
522 forM_ (HashMap.toList cs) $ \(them,c) -> do
523 startConnecting0 tx (id2key them) c "enabled account"
524
525 -- Loop endlessly until accountExtra is null.
526 fix $ \loop -> do
527 mev <- atomically $
528 (Just <$> readTChan chan)
529 `orElse` do
530 refs <- readTVar $ accountExtra acc
531 check $ Map.null refs
532 return Nothing
533
534 forM_ mev $ \ev -> dispatch tx ev >> loop
535
536 cs <- atomically $ readTVar (contacts acc)
537 forM_ (HashMap.toList cs) $ \(them,c) -> do
538 stopConnecting tx (id2key them) "disabled account"
539
540
541toxQSearch :: Tox extra -> Search Tox.NodeId (IP, PortNumber) Nonce32 Tox.NodeInfo Tox.Rendezvous
542toxQSearch tox = Tox.toxidSearch (Tox.onionTimeout tox) (Tox.toxCryptoKeys tox) (Tox.toxOnion tox)
543
544toxAnnounceInterval :: POSIXTime
545toxAnnounceInterval = 15
546
547
548