|
|
|
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
|
|
|
|
}
|