Skip to content

Commit f86f75d

Browse files
Add proxy support (#1120)
* - Read proxy env variables and config settings - Define a function to test that a given path is an active UDS * Expand paths to the OS user's home directory if they start with "~/" or "%USERPROFILE%\". * Switch from using "os/user" and `user.Current()` for finding the OS user's home dir, to using "os" and `os.UserHomeDir()`. It requires Go 1.12+. * Use the UNIX Domain Socket when making connections * Add SRC_PROXY_SOCKET to the usage output, and remove config settings for a proxy URL. * Document expandHomeDir function * Add support for HTTP(S) and SOCKS5 proxies * Mollify tests * Add tests * Refactor: move the proxy handling to a separate file. * Add tests for a UNIX Domain Socket. * Fix analysis of url schemes * Ad tests for socks proxies in the config * SOCKS proxies work OOTB, so remove the manual dialing for them. * Use `tls.HandshakeContext` * - add a proxy test script - remove "endpoint" from all of the proxy names. The environment variable is now "SRC_PROXY" - clean up the http(s) proxy dialing code. Experiemented with using http.Request instead of spelling out the CONNECT request manually, but it had enough quircks that I went back to spelling it out manually. - Add more desriptive messages to the socket config test. * fix socket test by shortening the length of the file path - UNIX socket paths need to be less than 108-ish characters. * Fix parameter capitalization * Add comment about th UDS path length to main_test.go * Fix formatting in usage message * Whoops; meant to use `w` instead of `v`! Thanks for catching that. Co-authored-by: Camden Cheek <[email protected]> * Whoops; meant to use `w` instead of `v`! Thanks for catching that. Co-authored-by: Camden Cheek <[email protected]> * Update comment about InsecureSkipVerify * Make CHANGELOG entry --------- Co-authored-by: Camden Cheek <[email protected]>
1 parent bca0a8b commit f86f75d

File tree

10 files changed

+651
-30
lines changed

10 files changed

+651
-30
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ All notable changes to `src-cli` are documented in this file.
1111

1212
## Unreleased
1313

14+
### Added
15+
16+
- Support HTTP(S), SOCKS5, and UNIX Domain Socket proxies via SRC_PROXY environment variable. [#1120](https://github.com/sourcegraph/src-cli/pull/1120)
17+
1418
## 5.8.1
1519

1620
### Fixed

cmd/src/login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func loginCmd(ctx context.Context, cfg *config, client api.Client, endpointArg s
8181

8282
if cfg.ConfigFilePath != "" {
8383
fmt.Fprintln(out)
84-
fmt.Fprintf(out, "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT and SRC_ACCESS_TOKEN instead, and then remove %s. See https://github.com/sourcegraph/src-cli#readme for more information.\n", cfg.ConfigFilePath)
84+
fmt.Fprintf(out, "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT, SRC_ACCESS_TOKEN, and SRC_PROXY instead, and then remove %s. See https://github.com/sourcegraph/src-cli#readme for more information.\n", cfg.ConfigFilePath)
8585
}
8686

8787
noToken := cfg.AccessToken == ""

cmd/src/login_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestLogin(t *testing.T) {
4949
if err != cmderrors.ExitCode1 {
5050
t.Fatal(err)
5151
}
52-
wantOut := "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT and SRC_ACCESS_TOKEN instead, and then remove f. See https://github.com/sourcegraph/src-cli#readme for more information.\n\n❌ Problem: No access token is configured.\n\n🛠 To fix: Create an access token at https://example.com/user/settings/tokens, then set the following environment variables:\n\n SRC_ENDPOINT=https://example.com\n SRC_ACCESS_TOKEN=(the access token you just created)\n\n To verify that it's working, run this command again."
52+
wantOut := "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT, SRC_ACCESS_TOKEN, and SRC_PROXY instead, and then remove f. See https://github.com/sourcegraph/src-cli#readme for more information.\n\n❌ Problem: No access token is configured.\n\n🛠 To fix: Create an access token at https://example.com/user/settings/tokens, then set the following environment variables:\n\n SRC_ENDPOINT=https://example.com\n SRC_ACCESS_TOKEN=(the access token you just created)\n\n To verify that it's working, run this command again."
5353
if out != wantOut {
5454
t.Errorf("got output %q, want %q", out, wantOut)
5555
}

cmd/src/main.go

Lines changed: 148 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import (
55
"flag"
66
"io"
77
"log"
8+
"net"
9+
"net/url"
810
"os"
9-
"os/user"
1011
"path/filepath"
1112
"strings"
1213

@@ -25,6 +26,20 @@ Usage:
2526
Environment variables
2627
SRC_ACCESS_TOKEN Sourcegraph access token
2728
SRC_ENDPOINT endpoint to use, if unset will default to "https://sourcegraph.com"
29+
SRC_PROXY A proxy to use for proxying requests to the Sourcegraph endpoint.
30+
Supports HTTP(S), SOCKS5/5h, and UNIX Domain Socket proxies.
31+
If a UNIX Domain Socket, the path can be either an absolute path,
32+
or can start with ~/ or %USERPROFILE%\ for a path in the user's home directory.
33+
Examples:
34+
- https://localhost:3080
35+
- https://<user>:<password>localhost:8080
36+
- socks5h://localhost:1080
37+
- socks5://<username>:<password>@localhost:1080
38+
- unix://~/src-proxy.sock
39+
- unix://%USERPROFILE%\src-proxy.sock
40+
- ~/src-proxy.sock
41+
- %USERPROFILE%\src-proxy.sock
42+
- C:\some\path\src-proxy.sock
2843
2944
The options are:
3045
@@ -83,8 +98,10 @@ type config struct {
8398
Endpoint string `json:"endpoint"`
8499
AccessToken string `json:"accessToken"`
85100
AdditionalHeaders map[string]string `json:"additionalHeaders"`
86-
87-
ConfigFilePath string
101+
Proxy string `json:"proxy"`
102+
ProxyURL *url.URL
103+
ProxyPath string
104+
ConfigFilePath string
88105
}
89106

90107
// apiClient returns an api.Client built from the configuration.
@@ -95,32 +112,25 @@ func (c *config) apiClient(flags *api.Flags, out io.Writer) api.Client {
95112
AdditionalHeaders: c.AdditionalHeaders,
96113
Flags: flags,
97114
Out: out,
115+
ProxyURL: c.ProxyURL,
116+
ProxyPath: c.ProxyPath,
98117
})
99118
}
100119

101-
var testHomeDir string // used by tests to mock the user's $HOME
102-
103120
// readConfig reads the config file from the given path.
104121
func readConfig() (*config, error) {
105-
cfgPath := *configPath
122+
cfgFile := *configPath
106123
userSpecified := *configPath != ""
107124

108-
var homeDir string
109-
if testHomeDir != "" {
110-
homeDir = testHomeDir
111-
} else {
112-
u, err := user.Current()
113-
if err != nil {
114-
return nil, err
115-
}
116-
homeDir = u.HomeDir
125+
if !userSpecified {
126+
cfgFile = "~/src-config.json"
117127
}
118128

119-
if !userSpecified {
120-
cfgPath = filepath.Join(homeDir, "src-config.json")
121-
} else if strings.HasPrefix(cfgPath, "~/") {
122-
cfgPath = filepath.Join(homeDir, cfgPath[2:])
129+
cfgPath, err := expandHomeDir(cfgFile)
130+
if err != nil {
131+
return nil, err
123132
}
133+
124134
data, err := os.ReadFile(os.ExpandEnv(cfgPath))
125135
if err != nil && (!os.IsNotExist(err) || userSpecified) {
126136
return nil, err
@@ -135,10 +145,12 @@ func readConfig() (*config, error) {
135145

136146
envToken := os.Getenv("SRC_ACCESS_TOKEN")
137147
envEndpoint := os.Getenv("SRC_ENDPOINT")
148+
envProxy := os.Getenv("SRC_PROXY")
138149

139150
if userSpecified {
140-
// If a config file is present, either zero or both environment variables must be present.
151+
// If a config file is present, either zero or both required environment variables must be present.
141152
// We don't want to partially apply environment variables.
153+
// Note that SRC_PROXY is optional so we don't test for it.
142154
if envToken == "" && envEndpoint != "" {
143155
return nil, errConfigMerge
144156
}
@@ -157,6 +169,60 @@ func readConfig() (*config, error) {
157169
if cfg.Endpoint == "" {
158170
cfg.Endpoint = "https://sourcegraph.com"
159171
}
172+
if envProxy != "" {
173+
cfg.Proxy = envProxy
174+
}
175+
176+
if cfg.Proxy != "" {
177+
178+
parseEndpoint := func(endpoint string) (scheme string, address string) {
179+
parts := strings.SplitN(endpoint, "://", 2)
180+
if len(parts) == 2 {
181+
return parts[0], parts[1]
182+
}
183+
return "", endpoint
184+
}
185+
186+
urlSchemes := []string{"http", "https", "socks", "socks5", "socks5h"}
187+
188+
isURLScheme := func(scheme string) bool {
189+
for _, s := range urlSchemes {
190+
if scheme == s {
191+
return true
192+
}
193+
}
194+
return false
195+
}
196+
197+
scheme, address := parseEndpoint(cfg.Proxy)
198+
199+
if isURLScheme(scheme) {
200+
endpoint := cfg.Proxy
201+
// assume socks means socks5, because that's all we support
202+
if scheme == "socks" {
203+
endpoint = "socks5://" + address
204+
}
205+
cfg.ProxyURL, err = url.Parse(endpoint)
206+
if err != nil {
207+
return nil, err
208+
}
209+
} else if scheme == "" || scheme == "unix" {
210+
path, err := expandHomeDir(address)
211+
if err != nil {
212+
return nil, err
213+
}
214+
isValidUDS, err := isValidUnixSocket(path)
215+
if err != nil {
216+
return nil, errors.Newf("Invalid proxy configuration: %w", err)
217+
}
218+
if !isValidUDS {
219+
return nil, errors.Newf("invalid proxy socket: %s", path)
220+
}
221+
cfg.ProxyPath = path
222+
} else {
223+
return nil, errors.Newf("invalid proxy endpoint: %s", cfg.Proxy)
224+
}
225+
}
160226

161227
cfg.AdditionalHeaders = parseAdditionalHeaders()
162228
// Ensure that we're not clashing additonal headers
@@ -178,3 +244,65 @@ func readConfig() (*config, error) {
178244
func cleanEndpoint(urlStr string) string {
179245
return strings.TrimSuffix(urlStr, "/")
180246
}
247+
248+
// isValidUnixSocket checks if the given path is a valid Unix socket.
249+
//
250+
// Parameters:
251+
// - path: A string representing the file path to check.
252+
//
253+
// Returns:
254+
// - bool: true if the path is a valid Unix socket, false otherwise.
255+
// - error: nil if the check was successful, or an error if an unexpected issue occurred.
256+
//
257+
// The function attempts to establish a connection to the Unix socket at the given path.
258+
// If the connection succeeds, it's considered a valid Unix socket.
259+
// If the file doesn't exist, it returns false without an error.
260+
// For any other errors, it returns false and the encountered error.
261+
func isValidUnixSocket(path string) (bool, error) {
262+
conn, err := net.Dial("unix", path)
263+
if err != nil {
264+
if os.IsNotExist(err) {
265+
return false, nil
266+
}
267+
return false, errors.Newf("Not a UNIX Domain Socket: %v: %w", path, err)
268+
}
269+
defer conn.Close()
270+
271+
return true, nil
272+
}
273+
274+
var testHomeDir string // used by tests to mock the user's $HOME
275+
276+
// expandHomeDir expands to the user's home directory a tilde (~) or %USERPROFILE% at the beginning of a file path.
277+
//
278+
// Parameters:
279+
// - filePath: A string representing the file path that may start with "~/" or "%USERPROFILE%\".
280+
//
281+
// Returns:
282+
// - string: The expanded file path with the home directory resolved.
283+
// - error: An error if the user's home directory cannot be determined.
284+
//
285+
// The function handles both Unix-style paths starting with "~/" and Windows-style paths starting with "%USERPROFILE%\".
286+
// It uses the testHomeDir variable for testing purposes if set, otherwise it uses os.UserHomeDir() to get the user's home directory.
287+
// If the input path doesn't start with either prefix, it returns the original path unchanged.
288+
func expandHomeDir(filePath string) (string, error) {
289+
if strings.HasPrefix(filePath, "~/") || strings.HasPrefix(filePath, "%USERPROFILE%\\") {
290+
var homeDir string
291+
if testHomeDir != "" {
292+
homeDir = testHomeDir
293+
} else {
294+
hd, err := os.UserHomeDir()
295+
if err != nil {
296+
return "", err
297+
}
298+
homeDir = hd
299+
}
300+
301+
if strings.HasPrefix(filePath, "~/") {
302+
return filepath.Join(homeDir, filePath[2:]), nil
303+
}
304+
return filepath.Join(homeDir, filePath[14:]), nil
305+
}
306+
307+
return filePath, nil
308+
}

0 commit comments

Comments
 (0)