diff --git a/frontend/udp/parser.go b/frontend/udp/parser.go index 43dc329..3c0fed5 100644 --- a/frontend/udp/parser.go +++ b/frontend/udp/parser.go @@ -1,8 +1,11 @@ package udp import ( + "bytes" "encoding/binary" + "fmt" "net" + "sync" "github.com/chihaya/chihaya/bittorrent" ) @@ -37,11 +40,12 @@ var ( bittorrent.Stopped, } - errMalformedPacket = bittorrent.ClientError("malformed packet") - errMalformedIP = bittorrent.ClientError("malformed IP address") - errMalformedEvent = bittorrent.ClientError("malformed event ID") - errUnknownAction = bittorrent.ClientError("unknown action ID") - errBadConnectionID = bittorrent.ClientError("bad connection ID") + errMalformedPacket = bittorrent.ClientError("malformed packet") + errMalformedIP = bittorrent.ClientError("malformed IP address") + errMalformedEvent = bittorrent.ClientError("malformed event ID") + errUnknownAction = bittorrent.ClientError("unknown action ID") + errBadConnectionID = bittorrent.ClientError("bad connection ID") + errUnknownOptionType = bittorrent.ClientError("unknown option type") ) // ParseAnnounce parses an AnnounceRequest from a UDP request. @@ -76,7 +80,7 @@ func ParseAnnounce(r Request, allowIPSpoofing bool) (*bittorrent.AnnounceRequest numWant := binary.BigEndian.Uint32(r.Packet[92:96]) port := binary.BigEndian.Uint16(r.Packet[96:98]) - params, err := handleOptionalParameters(r.Packet) + params, err := handleOptionalParameters(r.Packet[98:]) if err != nil { return nil, err } @@ -97,43 +101,65 @@ func ParseAnnounce(r Request, allowIPSpoofing bool) (*bittorrent.AnnounceRequest }, nil } +type buffer struct { + bytes.Buffer +} + +var bufferFree = sync.Pool{ + New: func() interface{} { return new(buffer) }, +} + +func newBuffer() *buffer { + return bufferFree.Get().(*buffer) +} + +func (b *buffer) free() { + b.Reset() + bufferFree.Put(b) +} + // handleOptionalParameters parses the optional parameters as described in BEP // 41 and updates an announce with the values parsed. -func handleOptionalParameters(packet []byte) (params bittorrent.Params, err error) { - if len(packet) <= 98 { - return +func handleOptionalParameters(packet []byte) (bittorrent.Params, error) { + if len(packet) == 0 { + return bittorrent.ParseURLData("") } - optionStartIndex := 98 - for optionStartIndex < len(packet)-1 { - option := packet[optionStartIndex] + var buf = newBuffer() + defer buf.free() + + for i := 0; i < len(packet); { + option := packet[i] switch option { case optionEndOfOptions: - return - + return bittorrent.ParseURLData(buf.String()) case optionNOP: - optionStartIndex++ - + i++ case optionURLData: - if optionStartIndex+1 > len(packet)-1 { - return params, errMalformedPacket + if i+1 >= len(packet) { + return nil, errMalformedPacket } - length := int(packet[optionStartIndex+1]) - if optionStartIndex+1+length > len(packet)-1 { - return params, errMalformedPacket + length := int(packet[i+1]) + if i+2+length > len(packet) { + return nil, errMalformedPacket } - // TODO(chihaya): Actually parse the URL Data as described in BEP 41 - // into something that fulfills the bittorrent.Params interface. + n, err := buf.Write(packet[i+2 : i+2+length]) + if err != nil { + return nil, err + } + if n != length { + return nil, fmt.Errorf("expected to write %d bytes, wrote %d", length, n) + } - optionStartIndex += 1 + length + i += 2 + length default: - return + return nil, errUnknownOptionType } } - return + return bittorrent.ParseURLData(buf.String()) } // ParseScrape parses a ScrapeRequest from a UDP request. diff --git a/frontend/udp/parser_test.go b/frontend/udp/parser_test.go new file mode 100644 index 0000000..a6c6b92 --- /dev/null +++ b/frontend/udp/parser_test.go @@ -0,0 +1,71 @@ +package udp + +import "testing" + +var table = []struct { + data []byte + values map[string]string + err error +}{ + { + []byte{0x2, 0x5, '/', '?', 'a', '=', 'b'}, + map[string]string{"a": "b"}, + nil, + }, + { + []byte{0x2, 0x0}, + map[string]string{}, + nil, + }, + { + []byte{0x2, 0x1}, + nil, + errMalformedPacket, + }, + { + []byte{0x2}, + nil, + errMalformedPacket, + }, + { + []byte{0x2, 0x8, '/', 'c', '/', 'd', '?', 'a', '=', 'b'}, + map[string]string{"a": "b"}, + nil, + }, + { + []byte{0x2, 0x2, '/', '?', 0x2, 0x3, 'a', '=', 'b'}, + map[string]string{"a": "b"}, + nil, + }, + { + []byte{0x2, 0x9, '/', '?', 'a', '=', 'b', '%', '2', '0', 'c'}, + map[string]string{"a": "b c"}, + nil, + }, +} + +func TestHandleOptionalParameters(t *testing.T) { + for _, testCase := range table { + params, err := handleOptionalParameters(testCase.data) + if err != testCase.err { + if testCase.err == nil { + t.Fatalf("expected no parsing error for %x but got %s", testCase.data, err) + } else { + t.Fatalf("expected parsing error for %x", testCase.data) + } + } + if testCase.values != nil { + if params == nil { + t.Fatalf("expected values %v for %x", testCase.values, testCase.data) + } else { + for key, want := range testCase.values { + if got, ok := params.String(key); !ok { + t.Fatalf("params missing entry %s for data %x", key, testCase.data) + } else if got != want { + t.Fatalf("expected param %s=%s, but was %s for data %x", key, want, got, testCase.data) + } + } + } + } + } +}