diff --git a/handlers/sshd_config/analyzer/analyzer.go b/handlers/sshd_config/analyzer/analyzer.go index 77cf4c2..7f93902 100644 --- a/handlers/sshd_config/analyzer/analyzer.go +++ b/handlers/sshd_config/analyzer/analyzer.go @@ -3,6 +3,7 @@ package analyzer import ( "config-lsp/common" "config-lsp/handlers/sshd_config" + "config-lsp/handlers/sshd_config/indexes" "config-lsp/utils" protocol "github.com/tliron/glsp/protocol_3_16" @@ -22,5 +23,19 @@ func Analyze( ) } + indexes, indexErrors := indexes.CreateIndexes(*d.Config) + _ = indexes + + errors = append(errors, indexErrors...) + + if len(errors) > 0 { + return utils.Map( + errors, + func(err common.LSPError) protocol.Diagnostic { + return err.ToDiagnostic() + }, + ) + } + return nil } diff --git a/handlers/sshd_config/ast/parser_test.go b/handlers/sshd_config/ast/parser_test.go index 9ea5e09..c7c4876 100644 --- a/handlers/sshd_config/ast/parser_test.go +++ b/handlers/sshd_config/ast/parser_test.go @@ -158,7 +158,7 @@ Match 192.168.0.2 t.Errorf("Expected sixth entry to be 'MaxAuthTries 3', but got: %v", sixthEntry.Value) } - firstOption, firstMatchBlock := p.FindOption(uint32(4)) + firstOption, firstMatchBlock := p.FindOption(uint32(3)) if !(firstOption.Key.Value == "PasswordAuthentication" && firstOption.OptionValue.Value == "yes" && firstMatchBlock.MatchEntry.Value == "Match 192.168.0.1") { t.Errorf("Expected first option to be 'PasswordAuthentication yes' and first match block to be 'Match 192.168.0.1', but got: %v, %v", firstOption, firstMatchBlock) diff --git a/handlers/sshd_config/ast/sshd_config.go b/handlers/sshd_config/ast/sshd_config.go index 2f24577..5b30c37 100644 --- a/handlers/sshd_config/ast/sshd_config.go +++ b/handlers/sshd_config/ast/sshd_config.go @@ -103,7 +103,12 @@ func (c SSHConfig) FindOption(line uint32) (*SSHOption, *SSHMatchBlock) { rawEntry, found := c.Options.Get(line) if found { - return rawEntry.(*SSHOption), nil + switch rawEntry.(type) { + case *SSHMatchBlock: + return rawEntry.(*SSHMatchBlock).MatchEntry, rawEntry.(*SSHMatchBlock) + case *SSHOption: + return rawEntry.(*SSHOption), nil + } } return nil, nil diff --git a/handlers/sshd_config/indexes/indexes.go b/handlers/sshd_config/indexes/indexes.go new file mode 100644 index 0000000..5f46a41 --- /dev/null +++ b/handlers/sshd_config/indexes/indexes.go @@ -0,0 +1,88 @@ +package indexes + +import ( + "config-lsp/common" + "config-lsp/handlers/sshd_config/ast" + "errors" + "fmt" +) + +var allowedDoubleOptions = map[string]struct{}{ + "AllowGroups": {}, + "AllowUsers": {}, + "DenyGroups": {}, + "DenyUsers": {}, + "ListenAddress": {}, + "Match": {}, + "Port": {}, +} + +type SSHIndexEntry struct { + Option string + MatchBlock *ast.SSHMatchBlock +} + +type SSHIndexes struct { + EntriesPerKey map[SSHIndexEntry][]*ast.SSHOption +} + +func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) { + errs := make([]common.LSPError, 0) + indexes := &SSHIndexes{ + EntriesPerKey: make(map[SSHIndexEntry][]*ast.SSHOption), + } + + it := config.Options.Iterator() + + for it.Next() { + entry := it.Value().(ast.SSHEntry) + + switch entry.(type) { + case *ast.SSHOption: + option := entry.(*ast.SSHOption) + + errs = append(errs, addOption(indexes, option, nil)...) + case *ast.SSHMatchBlock: + matchBlock := entry.(*ast.SSHMatchBlock) + + it := matchBlock.Options.Iterator() + + for it.Next() { + option := it.Value().(*ast.SSHOption) + + errs = append(errs, addOption(indexes, option, matchBlock)...) + } + } + } + + return indexes, errs +} + +func addOption( + i *SSHIndexes, + option *ast.SSHOption, + matchBlock *ast.SSHMatchBlock, +) []common.LSPError { + var errs []common.LSPError + + indexEntry := SSHIndexEntry{ + Option: option.Key.Value, + MatchBlock: matchBlock, + } + + if existingEntry, found := i.EntriesPerKey[indexEntry]; found { + if _, found := allowedDoubleOptions[option.Key.Value]; found { + // Double value, but doubles are allowed + i.EntriesPerKey[indexEntry] = append(existingEntry, option) + } else { + errs = append(errs, common.LSPError{ + Range: option.Key.LocationRange, + Err: errors.New(fmt.Sprintf("Option %s is already defined on line %d", option.Key.Value, existingEntry[0].Start.Line+1)), + }) + } + } else { + i.EntriesPerKey[indexEntry] = []*ast.SSHOption{option} + } + + return errs +} diff --git a/handlers/sshd_config/indexes/indexes_test.go b/handlers/sshd_config/indexes/indexes_test.go new file mode 100644 index 0000000..e9dec93 --- /dev/null +++ b/handlers/sshd_config/indexes/indexes_test.go @@ -0,0 +1,105 @@ +package indexes + +import ( + "config-lsp/handlers/sshd_config/ast" + "config-lsp/utils" + "testing" +) + +func TestSimpleExample( + t *testing.T, +) { + input := utils.Dedent(` +PermitRootLogin yes +RootLogin yes +PermitRootLogin no +`) + config := ast.NewSSHConfig() + errors := config.Parse(input) + + if len(errors) > 0 { + t.Fatalf("Unexpected errors: %v", errors) + } + + indexes, errors := CreateIndexes(*config) + + if !(len(errors) == 1) { + t.Fatalf("Expected 1 error, but got %v", len(errors)) + } + + indexEntry := SSHIndexEntry{ + Option: "PermitRootLogin", + MatchBlock: nil, + } + if !(indexes.EntriesPerKey[indexEntry][0].Value == "PermitRootLogin yes" && indexes.EntriesPerKey[indexEntry][0].Start.Line == 0) { + t.Errorf("Expected 'PermitRootLogin yes', but got %v", indexes.EntriesPerKey[indexEntry][0].Value) + } + + indexEntry = SSHIndexEntry{ + Option: "RootLogin", + MatchBlock: nil, + } + if !(indexes.EntriesPerKey[indexEntry][0].Value == "RootLogin yes" && indexes.EntriesPerKey[indexEntry][0].Start.Line == 1) { + t.Errorf("Expected 'RootLogin yes', but got %v", indexes.EntriesPerKey[indexEntry][0].Value) + } +} + +func TestComplexExample( + t *testing.T, +) { + input := utils.Dedent(` +PermitRootLogin yes +Port 22 +Port 2022 +Port 2024 + +Match Address 192.168.0.1/24 + PermitRootLogin no + RoomLogin yes + PermitRootLogin yes +`) + config := ast.NewSSHConfig() + errors := config.Parse(input) + + if len(errors) > 0 { + t.Fatalf("Expected no errors, but got %v", len(errors)) + } + + indexes, errors := CreateIndexes(*config) + + if !(len(errors) == 1) { + t.Fatalf("Expected no errors, but got %v", len(errors)) + } + + indexEntry := SSHIndexEntry{ + Option: "PermitRootLogin", + MatchBlock: nil, + } + if !(indexes.EntriesPerKey[indexEntry][0].Value == "PermitRootLogin yes" && indexes.EntriesPerKey[indexEntry][0].Start.Line == 0) { + t.Errorf("Expected 'PermitRootLogin yes' on line 0, but got %v on line %v", indexes.EntriesPerKey[indexEntry][0].Value, indexes.EntriesPerKey[indexEntry][0].Start.Line) + } + + firstMatchBlock := config.FindMatchBlock(uint32(6)) + indexEntry = SSHIndexEntry{ + Option: "PermitRootLogin", + MatchBlock: firstMatchBlock, + } + if !(indexes.EntriesPerKey[indexEntry][0].Value == "\tPermitRootLogin no" && indexes.EntriesPerKey[indexEntry][0].Start.Line == 6) { + t.Errorf("Expected 'PermitRootLogin no' on line 6, but got %v on line %v", indexes.EntriesPerKey[indexEntry][0].Value, indexes.EntriesPerKey[indexEntry][0].Start.Line) + } + + // Double check + indexEntry = SSHIndexEntry{ + Option: "Port", + MatchBlock: nil, + } + if !(indexes.EntriesPerKey[indexEntry][0].Value == "Port 22" && + indexes.EntriesPerKey[indexEntry][0].Start.Line == 1 && + len(indexes.EntriesPerKey[indexEntry]) == 3 && + indexes.EntriesPerKey[indexEntry][1].Value == "Port 2022" && + indexes.EntriesPerKey[indexEntry][1].Start.Line == 2 && + indexes.EntriesPerKey[indexEntry][2].Value == "Port 2024" && + indexes.EntriesPerKey[indexEntry][2].Start.Line == 3) { + t.Errorf("Expected 'Port 22' on line 1, but got %v on line %v", indexes.EntriesPerKey[indexEntry][0].Value, indexes.EntriesPerKey[indexEntry][0].Start.Line) + } +}