Skip to content

Commit

Permalink
Add proxy support (#1120)
Browse files Browse the repository at this point in the history
* - 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 <camden@ccheek.com>

* Whoops; meant to use `w` instead of `v`! Thanks for catching that.

Co-authored-by: Camden Cheek <camden@ccheek.com>

* Update comment about InsecureSkipVerify

* Make CHANGELOG entry

---------

Co-authored-by: Camden Cheek <camden@ccheek.com>
  • Loading branch information
peterguy and camdencheek authored Oct 24, 2024
1 parent bca0a8b commit f86f75d
Show file tree
Hide file tree
Showing 10 changed files with 651 additions and 30 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ All notable changes to `src-cli` are documented in this file.

## Unreleased

### Added

- Support HTTP(S), SOCKS5, and UNIX Domain Socket proxies via SRC_PROXY environment variable. [#1120](https://github.com/sourcegraph/src-cli/pull/1120)

## 5.8.1

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion cmd/src/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func loginCmd(ctx context.Context, cfg *config, client api.Client, endpointArg s

if cfg.ConfigFilePath != "" {
fmt.Fprintln(out)
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)
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)
}

noToken := cfg.AccessToken == ""
Expand Down
2 changes: 1 addition & 1 deletion cmd/src/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestLogin(t *testing.T) {
if err != cmderrors.ExitCode1 {
t.Fatal(err)
}
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."
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."
if out != wantOut {
t.Errorf("got output %q, want %q", out, wantOut)
}
Expand Down
168 changes: 148 additions & 20 deletions cmd/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
"flag"
"io"
"log"
"net"
"net/url"
"os"
"os/user"
"path/filepath"
"strings"

Expand All @@ -25,6 +26,20 @@ Usage:
Environment variables
SRC_ACCESS_TOKEN Sourcegraph access token
SRC_ENDPOINT endpoint to use, if unset will default to "https://sourcegraph.com"
SRC_PROXY A proxy to use for proxying requests to the Sourcegraph endpoint.
Supports HTTP(S), SOCKS5/5h, and UNIX Domain Socket proxies.
If a UNIX Domain Socket, the path can be either an absolute path,
or can start with ~/ or %USERPROFILE%\ for a path in the user's home directory.
Examples:
- https://localhost:3080
- https://<user>:<password>localhost:8080
- socks5h://localhost:1080
- socks5://<username>:<password>@localhost:1080
- unix://~/src-proxy.sock
- unix://%USERPROFILE%\src-proxy.sock
- ~/src-proxy.sock
- %USERPROFILE%\src-proxy.sock
- C:\some\path\src-proxy.sock
The options are:
Expand Down Expand Up @@ -83,8 +98,10 @@ type config struct {
Endpoint string `json:"endpoint"`
AccessToken string `json:"accessToken"`
AdditionalHeaders map[string]string `json:"additionalHeaders"`

ConfigFilePath string
Proxy string `json:"proxy"`
ProxyURL *url.URL
ProxyPath string
ConfigFilePath string
}

// apiClient returns an api.Client built from the configuration.
Expand All @@ -95,32 +112,25 @@ func (c *config) apiClient(flags *api.Flags, out io.Writer) api.Client {
AdditionalHeaders: c.AdditionalHeaders,
Flags: flags,
Out: out,
ProxyURL: c.ProxyURL,
ProxyPath: c.ProxyPath,
})
}

var testHomeDir string // used by tests to mock the user's $HOME

// readConfig reads the config file from the given path.
func readConfig() (*config, error) {
cfgPath := *configPath
cfgFile := *configPath
userSpecified := *configPath != ""

var homeDir string
if testHomeDir != "" {
homeDir = testHomeDir
} else {
u, err := user.Current()
if err != nil {
return nil, err
}
homeDir = u.HomeDir
if !userSpecified {
cfgFile = "~/src-config.json"
}

if !userSpecified {
cfgPath = filepath.Join(homeDir, "src-config.json")
} else if strings.HasPrefix(cfgPath, "~/") {
cfgPath = filepath.Join(homeDir, cfgPath[2:])
cfgPath, err := expandHomeDir(cfgFile)
if err != nil {
return nil, err
}

data, err := os.ReadFile(os.ExpandEnv(cfgPath))
if err != nil && (!os.IsNotExist(err) || userSpecified) {
return nil, err
Expand All @@ -135,10 +145,12 @@ func readConfig() (*config, error) {

envToken := os.Getenv("SRC_ACCESS_TOKEN")
envEndpoint := os.Getenv("SRC_ENDPOINT")
envProxy := os.Getenv("SRC_PROXY")

if userSpecified {
// If a config file is present, either zero or both environment variables must be present.
// If a config file is present, either zero or both required environment variables must be present.
// We don't want to partially apply environment variables.
// Note that SRC_PROXY is optional so we don't test for it.
if envToken == "" && envEndpoint != "" {
return nil, errConfigMerge
}
Expand All @@ -157,6 +169,60 @@ func readConfig() (*config, error) {
if cfg.Endpoint == "" {
cfg.Endpoint = "https://sourcegraph.com"
}
if envProxy != "" {
cfg.Proxy = envProxy
}

if cfg.Proxy != "" {

parseEndpoint := func(endpoint string) (scheme string, address string) {
parts := strings.SplitN(endpoint, "://", 2)
if len(parts) == 2 {
return parts[0], parts[1]
}
return "", endpoint
}

urlSchemes := []string{"http", "https", "socks", "socks5", "socks5h"}

isURLScheme := func(scheme string) bool {
for _, s := range urlSchemes {
if scheme == s {
return true
}
}
return false
}

scheme, address := parseEndpoint(cfg.Proxy)

if isURLScheme(scheme) {
endpoint := cfg.Proxy
// assume socks means socks5, because that's all we support
if scheme == "socks" {
endpoint = "socks5://" + address
}
cfg.ProxyURL, err = url.Parse(endpoint)
if err != nil {
return nil, err
}
} else if scheme == "" || scheme == "unix" {
path, err := expandHomeDir(address)
if err != nil {
return nil, err
}
isValidUDS, err := isValidUnixSocket(path)
if err != nil {
return nil, errors.Newf("Invalid proxy configuration: %w", err)
}
if !isValidUDS {
return nil, errors.Newf("invalid proxy socket: %s", path)
}
cfg.ProxyPath = path
} else {
return nil, errors.Newf("invalid proxy endpoint: %s", cfg.Proxy)
}
}

cfg.AdditionalHeaders = parseAdditionalHeaders()
// Ensure that we're not clashing additonal headers
Expand All @@ -178,3 +244,65 @@ func readConfig() (*config, error) {
func cleanEndpoint(urlStr string) string {
return strings.TrimSuffix(urlStr, "/")
}

// isValidUnixSocket checks if the given path is a valid Unix socket.
//
// Parameters:
// - path: A string representing the file path to check.
//
// Returns:
// - bool: true if the path is a valid Unix socket, false otherwise.
// - error: nil if the check was successful, or an error if an unexpected issue occurred.
//
// The function attempts to establish a connection to the Unix socket at the given path.
// If the connection succeeds, it's considered a valid Unix socket.
// If the file doesn't exist, it returns false without an error.
// For any other errors, it returns false and the encountered error.
func isValidUnixSocket(path string) (bool, error) {
conn, err := net.Dial("unix", path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, errors.Newf("Not a UNIX Domain Socket: %v: %w", path, err)
}
defer conn.Close()

return true, nil
}

var testHomeDir string // used by tests to mock the user's $HOME

// expandHomeDir expands to the user's home directory a tilde (~) or %USERPROFILE% at the beginning of a file path.
//
// Parameters:
// - filePath: A string representing the file path that may start with "~/" or "%USERPROFILE%\".
//
// Returns:
// - string: The expanded file path with the home directory resolved.
// - error: An error if the user's home directory cannot be determined.
//
// The function handles both Unix-style paths starting with "~/" and Windows-style paths starting with "%USERPROFILE%\".
// It uses the testHomeDir variable for testing purposes if set, otherwise it uses os.UserHomeDir() to get the user's home directory.
// If the input path doesn't start with either prefix, it returns the original path unchanged.
func expandHomeDir(filePath string) (string, error) {
if strings.HasPrefix(filePath, "~/") || strings.HasPrefix(filePath, "%USERPROFILE%\\") {
var homeDir string
if testHomeDir != "" {
homeDir = testHomeDir
} else {
hd, err := os.UserHomeDir()
if err != nil {
return "", err
}
homeDir = hd
}

if strings.HasPrefix(filePath, "~/") {
return filepath.Join(homeDir, filePath[2:]), nil
}
return filepath.Join(homeDir, filePath[14:]), nil
}

return filePath, nil
}
Loading

0 comments on commit f86f75d

Please sign in to comment.