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