]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package netrc |
2 | ||
3 | import ( | |
4 | "bufio" | |
5 | "bytes" | |
6 | "fmt" | |
7 | "io" | |
8 | "io/ioutil" | |
9 | "os" | |
10 | "strings" | |
11 | "sync" | |
12 | "unicode" | |
13 | "unicode/utf8" | |
14 | ) | |
15 | ||
16 | type tkType int | |
17 | ||
18 | const ( | |
19 | tkMachine tkType = iota | |
20 | tkDefault | |
21 | tkLogin | |
22 | tkPassword | |
23 | tkAccount | |
24 | tkMacdef | |
25 | tkComment | |
26 | tkWhitespace | |
27 | ) | |
28 | ||
29 | var keywords = map[string]tkType{ | |
30 | "machine": tkMachine, | |
31 | "default": tkDefault, | |
32 | "login": tkLogin, | |
33 | "password": tkPassword, | |
34 | "account": tkAccount, | |
35 | "macdef": tkMacdef, | |
36 | "#": tkComment, | |
37 | } | |
38 | ||
39 | type Netrc struct { | |
40 | tokens []*token | |
41 | machines []*Machine | |
42 | macros Macros | |
43 | updateLock sync.Mutex | |
44 | } | |
45 | ||
46 | // FindMachine returns the Machine in n named by name. If a machine named by | |
47 | // name exists, it is returned. If no Machine with name name is found and there | |
48 | // is a ``default'' machine, the ``default'' machine is returned. Otherwise, nil | |
49 | // is returned. | |
50 | func (n *Netrc) FindMachine(name string) (m *Machine) { | |
51 | // TODO(bgentry): not safe for concurrency | |
52 | var def *Machine | |
53 | for _, m = range n.machines { | |
54 | if m.Name == name { | |
55 | return m | |
56 | } | |
57 | if m.IsDefault() { | |
58 | def = m | |
59 | } | |
60 | } | |
61 | if def == nil { | |
62 | return nil | |
63 | } | |
64 | return def | |
65 | } | |
66 | ||
67 | // MarshalText implements the encoding.TextMarshaler interface to encode a | |
68 | // Netrc into text format. | |
69 | func (n *Netrc) MarshalText() (text []byte, err error) { | |
70 | // TODO(bgentry): not safe for concurrency | |
71 | for i := range n.tokens { | |
72 | switch n.tokens[i].kind { | |
73 | case tkComment, tkDefault, tkWhitespace: // always append these types | |
74 | text = append(text, n.tokens[i].rawkind...) | |
75 | default: | |
76 | if n.tokens[i].value != "" { // skip empty-value tokens | |
77 | text = append(text, n.tokens[i].rawkind...) | |
78 | } | |
79 | } | |
80 | if n.tokens[i].kind == tkMacdef { | |
81 | text = append(text, ' ') | |
82 | text = append(text, n.tokens[i].macroName...) | |
83 | } | |
84 | text = append(text, n.tokens[i].rawvalue...) | |
85 | } | |
86 | return | |
87 | } | |
88 | ||
89 | func (n *Netrc) NewMachine(name, login, password, account string) *Machine { | |
90 | n.updateLock.Lock() | |
91 | defer n.updateLock.Unlock() | |
92 | ||
93 | prefix := "\n" | |
94 | if len(n.tokens) == 0 { | |
95 | prefix = "" | |
96 | } | |
97 | m := &Machine{ | |
98 | Name: name, | |
99 | Login: login, | |
100 | Password: password, | |
101 | Account: account, | |
102 | ||
103 | nametoken: &token{ | |
104 | kind: tkMachine, | |
105 | rawkind: []byte(prefix + "machine"), | |
106 | value: name, | |
107 | rawvalue: []byte(" " + name), | |
108 | }, | |
109 | logintoken: &token{ | |
110 | kind: tkLogin, | |
111 | rawkind: []byte("\n\tlogin"), | |
112 | value: login, | |
113 | rawvalue: []byte(" " + login), | |
114 | }, | |
115 | passtoken: &token{ | |
116 | kind: tkPassword, | |
117 | rawkind: []byte("\n\tpassword"), | |
118 | value: password, | |
119 | rawvalue: []byte(" " + password), | |
120 | }, | |
121 | accounttoken: &token{ | |
122 | kind: tkAccount, | |
123 | rawkind: []byte("\n\taccount"), | |
124 | value: account, | |
125 | rawvalue: []byte(" " + account), | |
126 | }, | |
127 | } | |
128 | n.insertMachineTokensBeforeDefault(m) | |
129 | for i := range n.machines { | |
130 | if n.machines[i].IsDefault() { | |
131 | n.machines = append(append(n.machines[:i], m), n.machines[i:]...) | |
132 | return m | |
133 | } | |
134 | } | |
135 | n.machines = append(n.machines, m) | |
136 | return m | |
137 | } | |
138 | ||
139 | func (n *Netrc) insertMachineTokensBeforeDefault(m *Machine) { | |
140 | newtokens := []*token{m.nametoken} | |
141 | if m.logintoken.value != "" { | |
142 | newtokens = append(newtokens, m.logintoken) | |
143 | } | |
144 | if m.passtoken.value != "" { | |
145 | newtokens = append(newtokens, m.passtoken) | |
146 | } | |
147 | if m.accounttoken.value != "" { | |
148 | newtokens = append(newtokens, m.accounttoken) | |
149 | } | |
150 | for i := range n.tokens { | |
151 | if n.tokens[i].kind == tkDefault { | |
152 | // found the default, now insert tokens before it | |
153 | n.tokens = append(n.tokens[:i], append(newtokens, n.tokens[i:]...)...) | |
154 | return | |
155 | } | |
156 | } | |
157 | // didn't find a default, just add the newtokens to the end | |
158 | n.tokens = append(n.tokens, newtokens...) | |
159 | return | |
160 | } | |
161 | ||
162 | func (n *Netrc) RemoveMachine(name string) { | |
163 | n.updateLock.Lock() | |
164 | defer n.updateLock.Unlock() | |
165 | ||
166 | for i := range n.machines { | |
167 | if n.machines[i] != nil && n.machines[i].Name == name { | |
168 | m := n.machines[i] | |
169 | for _, t := range []*token{ | |
170 | m.nametoken, m.logintoken, m.passtoken, m.accounttoken, | |
171 | } { | |
172 | n.removeToken(t) | |
173 | } | |
174 | n.machines = append(n.machines[:i], n.machines[i+1:]...) | |
175 | return | |
176 | } | |
177 | } | |
178 | } | |
179 | ||
180 | func (n *Netrc) removeToken(t *token) { | |
181 | if t != nil { | |
182 | for i := range n.tokens { | |
183 | if n.tokens[i] == t { | |
184 | n.tokens = append(n.tokens[:i], n.tokens[i+1:]...) | |
185 | return | |
186 | } | |
187 | } | |
188 | } | |
189 | } | |
190 | ||
191 | // Machine contains information about a remote machine. | |
192 | type Machine struct { | |
193 | Name string | |
194 | Login string | |
195 | Password string | |
196 | Account string | |
197 | ||
198 | nametoken *token | |
199 | logintoken *token | |
200 | passtoken *token | |
201 | accounttoken *token | |
202 | } | |
203 | ||
204 | // IsDefault returns true if the machine is a "default" token, denoted by an | |
205 | // empty name. | |
206 | func (m *Machine) IsDefault() bool { | |
207 | return m.Name == "" | |
208 | } | |
209 | ||
210 | // UpdatePassword sets the password for the Machine m. | |
211 | func (m *Machine) UpdatePassword(newpass string) { | |
212 | m.Password = newpass | |
213 | updateTokenValue(m.passtoken, newpass) | |
214 | } | |
215 | ||
216 | // UpdateLogin sets the login for the Machine m. | |
217 | func (m *Machine) UpdateLogin(newlogin string) { | |
218 | m.Login = newlogin | |
219 | updateTokenValue(m.logintoken, newlogin) | |
220 | } | |
221 | ||
222 | // UpdateAccount sets the login for the Machine m. | |
223 | func (m *Machine) UpdateAccount(newaccount string) { | |
224 | m.Account = newaccount | |
225 | updateTokenValue(m.accounttoken, newaccount) | |
226 | } | |
227 | ||
228 | func updateTokenValue(t *token, value string) { | |
229 | oldvalue := t.value | |
230 | t.value = value | |
231 | newraw := make([]byte, len(t.rawvalue)) | |
232 | copy(newraw, t.rawvalue) | |
233 | t.rawvalue = append( | |
234 | bytes.TrimSuffix(newraw, []byte(oldvalue)), | |
235 | []byte(value)..., | |
236 | ) | |
237 | } | |
238 | ||
239 | // Macros contains all the macro definitions in a netrc file. | |
240 | type Macros map[string]string | |
241 | ||
242 | type token struct { | |
243 | kind tkType | |
244 | macroName string | |
245 | value string | |
246 | rawkind []byte | |
247 | rawvalue []byte | |
248 | } | |
249 | ||
250 | // Error represents a netrc file parse error. | |
251 | type Error struct { | |
252 | LineNum int // Line number | |
253 | Msg string // Error message | |
254 | } | |
255 | ||
256 | // Error returns a string representation of error e. | |
257 | func (e *Error) Error() string { | |
258 | return fmt.Sprintf("line %d: %s", e.LineNum, e.Msg) | |
259 | } | |
260 | ||
261 | func (e *Error) BadDefaultOrder() bool { | |
262 | return e.Msg == errBadDefaultOrder | |
263 | } | |
264 | ||
265 | const errBadDefaultOrder = "default token must appear after all machine tokens" | |
266 | ||
267 | // scanLinesKeepPrefix is a split function for a Scanner that returns each line | |
268 | // of text. The returned token may include newlines if they are before the | |
269 | // first non-space character. The returned line may be empty. The end-of-line | |
270 | // marker is one optional carriage return followed by one mandatory newline. In | |
271 | // regular expression notation, it is `\r?\n`. The last non-empty line of | |
272 | // input will be returned even if it has no newline. | |
273 | func scanLinesKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { | |
274 | if atEOF && len(data) == 0 { | |
275 | return 0, nil, nil | |
276 | } | |
277 | // Skip leading spaces. | |
278 | start := 0 | |
279 | for width := 0; start < len(data); start += width { | |
280 | var r rune | |
281 | r, width = utf8.DecodeRune(data[start:]) | |
282 | if !unicode.IsSpace(r) { | |
283 | break | |
284 | } | |
285 | } | |
286 | if i := bytes.IndexByte(data[start:], '\n'); i >= 0 { | |
287 | // We have a full newline-terminated line. | |
288 | return start + i, data[0 : start+i], nil | |
289 | } | |
290 | // If we're at EOF, we have a final, non-terminated line. Return it. | |
291 | if atEOF { | |
292 | return len(data), data, nil | |
293 | } | |
294 | // Request more data. | |
295 | return 0, nil, nil | |
296 | } | |
297 | ||
298 | // scanWordsKeepPrefix is a split function for a Scanner that returns each | |
299 | // space-separated word of text, with prefixing spaces included. It will never | |
300 | // return an empty string. The definition of space is set by unicode.IsSpace. | |
301 | // | |
302 | // Adapted from bufio.ScanWords(). | |
303 | func scanTokensKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { | |
304 | // Skip leading spaces. | |
305 | start := 0 | |
306 | for width := 0; start < len(data); start += width { | |
307 | var r rune | |
308 | r, width = utf8.DecodeRune(data[start:]) | |
309 | if !unicode.IsSpace(r) { | |
310 | break | |
311 | } | |
312 | } | |
313 | if atEOF && len(data) == 0 || start == len(data) { | |
314 | return len(data), data, nil | |
315 | } | |
316 | if len(data) > start && data[start] == '#' { | |
317 | return scanLinesKeepPrefix(data, atEOF) | |
318 | } | |
319 | // Scan until space, marking end of word. | |
320 | for width, i := 0, start; i < len(data); i += width { | |
321 | var r rune | |
322 | r, width = utf8.DecodeRune(data[i:]) | |
323 | if unicode.IsSpace(r) { | |
324 | return i, data[:i], nil | |
325 | } | |
326 | } | |
327 | // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. | |
328 | if atEOF && len(data) > start { | |
329 | return len(data), data, nil | |
330 | } | |
331 | // Request more data. | |
332 | return 0, nil, nil | |
333 | } | |
334 | ||
335 | func newToken(rawb []byte) (*token, error) { | |
336 | _, tkind, err := bufio.ScanWords(rawb, true) | |
337 | if err != nil { | |
338 | return nil, err | |
339 | } | |
340 | var ok bool | |
341 | t := token{rawkind: rawb} | |
342 | t.kind, ok = keywords[string(tkind)] | |
343 | if !ok { | |
344 | trimmed := strings.TrimSpace(string(tkind)) | |
345 | if trimmed == "" { | |
346 | t.kind = tkWhitespace // whitespace-only, should happen only at EOF | |
347 | return &t, nil | |
348 | } | |
349 | if strings.HasPrefix(trimmed, "#") { | |
350 | t.kind = tkComment // this is a comment | |
351 | return &t, nil | |
352 | } | |
353 | return &t, fmt.Errorf("keyword expected; got " + string(tkind)) | |
354 | } | |
355 | return &t, nil | |
356 | } | |
357 | ||
358 | func scanValue(scanner *bufio.Scanner, pos int) ([]byte, string, int, error) { | |
359 | if scanner.Scan() { | |
360 | raw := scanner.Bytes() | |
361 | pos += bytes.Count(raw, []byte{'\n'}) | |
362 | return raw, strings.TrimSpace(string(raw)), pos, nil | |
363 | } | |
364 | if err := scanner.Err(); err != nil { | |
365 | return nil, "", pos, &Error{pos, err.Error()} | |
366 | } | |
367 | return nil, "", pos, nil | |
368 | } | |
369 | ||
370 | func parse(r io.Reader, pos int) (*Netrc, error) { | |
371 | b, err := ioutil.ReadAll(r) | |
372 | if err != nil { | |
373 | return nil, err | |
374 | } | |
375 | ||
376 | nrc := Netrc{machines: make([]*Machine, 0, 20), macros: make(Macros, 10)} | |
377 | ||
378 | defaultSeen := false | |
379 | var currentMacro *token | |
380 | var m *Machine | |
381 | var t *token | |
382 | scanner := bufio.NewScanner(bytes.NewReader(b)) | |
383 | scanner.Split(scanTokensKeepPrefix) | |
384 | ||
385 | for scanner.Scan() { | |
386 | rawb := scanner.Bytes() | |
387 | if len(rawb) == 0 { | |
388 | break | |
389 | } | |
390 | pos += bytes.Count(rawb, []byte{'\n'}) | |
391 | t, err = newToken(rawb) | |
392 | if err != nil { | |
393 | if currentMacro == nil { | |
394 | return nil, &Error{pos, err.Error()} | |
395 | } | |
396 | currentMacro.rawvalue = append(currentMacro.rawvalue, rawb...) | |
397 | continue | |
398 | } | |
399 | ||
400 | if currentMacro != nil && bytes.Contains(rawb, []byte{'\n', '\n'}) { | |
401 | // if macro rawvalue + rawb would contain \n\n, then macro def is over | |
402 | currentMacro.value = strings.TrimLeft(string(currentMacro.rawvalue), "\r\n") | |
403 | nrc.macros[currentMacro.macroName] = currentMacro.value | |
404 | currentMacro = nil | |
405 | } | |
406 | ||
407 | switch t.kind { | |
408 | case tkMacdef: | |
409 | if _, t.macroName, pos, err = scanValue(scanner, pos); err != nil { | |
410 | return nil, &Error{pos, err.Error()} | |
411 | } | |
412 | currentMacro = t | |
413 | case tkDefault: | |
414 | if defaultSeen { | |
415 | return nil, &Error{pos, "multiple default token"} | |
416 | } | |
417 | if m != nil { | |
418 | nrc.machines, m = append(nrc.machines, m), nil | |
419 | } | |
420 | m = new(Machine) | |
421 | m.Name = "" | |
422 | defaultSeen = true | |
423 | case tkMachine: | |
424 | if defaultSeen { | |
425 | return nil, &Error{pos, errBadDefaultOrder} | |
426 | } | |
427 | if m != nil { | |
428 | nrc.machines, m = append(nrc.machines, m), nil | |
429 | } | |
430 | m = new(Machine) | |
431 | if t.rawvalue, m.Name, pos, err = scanValue(scanner, pos); err != nil { | |
432 | return nil, &Error{pos, err.Error()} | |
433 | } | |
434 | t.value = m.Name | |
435 | m.nametoken = t | |
436 | case tkLogin: | |
437 | if m == nil || m.Login != "" { | |
438 | return nil, &Error{pos, "unexpected token login "} | |
439 | } | |
440 | if t.rawvalue, m.Login, pos, err = scanValue(scanner, pos); err != nil { | |
441 | return nil, &Error{pos, err.Error()} | |
442 | } | |
443 | t.value = m.Login | |
444 | m.logintoken = t | |
445 | case tkPassword: | |
446 | if m == nil || m.Password != "" { | |
447 | return nil, &Error{pos, "unexpected token password"} | |
448 | } | |
449 | if t.rawvalue, m.Password, pos, err = scanValue(scanner, pos); err != nil { | |
450 | return nil, &Error{pos, err.Error()} | |
451 | } | |
452 | t.value = m.Password | |
453 | m.passtoken = t | |
454 | case tkAccount: | |
455 | if m == nil || m.Account != "" { | |
456 | return nil, &Error{pos, "unexpected token account"} | |
457 | } | |
458 | if t.rawvalue, m.Account, pos, err = scanValue(scanner, pos); err != nil { | |
459 | return nil, &Error{pos, err.Error()} | |
460 | } | |
461 | t.value = m.Account | |
462 | m.accounttoken = t | |
463 | } | |
464 | ||
465 | nrc.tokens = append(nrc.tokens, t) | |
466 | } | |
467 | ||
468 | if err := scanner.Err(); err != nil { | |
469 | return nil, err | |
470 | } | |
471 | ||
472 | if m != nil { | |
473 | nrc.machines, m = append(nrc.machines, m), nil | |
474 | } | |
475 | return &nrc, nil | |
476 | } | |
477 | ||
478 | // ParseFile opens the file at filename and then passes its io.Reader to | |
479 | // Parse(). | |
480 | func ParseFile(filename string) (*Netrc, error) { | |
481 | fd, err := os.Open(filename) | |
482 | if err != nil { | |
483 | return nil, err | |
484 | } | |
485 | defer fd.Close() | |
486 | return Parse(fd) | |
487 | } | |
488 | ||
489 | // Parse parses from the the Reader r as a netrc file and returns the set of | |
490 | // machine information and macros defined in it. The ``default'' machine, | |
491 | // which is intended to be used when no machine name matches, is identified | |
492 | // by an empty machine name. There can be only one ``default'' machine. | |
493 | // | |
494 | // If there is a parsing error, an Error is returned. | |
495 | func Parse(r io.Reader) (*Netrc, error) { | |
496 | return parse(r, 1) | |
497 | } | |
498 | ||
499 | // FindMachine parses the netrc file identified by filename and returns the | |
500 | // Machine named by name. If a problem occurs parsing the file at filename, an | |
501 | // error is returned. If a machine named by name exists, it is returned. If no | |
502 | // Machine with name name is found and there is a ``default'' machine, the | |
503 | // ``default'' machine is returned. Otherwise, nil is returned. | |
504 | func FindMachine(filename, name string) (m *Machine, err error) { | |
505 | n, err := ParseFile(filename) | |
506 | if err != nil { | |
507 | return nil, err | |
508 | } | |
509 | return n.FindMachine(name), nil | |
510 | } |