mirror of
https://github.com/go-i2p/go-i2ptunnel-config.git
synced 2025-12-20 15:15:52 -05:00
check in examples, perform round-trip-tests, fix yaml conversion bugs due to inconsistent nesting
This commit is contained in:
12
README.md
12
README.md
@@ -51,6 +51,18 @@ go-i2ptunnel-config --batch "*.config"
|
|||||||
go-i2ptunnel-config --batch --out-format ini "tunnels/*.properties"
|
go-i2ptunnel-config --batch --out-format ini "tunnels/*.properties"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
The `examples/` directory contains ready-to-use configuration templates for common tunnel types in all three formats:
|
||||||
|
|
||||||
|
- HTTP client (web proxy)
|
||||||
|
- HTTP server (eepsite hosting)
|
||||||
|
- SOCKS proxy
|
||||||
|
- Generic client tunnel
|
||||||
|
- Generic server tunnel
|
||||||
|
|
||||||
|
Each example includes detailed comments explaining the configuration options. See [examples/README.md](examples/README.md) for more information.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork repository
|
1. Fork repository
|
||||||
|
|||||||
231
examples/README.md
Normal file
231
examples/README.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# I2P Tunnel Configuration Examples
|
||||||
|
|
||||||
|
This directory contains example tunnel configuration files for all three supported formats:
|
||||||
|
|
||||||
|
- **Java I2P format** (`.properties` files)
|
||||||
|
- **i2pd format** (`.conf` files)
|
||||||
|
- **go-i2p format** (`.yaml` files)
|
||||||
|
|
||||||
|
## Available Examples
|
||||||
|
|
||||||
|
### HTTP Client Tunnel (HTTP Proxy)
|
||||||
|
|
||||||
|
Creates a local HTTP proxy to access I2P websites (eepsites).
|
||||||
|
|
||||||
|
- `httpclient.properties` - Java I2P format
|
||||||
|
- `httpclient.conf` - i2pd format
|
||||||
|
- `httpclient.yaml` - go-i2p format
|
||||||
|
|
||||||
|
**Use case**: Browse eepsites through your web browser configured to use the proxy.
|
||||||
|
|
||||||
|
**Default port**: 4444
|
||||||
|
|
||||||
|
### HTTP Server Tunnel (Eepsite)
|
||||||
|
|
||||||
|
Publishes a local web server as an I2P hidden service.
|
||||||
|
|
||||||
|
- `httpserver.properties` - Java I2P format
|
||||||
|
- `httpserver.conf` - i2pd format
|
||||||
|
- `httpserver.yaml` - go-i2p format
|
||||||
|
|
||||||
|
**Use case**: Host your own website accessible only through I2P.
|
||||||
|
|
||||||
|
**Target**: Local web server (default: 127.0.0.1:8080)
|
||||||
|
|
||||||
|
### SOCKS Tunnel (SOCKS Proxy)
|
||||||
|
|
||||||
|
Creates a SOCKS5 proxy for general I2P network access.
|
||||||
|
|
||||||
|
- `socks.properties` - Java I2P format
|
||||||
|
- `socks.conf` - i2pd format
|
||||||
|
- `socks.yaml` - go-i2p format
|
||||||
|
|
||||||
|
**Use case**: Route any SOCKS-compatible application through I2P (IRC, SSH, etc.).
|
||||||
|
|
||||||
|
**Default port**: 9050
|
||||||
|
|
||||||
|
### Generic Client Tunnel
|
||||||
|
|
||||||
|
Creates a TCP client tunnel for any protocol.
|
||||||
|
|
||||||
|
- `client.properties` - Java I2P format
|
||||||
|
- `client.conf` - i2pd format
|
||||||
|
- `client.yaml` - go-i2p format
|
||||||
|
|
||||||
|
**Use case**: Connect to any I2P service using a custom protocol.
|
||||||
|
|
||||||
|
**Default port**: 7000
|
||||||
|
|
||||||
|
### Generic Server Tunnel
|
||||||
|
|
||||||
|
Publishes any local TCP service as an I2P hidden service.
|
||||||
|
|
||||||
|
- `server.properties` - Java I2P format
|
||||||
|
- `server.conf` - i2pd format
|
||||||
|
- `server.yaml` - go-i2p format
|
||||||
|
|
||||||
|
**Use case**: Make any TCP service accessible through I2P (SSH server, game server, etc.).
|
||||||
|
|
||||||
|
**Target**: Local service (default: 127.0.0.1:9000)
|
||||||
|
|
||||||
|
## Using the Examples
|
||||||
|
|
||||||
|
### Converting Between Formats
|
||||||
|
|
||||||
|
Convert any example to a different format using the tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Convert Java I2P properties to YAML
|
||||||
|
go-i2ptunnel-config httpclient.properties
|
||||||
|
|
||||||
|
# Convert i2pd conf to properties
|
||||||
|
go-i2ptunnel-config --out-format properties httpserver.conf
|
||||||
|
|
||||||
|
# Convert YAML to i2pd conf
|
||||||
|
go-i2ptunnel-config --out-format ini socks.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validating Configuration
|
||||||
|
|
||||||
|
Before using a configuration file, validate it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go-i2ptunnel-config --validate httpclient.properties
|
||||||
|
go-i2ptunnel-config --validate --strict server.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customizing Examples
|
||||||
|
|
||||||
|
All examples contain sensible defaults. Modify these fields for your use case:
|
||||||
|
|
||||||
|
#### Required Fields
|
||||||
|
|
||||||
|
- **name**: Unique identifier for your tunnel
|
||||||
|
- **type**: Tunnel type (don't change unless you know what you're doing)
|
||||||
|
|
||||||
|
#### Common Fields to Customize
|
||||||
|
|
||||||
|
**For Client Tunnels (HTTP Client, SOCKS, Generic Client)**:
|
||||||
|
|
||||||
|
- **interface**: Network interface to bind (default: 127.0.0.1)
|
||||||
|
- **port**: Local port to listen on
|
||||||
|
- **target/destination**: I2P destination to connect to (for generic client)
|
||||||
|
|
||||||
|
**For Server Tunnels (HTTP Server, Generic Server)**:
|
||||||
|
|
||||||
|
- **target**: Local service address (format: `host:port`)
|
||||||
|
- **spoofedHost**: Custom hostname for your eepsite (optional)
|
||||||
|
|
||||||
|
#### Advanced Options
|
||||||
|
|
||||||
|
**I2CP Options** (i2cp.*):
|
||||||
|
|
||||||
|
- `leaseSetEncType`: Encryption type for the lease set (recommended: "4,0")
|
||||||
|
- `closeIdleTime`: Time in milliseconds before closing idle connections
|
||||||
|
- `newDestOnResume`: Whether to create a new destination on restart
|
||||||
|
|
||||||
|
**Tunnel Options** (inbound/outbound):
|
||||||
|
|
||||||
|
- `length`: Number of hops in the tunnel (higher = more anonymous, slower)
|
||||||
|
- `quantity`: Number of parallel tunnels (higher = more reliable, more resources)
|
||||||
|
|
||||||
|
**Other Options**:
|
||||||
|
|
||||||
|
- `persistentKey`: Keep the same I2P address across restarts (true/false)
|
||||||
|
- `gzip`: Enable compression (true/false)
|
||||||
|
|
||||||
|
## Format Differences
|
||||||
|
|
||||||
|
### Java I2P Properties Format (`.properties`)
|
||||||
|
|
||||||
|
- Flat key-value pairs
|
||||||
|
- Uses prefixes like `option.i2cp.*`, `option.inbound.*`
|
||||||
|
- Standard in Java I2P router
|
||||||
|
- Example: `option.inbound.length=3`
|
||||||
|
|
||||||
|
### i2pd INI Format (`.conf`)
|
||||||
|
|
||||||
|
- Section-based structure `[TunnelName]`
|
||||||
|
- Simpler syntax, more readable
|
||||||
|
- Native to i2pd router
|
||||||
|
- Example: `inbound.length = 3`
|
||||||
|
|
||||||
|
### go-i2p YAML Format (`.yaml`)
|
||||||
|
|
||||||
|
- Hierarchical nested structure with `tunnels` map
|
||||||
|
- Most readable and maintainable
|
||||||
|
- Native to go-i2p implementation
|
||||||
|
- Supports multiple tunnels in one file
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tunnels:
|
||||||
|
MyTunnel:
|
||||||
|
type: client
|
||||||
|
port: 7000
|
||||||
|
inbound:
|
||||||
|
length: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tunnel Types Reference
|
||||||
|
|
||||||
|
| Type | Description | Requires Port | Requires Target |
|
||||||
|
|------|-------------|---------------|-----------------|
|
||||||
|
| `httpclient` | HTTP proxy client | Yes | No |
|
||||||
|
| `httpserver` | HTTP server (eepsite) | No | Yes |
|
||||||
|
| `sockstunnel` | SOCKS5 proxy | Yes | No |
|
||||||
|
| `client` | Generic client tunnel | Yes | Optional |
|
||||||
|
| `server` | Generic server tunnel | No | Yes |
|
||||||
|
| `ircclient` | IRC client tunnel | Yes | No |
|
||||||
|
| `ircserver` | IRC server tunnel | No | Yes |
|
||||||
|
| `streamrclient` | Streaming client | Yes | No |
|
||||||
|
| `streamrserver` | Streaming server | No | Yes |
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **Persistent Keys**: Set `persistentKey: true` to keep the same I2P address across restarts. This is important for servers so users can find you again.
|
||||||
|
- **Tunnel Length**: Higher values (3-7) provide better anonymity but slower performance.
|
||||||
|
- **Tunnel Quantity**: More tunnels provide better reliability and performance but use more resources.
|
||||||
|
- **Local Interface**: Binding to `127.0.0.1` ensures only local applications can use your tunnel. Never bind to `0.0.0.0` unless you understand the security implications.
|
||||||
|
|
||||||
|
## Testing Configurations
|
||||||
|
|
||||||
|
Use dry-run mode to test conversions without creating files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go-i2ptunnel-config --dry-run httpclient.properties
|
||||||
|
go-i2ptunnel-config --dry-run --out-format yaml httpserver.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Setting up an eepsite
|
||||||
|
|
||||||
|
1. Use `httpserver.properties` (or `.conf`/`.yaml`)
|
||||||
|
2. Modify `target` to point to your web server (e.g., `127.0.0.1:8080`)
|
||||||
|
3. Set `persistentKey: true` so your address stays the same
|
||||||
|
4. Optionally set `spoofedHost` to a memorable `.i2p` hostname
|
||||||
|
|
||||||
|
### Browsing eepsites
|
||||||
|
|
||||||
|
1. Use `httpclient.properties` (or `.conf`/`.yaml`)
|
||||||
|
2. Keep default port `4444` or choose your own
|
||||||
|
3. Configure your browser to use `127.0.0.1:4444` as HTTP proxy
|
||||||
|
4. Visit `.i2p` addresses in your browser
|
||||||
|
|
||||||
|
### General I2P Network Access
|
||||||
|
|
||||||
|
1. Use `socks.properties` (or `.conf`/`.yaml`)
|
||||||
|
2. Keep default port `9050` or choose your own
|
||||||
|
3. Configure applications to use `127.0.0.1:9050` as SOCKS5 proxy
|
||||||
|
4. Works with SSH, IRC, and other SOCKS-compatible applications
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [I2P Documentation](https://geti2p.net/en/docs)
|
||||||
|
- [i2pd Documentation](https://i2pd.readthedocs.io/)
|
||||||
|
- [go-i2p GitHub](https://github.com/go-i2p)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Found an issue with the examples or want to add more? Please submit a pull request or open an issue on GitHub.
|
||||||
14
examples/client.conf
Normal file
14
examples/client.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Generic Client Tunnel Configuration (i2pd format)
|
||||||
|
# This creates a standard TCP client tunnel for any protocol
|
||||||
|
|
||||||
|
[MyClientTunnel]
|
||||||
|
type = client
|
||||||
|
address = 127.0.0.1
|
||||||
|
port = 7000
|
||||||
|
destination = example.i2p
|
||||||
|
keys = client-keys.dat
|
||||||
|
inbound.length = 3
|
||||||
|
inbound.quantity = 2
|
||||||
|
outbound.length = 3
|
||||||
|
outbound.quantity = 2
|
||||||
|
i2cp.leaseSetEncType = 4,0
|
||||||
28
examples/client.properties
Normal file
28
examples/client.properties
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generic Client Tunnel Configuration (Java I2P format)
|
||||||
|
# This creates a standard TCP client tunnel for any protocol
|
||||||
|
|
||||||
|
# Basic tunnel identification
|
||||||
|
name=MyClientTunnel
|
||||||
|
type=client
|
||||||
|
description=Generic client tunnel for custom protocols
|
||||||
|
|
||||||
|
# Network settings
|
||||||
|
interface=127.0.0.1
|
||||||
|
listenPort=7000
|
||||||
|
|
||||||
|
# Target destination (I2P destination base64 or hostname)
|
||||||
|
targetDestination=example.i2p
|
||||||
|
|
||||||
|
# I2CP options
|
||||||
|
option.i2cp.leaseSetEncType=4,0
|
||||||
|
option.i2cp.closeIdleTime=1800000
|
||||||
|
|
||||||
|
# Tunnel options
|
||||||
|
option.inbound.length=3
|
||||||
|
option.inbound.quantity=2
|
||||||
|
option.outbound.length=3
|
||||||
|
option.outbound.quantity=2
|
||||||
|
|
||||||
|
# Client options
|
||||||
|
option.persistentClientKey=true
|
||||||
|
option.i2ptunnel.gzip=false
|
||||||
30
examples/client.yaml
Normal file
30
examples/client.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Generic Client Tunnel Configuration (YAML format - go-i2p)
|
||||||
|
# This creates a standard TCP client tunnel for any protocol
|
||||||
|
|
||||||
|
tunnels:
|
||||||
|
MyClientTunnel:
|
||||||
|
type: client
|
||||||
|
description: Generic client tunnel for custom protocols
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 7000
|
||||||
|
target: example.i2p
|
||||||
|
persistentKey: true
|
||||||
|
|
||||||
|
# I2CP options
|
||||||
|
i2cp:
|
||||||
|
leaseSetEncType: "4,0"
|
||||||
|
closeIdleTime: 1800000
|
||||||
|
|
||||||
|
# Tunnel configuration options
|
||||||
|
options:
|
||||||
|
gzip: false
|
||||||
|
|
||||||
|
# Inbound tunnel settings
|
||||||
|
inbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 2
|
||||||
|
|
||||||
|
# Outbound tunnel settings
|
||||||
|
outbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 2
|
||||||
14
examples/httpclient.conf
Normal file
14
examples/httpclient.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# HTTP Client Tunnel Configuration (i2pd format)
|
||||||
|
# This creates an HTTP proxy that allows you to access I2P sites (eepsites)
|
||||||
|
|
||||||
|
[MyHTTPProxy]
|
||||||
|
type = http
|
||||||
|
address = 127.0.0.1
|
||||||
|
port = 4444
|
||||||
|
keys = httpclient-keys.dat
|
||||||
|
inbound.length = 3
|
||||||
|
inbound.quantity = 2
|
||||||
|
outbound.length = 3
|
||||||
|
outbound.quantity = 2
|
||||||
|
i2cp.leaseSetEncType = 4,0
|
||||||
|
gzip = true
|
||||||
28
examples/httpclient.properties
Normal file
28
examples/httpclient.properties
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# HTTP Client Tunnel Configuration (Java I2P format)
|
||||||
|
# This creates an HTTP proxy that allows you to access I2P sites (eepsites)
|
||||||
|
# through a local proxy on your computer.
|
||||||
|
|
||||||
|
# Basic tunnel identification
|
||||||
|
name=MyHTTPProxy
|
||||||
|
type=httpclient
|
||||||
|
description=HTTP proxy for accessing eepsites
|
||||||
|
|
||||||
|
# Network settings - where the local proxy will listen
|
||||||
|
interface=127.0.0.1
|
||||||
|
listenPort=4444
|
||||||
|
|
||||||
|
# I2CP options - control how the tunnel connects to the I2P router
|
||||||
|
option.i2cp.leaseSetEncType=4,0
|
||||||
|
option.i2cp.closeIdleTime=1800000
|
||||||
|
option.i2cp.newDestOnResume=false
|
||||||
|
|
||||||
|
# Tunnel options - configure tunnel behavior
|
||||||
|
option.inbound.length=3
|
||||||
|
option.inbound.quantity=2
|
||||||
|
option.outbound.length=3
|
||||||
|
option.outbound.quantity=2
|
||||||
|
|
||||||
|
# Client options
|
||||||
|
option.persistentClientKey=true
|
||||||
|
option.i2ptunnel.httpclient.allowInternalSSL=false
|
||||||
|
option.outbound.randomKey=true
|
||||||
31
examples/httpclient.yaml
Normal file
31
examples/httpclient.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# HTTP Client Tunnel Configuration (YAML format - go-i2p)
|
||||||
|
# This creates an HTTP proxy that allows you to access I2P sites (eepsites)
|
||||||
|
|
||||||
|
tunnels:
|
||||||
|
MyHTTPProxy:
|
||||||
|
type: httpclient
|
||||||
|
description: HTTP proxy for accessing eepsites
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 4444
|
||||||
|
persistentKey: true
|
||||||
|
|
||||||
|
# I2CP options - control how the tunnel connects to the I2P router
|
||||||
|
i2cp:
|
||||||
|
leaseSetEncType: "4,0"
|
||||||
|
closeIdleTime: 1800000
|
||||||
|
newDestOnResume: false
|
||||||
|
|
||||||
|
# Tunnel configuration options
|
||||||
|
options:
|
||||||
|
gzip: true
|
||||||
|
|
||||||
|
# Inbound tunnel settings
|
||||||
|
inbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 2
|
||||||
|
|
||||||
|
# Outbound tunnel settings
|
||||||
|
outbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 2
|
||||||
|
randomKey: true
|
||||||
14
examples/httpserver.conf
Normal file
14
examples/httpserver.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# HTTP Server Tunnel Configuration (i2pd format)
|
||||||
|
# This publishes a local web server as an I2P hidden service (eepsite)
|
||||||
|
|
||||||
|
[MyWebsite]
|
||||||
|
type = http
|
||||||
|
address = 127.0.0.1:8080
|
||||||
|
keys = mywebsite-keys.dat
|
||||||
|
inbound.length = 3
|
||||||
|
inbound.quantity = 3
|
||||||
|
outbound.length = 3
|
||||||
|
outbound.quantity = 3
|
||||||
|
i2cp.leaseSetEncType = 4,0
|
||||||
|
gzip = true
|
||||||
|
hostoverride = mysite.i2p
|
||||||
26
examples/httpserver.properties
Normal file
26
examples/httpserver.properties
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# HTTP Server Tunnel Configuration (Java I2P format)
|
||||||
|
# This publishes a local web server as an I2P hidden service (eepsite)
|
||||||
|
|
||||||
|
# Basic tunnel identification
|
||||||
|
name=MyWebsite
|
||||||
|
type=httpserver
|
||||||
|
description=My personal eepsite
|
||||||
|
|
||||||
|
# Target - the local web server to publish
|
||||||
|
targetHost=127.0.0.1
|
||||||
|
targetPort=8080
|
||||||
|
|
||||||
|
# I2CP options - control destination management
|
||||||
|
option.i2cp.leaseSetEncType=4,0
|
||||||
|
option.i2cp.closeIdleTime=1800000
|
||||||
|
option.i2cp.newDestOnResume=false
|
||||||
|
|
||||||
|
# Tunnel options - configure tunnel behavior
|
||||||
|
option.inbound.length=3
|
||||||
|
option.inbound.quantity=3
|
||||||
|
option.outbound.length=3
|
||||||
|
option.outbound.quantity=3
|
||||||
|
|
||||||
|
# Server options
|
||||||
|
option.persistentClientKey=true
|
||||||
|
spoofedHost=mysite.i2p
|
||||||
30
examples/httpserver.yaml
Normal file
30
examples/httpserver.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# HTTP Server Tunnel Configuration (YAML format - go-i2p)
|
||||||
|
# This publishes a local web server as an I2P hidden service (eepsite)
|
||||||
|
|
||||||
|
tunnels:
|
||||||
|
MyWebsite:
|
||||||
|
type: httpserver
|
||||||
|
description: My personal eepsite
|
||||||
|
target: 127.0.0.1:8080
|
||||||
|
persistentKey: true
|
||||||
|
|
||||||
|
# I2CP options - control destination management
|
||||||
|
i2cp:
|
||||||
|
leaseSetEncType: "4,0"
|
||||||
|
closeIdleTime: 1800000
|
||||||
|
newDestOnResume: false
|
||||||
|
|
||||||
|
# Tunnel configuration options
|
||||||
|
options:
|
||||||
|
gzip: true
|
||||||
|
spoofedHost: mysite.i2p
|
||||||
|
|
||||||
|
# Inbound tunnel settings
|
||||||
|
inbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 3
|
||||||
|
|
||||||
|
# Outbound tunnel settings
|
||||||
|
outbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 3
|
||||||
12
examples/server.conf
Normal file
12
examples/server.conf
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Generic Server Tunnel Configuration (i2pd format)
|
||||||
|
# This publishes a local TCP service as an I2P hidden service
|
||||||
|
|
||||||
|
[MyServerTunnel]
|
||||||
|
type = server
|
||||||
|
address = 127.0.0.1:9000
|
||||||
|
keys = server-keys.dat
|
||||||
|
inbound.length = 3
|
||||||
|
inbound.quantity = 3
|
||||||
|
outbound.length = 3
|
||||||
|
outbound.quantity = 3
|
||||||
|
i2cp.leaseSetEncType = 4,0
|
||||||
25
examples/server.properties
Normal file
25
examples/server.properties
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generic Server Tunnel Configuration (Java I2P format)
|
||||||
|
# This publishes a local TCP service as an I2P hidden service
|
||||||
|
|
||||||
|
# Basic tunnel identification
|
||||||
|
name=MyServerTunnel
|
||||||
|
type=server
|
||||||
|
description=Generic server tunnel for custom protocols
|
||||||
|
|
||||||
|
# Target - the local service to publish
|
||||||
|
targetHost=127.0.0.1
|
||||||
|
targetPort=9000
|
||||||
|
|
||||||
|
# I2CP options
|
||||||
|
option.i2cp.leaseSetEncType=4,0
|
||||||
|
option.i2cp.closeIdleTime=1800000
|
||||||
|
|
||||||
|
# Tunnel options
|
||||||
|
option.inbound.length=3
|
||||||
|
option.inbound.quantity=3
|
||||||
|
option.outbound.length=3
|
||||||
|
option.outbound.quantity=3
|
||||||
|
|
||||||
|
# Server options
|
||||||
|
option.persistentClientKey=true
|
||||||
|
option.i2ptunnel.gzip=false
|
||||||
28
examples/server.yaml
Normal file
28
examples/server.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generic Server Tunnel Configuration (YAML format - go-i2p)
|
||||||
|
# This publishes a local TCP service as an I2P hidden service
|
||||||
|
|
||||||
|
tunnels:
|
||||||
|
MyServerTunnel:
|
||||||
|
type: server
|
||||||
|
description: Generic server tunnel for custom protocols
|
||||||
|
target: 127.0.0.1:9000
|
||||||
|
persistentKey: true
|
||||||
|
|
||||||
|
# I2CP options
|
||||||
|
i2cp:
|
||||||
|
leaseSetEncType: "4,0"
|
||||||
|
closeIdleTime: 1800000
|
||||||
|
|
||||||
|
# Tunnel configuration options
|
||||||
|
options:
|
||||||
|
gzip: false
|
||||||
|
|
||||||
|
# Inbound tunnel settings
|
||||||
|
inbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 3
|
||||||
|
|
||||||
|
# Outbound tunnel settings
|
||||||
|
outbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 3
|
||||||
14
examples/socks.conf
Normal file
14
examples/socks.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# SOCKS Tunnel Configuration (i2pd format)
|
||||||
|
# This creates a SOCKS proxy that provides access to I2P network services
|
||||||
|
|
||||||
|
[MySOCKSProxy]
|
||||||
|
type = socks
|
||||||
|
address = 127.0.0.1
|
||||||
|
port = 9050
|
||||||
|
keys = socksproxy-keys.dat
|
||||||
|
inbound.length = 3
|
||||||
|
inbound.quantity = 2
|
||||||
|
outbound.length = 3
|
||||||
|
outbound.quantity = 2
|
||||||
|
i2cp.leaseSetEncType = 4,0
|
||||||
|
gzip = true
|
||||||
26
examples/socks.properties
Normal file
26
examples/socks.properties
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# SOCKS Tunnel Configuration (Java I2P format)
|
||||||
|
# This creates a SOCKS proxy that provides access to I2P network services
|
||||||
|
|
||||||
|
# Basic tunnel identification
|
||||||
|
name=MySOCKSProxy
|
||||||
|
type=sockstunnel
|
||||||
|
description=SOCKS5 proxy for I2P network access
|
||||||
|
|
||||||
|
# Network settings - where the SOCKS proxy will listen
|
||||||
|
interface=127.0.0.1
|
||||||
|
listenPort=9050
|
||||||
|
|
||||||
|
# I2CP options
|
||||||
|
option.i2cp.leaseSetEncType=4,0
|
||||||
|
option.i2cp.closeIdleTime=1800000
|
||||||
|
|
||||||
|
# Tunnel options
|
||||||
|
option.inbound.length=3
|
||||||
|
option.inbound.quantity=2
|
||||||
|
option.outbound.length=3
|
||||||
|
option.outbound.quantity=2
|
||||||
|
|
||||||
|
# SOCKS-specific options
|
||||||
|
option.persistentClientKey=true
|
||||||
|
option.outbound.randomKey=true
|
||||||
|
option.i2ptunnel.gzip=true
|
||||||
30
examples/socks.yaml
Normal file
30
examples/socks.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# SOCKS Tunnel Configuration (YAML format - go-i2p)
|
||||||
|
# This creates a SOCKS proxy that provides access to I2P network services
|
||||||
|
|
||||||
|
tunnels:
|
||||||
|
MySOCKSProxy:
|
||||||
|
type: sockstunnel
|
||||||
|
description: SOCKS5 proxy for I2P network access
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 9050
|
||||||
|
persistentKey: true
|
||||||
|
|
||||||
|
# I2CP options
|
||||||
|
i2cp:
|
||||||
|
leaseSetEncType: "4,0"
|
||||||
|
closeIdleTime: 1800000
|
||||||
|
|
||||||
|
# Tunnel configuration options
|
||||||
|
options:
|
||||||
|
gzip: true
|
||||||
|
|
||||||
|
# Inbound tunnel settings
|
||||||
|
inbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 2
|
||||||
|
|
||||||
|
# Outbound tunnel settings
|
||||||
|
outbound:
|
||||||
|
length: 3
|
||||||
|
quantity: 2
|
||||||
|
randomKey: true
|
||||||
@@ -145,7 +145,7 @@ func TestCrossFormatConversion(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestYAMLParser tests the parseYAML function specifically (0% coverage)
|
// TestYAMLParser tests the parseYAML function with the nested tunnels format
|
||||||
func TestYAMLParser(t *testing.T) {
|
func TestYAMLParser(t *testing.T) {
|
||||||
converter := &Converter{}
|
converter := &Converter{}
|
||||||
|
|
||||||
@@ -156,26 +156,27 @@ func TestYAMLParser(t *testing.T) {
|
|||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid_yaml_direct_format",
|
name: "valid_yaml_nested_format",
|
||||||
input: `name: HttpProxy
|
input: `tunnels:
|
||||||
type: httpclient
|
HttpProxy:
|
||||||
interface: 127.0.0.1
|
type: httpclient
|
||||||
port: 4444
|
interface: 127.0.0.1
|
||||||
target: example.i2p
|
port: 4444
|
||||||
description: HTTP proxy tunnel
|
target: example.i2p
|
||||||
persistentKey: true
|
description: HTTP proxy tunnel
|
||||||
i2cp:
|
persistentKey: true
|
||||||
leaseSetEncType:
|
i2cp:
|
||||||
- "4"
|
leaseSetEncType:
|
||||||
- "0"
|
- "4"
|
||||||
reduceIdleTime: 900000
|
- "0"
|
||||||
options:
|
reduceIdleTime: 900000
|
||||||
proxyList: "proxy1.i2p,proxy2.i2p"
|
options:
|
||||||
sharedClient: true
|
proxyList: "proxy1.i2p,proxy2.i2p"
|
||||||
inbound:
|
sharedClient: true
|
||||||
length: 3
|
inbound:
|
||||||
outbound:
|
length: 3
|
||||||
length: 2`,
|
outbound:
|
||||||
|
length: 2`,
|
||||||
expected: &TunnelConfig{
|
expected: &TunnelConfig{
|
||||||
Name: "HttpProxy",
|
Name: "HttpProxy",
|
||||||
Type: "httpclient",
|
Type: "httpclient",
|
||||||
@@ -203,8 +204,9 @@ outbound:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "minimal_yaml_tunnel",
|
name: "minimal_yaml_tunnel",
|
||||||
input: `name: MinimalTunnel
|
input: `tunnels:
|
||||||
type: client`,
|
MinimalTunnel:
|
||||||
|
type: client`,
|
||||||
expected: &TunnelConfig{
|
expected: &TunnelConfig{
|
||||||
Name: "MinimalTunnel",
|
Name: "MinimalTunnel",
|
||||||
Type: "client",
|
Type: "client",
|
||||||
@@ -213,19 +215,20 @@ type: client`,
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid_yaml_syntax",
|
name: "invalid_yaml_syntax",
|
||||||
input: `name: BadTunnel
|
input: `tunnels:
|
||||||
type: httpclient
|
BadTunnel:
|
||||||
port: "not_a_number"
|
type: httpclient
|
||||||
invalid_yaml: [
|
port: "not_a_number"
|
||||||
- missing closing bracket`,
|
invalid_yaml: [
|
||||||
|
- missing closing bracket`,
|
||||||
expected: nil,
|
expected: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty_yaml",
|
name: "empty_tunnels_map",
|
||||||
input: "",
|
input: "tunnels: {}",
|
||||||
expected: &TunnelConfig{},
|
expected: nil,
|
||||||
wantErr: false,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +423,12 @@ description=Load test tunnel`
|
|||||||
t.Errorf("LoadConfig() Port = %d, want %d", config.Port, 8080)
|
t.Errorf("LoadConfig() Port = %d, want %d", config.Port, 8080)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test loading YAML file (direct format, not wrapped)
|
// Test loading YAML file (nested format with tunnels map)
|
||||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||||
yamlContent := `name: YamlTest
|
yamlContent := `tunnels:
|
||||||
type: server
|
YamlTest:
|
||||||
port: 9090`
|
type: server
|
||||||
|
port: 9090`
|
||||||
err = os.WriteFile(yamlFile, []byte(yamlContent), 0644)
|
err = os.WriteFile(yamlFile, []byte(yamlContent), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||||
|
|||||||
287
lib/examples_test.go
Normal file
287
lib/examples_test.go
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
package i2pconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestExampleFiles validates all example configuration files in the examples directory.
|
||||||
|
// This ensures that the provided examples are syntactically correct and can be parsed.
|
||||||
|
func TestExampleFiles(t *testing.T) {
|
||||||
|
examplesDir := filepath.Join("..", "examples")
|
||||||
|
|
||||||
|
// Check if examples directory exists
|
||||||
|
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
|
||||||
|
t.Skip("examples directory not found, skipping example validation tests")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
format string
|
||||||
|
}{
|
||||||
|
// Properties format examples
|
||||||
|
{"httpclient.properties", "properties"},
|
||||||
|
{"httpserver.properties", "properties"},
|
||||||
|
{"socks.properties", "properties"},
|
||||||
|
{"client.properties", "properties"},
|
||||||
|
{"server.properties", "properties"},
|
||||||
|
|
||||||
|
// INI format examples
|
||||||
|
{"httpclient.conf", "ini"},
|
||||||
|
{"httpserver.conf", "ini"},
|
||||||
|
{"socks.conf", "ini"},
|
||||||
|
{"client.conf", "ini"},
|
||||||
|
{"server.conf", "ini"},
|
||||||
|
|
||||||
|
// YAML format examples
|
||||||
|
{"httpclient.yaml", "yaml"},
|
||||||
|
{"httpserver.yaml", "yaml"},
|
||||||
|
{"socks.yaml", "yaml"},
|
||||||
|
{"client.yaml", "yaml"},
|
||||||
|
{"server.yaml", "yaml"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
filePath := filepath.Join(examplesDir, tc.name)
|
||||||
|
|
||||||
|
// Read the example file
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read example file %s: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the configuration
|
||||||
|
conv := &Converter{strict: false}
|
||||||
|
config, err := conv.ParseInput(data, tc.format)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse example file %s: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the configuration
|
||||||
|
if err := conv.validate(config); err != nil {
|
||||||
|
t.Errorf("Example file %s failed validation: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify basic required fields
|
||||||
|
if config.Name == "" {
|
||||||
|
t.Errorf("Example file %s has empty name", tc.name)
|
||||||
|
}
|
||||||
|
if config.Type == "" {
|
||||||
|
t.Errorf("Example file %s has empty type", tc.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExampleFileConversion verifies that example files can be converted between formats.
|
||||||
|
// This ensures cross-format compatibility and that conversions maintain data integrity.
|
||||||
|
// Note: YAML format has a nested structure (tunnels: map) so conversions TO YAML
|
||||||
|
// produce valid output, but may not round-trip perfectly without special handling.
|
||||||
|
func TestExampleFileConversion(t *testing.T) {
|
||||||
|
examplesDir := filepath.Join("..", "examples")
|
||||||
|
|
||||||
|
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
|
||||||
|
t.Skip("examples directory not found, skipping conversion tests")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
sourceFile string
|
||||||
|
sourceFormat string
|
||||||
|
targetFormat string
|
||||||
|
}{
|
||||||
|
// Properties and INI formats are flat and convert well in both directions
|
||||||
|
{"httpclient.properties", "properties", "ini"},
|
||||||
|
{"httpserver.conf", "ini", "properties"},
|
||||||
|
// YAML format converts FROM yaml to other formats reliably
|
||||||
|
{"socks.yaml", "yaml", "properties"},
|
||||||
|
{"socks.yaml", "yaml", "ini"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.sourceFile+"_to_"+tc.targetFormat, func(t *testing.T) {
|
||||||
|
filePath := filepath.Join(examplesDir, tc.sourceFile)
|
||||||
|
|
||||||
|
// Read source file
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read source file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to target format
|
||||||
|
conv := &Converter{strict: false}
|
||||||
|
output, err := conv.Convert(data, tc.sourceFormat, tc.targetFormat)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Conversion failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify output is not empty
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Error("Conversion produced empty output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the converted output
|
||||||
|
config, err := conv.ParseInput(output, tc.targetFormat)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse converted output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the converted configuration
|
||||||
|
if err := conv.validate(config); err != nil {
|
||||||
|
t.Errorf("Converted configuration failed validation: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExampleFileRoundTrip verifies that converting an example file to another format
|
||||||
|
// and back produces equivalent configuration (round-trip conversion).
|
||||||
|
// With the nested YAML format, all formats now support proper round-trip conversion.
|
||||||
|
func TestExampleFileRoundTrip(t *testing.T) {
|
||||||
|
examplesDir := filepath.Join("..", "examples")
|
||||||
|
|
||||||
|
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
|
||||||
|
t.Skip("examples directory not found, skipping round-trip tests")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
file string
|
||||||
|
format string
|
||||||
|
intermediate string
|
||||||
|
}{
|
||||||
|
// Properties <-> INI conversions work well in both directions
|
||||||
|
{"httpclient.properties", "properties", "ini"},
|
||||||
|
{"httpserver.conf", "ini", "properties"},
|
||||||
|
{"client.properties", "properties", "ini"},
|
||||||
|
// Properties/INI <-> YAML conversions now work with nested format
|
||||||
|
{"httpclient.properties", "properties", "yaml"},
|
||||||
|
{"socks.conf", "ini", "yaml"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.file+"_via_"+tc.intermediate, func(t *testing.T) {
|
||||||
|
filePath := filepath.Join(examplesDir, tc.file)
|
||||||
|
|
||||||
|
// Read original file
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv := &Converter{strict: false}
|
||||||
|
|
||||||
|
// Parse original
|
||||||
|
original, err := conv.ParseInput(data, tc.format)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse original: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to intermediate format
|
||||||
|
intermediate, err := conv.generateOutput(original, tc.intermediate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to convert to intermediate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse intermediate
|
||||||
|
intermediateConfig, err := conv.ParseInput(intermediate, tc.intermediate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse intermediate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to original format
|
||||||
|
final, err := conv.generateOutput(intermediateConfig, tc.format)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to convert back to original: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse final result
|
||||||
|
finalConfig, err := conv.ParseInput(final, tc.format)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse final: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare key fields between original and final
|
||||||
|
if original.Name != finalConfig.Name {
|
||||||
|
t.Errorf("Name mismatch: original=%s, final=%s", original.Name, finalConfig.Name)
|
||||||
|
}
|
||||||
|
if original.Type != finalConfig.Type {
|
||||||
|
t.Errorf("Type mismatch: original=%s, final=%s", original.Type, finalConfig.Type)
|
||||||
|
}
|
||||||
|
if original.Port != finalConfig.Port {
|
||||||
|
t.Errorf("Port mismatch: original=%d, final=%d", original.Port, finalConfig.Port)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExampleFilesAgainstTunnelTypes verifies that each example file uses
|
||||||
|
// a valid tunnel type and meets the requirements for that type.
|
||||||
|
func TestExampleFilesAgainstTunnelTypes(t *testing.T) {
|
||||||
|
examplesDir := filepath.Join("..", "examples")
|
||||||
|
|
||||||
|
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
|
||||||
|
t.Skip("examples directory not found, skipping tunnel type tests")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
file string
|
||||||
|
format string
|
||||||
|
tunnelType string
|
||||||
|
requirePort bool
|
||||||
|
requireTarget bool
|
||||||
|
}{
|
||||||
|
{"httpclient.properties", "properties", "httpclient", true, false},
|
||||||
|
{"httpclient.conf", "ini", "http", true, false},
|
||||||
|
{"httpclient.yaml", "yaml", "httpclient", true, false},
|
||||||
|
{"httpserver.properties", "properties", "httpserver", false, true},
|
||||||
|
{"httpserver.conf", "ini", "http", false, true},
|
||||||
|
{"httpserver.yaml", "yaml", "httpserver", false, true},
|
||||||
|
{"socks.properties", "properties", "sockstunnel", true, false},
|
||||||
|
{"socks.conf", "ini", "socks", true, false},
|
||||||
|
{"socks.yaml", "yaml", "sockstunnel", true, false},
|
||||||
|
{"client.properties", "properties", "client", true, false},
|
||||||
|
{"client.conf", "ini", "client", true, false},
|
||||||
|
{"client.yaml", "yaml", "client", true, false},
|
||||||
|
{"server.properties", "properties", "server", false, true},
|
||||||
|
{"server.conf", "ini", "server", false, true},
|
||||||
|
{"server.yaml", "yaml", "server", false, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.file, func(t *testing.T) {
|
||||||
|
filePath := filepath.Join(examplesDir, tc.file)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv := &Converter{strict: false}
|
||||||
|
config, err := conv.ParseInput(data, tc.format)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: tunnel types may vary between formats (e.g., "http" in i2pd vs "httpclient" in Java I2P)
|
||||||
|
// We check if the type is reasonable but don't enforce exact matches across formats
|
||||||
|
if config.Type == "" {
|
||||||
|
t.Error("Tunnel type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check port requirement
|
||||||
|
if tc.requirePort && config.Port == 0 {
|
||||||
|
t.Errorf("Expected port to be set for %s tunnel type", tc.tunnelType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check target requirement
|
||||||
|
if tc.requireTarget && config.Target == "" {
|
||||||
|
t.Errorf("Expected target to be set for %s tunnel type", tc.tunnelType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -173,7 +173,7 @@ type client`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestYAMLParserErrors tests enhanced error reporting for YAML format
|
// TestYAMLParserErrors tests enhanced error reporting for YAML format (nested structure)
|
||||||
func TestYAMLParserErrors(t *testing.T) {
|
func TestYAMLParserErrors(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -184,32 +184,37 @@ func TestYAMLParserErrors(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid yaml",
|
name: "valid yaml",
|
||||||
input: `name: test-tunnel
|
input: `tunnels:
|
||||||
type: client
|
test-tunnel:
|
||||||
interface: 127.0.0.1
|
type: client
|
||||||
port: 4444`,
|
interface: 127.0.0.1
|
||||||
|
port: 4444`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid indentation",
|
name: "invalid indentation",
|
||||||
input: `name: test
|
input: `tunnels:
|
||||||
type: client
|
test:
|
||||||
interface: 127.0.0.1`,
|
type: client
|
||||||
|
interface: 127.0.0.1`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectParseErr: true,
|
expectParseErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid yaml structure",
|
name: "invalid yaml structure",
|
||||||
input: `name: test
|
input: `tunnels:
|
||||||
type client`,
|
test
|
||||||
|
type: client`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectParseErr: true,
|
expectParseErr: true,
|
||||||
expectLineNum: 3, // YAML error reports line 3 (end of file)
|
expectLineNum: 3, // YAML error reports line 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unclosed quote",
|
name: "unclosed quote",
|
||||||
input: `name: "test
|
input: `tunnels:
|
||||||
type: client`,
|
test:
|
||||||
|
name: "test
|
||||||
|
type: client`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectParseErr: true,
|
expectParseErr: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -331,8 +331,19 @@ func (c *Converter) generateJavaProperties(config *TunnelConfig) ([]byte, error)
|
|||||||
// formatPropertyValue formats a property value for output
|
// formatPropertyValue formats a property value for output
|
||||||
// Arrays/slices are formatted as comma-separated values
|
// Arrays/slices are formatted as comma-separated values
|
||||||
func formatPropertyValue(v interface{}) string {
|
func formatPropertyValue(v interface{}) string {
|
||||||
|
// Handle []string
|
||||||
if slice, ok := v.([]string); ok {
|
if slice, ok := v.([]string); ok {
|
||||||
return strings.Join(slice, ",")
|
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)
|
return fmt.Sprint(v)
|
||||||
}
|
}
|
||||||
|
|||||||
29
lib/yaml.go
29
lib/yaml.go
@@ -7,13 +7,33 @@ import (
|
|||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// parseYAML parses YAML using the standard nested structure with "tunnels" map.
|
||||||
|
// This is the go-i2p format where tunnels are defined in a "tunnels" map.
|
||||||
|
// The parser extracts the first tunnel for single-tunnel conversion workflows.
|
||||||
|
// The tunnel name is set from the map key.
|
||||||
func (c *Converter) parseYAML(input []byte) (*TunnelConfig, error) {
|
func (c *Converter) parseYAML(input []byte) (*TunnelConfig, error) {
|
||||||
config := &TunnelConfig{}
|
type wrapper struct {
|
||||||
err := yaml.Unmarshal(input, config)
|
Tunnels map[string]*TunnelConfig `yaml:"tunnels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var w wrapper
|
||||||
|
err := yaml.Unmarshal(input, &w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, c.enhanceYAMLError(input, err)
|
return nil, c.enhanceYAMLError(input, err)
|
||||||
}
|
}
|
||||||
return config, nil
|
|
||||||
|
// Extract the first tunnel (for single-tunnel conversion)
|
||||||
|
if len(w.Tunnels) == 0 {
|
||||||
|
return nil, fmt.Errorf("yaml: no tunnels found in tunnels map")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first tunnel with its name set from the map key
|
||||||
|
for name, config := range w.Tunnels {
|
||||||
|
config.Name = name
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("yaml: failed to extract tunnel from nested structure")
|
||||||
}
|
}
|
||||||
|
|
||||||
// enhanceYAMLError wraps YAML parsing errors with line context.
|
// enhanceYAMLError wraps YAML parsing errors with line context.
|
||||||
@@ -60,6 +80,9 @@ func (c *Converter) enhanceYAMLError(input []byte, err error) error {
|
|||||||
return fmt.Errorf("yaml parse error: %w", err)
|
return fmt.Errorf("yaml parse error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateYAML creates YAML output in the standard nested structure format.
|
||||||
|
// This is the go-i2p format where tunnels are defined in a "tunnels" map.
|
||||||
|
// The tunnel is keyed by its name in the map, allowing for future multi-tunnel support.
|
||||||
func (c *Converter) generateYAML(config *TunnelConfig) ([]byte, error) {
|
func (c *Converter) generateYAML(config *TunnelConfig) ([]byte, error) {
|
||||||
type wrapper struct {
|
type wrapper struct {
|
||||||
Tunnels map[string]*TunnelConfig `yaml:"tunnels"`
|
Tunnels map[string]*TunnelConfig `yaml:"tunnels"`
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -13,7 +13,7 @@ func main() {
|
|||||||
cmd := &cli.App{
|
cmd := &cli.App{
|
||||||
Name: "go-i2ptunnel-config",
|
Name: "go-i2ptunnel-config",
|
||||||
Usage: "Convert I2P tunnel configurations between formats",
|
Usage: "Convert I2P tunnel configurations between formats",
|
||||||
Version: "1.0.0",
|
Version: "0.33.0",
|
||||||
Description: `A command line utility to convert I2P tunnel configurations between Java I2P, i2pd, and go-i2p formats.
|
Description: `A command line utility to convert I2P tunnel configurations between Java I2P, i2pd, and go-i2p formats.
|
||||||
|
|
||||||
Supports automatic format detection based on file extensions:
|
Supports automatic format detection based on file extensions:
|
||||||
|
|||||||
Reference in New Issue
Block a user