mirror of
https://github.com/go-i2p/go-i2ptunnel-config.git
synced 2025-12-20 15:15:52 -05:00
Enhance properties parser to support numbered tunnel properties and option prefixes
This commit is contained in:
@@ -26,11 +26,41 @@ func (c *Converter) parseJavaProperties(input []byte) (*TunnelConfig, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// parsePropertyKey parses a single Java I2P property key-value pair and updates the TunnelConfig.
|
||||
// Supports multiple Java I2P property patterns:
|
||||
//
|
||||
// Flat properties:
|
||||
// - name, type, interface, listenPort, targetDestination, targetHost, description
|
||||
// - proxyList, sharedClient, startOnLoad, accessList, targetPort, spoofedHost (stored in Tunnel map)
|
||||
// - i2cpHost, i2cpPort (stored in I2CP map)
|
||||
//
|
||||
// Numbered tunnel patterns:
|
||||
// - tunnel.N.property (e.g., tunnel.0.name, tunnel.1.type, tunnel.2.interface)
|
||||
//
|
||||
// Option prefixes:
|
||||
// - option.i2cp.* -> stored in I2CP map
|
||||
// - option.i2ptunnel.* -> stored in Tunnel map
|
||||
// - option.inbound.* -> stored in Inbound map
|
||||
// - option.outbound.* -> stored in Outbound map
|
||||
// - option.persistentClientKey -> sets PersistentKey field
|
||||
//
|
||||
// Comments (#) and configFile properties are ignored.
|
||||
func (c *Converter) parsePropertyKey(k, s string, config *TunnelConfig) {
|
||||
if strings.HasPrefix(k, "#") || strings.HasPrefix(k, "configFile") {
|
||||
return // Skip comments and config file path
|
||||
}
|
||||
|
||||
// Handle tunnel.N.property patterns for numbered tunnels
|
||||
if strings.HasPrefix(k, "tunnel.") && strings.Contains(k, ".") {
|
||||
parts := strings.SplitN(k, ".", 3)
|
||||
if len(parts) == 3 {
|
||||
// Format: tunnel.N.property (e.g., tunnel.0.name, tunnel.1.type)
|
||||
property := parts[2]
|
||||
c.parseNumberedTunnelProperty(property, s, config)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle flat keys
|
||||
switch k {
|
||||
case "name":
|
||||
@@ -47,8 +77,40 @@ func (c *Converter) parsePropertyKey(k, s string, config *TunnelConfig) {
|
||||
config.Target = s
|
||||
case "targetHost":
|
||||
config.Target = s // Alternative naming
|
||||
case "targetPort":
|
||||
if port, err := strconv.Atoi(s); err == nil {
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["targetPort"] = port
|
||||
}
|
||||
case "description":
|
||||
config.Description = s
|
||||
case "proxyList":
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["proxyList"] = parseValue(s)
|
||||
case "sharedClient":
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["sharedClient"] = parseValue(s)
|
||||
case "startOnLoad":
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["startOnLoad"] = parseValue(s)
|
||||
case "accessList":
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["accessList"] = parseValue(s)
|
||||
case "spoofedHost":
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["spoofedHost"] = parseValue(s)
|
||||
case "i2cpHost":
|
||||
if config.I2CP == nil {
|
||||
config.I2CP = make(map[string]interface{})
|
||||
@@ -70,6 +132,84 @@ func (c *Converter) parsePropertyKey(k, s string, config *TunnelConfig) {
|
||||
}
|
||||
key := strings.TrimPrefix(k, "option.i2cp.")
|
||||
config.I2CP[key] = parseValue(s)
|
||||
} else if strings.HasPrefix(k, "option.i2ptunnel.") {
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
key := strings.TrimPrefix(k, "option.i2ptunnel.")
|
||||
config.Tunnel[key] = parseValue(s)
|
||||
} else if strings.HasPrefix(k, "option.inbound.") {
|
||||
if config.Inbound == nil {
|
||||
config.Inbound = make(map[string]interface{})
|
||||
}
|
||||
key := strings.TrimPrefix(k, "option.inbound.")
|
||||
config.Inbound[key] = parseValue(s)
|
||||
} else if strings.HasPrefix(k, "option.outbound.") {
|
||||
if config.Outbound == nil {
|
||||
config.Outbound = make(map[string]interface{})
|
||||
}
|
||||
key := strings.TrimPrefix(k, "option.outbound.")
|
||||
config.Outbound[key] = parseValue(s)
|
||||
} else if k == "option.persistentClientKey" {
|
||||
config.PersistentKey = parseValue(s).(bool)
|
||||
}
|
||||
}
|
||||
|
||||
// parseNumberedTunnelProperty handles properties from tunnel.N.property patterns
|
||||
// For numbered tunnel configurations, we treat each as a separate tunnel instance
|
||||
// but for this converter we only support single tunnel configs, so we merge all numbered properties
|
||||
func (c *Converter) parseNumberedTunnelProperty(property, value string, config *TunnelConfig) {
|
||||
switch property {
|
||||
case "name":
|
||||
// If config.Name is empty, use this as the primary name
|
||||
// If config.Name exists, treat as additional tunnel option
|
||||
if config.Name == "" {
|
||||
config.Name = value
|
||||
} else {
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["alternateName"] = value
|
||||
}
|
||||
case "type":
|
||||
if config.Type == "" {
|
||||
config.Type = value
|
||||
}
|
||||
case "interface":
|
||||
if config.Interface == "" {
|
||||
config.Interface = value
|
||||
}
|
||||
case "listenPort":
|
||||
if config.Port == 0 {
|
||||
if port, err := strconv.Atoi(value); err == nil {
|
||||
config.Port = port
|
||||
}
|
||||
}
|
||||
case "targetDestination":
|
||||
if config.Target == "" {
|
||||
config.Target = value
|
||||
}
|
||||
case "targetHost":
|
||||
if config.Target == "" {
|
||||
config.Target = value
|
||||
}
|
||||
case "targetPort":
|
||||
if port, err := strconv.Atoi(value); err == nil {
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel["targetPort"] = port
|
||||
}
|
||||
case "description":
|
||||
if config.Description == "" {
|
||||
config.Description = value
|
||||
}
|
||||
default:
|
||||
// Store other numbered tunnel properties in the Tunnel map
|
||||
if config.Tunnel == nil {
|
||||
config.Tunnel = make(map[string]interface{})
|
||||
}
|
||||
config.Tunnel[property] = parseValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,20 +275,36 @@ func (c *Converter) generateJavaProperties(config *TunnelConfig) ([]byte, error)
|
||||
}
|
||||
|
||||
for k, v := range config.I2CP {
|
||||
sb.WriteString(fmt.Sprintf("option.i2cp.%s=%v\n", k, v))
|
||||
sb.WriteString(fmt.Sprintf("option.i2cp.%s=%s\n", k, formatPropertyValue(v)))
|
||||
}
|
||||
|
||||
for k, v := range config.Tunnel {
|
||||
sb.WriteString(fmt.Sprintf("option.i2ptunnel.%s=%v\n", k, v))
|
||||
// Handle special flat properties that should not have option.i2ptunnel prefix
|
||||
switch k {
|
||||
case "proxyList", "sharedClient", "startOnLoad", "accessList", "spoofedHost", "targetPort":
|
||||
sb.WriteString(fmt.Sprintf("%s=%s\n", k, formatPropertyValue(v)))
|
||||
default:
|
||||
// Other tunnel options use the option.i2ptunnel prefix
|
||||
sb.WriteString(fmt.Sprintf("option.i2ptunnel.%s=%s\n", k, formatPropertyValue(v)))
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range config.Inbound {
|
||||
sb.WriteString(fmt.Sprintf("option.inbound.%s=%v\n", k, v))
|
||||
sb.WriteString(fmt.Sprintf("option.inbound.%s=%s\n", k, formatPropertyValue(v)))
|
||||
}
|
||||
|
||||
for k, v := range config.Outbound {
|
||||
sb.WriteString(fmt.Sprintf("option.outbound.%s=%v\n", k, v))
|
||||
sb.WriteString(fmt.Sprintf("option.outbound.%s=%s\n", k, formatPropertyValue(v)))
|
||||
}
|
||||
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
// formatPropertyValue formats a property value for output
|
||||
// Arrays/slices are formatted as comma-separated values
|
||||
func formatPropertyValue(v interface{}) string {
|
||||
if slice, ok := v.([]string); ok {
|
||||
return strings.Join(slice, ",")
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package i2pconv
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPropertyConversion(t *testing.T) {
|
||||
input := `
|
||||
@@ -26,7 +30,7 @@ sharedClient=true
|
||||
t.Fatalf("Failed to generate YAML: %v", err)
|
||||
}
|
||||
|
||||
// Expected YAML structure matching actual format:
|
||||
// Expected YAML structure matching enhanced parsing:
|
||||
expected := `tunnels:
|
||||
I2P HTTP Proxy:
|
||||
name: I2P HTTP Proxy
|
||||
@@ -39,6 +43,11 @@ sharedClient=true
|
||||
- "4"
|
||||
- "0"
|
||||
reduceIdleTime: 900000
|
||||
options:
|
||||
proxyList: exit.stormycloud.i2p
|
||||
sharedClient: true
|
||||
inbound:
|
||||
length: 3
|
||||
`
|
||||
// Compare YAML output
|
||||
if string(yaml) != expected {
|
||||
@@ -47,3 +56,312 @@ sharedClient=true
|
||||
t.Logf("YAML output:\n%s", yaml)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnhancedPropertiesParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected *TunnelConfig
|
||||
}{
|
||||
{
|
||||
name: "numbered tunnel properties",
|
||||
input: `
|
||||
tunnel.0.name=HTTPProxy
|
||||
tunnel.0.type=httpclient
|
||||
tunnel.0.interface=127.0.0.1
|
||||
tunnel.0.listenPort=4444
|
||||
tunnel.0.description=HTTP proxy tunnel
|
||||
`,
|
||||
expected: &TunnelConfig{
|
||||
Name: "HTTPProxy",
|
||||
Type: "httpclient",
|
||||
Interface: "127.0.0.1",
|
||||
Port: 4444,
|
||||
Description: "HTTP proxy tunnel",
|
||||
I2CP: make(map[string]interface{}),
|
||||
Tunnel: make(map[string]interface{}),
|
||||
Inbound: make(map[string]interface{}),
|
||||
Outbound: make(map[string]interface{}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "option prefixes",
|
||||
input: `
|
||||
name=TestTunnel
|
||||
type=server
|
||||
option.i2ptunnel.useCompression=true
|
||||
option.inbound.length=3
|
||||
option.outbound.length=2
|
||||
option.persistentClientKey=true
|
||||
`,
|
||||
expected: &TunnelConfig{
|
||||
Name: "TestTunnel",
|
||||
Type: "server",
|
||||
PersistentKey: true,
|
||||
I2CP: make(map[string]interface{}),
|
||||
Tunnel: map[string]interface{}{
|
||||
"useCompression": true,
|
||||
},
|
||||
Inbound: map[string]interface{}{
|
||||
"length": 3,
|
||||
},
|
||||
Outbound: map[string]interface{}{
|
||||
"length": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "additional flat properties",
|
||||
input: `
|
||||
name=WebServer
|
||||
type=httpserver
|
||||
proxyList=example.i2p,another.i2p
|
||||
sharedClient=false
|
||||
startOnLoad=true
|
||||
accessList=allow
|
||||
targetPort=8080
|
||||
spoofedHost=example.com
|
||||
`,
|
||||
expected: &TunnelConfig{
|
||||
Name: "WebServer",
|
||||
Type: "httpserver",
|
||||
I2CP: make(map[string]interface{}),
|
||||
Tunnel: map[string]interface{}{
|
||||
"proxyList": []string{"example.i2p", "another.i2p"},
|
||||
"sharedClient": false,
|
||||
"startOnLoad": true,
|
||||
"accessList": "allow",
|
||||
"targetPort": 8080,
|
||||
"spoofedHost": "example.com",
|
||||
},
|
||||
Inbound: make(map[string]interface{}),
|
||||
Outbound: make(map[string]interface{}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed numbered and flat properties",
|
||||
input: `
|
||||
tunnel.0.name=MixedTunnel
|
||||
tunnel.1.type=client
|
||||
interface=192.168.1.1
|
||||
listenPort=7070
|
||||
proxyList=proxy.i2p
|
||||
option.i2cp.reduceIdleTime=600000
|
||||
`,
|
||||
expected: &TunnelConfig{
|
||||
Name: "MixedTunnel",
|
||||
Type: "client",
|
||||
Interface: "192.168.1.1",
|
||||
Port: 7070,
|
||||
I2CP: map[string]interface{}{
|
||||
"reduceIdleTime": 600000,
|
||||
},
|
||||
Tunnel: map[string]interface{}{
|
||||
"proxyList": "proxy.i2p",
|
||||
},
|
||||
Inbound: make(map[string]interface{}),
|
||||
Outbound: make(map[string]interface{}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conv := &Converter{}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config, err := conv.parseJavaProperties([]byte(tt.input))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse properties: %v", err)
|
||||
}
|
||||
|
||||
// Check basic fields
|
||||
if config.Name != tt.expected.Name {
|
||||
t.Errorf("Name: expected %q, got %q", tt.expected.Name, config.Name)
|
||||
}
|
||||
if config.Type != tt.expected.Type {
|
||||
t.Errorf("Type: expected %q, got %q", tt.expected.Type, config.Type)
|
||||
}
|
||||
if config.Interface != tt.expected.Interface {
|
||||
t.Errorf("Interface: expected %q, got %q", tt.expected.Interface, config.Interface)
|
||||
}
|
||||
if config.Port != tt.expected.Port {
|
||||
t.Errorf("Port: expected %d, got %d", tt.expected.Port, config.Port)
|
||||
}
|
||||
if config.Description != tt.expected.Description {
|
||||
t.Errorf("Description: expected %q, got %q", tt.expected.Description, config.Description)
|
||||
}
|
||||
if config.PersistentKey != tt.expected.PersistentKey {
|
||||
t.Errorf("PersistentKey: expected %t, got %t", tt.expected.PersistentKey, config.PersistentKey)
|
||||
}
|
||||
|
||||
// Check map fields using proper comparison
|
||||
for k, v := range tt.expected.I2CP {
|
||||
if actualV, ok := config.I2CP[k]; !ok {
|
||||
t.Errorf("I2CP[%q]: missing key", k)
|
||||
} else if !valuesEqual(v, actualV) {
|
||||
t.Errorf("I2CP[%q]: expected %v, got %v", k, v, actualV)
|
||||
}
|
||||
}
|
||||
for k, v := range tt.expected.Tunnel {
|
||||
if actualV, ok := config.Tunnel[k]; !ok {
|
||||
t.Errorf("Tunnel[%q]: missing key", k)
|
||||
} else if !valuesEqual(v, actualV) {
|
||||
t.Errorf("Tunnel[%q]: expected %v, got %v", k, v, actualV)
|
||||
}
|
||||
}
|
||||
for k, v := range tt.expected.Inbound {
|
||||
if actualV, ok := config.Inbound[k]; !ok {
|
||||
t.Errorf("Inbound[%q]: missing key", k)
|
||||
} else if !valuesEqual(v, actualV) {
|
||||
t.Errorf("Inbound[%q]: expected %v, got %v", k, v, actualV)
|
||||
}
|
||||
}
|
||||
for k, v := range tt.expected.Outbound {
|
||||
if actualV, ok := config.Outbound[k]; !ok {
|
||||
t.Errorf("Outbound[%q]: missing key", k)
|
||||
} else if !valuesEqual(v, actualV) {
|
||||
t.Errorf("Outbound[%q]: expected %v, got %v", k, v, actualV)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertiesRoundTrip(t *testing.T) {
|
||||
// Test that parsing and then generating produces consistent results
|
||||
input := `name=RoundTripTest
|
||||
type=httpclient
|
||||
interface=127.0.0.1
|
||||
listenPort=8888
|
||||
targetDestination=example.i2p
|
||||
description=Test tunnel for round-trip conversion
|
||||
proxyList=proxy1.i2p,proxy2.i2p
|
||||
sharedClient=true
|
||||
startOnLoad=false
|
||||
option.i2cp.leaseSetEncType=4,0
|
||||
option.i2cp.reduceIdleTime=900000
|
||||
option.i2ptunnel.useCompression=true
|
||||
option.inbound.length=3
|
||||
option.outbound.length=2
|
||||
option.persistentClientKey=true`
|
||||
|
||||
conv := &Converter{}
|
||||
|
||||
// Parse properties
|
||||
config, err := conv.parseJavaProperties([]byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse properties: %v", err)
|
||||
}
|
||||
|
||||
// Generate properties back
|
||||
output, err := conv.generateJavaProperties(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate properties: %v", err)
|
||||
}
|
||||
|
||||
// Parse the generated output again
|
||||
config2, err := conv.parseJavaProperties(output)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to re-parse generated properties: %v", err)
|
||||
}
|
||||
|
||||
// Compare the two configs
|
||||
if config.Name != config2.Name {
|
||||
t.Errorf("Round-trip Name: expected %q, got %q", config.Name, config2.Name)
|
||||
}
|
||||
if config.Type != config2.Type {
|
||||
t.Errorf("Round-trip Type: expected %q, got %q", config.Type, config2.Type)
|
||||
}
|
||||
if config.Interface != config2.Interface {
|
||||
t.Errorf("Round-trip Interface: expected %q, got %q", config.Interface, config2.Interface)
|
||||
}
|
||||
if config.Port != config2.Port {
|
||||
t.Errorf("Round-trip Port: expected %d, got %d", config.Port, config2.Port)
|
||||
}
|
||||
if config.Target != config2.Target {
|
||||
t.Errorf("Round-trip Target: expected %q, got %q", config.Target, config2.Target)
|
||||
}
|
||||
if config.Description != config2.Description {
|
||||
t.Errorf("Round-trip Description: expected %q, got %q", config.Description, config2.Description)
|
||||
}
|
||||
if config.PersistentKey != config2.PersistentKey {
|
||||
t.Errorf("Round-trip PersistentKey: expected %t, got %t", config.PersistentKey, config2.PersistentKey)
|
||||
}
|
||||
|
||||
// Check that all options were preserved
|
||||
for k, v := range config.I2CP {
|
||||
if v2, ok := config2.I2CP[k]; !ok || fmt.Sprint(v) != fmt.Sprint(v2) {
|
||||
t.Errorf("Round-trip I2CP[%q]: expected %v, got %v", k, v, v2)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Round-trip successful. Generated properties:\n%s", string(output))
|
||||
}
|
||||
|
||||
func TestPropertiesEdgeCases(t *testing.T) {
|
||||
conv := &Converter{}
|
||||
|
||||
// Test comments and config file paths are ignored
|
||||
input := `# This is a comment
|
||||
configFile=/path/to/file
|
||||
name=TestTunnel
|
||||
# Another comment
|
||||
type=client`
|
||||
|
||||
config, err := conv.ParseInput([]byte(input), "properties")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse properties: %v", err)
|
||||
}
|
||||
|
||||
if config.Name != "TestTunnel" {
|
||||
t.Errorf("Expected name 'TestTunnel', got %q", config.Name)
|
||||
}
|
||||
if config.Type != "client" {
|
||||
t.Errorf("Expected type 'client', got %q", config.Type)
|
||||
}
|
||||
|
||||
// Test alternate tunnel destination naming
|
||||
input2 := `name=AltTunnel
|
||||
type=server
|
||||
targetHost=alt.example.i2p
|
||||
i2cpHost=127.0.0.1
|
||||
i2cpPort=7654`
|
||||
|
||||
config2, err := conv.ParseInput([]byte(input2), "properties")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse properties: %v", err)
|
||||
}
|
||||
|
||||
if config2.Target != "alt.example.i2p" {
|
||||
t.Errorf("Expected target 'alt.example.i2p', got %q", config2.Target)
|
||||
}
|
||||
if config2.I2CP["host"] != "127.0.0.1" {
|
||||
t.Errorf("Expected I2CP host '127.0.0.1', got %v", config2.I2CP["host"])
|
||||
}
|
||||
if config2.I2CP["port"] != 7654 {
|
||||
t.Errorf("Expected I2CP port 7654, got %v", config2.I2CP["port"])
|
||||
}
|
||||
|
||||
// Test numbered tunnel conflicts (second values should be ignored when flat values exist)
|
||||
input3 := `name=FirstName
|
||||
tunnel.0.name=SecondName
|
||||
type=client
|
||||
tunnel.1.type=server`
|
||||
|
||||
config3, err := conv.ParseInput([]byte(input3), "properties")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse properties: %v", err)
|
||||
}
|
||||
|
||||
// Should keep the first flat value
|
||||
if config3.Name != "FirstName" {
|
||||
t.Errorf("Expected name 'FirstName', got %q", config3.Name)
|
||||
}
|
||||
if config3.Type != "client" {
|
||||
t.Errorf("Expected type 'client', got %q", config3.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// valuesEqual compares two values, handling slices properly
|
||||
func valuesEqual(expected, actual interface{}) bool {
|
||||
return reflect.DeepEqual(expected, actual)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user