rename frontends to frontend
This commit is contained in:
parent
88567d5b2e
commit
ae18d89627
20 changed files with 13 additions and 13 deletions
30
frontend/frontend.go
Normal file
30
frontend/frontend.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package frontend
|
||||
|
||||
import (
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// TrackerFuncs is the collection of callback functions provided by the Backend
|
||||
// to (1) generate a response from a parsed request, and (2) observe anything
|
||||
// after the response has been delivered to the client.
|
||||
type TrackerFuncs struct {
|
||||
HandleAnnounce AnnounceHandler
|
||||
HandleScrape ScrapeHandler
|
||||
AfterAnnounce AnnounceCallback
|
||||
AfterScrape ScrapeCallback
|
||||
}
|
||||
|
||||
// AnnounceHandler is a function that generates a response for an Announce.
|
||||
type AnnounceHandler func(context.Context, *bittorrent.AnnounceRequest) (*bittorrent.AnnounceResponse, error)
|
||||
|
||||
// AnnounceCallback is a function that does something with the results of an
|
||||
// Announce after it has been completed.
|
||||
type AnnounceCallback func(*bittorrent.AnnounceRequest, *bittorrent.AnnounceResponse)
|
||||
|
||||
// ScrapeHandler is a function that generates a response for a Scrape.
|
||||
type ScrapeHandler func(context.Context, *bittorrent.ScrapeRequest) (*bittorrent.ScrapeResponse, error)
|
||||
|
||||
// ScrapeCallback is a function that does something with the results of a
|
||||
// Scrape after it has been completed.
|
||||
type ScrapeCallback func(*bittorrent.ScrapeRequest, *bittorrent.ScrapeResponse)
|
33
frontend/http/bencode/bencode.go
Normal file
33
frontend/http/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
frontend/http/bencode/decoder.go
Normal file
145
frontend/http/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
frontend/http/bencode/decoder_test.go
Normal file
96
frontend/http/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
frontend/http/bencode/encoder.go
Normal file
173
frontend/http/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
frontend/http/bencode/encoder_test.go
Normal file
81
frontend/http/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)
|
||||
}
|
||||
}
|
197
frontend/http/frontend.go
Normal file
197
frontend/http/frontend.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
// 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 implements a BitTorrent frontend via the HTTP protocol as
|
||||
// described in BEP 3 and BEP 23.
|
||||
package http
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/tylerb/graceful"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/jzelinskie/trakr/frontend"
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(promResponseDurationMilliseconds)
|
||||
recordResponseDuration("action", nil, time.Second)
|
||||
}
|
||||
|
||||
var promResponseDurationMilliseconds = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "trakr_http_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"},
|
||||
)
|
||||
|
||||
// recordResponseDuration records the duration of time to respond to a Request
|
||||
// in milliseconds .
|
||||
func recordResponseDuration(action string, err error, duration time.Duration) {
|
||||
var errString string
|
||||
if err != nil {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
promResponseDurationMilliseconds.
|
||||
WithLabelValues(action, errString).
|
||||
Observe(float64(duration.Nanoseconds()) / float64(time.Millisecond))
|
||||
}
|
||||
|
||||
// Config represents all of the configurable options for an HTTP BitTorrent
|
||||
// Frontend.
|
||||
type Config struct {
|
||||
Addr string
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
RequestTimeout time.Duration
|
||||
AllowIPSpoofing bool
|
||||
RealIPHeader string
|
||||
}
|
||||
|
||||
// Frontend holds the state of an HTTP BitTorrent Frontend.
|
||||
type Frontend struct {
|
||||
grace *graceful.Server
|
||||
|
||||
frontend.TrackerFuncs
|
||||
Config
|
||||
}
|
||||
|
||||
// NewFrontend allocates a new instance of a Frontend.
|
||||
func NewFrontend(funcs frontend.TrackerFuncs, cfg Config) *Frontend {
|
||||
return &Frontend{
|
||||
TrackerFuncs: funcs,
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Stop provides a thread-safe way to shutdown a currently running Tracker.
|
||||
func (t *Frontend) Stop() {
|
||||
t.grace.Stop(t.grace.Timeout)
|
||||
<-t.grace.StopChan()
|
||||
}
|
||||
|
||||
func (t *Frontend) handler() http.Handler {
|
||||
router := httprouter.New()
|
||||
router.GET("/announce", t.announceRoute)
|
||||
router.GET("/scrape", t.scrapeRoute)
|
||||
return router
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the TCP network address t.Addr and blocks serving
|
||||
// BitTorrent requests until t.Stop() is called or an error is returned.
|
||||
func (t *Frontend) ListenAndServe() error {
|
||||
t.grace = &graceful.Server{
|
||||
Server: &http.Server{
|
||||
Addr: t.Addr,
|
||||
Handler: t.handler(),
|
||||
ReadTimeout: t.ReadTimeout,
|
||||
WriteTimeout: t.WriteTimeout,
|
||||
},
|
||||
Timeout: t.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")
|
||||
}
|
||||
},
|
||||
}
|
||||
t.grace.SetKeepAlivesEnabled(false)
|
||||
|
||||
if err := t.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())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// announceRoute parses and responds to an Announce by using t.TrackerFuncs.
|
||||
func (t *Frontend) announceRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
var err error
|
||||
start := time.Now()
|
||||
defer recordResponseDuration("announce", err, time.Since(start))
|
||||
|
||||
req, err := ParseAnnounce(r, t.RealIPHeader, t.AllowIPSpoofing)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := t.HandleAnnounce(context.TODO(), req)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = WriteAnnounceResponse(w, resp)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if t.AfterAnnounce != nil {
|
||||
go t.AfterAnnounce(req, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// scrapeRoute parses and responds to a Scrape by using t.TrackerFuncs.
|
||||
func (t *Frontend) scrapeRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
var err error
|
||||
start := time.Now()
|
||||
defer recordResponseDuration("scrape", err, time.Since(start))
|
||||
|
||||
req, err := ParseScrape(r)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := t.HandleScrape(context.TODO(), req)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = WriteScrapeResponse(w, resp)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if t.AfterScrape != nil {
|
||||
go t.AfterScrape(req, resp)
|
||||
}
|
||||
}
|
151
frontend/http/parser.go
Normal file
151
frontend/http/parser.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
// 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: qp}
|
||||
|
||||
eventStr, _ := qp.String("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, ok := qp.String("peer_id")
|
||||
if !ok {
|
||||
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.Peer.ID = 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, err := qp.Uint64("numwant")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: numwant")
|
||||
}
|
||||
request.NumWant = uint32(numwant)
|
||||
|
||||
port, err := qp.Uint64("port")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: port")
|
||||
}
|
||||
request.Peer.Port = uint16(port)
|
||||
|
||||
request.Peer.IP = requestedIP(r, qp, realIPHeader, allowIPSpoofing)
|
||||
if request.Peer.IP == nil {
|
||||
return nil, bittorrent.ClientError("failed to parse peer IP address")
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// ParseScrape parses an bittorrent.ScrapeRequest from an http.Request.
|
||||
func ParseScrape(r *http.Request) (*bittorrent.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: qp,
|
||||
}
|
||||
|
||||
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 bittorrent.Params, realIPHeader string, allowIPSpoofing bool) net.IP {
|
||||
if allowIPSpoofing {
|
||||
if ipstr, ok := p.String("ip"); ok {
|
||||
ip := net.ParseIP(ipstr)
|
||||
return ip
|
||||
}
|
||||
|
||||
if ipstr, ok := p.String("ipv4"); ok {
|
||||
ip := net.ParseIP(ipstr)
|
||||
return ip
|
||||
}
|
||||
|
||||
if ipstr, ok := p.String("ipv6"); ok {
|
||||
ip := net.ParseIP(ipstr)
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
if realIPHeader != "" {
|
||||
if ips, ok := r.Header[realIPHeader]; ok && len(ips) > 0 {
|
||||
ip := net.ParseIP(ips[0])
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
return net.ParseIP(host)
|
||||
}
|
138
frontend/http/query_params.go
Normal file
138
frontend/http/query_params.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
// 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) (*QueryParams, error) {
|
||||
var (
|
||||
keyStart, keyEnd int
|
||||
valStart, valEnd int
|
||||
|
||||
onKey = true
|
||||
|
||||
q = &QueryParams{
|
||||
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 (qp *QueryParams) String(key string) (string, bool) {
|
||||
value, ok := qp.params[key]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Uint64 returns a uint parsed from a query. After being called, it is safe to
|
||||
// cast the uint64 to your desired length.
|
||||
func (qp *QueryParams) Uint64(key string) (uint64, error) {
|
||||
str, exists := qp.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 (qp *QueryParams) InfoHashes() []bittorrent.InfoHash {
|
||||
return qp.infoHashes
|
||||
}
|
110
frontend/http/query_params_test.go
Normal file
110
frontend/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
112
frontend/http/writer.go
Normal file
112
frontend/http/writer.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
// 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"
|
||||
"github.com/jzelinskie/trakr/frontend/http/bencode"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
47
frontend/http/writer_test.go
Normal file
47
frontend/http/writer_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
// 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/stretchr/testify/assert"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
38
frontend/udp/bytepool/bytepool.go
Normal file
38
frontend/udp/bytepool/bytepool.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2016 The Chihaya Authors. All rights reserved.
|
||||
// Use of this source code is governed by the BSD 2-Clause license,
|
||||
// which can be found in the LICENSE file.
|
||||
|
||||
package bytepool
|
||||
|
||||
import "sync"
|
||||
|
||||
// BytePool is a cached pool of reusable byte slices.
|
||||
type BytePool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
// New allocates a new BytePool with slices of the provided capacity.
|
||||
func New(length, capacity int) *BytePool {
|
||||
var bp BytePool
|
||||
bp.Pool.New = func() interface{} {
|
||||
return make([]byte, length, capacity)
|
||||
}
|
||||
return &bp
|
||||
}
|
||||
|
||||
// Get returns a byte slice from the pool.
|
||||
func (bp *BytePool) Get() []byte {
|
||||
return bp.Pool.Get().([]byte)
|
||||
}
|
||||
|
||||
// Put returns a byte slice to the pool.
|
||||
func (bp *BytePool) Put(b []byte) {
|
||||
b = b[:cap(b)]
|
||||
// Zero out the bytes.
|
||||
// Apparently this specific expression is optimized by the compiler, see
|
||||
// github.com/golang/go/issues/5373.
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
bp.Pool.Put(b)
|
||||
}
|
64
frontend/udp/connection_id.go
Normal file
64
frontend/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
frontend/udp/connection_id_test.go
Normal file
43
frontend/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)
|
||||
}
|
||||
}
|
||||
}
|
268
frontend/udp/frontend.go
Normal file
268
frontend/udp/frontend.go
Normal file
|
@ -0,0 +1,268 @@
|
|||
// 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 implements a BitTorrent tracker via the UDP protocol as
|
||||
// described in BEP 15.
|
||||
package udp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
"github.com/jzelinskie/trakr/frontend"
|
||||
"github.com/jzelinskie/trakr/frontend/udp/bytepool"
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(promResponseDurationMilliseconds)
|
||||
recordResponseDuration("action", nil, time.Second)
|
||||
}
|
||||
|
||||
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"},
|
||||
)
|
||||
|
||||
// recordResponseDuration records the duration of time to respond to a UDP
|
||||
// Request in milliseconds .
|
||||
func recordResponseDuration(action string, err error, duration time.Duration) {
|
||||
var errString string
|
||||
if err != nil {
|
||||
errString = err.Error()
|
||||
}
|
||||
|
||||
promResponseDurationMilliseconds.
|
||||
WithLabelValues(action, errString).
|
||||
Observe(float64(duration.Nanoseconds()) / float64(time.Millisecond))
|
||||
}
|
||||
|
||||
// Config represents all of the configurable options for a UDP BitTorrent
|
||||
// Tracker.
|
||||
type Config struct {
|
||||
Addr string
|
||||
PrivateKey string
|
||||
MaxClockSkew time.Duration
|
||||
AllowIPSpoofing bool
|
||||
}
|
||||
|
||||
// Frontend holds the state of a UDP BitTorrent Frontend.
|
||||
type Frontend struct {
|
||||
socket *net.UDPConn
|
||||
closing chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
frontend.TrackerFuncs
|
||||
Config
|
||||
}
|
||||
|
||||
// NewFrontend allocates a new instance of a Frontend.
|
||||
func NewFrontend(funcs frontend.TrackerFuncs, cfg Config) *Frontend {
|
||||
return &Frontend{
|
||||
closing: make(chan struct{}),
|
||||
TrackerFuncs: funcs,
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Stop provides a thread-safe way to shutdown a currently running Frontend.
|
||||
func (t *Frontend) Stop() {
|
||||
close(t.closing)
|
||||
t.socket.SetReadDeadline(time.Now())
|
||||
t.wg.Wait()
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the UDP network address t.Addr and blocks serving
|
||||
// BitTorrent requests until t.Stop() is called or an error is returned.
|
||||
func (t *Frontend) ListenAndServe() error {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", t.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.socket, err = net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer t.socket.Close()
|
||||
|
||||
pool := bytepool.New(256, 2048)
|
||||
|
||||
for {
|
||||
// Check to see if we need to shutdown.
|
||||
select {
|
||||
case <-t.closing:
|
||||
t.wg.Wait()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Read a UDP packet into a reusable buffer.
|
||||
buffer := pool.Get()
|
||||
t.socket.SetReadDeadline(time.Now().Add(time.Second))
|
||||
n, addr, err := t.socket.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 Request")
|
||||
t.wg.Add(1)
|
||||
go func() {
|
||||
defer t.wg.Done()
|
||||
defer pool.Put(buffer)
|
||||
|
||||
// Handle the request.
|
||||
start := time.Now()
|
||||
response, action, err := t.handleRequest(
|
||||
Request{buffer[:n], addr.IP},
|
||||
ResponseWriter{t.socket, addr},
|
||||
)
|
||||
log.Printf("Handled UDP Request: %s, %s, %s\n", response, action, err)
|
||||
recordResponseDuration(action, err, time.Since(start))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Request represents a UDP payload received by a Tracker.
|
||||
type Request struct {
|
||||
Packet []byte
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
// ResponseWriter implements the ability to respond to a Request via the
|
||||
// io.Writer interface.
|
||||
type ResponseWriter struct {
|
||||
socket *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface for a ResponseWriter.
|
||||
func (w ResponseWriter) Write(b []byte) (int, error) {
|
||||
w.socket.WriteToUDP(b, w.addr)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// handleRequest parses and responds to a UDP Request.
|
||||
func (t *Frontend) handleRequest(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(), t.MaxClockSkew, t.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(), t.PrivateKey))
|
||||
return
|
||||
|
||||
case announceActionID:
|
||||
actionName = "announce"
|
||||
|
||||
var req *bittorrent.AnnounceRequest
|
||||
req, err = ParseAnnounce(r, t.AllowIPSpoofing)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
var resp *bittorrent.AnnounceResponse
|
||||
resp, err = t.HandleAnnounce(context.TODO(), req)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteAnnounce(w, txID, resp)
|
||||
|
||||
if t.AfterAnnounce != nil {
|
||||
go t.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
|
||||
resp, err = t.HandleScrape(context.TODO(), req)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteScrape(w, txID, resp)
|
||||
|
||||
if t.AfterScrape != nil {
|
||||
go t.AfterScrape(req, resp)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
default:
|
||||
err = errUnknownAction
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
}
|
||||
}
|
178
frontend/udp/parser.go
Normal file
178
frontend/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
|
||||
}
|
76
frontend/udp/writer.go
Normal file
76
frontend/udp/writer.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
// 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"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/jzelinskie/trakr/bittorrent"
|
||||
)
|
||||
|
||||
// WriteError writes the failure reason as a null-terminated string.
|
||||
func WriteError(w 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')
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// WriteAnnounce encodes an announce response according to BEP 15.
|
||||
func WriteAnnounce(w io.Writer, txID []byte, resp *bittorrent.AnnounceResponse) {
|
||||
writeHeader(w, txID, announceActionID)
|
||||
binary.Write(w, binary.BigEndian, uint32(resp.Interval/time.Second))
|
||||
binary.Write(w, binary.BigEndian, uint32(resp.Incomplete))
|
||||
binary.Write(w, binary.BigEndian, uint32(resp.Complete))
|
||||
|
||||
for _, peer := range resp.IPv4Peers {
|
||||
w.Write(peer.IP)
|
||||
binary.Write(w, binary.BigEndian, peer.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteScrape encodes a scrape response according to BEP 15.
|
||||
func WriteScrape(w io.Writer, txID []byte, resp *bittorrent.ScrapeResponse) {
|
||||
writeHeader(w, txID, scrapeActionID)
|
||||
|
||||
for _, scrape := range resp.Files {
|
||||
binary.Write(w, binary.BigEndian, scrape.Complete)
|
||||
binary.Write(w, binary.BigEndian, scrape.Snatches)
|
||||
binary.Write(w, binary.BigEndian, scrape.Incomplete)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteConnectionID encodes a new connection response according to BEP 15.
|
||||
func WriteConnectionID(w io.Writer, txID, connID []byte) {
|
||||
writeHeader(w, txID, connectActionID)
|
||||
w.Write(connID)
|
||||
}
|
||||
|
||||
// writeHeader writes the action and transaction ID to the provided response
|
||||
// buffer.
|
||||
func writeHeader(w io.Writer, txID []byte, action uint32) {
|
||||
binary.Write(w, binary.BigEndian, action)
|
||||
w.Write(txID)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue