mirror of
https://github.com/Myzel394/config-lsp.git
synced 2025-06-18 23:15:26 +02:00
commit
ff60b34d25
18
.github/workflows/pr-tests.yaml
vendored
18
.github/workflows/pr-tests.yaml
vendored
@ -11,16 +11,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
# You can test your matrix by printing the current Go version
|
||||
- name: Display Go version
|
||||
run: go version
|
||||
|
||||
- name: Get dependencies
|
||||
run: go mod download
|
||||
- uses: cachix/install-nix-action@v27
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check Nix flake
|
||||
run: nix flake check
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
run: nix develop --command bash -c 'go test ./...'
|
||||
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,3 +25,6 @@ go.work.sum
|
||||
.env
|
||||
|
||||
test.lua
|
||||
config-lsp
|
||||
result
|
||||
bin
|
||||
|
BIN
config-lsp
Executable file
BIN
config-lsp
Executable file
Binary file not shown.
28
doc-values/value-documentation.go
Normal file
28
doc-values/value-documentation.go
Normal file
@ -0,0 +1,28 @@
|
||||
package docvalues
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
type DocumentationValue struct {
|
||||
Documentation string
|
||||
Value Value
|
||||
}
|
||||
|
||||
func (v DocumentationValue) GetTypeDescription() []string {
|
||||
return v.Value.GetTypeDescription()
|
||||
}
|
||||
|
||||
func (v DocumentationValue) CheckIsValid(value string) []*InvalidValue {
|
||||
return v.Value.CheckIsValid(value)
|
||||
}
|
||||
|
||||
func (v DocumentationValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem {
|
||||
return v.Value.FetchCompletions(line, cursor)
|
||||
}
|
||||
|
||||
func (v DocumentationValue) FetchHoverInfo(line string, cursor uint32) []string {
|
||||
return strings.Split(v.Documentation, "\n")
|
||||
}
|
@ -4,6 +4,8 @@ import (
|
||||
"config-lsp/utils"
|
||||
"fmt"
|
||||
net "net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
@ -161,6 +163,24 @@ func (v IPAddressValue) FetchCompletions(line string, cursor uint32) []protocol.
|
||||
})
|
||||
}
|
||||
|
||||
if v.AllowRange {
|
||||
slashIndex := strings.LastIndex(line, "/")
|
||||
|
||||
if slashIndex > -1 && cursor >= uint32(slashIndex) {
|
||||
completions := make([]protocol.CompletionItem, 33)
|
||||
|
||||
for i := 0; i < len(completions); i++ {
|
||||
kind := protocol.CompletionItemKindValue
|
||||
completions[i] = protocol.CompletionItem{
|
||||
Label: strconv.Itoa(i),
|
||||
Kind: &kind,
|
||||
}
|
||||
}
|
||||
|
||||
return completions
|
||||
}
|
||||
}
|
||||
|
||||
return []protocol.CompletionItem{}
|
||||
}
|
||||
|
||||
|
116
flake.lock
generated
Normal file
116
flake.lock
generated
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gomod2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1722589758,
|
||||
"narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=",
|
||||
"owner": "tweag",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tweag",
|
||||
"repo": "gomod2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1723637854,
|
||||
"narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"gomod2nix": "gomod2nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
49
flake.nix
Normal file
49
flake.nix
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
description = "Build config-lsp";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
gomod2nix = {
|
||||
url = "github:tweag/gomod2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.utils.follows = "utils";
|
||||
};
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, gomod2nix }:
|
||||
utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
(final: prev: {
|
||||
go = prev.go_1_22;
|
||||
buildGoModule = prev.buildGo122Module;
|
||||
})
|
||||
gomod2nix.overlays.default
|
||||
];
|
||||
};
|
||||
inputs = [
|
||||
pkgs.go_1_22
|
||||
pkgs.wireguard-tools
|
||||
];
|
||||
in {
|
||||
packages = {
|
||||
default = pkgs.buildGoModule {
|
||||
nativeBuildInputs = inputs;
|
||||
pname = "github.com/Myzel394/config-lsp";
|
||||
version = "v0.0.1";
|
||||
src = ./.;
|
||||
vendorHash = "sha256-KhyqogTyb3jNrGP+0Zmn/nfx+WxzjgcrFOp2vivFgT0=";
|
||||
checkPhase = ''
|
||||
go test -v $(pwd)/...
|
||||
'';
|
||||
};
|
||||
};
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = inputs;
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
2
go.mod
2
go.mod
@ -11,7 +11,9 @@ require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||
github.com/k0kubun/pp v3.0.1+incompatible // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -5,8 +5,13 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40=
|
||||
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
@ -37,6 +42,7 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
@ -71,7 +71,7 @@ func TestValidBasicExample(t *testing.T) {
|
||||
t.Fatal("getCompletion failed to return correct number of completions. Got:", len(completions), "but expected:", 4)
|
||||
}
|
||||
|
||||
if completions[0].Label != "UUID" {
|
||||
if completions[0].Label != "UUID" && completions[0].Label != "PARTUID" {
|
||||
t.Fatal("getCompletion failed to return correct label. Got:", completions[0].Label, "but expected:", "UUID")
|
||||
}
|
||||
}
|
||||
|
52
handlers/wireguard/commands/wg-commands.go
Normal file
52
handlers/wireguard/commands/wg-commands.go
Normal file
@ -0,0 +1,52 @@
|
||||
package wgcommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var whitespacePattern = regexp.MustCompile(`[\s\n]+`)
|
||||
|
||||
func AreWireguardToolsAvailable() bool {
|
||||
_, err := exec.LookPath("wg")
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func CreateNewPrivateKey() (string, error) {
|
||||
cmd := exec.Command("wg", "genkey")
|
||||
|
||||
bytes, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(whitespacePattern.ReplaceAll(bytes, []byte(""))), nil
|
||||
}
|
||||
|
||||
func CreatePublicKey(privateKey string) (string, error) {
|
||||
cmd := exec.Command("wg", "pubkey")
|
||||
cmd.Stdin = strings.NewReader(privateKey)
|
||||
|
||||
bytes, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(whitespacePattern.ReplaceAll(bytes, []byte(""))), nil
|
||||
}
|
||||
|
||||
func CreatePresharedKey() (string, error) {
|
||||
cmd := exec.Command("wg", "genpsk")
|
||||
|
||||
bytes, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(whitespacePattern.ReplaceAll(bytes, []byte(""))), nil
|
||||
}
|
40
handlers/wireguard/commands/wg-commands_test.go
Normal file
40
handlers/wireguard/commands/wg-commands_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
package wgcommands
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWireguardAvailable(
|
||||
t *testing.T,
|
||||
) {
|
||||
if !AreWireguardToolsAvailable() {
|
||||
t.Skip("Wireguard tools not available")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireguardPrivateKey(
|
||||
t *testing.T,
|
||||
) {
|
||||
privateKey, err := CreateNewPrivateKey()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log(privateKey)
|
||||
}
|
||||
|
||||
func TestWireguardPublicKey(
|
||||
t *testing.T,
|
||||
) {
|
||||
privateKey := "UPBKR0kLF2C/+Ei5fwN5KHsAcon9xfBX+RWhebYFGWg="
|
||||
publicKey, err := CreatePublicKey(privateKey)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if publicKey != "3IPUqUKXUkkU7tNp/G/KgcBqUh3N0WWJpfQf79lGdl0=" {
|
||||
t.Fatalf("Public key does not match, it's: %v", publicKey)
|
||||
}
|
||||
|
||||
t.Log(publicKey)
|
||||
}
|
326
handlers/wireguard/fields/documentation-fields.go
Normal file
326
handlers/wireguard/fields/documentation-fields.go
Normal file
@ -0,0 +1,326 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
docvalues "config-lsp/doc-values"
|
||||
)
|
||||
|
||||
// Documentation taken from https://github.com/pirate/wireguard-docs
|
||||
var HeaderInterfaceEnum = docvalues.CreateEnumStringWithDoc(
|
||||
"[Interface]",
|
||||
"Defines the VPN settings for the local node.",
|
||||
)
|
||||
var HeaderPeerEnum = docvalues.CreateEnumStringWithDoc(
|
||||
"[Peer]",
|
||||
`Defines the VPN settings for a remote peer capable of routing traffic for one or more addresses (itself and/or other peers). Peers can be either a public bounce server that relays traffic to other peers, or a directly accessible client via LAN/internet that is not behind a NAT and only routes traffic for itself.
|
||||
|
||||
All clients must be defined as peers on the public bounce server. Simple clients that only route traffic for themselves, only need to define peers for the public relay, and any other nodes directly accessible. Nodes that are behind separate NATs should not be defined as peers outside of the public server config, as no direct route is available between separate NATs. Instead, nodes behind NATs should only define the public relay servers and other public clients as their peers, and should specify AllowedIPs = 192.0.2.1/24 on the public server that accept routes and bounce traffic for the VPN subnet to the remote NAT-ed peers.
|
||||
|
||||
In summary, all nodes must be defined on the main bounce server. On client servers, only peers that are directly accessible from a node should be defined as peers of that node, any peers that must be relayed by a bounce server should be left out and will be handled by the relay server's catchall route.`,
|
||||
)
|
||||
|
||||
var minPortValue = 1
|
||||
var maxPortValue = 65535
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc791
|
||||
var minMTUValue = 68
|
||||
var maxMTUValue = 1500
|
||||
|
||||
var InterfaceOptions = map[string]docvalues.DocumentationValue{
|
||||
"Address": {
|
||||
Documentation: `Defines what address range the local node should route traffic for. Depending on whether the node is a simple client joining the VPN subnet, or a bounce server that's relaying traffic between multiple clients, this can be set to a single IP of the node itself (specified with CIDR notation), e.g. 192.0.2.3/32), or a range of IPv4/IPv6 subnets that the node can route traffic for.
|
||||
|
||||
## Examples
|
||||
Node is a client that only routes traffic for itself
|
||||
|
||||
Address = 192.0.2.3/32
|
||||
|
||||
Node is a public bounce server that can relay traffic to other peers
|
||||
When the node is acting as the public bounce server, it should set this to be the entire subnet that it can route traffic, not just a single IP for itself.
|
||||
|
||||
Address = 192.0.2.1/24
|
||||
|
||||
You can also specify multiple subnets or IPv6 subnets like so:
|
||||
|
||||
Address = 192.0.2.1/24,2001:DB8::/64
|
||||
`,
|
||||
Value: docvalues.IPAddressValue{
|
||||
AllowIPv4: true,
|
||||
AllowIPv6: true,
|
||||
AllowRange: true,
|
||||
},
|
||||
},
|
||||
"ListenPort": {
|
||||
Documentation: `When the node is acting as a public bounce server, it should hardcode a port to listen for incoming VPN connections from the public internet. Clients not acting as relays should not set this value. If not specified, chosen randomly.
|
||||
|
||||
## Examples
|
||||
Using default WireGuard port
|
||||
|
||||
ListenPort = 51820
|
||||
|
||||
Using custom WireGuard port
|
||||
|
||||
ListenPort = 7000
|
||||
`,
|
||||
Value: docvalues.NumberValue{
|
||||
Min: &minPortValue,
|
||||
Max: &maxPortValue,
|
||||
},
|
||||
},
|
||||
"PrivateKey": {
|
||||
Documentation: `This is the private key for the local node, never shared with other servers. All nodes must have a private key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN.
|
||||
|
||||
This key can be generated with [wg genkey > example.key]
|
||||
`,
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
"DNS": {
|
||||
Documentation: `The DNS server(s) to announce to VPN clients via DHCP, most clients will use this server for DNS requests over the VPN, but clients can also override this value locally on their nodes
|
||||
|
||||
The value can be left unconfigured to use the system's default DNS servers
|
||||
|
||||
## Examples
|
||||
A single DNS server can be provided
|
||||
|
||||
DNS = 9.9.9.9
|
||||
|
||||
or multiple DNS servers can be provided
|
||||
|
||||
DNS = 9.9.9.9,1.1.1.1,8.8.8.8
|
||||
`,
|
||||
Value: docvalues.ArrayValue{
|
||||
Separator: ",",
|
||||
DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor,
|
||||
SubValue: docvalues.IPAddressValue{
|
||||
AllowIPv4: true,
|
||||
AllowIPv6: true,
|
||||
AllowRange: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Table": {
|
||||
Documentation: `Optionally defines which routing table to use for the WireGuard routes, not necessary to configure for most setups.
|
||||
|
||||
There are two special values: ‘off’ disables the creation of routes altogether, and ‘auto’ (the default) adds routes to the default table and enables special handling of default routes.
|
||||
|
||||
https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
|
||||
|
||||
## Examples
|
||||
|
||||
Table = 1234
|
||||
`,
|
||||
Value: docvalues.OrValue{
|
||||
Values: []docvalues.Value{
|
||||
docvalues.EnumValue{
|
||||
EnforceValues: false,
|
||||
Values: []docvalues.EnumString{
|
||||
docvalues.CreateEnumStringWithDoc(
|
||||
"off",
|
||||
"Disable the creation of routes altogether",
|
||||
),
|
||||
docvalues.CreateEnumStringWithDoc(
|
||||
"auto",
|
||||
"Adds routes to the default table and enables special handling of default routes",
|
||||
),
|
||||
},
|
||||
},
|
||||
docvalues.StringValue{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"MTU": {
|
||||
Documentation: `Optionally defines the maximum transmission unit (MTU, aka packet/frame size) to use when connecting to the peer, not necessary to configure for most setups.
|
||||
|
||||
The MTU is automatically determined from the endpoint addresses or the system default route, which is usually a sane choice.
|
||||
|
||||
https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
|
||||
|
||||
## Examples
|
||||
|
||||
MTU = 1500
|
||||
`, Value: docvalues.NumberValue{
|
||||
Min: &minMTUValue,
|
||||
Max: &maxMTUValue,
|
||||
},
|
||||
},
|
||||
"PreUp": {
|
||||
Documentation: `Optionally run a command before the interface is brought up. This option can be specified multiple times, with commands executed in the order they appear in the file.
|
||||
|
||||
## Examples
|
||||
|
||||
Add an IP route
|
||||
|
||||
PreUp = ip rule add ipproto tcp dport 22 table 1234
|
||||
`, Value: docvalues.StringValue{},
|
||||
},
|
||||
"PostUp": {
|
||||
Documentation: `Optionally run a command after the interface is brought up. This option can appear multiple times, as with PreUp
|
||||
|
||||
## Examples
|
||||
Read in a config value from a file or some command's output
|
||||
|
||||
PostUp = wg set %i private-key /etc/wireguard/wg0.key <(some command here)
|
||||
|
||||
Log a line to a file
|
||||
|
||||
PostUp = echo "$(date +%s) WireGuard Started" >> /var/log/wireguard.log
|
||||
|
||||
Hit a webhook on another server
|
||||
|
||||
PostUp = curl https://events.example.dev/wireguard/started/?key=abcdefg
|
||||
|
||||
Add a route to the system routing table
|
||||
|
||||
PostUp = ip rule add ipproto tcp dport 22 table 1234
|
||||
|
||||
Add an iptables rule to enable packet forwarding on the WireGuard interface
|
||||
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
Force WireGuard to re-resolve IP address for peer domain
|
||||
|
||||
PostUp = resolvectl domain %i "~."; resolvectl dns %i 192.0.2.1; resolvectl dnssec %i yes
|
||||
`,
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
"PreDown": {
|
||||
Documentation: `Optionally run a command before the interface is brought down. This option can appear multiple times, as with PreUp
|
||||
|
||||
## Examples
|
||||
Log a line to a file
|
||||
|
||||
PostDown = echo "$(date +%s) WireGuard Going Down" >> /var/log/wireguard.log
|
||||
|
||||
Hit a webhook on another server
|
||||
|
||||
PostDown = curl https://events.example.dev/wireguard/stopping/?key=abcdefg
|
||||
`,
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
"PostDown": {
|
||||
Documentation: `Optionally run a command after the interface is brought down. This option can appear multiple times, as with PreUp
|
||||
|
||||
## Examples
|
||||
|
||||
Log a line to a file
|
||||
|
||||
PostDown = echo "$(date +%s) WireGuard Stopped" >> /var/log/wireguard.log
|
||||
|
||||
Hit a webhook on another server
|
||||
|
||||
PostDown = curl https://events.example.dev/wireguard/stopped/?key=abcdefg
|
||||
|
||||
Remove the iptables rule that forwards packets on the WireGuard interface
|
||||
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
`,
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
"FwMark": {
|
||||
Documentation: "a 32-bit fwmark for outgoing packets. If set to 0 or \"off\", this option is disabled. May be specified in hexadecimal by prepending \"0x\". Optional",
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
}
|
||||
|
||||
var InterfaceAllowedDuplicateFields = map[string]struct{}{
|
||||
"PreUp": {},
|
||||
"PostUp": {},
|
||||
"PreDown": {},
|
||||
"PostDown": {},
|
||||
}
|
||||
|
||||
var PeerOptions = map[string]docvalues.DocumentationValue{
|
||||
"Endpoint": {
|
||||
Documentation: `Defines the publicly accessible address for a remote peer. This should be left out for peers behind a NAT or peers that don't have a stable publicly accessible IP:PORT pair. Typically, this only needs to be defined on the main bounce server, but it can also be defined on other public nodes with stable IPs like public-server2 in the example config below.
|
||||
|
||||
## Examples
|
||||
Endpoint is an IP address
|
||||
|
||||
[Endpoint = 123.124.125.126:51820] (IPv6 is also supported)
|
||||
|
||||
Endpoint is a hostname/FQDN
|
||||
|
||||
Endpoint = public-server1.example-vpn.tld:51820
|
||||
`,
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
"AllowedIPs": {
|
||||
Documentation: `This defines the IP ranges for which a peer will route traffic. On simple clients, this is usually a single address (the VPN address of the simple client itself). For bounce servers this will be a range of the IPs or subnets that the relay server is capable of routing traffic for. Multiple IPs and subnets may be specified using comma-separated IPv4 or IPv6 CIDR notation (from a single /32 or /128 address, all the way up to 0.0.0.0/0 and ::/0 to indicate a default route to send all internet and VPN traffic through that peer). This option may be specified multiple times.
|
||||
|
||||
When deciding how to route a packet, the system chooses the most specific route first, and falls back to broader routes. So for a packet destined to 192.0.2.3, the system would first look for a peer advertising 192.0.2.3/32 specifically, and would fall back to a peer advertising 192.0.2.1/24 or a larger range like 0.0.0.0/0 as a last resort.
|
||||
|
||||
## Examples
|
||||
|
||||
Peer is a simple client that only accepts traffic to/from itself
|
||||
|
||||
AllowedIPs = 192.0.2.3/32
|
||||
|
||||
Peer is a relay server that can bounce VPN traffic to all other peers
|
||||
|
||||
AllowedIPs = 192.0.2.1/24
|
||||
|
||||
Peer is a relay server that bounces all internet & VPN traffic (like a proxy), including IPv6
|
||||
|
||||
AllowedIPs = 0.0.0.0/0,::/0
|
||||
|
||||
Peer is a relay server that routes to itself and only one other peer
|
||||
|
||||
AllowedIPs = 192.0.2.3/32,192.0.2.4/32
|
||||
|
||||
Peer is a relay server that routes to itself and all nodes on its local LAN
|
||||
|
||||
AllowedIPs = 192.0.2.3/32,192.168.1.1/24
|
||||
`,
|
||||
Value: docvalues.ArrayValue{
|
||||
Separator: ",",
|
||||
DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor,
|
||||
SubValue: docvalues.IPAddressValue{
|
||||
AllowIPv4: true,
|
||||
AllowIPv6: true,
|
||||
AllowRange: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"PublicKey": {
|
||||
Documentation: `This is the public key for the remote node, shareable with all peers. All nodes must have a public key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN.
|
||||
|
||||
This key can be generated with wg pubkey < example.key > example.key.pub. (see above for how to generate the private key example.key)
|
||||
|
||||
## Examples
|
||||
|
||||
PublicKey = somePublicKeyAbcdAbcdAbcdAbcd=
|
||||
`,
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
"PersistentKeepalive": {
|
||||
Documentation: `If the connection is going from a NAT-ed peer to a public peer, the node behind the NAT must regularly send an outgoing ping in order to keep the bidirectional connection alive in the NAT router's connection table.
|
||||
|
||||
## Examples
|
||||
|
||||
Local public node to remote public node
|
||||
|
||||
This value should be left undefined as persistent pings are not needed.
|
||||
|
||||
Local public node to remote NAT-ed node
|
||||
|
||||
This value should be left undefined as it's the client's responsibility to keep the connection alive because the server cannot reopen a dead connection to the client if it times out.
|
||||
|
||||
Oocal NAT-ed node to remote public node
|
||||
|
||||
PersistentKeepalive = 25 this will send a ping to every 25 seconds keeping the connection open in the local NAT router's connection table.
|
||||
`,
|
||||
Value: docvalues.PositiveNumberValue(),
|
||||
},
|
||||
"PresharedKey": {
|
||||
Documentation: "Optionally defines a pre-shared key for the peer, used to authenticate the connection. This is not necessary, but strongly recommended for security.",
|
||||
Value: docvalues.StringValue{},
|
||||
},
|
||||
}
|
||||
|
||||
var PeerAllowedDuplicateFields = map[string]struct{}{
|
||||
"AllowedIPs": {},
|
||||
}
|
||||
|
||||
var OptionsHeaderMap = map[string](map[string]docvalues.DocumentationValue){
|
||||
"Interface": InterfaceOptions,
|
||||
"Peer": PeerOptions,
|
||||
}
|
452
handlers/wireguard/handlers/analyzer.go
Normal file
452
handlers/wireguard/handlers/analyzer.go
Normal file
@ -0,0 +1,452 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
docvalues "config-lsp/doc-values"
|
||||
"config-lsp/handlers/wireguard/fields"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"config-lsp/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func Analyze(
|
||||
p parser.WireguardParser,
|
||||
) []protocol.Diagnostic {
|
||||
sectionsErrors := analyzeSections(p.Sections)
|
||||
sectionsErrors = append(sectionsErrors, analyzeOnlyOneInterfaceSectionSpecified(p)...)
|
||||
|
||||
if len(sectionsErrors) > 0 {
|
||||
return sectionsErrors
|
||||
}
|
||||
|
||||
validCheckErrors := checkIfValuesAreValid(p.Sections)
|
||||
|
||||
if len(validCheckErrors) > 0 {
|
||||
return validCheckErrors
|
||||
}
|
||||
|
||||
diagnostics := make([]protocol.Diagnostic, 0)
|
||||
diagnostics = append(diagnostics, analyzeParserForDuplicateProperties(p)...)
|
||||
diagnostics = append(diagnostics, analyzeDNSContainsFallback(p)...)
|
||||
diagnostics = append(diagnostics, analyzeKeepAliveIsSet(p)...)
|
||||
diagnostics = append(diagnostics, analyzeSymmetricPropertiesExist(p)...)
|
||||
diagnostics = append(diagnostics, analyzeDuplicateAllowedIPs(p)...)
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func analyzeSections(
|
||||
sections []*parser.WireguardSection,
|
||||
) []protocol.Diagnostic {
|
||||
var diagnostics []protocol.Diagnostic
|
||||
|
||||
for _, section := range sections {
|
||||
sectionDiagnostics := analyzeSection(*section)
|
||||
|
||||
if len(sectionDiagnostics) > 0 {
|
||||
diagnostics = append(diagnostics, sectionDiagnostics...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(diagnostics) > 0 {
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func analyzeOnlyOneInterfaceSectionSpecified(
|
||||
p parser.WireguardParser,
|
||||
) []protocol.Diagnostic {
|
||||
var diagnostics []protocol.Diagnostic
|
||||
alreadyFound := false
|
||||
|
||||
for _, section := range p.GetSectionsByName("Interface") {
|
||||
if alreadyFound {
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: "Only one [Interface] section is allowed",
|
||||
Severity: &severity,
|
||||
Range: section.GetHeaderLineRange(),
|
||||
})
|
||||
}
|
||||
|
||||
alreadyFound = true
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func analyzeDNSContainsFallback(
|
||||
p parser.WireguardParser,
|
||||
) []protocol.Diagnostic {
|
||||
lineNumber, property := p.FindFirstPropertyByName("DNS")
|
||||
|
||||
if property == nil {
|
||||
return []protocol.Diagnostic{}
|
||||
}
|
||||
|
||||
dnsAmount := len(strings.Split(property.Value.Value, ","))
|
||||
|
||||
if dnsAmount == 1 {
|
||||
severity := protocol.DiagnosticSeverityHint
|
||||
|
||||
return []protocol.Diagnostic{
|
||||
{
|
||||
Message: "There is only one DNS server specified. It is recommended to set up fallback DNS servers",
|
||||
Severity: &severity,
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: *lineNumber,
|
||||
Character: property.Value.Location.Start,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: *lineNumber,
|
||||
Character: property.Value.Location.End,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return []protocol.Diagnostic{}
|
||||
}
|
||||
|
||||
func analyzeKeepAliveIsSet(
|
||||
p parser.WireguardParser,
|
||||
) []protocol.Diagnostic {
|
||||
var diagnostics []protocol.Diagnostic
|
||||
|
||||
for _, section := range p.GetSectionsByName("Peer") {
|
||||
// If an endpoint is set, then we should only check for the keepalive property
|
||||
if section.ExistsProperty("Endpoint") && !section.ExistsProperty("PersistentKeepalive") {
|
||||
severity := protocol.DiagnosticSeverityHint
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: "PersistentKeepalive is not set. It is recommended to set this property, as it helps to maintain the connection when users are behind NAT",
|
||||
Severity: &severity,
|
||||
Range: section.GetRange(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
// Check if the values are valid.
|
||||
// Assumes that sections have been analyzed already.
|
||||
func checkIfValuesAreValid(
|
||||
sections []*parser.WireguardSection,
|
||||
) []protocol.Diagnostic {
|
||||
var diagnostics []protocol.Diagnostic
|
||||
|
||||
for _, section := range sections {
|
||||
for lineNumber, property := range section.Properties {
|
||||
diagnostics = append(
|
||||
diagnostics,
|
||||
analyzeProperty(property, section, lineNumber)...,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func analyzeSection(
|
||||
s parser.WireguardSection,
|
||||
) []protocol.Diagnostic {
|
||||
var diagnostics []protocol.Diagnostic
|
||||
|
||||
if s.Name == nil {
|
||||
// No section name
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: "This section is missing a name",
|
||||
Severity: &severity,
|
||||
Range: s.GetRange(),
|
||||
})
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
if _, found := fields.OptionsHeaderMap[*s.Name]; !found {
|
||||
// Unknown section
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: fmt.Sprintf("Unknown section '%s'. It must be one of: [Interface], [Peer]", *s.Name),
|
||||
Severity: &severity,
|
||||
Range: s.GetHeaderLineRange(),
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
// Check if the property is valid.
|
||||
// Returns a list of diagnostics.
|
||||
// `belongingSection` is the section to which the property belongs. This value is
|
||||
// expected to be non-nil and expected to be a valid Wireguard section.
|
||||
func analyzeProperty(
|
||||
p parser.WireguardProperty,
|
||||
belongingSection *parser.WireguardSection,
|
||||
propertyLine uint32,
|
||||
) []protocol.Diagnostic {
|
||||
sectionOptions := fields.OptionsHeaderMap[*belongingSection.Name]
|
||||
option, found := sectionOptions[p.Key.Name]
|
||||
|
||||
if !found {
|
||||
// Unknown property
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
return []protocol.Diagnostic{
|
||||
{
|
||||
Message: fmt.Sprintf("Unknown property '%s'", p.Key.Name),
|
||||
Severity: &severity,
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: propertyLine,
|
||||
Character: p.Key.Location.Start,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: propertyLine,
|
||||
Character: p.Key.Location.End,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if p.Value == nil {
|
||||
// No value to check
|
||||
severity := protocol.DiagnosticSeverityWarning
|
||||
return []protocol.Diagnostic{
|
||||
{
|
||||
Message: "Property is missing a value",
|
||||
Severity: &severity,
|
||||
Range: p.GetLineRange(propertyLine),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
errors := option.CheckIsValid(p.Value.Value)
|
||||
|
||||
return utils.Map(errors, func(err *docvalues.InvalidValue) protocol.Diagnostic {
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
return protocol.Diagnostic{
|
||||
Message: err.GetMessage(),
|
||||
Severity: &severity,
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: propertyLine,
|
||||
Character: p.Value.Location.Start + err.Start,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: propertyLine,
|
||||
Character: p.Value.Location.Start + err.End,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func analyzeParserForDuplicateProperties(
|
||||
p parser.WireguardParser,
|
||||
) []protocol.Diagnostic {
|
||||
diagnostics := make([]protocol.Diagnostic, 0)
|
||||
|
||||
for _, section := range p.Sections {
|
||||
diagnostics = append(diagnostics, analyzeDuplicateProperties(*section)...)
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func analyzeDuplicateProperties(
|
||||
s parser.WireguardSection,
|
||||
) []protocol.Diagnostic {
|
||||
var diagnostics []protocol.Diagnostic
|
||||
|
||||
existingProperties := make(map[string]uint32)
|
||||
|
||||
lines := utils.KeysOfMap(s.Properties)
|
||||
slices.Sort(lines)
|
||||
|
||||
for _, currentLineNumber := range lines {
|
||||
property := s.Properties[currentLineNumber]
|
||||
var skipCheck = false
|
||||
|
||||
if s.Name != nil {
|
||||
switch *s.Name {
|
||||
case "Interface":
|
||||
if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found {
|
||||
skipCheck = true
|
||||
}
|
||||
case "Peer":
|
||||
if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found {
|
||||
skipCheck = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if skipCheck {
|
||||
continue
|
||||
}
|
||||
|
||||
if existingLineNumber, found := existingProperties[property.Key.Name]; found {
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: fmt.Sprintf("Property '%s' is already defined on line %d", property.Key.Name, existingLineNumber+1),
|
||||
Severity: &severity,
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: currentLineNumber,
|
||||
Character: 0,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: currentLineNumber,
|
||||
Character: 99999,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
existingProperties[property.Key.Name] = currentLineNumber
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
type propertyWithLine struct {
|
||||
Line uint32
|
||||
Property parser.WireguardProperty
|
||||
IpPrefix netip.Prefix
|
||||
}
|
||||
|
||||
func mapAllowedIPsToMasks(p parser.WireguardParser) map[uint8][]propertyWithLine {
|
||||
ips := make(map[uint8][]propertyWithLine)
|
||||
|
||||
for _, section := range p.GetSectionsByName("Peer") {
|
||||
for lineNumber, property := range section.Properties {
|
||||
if property.Key.Name == "AllowedIPs" {
|
||||
ipAddress, err := netip.ParsePrefix(property.Value.Value)
|
||||
|
||||
if err != nil {
|
||||
// This should not happen...
|
||||
continue
|
||||
}
|
||||
|
||||
hostBits := uint8(ipAddress.Bits())
|
||||
|
||||
if _, found := ips[uint8(hostBits)]; !found {
|
||||
ips[hostBits] = make([]propertyWithLine, 0)
|
||||
}
|
||||
|
||||
ips[hostBits] = append(ips[hostBits], propertyWithLine{
|
||||
Line: uint32(lineNumber),
|
||||
Property: property,
|
||||
IpPrefix: ipAddress,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
// Strategy
|
||||
// Simply compare the host bits of the IP addresses.
|
||||
// Use a binary tree to store the host bits.
|
||||
func analyzeDuplicateAllowedIPs(p parser.WireguardParser) []protocol.Diagnostic {
|
||||
diagnostics := make([]protocol.Diagnostic, 0)
|
||||
|
||||
maskedIPs := mapAllowedIPsToMasks(p)
|
||||
hostBits := utils.KeysOfMap(maskedIPs)
|
||||
slices.Sort(hostBits)
|
||||
|
||||
ipHostSet := utils.CreateIPv4HostSet()
|
||||
|
||||
for _, hostBit := range hostBits {
|
||||
ips := maskedIPs[hostBit]
|
||||
|
||||
for _, ipInfo := range ips {
|
||||
if ctx, _ := ipHostSet.ContainsIP(ipInfo.IpPrefix); ctx != nil {
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
definedLine := (*ctx).Value("line").(uint32)
|
||||
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine),
|
||||
Severity: &severity,
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: ipInfo.Line,
|
||||
Character: ipInfo.Property.Key.Location.Start,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: ipInfo.Line,
|
||||
Character: ipInfo.Property.Value.Location.End,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
humanLineNumber := ipInfo.Line + 1
|
||||
ctx := context.WithValue(context.Background(), "line", humanLineNumber)
|
||||
|
||||
ipHostSet.AddIP(
|
||||
ipInfo.IpPrefix,
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func analyzeSymmetricPropertiesExist(
|
||||
p parser.WireguardParser,
|
||||
) []protocol.Diagnostic {
|
||||
diagnostics := make([]protocol.Diagnostic, 0, 4)
|
||||
severity := protocol.DiagnosticSeverityHint
|
||||
|
||||
for _, section := range p.GetSectionsByName("Interface") {
|
||||
preUpLine, preUpProperty := section.FetchFirstProperty("PreUp")
|
||||
preDownLine, preDownProperty := section.FetchFirstProperty("PreDown")
|
||||
|
||||
postUpLine, postUpProperty := section.FetchFirstProperty("PostUp")
|
||||
postDownLine, postDownProperty := section.FetchFirstProperty("PostDown")
|
||||
|
||||
if preUpProperty != nil && preDownProperty == nil {
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: "PreUp is set, but PreDown is not. It is recommended to set both properties symmetrically",
|
||||
Range: preUpProperty.GetLineRange(*preUpLine),
|
||||
Severity: &severity,
|
||||
})
|
||||
} else if preUpProperty == nil && preDownProperty != nil {
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: "PreDown is set, but PreUp is not. It is recommended to set both properties symmetrically",
|
||||
Range: preDownProperty.GetLineRange(*preDownLine),
|
||||
Severity: &severity,
|
||||
})
|
||||
}
|
||||
|
||||
if postUpProperty != nil && postDownProperty == nil {
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: "PostUp is set, but PostDown is not. It is recommended to set both properties symmetrically",
|
||||
Range: postUpProperty.GetLineRange(*postUpLine),
|
||||
Severity: &severity,
|
||||
})
|
||||
} else if postUpProperty == nil && postDownProperty != nil {
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Message: "PostDown is set, but PostUp is not. It is recommended to set both properties symmetrically",
|
||||
Range: postDownProperty.GetLineRange(*postDownLine),
|
||||
Severity: &severity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
57
handlers/wireguard/handlers/analyzer_test.go
Normal file
57
handlers/wireguard/handlers/analyzer_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"config-lsp/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMultipleIntefaces(t *testing.T) {
|
||||
content := utils.Dedent(`
|
||||
[Interface]
|
||||
PrivateKey = abc
|
||||
|
||||
[Interface]
|
||||
PrivateKey = def
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(content)
|
||||
|
||||
diagnostics := Analyze(p)
|
||||
|
||||
if len(diagnostics) == 0 {
|
||||
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidValue(t *testing.T) {
|
||||
content := utils.Dedent(`
|
||||
[Interface]
|
||||
DNS = nope
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(content)
|
||||
|
||||
diagnostics := Analyze(p)
|
||||
|
||||
if len(diagnostics) == 0 {
|
||||
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateProperties(t *testing.T) {
|
||||
content := utils.Dedent(`
|
||||
[Interface]
|
||||
PrivateKey = abc
|
||||
DNS = 1.1.1.1
|
||||
PrivateKey = def
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(content)
|
||||
|
||||
diagnostics := Analyze(p)
|
||||
|
||||
if len(diagnostics) == 0 {
|
||||
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
||||
}
|
||||
}
|
143
handlers/wireguard/handlers/code-actions.go
Normal file
143
handlers/wireguard/handlers/code-actions.go
Normal file
@ -0,0 +1,143 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
wgcommands "config-lsp/handlers/wireguard/commands"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
type CodeActionName string
|
||||
|
||||
const (
|
||||
CodeActionGeneratePrivateKey CodeActionName = "generatePrivateKey"
|
||||
CodeActionGeneratePresharedKey CodeActionName = "generatePresharedKey"
|
||||
CodeActionAddKeepalive CodeActionName = "addKeepalive"
|
||||
)
|
||||
|
||||
type CodeAction interface {
|
||||
RunCommand(*parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error)
|
||||
}
|
||||
|
||||
type CodeActionArgs interface{}
|
||||
|
||||
type CodeActionGeneratePrivateKeyArgs struct {
|
||||
URI protocol.DocumentUri
|
||||
Line uint32
|
||||
}
|
||||
|
||||
func CodeActionGeneratePrivateKeyArgsFromArguments(arguments map[string]any) CodeActionGeneratePrivateKeyArgs {
|
||||
return CodeActionGeneratePrivateKeyArgs{
|
||||
URI: arguments["URI"].(protocol.DocumentUri),
|
||||
Line: uint32(arguments["Line"].(float64)),
|
||||
}
|
||||
}
|
||||
|
||||
func (args CodeActionGeneratePrivateKeyArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) {
|
||||
privateKey, err := wgcommands.CreateNewPrivateKey()
|
||||
|
||||
if err != nil {
|
||||
return &protocol.ApplyWorkspaceEditParams{}, err
|
||||
}
|
||||
|
||||
section, property := p.GetPropertyByLine(args.Line)
|
||||
|
||||
if section == nil || property == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
label := "Generate Private Key"
|
||||
return &protocol.ApplyWorkspaceEditParams{
|
||||
Label: &label,
|
||||
Edit: protocol.WorkspaceEdit{
|
||||
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
|
||||
args.URI: {
|
||||
{
|
||||
NewText: " " + privateKey,
|
||||
Range: property.GetInsertRange(args.Line),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CodeActionGeneratePresharedKeyArgs struct {
|
||||
URI protocol.DocumentUri
|
||||
Line uint32
|
||||
}
|
||||
|
||||
func CodeActionGeneratePresharedKeyArgsFromArguments(arguments map[string]any) CodeActionGeneratePresharedKeyArgs {
|
||||
return CodeActionGeneratePresharedKeyArgs{
|
||||
URI: arguments["URI"].(protocol.DocumentUri),
|
||||
Line: uint32(arguments["Line"].(float64)),
|
||||
}
|
||||
}
|
||||
|
||||
func (args CodeActionGeneratePresharedKeyArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) {
|
||||
presharedKey, err := wgcommands.CreatePresharedKey()
|
||||
|
||||
if err != nil {
|
||||
return &protocol.ApplyWorkspaceEditParams{}, err
|
||||
}
|
||||
|
||||
section, property := p.GetPropertyByLine(args.Line)
|
||||
|
||||
if section == nil || property == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
label := "Generate Preshared Key"
|
||||
return &protocol.ApplyWorkspaceEditParams{
|
||||
Label: &label,
|
||||
Edit: protocol.WorkspaceEdit{
|
||||
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
|
||||
args.URI: {
|
||||
{
|
||||
NewText: " " + presharedKey,
|
||||
Range: property.GetInsertRange(args.Line),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CodeActionAddKeepaliveArgs struct {
|
||||
URI protocol.DocumentUri
|
||||
SectionIndex uint32
|
||||
}
|
||||
|
||||
func CodeActionAddKeepaliveArgsFromArguments(arguments map[string]any) CodeActionAddKeepaliveArgs {
|
||||
return CodeActionAddKeepaliveArgs{
|
||||
URI: arguments["URI"].(protocol.DocumentUri),
|
||||
SectionIndex: uint32(arguments["SectionIndex"].(float64)),
|
||||
}
|
||||
}
|
||||
|
||||
func (args CodeActionAddKeepaliveArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) {
|
||||
section := p.Sections[args.SectionIndex]
|
||||
|
||||
label := "Add PersistentKeepalive"
|
||||
return &protocol.ApplyWorkspaceEditParams{
|
||||
Label: &label,
|
||||
Edit: protocol.WorkspaceEdit{
|
||||
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
|
||||
args.URI: {
|
||||
{
|
||||
NewText: "PersistentKeepalive = 25\n",
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: section.EndLine + 1,
|
||||
Character: 0,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: section.EndLine + 1,
|
||||
Character: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
160
handlers/wireguard/handlers/completions.go
Normal file
160
handlers/wireguard/handlers/completions.go
Normal file
@ -0,0 +1,160 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
docvalues "config-lsp/doc-values"
|
||||
"config-lsp/handlers/wireguard/fields"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"config-lsp/utils"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
"maps"
|
||||
)
|
||||
|
||||
func getHeaderCompletion(name string, documentation string) protocol.CompletionItem {
|
||||
textFormat := protocol.InsertTextFormatPlainText
|
||||
kind := protocol.CompletionItemKindEnum
|
||||
|
||||
insertText := "[" + name + "]\n"
|
||||
|
||||
return protocol.CompletionItem{
|
||||
Label: "[" + name + "]",
|
||||
InsertTextFormat: &textFormat,
|
||||
InsertText: &insertText,
|
||||
Kind: &kind,
|
||||
Documentation: &documentation,
|
||||
}
|
||||
}
|
||||
|
||||
func GetRootCompletionsForEmptyLine(
|
||||
p parser.WireguardParser,
|
||||
) ([]protocol.CompletionItem, error) {
|
||||
completions := make([]protocol.CompletionItem, 0)
|
||||
|
||||
if _, found := p.GetInterfaceSection(); !found {
|
||||
completions = append(completions, getHeaderCompletion("Interface", fields.HeaderInterfaceEnum.Documentation))
|
||||
}
|
||||
|
||||
completions = append(completions, getHeaderCompletion("Peer", fields.HeaderPeerEnum.Documentation))
|
||||
|
||||
return completions, nil
|
||||
}
|
||||
|
||||
func GetCompletionsForSectionEmptyLine(
|
||||
s parser.WireguardSection,
|
||||
) ([]protocol.CompletionItem, error) {
|
||||
if s.Name == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
options := make(map[string]docvalues.DocumentationValue)
|
||||
|
||||
switch *s.Name {
|
||||
case "Interface":
|
||||
maps.Copy(options, fields.InterfaceOptions)
|
||||
|
||||
// Remove existing options
|
||||
for _, property := range s.Properties {
|
||||
if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(options, property.Key.Name)
|
||||
}
|
||||
case "Peer":
|
||||
maps.Copy(options, fields.PeerOptions)
|
||||
|
||||
// Remove existing options
|
||||
for _, property := range s.Properties {
|
||||
if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(options, property.Key.Name)
|
||||
}
|
||||
}
|
||||
|
||||
kind := protocol.CompletionItemKindProperty
|
||||
|
||||
return utils.MapMapToSlice(
|
||||
options,
|
||||
func(optionName string, value docvalues.DocumentationValue) protocol.CompletionItem {
|
||||
insertText := optionName + " = "
|
||||
|
||||
return protocol.CompletionItem{
|
||||
Kind: &kind,
|
||||
Documentation: value.Documentation,
|
||||
Label: optionName,
|
||||
InsertText: &insertText,
|
||||
}
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func GetSeparatorCompletion(property parser.WireguardProperty, character uint32) ([]protocol.CompletionItem, error) {
|
||||
var insertText string
|
||||
|
||||
if character == property.Key.Location.End {
|
||||
insertText = property.Key.Name + " = "
|
||||
} else {
|
||||
insertText = "= "
|
||||
}
|
||||
|
||||
kind := protocol.CompletionItemKindValue
|
||||
|
||||
return []protocol.CompletionItem{
|
||||
{
|
||||
Label: insertText,
|
||||
InsertText: &insertText,
|
||||
Kind: &kind,
|
||||
},
|
||||
}, parser.PropertyNotFullyTypedError{}
|
||||
}
|
||||
|
||||
func GetCompletionsForSectionPropertyLine(
|
||||
s parser.WireguardSection,
|
||||
lineNumber uint32,
|
||||
character uint32,
|
||||
) ([]protocol.CompletionItem, error) {
|
||||
property, err := s.GetPropertyByLine(lineNumber)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.Name == nil {
|
||||
return nil, parser.PropertyNotFoundError{}
|
||||
}
|
||||
|
||||
options, found := fields.OptionsHeaderMap[*s.Name]
|
||||
|
||||
if !found {
|
||||
return nil, parser.PropertyNotFoundError{}
|
||||
}
|
||||
|
||||
if property.Separator == nil {
|
||||
if _, found := options[property.Key.Name]; found {
|
||||
return GetSeparatorCompletion(*property, character)
|
||||
}
|
||||
// Get empty line completions
|
||||
return nil, parser.PropertyNotFullyTypedError{}
|
||||
}
|
||||
|
||||
option, found := options[property.Key.Name]
|
||||
|
||||
if !found {
|
||||
if character < property.Separator.Location.Start {
|
||||
return nil, parser.PropertyNotFullyTypedError{}
|
||||
} else {
|
||||
return nil, parser.PropertyNotFoundError{}
|
||||
}
|
||||
}
|
||||
|
||||
if property.Value == nil {
|
||||
if character >= property.Separator.Location.End {
|
||||
return option.FetchCompletions("", 0), nil
|
||||
}
|
||||
}
|
||||
|
||||
relativeCursor := character - property.Value.Location.Start
|
||||
|
||||
return option.FetchCompletions(property.Value.Value, relativeCursor), nil
|
||||
}
|
174
handlers/wireguard/handlers/completions_test.go
Normal file
174
handlers/wireguard/handlers/completions_test.go
Normal file
@ -0,0 +1,174 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/fields"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"config-lsp/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSimplePropertyInInterface(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0])
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
||||
}
|
||||
|
||||
if len(completions) != len(fields.InterfaceOptions) {
|
||||
t.Fatalf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleOneExistingPropertyInInterface(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
PrivateKey = 1234567890
|
||||
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0])
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
||||
}
|
||||
|
||||
expected := len(fields.InterfaceOptions) - 1
|
||||
if len(completions) != expected {
|
||||
t.Fatalf("getCompletionsForEmptyLine: Expected %v completions, but got %v", expected, len(completions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyRootCompletionsWork(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
`)
|
||||
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, _ := GetRootCompletionsForEmptyLine(p)
|
||||
|
||||
if len(completions) != 2 {
|
||||
t.Fatalf("getRootCompletionsForEmptyLine: Expected 2 completions, but got %v", len(completions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceSectionRootCompletionsBeforeWork(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
|
||||
[Interface]
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, _ := GetRootCompletionsForEmptyLine(p)
|
||||
|
||||
if len(completions) != 1 {
|
||||
t.Fatalf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceAndPeerSectionRootCompletionsWork(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
|
||||
[Peer]
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, _ := GetRootCompletionsForEmptyLine(p)
|
||||
|
||||
if len(completions) != 1 {
|
||||
t.Fatalf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertyNoSepatorShouldCompleteSeparator(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
DNS
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, err := GetCompletionsForSectionPropertyLine(*p.Sections[0], 1, 3)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("getCompletionsForPropertyLine err is nil but should not be")
|
||||
}
|
||||
|
||||
if len(completions) != 1 {
|
||||
t.Fatalf("getCompletionsForPropertyLine: Expected 1 completion, but got %v", len(completions))
|
||||
}
|
||||
|
||||
if *completions[0].InsertText != "DNS = " {
|
||||
t.Fatalf("getCompletionsForPropertyLine: Expected completion to be 'DNS = ', but got '%v'", completions[0].Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertyNoSeparatorWithSpaceShouldCompleteSeparator(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
DNS
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, err := GetCompletionsForSectionPropertyLine(*p.Sections[0], 1, 4)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("getCompletionsForPropertyLine err is nil but should not be")
|
||||
}
|
||||
|
||||
if len(completions) != 1 {
|
||||
t.Fatalf("getCompletionsForPropertyLine: Expected 1 completion, but got %v", len(completions))
|
||||
}
|
||||
|
||||
if *completions[0].InsertText != "= " {
|
||||
t.Fatalf("getCompletionsForPropertyLine: Expected completion to be '= ', but got '%v'", completions[0].Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderButNoProperty(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
|
||||
`)
|
||||
p := parser.CreateWireguardParser()
|
||||
p.ParseFromString(sample)
|
||||
|
||||
completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0])
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
||||
}
|
||||
|
||||
if len(completions) != len(fields.InterfaceOptions) {
|
||||
t.Fatalf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions))
|
||||
}
|
||||
}
|
104
handlers/wireguard/handlers/fetch-code-actions.go
Normal file
104
handlers/wireguard/handlers/fetch-code-actions.go
Normal file
@ -0,0 +1,104 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/commands"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func GetKeepaliveCodeActions(
|
||||
p *parser.WireguardParser,
|
||||
params *protocol.CodeActionParams,
|
||||
) []protocol.CodeAction {
|
||||
line := params.Range.Start.Line
|
||||
|
||||
for index, section := range p.Sections {
|
||||
if section.StartLine >= line && line <= section.EndLine && section.Name != nil && *section.Name == "Peer" {
|
||||
if section.ExistsProperty("Endpoint") && !section.ExistsProperty("PersistentKeepalive") {
|
||||
commandID := "wireguard." + CodeActionAddKeepalive
|
||||
command := protocol.Command{
|
||||
Title: "Add PersistentKeepalive",
|
||||
Command: string(commandID),
|
||||
Arguments: []any{
|
||||
CodeActionAddKeepaliveArgs{
|
||||
URI: params.TextDocument.URI,
|
||||
SectionIndex: uint32(index),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []protocol.CodeAction{
|
||||
{
|
||||
Title: "Add PersistentKeepalive",
|
||||
Command: &command,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetKeyGenerationCodeActions(
|
||||
p *parser.WireguardParser,
|
||||
params *protocol.CodeActionParams,
|
||||
) []protocol.CodeAction {
|
||||
line := params.Range.Start.Line
|
||||
section, property := p.GetPropertyByLine(line)
|
||||
|
||||
if section == nil || property == nil || property.Separator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch property.Key.Name {
|
||||
case "PrivateKey":
|
||||
if !wgcommands.AreWireguardToolsAvailable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
commandID := "wireguard." + CodeActionGeneratePrivateKey
|
||||
command := protocol.Command{
|
||||
Title: "Generate Private Key",
|
||||
Command: string(commandID),
|
||||
Arguments: []any{
|
||||
CodeActionGeneratePrivateKeyArgs{
|
||||
URI: params.TextDocument.URI,
|
||||
Line: line,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []protocol.CodeAction{
|
||||
{
|
||||
Title: "Generate Private Key",
|
||||
Command: &command,
|
||||
},
|
||||
}
|
||||
case "PresharedKey":
|
||||
if !wgcommands.AreWireguardToolsAvailable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
commandID := "wireguard." + CodeActionGeneratePresharedKey
|
||||
command := protocol.Command{
|
||||
Title: "Generate PresharedKey",
|
||||
Command: string(commandID),
|
||||
Arguments: []any{
|
||||
CodeActionGeneratePresharedKeyArgs{
|
||||
URI: params.TextDocument.URI,
|
||||
Line: line,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []protocol.CodeAction{
|
||||
{
|
||||
Title: "Generate PresharedKey",
|
||||
Command: &command,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
105
handlers/wireguard/handlers/hover.go
Normal file
105
handlers/wireguard/handlers/hover.go
Normal file
@ -0,0 +1,105 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
docvalues "config-lsp/doc-values"
|
||||
"config-lsp/handlers/wireguard/fields"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getPropertyInfo(
|
||||
p parser.WireguardProperty,
|
||||
cursor uint32,
|
||||
section parser.WireguardSection,
|
||||
) []string {
|
||||
if cursor <= p.Key.Location.End {
|
||||
options, found := fields.OptionsHeaderMap[*section.Name]
|
||||
|
||||
if !found {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
option, found := options[p.Key.Name]
|
||||
|
||||
if !found {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return strings.Split(option.Documentation, "\n")
|
||||
}
|
||||
|
||||
options, found := fields.OptionsHeaderMap[*section.Name]
|
||||
|
||||
if !found {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if option, found := options[p.Key.Name]; found {
|
||||
return option.GetTypeDescription()
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func getSectionInfo(s parser.WireguardSection) []string {
|
||||
if s.Name == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
contents := []string{
|
||||
fmt.Sprintf("## [%s]", *s.Name),
|
||||
"",
|
||||
}
|
||||
|
||||
var option *docvalues.EnumString = nil
|
||||
|
||||
switch *s.Name {
|
||||
case "Interface":
|
||||
option = &fields.HeaderInterfaceEnum
|
||||
case "Peer":
|
||||
option = &fields.HeaderPeerEnum
|
||||
}
|
||||
|
||||
if option == nil {
|
||||
return contents
|
||||
}
|
||||
|
||||
contents = append(contents, strings.Split(option.Documentation, "\n")...)
|
||||
|
||||
return contents
|
||||
}
|
||||
|
||||
func GetHoverContent(
|
||||
p parser.WireguardParser,
|
||||
line uint32,
|
||||
cursor uint32,
|
||||
) []string {
|
||||
section := p.GetSectionByLine(line)
|
||||
|
||||
if section == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
sectionInfo := getSectionInfo(*section)
|
||||
|
||||
property, _ := section.GetPropertyByLine(line)
|
||||
|
||||
if property == nil {
|
||||
return sectionInfo
|
||||
}
|
||||
|
||||
propertyInfo := getPropertyInfo(*property, cursor, *section)
|
||||
|
||||
if len(propertyInfo) == 0 {
|
||||
return sectionInfo
|
||||
}
|
||||
|
||||
contents := append(sectionInfo, []string{
|
||||
"",
|
||||
fmt.Sprintf("### %s", property.Key.Name),
|
||||
}...)
|
||||
contents = append(contents, propertyInfo...)
|
||||
|
||||
return contents
|
||||
}
|
8
handlers/wireguard/lsp/shared.go
Normal file
8
handlers/wireguard/lsp/shared.go
Normal file
@ -0,0 +1,8 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
var documentParserMap = map[protocol.DocumentUri]*parser.WireguardParser{}
|
22
handlers/wireguard/lsp/text-document-code-action.go
Normal file
22
handlers/wireguard/lsp/text-document-code-action.go
Normal file
@ -0,0 +1,22 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/handlers"
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
|
||||
p := documentParserMap[params.TextDocument.URI]
|
||||
|
||||
actions := make([]protocol.CodeAction, 0, 2)
|
||||
|
||||
actions = append(actions, handlers.GetKeyGenerationCodeActions(p, params)...)
|
||||
actions = append(actions, handlers.GetKeepaliveCodeActions(p, params)...)
|
||||
|
||||
if len(actions) > 0 {
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
60
handlers/wireguard/lsp/text-document-completion.go
Normal file
60
handlers/wireguard/lsp/text-document-completion.go
Normal file
@ -0,0 +1,60 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/handlers"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) {
|
||||
p := documentParserMap[params.TextDocument.URI]
|
||||
|
||||
lineNumber := params.Position.Line
|
||||
|
||||
section := p.GetBelongingSectionByLine(lineNumber)
|
||||
lineType := p.GetTypeByLine(lineNumber)
|
||||
|
||||
switch lineType {
|
||||
case parser.LineTypeComment:
|
||||
return nil, nil
|
||||
case parser.LineTypeHeader:
|
||||
return handlers.GetRootCompletionsForEmptyLine(*p)
|
||||
case parser.LineTypeEmpty:
|
||||
if section.Name == nil {
|
||||
// Root completions
|
||||
return handlers.GetRootCompletionsForEmptyLine(*p)
|
||||
}
|
||||
|
||||
completions, err := handlers.GetCompletionsForSectionEmptyLine(*section)
|
||||
|
||||
// === Smart rules ===
|
||||
|
||||
// If previous line is empty too, maybe new section?
|
||||
if lineNumber >= 1 && p.GetTypeByLine(lineNumber-1) == parser.LineTypeEmpty && len(p.GetBelongingSectionByLine(lineNumber).Properties) > 0 {
|
||||
rootCompletions, err := handlers.GetRootCompletionsForEmptyLine(*p)
|
||||
|
||||
if err == nil {
|
||||
completions = append(completions, rootCompletions...)
|
||||
}
|
||||
}
|
||||
|
||||
return completions, err
|
||||
case parser.LineTypeProperty:
|
||||
completions, err := handlers.GetCompletionsForSectionPropertyLine(*section, lineNumber, params.Position.Character)
|
||||
|
||||
if completions == nil && err != nil {
|
||||
switch err.(type) {
|
||||
// Ignore
|
||||
case parser.PropertyNotFullyTypedError:
|
||||
return handlers.GetCompletionsForSectionEmptyLine(*section)
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return completions, nil
|
||||
}
|
||||
|
||||
panic("TextDocumentCompletion: unexpected line type")
|
||||
}
|
41
handlers/wireguard/lsp/text-document-did-change.go
Normal file
41
handlers/wireguard/lsp/text-document-did-change.go
Normal file
@ -0,0 +1,41 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
"config-lsp/handlers/wireguard/handlers"
|
||||
"config-lsp/utils"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentDidChange(
|
||||
context *glsp.Context,
|
||||
params *protocol.DidChangeTextDocumentParams,
|
||||
) error {
|
||||
content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text
|
||||
common.ClearDiagnostics(context, params.TextDocument.URI)
|
||||
|
||||
p := documentParserMap[params.TextDocument.URI]
|
||||
p.Clear()
|
||||
|
||||
diagnostics := make([]protocol.Diagnostic, 0)
|
||||
errors := p.ParseFromString(content)
|
||||
|
||||
if len(errors) > 0 {
|
||||
diagnostics = append(diagnostics, utils.Map(
|
||||
errors,
|
||||
func(err common.ParseError) protocol.Diagnostic {
|
||||
return err.ToDiagnostic()
|
||||
},
|
||||
)...)
|
||||
}
|
||||
|
||||
diagnostics = append(diagnostics, handlers.Analyze(*p)...)
|
||||
|
||||
if len(diagnostics) > 0 {
|
||||
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
12
handlers/wireguard/lsp/text-document-did-close.go
Normal file
12
handlers/wireguard/lsp/text-document-did-close.go
Normal file
@ -0,0 +1,12 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentDidClose(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
|
||||
delete(documentParserMap, params.TextDocument.URI)
|
||||
|
||||
return nil
|
||||
}
|
35
handlers/wireguard/lsp/text-document-did-open.go
Normal file
35
handlers/wireguard/lsp/text-document-did-open.go
Normal file
@ -0,0 +1,35 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"config-lsp/utils"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentDidOpen(
|
||||
context *glsp.Context,
|
||||
params *protocol.DidOpenTextDocumentParams,
|
||||
) error {
|
||||
common.ClearDiagnostics(context, params.TextDocument.URI)
|
||||
|
||||
p := parser.CreateWireguardParser()
|
||||
documentParserMap[params.TextDocument.URI] = &p
|
||||
|
||||
errors := p.ParseFromString(params.TextDocument.Text)
|
||||
|
||||
diagnostics := utils.Map(
|
||||
errors,
|
||||
func(err common.ParseError) protocol.Diagnostic {
|
||||
return err.ToDiagnostic()
|
||||
},
|
||||
)
|
||||
|
||||
if len(diagnostics) > 0 {
|
||||
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
42
handlers/wireguard/lsp/text-document-hover.go
Normal file
42
handlers/wireguard/lsp/text-document-hover.go
Normal file
@ -0,0 +1,42 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/handlers"
|
||||
"config-lsp/handlers/wireguard/parser"
|
||||
"strings"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentHover(
|
||||
context *glsp.Context,
|
||||
params *protocol.HoverParams,
|
||||
) (*protocol.Hover, error) {
|
||||
p := documentParserMap[params.TextDocument.URI]
|
||||
|
||||
switch p.GetTypeByLine(params.Position.Line) {
|
||||
case parser.LineTypeComment:
|
||||
return nil, nil
|
||||
case parser.LineTypeEmpty:
|
||||
return nil, nil
|
||||
case parser.LineTypeHeader:
|
||||
fallthrough
|
||||
case parser.LineTypeProperty:
|
||||
documentation := handlers.GetHoverContent(
|
||||
*p,
|
||||
params.Position.Line,
|
||||
params.Position.Character,
|
||||
)
|
||||
|
||||
hover := protocol.Hover{
|
||||
Contents: protocol.MarkupContent{
|
||||
Kind: protocol.MarkupKindMarkdown,
|
||||
Value: strings.Join(documentation, "\n"),
|
||||
},
|
||||
}
|
||||
return &hover, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
36
handlers/wireguard/lsp/workspace-execute-command.go
Normal file
36
handlers/wireguard/lsp/workspace-execute-command.go
Normal file
@ -0,0 +1,36 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/wireguard/handlers"
|
||||
"strings"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func WorkspaceExecuteCommand(context *glsp.Context, params *protocol.ExecuteCommandParams) (*protocol.ApplyWorkspaceEditParams, error) {
|
||||
_, command, _ := strings.Cut(params.Command, ".")
|
||||
|
||||
switch command {
|
||||
case string(handlers.CodeActionGeneratePrivateKey):
|
||||
args := handlers.CodeActionGeneratePrivateKeyArgsFromArguments(params.Arguments[0].(map[string]any))
|
||||
|
||||
p := documentParserMap[args.URI]
|
||||
|
||||
return args.RunCommand(p)
|
||||
case string(handlers.CodeActionGeneratePresharedKey):
|
||||
args := handlers.CodeActionGeneratePresharedKeyArgsFromArguments(params.Arguments[0].(map[string]any))
|
||||
|
||||
parser := documentParserMap[args.URI]
|
||||
|
||||
return args.RunCommand(parser)
|
||||
case string(handlers.CodeActionAddKeepalive):
|
||||
args := handlers.CodeActionAddKeepaliveArgsFromArguments(params.Arguments[0].(map[string]any))
|
||||
|
||||
p := documentParserMap[args.URI]
|
||||
|
||||
return args.RunCommand(p)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
127
handlers/wireguard/parser/wg-parser-type_test.go
Normal file
127
handlers/wireguard/parser/wg-parser-type_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"config-lsp/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetLineTypeWorksCorrectly(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
# A comment at the very top
|
||||
Test=Hello
|
||||
|
||||
[Interface]
|
||||
PrivateKey = 1234567890 # Some comment
|
||||
Address = 10.0.0.1
|
||||
|
||||
|
||||
|
||||
[Peer]
|
||||
PublicKey = 1234567890
|
||||
|
||||
; I'm a comment
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
parser.ParseFromString(sample)
|
||||
|
||||
lineType := parser.GetTypeByLine(0)
|
||||
if lineType != LineTypeComment {
|
||||
t.Fatalf("getTypeByLine: Expected line 0 to be a comment, but it is %v", lineType)
|
||||
}
|
||||
|
||||
lineType = parser.GetTypeByLine(1)
|
||||
if lineType != LineTypeProperty {
|
||||
t.Fatalf("getTypeByLine: Expected line 1 to be a property, but it is %v", lineType)
|
||||
}
|
||||
|
||||
lineType = parser.GetTypeByLine(2)
|
||||
if lineType != LineTypeEmpty {
|
||||
t.Fatalf("getTypeByLine: Expected line 2 to be empty, but it is %v", lineType)
|
||||
}
|
||||
|
||||
lineType = parser.GetTypeByLine(3)
|
||||
if lineType != LineTypeHeader {
|
||||
t.Fatalf("getTypeByLine: Expected line 3 to be a header, but it is %v", lineType)
|
||||
}
|
||||
|
||||
lineType = parser.GetTypeByLine(4)
|
||||
if lineType != LineTypeProperty {
|
||||
t.Fatalf("getTypeByLine: Expected line 4 to be a property, but it is %v", lineType)
|
||||
}
|
||||
|
||||
lineType = parser.GetTypeByLine(12)
|
||||
if lineType != LineTypeComment {
|
||||
t.Fatalf("getTypeByLine: Expected line 12 to be a comment, but it is %v", lineType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBelongingSectionWorksCorrectly(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
# A comment at the very top
|
||||
Test=Hello
|
||||
|
||||
[Interface]
|
||||
PrivateKey = 1234567890 # Some comment
|
||||
Address = 10.0.0.1
|
||||
|
||||
|
||||
|
||||
[Peer]
|
||||
PublicKey = 1234567890
|
||||
|
||||
; I'm a comment
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
parser.ParseFromString(sample)
|
||||
|
||||
section := parser.GetBelongingSectionByLine(0)
|
||||
|
||||
// Comment
|
||||
if section != nil {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 0 to be in no section, but it is in %v", section)
|
||||
}
|
||||
|
||||
section = parser.GetBelongingSectionByLine(1)
|
||||
|
||||
if section != parser.Sections[1] {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 1 to be in global section, but it is in %v", section)
|
||||
}
|
||||
|
||||
section = parser.GetBelongingSectionByLine(2)
|
||||
if section != parser.Sections[1] {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 2 to be in global section, but it is in %v", section)
|
||||
}
|
||||
|
||||
section = parser.GetBelongingSectionByLine(3)
|
||||
if section != parser.Sections[2] {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 3 to be in section Interface, but it is in %v", section)
|
||||
}
|
||||
|
||||
section = parser.GetBelongingSectionByLine(4)
|
||||
if section != parser.Sections[2] {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 4 to be in section Interface, but it is in %v", section)
|
||||
}
|
||||
|
||||
section = parser.GetBelongingSectionByLine(6)
|
||||
if section != parser.Sections[2] {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 6 to be in section Interface, but it is in %v", section)
|
||||
}
|
||||
|
||||
section = parser.GetBelongingSectionByLine(10)
|
||||
if section != parser.Sections[3] {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 10 to be in section Peer, but it is in %v", section)
|
||||
}
|
||||
|
||||
section = parser.GetBelongingSectionByLine(12)
|
||||
|
||||
// Comment
|
||||
if section != nil {
|
||||
t.Fatalf("getBelongingSectionByLine: Expected line 12 to be in no section, but it is in %v", section)
|
||||
}
|
||||
}
|
328
handlers/wireguard/parser/wg-parser.go
Normal file
328
handlers/wireguard/parser/wg-parser.go
Normal file
@ -0,0 +1,328 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var commentPattern = regexp.MustCompile(`^\s*(;|#)`)
|
||||
var emptyLinePattern = regexp.MustCompile(`^\s*$`)
|
||||
var headerPattern = regexp.MustCompile(`^\s*\[`)
|
||||
|
||||
type CharacterLocation struct {
|
||||
Start uint32
|
||||
End uint32
|
||||
}
|
||||
|
||||
type wireguardLineIndex struct {
|
||||
Type LineType
|
||||
BelongingSection *WireguardSection
|
||||
}
|
||||
|
||||
type WireguardParser struct {
|
||||
// <key = name>: if nil then does not belong to a section
|
||||
Sections []*WireguardSection
|
||||
// Used to identify where not to show diagnostics
|
||||
commentLines map[uint32]struct{}
|
||||
|
||||
// Indexes
|
||||
linesIndexes map[uint32]wireguardLineIndex
|
||||
}
|
||||
|
||||
func (p *WireguardParser) Clear() {
|
||||
p.Sections = []*WireguardSection{}
|
||||
p.commentLines = map[uint32]struct{}{}
|
||||
p.linesIndexes = map[uint32]wireguardLineIndex{}
|
||||
}
|
||||
|
||||
func (p *WireguardParser) ParseFromString(input string) []common.ParseError {
|
||||
var errors []common.ParseError
|
||||
lines := strings.Split(
|
||||
input,
|
||||
"\n",
|
||||
)
|
||||
|
||||
slices.Reverse(lines)
|
||||
|
||||
collectedProperties := WireguardProperties{}
|
||||
var lastPropertyLine *uint32
|
||||
|
||||
for index, line := range lines {
|
||||
currentLineNumber := uint32(len(lines) - index - 1)
|
||||
lineType := getLineType(line)
|
||||
|
||||
switch lineType {
|
||||
case LineTypeComment:
|
||||
p.commentLines[currentLineNumber] = struct{}{}
|
||||
p.linesIndexes[currentLineNumber] = wireguardLineIndex{
|
||||
Type: LineTypeComment,
|
||||
BelongingSection: nil,
|
||||
}
|
||||
|
||||
case LineTypeEmpty:
|
||||
continue
|
||||
|
||||
case LineTypeProperty:
|
||||
err := collectedProperties.AddLine(currentLineNumber, line)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, common.ParseError{
|
||||
Line: currentLineNumber,
|
||||
Err: err,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if lastPropertyLine == nil {
|
||||
lastPropertyLine = ¤tLineNumber
|
||||
}
|
||||
|
||||
case LineTypeHeader:
|
||||
var lastLine uint32
|
||||
|
||||
if lastPropertyLine == nil {
|
||||
// Current line
|
||||
lastLine = currentLineNumber
|
||||
} else {
|
||||
lastLine = *lastPropertyLine
|
||||
}
|
||||
|
||||
section := CreateWireguardSection(
|
||||
currentLineNumber,
|
||||
lastLine,
|
||||
line,
|
||||
collectedProperties,
|
||||
)
|
||||
|
||||
p.Sections = append(p.Sections, §ion)
|
||||
|
||||
// Add indexes
|
||||
for lineNumber := range collectedProperties {
|
||||
p.linesIndexes[lineNumber] = wireguardLineIndex{
|
||||
Type: LineTypeProperty,
|
||||
BelongingSection: §ion,
|
||||
}
|
||||
}
|
||||
p.linesIndexes[currentLineNumber] = wireguardLineIndex{
|
||||
Type: LineTypeHeader,
|
||||
BelongingSection: §ion,
|
||||
}
|
||||
|
||||
// Reset
|
||||
collectedProperties = WireguardProperties{}
|
||||
lastPropertyLine = nil
|
||||
}
|
||||
}
|
||||
|
||||
var emptySection *WireguardSection
|
||||
|
||||
if len(collectedProperties) > 0 {
|
||||
var endLine uint32
|
||||
|
||||
if len(p.Sections) == 0 {
|
||||
endLine = uint32(len(lines))
|
||||
} else {
|
||||
endLine = p.Sections[len(p.Sections)-1].StartLine
|
||||
}
|
||||
|
||||
emptySection = &WireguardSection{
|
||||
StartLine: 0,
|
||||
EndLine: endLine,
|
||||
Properties: collectedProperties,
|
||||
}
|
||||
|
||||
p.Sections = append(p.Sections, emptySection)
|
||||
|
||||
for lineNumber := range collectedProperties {
|
||||
p.linesIndexes[lineNumber] = wireguardLineIndex{
|
||||
Type: LineTypeProperty,
|
||||
BelongingSection: emptySection,
|
||||
}
|
||||
}
|
||||
p.Sections = append(p.Sections, emptySection)
|
||||
} else {
|
||||
// Add empty section
|
||||
var endLine = uint32(len(lines))
|
||||
|
||||
if len(p.Sections) > 0 {
|
||||
endLine = p.Sections[len(p.Sections)-1].StartLine
|
||||
}
|
||||
|
||||
// Add empty section
|
||||
if endLine != 0 {
|
||||
emptySection = &WireguardSection{
|
||||
StartLine: 0,
|
||||
EndLine: endLine,
|
||||
Properties: collectedProperties,
|
||||
}
|
||||
|
||||
p.Sections = append(p.Sections, emptySection)
|
||||
|
||||
for newLine := uint32(0); newLine < endLine; newLine++ {
|
||||
if _, found := p.linesIndexes[newLine]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
p.linesIndexes[newLine] = wireguardLineIndex{
|
||||
Type: LineTypeEmpty,
|
||||
BelongingSection: emptySection,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since we parse the content from bottom to top, we need to reverse the sections
|
||||
// so its in correct order
|
||||
slices.Reverse(p.Sections)
|
||||
|
||||
// Fill empty lines between sections
|
||||
for lineNumber, section := range p.Sections {
|
||||
var endLine uint32
|
||||
|
||||
if len(p.Sections) > lineNumber+1 {
|
||||
nextSection := p.Sections[lineNumber+1]
|
||||
endLine = nextSection.StartLine
|
||||
} else {
|
||||
endLine = uint32(len(lines))
|
||||
}
|
||||
|
||||
for newLine := section.StartLine; newLine < endLine; newLine++ {
|
||||
if _, found := p.linesIndexes[newLine]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
p.linesIndexes[newLine] = wireguardLineIndex{
|
||||
Type: LineTypeEmpty,
|
||||
BelongingSection: section,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func (p *WireguardParser) GetSectionByLine(line uint32) *WireguardSection {
|
||||
for _, section := range p.Sections {
|
||||
if section.StartLine <= line && section.EndLine >= line {
|
||||
return section
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search for a property by name
|
||||
// Returns (line number, property)
|
||||
func (p *WireguardParser) FindFirstPropertyByName(name string) (*uint32, *WireguardProperty) {
|
||||
for _, section := range p.Sections {
|
||||
for lineNumber, property := range section.Properties {
|
||||
if property.Key.Name == name {
|
||||
return &lineNumber, &property
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p WireguardParser) GetInterfaceSection() (*WireguardSection, bool) {
|
||||
for _, section := range p.Sections {
|
||||
if section.Name != nil && *section.Name == "Interface" {
|
||||
return section, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (p WireguardParser) GetTypeByLine(line uint32) LineType {
|
||||
// Check if line is a comment
|
||||
if _, found := p.commentLines[line]; found {
|
||||
return LineTypeComment
|
||||
}
|
||||
|
||||
if info, found := p.linesIndexes[line]; found {
|
||||
return info.Type
|
||||
}
|
||||
|
||||
return LineTypeEmpty
|
||||
}
|
||||
|
||||
// Get the section that the line belongs to
|
||||
// Example:
|
||||
// [Interface]
|
||||
// Address = 10.0.0.1
|
||||
//
|
||||
// <line here>
|
||||
// [Peer]
|
||||
//
|
||||
// This would return the section [Interface]
|
||||
func (p *WireguardParser) GetBelongingSectionByLine(line uint32) *WireguardSection {
|
||||
if info, found := p.linesIndexes[line]; found {
|
||||
return info.BelongingSection
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *WireguardParser) GetPropertyByLine(line uint32) (*WireguardSection, *WireguardProperty) {
|
||||
section := p.GetSectionByLine(line)
|
||||
|
||||
if section == nil || section.Name == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
property, _ := section.GetPropertyByLine(line)
|
||||
|
||||
if property == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return section, property
|
||||
}
|
||||
|
||||
func (p *WireguardParser) GetSectionsByName(name string) []*WireguardSection {
|
||||
var sections []*WireguardSection
|
||||
|
||||
for _, section := range p.Sections {
|
||||
if section.Name != nil && *section.Name == name {
|
||||
sections = append(sections, section)
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
func CreateWireguardParser() WireguardParser {
|
||||
parser := WireguardParser{}
|
||||
parser.Clear()
|
||||
|
||||
return parser
|
||||
}
|
||||
|
||||
type LineType string
|
||||
|
||||
const (
|
||||
LineTypeComment LineType = "comment"
|
||||
LineTypeEmpty LineType = "empty"
|
||||
LineTypeHeader LineType = "header"
|
||||
LineTypeProperty LineType = "property"
|
||||
)
|
||||
|
||||
func getLineType(line string) LineType {
|
||||
if commentPattern.MatchString(line) {
|
||||
return LineTypeComment
|
||||
}
|
||||
|
||||
if emptyLinePattern.MatchString(line) {
|
||||
return LineTypeEmpty
|
||||
}
|
||||
|
||||
if headerPattern.MatchString(line) {
|
||||
return LineTypeHeader
|
||||
}
|
||||
|
||||
return LineTypeProperty
|
||||
}
|
357
handlers/wireguard/parser/wg-parser_test.go
Normal file
357
handlers/wireguard/parser/wg-parser_test.go
Normal file
@ -0,0 +1,357 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"config-lsp/utils"
|
||||
"testing"
|
||||
|
||||
"github.com/k0kubun/pp"
|
||||
)
|
||||
|
||||
func TestValidWildTestWorksFine(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
PrivateKey = 1234567890
|
||||
Address = 192.168.1.0/24
|
||||
|
||||
# I'm a comment
|
||||
[Peer]
|
||||
PublicKey = 1234567890
|
||||
Endpoint = 1.2.3.4 ; I'm just a comment
|
||||
|
||||
[Peer]
|
||||
PublicKey = 5555
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error %v", errors)
|
||||
}
|
||||
|
||||
if !(len(parser.commentLines) == 1 && utils.KeyExists(parser.commentLines, 4)) {
|
||||
t.Fatalf("parseFromString failed to collect comment lines %v", parser.commentLines)
|
||||
}
|
||||
|
||||
if !((len(parser.Sections) == 3) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer") && (*parser.Sections[2].Name == "Peer")) {
|
||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].StartLine == 0 && parser.Sections[0].EndLine == 2 && parser.Sections[1].StartLine == 5 && parser.Sections[1].EndLine == 7 && parser.Sections[2].StartLine == 9 && parser.Sections[2].EndLine == 10) {
|
||||
t.Fatalf("parseFromString: Invalid start and end lines %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !((len(parser.Sections[0].Properties) == 2) && (len(parser.Sections[1].Properties) == 2) && (len(parser.Sections[2].Properties) == 1)) {
|
||||
t.Fatalf("parseFromString: Invalid amount of properties %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !((parser.Sections[0].Properties[1].Key.Name == "PrivateKey") && (parser.Sections[0].Properties[2].Key.Name == "Address")) {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0 %v", parser.Sections[0].Properties)
|
||||
}
|
||||
|
||||
if !((parser.Sections[1].Properties[6].Key.Name == "PublicKey") && (parser.Sections[1].Properties[7].Key.Name == "Endpoint")) {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 1 %v", parser.Sections[1].Properties)
|
||||
}
|
||||
|
||||
if !(parser.Sections[2].Properties[10].Key.Name == "PublicKey") {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 2 %v", parser.Sections[2].Properties)
|
||||
}
|
||||
|
||||
// Check if line indexes are correct
|
||||
if !(parser.linesIndexes[0].Type == LineTypeHeader &&
|
||||
parser.linesIndexes[1].Type == LineTypeProperty &&
|
||||
parser.linesIndexes[2].Type == LineTypeProperty &&
|
||||
parser.linesIndexes[3].Type == LineTypeEmpty &&
|
||||
parser.linesIndexes[4].Type == LineTypeComment &&
|
||||
parser.linesIndexes[5].Type == LineTypeHeader &&
|
||||
parser.linesIndexes[6].Type == LineTypeProperty &&
|
||||
parser.linesIndexes[7].Type == LineTypeProperty &&
|
||||
parser.linesIndexes[8].Type == LineTypeEmpty &&
|
||||
parser.linesIndexes[9].Type == LineTypeHeader &&
|
||||
parser.linesIndexes[10].Type == LineTypeProperty) {
|
||||
pp.Println(parser.linesIndexes)
|
||||
t.Fatal("parseFromString: Invalid line indexes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptySectionAtStartWorksFine(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
|
||||
[Peer]
|
||||
PublicKey = 1234567890
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error %v", errors)
|
||||
}
|
||||
|
||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer")) {
|
||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) {
|
||||
t.Fatalf("parseFromString failed to collect properties %v", parser.Sections)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptySectionAtEndWorksFine(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Inteface]
|
||||
PrivateKey = 1234567890
|
||||
|
||||
[Peer]
|
||||
# Just sneaking in here, hehe
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error %v", errors)
|
||||
}
|
||||
|
||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inteface") && (*parser.Sections[1].Name == "Peer")) {
|
||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) {
|
||||
t.Fatalf("parseFromString failed to collect properties %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.commentLines) == 1 && utils.KeyExists(parser.commentLines, 4)) {
|
||||
t.Fatalf("parseFromString failed to collect comment lines %v", parser.commentLines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFileWorksFine(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error %v", errors)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections) == 1) {
|
||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialSectionWithNoPropertiesWorksFine(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Inte
|
||||
|
||||
[Peer]
|
||||
PublicKey = 1234567890
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error %v", errors)
|
||||
}
|
||||
|
||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) {
|
||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) {
|
||||
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.commentLines) == 0) {
|
||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
||||
}
|
||||
|
||||
if !(parser.Sections[1].Properties[3].Key.Name == "PublicKey") {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 1: %v", parser.Sections[1].Properties)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialSectionWithPropertiesWorksFine(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Inte
|
||||
PrivateKey = 1234567890
|
||||
|
||||
[Peer]
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
||||
}
|
||||
|
||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) {
|
||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) {
|
||||
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].Properties[1].Key.Name == "PrivateKey") {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileWithOnlyComments(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
# This is a comment
|
||||
# Another comment
|
||||
`)
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections) == 1) {
|
||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.commentLines) == 2) {
|
||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
||||
}
|
||||
|
||||
if !(utils.KeyExists(parser.commentLines, 0) && utils.KeyExists(parser.commentLines, 1)) {
|
||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleSectionsNoProperties(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
[Peer]
|
||||
[Peer]
|
||||
`)
|
||||
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections) == 3) {
|
||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
||||
}
|
||||
|
||||
for _, section := range parser.Sections {
|
||||
if len(section.Properties) != 0 {
|
||||
t.Fatalf("parseFromString failed to collect properties: %v", section.Properties)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildTest1WorksCorrectly(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
DNS=1.1.1.1
|
||||
|
||||
|
||||
`)
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections) == 1) {
|
||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
||||
}
|
||||
|
||||
if !(len(parser.Sections[0].Properties) == 1) {
|
||||
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].Properties[1].Key.Name == "DNS") {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].Properties[1].Value.Value == "1.1.1.1") {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
|
||||
if !(len(parser.commentLines) == 0) {
|
||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].StartLine == 0 && parser.Sections[0].EndLine == 1) {
|
||||
t.Fatalf("parseFromString: Invalid start and end lines %v", parser.Sections)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialKeyWorksCorrectly(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
DNS
|
||||
`)
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].Properties[1].Key.Name == "DNS") {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].Properties[1].Separator == nil) {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialValueWithSeparatorWorksCorrectly(
|
||||
t *testing.T,
|
||||
) {
|
||||
sample := utils.Dedent(`
|
||||
[Interface]
|
||||
DNS=
|
||||
`)
|
||||
parser := CreateWireguardParser()
|
||||
errors := parser.ParseFromString(sample)
|
||||
|
||||
if len(errors) > 0 {
|
||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].Properties[1].Value == nil) {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
|
||||
if !(parser.Sections[0].Properties[1].Separator != nil) {
|
||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
||||
}
|
||||
}
|
158
handlers/wireguard/parser/wg-property.go
Normal file
158
handlers/wireguard/parser/wg-property.go
Normal file
@ -0,0 +1,158 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
docvalues "config-lsp/doc-values"
|
||||
"config-lsp/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
var linePattern = regexp.MustCompile(`^\s*(?P<key>.+?)\s*(?P<separator>=)\s*(?P<value>\S.*?)?\s*(?:(?:;|#).*)?\s*$`)
|
||||
|
||||
type WireguardPropertyKey struct {
|
||||
Location CharacterLocation
|
||||
Name string
|
||||
}
|
||||
|
||||
type WireguardPropertyValue struct {
|
||||
Location CharacterLocation
|
||||
Value string
|
||||
}
|
||||
|
||||
type WireguardPropertySeparator struct {
|
||||
Location CharacterLocation
|
||||
}
|
||||
|
||||
type WireguardProperty struct {
|
||||
Key WireguardPropertyKey
|
||||
Separator *WireguardPropertySeparator
|
||||
Value *WireguardPropertyValue
|
||||
}
|
||||
|
||||
func (p WireguardProperty) String() string {
|
||||
if p.Value == nil {
|
||||
return p.Key.Name
|
||||
}
|
||||
|
||||
return p.Key.Name + "=" + p.Value.Value
|
||||
}
|
||||
|
||||
func (p WireguardProperty) GetLineRange(line uint32) protocol.Range {
|
||||
return protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: line,
|
||||
Character: p.Key.Location.Start,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: line,
|
||||
Character: p.Key.Location.End,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p WireguardProperty) GetInsertRange(line uint32) protocol.Range {
|
||||
var insertPosition uint32 = p.Separator.Location.End
|
||||
var length uint32 = 0
|
||||
|
||||
if p.Value != nil {
|
||||
insertPosition = p.Value.Location.Start - 1
|
||||
// Length of the value; +1 because of the starting space
|
||||
length = (p.Value.Location.End - p.Value.Location.Start) + 1
|
||||
}
|
||||
|
||||
return protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: line,
|
||||
Character: insertPosition,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: line,
|
||||
Character: insertPosition + length,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WireguardProperties [<line number>]: <property>
|
||||
type WireguardProperties map[uint32]WireguardProperty
|
||||
|
||||
func (p *WireguardProperties) AddLine(lineNumber uint32, line string) error {
|
||||
property, err := CreateWireguardProperty(line)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
(*p)[lineNumber] = *property
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateWireguardProperty(line string) (*WireguardProperty, error) {
|
||||
if !strings.Contains(line, "=") {
|
||||
indexes := utils.GetTrimIndex(line)
|
||||
|
||||
if indexes == nil {
|
||||
// weird, should not happen
|
||||
return nil, &docvalues.MalformedLineError{}
|
||||
}
|
||||
|
||||
return &WireguardProperty{
|
||||
Key: WireguardPropertyKey{
|
||||
Name: line[indexes[0]:indexes[1]],
|
||||
Location: CharacterLocation{
|
||||
Start: uint32(indexes[0]),
|
||||
End: uint32(indexes[1]),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
indexes := linePattern.FindStringSubmatchIndex(line)
|
||||
|
||||
if indexes == nil || len(indexes) == 0 {
|
||||
return nil, &docvalues.MalformedLineError{}
|
||||
}
|
||||
|
||||
keyStart := uint32(indexes[2])
|
||||
keyEnd := uint32(indexes[3])
|
||||
key := WireguardPropertyKey{
|
||||
Location: CharacterLocation{
|
||||
Start: keyStart,
|
||||
End: keyEnd,
|
||||
},
|
||||
Name: line[keyStart:keyEnd],
|
||||
}
|
||||
|
||||
separatorStart := uint32(indexes[4])
|
||||
separatorEnd := uint32(indexes[5])
|
||||
separator := WireguardPropertySeparator{
|
||||
Location: CharacterLocation{
|
||||
Start: separatorStart,
|
||||
End: separatorEnd,
|
||||
},
|
||||
}
|
||||
|
||||
var value *WireguardPropertyValue
|
||||
|
||||
if indexes[6] != -1 && indexes[7] != -1 {
|
||||
// value exists
|
||||
valueStart := uint32(indexes[6])
|
||||
valueEnd := uint32(indexes[7])
|
||||
|
||||
value = &WireguardPropertyValue{
|
||||
Location: CharacterLocation{
|
||||
Start: valueStart,
|
||||
End: valueEnd,
|
||||
},
|
||||
Value: line[valueStart:valueEnd],
|
||||
}
|
||||
}
|
||||
|
||||
return &WireguardProperty{
|
||||
Key: key,
|
||||
Separator: &separator,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
120
handlers/wireguard/parser/wg-section.go
Normal file
120
handlers/wireguard/parser/wg-section.go
Normal file
@ -0,0 +1,120 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
type PropertyNotFoundError struct{}
|
||||
|
||||
func (e PropertyNotFoundError) Error() string {
|
||||
return "Property not found"
|
||||
}
|
||||
|
||||
type PropertyNotFullyTypedError struct{}
|
||||
|
||||
func (e PropertyNotFullyTypedError) Error() string {
|
||||
return "Property not fully typed"
|
||||
}
|
||||
|
||||
type WireguardSection struct {
|
||||
Name *string
|
||||
StartLine uint32
|
||||
EndLine uint32
|
||||
Properties WireguardProperties
|
||||
}
|
||||
|
||||
func (s WireguardSection) String() string {
|
||||
var name string
|
||||
|
||||
if s.Name == nil {
|
||||
name = "<nil>"
|
||||
} else {
|
||||
name = *s.Name
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s]; %d-%d: %v", name, s.StartLine, s.EndLine, s.Properties)
|
||||
}
|
||||
|
||||
func (s WireguardSection) GetHeaderLineRange() protocol.Range {
|
||||
return protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: s.StartLine,
|
||||
Character: 0,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: s.StartLine,
|
||||
Character: 99999999,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s WireguardSection) GetRange() protocol.Range {
|
||||
return protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: s.StartLine,
|
||||
Character: 0,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: s.EndLine,
|
||||
Character: 99999999,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s WireguardSection) FetchFirstProperty(name string) (*uint32, *WireguardProperty) {
|
||||
for line, property := range s.Properties {
|
||||
if property.Key.Name == name {
|
||||
return &line, &property
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s WireguardSection) ExistsProperty(name string) bool {
|
||||
_, property := s.FetchFirstProperty(name)
|
||||
|
||||
return property != nil
|
||||
}
|
||||
|
||||
func (s WireguardSection) GetPropertyByLine(lineNumber uint32) (*WireguardProperty, error) {
|
||||
property, found := s.Properties[lineNumber]
|
||||
|
||||
if !found {
|
||||
return nil, PropertyNotFoundError{}
|
||||
}
|
||||
|
||||
return &property, nil
|
||||
}
|
||||
|
||||
var validHeaderPattern = regexp.MustCompile(`^\s*\[(?P<header>.+?)\]\s*$`)
|
||||
|
||||
// Create a new create section
|
||||
// Return (<name>, <new section>)
|
||||
func CreateWireguardSection(
|
||||
startLine uint32,
|
||||
endLine uint32,
|
||||
headerLine string,
|
||||
props WireguardProperties,
|
||||
) WireguardSection {
|
||||
match := validHeaderPattern.FindStringSubmatch(headerLine)
|
||||
|
||||
var header string
|
||||
|
||||
if match == nil {
|
||||
// Still typing it
|
||||
header = headerLine[1:]
|
||||
} else {
|
||||
header = match[1]
|
||||
}
|
||||
|
||||
return WireguardSection{
|
||||
Name: &header,
|
||||
StartLine: startLine,
|
||||
EndLine: endLine,
|
||||
Properties: props,
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
vim.lsp.start {
|
||||
name = "config-lsp",
|
||||
cmd = { "./bin/config-lsp" },
|
||||
cmd = { "./result/bin/config-lsp" },
|
||||
root_dir = vim.fn.getcwd(),
|
||||
};
|
||||
|
@ -17,14 +17,17 @@ var lspHandler protocol.Handler
|
||||
func SetUpRootHandler() {
|
||||
rootHandler = NewRootHandler()
|
||||
lspHandler = protocol.Handler{
|
||||
Initialize: initialize,
|
||||
Initialized: initialized,
|
||||
Shutdown: shutdown,
|
||||
SetTrace: setTrace,
|
||||
TextDocumentDidOpen: TextDocumentDidOpen,
|
||||
TextDocumentDidChange: TextDocumentDidChange,
|
||||
TextDocumentCompletion: TextDocumentCompletion,
|
||||
TextDocumentHover: TextDocumentHover,
|
||||
Initialize: initialize,
|
||||
Initialized: initialized,
|
||||
Shutdown: shutdown,
|
||||
SetTrace: setTrace,
|
||||
TextDocumentDidOpen: TextDocumentDidOpen,
|
||||
TextDocumentDidChange: TextDocumentDidChange,
|
||||
TextDocumentCompletion: TextDocumentCompletion,
|
||||
TextDocumentHover: TextDocumentHover,
|
||||
TextDocumentDidClose: TextDocumentDidClose,
|
||||
TextDocumentCodeAction: TextDocumentCodeAction,
|
||||
WorkspaceExecuteCommand: WorkspaceExecuteCommand,
|
||||
}
|
||||
|
||||
server := server.NewServer(&lspHandler, lsName, false)
|
||||
|
@ -15,11 +15,13 @@ type SupportedLanguage string
|
||||
const (
|
||||
LanguageSSHDConfig SupportedLanguage = "sshd_config"
|
||||
LanguageFstab SupportedLanguage = "fstab"
|
||||
LanguageWireguard SupportedLanguage = "languagewireguard"
|
||||
)
|
||||
|
||||
var AllSupportedLanguages = []string{
|
||||
string(LanguageSSHDConfig),
|
||||
string(LanguageFstab),
|
||||
string(LanguageWireguard),
|
||||
}
|
||||
|
||||
type FatalFileNotReadableError struct {
|
||||
@ -53,9 +55,14 @@ var valueToLanguageMap = map[string]SupportedLanguage{
|
||||
|
||||
"fstab": LanguageFstab,
|
||||
"etc/fstab": LanguageFstab,
|
||||
|
||||
"wireguard": LanguageWireguard,
|
||||
"wg": LanguageWireguard,
|
||||
"languagewireguard": LanguageWireguard,
|
||||
}
|
||||
|
||||
var typeOverwriteRegex = regexp.MustCompile(`^#\?\s*lsp\.language\s*=\s*(\w+)\s*$`)
|
||||
var wireguardPattern = regexp.MustCompile(`/wg\d+\.conf$`)
|
||||
|
||||
func DetectLanguage(
|
||||
content string,
|
||||
@ -94,6 +101,10 @@ func DetectLanguage(
|
||||
return LanguageFstab, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(uri, "file:///etc/wireguard/") || wireguardPattern.MatchString(uri) {
|
||||
return LanguageWireguard, nil
|
||||
}
|
||||
|
||||
return "", common.ParseError{
|
||||
Line: 0,
|
||||
Err: LanguageUndetectableError{},
|
||||
|
5
root-handler/shared.go
Normal file
5
root-handler/shared.go
Normal file
@ -0,0 +1,5 @@
|
||||
package roothandler
|
||||
|
||||
import protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
|
||||
var openedFiles = make(map[protocol.DocumentUri]struct{})
|
22
root-handler/text-document-code-action.go
Normal file
22
root-handler/text-document-code-action.go
Normal file
@ -0,0 +1,22 @@
|
||||
package roothandler
|
||||
|
||||
import (
|
||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) (any, error) {
|
||||
language := rootHandler.GetLanguageForDocument(params.TextDocument.URI)
|
||||
|
||||
switch language {
|
||||
case LanguageFstab:
|
||||
return nil, nil
|
||||
case LanguageSSHDConfig:
|
||||
return nil, nil
|
||||
case LanguageWireguard:
|
||||
return wireguard.TextDocumentCodeAction(context, params)
|
||||
}
|
||||
|
||||
panic("root-handler/TextDocumentCompletion: unexpected language" + language)
|
||||
}
|
@ -2,6 +2,7 @@ package roothandler
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/fstab"
|
||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
@ -15,6 +16,8 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa
|
||||
return fstab.TextDocumentCompletion(context, params)
|
||||
case LanguageSSHDConfig:
|
||||
return nil, nil
|
||||
case LanguageWireguard:
|
||||
return wireguard.TextDocumentCompletion(context, params)
|
||||
}
|
||||
|
||||
panic("root-handler/TextDocumentCompletion: unexpected language" + language)
|
||||
|
@ -2,6 +2,7 @@ package roothandler
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/fstab"
|
||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
@ -15,6 +16,8 @@ func TextDocumentDidChange(context *glsp.Context, params *protocol.DidChangeText
|
||||
return fstab.TextDocumentDidChange(context, params)
|
||||
case LanguageSSHDConfig:
|
||||
return nil
|
||||
case LanguageWireguard:
|
||||
return wireguard.TextDocumentDidChange(context, params)
|
||||
}
|
||||
|
||||
panic("root-handler/TextDocumentDidChange: unexpected language" + language)
|
||||
|
25
root-handler/text-document-did-close.go
Normal file
25
root-handler/text-document-did-close.go
Normal file
@ -0,0 +1,25 @@
|
||||
package roothandler
|
||||
|
||||
import (
|
||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func TextDocumentDidClose(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
|
||||
language := rootHandler.GetLanguageForDocument(params.TextDocument.URI)
|
||||
|
||||
delete(openedFiles, params.TextDocument.URI)
|
||||
rootHandler.RemoveDocument(params.TextDocument.URI)
|
||||
|
||||
switch language {
|
||||
case LanguageFstab:
|
||||
case LanguageSSHDConfig:
|
||||
case LanguageWireguard:
|
||||
return wireguard.TextDocumentDidClose(context, params)
|
||||
default:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -3,6 +3,7 @@ package roothandler
|
||||
import (
|
||||
"config-lsp/common"
|
||||
fstab "config-lsp/handlers/fstab"
|
||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||
"fmt"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
@ -27,6 +28,8 @@ func TextDocumentDidOpen(context *glsp.Context, params *protocol.DidOpenTextDocu
|
||||
return parseError.Err
|
||||
}
|
||||
|
||||
openedFiles[params.TextDocument.URI] = struct{}{}
|
||||
|
||||
// Everything okay, now we can handle the file
|
||||
rootHandler.AddDocument(params.TextDocument.URI, language)
|
||||
|
||||
@ -34,11 +37,12 @@ func TextDocumentDidOpen(context *glsp.Context, params *protocol.DidOpenTextDocu
|
||||
case LanguageFstab:
|
||||
return fstab.TextDocumentDidOpen(context, params)
|
||||
case LanguageSSHDConfig:
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected roothandler.SupportedLanguage: %#v", language))
|
||||
break
|
||||
case LanguageWireguard:
|
||||
return wireguard.TextDocumentDidOpen(context, params)
|
||||
}
|
||||
|
||||
return nil
|
||||
panic(fmt.Sprintf("unexpected roothandler.SupportedLanguage: %#v", language))
|
||||
}
|
||||
|
||||
func showParseError(
|
||||
|
@ -2,6 +2,7 @@ package roothandler
|
||||
|
||||
import (
|
||||
"config-lsp/handlers/fstab"
|
||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
@ -15,6 +16,8 @@ func TextDocumentHover(context *glsp.Context, params *protocol.HoverParams) (*pr
|
||||
return fstab.TextDocumentHover(context, params)
|
||||
case LanguageSSHDConfig:
|
||||
return nil, nil
|
||||
case LanguageWireguard:
|
||||
return wireguard.TextDocumentHover(context, params)
|
||||
}
|
||||
|
||||
panic("root-handler/TextDocumentHover: unexpected language" + language)
|
||||
|
32
root-handler/workspace-execute-command.go
Normal file
32
root-handler/workspace-execute-command.go
Normal file
@ -0,0 +1,32 @@
|
||||
package roothandler
|
||||
|
||||
import (
|
||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||
"strings"
|
||||
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
func WorkspaceExecuteCommand(context *glsp.Context, params *protocol.ExecuteCommandParams) (any, error) {
|
||||
commandSection, _, _ := strings.Cut(params.Command, ".")
|
||||
|
||||
var edit *protocol.ApplyWorkspaceEditParams
|
||||
var err error
|
||||
|
||||
switch commandSection {
|
||||
case "wireguard":
|
||||
edit, err = wireguard.WorkspaceExecuteCommand(context, params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
context.Notify(
|
||||
"workspace/applyEdit",
|
||||
edit,
|
||||
)
|
||||
|
||||
return nil, nil
|
||||
}
|
@ -29,6 +29,26 @@ func Map[T any, O any](values []T, f func(T) O) []O {
|
||||
return result
|
||||
}
|
||||
|
||||
func MapMap[T comparable, O any, P any](values map[T]O, f func(T, O) P) map[T]P {
|
||||
result := make(map[T]P)
|
||||
|
||||
for key, value := range values {
|
||||
result[key] = f(key, value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func MapMapToSlice[T comparable, O any, P any](values map[T]O, f func(T, O) P) []P {
|
||||
result := make([]P, 0)
|
||||
|
||||
for key, value := range values {
|
||||
result = append(result, f(key, value))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func SliceToSet[T comparable](values []T) map[T]struct{} {
|
||||
set := make(map[T]struct{})
|
||||
|
||||
@ -108,6 +128,16 @@ func KeysOfMap[T comparable, O any](values map[T]O) []T {
|
||||
return keys
|
||||
}
|
||||
|
||||
func ValuesOfMap[T comparable, O any](values map[T]O) []O {
|
||||
keys := make([]O, 0)
|
||||
|
||||
for _, value := range values {
|
||||
keys = append(keys, value)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func DoesPathExist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
|
||||
|
144
utils/ip-host.go
Normal file
144
utils/ip-host.go
Normal file
@ -0,0 +1,144 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
net "net/netip"
|
||||
)
|
||||
|
||||
type iPv4Tree struct {
|
||||
TrueNode *iPv4Tree
|
||||
FalseNode *iPv4Tree
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func (t *iPv4Tree) addHostBits(
|
||||
hostBits []bool,
|
||||
ctx context.Context,
|
||||
) {
|
||||
if len(hostBits) == 0 {
|
||||
t.Context = ctx
|
||||
return
|
||||
}
|
||||
|
||||
if hostBits[0] {
|
||||
if t.TrueNode == nil {
|
||||
t.TrueNode = &iPv4Tree{}
|
||||
}
|
||||
t.TrueNode.addHostBits(hostBits[1:], ctx)
|
||||
} else {
|
||||
if t.FalseNode == nil {
|
||||
t.FalseNode = &iPv4Tree{}
|
||||
}
|
||||
t.FalseNode.addHostBits(hostBits[1:], ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *iPv4Tree) getFromHostBits(hostBits []bool) *context.Context {
|
||||
if t.Context != nil || len(hostBits) == 0 {
|
||||
return &t.Context
|
||||
}
|
||||
|
||||
if hostBits[0] {
|
||||
if t.TrueNode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.TrueNode.getFromHostBits(hostBits[1:])
|
||||
} else {
|
||||
if t.FalseNode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.FalseNode.getFromHostBits(hostBits[1:])
|
||||
}
|
||||
}
|
||||
|
||||
func createIPv4Tree(
|
||||
hostBits []bool,
|
||||
ctx context.Context,
|
||||
) iPv4Tree {
|
||||
tree := iPv4Tree{}
|
||||
tree.addHostBits(hostBits, ctx)
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
type IPv4HostSet struct {
|
||||
tree iPv4Tree
|
||||
}
|
||||
|
||||
func CreateIPv4HostSet() IPv4HostSet {
|
||||
return IPv4HostSet{
|
||||
tree: iPv4Tree{},
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new ip to the host set
|
||||
// `hostAmount`: Amount of host bits
|
||||
// Return: (<Whether the ip has been added>, <error>)
|
||||
func (h *IPv4HostSet) AddIP(
|
||||
ip net.Prefix,
|
||||
ctx context.Context,
|
||||
) (bool, error) {
|
||||
hostBits, err := ipToHostBits(ip)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if h.tree.getFromHostBits(hostBits) != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
h.tree.addHostBits(hostBits, ctx)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (h IPv4HostSet) ContainsIP(
|
||||
ip net.Prefix,
|
||||
) (*context.Context, error) {
|
||||
hostBits, err := ipToHostBits(ip)
|
||||
|
||||
if err != nil {
|
||||
ctx := context.Background()
|
||||
return &ctx, err
|
||||
}
|
||||
|
||||
ctx := h.tree.getFromHostBits(hostBits)
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func ipToHostBits(ip net.Prefix) ([]bool, error) {
|
||||
if !ip.Addr().Is4() {
|
||||
return nil, errors.New("Only IPv4 is supported currently")
|
||||
}
|
||||
|
||||
ipv4 := ip.Addr().As4()
|
||||
allHostBits := [32]bool{}
|
||||
for i, b := range ipv4 {
|
||||
bits := byteToBits(b)
|
||||
for j, bit := range bits {
|
||||
allHostBits[i*8+j] = bit
|
||||
}
|
||||
}
|
||||
|
||||
hostBits := allHostBits[:ip.Bits()]
|
||||
|
||||
return hostBits, nil
|
||||
}
|
||||
|
||||
func byteToBits(b byte) [8]bool {
|
||||
return [8]bool{
|
||||
(b>>0)&1 != 0,
|
||||
(b>>1)&1 != 0,
|
||||
(b>>2)&1 != 0,
|
||||
(b>>3)&1 != 0,
|
||||
(b>>4)&1 != 0,
|
||||
(b>>5)&1 != 0,
|
||||
(b>>6)&1 != 0,
|
||||
(b>>7)&1 != 0,
|
||||
}
|
||||
}
|
68
utils/ip-host_test.go
Normal file
68
utils/ip-host_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFullHostIpAddresses(t *testing.T) {
|
||||
// Test the full host IP address
|
||||
hostSet := CreateIPv4HostSet()
|
||||
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.1/32"), context.Background())
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.2/32"), context.Background())
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.3/32"), context.Background())
|
||||
|
||||
if ctx, _ := hostSet.ContainsIP(netip.MustParsePrefix("10.0.0.1/32")); ctx == nil {
|
||||
t.Fatalf("Expected to find 10.0.0.1/32 in the host set")
|
||||
}
|
||||
|
||||
if ctx, _ := hostSet.ContainsIP(netip.MustParsePrefix("10.0.0.5/32")); ctx != nil {
|
||||
t.Fatalf("Expected NOT to find 10.0.0.5/32 in the host set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialHostIpAddresses(t *testing.T) {
|
||||
// Test the partial host IP address
|
||||
hostSet := CreateIPv4HostSet()
|
||||
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.1/32"), context.Background())
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.2/32"), context.Background())
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.3/32"), context.Background())
|
||||
|
||||
if ctx, _ := hostSet.ContainsIP(netip.MustParsePrefix("10.0.0.1/16")); ctx == nil {
|
||||
t.Fatalf("Expected to find 10.0.0.1/16 in the host set")
|
||||
}
|
||||
|
||||
if ctx, _ := hostSet.ContainsIP(netip.MustParsePrefix("192.168.0.1/16")); ctx != nil {
|
||||
t.Fatalf("Expected NOT to find 192.168.0.1/16 in the host set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixedHostIpAddresses(t *testing.T) {
|
||||
// Test the mixed host IP address
|
||||
hostSet := CreateIPv4HostSet()
|
||||
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.1/16"), context.Background())
|
||||
hostSet.AddIP(netip.MustParsePrefix("192.168.0.1/32"), context.Background())
|
||||
|
||||
if ctx, _ := hostSet.ContainsIP(netip.MustParsePrefix("10.0.0.2/32")); ctx == nil {
|
||||
t.Fatalf("Expected to find 10.0.0.3/32 in the host set")
|
||||
}
|
||||
|
||||
if ctx, _ := hostSet.ContainsIP(netip.MustParsePrefix("192.168.0.2/32")); ctx != nil {
|
||||
t.Fatalf("Expected NOT to find 192.168.0.2/32 in the host set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleExactCheck(t *testing.T) {
|
||||
// Test the real example
|
||||
hostSet := CreateIPv4HostSet()
|
||||
|
||||
hostSet.AddIP(netip.MustParsePrefix("10.0.0.1/16"), context.Background())
|
||||
|
||||
if ctx, _ := hostSet.ContainsIP(netip.MustParsePrefix("10.0.0.1/16")); ctx == nil {
|
||||
t.Fatalf("Expected to find 10.0.0.1/16 in the host set")
|
||||
}
|
||||
}
|
@ -1,7 +1,17 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func IndexOffset(s string, search string, start int) int {
|
||||
return strings.Index(s[start:], search) + start
|
||||
var trimIndexPattern = regexp.MustCompile(`^\s*(.+?)\s*$`)
|
||||
|
||||
func GetTrimIndex(s string) []int {
|
||||
indexes := trimIndexPattern.FindStringSubmatchIndex(s)
|
||||
|
||||
if indexes == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return indexes[2:4]
|
||||
}
|
||||
|
13
utils/tests.go
Normal file
13
utils/tests.go
Normal file
@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
func Dedent(s string) string {
|
||||
return strings.TrimLeft(s, "\n")
|
||||
}
|
||||
|
||||
func KeyExists[T comparable, V any](keys map[T]V, key T) bool {
|
||||
_, found := keys[key]
|
||||
|
||||
return found
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user