mirror of
https://github.com/meehow/privtracker.git
synced 2026-05-07 07:37:39 +08:00
removed dependency on fiber and solved memory leaks
This commit is contained in:
2
Makefile
2
Makefile
@@ -1,5 +1,5 @@
|
||||
build:
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w -buildid=" -trimpath
|
||||
|
||||
deploy: build
|
||||
rsync -avzL --exclude '*.fiber.gz' docs privtracker privtracker:web/
|
||||
|
||||
94
announce.go
94
announce.go
@@ -2,32 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/jackpal/bencode-go"
|
||||
)
|
||||
|
||||
type AnnounceRequest struct {
|
||||
InfoHash string `query:"info_hash"`
|
||||
PeerID string `query:"peer_id"`
|
||||
IP string `query:"ip"`
|
||||
Port uint16 `query:"port"`
|
||||
Uploaded uint `query:"uploaded"`
|
||||
Downloaded uint `query:"downloaded"`
|
||||
Left uint `query:"left"`
|
||||
Numwant uint `query:"numwant"`
|
||||
Key string `query:"key"`
|
||||
Compact bool `query:"compact"`
|
||||
SupportCrypto bool `query:"supportcrypto"`
|
||||
Event string `query:"event"`
|
||||
}
|
||||
|
||||
func (req *AnnounceRequest) IsSeeding() bool {
|
||||
return req.Left == 0
|
||||
}
|
||||
|
||||
type AnnounceResponse struct {
|
||||
Interval int `bencode:"interval"`
|
||||
Complete int `bencode:"complete"`
|
||||
@@ -36,38 +20,44 @@ type AnnounceResponse struct {
|
||||
PeersIPv6 []byte `bencode:"peers_ipv6"`
|
||||
}
|
||||
|
||||
func announce(c *fiber.Ctx) error {
|
||||
var req AnnounceRequest
|
||||
err := c.QueryParser(&req)
|
||||
func announce(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
port, err := strconv.Atoi(query.Get("port"))
|
||||
if err != nil {
|
||||
return err
|
||||
http.Error(w, "port missing", 400)
|
||||
return
|
||||
}
|
||||
ip := net.ParseIP(c.IP())
|
||||
ip := getRemoteIP(r)
|
||||
if ip == nil {
|
||||
ip = c.Context().RemoteIP()
|
||||
http.Error(w, "can't parse IP", 400)
|
||||
return
|
||||
}
|
||||
if req.Numwant < 1 {
|
||||
req.Numwant = 30
|
||||
numwant, err := strconv.Atoi(query.Get("numwant"))
|
||||
if err != nil || numwant < 1 {
|
||||
numwant = 30
|
||||
}
|
||||
swarmHash := sha1.Sum([]byte(c.Params("room") + req.InfoHash))
|
||||
peer := NewPeer(ip, req.Port)
|
||||
switch req.Event {
|
||||
swarmHash := sha1.Sum([]byte(r.PathValue("room") + query.Get("info_hash")))
|
||||
peer := NewPeer(ip, uint16(port))
|
||||
isSeeding := query.Get("left") == "0"
|
||||
switch query.Get("event") {
|
||||
case "stopped":
|
||||
DeletePeer(swarmHash, peer)
|
||||
case "completed":
|
||||
GraduateLeecher(swarmHash, peer)
|
||||
default:
|
||||
PutPeer(swarmHash, peer, req.IsSeeding())
|
||||
PutPeer(swarmHash, peer, isSeeding)
|
||||
}
|
||||
peersIPv4, peersIPv6, numSeeders, numLeechers := GetPeers(swarmHash, peer, req.IsSeeding(), req.Numwant)
|
||||
interval := int(time.Now().Unix()+int64(swarmHash[0]))%256 + 60
|
||||
peersIPv4, peersIPv6, numSeeders, numLeechers := GetPeers(swarmHash, peer, isSeeding, numwant)
|
||||
interval := 480 // must be smaller than cleanup interval
|
||||
switch {
|
||||
// case numSeeders == 0:
|
||||
// interval -= 30
|
||||
case numLeechers == 0:
|
||||
interval += 240
|
||||
case numSeeders+numLeechers > 10:
|
||||
interval += 480
|
||||
case numSeeders+numLeechers < 10:
|
||||
// try to synchronize peer requests. Maybe it will help µTP with UDP port punching... not sure
|
||||
interval = 240 - int(time.Now().Unix()+int64(swarmHash[0]))%240
|
||||
if interval < 60 {
|
||||
interval += 240
|
||||
}
|
||||
case numSeeders+numLeechers > 30:
|
||||
interval = 900
|
||||
}
|
||||
resp := AnnounceResponse{
|
||||
Interval: interval,
|
||||
@@ -76,5 +66,27 @@ func announce(c *fiber.Ctx) error {
|
||||
Peers: peersIPv4,
|
||||
PeersIPv6: peersIPv6,
|
||||
}
|
||||
return bencode.Marshal(c, resp)
|
||||
w.Header().Add("X-PrivTracker", fmt.Sprintf("s:%d l:%d", numSeeders, numLeechers))
|
||||
if err := bencode.Marshal(w, resp); err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
}
|
||||
}
|
||||
|
||||
func getRemoteIP(r *http.Request) net.IP {
|
||||
addr := r.RemoteAddr
|
||||
if colonIndex := strings.LastIndex(addr, ":"); colonIndex != -1 {
|
||||
addr = addr[:colonIndex]
|
||||
}
|
||||
addr = strings.Trim(addr, "[]")
|
||||
ip := net.ParseIP(addr)
|
||||
if ip.IsPrivate() {
|
||||
ips := strings.Split(r.Header.Get("X-Forwarded-For"), ",")
|
||||
if len(ips) > 0 {
|
||||
ipForwarded := net.ParseIP(strings.TrimSpace(ips[0]))
|
||||
if ipForwarded != nil {
|
||||
ip = ipForwarded
|
||||
}
|
||||
}
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
17
announce_test.go
Normal file
17
announce_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkAnnounce(b *testing.B) {
|
||||
server := httptest.NewServer(router())
|
||||
client := server.Client()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := client.Get(server.URL + "/test/announce?port=1234")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
go.mod
14
go.mod
@@ -1,25 +1,13 @@
|
||||
module github.com/meehow/privtracker
|
||||
|
||||
go 1.21.4
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/jackpal/bencode-go v1.0.2
|
||||
golang.org/x/crypto v0.32.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
)
|
||||
|
||||
27
go.sum
27
go.sum
@@ -1,35 +1,8 @@
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackpal/bencode-go v1.0.2 h1:LcCNfZ344u0LpBPOZNjpCLps/wUOuN4r87Fy9+5yU8g=
|
||||
github.com/jackpal/bencode-go v1.0.2/go.mod h1:6jI9mUjO3GQbZti3JizEfxTzRfWOM8oBBcwbwlTfceI=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
|
||||
93
main.go
93
main.go
@@ -5,64 +5,37 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/monitor"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("PORT")
|
||||
tlsEnabled := port == "443"
|
||||
if port == "" {
|
||||
port = "1337"
|
||||
}
|
||||
config := fiber.Config{
|
||||
AppName: "PrivTracker",
|
||||
ServerHeader: "PrivTracker",
|
||||
ReadTimeout: time.Second * 245,
|
||||
WriteTimeout: time.Second * 30,
|
||||
Network: fiber.NetworkTCP,
|
||||
GETOnly: true,
|
||||
DisableKeepalive: true,
|
||||
Immutable: true,
|
||||
}
|
||||
// if you disable TLS, then I guess you want to use existing proxy
|
||||
if !tlsEnabled {
|
||||
config.EnableTrustedProxyCheck = true
|
||||
config.TrustedProxies = []string{"127.0.0.1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
|
||||
config.ProxyHeader = fiber.HeaderXForwardedFor
|
||||
}
|
||||
|
||||
go Cleanup(time.Minute * 16)
|
||||
|
||||
app := fiber.New(config)
|
||||
app.Use(recover.New())
|
||||
// app.Use(pprof.New())
|
||||
app.Use(myLogger())
|
||||
app.Use(hsts)
|
||||
app.Static("/", "docs", fiber.Static{
|
||||
MaxAge: 3600 * 24 * 7,
|
||||
Compress: true,
|
||||
CacheDuration: time.Hour,
|
||||
})
|
||||
app.Get("/dashboard", monitor.New())
|
||||
app.Get("/:room/announce", announce)
|
||||
app.Get("/:room/scrape", scrape)
|
||||
app.Server().LogAllErrors = true
|
||||
if tlsEnabled {
|
||||
go redirect80(config)
|
||||
log.Fatal(app.Listener(autocertListener()))
|
||||
handler := router(recoveryMiddleware, headersMiddleware, logRequestMiddleware)
|
||||
if port == "443" {
|
||||
go redirect80()
|
||||
fmt.Println("PrivTracker listening on https://0.0.0.0/")
|
||||
log.Fatal(http.Serve(autocertListener(), handler))
|
||||
} else {
|
||||
log.Fatal(app.Listen(":" + port))
|
||||
fmt.Printf("PrivTracker listening on http://0.0.0.0:%s/\n", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, handler))
|
||||
}
|
||||
}
|
||||
|
||||
func router(middlewares ...Middleware) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.FileServer(http.Dir("docs")))
|
||||
mux.HandleFunc("GET /{room}/announce", announce)
|
||||
mux.HandleFunc("GET /{room}/scrape", scrape)
|
||||
return chainMiddleware(mux, middlewares...)
|
||||
}
|
||||
|
||||
func autocertListener() net.Listener {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -75,33 +48,21 @@ func autocertListener() net.Listener {
|
||||
}
|
||||
cfg := &tls.Config{
|
||||
GetCertificate: m.GetCertificate,
|
||||
NextProtos: []string{
|
||||
"http/1.1", "acme-tls/1",
|
||||
},
|
||||
NextProtos: []string{"h2", "http/1.1", "acme-tls/1"},
|
||||
}
|
||||
ln, err := tls.Listen("tcp", ":443", cfg)
|
||||
listener, err := tls.Listen("tcp", ":443", cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return ln
|
||||
return listener
|
||||
}
|
||||
|
||||
func redirect80(config fiber.Config) {
|
||||
config.DisableStartupMessage = true
|
||||
app := fiber.New(config)
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
return c.Redirect(fmt.Sprintf("https://%s/", c.Hostname()), fiber.StatusMovedPermanently)
|
||||
})
|
||||
log.Print(app.Listen(":80"))
|
||||
}
|
||||
|
||||
func myLogger() fiber.Handler {
|
||||
loggerConfig := logger.ConfigDefault
|
||||
loggerConfig.Format = "${status} - ${latency} ${ip} ${method} ${path} ${bytesSent} - ${referer} - ${ua}\n"
|
||||
return logger.New(loggerConfig)
|
||||
}
|
||||
|
||||
func hsts(c *fiber.Ctx) error {
|
||||
c.Set("Strict-Transport-Security", "max-age=31536000")
|
||||
return c.Next()
|
||||
func redirect80() {
|
||||
err := http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
url := fmt.Sprintf("https://%s/", r.Host)
|
||||
http.Redirect(w, r, url, http.StatusMovedPermanently)
|
||||
}))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
67
middleware.go
Normal file
67
middleware.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
|
||||
type ResponseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
StatusCode int
|
||||
BytesSent int
|
||||
}
|
||||
|
||||
func (rw *ResponseWriterWrapper) WriteHeader(code int) {
|
||||
rw.StatusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (rw *ResponseWriterWrapper) Write(data []byte) (int, error) {
|
||||
bytesWritten, err := rw.ResponseWriter.Write(data)
|
||||
rw.BytesSent += bytesWritten
|
||||
return bytesWritten, err
|
||||
}
|
||||
|
||||
func logRequestMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wrapper := &ResponseWriterWrapper{ResponseWriter: w, StatusCode: http.StatusOK}
|
||||
timer := time.Now()
|
||||
next.ServeHTTP(wrapper, r)
|
||||
fmt.Printf("%d - %5dµs %47s %4s %42s %6d %11s - %s - %s\n",
|
||||
wrapper.StatusCode, time.Since(timer).Microseconds(), r.RemoteAddr,
|
||||
r.Method, r.URL.Path, wrapper.BytesSent,
|
||||
wrapper.Header().Get("X-PrivTracker"), r.Referer(), r.UserAgent())
|
||||
})
|
||||
}
|
||||
|
||||
func headersMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000") // hsts
|
||||
w.Header().Set("Server", "PrivTracker")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func recoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Printf("Recovered from panic: %v\n%s\n", err, debug.Stack())
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func chainMiddleware(handler http.Handler, middlewares ...Middleware) http.Handler {
|
||||
// Apply middlewares in reverse order (outermost to innermost)
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
handler = middlewares[i](handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
26
scrape.go
26
scrape.go
@@ -2,15 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/jackpal/bencode-go"
|
||||
)
|
||||
|
||||
type ScrapeRequest struct {
|
||||
InfoHash string `query:"info_hash"`
|
||||
}
|
||||
|
||||
type ScrapeResponse struct {
|
||||
Files map[string]Stat `bencode:"files"`
|
||||
}
|
||||
@@ -21,21 +18,22 @@ type Stat struct {
|
||||
// Downloaded uint `bencode:"downloaded"`
|
||||
}
|
||||
|
||||
func scrape(c *fiber.Ctx) error {
|
||||
var req ScrapeRequest
|
||||
err := c.QueryParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
swarmHash := sha1.Sum([]byte(c.Params("room") + req.InfoHash))
|
||||
func scrape(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
infoHash := query.Get("info_hash")
|
||||
swarmHash := sha1.Sum([]byte(r.PathValue("room") + infoHash))
|
||||
|
||||
numSeeders, numLeechers := GetStats(swarmHash)
|
||||
resp := ScrapeResponse{
|
||||
Files: map[string]Stat{
|
||||
req.InfoHash: {
|
||||
infoHash: {
|
||||
Complete: numSeeders,
|
||||
Incomplete: numLeechers,
|
||||
},
|
||||
},
|
||||
}
|
||||
return bencode.Marshal(c, resp)
|
||||
w.Header().Add("X-PrivTracker", fmt.Sprintf("s:%d l:%d", numSeeders, numLeechers))
|
||||
if err := bencode.Marshal(w, resp); err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
}
|
||||
}
|
||||
|
||||
78
storage.go
78
storage.go
@@ -5,9 +5,7 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -15,11 +13,11 @@ import (
|
||||
type Hash [sha1.Size]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
|
||||
|
||||
var shards = NewShards(256)
|
||||
var v4InV6Prefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
|
||||
var shards [256]*shard
|
||||
|
||||
type shard struct {
|
||||
swarms map[Hash]Swarm
|
||||
swarms map[Hash]*Swarm
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -28,8 +26,17 @@ type Swarm struct {
|
||||
leechers map[Peer]int64
|
||||
}
|
||||
|
||||
func NewSwarm() Swarm {
|
||||
return Swarm{
|
||||
func init() {
|
||||
for i := range shards {
|
||||
shards[i] = &shard{
|
||||
swarms: make(map[Hash]*Swarm),
|
||||
}
|
||||
}
|
||||
go Cleanup(time.Minute * 16) // needs to be bigget than biggest interval in announce.go
|
||||
}
|
||||
|
||||
func NewSwarm() *Swarm {
|
||||
return &Swarm{
|
||||
seeders: make(map[Peer]int64),
|
||||
leechers: make(map[Peer]int64),
|
||||
}
|
||||
@@ -47,23 +54,15 @@ func (peer Peer) String() string {
|
||||
return fmt.Sprintf("%s:%d", ip, port)
|
||||
}
|
||||
|
||||
func shardIndex(hash Hash) int {
|
||||
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),
|
||||
}
|
||||
}
|
||||
return shards
|
||||
func shardIndex(hash Hash) uint8 {
|
||||
// return int(binary.BigEndian.Uint16(hash[:2])) % len(shards)
|
||||
return hash[0] // works only with 256 shards
|
||||
}
|
||||
|
||||
func PutPeer(h Hash, peer Peer, seeding bool) {
|
||||
shard := shards[shardIndex(h)]
|
||||
shard.Lock()
|
||||
defer shard.Unlock()
|
||||
if _, ok := shard.swarms[h]; !ok {
|
||||
shard.swarms[h] = NewSwarm()
|
||||
}
|
||||
@@ -72,34 +71,40 @@ func PutPeer(h Hash, peer Peer, seeding bool) {
|
||||
} else {
|
||||
shard.swarms[h].leechers[peer] = time.Now().Unix()
|
||||
}
|
||||
shard.Unlock()
|
||||
}
|
||||
|
||||
func DeletePeer(h Hash, peer Peer) {
|
||||
shard := shards[shardIndex(h)]
|
||||
shard.Lock()
|
||||
defer shard.Unlock()
|
||||
if _, ok := shard.swarms[h]; !ok {
|
||||
return
|
||||
}
|
||||
delete(shard.swarms[h].seeders, peer)
|
||||
delete(shard.swarms[h].leechers, peer)
|
||||
shard.Unlock()
|
||||
}
|
||||
|
||||
func GraduateLeecher(h Hash, peer Peer) {
|
||||
shard := shards[shardIndex(h)]
|
||||
shard.Lock()
|
||||
defer shard.Unlock()
|
||||
if _, ok := shard.swarms[h]; !ok {
|
||||
shard.swarms[h] = NewSwarm()
|
||||
}
|
||||
shard.swarms[h].seeders[peer] = time.Now().Unix()
|
||||
delete(shard.swarms[h].leechers, peer)
|
||||
shard.Unlock()
|
||||
}
|
||||
|
||||
func GetPeers(h Hash, client Peer, seeding bool, numWant uint) (peersIPv4, peersIPv6 []byte, numSeeders, numLeechers int) {
|
||||
func GetPeers(h Hash, client Peer, seeding bool, numWant int) (peersIPv4, peersIPv6 []byte, numSeeders, numLeechers int) {
|
||||
shard := shards[shardIndex(h)]
|
||||
shard.RLock()
|
||||
defer shard.RUnlock()
|
||||
if _, ok := shard.swarms[h]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
numSeeders = len(shard.swarms[h].seeders)
|
||||
numLeechers = len(shard.swarms[h].leechers)
|
||||
|
||||
// seeders don't need other seeders
|
||||
if !seeding {
|
||||
@@ -107,6 +112,9 @@ func GetPeers(h Hash, client Peer, seeding bool, numWant uint) (peersIPv4, peers
|
||||
if numWant == 0 {
|
||||
break
|
||||
}
|
||||
if peer == client {
|
||||
continue
|
||||
}
|
||||
if bytes.HasPrefix(peer[:], v4InV6Prefix) {
|
||||
peersIPv4 = append(peersIPv4, peer[len(v4InV6Prefix):]...)
|
||||
} else {
|
||||
@@ -116,66 +124,56 @@ func GetPeers(h Hash, client Peer, seeding bool, numWant uint) (peersIPv4, peers
|
||||
}
|
||||
}
|
||||
for peer := range shard.swarms[h].leechers {
|
||||
if peer == client {
|
||||
continue
|
||||
}
|
||||
if numWant == 0 {
|
||||
break
|
||||
}
|
||||
if peer == client {
|
||||
continue
|
||||
}
|
||||
if bytes.HasPrefix(peer[:], v4InV6Prefix) {
|
||||
peersIPv4 = append(peersIPv4, peer[12:]...)
|
||||
peersIPv4 = append(peersIPv4, peer[len(v4InV6Prefix):]...)
|
||||
} else {
|
||||
peersIPv6 = append(peersIPv6, peer[:]...)
|
||||
}
|
||||
numWant--
|
||||
}
|
||||
numSeeders = len(shard.swarms[h].seeders)
|
||||
numLeechers = len(shard.swarms[h].leechers)
|
||||
shard.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func GetStats(h Hash) (numSeeders, numLeechers int) {
|
||||
shard := shards[shardIndex(h)]
|
||||
shard.RLock()
|
||||
defer shard.RUnlock()
|
||||
if _, ok := shard.swarms[h]; !ok {
|
||||
return
|
||||
}
|
||||
numSeeders = len(shard.swarms[h].seeders)
|
||||
numLeechers = len(shard.swarms[h].leechers)
|
||||
shard.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func Cleanup(duration time.Duration) {
|
||||
ticker := time.NewTicker(duration)
|
||||
for range ticker.C {
|
||||
var seeders, leechers, swarms, seedersDeleted, leechersDeleted, swarmsDeleted int
|
||||
expiration := time.Now().Unix() - int64(duration.Seconds())
|
||||
for _, shard := range shards {
|
||||
shard.Lock()
|
||||
swarms += len(shard.swarms)
|
||||
for h, swarm := range shard.swarms {
|
||||
seeders += len(swarm.seeders)
|
||||
leechers += len(swarm.leechers)
|
||||
for peer, lastSeen := range swarm.seeders {
|
||||
if lastSeen < expiration {
|
||||
seedersDeleted++
|
||||
delete(swarm.seeders, peer)
|
||||
}
|
||||
}
|
||||
for peer, lastSeen := range swarm.leechers {
|
||||
if lastSeen < expiration {
|
||||
leechersDeleted++
|
||||
delete(swarm.leechers, peer)
|
||||
}
|
||||
}
|
||||
if len(swarm.leechers) == 0 && len(swarm.seeders) == 0 {
|
||||
swarmsDeleted++
|
||||
delete(shard.swarms, h)
|
||||
}
|
||||
}
|
||||
shard.Unlock()
|
||||
}
|
||||
log.Printf("seeders: %d (%d deleted), leechers: %d (%d deleted), swarms: %d (%d deleted)",
|
||||
seeders, seedersDeleted, leechers, leechersDeleted, swarms, swarmsDeleted)
|
||||
runtime.GC()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user