New upstream release.
Debian Janitor
1 year, 11 months ago
0 | on: [push, pull_request] | |
1 | name: Test | |
2 | jobs: | |
3 | test: | |
4 | strategy: | |
5 | matrix: | |
6 | go-version: [1.17.x, 1.18.x] | |
7 | platform: [ubuntu-latest] | |
8 | runs-on: ${{ matrix.platform }} | |
9 | steps: | |
10 | - name: Install Go | |
11 | uses: WillAbides/setup-go-faster@main | |
12 | with: | |
13 | go-version: ${{ matrix.go-version }} | |
14 | - uses: actions/checkout@v2 | |
15 | with: | |
16 | path: './src/github.com/kevinburke/ssh_config' | |
17 | # staticcheck needs this for GOPATH | |
18 | - run: | | |
19 | echo "GO111MODULE=off" >> $GITHUB_ENV | |
20 | echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV | |
21 | echo "PATH=$GITHUB_WORKSPACE/bin:$PATH" >> $GITHUB_ENV | |
22 | - name: Run tests | |
23 | run: make lint race-test | |
24 | 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. |
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 | |
101 | 100 | return val, nil |
102 | 101 | } |
103 | 102 | |
103 | func findAll(c *Config, alias, key string) ([]string, error) { | |
104 | if c == nil { | |
105 | return nil, nil | |
106 | } | |
107 | return c.GetAll(alias, key) | |
108 | } | |
109 | ||
104 | 110 | // Get finds the first value for key within a declaration that matches the |
105 | 111 | // alias. Get returns the empty string if no value was found, or if IgnoreErrors |
106 | 112 | // is false and we could not parse the configuration file. Use GetStrict to |
113 | 119 | return DefaultUserSettings.Get(alias, key) |
114 | 120 | } |
115 | 121 | |
122 | // GetAll retrieves zero or more directives for key for the given alias. GetAll | |
123 | // returns nil if no value was found, or if IgnoreErrors is false and we could | |
124 | // not parse the configuration file. Use GetAllStrict to disambiguate the | |
125 | // latter cases. | |
126 | // | |
127 | // In most cases you want to use Get or GetStrict, which returns a single value. | |
128 | // However, a subset of ssh configuration values (IdentityFile, for example) | |
129 | // allow you to specify multiple directives. | |
130 | // | |
131 | // The match for key is case insensitive. | |
132 | // | |
133 | // GetAll is a wrapper around DefaultUserSettings.GetAll. | |
134 | func GetAll(alias, key string) []string { | |
135 | return DefaultUserSettings.GetAll(alias, key) | |
136 | } | |
137 | ||
116 | 138 | // GetStrict finds the first value for key within a declaration that matches the |
117 | 139 | // alias. If key has a default value and no matching configuration is found, the |
118 | 140 | // default will be returned. For more information on default values and the way |
119 | 141 | // patterns are matched, see the manpage for ssh_config. |
120 | 142 | // |
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. | |
143 | // The returned error will be non-nil if and only if a user's configuration file | |
144 | // or the system configuration file could not be parsed, and u.IgnoreErrors is | |
145 | // false. | |
123 | 146 | // |
124 | 147 | // GetStrict is a wrapper around DefaultUserSettings.GetStrict. |
125 | 148 | func GetStrict(alias, key string) (string, error) { |
126 | 149 | return DefaultUserSettings.GetStrict(alias, key) |
150 | } | |
151 | ||
152 | // GetAllStrict retrieves zero or more directives for key for the given alias. | |
153 | // | |
154 | // In most cases you want to use Get or GetStrict, which returns a single value. | |
155 | // However, a subset of ssh configuration values (IdentityFile, for example) | |
156 | // allow you to specify multiple directives. | |
157 | // | |
158 | // The returned error will be non-nil if and only if a user's configuration file | |
159 | // or the system configuration file could not be parsed, and u.IgnoreErrors is | |
160 | // false. | |
161 | // | |
162 | // GetAllStrict is a wrapper around DefaultUserSettings.GetAllStrict. | |
163 | func GetAllStrict(alias, key string) ([]string, error) { | |
164 | return DefaultUserSettings.GetAllStrict(alias, key) | |
127 | 165 | } |
128 | 166 | |
129 | 167 | // Get finds the first value for key within a declaration that matches the |
140 | 178 | return val |
141 | 179 | } |
142 | 180 | |
181 | // GetAll retrieves zero or more directives for key for the given alias. GetAll | |
182 | // returns nil if no value was found, or if IgnoreErrors is false and we could | |
183 | // not parse the configuration file. Use GetStrict to disambiguate the latter | |
184 | // cases. | |
185 | // | |
186 | // The match for key is case insensitive. | |
187 | func (u *UserSettings) GetAll(alias, key string) []string { | |
188 | val, _ := u.GetAllStrict(alias, key) | |
189 | return val | |
190 | } | |
191 | ||
143 | 192 | // GetStrict finds the first value for key within a declaration that matches the |
144 | 193 | // alias. If key has a default value and no matching configuration is found, the |
145 | 194 | // default will be returned. For more information on default values and the way |
148 | 197 | // error will be non-nil if and only if a user's configuration file or the |
149 | 198 | // system configuration file could not be parsed, and u.IgnoreErrors is false. |
150 | 199 | func (u *UserSettings) GetStrict(alias, key string) (string, error) { |
200 | u.doLoadConfigs() | |
201 | //lint:ignore S1002 I prefer it this way | |
202 | if u.onceErr != nil && u.IgnoreErrors == false { | |
203 | return "", u.onceErr | |
204 | } | |
205 | val, err := findVal(u.userConfig, alias, key) | |
206 | if err != nil || val != "" { | |
207 | return val, err | |
208 | } | |
209 | val2, err2 := findVal(u.systemConfig, alias, key) | |
210 | if err2 != nil || val2 != "" { | |
211 | return val2, err2 | |
212 | } | |
213 | return Default(key), nil | |
214 | } | |
215 | ||
216 | // GetAllStrict retrieves zero or more directives for key for the given alias. | |
217 | // If key has a default value and no matching configuration is found, the | |
218 | // default will be returned. For more information on default values and the way | |
219 | // patterns are matched, see the manpage for ssh_config. | |
220 | // | |
221 | // The returned error will be non-nil if and only if a user's configuration file | |
222 | // or the system configuration file could not be parsed, and u.IgnoreErrors is | |
223 | // false. | |
224 | func (u *UserSettings) GetAllStrict(alias, key string) ([]string, error) { | |
225 | u.doLoadConfigs() | |
226 | //lint:ignore S1002 I prefer it this way | |
227 | if u.onceErr != nil && u.IgnoreErrors == false { | |
228 | return nil, u.onceErr | |
229 | } | |
230 | val, err := findAll(u.userConfig, alias, key) | |
231 | if err != nil || val != nil { | |
232 | return val, err | |
233 | } | |
234 | val2, err2 := findAll(u.systemConfig, alias, key) | |
235 | if err2 != nil || val2 != nil { | |
236 | return val2, err2 | |
237 | } | |
238 | // TODO: IdentityFile has multiple default values that we should return. | |
239 | if def := Default(key); def != "" { | |
240 | return []string{def}, nil | |
241 | } | |
242 | return []string{}, nil | |
243 | } | |
244 | ||
245 | func (u *UserSettings) doLoadConfigs() { | |
151 | 246 | u.loadConfigs.Do(func() { |
152 | 247 | // can't parse user file, that's ok. |
153 | 248 | var filename string |
175 | 270 | return |
176 | 271 | } |
177 | 272 | }) |
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 | 273 | } |
192 | 274 | |
193 | 275 | func parseFile(filename string) (*Config, error) { |
195 | 277 | } |
196 | 278 | |
197 | 279 | func parseWithDepth(filename string, depth uint8) (*Config, error) { |
198 | b, err := ioutil.ReadFile(filename) | |
280 | b, err := os.ReadFile(filename) | |
199 | 281 | if err != nil { |
200 | 282 | return nil, err |
201 | 283 | } |
210 | 292 | // Decode reads r into a Config, or returns an error if r could not be parsed as |
211 | 293 | // an SSH config file. |
212 | 294 | func Decode(r io.Reader) (*Config, error) { |
213 | b, err := ioutil.ReadAll(r) | |
295 | b, err := io.ReadAll(r) | |
214 | 296 | if err != nil { |
215 | 297 | return nil, err |
216 | 298 | } |
299 | return decodeBytes(b, false, 0) | |
300 | } | |
301 | ||
302 | // DecodeBytes reads b into a Config, or returns an error if r could not be | |
303 | // parsed as an SSH config file. | |
304 | func DecodeBytes(b []byte) (*Config, error) { | |
217 | 305 | return decodeBytes(b, false, 0) |
218 | 306 | } |
219 | 307 | |
281 | 369 | return "", nil |
282 | 370 | } |
283 | 371 | |
372 | // GetAll returns all values in the configuration that match the alias and | |
373 | // contains key, or nil if none are present. | |
374 | func (c *Config) GetAll(alias, key string) ([]string, error) { | |
375 | lowerKey := strings.ToLower(key) | |
376 | all := []string(nil) | |
377 | for _, host := range c.Hosts { | |
378 | if !host.Matches(alias) { | |
379 | continue | |
380 | } | |
381 | for _, node := range host.Nodes { | |
382 | switch t := node.(type) { | |
383 | case *Empty: | |
384 | continue | |
385 | case *KV: | |
386 | // "keys are case insensitive" per the spec | |
387 | lkey := strings.ToLower(t.Key) | |
388 | if lkey == "match" { | |
389 | panic("can't handle Match directives") | |
390 | } | |
391 | if lkey == lowerKey { | |
392 | all = append(all, t.Value) | |
393 | } | |
394 | case *Include: | |
395 | val, _ := t.GetAll(alias, key) | |
396 | if len(val) > 0 { | |
397 | all = append(all, val...) | |
398 | } | |
399 | default: | |
400 | return nil, fmt.Errorf("unknown Node type %v", t) | |
401 | } | |
402 | } | |
403 | } | |
404 | ||
405 | return all, nil | |
406 | } | |
407 | ||
284 | 408 | // String returns a string representation of the Config file. |
285 | 409 | func (c Config) String() string { |
286 | 410 | return marshal(c).String() |
373 | 497 | // A Node is either a key/value pair or a comment line. |
374 | 498 | Nodes []Node |
375 | 499 | // EOLComment is the comment (if any) terminating the Host line. |
376 | EOLComment string | |
500 | EOLComment string | |
501 | // Whitespace if any between the Host declaration and a trailing comment. | |
502 | spaceBeforeComment string | |
503 | ||
377 | 504 | hasEquals bool |
378 | 505 | leadingSpace int // TODO: handle spaces vs tabs here. |
379 | 506 | // The file starts with an implicit "Host *" declaration. |
405 | 532 | // String prints h as it would appear in a config file. Minor tweaks may be |
406 | 533 | // present in the whitespace in the printed file. |
407 | 534 | func (h *Host) String() string { |
408 | var buf bytes.Buffer | |
535 | var buf strings.Builder | |
409 | 536 | //lint:ignore S1002 I prefer to write it this way |
410 | 537 | if h.implicit == false { |
411 | 538 | buf.WriteString(strings.Repeat(" ", int(h.leadingSpace))) |
421 | 548 | buf.WriteString(" ") |
422 | 549 | } |
423 | 550 | } |
551 | buf.WriteString(h.spaceBeforeComment) | |
424 | 552 | if h.EOLComment != "" { |
425 | buf.WriteString(" #") | |
553 | buf.WriteByte('#') | |
426 | 554 | buf.WriteString(h.EOLComment) |
427 | 555 | } |
428 | 556 | buf.WriteByte('\n') |
443 | 571 | // KV is a line in the config file that contains a key, a value, and possibly |
444 | 572 | // a comment. |
445 | 573 | 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 | |
574 | Key string | |
575 | Value string | |
576 | // Whitespace after the value but before any comment | |
577 | spaceAfterValue string | |
578 | Comment string | |
579 | hasEquals bool | |
580 | leadingSpace int // Space before the key. TODO handle spaces vs tabs. | |
581 | position Position | |
452 | 582 | } |
453 | 583 | |
454 | 584 | // Pos returns k's Position. |
456 | 586 | return k.position |
457 | 587 | } |
458 | 588 | |
459 | // String prints k as it was parsed in the config file. There may be slight | |
460 | // changes to the whitespace between values. | |
589 | // String prints k as it was parsed in the config file. | |
461 | 590 | func (k *KV) String() string { |
462 | 591 | if k == nil { |
463 | 592 | return "" |
466 | 595 | if k.hasEquals { |
467 | 596 | equals = " = " |
468 | 597 | } |
469 | line := fmt.Sprintf("%s%s%s%s", strings.Repeat(" ", int(k.leadingSpace)), k.Key, equals, k.Value) | |
598 | line := strings.Repeat(" ", int(k.leadingSpace)) + k.Key + equals + k.Value + k.spaceAfterValue | |
470 | 599 | if k.Comment != "" { |
471 | line += " #" + k.Comment | |
600 | line += "#" + k.Comment | |
472 | 601 | } |
473 | 602 | return line |
474 | 603 | } |
610 | 739 | return "" |
611 | 740 | } |
612 | 741 | |
742 | // GetAll finds all values in the Include statement matching the alias and the | |
743 | // given key. | |
744 | func (inc *Include) GetAll(alias, key string) ([]string, error) { | |
745 | inc.mu.Lock() | |
746 | defer inc.mu.Unlock() | |
747 | var vals []string | |
748 | ||
749 | // TODO: we search files in any order which is not correct | |
750 | for i := range inc.matches { | |
751 | cfg := inc.files[inc.matches[i]] | |
752 | if cfg == nil { | |
753 | panic("nil cfg") | |
754 | } | |
755 | val, err := cfg.GetAll(alias, key) | |
756 | if err == nil && len(val) != 0 { | |
757 | // In theory if SupportsMultiple was false for this key we could | |
758 | // stop looking here. But the caller has asked us to find all | |
759 | // instances of the keyword (and could use Get() if they wanted) so | |
760 | // let's keep looking. | |
761 | vals = append(vals, val...) | |
762 | } | |
763 | } | |
764 | return vals, nil | |
765 | } | |
766 | ||
613 | 767 | // String prints out a string representation of this Include directive. Note |
614 | 768 | // included Config files are not printed as part of this representation. |
615 | 769 | 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 | } |
0 | golang-github-kevinburke-ssh-config (1.0-2) UNRELEASED; urgency=low | |
0 | golang-github-kevinburke-ssh-config (1.2.0-1) UNRELEASED; urgency=low | |
1 | 1 | |
2 | 2 | * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, |
3 | 3 | Repository-Browse. |
4 | 4 | * Update standards version to 4.5.0, no changes needed. |
5 | * New upstream release. | |
5 | 6 | |
6 | -- Debian Janitor <janitor@jelmer.uk> Sat, 06 Jun 2020 18:47:39 -0000 | |
7 | -- Debian Janitor <janitor@jelmer.uk> Sun, 22 May 2022 04:52:38 -0000 | |
7 | 8 | |
8 | 9 | golang-github-kevinburke-ssh-config (1.0-1) unstable; urgency=medium |
9 | 10 |
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 | # Config file with dos line endings | |
1 | Host wap | |
2 | HostName wap.example.org | |
3 | Port 22 | |
4 | User root | |
5 | KexAlgorithms diffie-hellman-group1-sha1 | |
6 | ||
7 | Host wap2 | |
8 | HostName 8.8.8.8 | |
9 | User google | |
0 | # Config file with dos line endings | |
1 | Host wap | |
2 | HostName wap.example.org | |
3 | Port 22 | |
4 | User root | |
5 | KexAlgorithms diffie-hellman-group1-sha1 | |
6 | ||
7 | Host wap2 | |
8 | HostName 8.8.8.8 | |
9 | User google |
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 | } |