go-pac provides a cross-platform way to retrieve the Proxy Auto-Configuration (PAC) URL from the operating system and evaluate PAC scripts for Go HTTP clients.
GetPACURL()reads the OS PAC URL and returns it as*url.URL.NewPACProxy()downloads and evaluates the PAC script, then returns a*PACProxy.PACProxy.FindProxyStringForURL()runs the PAC script for a target URL.PACProxy.ProxyFunc()returns ahttp.Transport.Proxycompatible function.
go get github.com/phlipse/go-pacpackage main
import (
"log"
"net/http"
"github.com/phlipse/go-pac"
)
func main() {
pacURL, err := pac.GetPACURL()
if err != nil {
log.Fatalf("get PAC URL: %v", err)
}
proxy, err := pac.NewPACProxy(pacURL, &pac.PACProxyConfig{})
if err != nil {
log.Fatalf("new PAC proxy: %v", err)
}
client := &http.Client{
Transport: &http.Transport{
Proxy: proxy.ProxyFunc(),
},
}
resp, err := client.Get("http://example.com")
if err != nil {
log.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
}func GetPACURL() (*url.URL, error)Reads the PAC URL from the operating system:
- Windows: registry
AutoConfigURL - macOS:
scutil --proxyoutput - Linux (GNOME):
gsettings get org.gnome.system.proxy autoconfig-url
Errors:
ErrPACURLNotFoundwhen no PAC URL is configured.ErrPACURLEmptywhen a PAC key exists but is empty.
func NewPACProxy(pacURL *url.URL, config *PACProxyConfig) (*PACProxy, error)Downloads the PAC script, evaluates it in the JavaScript runtime, and returns a PACProxy.
Errors:
ErrFetchPACScriptfor HTTP/network errors or non-200 status.ErrReadPACScriptfor read errors.ErrExecutePACScriptfor script execution errors.ErrPACScriptTooLargewhen the script exceedsMaxScriptSize.
type PACProxy struct {
// opaque
}Methods:
func (p *PACProxy) FindProxyStringForURL(targetURL *url.URL) (ProxyString, error)
func (p *PACProxy) ProxyFunc() func(*http.Request) (*url.URL, error)FindProxyStringForURL executes FindProxyForURL(url, host) inside the PAC script and returns the raw ProxyString.
ProxyFunc converts the ProxyString into a *url.URL suitable for http.Transport.Proxy.
Errors:
ErrEvaluatePACifFindProxyForURLis missing or execution fails.ErrConvertResultif the PAC result is not a string.ErrPACScriptTimeoutwhen execution exceeds the configured timeout.
type PACProxyConfig struct {
Client *http.Client
MaxScriptSize int64
ScriptTimeout time.Duration
DNSLookupTimeout time.Duration
HTTPTimeout time.Duration
Logger Logger
LogHook LogHook
}Defaults (used when values are zero):
HTTPTimeout: 10s (only used whenClient == nil)ScriptTimeout: 5sDNSLookupTimeout: 2sMaxScriptSize: 1 MiB
Disable a timeout or size limit by setting a negative value.
You can inject a logger via PACProxyConfig.Logger.
It uses a minimal interface and accepts key/value pairs (slog-style).
For central redaction/filters, use LogHook.
Example with slog:
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
_, _ := pac.NewPACProxy(pacURL, &pac.PACProxyConfig{
Logger: pac.LoggerFunc(func(ctx context.Context, level pac.LogLevel, msg string, args ...any) {
var slogLevel slog.Level
switch level {
case pac.LogDebug:
slogLevel = slog.LevelDebug
case pac.LogInfo:
slogLevel = slog.LevelInfo
case pac.LogWarn:
slogLevel = slog.LevelWarn
case pac.LogError:
slogLevel = slog.LevelError
}
logger.Log(ctx, slogLevel, msg, args...)
}),
LogHook: pac.RedactKeysHook("url", "proxy"),
})type ProxyString string
func (ps ProxyString) Parse() (*url.URL, error)Parses the PAC result. Supported directives:
DIRECTPROXY host:portSOCKS host:port(mapped tosocks5://)
If multiple directives are returned (e.g. PROXY a:1; PROXY b:2; DIRECT), the first valid one is used. If none is valid, ErrNoValidProxy is returned.
- PAC execution is serialized inside a single
PACProxyinstance (per script). Use multiple instances if you want to avoid lock contention. - PAC scripts are executed with a JavaScript runtime (goja). The standard PAC helper functions are implemented.
Run unit tests with the PAC URL lookup mocked:
go test -tags unit ./...Without the unit tag, tests will use the real OS PAC URL lookup.