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…
Add table
Reference in a new issue