-
Notifications
You must be signed in to change notification settings - Fork 68
Expand file tree
/
Copy pathmain.go
More file actions
402 lines (344 loc) · 11.9 KB
/
main.go
File metadata and controls
402 lines (344 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package main
import (
"context"
"encoding/json"
"flag"
"io"
"log"
"net"
"net/url"
"os"
"path/filepath"
"slices"
"strings"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/src-cli/internal/api"
"github.com/sourcegraph/src-cli/internal/oauth"
)
const usageText = `src is a tool that provides access to Sourcegraph instances.
For more information, see https://github.com/sourcegraph/src-cli
Usage:
src [options] command [command options]
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:
-v print verbose output
The commands are:
auth authentication helper commands
api interacts with the Sourcegraph GraphQL API
batch manages batch changes
code-intel manages code intelligence data
config manages global, org, and user settings
extensions,ext manages extensions (experimental)
extsvc manages external services
gateway interacts with Cody Gateway
login authenticate to a Sourcegraph instance with your user credentials
orgs,org manages organizations
teams,team manages teams
repos,repo manages repositories
search search for results on Sourcegraph
search-jobs manages search jobs
serve-git serves your local git repositories over HTTP for Sourcegraph to pull
users,user manages users
codeowners manages code ownership information
version display and compare the src-cli version against the recommended version for your instance
Use "src [command] -h" for more information about a command.
`
var (
verbose = flag.Bool("v", false, "print verbose output")
// The following arguments are deprecated which is why they are no longer documented
configPath = flag.String("config", "", "")
endpointFlag = flag.String("endpoint", "", "")
errConfigMerge = errors.New("when using a configuration file, zero or all environment variables must be set")
errConfigAuthorizationConflict = errors.New("when passing an 'Authorization' additional headers, SRC_ACCESS_TOKEN must never be set")
errCIAccessTokenRequired = errors.New("CI is true and SRC_ACCESS_TOKEN is not set or empty. When running in CI OAuth tokens cannot be used, only SRC_ACCESS_TOKEN. Either set CI=false or define a SRC_ACCESS_TOKEN")
)
// commands contains all registered subcommands.
var commands commander
func main() {
// Configure logging.
log.SetFlags(0)
log.SetPrefix("")
commands.run(flag.CommandLine, "src", usageText, normalizeDashHelp(os.Args[1:]))
}
// normalizeDashHelp converts --help to -help since Go's flag parser only supports single dash.
func normalizeDashHelp(args []string) []string {
args = slices.Clone(args)
for i, arg := range args {
if arg == "--" {
break
}
if arg == "--help" {
args[i] = "-help"
}
}
return args
}
func parseEndpoint(endpoint string) (*url.URL, error) {
u, err := url.ParseRequestURI(strings.TrimSuffix(endpoint, "/"))
if err != nil {
return nil, err
}
if !(u.Scheme == "http" || u.Scheme == "https") {
return nil, errors.Newf("invalid scheme %s: require http or https", u.Scheme)
}
if u.Host == "" {
return nil, errors.Newf("empty host")
}
// auth in the URL is not used, and could be explosed in log output.
// Explicitly clear it in case it's accidentally set in SRC_ENDPOINT or the config file.
u.User = nil
return u, nil
}
var cfg *config
// config holds the resolved configuration used at runtime.
type config struct {
accessToken string
additionalHeaders map[string]string
proxyURL *url.URL
proxyPath string
configFilePath string
endpointURL *url.URL // always non-nil; defaults to https://sourcegraph.com via readConfig
inCI bool
}
// configFromFile holds the config as read from the config file,
// which is validated and parsed into the config struct.
type configFromFile struct {
Endpoint string `json:"endpoint"`
AccessToken string `json:"accessToken"`
AdditionalHeaders map[string]string `json:"additionalHeaders"`
Proxy string `json:"proxy"`
}
type AuthMode int
const (
AuthModeOAuth AuthMode = iota
AuthModeAccessToken
)
func (c *config) AuthMode() AuthMode {
if c.accessToken != "" {
return AuthModeAccessToken
}
return AuthModeOAuth
}
func (c *config) InCI() bool {
return c.inCI
}
func (c *config) requireCIAccessToken() error {
// In CI we typically do not have access to the keyring and the machine is also typically headless
// we therefore require SRC_ACCESS_TOKEN to be set when in CI.
// If someone really wants to run with OAuth in CI they can temporarily do CI=false
if c.InCI() && c.AuthMode() != AuthModeAccessToken {
return errCIAccessTokenRequired
}
return nil
}
// apiClient returns an api.Client built from the configuration.
func (c *config) apiClient(flags *api.Flags, out io.Writer) api.Client {
opts := api.ClientOpts{
EndpointURL: c.endpointURL,
AccessToken: c.accessToken,
AdditionalHeaders: c.additionalHeaders,
Flags: flags,
Out: out,
ProxyURL: c.proxyURL,
ProxyPath: c.proxyPath,
RequireAccessTokenInCI: c.InCI(),
}
// Only use OAuth if we do not have SRC_ACCESS_TOKEN set
if c.accessToken == "" {
if t, err := oauth.LoadToken(context.Background(), c.endpointURL); err == nil {
opts.OAuthToken = t
}
}
return api.NewClient(opts)
}
// readConfig reads the config from the standard config file, the (deprecated) user-specified config file,
// the environment variables, and the (deprecated) command-line flags.
func readConfig() (*config, error) {
cfgFile := *configPath
userSpecified := *configPath != ""
if !userSpecified {
cfgFile = "~/src-config.json"
}
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
}
var cfgFromFile configFromFile
var cfg config
cfg.inCI = isCI()
var endpointStr string
var proxyStr string
if err == nil {
cfg.configFilePath = cfgPath
if err := json.Unmarshal(data, &cfgFromFile); err != nil {
return nil, err
}
endpointStr = cfgFromFile.Endpoint
cfg.accessToken = cfgFromFile.AccessToken
cfg.additionalHeaders = cfgFromFile.AdditionalHeaders
proxyStr = cfgFromFile.Proxy
}
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 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
}
if envToken != "" && envEndpoint == "" {
return nil, errConfigMerge
}
}
// Apply config overrides.
if envToken != "" {
cfg.accessToken = envToken
}
if envEndpoint != "" {
endpointStr = envEndpoint
}
if endpointStr == "" {
endpointStr = "https://sourcegraph.com"
}
if envProxy != "" {
proxyStr = envProxy
}
// Lastly, apply endpoint flag if set
if endpointFlag != nil && *endpointFlag != "" {
endpointStr = *endpointFlag
}
if endpointURL, err := parseEndpoint(endpointStr); err != nil {
return nil, errors.Newf("invalid endpoint: %s", endpointStr)
} else {
cfg.endpointURL = endpointURL
}
if proxyStr != "" {
parseProxyEndpoint := 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 {
return slices.Contains(urlSchemes, scheme)
}
scheme, address := parseProxyEndpoint(proxyStr)
if isURLScheme(scheme) {
endpoint := proxyStr
// 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", proxyStr)
}
}
cfg.additionalHeaders = parseAdditionalHeaders()
// Ensure that we're not clashing additonal headers
_, hasAuthorizationAdditonalHeader := cfg.additionalHeaders["authorization"]
if cfg.accessToken != "" && hasAuthorizationAdditonalHeader {
return nil, errConfigAuthorizationConflict
}
return &cfg, nil
}
func isCI() bool {
value, ok := os.LookupEnv("CI")
return ok && value != ""
}
// 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
}