mirror of
https://github.com/go-i2p/go-i2p-bt.git
synced 2025-07-01 03:57:55 -04:00
378 lines
10 KiB
Go
378 lines
10 KiB
Go
// Copyright 2020 xgfone, 2023 idk
|
|
//
|
|
// 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 httptracker implements the tracker protocol based on HTTP/HTTPS.
|
|
//
|
|
// You can use the package to implement a HTTP tracker server to track the
|
|
// information that other peers upload or download the file, or to create
|
|
// a HTTP tracker client to communicate with the HTTP tracker server.
|
|
package httptracker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base32"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-i2p/go-i2p-bt/bencode"
|
|
"github.com/go-i2p/go-i2p-bt/metainfo"
|
|
)
|
|
|
|
// AnnounceRequest is the tracker announce requests.
|
|
//
|
|
// BEP 3
|
|
type AnnounceRequest struct {
|
|
// InfoHash is the sha1 hash of the bencoded form of the info value from the metainfo file.
|
|
InfoHash metainfo.Hash `bencode:"info_hash"` // BEP 3
|
|
|
|
// PeerID is the id of the downloader.
|
|
//
|
|
// Each downloader generates its own id at random at the start of a new download.
|
|
PeerID metainfo.Hash `bencode:"peer_id"` // BEP 3
|
|
|
|
// Uploaded is the total amount uploaded so far, encoded in base ten ascii.
|
|
Uploaded int64 `bencode:"uploaded"` // BEP 3
|
|
|
|
// Downloaded is the total amount downloaded so far, encoded in base ten ascii.
|
|
Downloaded int64 `bencode:"downloaded"` // BEP 3
|
|
|
|
// Left is the number of bytes this peer still has to download,
|
|
// encoded in base ten ascii.
|
|
//
|
|
// Note that this can't be computed from downloaded and the file length
|
|
// since it might be a resume, and there's a chance that some of the
|
|
// downloaded data failed an integrity check and had to be re-downloaded.
|
|
//
|
|
// If less than 0, math.MaxInt64 will be used for HTTP trackers instead.
|
|
Left int64 `bencode:"left"` // BEP 3
|
|
|
|
// Port is the port that this peer is listening on.
|
|
//
|
|
// Common behavior is for a downloader to try to listen on port 6881,
|
|
// and if that port is taken try 6882, then 6883, etc. and give up after 6889.
|
|
Port uint16 `bencode:"port"` // BEP 3
|
|
|
|
// IP is the ip or DNS name which this peer is at, which generally used
|
|
// for the origin if it's on the same machine as the tracker.
|
|
//
|
|
// Optional.
|
|
IP string `bencode:"ip,omitempty"` // BEP 3
|
|
|
|
// If not present, this is one of the announcements done at regular intervals.
|
|
// An announcement using started is sent when a download first begins,
|
|
// and one using completed is sent when the download is complete.
|
|
// No completed is sent if the file was complete when started.
|
|
// Downloaders send an announcement using stopped when they cease downloading.
|
|
//
|
|
// Optional
|
|
Event uint32 `bencode:"event,omitempty"` // BEP 3
|
|
|
|
// Compact indicates whether it hopes the tracker to return the compact
|
|
// peer lists.
|
|
//
|
|
// Optional
|
|
Compact bool `bencode:"compact,omitempty"` // BEP 23
|
|
|
|
// NumWant is the number of peers that the client would like to receive
|
|
// from the tracker. This value is permitted to be zero. If omitted,
|
|
// typically defaults to 50 peers.
|
|
//
|
|
// See https://wiki.theory.org/index.php/BitTorrentSpecification
|
|
//
|
|
// Optional.
|
|
NumWant int32 `bencode:"numwant,omitempty"`
|
|
|
|
Key int32 `bencode:"key,omitempty"`
|
|
}
|
|
|
|
// ToQuery converts the Request to URL Query.
|
|
func (r AnnounceRequest) ToQuery() (vs url.Values) {
|
|
vs = make(url.Values, 9)
|
|
vs.Set("info_hash", r.InfoHash.BytesString())
|
|
vs.Set("peer_id", r.PeerID.BytesString())
|
|
vs.Set("uploaded", strconv.FormatInt(r.Uploaded, 10))
|
|
vs.Set("downloaded", strconv.FormatInt(r.Downloaded, 10))
|
|
vs.Set("left", strconv.FormatInt(r.Left, 10))
|
|
|
|
if r.IP != "" {
|
|
vs.Set("ip", r.IP)
|
|
}
|
|
if r.Event > 0 {
|
|
vs.Set("event", strconv.FormatInt(int64(r.Event), 10))
|
|
}
|
|
if r.Port > 0 {
|
|
vs.Set("port", strconv.FormatUint(uint64(r.Port), 10))
|
|
}
|
|
if r.NumWant != 0 {
|
|
vs.Set("numwant", strconv.FormatUint(uint64(r.NumWant), 10))
|
|
}
|
|
if r.Key != 0 {
|
|
vs.Set("key", strconv.FormatInt(int64(r.Key), 10))
|
|
}
|
|
|
|
// BEP 23
|
|
if r.Compact {
|
|
vs.Set("compact", "1")
|
|
} else {
|
|
vs.Set("compact", "0")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// FromQuery converts URL Query to itself.
|
|
func (r *AnnounceRequest) FromQuery(vs url.Values) (err error) {
|
|
if err = r.InfoHash.FromString(vs.Get("info_hash")); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = r.PeerID.FromString(vs.Get("peer_id")); err != nil {
|
|
return
|
|
}
|
|
|
|
v, err := strconv.ParseInt(vs.Get("uploaded"), 10, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
r.Uploaded = v
|
|
|
|
v, err = strconv.ParseInt(vs.Get("downloaded"), 10, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
r.Downloaded = v
|
|
|
|
v, err = strconv.ParseInt(vs.Get("left"), 10, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
r.Left = v
|
|
|
|
if s := vs.Get("event"); s != "" {
|
|
v, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Event = uint32(v)
|
|
}
|
|
|
|
if s := vs.Get("port"); s != "" {
|
|
v, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Port = uint16(v)
|
|
}
|
|
|
|
if s := vs.Get("numwant"); s != "" {
|
|
v, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.NumWant = int32(v)
|
|
}
|
|
|
|
if s := vs.Get("key"); s != "" {
|
|
v, err := strconv.ParseInt(s, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Key = int32(v)
|
|
}
|
|
|
|
r.IP = vs.Get("ip")
|
|
switch vs.Get("compact") {
|
|
case "1":
|
|
r.Compact = true
|
|
case "0":
|
|
r.Compact = false
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// AnnounceResponse is a announce response.
|
|
type AnnounceResponse struct {
|
|
FailureReason string `bencode:"failure reason,omitempty"`
|
|
|
|
// Interval is the seconds the downloader should wait before next rerequest.
|
|
Interval uint32 `bencode:"interval,omitempty"` // BEP 3
|
|
|
|
// Peers is the list of the peers.
|
|
Peers Peers `bencode:"peers,omitempty"` // BEP 3, BEP 23
|
|
|
|
// Peers6 is only used for ipv6 in the compact case.
|
|
Peers6 Peers6 `bencode:"peers6,omitempty"` // BEP 7
|
|
|
|
// Where's this specified?
|
|
// Mentioned at https://wiki.theory.org/index.php/BitTorrentSpecification.
|
|
|
|
// Complete is the number of peers with the entire file.
|
|
Complete uint32 `bencode:"complete,omitempty"`
|
|
// Incomplete is the number of non-seeder peers.
|
|
Incomplete uint32 `bencode:"incomplete,omitempty"`
|
|
// TrackerID is that the client should send back on its next announcements.
|
|
// If absent and a previous announce sent a tracker id,
|
|
// do not discard the old value; keep using it.
|
|
TrackerID string `bencode:"tracker id,omitempty"`
|
|
}
|
|
|
|
// ScrapeResponseResult is the result of the scraped file.
|
|
type ScrapeResponseResult struct {
|
|
// Complete is the number of active peers that have completed downloading.
|
|
Complete uint32 `bencode:"complete"` // BEP 48
|
|
|
|
// Incomplete is the number of active peers that have not completed downloading.
|
|
Incomplete uint32 `bencode:"incomplete"` // BEP 48
|
|
|
|
// The number of peers that have ever completed downloading.
|
|
Downloaded uint32 `bencode:"downloaded"` // BEP 48
|
|
}
|
|
|
|
// ScrapeResponse represents a Scrape response.
|
|
//
|
|
// BEP 48
|
|
type ScrapeResponse struct {
|
|
FailureReason string `bencode:"failure_reason,omitempty"`
|
|
|
|
Files map[metainfo.Hash]ScrapeResponseResult `bencode:"files,omitempty"`
|
|
}
|
|
|
|
// DecodeFrom reads the []byte data from r and decodes them to sr by bencode.
|
|
//
|
|
// r may be the body of the request from the http client.
|
|
func (sr *ScrapeResponse) DecodeFrom(r io.Reader) (err error) {
|
|
return bencode.NewDecoder(r).Decode(sr)
|
|
}
|
|
|
|
// EncodeTo encodes the response to []byte by bencode and write the result into w.
|
|
//
|
|
// w may be http.ResponseWriter.
|
|
func (sr ScrapeResponse) EncodeTo(w io.Writer) (err error) {
|
|
return bencode.NewEncoder(w).Encode(sr)
|
|
}
|
|
|
|
// Client represents a tracker client based on HTTP/HTTPS.
|
|
type Client struct {
|
|
Client *http.Client
|
|
ID metainfo.Hash
|
|
AnnounceURL string
|
|
ScrapeURL string
|
|
}
|
|
|
|
// NewClient returns a new HTTPClient.
|
|
//
|
|
// scrapeURL may be empty, which will replace the "announce" in announceURL
|
|
// with "scrape" to generate the scrapeURL.
|
|
func NewClient(announceURL, scrapeURL string) *Client {
|
|
if scrapeURL == "" {
|
|
scrapeURL = strings.Replace(announceURL, "announce", "scrape", -1)
|
|
}
|
|
id := metainfo.NewRandomHash()
|
|
return &Client{AnnounceURL: announceURL, ScrapeURL: scrapeURL, ID: id}
|
|
}
|
|
|
|
// Close closes the client, which does nothing at present.
|
|
func (t *Client) Close() error { return nil }
|
|
func (t *Client) String() string { return t.AnnounceURL }
|
|
|
|
var (
|
|
i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~")
|
|
i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
|
|
)
|
|
|
|
func Base32(b64addr string) string {
|
|
if len(b64addr) > 300 {
|
|
b64addr = strings.TrimSuffix(b64addr, ".i2p")
|
|
b64, err := i2pB64enc.DecodeString(b64addr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
//hash.Write([]byte(b64))
|
|
var s []byte
|
|
for _, e := range sha256.Sum256(b64) {
|
|
s = append(s, e)
|
|
}
|
|
return strings.ToLower(strings.Replace(i2pB32enc.EncodeToString(s), "=", "", -1)) + ".b32.i2p"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (t *Client) send(c context.Context, u string, vs url.Values, r interface{}) (err error) {
|
|
var url string
|
|
if strings.IndexByte(u, '?') < 0 {
|
|
url = fmt.Sprintf("%s?%s", u, vs.Encode())
|
|
} else {
|
|
url = fmt.Sprintf("%s&%s", u, vs.Encode())
|
|
}
|
|
|
|
req, err := NewRequestWithContext(c, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var resp *http.Response
|
|
if t.Client == nil {
|
|
resp, err = http.DefaultClient.Do(req)
|
|
} else {
|
|
resp, err = t.Client.Do(req)
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return bencode.NewDecoder(resp.Body).Decode(r)
|
|
}
|
|
|
|
// Announce sends a Announce request to the tracker.
|
|
func (t *Client) Announce(c context.Context, req *AnnounceRequest) (resp AnnounceResponse, err error) {
|
|
if req.PeerID.IsZero() {
|
|
if t.ID.IsZero() {
|
|
req.PeerID = metainfo.NewRandomHash()
|
|
} else {
|
|
req.PeerID = t.ID
|
|
}
|
|
}
|
|
|
|
err = t.send(c, t.AnnounceURL, req.ToQuery(), &resp)
|
|
|
|
return
|
|
}
|
|
|
|
// Scrape sends a Scrape request to the tracker.
|
|
func (t *Client) Scrape(c context.Context, infohashes []metainfo.Hash) (resp ScrapeResponse, err error) {
|
|
hs := make([]string, len(infohashes))
|
|
for i, h := range infohashes {
|
|
hs[i] = h.BytesString()
|
|
}
|
|
|
|
err = t.send(c, t.ScrapeURL, url.Values{"info_hash": hs}, &resp)
|
|
return
|
|
}
|