├── internal ├── monitor │ ├── types.go │ └── monitor.go ├── logging │ └── logger.go ├── client │ ├── http.go │ └── stock.go ├── ratelimit │ └── tokenbucket.go └── headers │ └── generator.go ├── go.mod ├── .gitignore ├── LICENSE ├── cmd ├── tonitor │ └── main.go └── example.go ├── .github └── workflows │ └── release.yml ├── go.sum └── README.md /internal/monitor/types.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | type Result struct { 4 | Status string 5 | ProductID string 6 | Timestamp int64 7 | WorkerID int 8 | Latency float64 9 | } 10 | 11 | type StockMessage struct { 12 | Status string `json:"status"` 13 | ProductID string `json:"product_id"` 14 | Retailer string `json:"retailer"` 15 | LastCheck float64 `json:"last_check"` 16 | InStock bool `json:"in_stock"` 17 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yourneighborhoodchef/tonitor 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/bogdanfinn/fhttp v0.5.36 7 | github.com/bogdanfinn/tls-client v1.9.1 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.1.1 // indirect 12 | github.com/bogdanfinn/utls v1.6.5 // indirect 13 | github.com/cloudflare/circl v1.5.0 // indirect 14 | github.com/klauspost/compress v1.17.11 // indirect 15 | github.com/quic-go/quic-go v0.48.1 // indirect 16 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect 17 | golang.org/x/crypto v0.29.0 // indirect 18 | golang.org/x/net v0.31.0 // indirect 19 | golang.org/x/sys v0.27.0 // indirect 20 | golang.org/x/text v0.20.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /internal/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type logMessage struct { 9 | message string 10 | json bool 11 | } 12 | 13 | var logChan chan logMessage 14 | 15 | func StartLogger() { 16 | logChan = make(chan logMessage, 1000) 17 | 18 | go func() { 19 | for msg := range logChan { 20 | if msg.json { 21 | fmt.Println(msg.message) 22 | } else { 23 | fmt.Fprintln(os.Stderr, msg.message) 24 | } 25 | } 26 | }() 27 | } 28 | 29 | func Printf(format string, args ...interface{}) { 30 | select { 31 | case logChan <- logMessage{message: fmt.Sprintf(format, args...), json: false}: 32 | default: 33 | } 34 | } 35 | 36 | func JSON(jsonStr string) { 37 | select { 38 | case logChan <- logMessage{message: jsonStr, json: true}: 39 | default: 40 | } 41 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Go sum file (dependency checksums) 21 | # go.sum 22 | 23 | # Build output 24 | /bin/ 25 | /dist/ 26 | 27 | # IDE files 28 | .vscode/ 29 | .idea/ 30 | *.swp 31 | *.swo 32 | *~ 33 | 34 | # OS generated files 35 | .DS_Store 36 | .DS_Store? 37 | ._* 38 | .Spotlight-V100 39 | .Trashes 40 | ehthumbs.db 41 | Thumbs.db 42 | 43 | # Logs 44 | *.log 45 | 46 | # Environment variables 47 | .env 48 | .env.local 49 | .env.*.local 50 | 51 | # Temporary files 52 | tmp/ 53 | temp/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cmd/tonitor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/yourneighborhoodchef/tonitor/internal/client" 12 | "github.com/yourneighborhoodchef/tonitor/internal/headers" 13 | "github.com/yourneighborhoodchef/tonitor/internal/monitor" 14 | ) 15 | 16 | func main() { 17 | rand.Seed(time.Now().UnixNano()) 18 | 19 | for _, arg := range os.Args[1:] { 20 | if arg == "-h" || arg == "--help" { 21 | fmt.Println("Usage: tonitor [delay_ms] [proxy_list]") 22 | fmt.Println(" TCIN: Target product ID to monitor") 23 | fmt.Println(" delay_ms: Delay between checks in milliseconds (default: 30000)") 24 | fmt.Println(" proxy_list: Comma-separated list of proxy URLs") 25 | return 26 | } 27 | } 28 | 29 | if len(os.Args) < 2 { 30 | fmt.Println("Error: Missing TCIN parameter. Use -h for help.") 31 | return 32 | } 33 | 34 | tcin := os.Args[1] 35 | 36 | targetDelayMs := 30000 37 | if len(os.Args) > 2 { 38 | if d, err := strconv.Atoi(os.Args[2]); err == nil && d > 0 { 39 | targetDelayMs = d 40 | } 41 | } 42 | 43 | if len(os.Args) > 3 { 44 | var proxyList []string 45 | for _, p := range strings.Split(os.Args[3], ",") { 46 | p = strings.TrimSpace(p) 47 | if p != "" { 48 | proxyList = append(proxyList, p) 49 | } 50 | } 51 | if len(proxyList) > 0 { 52 | fmt.Printf("Using %d proxy(ies)\n", len(proxyList)) 53 | client.SetProxyList(proxyList) 54 | } 55 | } 56 | 57 | initialWorkers := 5 58 | 59 | headers.InitProfilePool(15000) 60 | 61 | monitor.MonitorProduct(tcin, time.Duration(targetDelayMs)*time.Millisecond, initialWorkers) 62 | } -------------------------------------------------------------------------------- /internal/client/http.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "sync/atomic" 7 | 8 | tls_client "github.com/bogdanfinn/tls-client" 9 | "github.com/bogdanfinn/tls-client/profiles" 10 | ) 11 | 12 | var ( 13 | ErrProxyBlocked = errors.New("proxy blocked") 14 | proxyList []string 15 | proxyListMutex sync.Mutex 16 | proxyCounter uint32 17 | ) 18 | 19 | type ProxiedClient struct { 20 | tls_client.HttpClient 21 | ProxyURL string 22 | } 23 | 24 | func SetProxyList(proxies []string) { 25 | proxyListMutex.Lock() 26 | defer proxyListMutex.Unlock() 27 | proxyList = proxies 28 | } 29 | 30 | func RemoveProxy(proxyURL string) int { 31 | proxyListMutex.Lock() 32 | defer proxyListMutex.Unlock() 33 | 34 | if proxyURL != "" { 35 | for i, p := range proxyList { 36 | if p == proxyURL { 37 | proxyList = append(proxyList[:i], proxyList[i+1:]...) 38 | break 39 | } 40 | } 41 | } 42 | return len(proxyList) 43 | } 44 | 45 | func CreateClient() (*ProxiedClient, error) { 46 | jar := tls_client.NewCookieJar() 47 | options := []tls_client.HttpClientOption{ 48 | tls_client.WithTimeoutSeconds(30), 49 | tls_client.WithClientProfile(profiles.Chrome_120), 50 | tls_client.WithNotFollowRedirects(), 51 | tls_client.WithCookieJar(jar), 52 | } 53 | 54 | var proxyURL string 55 | proxyListMutex.Lock() 56 | if len(proxyList) > 0 { 57 | idx := atomic.AddUint32(&proxyCounter, 1) 58 | proxyURL = proxyList[int(idx-1)%len(proxyList)] 59 | options = append(options, tls_client.WithProxyUrl(proxyURL)) 60 | } 61 | proxyListMutex.Unlock() 62 | 63 | client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return &ProxiedClient{HttpClient: client, ProxyURL: proxyURL}, nil 69 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | include: 17 | - goos: linux 18 | goarch: amd64 19 | suffix: linux-amd64 20 | - goos: linux 21 | goarch: arm64 22 | suffix: linux-arm64 23 | - goos: darwin 24 | goarch: amd64 25 | suffix: darwin-amd64 26 | - goos: darwin 27 | goarch: arm64 28 | suffix: darwin-arm64 29 | - goos: windows 30 | goarch: amd64 31 | suffix: windows-amd64.exe 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Set up Go 37 | uses: actions/setup-go@v4 38 | with: 39 | go-version: '1.24.2' 40 | 41 | - name: Build 42 | env: 43 | GOOS: ${{ matrix.goos }} 44 | GOARCH: ${{ matrix.goarch }} 45 | run: | 46 | go build -ldflags="-s -w" -o tonitor-${{ matrix.suffix }} ./cmd/tonitor 47 | 48 | - name: Upload artifacts 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: tonitor-${{ matrix.suffix }} 52 | path: tonitor-${{ matrix.suffix }} 53 | 54 | release: 55 | needs: build 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | 60 | - name: Download all artifacts 61 | uses: actions/download-artifact@v4 62 | 63 | - name: Create Release 64 | uses: softprops/action-gh-release@v1 65 | with: 66 | files: | 67 | tonitor-*/tonitor-* 68 | generate_release_notes: true 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /cmd/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/yourneighborhoodchef/tonitor/internal/client" 8 | "github.com/yourneighborhoodchef/tonitor/internal/headers" 9 | "github.com/yourneighborhoodchef/tonitor/internal/monitor" 10 | ) 11 | 12 | func main() { 13 | fmt.Println("Testing tonitor with TCIN: 50516598") 14 | 15 | // Configure proxy list (optional) 16 | // Uncomment and add your proxies in the format: "protocol://username:password@host:port" 17 | // or "protocol://host:port" for proxies without authentication 18 | proxies := []string{ 19 | // "http://proxy1.example.com:8080", 20 | // "http://user:pass@proxy2.example.com:3128", 21 | // "socks5://proxy3.example.com:1080", 22 | } 23 | 24 | // Set up proxy rotation if proxies are provided 25 | if len(proxies) > 0 { 26 | client.SetProxyList(proxies) 27 | fmt.Printf("Configured %d proxies for rotation\n", len(proxies)) 28 | } else { 29 | fmt.Println("Running without proxies (direct connection)") 30 | } 31 | 32 | // Initialize header profiles 33 | headers.InitProfilePool(15000) 34 | 35 | // Set up monitoring parameters 36 | tcin := "50516598" 37 | delayMs := 3500 // 3500ms between checks 38 | workers := 3 // Start with 3 workers 39 | 40 | fmt.Printf("Monitoring TCIN %s with %d second intervals using %d workers\n", 41 | tcin, delayMs/1000, workers) 42 | 43 | // Example: Use custom compression types 44 | // Uncomment one of the following lines to test different compression types: 45 | 46 | // Only zstd compression: 47 | //monitor.MonitorProduct(tcin, time.Duration(delayMs)*time.Millisecond, workers, "zstd") 48 | 49 | // Multiple compression types: 50 | //monitor.MonitorProduct(tcin, time.Duration(delayMs)*time.Millisecond, workers, "gzip, deflate, br, zstd") 51 | 52 | // Brotli only: 53 | // monitor.MonitorProduct(tcin, time.Duration(delayMs)*time.Millisecond, workers, "br") 54 | 55 | // Start monitoring with default compression types (random selection) 56 | monitor.MonitorProduct(tcin, time.Duration(delayMs)*time.Millisecond, workers) 57 | } 58 | -------------------------------------------------------------------------------- /internal/ratelimit/tokenbucket.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type TokenJar struct { 9 | refillIntervalMs int64 10 | tokensPerRefill int 11 | maxTokens int 12 | tokens int 13 | mu sync.Mutex 14 | tokensAvailable chan struct{} 15 | done chan struct{} 16 | } 17 | 18 | func NewTokenJar(targetRPS float64, burstLimit int) *TokenJar { 19 | refillIntervalMs := int64(1000 / targetRPS) 20 | if refillIntervalMs < 10 { 21 | refillIntervalMs = 10 22 | } 23 | 24 | tokensPerRefill := 1 25 | if targetRPS > 10 { 26 | tokensPerRefill = int(targetRPS / 5) 27 | refillIntervalMs = int64(float64(tokensPerRefill) * 1000 / float64(targetRPS)) 28 | } 29 | 30 | if burstLimit <= 0 { 31 | burstLimit = int(targetRPS * 2) 32 | } 33 | if burstLimit < tokensPerRefill { 34 | burstLimit = tokensPerRefill 35 | } 36 | 37 | jar := &TokenJar{ 38 | refillIntervalMs: refillIntervalMs, 39 | tokensPerRefill: tokensPerRefill, 40 | maxTokens: burstLimit, 41 | tokens: burstLimit / 2, 42 | tokensAvailable: make(chan struct{}, 1), 43 | done: make(chan struct{}), 44 | } 45 | 46 | go jar.refiller() 47 | 48 | return jar 49 | } 50 | 51 | func (tj *TokenJar) refiller() { 52 | ticker := time.NewTicker(time.Duration(tj.refillIntervalMs) * time.Millisecond) 53 | defer ticker.Stop() 54 | 55 | for { 56 | select { 57 | case <-ticker.C: 58 | tj.mu.Lock() 59 | prevTokens := tj.tokens 60 | tj.tokens += tj.tokensPerRefill 61 | if tj.tokens > tj.maxTokens { 62 | tj.tokens = tj.maxTokens 63 | } 64 | 65 | if prevTokens == 0 && tj.tokens > 0 { 66 | select { 67 | case tj.tokensAvailable <- struct{}{}: 68 | default: 69 | } 70 | } 71 | tj.mu.Unlock() 72 | 73 | case <-tj.done: 74 | return 75 | } 76 | } 77 | } 78 | 79 | func (tj *TokenJar) getToken() bool { 80 | tj.mu.Lock() 81 | if tj.tokens > 0 { 82 | tj.tokens-- 83 | tj.mu.Unlock() 84 | return true 85 | } 86 | tj.mu.Unlock() 87 | return false 88 | } 89 | 90 | func (tj *TokenJar) GetStats() (tokens, maxTokens, tokensPerRefill int, refillIntervalMs int64) { 91 | tj.mu.Lock() 92 | defer tj.mu.Unlock() 93 | return tj.tokens, tj.maxTokens, tj.tokensPerRefill, tj.refillIntervalMs 94 | } 95 | 96 | func (tj *TokenJar) WaitForToken() { 97 | if tj.getToken() { 98 | return 99 | } 100 | 101 | for { 102 | select { 103 | case <-tj.tokensAvailable: 104 | if tj.getToken() { 105 | return 106 | } 107 | case <-time.After(time.Duration(tj.refillIntervalMs) * time.Millisecond): 108 | if tj.getToken() { 109 | return 110 | } 111 | } 112 | } 113 | } 114 | 115 | func (tj *TokenJar) Stop() { 116 | close(tj.done) 117 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/bogdanfinn/fhttp v0.5.36 h1:t1sO/EkO4K40QD/Ti8f6t80leZIdh2AaeLfN7dMvjH8= 4 | github.com/bogdanfinn/fhttp v0.5.36/go.mod h1:BlcawVfXJ4uhk5yyNGOOY2bwo8UmMi6ccMszP1KGLkU= 5 | github.com/bogdanfinn/tls-client v1.9.1 h1:Br0WkKL+/7Q9FSNM1zBMdlYXW8bm+XXGMn9iyb9a/7Y= 6 | github.com/bogdanfinn/tls-client v1.9.1/go.mod h1:ehNITC7JBFeh6S7QNWtfD+PBKm0RsqvizAyyij2d/6g= 7 | github.com/bogdanfinn/utls v1.6.5 h1:rVMQvhyN3zodLxKFWMRLt19INGBCZ/OM2/vBWPNIt1w= 8 | github.com/bogdanfinn/utls v1.6.5/go.mod h1:czcHxHGsc1q9NjgWSeSinQZzn6MR76zUmGVIGanSXO0= 9 | github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= 10 | github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 14 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA= 18 | github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 19 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 20 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= 22 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= 23 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 24 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 25 | golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= 26 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 27 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 28 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 29 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 30 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 31 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 32 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 33 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 34 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /internal/client/stock.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "sync" 8 | "time" 9 | 10 | http "github.com/bogdanfinn/fhttp" 11 | "github.com/yourneighborhoodchef/tonitor/internal/headers" 12 | "github.com/yourneighborhoodchef/tonitor/internal/logging" 13 | ) 14 | 15 | type stockStatus struct { 16 | Data struct { 17 | Product struct { 18 | Fulfillment struct { 19 | ShippingOptions struct { 20 | AvailabilityStatus string `json:"availability_status"` 21 | } `json:"shipping_options"` 22 | SoldOut bool `json:"sold_out"` 23 | } `json:"fulfillment"` 24 | } `json:"product"` 25 | } `json:"data"` 26 | } 27 | 28 | var ( 29 | prevStatusCode int 30 | statusCodeMutex sync.Mutex 31 | ) 32 | 33 | func extractStockStatus(body []byte) (bool, error) { 34 | var s stockStatus 35 | if err := json.Unmarshal(body, &s); err != nil { 36 | sample := string(body) 37 | if len(sample) > 200 { 38 | sample = sample[:200] + "..." 39 | } 40 | logging.Printf("JSON parse error: %v\nSample response: %s", err, sample) 41 | return false, err 42 | } 43 | 44 | st := s.Data.Product.Fulfillment.ShippingOptions.AvailabilityStatus 45 | switch st { 46 | case "IN_STOCK", "LIMITED_STOCK", "PRE_ORDER_SELLABLE": 47 | return true, nil 48 | } 49 | return false, nil 50 | } 51 | 52 | func CheckStock(client *ProxiedClient, tcin string, compressionTypes ...string) (bool, error) { 53 | url := fmt.Sprintf( 54 | "https://redsky.target.com/redsky_aggregations/v1/web/product_fulfillment_v1"+ 55 | "?key=9f36aeafbe60771e321a7cc95a78140772ab3e96"+ 56 | "&is_bot=false&tcin=%s&store_id=1077&zip=27019&state=NC"+ 57 | "&channel=WEB&page=%%2Fp%%2FA-%s", tcin, tcin, 58 | ) 59 | 60 | req, err := http.NewRequest(http.MethodGet, url, nil) 61 | if err != nil { 62 | return false, err 63 | } 64 | 65 | req.Header = headers.BuildHeaders(tcin, compressionTypes...) 66 | 67 | resp, err := client.Do(req) 68 | if err != nil { 69 | return false, err 70 | } 71 | defer resp.Body.Close() 72 | 73 | body, err := io.ReadAll(resp.Body) 74 | if err != nil { 75 | return false, err 76 | } 77 | 78 | if resp.StatusCode == 404 { 79 | fmt.Printf("404 Not Found: Product %s may not exist (via proxy %s)\n", tcin, client.ProxyURL) 80 | statusCodeMutex.Lock() 81 | if prevStatusCode == 200 { 82 | fmt.Printf("Got 404 after previous 200 response, regenerating header profiles\n") 83 | headers.ResetProfilePool() 84 | go headers.InitProfilePool(50) 85 | } 86 | prevStatusCode = 404 87 | statusCodeMutex.Unlock() 88 | 89 | proxyTCIN := "10805587" 90 | fmt.Printf("Checking proxy with test TCIN: %s\n", proxyTCIN) 91 | url2 := fmt.Sprintf( 92 | "https://redsky.target.com/redsky_aggregations/v1/web/product_fulfillment_v1"+ 93 | "?key=9f36aeafbe60771e321a7cc95a78140772ab3e96"+ 94 | "&is_bot=false&tcin=%s&store_id=1077&zip=27019&state=NC"+ 95 | "&channel=WEB&page=%%2Fp%%2FA-%s", proxyTCIN, proxyTCIN, 96 | ) 97 | req2, err := http.NewRequest(http.MethodGet, url2, nil) 98 | if err != nil { 99 | return false, err 100 | } 101 | req2.Header = headers.BuildHeaders(proxyTCIN, compressionTypes...) 102 | resp2, err := client.Do(req2) 103 | if err != nil { 104 | return false, err 105 | } 106 | defer resp2.Body.Close() 107 | 108 | remaining := RemoveProxy(client.ProxyURL) 109 | if resp2.StatusCode == 404 { 110 | fmt.Printf("Proxy appears blocked: test TCIN %s also returned 404 (proxy %s)\n", proxyTCIN, client.ProxyURL) 111 | if remaining == 0 { 112 | time.Sleep(3500 * time.Millisecond) 113 | } 114 | return false, ErrProxyBlocked 115 | } 116 | if remaining == 0 { 117 | time.Sleep(3500 * time.Millisecond) 118 | } 119 | return false, nil 120 | } 121 | 122 | statusCodeMutex.Lock() 123 | defer statusCodeMutex.Unlock() 124 | 125 | if resp.StatusCode == 200 { 126 | prevStatusCode = 200 127 | } else { 128 | fmt.Printf("Unexpected status code: %d\n", resp.StatusCode) 129 | sample := string(body) 130 | if len(sample) > 200 { 131 | sample = sample[:200] + "..." 132 | } 133 | fmt.Printf("Response body: %s\n", sample) 134 | 135 | if resp.StatusCode == 404 { 136 | fmt.Printf("404 Not Found: Product %s may not exist\n", tcin) 137 | 138 | if prevStatusCode == 200 { 139 | fmt.Printf("Got 404 after previous 200 response, regenerating header profiles\n") 140 | headers.ResetProfilePool() 141 | go headers.InitProfilePool(50) 142 | } 143 | 144 | prevStatusCode = 404 145 | } 146 | } 147 | return extractStockStatus(body) 148 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tonitor - Beat Scalpers at Target.com 2 | 3 | Tonitor is a powerful monitoring tool designed to help Pokémon TCG collectors compete with scalpers when purchasing limited-edition and high-demand Pokémon cards from Target.com. 4 | 5 | ## Why Tonitor? 6 | 7 | Scalpers use bots and automation tools to purchase high-demand Pokémon products the moment they become available, making it nearly impossible for genuine collectors to get them at retail prices. Tonitor levels the playing field by: 8 | 9 | - **Real-time stock monitoring**: Continuously checks Target.com inventory for specific Pokémon products 10 | - **Anti-bot evasion**: Uses sophisticated browser fingerprinting techniques to avoid detection 11 | - **Proxy support**: Distributes requests across multiple IP addresses to prevent rate limiting 12 | - **Instant notifications**: Alerts you the moment your desired Pokémon products come in stock 13 | 14 | ## Features 15 | 16 | - Monitors Target.com inventory using product TCINs (Target's product IDs) 17 | - Automatically rotates between browser fingerprints to avoid bot detection 18 | - Supports proxy rotation to prevent IP bans and rate limits 19 | - Auto-adjusts request rates based on Target's server response 20 | - Outputs stock status as JSON for easy integration with notification systems 21 | 22 | ## Installation 23 | 24 | ### Prerequisites 25 | 26 | - Go 1.24.2 or later 27 | 28 | ### Build from Source 29 | 30 | ```bash 31 | git clone https://github.com/yourneighborhoodchef/tonitor.git 32 | cd tonitor 33 | go build -o tonitor ./cmd/tonitor 34 | ``` 35 | 36 | ### Using as a Go Package 37 | 38 | You can also integrate tonitor directly into your Go applications: 39 | 40 | ```bash 41 | go get github.com/yourneighborhoodchef/tonitor 42 | ``` 43 | 44 | #### Example Integration 45 | 46 | ```go 47 | package main 48 | 49 | import ( 50 | "fmt" 51 | "time" 52 | 53 | "github.com/yourneighborhoodchef/tonitor/internal/headers" 54 | "github.com/yourneighborhoodchef/tonitor/internal/monitor" 55 | ) 56 | 57 | func main() { 58 | // Initialize header profiles for anti-detection 59 | headers.InitProfilePool(15000) 60 | 61 | // Set up monitoring parameters 62 | tcin := "50516598" // Target product ID 63 | delayMs := 3500 // 3.5 seconds between checks 64 | workers := 3 // Number of concurrent workers 65 | 66 | fmt.Printf("Monitoring TCIN %s with %d second intervals using %d workers\n", 67 | tcin, delayMs/1000, workers) 68 | 69 | // Start monitoring - this will run indefinitely 70 | monitor.MonitorProduct(tcin, time.Duration(delayMs)*time.Millisecond, workers) 71 | } 72 | ``` 73 | 74 | ## Usage 75 | 76 | ```bash 77 | ./tonitor [delay_ms] [proxy_list] 78 | ``` 79 | 80 | - `TCIN`: Target's product ID for the Pokémon card product you want to monitor 81 | - `delay_ms`: Delay between checks in milliseconds (default: 30000) 82 | - `proxy_list`: Comma-separated list of proxy URLs (optional) 83 | 84 | ### Example 85 | 86 | ```bash 87 | # Monitor Pokémon TCG Elite Trainer Box with TCIN 83449367 88 | ./tonitor 83449367 15000 http://user:pass@proxy1.example.com,http://user:pass@proxy2.example.com 89 | ``` 90 | 91 | ### Help 92 | 93 | ```bash 94 | ./tonitor -h 95 | ``` 96 | 97 | ## Project Structure 98 | 99 | ``` 100 | tonitor/ 101 | ├── cmd/ 102 | │ └── tonitor/ # Main application entry point 103 | │ └── main.go 104 | ├── internal/ 105 | │ ├── client/ # HTTP client with anti-detection features 106 | │ │ ├── http.go 107 | │ │ └── stock.go 108 | │ ├── headers/ # Browser fingerprint generation 109 | │ │ └── generator.go 110 | │ ├── logging/ # Logging utilities 111 | │ │ └── logger.go 112 | │ ├── monitor/ # Core monitoring logic 113 | │ │ ├── monitor.go 114 | │ │ └── types.go 115 | │ └── ratelimit/ # Rate limiting implementation 116 | │ └── tokenbucket.go 117 | ├── go.mod 118 | ├── go.sum 119 | └── README.md 120 | ``` 121 | 122 | ## Tips for Success 123 | 124 | - Find the TCIN for Pokémon products by viewing the product page URL on Target.com (A-XXXXXXXX) 125 | - Use multiple proxies to avoid rate limiting 126 | - Set up notifications by piping the output to a Discord/Telegram bot 127 | - Be ready to purchase immediately when an in-stock notification appears 128 | 129 | ## Responsible Use 130 | 131 | Please use this tool responsibly. Tonitor is designed to help collectors compete with scalpers, not to deplete inventory unfairly. Only monitor products you genuinely intend to purchase. 132 | 133 | ## Legal Disclaimer 134 | 135 | This software is provided for educational purposes only. Users are responsible for complying with Target's terms of service and all applicable laws when using this software. -------------------------------------------------------------------------------- /internal/monitor/monitor.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "math/rand" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/yourneighborhoodchef/tonitor/internal/client" 13 | "github.com/yourneighborhoodchef/tonitor/internal/headers" 14 | "github.com/yourneighborhoodchef/tonitor/internal/ratelimit" 15 | ) 16 | 17 | func MonitorProduct(tcin string, targetDelayMs time.Duration, initialConcurrency int, compressionTypes ...string) { 18 | var prevStatus string 19 | resultChan := make(chan Result) 20 | 21 | var statusMutex sync.Mutex 22 | var totalRequests int64 23 | targetRPS := 1000.0 / float64(targetDelayMs.Milliseconds()) 24 | 25 | jar := ratelimit.NewTokenJar(targetRPS, initialConcurrency*2) 26 | defer jar.Stop() 27 | 28 | var requestsInWindow int64 29 | var requestWindowMutex sync.Mutex 30 | requestWindowStart := time.Now() 31 | 32 | workerControl := make(chan int) 33 | var activeWorkers int32 = 0 34 | 35 | go func() { 36 | numWorkers := initialConcurrency 37 | atomic.StoreInt32(&activeWorkers, int32(numWorkers)) 38 | 39 | for i := 0; i < numWorkers; i++ { 40 | workerControl <- i 41 | } 42 | 43 | ticker := time.NewTicker(10 * time.Second) 44 | defer ticker.Stop() 45 | 46 | for range ticker.C { 47 | requestWindowMutex.Lock() 48 | windowDuration := time.Since(requestWindowStart).Seconds() 49 | reqs := atomic.LoadInt64(&requestsInWindow) 50 | currentRPS := float64(reqs) / windowDuration 51 | 52 | atomic.StoreInt64(&requestsInWindow, 0) 53 | requestWindowStart = time.Now() 54 | requestWindowMutex.Unlock() 55 | 56 | if reqs == 0 { 57 | continue 58 | } 59 | 60 | currentWorkers := atomic.LoadInt32(&activeWorkers) 61 | 62 | var desiredWorkers int32 63 | 64 | if currentRPS < targetRPS*0.8 { 65 | desiredWorkers = int32(float64(currentWorkers) * 1.5) 66 | } else if currentRPS > targetRPS*1.2 { 67 | desiredWorkers = int32(float64(currentWorkers) * 0.8) 68 | } else { 69 | desiredWorkers = currentWorkers 70 | } 71 | 72 | if desiredWorkers < 1 { 73 | desiredWorkers = 1 74 | } else if desiredWorkers > 50 { 75 | desiredWorkers = 50 76 | } 77 | 78 | workerDiff := desiredWorkers - currentWorkers 79 | if workerDiff > 3 { 80 | workerDiff = 3 81 | } else if workerDiff < -3 { 82 | workerDiff = -3 83 | } 84 | 85 | newWorkerCount := currentWorkers + workerDiff 86 | 87 | if newWorkerCount > currentWorkers { 88 | for i := 0; i < int(newWorkerCount-currentWorkers); i++ { 89 | workerID := int(currentWorkers) + i 90 | workerControl <- workerID 91 | atomic.AddInt32(&activeWorkers, 1) 92 | } 93 | } else if newWorkerCount < currentWorkers { 94 | atomic.StoreInt32(&activeWorkers, newWorkerCount) 95 | } 96 | } 97 | }() 98 | 99 | workerFactory := func(workerID int) { 100 | httpClient, err := client.CreateClient() 101 | if err != nil { 102 | return 103 | } 104 | 105 | workerRequests := 0 106 | 107 | var totalDuration time.Duration 108 | var headerGenDuration time.Duration 109 | 110 | for { 111 | if int32(workerID) >= atomic.LoadInt32(&activeWorkers) { 112 | return 113 | } 114 | 115 | jar.WaitForToken() 116 | 117 | headerStart := time.Now() 118 | _ = headers.BuildHeaders(tcin, compressionTypes...) 119 | headerTime := time.Since(headerStart) 120 | headerGenDuration += headerTime 121 | 122 | requestStart := time.Now() 123 | ok, err := client.CheckStock(httpClient, tcin, compressionTypes...) 124 | requestDuration := time.Since(requestStart) 125 | totalDuration += requestDuration 126 | 127 | status := "out-of-stock" 128 | if err != nil { 129 | if errors.Is(err, client.ErrProxyBlocked) { 130 | status = "proxy-blocked" 131 | } else { 132 | status = "error" 133 | } 134 | } else if ok { 135 | status = "in-stock" 136 | } 137 | 138 | workerRequests++ 139 | atomic.AddInt64(&totalRequests, 1) 140 | atomic.AddInt64(&requestsInWindow, 1) 141 | 142 | resultChan <- Result{ 143 | Status: status, 144 | ProductID: tcin, 145 | Timestamp: time.Now().Unix(), 146 | WorkerID: workerID, 147 | Latency: requestDuration.Seconds(), 148 | } 149 | 150 | if errors.Is(err, client.ErrProxyBlocked) { 151 | newClient, cerr := client.CreateClient() 152 | if cerr != nil { 153 | return 154 | } 155 | httpClient = newClient 156 | } 157 | 158 | time.Sleep(time.Duration(10+rand.Intn(40)) * time.Millisecond) 159 | } 160 | } 161 | 162 | go func() { 163 | for workerID := range workerControl { 164 | go workerFactory(workerID) 165 | } 166 | }() 167 | 168 | heartbeatInterval := targetDelayMs 169 | lastPublishTime := time.Now().Add(-heartbeatInterval) 170 | 171 | initialStatusMsg := map[string]interface{}{ 172 | "status": "initializing", 173 | "product_id": tcin, 174 | "retailer": "target", 175 | "timestamp": time.Now().Unix(), 176 | "last_check": float64(time.Now().UnixNano()) / 1e9, 177 | "in_stock": false, 178 | "latency": 0.0, 179 | "worker_id": 0, 180 | } 181 | initialJSON, _ := json.Marshal(initialStatusMsg) 182 | fmt.Println(string(initialJSON)) 183 | 184 | for r := range resultChan { 185 | statusMutex.Lock() 186 | now := time.Now() 187 | if r.Status != prevStatus || r.Status == "error" || now.Sub(lastPublishTime) >= heartbeatInterval { 188 | resultObj := map[string]interface{}{ 189 | "status": r.Status, 190 | "product_id": r.ProductID, 191 | "timestamp": r.Timestamp, 192 | "retailer": "target", 193 | "last_check": float64(now.UnixNano()) / 1e9, 194 | "in_stock": r.Status == "in-stock", 195 | "worker_id": r.WorkerID, 196 | "latency": r.Latency, 197 | } 198 | 199 | resultJSON, err := json.Marshal(resultObj) 200 | if err != nil { 201 | fmt.Printf("Error serializing message: %v\n", err) 202 | } else { 203 | fmt.Println(string(resultJSON)) 204 | } 205 | 206 | lastPublishTime = now 207 | prevStatus = r.Status 208 | } 209 | statusMutex.Unlock() 210 | } 211 | } -------------------------------------------------------------------------------- /internal/headers/generator.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | 10 | http "github.com/bogdanfinn/fhttp" 11 | ) 12 | 13 | type viewport struct { 14 | Width int 15 | Height int 16 | PixelRatio float64 17 | } 18 | 19 | type clientHints struct { 20 | ConnectionType string 21 | EffectiveType string 22 | Downlink float64 23 | RTT int 24 | SaveData string 25 | } 26 | 27 | type Profile struct { 28 | ua string 29 | secCHUA string 30 | viewport viewport 31 | hints clientHints 32 | acceptIdx int 33 | langIdx int 34 | encIdx int 35 | cacheIdx int 36 | memIdx int 37 | viewportProb []float64 38 | hintsProb []float64 39 | memProb float64 40 | } 41 | 42 | var ( 43 | acceptOpts = []string{ 44 | "application/json", 45 | "application/json, text/plain, */*", 46 | "application/json, text/javascript, */*; q=0.01", 47 | } 48 | encOpts = []string{ 49 | "gzip, deflate, br", 50 | "gzip, deflate, br, zstd", 51 | "br, gzip, deflate", 52 | } 53 | langOpts = []string{ 54 | "en-US,en;q=0.9", 55 | "en-US,en;q=0.8", 56 | "en-GB,en;q=0.9,en-US;q=0.8", 57 | "en-CA,en;q=0.9,en-US;q=0.8", 58 | "en,en-US;q=0.9", 59 | "en-US,en;q=0.9,es;q=0.8", 60 | "en-US", 61 | } 62 | cacheOpts = []string{ 63 | "max-age=0", 64 | "no-cache", 65 | "max-age=0, private, must-revalidate", 66 | "", 67 | } 68 | memOpts = []string{"2", "4", "8"} 69 | 70 | headerOrder = []string{ 71 | "Accept", 72 | "Accept-Language", 73 | "Accept-Encoding", 74 | "User-Agent", 75 | "Sec-CH-UA", 76 | "Sec-CH-UA-Mobile", 77 | "Sec-CH-UA-Platform", 78 | "Sec-Fetch-Site", 79 | "Sec-Fetch-Mode", 80 | "Sec-Fetch-Dest", 81 | "Sec-CH-Viewport-Width", 82 | "Sec-CH-Viewport-Height", 83 | "Sec-CH-DPR", 84 | "Sec-CH-UA-Netinfo", 85 | "Sec-CH-UA-Downlink", 86 | "Sec-CH-UA-RTT", 87 | "Save-Data", 88 | "Device-Memory", 89 | "Connection", 90 | "Cache-Control", 91 | "Origin", 92 | "Referer", 93 | "Priority", 94 | } 95 | ) 96 | 97 | var profilePool = sync.Pool{ 98 | New: func() interface{} { 99 | return generateProfile() 100 | }, 101 | } 102 | 103 | func randomBuildToken(n int) string { 104 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 105 | b := make([]byte, n) 106 | for i := range b { 107 | b[i] = chars[rand.Intn(len(chars))] 108 | } 109 | return string(b) 110 | } 111 | 112 | func generateRandomViewport() viewport { 113 | w := rand.Intn(80) + 360 114 | h := rand.Intn(276) + 640 115 | dprChoices := []float64{2, 2.5, 3, 3.5} 116 | return viewport{Width: w, Height: h, PixelRatio: dprChoices[rand.Intn(len(dprChoices))]} 117 | } 118 | 119 | func generateClientHints() clientHints { 120 | connOpts := []string{"keep-alive", "close", ""} 121 | effOpts := []string{"slow-2g", "2g", "3g", "4g", ""} 122 | saveOpts := []string{"on", ""} 123 | return clientHints{ 124 | ConnectionType: connOpts[rand.Intn(len(connOpts))], 125 | EffectiveType: effOpts[rand.Intn(len(effOpts))], 126 | Downlink: rand.Float64()*9.9 + 0.1, 127 | RTT: rand.Intn(251) + 50, 128 | SaveData: saveOpts[rand.Intn(len(saveOpts))], 129 | } 130 | } 131 | 132 | func generateRandomUA() string { 133 | androidVer := rand.Intn(10) + 8 134 | architectures := []string{"arm64-v8a", "armeabi-v7a", "x86", "x86_64"} 135 | arch := architectures[rand.Intn(len(architectures))] 136 | 137 | switch rand.Intn(5) { 138 | case 0: // Android Chrome 139 | maj := rand.Intn(11) + 130 140 | min := rand.Intn(10) 141 | return fmt.Sprintf( 142 | "Mozilla/5.0 (Linux; Android %d; AndroidDeviceModel%02d; %s Build/%s) "+ 143 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.%d.0 Mobile Safari/537.36 XTR/%s", 144 | androidVer, rand.Intn(80)+1, arch, randomBuildToken(5), 145 | maj, min, randomBuildToken(5), 146 | ) 147 | case 1: // iOS Safari 148 | maj := rand.Intn(8) + 12 149 | min := rand.Intn(10) 150 | device := fmt.Sprintf("iPhone%02d", rand.Intn(80)+6) 151 | return fmt.Sprintf( 152 | "Mozilla/5.0 (%s; CPU %s OS %d_%d like Mac OS X) "+ 153 | "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%d.%d Mobile/%s Safari/604.1 XTR/%s", 154 | device, device, maj, min, maj, rand.Intn(2), randomBuildToken(5), randomBuildToken(5), 155 | ) 156 | case 2: // Samsung Internet 157 | sbMaj := rand.Intn(7) + 20 158 | sbMin := rand.Intn(10) 159 | return fmt.Sprintf( 160 | "Mozilla/5.0 (Linux; Android %d; SAMSUNG SM-G%03d; %s Build/%s) "+ 161 | "AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/%d.%d "+ 162 | "Chrome/%d.0.%d.0 Mobile Safari/537.36 XTR/%s", 163 | androidVer, rand.Intn(80)+900, arch, randomBuildToken(5), 164 | sbMaj, sbMin, rand.Intn(11)+130, rand.Intn(10), randomBuildToken(5), 165 | ) 166 | case 3: // Android Firefox 167 | rvMaj := rand.Intn(16) + 115 168 | rvMin := rand.Intn(10) 169 | return fmt.Sprintf( 170 | "Mozilla/5.0 (Android %d; %s; Mobile; rv:%d.%d) Gecko/%d.%d Firefox/%d.%d Build/%s XTR/%s", 171 | androidVer, arch, rvMaj, rvMin, rvMaj, rvMin, rvMaj, rvMin, randomBuildToken(5), randomBuildToken(5), 172 | ) 173 | default: // Edge Mobile 174 | edgeMaj := rand.Intn(11) + 120 175 | edgeMin := rand.Intn(10) 176 | return fmt.Sprintf( 177 | "Mozilla/5.0 (Linux; Android %d; EdgeDeviceModel%02d; %s Build/%s) "+ 178 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.%d.0 Mobile Safari/537.36 EdgA/%d.%d XTR/%s", 179 | androidVer, rand.Intn(80)+1, arch, randomBuildToken(5), 180 | rand.Intn(11)+130, rand.Intn(10), edgeMaj, edgeMin, randomBuildToken(5), 181 | ) 182 | } 183 | } 184 | 185 | func generateSecCHUA(ua string) string { 186 | const fallback = "136.0.0.0" 187 | ver := fallback 188 | if idx := strings.Index(ua, "Chrome/"); idx != -1 { 189 | rest := ua[idx+7:] 190 | if j := strings.Index(rest, " "); j != -1 { 191 | ver = rest[:j] 192 | } else { 193 | ver = rest 194 | } 195 | } 196 | return fmt.Sprintf( 197 | `"Not:A-Brand";v="24", "Chromium";v="%s", "Google Chrome";v="%s"`, 198 | ver, ver, 199 | ) 200 | } 201 | 202 | func generateProfile() Profile { 203 | ua := generateRandomUA() 204 | 205 | viewportProbs := make([]float64, 3) 206 | for i := range viewportProbs { 207 | viewportProbs[i] = rand.Float64() 208 | } 209 | 210 | hintsProbs := make([]float64, 5) 211 | for i := range hintsProbs { 212 | hintsProbs[i] = rand.Float64() 213 | } 214 | 215 | return Profile{ 216 | ua: ua, 217 | secCHUA: generateSecCHUA(ua), 218 | viewport: generateRandomViewport(), 219 | hints: generateClientHints(), 220 | acceptIdx: rand.Intn(len(acceptOpts)), 221 | langIdx: rand.Intn(len(langOpts)), 222 | encIdx: rand.Intn(len(encOpts)), 223 | cacheIdx: rand.Intn(len(cacheOpts)), 224 | memIdx: rand.Intn(len(memOpts)), 225 | viewportProb: viewportProbs, 226 | hintsProb: hintsProbs, 227 | memProb: rand.Float64(), 228 | } 229 | } 230 | 231 | func BuildHeaders(tcin string, compressionTypes ...string) http.Header { 232 | profile := profilePool.Get().(Profile) 233 | defer profilePool.Put(profile) 234 | 235 | h := http.Header{} 236 | h.Set("Accept", acceptOpts[profile.acceptIdx]) 237 | h.Set("Accept-Language", langOpts[profile.langIdx]) 238 | 239 | // Use custom compression types if provided, otherwise use default options 240 | if len(compressionTypes) > 0 { 241 | h.Set("Accept-Encoding", compressionTypes[0]) 242 | } else { 243 | h.Set("Accept-Encoding", encOpts[profile.encIdx]) 244 | } 245 | h.Set("User-Agent", profile.ua) 246 | h.Set("Sec-CH-UA", profile.secCHUA) 247 | h.Set("Sec-CH-UA-Mobile", func() string { 248 | if strings.Contains(profile.ua, "Mobile") { 249 | return "?1" 250 | } 251 | return "?0" 252 | }()) 253 | h.Set("Sec-CH-UA-Platform", `"`+func() string { 254 | if strings.Contains(profile.ua, "Android") { 255 | return "Android" 256 | } 257 | return "macOS" 258 | }()+`"`) 259 | h.Set("Origin", "https://www.target.com") 260 | h.Set("Referer", fmt.Sprintf( 261 | "https://www.target.com/p/2023-panini-select-baseball-trading-card-blaster-box/-/A-%s", 262 | tcin, 263 | )) 264 | h.Set("Sec-Fetch-Site", "same-site") 265 | h.Set("Sec-Fetch-Mode", "cors") 266 | h.Set("Sec-Fetch-Dest", "empty") 267 | h.Set("Priority", "u=1,i") 268 | 269 | if cc := cacheOpts[profile.cacheIdx]; cc != "" { 270 | h.Set("Cache-Control", cc) 271 | } 272 | 273 | if profile.viewportProb[0] < 0.7 { 274 | h.Set("Sec-CH-Viewport-Width", strconv.Itoa(profile.viewport.Width)) 275 | } 276 | if profile.viewportProb[1] < 0.7 { 277 | h.Set("Sec-CH-Viewport-Height", strconv.Itoa(profile.viewport.Height)) 278 | } 279 | if profile.viewportProb[2] < 0.7 { 280 | h.Set("Sec-CH-DPR", fmt.Sprintf("%.1f", profile.viewport.PixelRatio)) 281 | } 282 | 283 | if profile.hints.ConnectionType != "" && profile.hintsProb[0] < 0.6 { 284 | h.Set("Connection", profile.hints.ConnectionType) 285 | } 286 | if profile.hints.EffectiveType != "" && profile.hintsProb[1] < 0.5 { 287 | h.Set("Sec-CH-UA-Netinfo", profile.hints.EffectiveType) 288 | } 289 | if profile.hintsProb[2] < 0.4 { 290 | h.Set("Sec-CH-UA-Downlink", fmt.Sprintf("%.1f", profile.hints.Downlink)) 291 | } 292 | if profile.hintsProb[3] < 0.3 { 293 | h.Set("Sec-CH-UA-RTT", strconv.Itoa(profile.hints.RTT)) 294 | } 295 | if profile.hints.SaveData == "on" { 296 | h.Set("Save-Data", "on") 297 | } 298 | 299 | if profile.memProb < 0.3 { 300 | h.Set("Device-Memory", memOpts[profile.memIdx]) 301 | } 302 | 303 | h[http.HeaderOrderKey] = headerOrder 304 | 305 | return h 306 | } 307 | 308 | func InitProfilePool(count int) { 309 | profiles := make([]interface{}, count) 310 | for i := 0; i < count; i++ { 311 | profiles[i] = generateProfile() 312 | } 313 | for _, profile := range profiles { 314 | profilePool.Put(profile) 315 | } 316 | } 317 | 318 | func ResetProfilePool() { 319 | profilePool = sync.Pool{New: func() interface{} { return generateProfile() }} 320 | } --------------------------------------------------------------------------------