diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..64db931 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,151 @@ +name: CI + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + +# Prevent concurrent workflow runs on same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + build-and-test: + name: Build and Test (Go ${{ matrix.go-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + go-version: ['1.25'] + include: + - go-version: '1.25' + race: true + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Download dependencies + run: go mod download + + - name: Build + run: go build -v ./... + + - name: Run tests + run: | + if [ "${{ matrix.go-version }}" = "1.25" ]; then + go test -v -race -coverprofile=coverage.out -covermode=atomic ./... + else + go test -v -short ./... + fi + + - name: Verify module validity + run: | + go mod verify + go build -v ./... + + - name: Upload coverage + if: matrix.race == true + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage.out + + test-coverage: + name: Coverage Report + needs: build-and-test + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Download coverage artifact + uses: actions/download-artifact@v4 + with: + name: coverage + path: . + + - name: Generate coverage report + run: | + go test -coverprofile=coverage.out -covermode=atomic ./... + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + files: coverage.out + flags: unittests + name: fingerprintproxy + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Coverage Summary + run: | + echo "## Coverage Summary" >> "$GITHUB_STEP_SUMMARY" + go tool cover -func=coverage.out >> "$GITHUB_STEP_SUMMARY" + + cross-platform: + name: Cross-platform Build + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + go-version: ['1.25'] + include: + - os: ubuntu-latest + artifact_suffix: '-linux-amd64' + goarch: amd64 + - os: macos-latest + artifact_suffix: '-darwin-arm64' + goarch: arm64 + - os: windows-latest + artifact_suffix: '-windows-amd64.exe' + goarch: amd64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Build + run: go build -ldflags="-s -w" -o fingerprintproxy${{ matrix.artifact_suffix }} . + env: + GOARCH: ${{ matrix.goarch }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: fingerprintproxy${{ matrix.artifact_suffix }} + path: fingerprintproxy${{ matrix.artifact_suffix }} diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 0000000..a580971 --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,59 @@ +name: Dependencies + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + go-mod-updater: + name: Update Go Dependencies + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Update dependencies + run: | + go get -u ./... + go mod tidy + + - name: Create PR with updates + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore(deps): update Go dependencies' + title: 'Chore: Update Go Dependencies' + body: | + This PR updates Go module dependencies. + + ## Update Summary + - Run `go mod tidy` to clean up dependencies + - Run `go get -u ./...` to update indirect dependencies + + Please review the changes before merging. + labels: dependencies, automated + branch: dependency-updates + delete-branch: true + draft: true + + # Enable Dependabot for GitHub Actions as well + # Note: Add .github/dependabot.yml manually for Actions updates diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2bd7476 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,138 @@ +name: Lint + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + STATICCHECK_VERSION: '0.7.0' + ACTIONLINT_VERSION: '1.7.1' + +jobs: + golangci-lint: + name: Lint with golangci-lint + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.11.4 + args: --timeout=5m + + gofmt: + name: Check formatting + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Check formatting + run: | + if [ -n "$(gofmt -l .)" ]; then + echo "The following files are not formatted correctly:" + gofmt -l . + exit 1 + fi + + govet: + name: Run go vet + runs-on: ubuntu-latest + timeout-minutes: 3 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run go vet + run: go vet ./... + + staticcheck: + name: Static Analysis + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Install staticcheck + run: go install "honnef.co/go/tools/cmd/staticcheck@v${STATICCHECK_VERSION}" + + - name: Run staticcheck + run: staticcheck ./... + + actionlint: + name: Workflow Lint + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Install actionlint + run: go install "github.com/rhysd/actionlint/cmd/actionlint@v${ACTIONLINT_VERSION}" + + - name: Run actionlint + run: actionlint + shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3e459f4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + id-token: write + +jobs: + release: + name: Create Release + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v7 + with: + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Nightly builds available via workflow_dispatch + # To enable: remove the "if: false" line and trigger manually diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..894b576 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,131 @@ +name: Security + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday at midnight + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + pull-requests: write + +jobs: + govulncheck: + name: Vulnerability Scanning + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run govulncheck + uses: golang/govulncheck-action@v1 + with: + args: ./... + + gosec: + name: Security Analysis (Gosec) + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Run gosec + run: gosec -no-fail ./... + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: + contents: read + if: github.event_name == 'pull_request' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run dependency review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + license-check: true + + codeql: + name: CodeQL Analysis + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + queries: security-extended + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:go" + + actionlint: + name: Workflow Security Check + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Install actionlint + run: go install github.com/rhysd/actionlint/cmd/actionlint@latest + + - name: Run actionlint + run: actionlint + shell: bash diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..ebd13e2 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,54 @@ +# GoReleaser configuration for fingerprintproxy +# See https://goreleaser.com/customization/ for details + +project_name: fingerprintproxy + +version: 2 + +before: + hooks: + - go mod download + +builds: + - id: fingerprintproxy + main: . + binary: fingerprintproxy + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X main.commit={{.Commit}} + - -X main.date={{.Date}} + +archives: + - id: default + format: tar.gz + format_overrides: + - goos: windows + format: zip + +checksums: + - algorithm: sha256 + +snapshot: + name_template: "{{ .Tag }}-next" + +# Disable announce as defaults aren't configured +announce: + twitter: + enabled: false + slack: + enabled: false + +# For GitHub Releases +release: + github: + enabled: true diff --git a/go.mod b/go.mod index 2e53a57..b563c51 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tomkabel/fingerprintproxy -go 1.24.1 +go 1.25.0 require ( github.com/elazarl/goproxy v1.8.3 @@ -16,11 +16,10 @@ require ( github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect github.com/bogdanfinn/utls v1.7.7-barnius // indirect github.com/bogdanfinn/websocket v1.5.5-barnius // indirect - github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect + github.com/tomkabel/tls-client v1.7.7-barnius-4 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sys v0.42.0 // indirect diff --git a/go.sum b/go.sum index a518b46..3510695 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,10 @@ github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE= -github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= +github.com/elazarl/goproxy v1.8.3 h1:XhiZpzW0NvsGOqSv/F3v4+1F29842yYaJNN+In5Fnuc= +github.com/elazarl/goproxy v1.8.3/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E= +github.com/inconshreveable/go-vhost v1.0.0 h1:IK4VZTlXL4l9vz2IZoiSFbYaaqUW7dXJAiPriUN5Ur8= +github.com/inconshreveable/go-vhost v1.0.0/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -28,6 +30,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= +github.com/tomkabel/browser-fingerprint-transport v0.1.0 h1:nDZ428bOFaXmEPpjcl5Sbd6ZensS0mV756//SSA1FSk= +github.com/tomkabel/browser-fingerprint-transport v0.1.0/go.mod h1:Lg+avDAetxJ67rBSqQOrwjeGmYAJOFmuEBby3FM3Nr8= +github.com/tomkabel/tls-client v1.7.7-barnius-4 h1:8m96kyDJ7gdhiO/B8n085iQJeGQpkqCjMHzF7x36NNA= +github.com/tomkabel/tls-client v1.7.7-barnius-4/go.mod h1:bnPmX7eo3+sDL+4lRYfeNzQVc/CguIHYFRL4FxdDEPs= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= diff --git a/main.go b/main.go index 2b5f020..7a83fc0 100644 --- a/main.go +++ b/main.go @@ -239,7 +239,7 @@ func (fp *fingerprintProxy) setupHandlers() { // Non-proxy handler for transparent mode fp.proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.Host == "" { - fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") + _, _ = fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") return } req.URL.Scheme = "http" @@ -347,7 +347,7 @@ func (fp *fingerprintProxy) Run(httpAddr, httpsAddr string) error { } // Close HTTPS listener - ln.Close() + _ = ln.Close() // Close idle connections in cache fp.transportCache.CloseIdleConnections() @@ -362,7 +362,7 @@ func (fp *fingerprintProxy) handleHTTPS(c net.Conn) { if r := recover(); r != nil { log.Printf("[Error] panic in handleHTTPS: %v", r) } - c.Close() + _ = c.Close() }() tlsConn, err := vhost.TLS(c) diff --git a/main_test.go b/main_test.go index 560351f..8f3ea6e 100644 --- a/main_test.go +++ b/main_test.go @@ -381,7 +381,7 @@ func TestChromeFingerprintAgainstAPI(t *testing.T) { if err != nil { t.Fatalf("request failed: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { t.Fatalf("expected status 200, got %d", resp.StatusCode) @@ -434,7 +434,7 @@ func TestFirefoxFingerprintAgainstAPI(t *testing.T) { if err != nil { t.Fatalf("request failed: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { t.Fatalf("expected status 200, got %d", resp.StatusCode) @@ -568,18 +568,24 @@ func TestCacheLength(t *testing.T) { t.Errorf("expected initial length 0, got %d", cache.Len()) } - cache.GetOrCreate("chrome_133") + if _, err := cache.GetOrCreate("chrome_133"); err != nil { + t.Fatalf("failed to create chrome transport: %v", err) + } if cache.Len() != 1 { t.Errorf("expected length 1, got %d", cache.Len()) } - cache.GetOrCreate("firefox_147") + if _, err := cache.GetOrCreate("firefox_147"); err != nil { + t.Fatalf("failed to create firefox transport: %v", err) + } if cache.Len() != 2 { t.Errorf("expected length 2, got %d", cache.Len()) } // Same profile should not increase length - cache.GetOrCreate("chrome_133") + if _, err := cache.GetOrCreate("chrome_133"); err != nil { + t.Fatalf("failed to create chrome transport: %v", err) + } if cache.Len() != 2 { t.Errorf("expected length 2 after duplicate, got %d", cache.Len()) }