diff --git a/handlers/wireguard/errors.go b/handlers/wireguard/errors.go new file mode 100644 index 0000000..324835d --- /dev/null +++ b/handlers/wireguard/errors.go @@ -0,0 +1,12 @@ +package wireguard + +type malformedLineError struct{} + +func (e *malformedLineError) Error() string { + return "Malformed line" +} + +type lineError struct { + Line uint32 + Err error +} diff --git a/handlers/wireguard/parser.go b/handlers/wireguard/parser.go new file mode 100644 index 0000000..f91b1ea --- /dev/null +++ b/handlers/wireguard/parser.go @@ -0,0 +1,117 @@ +package wireguard + +import ( + "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 wireguardParser struct { + Sections []wireguardSection + // Used to identify where not to show diagnostics + CommentLines []uint32 +} + +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 +} + +func (p *wireguardParser) parseFromString(input string) []lineError { + errors := []lineError{} + 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 = append(p.CommentLines, currentLineNumber) + + case LineTypeEmpty: + continue + + case LineTypeProperty: + err := collectedProperties.AddLine(currentLineNumber, line) + + if err != nil { + errors = append(errors, lineError{ + 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, section) + + // Reset + collectedProperties = wireguardProperties{} + lastPropertyLine = nil + } + } + + // Since we parse the content from bottom to top, + // we need to reverse the order + slices.Reverse(p.CommentLines) + slices.Reverse(p.Sections) + + return errors +} diff --git a/handlers/wireguard/parser_test.go b/handlers/wireguard/parser_test.go new file mode 100644 index 0000000..713804c --- /dev/null +++ b/handlers/wireguard/parser_test.go @@ -0,0 +1,255 @@ +package wireguard + +import ( + "strings" + "testing" +) + +func dedent(s string) string { + return strings.TrimLeft(s, "\n") +} + +func TestValidWildTestWorksFine( + t *testing.T, +) { + sample := 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 := wireguardParser{} + errors := parser.parseFromString(sample) + + if len(errors) > 0 { + t.Fatalf("parseFromString failed with error %v", errors) + } + + if !(len(parser.CommentLines) == 1 && parser.CommentLines[0] == 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) + } +} + +func TestEmptySectionAtStartWorksFine( + t *testing.T, +) { + sample := dedent(` +[Interface] + +[Peer] +PublicKey = 1234567890 +`) + + parser := wireguardParser{} + 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 := dedent(` +[Inteface] +PrivateKey = 1234567890 + +[Peer] +# Just sneaking in here, hehe +`) + + parser := wireguardParser{} + 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 && parser.CommentLines[0] == 4) { + t.Fatalf("parseFromString failed to collect comment lines %v", parser.CommentLines) + } +} + +func TestEmptyFileWorksFine( + t *testing.T, +) { + sample := dedent(` +`) + + parser := wireguardParser{} + errors := parser.parseFromString(sample) + + if len(errors) > 0 { + t.Fatalf("parseFromString failed with error %v", errors) + } + + if !(len(parser.Sections) == 0) { + t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) + } +} + +func TestPartialSectionWithNoPropertiesWorksFine( + t *testing.T, +) { + sample := dedent(` +[Inte + +[Peer] +PublicKey = 1234567890 +`) + + parser := wireguardParser{} + 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 := dedent(` +[Inte +PrivateKey = 1234567890 + +[Peer] +`) + + parser := wireguardParser{} + 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 := dedent(` +# This is a comment +# Another comment +`) + parser := wireguardParser{} + errors := parser.parseFromString(sample) + + if len(errors) > 0 { + t.Fatalf("parseFromString failed with error: %v", errors) + } + + if !(len(parser.Sections) == 0) { + 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 !(parser.CommentLines[0] == 0 && parser.CommentLines[1] == 1) { + t.Fatalf("parseFromString failed to collect comment lines: %v", parser.CommentLines) + } +} + +func TestMultipleSectionsNoProperties( + t *testing.T, +) { + sample := dedent(` +[Interface] +[Peer] +[Peer] +`) + parser := wireguardParser{} + 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) + } + } +} diff --git a/handlers/wireguard/wg-property.go b/handlers/wireguard/wg-property.go new file mode 100644 index 0000000..6b49dbd --- /dev/null +++ b/handlers/wireguard/wg-property.go @@ -0,0 +1,119 @@ +package wireguard + +import ( + "config-lsp/utils" + "regexp" + "strings" +) + +var linePattern = regexp.MustCompile(`^\s*(?P.+?)\s*(?P=)\s*(?P\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 createWireguardProperty(line string) (*wireguardProperty, error) { + if !strings.Contains(line, "=") { + indexes := utils.GetTrimIndex(line) + + if indexes == nil { + // weird, should not happen + return nil, &malformedLineError{} + } + + return &wireguardProperty{ + Key: wireguardPropertyKey{ + Location: characterLocation{ + Start: uint32(indexes[0]), + End: uint32(indexes[1]), + }, + }, + }, nil + } + + indexes := linePattern.FindStringSubmatchIndex(line) + + if indexes == nil || len(indexes) == 0 { + return nil, &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 len(indexes) > 6 { + // 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 +} + +// []: +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 +} diff --git a/handlers/wireguard/wg-section.go b/handlers/wireguard/wg-section.go new file mode 100644 index 0000000..52b34bc --- /dev/null +++ b/handlers/wireguard/wg-section.go @@ -0,0 +1,48 @@ +package wireguard + +import ( + "fmt" + "regexp" +) + +type wireguardSection struct { + StartLine uint32 + EndLine uint32 + // nil = do not belong to a section + Name *string + Properties wireguardProperties +} + +func (s wireguardSection) String() string { + var name string + + if s.Name == nil { + name = "////" + } else { + name = *s.Name + } + + return fmt.Sprintf("[%s]; %d-%d: %v", name, s.StartLine, s.EndLine, s.Properties) +} + +var validHeaderPattern = regexp.MustCompile(`^\s*\[(?P
.+?)\]\s*$`) + +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{ + StartLine: startLine, + EndLine: endLine, + Name: &header, + Properties: props, + } +}