11 Commits

Author SHA1 Message Date
56bb1ddc12 Add hop-by-hop header handling in ServeHTTP method
All checks were successful
Release Tag / release (push) Successful in 12s
- Introduced a map for hop-by-hop headers to be removed from responses.
- Enhanced cache serving logic to read and filter HTTP responses, ensuring only relevant headers are forwarded.
- Updated cache writing to handle full HTTP responses, improving cache integrity and performance.
2025-07-19 05:07:36 -05:00
9c65cdb156 Fix HTTP status code for root path in ServeHTTP method to ensure correct response for upstream verification
All checks were successful
Release Tag / release (push) Successful in 12s
2025-07-19 04:42:20 -05:00
ae013f9a3b Enhance SteamCache configuration and HTTP client settings
All checks were successful
Release Tag / release (push) Successful in 14s
- Added upstream server configuration to launch.json for improved connectivity.
- Increased HTTP client timeout from 60s to 120s for better handling of slow responses.
- Updated server timeouts in steamcache.go: increased ReadTimeout to 30s and WriteTimeout to 60s.
- Introduced ReadHeaderTimeout to mitigate header attacks and set MaxHeaderBytes to 1MB.
- Improved error logging in the Run method to include HTTP status codes for better debugging.
- Adjusted ServeHTTP method to handle root path and metrics endpoint correctly.
2025-07-19 04:40:05 -05:00
d94b53c395 Merge pull request 'Update .goreleaser.yaml and enhance HTTP client settings in steamcache.go' (#10) from fix/connection-pooling into main
All checks were successful
Release Tag / release (push) Successful in 15s
Reviewed-on: s1d3sw1ped/SteamCache2#10
2025-07-19 09:13:37 +00:00
847931ed43 Update .goreleaser.yaml and enhance HTTP client settings in steamcache.go
All checks were successful
PR Check / check-and-test (pull_request) Successful in 18s
- Removed copyright footer from .goreleaser.yaml.
- Increased HTTP client connection settings in steamcache.go for improved performance:
  - MaxIdleConns from 100 to 200
  - MaxIdleConnsPerHost from 10 to 50
  - IdleConnTimeout from 90s to 120s
  - TLSHandshakeTimeout from 10s to 15s
  - ResponseHeaderTimeout from 10s to 30s
  - ExpectContinueTimeout from 1s to 5s
  - Added DisableCompression and ForceAttemptHTTP2 options.
- Removed debug logging for manifest files in ServeHTTP method.
2025-07-19 04:12:56 -05:00
4387236d22 Merge pull request 'Update .goreleaser.yaml to use hyphens in name templates for archives and releases' (#9) from fix/goreleaser-config-fix-really into main
All checks were successful
Release Tag / release (push) Successful in 21s
Reviewed-on: s1d3sw1ped/SteamCache2#9
2025-07-19 08:23:09 +00:00
f6ce004922 Update .goreleaser.yaml to use hyphens in name templates for archives and releases
All checks were successful
PR Check / check-and-test (pull_request) Successful in 9s
2025-07-19 03:22:08 -05:00
8e487876d2 Merge pull request 'Remove steamcache2 from the list of files in .goreleaser.yaml archives section.' (#8) from fix/goreleaser-config-fix into main
Some checks failed
Release Tag / release (push) Failing after 20s
Reviewed-on: s1d3sw1ped/SteamCache2#8
2025-07-19 08:04:40 +00:00
1be7f5bd20 Remove steamcache2 from the list of files in .goreleaser.yaml archives section.
All checks were successful
PR Check / check-and-test (pull_request) Successful in 9s
2025-07-19 03:02:39 -05:00
f237b89ca7 Merge pull request 'Update versioning and logging in SteamCache2' (#7) from fix/goreleaser-config into main
Some checks failed
Release Tag / release (push) Failing after 22s
Reviewed-on: s1d3sw1ped/SteamCache2#7
2025-07-19 07:59:02 +00:00
ae07239021 Update versioning and logging in SteamCache2
All checks were successful
PR Check / check-and-test (pull_request) Successful in 11s
- Enhanced .goreleaser.yaml for improved build configuration, including static linking and ARM64 support.
- Updated logging in root.go to include version date during startup.
- Modified version.go to initialize and expose the build date alongside the version.
- Adjusted version command output to display both version and date for better clarity.
2025-07-19 02:58:19 -05:00
6 changed files with 130 additions and 72 deletions

View File

@@ -2,11 +2,17 @@ version: 2
before: before:
hooks: hooks:
- go mod tidy - go mod tidy -v
builds: builds:
- ldflags: - id: default
binary: steamcache2
ldflags:
- -s
- -w
- -extldflags "-static"
- -X s1d3sw1ped/SteamCache2/version.Version={{.Version}} - -X s1d3sw1ped/SteamCache2/version.Version={{.Version}}
- -X s1d3sw1ped/SteamCache2/version.Date={{.Date}}
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:
@@ -14,19 +20,24 @@ builds:
- windows - windows
goarch: goarch:
- amd64 - amd64
- arm64
ignore:
- goos: windows
goarch: arm64
checksum:
name_template: "checksums.txt"
archives: archives:
- formats: tar.gz - id: default
name_template: >- name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
{{ .ProjectName }}_ formats: tar.gz
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides: format_overrides:
- goos: windows - goos: windows
formats: zip formats: zip
files:
- README.md
- LICENSE
changelog: changelog:
sort: asc sort: asc
@@ -36,12 +47,7 @@ changelog:
- "^test:" - "^test:"
release: release:
name_template: '{{.ProjectName}}-{{.Version}}' name_template: "{{ .ProjectName }}-{{ .Version }}"
footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
gitea_urls: gitea_urls:
api: https://git.s1d3sw1ped.com/api/v1 api: https://git.s1d3sw1ped.com/api/v1

6
.vscode/launch.json vendored
View File

@@ -23,6 +23,8 @@
"lru", "lru",
"--log-level", "--log-level",
"debug", "debug",
"--upstream",
"http://192.168.2.5:80",
], ],
}, },
{ {
@@ -40,6 +42,8 @@
"hybrid", "hybrid",
"--log-level", "--log-level",
"debug", "debug",
"--upstream",
"http://192.168.2.5:80",
], ],
}, },
{ {
@@ -55,6 +59,8 @@
"lfu", "lfu",
"--log-level", "--log-level",
"debug", "debug",
"--upstream",
"http://192.168.2.5:80",
], ],
} }
] ]

View File

@@ -56,7 +56,7 @@ var rootCmd = &cobra.Command{
logger.Logger = zerolog.New(writer).With().Timestamp().Logger() logger.Logger = zerolog.New(writer).With().Timestamp().Logger()
logger.Logger.Info(). logger.Logger.Info().
Msg("SteamCache2 " + version.Version + " starting...") Msg("SteamCache2 " + version.Version + " " + version.Date + " starting...")
address := ":80" address := ":80"

View File

@@ -15,7 +15,7 @@ var versionCmd = &cobra.Command{
Short: "prints the version of SteamCache2", Short: "prints the version of SteamCache2",
Long: `Prints the version of SteamCache2. This command is useful for checking the version of the application.`, Long: `Prints the version of SteamCache2. This command is useful for checking the version of the application.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "SteamCache2", version.Version) fmt.Fprintln(os.Stderr, "SteamCache2", version.Version, version.Date)
}, },
} }

View File

@@ -2,6 +2,7 @@
package steamcache package steamcache
import ( import (
"bufio"
"context" "context"
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
@@ -24,6 +25,8 @@ import (
"sync" "sync"
"time" "time"
"bytes"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
@@ -146,6 +149,19 @@ func verifyResponseHash(resp *http.Response, bodyData []byte, expectedHash strin
return strings.EqualFold(actualHash, expectedHash) return strings.EqualFold(actualHash, expectedHash)
} }
var hopByHopHeaders = map[string]struct{}{
"Connection": {},
"Keep-Alive": {},
"Proxy-Authenticate": {},
"Proxy-Authorization": {},
"TE": {},
"Trailer": {},
"Transfer-Encoding": {},
"Upgrade": {},
"Date": {},
"Server": {},
}
type SteamCache struct { type SteamCache struct {
address string address string
upstream string upstream string
@@ -222,21 +238,23 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
} }
transport := &http.Transport{ transport := &http.Transport{
MaxIdleConns: 100, MaxIdleConns: 200, // Increased from 100
MaxIdleConnsPerHost: 10, MaxIdleConnsPerHost: 50, // Increased from 10
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 120 * time.Second, // Increased from 90s
DialContext: (&net.Dialer{ DialContext: (&net.Dialer{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
}).DialContext, }).DialContext,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 15 * time.Second, // Increased from 10s
ResponseHeaderTimeout: 10 * time.Second, ResponseHeaderTimeout: 30 * time.Second, // Increased from 10s
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 5 * time.Second, // Increased from 1s
DisableCompression: true, // Steam doesn't use compression
ForceAttemptHTTP2: true, // Enable HTTP/2 if available
} }
client := &http.Client{ client := &http.Client{
Transport: transport, Transport: transport,
Timeout: 60 * time.Second, Timeout: 120 * time.Second, // Increased from 60s
} }
sc := &SteamCache{ sc := &SteamCache{
@@ -249,10 +267,12 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
diskgc: dgc, diskgc: dgc,
client: client, client: client,
server: &http.Server{ server: &http.Server{
Addr: address, Addr: address,
ReadTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, // Increased
WriteTimeout: 10 * time.Second, WriteTimeout: 60 * time.Second, // Increased
IdleTimeout: 120 * time.Second, IdleTimeout: 120 * time.Second, // Good for keep-alive
ReadHeaderTimeout: 10 * time.Second, // New, for header attacks
MaxHeaderBytes: 1 << 20, // 1MB, optional
}, },
} }
@@ -266,7 +286,8 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
if d != nil { if d != nil {
if d.Size() > d.Capacity() { if d.Size() > d.Capacity() {
gc.LRUGC(d, uint(d.Size()-d.Capacity())) gcHandler := gc.GetGCAlgorithm(gc.GCAlgorithm(diskGC))
gcHandler(d, uint(d.Size()-d.Capacity()))
} }
} }
@@ -277,7 +298,7 @@ func (sc *SteamCache) Run() {
if sc.upstream != "" { if sc.upstream != "" {
resp, err := sc.client.Get(sc.upstream) resp, err := sc.client.Get(sc.upstream)
if err != nil || resp.StatusCode != http.StatusOK { if err != nil || resp.StatusCode != http.StatusOK {
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed to connect to upstream server") logger.Logger.Error().Err(err).Int("status_code", resp.StatusCode).Str("upstream", sc.upstream).Msg("Failed to connect to upstream server")
os.Exit(1) os.Exit(1)
} }
resp.Body.Close() resp.Body.Close()
@@ -310,11 +331,6 @@ func (sc *SteamCache) Shutdown() {
} }
func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/metrics" {
promhttp.Handler().ServeHTTP(w, r)
return
}
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
requestsTotal.WithLabelValues(r.Method, "405").Inc() requestsTotal.WithLabelValues(r.Method, "405").Inc()
logger.Logger.Warn().Str("method", r.Method).Msg("Only GET method is supported") logger.Logger.Warn().Str("method", r.Method).Msg("Only GET method is supported")
@@ -322,6 +338,11 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if r.URL.Path == "/" {
w.WriteHeader(http.StatusOK) // this is used by steamcache2's upstream verification at startup
return
}
if r.URL.String() == "/lancache-heartbeat" { if r.URL.String() == "/lancache-heartbeat" {
w.Header().Add("X-LanCache-Processed-By", "SteamCache2") w.Header().Add("X-LanCache-Processed-By", "SteamCache2")
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
@@ -329,6 +350,11 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if r.URL.Path == "/metrics" {
promhttp.Handler().ServeHTTP(w, r)
return
}
if strings.HasPrefix(r.URL.String(), "/depot/") { if strings.HasPrefix(r.URL.String(), "/depot/") {
// trim the query parameters from the URL path // trim the query parameters from the URL path
// this is necessary because the cache key should not include query parameters // this is necessary because the cache key should not include query parameters
@@ -347,25 +373,41 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-LanCache-Processed-By", "SteamCache2") // SteamPrefill uses this header to determine if the request was processed by the cache maybe steam uses it too w.Header().Add("X-LanCache-Processed-By", "SteamCache2") // SteamPrefill uses this header to determine if the request was processed by the cache maybe steam uses it too
reader, err := sc.vfs.Open(cacheKey) cachePath := cacheKey // You may want to add a .http or .cache extension for clarity
// Try to serve from cache
file, err := sc.vfs.Open(cachePath)
if err == nil { if err == nil {
defer reader.Close() defer file.Close()
w.Header().Add("X-LanCache-Status", "HIT") buf := bufio.NewReader(file)
resp, err := http.ReadResponse(buf, nil)
io.Copy(w, reader) if err == nil {
// Remove hop-by-hop and server-specific headers
logger.Logger.Info(). for k, vv := range resp.Header {
Str("key", cacheKey). if _, skip := hopByHopHeaders[http.CanonicalHeaderKey(k)]; skip {
Str("host", r.Host). continue
Str("status", "HIT"). }
Dur("duration", time.Since(tstart)). for _, v := range vv {
Msg("request") w.Header().Add(k, v)
}
requestsTotal.WithLabelValues(r.Method, "200").Inc() }
cacheStatusTotal.WithLabelValues("HIT").Inc() // Add our own headers
responseTime.WithLabelValues("HIT").Observe(time.Since(tstart).Seconds()) w.Header().Set("X-LanCache-Status", "HIT")
w.Header().Set("X-LanCache-Processed-By", "SteamCache2")
return w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
resp.Body.Close()
logger.Logger.Info().
Str("key", cacheKey).
Str("host", r.Host).
Str("status", "HIT").
Dur("duration", time.Since(tstart)).
Msg("request")
requestsTotal.WithLabelValues(r.Method, "200").Inc()
cacheStatusTotal.WithLabelValues("HIT").Inc()
responseTime.WithLabelValues("HIT").Observe(time.Since(tstart).Seconds())
return
}
} }
var req *http.Request var req *http.Request
@@ -438,8 +480,6 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
defer resp.Body.Close() defer resp.Body.Close()
size := resp.ContentLength
// Read the entire response body into memory for hash verification // Read the entire response body into memory for hash verification
bodyData, err := io.ReadAll(resp.Body) bodyData, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
@@ -453,18 +493,6 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(cacheKey) filename := filepath.Base(cacheKey)
expectedHash, hasHash := extractHashFromFilename(filename) expectedHash, hasHash := extractHashFromFilename(filename)
// Debug logging for manifest files
if strings.Contains(cacheKey, "manifest") {
logger.Logger.Debug().
Str("key", cacheKey).
Str("filename", filename).
Bool("hasHash", hasHash).
Str("expectedHash", expectedHash).
Int64("content_length_header", resp.ContentLength).
Int("actual_content_length", len(bodyData)).
Msg("Manifest file hash verification debug")
}
// Hash verification using Steam's X-Content-Sha header and content length verification // Hash verification using Steam's X-Content-Sha header and content length verification
hashVerified := true hashVerified := true
if hasHash { if hasHash {
@@ -502,15 +530,28 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
// Write to response (always serve the file) // Write to response (always serve the file)
w.Header().Add("X-LanCache-Status", "MISS") // Remove hop-by-hop and server-specific headers
for k, vv := range resp.Header {
if _, skip := hopByHopHeaders[http.CanonicalHeaderKey(k)]; skip {
continue
}
for _, v := range vv {
w.Header().Add(k, v)
}
}
// Add our own headers
w.Header().Set("X-LanCache-Status", "MISS")
w.Header().Set("X-LanCache-Processed-By", "SteamCache2")
w.Write(bodyData) w.Write(bodyData)
// Only cache the file if hash verification passed (or no hash was present) // Only cache the file if hash verification passed (or no hash was present)
if hashVerified { if hashVerified {
writer, _ := sc.vfs.Create(cacheKey, size) writer, _ := sc.vfs.Create(cachePath, int64(0)) // size is not known in advance
if writer != nil { if writer != nil {
defer writer.Close() defer writer.Close()
writer.Write(bodyData) // Write the full HTTP response to cache
resp.Body = io.NopCloser(bytes.NewReader(bodyData)) // Reset body for writing
resp.Write(writer)
} }
} else { } else {
logger.Logger.Warn(). logger.Logger.Warn().
@@ -522,7 +563,6 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Str("key", cacheKey). Str("key", cacheKey).
Str("host", r.Host). Str("host", r.Host).
Str("status", "MISS"). Str("status", "MISS").
Bool("hash_verified", hasHash).
Dur("duration", time.Since(tstart)). Dur("duration", time.Since(tstart)).
Msg("request") Msg("request")

View File

@@ -1,10 +1,16 @@
// version/version.go // version/version.go
package version package version
import "time"
var Version string var Version string
var Date string
func init() { func init() {
if Version == "" { if Version == "" {
Version = "0.0.0-dev" Version = "0.0.0-dev"
} }
if Date == "" {
Date = time.Now().Format("2006-01-02 15:04:05")
}
} }