Import upstream version 1.2.0+git20220615.1.7df8445+ds
Debian Janitor
1 year, 6 months ago
0 | on: [push, pull_request] | |
1 | name: Test | |
2 | jobs: | |
3 | lint: | |
4 | runs-on: ubuntu-latest | |
5 | steps: | |
6 | - name: Install Go | |
7 | uses: WillAbides/setup-go-faster@main | |
8 | with: | |
9 | go-version: 1.18.x | |
10 | - uses: actions/checkout@v2 | |
11 | with: | |
12 | path: './src/github.com/kevinburke/ssh_config' | |
13 | # staticcheck needs this for GOPATH | |
14 | - run: | | |
15 | echo "GO111MODULE=off" >> $GITHUB_ENV | |
16 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV | |
17 | echo "PATH=$GITHUB_WORKSPACE/bin:$PATH" >> $GITHUB_ENV | |
18 | - name: Run tests | |
19 | run: make lint | |
20 | working-directory: './src/github.com/kevinburke/ssh_config' | |
21 | ||
22 | test: | |
23 | strategy: | |
24 | matrix: | |
25 | go-version: [1.17.x, 1.18.x] | |
26 | runs-on: ubuntu-latest | |
27 | steps: | |
28 | - name: Install Go | |
29 | uses: WillAbides/setup-go-faster@main | |
30 | with: | |
31 | go-version: ${{ matrix.go-version }} | |
32 | - uses: actions/checkout@v2 | |
33 | with: | |
34 | path: './src/github.com/kevinburke/ssh_config' | |
35 | - run: | | |
36 | echo "GO111MODULE=off" >> $GITHUB_ENV | |
37 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV | |
38 | echo "PATH=$GITHUB_WORKSPACE/bin:$PATH" >> $GITHUB_ENV | |
39 | - name: Run tests with race detector on | |
40 | run: make race-test | |
41 | working-directory: './src/github.com/kevinburke/ssh_config' |
0 | go_import_path: github.com/kevinburke/ssh_config | |
1 | ||
2 | language: go | |
3 | ||
4 | go: | |
5 | - 1.11.x | |
6 | - 1.12.x | |
7 | - master | |
8 | ||
9 | before_script: | |
10 | - go get -u ./... | |
11 | ||
12 | script: | |
13 | - make race-test |
0 | Carlos A Becker <caarlos0@gmail.com> | |
1 | Dustin Spicuzza <dustin@virtualroadside.com> | |
0 | 2 | Eugene Terentev <eugene@terentev.net> |
1 | Kevin Burke <kev@inburke.com> | |
3 | Kevin Burke <kevin@burke.dev> | |
4 | Mark Nevill <nev@improbable.io> | |
5 | Scott Lessans <slessans@gmail.com> | |
2 | 6 | Sergey Lukjanov <me@slukjanov.name> |
3 | 7 | Wayne Ashley Berry <wayneashleyberry@gmail.com> |
8 | santosh653 <70637961+santosh653@users.noreply.github.com> |
0 | # Changes | |
1 | ||
2 | ## Version 1.2 | |
3 | ||
4 | Previously, if a Host declaration or a value had trailing whitespace, that | |
5 | whitespace would have been included as part of the value. This led to unexpected | |
6 | consequences. For example: | |
7 | ||
8 | ``` | |
9 | Host example # A comment | |
10 | HostName example.com # Another comment | |
11 | ``` | |
12 | ||
13 | Prior to version 1.2, the value for Host would have been "example " and the | |
14 | value for HostName would have been "example.com ". Both of these are | |
15 | unintuitive. | |
16 | ||
17 | Instead, we strip the trailing whitespace in the configuration, which leads to | |
18 | more intuitive behavior. |
8 | 8 | go vet ./... |
9 | 9 | $(STATICCHECK) |
10 | 10 | |
11 | test: lint | |
11 | test: | |
12 | 12 | @# the timeout helps guard against infinite recursion |
13 | 13 | go test -timeout=250ms ./... |
14 | 14 | |
15 | race-test: lint | |
15 | race-test: | |
16 | 16 | go test -timeout=500ms -race ./... |
17 | 17 | |
18 | 18 | $(BUMP_VERSION): |
19 | 19 | go get -u github.com/kevinburke/bump_version |
20 | 20 | |
21 | $(WRITE_MAILMAP): | |
22 | go get -u github.com/kevinburke/write_mailmap | |
23 | ||
21 | 24 | release: test | $(BUMP_VERSION) |
22 | $(BUMP_VERSION) minor config.go | |
25 | $(BUMP_VERSION) --tag-prefix=v minor config.go | |
23 | 26 | |
24 | 27 | force: ; |
25 | 28 |
14 | 14 | |
15 | 15 | ```go |
16 | 16 | port := ssh_config.Get("myhost", "Port") |
17 | ``` | |
18 | ||
19 | Certain directives can occur multiple times for a host (such as `IdentityFile`), | |
20 | so you should use the `GetAll` or `GetAllStrict` directive to retrieve those | |
21 | instead. | |
22 | ||
23 | ```go | |
24 | files := ssh_config.GetAll("myhost", "IdentityFile") | |
17 | 25 | ``` |
18 | 26 | |
19 | 27 | You can also load a config file and read values from it. |
75 | 83 | |
76 | 84 | ## Donating |
77 | 85 | |
78 | Donations free up time to make improvements to the library, and respond to | |
79 | bug reports. You can send donations via Paypal's "Send Money" feature to | |
80 | kev@inburke.com. Donations are not tax deductible in the USA. | |
86 | I don't get paid to maintain this project. Donations free up time to make | |
87 | improvements to the library, and respond to bug reports. You can send donations | |
88 | via Paypal's "Send Money" feature to kev@inburke.com. Donations are not tax | |
89 | deductible in the USA. | |
90 | ||
91 | You can also reach out about a consulting engagement: https://burke.services |
33 | 33 | "errors" |
34 | 34 | "fmt" |
35 | 35 | "io" |
36 | "io/ioutil" | |
37 | 36 | "os" |
38 | 37 | osuser "os/user" |
39 | 38 | "path/filepath" |
43 | 42 | "sync" |
44 | 43 | ) |
45 | 44 | |
46 | const version = "1.0" | |
45 | const version = "1.2" | |
47 | 46 | |
48 | 47 | var _ = version |
49 | 48 | |
53 | 52 | // files are parsed and cached the first time Get() or GetStrict() is called. |
54 | 53 | type UserSettings struct { |
55 | 54 | IgnoreErrors bool |
55 | customConfig *Config | |
56 | customConfigFinder configFinder | |
56 | 57 | systemConfig *Config |
57 | 58 | systemConfigFinder configFinder |
58 | 59 | userConfig *Config |
101 | 102 | return val, nil |
102 | 103 | } |
103 | 104 | |
105 | func findAll(c *Config, alias, key string) ([]string, error) { | |
106 | if c == nil { | |
107 | return nil, nil | |
108 | } | |
109 | return c.GetAll(alias, key) | |
110 | } | |
111 | ||
104 | 112 | // Get finds the first value for key within a declaration that matches the |
105 | 113 | // alias. Get returns the empty string if no value was found, or if IgnoreErrors |
106 | 114 | // is false and we could not parse the configuration file. Use GetStrict to |
113 | 121 | return DefaultUserSettings.Get(alias, key) |
114 | 122 | } |
115 | 123 | |
124 | // GetAll retrieves zero or more directives for key for the given alias. GetAll | |
125 | // returns nil if no value was found, or if IgnoreErrors is false and we could | |
126 | // not parse the configuration file. Use GetAllStrict to disambiguate the | |
127 | // latter cases. | |
128 | // | |
129 | // In most cases you want to use Get or GetStrict, which returns a single value. | |
130 | // However, a subset of ssh configuration values (IdentityFile, for example) | |
131 | // allow you to specify multiple directives. | |
132 | // | |
133 | // The match for key is case insensitive. | |
134 | // | |
135 | // GetAll is a wrapper around DefaultUserSettings.GetAll. | |
136 | func GetAll(alias, key string) []string { | |
137 | return DefaultUserSettings.GetAll(alias, key) | |
138 | } | |
139 | ||
116 | 140 | // GetStrict finds the first value for key within a declaration that matches the |
117 | 141 | // alias. If key has a default value and no matching configuration is found, the |
118 | 142 | // default will be returned. For more information on default values and the way |
119 | 143 | // patterns are matched, see the manpage for ssh_config. |
120 | 144 | // |
121 | // error will be non-nil if and only if a user's configuration file or the | |
122 | // system configuration file could not be parsed, and u.IgnoreErrors is false. | |
145 | // The returned error will be non-nil if and only if a user's configuration file | |
146 | // or the system configuration file could not be parsed, and u.IgnoreErrors is | |
147 | // false. | |
123 | 148 | // |
124 | 149 | // GetStrict is a wrapper around DefaultUserSettings.GetStrict. |
125 | 150 | func GetStrict(alias, key string) (string, error) { |
126 | 151 | return DefaultUserSettings.GetStrict(alias, key) |
152 | } | |
153 | ||
154 | // GetAllStrict retrieves zero or more directives for key for the given alias. | |
155 | // | |
156 | // In most cases you want to use Get or GetStrict, which returns a single value. | |
157 | // However, a subset of ssh configuration values (IdentityFile, for example) | |
158 | // allow you to specify multiple directives. | |
159 | // | |
160 | // The returned error will be non-nil if and only if a user's configuration file | |
161 | // or the system configuration file could not be parsed, and u.IgnoreErrors is | |
162 | // false. | |
163 | // | |
164 | // GetAllStrict is a wrapper around DefaultUserSettings.GetAllStrict. | |
165 | func GetAllStrict(alias, key string) ([]string, error) { | |
166 | return DefaultUserSettings.GetAllStrict(alias, key) | |
127 | 167 | } |
128 | 168 | |
129 | 169 | // Get finds the first value for key within a declaration that matches the |
140 | 180 | return val |
141 | 181 | } |
142 | 182 | |
183 | // GetAll retrieves zero or more directives for key for the given alias. GetAll | |
184 | // returns nil if no value was found, or if IgnoreErrors is false and we could | |
185 | // not parse the configuration file. Use GetStrict to disambiguate the latter | |
186 | // cases. | |
187 | // | |
188 | // The match for key is case insensitive. | |
189 | func (u *UserSettings) GetAll(alias, key string) []string { | |
190 | val, _ := u.GetAllStrict(alias, key) | |
191 | return val | |
192 | } | |
193 | ||
143 | 194 | // GetStrict finds the first value for key within a declaration that matches the |
144 | 195 | // alias. If key has a default value and no matching configuration is found, the |
145 | 196 | // default will be returned. For more information on default values and the way |
148 | 199 | // error will be non-nil if and only if a user's configuration file or the |
149 | 200 | // system configuration file could not be parsed, and u.IgnoreErrors is false. |
150 | 201 | func (u *UserSettings) GetStrict(alias, key string) (string, error) { |
202 | u.doLoadConfigs() | |
203 | //lint:ignore S1002 I prefer it this way | |
204 | if u.onceErr != nil && u.IgnoreErrors == false { | |
205 | return "", u.onceErr | |
206 | } | |
207 | // TODO this is getting repetitive | |
208 | if u.customConfig != nil { | |
209 | val, err := findVal(u.customConfig, alias, key) | |
210 | if err != nil || val != "" { | |
211 | return val, err | |
212 | } | |
213 | } | |
214 | val, err := findVal(u.userConfig, alias, key) | |
215 | if err != nil || val != "" { | |
216 | return val, err | |
217 | } | |
218 | val2, err2 := findVal(u.systemConfig, alias, key) | |
219 | if err2 != nil || val2 != "" { | |
220 | return val2, err2 | |
221 | } | |
222 | return Default(key), nil | |
223 | } | |
224 | ||
225 | // GetAllStrict retrieves zero or more directives for key for the given alias. | |
226 | // If key has a default value and no matching configuration is found, the | |
227 | // default will be returned. For more information on default values and the way | |
228 | // patterns are matched, see the manpage for ssh_config. | |
229 | // | |
230 | // The returned error will be non-nil if and only if a user's configuration file | |
231 | // or the system configuration file could not be parsed, and u.IgnoreErrors is | |
232 | // false. | |
233 | func (u *UserSettings) GetAllStrict(alias, key string) ([]string, error) { | |
234 | u.doLoadConfigs() | |
235 | //lint:ignore S1002 I prefer it this way | |
236 | if u.onceErr != nil && u.IgnoreErrors == false { | |
237 | return nil, u.onceErr | |
238 | } | |
239 | if u.customConfig != nil { | |
240 | val, err := findAll(u.customConfig, alias, key) | |
241 | if err != nil || val != nil { | |
242 | return val, err | |
243 | } | |
244 | } | |
245 | val, err := findAll(u.userConfig, alias, key) | |
246 | if err != nil || val != nil { | |
247 | return val, err | |
248 | } | |
249 | val2, err2 := findAll(u.systemConfig, alias, key) | |
250 | if err2 != nil || val2 != nil { | |
251 | return val2, err2 | |
252 | } | |
253 | // TODO: IdentityFile has multiple default values that we should return. | |
254 | if def := Default(key); def != "" { | |
255 | return []string{def}, nil | |
256 | } | |
257 | return []string{}, nil | |
258 | } | |
259 | ||
260 | // ConfigFinder will invoke f to try to find a ssh config file in a custom | |
261 | // location on disk, instead of in /etc/ssh or $HOME/.ssh. f should return the | |
262 | // name of a file containing SSH configuration. | |
263 | // | |
264 | // ConfigFinder must be invoked before any calls to Get or GetStrict and panics | |
265 | // if f is nil. Most users should not need to use this function. | |
266 | func (u *UserSettings) ConfigFinder(f func() string) { | |
267 | if f == nil { | |
268 | panic("cannot call ConfigFinder with nil function") | |
269 | } | |
270 | u.customConfigFinder = f | |
271 | } | |
272 | ||
273 | func (u *UserSettings) doLoadConfigs() { | |
151 | 274 | u.loadConfigs.Do(func() { |
152 | // can't parse user file, that's ok. | |
153 | 275 | var filename string |
276 | var err error | |
277 | if u.customConfigFinder != nil { | |
278 | filename = u.customConfigFinder() | |
279 | u.customConfig, err = parseFile(filename) | |
280 | // IsNotExist should be returned because a user specified this | |
281 | // function - not existing likely means they made an error | |
282 | if err != nil { | |
283 | u.onceErr = err | |
284 | } | |
285 | return | |
286 | } | |
154 | 287 | if u.userConfigFinder == nil { |
155 | 288 | filename = userConfigFinder() |
156 | 289 | } else { |
157 | 290 | filename = u.userConfigFinder() |
158 | 291 | } |
159 | var err error | |
160 | 292 | u.userConfig, err = parseFile(filename) |
161 | 293 | //lint:ignore S1002 I prefer it this way |
162 | 294 | if err != nil && os.IsNotExist(err) == false { |
175 | 307 | return |
176 | 308 | } |
177 | 309 | }) |
178 | //lint:ignore S1002 I prefer it this way | |
179 | if u.onceErr != nil && u.IgnoreErrors == false { | |
180 | return "", u.onceErr | |
181 | } | |
182 | val, err := findVal(u.userConfig, alias, key) | |
183 | if err != nil || val != "" { | |
184 | return val, err | |
185 | } | |
186 | val2, err2 := findVal(u.systemConfig, alias, key) | |
187 | if err2 != nil || val2 != "" { | |
188 | return val2, err2 | |
189 | } | |
190 | return Default(key), nil | |
191 | 310 | } |
192 | 311 | |
193 | 312 | func parseFile(filename string) (*Config, error) { |
195 | 314 | } |
196 | 315 | |
197 | 316 | func parseWithDepth(filename string, depth uint8) (*Config, error) { |
198 | b, err := ioutil.ReadFile(filename) | |
317 | b, err := os.ReadFile(filename) | |
199 | 318 | if err != nil { |
200 | 319 | return nil, err |
201 | 320 | } |
210 | 329 | // Decode reads r into a Config, or returns an error if r could not be parsed as |
211 | 330 | // an SSH config file. |
212 | 331 | func Decode(r io.Reader) (*Config, error) { |
213 | b, err := ioutil.ReadAll(r) | |
332 | b, err := io.ReadAll(r) | |
214 | 333 | if err != nil { |
215 | 334 | return nil, err |
216 | 335 | } |
336 | return decodeBytes(b, false, 0) | |
337 | } | |
338 | ||
339 | // DecodeBytes reads b into a Config, or returns an error if r could not be | |
340 | // parsed as an SSH config file. | |
341 | func DecodeBytes(b []byte) (*Config, error) { | |
217 | 342 | return decodeBytes(b, false, 0) |
218 | 343 | } |
219 | 344 | |
281 | 406 | return "", nil |
282 | 407 | } |
283 | 408 | |
409 | // GetAll returns all values in the configuration that match the alias and | |
410 | // contains key, or nil if none are present. | |
411 | func (c *Config) GetAll(alias, key string) ([]string, error) { | |
412 | lowerKey := strings.ToLower(key) | |
413 | all := []string(nil) | |
414 | for _, host := range c.Hosts { | |
415 | if !host.Matches(alias) { | |
416 | continue | |
417 | } | |
418 | for _, node := range host.Nodes { | |
419 | switch t := node.(type) { | |
420 | case *Empty: | |
421 | continue | |
422 | case *KV: | |
423 | // "keys are case insensitive" per the spec | |
424 | lkey := strings.ToLower(t.Key) | |
425 | if lkey == "match" { | |
426 | panic("can't handle Match directives") | |
427 | } | |
428 | if lkey == lowerKey { | |
429 | all = append(all, t.Value) | |
430 | } | |
431 | case *Include: | |
432 | val, _ := t.GetAll(alias, key) | |
433 | if len(val) > 0 { | |
434 | all = append(all, val...) | |
435 | } | |
436 | default: | |
437 | return nil, fmt.Errorf("unknown Node type %v", t) | |
438 | } | |
439 | } | |
440 | } | |
441 | ||
442 | return all, nil | |
443 | } | |
444 | ||
284 | 445 | // String returns a string representation of the Config file. |
285 | 446 | func (c Config) String() string { |
286 | 447 | return marshal(c).String() |
373 | 534 | // A Node is either a key/value pair or a comment line. |
374 | 535 | Nodes []Node |
375 | 536 | // EOLComment is the comment (if any) terminating the Host line. |
376 | EOLComment string | |
537 | EOLComment string | |
538 | // Whitespace if any between the Host declaration and a trailing comment. | |
539 | spaceBeforeComment string | |
540 | ||
377 | 541 | hasEquals bool |
378 | 542 | leadingSpace int // TODO: handle spaces vs tabs here. |
379 | 543 | // The file starts with an implicit "Host *" declaration. |
405 | 569 | // String prints h as it would appear in a config file. Minor tweaks may be |
406 | 570 | // present in the whitespace in the printed file. |
407 | 571 | func (h *Host) String() string { |
408 | var buf bytes.Buffer | |
572 | var buf strings.Builder | |
409 | 573 | //lint:ignore S1002 I prefer to write it this way |
410 | 574 | if h.implicit == false { |
411 | 575 | buf.WriteString(strings.Repeat(" ", int(h.leadingSpace))) |
421 | 585 | buf.WriteString(" ") |
422 | 586 | } |
423 | 587 | } |
588 | buf.WriteString(h.spaceBeforeComment) | |
424 | 589 | if h.EOLComment != "" { |
425 | buf.WriteString(" #") | |
590 | buf.WriteByte('#') | |
426 | 591 | buf.WriteString(h.EOLComment) |
427 | 592 | } |
428 | 593 | buf.WriteByte('\n') |
443 | 608 | // KV is a line in the config file that contains a key, a value, and possibly |
444 | 609 | // a comment. |
445 | 610 | type KV struct { |
446 | Key string | |
447 | Value string | |
448 | Comment string | |
449 | hasEquals bool | |
450 | leadingSpace int // Space before the key. TODO handle spaces vs tabs. | |
451 | position Position | |
611 | Key string | |
612 | Value string | |
613 | // Whitespace after the value but before any comment | |
614 | spaceAfterValue string | |
615 | Comment string | |
616 | hasEquals bool | |
617 | leadingSpace int // Space before the key. TODO handle spaces vs tabs. | |
618 | position Position | |
452 | 619 | } |
453 | 620 | |
454 | 621 | // Pos returns k's Position. |
456 | 623 | return k.position |
457 | 624 | } |
458 | 625 | |
459 | // String prints k as it was parsed in the config file. There may be slight | |
460 | // changes to the whitespace between values. | |
626 | // String prints k as it was parsed in the config file. | |
461 | 627 | func (k *KV) String() string { |
462 | 628 | if k == nil { |
463 | 629 | return "" |
466 | 632 | if k.hasEquals { |
467 | 633 | equals = " = " |
468 | 634 | } |
469 | line := fmt.Sprintf("%s%s%s%s", strings.Repeat(" ", int(k.leadingSpace)), k.Key, equals, k.Value) | |
635 | line := strings.Repeat(" ", int(k.leadingSpace)) + k.Key + equals + k.Value + k.spaceAfterValue | |
470 | 636 | if k.Comment != "" { |
471 | line += " #" + k.Comment | |
637 | line += "#" + k.Comment | |
472 | 638 | } |
473 | 639 | return line |
474 | 640 | } |
610 | 776 | return "" |
611 | 777 | } |
612 | 778 | |
779 | // GetAll finds all values in the Include statement matching the alias and the | |
780 | // given key. | |
781 | func (inc *Include) GetAll(alias, key string) ([]string, error) { | |
782 | inc.mu.Lock() | |
783 | defer inc.mu.Unlock() | |
784 | var vals []string | |
785 | ||
786 | // TODO: we search files in any order which is not correct | |
787 | for i := range inc.matches { | |
788 | cfg := inc.files[inc.matches[i]] | |
789 | if cfg == nil { | |
790 | panic("nil cfg") | |
791 | } | |
792 | val, err := cfg.GetAll(alias, key) | |
793 | if err == nil && len(val) != 0 { | |
794 | // In theory if SupportsMultiple was false for this key we could | |
795 | // stop looking here. But the caller has asked us to find all | |
796 | // instances of the keyword (and could use Get() if they wanted) so | |
797 | // let's keep looking. | |
798 | vals = append(vals, val...) | |
799 | } | |
800 | } | |
801 | return vals, nil | |
802 | } | |
803 | ||
613 | 804 | // String prints out a string representation of this Include directive. Note |
614 | 805 | // included Config files are not printed as part of this representation. |
615 | 806 | func (inc *Include) String() string { |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "bytes" |
4 | "io/ioutil" | |
5 | 4 | "log" |
6 | 5 | "os" |
7 | 6 | "path/filepath" |
10 | 9 | ) |
11 | 10 | |
12 | 11 | func loadFile(t *testing.T, filename string) []byte { |
13 | data, err := ioutil.ReadFile(filename) | |
12 | t.Helper() | |
13 | data, err := os.ReadFile(filename) | |
14 | 14 | if err != nil { |
15 | 15 | t.Fatal(err) |
16 | 16 | } |
17 | 17 | return data |
18 | 18 | } |
19 | 19 | |
20 | var files = []string{"testdata/config1", "testdata/config2"} | |
20 | var files = []string{ | |
21 | "testdata/config1", | |
22 | "testdata/config2", | |
23 | "testdata/eol-comments", | |
24 | } | |
21 | 25 | |
22 | 26 | func TestDecode(t *testing.T) { |
23 | 27 | for _, filename := range files { |
28 | 32 | } |
29 | 33 | out := cfg.String() |
30 | 34 | if out != string(data) { |
31 | t.Errorf("out != data: out: %q\ndata: %q", out, string(data)) | |
35 | t.Errorf("%s out != data: got:\n%s\nwant:\n%s\n", filename, out, string(data)) | |
32 | 36 | } |
33 | 37 | } |
34 | 38 | } |
66 | 70 | } |
67 | 71 | } |
68 | 72 | |
73 | func TestGetAllWithDefault(t *testing.T) { | |
74 | us := &UserSettings{ | |
75 | userConfigFinder: testConfigFinder("testdata/config1"), | |
76 | } | |
77 | ||
78 | val, err := us.GetAllStrict("wap", "PasswordAuthentication") | |
79 | if err != nil { | |
80 | t.Fatalf("expected nil err, got %v", err) | |
81 | } | |
82 | if len(val) != 1 || val[0] != "yes" { | |
83 | t.Errorf("expected to get PasswordAuthentication yes, got %q", val) | |
84 | } | |
85 | } | |
86 | ||
87 | func TestGetIdentities(t *testing.T) { | |
88 | us := &UserSettings{ | |
89 | userConfigFinder: testConfigFinder("testdata/identities"), | |
90 | } | |
91 | ||
92 | val, err := us.GetAllStrict("hasidentity", "IdentityFile") | |
93 | if err != nil { | |
94 | t.Errorf("expected nil err, got %v", err) | |
95 | } | |
96 | if len(val) != 1 || val[0] != "file1" { | |
97 | t.Errorf(`expected ["file1"], got %v`, val) | |
98 | } | |
99 | ||
100 | val, err = us.GetAllStrict("has2identity", "IdentityFile") | |
101 | if err != nil { | |
102 | t.Errorf("expected nil err, got %v", err) | |
103 | } | |
104 | if len(val) != 2 || val[0] != "f1" || val[1] != "f2" { | |
105 | t.Errorf(`expected [\"f1\", \"f2\"], got %v`, val) | |
106 | } | |
107 | ||
108 | val, err = us.GetAllStrict("randomhost", "IdentityFile") | |
109 | if err != nil { | |
110 | t.Errorf("expected nil err, got %v", err) | |
111 | } | |
112 | if len(val) != len(defaultProtocol2Identities) { | |
113 | // TODO: return the right values here. | |
114 | log.Printf("expected defaults, got %v", val) | |
115 | } else { | |
116 | for i, v := range defaultProtocol2Identities { | |
117 | if val[i] != v { | |
118 | t.Errorf("invalid %d in val, expected %s got %s", i, v, val[i]) | |
119 | } | |
120 | } | |
121 | } | |
122 | ||
123 | val, err = us.GetAllStrict("protocol1", "IdentityFile") | |
124 | if err != nil { | |
125 | t.Errorf("expected nil err, got %v", err) | |
126 | } | |
127 | if len(val) != 1 || val[0] != "~/.ssh/identity" { | |
128 | t.Errorf("expected [\"~/.ssh/identity\"], got %v", val) | |
129 | } | |
130 | } | |
131 | ||
69 | 132 | func TestGetInvalidPort(t *testing.T) { |
70 | 133 | us := &UserSettings{ |
71 | 134 | userConfigFinder: testConfigFinder("testdata/invalid-port"), |
93 | 156 | t.Fatalf("expected nil err, got %v", err) |
94 | 157 | } |
95 | 158 | if val != "" { |
159 | t.Errorf("expected to get CanonicalDomains '', got %q", val) | |
160 | } | |
161 | } | |
162 | ||
163 | func TestGetAllNotFoundNoDefault(t *testing.T) { | |
164 | us := &UserSettings{ | |
165 | userConfigFinder: testConfigFinder("testdata/config1"), | |
166 | } | |
167 | ||
168 | val, err := us.GetAllStrict("wap", "CanonicalDomains") | |
169 | if err != nil { | |
170 | t.Fatalf("expected nil err, got %v", err) | |
171 | } | |
172 | if len(val) != 0 { | |
96 | 173 | t.Errorf("expected to get CanonicalDomains '', got %q", val) |
97 | 174 | } |
98 | 175 | } |
193 | 270 | t.Skip("skipping fs write in short mode") |
194 | 271 | } |
195 | 272 | testPath := filepath.Join(homedir(), ".ssh", "kevinburke-ssh-config-test-file") |
196 | err := ioutil.WriteFile(testPath, includeFile, 0644) | |
273 | err := os.WriteFile(testPath, includeFile, 0644) | |
197 | 274 | if err != nil { |
198 | 275 | t.Skipf("couldn't write SSH config file: %v", err.Error()) |
199 | 276 | } |
212 | 289 | t.Skip("skipping fs write in short mode") |
213 | 290 | } |
214 | 291 | testPath := filepath.Join("/", "etc", "ssh", "kevinburke-ssh-config-test-file") |
215 | err := ioutil.WriteFile(testPath, includeFile, 0644) | |
292 | err := os.WriteFile(testPath, includeFile, 0644) | |
216 | 293 | if err != nil { |
217 | 294 | t.Skipf("couldn't write SSH config file: %v", err.Error()) |
218 | 295 | } |
236 | 313 | t.Skip("skipping fs write in short mode") |
237 | 314 | } |
238 | 315 | testPath := filepath.Join(homedir(), ".ssh", "kevinburke-ssh-config-recursive-include") |
239 | err := ioutil.WriteFile(testPath, recursiveIncludeFile, 0644) | |
316 | err := os.WriteFile(testPath, recursiveIncludeFile, 0644) | |
240 | 317 | if err != nil { |
241 | 318 | t.Skipf("couldn't write SSH config file: %v", err.Error()) |
242 | 319 | } |
257 | 334 | if testing.Short() { |
258 | 335 | t.Skip("skipping fs write in short mode") |
259 | 336 | } |
260 | data, err := ioutil.ReadFile("testdata/include") | |
337 | data, err := os.ReadFile("testdata/include") | |
261 | 338 | if err != nil { |
262 | 339 | log.Fatal(err) |
263 | 340 | } |
377 | 454 | t.Errorf("wrong port: got %q want 4242", port) |
378 | 455 | } |
379 | 456 | } |
457 | ||
458 | func TestCustomFinder(t *testing.T) { | |
459 | us := &UserSettings{} | |
460 | us.ConfigFinder(func() string { | |
461 | return "testdata/config1" | |
462 | }) | |
463 | ||
464 | val := us.Get("wap", "User") | |
465 | if val != "root" { | |
466 | t.Errorf("expected to find User root, got %q", val) | |
467 | } | |
468 | } |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "fmt" |
4 | "path/filepath" | |
4 | 5 | "strings" |
5 | 6 | |
6 | 7 | "github.com/kevinburke/ssh_config" |
45 | 46 | // 22 |
46 | 47 | // |
47 | 48 | } |
49 | ||
50 | func ExampleUserSettings_ConfigFinder() { | |
51 | // This can be used to test SSH config parsing. | |
52 | u := ssh_config.UserSettings{} | |
53 | u.ConfigFinder(func() string { | |
54 | return filepath.Join("testdata", "test_config") | |
55 | }) | |
56 | u.Get("example.com", "Host") | |
57 | } |
0 | //go:build 1.18 | |
1 | // +build 1.18 | |
2 | ||
3 | package ssh_config | |
4 | ||
5 | import ( | |
6 | "bytes" | |
7 | "testing" | |
8 | ) | |
9 | ||
10 | func FuzzDecode(f *testing.F) { | |
11 | f.Fuzz(func(t *testing.T, in []byte) { | |
12 | _, err := Decode(bytes.NewReader(in)) | |
13 | if err != nil { | |
14 | t.Fatalf("decode %q: %v", string(in), err) | |
15 | } | |
16 | }) | |
17 | } |
2 | 2 | import ( |
3 | 3 | "fmt" |
4 | 4 | "strings" |
5 | "unicode" | |
5 | 6 | ) |
6 | 7 | |
7 | 8 | type sshParser struct { |
121 | 122 | } |
122 | 123 | patterns = append(patterns, pat) |
123 | 124 | } |
125 | // val.val at this point could be e.g. "example.com " | |
126 | hostval := strings.TrimRightFunc(val.val, unicode.IsSpace) | |
127 | spaceBeforeComment := val.val[len(hostval):] | |
128 | val.val = hostval | |
124 | 129 | p.config.Hosts = append(p.config.Hosts, &Host{ |
125 | Patterns: patterns, | |
126 | Nodes: make([]Node, 0), | |
127 | EOLComment: comment, | |
128 | hasEquals: hasEquals, | |
130 | Patterns: patterns, | |
131 | Nodes: make([]Node, 0), | |
132 | EOLComment: comment, | |
133 | spaceBeforeComment: spaceBeforeComment, | |
134 | hasEquals: hasEquals, | |
129 | 135 | }) |
130 | 136 | return p.parseStart |
131 | 137 | } |
143 | 149 | lastHost.Nodes = append(lastHost.Nodes, inc) |
144 | 150 | return p.parseStart |
145 | 151 | } |
152 | shortval := strings.TrimRightFunc(val.val, unicode.IsSpace) | |
153 | spaceAfterValue := val.val[len(shortval):] | |
146 | 154 | kv := &KV{ |
147 | Key: key.val, | |
148 | Value: val.val, | |
149 | Comment: comment, | |
150 | hasEquals: hasEquals, | |
151 | leadingSpace: key.Position.Col - 1, | |
152 | position: key.Position, | |
155 | Key: key.val, | |
156 | Value: shortval, | |
157 | spaceAfterValue: spaceAfterValue, | |
158 | Comment: comment, | |
159 | hasEquals: hasEquals, | |
160 | leadingSpace: key.Position.Col - 1, | |
161 | position: key.Position, | |
153 | 162 | } |
154 | 163 | lastHost.Nodes = append(lastHost.Nodes, kv) |
155 | 164 | return p.parseStart |
0 | Host example # this comment terminates a Host line | |
1 | HostName example.com # aligned eol comment 1 | |
2 | ForwardX11Timeout 52w # aligned eol comment 2 | |
3 | # This comment takes up a whole line | |
4 | # This comment is offset and takes up a whole line | |
5 | AddressFamily inet # aligned eol comment 3 | |
6 | Port 4242 #compact comment |
+2
-0
0 | go test fuzz v1 | |
1 | []byte("#\t$OpenBSD: ssh_config,v 1.30 2016/02/20 23:06:23 sobrado Exp $\n\n# This is the ssh client system-wide configuration file. See\n# ssh_config(5) for more information. This file provides defaults for\n# users, and the values can be changed in per-user configuration files\n# or on the command line.\n\n# Configuration data is parsed as follows:\n# 1. command line options\n# 2. user-specific file\n# 3. system-wide file\n# Any configuration value is only changed the first time it is set.\n# Thus, host-specific definitions should be at the beginning of the\n# configuration file, and defaults at the end.\n\n# Site-wide defaults for some commonly used options. For a comprehensive\n# list of available options, their meanings and defaults, please see the\n# ssh_config(5) man page.\n\n# Host *\n# ForwardAgent no\n# ForwardX11 no\n# RhostsRSAAuthentication no\n# RSAAuthentication yes\n# PasswordAuthentication yes\n# HostbasedAuthentication no\n# GSSAPIAuthentication no\n# GSSAPIDelegateCredentials no\n# BatchMode no\n# CheckHostIP yes\n# AddressFamily any\n# ConnectTimeout 0\n# StrictHostKeyChecking ask\n# IdentityFile ~/.ssh/identity\n# IdentityFile ~/.ssh/id_rsa\n# IdentityFile ~/.ssh/id_dsa\n# IdentityFile ~/.ssh/id_ecdsa\n# IdentityFile ~/.ssh/id_ed25519\n# Port 22\n# Protocol 2\n# Cipher 3des\n# Ciphers aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc\n# MACs hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160\n# EscapeChar ~\n# Tunnel no\n# TunnelDevice any:any\n# PermitLocalCommand no\n# VisualHostKey no\n# ProxyCommand ssh -q -W %h:%p gateway.example.com\n# RekeyLimit 1G 1h\n")⏎ |
+2
-0
0 | go test fuzz v1 | |
1 | []byte("Host localhost 127.0.0.1 # A comment at the end of a host line.\n NoHostAuthenticationForLocalhost yes\n\n# A comment\n # A comment with leading spaces.\n\nHost wap\n User root\n KexAlgorithms diffie-hellman-group1-sha1\n\nHost [some stuff behind a NAT]\n Compression yes\n ProxyCommand ssh -qW %h:%p [NATrouter]\n\nHost wopr # there are 2 proxies available for this one...\n User root\n ProxyCommand sh -c \"ssh proxy1 -qW %h:22 || ssh proxy2 -qW %h:22\"\n\nHost dhcp-??\n UserKnownHostsFile /dev/null\n StrictHostKeyChecking no\n User root\n\nHost [my boxes] [*.mydomain]\n ForwardAgent yes\n ForwardX11 yes\n ForwardX11Trusted yes\n\nHost *\n #ControlMaster auto\n #ControlPath /tmp/ssh-master-%C\n #ControlPath /tmp/ssh-%u-%r@%h:%p\n #ControlPersist yes\n ForwardX11Timeout 52w\n XAuthLocation /usr/bin/xauth\n SendEnv LANG LC_*\n HostKeyAlgorithms ssh-ed25519,ssh-rsa\n AddressFamily inet\n #UpdateHostKeys ask\n")⏎ |
0 | ||
1 | Host hasidentity | |
2 | IdentityFile file1 | |
3 | ||
4 | Host has2identity | |
5 | IdentityFile f1 | |
6 | IdentityFile f2 | |
7 | ||
8 | Host protocol1 | |
9 | Protocol 1 | |
10 |
159 | 159 | strings.ToLower("VisualHostKey"): "no", |
160 | 160 | strings.ToLower("XAuthLocation"): "/usr/X11R6/bin/xauth", |
161 | 161 | } |
162 | ||
163 | // these identities are used for SSH protocol 2 | |
164 | var defaultProtocol2Identities = []string{ | |
165 | "~/.ssh/id_dsa", | |
166 | "~/.ssh/id_ecdsa", | |
167 | "~/.ssh/id_ed25519", | |
168 | "~/.ssh/id_rsa", | |
169 | } | |
170 | ||
171 | // these directives support multiple items that can be collected | |
172 | // across multiple files | |
173 | var pluralDirectives = map[string]bool{ | |
174 | "CertificateFile": true, | |
175 | "IdentityFile": true, | |
176 | "DynamicForward": true, | |
177 | "RemoteForward": true, | |
178 | "SendEnv": true, | |
179 | "SetEnv": true, | |
180 | } | |
181 | ||
182 | // SupportsMultiple reports whether a directive can be specified multiple times. | |
183 | func SupportsMultiple(key string) bool { | |
184 | return pluralDirectives[strings.ToLower(key)] | |
185 | } |