feat(sshd_config): Add match block analyzer

This commit is contained in:
Myzel394 2024-09-15 22:51:45 +02:00
parent 5d84e27a1f
commit 6326ccb609
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
11 changed files with 228 additions and 15 deletions

View File

@ -1,6 +1,7 @@
package lsp
import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
@ -13,7 +14,7 @@ func TextDocumentSignatureHelp(context *glsp.Context, params *protocol.Signature
document := aliases.DocumentParserMap[params.TextDocument.URI]
line := params.Position.Line
character := params.Position.Character
character := common.CursorToCharacterIndex(params.Position.Character)
if _, found := document.Parser.CommentLines[line]; found {
// Comment

View File

@ -1,7 +1,7 @@
grammar Config;
lineStatement
: (entry | (leadingComment) | WHITESPACE?) EOF
: (entry | leadingComment | WHITESPACE?) EOF
;
entry

View File

@ -49,6 +49,8 @@ func Analyze(
}
}
errors = append(errors, analyzeMatchBlocks(d)...)
if len(errors) > 0 {
return errsToDiagnostics(errors)
}

View File

@ -0,0 +1,112 @@
package analyzer
import (
"config-lsp/common"
sshdconfig "config-lsp/handlers/sshd_config"
match_parser "config-lsp/handlers/sshd_config/fields/match-parser"
"config-lsp/utils"
"errors"
"fmt"
"strings"
)
func analyzeMatchBlocks(
d *sshdconfig.SSHDocument,
) []common.LSPError {
errs := make([]common.LSPError, 0)
for _, indexOption := range d.Indexes.AllOptionsPerName["Match"] {
matchBlock := indexOption.MatchBlock.MatchValue
// Check if the match block has filled out all fields
if matchBlock == nil || len(matchBlock.Entries) == 0 {
errs = append(errs, common.LSPError{
Range: indexOption.Option.LocationRange,
Err: errors.New("A match expression is required"),
})
continue
}
for _, entry := range matchBlock.Entries {
if entry.Values == nil {
errs = append(errs, common.LSPError{
Range: entry.LocationRange,
Err: errors.New(fmt.Sprintf("A value for %s is required", entry.Criteria.Type)),
})
} else {
errs = append(errs, analyzeMatchValuesContainsPositiveValue(entry.Values)...)
for _, value := range entry.Values.Values {
errs = append(errs, analyzeMatchValueNegation(value)...)
}
}
}
// Check if match blocks are not empty
if indexOption.MatchBlock.Options.Size() == 0 {
errs = append(errs, common.LSPError{
Range: indexOption.Option.LocationRange,
Err: errors.New("This match block is empty"),
})
}
}
return errs
}
func analyzeMatchValueNegation(
value *match_parser.MatchValue,
) []common.LSPError {
errs := make([]common.LSPError, 0)
positionsAsList := utils.AllIndexes(value.Value, "!")
positions := utils.SliceToMap(positionsAsList, struct{}{})
delete(positions, 0)
for position := range positions {
errs = append(errs, common.LSPError{
Range: common.LocationRange{
Start: common.Location{
Line: value.Start.Line,
Character: uint32(position) + value.Start.Character,
},
End: common.Location{
Line: value.End.Line,
Character: uint32(position) + value.End.Character,
},
},
Err: errors.New("The negation operator (!) may only occur at the beginning of a value"),
})
}
return errs
}
func analyzeMatchValuesContainsPositiveValue(
values *match_parser.MatchValues,
) []common.LSPError {
if len(values.Values) == 0 {
return nil
}
containsPositive := false
for _, value := range values.Values {
if !strings.HasPrefix(value.Value, "!") {
containsPositive = true
break
}
}
if !containsPositive {
return []common.LSPError{
{
Range: values.LocationRange,
Err: errors.New("At least one positive value is required. A negated match will never produce a positive result by itself"),
},
}
}
return nil
}

View File

@ -0,0 +1,63 @@
package analyzer
import (
sshdconfig "config-lsp/handlers/sshd_config"
"config-lsp/handlers/sshd_config/ast"
"config-lsp/handlers/sshd_config/indexes"
"config-lsp/utils"
"testing"
)
func TestEmptyMatchBlocksMakesErrors(
t *testing.T,
) {
input := utils.Dedent(`
PermitRootLogin yes
Match User root
`)
c := ast.NewSSHConfig()
errors := c.Parse(input)
if len(errors) > 0 {
t.Fatalf("Parse error: %v", errors)
}
indexes, errors := indexes.CreateIndexes(*c)
if len(errors) > 0 {
t.Fatalf("Index error: %v", errors)
}
d := &sshdconfig.SSHDocument{
Config: c,
Indexes: indexes,
}
errors = analyzeMatchBlocks(d)
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
}
}
func TestContainsOnlyNegativeValues(
t *testing.T,
) {
input := utils.Dedent(`
PermitRootLogin yes
Match User !root,!admin
`)
c := ast.NewSSHConfig()
errors := c.Parse(input)
if len(errors) > 0 {
t.Fatalf("Parse error: %v", errors)
}
_, matchBlock := c.FindOption(uint32(1))
errors = analyzeMatchValuesContainsPositiveValue(matchBlock.MatchValue.Entries[0].Values)
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
}
}

View File

@ -93,6 +93,10 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.sshContext.line)
defer (func() {
s.sshContext.currentOption = nil
})()
if s.sshContext.isKeyAMatchBlock {
// Add new match block
var match *match_parser.Match
@ -131,17 +135,20 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) {
s.sshContext.currentMatchBlock = matchBlock
s.sshContext.isKeyAMatchBlock = false
} else if s.sshContext.currentMatchBlock != nil {
return
}
if s.sshContext.currentMatchBlock != nil {
s.sshContext.currentMatchBlock.Options.Put(
location.Start.Line,
s.sshContext.currentOption,
)
s.sshContext.currentMatchBlock.End = s.sshContext.currentOption.End
} else {
s.Config.Options.Put(
location.Start.Line,
s.sshContext.currentOption,
)
}
s.sshContext.currentOption = nil
}

View File

@ -92,6 +92,10 @@ Match Address 192.168.0.1
t.Errorf("Expected second entry to be 'Match Address 192.168.0.1', but got: %v", secondEntry.MatchEntry.Value)
}
if !(secondEntry.Start.Line == 2 && secondEntry.Start.Character == 0 && secondEntry.End.Line == 3 && secondEntry.End.Character == 26) {
t.Errorf("Expected second entry's location to be 2:0-3:25, but got: %v", secondEntry.LocationRange)
}
if !(secondEntry.MatchValue.Entries[0].Criteria.Type == "Address" && secondEntry.MatchValue.Entries[0].Values.Values[0].Value == "192.168.0.1" && secondEntry.MatchEntry.OptionValue.Start.Character == 6) {
t.Errorf("Expected second entry to be 'Match Address 192.168.0.1', but got: %v", secondEntry.MatchValue)
}
@ -235,6 +239,17 @@ Match Address 192.168.0.2
if !(matchOption.Value == "Match User lena" && matchBlock.MatchEntry.Value == "Match User lena" && matchBlock.MatchValue.Entries[0].Values.Values[0].Value == "lena" && matchBlock.MatchEntry.OptionValue.Start.Character == 6) {
t.Errorf("Expected match option to be 'Match User lena', but got: %v, %v", matchOption, matchBlock)
}
if !(matchOption.Start.Line == 2 && matchOption.End.Line == 2 && matchOption.Start.Character == 0 && matchOption.End.Character == 14) {
t.Errorf("Expected match option to be at 2:0-14, but got: %v", matchOption.LocationRange)
}
if !(matchBlock.Start.Line == 2 &&
matchBlock.Start.Character == 0 &&
matchBlock.End.Line == 4 &&
matchBlock.End.Character == 20) {
t.Errorf("Expected match block to be at 2:0-4:20, but got: %v", matchBlock.LocationRange)
}
}
func TestSimpleExampleWithComments(

View File

@ -13,7 +13,7 @@ func getMatchCompletions(
match *match_parser.Match,
cursor uint32,
) ([]protocol.CompletionItem, error) {
if len(match.Entries) == 0 {
if match == nil || len(match.Entries) == 0 {
completions := getMatchCriteriaCompletions()
completions = append(completions, getMatchAllKeywordCompletion())

View File

@ -82,6 +82,8 @@ func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) {
case *ast.SSHMatchBlock:
matchBlock := entry.(*ast.SSHMatchBlock)
errs = append(errs, addOption(indexes, matchBlock.MatchEntry, matchBlock)...)
it := matchBlock.Options.Iterator()
for it.Next() {
option := it.Value().(*ast.SSHOption)

View File

@ -42,15 +42,7 @@ func SetUpRootHandler() {
func initialize(context *glsp.Context, params *protocol.InitializeParams) (any, error) {
capabilities := lspHandler.CreateServerCapabilities()
capabilities.TextDocumentSync = protocol.TextDocumentSyncKindFull
capabilities.SignatureHelpProvider = &protocol.SignatureHelpOptions{
TriggerCharacters: []string{
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"_", "-", ".", "/", ":", "@", "#", "!", "$", "%", "^", "&", "*", "(", ")", "+", "=", "[", "]", "{", "}", "<", ">", "?", ";", ",", "|",
" ",
},
}
capabilities.SignatureHelpProvider = &protocol.SignatureHelpOptions{}
if (*params.Capabilities.TextDocument.Rename.PrepareSupport) == true {
// Client supports rename preparation

View File

@ -2,6 +2,7 @@ package utils
import (
"regexp"
"strings"
)
var trimIndexPattern = regexp.MustCompile(`^\s*(.+?)\s*$`)
@ -57,3 +58,21 @@ var emptyRegex = regexp.MustCompile(`^\s*$`)
func IsEmpty(s string) bool {
return emptyRegex.MatchString(s)
}
func AllIndexes(s string, sub string) []int {
indexes := make([]int, 0)
current := s
for {
index := strings.Index(current, sub)
if index == -1 {
break
}
indexes = append(indexes, index)
current = current[index+1:]
}
return indexes
}