From 2f092bad45ffeacd4ec30a6658a251b4b5d3666a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0irhoe=20Biazhkovi=C4=8D?= Date: Tue, 26 Oct 2021 19:03:05 +0300 Subject: [PATCH] Initial torrentV2 hash support --- bittorrent/bittorrent.go | 66 ++++++++++++++++++---------- bittorrent/bittorrent_test.go | 5 ++- bittorrent/params.go | 7 +-- cmd/chihaya/config.go | 20 +++------ cmd/chihaya/e2e.go | 22 ++++------ frontend/http/frontend.go | 82 ++++++++++------------------------- frontend/http/writer.go | 33 +++++++------- frontend/udp/parser.go | 28 +++++++++--- storage/storage_tests.go | 6 ++- 9 files changed, 131 insertions(+), 138 deletions(-) diff --git a/bittorrent/bittorrent.go b/bittorrent/bittorrent.go index f3ecd1f..fc9c1fc 100644 --- a/bittorrent/bittorrent.go +++ b/bittorrent/bittorrent.go @@ -5,6 +5,7 @@ package bittorrent import ( "fmt" + "github.com/pkg/errors" "net" "time" @@ -12,6 +13,7 @@ import ( ) // PeerID represents a peer ID. +// TODO: check if torrentV2 also changed this field size type PeerID [20]byte // PeerIDFromBytes creates a PeerID from a byte slice. @@ -51,32 +53,52 @@ func PeerIDFromString(s string) PeerID { } // InfoHash represents an infohash. -type InfoHash [20]byte +type InfoHash []byte + +const( + InfoHashV1Len = 20 + InfoHashV2Len = 32 +) + +var invalidHashSize = errors.New("InfoHash must be either 20 (for torrent V1) or 32 (V2) bytes") +var isNotV1Hash = errors.New("InfoHash is not V1 (SHA1)") + +// BytesV1 returns 20-bytes length array of the corresponding InfoHash. +// If InfoHash is not 20-bytes long (is torrent V2 hash) zeroed array and error returned +func (i InfoHash) BytesV1() ([InfoHashV1Len]byte, error){ + var bb [InfoHashV1Len]byte + if len(i) != InfoHashV1Len { + return bb, isNotV1Hash + } + copy(bb[:], i) + return bb, nil +} + +// ValidateInfoHash validates input bytes size and returns it +// if size one of InfoHashV1Len or InfoHashV2Len. +// In other case 0 and non-nil error returned +func ValidateInfoHash(b []byte) (int, error) { + l := len(b) + if l != InfoHashV1Len && l != InfoHashV2Len { + return 0, invalidHashSize + } + return l, nil +} // InfoHashFromBytes creates an InfoHash from a byte slice. -// -// It panics if b is not 20 bytes long. -func InfoHashFromBytes(b []byte) InfoHash { - if len(b) != 20 { - panic("infohash must be 20 bytes") +func InfoHashFromBytes(b []byte) (InfoHash, error) { + if l, err := ValidateInfoHash(b); err != nil{ + return nil, err + } else { + buf := make([]byte, l) + copy(buf[:], b) + return buf, nil } - - var buf [20]byte - copy(buf[:], b) - return buf } // InfoHashFromString creates an InfoHash from a string. -// -// It panics if s is not 20 bytes long. -func InfoHashFromString(s string) InfoHash { - if len(s) != 20 { - panic("infohash must be 20 bytes") - } - - var buf [20]byte - copy(buf[:], s) - return buf +func InfoHashFromString(s string) (InfoHash, error) { + return InfoHashFromBytes([]byte(s)) } // String implements fmt.Stringer, returning the base16 encoded InfoHash. @@ -84,9 +106,9 @@ func (i InfoHash) String() string { return fmt.Sprintf("%x", i[:]) } -// RawString returns a 20-byte string of the raw bytes of the InfoHash. +// RawString returns a string of the raw bytes of the InfoHash. func (i InfoHash) RawString() string { - return string(i[:]) + return string(i) } // AnnounceRequest represents the parsed parameters from an announce request. diff --git a/bittorrent/bittorrent_test.go b/bittorrent/bittorrent_test.go index 504d5b1..7ef007b 100644 --- a/bittorrent/bittorrent_test.go +++ b/bittorrent/bittorrent_test.go @@ -41,8 +41,9 @@ func TestPeerID_String(t *testing.T) { } func TestInfoHash_String(t *testing.T) { - s := InfoHashFromBytes(b).String() - require.Equal(t, expected, s) + ih, err := InfoHashFromBytes(b) + require.Nil(t, err) + require.Equal(t, expected, ih.String()) } func TestPeer_String(t *testing.T) { diff --git a/bittorrent/params.go b/bittorrent/params.go index b820108..453fe00 100644 --- a/bittorrent/params.go +++ b/bittorrent/params.go @@ -168,10 +168,11 @@ func parseQuery(query string) (q *QueryParams, err error) { } if key == "info_hash" { - if len(value) != 20 { - return nil, ErrInvalidInfohash + if ih, err := InfoHashFromString(value); err == nil{ + q.infoHashes = append(q.infoHashes, ih) + } else { + return nil, err } - q.infoHashes = append(q.infoHashes, InfoHashFromString(value)) } else { q.params[strings.ToLower(key)] = value } diff --git a/cmd/chihaya/config.go b/cmd/chihaya/config.go index 939ac34..ea9b65f 100644 --- a/cmd/chihaya/config.go +++ b/cmd/chihaya/config.go @@ -2,7 +2,6 @@ package main import ( "errors" - "io/ioutil" "os" yaml "gopkg.in/yaml.v2" @@ -73,19 +72,10 @@ func ParseConfigFile(path string) (*ConfigFile, error) { f, err := os.Open(os.ExpandEnv(path)) if err != nil { return nil, err + } else { + defer f.Close() + cfgFile := new(ConfigFile) + err = yaml.NewDecoder(f).Decode(cfgFile) + return cfgFile, err } - defer f.Close() - - contents, err := ioutil.ReadAll(f) - if err != nil { - return nil, err - } - - var cfgFile ConfigFile - err = yaml.Unmarshal(contents, &cfgFile) - if err != nil { - return nil, err - } - - return &cfgFile, nil } diff --git a/cmd/chihaya/e2e.go b/cmd/chihaya/e2e.go index b527e1c..a550f6c 100644 --- a/cmd/chihaya/e2e.go +++ b/cmd/chihaya/e2e.go @@ -1,8 +1,8 @@ package main import ( - "crypto/rand" "fmt" + "math/rand" "time" "github.com/anacrolix/torrent/tracker" @@ -54,28 +54,22 @@ func EndToEndRunCmdFunc(cmd *cobra.Command, args []string) error { return nil } -func generateInfohash() [20]byte { +func generateInfohash() bittorrent.InfoHash { b := make([]byte, 20) - - n, err := rand.Read(b) - if err != nil { - panic(err) - } - if n != 20 { - panic(fmt.Errorf("not enough randomness? Got %d bytes", n)) - } - - return bittorrent.InfoHashFromBytes(b) + rand.Read(b) + ih, _ := bittorrent.InfoHashFromBytes(b) + return ih } func test(addr string, delay time.Duration) error { - ih := generateInfohash() + ih, _ := generateInfohash().BytesV1() return testWithInfohash(ih, addr, delay) } func testWithInfohash(infoHash [20]byte, url string, delay time.Duration) error { + var ih [20]byte req := tracker.AnnounceRequest{ - InfoHash: infoHash, + InfoHash: ih, PeerId: [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, Downloaded: 50, Left: 100, diff --git a/frontend/http/frontend.go b/frontend/http/frontend.go index 556476f..8cc45d7 100644 --- a/frontend/http/frontend.go +++ b/frontend/http/frontend.go @@ -172,34 +172,21 @@ func NewFrontend(logic frontend.TrackerLogic, provided Config) (*Frontend, error } } - if cfg.HTTPSAddr != "" && f.tlsCfg == nil { - return nil, errors.New("must specify tls_cert_path and tls_key_path when using https_addr") - } - if cfg.HTTPSAddr == "" && f.tlsCfg != nil { - return nil, errors.New("must specify https_addr when using tls_cert_path and tls_key_path") + if cfg.HTTPSAddr == "" || f.tlsCfg == nil { + return nil, errors.New("must specify both https_addr, tls_cert_path and tls_key_path") } - var listenerHTTP, listenerHTTPS net.Listener - var err error - if cfg.Addr != "" { - listenerHTTP, err = net.Listen("tcp", f.Addr) - if err != nil { - return nil, err - } + router := httprouter.New() + for _, route := range f.AnnounceRoutes { + router.GET(route, f.announceRoute) } - if cfg.HTTPSAddr != "" { - listenerHTTPS, err = net.Listen("tcp", f.HTTPSAddr) - if err != nil { - if listenerHTTP != nil { - listenerHTTP.Close() - } - return nil, err - } + for _, route := range f.ScrapeRoutes { + router.GET(route, f.scrapeRoute) } if cfg.Addr != "" { go func() { - if err := f.serveHTTP(listenerHTTP); err != nil { + if err := f.serveHTTP(router,false); err != nil { log.Fatal("failed while serving http", log.Err(err)) } }() @@ -207,7 +194,7 @@ func NewFrontend(logic frontend.TrackerLogic, provided Config) (*Frontend, error if cfg.HTTPSAddr != "" { go func() { - if err := f.serveHTTPS(listenerHTTPS); err != nil { + if err := f.serveHTTP(router,true); err != nil { log.Fatal("failed while serving https", log.Err(err)) } }() @@ -240,52 +227,31 @@ func (f *Frontend) makeStopFunc(stopSrv *http.Server) stop.Func { } } -func (f *Frontend) handler() http.Handler { - router := httprouter.New() - for _, route := range f.AnnounceRoutes { - router.GET(route, f.announceRoute) - } - for _, route := range f.ScrapeRoutes { - router.GET(route, f.scrapeRoute) - } - return router -} - // serveHTTP blocks while listening and serving non-TLS HTTP BitTorrent // requests until Stop() is called or an error is returned. -func (f *Frontend) serveHTTP(l net.Listener) error { - f.srv = &http.Server{ - Addr: f.Addr, - Handler: f.handler(), +func (f *Frontend) serveHTTP(handler http.Handler, tls bool) error { + srv := &http.Server{ + Handler: handler, ReadTimeout: f.ReadTimeout, WriteTimeout: f.WriteTimeout, IdleTimeout: f.IdleTimeout, } - f.srv.SetKeepAlivesEnabled(f.EnableKeepAlive) + srv.SetKeepAlivesEnabled(f.EnableKeepAlive) - // Start the HTTP server. - if err := f.srv.Serve(l); err != http.ErrServerClosed { - return err + var err error + if tls { + srv.Addr = f.HTTPSAddr + srv.TLSConfig = f.tlsCfg + f.tlsSrv = srv + err = srv.ListenAndServe() + } else{ + srv.Addr = f.Addr + f.srv = srv + err = f.tlsSrv.ListenAndServeTLS("", "") } - return nil -} - -// serveHTTPS blocks while listening and serving TLS HTTP BitTorrent -// requests until Stop() is called or an error is returned. -func (f *Frontend) serveHTTPS(l net.Listener) error { - f.tlsSrv = &http.Server{ - Addr: f.HTTPSAddr, - TLSConfig: f.tlsCfg, - Handler: f.handler(), - ReadTimeout: f.ReadTimeout, - WriteTimeout: f.WriteTimeout, - } - - f.tlsSrv.SetKeepAlivesEnabled(f.EnableKeepAlive) - // Start the HTTP server. - if err := f.tlsSrv.ServeTLS(l, "", ""); err != http.ErrServerClosed { + if err != http.ErrServerClosed { return err } return nil diff --git a/frontend/http/writer.go b/frontend/http/writer.go index e528156..deb1042 100644 --- a/frontend/http/writer.go +++ b/frontend/http/writer.go @@ -12,7 +12,7 @@ import ( type strMap map[string]interface{} // WriteError communicates an error to a BitTorrent client over HTTP. -func WriteError(w http.ResponseWriter, err error) error { +func WriteError(w http.ResponseWriter, err error) { message := "internal server error" if _, clientErr := err.(bittorrent.ClientError); clientErr { message = err.Error() @@ -21,9 +21,11 @@ func WriteError(w http.ResponseWriter, err error) error { } w.WriteHeader(http.StatusOK) - return bencode.NewEncoder(w).Encode(map[string]interface{}{ + if err = bencode.NewEncoder(w).Encode(map[string]interface{}{ "failure reason": message, - }) + }); err != nil{ + log.Error("unable to encode string", log.Err(err)) + } } // WriteAnnounceResponse communicates the results of an Announce to a @@ -64,19 +66,18 @@ func WriteAnnounceResponse(w http.ResponseWriter, resp *bittorrent.AnnounceRespo bdict["peers6"] = IPv6CompactDict } - return bencode.NewEncoder(w).Encode(bdict) + } else { + // Add the peers to the dictionary. + var peers []strMap + for _, peer := range resp.IPv4Peers { + peers = append(peers, dict(peer)) + } + for _, peer := range resp.IPv6Peers { + peers = append(peers, dict(peer)) + } + bdict["peers"] = peers } - // Add the peers to the dictionary. - var peers []strMap - 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) } @@ -100,7 +101,7 @@ func compact4(peer bittorrent.Peer) (buf []byte) { if ip := peer.IP.To4(); ip == nil { panic("non-IPv4 IP for Peer in IPv4Peers") } else { - buf = []byte(ip) + buf = ip } buf = append(buf, byte(peer.Port>>8)) buf = append(buf, byte(peer.Port&0xff)) @@ -111,7 +112,7 @@ func compact6(peer bittorrent.Peer) (buf []byte) { if ip := peer.IP.To16(); ip == nil { panic("non-IPv6 IP for Peer in IPv6Peers") } else { - buf = []byte(ip) + buf = ip } buf = append(buf, byte(peer.Port>>8)) buf = append(buf, byte(peer.Port&0xff)) diff --git a/frontend/udp/parser.go b/frontend/udp/parser.go index 96ea9d8..9df6b3c 100644 --- a/frontend/udp/parser.go +++ b/frontend/udp/parser.go @@ -112,10 +112,15 @@ func ParseAnnounce(r Request, v6Action bool, opts ParseOptions) (*bittorrent.Ann return nil, err } + ih, err := bittorrent.InfoHashFromBytes(infohash) + if err != nil{ + return nil, err + } + request := &bittorrent.AnnounceRequest{ Event: eventIDs[eventID], - InfoHash: bittorrent.InfoHashFromBytes(infohash), - NumWant: uint32(numWant), + InfoHash: ih, + NumWant: numWant, Left: left, Downloaded: downloaded, Uploaded: uploaded, @@ -208,15 +213,26 @@ func ParseScrape(r Request, opts ParseOptions) (*bittorrent.ScrapeRequest, error // 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 { + l := len(r.Packet) + isV1, isV2 := l%bittorrent.InfoHashV1Len == 0, l%bittorrent.InfoHashV2Len == 0 + + if !(isV1 || isV2) { 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:] + pageSize := bittorrent.InfoHashV1Len + if isV2 { + pageSize = bittorrent.InfoHashV2Len + } + for len(r.Packet) >= pageSize { + if ih, err := bittorrent.InfoHashFromBytes(r.Packet[:pageSize]); err != nil{ + return nil, err + } else { + infohashes = append(infohashes, ih) + r.Packet = r.Packet[pageSize:] + } } // Sanitize the request. diff --git a/storage/storage_tests.go b/storage/storage_tests.go index 4cf0f41..2419d55 100644 --- a/storage/storage_tests.go +++ b/storage/storage_tests.go @@ -17,16 +17,18 @@ var PeerEqualityFunc = func(p1, p2 bittorrent.Peer) bool { return p1.Equal(p2) } // TestPeerStore tests a PeerStore implementation against the interface. func TestPeerStore(t *testing.T, p PeerStore) { + ih0, _ := bittorrent.InfoHashFromString("00000000000000000001") + ih1, _ := bittorrent.InfoHashFromString("00000000000000000002") testData := []struct { ih bittorrent.InfoHash peer bittorrent.Peer }{ { - bittorrent.InfoHashFromString("00000000000000000001"), + ih0, bittorrent.Peer{ID: bittorrent.PeerIDFromString("00000000000000000001"), Port: 1, IP: bittorrent.IP{IP: net.ParseIP("1.1.1.1").To4(), AddressFamily: bittorrent.IPv4}}, }, { - bittorrent.InfoHashFromString("00000000000000000002"), + ih1, bittorrent.Peer{ID: bittorrent.PeerIDFromString("00000000000000000002"), Port: 2, IP: bittorrent.IP{IP: net.ParseIP("abab::0001"), AddressFamily: bittorrent.IPv6}}, }, }