feat(sshd_config): Add include support

This commit is contained in:
Myzel394 2024-09-14 18:31:31 +02:00
parent 774ee52a3b
commit 6c9ebc1b16
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
10 changed files with 350 additions and 42 deletions

View File

@ -13,19 +13,54 @@ func Analyze(
d *sshdconfig.SSHDocument,
) []protocol.Diagnostic {
errors := analyzeOptionsAreValid(d)
if len(errors) > 0 {
return errsToDiagnostics(errors)
}
indexes, indexErrors := indexes.CreateIndexes(*d.Config)
_ = indexes
d.Indexes = indexes
errors = append(errors, indexErrors...)
if len(errors) > 0 {
return utils.Map(
errors,
func(err common.LSPError) protocol.Diagnostic {
return err.ToDiagnostic()
},
)
return errsToDiagnostics(errors)
}
includeErrors := analyzeIncludeValues(d)
if len(includeErrors) > 0 {
errors = append(errors, includeErrors...)
} else {
for _, include := range d.Indexes.Includes {
for _, value := range include.Values {
for _, path := range value.Paths {
_, err := parseFile(string(path))
if err != nil {
errors = append(errors, common.LSPError{
Range: value.LocationRange,
Err: err,
})
}
}
}
}
}
if len(errors) > 0 {
return errsToDiagnostics(errors)
}
return nil
}
func errsToDiagnostics(errs []common.LSPError) []protocol.Diagnostic {
return utils.Map(
errs,
func(err common.LSPError) protocol.Diagnostic {
return err.ToDiagnostic()
},
)
}

View File

@ -0,0 +1,95 @@
package analyzer
import (
"config-lsp/common"
sshdconfig "config-lsp/handlers/sshd_config"
"config-lsp/handlers/sshd_config/ast"
"config-lsp/handlers/sshd_config/indexes"
"config-lsp/utils"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
)
var whitespacePattern = regexp.MustCompile(`\S+`)
func analyzeIncludeValues(
d *sshdconfig.SSHDocument,
) []common.LSPError {
errs := make([]common.LSPError, 0)
for _, include := range d.Indexes.Includes {
for _, value := range include.Values {
validPaths, err := createIncludePaths(value.Value)
if err != nil {
errs = append(errs, common.LSPError{
Range: value.LocationRange,
Err: err,
})
} else {
value.Paths = validPaths
}
}
}
return errs
}
func createIncludePaths(
suggestedPath string,
) ([]indexes.ValidPath, error) {
var absolutePath string
if path.IsAbs(suggestedPath) {
absolutePath = suggestedPath
} else {
absolutePath = path.Join("/etc", "ssh", suggestedPath)
}
files, err := filepath.Glob(absolutePath)
if err != nil {
return nil, errors.New(fmt.Sprintf("Could not find file %s (error: %s)", absolutePath, err))
}
if len(files) == 0 {
return nil, errors.New(fmt.Sprintf("Could not find file %s", absolutePath))
}
return utils.Map(
files,
func(file string) indexes.ValidPath {
return indexes.ValidPath(file)
},
), nil
}
func parseFile(
filePath string,
) (*sshdconfig.SSHDocument, error) {
c := ast.NewSSHConfig()
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
c.Parse(string(content))
d := &sshdconfig.SSHDocument{
Config: c,
}
errs := Analyze(d)
if len(errs) > 0 {
return nil, errors.New(fmt.Sprintf("Errors in %s", filePath))
}
return d, nil
}

View File

@ -60,24 +60,27 @@ func checkOption(
return errs
}
if option.OptionValue == nil {
return errs
if option.OptionValue == nil || option.OptionValue.Value == "" {
errs = append(errs, common.LSPError{
Range: option.Key.LocationRange,
Err: errors.New(fmt.Sprintf("Option '%s' requires a value", option.Key.Value)),
})
} else {
invalidValues := docOption.CheckIsValid(option.OptionValue.Value)
errs = append(
errs,
utils.Map(
invalidValues,
func(invalidValue *docvalues.InvalidValue) common.LSPError {
err := docvalues.LSPErrorFromInvalidValue(option.Start.Line, *invalidValue)
err.ShiftCharacter(option.OptionValue.Start.Character)
return err
},
)...,
)
}
invalidValues := docOption.CheckIsValid(option.OptionValue.Value)
errs = append(
errs,
utils.Map(
invalidValues,
func(invalidValue *docvalues.InvalidValue) common.LSPError {
err := docvalues.LSPErrorFromInvalidValue(option.Start.Line, *invalidValue)
err.ShiftCharacter(option.OptionValue.Start.Character)
return err
},
)...,
)
}
return errs

View File

@ -391,9 +391,7 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
Value: docvalues.ArrayValue{
Separator: " ",
DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor,
SubValue: docvalues.PathValue{
RequiredType: docvalues.PathTypeFile,
},
SubValue: docvalues.StringValue{},
},
// TODO: Add extra check
},

View File

@ -0,0 +1,47 @@
package handlers
import (
"config-lsp/handlers/sshd_config/indexes"
"config-lsp/utils"
"fmt"
"slices"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func GetIncludeOptionLocation(
include *indexes.SSHIndexIncludeLine,
cursor uint32,
) []protocol.Location {
index, found := slices.BinarySearchFunc(
include.Values,
cursor,
func(i *indexes.SSHIndexIncludeValue, cursor uint32) int {
if cursor < i.Start.Character {
return -1
}
if cursor > i.End.Character {
return 1
}
return 0
},
)
if !found {
return nil
}
path := include.Values[index]
println("paths", fmt.Sprintf("%v", path.Paths))
return utils.Map(
path.Paths,
func(path indexes.ValidPath) protocol.Location {
return protocol.Location{
URI: path.AsURI(),
}
},
)
}

View File

@ -5,6 +5,7 @@ import (
"config-lsp/handlers/sshd_config/ast"
"errors"
"fmt"
"regexp"
)
var allowedDoubleOptions = map[string]struct{}{
@ -24,7 +25,26 @@ type SSHIndexKey struct {
type SSHIndexAllOption struct {
MatchBlock *ast.SSHMatchBlock
Option *ast.SSHOption
Option *ast.SSHOption
}
type ValidPath string
func (v ValidPath) AsURI() string {
return "file://" + string(v)
}
type SSHIndexIncludeValue struct {
common.LocationRange
Value string
// Actual valid paths, these will be set by the analyzer
Paths []ValidPath
}
type SSHIndexIncludeLine struct {
Values []*SSHIndexIncludeValue
Option *SSHIndexAllOption
}
type SSHIndexes struct {
@ -35,18 +55,22 @@ type SSHIndexes struct {
OptionsPerRelativeKey map[SSHIndexKey][]*ast.SSHOption
// This is a map of `Option name` to a list of options with that name
AllOptionsPerName map[string][]*SSHIndexAllOption
AllOptionsPerName map[string]map[uint32]*SSHIndexAllOption
Includes map[uint32]*SSHIndexIncludeLine
}
var whitespacePattern = regexp.MustCompile(`\S+`)
func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) {
errs := make([]common.LSPError, 0)
indexes := &SSHIndexes{
OptionsPerRelativeKey: make(map[SSHIndexKey][]*ast.SSHOption),
AllOptionsPerName: make(map[string][]*SSHIndexAllOption),
AllOptionsPerName: make(map[string]map[uint32]*SSHIndexAllOption),
Includes: make(map[uint32]*SSHIndexIncludeLine),
}
it := config.Options.Iterator()
for it.Next() {
entry := it.Value().(ast.SSHEntry)
@ -59,7 +83,6 @@ func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) {
matchBlock := entry.(*ast.SSHMatchBlock)
it := matchBlock.Options.Iterator()
for it.Next() {
option := it.Value().(*ast.SSHOption)
@ -68,6 +91,43 @@ func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) {
}
}
// Add Includes
for _, includeOption := range indexes.AllOptionsPerName["Include"] {
rawValue := includeOption.Option.OptionValue.Value
pathIndexes := whitespacePattern.FindAllStringIndex(rawValue, -1)
paths := make([]*SSHIndexIncludeValue, 0)
for _, pathIndex := range pathIndexes {
startIndex := pathIndex[0]
endIndex := pathIndex[1]
rawPath := rawValue[startIndex:endIndex]
offset := includeOption.Option.OptionValue.Start.Character
path := SSHIndexIncludeValue{
LocationRange: common.LocationRange{
Start: common.Location{
Line: includeOption.Option.Start.Line,
Character: uint32(startIndex) + offset,
},
End: common.Location{
Line: includeOption.Option.Start.Line,
Character: uint32(endIndex) + offset - 1,
},
},
Value: rawPath,
Paths: make([]ValidPath, 0),
}
paths = append(paths, &path)
}
indexes.Includes[includeOption.Option.Start.Line] = &SSHIndexIncludeLine{
Values: paths,
Option: includeOption,
}
}
return indexes, errs
}
@ -97,17 +157,16 @@ func addOption(
i.OptionsPerRelativeKey[indexEntry] = []*ast.SSHOption{option}
}
if existingEntry, found := i.AllOptionsPerName[option.Key.Value]; found {
i.AllOptionsPerName[option.Key.Value] = append(existingEntry, &SSHIndexAllOption{
if _, found := i.AllOptionsPerName[option.Key.Value]; found {
i.AllOptionsPerName[option.Key.Value][option.Start.Line] = &SSHIndexAllOption{
MatchBlock: matchBlock,
Option: option,
})
Option: option,
}
} else {
i.AllOptionsPerName[option.Key.Value] = []*SSHIndexAllOption{
{
i.AllOptionsPerName[option.Key.Value] = map[uint32]*SSHIndexAllOption{
option.Start.Line: {
MatchBlock: matchBlock,
Option: option,
Option: option,
},
}
}

View File

@ -116,3 +116,48 @@ Match Address 192.168.0.1/24
t.Errorf("Expected 3 PermitRootLogin options, but got %v", indexes.AllOptionsPerName["PermitRootLogin"])
}
}
func TestIncludeExample(
t *testing.T,
) {
input := utils.Dedent(`
PermitRootLogin yes
Include /etc/ssh/sshd_config.d/*.conf hello_world
`)
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) > 0 {
t.Fatalf("Expected no errors, but got %v", len(errors))
}
if !(len(indexes.Includes) == 1) {
t.Fatalf("Expected 1 include, but got %v", len(indexes.Includes))
}
if !(len(indexes.Includes[1].Values) == 2) {
t.Fatalf("Expected 2 include path, but got %v", len(indexes.Includes[1].Values))
}
if !(indexes.Includes[1].Values[0].Value == "/etc/ssh/sshd_config.d/*.conf" &&
indexes.Includes[1].Values[0].Start.Line == 1 &&
indexes.Includes[1].Values[0].End.Line == 1 &&
indexes.Includes[1].Values[0].Start.Character == 8 &&
indexes.Includes[1].Values[0].End.Character == 36) {
t.Errorf("Expected '/etc/ssh/sshd_config.d/*.conf' on line 1, but got %v on line %v", indexes.Includes[1].Values[0].Value, indexes.Includes[1].Values[0].Start.Line)
}
if !(indexes.Includes[1].Values[1].Value == "hello_world" &&
indexes.Includes[1].Values[1].Start.Line == 1 &&
indexes.Includes[1].Values[1].End.Line == 1 &&
indexes.Includes[1].Values[1].Start.Character == 38 &&
indexes.Includes[1].Values[1].End.Character == 48) {
t.Errorf("Expected 'hello_world' on line 1, but got %v on line %v", indexes.Includes[1].Values[1].Value, indexes.Includes[1].Values[1].Start.Line)
}
}

View File

@ -0,0 +1,23 @@
package lsp
import (
sshdconfig "config-lsp/handlers/sshd_config"
"config-lsp/handlers/sshd_config/handlers"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentDefinition(context *glsp.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
d := sshdconfig.DocumentParserMap[params.TextDocument.URI]
cursor := params.Position.Character
line := params.Position.Line
if include, found := d.Indexes.Includes[line]; found {
relativeCursor := cursor - include.Option.Option.LocationRange.Start.Character
return handlers.GetIncludeOptionLocation(include, relativeCursor), nil
}
return nil, nil
}

View File

@ -2,12 +2,14 @@ package sshdconfig
import (
"config-lsp/handlers/sshd_config/ast"
"config-lsp/handlers/sshd_config/indexes"
protocol "github.com/tliron/glsp/protocol_3_16"
)
type SSHDocument struct {
Config *ast.SSHConfig
Config *ast.SSHConfig
Indexes *indexes.SSHIndexes
}
var DocumentParserMap = map[protocol.DocumentUri]*SSHDocument{}

View File

@ -2,6 +2,7 @@ package roothandler
import (
aliases "config-lsp/handlers/aliases/lsp"
sshdconfig "config-lsp/handlers/sshd_config/lsp"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
@ -24,7 +25,7 @@ func TextDocumentDefinition(context *glsp.Context, params *protocol.DefinitionPa
case LanguageHosts:
return nil, nil
case LanguageSSHDConfig:
return nil, nil
return sshdconfig.TextDocumentDefinition(context, params)
case LanguageFstab:
return nil, nil
case LanguageWireguard: