main
Raw Download raw file
  1package ssh_config
  2
  3import (
  4	"fmt"
  5	"strings"
  6	"unicode"
  7)
  8
  9type sshParser struct {
 10	flow          chan token
 11	config        *Config
 12	tokensBuffer  []token
 13	currentTable  []string
 14	seenTableKeys []string
 15	// /etc/ssh parser or local parser - used to find the default for relative
 16	// filepaths in the Include directive
 17	system bool
 18	depth  uint8
 19}
 20
 21type sshParserStateFn func() sshParserStateFn
 22
 23// Formats and panics an error message based on a token
 24func (p *sshParser) raiseErrorf(tok *token, msg string, args ...interface{}) {
 25	// TODO this format is ugly
 26	panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
 27}
 28
 29func (p *sshParser) raiseError(tok *token, err error) {
 30	if err == ErrDepthExceeded {
 31		panic(err)
 32	}
 33	// TODO this format is ugly
 34	panic(tok.Position.String() + ": " + err.Error())
 35}
 36
 37func (p *sshParser) run() {
 38	for state := p.parseStart; state != nil; {
 39		state = state()
 40	}
 41}
 42
 43func (p *sshParser) peek() *token {
 44	if len(p.tokensBuffer) != 0 {
 45		return &(p.tokensBuffer[0])
 46	}
 47
 48	tok, ok := <-p.flow
 49	if !ok {
 50		return nil
 51	}
 52	p.tokensBuffer = append(p.tokensBuffer, tok)
 53	return &tok
 54}
 55
 56func (p *sshParser) getToken() *token {
 57	if len(p.tokensBuffer) != 0 {
 58		tok := p.tokensBuffer[0]
 59		p.tokensBuffer = p.tokensBuffer[1:]
 60		return &tok
 61	}
 62	tok, ok := <-p.flow
 63	if !ok {
 64		return nil
 65	}
 66	return &tok
 67}
 68
 69func (p *sshParser) parseStart() sshParserStateFn {
 70	tok := p.peek()
 71
 72	// end of stream, parsing is finished
 73	if tok == nil {
 74		return nil
 75	}
 76
 77	switch tok.typ {
 78	case tokenComment, tokenEmptyLine:
 79		return p.parseComment
 80	case tokenKey:
 81		return p.parseKV
 82	case tokenEOF:
 83		return nil
 84	default:
 85		p.raiseErrorf(tok, fmt.Sprintf("unexpected token %q\n", tok))
 86	}
 87	return nil
 88}
 89
 90func (p *sshParser) parseKV() sshParserStateFn {
 91	key := p.getToken()
 92	hasEquals := false
 93	val := p.getToken()
 94	if val.typ == tokenEquals {
 95		hasEquals = true
 96		val = p.getToken()
 97	}
 98	comment := ""
 99	tok := p.peek()
100	if tok == nil {
101		tok = &token{typ: tokenEOF}
102	}
103	if tok.typ == tokenComment && tok.Position.Line == val.Position.Line {
104		tok = p.getToken()
105		comment = tok.val
106	}
107	if strings.ToLower(key.val) == "match" {
108		// https://github.com/kevinburke/ssh_config/issues/6
109		p.raiseErrorf(val, "ssh_config: Match directive parsing is unsupported")
110		return nil
111	}
112	if strings.ToLower(key.val) == "host" {
113		strPatterns := strings.Split(val.val, " ")
114		patterns := make([]*Pattern, 0)
115		for i := range strPatterns {
116			if strPatterns[i] == "" {
117				continue
118			}
119			pat, err := NewPattern(strPatterns[i])
120			if err != nil {
121				p.raiseErrorf(val, "Invalid host pattern: %v", err)
122				return nil
123			}
124			patterns = append(patterns, pat)
125		}
126		// val.val at this point could be e.g. "example.com       "
127		hostval := strings.TrimRightFunc(val.val, unicode.IsSpace)
128		spaceBeforeComment := val.val[len(hostval):]
129		val.val = hostval
130		p.config.Hosts = append(p.config.Hosts, &Host{
131			Patterns:           patterns,
132			Nodes:              make([]Node, 0),
133			EOLComment:         comment,
134			spaceBeforeComment: spaceBeforeComment,
135			hasEquals:          hasEquals,
136		})
137		return p.parseStart
138	}
139	lastHost := p.config.Hosts[len(p.config.Hosts)-1]
140	if strings.ToLower(key.val) == "include" {
141		inc, err := NewInclude(strings.Split(val.val, " "), hasEquals, key.Position, comment, p.system, p.depth+1)
142		if err == ErrDepthExceeded {
143			p.raiseError(val, err)
144			return nil
145		}
146		if err != nil {
147			p.raiseErrorf(val, "Error parsing Include directive: %v", err)
148			return nil
149		}
150		lastHost.Nodes = append(lastHost.Nodes, inc)
151		return p.parseStart
152	}
153	shortval := strings.TrimRightFunc(val.val, unicode.IsSpace)
154	spaceAfterValue := val.val[len(shortval):]
155	kv := &KV{
156		Key:             key.val,
157		Value:           shortval,
158		spaceAfterValue: spaceAfterValue,
159		Comment:         comment,
160		hasEquals:       hasEquals,
161		leadingSpace:    key.Position.Col - 1,
162		position:        key.Position,
163	}
164	lastHost.Nodes = append(lastHost.Nodes, kv)
165	return p.parseStart
166}
167
168func (p *sshParser) parseComment() sshParserStateFn {
169	comment := p.getToken()
170	lastHost := p.config.Hosts[len(p.config.Hosts)-1]
171	lastHost.Nodes = append(lastHost.Nodes, &Empty{
172		Comment: comment.val,
173		// account for the "#" as well
174		leadingSpace: comment.Position.Col - 2,
175		position:     comment.Position,
176	})
177	return p.parseStart
178}
179
180func parseSSH(flow chan token, system bool, depth uint8) *Config {
181	// Ensure we consume tokens to completion even if parser exits early
182	defer func() {
183		for range flow {
184		}
185	}()
186
187	result := newConfig()
188	result.position = Position{1, 1}
189	parser := &sshParser{
190		flow:          flow,
191		config:        result,
192		tokensBuffer:  make([]token, 0),
193		currentTable:  make([]string, 0),
194		seenTableKeys: make([]string, 0),
195		system:        system,
196		depth:         depth,
197	}
198	parser.run()
199	return result
200}