{-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NondecreasingIndentation #-} module ToxToXMPP ( forkAccountWatcher , JabberClients , PerClient , initPerClient , toxQSearch , toxAnnounceInterval , xmppToTox , toxToXmpp , interweave ) where import Data.Conduit as C import qualified Data.Conduit.List as CL import Data.XML.Types as XML import EventUtil import Network.Tox.Crypto.Transport as Tox import Util (unsplitJID) import XMPPServer as XMPP import Network.Tox.DHT.Handlers (nodeSearch, nodesOfInterest) import Announcer import Announcer.Tox import Connection import Network.QueryResponse -- import Control.Concurrent import Control.Concurrent.STM import Control.Monad import Crypto.Tox import qualified Data.HashMap.Strict as HashMap import Data.Maybe import qualified Data.Set as Set import qualified Data.Text as T import Data.Time.Clock.POSIX import Network.Address import Network.Kademlia.Search import qualified Network.Tox as Tox import Network.Tox.ContactInfo as Tox import qualified Network.Tox.Crypto.Handlers as Tox -- import qualified Network.Tox.DHT.Handlers as Tox import ClientState import Data.Bits import Data.Function import qualified Data.Map as Map import Data.Text (Text) import Data.Word import qualified Network.Kademlia.Routing as R import Network.Tox import Network.Tox.DHT.Handlers import qualified Network.Tox.DHT.Transport as Tox ;import Network.Tox.DHT.Transport (FriendRequest (..), dhtpk) import Network.Tox.NodeId import qualified Network.Tox.Onion.Handlers as Tox import qualified Network.Tox.Onion.Transport as Tox ;import Network.Tox.Onion.Transport (OnionData (..)) import Presence import Text.Read import XMPPServer (ConnectionKey) #ifdef THREAD_DEBUG import Control.Concurrent.Lifted.Instrument #else import Control.Concurrent.Lifted import GHC.Conc (labelThread) #endif import DPut import Nesting xmppToTox :: Conduit XML.Event IO Tox.CryptoMessage xmppToTox = doNestingXML $ do eventBeginDocument <- await streamTag <- await fix $ \loop -> do e <- nextElement -- dput DPut.XMan $ "xmppToTox: " ++ show e -- -- (yield e >> awaitForever yield) $$ prettyPrint "xmpp->Tox" -- prettyPrint loop toxToXmpp :: Monad m => SockAddr -> PublicKey -> Text -> Conduit Tox.CryptoMessage m XML.Event toxToXmpp laddr me theirhost = do CL.sourceList $ XMPP.greet' "jabber:server" theirhost let me_u = T.pack $ show (key2id me) awaitForever $ \toxmsg -> do xmppInstantMessage "jabber:server" (Just theirhost) -- /from/ (Just $ unsplitJID (Just me_u,T.pack (show laddr),Nothing)) -- /to/ should match local address of this node. (T.pack $ show $ msgID toxmsg) xmppInstantMessage :: Monad m => Text -> Maybe Text -> Maybe Text -> Text -> ConduitM i Event m () xmppInstantMessage namespace mfrom mto text = do let ns n = n { nameNamespace = Just namespace } C.yield $ EventBeginElement (ns "message") ( maybe id (\t->(attr "from" t:)) mfrom $ maybe id (\t->(attr "to" t:)) mto $ [attr "type" "normal" ] ) C.yield $ EventBeginElement (ns "body") [] C.yield $ EventContent $ ContentText text C.yield $ EventEndElement (ns "body") C.yield $ EventBeginElement "{http://jabber.org/protocol/xhtml-im}html" [] C.yield $ EventBeginElement "{http://www.w3.org/1999/xhtml}body" [] C.yield $ EventBeginElement "{http://www.w3.org/1999/xhtml}p" [ attr "style" "font-weight:bold; color:red" ] C.yield $ EventContent $ ContentText text C.yield $ EventEndElement "{http://www.w3.org/1999/xhtml}p" C.yield $ EventEndElement "{http://www.w3.org/1999/xhtml}body" C.yield $ EventEndElement "{http://jabber.org/protocol/xhtml-im}html" C.yield $ EventEndElement (ns "message") key2jid :: Word32 -> PublicKey -> Text key2jid nospam key = T.pack $ show $ NoSpamId nsp key where nsp = NoSpam nospam (Just sum) sum = nlo `xor` nhi `xor` xorsum key nlo = fromIntegral (0x0FFFF .&. nospam) :: Word16 nhi = fromIntegral (0x0FFFF .&. (nospam `shiftR` 16)) :: Word16 type JabberClients = Map.Map ConnectionKey PerClient data PerClient = PerClient { pcDeliveredFRs :: TVar (Set.Set Tox.FriendRequest) } initPerClient :: STM PerClient initPerClient = do frs <- newTVar Set.empty return PerClient { pcDeliveredFRs = frs } data ToxToXMPP = ToxToXMPP { txAnnouncer :: Announcer , txAccount :: Account JabberClients , txPresence :: PresenceState , txTox :: Tox JabberClients } default_nospam :: Word32 default_nospam = 0x6a7a27fc -- big-endian base64: anon/A== nodeinfoStaleTime :: POSIXTime nodeinfoStaleTime = 600 nodeinfoSearchInterval :: POSIXTime nodeinfoSearchInterval = 15 gotDhtPubkey :: Tox.DHTPublicKey -> ToxToXMPP -> PublicKey -> IO () gotDhtPubkey pubkey tx theirKey = do contact <- atomically $ getContact theirKey (txAccount tx) >>= mapM (readTVar . contactLastSeenAddr) forM_ contact $ \lastSeen -> do case lastSeen of Nothing -> doSearch Just (tm, _) -> do now <- getPOSIXTime when (now - tm > nodeinfoStaleTime) doSearch where doSearch = do let pub = toPublic $ userSecret (txAccount tx) me = key2id pub akey <- akeyConnect (txAnnouncer tx) me theirKey atomically $ registerNodeCallback (toxRouting tox) (nic akey) scheduleSearch (txAnnouncer tx) akey meth pubkey tox :: Tox JabberClients tox = txTox tx byKey :: TVar (Map.Map PublicKey [Tox.NetCryptoSession]) byKey = Tox.netCryptoSessionsByKey $ toxCryptoSessions tox chillSesh :: STM (Maybe (Status Tox.ToxProgress)) chillSesh = do x <- (fmap.fmap) Tox.ncState . Map.lookup theirKey <$> readTVar byKey y <- (traverse.traverse) readTVar x return $ join $ listToMaybe <$> y activeSesh :: STM Bool activeSesh = chillSesh >>= return . \case Just Established -> True _ -> False target = key2id $ dhtpk pubkey meth :: SearchMethod Tox.DHTPublicKey meth = SearchMethod { sSearch = nodeSearch (toxDHT tox) (nodesOfInterest $ toxRouting tox) , sNearestNodes = nearNodes tox , sTarget = target , sInterval = nodeinfoSearchInterval , sWithResult = \r sr -> return () } nic akey = NodeInfoCallback { interestingNodeId = target , listenerId = 2 , observedAddress = observe akey , rumoredAddress = assume akey } assume :: AnnounceKey -> POSIXTime -> SockAddr -> NodeInfo -> STM () assume akey time addr ni = tput XNodeinfoSearch $ show ("rumor", akey, time, addr, ni) observe :: AnnounceKey -> POSIXTime -> NodeInfo -> STM () observe akey time ni = do tput XNodeinfoSearch $ show ("observation", akey, time, ni) scheduleImmediately (txAnnouncer tx) akey $ ScheduledItem $ shakeHands activeSesh (getContact theirKey (txAccount tx)) setContactAddr time theirKey (nodeAddr ni) (txAccount tx) shakeHands :: STM Bool -> STM (Maybe Contact) -> Announcer -> AnnounceKey -> POSIXTime -> STM (IO ()) shakeHands isActive getC ann akey now = do mbContact <- getC case mbContact of Nothing -> return $ return () Just contact -> do active <- isActive if (not active) then do scheduleAbs ann akey (ScheduledItem $ shakeHands isActive getC) (now + 5) return $ shakeHandsIO contact else return $ return () shakeHandsIO :: Contact -> IO () shakeHandsIO _ = return () dispatch :: ToxToXMPP -> ContactEvent -> IO () dispatch tx (SessionEstablished theirKey) = stopConnecting tx theirKey dispatch tx (SessionTerminated theirKey) = startConnecting tx theirKey dispatch tx (AddrChange theirkey saddr) = return () -- todo dispatch tx (PolicyChange theirkey TryingToConnect ) = startConnecting tx theirkey dispatch tx (PolicyChange theirkey policy ) = stopConnecting tx theirkey dispatch tx (OnionRouted theirKey (OnionDHTPublicKey pkey)) = gotDhtPubkey pkey tx theirKey dispatch tx (OnionRouted theirkey (OnionFriendRequest fr) ) = do let ToxToXMPP { txAnnouncer = acr , txAccount = acnt , txPresence = st } = tx k2c <- atomically $ do refs <- readTVar (accountExtra acnt) k2c <- Map.intersectionWith (,) refs <$> readTVar (keyToChan st) clients <- readTVar (clients st) return $ Map.intersectionWith (,) k2c clients -- TODO: Below we're using a hard coded default as their jabber user id. -- This isn't the right thing, but we don't know their user-id. Perhaps -- there should be a way to parse it out of the friend request text. Maybe -- after a zero-termination, or as visible text (nospam:...). let theirjid = key2jid default_nospam theirkey forM_ k2c $ \((PerClient{pcDeliveredFRs},conn),client) -> do alreadyDelivered <- atomically $ do frs <- readTVar pcDeliveredFRs writeTVar pcDeliveredFRs $ Set.insert fr frs return $ Set.member fr frs when (not alreadyDelivered) $ do self <- localJID (clientUser client) (clientProfile client) (clientResource client) ask <- presenceSolicitation theirjid self -- TODO Send friend-request text as an instant message or at least -- embed it in the stanza as a element. sendModifiedStanzaToClient ask (connChan conn) interweave :: [a] -> [a] -> [a] interweave [] ys = ys interweave (x:xs) ys = x : interweave ys xs akeyDHTKeyShare :: Announcer -> NodeId -> PublicKey -> IO AnnounceKey akeyDHTKeyShare announcer me them = atomically $ do packAnnounceKey announcer $ "dhtkey:" ++ (take 8 $ show me) ++ ":" ++ show (key2id them) akeyConnect :: Announcer -> NodeId -> PublicKey -> IO AnnounceKey akeyConnect announcer me them = atomically $ do packAnnounceKey announcer $ "connect:" ++ (take 8 $ show me) ++ ":" ++ show (key2id them) -- | Returns a list of nospam values to use for friend requests to send to a -- remote peer. This list is non-empty only when it is desirable to send -- friend requests. checkSoliciting :: PresenceState -> PublicKey -> PublicKey -> Contact -> IO [NoSpam] checkSoliciting presence me them contact = do let theirhost = T.pack $ show (key2id them) ++ ".tox" myhost = T.pack $ show (key2id me) ++ ".tox" xs <- getBuddiesAndSolicited presence $ \h -> do -- TODO: /h/ matches hostname? return $ T.toLower h == T.toLower theirhost return $ do (is_buddy,their_u,my_uid,xmpp_client_profile) <- xs guard $ xmpp_client_profile == myhost NoSpamId nospam _ <- case fmap T.unpack $ their_u of Just ('$':_) -> maybeToList $ readMaybe $ T.unpack $ unsplitJID (their_u,theirhost,Nothing) Just ('0':'x':_) -> maybeToList $ readMaybe $ T.unpack $ unsplitJID (their_u,theirhost,Nothing) _ -> maybeToList $ readMaybe $ T.unpack $ key2jid default_nospam them return nospam nearNodes :: Tox extra -> NodeId -> STM [NodeInfo] nearNodes tox nid = do bkts4 <- readTVar $ routing4 $ toxRouting tox bkts6 <- readTVar $ routing6 $ toxRouting tox let nss = map (R.kclosest (searchSpace (toxQSearch tox)) searchK nid) [bkts4, bkts6] return $ foldr interweave [] nss startConnecting0 :: ToxToXMPP -> PublicKey -> Contact -> IO () startConnecting0 tx them contact = do dput XMan $ "START CONNECTING " ++ show (key2id them) let ToxToXMPP { txTox = tox , txAnnouncer = announcer , txAccount = acnt } = tx let nearNodes nid = do bkts4 <- readTVar $ routing4 $ toxRouting tox bkts6 <- readTVar $ routing6 $ toxRouting tox let nss = map (R.kclosest (searchSpace (toxQSearch tox)) searchK nid) [bkts4,bkts6] return $ foldr interweave [] nss wanted <- atomically $ (==Just TryingToConnect) <$> readTVar (contactPolicy contact) let mypub = toPublic $ userSecret acnt me = key2id mypub soliciting <- checkSoliciting (txPresence tx) mypub them contact when wanted $ do akey <- akeyDHTKeyShare announcer me them -- We send this packet every 30 seconds if there is more -- than one peer (in the 8) that says they our friend is -- announced on them. This packet can also be sent through -- the DHT module as a DHT request packet (see DHT) if we -- know the DHT public key of the friend and are looking -- for them in the DHT but have not connected to them yet. -- 30 second is a reasonable timeout to not flood the -- network with too many packets while making sure the -- other will eventually receive the packet. Since packets -- are sent through every peer that knows the friend, -- resending it right away without waiting has a high -- likelihood of failure as the chances of packet loss -- happening to all (up to to 8) packets sent is low. -- let meth = SearchMethod (toxQSearch tox) onResult nearNodes (key2id them) 30 where onResult theirkey rendezvous = do dkey <- Tox.getContactInfo tox let tr = Tox.toxToRoute tox route = Tox.AnnouncedRendezvous theirkey rendezvous dput XMan $ unwords [ take 8 (show $ key2id mypub) ++ ":" , "Sending my DHT-key" , show (key2id $ Tox.dhtpk dkey) , "to" , show (key2id theirkey) , "via" , show (Tox.rendezvousNode rendezvous) ] sendMessage tr route (mypub,Tox.OnionDHTPublicKey dkey) forM_ soliciting $ \cksum@(NoSpam nospam _)-> do dput XMan $ unwords [ take 8 (show $ key2id mypub) ++ ":" , "Sending friend-request" , "with nospam" , "(" ++ nospam64 cksum ++ "," ++nospam16 cksum ++ ")" , "to" , show (key2id theirkey) , "via" , show (Tox.rendezvousNode rendezvous) ] let fr = FriendRequest { friendNoSpam = nospam , friendRequestText = "XMPP friend request" } sendMessage tr route (mypub,Tox.OnionFriendRequest fr) scheduleSearch announcer akey meth them startConnecting :: ToxToXMPP -> PublicKey -> IO () startConnecting tx them = do mc <- atomically $ HashMap.lookup (key2id them) <$> readTVar (contacts $ txAccount tx) forM_ mc $ startConnecting0 tx them stopConnecting :: ToxToXMPP -> PublicKey -> IO () stopConnecting ToxToXMPP{txAnnouncer=announcer,txAccount=acnt} them = do dput XMan $ "STOP CONNECTING " ++ show (key2id them) let pub = toPublic $ userSecret acnt me = key2id pub akey <- akeyDHTKeyShare announcer me them cancel announcer akey forkAccountWatcher :: Account JabberClients -> Tox JabberClients -> PresenceState -> Announcer -> IO ThreadId forkAccountWatcher acc tox st announcer = forkIO $ do myThreadId >>= flip labelThread ("tox-xmpp:" ++ show (key2id $ toPublic $ userSecret acc)) (chan,cs) <- atomically $ do chan <- dupTChan $ eventChan acc -- duplicate broadcast channel for reading. contacts <- readTVar (contacts acc) return (chan,contacts) let tx = ToxToXMPP { txAnnouncer = announcer , txAccount = acc , txPresence = st , txTox = tox } forM_ (HashMap.toList cs) $ \(them,c) -> do startConnecting0 tx (id2key them) c -- Loop endlessly until accountExtra is null. fix $ \loop -> do mev <- atomically $ (Just <$> readTChan chan) `orElse` do refs <- readTVar $ accountExtra acc check $ Map.null refs return Nothing forM_ mev $ \ev -> dispatch tx ev >> loop cs <- atomically $ readTVar (contacts acc) forM_ (HashMap.toList cs) $ \(them,c) -> do stopConnecting tx (id2key them) toxQSearch :: Tox extra -> Search Tox.NodeId (IP, PortNumber) Nonce32 Tox.NodeInfo Tox.Rendezvous toxQSearch tox = Tox.toxidSearch (Tox.onionTimeout tox) (Tox.toxCryptoKeys tox) (Tox.toxOnion tox) toxAnnounceInterval :: POSIXTime toxAnnounceInterval = 15