From 62605706359e0b047249466b224b974cd062418c Mon Sep 17 00:00:00 2001 From: Leo Balduf Date: Fri, 2 Sep 2016 15:53:28 -0400 Subject: [PATCH] http: extract query parser to bittorrent package --- bittorrent/bittorrent.go | 5 - bittorrent/params.go | 192 ++++++++++++++++++ .../params_test.go | 43 ++-- frontend/http/parser.go | 4 +- frontend/http/query_params.go | 124 ----------- 5 files changed, 222 insertions(+), 146 deletions(-) create mode 100644 bittorrent/params.go rename frontend/http/query_params_test.go => bittorrent/params_test.go (74%) delete mode 100644 frontend/http/query_params.go diff --git a/bittorrent/bittorrent.go b/bittorrent/bittorrent.go index eb18451..75c7289 100644 --- a/bittorrent/bittorrent.go +++ b/bittorrent/bittorrent.go @@ -124,11 +124,6 @@ func (p Peer) Equal(x Peer) bool { return p.EqualEndpoint(x) && p.ID == x.ID } // EqualEndpoint reports whether p and x have the same endpoint. func (p Peer) EqualEndpoint(x Peer) bool { return p.Port == x.Port && p.IP.Equal(x.IP) } -// Params is used to fetch request optional parameters from an Announce. -type Params interface { - String(key string) (string, bool) -} - // ClientError represents an error that should be exposed to the client over // the BitTorrent protocol implementation. type ClientError string diff --git a/bittorrent/params.go b/bittorrent/params.go new file mode 100644 index 0000000..9cb33c6 --- /dev/null +++ b/bittorrent/params.go @@ -0,0 +1,192 @@ +package bittorrent + +import ( + "errors" + "net/url" + "strconv" + "strings" +) + +// Params is used to fetch (optional) request parameters from an Announce. +// For HTTP Announces this includes the request path and parsed query, for UDP +// Announces this is the extracted path and parsed query from optional URLData +// as specified in BEP41. +// +// See ParseURLData for specifics on parsing and limitations. +type Params interface { + // 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. + String(key string) (string, bool) + + // RawPath returns the raw path from the request URL. + // The path returned can contain URL encoded data. + // For a request of the form "/announce?port=1234" this would return + // "/announce". + RawPath() string + + // RawQuery returns the raw query from the request URL, excluding the + // delimiter '?'. + // For a request of the form "/announce?port=1234" this would return + // "port=1234" + RawQuery() string +} + +// ErrKeyNotFound is returned when a provided key has no value associated with +// it. +var ErrKeyNotFound = errors.New("query: 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("query: invalid infohash") + +// QueryParams parses a URL Query and implements the Params interface with some +// additional helpers. +type QueryParams struct { + path string + query string + params map[string]string + infoHashes []InfoHash +} + +// ParseURLData parses a request URL or UDP URLData as defined in BEP41. +// It expects a concatenated string of the request's path and query parts as +// defined in RFC 3986. As both the udp: and http: scheme used by BitTorrent +// include an authority part the path part must always begin with a slash. +// An example of the expected URLData would be "/announce?port=1234&uploaded=0" +// or "/?auth=0x1337". +// HTTP servers should pass (*http.Request).RequestURI, UDP servers should +// pass the concatenated, unchanged URLData as defined in BEP41. +// +// Note that, in the case of a key occurring multiple times in the query, only +// the last value for that key is kept. +// The only exception to this rule is the key "info_hash" which will attempt to +// parse each value as an InfoHash and return an error if parsing fails. All +// InfoHashes are collected and can later be retrieved by calling the InfoHashes +// method. +func ParseURLData(urlData string) (*QueryParams, error) { + var path, query string + + queryDelim := strings.IndexAny(urlData, "?") + if queryDelim == -1 { + path = urlData + } else { + path = urlData[:queryDelim] + query = urlData[queryDelim+1:] + } + + q, err := parseQuery(query) + if err != nil { + return nil, err + } + q.path = path + return q, nil +} + +// parseQuery parses a URL query into QueryParams. +// The query is expected to exclude the delimiting '?'. +func parseQuery(rawQuery string) (*QueryParams, error) { + var ( + keyStart, keyEnd int + valStart, valEnd int + + onKey = true + + q = &QueryParams{ + query: rawQuery, + infoHashes: nil, + params: make(map[string]string), + } + ) + + for i, length := 0, len(rawQuery); i < length; i++ { + separator := rawQuery[i] == '&' || rawQuery[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(rawQuery[keyStart : keyEnd+1]) + if err != nil { + return nil, err + } + + var valStr string + + if valEnd > 0 { + valStr, err = url.QueryUnescape(rawQuery[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, InfoHashFromString(valStr)) + } else { + q.params[strings.ToLower(keyStr)] = valStr + } + + valEnd = 0 + onKey = true + keyStart = i + 1 + + } else if rawQuery[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() []InfoHash { + return qp.infoHashes +} + +// RawPath returns the raw path from the parsed URL. +func (qp *QueryParams) RawPath() string { + return qp.path +} + +// RawQuery returns the raw query from the parsed URL. +func (qp *QueryParams) RawQuery() string { + return qp.query +} diff --git a/frontend/http/query_params_test.go b/bittorrent/params_test.go similarity index 74% rename from frontend/http/query_params_test.go rename to bittorrent/params_test.go index ec9a0d0..36d0819 100644 --- a/frontend/http/query_params_test.go +++ b/bittorrent/params_test.go @@ -1,4 +1,4 @@ -package http +package bittorrent import ( "net/url" @@ -6,11 +6,10 @@ import ( ) var ( - baseAddr = "https://www.subdomain.tracker.com:80/" - testInfoHash = "01234567890123456789" - testPeerID = "-TEST01-6wfG2wk6wWLc" + 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"}}, @@ -26,7 +25,7 @@ var ( } InvalidQueries = []string{ - baseAddr + "announce/?" + "info_hash=%0%a", + "/announce?" + "info_hash=%0%a", } ) @@ -45,28 +44,42 @@ func mapArrayEqual(boxed map[string][]string, unboxed map[string]string) bool { return true } -func TestValidQueries(t *testing.T) { +func TestParseEmptyURLData(t *testing.T) { + parsedQuery, err := ParseURLData("") + if err != nil { + t.Fatal(err) + } + if parsedQuery == nil { + t.Fatal("Parsed query must not be nil") + } +} + +func TestParseValidURLData(t *testing.T) { for parseIndex, parseVal := range ValidAnnounceArguments { - parsedQueryObj, err := NewQueryParams(baseAddr + "announce/?" + parseVal.Encode()) + parsedQueryObj, err := ParseURLData("/announce?" + parseVal.Encode()) if err != nil { - t.Error(err) + t.Fatal(err) } if !mapArrayEqual(parseVal, parsedQueryObj.params) { - t.Errorf("Incorrect parse at item %d.\n Expected=%v\n Recieved=%v\n", parseIndex, parseVal, parsedQueryObj.params) + t.Fatalf("Incorrect parse at item %d.\n Expected=%v\n Recieved=%v\n", parseIndex, parseVal, parsedQueryObj.params) + } + + if parsedQueryObj.path != "/announce" { + t.Fatalf("Incorrect path, expected %q, got %q", "/announce", parsedQueryObj.path) } } } -func TestInvalidQueries(t *testing.T) { +func TestParseInvalidURLData(t *testing.T) { for parseIndex, parseStr := range InvalidQueries { - parsedQueryObj, err := NewQueryParams(parseStr) + parsedQueryObj, err := ParseURLData(parseStr) if err == nil { - t.Error("Should have produced error", parseIndex) + t.Fatal("Should have produced error", parseIndex) } if parsedQueryObj != nil { - t.Error("Should be nil after error", parsedQueryObj, parseIndex) + t.Fatal("Should be nil after error", parsedQueryObj, parseIndex) } } } @@ -74,7 +87,7 @@ func TestInvalidQueries(t *testing.T) { func BenchmarkParseQuery(b *testing.B) { for bCount := 0; bCount < b.N; bCount++ { for parseIndex, parseStr := range ValidAnnounceArguments { - parsedQueryObj, err := NewQueryParams(baseAddr + "announce/?" + parseStr.Encode()) + parsedQueryObj, err := parseQuery(parseStr.Encode()) if err != nil { b.Error(err, parseIndex) b.Log(parsedQueryObj) @@ -86,7 +99,7 @@ func BenchmarkParseQuery(b *testing.B) { 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()) + parsedQueryObj, err := url.ParseQuery(parseStr.Encode()) if err != nil { b.Error(err, parseIndex) b.Log(parsedQueryObj) diff --git a/frontend/http/parser.go b/frontend/http/parser.go index 7e7674a..d873f40 100644 --- a/frontend/http/parser.go +++ b/frontend/http/parser.go @@ -13,7 +13,7 @@ import ( // 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) + qp, err := bittorrent.ParseURLData(r.RequestURI) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func ParseAnnounce(r *http.Request, realIPHeader string, allowIPSpoofing bool) ( // ParseScrape parses an bittorrent.ScrapeRequest from an http.Request. func ParseScrape(r *http.Request) (*bittorrent.ScrapeRequest, error) { - qp, err := NewQueryParams(r.URL.RawQuery) + qp, err := bittorrent.ParseURLData(r.RequestURI) if err != nil { return nil, err } diff --git a/frontend/http/query_params.go b/frontend/http/query_params.go deleted file mode 100644 index 415b4fc..0000000 --- a/frontend/http/query_params.go +++ /dev/null @@ -1,124 +0,0 @@ -package http - -import ( - "errors" - "net/url" - "strconv" - "strings" - - "github.com/chihaya/chihaya/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 -}