Expand the common package some more to include reply parser from goSam, move version to smarter structure

This commit is contained in:
eyedeekay
2024-11-29 21:07:26 -05:00
parent 82eeba6275
commit 1f27cb656e
8 changed files with 244 additions and 78 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*~
*.swp
README.md.asc
log

60
common/formatter.go Normal file
View 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
View 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"
}

View File

@ -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
View 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,
}
)

View File

@ -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
View File

@ -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
View File

@ -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=