initial foundation
Definitely doesn't work, certainly has the right ideas.
This commit is contained in:
parent
1bff8d1571
commit
a2d3080a60
27 changed files with 2378 additions and 0 deletions
36
DCO
Normal file
36
DCO
Normal file
|
@ -0,0 +1,36 @@
|
|||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
1
MAINTAINERS
Normal file
1
MAINTAINERS
Normal file
|
@ -0,0 +1 @@
|
|||
Jimmy Zelinskie <jimmyzelinskie@gmail.com> (@jzelinskie) pkg:*
|
33
bittorrent/bencode/bencode.go
Normal file
33
bittorrent/bencode/bencode.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package bencode implements bencoding of data as defined in BEP 3 using
|
||||
// type assertion over reflection for performance.
|
||||
package bencode
|
||||
|
||||
// Dict represents a bencode dictionary.
|
||||
type Dict map[string]interface{}
|
||||
|
||||
// NewDict allocates the memory for a Dict.
|
||||
func NewDict() Dict {
|
||||
return make(Dict)
|
||||
}
|
||||
|
||||
// List represents a bencode list.
|
||||
type List []interface{}
|
||||
|
||||
// NewList allocates the memory for a List.
|
||||
func NewList() List {
|
||||
return make(List, 0)
|
||||
}
|
145
bittorrent/bencode/decoder.go
Normal file
145
bittorrent/bencode/decoder.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bencode
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// A Decoder reads bencoded objects from an input stream.
|
||||
type Decoder struct {
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
// NewDecoder returns a new decoder that reads from r.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
return &Decoder{r: bufio.NewReader(r)}
|
||||
}
|
||||
|
||||
// Decode unmarshals the next bencoded value in the stream.
|
||||
func (dec *Decoder) Decode() (interface{}, error) {
|
||||
return unmarshal(dec.r)
|
||||
}
|
||||
|
||||
// Unmarshal deserializes and returns the bencoded value in buf.
|
||||
func Unmarshal(buf []byte) (interface{}, error) {
|
||||
r := bufio.NewReader(bytes.NewBuffer(buf))
|
||||
return unmarshal(r)
|
||||
}
|
||||
|
||||
// unmarshal reads bencoded values from a bufio.Reader
|
||||
func unmarshal(r *bufio.Reader) (interface{}, error) {
|
||||
tok, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch tok {
|
||||
case 'i':
|
||||
return readTerminatedInt(r, 'e')
|
||||
|
||||
case 'l':
|
||||
list := NewList()
|
||||
for {
|
||||
ok, err := readTerminator(r, 'e')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
break
|
||||
}
|
||||
|
||||
v, err := unmarshal(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, v)
|
||||
}
|
||||
return list, nil
|
||||
|
||||
case 'd':
|
||||
dict := NewDict()
|
||||
for {
|
||||
ok, err := readTerminator(r, 'e')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
break
|
||||
}
|
||||
|
||||
v, err := unmarshal(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("bencode: non-string map key")
|
||||
}
|
||||
|
||||
dict[key], err = unmarshal(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return dict, nil
|
||||
|
||||
default:
|
||||
err = r.UnreadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
length, err := readTerminatedInt(r, ':')
|
||||
if err != nil {
|
||||
return nil, errors.New("bencode: unknown input sequence")
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
n, err := r.Read(buf)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if int64(n) != length {
|
||||
return nil, errors.New("bencode: short read")
|
||||
}
|
||||
|
||||
return string(buf), nil
|
||||
}
|
||||
}
|
||||
|
||||
func readTerminator(r io.ByteScanner, term byte) (bool, error) {
|
||||
tok, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if tok == term {
|
||||
return true, nil
|
||||
}
|
||||
return false, r.UnreadByte()
|
||||
}
|
||||
|
||||
func readTerminatedInt(r *bufio.Reader, term byte) (int64, error) {
|
||||
buf, err := r.ReadSlice(term)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if len(buf) <= 1 {
|
||||
return 0, errors.New("bencode: empty integer field")
|
||||
}
|
||||
|
||||
return strconv.ParseInt(string(buf[:len(buf)-1]), 10, 64)
|
||||
}
|
96
bittorrent/bencode/decoder_test.go
Normal file
96
bittorrent/bencode/decoder_test.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bencode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var unmarshalTests = []struct {
|
||||
input string
|
||||
expected interface{}
|
||||
}{
|
||||
{"i42e", int64(42)},
|
||||
{"i-42e", int64(-42)},
|
||||
|
||||
{"7:example", "example"},
|
||||
|
||||
{"l3:one3:twoe", List{"one", "two"}},
|
||||
{"le", List{}},
|
||||
|
||||
{"d3:one2:aa3:two2:bbe", Dict{"one": "aa", "two": "bb"}},
|
||||
{"de", Dict{}},
|
||||
}
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
for _, tt := range unmarshalTests {
|
||||
got, err := Unmarshal([]byte(tt.input))
|
||||
assert.Nil(t, err, "unmarshal should not fail")
|
||||
assert.Equal(t, got, tt.expected, "unmarshalled values should match the expected results")
|
||||
}
|
||||
}
|
||||
|
||||
type bufferLoop struct {
|
||||
val string
|
||||
}
|
||||
|
||||
func (r *bufferLoop) Read(b []byte) (int, error) {
|
||||
n := copy(b, r.val)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalScalar(b *testing.B) {
|
||||
d1 := NewDecoder(&bufferLoop{"7:example"})
|
||||
d2 := NewDecoder(&bufferLoop{"i42e"})
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
d1.Decode()
|
||||
d2.Decode()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalLarge(t *testing.T) {
|
||||
data := Dict{
|
||||
"k1": List{"a", "b", "c"},
|
||||
"k2": int64(42),
|
||||
"k3": "val",
|
||||
"k4": int64(-42),
|
||||
}
|
||||
|
||||
buf, _ := Marshal(data)
|
||||
dec := NewDecoder(&bufferLoop{string(buf)})
|
||||
|
||||
got, err := dec.Decode()
|
||||
assert.Nil(t, err, "decode should not fail")
|
||||
assert.Equal(t, got, data, "encoding and decoding should equal the original value")
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalLarge(b *testing.B) {
|
||||
data := map[string]interface{}{
|
||||
"k1": []string{"a", "b", "c"},
|
||||
"k2": 42,
|
||||
"k3": "val",
|
||||
"k4": uint(42),
|
||||
}
|
||||
|
||||
buf, _ := Marshal(data)
|
||||
dec := NewDecoder(&bufferLoop{string(buf)})
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
dec.Decode()
|
||||
}
|
||||
}
|
173
bittorrent/bencode/encoder.go
Normal file
173
bittorrent/bencode/encoder.go
Normal file
|
@ -0,0 +1,173 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bencode
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// An Encoder writes bencoded objects to an output stream.
|
||||
type Encoder struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// NewEncoder returns a new encoder that writes to w.
|
||||
func NewEncoder(w io.Writer) *Encoder {
|
||||
return &Encoder{w: w}
|
||||
}
|
||||
|
||||
// Encode writes the bencoding of v to the stream.
|
||||
func (enc *Encoder) Encode(v interface{}) error {
|
||||
return marshal(enc.w, v)
|
||||
}
|
||||
|
||||
// Marshal returns the bencoding of v.
|
||||
func Marshal(v interface{}) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
err := marshal(buf, v)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
// Marshaler is the interface implemented by objects that can marshal
|
||||
// themselves.
|
||||
type Marshaler interface {
|
||||
MarshalBencode() ([]byte, error)
|
||||
}
|
||||
|
||||
// marshal writes types bencoded to an io.Writer
|
||||
func marshal(w io.Writer, data interface{}) error {
|
||||
switch v := data.(type) {
|
||||
case Marshaler:
|
||||
bencoded, err := v.MarshalBencode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(bencoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case string:
|
||||
marshalString(w, v)
|
||||
|
||||
case int:
|
||||
marshalInt(w, int64(v))
|
||||
|
||||
case uint:
|
||||
marshalUint(w, uint64(v))
|
||||
|
||||
case int16:
|
||||
marshalInt(w, int64(v))
|
||||
|
||||
case uint16:
|
||||
marshalUint(w, uint64(v))
|
||||
|
||||
case int32:
|
||||
marshalInt(w, int64(v))
|
||||
|
||||
case uint32:
|
||||
marshalUint(w, uint64(v))
|
||||
|
||||
case int64:
|
||||
marshalInt(w, v)
|
||||
|
||||
case uint64:
|
||||
marshalUint(w, v)
|
||||
|
||||
case []byte:
|
||||
marshalBytes(w, v)
|
||||
|
||||
case time.Duration: // Assume seconds
|
||||
marshalInt(w, int64(v/time.Second))
|
||||
|
||||
case Dict:
|
||||
marshal(w, map[string]interface{}(v))
|
||||
|
||||
case []Dict:
|
||||
w.Write([]byte{'l'})
|
||||
for _, val := range v {
|
||||
err := marshal(w, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.Write([]byte{'e'})
|
||||
|
||||
case map[string]interface{}:
|
||||
w.Write([]byte{'d'})
|
||||
for key, val := range v {
|
||||
marshalString(w, key)
|
||||
err := marshal(w, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.Write([]byte{'e'})
|
||||
|
||||
case []string:
|
||||
w.Write([]byte{'l'})
|
||||
for _, val := range v {
|
||||
err := marshal(w, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.Write([]byte{'e'})
|
||||
|
||||
case List:
|
||||
marshal(w, []interface{}(v))
|
||||
|
||||
case []interface{}:
|
||||
w.Write([]byte{'l'})
|
||||
for _, val := range v {
|
||||
err := marshal(w, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.Write([]byte{'e'})
|
||||
|
||||
default:
|
||||
return fmt.Errorf("attempted to marshal unsupported type:\n%t", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalInt(w io.Writer, v int64) {
|
||||
w.Write([]byte{'i'})
|
||||
w.Write([]byte(strconv.FormatInt(v, 10)))
|
||||
w.Write([]byte{'e'})
|
||||
}
|
||||
|
||||
func marshalUint(w io.Writer, v uint64) {
|
||||
w.Write([]byte{'i'})
|
||||
w.Write([]byte(strconv.FormatUint(v, 10)))
|
||||
w.Write([]byte{'e'})
|
||||
}
|
||||
|
||||
func marshalBytes(w io.Writer, v []byte) {
|
||||
w.Write([]byte(strconv.Itoa(len(v))))
|
||||
w.Write([]byte{':'})
|
||||
w.Write(v)
|
||||
}
|
||||
|
||||
func marshalString(w io.Writer, v string) {
|
||||
marshalBytes(w, []byte(v))
|
||||
}
|
81
bittorrent/bencode/encoder_test.go
Normal file
81
bittorrent/bencode/encoder_test.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bencode
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var marshalTests = []struct {
|
||||
input interface{}
|
||||
expected []string
|
||||
}{
|
||||
{int(42), []string{"i42e"}},
|
||||
{int(-42), []string{"i-42e"}},
|
||||
{uint(43), []string{"i43e"}},
|
||||
{int64(44), []string{"i44e"}},
|
||||
{uint64(45), []string{"i45e"}},
|
||||
{int16(44), []string{"i44e"}},
|
||||
{uint16(45), []string{"i45e"}},
|
||||
|
||||
{"example", []string{"7:example"}},
|
||||
{[]byte("example"), []string{"7:example"}},
|
||||
{30 * time.Minute, []string{"i1800e"}},
|
||||
|
||||
{[]string{"one", "two"}, []string{"l3:one3:twoe", "l3:two3:onee"}},
|
||||
{[]interface{}{"one", "two"}, []string{"l3:one3:twoe", "l3:two3:onee"}},
|
||||
{[]string{}, []string{"le"}},
|
||||
|
||||
{map[string]interface{}{"one": "aa", "two": "bb"}, []string{"d3:one2:aa3:two2:bbe", "d3:two2:bb3:one2:aae"}},
|
||||
{map[string]interface{}{}, []string{"de"}},
|
||||
}
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
for _, test := range marshalTests {
|
||||
got, err := Marshal(test.input)
|
||||
assert.Nil(t, err, "marshal should not fail")
|
||||
assert.Contains(t, test.expected, string(got), "the marshaled result should be one of the expected permutations")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalScalar(b *testing.B) {
|
||||
buf := &bytes.Buffer{}
|
||||
encoder := NewEncoder(buf)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
encoder.Encode("test")
|
||||
encoder.Encode(123)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalLarge(b *testing.B) {
|
||||
data := map[string]interface{}{
|
||||
"k1": []string{"a", "b", "c"},
|
||||
"k2": 42,
|
||||
"k3": "val",
|
||||
"k4": uint(42),
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
encoder := NewEncoder(buf)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
encoder.Encode(data)
|
||||
}
|
||||
}
|
177
bittorrent/bittorrent.go
Normal file
177
bittorrent/bittorrent.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bittorrent
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PeerID represents a peer ID.
|
||||
type PeerID [20]byte
|
||||
|
||||
// PeerIDFromBytes creates a PeerID from a byte slice.
|
||||
//
|
||||
// It panics if b is not 20 bytes long.
|
||||
func PeerIDFromBytes(b []byte) PeerID {
|
||||
if len(b) != 20 {
|
||||
panic("peer ID must be 20 bytes")
|
||||
}
|
||||
|
||||
var buf [20]byte
|
||||
copy(buf[:], b)
|
||||
return PeerID(buf)
|
||||
}
|
||||
|
||||
// PeerIDFromString creates a PeerID from a string.
|
||||
//
|
||||
// It panics if s is not 20 bytes long.
|
||||
func PeerIDFromString(s string) PeerID {
|
||||
if len(s) != 20 {
|
||||
panic("peer ID must be 20 bytes")
|
||||
}
|
||||
|
||||
var buf [20]byte
|
||||
copy(buf[:], s)
|
||||
return PeerID(buf)
|
||||
}
|
||||
|
||||
// InfoHash represents an infohash.
|
||||
type InfoHash [20]byte
|
||||
|
||||
// InfoHashFromBytes creates an InfoHash from a byte slice.
|
||||
//
|
||||
// It panics if b is not 20 bytes long.
|
||||
func InfoHashFromBytes(b []byte) InfoHash {
|
||||
if len(b) != 20 {
|
||||
panic("infohash must be 20 bytes")
|
||||
}
|
||||
|
||||
var buf [20]byte
|
||||
copy(buf[:], b)
|
||||
return InfoHash(buf)
|
||||
}
|
||||
|
||||
// InfoHashFromString creates an InfoHash from a string.
|
||||
//
|
||||
// It panics if s is not 20 bytes long.
|
||||
func InfoHashFromString(s string) InfoHash {
|
||||
if len(s) != 20 {
|
||||
panic("infohash must be 20 bytes")
|
||||
}
|
||||
|
||||
var buf [20]byte
|
||||
copy(buf[:], s)
|
||||
return InfoHash(buf)
|
||||
}
|
||||
|
||||
// AnnounceRequest represents the parsed parameters from an announce request.
|
||||
type AnnounceRequest struct {
|
||||
Event Event
|
||||
InfoHash InfoHash
|
||||
Compact bool
|
||||
NumWant uint32
|
||||
Left uint64
|
||||
Downloaded uint64
|
||||
Uploaded uint64
|
||||
|
||||
Peer
|
||||
Params
|
||||
}
|
||||
|
||||
// AnnounceResponse represents the parameters used to create an announce
|
||||
// response.
|
||||
type AnnounceResponse struct {
|
||||
Compact bool
|
||||
Complete int32
|
||||
Incomplete int32
|
||||
Interval time.Duration
|
||||
MinInterval time.Duration
|
||||
IPv4Peers []Peer
|
||||
IPv6Peers []Peer
|
||||
}
|
||||
|
||||
// AnnounceHandler is a function that generates a response for an Announce.
|
||||
type AnnounceHandler func(*AnnounceRequest) *AnnounceResponse
|
||||
|
||||
// AnnounceCallback is a function that does something with the results of an
|
||||
// Announce after it has been completed.
|
||||
type AnnounceCallback func(*AnnounceRequest, *AnnounceResponse)
|
||||
|
||||
// ScrapeRequest represents the parsed parameters from a scrape request.
|
||||
type ScrapeRequest struct {
|
||||
InfoHashes []InfoHash
|
||||
Params Params
|
||||
}
|
||||
|
||||
// ScrapeResponse represents the parameters used to create a scrape response.
|
||||
type ScrapeResponse struct {
|
||||
Files map[InfoHash]Scrape
|
||||
}
|
||||
|
||||
// Scrape represents the state of a swarm that is returned in a scrape response.
|
||||
type Scrape struct {
|
||||
Snatches uint32
|
||||
Complete uint32
|
||||
Incomplete uint32
|
||||
}
|
||||
|
||||
// ScrapeHandler is a function that generates a response for a Scrape.
|
||||
type ScrapeHandler func(*ScrapeRequest) *ScrapeResponse
|
||||
|
||||
// ScrapeCallback is a function that does something with the results of a
|
||||
// Scrape after it has been completed.
|
||||
type ScrapeCallback func(*ScrapeRequest, *ScrapeResponse)
|
||||
|
||||
// Peer represents the connection details of a peer that is returned in an
|
||||
// announce response.
|
||||
type Peer struct {
|
||||
ID PeerID
|
||||
IP net.IP
|
||||
Port uint16
|
||||
}
|
||||
|
||||
// Equal reports whether p and x are the same.
|
||||
func (p Peer) Equal(x Peer) bool { return p.EqualEndpoint(x) && p.ID == x.ID }
|
||||
|
||||
// EqualEndpoint reports whether p and x have the same endpoint.
|
||||
func (p Peer) EqualEndpoint(x Peer) bool { return p.Port == x.Port && p.IP.Equal(x.IP) }
|
||||
|
||||
// Params is used to fetch request optional parameters.
|
||||
type Params interface {
|
||||
String(key string) (string, error)
|
||||
}
|
||||
|
||||
// ClientError represents an error that should be exposed to the client over
|
||||
// the BitTorrent protocol implementation.
|
||||
type ClientError string
|
||||
|
||||
// Error implements the error interface for ClientError.
|
||||
func (c ClientError) Error() string { return string(c) }
|
||||
|
||||
// Server represents an implementation of the BitTorrent tracker protocol.
|
||||
type Server interface {
|
||||
ListenAndServe() error
|
||||
Stop()
|
||||
}
|
||||
|
||||
// ServerFuncs are the collection of protocol-agnostic functions used to handle
|
||||
// requests in a Server.
|
||||
type ServerFuncs struct {
|
||||
HandleAnnounce AnnounceHandler
|
||||
HandleScrape ScrapeHandler
|
||||
AfterAnnounce AnnounceCallback
|
||||
AfterScrape ScrapeCallback
|
||||
}
|
32
bittorrent/client_id.go
Normal file
32
bittorrent/client_id.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bittorrent
|
||||
|
||||
// NewClientID returns the part of a PeerID that identifies a peer's client
|
||||
// software.
|
||||
func NewClientID(peerID string) (clientID string) {
|
||||
length := len(peerID)
|
||||
if length >= 6 {
|
||||
if peerID[0] == '-' {
|
||||
if length >= 7 {
|
||||
clientID = peerID[1:7]
|
||||
}
|
||||
} else {
|
||||
clientID = peerID[:6]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
72
bittorrent/client_id_test.go
Normal file
72
bittorrent/client_id_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bittorrent
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClientID(t *testing.T) {
|
||||
var clientTable = []struct {
|
||||
peerID string
|
||||
clientID string
|
||||
}{
|
||||
{"-AZ3034-6wfG2wk6wWLc", "AZ3034"},
|
||||
{"-AZ3042-6ozMq5q6Q3NX", "AZ3042"},
|
||||
{"-BS5820-oy4La2MWGEFj", "BS5820"},
|
||||
{"-AR6360-6oZyyMWoOOBe", "AR6360"},
|
||||
{"-AG2083-s1hiF8vGAAg0", "AG2083"},
|
||||
{"-AG3003-lEl2Mm4NEO4n", "AG3003"},
|
||||
{"-MR1100-00HS~T7*65rm", "MR1100"},
|
||||
{"-LK0140-ATIV~nbEQAMr", "LK0140"},
|
||||
{"-KT2210-347143496631", "KT2210"},
|
||||
{"-TR0960-6ep6svaa61r4", "TR0960"},
|
||||
{"-XX1150-dv220cotgj4d", "XX1150"},
|
||||
{"-AZ2504-192gwethivju", "AZ2504"},
|
||||
{"-KT4310-3L4UvarKuqIu", "KT4310"},
|
||||
{"-AZ2060-0xJQ02d4309O", "AZ2060"},
|
||||
{"-BD0300-2nkdf08Jd890", "BD0300"},
|
||||
{"-A~0010-a9mn9DFkj39J", "A~0010"},
|
||||
{"-UT2300-MNu93JKnm930", "UT2300"},
|
||||
{"-UT2300-KT4310KT4301", "UT2300"},
|
||||
|
||||
{"T03A0----f089kjsdf6e", "T03A0-"},
|
||||
{"S58B-----nKl34GoNb75", "S58B--"},
|
||||
{"M4-4-0--9aa757Efd5Bl", "M4-4-0"},
|
||||
|
||||
{"AZ2500BTeYUzyabAfo6U", "AZ2500"}, // BitTyrant
|
||||
{"exbc0JdSklm834kj9Udf", "exbc0J"}, // Old BitComet
|
||||
{"FUTB0L84j542mVc84jkd", "FUTB0L"}, // Alt BitComet
|
||||
{"XBT054d-8602Jn83NnF9", "XBT054"}, // XBT
|
||||
{"OP1011affbecbfabeefb", "OP1011"}, // Opera
|
||||
{"-ML2.7.2-kgjjfkd9762", "ML2.7."}, // MLDonkey
|
||||
{"-BOWA0C-SDLFJWEIORNM", "BOWA0C"}, // Bits on Wheels
|
||||
{"Q1-0-0--dsn34DFn9083", "Q1-0-0"}, // Queen Bee
|
||||
{"Q1-10-0-Yoiumn39BDfO", "Q1-10-"}, // Queen Bee Alt
|
||||
{"346------SDFknl33408", "346---"}, // TorreTopia
|
||||
{"QVOD0054ABFFEDCCDEDB", "QVOD00"}, // Qvod
|
||||
|
||||
{"", ""},
|
||||
{"-", ""},
|
||||
{"12345", ""},
|
||||
{"-12345", ""},
|
||||
{"123456", "123456"},
|
||||
{"-123456", "123456"},
|
||||
}
|
||||
|
||||
for _, tt := range clientTable {
|
||||
if parsedID := NewClientID(tt.peerID); parsedID != tt.clientID {
|
||||
t.Error("Incorrectly parsed peer ID", tt.peerID, "as", parsedID)
|
||||
}
|
||||
}
|
||||
}
|
78
bittorrent/event.go
Normal file
78
bittorrent/event.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bittorrent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrUnknownEvent is returned when New fails to return an event.
|
||||
var ErrUnknownEvent = errors.New("unknown event")
|
||||
|
||||
// Event represents an event done by a BitTorrent client.
|
||||
type Event uint8
|
||||
|
||||
const (
|
||||
// None is the event when a BitTorrent client announces due to time lapsed
|
||||
// since the previous announce.
|
||||
None Event = iota
|
||||
|
||||
// Started is the event sent by a BitTorrent client when it joins a swarm.
|
||||
Started
|
||||
|
||||
// Stopped is the event sent by a BitTorrent client when it leaves a swarm.
|
||||
Stopped
|
||||
|
||||
// Completed is the event sent by a BitTorrent client when it finishes
|
||||
// downloading all of the required chunks.
|
||||
Completed
|
||||
)
|
||||
|
||||
var (
|
||||
eventToString = make(map[Event]string)
|
||||
stringToEvent = make(map[string]Event)
|
||||
)
|
||||
|
||||
func init() {
|
||||
eventToString[None] = "none"
|
||||
eventToString[Started] = "started"
|
||||
eventToString[Stopped] = "stopped"
|
||||
eventToString[Completed] = "completed"
|
||||
|
||||
stringToEvent[""] = None
|
||||
|
||||
for k, v := range eventToString {
|
||||
stringToEvent[v] = k
|
||||
}
|
||||
}
|
||||
|
||||
// NewEvent returns the proper Event given a string.
|
||||
func NewEvent(eventStr string) (Event, error) {
|
||||
if e, ok := stringToEvent[strings.ToLower(eventStr)]; ok {
|
||||
return e, nil
|
||||
}
|
||||
|
||||
return None, ErrUnknownEvent
|
||||
}
|
||||
|
||||
// String implements Stringer for an event.
|
||||
func (e Event) String() string {
|
||||
if name, ok := eventToString[e]; ok {
|
||||
return name
|
||||
}
|
||||
|
||||
panic("bittorrent: event has no associated name")
|
||||
}
|
43
bittorrent/event_test.go
Normal file
43
bittorrent/event_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bittorrent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
var table = []struct {
|
||||
data string
|
||||
expected Event
|
||||
expectedErr error
|
||||
}{
|
||||
{"", None, nil},
|
||||
{"NONE", None, nil},
|
||||
{"none", None, nil},
|
||||
{"started", Started, nil},
|
||||
{"stopped", Stopped, nil},
|
||||
{"completed", Completed, nil},
|
||||
{"notAnEvent", None, ErrUnknownEvent},
|
||||
}
|
||||
|
||||
for _, tt := range table {
|
||||
got, err := NewEvent(tt.data)
|
||||
assert.Equal(t, err, tt.expectedErr, "errors should equal the expected value")
|
||||
assert.Equal(t, got, tt.expected, "events should equal the expected value")
|
||||
}
|
||||
}
|
168
bittorrent/http/parser.go
Normal file
168
bittorrent/http/parser.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
// ParseAnnounce parses an bittorrent.AnnounceRequest from an http.Request.
|
||||
//
|
||||
// If allowIPSpoofing is true, IPs provided via params will be used.
|
||||
// If realIPHeader is not empty string, the first value of the HTTP Header with
|
||||
// that name will be used.
|
||||
func ParseAnnounce(r *http.Request, realIPHeader string, allowIPSpoofing bool) (*bittorrent.AnnounceRequest, error) {
|
||||
qp, err := NewQueryParams(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request := &bittorrent.AnnounceRequest{Params: q}
|
||||
|
||||
eventStr, err := qp.String("event")
|
||||
if err == query.ErrKeyNotFound {
|
||||
eventStr = ""
|
||||
} else if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: event")
|
||||
}
|
||||
request.Event, err = bittorrent.NewEvent(eventStr)
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to provide valid client event")
|
||||
}
|
||||
|
||||
compactStr, _ := qp.String("compact")
|
||||
request.Compact = compactStr != "" && compactStr != "0"
|
||||
|
||||
infoHashes := qp.InfoHashes()
|
||||
if len(infoHashes) < 1 {
|
||||
return nil, bittorrent.ClientError("no info_hash parameter supplied")
|
||||
}
|
||||
if len(infoHashes) > 1 {
|
||||
return nil, bittorrent.ClientError("multiple info_hash parameters supplied")
|
||||
}
|
||||
request.InfoHash = infoHashes[0]
|
||||
|
||||
peerID, err := qp.String("peer_id")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: peer_id")
|
||||
}
|
||||
if len(peerID) != 20 {
|
||||
return nil, bittorrent.ClientError("failed to provide valid peer_id")
|
||||
}
|
||||
request.PeerID = bittorrent.PeerIDFromString(peerID)
|
||||
|
||||
request.Left, err = qp.Uint64("left")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: left")
|
||||
}
|
||||
|
||||
request.Downloaded, err = qp.Uint64("downloaded")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: downloaded")
|
||||
}
|
||||
|
||||
request.Uploaded, err = qp.Uint64("uploaded")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: uploaded")
|
||||
}
|
||||
|
||||
numwant, _ := qp.Uint64("numwant")
|
||||
request.NumWant = int32(numwant)
|
||||
|
||||
port, err := qp.Uint64("port")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: port")
|
||||
}
|
||||
request.Port = uint16(port)
|
||||
|
||||
request.IP, err = requestedIP(q, r, realIPHeader, allowIPSpoofing)
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse peer IP address: " + err.Error())
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// ParseScrape parses an bittorrent.ScrapeRequest from an http.Request.
|
||||
func ParseScrape(r *http.Request) (*bittorent.ScrapeRequest, error) {
|
||||
qp, err := NewQueryParams(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infoHashes := qp.InfoHashes()
|
||||
if len(infoHashes) < 1 {
|
||||
return nil, bittorrent.ClientError("no info_hash parameter supplied")
|
||||
}
|
||||
|
||||
request := &bittorrent.ScrapeRequest{
|
||||
InfoHashes: infoHashes,
|
||||
Params: q,
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// requestedIP determines the IP address for a BitTorrent client request.
|
||||
//
|
||||
// If allowIPSpoofing is true, IPs provided via params will be used.
|
||||
// If realIPHeader is not empty string, the first value of the HTTP Header with
|
||||
// that name will be used.
|
||||
func requestedIP(r *http.Request, p bittorent.Params, realIPHeader string, allowIPSpoofing bool) (net.IP, error) {
|
||||
if allowIPSpoofing {
|
||||
if ipstr, err := p.String("ip"); err == nil {
|
||||
ip, err := net.ParseIP(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
if ipstr, err := p.String("ipv4"); err == nil {
|
||||
ip, err := net.ParseIP(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
if ipstr, err := p.String("ipv6"); err == nil {
|
||||
ip, err := net.ParseIP(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
if realIPHeader != "" {
|
||||
if ips, ok := r.Header[realIPHeader]; ok && len(ips) > 0 {
|
||||
ip, err := net.ParseIP(ips[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
return r.RemoteAddr
|
||||
}
|
141
bittorrent/http/query_params.go
Normal file
141
bittorrent/http/query_params.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
// ErrKeyNotFound is returned when a provided key has no value associated with
|
||||
// it.
|
||||
var ErrKeyNotFound = errors.New("http: value for the provided key does not exist")
|
||||
|
||||
// ErrInvalidInfohash is returned when parsing a query encounters an infohash
|
||||
// with invalid length.
|
||||
var ErrInvalidInfohash = errors.New("http: invalid infohash")
|
||||
|
||||
// QueryParams parses an HTTP Query and implements the bittorrent.Params
|
||||
// interface with some additional helpers.
|
||||
type QueryParams struct {
|
||||
query string
|
||||
params map[string]string
|
||||
infoHashes []bittorrent.InfoHash
|
||||
}
|
||||
|
||||
// NewQueryParams parses a raw URL query.
|
||||
func NewQueryParams(query string) (*Query, error) {
|
||||
var (
|
||||
keyStart, keyEnd int
|
||||
valStart, valEnd int
|
||||
|
||||
onKey = true
|
||||
|
||||
q = &Query{
|
||||
query: query,
|
||||
infoHashes: nil,
|
||||
params: make(map[string]string),
|
||||
}
|
||||
)
|
||||
|
||||
for i, length := 0, len(query); i < length; i++ {
|
||||
separator := query[i] == '&' || query[i] == ';' || query[i] == '?'
|
||||
last := i == length-1
|
||||
|
||||
if separator || last {
|
||||
if onKey && !last {
|
||||
keyStart = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if last && !separator && !onKey {
|
||||
valEnd = i
|
||||
}
|
||||
|
||||
keyStr, err := url.QueryUnescape(query[keyStart : keyEnd+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var valStr string
|
||||
|
||||
if valEnd > 0 {
|
||||
valStr, err = url.QueryUnescape(query[valStart : valEnd+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if keyStr == "info_hash" {
|
||||
if len(valStr) != 20 {
|
||||
return nil, ErrInvalidInfohash
|
||||
}
|
||||
q.infoHashes = append(q.infoHashes, bittorrent.InfoHashFromString(valStr))
|
||||
} else {
|
||||
q.params[strings.ToLower(keyStr)] = valStr
|
||||
}
|
||||
|
||||
valEnd = 0
|
||||
onKey = true
|
||||
keyStart = i + 1
|
||||
|
||||
} else if query[i] == '=' {
|
||||
onKey = false
|
||||
valStart = i + 1
|
||||
valEnd = 0
|
||||
} else if onKey {
|
||||
keyEnd = i
|
||||
} else {
|
||||
valEnd = i
|
||||
}
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// String returns a string parsed from a query. Every key can be returned as a
|
||||
// string because they are encoded in the URL as strings.
|
||||
func (q *Query) String(key string) (string, error) {
|
||||
val, exists := q.params[key]
|
||||
if !exists {
|
||||
return "", ErrKeyNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Uint64 returns a uint parsed from a query. After being called, it is safe to
|
||||
// cast the uint64 to your desired length.
|
||||
func (q *Query) Uint64(key string) (uint64, error) {
|
||||
str, exists := q.params[key]
|
||||
if !exists {
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
|
||||
val, err := strconv.ParseUint(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// InfoHashes returns a list of requested infohashes.
|
||||
func (q *Query) InfoHashes() []bittorrent.InfoHash {
|
||||
return q.infoHashes
|
||||
}
|
110
bittorrent/http/query_params_test.go
Normal file
110
bittorrent/http/query_params_test.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
baseAddr = "https://www.subdomain.tracker.com:80/"
|
||||
testInfoHash = "01234567890123456789"
|
||||
testPeerID = "-TEST01-6wfG2wk6wWLc"
|
||||
|
||||
ValidAnnounceArguments = []url.Values{
|
||||
{"peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}},
|
||||
{"peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}},
|
||||
{"peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "numwant": {"28"}},
|
||||
{"peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "event": {"stopped"}},
|
||||
{"peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "event": {"started"}, "numwant": {"13"}},
|
||||
{"peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "no_peer_id": {"1"}},
|
||||
{"peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}},
|
||||
{"peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}, "key": {"peerKey"}},
|
||||
{"peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}, "key": {"peerKey"}, "trackerid": {"trackerId"}},
|
||||
{"peer_id": {"%3Ckey%3A+0x90%3E"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}, "key": {"peerKey"}, "trackerid": {"trackerId"}},
|
||||
{"peer_id": {"%3Ckey%3A+0x90%3E"}, "compact": {"1"}},
|
||||
{"peer_id": {""}, "compact": {""}},
|
||||
}
|
||||
|
||||
InvalidQueries = []string{
|
||||
baseAddr + "announce/?" + "info_hash=%0%a",
|
||||
}
|
||||
)
|
||||
|
||||
func mapArrayEqual(boxed map[string][]string, unboxed map[string]string) bool {
|
||||
if len(boxed) != len(unboxed) {
|
||||
return false
|
||||
}
|
||||
|
||||
for mapKey, mapVal := range boxed {
|
||||
// Always expect box to hold only one element
|
||||
if len(mapVal) != 1 || mapVal[0] != unboxed[mapKey] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestValidQueries(t *testing.T) {
|
||||
for parseIndex, parseVal := range ValidAnnounceArguments {
|
||||
parsedQueryObj, err := NewQueryParams(baseAddr + "announce/?" + parseVal.Encode())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !mapArrayEqual(parseVal, parsedQueryObj.params) {
|
||||
t.Errorf("Incorrect parse at item %d.\n Expected=%v\n Recieved=%v\n", parseIndex, parseVal, parsedQueryObj.params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidQueries(t *testing.T) {
|
||||
for parseIndex, parseStr := range InvalidQueries {
|
||||
parsedQueryObj, err := NewQueryParams(parseStr)
|
||||
if err == nil {
|
||||
t.Error("Should have produced error", parseIndex)
|
||||
}
|
||||
|
||||
if parsedQueryObj != nil {
|
||||
t.Error("Should be nil after error", parsedQueryObj, parseIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseQuery(b *testing.B) {
|
||||
for bCount := 0; bCount < b.N; bCount++ {
|
||||
for parseIndex, parseStr := range ValidAnnounceArguments {
|
||||
parsedQueryObj, err := NewQueryParams(baseAddr + "announce/?" + parseStr.Encode())
|
||||
if err != nil {
|
||||
b.Error(err, parseIndex)
|
||||
b.Log(parsedQueryObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkURLParseQuery(b *testing.B) {
|
||||
for bCount := 0; bCount < b.N; bCount++ {
|
||||
for parseIndex, parseStr := range ValidAnnounceArguments {
|
||||
parsedQueryObj, err := url.ParseQuery(baseAddr + "announce/?" + parseStr.Encode())
|
||||
if err != nil {
|
||||
b.Error(err, parseIndex)
|
||||
b.Log(parsedQueryObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
bittorrent/http/server.go
Normal file
136
bittorrent/http/server.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
RequestTimeout time.Duration
|
||||
AllowIPSpoofing bool
|
||||
RealIPHeader string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
grace *graceful.Server
|
||||
|
||||
bittorrent.ServerFuncs
|
||||
Config
|
||||
}
|
||||
|
||||
func NewServer(funcs bittorrent.ServerFuncs, cfg Config) {
|
||||
return &Server{
|
||||
ServerFuncs: funcs,
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
s.grace.Stop(s.grace.Timeout)
|
||||
<-s.grace.StopChan()
|
||||
}
|
||||
|
||||
func (s *Server) handler() {
|
||||
router := httprouter.New()
|
||||
router.GET("/announce", s.announceRoute)
|
||||
router.GET("/scrape", s.scrapeRoute)
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe() error {
|
||||
s.grace = &graceful.Server{
|
||||
Server: &http.Server{
|
||||
Addr: s.Addr,
|
||||
Handler: s.handler(),
|
||||
ReadTimeout: s.ReadTimeout,
|
||||
WriteTimeout: s.WriteTimeout,
|
||||
},
|
||||
Timeout: s.RequestTimeout,
|
||||
NoSignalHandling: true,
|
||||
ConnState: func(conn net.Conn, state http.ConnState) {
|
||||
switch state {
|
||||
case http.StateNew:
|
||||
//stats.RecordEvent(stats.AcceptedConnection)
|
||||
|
||||
case http.StateClosed:
|
||||
//stats.RecordEvent(stats.ClosedConnection)
|
||||
|
||||
case http.StateHijacked:
|
||||
panic("http: connection impossibly hijacked")
|
||||
|
||||
// Ignore the following cases.
|
||||
case http.StateActive, http.StateIdle:
|
||||
|
||||
default:
|
||||
panic("http: connection transitioned to unknown state")
|
||||
}
|
||||
},
|
||||
}
|
||||
s.grace.SetKeepAlivesEnabled(false)
|
||||
|
||||
if err := s.grace.ListenAndServe(); err != nil {
|
||||
if opErr, ok := err.(*net.OpError); !ok || (ok && opErr.Op != "accept") {
|
||||
panic("http: failed to gracefully run HTTP server: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) announceRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
req, err := ParseAnnounce(r, s.RealIPHeader, s.AllowIPSpoofing)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := s.HandleAnnounce(req)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = WriteAnnounceResponse(w, resp)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if s.AfterAnnounce != nil {
|
||||
s.AfterAnnounce(req, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) scrapeRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
req, err := ParseScrape(r)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := s.HandleScrape(req)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = WriteScrapeResponse(w, resp)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if s.AfterScrape != nil {
|
||||
s.AfterScrape(req, resp)
|
||||
}
|
||||
}
|
111
bittorrent/http/writer.go
Normal file
111
bittorrent/http/writer.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
// WriteError communicates an error to a BitTorrent client over HTTP.
|
||||
func WriteError(w http.ResponseWriter, err error) error {
|
||||
message := "internal server error"
|
||||
if _, clientErr := err.(bittorrent.ClientError); clientErr {
|
||||
message = err.Error()
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return bencode.NewEncoder(w).Encode(bencode.Dict{
|
||||
"failure reason": message,
|
||||
})
|
||||
}
|
||||
|
||||
// WriteAnnounceResponse communicates the results of an Announce to a
|
||||
// BitTorrent client over HTTP.
|
||||
func WriteAnnounceResponse(w http.ResponseWriter, resp *bittorrent.AnnounceResponse) error {
|
||||
bdict := bencode.Dict{
|
||||
"complete": resp.Complete,
|
||||
"incomplete": resp.Incomplete,
|
||||
"interval": resp.Interval,
|
||||
"min interval": resp.MinInterval,
|
||||
}
|
||||
|
||||
// Add the peers to the dictionary in the compact format.
|
||||
if resp.Compact {
|
||||
var IPv4CompactDict, IPv6CompactDict []byte
|
||||
|
||||
// Add the IPv4 peers to the dictionary.
|
||||
for _, peer := range resp.IPv4Peers {
|
||||
IPv4CompactDict = append(IPv4CompactDict, compact(peer)...)
|
||||
}
|
||||
if len(IPv4CompactDict) > 0 {
|
||||
bdict["peers"] = IPv4CompactDict
|
||||
}
|
||||
|
||||
// Add the IPv6 peers to the dictionary.
|
||||
for _, peer := range resp.IPv6Peers {
|
||||
IPv6CompactDict = append(IPv6CompactDict, compact(peer)...)
|
||||
}
|
||||
if len(IPv6CompactDict) > 0 {
|
||||
bdict["peers6"] = IPv6CompactDict
|
||||
}
|
||||
|
||||
return bencode.NewEncoder(w).Encode(bdict)
|
||||
}
|
||||
|
||||
// Add the peers to the dictionary.
|
||||
var peers []bencode.Dict
|
||||
for _, peer := range resp.IPv4Peers {
|
||||
peers = append(peers, dict(peer))
|
||||
}
|
||||
for _, peer := range resp.IPv6Peers {
|
||||
peers = append(peers, dict(peer))
|
||||
}
|
||||
bdict["peers"] = peers
|
||||
|
||||
return bencode.NewEncoder(w).Encode(bdict)
|
||||
}
|
||||
|
||||
// WriteScrapeResponse communicates the results of a Scrape to a BitTorrent
|
||||
// client over HTTP.
|
||||
func WriteScrapeResponse(w http.ResponseWriter, resp *bittorrent.ScrapeResponse) error {
|
||||
filesDict := bencode.NewDict()
|
||||
for infohash, scrape := range resp.Files {
|
||||
filesDict[string(infohash[:])] = bencode.Dict{
|
||||
"complete": scrape.Complete,
|
||||
"incomplete": scrape.Incomplete,
|
||||
}
|
||||
}
|
||||
|
||||
return bencode.NewEncoder(w).Encode(bencode.Dict{
|
||||
"files": filesDict,
|
||||
})
|
||||
}
|
||||
|
||||
func compact(peer bittorrent.Peer) (buf []byte) {
|
||||
buf = []byte(peer.IP)
|
||||
buf = append(buf, byte(peer.Port>>8))
|
||||
buf = append(buf, byte(peer.Port&0xff))
|
||||
return
|
||||
}
|
||||
|
||||
func dict(peer bittorrent.Peer) bencode.Dict {
|
||||
return bencode.Dict{
|
||||
"peer id": string(peer.ID[:]),
|
||||
"ip": peer.IP.String(),
|
||||
"port": peer.Port,
|
||||
}
|
||||
}
|
46
bittorrent/http/writer_test.go
Normal file
46
bittorrent/http/writer_test.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWriteError(t *testing.T) {
|
||||
var table = []struct {
|
||||
reason, expected string
|
||||
}{
|
||||
{"hello world", "d14:failure reason11:hello worlde"},
|
||||
{"what's up", "d14:failure reason9:what's upe"},
|
||||
}
|
||||
|
||||
for _, tt := range table {
|
||||
r := httptest.NewRecorder()
|
||||
err := writeError(r, bittorrent.ClientError(tt.reason))
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, r.Body.String(), tt.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteStatus(t *testing.T) {
|
||||
r := httptest.NewRecorder()
|
||||
err := writeError(r, bittorrent.ClientError("something is missing"))
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, r.Body.String(), "d14:failure reason20:something is missinge")
|
||||
}
|
64
bittorrent/udp/connection_id.go
Normal file
64
bittorrent/udp/connection_id.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ttl is the number of seconds a connection ID should be valid according to
|
||||
// BEP 15.
|
||||
const ttl = 2 * time.Minute
|
||||
|
||||
// NewConnectionID creates a new 8 byte connection identifier for UDP packets
|
||||
// as described by BEP 15.
|
||||
//
|
||||
// The first 4 bytes of the connection identifier is a unix timestamp and the
|
||||
// last 4 bytes are a truncated HMAC token created from the aforementioned
|
||||
// unix timestamp and the source IP address of the UDP packet.
|
||||
//
|
||||
// Truncated HMAC is known to be safe for 2^(-n) where n is the size in bits
|
||||
// of the truncated HMAC token. In this use case we have 32 bits, thus a
|
||||
// forgery probability of approximately 1 in 4 billion.
|
||||
func NewConnectionID(ip net.IP, now time.Time, key string) []byte {
|
||||
buf := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(buf, uint32(now.UTC().Unix()))
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write(buf[:4])
|
||||
mac.Write(ip)
|
||||
macBytes := mac.Sum(nil)[:4]
|
||||
copy(buf[4:], macBytes)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// ValidConnectionID determines whether a connection identifier is legitimate.
|
||||
func ValidConnectionID(connectionID []byte, ip net.IP, now time.Time, maxClockSkew time.Duration, key string) bool {
|
||||
ts := time.Unix(int64(binary.BigEndian.Uint32(connectionID[:4])), 0)
|
||||
if now.After(ts.Add(ttl)) || ts.After(now.Add(maxClockSkew)) {
|
||||
return false
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write(connectionID[:4])
|
||||
mac.Write(ip)
|
||||
expectedMAC := mac.Sum(nil)[:4]
|
||||
return hmac.Equal(expectedMAC, connectionID[4:])
|
||||
}
|
43
bittorrent/udp/connection_id_test.go
Normal file
43
bittorrent/udp/connection_id_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var golden = []struct {
|
||||
createdAt int64
|
||||
now int64
|
||||
ip string
|
||||
key string
|
||||
valid bool
|
||||
}{
|
||||
{0, 1, "127.0.0.1", "", true},
|
||||
{0, 420420, "127.0.0.1", "", false},
|
||||
{0, 0, "[::]", "", true},
|
||||
}
|
||||
|
||||
func TestVerification(t *testing.T) {
|
||||
for _, tt := range golden {
|
||||
cid := NewConnectionID(net.ParseIP(tt.ip), time.Unix(tt.createdAt, 0), tt.key)
|
||||
got := ValidConnectionID(cid, net.ParseIP(tt.ip), time.Unix(tt.now, 0), time.Minute, tt.key)
|
||||
if got != tt.valid {
|
||||
t.Errorf("expected validity: %t got validity: %t", tt.valid, got)
|
||||
}
|
||||
}
|
||||
}
|
178
bittorrent/udp/parser.go
Normal file
178
bittorrent/udp/parser.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
const (
|
||||
connectActionID uint32 = iota
|
||||
announceActionID
|
||||
scrapeActionID
|
||||
errorActionID
|
||||
announceDualStackActionID
|
||||
)
|
||||
|
||||
// Option-Types as described in BEP 41 and BEP 45.
|
||||
const (
|
||||
optionEndOfOptions byte = 0x0
|
||||
optionNOP = 0x1
|
||||
optionURLData = 0x2
|
||||
)
|
||||
|
||||
var (
|
||||
// initialConnectionID is the magic initial connection ID specified by BEP 15.
|
||||
initialConnectionID = []byte{0, 0, 0x04, 0x17, 0x27, 0x10, 0x19, 0x80}
|
||||
|
||||
// emptyIPs are the value of an IP field that has been left blank.
|
||||
emptyIPv4 = []byte{0, 0, 0, 0}
|
||||
emptyIPv6 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
|
||||
// eventIDs map values described in BEP 15 to Events.
|
||||
eventIDs = []bittorrent.Event{
|
||||
bittorrent.None,
|
||||
bittorrent.Completed,
|
||||
bittorrent.Started,
|
||||
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")
|
||||
)
|
||||
|
||||
// ParseAnnounce parses an AnnounceRequest from a UDP request.
|
||||
//
|
||||
// If allowIPSpoofing is true, IPs provided via params will be used.
|
||||
func ParseAnnounce(r Request, allowIPSpoofing bool) (*bittorrent.AnnounceRequest, error) {
|
||||
if len(r.packet) < 98 {
|
||||
return nil, errMalformedPacket
|
||||
}
|
||||
|
||||
infohash := r.packet[16:36]
|
||||
peerID := r.packet[36:56]
|
||||
downloaded := binary.BigEndian.Uint64(r.packet[56:64])
|
||||
left := binary.BigEndian.Uint64(r.packet[64:72])
|
||||
uploaded := binary.BigEndian.Uint64(r.packet[72:80])
|
||||
|
||||
eventID := int(r.packet[83])
|
||||
if eventID >= len(eventIDs) {
|
||||
return nil, errMalformedEvent
|
||||
}
|
||||
|
||||
ip := r.IP
|
||||
ipbytes := r.packet[84:88]
|
||||
if allowIPSpoofing {
|
||||
ip = net.IP(ipbytes)
|
||||
}
|
||||
if !allowIPSpoofing && r.ip == nil {
|
||||
// We have no IP address to fallback on.
|
||||
return nil, errMalformedIP
|
||||
}
|
||||
|
||||
numWant := binary.BigEndian.Uint32(r.packet[92:96])
|
||||
port := binary.BigEndian.Uint16(r.packet[96:98])
|
||||
|
||||
params, err := handleOptionalParameters(r.packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &bittorrent.AnnounceRequest{
|
||||
Event: eventIDs[eventID],
|
||||
InfoHash: bittorrent.InfoHashFromBytes(infohash),
|
||||
NumWant: uint32(numWant),
|
||||
Left: left,
|
||||
Downloaded: downloaded,
|
||||
Uploaded: uploaded,
|
||||
Peer: bittorrent.Peer{
|
||||
ID: bittorrent.PeerIDFromBytes(peerID),
|
||||
IP: ip,
|
||||
Port: port,
|
||||
},
|
||||
Params: params,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
optionStartIndex := 98
|
||||
for optionStartIndex < len(packet)-1 {
|
||||
option := packet[optionStartIndex]
|
||||
switch option {
|
||||
case optionEndOfOptions:
|
||||
return
|
||||
|
||||
case optionNOP:
|
||||
optionStartIndex++
|
||||
|
||||
case optionURLData:
|
||||
if optionStartIndex+1 > len(packet)-1 {
|
||||
return params, errMalformedPacket
|
||||
}
|
||||
|
||||
length := int(packet[optionStartIndex+1])
|
||||
if optionStartIndex+1+length > len(packet)-1 {
|
||||
return params, errMalformedPacket
|
||||
}
|
||||
|
||||
// TODO(jzelinskie): Actually parse the URL Data as described in BEP 41
|
||||
// into something that fulfills the bittorrent.Params interface.
|
||||
|
||||
optionStartIndex += 1 + length
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ParseScrape parses a ScrapeRequest from a UDP request.
|
||||
func parseScrape(r Request) (*bittorrent.ScrapeRequest, error) {
|
||||
// If a scrape isn't at least 36 bytes long, it's malformed.
|
||||
if len(r.packet) < 36 {
|
||||
return nil, errMalformedPacket
|
||||
}
|
||||
|
||||
// Skip past the initial headers and check that the bytes left equal the
|
||||
// length of a valid list of infohashes.
|
||||
r.packet = r.packet[16:]
|
||||
if len(r.packet)%20 != 0 {
|
||||
return nil, errMalformedPacket
|
||||
}
|
||||
|
||||
// Allocate a list of infohashes and append it to the list until we're out.
|
||||
var infohashes []bittorrent.InfoHash
|
||||
for len(r.packet) >= 20 {
|
||||
infohashes = append(infohashes, bittorrent.InfoHashFromBytes(r.packet[:20]))
|
||||
r.packet = r.packet[20:]
|
||||
}
|
||||
|
||||
return &bittorrent.ScrapeRequest{
|
||||
InfoHashes: infohashes,
|
||||
}, nil
|
||||
}
|
234
bittorrent/udp/server.go
Normal file
234
bittorrent/udp/server.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
var promResponseDurationMilliseconds = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "trakr_udp_response_duration_milliseconds",
|
||||
Help: "The duration of time it takes to receive and write a response to an API request",
|
||||
Buckets: prometheus.ExponentialBuckets(9.375, 2, 10),
|
||||
},
|
||||
[]string{"action", "error"},
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
PrivateKey string
|
||||
AllowIPSpoofing bool
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
sock *net.UDPConn
|
||||
closing chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
bittorrent.ServerFuncs
|
||||
Config
|
||||
}
|
||||
|
||||
func NewServer(funcs bittorrent.ServerFuncs, cfg Config) {
|
||||
return &Server{
|
||||
closing: make(chan struct{}),
|
||||
ServerFuncs: funcs,
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *udpServer) Stop() {
|
||||
close(s.closing)
|
||||
s.sock.SetReadDeadline(time.Now())
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe() error {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", s.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.sock, err = net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.sock.Close()
|
||||
|
||||
pool := bytepool.New(256, 2048)
|
||||
|
||||
for {
|
||||
// Check to see if we need to shutdown.
|
||||
select {
|
||||
case <-s.closing:
|
||||
s.wg.Wait()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Read a UDP packet into a reusable buffer.
|
||||
buffer := pool.Get()
|
||||
s.sock.SetReadDeadline(time.Now().Add(time.Second))
|
||||
n, addr, err := s.sock.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
pool.Put(buffer)
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
|
||||
// A temporary failure is not fatal; just pretend it never happened.
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// We got nothin'
|
||||
if n == 0 {
|
||||
pool.Put(buffer)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("Got UDP packet")
|
||||
start := time.Now()
|
||||
s.wg.Add(1)
|
||||
go func(start time.Time) {
|
||||
defer s.wg.Done()
|
||||
defer pool.Put(buffer)
|
||||
|
||||
// Handle the response.
|
||||
response, action, err := s.handlePacket(buffer[:n], addr)
|
||||
log.Printf("Handled UDP packet: %s, %s, %s\n", response, action, err)
|
||||
|
||||
// Record to Prometheus the time in milliseconds to receive, handle, and
|
||||
// respond to the request.
|
||||
duration := time.Since(start)
|
||||
if err != nil {
|
||||
promResponseDurationMilliseconds.WithLabelValues(action, err.Error()).Observe(float64(duration.Nanoseconds()) / float64(time.Millisecond))
|
||||
} else {
|
||||
promResponseDurationMilliseconds.WithLabelValues(action, "").Observe(float64(duration.Nanoseconds()) / float64(time.Millisecond))
|
||||
}
|
||||
}(start)
|
||||
}
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Packet []byte
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
type ResponseWriter struct {
|
||||
socket net.UDPConn
|
||||
addr net.UDPAddr
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) Write(b []byte) (int, error) {
|
||||
w.socket.WriteToUDP(b, w.addr)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (s *Server) handlePacket(r *Request, w *ResponseWriter) (response []byte, actionName string, err error) {
|
||||
if len(r.packet) < 16 {
|
||||
// Malformed, no client packets are less than 16 bytes.
|
||||
// We explicitly return nothing in case this is a DoS attempt.
|
||||
err = errMalformedPacket
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the headers of the UDP packet.
|
||||
connID := r.packet[0:8]
|
||||
actionID := binary.BigEndian.Uint32(r.packet[8:12])
|
||||
txID := r.packet[12:16]
|
||||
|
||||
// If this isn't requesting a new connection ID and the connection ID is
|
||||
// invalid, then fail.
|
||||
if actionID != connectActionID && !ValidConnectionID(connID, r.IP, time.Now(), s.PrivateKey) {
|
||||
err = errBadConnectionID
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the requested action.
|
||||
switch actionID {
|
||||
case connectActionID:
|
||||
actionName = "connect"
|
||||
|
||||
if !bytes.Equal(connID, initialConnectionID) {
|
||||
err = errMalformedPacket
|
||||
return
|
||||
}
|
||||
|
||||
WriteConnectionID(w, txID, NewConnectionID(r.IP, time.Now(), s.PrivateKey))
|
||||
return
|
||||
|
||||
case announceActionID:
|
||||
actionName = "announce"
|
||||
|
||||
var req *bittorrent.AnnounceRequest
|
||||
req, err = ParseAnnounce(r, s.AllowIPSpoofing)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
var resp *bittorrent.AnnounceResponse
|
||||
resp, err = s.HandleAnnounce(req)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteAnnounce(w, txID, resp)
|
||||
|
||||
if s.AfterAnnounce != nil {
|
||||
s.AfterAnnounce(req, resp)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case scrapeActionID:
|
||||
actionName = "scrape"
|
||||
|
||||
var req *bittorrent.ScrapeRequest
|
||||
req, err = ParseScrape(r)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
var resp *bittorrent.ScrapeResponse
|
||||
ctx := context.TODO()
|
||||
resp, err = s.HandleScrape(ctx, req)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteScrape(w, txID, resp)
|
||||
|
||||
if s.AfterScrape != nil {
|
||||
s.AfterScrape(req, resp)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
default:
|
||||
err = errUnknownAction
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
}
|
75
bittorrent/udp/writer.go
Normal file
75
bittorrent/udp/writer.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
// WriteError writes the failure reason as a null-terminated string.
|
||||
func WriteError(writer io.Writer, txID []byte, err error) {
|
||||
// If the client wasn't at fault, acknowledge it.
|
||||
if _, ok := err.(bittorrent.ClientError); !ok {
|
||||
err = fmt.Errorf("internal error occurred: %s", err.Error())
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
writeHeader(buf, txID, errorActionID)
|
||||
buf.WriteString(err.Error())
|
||||
buf.WriteRune('\000')
|
||||
writer.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// WriteAnnounce encodes an announce response according to BEP 15.
|
||||
func WriteAnnounce(respBuf *bytes.Buffer, txID []byte, resp *bittorrent.AnnounceResponse) {
|
||||
writeHeader(respBuf, txID, announceActionID)
|
||||
binary.Write(respBuf, binary.BigEndian, uint32(resp.Interval/time.Second))
|
||||
binary.Write(respBuf, binary.BigEndian, uint32(resp.Incomplete))
|
||||
binary.Write(respBuf, binary.BigEndian, uint32(resp.Complete))
|
||||
|
||||
for _, peer := range resp.IPv4Peers {
|
||||
respBuf.Write(peer.IP)
|
||||
binary.Write(respBuf, binary.BigEndian, peer.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteScrape encodes a scrape response according to BEP 15.
|
||||
func WriteScrape(respBuf *bytes.Buffer, txID []byte, resp *bittorrent.ScrapeResponse) {
|
||||
writeHeader(respBuf, txID, scrapeActionID)
|
||||
|
||||
for _, scrape := range resp.Files {
|
||||
binary.Write(respBuf, binary.BigEndian, scrape.Complete)
|
||||
binary.Write(respBuf, binary.BigEndian, scrape.Snatches)
|
||||
binary.Write(respBuf, binary.BigEndian, scrape.Incomplete)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteConnectionID encodes a new connection response according to BEP 15.
|
||||
func WriteConnectionID(respBuf *bytes.Buffer, txID, connID []byte) {
|
||||
writeHeader(respBuf, txID, connectActionID)
|
||||
respBuf.Write(connID)
|
||||
}
|
||||
|
||||
// writeHeader writes the action and transaction ID to the provided response
|
||||
// buffer.
|
||||
func writeHeader(respBuf *bytes.Buffer, txID []byte, action uint32) {
|
||||
binary.Write(respBuf, binary.BigEndian, action)
|
||||
respBuf.Write(txID)
|
||||
}
|
0
cmd/trakr/config.go
Normal file
0
cmd/trakr/config.go
Normal file
0
cmd/trakr/main.go
Normal file
0
cmd/trakr/main.go
Normal file
77
hook.go
Normal file
77
hook.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package trakr
|
||||
|
||||
import "github.com/jzelinskie/trakr/bittorrent"
|
||||
|
||||
// HookConstructor is a function used to create a new instance of a Hook.
|
||||
type HookConstructor func(interface{}) (Hook, error)
|
||||
|
||||
// Hook abstracts the concept of anything that needs to interact with a
|
||||
// BitTorrent client's request and response to a BitTorrent tracker.
|
||||
type Hook interface {
|
||||
HandleAnnounce(context.Context, bittorrent.AnnounceRequest, bittorrent.AnnounceResponse) error
|
||||
HandleScrape(context.Context, bittorrent.ScrapeRequest, bittorrent.ScrapeResponse) error
|
||||
}
|
||||
|
||||
var preHooks = make(map[string]HookConstructor)
|
||||
|
||||
// RegisterPreHook makes a HookConstructor available by the provided name.
|
||||
//
|
||||
// If this function is called twice with the same name or if the
|
||||
// HookConstructor is nil, it panics.
|
||||
func RegisterPreHook(name string, con HookConstructor) {
|
||||
if con == nil {
|
||||
panic("trakr: could not register nil HookConstructor")
|
||||
}
|
||||
if _, dup := constructors[name]; dup {
|
||||
panic("trakr: could not register duplicate HookConstructor: " + name)
|
||||
}
|
||||
preHooks[name] = con
|
||||
}
|
||||
|
||||
// NewPreHook creates an instance of the given PreHook by name.
|
||||
func NewPreHook(name string, config interface{}) (Hook, error) {
|
||||
con := preHooks[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("trakr: unknown PreHook %q (forgotten import?)", name)
|
||||
}
|
||||
return con(config)
|
||||
}
|
||||
|
||||
var postHooks = make(map[string]HookConstructor)
|
||||
|
||||
// RegisterPostHook makes a HookConstructor available by the provided name.
|
||||
//
|
||||
// If this function is called twice with the same name or if the
|
||||
// HookConstructor is nil, it panics.
|
||||
func RegisterPostHook(name string, con HookConstructor) {
|
||||
if con == nil {
|
||||
panic("trakr: could not register nil HookConstructor")
|
||||
}
|
||||
if _, dup := constructors[name]; dup {
|
||||
panic("trakr: could not register duplicate HookConstructor: " + name)
|
||||
}
|
||||
preHooks[name] = con
|
||||
}
|
||||
|
||||
// NewPostHook creates an instance of the given PostHook by name.
|
||||
func NewPostHook(name string, config interface{}) (Hook, error) {
|
||||
con := preHooks[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("trakr: unknown PostHook %q (forgotten import?)", name)
|
||||
}
|
||||
return con(config)
|
||||
}
|
28
server.go
Normal file
28
server.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2016 Jimmy Zelinskie
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package trakr
|
||||
|
||||
type Server struct {
|
||||
HTTPConfig http.Config
|
||||
UDPConfig udp.Config
|
||||
Interval time.Duration
|
||||
PreHooks []string
|
||||
PostHooks []string
|
||||
|
||||
udpserver
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe() error {
|
||||
}
|
Loading…
Reference in a new issue