-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4d656be
commit 0267e18
Showing
11 changed files
with
599 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
# gotorrent | ||
# GoTorrent |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.