| package ssh_config |
| |
| import ( |
| "fmt" |
| "strings" |
| "unicode" |
| ) |
| |
| 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) |
| } |
| // val.val at this point could be e.g. "example.com " |
| hostval := strings.TrimRightFunc(val.val, unicode.IsSpace) |
| spaceBeforeComment := val.val[len(hostval):] |
| val.val = hostval |
| p.config.Hosts = append(p.config.Hosts, &Host{ |
| Patterns: patterns, |
| Nodes: make([]Node, 0), |
| EOLComment: comment, |
| spaceBeforeComment: spaceBeforeComment, |
| 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 |
| } |
| shortval := strings.TrimRightFunc(val.val, unicode.IsSpace) |
| spaceAfterValue := val.val[len(shortval):] |
| kv := &KV{ |
| Key: key.val, |
| Value: shortval, |
| spaceAfterValue: spaceAfterValue, |
| 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 |
| } |