Add alpha SSH config parser
The error handling is nonexistent and there's no easy way to get data out. But we can parse a SSH config file into a Go struct, and roundtrip that struct back to a file that looks (roughly) the same.
This commit is contained in:
151
config.go
Normal file
151
config.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package ssh_config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func User(hostname string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigFinder struct {
|
||||||
|
IgnoreSystemConfig bool
|
||||||
|
IgnoreUserConfig bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigFinder) User(hostname string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultFinder = &ConfigFinder{IgnoreSystemConfig: false, IgnoreUserConfig: false}
|
||||||
|
|
||||||
|
func parseFile(filename string) (*Config, error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return LoadReader(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadReader(r io.Reader) (c *Config, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if _, ok := r.(runtime.Error); ok {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
err = errors.New(r.(string))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c = parseSSH(lexSSH(r))
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents an SSH config file.
|
||||||
|
type Config struct {
|
||||||
|
position Position
|
||||||
|
Hosts []*Host
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for i := range c.Hosts {
|
||||||
|
buf.WriteString(c.Hosts[i].String())
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Host struct {
|
||||||
|
// A list of host patterns that should match this host.
|
||||||
|
Patterns []string
|
||||||
|
// A Node is either a key/value pair or a comment line.
|
||||||
|
Nodes []Node
|
||||||
|
// EOLComment is the comment (if any) terminating the Host line.
|
||||||
|
EOLComment string
|
||||||
|
leadingSpace uint16 // TODO: handle spaces vs tabs here.
|
||||||
|
// The file starts with an implicit "Host *" declaration.
|
||||||
|
implicit bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Host) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if h.implicit == false {
|
||||||
|
buf.WriteString(strings.Repeat(" ", int(h.leadingSpace)))
|
||||||
|
buf.WriteString("Host ")
|
||||||
|
buf.WriteString(strings.Join(h.Patterns, " "))
|
||||||
|
if h.EOLComment != "" {
|
||||||
|
buf.WriteString(" #")
|
||||||
|
buf.WriteString(h.EOLComment)
|
||||||
|
}
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
for i := range h.Nodes {
|
||||||
|
//fmt.Printf("%q\n", h.Nodes[i].String())
|
||||||
|
buf.WriteString(h.Nodes[i].String())
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node interface {
|
||||||
|
Pos() Position
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
Comment string
|
||||||
|
leadingSpace uint16 // Space before the key. TODO handle spaces vs tabs.
|
||||||
|
position Position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KV) Pos() Position {
|
||||||
|
return k.position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KV) String() string {
|
||||||
|
if k == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf("%s%s %s", strings.Repeat(" ", int(k.leadingSpace)), k.Key, k.Value)
|
||||||
|
if k.Comment != "" {
|
||||||
|
line += " #" + k.Comment
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
type Empty struct {
|
||||||
|
Comment string
|
||||||
|
leadingSpace uint16 // TODO handle spaces vs tabs.
|
||||||
|
position Position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Empty) Pos() Position {
|
||||||
|
return e.position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Empty) String() string {
|
||||||
|
if e == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if e.Comment == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s#%s", strings.Repeat(" ", int(e.leadingSpace)), e.Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Hosts: []*Host{
|
||||||
|
&Host{implicit: true, Patterns: []string{"*"}, Nodes: make([]Node, 0)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
31
config_test.go
Normal file
31
config_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package ssh_config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadFile(t *testing.T, filename string) []byte {
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = []string{"testdata/config1", "testdata/config2"}
|
||||||
|
|
||||||
|
func TestLoadReader(t *testing.T) {
|
||||||
|
for _, filename := range files {
|
||||||
|
data := loadFile(t, filename)
|
||||||
|
cfg, err := LoadReader(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out := cfg.String()
|
||||||
|
if out != string(data) {
|
||||||
|
t.Errorf("out != data: out: %q\ndata: %q", out, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
lexer.go
Normal file
207
lexer.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package ssh_config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
buffruneio "github.com/pelletier/go-buffruneio"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define state functions
|
||||||
|
type sshLexStateFn func() sshLexStateFn
|
||||||
|
|
||||||
|
type sshLexer struct {
|
||||||
|
input *buffruneio.Reader // Textual source
|
||||||
|
buffer []rune // Runes composing the current token
|
||||||
|
tokens chan token
|
||||||
|
depth int
|
||||||
|
line uint32
|
||||||
|
col uint16
|
||||||
|
endbufferLine uint32
|
||||||
|
endbufferCol uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) lexComment(previousState sshLexStateFn) sshLexStateFn {
|
||||||
|
return func() sshLexStateFn {
|
||||||
|
growingString := ""
|
||||||
|
for next := s.peek(); next != '\n' && next != eof; next = s.peek() {
|
||||||
|
if next == '\r' && s.follow("\r\n") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
growingString += string(next)
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
s.emitWithValue(tokenComment, growingString)
|
||||||
|
s.skip()
|
||||||
|
return previousState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) lexKey() sshLexStateFn {
|
||||||
|
growingString := ""
|
||||||
|
|
||||||
|
for r := s.peek(); isKeyChar(r); r = s.peek() {
|
||||||
|
// simplified a lot here
|
||||||
|
if isSpace(r) {
|
||||||
|
s.emitWithValue(tokenKey, growingString)
|
||||||
|
s.skip()
|
||||||
|
return s.lexRvalue
|
||||||
|
}
|
||||||
|
growingString += string(r)
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
s.emitWithValue(tokenKey, growingString)
|
||||||
|
return s.lexVoid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) lexRvalue() sshLexStateFn {
|
||||||
|
growingString := ""
|
||||||
|
for {
|
||||||
|
next := s.peek()
|
||||||
|
switch next {
|
||||||
|
case '\n':
|
||||||
|
s.emitWithValue(tokenString, growingString)
|
||||||
|
s.skip()
|
||||||
|
return s.lexVoid
|
||||||
|
case '#':
|
||||||
|
s.emitWithValue(tokenString, growingString)
|
||||||
|
s.skip()
|
||||||
|
return s.lexComment(s.lexVoid)
|
||||||
|
case eof:
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
if next == eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
growingString += string(next)
|
||||||
|
s.next()
|
||||||
|
}
|
||||||
|
s.emit(tokenEOF)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) read() rune {
|
||||||
|
r, _, err := s.input.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if r == '\n' {
|
||||||
|
s.endbufferLine++
|
||||||
|
s.endbufferCol = 1
|
||||||
|
} else {
|
||||||
|
s.endbufferCol++
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) next() rune {
|
||||||
|
r := s.read()
|
||||||
|
|
||||||
|
if r != eof {
|
||||||
|
s.buffer = append(s.buffer, r)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) lexVoid() sshLexStateFn {
|
||||||
|
for {
|
||||||
|
next := s.peek()
|
||||||
|
switch next {
|
||||||
|
case '#':
|
||||||
|
s.skip()
|
||||||
|
return s.lexComment(s.lexVoid)
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
|
case '\n':
|
||||||
|
s.emit(tokenEmptyLine)
|
||||||
|
s.skip()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSpace(next) {
|
||||||
|
s.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
if isKeyStartChar(next) {
|
||||||
|
return s.lexKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// removed IsKeyStartChar and lexKey. probably will need to readd
|
||||||
|
|
||||||
|
if next == eof {
|
||||||
|
s.next()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.emit(tokenEOF)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) ignore() {
|
||||||
|
s.buffer = make([]rune, 0)
|
||||||
|
s.line = s.endbufferLine
|
||||||
|
s.col = s.endbufferCol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) skip() {
|
||||||
|
s.next()
|
||||||
|
s.ignore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) emit(t tokenType) {
|
||||||
|
s.emitWithValue(t, string(s.buffer))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) emitWithValue(t tokenType, value string) {
|
||||||
|
tok := token{
|
||||||
|
Position: Position{s.line, s.col},
|
||||||
|
typ: t,
|
||||||
|
val: value,
|
||||||
|
}
|
||||||
|
s.tokens <- tok
|
||||||
|
s.ignore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) peek() rune {
|
||||||
|
r, _, err := s.input.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
s.input.UnreadRune()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) follow(next string) bool {
|
||||||
|
for _, expectedRune := range next {
|
||||||
|
r, _, err := s.input.ReadRune()
|
||||||
|
defer s.input.UnreadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if expectedRune != r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sshLexer) run() {
|
||||||
|
for state := s.lexVoid; state != nil; {
|
||||||
|
state = state()
|
||||||
|
}
|
||||||
|
close(s.tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexSSH(input io.Reader) chan token {
|
||||||
|
bufferedInput := buffruneio.NewReader(input)
|
||||||
|
l := &sshLexer{
|
||||||
|
input: bufferedInput,
|
||||||
|
tokens: make(chan token),
|
||||||
|
line: 1,
|
||||||
|
col: 1,
|
||||||
|
endbufferLine: 1,
|
||||||
|
endbufferCol: 1,
|
||||||
|
}
|
||||||
|
go l.run()
|
||||||
|
return l.tokens
|
||||||
|
}
|
||||||
147
parser.go
Normal file
147
parser.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package ssh_config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sshParser struct {
|
||||||
|
flow chan token
|
||||||
|
config *Config
|
||||||
|
tokensBuffer []token
|
||||||
|
currentTable []string
|
||||||
|
seenTableKeys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type sshParserStateFn func() sshParserStateFn
|
||||||
|
|
||||||
|
// Formats and panics an error message based on a token
|
||||||
|
func (p *sshParser) raiseError(tok *token, msg string, args ...interface{}) {
|
||||||
|
panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
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.raiseError(tok, fmt.Sprintf("unexpected token %q\n", tok))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshParser) parseKV() sshParserStateFn {
|
||||||
|
key := p.getToken()
|
||||||
|
p.assume(tokenString)
|
||||||
|
val := p.getToken()
|
||||||
|
comment := ""
|
||||||
|
tok := p.peek()
|
||||||
|
if tok.typ == tokenComment && tok.Position.Line == val.Position.Line {
|
||||||
|
tok = p.getToken()
|
||||||
|
comment = tok.val
|
||||||
|
}
|
||||||
|
if key.val == "Host" {
|
||||||
|
patterns := strings.Split(val.val, " ")
|
||||||
|
for i := range patterns {
|
||||||
|
if patterns[i] == "" {
|
||||||
|
patterns = append(patterns[:i], patterns[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.config.Hosts = append(p.config.Hosts, &Host{
|
||||||
|
Patterns: patterns,
|
||||||
|
Nodes: make([]Node, 0),
|
||||||
|
EOLComment: comment,
|
||||||
|
})
|
||||||
|
return p.parseStart
|
||||||
|
}
|
||||||
|
lastHost := p.config.Hosts[len(p.config.Hosts)-1]
|
||||||
|
kv := &KV{
|
||||||
|
Key: key.val,
|
||||||
|
Value: val.val,
|
||||||
|
Comment: comment,
|
||||||
|
leadingSpace: uint16(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// assume peeks at the next token and ensures it's the right type
|
||||||
|
func (p *sshParser) assume(typ tokenType) {
|
||||||
|
tok := p.peek()
|
||||||
|
if tok == nil {
|
||||||
|
p.raiseError(tok, "was expecting token %s, but token stream is empty", tok)
|
||||||
|
}
|
||||||
|
if tok.typ != typ {
|
||||||
|
p.raiseError(tok, "was expecting token %s, but got %s instead", typ, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSSH(flow chan token) *Config {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
parser.run()
|
||||||
|
return result
|
||||||
|
}
|
||||||
25
position.go
Normal file
25
position.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package ssh_config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Position of a document element within a SSH document.
|
||||||
|
//
|
||||||
|
// Line and Col are both 1-indexed positions for the element's line number and
|
||||||
|
// column number, respectively. Values of zero or less will cause Invalid(),
|
||||||
|
// to return true.
|
||||||
|
type Position struct {
|
||||||
|
Line uint32 // line within the document
|
||||||
|
Col uint16 // column within the line
|
||||||
|
}
|
||||||
|
|
||||||
|
// String representation of the position.
|
||||||
|
// Displays 1-indexed line and column numbers.
|
||||||
|
func (p Position) String() string {
|
||||||
|
return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid returns whether or not the position is valid (i.e. with negative or
|
||||||
|
// null values)
|
||||||
|
func (p Position) Invalid() bool {
|
||||||
|
return p.Line <= 0 || p.Col <= 0
|
||||||
|
}
|
||||||
39
testdata/config1
vendored
Normal file
39
testdata/config1
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
Host localhost 127.0.0.1 # A comment at the end of a host line.
|
||||||
|
NoHostAuthenticationForLocalhost yes
|
||||||
|
|
||||||
|
# A comment
|
||||||
|
# A comment with leading spaces.
|
||||||
|
|
||||||
|
Host wap
|
||||||
|
User root
|
||||||
|
KexAlgorithms diffie-hellman-group1-sha1
|
||||||
|
|
||||||
|
Host [some stuff behind a NAT]
|
||||||
|
Compression yes
|
||||||
|
ProxyCommand ssh -qW %h:%p [NATrouter]
|
||||||
|
|
||||||
|
Host wopr # there are 2 proxies available for this one...
|
||||||
|
User root
|
||||||
|
ProxyCommand sh -c "ssh proxy1 -qW %h:22 || ssh proxy2 -qW %h:22"
|
||||||
|
|
||||||
|
Host dhcp-??
|
||||||
|
UserKnownHostsFile /dev/null
|
||||||
|
StrictHostKeyChecking no
|
||||||
|
User root
|
||||||
|
|
||||||
|
Host [my boxes] [*.mydomain]
|
||||||
|
ForwardAgent yes
|
||||||
|
ForwardX11 yes
|
||||||
|
ForwardX11Trusted yes
|
||||||
|
|
||||||
|
Host *
|
||||||
|
#ControlMaster auto
|
||||||
|
#ControlPath /tmp/ssh-master-%C
|
||||||
|
#ControlPath /tmp/ssh-%u-%r@%h:%p
|
||||||
|
#ControlPersist yes
|
||||||
|
ForwardX11Timeout 52w
|
||||||
|
XAuthLocation /usr/bin/xauth
|
||||||
|
SendEnv LANG LC_*
|
||||||
|
HostKeyAlgorithms ssh-ed25519,ssh-rsa
|
||||||
|
AddressFamily inet
|
||||||
|
#UpdateHostKeys ask
|
||||||
50
testdata/config2
vendored
Normal file
50
testdata/config2
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# $OpenBSD: ssh_config,v 1.30 2016/02/20 23:06:23 sobrado Exp $
|
||||||
|
|
||||||
|
# This is the ssh client system-wide configuration file. See
|
||||||
|
# ssh_config(5) for more information. This file provides defaults for
|
||||||
|
# users, and the values can be changed in per-user configuration files
|
||||||
|
# or on the command line.
|
||||||
|
|
||||||
|
# Configuration data is parsed as follows:
|
||||||
|
# 1. command line options
|
||||||
|
# 2. user-specific file
|
||||||
|
# 3. system-wide file
|
||||||
|
# Any configuration value is only changed the first time it is set.
|
||||||
|
# Thus, host-specific definitions should be at the beginning of the
|
||||||
|
# configuration file, and defaults at the end.
|
||||||
|
|
||||||
|
# Site-wide defaults for some commonly used options. For a comprehensive
|
||||||
|
# list of available options, their meanings and defaults, please see the
|
||||||
|
# ssh_config(5) man page.
|
||||||
|
|
||||||
|
# Host *
|
||||||
|
# ForwardAgent no
|
||||||
|
# ForwardX11 no
|
||||||
|
# RhostsRSAAuthentication no
|
||||||
|
# RSAAuthentication yes
|
||||||
|
# PasswordAuthentication yes
|
||||||
|
# HostbasedAuthentication no
|
||||||
|
# GSSAPIAuthentication no
|
||||||
|
# GSSAPIDelegateCredentials no
|
||||||
|
# BatchMode no
|
||||||
|
# CheckHostIP yes
|
||||||
|
# AddressFamily any
|
||||||
|
# ConnectTimeout 0
|
||||||
|
# StrictHostKeyChecking ask
|
||||||
|
# IdentityFile ~/.ssh/identity
|
||||||
|
# IdentityFile ~/.ssh/id_rsa
|
||||||
|
# IdentityFile ~/.ssh/id_dsa
|
||||||
|
# IdentityFile ~/.ssh/id_ecdsa
|
||||||
|
# IdentityFile ~/.ssh/id_ed25519
|
||||||
|
# Port 22
|
||||||
|
# Protocol 2
|
||||||
|
# Cipher 3des
|
||||||
|
# Ciphers aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc
|
||||||
|
# MACs hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160
|
||||||
|
# EscapeChar ~
|
||||||
|
# Tunnel no
|
||||||
|
# TunnelDevice any:any
|
||||||
|
# PermitLocalCommand no
|
||||||
|
# VisualHostKey no
|
||||||
|
# ProxyCommand ssh -q -W %h:%p gateway.example.com
|
||||||
|
# RekeyLimit 1G 1h
|
||||||
48
token.go
Normal file
48
token.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package ssh_config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
Position
|
||||||
|
typ tokenType
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t token) String() string {
|
||||||
|
switch t.typ {
|
||||||
|
case tokenEOF:
|
||||||
|
return "EOF"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%q", t.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
eof = -(iota + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenError tokenType = iota
|
||||||
|
tokenEOF
|
||||||
|
tokenEmptyLine
|
||||||
|
tokenComment
|
||||||
|
tokenKey
|
||||||
|
tokenString
|
||||||
|
)
|
||||||
|
|
||||||
|
func isSpace(r rune) bool {
|
||||||
|
return r == ' ' || r == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKeyStartChar(r rune) bool {
|
||||||
|
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I'm not sure that this is correct
|
||||||
|
func isKeyChar(r rune) bool {
|
||||||
|
// Keys start with the first character that isn't whitespace or [ and end
|
||||||
|
// with the last non-whitespace character before the equals sign. Keys
|
||||||
|
// cannot contain a # character."
|
||||||
|
return !(r == '\r' || r == '\n' || r == eof || r == '=')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user