CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 23s
CI / web (push) Failing after 34s
CI / go (push) Failing after 41s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
Уточнение godoc ExtractCIDRs; тесты repository (interface + optional integration). Co-authored-by: Cursor <cursoragent@cursor.com>
176 lines
4.0 KiB
Go
176 lines
4.0 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
)
|
|
|
|
// Защита от pathological JSON: глубокая рекурсия при обходе и взрыв числа узлов по пути.
|
|
const (
|
|
maxJSONWalkDepth = 512
|
|
maxJSONPathBreadth = 50000
|
|
)
|
|
|
|
// ParseCIDRLines extracts unique IPv4/IPv6 CIDRs from plain text (one per line, # comments, empty lines skipped).
|
|
func ParseCIDRLines(body string) []netip.Prefix {
|
|
seen := make(map[string]struct{})
|
|
var out []netip.Prefix
|
|
sc := bufio.NewScanner(strings.NewReader(body))
|
|
for sc.Scan() {
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
pfx := parseOneCIDR(line)
|
|
if !pfx.IsValid() {
|
|
continue
|
|
}
|
|
m := pfx.Masked()
|
|
s := m.String()
|
|
if _, ok := seen[s]; ok {
|
|
continue
|
|
}
|
|
seen[s] = struct{}{}
|
|
out = append(out, m)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ExtractCIDRs parses CIDR prefixes from plaintext lines or a JSON payload (see sourceKind and prefixPath).
|
|
// For sourceKind="json", prefixPath supports dotted traversal, with [] for arrays:
|
|
// e.g. "prefixes[]", "data.items[].cidr".
|
|
func ExtractCIDRs(body, sourceKind, prefixPath string) ([]netip.Prefix, error) {
|
|
if strings.EqualFold(strings.TrimSpace(sourceKind), "json") {
|
|
return parseCIDRsFromJSON(body, prefixPath)
|
|
}
|
|
return ParseCIDRLines(body), nil
|
|
}
|
|
|
|
func parseCIDRsFromJSON(body, prefixPath string) ([]netip.Prefix, error) {
|
|
var root any
|
|
if err := json.Unmarshal([]byte(body), &root); err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := jsonValuesAtPath(root, prefixPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seen := make(map[string]struct{})
|
|
var out []netip.Prefix
|
|
for _, raw := range values {
|
|
pfx := parseOneCIDR(raw)
|
|
if !pfx.IsValid() {
|
|
continue
|
|
}
|
|
m := pfx.Masked()
|
|
s := m.String()
|
|
if _, ok := seen[s]; ok {
|
|
continue
|
|
}
|
|
seen[s] = struct{}{}
|
|
out = append(out, m)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func jsonValuesAtPath(root any, prefixPath string) ([]string, error) {
|
|
path := strings.TrimSpace(prefixPath)
|
|
if path == "" {
|
|
return flattenJSONStrings(root, 0)
|
|
}
|
|
parts := strings.Split(path, ".")
|
|
nodes := []any{root}
|
|
for _, p := range parts {
|
|
part := strings.TrimSpace(p)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
iter := strings.HasSuffix(part, "[]")
|
|
key := strings.TrimSuffix(part, "[]")
|
|
var next []any
|
|
for _, n := range nodes {
|
|
obj, ok := n.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
child, ok := obj[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if iter {
|
|
if arr, ok := child.([]any); ok {
|
|
next = append(next, arr...)
|
|
}
|
|
continue
|
|
}
|
|
next = append(next, child)
|
|
}
|
|
if len(next) > maxJSONPathBreadth {
|
|
return nil, fmt.Errorf("json path: слишком много узлов на шаге (>%d)", maxJSONPathBreadth)
|
|
}
|
|
nodes = next
|
|
if len(nodes) == 0 {
|
|
return nil, nil
|
|
}
|
|
}
|
|
var out []string
|
|
for _, n := range nodes {
|
|
part, err := flattenJSONStrings(n, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, part...)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func flattenJSONStrings(v any, depth int) ([]string, error) {
|
|
if depth > maxJSONWalkDepth {
|
|
return nil, fmt.Errorf("json: глубина вложенности превышает %d", maxJSONWalkDepth)
|
|
}
|
|
switch x := v.(type) {
|
|
case string:
|
|
return []string{strings.TrimSpace(x)}, nil
|
|
case []any:
|
|
var out []string
|
|
for _, item := range x {
|
|
part, err := flattenJSONStrings(item, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, part...)
|
|
}
|
|
return out, nil
|
|
case map[string]any:
|
|
var out []string
|
|
for _, item := range x {
|
|
part, err := flattenJSONStrings(item, depth+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, part...)
|
|
}
|
|
return out, nil
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func parseOneCIDR(s string) netip.Prefix {
|
|
if p, err := netip.ParsePrefix(s); err == nil {
|
|
return p
|
|
}
|
|
if addr, err := netip.ParseAddr(s); err == nil {
|
|
if addr.Is4() {
|
|
p, _ := addr.Prefix(32)
|
|
return p
|
|
}
|
|
p, _ := addr.Prefix(128)
|
|
return p
|
|
}
|
|
return netip.Prefix{}
|
|
}
|