initial foundation

Definitely doesn't work, certainly has the right ideas.
This commit is contained in:
Jimmy Zelinskie 2016-08-03 03:11:52 -04:00
parent 1bff8d1571
commit a2d3080a60
27 changed files with 2378 additions and 0 deletions

36
DCO Normal file
View 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
View file

@ -0,0 +1 @@
Jimmy Zelinskie <jimmyzelinskie@gmail.com> (@jzelinskie) pkg:*

View 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)
}

View 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)
}

View 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()
}
}

View 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))
}

View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
}

View 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
}

View 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
View 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
View 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,
}
}

View 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")
}

View 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:])
}

View 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
View 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
View 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
View 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
View file

0
cmd/trakr/main.go Normal file
View file

77
hook.go Normal file
View 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
View 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 {
}