Merge pull request #363 from jzelinskie/req-san
Request Sanitizer via library
This commit is contained in:
commit
80558648d7
10 changed files with 253 additions and 165 deletions
|
@ -81,6 +81,9 @@ type AnnounceRequest struct {
|
|||
Event Event
|
||||
InfoHash InfoHash
|
||||
Compact bool
|
||||
EventProvided bool
|
||||
NumWantProvided bool
|
||||
IPProvided bool
|
||||
NumWant uint32
|
||||
Left uint64
|
||||
Downloaded uint64
|
||||
|
@ -90,6 +93,24 @@ type AnnounceRequest struct {
|
|||
Params
|
||||
}
|
||||
|
||||
// LogFields renders the current response as a set of log fields.
|
||||
func (r AnnounceRequest) LogFields() log.Fields {
|
||||
return log.Fields{
|
||||
"event": r.Event,
|
||||
"infoHash": r.InfoHash,
|
||||
"compact": r.Compact,
|
||||
"eventProvided": r.EventProvided,
|
||||
"numWantProvided": r.NumWantProvided,
|
||||
"ipProvided": r.IPProvided,
|
||||
"numWant": r.NumWant,
|
||||
"left": r.Left,
|
||||
"downloaded": r.Downloaded,
|
||||
"uploaded": r.Uploaded,
|
||||
"peer": r.Peer,
|
||||
"params": r.Params,
|
||||
}
|
||||
}
|
||||
|
||||
// AnnounceResponse represents the parameters used to create an announce
|
||||
// response.
|
||||
type AnnounceResponse struct {
|
||||
|
@ -102,15 +123,15 @@ type AnnounceResponse struct {
|
|||
IPv6Peers []Peer
|
||||
}
|
||||
|
||||
// LogFields renders the current response as a set of Logrus fields.
|
||||
func (ar AnnounceResponse) LogFields() log.Fields {
|
||||
// LogFields renders the current response as a set of log fields.
|
||||
func (r AnnounceResponse) LogFields() log.Fields {
|
||||
return log.Fields{
|
||||
"compact": ar.Compact,
|
||||
"complete": ar.Complete,
|
||||
"interval": ar.Interval,
|
||||
"minInterval": ar.MinInterval,
|
||||
"ipv4Peers": ar.IPv4Peers,
|
||||
"ipv6Peers": ar.IPv6Peers,
|
||||
"compact": r.Compact,
|
||||
"complete": r.Complete,
|
||||
"interval": r.Interval,
|
||||
"minInterval": r.MinInterval,
|
||||
"ipv4Peers": r.IPv4Peers,
|
||||
"ipv6Peers": r.IPv6Peers,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,6 +142,15 @@ type ScrapeRequest struct {
|
|||
Params Params
|
||||
}
|
||||
|
||||
// LogFields renders the current response as a set of log fields.
|
||||
func (r ScrapeRequest) LogFields() log.Fields {
|
||||
return log.Fields{
|
||||
"addressFamily": r.AddressFamily,
|
||||
"infoHashes": r.InfoHashes,
|
||||
"params": r.Params,
|
||||
}
|
||||
}
|
||||
|
||||
// ScrapeResponse represents the parameters used to create a scrape response.
|
||||
//
|
||||
// The Scrapes must be in the same order as the InfoHashes in the corresponding
|
||||
|
@ -147,6 +177,17 @@ type Scrape struct {
|
|||
// AddressFamily is the address family of an IP address.
|
||||
type AddressFamily uint8
|
||||
|
||||
func (af AddressFamily) String() string {
|
||||
switch af {
|
||||
case IPv4:
|
||||
return "IPv4"
|
||||
case IPv6:
|
||||
return "IPv6"
|
||||
default:
|
||||
panic("tried to print unknown AddressFamily")
|
||||
}
|
||||
}
|
||||
|
||||
// AddressFamily constants.
|
||||
const (
|
||||
IPv4 AddressFamily = iota
|
||||
|
@ -159,6 +200,10 @@ type IP struct {
|
|||
AddressFamily
|
||||
}
|
||||
|
||||
func (ip IP) String() string {
|
||||
return ip.IP.String()
|
||||
}
|
||||
|
||||
// Peer represents the connection details of a peer that is returned in an
|
||||
// announce response.
|
||||
type Peer struct {
|
||||
|
|
48
bittorrent/sanitize.go
Normal file
48
bittorrent/sanitize.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package bittorrent
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/chihaya/chihaya/pkg/log"
|
||||
)
|
||||
|
||||
// ErrInvalidIP indicates an invalid IP for an Announce.
|
||||
var ErrInvalidIP = ClientError("invalid IP")
|
||||
|
||||
// SanitizeAnnounce enforces a max and default NumWant and coerces the peer's
|
||||
// IP address into the proper format.
|
||||
func SanitizeAnnounce(r *AnnounceRequest, maxNumWant, defaultNumWant uint32) error {
|
||||
if !r.NumWantProvided {
|
||||
r.NumWant = defaultNumWant
|
||||
} else if r.NumWant > maxNumWant {
|
||||
r.NumWant = maxNumWant
|
||||
}
|
||||
|
||||
if ip := r.Peer.IP.To4(); ip != nil {
|
||||
r.Peer.IP.IP = ip
|
||||
r.Peer.IP.AddressFamily = IPv4
|
||||
} else if len(r.Peer.IP.IP) == net.IPv6len { // implies r.Peer.IP.To4() == nil
|
||||
r.Peer.IP.AddressFamily = IPv6
|
||||
} else {
|
||||
return ErrInvalidIP
|
||||
}
|
||||
|
||||
log.Debug("sanitized announce", r, log.Fields{
|
||||
"maxNumWant": maxNumWant,
|
||||
"defaultNumWant": defaultNumWant,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeScrape enforces a max number of infohashes for a single scrape
|
||||
// request.
|
||||
func SanitizeScrape(r *ScrapeRequest, maxScrapeInfoHashes uint32) error {
|
||||
if len(r.InfoHashes) > int(maxScrapeInfoHashes) {
|
||||
r.InfoHashes = r.InfoHashes[:maxScrapeInfoHashes]
|
||||
}
|
||||
|
||||
log.Debug("sanitized scrape", r, log.Fields{
|
||||
"maxScrapeInfoHashes": maxScrapeInfoHashes,
|
||||
})
|
||||
return nil
|
||||
}
|
|
@ -24,14 +24,6 @@ chihaya:
|
|||
# BitTorrent traffic.
|
||||
addr: "0.0.0.0:6881"
|
||||
|
||||
# When enabled, the IP address used to connect to the tracker will not
|
||||
# override the value clients advertise as their IP address.
|
||||
allow_ip_spoofing: false
|
||||
|
||||
# The HTTP Header containing the IP address of the client.
|
||||
# This is only necessary if using a reverse proxy.
|
||||
real_ip_header: "x-real-ip"
|
||||
|
||||
# The path to the required files to listen via HTTPS.
|
||||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
|
@ -44,6 +36,23 @@ chihaya:
|
|||
# Disabling this should increase performance/decrease load.
|
||||
enable_request_timing: false
|
||||
|
||||
# When enabled, the IP address used to connect to the tracker will not
|
||||
# override the value clients advertise as their IP address.
|
||||
allow_ip_spoofing: false
|
||||
|
||||
# The HTTP Header containing the IP address of the client.
|
||||
# This is only necessary if using a reverse proxy.
|
||||
real_ip_header: "x-real-ip"
|
||||
|
||||
# The maximum number of peers returned for an individual request.
|
||||
max_numwant: 100
|
||||
|
||||
# The default number of peers returned for an individual request.
|
||||
default_numwant: 50
|
||||
|
||||
# The maximum number of infohashes that can be scraped in one request.
|
||||
max_scrape_infohashes: 50
|
||||
|
||||
# This block defines configuration for the tracker's UDP interface.
|
||||
# If you do not wish to run this, delete this section.
|
||||
udp:
|
||||
|
@ -51,10 +60,6 @@ chihaya:
|
|||
# BitTorrent traffic.
|
||||
addr: "0.0.0.0:6881"
|
||||
|
||||
# When enabled, the IP address used to connect to the tracker will not
|
||||
# override the value clients advertise as their IP address.
|
||||
allow_ip_spoofing: false
|
||||
|
||||
# The leeway for a timestamp on a connection ID.
|
||||
max_clock_skew: 10s
|
||||
|
||||
|
@ -65,6 +70,20 @@ chihaya:
|
|||
# Disabling this should increase performance/decrease load.
|
||||
enable_request_timing: false
|
||||
|
||||
# When enabled, the IP address used to connect to the tracker will not
|
||||
# override the value clients advertise as their IP address.
|
||||
allow_ip_spoofing: false
|
||||
|
||||
# The maximum number of peers returned for an individual request.
|
||||
max_numwant: 100
|
||||
|
||||
# The default number of peers returned for an individual request.
|
||||
default_numwant: 50
|
||||
|
||||
# The maximum number of infohashes that can be scraped in one request.
|
||||
max_scrape_infohashes: 50
|
||||
|
||||
|
||||
# This block defines configuration used for the storage of peer data.
|
||||
storage:
|
||||
name: memory
|
||||
|
|
|
@ -21,9 +21,6 @@ func init() {
|
|||
prometheus.MustRegister(promResponseDurationMilliseconds)
|
||||
}
|
||||
|
||||
// ErrInvalidIP indicates an invalid IP.
|
||||
var ErrInvalidIP = bittorrent.ClientError("invalid IP")
|
||||
|
||||
var promResponseDurationMilliseconds = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "chihaya_http_response_duration_milliseconds",
|
||||
|
@ -65,11 +62,10 @@ type Config struct {
|
|||
Addr string `yaml:"addr"`
|
||||
ReadTimeout time.Duration `yaml:"read_timeout"`
|
||||
WriteTimeout time.Duration `yaml:"write_timeout"`
|
||||
AllowIPSpoofing bool `yaml:"allow_ip_spoofing"`
|
||||
RealIPHeader string `yaml:"real_ip_header"`
|
||||
TLSCertPath string `yaml:"tls_cert_path"`
|
||||
TLSKeyPath string `yaml:"tls_key_path"`
|
||||
EnableRequestTiming bool `yaml:"enable_request_timing"`
|
||||
ParseOptions `yaml:",inline"`
|
||||
}
|
||||
|
||||
// LogFields renders the current config as a set of Logrus fields.
|
||||
|
@ -78,11 +74,14 @@ func (cfg Config) LogFields() log.Fields {
|
|||
"addr": cfg.Addr,
|
||||
"readTimeout": cfg.ReadTimeout,
|
||||
"writeTimeout": cfg.WriteTimeout,
|
||||
"allowIPSpoofing": cfg.AllowIPSpoofing,
|
||||
"realIPHeader": cfg.RealIPHeader,
|
||||
"tlsCertPath": cfg.TLSCertPath,
|
||||
"tlsKeyPath": cfg.TLSKeyPath,
|
||||
"enableRequestTiming": cfg.EnableRequestTiming,
|
||||
"allowIPSpoofing": cfg.AllowIPSpoofing,
|
||||
"realIPHeader": cfg.RealIPHeader,
|
||||
"maxNumWant": cfg.MaxNumWant,
|
||||
"defaultNumWant": cfg.DefaultNumWant,
|
||||
"maxScrapeInfoHashes": cfg.MaxScrapeInfoHashes,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,7 +218,7 @@ func (f *Frontend) announceRoute(w http.ResponseWriter, r *http.Request, _ httpr
|
|||
}
|
||||
}()
|
||||
|
||||
req, err := ParseAnnounce(r, f.RealIPHeader, f.AllowIPSpoofing)
|
||||
req, err := ParseAnnounce(r, f.ParseOptions)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
|
@ -258,7 +257,7 @@ func (f *Frontend) scrapeRoute(w http.ResponseWriter, r *http.Request, _ httprou
|
|||
}
|
||||
}()
|
||||
|
||||
req, err := ParseScrape(r)
|
||||
req, err := ParseScrape(r, f.ParseOptions)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
|
@ -278,7 +277,7 @@ func (f *Frontend) scrapeRoute(w http.ResponseWriter, r *http.Request, _ httprou
|
|||
req.AddressFamily = bittorrent.IPv6
|
||||
} else {
|
||||
log.Error("http: invalid IP: neither v4 nor v6", log.Fields{"RemoteAddr": r.RemoteAddr})
|
||||
WriteError(w, ErrInvalidIP)
|
||||
WriteError(w, bittorrent.ErrInvalidIP)
|
||||
return
|
||||
}
|
||||
af = new(bittorrent.AddressFamily)
|
||||
|
|
|
@ -7,12 +7,21 @@ import (
|
|||
"github.com/chihaya/chihaya/bittorrent"
|
||||
)
|
||||
|
||||
// ParseAnnounce parses an bittorrent.AnnounceRequest from an http.Request.
|
||||
// ParseOptions is the configuration used to parse an Announce 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
|
||||
// If AllowIPSpoofing is true, IPs provided via BitTorrent params will be used.
|
||||
// If RealIPHeader is not empty string, the value of the first HTTP Header with
|
||||
// that name will be used.
|
||||
func ParseAnnounce(r *http.Request, realIPHeader string, allowIPSpoofing bool) (*bittorrent.AnnounceRequest, error) {
|
||||
type ParseOptions struct {
|
||||
AllowIPSpoofing bool `yaml:"allow_ip_spoofing"`
|
||||
RealIPHeader string `yaml:"real_ip_header"`
|
||||
MaxNumWant uint32 `yaml:"max_numwant"`
|
||||
DefaultNumWant uint32 `yaml:"default_numwant"`
|
||||
MaxScrapeInfoHashes uint32 `yaml:"max_scrape_infohashes"`
|
||||
}
|
||||
|
||||
// ParseAnnounce parses an bittorrent.AnnounceRequest from an http.Request.
|
||||
func ParseAnnounce(r *http.Request, opts ParseOptions) (*bittorrent.AnnounceRequest, error) {
|
||||
qp, err := bittorrent.ParseURLData(r.RequestURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -20,15 +29,23 @@ func ParseAnnounce(r *http.Request, realIPHeader string, allowIPSpoofing bool) (
|
|||
|
||||
request := &bittorrent.AnnounceRequest{Params: qp}
|
||||
|
||||
eventStr, _ := qp.String("event")
|
||||
// Attempt to parse the event from the request.
|
||||
var eventStr string
|
||||
eventStr, request.EventProvided = qp.String("event")
|
||||
if request.EventProvided {
|
||||
request.Event, err = bittorrent.NewEvent(eventStr)
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to provide valid client event")
|
||||
}
|
||||
} else {
|
||||
request.Event = bittorrent.None
|
||||
}
|
||||
|
||||
// Determine if the client expects a compact response.
|
||||
compactStr, _ := qp.String("compact")
|
||||
request.Compact = compactStr != "" && compactStr != "0"
|
||||
|
||||
// Parse the infohash from the request.
|
||||
infoHashes := qp.InfoHashes()
|
||||
if len(infoHashes) < 1 {
|
||||
return nil, bittorrent.ClientError("no info_hash parameter supplied")
|
||||
|
@ -38,6 +55,7 @@ func ParseAnnounce(r *http.Request, realIPHeader string, allowIPSpoofing bool) (
|
|||
}
|
||||
request.InfoHash = infoHashes[0]
|
||||
|
||||
// Parse the PeerID from the request.
|
||||
peerID, ok := qp.String("peer_id")
|
||||
if !ok {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: peer_id")
|
||||
|
@ -47,43 +65,55 @@ func ParseAnnounce(r *http.Request, realIPHeader string, allowIPSpoofing bool) (
|
|||
}
|
||||
request.Peer.ID = bittorrent.PeerIDFromString(peerID)
|
||||
|
||||
// Determine the number of remaining bytes for the client.
|
||||
request.Left, err = qp.Uint64("left")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: left")
|
||||
}
|
||||
|
||||
// Determine the number of bytes downloaded by the client.
|
||||
request.Downloaded, err = qp.Uint64("downloaded")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: downloaded")
|
||||
}
|
||||
|
||||
// Determine the number of bytes shared by the client.
|
||||
request.Uploaded, err = qp.Uint64("uploaded")
|
||||
if err != nil {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: uploaded")
|
||||
}
|
||||
|
||||
// Determine the number of peers the client wants in the response.
|
||||
numwant, err := qp.Uint64("numwant")
|
||||
if err != nil && err != bittorrent.ErrKeyNotFound {
|
||||
return nil, bittorrent.ClientError("failed to parse parameter: numwant")
|
||||
}
|
||||
// If there were no errors, the user actually provided the numwant.
|
||||
request.NumWantProvided = err == nil
|
||||
request.NumWant = uint32(numwant)
|
||||
|
||||
// Parse the port where the client is listening.
|
||||
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.IP = requestedIP(r, qp, realIPHeader, allowIPSpoofing)
|
||||
// Parse the IP address where the client is listening.
|
||||
request.Peer.IP.IP, request.IPProvided = requestedIP(r, qp, opts)
|
||||
if request.Peer.IP.IP == nil {
|
||||
return nil, bittorrent.ClientError("failed to parse peer IP address")
|
||||
}
|
||||
|
||||
if err := bittorrent.SanitizeAnnounce(request, opts.MaxNumWant, opts.DefaultNumWant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// ParseScrape parses an bittorrent.ScrapeRequest from an http.Request.
|
||||
func ParseScrape(r *http.Request) (*bittorrent.ScrapeRequest, error) {
|
||||
func ParseScrape(r *http.Request, opts ParseOptions) (*bittorrent.ScrapeRequest, error) {
|
||||
qp, err := bittorrent.ParseURLData(r.RequestURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -99,39 +129,35 @@ func ParseScrape(r *http.Request) (*bittorrent.ScrapeRequest, error) {
|
|||
Params: qp,
|
||||
}
|
||||
|
||||
if err := bittorrent.SanitizeScrape(request, opts.MaxScrapeInfoHashes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
func requestedIP(r *http.Request, p bittorrent.Params, opts ParseOptions) (ip net.IP, provided bool) {
|
||||
if opts.AllowIPSpoofing {
|
||||
if ipstr, ok := p.String("ip"); ok {
|
||||
ip := net.ParseIP(ipstr)
|
||||
return ip
|
||||
return net.ParseIP(ipstr), true
|
||||
}
|
||||
|
||||
if ipstr, ok := p.String("ipv4"); ok {
|
||||
ip := net.ParseIP(ipstr)
|
||||
return ip
|
||||
return net.ParseIP(ipstr), true
|
||||
}
|
||||
|
||||
if ipstr, ok := p.String("ipv6"); ok {
|
||||
ip := net.ParseIP(ipstr)
|
||||
return ip
|
||||
return net.ParseIP(ipstr), true
|
||||
}
|
||||
}
|
||||
|
||||
if realIPHeader != "" {
|
||||
if ips, ok := r.Header[realIPHeader]; ok && len(ips) > 0 {
|
||||
ip := net.ParseIP(ips[0])
|
||||
return ip
|
||||
if opts.RealIPHeader != "" {
|
||||
if ips, ok := r.Header[opts.RealIPHeader]; ok && len(ips) > 0 {
|
||||
return net.ParseIP(ips[0]), false
|
||||
}
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
return net.ParseIP(host)
|
||||
return net.ParseIP(host), false
|
||||
}
|
||||
|
|
|
@ -27,9 +27,6 @@ func init() {
|
|||
prometheus.MustRegister(promResponseDurationMilliseconds)
|
||||
}
|
||||
|
||||
// ErrInvalidIP indicates an invalid IP.
|
||||
var ErrInvalidIP = bittorrent.ClientError("invalid IP")
|
||||
|
||||
var promResponseDurationMilliseconds = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "chihaya_udp_response_duration_milliseconds",
|
||||
|
@ -71,8 +68,8 @@ type Config struct {
|
|||
Addr string `yaml:"addr"`
|
||||
PrivateKey string `yaml:"private_key"`
|
||||
MaxClockSkew time.Duration `yaml:"max_clock_skew"`
|
||||
AllowIPSpoofing bool `yaml:"allow_ip_spoofing"`
|
||||
EnableRequestTiming bool `yaml:"enable_request_timing"`
|
||||
ParseOptions `yaml:",inline"`
|
||||
}
|
||||
|
||||
// LogFields renders the current config as a set of Logrus fields.
|
||||
|
@ -81,8 +78,11 @@ func (cfg Config) LogFields() log.Fields {
|
|||
"addr": cfg.Addr,
|
||||
"privateKey": cfg.PrivateKey,
|
||||
"maxClockSkew": cfg.MaxClockSkew,
|
||||
"allowIPSpoofing": cfg.AllowIPSpoofing,
|
||||
"enableRequestTiming": cfg.EnableRequestTiming,
|
||||
"allowIPSpoofing": cfg.AllowIPSpoofing,
|
||||
"maxNumWant": cfg.MaxNumWant,
|
||||
"defaultNumWant": cfg.DefaultNumWant,
|
||||
"maxScrapeInfoHashes": cfg.MaxScrapeInfoHashes,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,7 +279,7 @@ func (t *Frontend) handleRequest(r Request, w ResponseWriter) (actionName string
|
|||
actionName = "announce"
|
||||
|
||||
var req *bittorrent.AnnounceRequest
|
||||
req, err = ParseAnnounce(r, t.AllowIPSpoofing, actionID == announceV6ActionID)
|
||||
req, err = ParseAnnounce(r, actionID == announceV6ActionID, t.ParseOptions)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
|
@ -303,7 +303,7 @@ func (t *Frontend) handleRequest(r Request, w ResponseWriter) (actionName string
|
|||
actionName = "scrape"
|
||||
|
||||
var req *bittorrent.ScrapeRequest
|
||||
req, err = ParseScrape(r)
|
||||
req, err = ParseScrape(r, t.ParseOptions)
|
||||
if err != nil {
|
||||
WriteError(w, txID, err)
|
||||
return
|
||||
|
@ -315,7 +315,7 @@ func (t *Frontend) handleRequest(r Request, w ResponseWriter) (actionName string
|
|||
req.AddressFamily = bittorrent.IPv6
|
||||
} else {
|
||||
log.Error("udp: invalid IP: neither v4 nor v6", log.Fields{"IP": r.IP})
|
||||
WriteError(w, txID, ErrInvalidIP)
|
||||
WriteError(w, txID, bittorrent.ErrInvalidIP)
|
||||
return
|
||||
}
|
||||
af = new(bittorrent.AddressFamily)
|
||||
|
|
|
@ -45,14 +45,21 @@ var (
|
|||
errUnknownOptionType = bittorrent.ClientError("unknown option type")
|
||||
)
|
||||
|
||||
// ParseOptions is the configuration used to parse an Announce Request.
|
||||
//
|
||||
// If AllowIPSpoofing is true, IPs provided via params will be used.
|
||||
type ParseOptions struct {
|
||||
AllowIPSpoofing bool `yaml:"allow_ip_spoofing"`
|
||||
MaxNumWant uint32 `yaml:"max_numwant"`
|
||||
DefaultNumWant uint32 `yaml:"default_numwant"`
|
||||
MaxScrapeInfoHashes uint32 `yaml:"max_scrape_infohashes"`
|
||||
}
|
||||
|
||||
// ParseAnnounce parses an AnnounceRequest from a UDP request.
|
||||
//
|
||||
// If allowIPSpoofing is true, IPs provided via params will be used.
|
||||
//
|
||||
// If v6 is true the announce will be parsed as an IPv6 announce "the
|
||||
// opentracker way", see
|
||||
// If v6 is true, the announce is parsed the "opentracker way":
|
||||
// http://opentracker.blog.h3q.com/2007/12/28/the-ipv6-situation/
|
||||
func ParseAnnounce(r Request, allowIPSpoofing, v6 bool) (*bittorrent.AnnounceRequest, error) {
|
||||
func ParseAnnounce(r Request, v6 bool, opts ParseOptions) (*bittorrent.AnnounceRequest, error) {
|
||||
ipEnd := 84 + net.IPv4len
|
||||
if v6 {
|
||||
ipEnd = 84 + net.IPv6len
|
||||
|
@ -74,12 +81,14 @@ func ParseAnnounce(r Request, allowIPSpoofing, v6 bool) (*bittorrent.AnnounceReq
|
|||
}
|
||||
|
||||
ip := r.IP
|
||||
ipProvided := false
|
||||
ipbytes := r.Packet[84:ipEnd]
|
||||
if allowIPSpoofing {
|
||||
if opts.AllowIPSpoofing {
|
||||
// Make sure the bytes are copied to a new slice.
|
||||
copy(ip, net.IP(ipbytes))
|
||||
ipProvided = true
|
||||
}
|
||||
if !allowIPSpoofing && r.IP == nil {
|
||||
if !opts.AllowIPSpoofing && r.IP == nil {
|
||||
// We have no IP address to fallback on.
|
||||
return nil, errMalformedIP
|
||||
}
|
||||
|
@ -92,20 +101,29 @@ func ParseAnnounce(r Request, allowIPSpoofing, v6 bool) (*bittorrent.AnnounceReq
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &bittorrent.AnnounceRequest{
|
||||
request := &bittorrent.AnnounceRequest{
|
||||
Event: eventIDs[eventID],
|
||||
InfoHash: bittorrent.InfoHashFromBytes(infohash),
|
||||
NumWant: uint32(numWant),
|
||||
Left: left,
|
||||
Downloaded: downloaded,
|
||||
Uploaded: uploaded,
|
||||
IPProvided: ipProvided,
|
||||
NumWantProvided: true,
|
||||
EventProvided: true,
|
||||
Peer: bittorrent.Peer{
|
||||
ID: bittorrent.PeerIDFromBytes(peerID),
|
||||
IP: bittorrent.IP{IP: ip},
|
||||
Port: port,
|
||||
},
|
||||
Params: params,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := bittorrent.SanitizeAnnounce(request, opts.MaxNumWant, opts.DefaultNumWant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
|
@ -170,7 +188,7 @@ func handleOptionalParameters(packet []byte) (bittorrent.Params, error) {
|
|||
}
|
||||
|
||||
// ParseScrape parses a ScrapeRequest from a UDP request.
|
||||
func ParseScrape(r Request) (*bittorrent.ScrapeRequest, error) {
|
||||
func ParseScrape(r Request, opts ParseOptions) (*bittorrent.ScrapeRequest, error) {
|
||||
// If a scrape isn't at least 36 bytes long, it's malformed.
|
||||
if len(r.Packet) < 36 {
|
||||
return nil, errMalformedPacket
|
||||
|
@ -190,7 +208,11 @@ func ParseScrape(r Request) (*bittorrent.ScrapeRequest, error) {
|
|||
r.Packet = r.Packet[20:]
|
||||
}
|
||||
|
||||
return &bittorrent.ScrapeRequest{
|
||||
InfoHashes: infohashes,
|
||||
}, nil
|
||||
// Sanitize the request.
|
||||
request := &bittorrent.ScrapeRequest{InfoHashes: infohashes}
|
||||
if err := bittorrent.SanitizeScrape(request, opts.MaxScrapeInfoHashes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ package middleware
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/chihaya/chihaya/bittorrent"
|
||||
"github.com/chihaya/chihaya/storage"
|
||||
|
@ -67,56 +65,6 @@ func (h *swarmInteractionHook) HandleScrape(ctx context.Context, _ *bittorrent.S
|
|||
return ctx, nil
|
||||
}
|
||||
|
||||
// ErrInvalidIP indicates an invalid IP for an Announce.
|
||||
var ErrInvalidIP = errors.New("invalid IP")
|
||||
|
||||
// sanitizationHook enforces semantic assumptions about requests that may have
|
||||
// not been accounted for in a tracker frontend.
|
||||
//
|
||||
// The SanitizationHook performs the following checks:
|
||||
// - maxNumWant: Checks whether the numWant parameter of an announce is below
|
||||
// a limit. Sets it to the limit if the value is higher.
|
||||
// - defaultNumWant: Checks whether the numWant parameter of an announce is
|
||||
// zero. Sets it to the default if it is.
|
||||
// - IP sanitization: Checks whether the announcing Peer's IP address is either
|
||||
// IPv4 or IPv6. Returns ErrInvalidIP if the address is neither IPv4 nor
|
||||
// IPv6. Sets the Peer.AddressFamily field accordingly. Truncates IPv4
|
||||
// addresses to have a length of 4 bytes.
|
||||
type sanitizationHook struct {
|
||||
maxNumWant uint32
|
||||
defaultNumWant uint32
|
||||
maxScrapeInfoHashes uint32
|
||||
}
|
||||
|
||||
func (h *sanitizationHook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceRequest, resp *bittorrent.AnnounceResponse) (context.Context, error) {
|
||||
if req.NumWant > h.maxNumWant {
|
||||
req.NumWant = h.maxNumWant
|
||||
}
|
||||
|
||||
if req.NumWant == 0 {
|
||||
req.NumWant = h.defaultNumWant
|
||||
}
|
||||
|
||||
if ip := req.Peer.IP.To4(); ip != nil {
|
||||
req.Peer.IP.IP = ip
|
||||
req.Peer.IP.AddressFamily = bittorrent.IPv4
|
||||
} else if len(req.Peer.IP.IP) == net.IPv6len { // implies req.Peer.IP.To4() == nil
|
||||
req.Peer.IP.AddressFamily = bittorrent.IPv6
|
||||
} else {
|
||||
return ctx, ErrInvalidIP
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (h *sanitizationHook) HandleScrape(ctx context.Context, req *bittorrent.ScrapeRequest, resp *bittorrent.ScrapeResponse) (context.Context, error) {
|
||||
if len(req.InfoHashes) > int(h.maxScrapeInfoHashes) {
|
||||
req.InfoHashes = req.InfoHashes[:h.maxScrapeInfoHashes]
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
type skipResponseHook struct{}
|
||||
|
||||
// SkipResponseHookKey is a key for the context of an Announce or Scrape to
|
||||
|
|
|
@ -16,9 +16,6 @@ import (
|
|||
// Config holds the configuration common across all middleware.
|
||||
type Config struct {
|
||||
AnnounceInterval time.Duration `yaml:"announce_interval"`
|
||||
MaxNumWant uint32 `yaml:"max_numwant"`
|
||||
DefaultNumWant uint32 `yaml:"default_numwant"`
|
||||
MaxScrapeInfoHashes uint32 `yaml:"max_scrape_infohashes"`
|
||||
}
|
||||
|
||||
var _ frontend.TrackerLogic = &Logic{}
|
||||
|
@ -26,17 +23,12 @@ var _ frontend.TrackerLogic = &Logic{}
|
|||
// NewLogic creates a new instance of a TrackerLogic that executes the provided
|
||||
// middleware hooks.
|
||||
func NewLogic(cfg Config, peerStore storage.PeerStore, preHooks, postHooks []Hook) *Logic {
|
||||
l := &Logic{
|
||||
return &Logic{
|
||||
announceInterval: cfg.AnnounceInterval,
|
||||
peerStore: peerStore,
|
||||
preHooks: []Hook{&sanitizationHook{cfg.MaxNumWant, cfg.DefaultNumWant, cfg.MaxScrapeInfoHashes}},
|
||||
preHooks: append(preHooks, &responseHook{store: peerStore}),
|
||||
postHooks: append(postHooks, &swarmInteractionHook{store: peerStore}),
|
||||
}
|
||||
|
||||
l.preHooks = append(l.preHooks, preHooks...)
|
||||
l.preHooks = append(l.preHooks, &responseHook{store: peerStore})
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// Logic is an implementation of the TrackerLogic that functions by
|
||||
|
|
|
@ -80,15 +80,4 @@ func BenchmarkHookOverhead(b *testing.B) {
|
|||
benchHookListV6(b, nopHooks)
|
||||
})
|
||||
}
|
||||
|
||||
var sanHooks hookList
|
||||
for i := 1; i < 4; i++ {
|
||||
sanHooks = append(sanHooks, &sanitizationHook{maxNumWant: 50})
|
||||
b.Run(fmt.Sprintf("%dsanitation-v4", i), func(b *testing.B) {
|
||||
benchHookListV4(b, sanHooks)
|
||||
})
|
||||
b.Run(fmt.Sprintf("%dsanitation-v6", i), func(b *testing.B) {
|
||||
benchHookListV6(b, sanHooks)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue