Files
torrserver/server/cmd/main.go
T
nkozobrod 616c6b1c62
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled
Initial commit: docker compose config
2026-05-30 12:07:11 +00:00

494 lines
13 KiB
Go

package main
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"server/torr/utils"
"github.com/alexflint/go-arg"
"github.com/pkg/browser"
"server"
"server/docs"
"server/log"
"server/settings"
"server/torr"
"server/version"
)
type args struct {
Port string `arg:"-p" help:"web server port (default 8090)"`
IP string `arg:"-i" help:"web server addr (default empty)"`
Ssl bool `help:"enables https"`
SslPort string `help:"web server ssl port, If not set, will be set to default 8091 or taken from db(if stored previously). Accepted if --ssl enabled."`
SslCert string `help:"path to ssl cert file. If not set, will be taken from db(if stored previously) or default self-signed certificate/key will be generated. Accepted if --ssl enabled."`
SslKey string `help:"path to ssl key file. If not set, will be taken from db(if stored previously) or default self-signed certificate/key will be generated. Accepted if --ssl enabled."`
Path string `arg:"-d" help:"database and config dir path"`
LogPath string `arg:"-l" help:"server log file path"`
WebLogPath string `arg:"-w" help:"web access log file path"`
RDB bool `arg:"-r" help:"start in read-only DB mode"`
HttpAuth bool `arg:"-a" help:"enable http auth on all requests"`
DontKill bool `arg:"-k" help:"don't kill server on signal"`
UI bool `arg:"-u" help:"open torrserver page in browser"`
TorrentsDir string `arg:"-t" help:"autoload torrents from dir"`
TorrentAddr string `help:"Torrent client address, like 127.0.0.1:1337 (default :PeersListenPort)"`
PubIPv4 string `arg:"-4" help:"set public IPv4 addr"`
PubIPv6 string `arg:"-6" help:"set public IPv6 addr"`
SearchWA bool `arg:"-s" help:"search without auth"`
MaxSize string `arg:"-m" help:"max allowed stream size (in Bytes)"`
TGToken string `arg:"-T" help:"telegram bot token"`
FusePath string `arg:"-f" help:"fuse mount path"`
WebDAV bool `help:"web dav enable"`
ProxyURL string `help:"proxy URL for BitTorrent traffic (http, socks4, socks5, socks5h), e.g. socks5://user:password@127.0.0.1:8080"`
ProxyMode string `help:"proxy mode: tracker (only HTTP trackers, default), peers (only peer connections), or full (all traffic)"`
ForceHTTPS bool `arg:"--force-https" help:"redirect all HTTP requests to HTTPS (requires --ssl)"`
}
func (args) Version() string {
return "TorrServer " + version.Version
}
var params args
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
arg.MustParse(&params)
if params.Path == "" {
params.Path, _ = os.Getwd()
}
if params.Port == "" {
params.Port = "8090"
}
settings.Path = params.Path
settings.HttpAuth = params.HttpAuth
log.Init(params.LogPath, params.WebLogPath)
fmt.Println("=========== START ===========")
fmt.Println("TorrServer", version.Version+",", runtime.Version()+",", "CPU Num:", runtime.NumCPU())
if params.HttpAuth {
log.TLogln("Use HTTP Auth file", settings.Path+"/accs.db")
}
if params.RDB {
log.TLogln("Running in Read-only DB mode!")
}
docs.SwaggerInfo.Version = version.Version
// Simple Usage:
dnsResolve()
// Advanced Usage:
// config := DNSConfig{
// PrimaryServers: []string{"1.1.1.1:53", "8.8.8.8:53"},
// Timeout: 3 * time.Second,
// }
// checker := NewDNSChecker(config)
// // Perform DNS lookup with automatic fallback
// addrs, err := checker.LookupHostWithFallback("themoviedb.org")
// if err != nil {
// log.TLogln("DNS lookup failed:", err)
// } else {
// fmt.Println("DNS resolved:", addrs)
// }
Preconfig(params.DontKill)
if params.UI {
go func() {
time.Sleep(time.Second)
if params.Ssl {
browser.OpenURL("https://127.0.0.1:" + params.SslPort)
} else {
browser.OpenURL("http://127.0.0.1:" + params.Port)
}
}()
}
if params.TorrentAddr != "" {
settings.TorAddr = params.TorrentAddr
}
if params.PubIPv4 != "" {
settings.PubIPv4 = params.PubIPv4
}
if params.PubIPv6 != "" {
settings.PubIPv6 = params.PubIPv6
}
if params.TorrentsDir != "" {
go watchTDir(params.TorrentsDir)
}
if params.MaxSize != "" {
maxSize, err := strconv.ParseInt(params.MaxSize, 10, 64)
if err == nil {
settings.MaxSize = maxSize
}
}
if params.ProxyURL != "" && params.ProxyMode == "" {
params.ProxyMode = "tracker" // default
}
if params.ProxyMode != "" && params.ProxyMode != "tracker" && params.ProxyMode != "peers" && params.ProxyMode != "full" {
log.TLogln("Invalid proxy mode, using default 'tracker'")
params.ProxyMode = "tracker"
}
settings.Args = &settings.ExecArgs{
Port: params.Port,
IP: params.IP,
Ssl: params.Ssl,
SslPort: params.SslPort,
SslCert: params.SslCert,
SslKey: params.SslKey,
Path: params.Path,
LogPath: params.LogPath,
WebLogPath: params.WebLogPath,
RDB: params.RDB,
HttpAuth: params.HttpAuth,
DontKill: params.DontKill,
UI: params.UI,
TorrentsDir: params.TorrentsDir,
TorrentAddr: params.TorrentAddr,
PubIPv4: params.PubIPv4,
PubIPv6: params.PubIPv6,
SearchWA: params.SearchWA,
MaxSize: params.MaxSize,
TGToken: params.TGToken,
FusePath: params.FusePath,
WebDAV: params.WebDAV,
ProxyURL: params.ProxyURL,
ProxyMode: params.ProxyMode,
ForceHTTPS: params.ForceHTTPS,
}
if params.ProxyURL != "" {
log.TLogln("Proxy configured from CLI:", params.ProxyURL, "mode:", settings.Args.ProxyMode)
}
if params.ForceHTTPS && !params.Ssl {
log.TLogln("Error: --force-https requires --ssl")
os.Exit(1)
}
server.Start()
log.TLogln(server.WaitServer())
log.Close()
time.Sleep(time.Second * 3)
os.Exit(0)
}
func watchTDir(dir string) {
time.Sleep(5 * time.Second)
path, err := filepath.Abs(dir)
if err != nil {
path = dir
}
for {
files, err := os.ReadDir(path)
if err == nil {
for _, file := range files {
filename := filepath.Join(path, file.Name())
if strings.ToLower(filepath.Ext(file.Name())) == ".torrent" {
sp, err := utils.OpenTorrentFile(filename)
if err == nil {
tor, err := torr.AddTorrent(sp, "", "", "", "")
if err == nil {
if tor.GotInfo() {
if tor.Title == "" {
tor.Title = tor.Name()
}
torr.SaveTorrentToDB(tor)
tor.Drop()
os.Remove(filename)
time.Sleep(time.Second)
} else {
log.TLogln("Error get info from torrent")
}
} else {
log.TLogln("Error parse torrent file:", err)
}
} else {
log.TLogln("Error parse file name:", err)
}
}
}
} else {
log.TLogln("Error read dir:", err)
}
time.Sleep(time.Second * 5)
}
}
///////////
/// DNS
///
// DNSConfig holds DNS resolver configuration
type DNSConfig struct {
PrimaryServers []string
FallbackServers []string
Timeout time.Duration
CacheDuration time.Duration
}
// DefaultDNSConfig returns a sensible default configuration
func DefaultDNSConfig() DNSConfig {
return DNSConfig{
PrimaryServers: []string{
"8.8.8.8:53", // Google DNS
"1.1.1.1:53", // CloudFlare DNS
"9.9.9.9:53", // Quad9 DNS
},
FallbackServers: []string{
"208.67.222.222:53", // OpenDNS
"64.6.64.6:53", // Verisign
},
Timeout: 5 * time.Second,
CacheDuration: 5 * time.Minute,
}
}
// DNSChecker manages DNS resolution with fallback support
type DNSChecker struct {
config DNSConfig
customResolver *net.Resolver
cache map[string][]string
cacheTime map[string]time.Time
mu sync.RWMutex
useFallback bool
}
// NewDNSChecker creates a new DNS checker instance
func NewDNSChecker(config DNSConfig) *DNSChecker {
if len(config.PrimaryServers) == 0 {
config = DefaultDNSConfig()
}
return &DNSChecker{
config: config,
cache: make(map[string][]string),
cacheTime: make(map[string]time.Time),
}
}
// CheckAndResolve performs DNS check and returns a resolver
func (d *DNSChecker) CheckAndResolve() *net.Resolver {
// Test system DNS first
if d.testSystemDNS() {
log.TLogln("System DNS check passed")
return net.DefaultResolver
}
log.TLogln("System DNS check failed, using custom resolver")
d.initCustomResolver()
return d.customResolver
}
// testSystemDNS checks if system DNS is working properly
func (d *DNSChecker) testSystemDNS() bool {
_, cancel := context.WithTimeout(context.Background(), d.config.Timeout)
defer cancel()
addrs, err := net.LookupHost("themoviedb.org")
if err != nil {
log.TLogln("DNS lookup error:", err)
return false
}
if len(addrs) == 0 {
log.TLogln("DNS lookup returned no addresses")
return false
}
// Check for suspicious addresses (DNS hijacking/pollution)
for _, addr := range addrs {
if isSuspiciousAddress(addr) {
log.TLogln("Suspicious DNS address detected:", addr)
return false
}
}
return true
}
// isSuspiciousAddress checks if an address indicates DNS issues
func isSuspiciousAddress(addr string) bool {
suspiciousPrefixes := []string{
"127.0.0.1", // Localhost
"0.0.0.0", // Invalid address
"::1", // IPv6 localhost
// "10.", // Private network
"192.168.", // Private network
"169.254.", // Link-local
// "172.16.", "172.17.", "172.18.", "172.19.",
// "172.20.", "172.21.", "172.22.", "172.23.",
// "172.24.", "172.25.", "172.26.", "172.27.",
// "172.28.", "172.29.", "172.30.", "172.31.", // Private network range
}
for _, prefix := range suspiciousPrefixes {
if strings.HasPrefix(addr, prefix) {
return true
}
}
return false
}
// initCustomResolver creates a custom resolver with fallback support
func (d *DNSChecker) initCustomResolver() {
d.customResolver = &net.Resolver{
PreferGo: true, // Use Go's DNS implementation
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: d.config.Timeout,
KeepAlive: 30 * time.Second,
}
// Try primary servers first
for _, dns := range d.config.PrimaryServers {
conn, err := dialer.DialContext(ctx, network, dns)
if err == nil {
return conn, nil
}
log.TLogln("Failed to connect to DNS server", dns, ":", err)
}
// Try fallback servers if primary fails
for _, dns := range d.config.FallbackServers {
conn, err := dialer.DialContext(ctx, network, dns)
if err == nil {
log.TLogln("Using fallback DNS server:", dns)
return conn, nil
}
log.TLogln("Failed to connect to fallback DNS", dns, ":", err)
}
return nil, fmt.Errorf("all DNS servers failed")
},
}
d.useFallback = true
}
// LookupHostWithFallback performs DNS lookup with automatic fallback
func (d *DNSChecker) LookupHostWithFallback(host string) ([]string, error) {
// Check cache first
if addrs, ok := d.getFromCache(host); ok {
return addrs, nil
}
// Use appropriate resolver
var resolver *net.Resolver
if d.useFallback && d.customResolver != nil {
resolver = d.customResolver
} else {
resolver = net.DefaultResolver
}
// Perform lookup with timeout
ctx, cancel := context.WithTimeout(context.Background(), d.config.Timeout)
defer cancel()
addrs, err := resolver.LookupHost(ctx, host)
if err != nil {
// If using system DNS fails, try custom resolver
if !d.useFallback && d.customResolver != nil {
log.TLogln("System DNS failed, trying custom resolver")
addrs, err = d.customResolver.LookupHost(ctx, host)
}
}
// Cache successful results
if err == nil && len(addrs) > 0 {
d.addToCache(host, addrs)
}
return addrs, err
}
// getFromCache retrieves DNS results from cache
func (d *DNSChecker) getFromCache(host string) ([]string, bool) {
d.mu.RLock()
defer d.mu.RUnlock()
if addrs, ok := d.cache[host]; ok {
if time.Since(d.cacheTime[host]) < d.config.CacheDuration {
return addrs, true
}
// Expired, remove from cache
delete(d.cache, host)
delete(d.cacheTime, host)
}
return nil, false
}
// addToCache adds DNS results to cache
func (d *DNSChecker) addToCache(host string, addrs []string) {
d.mu.Lock()
defer d.mu.Unlock()
d.cache[host] = addrs
d.cacheTime[host] = time.Now()
}
// Simple usage function (backward compatible)
func dnsResolve() {
checker := NewDNSChecker(DefaultDNSConfig())
resolver := checker.CheckAndResolve()
// Store the resolver for later use if needed
net.DefaultResolver = resolver // Optional: replace global resolver
// Test the resolver
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
addrs, err := resolver.LookupHost(ctx, "themoviedb.org")
if err != nil {
log.TLogln("DNS resolution test failed:", err)
} else {
log.TLogln("DNS resolution successful, addresses:", addrs)
}
}
// func dnsResolve() {
// addrs, err := net.LookupHost("themoviedb.org")
// if len(addrs) == 0 {
// log.TLogln("System DNS check failed", err)
// fn := func(ctx context.Context, network, address string) (net.Conn, error) {
// d := net.Dialer{}
// return d.DialContext(ctx, "udp", "1.1.1.1:53")
// }
// net.DefaultResolver = &net.Resolver{
// Dial: fn,
// }
// addrs, err = net.LookupHost("themoviedb.org")
// if err != nil {
// log.TLogln("Check CloudFlare DNS error:", err)
// } else {
// log.TLogln("Use CloudFlare DNS")
// }
// } else {
// log.TLogln("System DNS check passed")
// }
// }