Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
rohanxminocha committed Sep 11, 2023
1 parent 4d656be commit 0267e18
Show file tree
Hide file tree
Showing 11 changed files with 599 additions and 1 deletion.
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Default Go gitignore
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
##

# Torrents
*.torrent

# VS Code launch config
launch.json

# Logs, etc
*.txt
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# gotorrent
# GoTorrent
66 changes: 66 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
The client package defines a client, which is a collection of torrents.
Each torrent stores the metainfo of the torrent and the peer connection
info.
*/
package client

import (
"fmt"
"strings"
)

type Client struct {
Torrents map[string]torrent
}

func New() Client {
return Client{Torrents: make(map[string]torrent)}
}

func (c *Client) AddTorrent(input string) error {
torrent, err := newTorrent(input)
if err != nil {
return err
}
newName := torrent.metainfo.Info.Name

// Later, to make this more efficient, do the following check immediately
// after setting the metainfo
for name := range c.Torrents {
if name == newName {
return fmt.Errorf("torrent %s already exists", name)
}
}

c.Torrents[newName] = *torrent
c.StartTorrent(newName)
return nil
}

func (c *Client) RemoveTorrent(prefix string) error {
for name := range c.Torrents {
if strings.HasPrefix(name, prefix) {
fmt.Printf("Removed torrent %s\n", name)
c.StopTorrent(name)
delete(c.Torrents, name)
return nil
}
}

return fmt.Errorf("no torrent matches prefix %s", prefix)
}

func (c *Client) StartTorrent(prefix string) error {
return nil
}

func (c *Client) StopTorrent(prefix string) error {
return nil
}

func (c Client) ShowTorrents() {
for name := range c.Torrents {
fmt.Printf("%s\n", name)
}
}
87 changes: 87 additions & 0 deletions client/metainfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package client

import (
"crypto/sha1"
"encoding/json"
"errors"
"io"
"reflect"

"github.com/marksamman/bencode"
)

// Some of the following fields don't make sense, but the goal with these
// structs is to store data directly from the decoded torrent file stream.
// Thus, this represents the structure of that stream.
type file struct {
Length int64 `json:"length"`
Path []string `json:"path"`
}

type info struct {
Files []file `json:"files"`
hasMultipleFiles bool
Length int64 `json:"length"`
Name string `json:"name"`
PieceLength int64 `json:"piece length"`
Pieces string `json:"pieces"`
}

type metainfo struct {
Announce string `json:"announce"`
AnnounceList [][]string `json:"announce-list"`
Comment string `json:"comment"`
CreatedBy string `json:"created by"`
CreationDate int64 `json:"creation date"`
Info info `json:"info"`
infoHash [20]byte
}

func (m metainfo) checkFieldsPostUnmarshal() error {
if m.Announce == "" && m.AnnounceList == nil {
return errors.New("no url in metainfo to announce to")
} else if reflect.DeepEqual(m.Info, info{}) {
return errors.New("no info in metainfo")
} else if m.Info.Name == "" {
return errors.New("no name field in metainfo info")
} else if m.Info.PieceLength == 0 || m.Info.Pieces == "" {
return errors.New("no piece info in metainfo info")
} else if len(m.Info.Files) == 0 && m.Info.Length == 0 {
return errors.New("neither files nor length exists in metainfo info")
}

return nil
}

func (m *metainfo) setRemainingFields(d map[string]interface{}) {
m.infoHash = sha1.Sum(bencode.Encode(
d["info"].(map[string]interface{}),
))
m.Info.hasMultipleFiles = len(m.Info.Files) != 0
}

func newMetainfo(r io.Reader) (*metainfo, error) {
decodedStream, err := bencode.Decode(r)
if err != nil {
return nil, err
}

bytes, err := json.Marshal(decodedStream)
if err != nil {
return nil, err
}

m := metainfo{}
err = json.Unmarshal(bytes, &m)
if err != nil {
return nil, err
}

err = m.checkFieldsPostUnmarshal()
if err != nil {
return nil, err
}
m.setRemainingFields(decodedStream)

return &m, nil
}
93 changes: 93 additions & 0 deletions client/peer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package client

import (
"fmt"
"net"
"sync"
"time"
)

type peerConnection struct {
Id string `json:"peer id"`
Ip string `json:"ip"`
Port int `json:"port"`
}

type peer struct {
choked bool
available bool
connection peerConnection
interested bool
}

func (t *torrent) handshake() error {
clientHandshake := fmt.Sprintf(
"%s%s%s",
"\023BitTorrent protocol00000000",
string(t.metainfo.infoHash[:]),
t.id,
)

var wg sync.WaitGroup
for i := 0; i < len(t.peers); i++ {
wg.Add(1)

go func(idx int) {
defer wg.Done()

ip := t.peers[idx].connection.Ip
port := t.peers[idx].connection.Port
dialer := net.Dialer{
Timeout: 5 * time.Second,
}
conn, err := dialer.Dial(
"tcp",
fmt.Sprintf("%s:%d", ip, port),
)
if err != nil {
return
}
defer conn.Close()

// Send the handshake message to the conn and wait on and read the
// response. Wait time is arbitrary, haven't read any literature
// on an optimal time yet.
conn.SetDeadline(time.Now().Add(5 * time.Second))
conn.Write([]byte(clientHandshake))

// Can't assume that the handshake starts with
// "19Bittorrent protocol". Read the length first and then read
// the rest based on that length.
pstrlenBytes := make([]byte, 1)
_, err = conn.Read(pstrlenBytes)
if err != nil {
return
}
pstrlen := int(pstrlenBytes[0])
if pstrlen == 0 {
return
}

peerHandshake := make([]byte, 48+pstrlen)
n, err := conn.Read(peerHandshake)
if err != nil || n == 0 {
return
}

// Return if the info hash coming in isn't the same as the one
// we're requesting
l := len(peerHandshake)
if string(t.metainfo.infoHash[:]) != string(peerHandshake[l-40:l-20]) {
return
}

// Since we could complete the handshake, set the peer ID and
// connected fields
t.peers[idx].connection.Id = string(peerHandshake[l-20 : l-1])
t.peers[idx].available = true
}(i)
}

wg.Wait()
return nil
}
97 changes: 97 additions & 0 deletions client/torrent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package client

import (
"fmt"
"io"
"math/rand"
"os"
)

type inputType int

const (
// Just add support for file paths for now
path inputType = iota
// url
// info hash
// magnet link
invalid
)

type torrent struct {
// torrents are considered clients here, so this is the peer id
id []byte
metainfo metainfo
trackers []tracker
peers []peer
}

func interpretInput(input string) inputType {
_, err := os.Open(input)
if err == nil {
return path
}

return invalid
}

func createTorrentFromFileContents(path string) (*torrent, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}

r := io.Reader(f)
m, err := newMetainfo(r)
if err != nil {
return nil, err
}

return &torrent{metainfo: *m}, err
}

func createId() ([]byte, error) {
// Azureus style with arbitrary client id and version number
base := []byte("-GG0001-")
randSuffix := make([]byte, 12)
_, err := rand.Read(randSuffix)
if err != nil {
return nil, err
}

id := append(base, randSuffix...)
return id, nil
}

func newTorrent(input string) (*torrent, error) {
var t *torrent
var err error

inputType := interpretInput(input)
switch inputType {
case path:
t, err = createTorrentFromFileContents(input)
if err != nil {
return nil, err
}
case invalid:
return nil, fmt.Errorf("input %s is invalid", input)
}

t.id, err = createId()
if err != nil {
return nil, err
}

err = t.requestPeers()
if err != nil {
return nil, err
}

err = t.handshake()
if err != nil {
return nil, err
}

return t, nil
}
Loading

0 comments on commit 0267e18

Please sign in to comment.