diff --git a/.gitignore b/.gitignore index 662a97a..ceacfb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *~ *.swp README.md.asc +log \ No newline at end of file diff --git a/common/formatter.go b/common/formatter.go new file mode 100644 index 0000000..bdb1bcd --- /dev/null +++ b/common/formatter.go @@ -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, "") +} diff --git a/common/reply.go b/common/reply.go new file mode 100644 index 0000000..0db04b1 --- /dev/null +++ b/common/reply.go @@ -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" +} diff --git a/common/udp.go b/common/udp.go index 71903b0..4867678 100644 --- a/common/udp.go +++ b/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 -} \ No newline at end of file + return laddr, raddr, nil +} diff --git a/common/version.go b/common/version.go new file mode 100644 index 0000000..d95e047 --- /dev/null +++ b/common/version.go @@ -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, + } +) diff --git a/config.go b/config.go index af5aa57..618d2b9 100644 --- a/config.go +++ b/config.go @@ -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 "" } diff --git a/go.mod b/go.mod index 59161da..9e310ce 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 406c93f..0413b26 100644 --- a/go.sum +++ b/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=