Skip to content

Commit

Permalink
feat(sshd_config): Add match block analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
Myzel394 committed Sep 15, 2024
1 parent 5d84e27 commit 6326ccb
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 15 deletions.
3 changes: 2 additions & 1 deletion handlers/aliases/lsp/text-document-signature-help.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lsp

import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion handlers/sshd_config/Config.g4
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
grammar Config;

lineStatement
: (entry | (leadingComment) | WHITESPACE?) EOF
: (entry | leadingComment | WHITESPACE?) EOF
;

entry
Expand Down
2 changes: 2 additions & 0 deletions handlers/sshd_config/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func Analyze(
}
}

errors = append(errors, analyzeMatchBlocks(d)...)

if len(errors) > 0 {
return errsToDiagnostics(errors)
}
Expand Down
112 changes: 112 additions & 0 deletions handlers/sshd_config/analyzer/match.go
Original file line number Diff line number Diff line change
@@ -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
}
63 changes: 63 additions & 0 deletions handlers/sshd_config/analyzer/match_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
13 changes: 10 additions & 3 deletions handlers/sshd_config/ast/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
15 changes: 15 additions & 0 deletions handlers/sshd_config/ast/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion handlers/sshd_config/handlers/completions_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
2 changes: 2 additions & 0 deletions handlers/sshd_config/indexes/indexes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 1 addition & 9 deletions root-handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions utils/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package utils

import (
"regexp"
"strings"
)

var trimIndexPattern = regexp.MustCompile(`^\s*(.+?)\s*$`)
Expand Down Expand Up @@ -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
}

0 comments on commit 6326ccb

Please sign in to comment.