summaryrefslogtreecommitdiff
path: root/dht/src/Network/Tox/TCP.hs
blob: 13da804f33dcf8deb4df94a3f4e9a6f41a9f1f02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
{-# 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.Functor.Contravariant
import Data.Functor.Identity
import Data.Hashable
import qualified Data.HashMap.Strict as HashMap
import Data.IP
import Data.Maybe
import Data.Monoid
import Data.Serialize
import Data.Word
import qualified Data.Vector as Vector
import Network.Socket (SockAddr(..))
import qualified Text.ParserCombinators.ReadP as RP
import System.IO.Error
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)
import Network.Kademlia.Routing
import Network.Kademlia.Search hiding (sendQuery)
import Network.QueryResponse
import Network.QueryResponse.TCP
import Network.Tox.DHT.Handlers (toxSpace)
import Network.Tox.Onion.Transport hiding (encrypt,decrypt)
import Network.Tox.Onion.Handlers (unwrapAnnounceResponse)
import qualified Network.Tox.NodeId as UDP


withSize :: Sized x => (Size x -> m (p x)) -> m (p x)
withSize f = case size of len -> f len


type NodeId = UDP.NodeId

-- example:
-- KEyW2Bm.S-DpIGp72380BAfgintUWX1KX.6ZU.4m5Ex@80.99.99.99:33400{tcp:443}
instance Show NodeInfo where
    show (NodeInfo udp port) = show udp ++ "{tcp:"++show port++"}"

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 -> StreamHandshake NodeInfo x y
tcpStream crypto = 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 <- withSize $ fmap decode . hGet h . constSize
        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 (\_ -> 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
        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
                                    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 r
                 in bracket (takeMVar hvar) (putMVar hvar)
                     $ \h -> go h `catchIOError` \e -> do
                                        dput XTCP $ "TCP exception: " ++ show e
                                        return Nothing
            , streamEncode = \y -> do
                dput XTCP $ "TCP(acquire nonce):" ++ show addr ++ " <-- " ++ show y
                n24 <- takeMVar nsend
                dput XTCP $ "TCP(got nonce):" ++ show addr ++ " <-- " ++ show y
                let bs = encode $ encrypt (noncef' n24) $ encodePlain y
                ($ h) -- bracket (takeMVar hvar) (putMVar hvar)
                    $ \h -> hPut h (encode (fromIntegral $ Data.ByteString.length bs :: Word16) <> bs)
                                `catchIOError` \e -> dput XTCP $ "TCP write exception: " ++ show e
                dput XTCP $ "TCP(incrementing nonce): " ++ show addr ++ " <-- " ++ show y
                putMVar nsend (incrementNonce24 n24)
                dput XTCP $ "TCP(finished): " ++ show addr ++ " <-- " ++ show y
            }
    , streamAddr = nodeAddr
    }

toxTCP :: TransportCrypto -> IO ( TCPCache (SessionProtocol RelayPacket RelayPacket)
                                , TransportA err NodeInfo RelayPacket (Bool,RelayPacket) )
toxTCP crypto = tcpTransport 30 (tcpStream crypto)

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 <$> getUDPNodes' tcp seeking dst

getUDPNodes' :: TCPClient err Nonce8 -> NodeId -> UDP.NodeInfo -> IO (Maybe (([UDP.NodeInfo], [UDP.NodeInfo], Maybe ()), NodeInfo))
getUDPNodes' tcp seeking dst0 = do
    mgateway <- atomically $ tcpGetGateway tcp dst0
    fmap join $ forM mgateway $ \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 = \tid 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) (nodeAddr 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
                    }
        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 )


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
    sendQuery client meth () dst
 where meth = MethodSerializer
                { wrapQuery      = \n8 src dst () -> (True,RelayPing n8)
                , unwrapResponse = \_ -> ()
                , methodTimeout  = \n8 dst -> return (dst,5000000)
                , method         = PingPacket
                }

type RelayClient = Client String PacketNumber Nonce8 NodeInfo (Bool,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
               -> ((Maybe (Bool,RelayPacket) -> IO ()) -> a) -- ^ store mvar for query
               -> (a -> RelayPacket -> IO void)  -- ^ load mvar for query
               -> IO ( ( TVar (ChaChaDRG, Data.Word64Map.Word64Map a)
                       , TCPCache (SessionProtocol RelayPacket RelayPacket) )
                     , Client String PacketNumber Nonce8 NodeInfo (Bool,RelayPacket))
newClient crypto store load = do
    (tcpcache,net) <- toxTCP crypto
    drg <- drgNew
    map_var <- atomically $ newTVar (drg, Data.Word64Map.empty)
    return $ (,) (map_var,tcpcache) Client
        { clientNet = {- XXX: Client type forces this pointless layering. -} layerTransport ((Right .) . (,) . (,) False) (,) net
        , clientDispatcher = DispatchMethods
            { classifyInbound = (. snd) $ \case
                RelayPing n           -> IsQuery PingPacket n
                RelayPong n           -> IsResponse n
                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 -> trace ("tcp-received-ping") $ Just MethodHandler
                    { methodParse     = \case (_,RelayPing n8) -> Right ()
                                              _                -> trace ("tcp-non-ping") $ Left "TCP: Non-ping?"
                    , methodSerialize = \n8 src dst () -> trace ("tcp-made-pong-"++show n8) (False, RelayPong n8)
                    , methodAction    = \src () -> dput XTCP $ "TCP pinged by "++show src
                    }
                w -> trace ("tcp-lookupHandler: "++show w) $ Just NoReply
                            { methodParse = \x -> Left "tcp-lookuphandler?" -- :: x -> Either err a
                            , noreplyAction = \addr a -> dput XTCP $ "tcp-lookupHandler: "++show w
                            }
            , tableMethods = transactionMethods' store (\x -> mapM_ (load 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
        }