From d5cb9c53e9f819b70839e1fc96df9f53d6c021ca Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:13:14 +0200 Subject: [PATCH] feat: Add KeyValueAssignmentValue --- TODO.md | 3 ++ common/documentation.go | 36 +++++++++++++ common/errors.go | 6 +++ common/utils.go | 18 +++++++ handlers/openssh/documentation.go | 50 +++++++++++------ handlers/openssh/text-document-completion.go | 56 ++++++++++++++------ 6 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..26b53af --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +* SSH ~/.ssh/config config file +* FTP config file + diff --git a/common/documentation.go b/common/documentation.go index 1e98e62..7f5892e 100644 --- a/common/documentation.go +++ b/common/documentation.go @@ -266,6 +266,42 @@ func (v PathValue) CheckIsValid(value string) error { return PathInvalidError{} } +type KeyValueAssignmentValue struct { + Key Value + Value Value + Separator string +} + +func (v KeyValueAssignmentValue) GetTypeDescription() []string { + return []string{ + fmt.Sprintf("Key-Value pair in form of 'key%svalue'", v.Separator), + fmt.Sprintf("#### Key\n%s", strings.Join(v.Key.GetTypeDescription(), "\n")), + fmt.Sprintf("#### Value:\n%s", strings.Join(v.Value.GetTypeDescription(), "\n")), + } +} +func (v KeyValueAssignmentValue) CheckIsValid(value string) error { + parts := strings.Split(value, v.Separator) + + if len(parts) != 2 { + return KeyValueAssignmentError{} + } + + err := v.Key.CheckIsValid(parts[0]) + + if err != nil { + return err + } + + err = v.Value.CheckIsValid(parts[1]) + + if err != nil { + return err + } + + return nil +} + + type Option struct { Documentation string Value Value diff --git a/common/errors.go b/common/errors.go index 1ec1650..fe4b83e 100644 --- a/common/errors.go +++ b/common/errors.go @@ -136,6 +136,12 @@ func (e PathDoesNotExistError) Error() string { return "This path does not exist" } +type KeyValueAssignmentError struct{} + +func (e KeyValueAssignmentError) Error() string { + return "This is not valid key-value assignment" +} + type PathInvalidError struct{} func (e PathInvalidError) Error() string { diff --git a/common/utils.go b/common/utils.go index 9362216..640bdc1 100644 --- a/common/utils.go +++ b/common/utils.go @@ -100,3 +100,21 @@ func IsPathFile(path string) bool { return err == nil && !info.IsDir() } + +func OffsetLineAtLeft(offset uint32, line string, cursor uint32) (string, uint32) { + if offset >= uint32(len(line)) { + return line, cursor + } + + return line[offset:], cursor - offset +} + +func FindPreviousCharacter(line string, character string) (uint32, bool) { + for index := len(line) - 1; index >= 0; index-- { + if string(line[index]) == character { + return uint32(index), true + } + } + + return 0, false +} diff --git a/handlers/openssh/documentation.go b/handlers/openssh/documentation.go index fe7731a..410e69a 100644 --- a/handlers/openssh/documentation.go +++ b/handlers/openssh/documentation.go @@ -205,21 +205,41 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may }, }, ), - // "ChannelTimeout": `Specifies whether and how quickly sshd(8) should close inactive channels. Timeouts are specified as one or more “type=interval” pairs separated by whitespace, where the “type” must be the special keyword “global” or a channel type name from the list below, optionally containing wildcard characters. - // The timeout value “interval” is specified in seconds or may use any of the units documented in the “TIME FORMATS” section. For example, “session=5m” would cause interactive sessions to terminate after five minutes of inactivity. Specifying a zero value disables the inactivity timeout. - // The special timeout “global” applies to all active channels, taken together. Traffic on any active channel will reset the timeout, but when the timeout expires then all open channels will be closed. Note that this global timeout is not matched by wildcards and must be specified explicitly. - // The available channel type names include: - // agent-connection Open connections to ssh-agent(1). - // direct-tcpip, direct-streamlocal@openssh.com Open TCP or Unix socket (respectively) connections that have been established from a ssh(1) local forwarding, i.e. LocalForward or DynamicForward. - // forwarded-tcpip, forwarded-streamlocal@openssh.com Open TCP or Unix socket (respectively) connections that have been established to a sshd(8) listening on behalf of a ssh(1) remote forwarding, i.e. RemoteForward. - // - // `, - // "session": `The interactive main session, including shell session, command execution, scp(1), sftp(1), etc. - // tun-connection Open TunnelForward connections. - // x11-connection Open X11 forwarding sessions. - // Note that in all the above cases, terminating an inactive session does not guarantee to remove all resources associated with the session, e.g. shell processes or X11 clients relating to the session may continue to execute. - // Moreover, terminating an inactive channel or session does not necessarily close the SSH connection, nor does it prevent a client from requesting another channel of the same type. In particular, expiring an inactive forwarding session does not prevent another identical forwarding from being subsequently created. - // The default is not to expire channels of any type for inactivity.`, + "ChannelTimeout": common.NewOption(`Specifies whether and how quickly sshd(8) should close inactive channels. Timeouts are specified as one or more “type=interval” pairs separated by whitespace, where the “type” must be the special keyword “global” or a channel type name from the list below, optionally containing wildcard characters. + The timeout value “interval” is specified in seconds or may use any of the units documented in the “TIME FORMATS” section. For example, “session=5m” would cause interactive sessions to terminate after five minutes of inactivity. Specifying a zero value disables the inactivity timeout. + The special timeout “global” applies to all active channels, taken together. Traffic on any active channel will reset the timeout, but when the timeout expires then all open channels will be closed. Note that this global timeout is not matched by wildcards and must be specified explicitly. + The available channel type names include: + + agent-connection Open connections to ssh-agent(1). + direct-tcpip, direct-streamlocal@openssh.com Open TCP or Unix socket (respectively) connections that have been established from a ssh(1) local forwarding, i.e. LocalForward or DynamicForward. + forwarded-tcpip, forwarded-streamlocal@openssh.com Open TCP or Unix socket (respectively) connections that have been established to a sshd(8) listening on behalf of a ssh(1) remote forwarding, i.e. RemoteForward. + session The interactive main session, including shell session, command execution, scp(1), sftp(1), etc. + tun-connection Open TunnelForward connections. + x11-connection Open X11 forwarding sessions. + + Note that in all the above cases, terminating an inactive session does not guarantee to remove all resources associated with the session, e.g. shell processes or X11 clients relating to the session may continue to execute. + Moreover, terminating an inactive channel or session does not necessarily close the SSH connection, nor does it prevent a client from requesting another channel of the same type. In particular, expiring an inactive forwarding session does not prevent another identical forwarding from being subsequently created. + The default is not to expire channels of any type for inactivity.`, + common.ArrayValue{ + Separator: " ", + AllowDuplicates: false, + SubValue: common.KeyValueAssignmentValue{ + Separator: "=", + Key: common.EnumValue{ + Values: []string{ + "global", + "agent-connection", + "direct-tcpip", "direct-streamlocal@openssh.com", + "forwarded-tcpip", "forwarded-streamlocal@openssh.com", + "session", + "tun-connection", + "x11-connection", + }, + }, + Value: common.StringValue{}, + }, + }, +), "ChrootDirectory": common.NewOption(`Specifies the pathname of a directory to chroot(2) to after authentication. At session startup sshd(8) checks that all components of the pathname are root-owned directories which are not writable by group or others. After the chroot, sshd(8) changes the working directory to the user's home directory. Arguments to ChrootDirectory accept the tokens described in the “TOKENS” section. The ChrootDirectory must contain the necessary files and directories to support the user's session. For an interactive session this requires at least a shell, typically sh(1), and basic /dev nodes such as null(4), zero(4), stdin(4), stdout(4), stderr(4), and tty(4) devices. For file transfer sessions using SFTP no additional configuration of the environment is necessary if the in-process sftp-server is used, though sessions which use logging may require /dev/log inside the chroot directory on some operating systems (see sftp-server(8) for details). For safety, it is very important that the directory hierarchy be prevented from modification by other processes on the system (especially those outside the jail). Misconfiguration can lead to unsafe environments which sshd(8) cannot detect. diff --git a/handlers/openssh/text-document-completion.go b/handlers/openssh/text-document-completion.go index dcd91d2..3f61e2f 100644 --- a/handlers/openssh/text-document-completion.go +++ b/handlers/openssh/text-document-completion.go @@ -3,6 +3,7 @@ package openssh import ( "config-lsp/common" "errors" + "unicode/utf8" "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" @@ -12,13 +13,19 @@ import ( ) func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (interface{}, error) { - option, line, err := Parser.FindByLineNumber(uint32(params.Position.Line)) + optionName, line, err := Parser.FindByLineNumber(uint32(params.Position.Line)) if err == nil { if line.IsCursorAtRootOption(int(params.Position.Character)) { return getRootCompletions(), nil } else { - return getOptionCompletions(option), nil + rawLine, cursor := common.OffsetLineAtLeft( + uint32(utf8.RuneCountInString(optionName + " ")), + line.Value, + params.Position.Character, + ) + + return getOptionCompletions(optionName, rawLine, cursor), nil } } else if errors.Is(err, common.LineNotFoundError{}) { return getRootCompletions(), nil @@ -52,11 +59,11 @@ func getRootCompletions() []protocol.CompletionItem { return completions } -func getCompletionsFromValue(value common.Value) []protocol.CompletionItem { - switch value.(type) { +func getCompletionsFromValue(requiredValue common.Value, line string, cursor uint32) []protocol.CompletionItem { + switch requiredValue.(type) { case common.EnumValue: - enumValue := value.(common.EnumValue) - completions := make([]protocol.CompletionItem, len(value.(common.EnumValue).Values)) + enumValue := requiredValue.(common.EnumValue) + completions := make([]protocol.CompletionItem, len(requiredValue.(common.EnumValue).Values)) for index, value := range enumValue.Values { textFormat := protocol.InsertTextFormatPlainText @@ -71,37 +78,54 @@ func getCompletionsFromValue(value common.Value) []protocol.CompletionItem { return completions case common.CustomValue: - customValue := value.(common.CustomValue) + customValue := requiredValue.(common.CustomValue) val := customValue.FetchValue() - return getCompletionsFromValue(val) + return getCompletionsFromValue(val, line, cursor) case common.ArrayValue: - arrayValue := value.(common.ArrayValue) + arrayValue := requiredValue.(common.ArrayValue) + relativePosition, found := common.FindPreviousCharacter(line, arrayValue.Separator) - return getCompletionsFromValue(arrayValue.SubValue) + if found { + line, cursor = common.OffsetLineAtLeft(relativePosition, line, cursor) + } + + return getCompletionsFromValue(arrayValue.SubValue, line, cursor) case common.OrValue: - orValue := value.(common.OrValue) + orValue := requiredValue.(common.OrValue) completions := make([]protocol.CompletionItem, 0) for _, subValue := range orValue.Values { - completions = append(completions, getCompletionsFromValue(subValue)...) + completions = append(completions, getCompletionsFromValue(subValue, line, cursor)...) } return completions case common.PrefixWithMeaningValue: - prefixWithMeaningValue := value.(common.PrefixWithMeaningValue) + prefixWithMeaningValue := requiredValue.(common.PrefixWithMeaningValue) - return getCompletionsFromValue(prefixWithMeaningValue.SubValue) + return getCompletionsFromValue(prefixWithMeaningValue.SubValue, line, cursor) + case common.KeyValueAssignmentValue: + keyValueAssignmentValue := requiredValue.(common.KeyValueAssignmentValue) + + relativePosition, found := common.FindPreviousCharacter(line, keyValueAssignmentValue.Separator) + + if found { + line, cursor = common.OffsetLineAtLeft(relativePosition, line, cursor) + + return getCompletionsFromValue(keyValueAssignmentValue.Value, line, cursor) + } else { + return getCompletionsFromValue(keyValueAssignmentValue.Key, line, cursor) + } } return []protocol.CompletionItem{} } -func getOptionCompletions(optionName string) []protocol.CompletionItem { +func getOptionCompletions(optionName string, line string, cursor uint32) []protocol.CompletionItem { option := Options[optionName] - completions := getCompletionsFromValue(option.Value) + completions := getCompletionsFromValue(option.Value, line, cursor) return completions }