Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
__debug_bin*
.idea
.DS_Store
cache/
cache/
Comment on lines +7 to +8

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we just need this once?

Suggested change
cache/
cache/
cache/

6 changes: 3 additions & 3 deletions blocklist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
17 changes: 9 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
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 (
github.com/alessio/shellescape v1.4.1 // indirect
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
)
26 changes: 16 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 24 additions & 32 deletions pacfetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -25,50 +24,52 @@ 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 {
pacFinder *pacFinder
monitor netMonitor
client *http.Client
connected bool
//cache []byte
//modified time.Time
//fetched time.Time
//expiry time.Time
//etag string

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good - this commented out code can stay removed, but we should maintain all the other comments that have been deleted.

}

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,
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to leave the original client initialisation above the if-else statement, so we don't repeat the timeout twice:

client := &http.Client{Timeout: 30 * time.Second}

And then for the else branch, maybe something like this?

Suggested change
}
client.Transport := httpcache.NewTransport(httpcache.NewMemoryCache())
// 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.Transport = &http.Transport{Proxy: nil}

}

return &pacFetcher{
pacFinder: newPacFinder(pacurl),
monitor: newNetMonitor(),
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
// <https://github.com/samuong/alpaca/issues/165>.
pf.client.CloseIdleConnections()

pacurl, err := pf.pacFinder.findPACURL()
Expand All @@ -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 {
Expand All @@ -191,4 +183,4 @@ func (pf *pacFetcher) download() []byte {

func (pf *pacFetcher) isConnected() bool {
return pf.connected
}
}
27 changes: 27 additions & 0 deletions pacfetcher_cache_test.go
Original file line number Diff line number Diff line change
@@ -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)")
}
}
2 changes: 1 addition & 1 deletion pacfinder_darwin_test.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pacfinder_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

package main

type pacFinder struct{
type pacFinder struct {
pacURL string
}

Expand Down
6 changes: 3 additions & 3 deletions transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
Expand Down