Skip to content

Commit 226f06f

Browse files
committed
context-aware API variants and fix cross-platform app config testscleaning golancilint err
1 parent 3862d0a commit 226f06f

14 files changed

Lines changed: 1255 additions & 161 deletions

README.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
Cross-platform system proxy management for Go — set, clear, and query the OS proxy from your application without shell scripts.
88

99
```go
10-
sysproxy.Set("socks5://user:pass@proxy.example.com:1080", sysproxy.ScopeGlobal)
11-
sysproxy.Unset(sysproxy.ScopeGlobal)
10+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
11+
defer cancel()
12+
13+
if err := sysproxy.SetContext(ctx, "socks5://user:pass@proxy.example.com:1080", sysproxy.ScopeGlobal); err != nil {
14+
log.Fatal(err)
15+
}
16+
defer sysproxy.UnsetContext(ctx, sysproxy.ScopeGlobal)
1217
```
1318

1419
## Why
1520

1621
Proxy-switching tools, VPN clients, and network-aware CLIs built in Go often need to set the OS system proxy — not just read it. The existing options are either buried inside a large SDK ([outline-sdk/x/sysproxy](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/sysproxy)), Windows-only, or rely on shipping pre-built binaries.
1722

18-
`go-sysproxy` is a focused, standalone package: macOS (`networksetup`), Linux (GNOME + KDE + `/etc/environment`), and Windows (registry + Credential Manager), with health checking, per-app config, and temporary proxy restore — zero external dependencies.
23+
`go-sysproxy` is a focused package for macOS (`networksetup`), Linux (GNOME + KDE + `/etc/environment`), and Windows (registry + Credential Manager). It covers system proxy changes, health checks, per-app config, and temporary proxy restore without external dependencies.
1924

2025
## Installation
2126

@@ -39,18 +44,19 @@ import (
3944
)
4045

4146
func main() {
42-
// Verify the proxy is reachable before committing
4347
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
4448
defer cancel()
49+
50+
// Verify the proxy is reachable before committing
4551
if err := sysproxy.Check(ctx, "http://proxy.example.com:8080"); err != nil {
4652
log.Fatal(err)
4753
}
4854

49-
// Set system proxy globally
50-
if err := sysproxy.Set("http://proxy.example.com:8080", sysproxy.ScopeGlobal); err != nil {
55+
// Apply the system proxy and restore it on exit
56+
if err := sysproxy.SetContext(ctx, "http://proxy.example.com:8080", sysproxy.ScopeGlobal); err != nil {
5157
log.Fatal(err)
5258
}
53-
defer sysproxy.Unset(sysproxy.ScopeGlobal)
59+
defer sysproxy.UnsetContext(ctx, sysproxy.ScopeGlobal)
5460
}
5561
```
5662

@@ -73,6 +79,18 @@ err = sysproxy.Unset(sysproxy.ScopeGlobal)
7379
url, err := sysproxy.Get() // reads current system proxy
7480
```
7581

82+
The plain wrappers stay available for backward compatibility. If you want cancellation and deadlines, use the context-aware variants:
83+
84+
```go
85+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
86+
defer cancel()
87+
88+
err := sysproxy.SetContext(ctx, "http://user:pass@proxy.example.com:8080", sysproxy.ScopeGlobal)
89+
err = sysproxy.UnsetContext(ctx, sysproxy.ScopeGlobal)
90+
91+
url, err := sysproxy.GetContext(ctx)
92+
```
93+
7694
### Per-protocol proxy
7795

7896
```go
@@ -84,12 +102,16 @@ err := sysproxy.SetMulti(sysproxy.ProxyConfig{
84102
}, sysproxy.ScopeGlobal)
85103
```
86104

105+
`SetMultiContext` is also available when you want the same API with cancellation support.
106+
87107
### PAC file
88108

89109
```go
90110
err := sysproxy.SetPAC("https://config.example.com/proxy.pac", sysproxy.ScopeGlobal)
91111
```
92112

113+
`SetPACContext` is also available for deadline-aware callers.
114+
93115
### Temporary proxy
94116

95117
`WithProxy` sets the proxy for the duration of `fn` and restores the previous state on return — even if `fn` returns an error.
@@ -130,6 +152,8 @@ sysproxy.WriteAppConfig(sysproxy.AppWget, "http://proxy.example.com:8080") // ~/
130152
sysproxy.ClearAppConfig(sysproxy.AppGit)
131153
```
132154

155+
`WriteAppConfigContext` and `ClearAppConfigContext` are available for `git` and `npm`, where configuration is applied through external commands.
156+
133157
### Logging / auditing
134158

135159
```go
@@ -164,6 +188,12 @@ _ = sysproxy.WriteAppConfig(sysproxy.AppCurl, "http://username:password@proxy.pr
164188

165189
> Credentials in proxy URLs are handled by the OS — on Windows they are stored in Credential Manager, not written to disk in plaintext.
166190
191+
## Notes
192+
193+
- `Check` verifies TCP reachability of the proxy endpoint. It does not validate credentials or perform a protocol-level handshake.
194+
- Context-aware APIs abort before starting side effects when the context is already canceled, and command-backed operations use `exec.CommandContext`.
195+
- `ScopeGlobal` may still require elevated permissions depending on the platform and the target settings store.
196+
167197
## Platform support
168198

169199
| Feature | macOS | Linux (GNOME) | Linux (KDE) | Windows |

appconfig.go

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sysproxy
22

33
import (
44
"bufio"
5+
"context"
56
"fmt"
67
"os"
78
"os/exec"
@@ -26,17 +27,27 @@ const (
2627
//
2728
// err := sysproxy.WriteAppConfig(sysproxy.AppGit, "http://proxy.example.com:8080")
2829
func WriteAppConfig(app AppName, proxyURL string) error {
30+
return WriteAppConfigContext(context.Background(), app, proxyURL)
31+
}
32+
33+
// WriteAppConfigContext writes proxy settings to the tool-specific config for
34+
// app. It aborts before side effects if ctx is already canceled.
35+
func WriteAppConfigContext(ctx context.Context, app AppName, proxyURL string) error {
2936
if err := validateProxyURL(proxyURL); err != nil {
3037
return err
3138
}
39+
ctx = normalizeContext(ctx)
40+
if err := ctx.Err(); err != nil {
41+
return err
42+
}
3243
var err error
3344
switch app {
3445
case AppCurl:
3546
err = writeCurlRC(proxyURL)
3647
case AppGit:
37-
err = writeGitProxy(proxyURL)
48+
err = writeGitProxy(ctx, proxyURL)
3849
case AppNPM:
39-
err = writeNPMProxy(proxyURL)
50+
err = writeNPMProxy(ctx, proxyURL)
4051
case AppPip:
4152
err = writePipConf(proxyURL)
4253
case AppWget:
@@ -50,14 +61,24 @@ func WriteAppConfig(app AppName, proxyURL string) error {
5061

5162
// ClearAppConfig removes proxy settings from the tool-specific config for app.
5263
func ClearAppConfig(app AppName) error {
64+
return ClearAppConfigContext(context.Background(), app)
65+
}
66+
67+
// ClearAppConfigContext removes proxy settings from the tool-specific config
68+
// for app. It aborts before side effects if ctx is already canceled.
69+
func ClearAppConfigContext(ctx context.Context, app AppName) error {
70+
ctx = normalizeContext(ctx)
71+
if err := ctx.Err(); err != nil {
72+
return err
73+
}
5374
var err error
5475
switch app {
5576
case AppCurl:
5677
err = clearCurlRC()
5778
case AppGit:
58-
err = clearGitProxy()
79+
err = clearGitProxy(ctx)
5980
case AppNPM:
60-
err = clearNPMProxy()
81+
err = clearNPMProxy(ctx)
6182
case AppPip:
6283
err = clearPipConf()
6384
case AppWget:
@@ -89,57 +110,57 @@ func clearCurlRC() error {
89110

90111
// ── git (git config --global) ─────────────────────────────────────────────────
91112

92-
func runGit(args ...string) error {
93-
return exec.Command("git", args...).Run() //nolint:noctx,gosec
113+
func runGit(ctx context.Context, args ...string) error {
114+
return exec.CommandContext(normalizeContext(ctx), "git", args...).Run() //nolint:gosec
94115
}
95116

96-
func writeGitProxy(proxyURL string) error {
117+
func writeGitProxy(ctx context.Context, proxyURL string) error {
97118
if !isAvailable("git") {
98119
return fmt.Errorf("sysproxy: git not found in PATH")
99120
}
100-
if err := runGit("config", "--global", "http.proxy", proxyURL); err != nil {
121+
if err := runGit(ctx, "config", "--global", "http.proxy", proxyURL); err != nil {
101122
return fmt.Errorf("sysproxy: git config http.proxy: %w", err)
102123
}
103-
if err := runGit("config", "--global", "https.proxy", proxyURL); err != nil {
124+
if err := runGit(ctx, "config", "--global", "https.proxy", proxyURL); err != nil {
104125
return fmt.Errorf("sysproxy: git config https.proxy: %w", err)
105126
}
106127
return nil
107128
}
108129

109-
func clearGitProxy() error {
130+
func clearGitProxy(ctx context.Context) error {
110131
if !isAvailable("git") {
111132
return fmt.Errorf("sysproxy: git not found in PATH")
112133
}
113-
_ = runGit("config", "--global", "--unset", "http.proxy")
114-
_ = runGit("config", "--global", "--unset", "https.proxy")
134+
_ = runGit(ctx, "config", "--global", "--unset", "http.proxy")
135+
_ = runGit(ctx, "config", "--global", "--unset", "https.proxy")
115136
return nil
116137
}
117138

118139
// ── npm (npm config set) ──────────────────────────────────────────────────────
119140

120-
func runNPM(args ...string) error {
121-
return exec.Command("npm", args...).Run() //nolint:noctx,gosec
141+
func runNPM(ctx context.Context, args ...string) error {
142+
return exec.CommandContext(normalizeContext(ctx), "npm", args...).Run() //nolint:gosec
122143
}
123144

124-
func writeNPMProxy(proxyURL string) error {
145+
func writeNPMProxy(ctx context.Context, proxyURL string) error {
125146
if !isAvailable("npm") {
126147
return fmt.Errorf("sysproxy: npm not found in PATH")
127148
}
128-
if err := runNPM("config", "set", "proxy", proxyURL); err != nil {
149+
if err := runNPM(ctx, "config", "set", "proxy", proxyURL); err != nil {
129150
return fmt.Errorf("sysproxy: npm config set proxy: %w", err)
130151
}
131-
if err := runNPM("config", "set", "https-proxy", proxyURL); err != nil {
152+
if err := runNPM(ctx, "config", "set", "https-proxy", proxyURL); err != nil {
132153
return fmt.Errorf("sysproxy: npm config set https-proxy: %w", err)
133154
}
134155
return nil
135156
}
136157

137-
func clearNPMProxy() error {
158+
func clearNPMProxy(ctx context.Context) error {
138159
if !isAvailable("npm") {
139160
return fmt.Errorf("sysproxy: npm not found in PATH")
140161
}
141-
_ = runNPM("config", "delete", "proxy")
142-
_ = runNPM("config", "delete", "https-proxy")
162+
_ = runNPM(ctx, "config", "delete", "proxy")
163+
_ = runNPM(ctx, "config", "delete", "https-proxy")
143164
return nil
144165
}
145166

0 commit comments

Comments
 (0)