mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 05:04:35 +03:00
feat: 🎉 first version
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sartoopjj/thefeed/internal/client"
|
||||
"github.com/sartoopjj/thefeed/internal/protocol"
|
||||
"github.com/sartoopjj/thefeed/internal/tui"
|
||||
"github.com/sartoopjj/thefeed/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
domain := flag.String("domain", "", "DNS domain (e.g., t.example.com)")
|
||||
key := flag.String("key", "", "Encryption passphrase")
|
||||
resolvers := flag.String("resolvers", "", "Comma-separated resolver IPs or path to resolvers file")
|
||||
scanPath := flag.String("scan", "", "File with IPs/CIDRs to scan for resolvers, or a single CIDR (e.g., 8.8.8.0/24)")
|
||||
cacheDir := flag.String("cache", "", "Cache directory (default: ~/.thefeed/cache)")
|
||||
scanWorkers := flag.Int("scan-workers", 50, "Concurrent scanner workers")
|
||||
rateLimit := flag.Float64("rate", 0, "Max DNS queries per second (0 = unlimited)")
|
||||
queryMode := flag.String("query-mode", "single", "DNS query encoding: single (base32) or double (hex)")
|
||||
showVersion := flag.Bool("version", false, "Show version and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Printf("thefeed-client %s (commit: %s, built: %s)\n", version.Version, version.Commit, version.Date)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *domain == "" {
|
||||
*domain = os.Getenv("THEFEED_DOMAIN")
|
||||
}
|
||||
if *key == "" {
|
||||
*key = os.Getenv("THEFEED_KEY")
|
||||
}
|
||||
|
||||
if *domain == "" || *key == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --domain and --key are required")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *cacheDir == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatalf("Get home dir: %v", err)
|
||||
}
|
||||
*cacheDir = filepath.Join(home, ".thefeed", "cache")
|
||||
}
|
||||
|
||||
cache, err := client.NewCache(*cacheDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Create cache: %v", err)
|
||||
}
|
||||
|
||||
resolverList := parseResolvers(*resolvers)
|
||||
|
||||
fetcher, err := client.NewFetcher(*domain, *key, resolverList)
|
||||
if err != nil {
|
||||
log.Fatalf("Create fetcher: %v", err)
|
||||
}
|
||||
|
||||
// Set query encoding mode
|
||||
if *queryMode == "double" {
|
||||
fetcher.SetQueryMode(protocol.QueryDoubleLabel)
|
||||
}
|
||||
|
||||
// Set rate limit
|
||||
if *rateLimit > 0 {
|
||||
fetcher.SetRateLimit(*rateLimit)
|
||||
fmt.Printf("Rate limit: %.1f queries/sec\n", *rateLimit)
|
||||
}
|
||||
|
||||
// Scan for resolvers (supports file with IPs/CIDRs or a single CIDR)
|
||||
if *scanPath != "" {
|
||||
var mu sync.Mutex
|
||||
var found []string
|
||||
|
||||
scanner := client.NewResolverScanner(fetcher, *scanWorkers)
|
||||
|
||||
// Check if it's a file
|
||||
if _, statErr := os.Stat(*scanPath); statErr == nil {
|
||||
fmt.Printf("Scanning resolvers from file %s...\n", *scanPath)
|
||||
err := scanner.ScanFile(*scanPath, func(ip string) {
|
||||
mu.Lock()
|
||||
found = append(found, ip)
|
||||
mu.Unlock()
|
||||
fmt.Printf(" Found: %s\n", ip)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Scan warning: %v\n", err)
|
||||
}
|
||||
} else if strings.Contains(*scanPath, "/") {
|
||||
// Treat as CIDR
|
||||
fmt.Printf("Scanning %s for DNS resolvers...\n", *scanPath)
|
||||
err := scanner.ScanCIDR(*scanPath, func(ip string) {
|
||||
mu.Lock()
|
||||
found = append(found, ip)
|
||||
mu.Unlock()
|
||||
fmt.Printf(" Found: %s\n", ip)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Scan warning: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error: --scan value %q is not a file or CIDR\n", *scanPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(found) > 0 {
|
||||
all := append(resolverList, found...)
|
||||
fetcher.SetResolvers(all)
|
||||
fmt.Printf("Using %d resolvers\n", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
if len(fetcher.Resolvers()) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Error: no resolvers available. Use --resolvers or --scan")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := tui.Run(fetcher, cache); err != nil {
|
||||
log.Fatalf("TUI error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseResolvers(input string) []string {
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(input); err == nil {
|
||||
resolvers, err := client.LoadResolversFile(input)
|
||||
if err != nil {
|
||||
log.Printf("Warning: could not load resolvers file %s: %v", input, err)
|
||||
} else {
|
||||
return resolvers
|
||||
}
|
||||
}
|
||||
|
||||
var resolvers []string
|
||||
for _, r := range strings.Split(input, ",") {
|
||||
r = strings.TrimSpace(r)
|
||||
if r != "" {
|
||||
resolvers = append(resolvers, r)
|
||||
}
|
||||
}
|
||||
return resolvers
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/sartoopjj/thefeed/internal/server"
|
||||
"github.com/sartoopjj/thefeed/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
listen := flag.String("listen", ":5300", "DNS listen address (host:port)")
|
||||
domain := flag.String("domain", "", "DNS domain (e.g., t.example.com)")
|
||||
key := flag.String("key", "", "Encryption passphrase")
|
||||
channelsFile := flag.String("channels", "channels.txt", "Path to channels file")
|
||||
apiID := flag.String("api-id", "", "Telegram API ID")
|
||||
apiHash := flag.String("api-hash", "", "Telegram API Hash")
|
||||
phone := flag.String("phone", "", "Telegram phone number")
|
||||
loginOnly := flag.Bool("login-only", false, "Authenticate to Telegram, save session, and exit")
|
||||
sessionPath := flag.String("session", "session.json", "Path to Telegram session file")
|
||||
maxPadding := flag.Int("padding", 32, "Max random padding bytes in DNS responses (anti-DPI, 0=disabled)")
|
||||
showVersion := flag.Bool("version", false, "Show version and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Printf("thefeed-server %s (commit: %s, built: %s)\n", version.Version, version.Commit, version.Date)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *domain == "" {
|
||||
*domain = os.Getenv("THEFEED_DOMAIN")
|
||||
}
|
||||
if *key == "" {
|
||||
*key = os.Getenv("THEFEED_KEY")
|
||||
}
|
||||
if *apiID == "" {
|
||||
*apiID = os.Getenv("TELEGRAM_API_ID")
|
||||
}
|
||||
if *apiHash == "" {
|
||||
*apiHash = os.Getenv("TELEGRAM_API_HASH")
|
||||
}
|
||||
if *phone == "" {
|
||||
*phone = os.Getenv("TELEGRAM_PHONE")
|
||||
}
|
||||
|
||||
if *domain == "" || *key == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --domain and --key are required")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
if *apiID == "" || *apiHash == "" || *phone == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --api-id, --api-hash, and --phone are required")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(*apiID)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid API ID: %v", err)
|
||||
}
|
||||
|
||||
// Interactive 2FA password prompt — only when --login-only or no existing session
|
||||
password := os.Getenv("TELEGRAM_PASSWORD")
|
||||
if password == "" {
|
||||
hasSession := false
|
||||
if info, statErr := os.Stat(*sessionPath); statErr == nil && info.Size() > 0 {
|
||||
hasSession = true
|
||||
}
|
||||
if *loginOnly || !hasSession {
|
||||
fmt.Print("Telegram 2FA password (press Enter if none): ")
|
||||
pwBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println()
|
||||
if err == nil && len(pwBytes) > 0 {
|
||||
password = string(pwBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg := server.Config{
|
||||
ListenAddr: *listen,
|
||||
Domain: *domain,
|
||||
Passphrase: *key,
|
||||
ChannelsFile: *channelsFile,
|
||||
MaxPadding: *maxPadding,
|
||||
Telegram: server.TelegramConfig{
|
||||
APIID: id,
|
||||
APIHash: *apiHash,
|
||||
Phone: *phone,
|
||||
Password: password,
|
||||
SessionPath: *sessionPath,
|
||||
LoginOnly: *loginOnly,
|
||||
CodePrompt: func(ctx context.Context) (string, error) {
|
||||
fmt.Print("Enter Telegram auth code: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
code, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(code), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
srv, err := server.New(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Create server: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
log.Printf("Starting thefeed server %s on %s for domain %s", version.Version, cfg.ListenAddr, cfg.Domain)
|
||||
if err := srv.Run(ctx); err != nil && ctx.Err() == nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
log.Println("Server stopped")
|
||||
}
|
||||
Reference in New Issue
Block a user