Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always check for new PAC files on darwin. #103

Merged
merged 3 commits into from
Sep 10, 2022
Merged
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: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func whoAmI() string {
}

func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
host := flag.String("l", "localhost", "address to listen on")
port := flag.Int("p", 3128, "port number to listen on")
pacurl := flag.String("C", "", "url of proxy auto-config (pac) file")
Expand Down
28 changes: 13 additions & 15 deletions pacfetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const maxResponseBytes = 1 * 1024 * 1024
var delayAfterFailedDownload = 2 * time.Second

type pacFetcher struct {
pacurl string
pacFinder *pacFinder
monitor netMonitor
client *http.Client
lookupAddr func(context.Context, string) ([]string, error)
Expand Down Expand Up @@ -64,7 +64,7 @@ func newPACFetcher(pacurl string) *pacFetcher {
client.Transport = &http.Transport{Proxy: nil}
}
return &pacFetcher{
pacurl: pacurl,
pacFinder: newPacFinder(pacurl),
monitor: newNetMonitor(),
client: client,
lookupAddr: net.DefaultResolver.LookupAddr,
Expand All @@ -83,22 +83,20 @@ func requireOK(resp *http.Response, err error) (*http.Response, error) {
}

func (pf *pacFetcher) download() []byte {
if !pf.monitor.addrsChanged() {
if !pf.monitor.addrsChanged() && !pf.pacFinder.pacChanged() {
return nil
}
pf.connected = false
pacurl := pf.pacurl
if pacurl == "" {
var err error
pacurl, err = findPACURL()
if err != nil {
log.Printf("Error while trying to detect PAC URL: %v", err)
return nil
} else if pacurl == "" {
log.Println("No PAC URL specified or detected; all requests will be made directly")
return nil
}

pacurl, err := pf.pacFinder.findPACURL()
if err != nil {
log.Printf("Error while trying to detect PAC URL: %v", err)
return nil
} else if pacurl == "" {
log.Println("No PAC URL specified or detected; all requests will be made directly")
return nil
}

log.Printf("Attempting to download PAC from %s", pacurl)
resp, err := requireOK(pf.client.Get(pacurl))
if err != nil {
Expand All @@ -116,7 +114,7 @@ func (pf *pacFetcher) download() []byte {
var buf bytes.Buffer
_, err = io.CopyN(&buf, resp.Body, maxResponseBytes)
if err == io.EOF {
if strings.HasPrefix(pf.pacurl, "file:") {
if strings.HasPrefix(pacurl, "file:") {
// When using a local PAC file the online/offline status can't be determined
// by the fact that the PAC file is returned. Instead try reverse DNS
// resolution of Google's Public DNS Servers.
Expand Down
2 changes: 1 addition & 1 deletion pacfetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestDownloadWithNetworkChanges(t *testing.T) {
s2 := httptest.NewServer(http.HandlerFunc(pacjsHandler("test script 2")))
defer s2.Close()
nm.changed = true
pf.pacurl = s2.URL
pf.pacFinder = newPacFinder(s2.URL)
assert.Equal(t, []byte("test script 2"), pf.download())
assert.True(t, pf.isConnected())
}
Expand Down
154 changes: 94 additions & 60 deletions pacfinder_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,76 +14,110 @@

package main

/*
#cgo LDFLAGS: -framework CoreFoundation -framework SystemConfiguration
#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SystemConfiguration.h>

static SCDynamicStoreRef SCDynamicStoreCreate_trampoline() {
return SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("alpaca"), NULL, NULL);
}

typedef const CFStringRef CFStringRef_Const;

const CFStringRef_Const kProxiesSettings = CFSTR("State:/Network/Global/Proxies");
const CFStringRef_Const kProxiesAutoConfigURLString = CFSTR("ProxyAutoConfigURLString");
const CFStringRef_Const kProxiesProxyAutoConfigEnable = CFSTR("ProxyAutoConfigEnable");

*/
import "C"
import (
"bufio"
"fmt"
"io"
"log"
"os/exec"
"strings"
"unsafe"
)

func findPACURL() (string, error) {
cmd := exec.Command("networksetup", "-listallnetworkservices")
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
type pacFinder struct {
pacUrl string
storeRef C.SCDynamicStoreRef
}

func newPacFinder(pacUrl string) *pacFinder {
if pacUrl != "" {
return &pacFinder{pacUrl, 0}
}
if err := cmd.Start(); err != nil {
return "", err

return &pacFinder{"", C.SCDynamicStoreCreate_trampoline()}
}

func (finder *pacFinder) findPACURL() (string, error) {
if finder.storeRef == 0 {
return finder.pacUrl, nil
}
defer cmd.Wait() //nolint:errcheck
r := bufio.NewReader(stdout)
// Discard the first line, which isn't the name of a network service.
if _, err := r.ReadString('\n'); err != nil {
return "", err

//start := time.Now()
url := finder.getPACUrl()

//elapsed := time.Since(start)
//log.Printf("PacUrl found in %v", elapsed)

return url, nil
}

func (finder *pacFinder) pacChanged() bool {
if url, _ := finder.findPACURL(); finder.pacUrl != url {
finder.pacUrl = url
return true
}
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
} else if err != nil {
return "", err
}
// An asterisk (*) denotes that a network service is disabled; ignore it.
networkService := strings.TrimSuffix(strings.TrimPrefix(line, "(*)"), "\n")
url, err := getAutoProxyURL(networkService)
if err != nil {
log.Printf("Error getting auto proxy URL for %v: %v", networkService, err)
continue
} else if url == "(null)" || url == "" {
continue
}
return url, nil

return false
}

func (finder *pacFinder) getPACUrl() string {
dict := C.CFDictionaryRef(C.SCDynamicStoreCopyValue(finder.storeRef, C.kProxiesSettings))

if dict == 0 {
return ""
}

defer C.CFRelease(C.CFTypeRef(dict))

pacEnabled := C.CFNumberRef(C.CFDictionaryGetValue(dict, unsafe.Pointer(C.kProxiesProxyAutoConfigEnable)))
if pacEnabled == 0 {
return ""
}
return "", nil

var enabled C.int
C.CFNumberGetValue(pacEnabled, C.kCFNumberIntType, unsafe.Pointer(&enabled))
if enabled == 0 {
return ""
}

url := C.CFStringRef_Const(C.CFDictionaryGetValue(dict, unsafe.Pointer(C.kProxiesAutoConfigURLString)))

if url == 0 {
return ""
}

return CFStringToString(url)
}

func getAutoProxyURL(networkService string) (string, error) {
cmd := exec.Command("networksetup", "-getautoproxyurl", networkService)
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
// CGO Helpers below..

// CFStringToString converts a CFStringRef to a string.
func CFStringToString(s C.CFStringRef) string {
p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8)
if p != nil {
return C.GoString(p)
}
if err := cmd.Start(); err != nil {
return "", err
length := C.CFStringGetLength(s)
if length == 0 {
return ""
}
defer cmd.Wait() //nolint:errcheck
r := bufio.NewReader(stdout)
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
} else if err != nil {
return "", err
}
if !strings.HasPrefix(line, "URL: ") {
// Ignore lines without a URL, including the "Enabled" line. Assume that any
// disabled network services might come back online at some point, in which
// case we should start using the PAC URL for that service.
continue
}
return strings.TrimSuffix(strings.TrimPrefix(line, "URL: "), "\n"), nil
maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8)
if maxBufLen == 0 {
return ""
}
return "", fmt.Errorf("No auto-proxy URL for network service %v", networkService)
buf := make([]byte, maxBufLen)
var usedBufLen C.CFIndex
_ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen)
return string(buf[:usedBufLen])
}
93 changes: 29 additions & 64 deletions pacfinder_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,81 +15,46 @@
package main

import (
"os"
"path/filepath"
"bytes"
"os/exec"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFindPACURL(t *testing.T) {
dir, err := os.MkdirTemp("", "alpaca")
require.NoError(t, err)
defer os.RemoveAll(dir)
oldpath := os.Getenv("PATH")
defer require.NoError(t, os.Setenv("PATH", oldpath))
require.NoError(t, os.Setenv("PATH", dir+":"+oldpath))
func TestFindPACURLStatic(t *testing.T) {
pac := "http://internal.anz.com/proxy.pac"
finder := newPacFinder(pac)

tmpfn := filepath.Join(dir, "networksetup")
mockcmd := `#!/bin/sh
listallnetworkservices() {
cat <<EOF
An asterisk (*) denotes that a network service is disabled.
iPhone USB
iPhone USB 2
(*)Wi-Fi
Bluetooth PAN
Thunderbolt Bridge
EOF
foundPac, _ := finder.findPACURL()
require.Equal(t, pac, foundPac)
}

getautoproxyurl() {
if [ "$1" = 'Wi-Fi' ]
then
cat <<EOF
URL: http://internal.anz.com/proxy.pac
Enabled: No
EOF
elif [ "$1" = 'iPhone USB 2' ]
then
cat <<EOF
URL:
Enabled: No
EOF
else
cat <<EOF
URL: (null)
Enabled: No
EOF
fi
func TestFindPACURL(t *testing.T) {
finder := newPacFinder("")

foundPac, _ := finder.findPACURL()

require.NotEqual(t, "", foundPac)
}

if [ "$1" = '-listallnetworkservices' ]
then
listallnetworkservices "$2"
elif [ "$1" = '-getautoproxyurl' ]
then
getautoproxyurl "$2"
else
exit 1
fi
// Removed TestFindPACURLWhenNetworkSetupIsntAvailable - we don't rely on NetworkSetup anymore

exit 0`
require.NoError(t, os.WriteFile(tmpfn, []byte(mockcmd), 0700))
func TestFallbackToDefaultWhenNoPACUrl(t *testing.T) {
// arrange
cmdStr := "scutil --proxy | awk '/ProxyAutoConfigURLString/ {printf $3}'"
cmd := exec.Command("bash", "-c", cmdStr)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
t.Fatal(err)
}

pacURL, err := findPACURL()
require.NoError(t, err)
assert.Equal(t, "http://internal.anz.com/proxy.pac", pacURL)
}
finder := newPacFinder("")

// act
foundPac, _ := finder.findPACURL()

func TestFindPACURLWhenNetworkSetupIsntAvailable(t *testing.T) {
dir, err := os.MkdirTemp("", "alpaca")
require.NoError(t, err)
defer os.RemoveAll(dir)
oldpath := os.Getenv("PATH")
defer require.NoError(t, os.Setenv("PATH", oldpath))
require.NoError(t, os.Setenv("PATH", dir))
_, err = findPACURL()
require.NotNil(t, err)
// assert
require.Equal(t, out.String(), foundPac)
}
Loading