Previously we used the buffruneio package to buffer input. However, the error handling was not good, and we would often panic when parsing inputs. SSH config files are generally not large, on the order of kilobytes or megabytes, and it's fine to just read the entire thing into memory and then parse from there. This also simplifies the parser significantly and lets us remove a dependency and several defer calls. Add a test that panicked with the old version and then modify the code to ensure the test no longer panics. Thanks to Mark Nevill (@devnev) for the initial error report and failing test case. Fixes #10. Fixes #24.
192 lines
4.3 KiB
Go
192 lines
4.3 KiB
Go
package ssh_config
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type sshParser struct {
|
|
flow chan token
|
|
config *Config
|
|
tokensBuffer []token
|
|
currentTable []string
|
|
seenTableKeys []string
|
|
// /etc/ssh parser or local parser - used to find the default for relative
|
|
// filepaths in the Include directive
|
|
system bool
|
|
depth uint8
|
|
}
|
|
|
|
type sshParserStateFn func() sshParserStateFn
|
|
|
|
// Formats and panics an error message based on a token
|
|
func (p *sshParser) raiseErrorf(tok *token, msg string, args ...interface{}) {
|
|
// TODO this format is ugly
|
|
panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
|
|
}
|
|
|
|
func (p *sshParser) raiseError(tok *token, err error) {
|
|
if err == ErrDepthExceeded {
|
|
panic(err)
|
|
}
|
|
// TODO this format is ugly
|
|
panic(tok.Position.String() + ": " + err.Error())
|
|
}
|
|
|
|
func (p *sshParser) run() {
|
|
for state := p.parseStart; state != nil; {
|
|
state = state()
|
|
}
|
|
}
|
|
|
|
func (p *sshParser) peek() *token {
|
|
if len(p.tokensBuffer) != 0 {
|
|
return &(p.tokensBuffer[0])
|
|
}
|
|
|
|
tok, ok := <-p.flow
|
|
if !ok {
|
|
return nil
|
|
}
|
|
p.tokensBuffer = append(p.tokensBuffer, tok)
|
|
return &tok
|
|
}
|
|
|
|
func (p *sshParser) getToken() *token {
|
|
if len(p.tokensBuffer) != 0 {
|
|
tok := p.tokensBuffer[0]
|
|
p.tokensBuffer = p.tokensBuffer[1:]
|
|
return &tok
|
|
}
|
|
tok, ok := <-p.flow
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &tok
|
|
}
|
|
|
|
func (p *sshParser) parseStart() sshParserStateFn {
|
|
tok := p.peek()
|
|
|
|
// end of stream, parsing is finished
|
|
if tok == nil {
|
|
return nil
|
|
}
|
|
|
|
switch tok.typ {
|
|
case tokenComment, tokenEmptyLine:
|
|
return p.parseComment
|
|
case tokenKey:
|
|
return p.parseKV
|
|
case tokenEOF:
|
|
return nil
|
|
default:
|
|
p.raiseErrorf(tok, fmt.Sprintf("unexpected token %q\n", tok))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *sshParser) parseKV() sshParserStateFn {
|
|
key := p.getToken()
|
|
hasEquals := false
|
|
val := p.getToken()
|
|
if val.typ == tokenEquals {
|
|
hasEquals = true
|
|
val = p.getToken()
|
|
}
|
|
comment := ""
|
|
tok := p.peek()
|
|
if tok == nil {
|
|
tok = &token{typ: tokenEOF}
|
|
}
|
|
if tok.typ == tokenComment && tok.Position.Line == val.Position.Line {
|
|
tok = p.getToken()
|
|
comment = tok.val
|
|
}
|
|
if strings.ToLower(key.val) == "match" {
|
|
// https://github.com/kevinburke/ssh_config/issues/6
|
|
p.raiseErrorf(val, "ssh_config: Match directive parsing is unsupported")
|
|
return nil
|
|
}
|
|
if strings.ToLower(key.val) == "host" {
|
|
strPatterns := strings.Split(val.val, " ")
|
|
patterns := make([]*Pattern, 0)
|
|
for i := range strPatterns {
|
|
if strPatterns[i] == "" {
|
|
continue
|
|
}
|
|
pat, err := NewPattern(strPatterns[i])
|
|
if err != nil {
|
|
p.raiseErrorf(val, "Invalid host pattern: %v", err)
|
|
return nil
|
|
}
|
|
patterns = append(patterns, pat)
|
|
}
|
|
p.config.Hosts = append(p.config.Hosts, &Host{
|
|
Patterns: patterns,
|
|
Nodes: make([]Node, 0),
|
|
EOLComment: comment,
|
|
hasEquals: hasEquals,
|
|
})
|
|
return p.parseStart
|
|
}
|
|
lastHost := p.config.Hosts[len(p.config.Hosts)-1]
|
|
if strings.ToLower(key.val) == "include" {
|
|
inc, err := NewInclude(strings.Split(val.val, " "), hasEquals, key.Position, comment, p.system, p.depth+1)
|
|
if err == ErrDepthExceeded {
|
|
p.raiseError(val, err)
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
p.raiseErrorf(val, "Error parsing Include directive: %v", err)
|
|
return nil
|
|
}
|
|
lastHost.Nodes = append(lastHost.Nodes, inc)
|
|
return p.parseStart
|
|
}
|
|
kv := &KV{
|
|
Key: key.val,
|
|
Value: val.val,
|
|
Comment: comment,
|
|
hasEquals: hasEquals,
|
|
leadingSpace: key.Position.Col - 1,
|
|
position: key.Position,
|
|
}
|
|
lastHost.Nodes = append(lastHost.Nodes, kv)
|
|
return p.parseStart
|
|
}
|
|
|
|
func (p *sshParser) parseComment() sshParserStateFn {
|
|
comment := p.getToken()
|
|
lastHost := p.config.Hosts[len(p.config.Hosts)-1]
|
|
lastHost.Nodes = append(lastHost.Nodes, &Empty{
|
|
Comment: comment.val,
|
|
// account for the "#" as well
|
|
leadingSpace: comment.Position.Col - 2,
|
|
position: comment.Position,
|
|
})
|
|
return p.parseStart
|
|
}
|
|
|
|
func parseSSH(flow chan token, system bool, depth uint8) *Config {
|
|
// Ensure we consume tokens to completion even if parser exits early
|
|
defer func() {
|
|
for range flow {
|
|
}
|
|
}()
|
|
|
|
result := newConfig()
|
|
result.position = Position{1, 1}
|
|
parser := &sshParser{
|
|
flow: flow,
|
|
config: result,
|
|
tokensBuffer: make([]token, 0),
|
|
currentTable: make([]string, 0),
|
|
seenTableKeys: make([]string, 0),
|
|
system: system,
|
|
depth: depth,
|
|
}
|
|
parser.run()
|
|
return result
|
|
}
|