Skip to content

Commit 4299434

Browse files
committed
refactor: drop utls, simplify to 2-tier strategy (HTTP + Chrome)
Remove utls TLS fingerprint spoofing entirely — it triggers Trojan:Win32/Bearfoos.B!ml on Windows Defender. New strategy: Tier 1: Plain HTTP with browser headers (~50ms) Tier 2: Headless Chrome via go-rod (~1-2s) Chrome provides a real TLS fingerprint — better than utls forgery, and passes all Cloudflare challenges utls was trying to handle. Removed: utls dep, --no-tls-spoof flag, build tags, stub files Renamed: fetch_tier3.go → fetch_chrome.go (now Tier 2)
1 parent 3b917ca commit 4299434

15 files changed

Lines changed: 27 additions & 242 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: Run tests
2929
id: tests
3030
run: |
31-
TEST_OUTPUT=$(go test -tags all -v -race -count=1 ./... 2>&1)
31+
TEST_OUTPUT=$(go test -v -race -count=1 ./... 2>&1)
3232
echo "$TEST_OUTPUT"
3333
TEST_COUNT=$(echo "$TEST_OUTPUT" | grep -c '^--- PASS:' || true)
3434
echo "test_count=$TEST_COUNT" >> "$GITHUB_OUTPUT"

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
grep 'var version' main.go
5858
5959
- name: Run tests
60-
run: go test -tags all -race -count=1 ./...
60+
run: go test -race -count=1 ./...
6161

6262
- name: Cross-compile binaries
6363
run: |
@@ -71,7 +71,7 @@ jobs:
7171
OUTPUT="${OUTPUT}.exe"
7272
fi
7373
echo "Building $OUTPUT"
74-
GOOS=$GOOS GOARCH=$GOARCH go build -tags all -ldflags="-s -w" -o "$OUTPUT" .
74+
GOOS=$GOOS GOARCH=$GOARCH go build -ldflags="-s -w" -o "$OUTPUT" .
7575
done
7676
ls -lh dist/
7777

README.md

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,10 @@ Single Go binary. No runtime downloads, no external services, no AI, no search.
1111
## Install
1212

1313
```bash
14-
# Default — Tier 1 only (plain HTTP, no AV issues)
1514
go install github.com/RandomCodeSpace/rawdoc@latest
16-
17-
# With all tiers (Tier 1 + TLS spoofing + headless Chrome)
18-
go install -tags all github.com/RandomCodeSpace/rawdoc@latest
1915
```
2016

21-
| Build | Size | Tiers | AV Safe |
22-
|-------|------|-------|---------|
23-
| Default | ~7MB | Tier 1 | Yes |
24-
| `-tags tier2` | ~10MB | Tier 1+2 | May flag |
25-
| `-tags tier3` | ~10MB | Tier 1+3 | Yes |
26-
| `-tags all` | ~13MB | All | May flag |
27-
28-
Tier 2 uses TLS fingerprint spoofing (`utls`) which triggers Windows Defender false positives (`Trojan:Win32/Bearfoos.B!ml`). The default build excludes it.
17+
Single binary, no AV flags, no runtime deps.
2918

3019
---
3120

@@ -58,12 +47,11 @@ rawdoc https://www.baeldung.com/spring-kafka -v
5847

5948
## How It Works
6049

61-
Three-tier fetch strategy — auto-escalates until a clean response is obtained:
50+
Two-tier fetch strategy — auto-escalates when needed:
6251

6352
```
6453
Tier 1: Plain HTTP (~50ms) — works for most doc sites
65-
Tier 2: TLS Spoofing (~100ms) — bypasses basic Cloudflare
66-
Tier 3: Headless Chrome (~2s) — JS-rendered pages (needs Chrome installed)
54+
Tier 2: Headless Chrome (~2s) — JS-rendered pages, Cloudflare (needs Chrome installed)
6755
```
6856

6957
Processing pipeline: **Fetch → Strip noise → Extract content → Convert to Markdown**
@@ -105,7 +93,6 @@ rawdoc [flags] <url>
10593
| `--max-time duration` | `10m` | Total runtime ceiling |
10694
| `--max-retries int` | `3` | Per-URL retries |
10795
| `--header K=V` || Extra header (repeatable) |
108-
| `--no-tls-spoof` || Disable utls fingerprint mimicry |
10996
| `--no-headless` || Disable Chrome fallback tier |
11097

11198
### Info
@@ -241,22 +228,5 @@ GOOS=darwin GOARCH=arm64 go build -o rawdoc-darwin-arm64 .
241228
| Requirement | Notes |
242229
|-------------|-------|
243230
| Go 1.24+ | Required to build from source |
244-
| Chrome / Chromium | Optional — only needed for Tier 3 (JS-rendered pages) |
245-
246-
## Windows Antivirus Note
247-
248-
Windows Defender may flag `rawdoc.exe` as a false positive. This happens because:
231+
| Chrome / Chromium | Optional — only needed for Tier 2 (JS-rendered pages, Cloudflare) |
249232

250-
- Go statically links everything into one large binary (triggers heuristic scanners)
251-
- `utls` dependency spoofs TLS fingerprints (anti-detection technique)
252-
- `go-rod` automates headless Chrome (browser automation flagged by behavior analysis)
253-
254-
**The code is fully open source — inspect it yourself.** To work around this:
255-
256-
```powershell
257-
# Option 1: Exclude the build directory
258-
Add-MpPreference -ExclusionPath "D:\Development\rawdoc"
259-
260-
# Option 2: Build with stripped symbols (smaller, fewer flags)
261-
go build -ldflags="-s -w" -o rawdoc.exe .
262-
```

crawl.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ func runCrawl(cfg *config, seedURL *url.URL) error {
187187
maxRetries: cfg.maxRetries,
188188
verbose: cfg.verbose,
189189
quiet: cfg.quiet,
190-
noTLSSpoof: cfg.noTLSSpoof,
191190
noHeadless: cfg.noHeadless,
192191
headers: []string(cfg.headers),
193192
}

fetch.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ type fetchOptions struct {
1414
maxRetries int
1515
verbose bool
1616
quiet bool
17-
noTLSSpoof bool
1817
noHeadless bool
1918
headers []string
2019
}
@@ -60,17 +59,9 @@ func fetch(rawURL string, opts *fetchOptions) (*fetchResult, error) {
6059
return result, nil
6160
}
6261

63-
if isEscalatable(err) && !opts.noTLSSpoof {
64-
logVerbose(opts, "[tier1] %s → %v, escalating to tier2", rawURL, err)
65-
result, err = fetchTier2(rawURL, opts)
66-
if err == nil {
67-
return result, nil
68-
}
69-
}
70-
7162
if isEscalatable(err) && !opts.noHeadless {
72-
logVerbose(opts, "[tier2] %s → %v, escalating to tier3", rawURL, err)
73-
result, err = fetchTier3(rawURL, opts)
63+
logVerbose(opts, "[tier1] %s → %v, escalating to tier2 (Chrome)", rawURL, err)
64+
result, err = fetchTier2(rawURL, opts)
7465
if err == nil {
7566
return result, nil
7667
}

fetch_tier3.go renamed to fetch_chrome.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//go:build tier3 || all
2-
31
package main
42

53
import (
@@ -13,53 +11,53 @@ import (
1311
"github.com/go-rod/rod/lib/proto"
1412
)
1513

16-
func fetchTier3(rawURL string, opts *fetchOptions) (*fetchResult, error) {
14+
func fetchTier2(rawURL string, opts *fetchOptions) (*fetchResult, error) {
1715
chromePath := findChrome()
1816
if chromePath == "" {
19-
logVerbose(opts, "[tier3] Chrome not found at any known path")
17+
logVerbose(opts, "[tier2] Chrome not found at any known path")
2018
return nil, &httpError{statusCode: 0, message: "Chrome not installed"}
2119
}
2220

23-
logVerbose(opts, "[tier3] %s → using Chrome at %s", rawURL, chromePath)
21+
logVerbose(opts, "[tier2] %s → using Chrome at %s", rawURL, chromePath)
2422

2523
l := launcher.New().Bin(chromePath).Headless(true)
2624
controlURL, err := l.Launch()
2725
if err != nil {
28-
logVerbose(opts, "[tier3] %s → Chrome launch failed: %v", rawURL, err)
26+
logVerbose(opts, "[tier2] %s → Chrome launch failed: %v", rawURL, err)
2927
return nil, fmt.Errorf("chrome launch: %w", err)
3028
}
3129

3230
browser := rod.New().ControlURL(controlURL)
3331
if err := browser.Connect(); err != nil {
34-
logVerbose(opts, "[tier3] %s → browser connect failed: %v", rawURL, err)
32+
logVerbose(opts, "[tier2] %s → browser connect failed: %v", rawURL, err)
3533
return nil, fmt.Errorf("browser connect: %w", err)
3634
}
3735
defer browser.Close()
3836

3937
page, err := browser.Page(proto.TargetCreateTarget{URL: rawURL})
4038
if err != nil {
41-
logVerbose(opts, "[tier3] %s → page open failed: %v", rawURL, err)
39+
logVerbose(opts, "[tier2] %s → page open failed: %v", rawURL, err)
4240
return nil, fmt.Errorf("browser open page: %w", err)
4341
}
4442

4543
if err := page.WaitStable(1 * time.Second); err != nil {
46-
logVerbose(opts, "[tier3] %s → page did not stabilize: %v", rawURL, err)
44+
logVerbose(opts, "[tier2] %s → page did not stabilize: %v", rawURL, err)
4745
}
4846
page.MustWaitLoad()
4947

5048
html, err := page.HTML()
5149
if err != nil {
52-
logVerbose(opts, "[tier3] %s → failed to get HTML: %v", rawURL, err)
50+
logVerbose(opts, "[tier2] %s → failed to get HTML: %v", rawURL, err)
5351
return nil, fmt.Errorf("page html: %w", err)
5452
}
5553

5654
if isCaptchaPage(html) {
57-
logVerbose(opts, "[tier3] %s → CAPTCHA detected", rawURL)
55+
logVerbose(opts, "[tier2] %s → CAPTCHA detected", rawURL)
5856
return nil, &httpError{statusCode: 0, message: "site requires interactive challenge (CAPTCHA)"}
5957
}
6058

61-
logVerbose(opts, "[tier3] %s → 200 (headless Chrome, %d bytes)", rawURL, len(html))
62-
return &fetchResult{html: html, tier: 3, url: rawURL}, nil
59+
logVerbose(opts, "[tier2] %s → 200 (headless Chrome, %d bytes)", rawURL, len(html))
60+
return &fetchResult{html: html, tier: 2, url: rawURL}, nil
6361
}
6462

6563
func findChrome() string {

fetch_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func TestTierEscalationOn403(t *testing.T) {
115115
opts := &fetchOptions{
116116
timeout: 5 * time.Second,
117117
maxRetries: 3,
118-
noTLSSpoof: true,
118+
119119
noHeadless: true,
120120
}
121121
_, err := fetch(ts.URL, opts)
@@ -133,7 +133,7 @@ func TestTier3SkipsWhenNoChrome(t *testing.T) {
133133
opts := &fetchOptions{
134134
timeout: 5 * time.Second,
135135
maxRetries: 1,
136-
noTLSSpoof: true,
136+
137137
noHeadless: false, // allow tier3, but Chrome likely not found in test env
138138
}
139139
_, err := fetch(server.URL, opts)

fetch_tier2.go

Lines changed: 0 additions & 134 deletions
This file was deleted.

fetch_tier2_stub.go

Lines changed: 0 additions & 8 deletions
This file was deleted.

fetch_tier3_stub.go

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)