Files
go-i2ptunnel-config/lib/properties.go

350 lines
10 KiB
Go

package i2pconv
import (
"fmt"
"strconv"
"strings"
"github.com/magiconair/properties"
)
func (c *Converter) parseJavaProperties(input []byte) (*TunnelConfig, error) {
// Use LoadString which returns error instead of MustLoadString which panics
p, err := properties.LoadString(string(input))
if err != nil {
// Try to extract line number from error message
// properties library errors often include "line X" in the message
return nil, c.enhancePropertiesError(input, err)
}
config := &TunnelConfig{
I2CP: make(map[string]interface{}),
Tunnel: make(map[string]interface{}),
Inbound: make(map[string]interface{}),
Outbound: make(map[string]interface{}),
}
// Parse tunnel.*.name pattern
for _, k := range p.Keys() {
c.parsePropertyKey(k, p.GetString(k, ""), config)
}
return config, nil
}
// enhancePropertiesError wraps properties parsing errors with line context.
// It attempts to extract line numbers from the error message and provide context.
func (c *Converter) enhancePropertiesError(input []byte, err error) error {
// Try to extract line number from error message
// The properties library typically formats errors like "line 5: invalid syntax"
errMsg := err.Error()
var lineNum int
var matched bool
// Try to parse "line N" pattern from error
if _, scanErr := fmt.Sscanf(errMsg, "line %d", &lineNum); scanErr == nil {
matched = true
}
if matched && lineNum > 0 {
return newParseError(input, lineNum, 0, "properties", errMsg)
}
// If we can't extract line number, return original error
return fmt.Errorf("properties parse error: %w", err)
}
// 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":
config.Name = s
case "type":
config.Type = s
case "interface":
config.Interface = s
case "listenPort":
if port, err := strconv.Atoi(s); err == nil {
config.Port = port
}
case "targetDestination":
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{})
}
config.I2CP["host"] = s
case "i2cpPort":
if config.I2CP == nil {
config.I2CP = make(map[string]interface{})
}
if port, err := strconv.Atoi(s); err == nil {
config.I2CP["port"] = port
}
}
// Handle prefixed keys
if strings.HasPrefix(k, "option.i2cp.") {
if config.I2CP == nil {
config.I2CP = make(map[string]interface{})
}
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)
}
}
// Helper to parse property values with type conversion
func parseValue(s string) interface{} {
// Try boolean
if b, err := strconv.ParseBool(s); err == nil {
return b
}
// Try integer
if i, err := strconv.Atoi(s); err == nil {
return i
}
// Try comma-separated list
if strings.Contains(s, ",") {
return strings.Split(s, ",")
}
// Default to string
return s
}
// generateJavaProperties generates a Java properties file content based on the provided TunnelConfig.
// It constructs the properties as a byte slice.
//
// Parameters:
// - config (*TunnelConfig): The configuration for the tunnel. It includes various fields such as Name, Type, Interface, Port, PersistentKey, Description, and maps for I2CP, Tunnel, Inbound, and Outbound options.
//
// Returns:
// - ([]byte): A byte slice containing the generated properties file content.
// - (error): An error if any occurs during the generation process.
//
// Notable Errors/Edge Cases:
// - The function does not handle any specific errors internally but returns any error encountered during the string building process.
//
// Related Code Entities:
// - TunnelConfig: The structure that holds the configuration for the tunnel.
func (c *Converter) generateJavaProperties(config *TunnelConfig) ([]byte, error) {
var sb strings.Builder
if config.Name != "" {
sb.WriteString(fmt.Sprintf("name=%s\n", config.Name))
}
if config.Type != "" {
sb.WriteString(fmt.Sprintf("type=%s\n", config.Type))
}
if config.Interface != "" {
sb.WriteString(fmt.Sprintf("interface=%s\n", config.Interface))
}
if config.Port != 0 {
sb.WriteString(fmt.Sprintf("listenPort=%d\n", config.Port))
}
if config.Target != "" {
sb.WriteString(fmt.Sprintf("targetDestination=%s\n", config.Target))
}
if config.PersistentKey {
sb.WriteString("option.persistentClientKey=true\n")
}
if config.Description != "" {
sb.WriteString(fmt.Sprintf("description=%s\n", config.Description))
}
for k, v := range config.I2CP {
sb.WriteString(fmt.Sprintf("option.i2cp.%s=%s\n", k, formatPropertyValue(v)))
}
for k, v := range config.Tunnel {
// 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=%s\n", k, formatPropertyValue(v)))
}
for k, v := range config.Outbound {
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 {
// Handle []string
if slice, ok := v.([]string); ok {
return strings.Join(slice, ",")
}
// Handle []interface{} (common from YAML unmarshaling)
if slice, ok := v.([]interface{}); ok {
strSlice := make([]string, len(slice))
for i, item := range slice {
strSlice[i] = fmt.Sprint(item)
}
return strings.Join(strSlice, ",")
}
return fmt.Sprint(v)
}