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 }