change map key type to fixed size array (Peer)

This commit is contained in:
Michał Adamski
2025-01-24 11:29:26 +01:00
parent 872a3252a4
commit 62a3d7f8b8
6 changed files with 1802 additions and 77 deletions

View File

@@ -1,6 +1,8 @@
package main
import (
"net"
"github.com/gofiber/fiber/v2"
"github.com/jackpal/bencode-go"
)
@@ -38,19 +40,24 @@ func announce(c *fiber.Ctx) error {
if err != nil {
return err
}
req.IP = c.IP()
room := c.Params("room")
ip := net.ParseIP(c.IP())
if ip == nil {
ip = c.Context().RemoteIP()
}
if req.Numwant == 0 {
req.Numwant = 30
}
peer := NewPeer(ip, req.Port)
switch req.Event {
case "stopped":
DeletePeer(c.Params("room"), req.InfoHash, req.IP, req.Port)
DeletePeer(room, req.InfoHash, peer)
case "completed":
GraduateLeecher(c.Params("room"), req.InfoHash, req.IP, req.Port)
GraduateLeecher(room, req.InfoHash, peer)
default:
PutPeer(c.Params("room"), req.InfoHash, req.IP, req.Port, req.IsSeeding())
PutPeer(room, req.InfoHash, peer, req.IsSeeding())
}
peersIPv4, peersIPv6, numSeeders, numLeechers := GetPeers(c.Params("room"), req.InfoHash, req.IP, req.Port, req.IsSeeding(), req.Numwant)
peersIPv4, peersIPv6, numSeeders, numLeechers := GetPeers(room, req.InfoHash, peer, req.IsSeeding(), req.Numwant)
interval := 120
if numSeeders == 0 {
interval /= 2

View File

@@ -4,30 +4,32 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<link rel="canonical" href="https://privtracker.com/" />
<link rel="stylesheet" href="water.css">
<link rel="canonical" href="https://privtracker.com/">
<title>PrivTracker - Private BitTorrent tracker for everyone</title>
<script>
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".url").forEach(url => {
url.innerText = url.innerText.replace('{{hostname}}', window.location.host);
url.innerText = url.innerText.replace('{{origin}}', window.location.origin);
})
})
function makeRoom(length) {
let roomID = '';
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
roomID += characters.charAt(Math.floor(Math.random() * charactersLength));
}
function makeRoom() {
const randomValues = new Uint8Array(16);
crypto.getRandomValues(randomValues);
const roomID = Array.from(randomValues).map(byte => byte.toString(16).padStart(2, '0')).join('');
document.querySelectorAll(".url").forEach(url => {
url.innerText = url.innerText.replace('{{room}}', roomID);
})
});
document.querySelector('#hidden').style.display = 'block';
}
function copyToClipboard() {
navigator.clipboard.writeText(document.querySelector('.url').innerText);
if (navigator.clipboard) {
navigator.clipboard.writeText(document.querySelector('.url').innerText);
} else {
window.alert('"Copy to clipboard" works only on https websites.');
}
}
</script>
</head>
@@ -45,33 +47,21 @@
It really works like a private tracker, but can be generated with one click of a button.
</p>
<button onclick="makeRoom(32)">I'm in. Generate private tracker for me</button>
<button onclick="makeRoom()">I'm in. Generate private tracker for me</button>
<div id="hidden" style="display:none">
<h3>Your announce URL:</h3>
<code class="url">https://{{hostname}}/{{room}}/announce</code>
<code class="url">{{origin}}/{{room}}/announce</code>
<button onclick="copyToClipboard()">Copy to clipboard</button>
<p>(You can also use any random string as the Room ID. We don't store it anywhere.)</p>
</div>
<div>
<h2>The Problem PrivTracker Solves</h2>
<div>
Sharing large files has always been difficult without compromising privacy or simplicity.
Centralized services require full uploads before sharing, often with account registration and fees for large files.
Hosting a file server demands technical knowledge, like opening firewall ports, and requires your computer to stay online.
PrivTracker solves this by enabling private torrent sharing within a trusted group.
Using UPnP, most torrent clients can automatically handle port opening, and only one person in the group needs an open port for everyone to download.
Unlike public trackers, PrivTracker shares peers' IPs only within the group and keeps files off public networks like DHT, ensuring privacy and efficient sharing.
</div>
</div>
<h2>How to create private torrent?</h2>
<h3>Using <a href="https://transmissionbt.com/" target="_blank">transmission</a> on Linux:</h3>
<ul>
<li>File <b>&rarr;</b> New&hellip;</li>
<li>Select file to share</li>
<li>In <b>Trackers</b> field enter <code class="url">https://{{hostname}}/{{room}}/announce</code></li>
<li>In <b>Trackers</b> field enter <code class="url">{{origin}}/{{room}}/announce</code></li>
<li>Select <b>Private torrent</b></li>
<li>Click <b>New</b></li>
<li>Click <b>Add</b> in next window</li>
@@ -84,7 +74,7 @@
<li>File <b>&rarr;</b> Create Torrent File&hellip;</li>
<li>Select file to share</li>
<li>Click <b>&plus;</b> under <b>Trackers</b> field and enter <code
class="url">https://{{hostname}}/{{room}}/announce</code></li>
class="url">{{origin}}/{{room}}/announce</code></li>
<li>Select <b>Private</b>
<li>Select <b>Open when created</b></li>
<li>Click <b>Create</b></li>
@@ -93,7 +83,20 @@
</ul>
<img src="new_torrent_mac.png" alt="New Torrent screenshot">
<h2>The Problem PrivTracker solves</h2>
<p>
Sharing large files has always been difficult without compromising privacy or simplicity.
Centralized services require full uploads before sharing, often with account registration and fees for
large files.
Hosting a file server demands technical knowledge, like opening firewall ports, and requires your
computer to stay online.
PrivTracker solves this by enabling private torrent sharing within a trusted group.
Using UPnP, most torrent clients can automatically handle port opening, and only one person in the group
needs an open port for everyone to download.
Unlike public trackers, PrivTracker shares peers' IPs only within the group and keeps files off public
networks like DHT, ensuring privacy and efficient sharing.
</p>
</div>
</body>
</html>
</html>

1689
docs/water.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,8 @@ func main() {
port = "1337"
}
config := fiber.Config{
AppName: "PrivTracker.com",
ServerHeader: "PrivTracker.com",
AppName: "PrivTracker",
ServerHeader: "PrivTracker",
ReadTimeout: time.Second * 245,
WriteTimeout: time.Second * 30,
Network: fiber.NetworkTCP,
@@ -38,7 +38,7 @@ func main() {
config.ProxyHeader = fiber.HeaderXForwardedFor
}
go Cleanup()
go Cleanup(time.Second * 600)
app := fiber.New(config)
app.Use(recover.New())

View File

@@ -4,109 +4,122 @@ import (
"bytes"
"crypto/sha1"
"encoding/binary"
"fmt"
"net"
"runtime"
"sync"
"time"
)
type serializedPeer string
type hash [20]byte
type Hash [20]byte // we use sha1 and we are not affraid of hash collisions
type Peer [18]byte // 16 bytes for IP and 2 bytes for port number
func NewPeer(ip net.IP, port uint16) (peer Peer) {
copy(peer[:], ip)
peer[16] = byte(port >> 8)
peer[17] = byte(port)
return
}
func (peer Peer) String() string {
return fmt.Sprintf("%s:%d", net.IP(peer[:16]), binary.BigEndian.Uint16(peer[16:]))
}
// type serializedPeer [18]byte
type shard struct {
swarms map[hash]swarm
swarms map[Hash]swarm
sync.RWMutex
}
type swarm struct {
seeders map[serializedPeer]int64
leechers map[serializedPeer]int64
seeders map[Peer]int64
leechers map[Peer]int64
}
var shards = NewShards(512)
var v4InV6Prefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
func shardIndex(hash [20]byte) int {
return int(binary.BigEndian.Uint32(hash[:4])) % len(shards)
return int(binary.BigEndian.Uint16(hash[:2])) % len(shards)
}
func NewShards(size int) []*shard {
shards := make([]*shard, size)
for i := 0; i < size; i++ {
shards[i] = &shard{
swarms: make(map[hash]swarm),
swarms: make(map[Hash]swarm),
}
}
return shards
}
func serialize(ip string, port uint16) serializedPeer {
return serializedPeer(append(net.ParseIP(ip), byte(port>>8), byte(port)))
}
// func serialize(ip net.IP, port uint16) serializedPeer {
// var sp serializedPeer
// copy(sp[:], ip)
// sp[16] = byte(port >> 8)
// sp[17] = byte(port)
// return sp
// }
func PutPeer(room, infoHash, ip string, port uint16, seeding bool) {
func PutPeer(room, infoHash string, peer Peer, seeding bool) {
h := sha1.Sum([]byte(room + infoHash))
shard := shards[shardIndex(h)]
shard.Lock()
if _, ok := shard.swarms[h]; !ok {
shard.swarms[h] = swarm{
seeders: make(map[serializedPeer]int64),
leechers: make(map[serializedPeer]int64),
seeders: make(map[Peer]int64),
leechers: make(map[Peer]int64),
}
}
client := serialize(ip, port)
if seeding {
shard.swarms[h].seeders[client] = time.Now().Unix()
shard.swarms[h].seeders[peer] = time.Now().Unix()
} else {
shard.swarms[h].leechers[client] = time.Now().Unix()
shard.swarms[h].leechers[peer] = time.Now().Unix()
}
shard.Unlock()
}
func DeletePeer(room, infoHash, ip string, port uint16) {
func DeletePeer(room, infoHash string, peer Peer) {
h := sha1.Sum([]byte(room + infoHash))
shard := shards[shardIndex(h)]
shard.Lock()
if _, ok := shard.swarms[h]; !ok {
return
}
client := serialize(ip, port)
delete(shard.swarms[h].seeders, client)
delete(shard.swarms[h].leechers, client)
delete(shard.swarms[h].seeders, peer)
delete(shard.swarms[h].leechers, peer)
shard.Unlock()
}
func GraduateLeecher(room, infoHash, ip string, port uint16) {
func GraduateLeecher(room, infoHash string, peer Peer) {
h := sha1.Sum([]byte(room + infoHash))
shard := shards[shardIndex(h)]
shard.Lock()
if _, ok := shard.swarms[h]; !ok {
shard.swarms[h] = swarm{
seeders: make(map[serializedPeer]int64),
leechers: make(map[serializedPeer]int64),
seeders: make(map[Peer]int64),
leechers: make(map[Peer]int64),
}
}
client := serialize(ip, port)
shard.swarms[h].seeders[client] = time.Now().Unix()
delete(shard.swarms[h].leechers, client)
shard.swarms[h].seeders[peer] = time.Now().Unix()
delete(shard.swarms[h].leechers, peer)
shard.Unlock()
}
func GetPeers(room, infoHash, ip string, port uint16, seeding bool, numWant uint) (peersIPv4, peersIPv6 []byte, numSeeders, numLeechers int) {
func GetPeers(room, infoHash string, client Peer, seeding bool, numWant uint) (peersIPv4, peersIPv6 []byte, numSeeders, numLeechers int) {
h := sha1.Sum([]byte(room + infoHash))
shard := shards[shardIndex(h)]
shard.RLock()
client := serialize(ip, port)
// seeders don't need other seeders
if !seeding {
for peer := range shard.swarms[h].seeders {
if numWant == 0 {
break
}
if bytes.HasPrefix([]byte(peer), v4InV6Prefix) {
peersIPv4 = append(peersIPv4, peer[12:]...)
if bytes.HasPrefix(peer[:], v4InV6Prefix) {
peersIPv4 = append(peersIPv4, peer[len(v4InV6Prefix):]...)
} else {
peersIPv6 = append(peersIPv6, peer...)
peersIPv6 = append(peersIPv6, peer[:]...)
}
numWant--
}
@@ -118,10 +131,10 @@ func GetPeers(room, infoHash, ip string, port uint16, seeding bool, numWant uint
if numWant == 0 {
break
}
if bytes.HasPrefix([]byte(peer), v4InV6Prefix) {
if bytes.HasPrefix(peer[:], v4InV6Prefix) {
peersIPv4 = append(peersIPv4, peer[12:]...)
} else {
peersIPv6 = append(peersIPv6, peer...)
peersIPv6 = append(peersIPv6, peer[:]...)
}
numWant--
}
@@ -141,10 +154,10 @@ func GetStats(room, infoHash string) (numSeeders, numLeechers int) {
return
}
func Cleanup() {
for {
time.Sleep(time.Second * 600)
expiration := time.Now().Unix() - 600
func Cleanup(duration time.Duration) {
ticker := time.NewTicker(duration)
for range ticker.C {
expiration := time.Now().Unix() - int64(duration.Seconds())
for _, shard := range shards {
shard.Lock()
for h, swarm := range shard.swarms {
@@ -164,6 +177,5 @@ func Cleanup() {
}
shard.Unlock()
}
runtime.GC()
}
}

14
storage_test.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import (
"net"
"testing"
)
func BenchmarkPutPeerGetPeers(b *testing.B) {
peer := NewPeer(net.ParseIP("127.0.0.1"), 6881)
for i := 0; i < b.N; i++ {
PutPeer("room", "infoHash", peer, true)
GetPeers("room", "infoHash", peer, true, 99)
}
}