Expand the common package some more to include reply parser from goSam, move version to smarter structure
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
*~
|
||||
*.swp
|
||||
README.md.asc
|
||||
log
|
60
common/formatter.go
Normal file
60
common/formatter.go
Normal file
@ -0,0 +1,60 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SAMFormatter struct {
|
||||
Version ProtocolVersion
|
||||
}
|
||||
|
||||
// Common SAM protocol message types
|
||||
const (
|
||||
HelloMsg = "HELLO"
|
||||
SessionMsg = "SESSION"
|
||||
StreamMsg = "STREAM"
|
||||
DatagramMsg = "DATAGRAM"
|
||||
RawMsg = "RAW"
|
||||
PrimaryMSG = "PRIMARY"
|
||||
NamingMsg = "NAMING"
|
||||
)
|
||||
|
||||
func NewSAMFormatter(version ProtocolVersion) *SAMFormatter {
|
||||
return &SAMFormatter{Version: version}
|
||||
}
|
||||
|
||||
// FormatHello formats the initial handshake message
|
||||
func (f *SAMFormatter) FormatHello() string {
|
||||
return fmt.Sprintf("HELLO VERSION MIN=%s MAX=%s\n", f.Version, f.Version)
|
||||
}
|
||||
|
||||
// FormatSession formats a session creation message
|
||||
func (f *SAMFormatter) FormatSession(style, id string, options map[string]string) string {
|
||||
optStr := formatOptions(options)
|
||||
return fmt.Sprintf("SESSION CREATE STYLE=%s ID=%s%s\n", style, id, optStr)
|
||||
}
|
||||
|
||||
// FormatDatagram formats a datagram message
|
||||
func (f *SAMFormatter) FormatDatagram(sessionID, dest string, options map[string]string) string {
|
||||
optStr := formatOptions(options)
|
||||
return fmt.Sprintf("DATAGRAM SEND ID=%s DESTINATION=%s%s\n", sessionID, dest, optStr)
|
||||
}
|
||||
|
||||
// FormatNamingLookup formats a naming lookup message
|
||||
func (f *SAMFormatter) FormatNamingLookup(name string) string {
|
||||
return fmt.Sprintf("NAMING LOOKUP NAME=%s\n", name)
|
||||
}
|
||||
|
||||
// Helper function to format options
|
||||
func formatOptions(options map[string]string) string {
|
||||
if len(options) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var opts []string
|
||||
for k, v := range options {
|
||||
opts = append(opts, fmt.Sprintf(" %s=%s", k, v))
|
||||
}
|
||||
return strings.Join(opts, "")
|
||||
}
|
87
common/reply.go
Normal file
87
common/reply.go
Normal file
@ -0,0 +1,87 @@
|
||||
// common/reply.go
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Reply represents a parsed SAM bridge response
|
||||
type Reply struct {
|
||||
Topic string // e.g., "HELLO", "SESSION", "STREAM", etc.
|
||||
Type string // Usually "REPLY"
|
||||
Result string // "OK" or error message
|
||||
KeyValues map[string]string // Additional key-value pairs in the response
|
||||
}
|
||||
|
||||
// ParseReply parses a raw SAM bridge response into a structured Reply
|
||||
func ParseReply(response string) (*Reply, error) {
|
||||
parts := strings.Fields(response)
|
||||
if len(parts) < 3 {
|
||||
return nil, fmt.Errorf("invalid reply format: %s", response)
|
||||
}
|
||||
|
||||
reply := &Reply{
|
||||
Topic: parts[0],
|
||||
Type: parts[1],
|
||||
KeyValues: make(map[string]string),
|
||||
}
|
||||
|
||||
// Parse remaining key=value pairs
|
||||
for _, part := range parts[2:] {
|
||||
if kv := strings.SplitN(part, "=", 2); len(kv) == 2 {
|
||||
key := strings.ToUpper(kv[0])
|
||||
if key == "RESULT" {
|
||||
reply.Result = kv[1]
|
||||
} else {
|
||||
reply.KeyValues[key] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reply.Result == "" {
|
||||
return nil, fmt.Errorf("missing RESULT in reply: %s", response)
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
// IsOk returns true if the reply indicates success
|
||||
func (r *Reply) IsOk() bool {
|
||||
return r.Result == "OK"
|
||||
}
|
||||
|
||||
// Error returns an error if the reply indicates failure
|
||||
func (r *Reply) Error() error {
|
||||
if r.IsOk() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s failed: %s", r.Topic, r.Result)
|
||||
}
|
||||
|
||||
// Value safely retrieves a value from KeyValues
|
||||
func (r *Reply) Value(key string) (string, bool) {
|
||||
v, ok := r.KeyValues[strings.ToUpper(key)]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// MustValue gets a value or panics if not found
|
||||
func (r *Reply) MustValue(key string) string {
|
||||
if v, ok := r.Value(key); ok {
|
||||
return v
|
||||
}
|
||||
panic(fmt.Sprintf("required key not found: %s", key))
|
||||
}
|
||||
|
||||
// Specific reply type checkers
|
||||
func (r *Reply) IsHello() bool {
|
||||
return r.Topic == HelloMsg && r.Type == "REPLY"
|
||||
}
|
||||
|
||||
func (r *Reply) IsSession() bool {
|
||||
return r.Topic == SessionMsg && r.Type == "REPLY"
|
||||
}
|
||||
|
||||
func (r *Reply) IsNaming() bool {
|
||||
return r.Topic == NamingMsg && r.Type == "REPLY"
|
||||
}
|
136
common/udp.go
136
common/udp.go
@ -34,37 +34,37 @@ import (
|
||||
|
||||
// UDPSessionConfig holds all UDP session configuration
|
||||
type UDPSessionConfig struct {
|
||||
Port int
|
||||
ParentConn net.Conn
|
||||
Log *logger.Logger
|
||||
DefaultPort int
|
||||
AllowZeroPort bool
|
||||
Style string
|
||||
FromPort string
|
||||
ToPort string
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
Port int
|
||||
ParentConn net.Conn
|
||||
Log *logger.Logger
|
||||
DefaultPort int
|
||||
AllowZeroPort bool
|
||||
Style string
|
||||
FromPort string
|
||||
ToPort string
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
}
|
||||
|
||||
// UDPSession represents an established UDP session
|
||||
type UDPSession struct {
|
||||
LocalAddr *net.UDPAddr
|
||||
RemoteAddr *net.UDPAddr
|
||||
Conn *net.UDPConn
|
||||
LocalAddr *net.UDPAddr
|
||||
RemoteAddr *net.UDPAddr
|
||||
Conn *net.UDPConn
|
||||
}
|
||||
|
||||
func (u *UDPSession) SetReadTimeout(timeout time.Duration) error {
|
||||
if u.Conn != nil {
|
||||
return u.Conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
}
|
||||
return nil
|
||||
if u.Conn != nil {
|
||||
return u.Conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UDPSession) SetWriteTimeout(timeout time.Duration) error {
|
||||
if u.Conn != nil {
|
||||
return u.Conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
}
|
||||
return nil
|
||||
if u.Conn != nil {
|
||||
return u.Conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u UDPSession) LocalPort() int {
|
||||
@ -77,64 +77,64 @@ func (u UDPSession) Close() {
|
||||
|
||||
// NewUDPSession creates and configures a new UDP session
|
||||
func NewUDPSession(cfg *UDPSessionConfig) (*UDPSession, error) {
|
||||
if err := validatePort(cfg.Port, cfg.AllowZeroPort); err != nil {
|
||||
cfg.Log.WithError(err).Error("Invalid UDP port configuration")
|
||||
return nil, err
|
||||
}
|
||||
if err := validatePort(cfg.Port, cfg.AllowZeroPort); err != nil {
|
||||
cfg.Log.WithError(err).Error("Invalid UDP port configuration")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port := cfg.Port
|
||||
if port == 0 {
|
||||
port = cfg.DefaultPort
|
||||
cfg.Log.WithField("port", port).Debug("Using default UDP port")
|
||||
}
|
||||
port := cfg.Port
|
||||
if port == 0 {
|
||||
port = cfg.DefaultPort
|
||||
cfg.Log.WithField("port", port).Debug("Using default UDP port")
|
||||
}
|
||||
|
||||
laddr, raddr, err := resolveAddresses(cfg.ParentConn, port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("address resolution failed: %w", err)
|
||||
}
|
||||
laddr, raddr, err := resolveAddresses(cfg.ParentConn, port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("address resolution failed: %w", err)
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp4", laddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UDP listen failed: %w", err)
|
||||
}
|
||||
conn, err := net.ListenUDP("udp4", laddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UDP listen failed: %w", err)
|
||||
}
|
||||
|
||||
return &UDPSession{
|
||||
LocalAddr: laddr,
|
||||
RemoteAddr: raddr,
|
||||
Conn: conn,
|
||||
}, nil
|
||||
return &UDPSession{
|
||||
LocalAddr: laddr,
|
||||
RemoteAddr: raddr,
|
||||
Conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validatePort(port int, allowZero bool) error {
|
||||
if port < 0 || port > 65535 {
|
||||
return errors.New("port must be between 0-65535")
|
||||
}
|
||||
if port == 0 && !allowZero {
|
||||
return errors.New("port 0 not allowed in this context")
|
||||
}
|
||||
return nil
|
||||
if port < 0 || port > 65535 {
|
||||
return errors.New("port must be between 0-65535")
|
||||
}
|
||||
if port == 0 && !allowZero {
|
||||
return errors.New("port 0 not allowed in this context")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAddresses(parent net.Conn, remotePort int) (*net.UDPAddr, *net.UDPAddr, error) {
|
||||
lhost, _, err := net.SplitHostPort(parent.LocalAddr().String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
lhost, _, err := net.SplitHostPort(parent.LocalAddr().String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
laddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
laddr, err := net.ResolveUDPAddr("udp4", lhost+":0")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rhost, _, err := net.SplitHostPort(parent.RemoteAddr().String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rhost, _, err := net.SplitHostPort(parent.RemoteAddr().String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
raddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(remotePort))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
raddr, err := net.ResolveUDPAddr("udp4", rhost+":"+strconv.Itoa(remotePort))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return laddr, raddr, nil
|
||||
}
|
||||
return laddr, raddr, nil
|
||||
}
|
||||
|
19
common/version.go
Normal file
19
common/version.go
Normal file
@ -0,0 +1,19 @@
|
||||
package common
|
||||
|
||||
type ProtocolVersion string
|
||||
|
||||
type Version struct {
|
||||
String ProtocolVersion
|
||||
Number float64
|
||||
}
|
||||
|
||||
var (
|
||||
SAM31Version = Version{
|
||||
String: "3.1",
|
||||
Number: 3.1,
|
||||
}
|
||||
SAM33Version = Version{
|
||||
String: "3.3",
|
||||
Number: 3.3,
|
||||
}
|
||||
)
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
"github.com/go-i2p/sam3/common"
|
||||
)
|
||||
|
||||
const DEFAULT_LEASESET_TYPE = "i2cp.leaseSetEncType=4"
|
||||
@ -103,6 +104,7 @@ func (f *I2PConfig) SetSAMAddress(addr string) {
|
||||
"host": f.SamHost,
|
||||
"port": f.SamPort,
|
||||
}).Debug("SAM address set")
|
||||
i2pkeys.DefaultSAMAddress = f.Sam()
|
||||
}
|
||||
|
||||
// ID returns the tunnel name in the form of "ID=name"
|
||||
@ -140,7 +142,7 @@ func (f *I2PConfig) Leasesetsettings() (string, string, string) {
|
||||
|
||||
// FromPort returns the from port setting in the form of "FROM_PORT=port"
|
||||
func (f *I2PConfig) FromPort() string {
|
||||
if f.samMax() < 3.1 {
|
||||
if f.samMax() < common.SAM31Version.Number {
|
||||
log.Debug("SAM version < 3.1, FromPort not applicable")
|
||||
return ""
|
||||
}
|
||||
@ -154,7 +156,7 @@ func (f *I2PConfig) FromPort() string {
|
||||
|
||||
// ToPort returns the to port setting in the form of "TO_PORT=port"
|
||||
func (f *I2PConfig) ToPort() string {
|
||||
if f.samMax() < 3.1 {
|
||||
if f.samMax() < common.SAM31Version.Number {
|
||||
log.Debug("SAM version < 3.1, ToPort not applicable")
|
||||
return ""
|
||||
}
|
||||
@ -218,7 +220,7 @@ func (f *I2PConfig) DestinationKey() string {
|
||||
|
||||
// SignatureType returns the signature type setting in the form of "SIGNATURE_TYPE=type"
|
||||
func (f *I2PConfig) SignatureType() string {
|
||||
if f.samMax() < 3.1 {
|
||||
if f.samMax() < common.SAM31Version.Number {
|
||||
log.Debug("SAM version < 3.1, SignatureType not applicable")
|
||||
return ""
|
||||
}
|
||||
|
3
go.mod
3
go.mod
@ -4,9 +4,10 @@ go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/go-i2p/i2pkeys v0.33.92
|
||||
github.com/go-i2p/logger v0.0.0-20241121221545-ce7ceeba699a
|
||||
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.27.0 // indirect
|
||||
|
||||
replace github.com/go-i2p/i2pkeys v0.33.92 => ../i2pkeys
|
||||
|
8
go.sum
8
go.sum
@ -1,12 +1,8 @@
|
||||
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/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4 h1:LRjaRCzg1ieGKZjELlaIg06Fx04RHzQLsWMYp1H6PQ4=
|
||||
github.com/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4/go.mod h1:m5TlHjPZrU5KbTd7Lr+I2rljyC6aJ88HdkeMQXV0U0E=
|
||||
github.com/go-i2p/i2pkeys v0.33.92 h1:e2vx3vf7tNesaJ8HmAlGPOcfiGM86jzeIGxh27I9J2Y=
|
||||
github.com/go-i2p/i2pkeys v0.33.92/go.mod h1:BRURQ/twxV0WKjZlFSKki93ivBi+MirZPWudfwTzMpE=
|
||||
github.com/go-i2p/logger v0.0.0-20241121221545-ce7ceeba699a h1:z19Zzn9uX/bGxzQ0XTSrXe+bl29XW60n6kUpg1elscA=
|
||||
github.com/go-i2p/logger v0.0.0-20241121221545-ce7ceeba699a/go.mod h1:qMpzyCtcUAeVIe38fKEmJyWoyGfLN2N9MmgiM6iQBdQ=
|
||||
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c h1:VTiECn3dFEmUlZjto+wOwJ7SSJTHPLyNprQMR5HzIMI=
|
||||
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c/go.mod h1:te7Zj3g3oMeIl8uBXAgO62UKmZ6m6kHRNg1Mm+X8Hzk=
|
||||
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
|
Reference in New Issue
Block a user