{-# LANGUAGE RecursiveDo #-} {-# LANGUAGE PartialTypeSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE FlexibleContexts #-} module Network.Tox.TCP ( module Network.Tox.TCP , NodeInfo(..) ) where import Debug.Trace import Control.Arrow import Control.Concurrent import Control.Concurrent.STM import Control.Exception import Control.Monad import Crypto.Random import Data.Aeson (ToJSON(..),FromJSON(..)) import qualified Data.Aeson as JSON import Data.ByteArray (withByteArray) import Data.Functor.Contravariant import Data.Functor.Identity import Data.Hashable import qualified Data.HashMap.Strict as HashMap import qualified Data.IntMap.Strict as IntMap import Data.IP import Data.Maybe import Data.Monoid import Data.Serialize import Data.Word import qualified Data.Vector as Vector import Foreign.Storable (peek) import Network.Socket (SockAddr(..)) import qualified Text.ParserCombinators.ReadP as RP import System.IO.Error import System.IO.Unsafe (unsafeDupablePerformIO) import System.Timeout import ControlMaybe import Crypto.Tox import Data.ByteString (hPut,hGet,ByteString,length) import Data.TableMethods import Data.Tox.Relay import qualified Data.Word64Map import DebugTag import DPut import Network.Address (setPort,PortNumber,localhost4,fromSockAddr,nullAddress4) import Network.Kademlia.Routing import Network.Kademlia.Search hiding (sendQuery) import Network.QueryResponse as QR import Network.QueryResponse.TCP import Network.Tox.TCP.NodeId () import Network.Tox.DHT.Transport (toxSpace) import Network.Tox.Onion.Transport hiding (encrypt,decrypt) import Network.Tox.Onion.Transport (unwrapAnnounceResponse) import qualified Network.Tox.NodeId as UDP import Text.XXD import Data.Proxy withSize :: Sized x => (Size x -> m (p x)) -> m (p x) withSize f = case size of len -> f len type NodeId = UDP.NodeId nodeId :: NodeInfo -> NodeId nodeId ni = UDP.nodeId $ udpNodeInfo ni nodeAddr :: NodeInfo -> SockAddr nodeAddr ni = setPort (tcpPort ni) $ UDP.nodeAddr $ udpNodeInfo ni nodeIP :: NodeInfo -> IP nodeIP ni = UDP.nodeIP $ udpNodeInfo ni tcpStream :: (Show y, Show x, Serialize y, Sized y, Serialize x, Sized x) => TransportCrypto -> (NodeInfo -> IO st) -> StreamHandshake NodeInfo (st,x) y tcpStream crypto mkst = StreamHandshake { streamHello = \addr h -> do (skey, hello) <- atomically $ do n24 <- transportNewNonce crypto skey <- transportNewKey crypto base24 <- transportNewNonce crypto return $ (,) skey $ Hello $ Asymm { senderKey = transportPublic crypto , asymmNonce = n24 , asymmData = pure HelloData { sessionPublicKey = toPublic $ skey , sessionBaseNonce = base24 } } noncef <- lookupNonceFunction crypto (transportSecret crypto) (UDP.id2key $ nodeId addr) dput XTCP $ "TCP:" ++ show addr ++ " <-- " ++ show hello hPut h $ encode $ encryptPayload (noncef $ helloNonce hello) hello (welcomeE, wbs) <- do let sz0 = size sz = constSize sz0 bs <- hGet h sz return ( fmap (`asProxyTypeOf` sz0) $ decode bs, bs ) let mwelcome = welcomeE >>= \w -> decryptPayload (noncef $ welcomeNonce w) w nil = SessionProtocol { streamGoodbye = return () , streamDecode = return Nothing , streamEncode = \y -> dput XTCP $ "TCP nil <-- " ++ show y } either (\e -> do dput XTCP $ "welcome: " ++ show (Data.ByteString.length wbs) ++ " bytes." forM_ (xxd2 0 wbs) $ dput XTCP dput XTCP $ "TCP(fail welcome): " ++ e return nil ) id $ mwelcome <&> \welcome -> do dput XTCP $ "TCP:" ++ show addr ++ " --> " ++ show welcome noncef' <- lookupNonceFunction crypto skey (sessionPublicKey $ runIdentity $ welcomeData welcome) nsend <- newMVar (sessionBaseNonce $ runIdentity $ helloData hello) nread <- newMVar (sessionBaseNonce $ runIdentity $ welcomeData welcome) let them = sessionPublicKey $ runIdentity $ welcomeData welcome hvar <- newMVar h st <- mkst addr return SessionProtocol { streamGoodbye = do dput XTCP $ "Closing " ++ show addr return () -- No goodbye packet? Seems rude. , streamDecode = let go h = decode <$> hGet h 2 >>= \case Left e -> do dput XTCP $ "TCP: (" ++ show addr ++ ") Failed to get length: " ++ e return Nothing Right len -> do decode <$> hGet h (fromIntegral (len :: Word16)) >>= \case Left e -> do dput XTCP $ "TCP: Failed to decode packet." return Nothing Right x -> do dput XTCP $ "TCP:"++ show addr ++ " --> packet!" m24 <- timeout 1000000 (takeMVar nread) fmap join $ forM m24 $ \n24 -> do let r = decrypt (noncef' n24) x >>= decodePlain putMVar nread (incrementNonce24 n24) either (dput XTCP . ("TCP decryption: " ++)) (\x' -> do dput XTCP $ "TCP:" ++ show addr ++ " --> " ++ show x' return ()) r return $ either (const Nothing) (Just . (,) st) r in bracket (takeMVar hvar) (putMVar hvar) $ \h -> go h `catchIOError` \e -> do dput XTCP $ "TCP exception: " ++ show e return Nothing , streamEncode = \y -> do -- We need this to throw so the tcp session state can be cleaned up elsewhere. bracket (takeMVar nsend) (putMVar nsend . incrementNonce24) $ \n24 -> do let bs = encode $ encrypt (noncef' n24) $ encodePlain y hPut h (encode (fromIntegral $ Data.ByteString.length bs :: Word16) <> bs) dput XTCP $ "TCP: " ++ show addr ++ " <-- " ++ show y } , streamAddr = nodeAddr } newtype SessionData = SessionData (TMVar (IntMap.IntMap NodeId)) newSessionData :: NodeInfo -> IO SessionData newSessionData _ = atomically $ SessionData <$> newTMVar IntMap.empty getRelayedRemote :: SessionData -> ConId -> STM NodeId getRelayedRemote (SessionData keymapVar) (ConId i) = do keymap <- takeTMVar keymapVar let k = fromMaybe UDP.zeroID $ IntMap.lookup (fromIntegral i) keymap putTMVar keymapVar keymap return k setRelayedRemote :: SessionData -> ConId -> NodeId -> STM () setRelayedRemote (SessionData keymapVar) (ConId conid) nid = do keymap <- takeTMVar keymapVar putTMVar keymapVar $ IntMap.insert (fromIntegral conid) nid keymap toxTCP :: TransportCrypto -> IO ( TCPCache (SessionProtocol (SessionData,RelayPacket) RelayPacket) , TransportA err NodeInfo (SessionData,RelayPacket) (Bool,RelayPacket) ) toxTCP crypto = tcpTransport 30 (tcpStream crypto newSessionData) tcpSpace :: KademliaSpace NodeId NodeInfo tcpSpace = contramap udpNodeInfo toxSpace {- nodeSearch :: TCPClient err () Nonce8 -> Search NodeId (IP, PortNumber) () NodeInfo NodeInfo nodeSearch tcp = Search { searchSpace = tcpSpace , searchNodeAddress = nodeIP &&& tcpPort , searchQuery = getNodes tcp } -} data TCPClient err tid = TCPClient { tcpCrypto :: TransportCrypto , tcpClient :: Client err PacketNumber tid NodeInfo (Bool,RelayPacket) , tcpGetGateway :: UDP.NodeInfo -> STM (Maybe NodeInfo) } {- getTCPNodes :: TCPClient err () Nonce8 -> NodeId -> NodeInfo -> IO (Maybe ([NodeInfo],[NodeInfo],Maybe ())) getTCPNodes tcp seeking dst = do r <- getUDPNodes' tcp seeking (udpNodeInfo dst) let tcps (ns,_,mb) = (ns',ns',mb) where ns' = do n <- ns [ NodeInfo n (fromIntegral 443) , NodeInfo n (fromIntegral 80) , NodeInfo n (UDP.nodePort n) ] fmap join $ forM r $ \(ns,gw) -> do let ts = tcps ns {- if nodeId gw == nodeId dst then return $ Just ts else do forkIO $ void $ tcpPing (tcpClient tcp) dst return $ Just ts -} forM_ ((\(xs,_,_) -> xs) ts) (forkIO . void . tcpPing (tcpClient tcp)) return $ Just ts -} getUDPNodes :: TCPClient err Nonce8 -> NodeId -> UDP.NodeInfo -> IO (Maybe ([UDP.NodeInfo], [UDP.NodeInfo], Maybe ())) getUDPNodes tcp seeking dst = fmap fst . resultToMaybe <$> getUDPNodes' tcp seeking dst getUDPNodes' :: TCPClient err Nonce8 -> NodeId -> UDP.NodeInfo -> IO (QR.Result (([UDP.NodeInfo], [UDP.NodeInfo], Maybe ()), NodeInfo)) getUDPNodes' tcp seeking dst0 = do goGetUDPNodes tcp seeking dst0 (return Canceled) $ \meth gateway dst -> do r <- sendQuery (tcpClient tcp) meth (AnnounceRequest zeros32 seeking UDP.zeroID) gateway forM r $ \response -> do let (ns,_,mb) = either (const ([],[],Nothing)) (unwrapAnnounceResponse Nothing dst) $ response return ( (ns,ns, const () <$> mb), gateway ) -- Failure case, currently not treated as special. -- The current searchQuery type demands a valid Nonce8 is returned -- even if we were unable to send a query. fixmeNonce :: Nonce8 fixmeNonce = Nonce8 0 asyncUDPNodes :: TCPClient err Nonce8 -> NodeId -> UDP.NodeInfo -> (Nonce8 -> QR.Result (([UDP.NodeInfo], [UDP.NodeInfo], Maybe ()), NodeInfo) -> IO ()) -> IO Nonce8 asyncUDPNodes tcp seeking dst0 withResult = goGetUDPNodes tcp seeking dst0 (return fixmeNonce) $ \meth gateway dst -> do asyncQuery (tcpClient tcp) meth (AnnounceRequest zeros32 seeking UDP.zeroID) gateway $ \qid response -> do let wut response = let (ns,_,mb) = either (const ([],[],Nothing)) (unwrapAnnounceResponse Nothing dst) $ response in ( (ns,ns, const () <$> mb), gateway ) withResult qid $ fmap wut response type Meth x = MethodSerializer Nonce8 x -- NodeInfo (Bool, RelayPacket) PacketNumber AnnounceRequest (Either String AnnounceResponse) goGetUDPNodes :: TCPClient err Nonce8 -> NodeId -> UDP.NodeInfo -> IO a -> (Meth x -> NodeInfo -> UDP.NodeInfo -> IO a) -> IO a goGetUDPNodes tcp seeking dst0 fail go = do mgateway <- atomically $ tcpGetGateway tcp dst0 case mgateway of Nothing -> fail Just gateway -> do (b,c,n24) <- atomically $ do b <- transportNewKey (tcpCrypto tcp) c <- transportNewKey (tcpCrypto tcp) n24 <- transportNewNonce (tcpCrypto tcp) return (b,c,n24) let (dst,gateway') = if UDP.nodeId dst0 == nodeId gateway then ( dst0 { UDP.nodeIP = fromJust $ Network.Address.fromSockAddr localhost4 } , gateway { udpNodeInfo = (udpNodeInfo gateway) { UDP.nodeIP = fromJust $ Network.Address.fromSockAddr localhost4 }}) else (dst0,gateway) wrap2 <- lookupNonceFunction (tcpCrypto tcp) b (UDP.id2key $ UDP.nodeId dst) wrap1 <- lookupNonceFunction (tcpCrypto tcp) c (UDP.id2key $ nodeId gateway) wrap0 <- lookupNonceFunction (tcpCrypto tcp) (transportSecret $ tcpCrypto tcp) (UDP.id2key $ UDP.nodeId dst) let meth :: MethodSerializer Nonce8 a -- NodeInfo (Bool, RelayPacket) PacketNumber AnnounceRequest (Either String AnnounceResponse) meth = MethodSerializer { methodTimeout = \addr -> return (addr,12000000) -- 12 second timeout , method = OnionPacketID -- meth , wrapQuery = \n8 src gateway x -> (,) True $ OnionPacket n24 $ Addressed (UDP.nodeAddr dst) $ wrapOnionPure b (wrap2 n24) (UDP.nodeAddr $ udpNodeInfo gateway') $ wrapOnionPure c (wrap1 n24) (UDP.nodeAddr dst) $ NotForwarded $ encryptPayload (wrap0 n24) $ OnionAnnounce Asymm { senderKey = transportPublic (tcpCrypto tcp) , asymmNonce = n24 , asymmData = pure (x,n8) } , unwrapResponse = \case (_,OnionPacketResponse (OnionAnnounceResponse _ n24' r)) -> decrypt (wrap0 n24') r >>= decodePlain x -> Left $ "getUDPNodes: unwrapResponse fail " ++ show x } go meth gateway dst handleOOB :: PublicKey -> ByteString -> NodeInfo -> NodeInfo -> IO (Maybe (x -> x)) handleOOB k bs src dst = do dput XMisc $ "TODO: handleOOB " ++ show src return Nothing handle2route :: OnionMessage Encrypted -> NodeInfo -> NodeInfo -> IO (Maybe (x -> x)) handle2route o src dst = do dput XMisc $ "TODO: handle2route " ++ show src return Nothing tcpPing :: Show addr => Client err PacketNumber Nonce8 addr (Bool,RelayPacket) -> addr -> IO (Maybe ()) tcpPing client dst = do dput XTCP $ "tcpPing " ++ show dst resultToMaybe <$> sendQuery client meth () dst where meth = MethodSerializer { wrapQuery = \n8 src dst () -> (True,RelayPing n8) , unwrapResponse = \_ -> () , methodTimeout = \dst -> return (dst,5000000) , method = PingPacket } sendConnectionRequest :: Client err PacketNumber tid addr (Bool, RelayPacket) -> PublicKey -> addr -> IO () sendConnectionRequest client pubkey ni = sendMessage (clientNet client) ni (True,RoutingRequest pubkey) tcpConnectionRequest_ :: Client err PacketNumber tid addr (Bool, RelayPacket) -> PublicKey -> addr -> IO (Maybe ConId) tcpConnectionRequest_ client pubkey ni = do resultToMaybe <$> sendQuery client meth pubkey ni where meth = MethodSerializer { wrapQuery = \n8 src dst pubkey -> (True,RoutingRequest pubkey) , unwrapResponse = \(_,RoutingResponse cid pubkey) -> cid , methodTimeout = \dst -> return (dst,5000000) , method = RoutingRequestPacket } type RelayClient = Client String PacketNumber Nonce8 NodeInfo (Bool,RelayPacket) keyToNonce :: PublicKey -> Nonce8 keyToNonce k = unsafeDupablePerformIO $ withByteArray k $ \ptr -> do w8 <- peek ptr return $ Nonce8 w8 type RelayCache = TCPCache (SessionProtocol (SessionData,RelayPacket) RelayPacket) -- | Create a new TCP relay client. Because polymorphic existential record -- updates are currently hard with GHC, this function accepts parameters for -- generalizing the table-entry type for pending transactions. Safe trivial -- defaults are 'id' and 'tryPutMVar'. The resulting customized table state -- will be returned to the caller along with the new client. newClient :: TransportCrypto -> ((Nonce8 -> QR.Result (Bool,RelayPacket) -> IO ()) -> a) -- ^ store mvar for relay query -> (a -> Nonce8 -> RelayPacket -> IO void) -- ^ load mvar for relay query -> (SockAddr -> Nonce8 -> STM (Maybe (OnionDestination RouteId))) -- ^ lookup sender of onion query -> (UDP.NodeInfo -> RouteId -> IO (Maybe OnionRoute)) -- ^ lookup OnionRoute by id -> IO ( ( TVar (ChaChaDRG, Data.Word64Map.Word64Map a) , RelayCache , Transport String ViaRelay ByteString , Transport String (OnionDestination RouteId) (OnionMessage Encrypted) ) , RelayClient ) newClient crypto store load lookupSender getRoute = do let runio io = return () -- TODO: run IO action (tcpcache,net0) <- toxTCP crypto (relaynet,net1) <- partitionRelay runio net0 (onionnet,net2) <- partitionOnion crypto lookupSender getRoute net1 let net3 = {- XXX: Client type forces this pointless layering. -} layerTransport ((Right .) . (,) . (,) False . snd) (,) net2 drg <- drgNew map_var <- atomically $ newTVar (drg, Data.Word64Map.empty) return $ (,) (map_var,tcpcache,relaynet,onionnet) Client { clientNet = net3 , clientDispatcher = DispatchMethods { classifyInbound = (. snd) $ \case RelayPing n -> IsQuery PingPacket n RelayPong n -> IsResponse n RoutingRequest k -> IsQuery RoutingRequestPacket (keyToNonce k) RoutingResponse conId k -> IsResponse (keyToNonce k) OnionPacketResponse (OnionAnnounceResponse n8 n24 ciphered) -> IsResponse n8 OnionPacketResponse o@(OnionToRouteResponse _) -> IsUnsolicited $ handle2route o OOBRecv k bs -> IsUnsolicited $ handleOOB k bs wut -> IsUnknown (show wut) , lookupHandler = \case PingPacket -> Just MethodHandler { methodParse = \case (_,RelayPing n8) -> Right () _ -> Left "TCP: Non-ping?" , methodSerialize = \n8 src dst () -> (False, RelayPong n8) , methodAction = \src () -> dput XTCP $ "TCP pinged by "++show src } w -> Just NoReply { methodParse = \x -> Left $ "tcp-lookuphandler? " ++ (concat $ take 1 $ words $ show x) -- :: x -> Either err a , noreplyAction = \addr a -> dput XTCP $ "tcp-lookupHandler: "++show w } , tableMethods = transactionMethods' store (\qid x -> mapM_ (load qid x . snd)) (contramap (\(Nonce8 w64) -> w64) w64MapMethods) $ first (either error Nonce8 . decode) . randomBytesGenerate 8 } , clientErrorReporter = logErrors , clientPending = map_var , clientAddress = \_ -> return $ NodeInfo { udpNodeInfo = either error id $ UDP.nodeInfo (UDP.key2id $ transportPublic crypto) (SockAddrInet 0 0) , tcpPort = 0 } , clientResponseId = return } data ViaRelay = ViaRelay (Maybe ConId) UDP.NodeId NodeInfo deriving (Eq,Ord,Show) showViaRelay :: ViaRelay -> String showViaRelay (ViaRelay mcon nid tcp) = "TCP:" ++ maybe "(oob)" (\(ConId con) -> "(" ++ show con ++ ")") mcon ++ show nid ++ "@@" ++ show (nodeAddr tcp) partitionRelay :: (IO () -> STM ()) -> TransportA err NodeInfo (SessionData,RelayPacket) (Bool,RelayPacket) -> IO ( Transport err ViaRelay ByteString , TransportA err NodeInfo (SessionData,RelayPacket) (Bool,RelayPacket)) partitionRelay runio tr = partitionTransportM parse encode tr where parse :: ((SessionData,RelayPacket), NodeInfo) -> STM (Either (ByteString, ViaRelay) ((SessionData,RelayPacket),NodeInfo)) parse ((st,RelayData bs conid), ni) = do nid <- getRelayedRemote st conid return $ Left (bs, ViaRelay (Just conid) nid ni) parse ((_,OOBRecv key bs), ni) = return $ Left (bs, ViaRelay Nothing (UDP.key2id key) ni) parse ((st,RoutingResponse conid k),ni) = do setRelayedRemote st conid (UDP.key2id k) -- Note: Rewriting inbound RoutingResponse to be a RoutingRequest -- instead. This is because the routing reqest is not yet fullfilled -- until a ConnectNotification is received. -- -- We could use Left here instead as inbound RoutingRequest packets are -- not normally responded to by a client. return $ Right ((st,RoutingRequest k),ni) parse ((st,ConnectNotification conid),ni) = do nid <- getRelayedRemote st conid -- Note: Rewriting inbound ConnectNotification to a RoutingResponse -- because we want to include the public key and connection id in a -- single message. return $ Right ((st,RoutingResponse conid (UDP.id2key nid)),ni) parse passthrough = return $ Right passthrough encode :: (ByteString, ViaRelay) -> IO (Maybe ((Bool,RelayPacket), NodeInfo)) encode (bs, ViaRelay (Just conid) _ ni) = return $ Just ((False,RelayData bs conid), ni) encode (bs, ViaRelay Nothing nid ni) = return $ Just ((True,OOBSend (UDP.id2key nid) bs), ni) partitionOnion :: TransportCrypto -> (SockAddr -> Nonce8 -> STM (Maybe (OnionDestination RouteId))) -> (UDP.NodeInfo -> RouteId -> IO (Maybe OnionRoute)) -> TransportA err NodeInfo (SessionData,RelayPacket) (Bool,RelayPacket) -> IO ( Transport err (OnionDestination RouteId) (OnionMessage Encrypted) , TransportA err NodeInfo (SessionData,RelayPacket) (Bool,RelayPacket)) partitionOnion crypto lookupSender getRoute tr = partitionTransportM parse encode tr where parse :: ((SessionData,RelayPacket), NodeInfo) -> STM (Either (OnionMessage Encrypted , OnionDestination RouteId) ((SessionData,RelayPacket), NodeInfo)) parse pass@((_,OnionPacketResponse msg@(OnionAnnounceResponse n8 _ _)), nodeA) = do m <- lookupSender (nodeAddr nodeA) n8 case m of Nothing -> return $ Right pass Just od -> return $ Left (msg, od) parse ((_,OnionPacketResponse msg@(OnionToRouteResponse asym)), nodeA) = do -- dput XOnion $ "TCP data-to-route response from " ++ show (UDP.key2id $ senderKey asym) return $ let Right ni = UDP.nodeInfo (UDP.key2id $ senderKey asym) nullAddress4 -- -- We have this information, but currently, we're discarding it... -- r = dummyRoute { routeNodeA = udpNodeInfo nodeA -- , routeRelayPort = Just $ tcpPort nodeA } tryAllKeys = SearchingAlias -- We unfortunately don't know what toxid was used to encrypt this. -- Toxcore only supports a single toxid per DHT node and in that case, -- it is unambiguous. in Left (msg, OnionDestination tryAllKeys ni Nothing) parse pass = return $ Right pass encode :: (OnionMessage Encrypted,OnionDestination RouteId) -> IO (Maybe ((Bool,RelayPacket),NodeInfo)) encode (msg,OnionDestination _ ni (Just rid)) = do moroute <- getRoute ni rid forM (moroute >>= \r -> (,) r <$> routeRelayPort r) $ \(oroute,tcpport) -> wrapIndirectHops crypto msg ni oroute $ \nonce saddr fwd -> return ( (True,OnionPacket nonce $ Addressed saddr fwd) , NodeInfo (routeNodeA oroute) tcpport ) encode _ = return Nothing