diff --git a/.gitignore b/.gitignore index 03c657a..acf34f8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __debug_bin* .idea .DS_Store +cache/ +cache/ diff --git a/blocklist_test.go b/blocklist_test.go index ac4425a..642bcfb 100644 --- a/blocklist_test.go +++ b/blocklist_test.go @@ -49,10 +49,10 @@ func TestBlocklistDuplicateEntry(t *testing.T) { var now time.Time b.now = func() time.Time { return now } b.add("foo") - now = now.Add(3*time.Minute) + now = now.Add(3 * time.Minute) b.add("foo") - now = now.Add(3*time.Minute) + now = now.Add(3 * time.Minute) b.contains("foo") - now = now.Add(3*time.Minute) + now = now.Add(3 * time.Minute) b.contains("foo") } diff --git a/go.mod b/go.mod index 09b11da..b94a153 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,16 @@ module github.com/samuong/alpaca/v2 -go 1.22.3 - -toolchain go1.22.4 +go 1.25.3 require ( github.com/gobwas/glob v0.2.3 github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 github.com/robertkrimen/otto v0.4.0 github.com/samuong/go-ntlmssp v0.0.0-20240616070040-65a20607c744 - github.com/stretchr/testify v1.9.0 + github.com/sandrolain/httpcache v1.4.0 + github.com/stretchr/testify v1.11.1 github.com/zalando/go-keyring v0.2.5 - golang.org/x/term v0.21.0 + golang.org/x/term v0.36.0 ) require ( @@ -19,10 +18,12 @@ require ( github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9e3d582..5de0c17 100644 --- a/go.sum +++ b/go.sum @@ -8,28 +8,34 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robertkrimen/otto v0.4.0 h1:/c0GRrK1XDPcgIasAsnlpBT5DelIeB9U/Z/JCQsgr7E= github.com/robertkrimen/otto v0.4.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw= github.com/samuong/go-ntlmssp v0.0.0-20240616070040-65a20607c744 h1:AD1UeK7fZRLY7TEeQQZNTuHX3RAspwLUC36mNi47Xcs= github.com/samuong/go-ntlmssp v0.0.0-20240616070040-65a20607c744/go.mod h1:ioghl8+axI3Mx5Cs1LU/LzW18JE71qbwXwpOv/F9lCc= +github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQKO9E0= +github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= diff --git a/main_test.go b/main_test.go index 3ff9cd1..e685473 100644 --- a/main_test.go +++ b/main_test.go @@ -17,6 +17,7 @@ // machines. So the tests in this file are disabled by default using a build // constraint, and need to be run using `go test ./... -tags=squid`. +//go:build squid // +build squid package main diff --git a/pacfetcher.go b/pacfetcher.go index 723e286..88713ca 100644 --- a/pacfetcher.go +++ b/pacfetcher.go @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - package main import ( @@ -25,18 +24,13 @@ import ( "runtime" "strings" "time" + + "github.com/sandrolain/httpcache" ) -// The maximum size (in bytes) allowed for a PAC script. At 1 MB, this matches the limit in Chrome. const maxResponseBytes = 1 * 1024 * 1024 - -// The maximum size (in bytes) allowed for a data URL. -// Chromium and Firefox limit data URLs to 512MB. -// See https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data#length_limitations const maxDataURLLength = 512 * 1024 * 1024 -// The time to wait before retrying a failed PAC download. This is similar to Chrome's delay: -// https://cs.chromium.org/chromium/src/net/proxy_resolution/proxy_resolution_service.cc?l=96&rcl=3db5f65968c3ecab3932c1ff7367ad28834f9502 var delayAfterFailedDownload = 2 * time.Second type pacFetcher struct { @@ -44,31 +38,38 @@ type pacFetcher struct { monitor netMonitor client *http.Client connected bool - //cache []byte - //modified time.Time - //fetched time.Time - //expiry time.Time - //etag string } func newPACFetcher(pacurl string) *pacFetcher { - client := &http.Client{Timeout: 30 * time.Second} + var client *http.Client + if strings.HasPrefix(pacurl, "file:") { log.Print("Warning: When using a local PAC file, the online/offline status can't ", "be determined by the fact that the PAC file is downloaded. Make sure you ", "check for proxy connectivity in your PAC file!") + + client = &http.Client{Timeout: 30 * time.Second} + if runtime.GOOS == "windows" { client.Transport = http.NewFileTransport(http.Dir("C:")) } else { client.Transport = http.NewFileTransport(http.Dir("/")) } + } else { - // The DefaultClient in net/http uses the proxy specified in the http(s)_proxy - // environment variable, which could be pointing at this instance of alpaca. When - // fetching the PAC file, we always use a client that goes directly to the server, - // rather than via a proxy. - client.Transport = &http.Transport{Proxy: nil} + // Base transport without proxy (important: avoid proxy loop) + baseTransport := &http.Transport{Proxy: nil} + + // ✅ Use in-memory cache (FIXED) + cacheTransport := httpcache.NewTransport(httpcache.NewMemoryCache()) + cacheTransport.Transport = baseTransport + + client = &http.Client{ + Timeout: 30 * time.Second, + Transport: cacheTransport, + } } + return &pacFetcher{ pacFinder: newPacFinder(pacurl), monitor: newNetMonitor(), @@ -87,12 +88,8 @@ func requireOK(resp *http.Response, err error) (*http.Response, error) { } } -// decodeDataURL decodes a data URL (e.g., data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==). -// It supports both base64 and URL-encoded data, and enforces the maxResponseBytes size limit. -// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs for details. func decodeDataURL(uri string) ([]byte, error) { parsedURL, err := url.Parse(uri) - if err != nil { return nil, fmt.Errorf("error parsing pac url: %w", err) } @@ -127,17 +124,11 @@ func decodeDataURL(uri string) ([]byte, error) { } func (pf *pacFetcher) download() []byte { - // TODO: Combine pacChanged() and findPACURL() as described in - // https://github.com/samuong/alpaca/pull/156#issuecomment-3125070335 if !pf.monitor.addrsChanged() && !pf.pacFinder.pacChanged() { return nil } pf.connected = false - // We've just detected a change in network state, so close any "idle" - // connections from the previous network. This forces a fresh DNS - // lookup and TCP dial during the next PAC download. For context, see - // . pf.client.CloseIdleConnections() pacurl, err := pf.pacFinder.findPACURL() @@ -164,17 +155,18 @@ func (pf *pacFetcher) download() []byte { resp, err := requireOK(pf.client.Get(pacurl)) if err != nil { - // Sometimes, if we try to download too soon after a network change, the PAC - // download can fail. See https://github.com/samuong/alpaca/issues/8 for details. log.Printf("Error downloading PAC file, will retry after %v: %q", delayAfterFailedDownload, err) + time.Sleep(delayAfterFailedDownload) + if resp, err = requireOK(pf.client.Get(pacurl)); err != nil { log.Printf("Error downloading PAC file, giving up: %q", err) return nil } } defer resp.Body.Close() + var buf bytes.Buffer _, err = io.CopyN(&buf, resp.Body, maxResponseBytes) if err == io.EOF { @@ -191,4 +183,4 @@ func (pf *pacFetcher) download() []byte { func (pf *pacFetcher) isConnected() bool { return pf.connected -} +} \ No newline at end of file diff --git a/pacfetcher_cache_test.go b/pacfetcher_cache_test.go new file mode 100644 index 0000000..e011ef9 --- /dev/null +++ b/pacfetcher_cache_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestPACCacheNoFallback(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("function FindProxyForURL() { return 'DIRECT'; }")) + })) + + pf := newPACFetcher(server.URL) + + data := pf.download() + if data == nil { + t.Fatal("expected PAC data on first fetch") + } + + server.Close() + + data = pf.download() + if data != nil { + t.Fatal("expected nil when server is down (no cache fallback)") + } +} diff --git a/pacfinder_darwin_test.go b/pacfinder_darwin_test.go index 28e9ec2..1781865 100644 --- a/pacfinder_darwin_test.go +++ b/pacfinder_darwin_test.go @@ -1,4 +1,4 @@ -// Copyright 2019, 2021, 2022 The Alpaca Authors +// Copyright 2019, 2021, 2022, 2025 The Alpaca Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pacfinder_windows.go b/pacfinder_windows.go index ed141d2..6fedc07 100644 --- a/pacfinder_windows.go +++ b/pacfinder_windows.go @@ -14,7 +14,7 @@ package main -type pacFinder struct{ +type pacFinder struct { pacURL string } diff --git a/transport_test.go b/transport_test.go index 04d0f6a..8ea31d1 100644 --- a/transport_test.go +++ b/transport_test.go @@ -69,18 +69,18 @@ func TestTransportErrors(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://alpaca.test", nil) require.NoError(t, err) - t.Run("NotConnected", func (t *testing.T) { + t.Run("NotConnected", func(t *testing.T) { _, err = tr.RoundTrip(req) assert.Error(t, err) }) - t.Run("Closed", func (t *testing.T) { + t.Run("Closed", func(t *testing.T) { require.NoError(t, tr.Close()) _, err = tr.RoundTrip(req) assert.Error(t, err) }) - t.Run("CloseTwice", func (t *testing.T) { + t.Run("CloseTwice", func(t *testing.T) { assert.NoError(t, tr.Close()) assert.NoError(t, tr.Close()) })