├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── wire.ex └── wire │ ├── decoder.ex │ └── encoder.ex ├── mix.exs ├── mix.lock └── test ├── decoder_test.exs ├── encode_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | otp_release: 3 | - 17.0 4 | env: MIX_ENV=test 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexander Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Wire 2 | ==== 3 | 4 | [![Build Status](https://travis-ci.org/alehander42/wire.svg)](https://travis-ci.org/alehander42/wire/) 5 | 6 | Wire is an elixir package for decoding and encoding 7 | bittorrent peer wire protocol messages. 8 | 9 | 10 | A message is represented in the library as a keyword list with 11 | `:type`: the type of the message (`:keep_alive`, :`not_interested`, etc) 12 | and the other fields of the message, e.g. 13 | 14 | ```elixir 15 | h = [type: :have, piece_index: 4] 16 | ``` 17 | 18 | ```elixir 19 | 20 | Wire.encode [type: :interested] # <<0, 0, 0, 1, 2>> 21 | Wire.encode [type: :bitfield, field: <<0, 4>>] # <<0, 0, 0, 3, 5, 0, 4>> 22 | ``` 23 | 24 | `decode_messages` decodes a binary containing 0 or more messages and 25 | returns a list of messages and the remaining bytes 26 | 27 | ```elixir 28 | 29 | 30 | Wire.decode_messages(<<0, 0, 0, 6, 5, 0, 2, 4, 3, 1>>) 31 | # {[[type: :bitfield, field: <<0, 2, 4, 3, 1>>]], <<>>} 32 | Wire.decode_messages(<< 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0xf, 5 >>) 33 | # {[[type: :keep_alive], [type: :interested]], <<0, 0, 0, 0xf, 5>>}) 34 | 35 | 36 | ``` 37 | 38 | ## Install 39 | 40 | Add to your mix.exs deps 41 | 42 | ```elixir 43 | {:wire, "~> 0.2.0"} 44 | ``` 45 | 46 | ## Development 47 | 48 | Built and maintained by Alexander Ivanov, with substantial contributions by [Florian Adamsky](https://github.com/cit) 49 | 50 | ## LICENSE 51 | 52 | MIT 53 | 54 | ## Copyright 55 | 56 | Copyright (c) 2014-2015 Alexander Ivanov. See [LICENSE](LICENSE) for further details 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/wire.ex: -------------------------------------------------------------------------------- 1 | defmodule Wire do 2 | defdelegate encode(message), to: Wire.Encoder 3 | defdelegate decode_messages(message), to: Wire.Decoder 4 | defdelegate decode_message(message), to: Wire.Decoder 5 | end 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/wire/decoder.ex: -------------------------------------------------------------------------------- 1 | defmodule Wire.Decoder do 2 | 3 | @doc ~S""" 4 | Parses a binary containing 0 or more messages and 5 | returns a list with messages and the unparsed part of the message 6 | 7 | """ 8 | @spec decode_messages(binary) :: {List.Keyword.t, binary} 9 | def decode_messages(binary) do 10 | decode_messages(binary, []) 11 | end 12 | 13 | def decode_messages(s, acc) do 14 | << l :: 32-integer-big-unsigned, rest :: binary >> = s 15 | << pstrlen :: size(8), _rest :: binary >> = s 16 | 17 | if byte_size(rest) < l and pstrlen != 19 do 18 | { acc, s } 19 | else 20 | { message, rest } = decode_message(s) 21 | if byte_size(rest) > 0 do 22 | decode_messages(rest, acc ++ [message]) 23 | else 24 | { acc ++ [message], rest } 25 | end 26 | end 27 | end 28 | 29 | def decode_message(message) do 30 | << pstrlen :: size(8), 31 | rest :: binary >> = message 32 | 33 | if pstrlen == 19 do 34 | << _pstr :: binary-size(19), ## "BitTorrent protocol" 35 | extension :: binary-size(8), 36 | info_hash :: binary-size(20), 37 | peer_id :: binary-size(20), 38 | rest :: binary >> = rest 39 | 40 | {[type: :handshake, extension: extension, 41 | info_hash: info_hash, peer_id: peer_id], rest} 42 | else 43 | << len :: 32-integer-big-unsigned, 44 | rest :: binary >> = message 45 | 46 | if len == 0 do 47 | {[type: :keep_alive], rest} 48 | else 49 | << id, rest :: binary >> = rest 50 | 51 | decode_message_type(len, id, rest) 52 | end 53 | end 54 | end 55 | 56 | def decode_message_type(len, id, rest) when id == 20 do 57 | msg_len = len - 2 58 | << ext_msg_id :: size(8), 59 | message :: binary-size(msg_len), 60 | rest :: binary >> = rest 61 | {[type: :ltep, ext_msg_id: ext_msg_id, msg: Bencoder.decode(message)], rest} 62 | end 63 | 64 | def decode_message_type(_len, id, rest) when id == 9 do 65 | << port :: 16-integer-big-unsigned, rest :: binary >> = rest 66 | {[type: :port, listen_port: port], rest} 67 | end 68 | 69 | def decode_message_type(len, id, rest) when id == 7 do 70 | block_length = len - 9 71 | << index :: 32-integer-big-unsigned, 72 | begin :: 32-integer-big-unsigned, 73 | block :: binary-size(block_length), 74 | rest :: binary >> = rest 75 | {[type: :piece, index: index, begin: begin, block: block], rest} 76 | end 77 | 78 | def decode_message_type(_len, id, rest) when id in [8, 6] do 79 | << index :: 32-integer-big-unsigned, 80 | begin :: 32-integer-big-unsigned, 81 | length :: 32-integer-big-unsigned, 82 | rest :: binary >> = rest 83 | type = if id == 8 do :cancel else :request end 84 | {[type: type, index: index, begin: begin, length: length], rest} 85 | end 86 | 87 | def decode_message_type(len, id, rest) when id == 5 do 88 | l2 = len - 1 89 | << field :: binary-size(l2), rest :: binary >> = rest 90 | {[type: :bitfield, field: field], rest} 91 | end 92 | 93 | def decode_message_type(_len, id, rest) when id == 4 do 94 | << piece_index :: 32-integer-big-unsigned, rest :: binary >> = rest 95 | {[type: :have, piece_index: piece_index], rest} 96 | end 97 | 98 | def decode_message_type(_len, id, rest) when id == 3 do 99 | {[type: :not_interested], rest} 100 | end 101 | 102 | def decode_message_type(_len, id, rest) when id == 2 do 103 | {[type: :interested], rest} 104 | 105 | end 106 | 107 | def decode_message_type(_len, id, rest) when id == 1 do 108 | {[type: :unchoke], rest} 109 | end 110 | 111 | def decode_message_type(_len, id, rest) when id == 0 do 112 | {[type: :choke], rest} 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /lib/wire/encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule Wire.Encoder do 2 | 3 | def encode(type: :handshake, extensions: extensions, info_hash: info_hash, 4 | peer_id: peer_id) do 5 | <<19 :: size(8), 6 | "BitTorrent protocol" :: binary, 7 | extensions :: binary-size(8), 8 | info_hash :: binary-size(20), 9 | peer_id :: binary-size(20)>> 10 | end 11 | 12 | def encode(type: :ltep, ext_msg_id: ext_msg_id, msg: msg) when is_map(msg) do 13 | bencoded_msg = Bencoder.encode(msg) 14 | length = byte_size(bencoded_msg) + 2 15 | 16 | << length :: 32-integer-big-unsigned, 17 | 20 :: size(8), 18 | ext_msg_id :: size(8), 19 | bencoded_msg :: binary >> 20 | end 21 | 22 | 23 | def encode(type: :port, listen_port: listen_port) do 24 | << 0, 0, 0, 3, listen_port :: 16-integer-big-unsigned, 0 >> 25 | end 26 | 27 | def encode(type: request_or_cancel, index: index, begin: begin, length: length) when request_or_cancel in [:cancel, :request] do 28 | a = if request_or_cancel == :cancel do 8 else 6 end 29 | 30 | << 0, 0, 0, 0xd, a, 31 | index :: 32-integer-big-unsigned, 32 | begin :: 32-integer-big-unsigned, 33 | length :: 32-integer-big-unsigned >> 34 | end 35 | 36 | def encode(type: :piece, index: index, begin: begin, block: block) do 37 | << (9 + byte_size(block)) :: 32-integer-big-unsigned, 8, index :: 32-integer-big-unsigned, begin :: 32-integer-big-unsigned, 38 | block :: binary >> 39 | end 40 | 41 | def encode(type: :bitfield, field: field) do 42 | << (1 + byte_size(field)) :: 32-integer-big-unsigned, 5, field :: binary>> 43 | end 44 | 45 | def encode(type: :have, piece_index: piece_index) do 46 | << 0, 0, 0, 5, 4, piece_index :: 32-integer-big-unsigned >> 47 | end 48 | 49 | def encode(type: :not_interested) do 50 | << 0, 0, 0, 1, 3 >> 51 | end 52 | 53 | def encode(type: :interested) do 54 | << 0, 0, 0, 1, 2 >> 55 | end 56 | 57 | def encode(type: :unchoke) do 58 | << 0, 0, 0, 1, 1 >> 59 | end 60 | 61 | def encode(type: :choke) do 62 | << 0, 0, 0, 1, 0 >> 63 | end 64 | 65 | def encode(type: :keep_alive) do 66 | << 0, 0, 0, 0 >> 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Wire.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :wire, 6 | version: "0.2.0", 7 | elixir: "~> 1.0.0", 8 | description: "Encode and decode bittorrent peer wire protocol messages", 9 | package: package, 10 | deps: deps] 11 | end 12 | 13 | defp package do 14 | [ contributors: ["alehander42"], 15 | licenses: ["MIT"], 16 | links: %{"Github" => "https://github.com/alehander42/wire"}] 17 | end 18 | 19 | def application do 20 | [applications: [:logger]] 21 | end 22 | 23 | defp deps do 24 | [{:bencoder, "~> 0.0.7"}] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bencoder": {:hex, :bencoder, "0.0.7"}} 2 | -------------------------------------------------------------------------------- /test/decoder_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule WireDecoderTest do 4 | use ExUnit.Case, async: True 5 | 6 | import Wire.Decoder, only: [decode_message: 1, decode_messages: 1, decode_messages: 2] 7 | 8 | test "parses keep_alive correctly" do 9 | assert decode_message(<< 0, 0, 0, 0 >>) == {[type: :keep_alive], <<>>} 10 | end 11 | 12 | test "parses have correctly" do 13 | assert decode_message(<< 0, 0, 0, 5, 4, 0, 0, 0, 96, 0, 96, 3, 5>>) == {[type: :have, piece_index: 96], <<0, 96, 3, 5>>} 14 | end 15 | 16 | test "parses interested correctly" do 17 | assert decode_message(<< 0, 0, 0, 1, 2 >>) == {[type: :interested], <<>>} 18 | end 19 | 20 | test "parses bitfield correctly" do 21 | assert decode_message(<< 0, 0, 0, 6, 5, 0, 2, 4, 0, 2 >>) == 22 | {[type: :bitfield, field: <<0, 2, 4, 0, 2>>], <<>>} 23 | end 24 | 25 | test "parses unchoke messages" do 26 | assert decode_messages(<< 0, 0, 0, 1, 1 >>) == {[[type: :unchoke]], <<>>} 27 | end 28 | 29 | test "parses ltep message correctly" do 30 | msg = <<0, 0, 0, 14, 20, 0, 100, 51, 58, 102, 111, 111, 51, 58, 98, 97, 114, 101>> 31 | result = {[[type: :ltep, ext_msg_id: 0, msg: %{"foo" => "bar"}]], <<>>} 32 | 33 | assert decode_messages(msg) == result 34 | end 35 | 36 | test "parses several messages" do 37 | assert decode_messages(<< 0, 0, 0, 0, 0, 0, 0, 1, 2 >>) == 38 | {[[type: :keep_alive], [type: :interested]], <<>>} 39 | end 40 | 41 | test "parses a part of a message as rest" do 42 | assert decode_messages(<< 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0xf, 5 >>) == 43 | {[[type: :keep_alive], [type: :interested]], <<0, 0, 0, 0xf, 5>>} 44 | assert decode_messages(<< 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 >>) == 45 | {[[type: :keep_alive], [type: :keep_alive]], <<0, 0, 0, 1>>} 46 | end 47 | 48 | test "parses several messages with acc" do 49 | assert decode_messages(<< 0, 0, 0, 0, 0, 0, 0, 1>>, []) == 50 | {[[type: :keep_alive]], <<0, 0, 0, 1>>} 51 | assert decode_messages(<< 0, 0, 0, 1, 2, 0, 0, 0, 5>>, [[type: :keep_alive]]) == 52 | {[[type: :keep_alive], [type: :interested]], <<0, 0, 0, 5>>} 53 | end 54 | 55 | test "parses handshake correctly" do 56 | extensions = <<0,0,0,0,0,0,0,0>> 57 | peer_id = "ffffffffffffffffffff" 58 | info_hash = "eeeeeeeeeeeeeeeeeeee" 59 | handshake = <<19>> <> "BitTorrent protocol" <> extensions <> info_hash <> peer_id 60 | 61 | assert decode_message(handshake) == {[type: :handshake, extension: <<0, 0, 0, 0, 0, 0, 0, 0>>, info_hash: info_hash, peer_id: peer_id], ""} 62 | end 63 | 64 | test "parses several messages correctly starting with a handshake" do 65 | extensions = <<0,0,0,0,0,0,0,0>> 66 | peer_id = "ffffffffffffffffffff" 67 | info_hash = "eeeeeeeeeeeeeeeeeeee" 68 | bitfield = << 0, 0, 0, 6, 5, 0, 2, 4, 0, 2 >> 69 | handshake = <<19>> <> "BitTorrent protocol" <> extensions <> info_hash <> peer_id <> bitfield 70 | 71 | assert decode_messages(handshake) == {[[type: :handshake, extension: <<0, 0, 0, 0, 0, 0, 0, 0>>, info_hash: info_hash, peer_id: peer_id], [type: :bitfield, field: <<0, 2, 4, 0, 2>>]], ""} 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /test/encode_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule WireEncoderTest do 4 | use ExUnit.Case, async: True 5 | 6 | import Wire.Encoder, only: [encode: 1] 7 | 8 | test "converts ltep correctly" do 9 | result = <<0, 0, 0, 14, 20, 0, 100, 51, 58, 102, 111, 111, 51, 58, 98, 97, 114, 101>> 10 | 11 | assert encode(type: :ltep, ext_msg_id: 0, msg: %{"foo" => "bar"}) == result 12 | end 13 | 14 | test "converts port correctly" do 15 | assert encode(type: :port, listen_port: 80) == 16 | << 0, 0, 0, 3, 0, 80, 0 >> 17 | end 18 | 19 | test "converts cancel correctly" do 20 | assert encode(type: :cancel, index: 2, begin: 0, length: 4) == 21 | << 0, 0, 0, 0xd, 8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 4 >> 22 | end 23 | 24 | test "converts request correctly" do 25 | assert encode(type: :request, index: 2, begin: 0, length: 4) == 26 | << 0, 0, 0, 0xd, 6, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 4 >> 27 | end 28 | 29 | test "converts bitfield correctly" do 30 | assert encode(type: :bitfield, field: << 0 >>) == 31 | << 0, 0, 0, 2, 5, 0 >> 32 | end 33 | 34 | test "converts have correctly" do 35 | assert encode(type: :have, piece_index: 22) == 36 | << 0, 0, 0, 5, 4, 0, 0, 0, 22 >> 37 | end 38 | 39 | test "converts interested correctly" do 40 | assert encode(type: :interested) == << 0, 0, 0, 1, 2 >> 41 | end 42 | 43 | test "converts handshake correctly" do 44 | extensions = <<0,0,0,0,0,0,0,0>> 45 | peer_id = "ffffffffffffffffffff" 46 | info_hash = "eeeeeeeeeeeeeeeeeeee" 47 | 48 | result = <<19, 66, 105, 116, 84, 111, 114, 114, 101, 110, 116, 32, 112, 114, 111, 116, 111, 99, 111, 108, 0, 0, 0, 0, 0, 0, 0, 0>> <> info_hash <> peer_id 49 | 50 | assert encode(type: :handshake, extensions: extensions, info_hash: info_hash, peer_id: peer_id) == result 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------