diff options
Diffstat (limited to 'src/Network/BitTorrent/Exchange/Message.hs')
-rw-r--r-- | src/Network/BitTorrent/Exchange/Message.hs | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/src/Network/BitTorrent/Exchange/Message.hs b/src/Network/BitTorrent/Exchange/Message.hs new file mode 100644 index 00000000..4ef7baf3 --- /dev/null +++ b/src/Network/BitTorrent/Exchange/Message.hs | |||
@@ -0,0 +1,400 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Normally peer to peer communication consisting of the following | ||
9 | -- steps: | ||
10 | -- | ||
11 | -- * In order to establish the connection between peers we should | ||
12 | -- send 'Handshake' message. The 'Handshake' is a required message | ||
13 | -- and must be the first message transmitted by the peer to the | ||
14 | -- another peer. Another peer should reply with a handshake as well. | ||
15 | -- | ||
16 | -- * Next peer might sent bitfield message, but might not. In the | ||
17 | -- former case we should update bitfield peer have. Again, if we | ||
18 | -- have some pieces we should send bitfield. Normally bitfield | ||
19 | -- message should sent after the handshake message. | ||
20 | -- | ||
21 | -- * Regular exchange messages. TODO docs | ||
22 | -- | ||
23 | -- For more high level API see "Network.BitTorrent.Exchange" module. | ||
24 | -- | ||
25 | -- For more infomation see: | ||
26 | -- <https://wiki.theory.org/BitTorrentSpecification#Peer_wire_protocol_.28TCP.29> | ||
27 | -- | ||
28 | {-# LANGUAGE TemplateHaskell #-} | ||
29 | {-# OPTIONS -fno-warn-orphans #-} | ||
30 | module Network.BitTorrent.Exchange.Protocol | ||
31 | ( -- * Initial handshake | ||
32 | Handshake(..) | ||
33 | , handshake | ||
34 | , handshakeCaps | ||
35 | , recvHandshake | ||
36 | , sendHandshake | ||
37 | |||
38 | -- ** Defaults | ||
39 | , defaultHandshake | ||
40 | , defaultBTProtocol | ||
41 | , defaultReserved | ||
42 | , handshakeMaxSize | ||
43 | |||
44 | -- * Regular messages | ||
45 | , Message(..) | ||
46 | ) where | ||
47 | |||
48 | import Control.Applicative | ||
49 | import Control.Exception | ||
50 | import Control.Monad | ||
51 | import Data.Binary as B | ||
52 | import Data.Binary.Get as B | ||
53 | import Data.Binary.Put as B | ||
54 | import Data.ByteString as BS | ||
55 | import Data.ByteString.Char8 as BC | ||
56 | import Data.ByteString.Lazy as BL | ||
57 | import Data.Default | ||
58 | import Data.Serialize as S | ||
59 | import Network | ||
60 | import Network.Socket.ByteString | ||
61 | import Text.PrettyPrint | ||
62 | import Text.PrettyPrint.Class | ||
63 | |||
64 | import Data.Torrent.Bitfield | ||
65 | import Data.Torrent.Block | ||
66 | import Data.Torrent.InfoHash | ||
67 | import Network.BitTorrent.Extension | ||
68 | import Network.BitTorrent.Core.PeerId | ||
69 | import Network.BitTorrent.Core.PeerAddr () | ||
70 | |||
71 | |||
72 | getInt :: S.Get Int | ||
73 | getInt = fromIntegral <$> S.getWord32be | ||
74 | {-# INLINE getInt #-} | ||
75 | |||
76 | putInt :: S.Putter Int | ||
77 | putInt = S.putWord32be . fromIntegral | ||
78 | {-# INLINE putInt #-} | ||
79 | |||
80 | getIntB :: B.Get Int | ||
81 | getIntB = fromIntegral <$> B.getWord32be | ||
82 | {-# INLINE getIntB #-} | ||
83 | |||
84 | putIntB :: Int -> B.Put | ||
85 | putIntB = B.putWord32be . fromIntegral | ||
86 | {-# INLINE putIntB #-} | ||
87 | |||
88 | {----------------------------------------------------------------------- | ||
89 | Handshake | ||
90 | -----------------------------------------------------------------------} | ||
91 | |||
92 | -- | Handshake message is used to exchange all information necessary | ||
93 | -- to establish connection between peers. | ||
94 | -- | ||
95 | data Handshake = Handshake { | ||
96 | -- | Identifier of the protocol. | ||
97 | hsProtocol :: BS.ByteString | ||
98 | |||
99 | -- | Reserved bytes used to specify supported BEP's. | ||
100 | , hsReserved :: Capabilities | ||
101 | |||
102 | -- | Info hash of the info part of the metainfo file. that is | ||
103 | -- transmitted in tracker requests. Info hash of the initiator | ||
104 | -- handshake and response handshake should match, otherwise | ||
105 | -- initiator should break the connection. | ||
106 | -- | ||
107 | , hsInfoHash :: InfoHash | ||
108 | |||
109 | -- | Peer id of the initiator. This is usually the same peer id | ||
110 | -- that is transmitted in tracker requests. | ||
111 | -- | ||
112 | , hsPeerId :: PeerId | ||
113 | |||
114 | } deriving (Show, Eq) | ||
115 | |||
116 | instance Serialize Handshake where | ||
117 | put hs = do | ||
118 | S.putWord8 (fromIntegral (BS.length (hsProtocol hs))) | ||
119 | S.putByteString (hsProtocol hs) | ||
120 | S.putWord64be (hsReserved hs) | ||
121 | S.put (hsInfoHash hs) | ||
122 | S.put (hsPeerId hs) | ||
123 | |||
124 | get = do | ||
125 | len <- S.getWord8 | ||
126 | Handshake <$> S.getBytes (fromIntegral len) | ||
127 | <*> S.getWord64be | ||
128 | <*> S.get | ||
129 | <*> S.get | ||
130 | |||
131 | instance Pretty Handshake where | ||
132 | pretty Handshake {..} | ||
133 | = text (BC.unpack hsProtocol) <+> pretty (clientInfo hsPeerId) | ||
134 | |||
135 | -- | Extract capabilities from a peer handshake message. | ||
136 | handshakeCaps :: Handshake -> Capabilities | ||
137 | handshakeCaps = hsReserved | ||
138 | |||
139 | |||
140 | -- | Get handshake message size in bytes from the length of protocol | ||
141 | -- string. | ||
142 | handshakeSize :: Word8 -> Int | ||
143 | handshakeSize n = 1 + fromIntegral n + 8 + 20 + 20 | ||
144 | |||
145 | -- | Maximum size of handshake message in bytes. | ||
146 | handshakeMaxSize :: Int | ||
147 | handshakeMaxSize = handshakeSize 255 | ||
148 | |||
149 | -- | Default protocol string "BitTorrent protocol" as is. | ||
150 | defaultBTProtocol :: BS.ByteString | ||
151 | defaultBTProtocol = "BitTorrent protocol" | ||
152 | |||
153 | -- | Default reserved word is 0. | ||
154 | defaultReserved :: Word64 | ||
155 | defaultReserved = 0 | ||
156 | |||
157 | -- | Length of info hash and peer id is unchecked, so it /should/ be | ||
158 | -- equal 20. | ||
159 | defaultHandshake :: InfoHash -> PeerId -> Handshake | ||
160 | defaultHandshake = Handshake defaultBTProtocol defaultReserved | ||
161 | |||
162 | sendHandshake :: Socket -> Handshake -> IO () | ||
163 | sendHandshake sock hs = sendAll sock (S.encode hs) | ||
164 | |||
165 | recvHandshake :: Socket -> IO Handshake | ||
166 | recvHandshake sock = do | ||
167 | header <- recv sock 1 | ||
168 | unless (BS.length header == 1) $ | ||
169 | throw $ userError "Unable to receive handshake header." | ||
170 | |||
171 | let protocolLen = BS.head header | ||
172 | let restLen = handshakeSize protocolLen - 1 | ||
173 | |||
174 | body <- recv sock restLen | ||
175 | let resp = BS.cons protocolLen body | ||
176 | either (throwIO . userError) return $ S.decode resp | ||
177 | |||
178 | -- | Handshaking with a peer specified by the second argument. | ||
179 | handshake :: Socket -> Handshake -> IO Handshake | ||
180 | handshake sock hs = do | ||
181 | sendHandshake sock hs | ||
182 | hs' <- recvHandshake sock | ||
183 | when (hsInfoHash hs /= hsInfoHash hs') $ do | ||
184 | throwIO $ userError "Handshake info hash do not match." | ||
185 | return hs' | ||
186 | |||
187 | {----------------------------------------------------------------------- | ||
188 | Regular messages | ||
189 | -----------------------------------------------------------------------} | ||
190 | |||
191 | data StatusUpdate | ||
192 | = Choke | ||
193 | | Unchoke | ||
194 | | Interested | ||
195 | | NotInterested | ||
196 | deriving (Show, Eq, Ord, Enum, Bounded) | ||
197 | |||
198 | data RegularMessage = | ||
199 | -- | Zero-based index of a piece that has just been successfully | ||
200 | -- downloaded and verified via the hash. | ||
201 | Have ! PieceIx | ||
202 | |||
203 | -- | The bitfield message may only be sent immediately after the | ||
204 | -- handshaking sequence is complete, and before any other message | ||
205 | -- are sent. If client have no pieces then bitfield need not to be | ||
206 | -- sent. | ||
207 | | Bitfield !Bitfield | ||
208 | |||
209 | -- | Request for a particular block. If a client is requested a | ||
210 | -- block that another peer do not have the peer might not answer | ||
211 | -- at all. | ||
212 | | Request ! BlockIx | ||
213 | |||
214 | -- | Response to a request for a block. | ||
215 | | Piece !(Block BL.ByteString) | ||
216 | |||
217 | -- | Used to cancel block requests. It is typically used during | ||
218 | -- "End Game". | ||
219 | | Cancel !BlockIx | ||
220 | deriving (Show, Eq) | ||
221 | |||
222 | data DHTMessage | ||
223 | = Port !PortNumber | ||
224 | deriving (Show, Eq) | ||
225 | |||
226 | -- | BEP6 messages. | ||
227 | data FastMessage = | ||
228 | -- | If a peer have all pieces it might send the 'HaveAll' message | ||
229 | -- instead of 'Bitfield' message. Used to save bandwidth. | ||
230 | HaveAll | ||
231 | |||
232 | -- | If a peer have no pieces it might send 'HaveNone' message | ||
233 | -- intead of 'Bitfield' message. Used to save bandwidth. | ||
234 | | HaveNone | ||
235 | |||
236 | -- | This is an advisory message meaning "you might like to | ||
237 | -- download this piece." Used to avoid excessive disk seeks and | ||
238 | -- amount of IO. | ||
239 | | SuggestPiece !PieceIx | ||
240 | |||
241 | -- | Notifies a requesting peer that its request will not be satisfied. | ||
242 | | RejectRequest !BlockIx | ||
243 | |||
244 | -- | This is an advisory messsage meaning "if you ask for this | ||
245 | -- piece, I'll give it to you even if you're choked." Used to | ||
246 | -- shorten starting phase. | ||
247 | | AllowedFast !PieceIx | ||
248 | deriving (Show, Eq) | ||
249 | |||
250 | -- TODO make Network.BitTorrent.Exchange.Session | ||
251 | |||
252 | -- | Messages used in communication between peers. | ||
253 | -- | ||
254 | -- Note: If some extensions are disabled (not present in extension | ||
255 | -- mask) and client receive message used by the disabled | ||
256 | -- extension then the client MUST close the connection. | ||
257 | -- | ||
258 | data Message | ||
259 | -- core | ||
260 | = KeepAlive | ||
261 | | Status !StatusUpdate | ||
262 | | Regular !RegularMessage | ||
263 | |||
264 | -- extensions | ||
265 | | DHT !DHTMessage | ||
266 | | Fast !FastMessage | ||
267 | deriving (Show, Eq) | ||
268 | |||
269 | instance Default Message where | ||
270 | def = KeepAlive | ||
271 | {-# INLINE def #-} | ||
272 | {- | ||
273 | -- | Payload bytes are omitted. | ||
274 | instance Pretty Message where | ||
275 | pretty (Bitfield _) = "Bitfield" | ||
276 | pretty (Piece blk) = "Piece" <+> pretty blk | ||
277 | pretty (Cancel i ) = "Cancel" <+> pretty i | ||
278 | pretty (SuggestPiece pix) = "Suggest" <+> int pix | ||
279 | pretty (RejectRequest i ) = "Reject" <+> pretty i | ||
280 | pretty msg = text (show msg) | ||
281 | |||
282 | instance Serialize Message where | ||
283 | get = do | ||
284 | len <- getInt | ||
285 | -- _ <- lookAhead $ ensure len | ||
286 | if len == 0 then return KeepAlive | ||
287 | else do | ||
288 | mid <- S.getWord8 | ||
289 | case mid of | ||
290 | 0x00 -> return Choke | ||
291 | 0x01 -> return Unchoke | ||
292 | 0x02 -> return Interested | ||
293 | 0x03 -> return NotInterested | ||
294 | 0x04 -> Have <$> getInt | ||
295 | 0x05 -> (Bitfield . fromBitmap) <$> S.getByteString (pred len) | ||
296 | 0x06 -> Request <$> S.get | ||
297 | 0x07 -> Piece <$> getBlock (len - 9) | ||
298 | 0x08 -> Cancel <$> S.get | ||
299 | 0x09 -> Port <$> S.get | ||
300 | 0x0D -> SuggestPiece <$> getInt | ||
301 | 0x0E -> return HaveAll | ||
302 | 0x0F -> return HaveNone | ||
303 | 0x10 -> RejectRequest <$> S.get | ||
304 | 0x11 -> AllowedFast <$> getInt | ||
305 | _ -> do | ||
306 | rm <- S.remaining >>= S.getBytes | ||
307 | fail $ "unknown message ID: " ++ show mid ++ "\n" | ||
308 | ++ "remaining available bytes: " ++ show rm | ||
309 | |||
310 | where | ||
311 | getBlock :: Int -> S.Get (Block BL.ByteString) | ||
312 | getBlock len = Block <$> getInt <*> getInt <*> S.getLazyByteString (fromIntegral len) | ||
313 | {-# INLINE getBlock #-} | ||
314 | |||
315 | |||
316 | put KeepAlive = putInt 0 | ||
317 | put Choke = putInt 1 >> S.putWord8 0x00 | ||
318 | put Unchoke = putInt 1 >> S.putWord8 0x01 | ||
319 | put Interested = putInt 1 >> S.putWord8 0x02 | ||
320 | put NotInterested = putInt 1 >> S.putWord8 0x03 | ||
321 | put (Have i) = putInt 5 >> S.putWord8 0x04 >> putInt i | ||
322 | put (Bitfield bf) = putInt l >> S.putWord8 0x05 >> S.putLazyByteString b | ||
323 | where b = toBitmap bf | ||
324 | l = succ (fromIntegral (BL.length b)) | ||
325 | {-# INLINE l #-} | ||
326 | put (Request blk) = putInt 13 >> S.putWord8 0x06 >> S.put blk | ||
327 | put (Piece blk) = putInt l >> S.putWord8 0x07 >> putBlock | ||
328 | where l = 9 + fromIntegral (BL.length (blkData blk)) | ||
329 | {-# INLINE l #-} | ||
330 | putBlock = do putInt (blkPiece blk) | ||
331 | putInt (blkOffset blk) | ||
332 | S.putLazyByteString (blkData blk) | ||
333 | {-# INLINE putBlock #-} | ||
334 | |||
335 | put (Cancel blk) = putInt 13 >> S.putWord8 0x08 >> S.put blk | ||
336 | put (Port p ) = putInt 3 >> S.putWord8 0x09 >> S.put p | ||
337 | put HaveAll = putInt 1 >> S.putWord8 0x0E | ||
338 | put HaveNone = putInt 1 >> S.putWord8 0x0F | ||
339 | put (SuggestPiece pix) = putInt 5 >> S.putWord8 0x0D >> putInt pix | ||
340 | put (RejectRequest i ) = putInt 13 >> S.putWord8 0x10 >> S.put i | ||
341 | put (AllowedFast i ) = putInt 5 >> S.putWord8 0x11 >> putInt i | ||
342 | -} | ||
343 | {- | ||
344 | instance Binary Message where | ||
345 | get = do | ||
346 | len <- getIntB | ||
347 | -- _ <- lookAhead $ ensure len | ||
348 | if len == 0 then return KeepAlive | ||
349 | else do | ||
350 | mid <- B.getWord8 | ||
351 | case mid of | ||
352 | 0x00 -> return Choke | ||
353 | 0x01 -> return Unchoke | ||
354 | 0x02 -> return Interested | ||
355 | 0x03 -> return NotInterested | ||
356 | 0x04 -> Have <$> getIntB | ||
357 | 0x05 -> (Bitfield . fromBitmap) <$> B.getByteString (pred len) | ||
358 | 0x06 -> Request <$> B.get | ||
359 | 0x07 -> Piece <$> getBlock (len - 9) | ||
360 | 0x08 -> Cancel <$> B.get | ||
361 | 0x09 -> (Port . fromIntegral) <$> B.getWord16be | ||
362 | 0x0E -> return HaveAll | ||
363 | 0x0F -> return HaveNone | ||
364 | 0x0D -> SuggestPiece <$> getIntB | ||
365 | 0x10 -> RejectRequest <$> B.get | ||
366 | 0x11 -> AllowedFast <$> getIntB | ||
367 | _ -> fail $ "unknown message ID: " ++ show mid | ||
368 | where | ||
369 | getBlock :: Int -> B.Get (Block BL.ByteString) | ||
370 | getBlock len = Block <$> getIntB <*> getIntB | ||
371 | <*> B.getLazyByteString (fromIntegral len) | ||
372 | {-# INLINE getBlock #-} | ||
373 | |||
374 | put KeepAlive = putIntB 0 | ||
375 | put Choke = putIntB 1 >> B.putWord8 0x00 | ||
376 | put Unchoke = putIntB 1 >> B.putWord8 0x01 | ||
377 | put Interested = putIntB 1 >> B.putWord8 0x02 | ||
378 | put NotInterested = putIntB 1 >> B.putWord8 0x03 | ||
379 | put (Have i) = putIntB 5 >> B.putWord8 0x04 >> putIntB i | ||
380 | put (Bitfield bf) = putIntB l >> B.putWord8 0x05 >> B.putLazyByteString b | ||
381 | where b = toBitmap bf | ||
382 | l = succ (fromIntegral (BL.length b)) | ||
383 | {-# INLINE l #-} | ||
384 | put (Request blk) = putIntB 13 >> B.putWord8 0x06 >> B.put blk | ||
385 | put (Piece blk) = putIntB l >> B.putWord8 0x07 >> putBlock | ||
386 | where l = 9 + fromIntegral (BL.length (blkData blk)) | ||
387 | {-# INLINE l #-} | ||
388 | putBlock = do putIntB (blkPiece blk) | ||
389 | putIntB (blkOffset blk) | ||
390 | B.putLazyByteString (blkData blk) | ||
391 | {-# INLINE putBlock #-} | ||
392 | |||
393 | put (Cancel blk) = putIntB 13 >> B.putWord8 0x08 >> B.put blk | ||
394 | put (Port p ) = putIntB 3 >> B.putWord8 0x09 >> B.putWord16be (fromIntegral p) | ||
395 | put HaveAll = putIntB 1 >> B.putWord8 0x0E | ||
396 | put HaveNone = putIntB 1 >> B.putWord8 0x0F | ||
397 | put (SuggestPiece pix) = putIntB 5 >> B.putWord8 0x0D >> putIntB pix | ||
398 | put (RejectRequest i ) = putIntB 13 >> B.putWord8 0x10 >> B.put i | ||
399 | put (AllowedFast i ) = putIntB 5 >> B.putWord8 0x11 >> putIntB i | ||
400 | -} \ No newline at end of file | ||