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