Check in config converter

This commit is contained in:
eyedeekay
2025-02-01 22:16:29 -05:00
parent 77b21ebca9
commit a5e36235d5
10 changed files with 476 additions and 1 deletions

View File

@ -1 +0,0 @@
# go-i2ptunnel-config

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module github.com/go-i2p/go-i2ptunnel-config
go 1.23.5
require (
github.com/magiconair/properties v1.8.9
github.com/urfave/cli v1.22.16
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
)

30
go.sum Normal file
View File

@ -0,0 +1,30 @@
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

48
lib/convert.go Normal file
View File

@ -0,0 +1,48 @@
package i2pconv
import (
"fmt"
"os"
"github.com/urfave/cli"
)
// ConvertCommand handles the CLI command for converting tunnel configurations
func ConvertCommand(c *cli.Context) error {
inputFormat := c.StringSlice("input-format")[0]
outputFormat := c.StringSlice("output-format")[0]
strict := c.Bool("strict")
dryRun := c.Bool("dry-run")
converter := &Converter{strict: strict}
inputData, err := os.ReadFile(c.Args().Get(0))
if err != nil {
return fmt.Errorf("failed to read input file: %w", err)
}
config, err := converter.parseInput(inputData, inputFormat)
if err != nil {
return fmt.Errorf("failed to parse input: %w", err)
}
if err := converter.validate(config); err != nil {
return fmt.Errorf("validation error: %w", err)
}
outputData, err := converter.generateOutput(config, outputFormat)
if err != nil {
return fmt.Errorf("failed to generate output: %w", err)
}
if dryRun {
fmt.Println(string(outputData))
} else {
outputFile := c.Args().Get(1)
if err := os.WriteFile(outputFile, outputData, 0644); err != nil {
return fmt.Errorf("failed to write output file: %w", err)
}
}
return nil
}

29
lib/errors.go Normal file
View File

@ -0,0 +1,29 @@
package i2pconv
// ConversionError represents an error during conversion
type ConversionError struct {
Op string
Err error
}
// ValidationError represents an error during validation
type ValidationError struct {
Config *TunnelConfig
Err error
}
func (e *ValidationError) Error() string {
return "validation: " + e.Err.Error()
}
func (e *ValidationError) Unwrap() error {
return e.Err
}
func (e *ConversionError) Error() string {
return e.Op + ": " + e.Err.Error()
}
func (e *ConversionError) Unwrap() error {
return e.Err
}

78
lib/i2pconv.go Normal file
View File

@ -0,0 +1,78 @@
package i2pconv
import (
"fmt"
)
// TunnelConfig represents a normalized tunnel configuration
type TunnelConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Interface string `yaml:"interface,omitempty"`
Port int `yaml:"port,omitempty"`
PersistentKey bool `yaml:"persistentKey,omitempty"`
Description string `yaml:"description,omitempty"`
I2CP map[string]interface{} `yaml:"i2cp,omitempty"`
Tunnel map[string]interface{} `yaml:"options,omitempty"`
Inbound map[string]interface{} `yaml:"inbound,omitempty"`
Outbound map[string]interface{} `yaml:"outbound,omitempty"`
}
// generateOutput generates the output based on the format
func (c *Converter) generateOutput(config *TunnelConfig, format string) ([]byte, error) {
switch format {
case "properties":
return c.generateJavaProperties(config)
case "yaml":
return c.generateYAML(config)
case "ini":
return c.generateINI(config)
default:
return nil, fmt.Errorf("unsupported output format: %s", format)
}
}
// validate checks the configuration for any errors
func (c *Converter) validate(config *TunnelConfig) error {
if config.Name == "" {
return fmt.Errorf("name is required")
}
if config.Type == "" {
return fmt.Errorf("type is required")
}
return nil
}
// Converter handles configuration format conversions
type Converter struct {
strict bool
//logger Logger
}
// Convert transforms between configuration formats
func (c *Converter) Convert(input []byte, inFormat, outFormat string) ([]byte, error) {
config, err := c.parseInput(input, inFormat)
if err != nil {
return nil, &ConversionError{Op: "parse", Err: err}
}
if err := c.validate(config); err != nil {
return nil, &ValidationError{Config: config, Err: err}
}
return c.generateOutput(config, outFormat)
}
// parseInput parses the input based on the format
func (c *Converter) parseInput(input []byte, format string) (*TunnelConfig, error) {
switch format {
case "properties":
return c.parseJavaProperties(input)
case "yaml":
return c.parseYAML(input)
case "ini":
return c.parseINI(input)
default:
return nil, fmt.Errorf("unsupported input format: %s", format)
}
}

100
lib/ini.go Normal file
View File

@ -0,0 +1,100 @@
package i2pconv
import (
"fmt"
"strconv"
"strings"
)
func (c *Converter) parseINI(input []byte) (*TunnelConfig, error) {
config := &TunnelConfig{
I2CP: make(map[string]interface{}),
Tunnel: make(map[string]interface{}),
Inbound: make(map[string]interface{}),
Outbound: make(map[string]interface{}),
}
lines := strings.Split(string(input), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch key {
case "keys":
config.PersistentKey = true
case "type":
config.Type = value
case "host":
config.Interface = value
case "port":
port, err := strconv.Atoi(value)
if err == nil {
config.Port = port
}
default:
if strings.HasPrefix(key, "i2cp.") {
config.I2CP[strings.TrimPrefix(key, "i2cp.")] = value
} else if strings.HasPrefix(key, "inbound") {
config.Inbound[strings.TrimPrefix(key, "inbound.")] = value
} else if strings.HasPrefix(key, "outbound") {
config.Outbound[strings.TrimPrefix(key, "outbound.")] = value
} else {
config.Tunnel[key] = value
}
}
}
return config, nil
}
func (c *Converter) generateINI(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("port = %d\n", config.Port))
}
if config.PersistentKey {
sb.WriteString(fmt.Sprintf("keys = %s\n", strings.TrimSpace(strings.ReplaceAll(config.Name, " ", "_"))))
}
if config.Description != "" {
sb.WriteString(fmt.Sprintf("description = %s\n", config.Description))
}
for k, v := range config.I2CP {
sb.WriteString(fmt.Sprintf("i2cp.%s = %v\n", k, v))
}
for k, v := range config.Tunnel {
sb.WriteString(fmt.Sprintf("%s = %v\n", k, v))
}
for k, v := range config.Inbound {
sb.WriteString(fmt.Sprintf("inbound.%s = %v\n", k, v))
}
for k, v := range config.Outbound {
sb.WriteString(fmt.Sprintf("outbound.%s = %v\n", k, v))
}
return []byte(sb.String()), nil
}

108
lib/properties.go Normal file
View File

@ -0,0 +1,108 @@
package i2pconv
import (
"fmt"
"strconv"
"strings"
"github.com/magiconair/properties"
)
// Example Java properties parser
func (c *Converter) parseJavaProperties(input []byte) (*TunnelConfig, error) {
p := properties.MustLoadString(string(input))
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
}
func (c *Converter) parsePropertyKey(k string, s string, config *TunnelConfig) {
if strings.HasPrefix(k, "#") {
return
}
kv := strings.Split(k, "=")
if len(kv) != 2 {
return
}
parts := strings.Split(kv[0], ".")
switch parts[1] {
case "name":
config.Name = s
case "type":
config.Type = s
case "interface":
config.Interface = s
case "listenPort":
port, err := strconv.Atoi(s)
if err == nil {
config.Port = port
}
case "option.persistentClientKey":
config.PersistentKey = true
case "description":
config.Description = s
default:
if strings.HasPrefix(parts[1], "option.i2cp") {
config.I2CP[parts[2]] = s
} else if strings.HasPrefix(parts[1], "option.i2ptunnel") {
config.Tunnel[parts[2]] = s
} else if strings.HasPrefix(parts[1], "option.inbound") {
config.Inbound[parts[2]] = s
} else if strings.HasPrefix(parts[1], "option.outbound") {
config.Outbound[parts[2]] = s
}
}
}
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.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=%v\n", k, v))
}
for k, v := range config.Tunnel {
sb.WriteString(fmt.Sprintf("option.i2ptunnel.%s=%v\n", k, v))
}
for k, v := range config.Inbound {
sb.WriteString(fmt.Sprintf("option.inbound.%s=%v\n", k, v))
}
for k, v := range config.Outbound {
sb.WriteString(fmt.Sprintf("option.outbound.%s=%v\n", k, v))
}
return []byte(sb.String()), nil
}

27
lib/yaml.go Normal file
View File

@ -0,0 +1,27 @@
package i2pconv
import "gopkg.in/yaml.v2"
// Example YAML parser
func (c *Converter) parseYAML(input []byte) (*TunnelConfig, error) {
config := &TunnelConfig{}
err := yaml.Unmarshal(input, config)
if err != nil {
return nil, err
}
return config, nil
}
func (c *Converter) generateYAML(config *TunnelConfig) ([]byte, error) {
type wrapper struct {
Tunnels map[string]*TunnelConfig `yaml:"tunnels"`
}
out := wrapper{
Tunnels: map[string]*TunnelConfig{
config.Name: config,
},
}
return yaml.Marshal(out)
}

42
main.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"log"
"os"
i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib"
"github.com/urfave/cli"
)
// CLI implementation
func main() {
cmd := &cli.App{
Name: "i2pconv",
Usage: "Convert I2P tunnel configurations between formats",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "input-format, if",
Usage: "Input format (properties|ini|yaml)",
Required: true,
},
&cli.StringSliceFlag{
Name: "output-format, of",
Usage: "Output format (properties|ini|yaml)",
Required: true,
},
&cli.BoolFlag{
Name: "strict",
Usage: "Enable strict validation",
},
&cli.BoolFlag{
Name: "dry-run",
Usage: "Validate without writing output",
},
},
Action: i2pconv.ConvertCommand,
}
if err := cmd.Run(os.Args); err != nil {
log.Fatal(err)
}
}