From 944cfe99f03034c37b2d963f7048c41c56f21b9f Mon Sep 17 00:00:00 2001 From: jet2tlf Date: Mon, 3 Jun 2024 14:54:41 -0300 Subject: codecrafters submit [skip ci] --- cmd/mybittorrent/client.go | 136 +++++++++++++++++++++++++++++++++++++++++++++ cmd/mybittorrent/file.go | 13 ----- cmd/mybittorrent/main.go | 41 +++++++++----- cmd/mybittorrent/meta.go | 37 ++++++++++++ 4 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 cmd/mybittorrent/client.go delete mode 100644 cmd/mybittorrent/file.go create mode 100644 cmd/mybittorrent/meta.go diff --git a/cmd/mybittorrent/client.go b/cmd/mybittorrent/client.go new file mode 100644 index 0000000..f3988b6 --- /dev/null +++ b/cmd/mybittorrent/client.go @@ -0,0 +1,136 @@ +package main + +import ( + "encoding/binary" + "fmt" + "log/slog" + "net/http" + "net/url" + "os" + "strconv" + + bencode "github.com/jackpal/bencode-go" +) + +type Client struct { + PeerId string + Port int + Torrents map[string]ClientTorrent +} + +type ClientTorrent struct { + Meta Meta + Uploaded int + Downloaded int + Left int +} + +type PeerResponse struct { + Interval int + Peers []string +} + +func NewClient(peerId string, port int) *Client { + return &Client{ + PeerId: peerId, + Port: port, + Torrents: make(map[string]ClientTorrent), + } +} + +func (c *Client) AddTorrentFile(filename string) (ClientTorrent, error) { + f, err := os.Open(filename) + defer func(f *os.File) { + err := f.Close() + if err != nil { + slog.Error("Failed to close file", "filename", filename) + } + }(f) + + if err != nil { + fmt.Println("Failed to open file: ", os.Args[2]) + } + + var meta Meta + + if err = bencode.Unmarshal(f, &meta); err != nil { + return ClientTorrent{}, err + } + + t := ClientTorrent{ + Meta: meta, + Uploaded: 0, + Downloaded: 0, + Left: meta.Info.Length, + } + + c.Torrents[filename] = t + + return t, nil +} + +func (ct ClientTorrent) getUrl(c Client) (string, error) { + u, err := url.Parse(ct.Meta.Announce) + if err != nil { + return "", err + } + + q := u.Query() + + infoHash, err := ct.Meta.InfoHash() + if err != nil { + return "", err + } + + q.Set("info_hash", string(infoHash)) + q.Set("peer_id", c.PeerId) + q.Set("port", strconv.Itoa(c.Port)) + q.Set("uploaded", strconv.Itoa(ct.Uploaded)) + q.Set("downloaded", strconv.Itoa(ct.Downloaded)) + q.Set("left", strconv.Itoa(ct.Left)) + q.Set("compact", "1") + u.RawQuery = q.Encode() + + return u.String(), nil +} + +func DecodePeers(b []byte) []string { + val := make([]string, 0, len(b)/6) + for i := 0; i < len(b); i += 6 { + val = append(val, fmt.Sprintf("%d.%d.%d.%d:%d", b[i], b[i+1], b[i+2], b[i+3], binary.BigEndian.Uint16(b[i+4:i+6]))) + } + + return val +} + +func (c *Client) GetPeers(filename string) (PeerResponse, error) { + ct, ok := c.Torrents[filename] + if !ok { + return PeerResponse{}, fmt.Errorf("missing file from client: %s", filename) + } + + u, err := ct.getUrl(*c) + if err != nil { + return PeerResponse{}, err + } + + getResp, err := http.Get(u) + if err != nil { + return PeerResponse{}, err + } + + var resp struct { + Interval int + Peers string + } + + err = bencode.Unmarshal(getResp.Body, &resp) + if err != nil { + return PeerResponse{}, err + } + + return PeerResponse{ + Interval: resp.Interval, + Peers: DecodePeers([]byte(resp.Peers)), + }, err +} diff --git a/cmd/mybittorrent/file.go b/cmd/mybittorrent/file.go deleted file mode 100644 index 498ea3f..0000000 --- a/cmd/mybittorrent/file.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -type Meta struct { - Announce string `bencode:"announce"` - Info FileInfo `bencode:"info"` -} - -type FileInfo struct { - Length int `bencode:"length"` - Name string `bencode:"name"` - PieceLength int `bencode:"piece length"` - Pieces string `bencode:"pieces"` -} diff --git a/cmd/mybittorrent/main.go b/cmd/mybittorrent/main.go index 5eecb20..c460c35 100644 --- a/cmd/mybittorrent/main.go +++ b/cmd/mybittorrent/main.go @@ -1,7 +1,6 @@ package main import ( - "crypto/sha1" "encoding/json" "fmt" "os" @@ -26,31 +25,45 @@ func main() { fmt.Println(string(jsonOutput)) case "info": - f, err := os.Open(os.Args[2]) + c := NewClient("00112233445566778899", 6881) + tf, err := c.AddTorrentFile(os.Args[2]) if err != nil { - fmt.Println("Failed to open file: ", os.Args[2]) + panic(err) } - var meta Meta - if err = bencode.Unmarshal(f, &meta); err != nil { + fmt.Printf("Tracker URL: %s\n", tf.Meta.Announce) + fmt.Printf("Length: %d\n", tf.Meta.Info.Length) + + infoHash, err := tf.Meta.InfoHash() + if err != nil { panic(err) } - fmt.Printf("Tracker URL: %s\n", meta.Announce) - fmt.Printf("Length: %d\n", meta.Info.Length) + fmt.Printf("Info Hash: %x", infoHash) + + fmt.Printf("Piece Length: %d\n", tf.Meta.Info.PieceLength) + fmt.Println("Piece Hashes:") + + for _, h := range tf.Meta.PieceHashes() { + fmt.Printf("%x\n", h) + } - sha := sha1.New() - if err = bencode.Marshal(sha, meta.Info); err != nil { + case "peers": + c := NewClient("00112233445566778899", 6881) + + _, err := c.AddTorrentFile(os.Args[2]) + if err != nil { panic(err) } - fmt.Printf("Info Hash: %x", sha.Sum(nil)) + pr, err := c.GetPeers(os.Args[2]) - fmt.Printf("Piece Length: %d\n", meta.Info.PieceLength) - fmt.Println("Piece Hashes:") + if err != nil { + panic(err) + } - for i := 0; i < len(meta.Info.Pieces); i += 20 { - fmt.Printf("%x\n", meta.Info.Pieces[i:i+20]) + for _, peer := range pr.Peers { + fmt.Println(peer) } default: diff --git a/cmd/mybittorrent/meta.go b/cmd/mybittorrent/meta.go new file mode 100644 index 0000000..34369c4 --- /dev/null +++ b/cmd/mybittorrent/meta.go @@ -0,0 +1,37 @@ +package main + +import ( + "crypto/sha1" + + bencode "github.com/jackpal/bencode-go" +) + +type Meta struct { + Announce string `bencode:"announce"` + Info FileInfo `bencode:"info"` +} + +type FileInfo struct { + Length int `bencode:"length"` + Name string `bencode:"name"` + PieceLength int `bencode:"piece length"` + Pieces string `bencode:"pieces"` +} + +func (m Meta) InfoHash() ([]byte, error) { + sha := sha1.New() + if err := bencode.Marshal(sha, m.Info); err != nil { + return nil, err + } + + return sha.Sum(nil), nil +} + +func (m Meta) PieceHashes() []string { + hashes := make([]string, 0, len(m.Info.Pieces)/20) + for i := 0; i < len(m.Info.Pieces); i += 20 { + hashes = append(hashes, m.Info.Pieces[i:i+20]) + } + + return hashes +} -- cgit v1.2.3