mirror of
https://github.com/go-i2p/go-sam-go.git
synced 2025-06-15 21:28:46 -04:00
compatibility layer
This commit is contained in:
204
config.go
Normal file
204
config.go
Normal file
@ -0,0 +1,204 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
)
|
||||
|
||||
// I2PConfig manages I2P configuration options
|
||||
type I2PConfig struct {
|
||||
*common.I2PConfig
|
||||
}
|
||||
|
||||
// NewConfig creates a new I2PConfig
|
||||
func NewConfig(opts ...func(*I2PConfig) error) (*I2PConfig, error) {
|
||||
baseConfig, err := common.NewConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &I2PConfig{
|
||||
I2PConfig: baseConfig,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// All the configuration method forwards
|
||||
func (f *I2PConfig) SetSAMAddress(addr string) {
|
||||
f.I2PConfig.SetSAMAddress(addr)
|
||||
}
|
||||
|
||||
func (f *I2PConfig) Sam() string {
|
||||
return f.I2PConfig.Sam()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) SAMAddress() string {
|
||||
return f.I2PConfig.SAMAddress()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) ID() string {
|
||||
return f.I2PConfig.ID()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) Print() []string {
|
||||
return f.I2PConfig.Print()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) SessionStyle() string {
|
||||
return f.I2PConfig.SessionStyle()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) MinSAM() string {
|
||||
return f.I2PConfig.MinSAM()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) MaxSAM() string {
|
||||
return f.I2PConfig.MaxSAM()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) DestinationKey() string {
|
||||
return f.I2PConfig.DestinationKey()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) SignatureType() string {
|
||||
return f.I2PConfig.SignatureType()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) ToPort() string {
|
||||
return f.I2PConfig.ToPort()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) Reduce() string {
|
||||
return f.I2PConfig.Reduce()
|
||||
}
|
||||
|
||||
func (f *I2PConfig) Reliability() string {
|
||||
return f.I2PConfig.Reliability()
|
||||
}
|
||||
|
||||
// Configuration option setters for all the missing Set* functions
|
||||
func SetInAllowZeroHop(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
e.InAllowZeroHop = s == "true"
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetOutAllowZeroHop(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
e.OutAllowZeroHop = s == "true"
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetInLength(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.InLength = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetOutLength(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.OutLength = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetInQuantity(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.InQuantity = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetOutQuantity(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.OutQuantity = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetInVariance(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.InVariance = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetOutVariance(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.OutVariance = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetInBackupQuantity(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.InBackupQuantity = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetOutBackupQuantity(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.OutBackupQuantity = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetUseCompression(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
e.UseCompression = s == "true"
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetReduceIdleTime(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.ReduceIdleTime = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetCloseIdleTime(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
e.CloseIdleTime = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetAccessListType(s string) func(*I2PConfig) error {
|
||||
return func(e *I2PConfig) error {
|
||||
e.AccessListType = s
|
||||
return nil
|
||||
}
|
||||
}
|
51
conn.go
Normal file
51
conn.go
Normal file
@ -0,0 +1,51 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SAMConn implements net.Conn for I2P connections
|
||||
type SAMConn struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// Read reads data from the connection
|
||||
func (sc *SAMConn) Read(buf []byte) (int, error) {
|
||||
return sc.conn.Read(buf)
|
||||
}
|
||||
|
||||
// Write writes data to the connection
|
||||
func (sc *SAMConn) Write(buf []byte) (int, error) {
|
||||
return sc.conn.Write(buf)
|
||||
}
|
||||
|
||||
// Close closes the connection
|
||||
func (sc *SAMConn) Close() error {
|
||||
return sc.conn.Close()
|
||||
}
|
||||
|
||||
// LocalAddr returns the local address
|
||||
func (sc *SAMConn) LocalAddr() net.Addr {
|
||||
return sc.conn.LocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote address
|
||||
func (sc *SAMConn) RemoteAddr() net.Addr {
|
||||
return sc.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
// SetDeadline sets read and write deadlines
|
||||
func (sc *SAMConn) SetDeadline(t time.Time) error {
|
||||
return sc.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline sets read deadline
|
||||
func (sc *SAMConn) SetReadDeadline(t time.Time) error {
|
||||
return sc.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets write deadline
|
||||
func (sc *SAMConn) SetWriteDeadline(t time.Time) error {
|
||||
return sc.conn.SetWriteDeadline(t)
|
||||
}
|
129
datagram.go
Normal file
129
datagram.go
Normal file
@ -0,0 +1,129 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/go-sam-go/datagram"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// DatagramSession implements net.PacketConn for I2P datagrams
|
||||
type DatagramSession struct {
|
||||
session *datagram.DatagramSession
|
||||
sam *common.SAM
|
||||
}
|
||||
|
||||
// NewDatagramSession creates a new datagram session
|
||||
func (s *SAM) NewDatagramSession(id string, keys i2pkeys.I2PKeys, options []string, udpPort int) (*DatagramSession, error) {
|
||||
session, err := datagram.NewDatagramSession(s.sam, id, keys, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DatagramSession{
|
||||
session: session,
|
||||
sam: s.sam,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadFrom reads a datagram from the session
|
||||
func (s *DatagramSession) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
||||
return s.session.ReadFrom(b)
|
||||
}
|
||||
|
||||
// WriteTo writes a datagram to the specified address
|
||||
func (s *DatagramSession) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||
return s.session.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
// Close closes the datagram session
|
||||
func (s *DatagramSession) Close() error {
|
||||
return s.session.Close()
|
||||
}
|
||||
|
||||
// LocalAddr returns the local address
|
||||
func (s *DatagramSession) LocalAddr() net.Addr {
|
||||
return s.session.LocalAddr()
|
||||
}
|
||||
|
||||
// LocalI2PAddr returns the I2P destination
|
||||
func (s *DatagramSession) LocalI2PAddr() i2pkeys.I2PAddr {
|
||||
return s.session.Addr()
|
||||
}
|
||||
|
||||
// SetDeadline sets read and write deadlines
|
||||
func (s *DatagramSession) SetDeadline(t time.Time) error {
|
||||
return s.session.SetDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline sets read deadline
|
||||
func (s *DatagramSession) SetReadDeadline(t time.Time) error {
|
||||
return s.session.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets write deadline
|
||||
func (s *DatagramSession) SetWriteDeadline(t time.Time) error {
|
||||
return s.session.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
// Read reads from the session
|
||||
func (s *DatagramSession) Read(b []byte) (n int, err error) {
|
||||
return s.session.Read(b)
|
||||
}
|
||||
|
||||
// Write writes to the session
|
||||
func (s *DatagramSession) Write(b []byte) (int, error) {
|
||||
return s.session.Write(b)
|
||||
}
|
||||
|
||||
// Addr returns the session address
|
||||
func (s *DatagramSession) Addr() net.Addr {
|
||||
return s.session.LocalAddr()
|
||||
}
|
||||
|
||||
// B32 returns the base32 address
|
||||
func (s *DatagramSession) B32() string {
|
||||
return s.session.Addr().Base32()
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote address
|
||||
func (s *DatagramSession) RemoteAddr() net.Addr {
|
||||
return s.session.RemoteAddr()
|
||||
}
|
||||
|
||||
// Lookup performs name lookup
|
||||
func (s *DatagramSession) Lookup(name string) (a net.Addr, err error) {
|
||||
addr, err := s.sam.Lookup(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Accept accepts connections (not applicable for datagrams)
|
||||
func (s *DatagramSession) Accept() (net.Conn, error) {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
// Dial dials a connection (returns self for datagrams)
|
||||
func (s *DatagramSession) Dial(net string, addr string) (*DatagramSession, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// DialI2PRemote dials to I2P remote
|
||||
func (s *DatagramSession) DialI2PRemote(net string, addr net.Addr) (*DatagramSession, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// DialRemote dials to remote address
|
||||
func (s *DatagramSession) DialRemote(net, addr string) (net.PacketConn, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// SetWriteBuffer sets write buffer size
|
||||
func (s *DatagramSession) SetWriteBuffer(bytes int) error {
|
||||
// Not implemented in underlying library
|
||||
return nil
|
||||
}
|
97
emit.go
Normal file
97
emit.go
Normal file
@ -0,0 +1,97 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
)
|
||||
|
||||
// SAMEmit handles SAM protocol message generation
|
||||
type SAMEmit struct {
|
||||
I2PConfig
|
||||
emit *common.SAMEmit
|
||||
}
|
||||
|
||||
// NewEmit creates a new SAMEmit
|
||||
func NewEmit(opts ...func(*SAMEmit) error) (*SAMEmit, error) {
|
||||
config, err := NewConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emit := &SAMEmit{
|
||||
I2PConfig: *config,
|
||||
emit: &common.SAMEmit{I2PConfig: *config.I2PConfig},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(emit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return emit, nil
|
||||
}
|
||||
|
||||
// Hello generates hello message
|
||||
func (e *SAMEmit) Hello() string {
|
||||
return e.emit.Hello()
|
||||
}
|
||||
|
||||
// HelloBytes generates hello message as bytes
|
||||
func (e *SAMEmit) HelloBytes() []byte {
|
||||
return e.emit.HelloBytes()
|
||||
}
|
||||
|
||||
// Create generates session create message
|
||||
func (e *SAMEmit) Create() string {
|
||||
return e.emit.Create()
|
||||
}
|
||||
|
||||
// CreateBytes generates session create message as bytes
|
||||
func (e *SAMEmit) CreateBytes() []byte {
|
||||
return e.emit.CreateBytes()
|
||||
}
|
||||
|
||||
// Connect generates connect message
|
||||
func (e *SAMEmit) Connect(dest string) string {
|
||||
return e.emit.Connect(dest)
|
||||
}
|
||||
|
||||
// ConnectBytes generates connect message as bytes
|
||||
func (e *SAMEmit) ConnectBytes(dest string) []byte {
|
||||
return e.emit.ConnectBytes(dest)
|
||||
}
|
||||
|
||||
// Accept generates accept message
|
||||
func (e *SAMEmit) Accept() string {
|
||||
return e.emit.Accept()
|
||||
}
|
||||
|
||||
// AcceptBytes generates accept message as bytes
|
||||
func (e *SAMEmit) AcceptBytes() []byte {
|
||||
return e.emit.AcceptBytes()
|
||||
}
|
||||
|
||||
// Lookup generates lookup message
|
||||
func (e *SAMEmit) Lookup(name string) string {
|
||||
return e.emit.Lookup(name)
|
||||
}
|
||||
|
||||
// LookupBytes generates lookup message as bytes
|
||||
func (e *SAMEmit) LookupBytes(name string) []byte {
|
||||
return e.emit.LookupBytes(name)
|
||||
}
|
||||
|
||||
// GenerateDestination generates destination message
|
||||
func (e *SAMEmit) GenerateDestination() string {
|
||||
return e.emit.GenerateDestination()
|
||||
}
|
||||
|
||||
// GenerateDestinationBytes generates destination message as bytes
|
||||
func (e *SAMEmit) GenerateDestinationBytes() []byte {
|
||||
return e.emit.GenerateDestinationBytes()
|
||||
}
|
||||
|
||||
// SamOptionsString returns SAM options as string
|
||||
func (e *SAMEmit) SamOptionsString() string {
|
||||
return e.emit.SamOptionsString()
|
||||
}
|
50
listener.go
Normal file
50
listener.go
Normal file
@ -0,0 +1,50 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/stream"
|
||||
)
|
||||
|
||||
// StreamListener implements net.Listener for I2P streams
|
||||
type StreamListener struct {
|
||||
listener *stream.StreamListener
|
||||
}
|
||||
|
||||
// Accept accepts new inbound connections
|
||||
func (l *StreamListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SAMConn{conn: conn}, nil
|
||||
}
|
||||
|
||||
// AcceptI2P accepts a new inbound I2P connection
|
||||
func (l *StreamListener) AcceptI2P() (*SAMConn, error) {
|
||||
conn, err := l.listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SAMConn{conn: conn}, nil
|
||||
}
|
||||
|
||||
// Addr returns the listener's address
|
||||
func (l *StreamListener) Addr() net.Addr {
|
||||
return l.listener.Addr()
|
||||
}
|
||||
|
||||
// Close closes the listener
|
||||
func (l *StreamListener) Close() error {
|
||||
return l.listener.Close()
|
||||
}
|
||||
|
||||
// From returns the from port
|
||||
func (l *StreamListener) From() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// To returns the to port
|
||||
func (l *StreamListener) To() string {
|
||||
return ""
|
||||
}
|
165
primary.go
Normal file
165
primary.go
Normal file
@ -0,0 +1,165 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// PrimarySession represents a primary session
|
||||
type PrimarySession struct {
|
||||
Timeout time.Duration
|
||||
Deadline time.Time
|
||||
Config SAMEmit
|
||||
sam *common.SAM
|
||||
keys i2pkeys.I2PKeys
|
||||
id string
|
||||
}
|
||||
|
||||
// NewPrimarySession creates a new PrimarySession
|
||||
func (sam *SAM) NewPrimarySession(id string, keys i2pkeys.I2PKeys, options []string) (*PrimarySession, error) {
|
||||
return &PrimarySession{
|
||||
Config: sam.Config,
|
||||
sam: sam.sam,
|
||||
keys: keys,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewPrimarySessionWithSignature creates a new PrimarySession with signature
|
||||
func (sam *SAM) NewPrimarySessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*PrimarySession, error) {
|
||||
return &PrimarySession{
|
||||
Config: sam.Config,
|
||||
sam: sam.sam,
|
||||
keys: keys,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ID returns the session ID
|
||||
func (ss *PrimarySession) ID() string {
|
||||
return ss.id
|
||||
}
|
||||
|
||||
// Keys returns the session keys
|
||||
func (ss *PrimarySession) Keys() i2pkeys.I2PKeys {
|
||||
return ss.keys
|
||||
}
|
||||
|
||||
// Addr returns the I2P address
|
||||
func (ss *PrimarySession) Addr() i2pkeys.I2PAddr {
|
||||
return ss.keys.Addr()
|
||||
}
|
||||
|
||||
// Close closes the session
|
||||
func (ss *PrimarySession) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewStreamSubSession creates a new stream sub-session
|
||||
func (sam *PrimarySession) NewStreamSubSession(id string) (*StreamSession, error) {
|
||||
samWrapper := &SAM{sam: sam.sam}
|
||||
return samWrapper.NewStreamSession(id, sam.keys, nil)
|
||||
}
|
||||
|
||||
// NewStreamSubSessionWithPorts creates a new stream sub-session with ports
|
||||
func (sam *PrimarySession) NewStreamSubSessionWithPorts(id, from, to string) (*StreamSession, error) {
|
||||
samWrapper := &SAM{sam: sam.sam}
|
||||
return samWrapper.NewStreamSessionWithSignatureAndPorts(id, from, to, sam.keys, nil, Sig_EdDSA_SHA512_Ed25519)
|
||||
}
|
||||
|
||||
// NewUniqueStreamSubSession creates a unique stream sub-session
|
||||
func (sam *PrimarySession) NewUniqueStreamSubSession(id string) (*StreamSession, error) {
|
||||
return sam.NewStreamSubSession(id + RandString())
|
||||
}
|
||||
|
||||
// NewDatagramSubSession creates a new datagram sub-session
|
||||
func (s *PrimarySession) NewDatagramSubSession(id string, udpPort int) (*DatagramSession, error) {
|
||||
samWrapper := &SAM{sam: s.sam}
|
||||
return samWrapper.NewDatagramSession(id, s.keys, nil, udpPort)
|
||||
}
|
||||
|
||||
// NewRawSubSession creates a new raw sub-session
|
||||
func (s *PrimarySession) NewRawSubSession(id string, udpPort int) (*RawSession, error) {
|
||||
samWrapper := &SAM{sam: s.sam}
|
||||
return samWrapper.NewRawSession(id, s.keys, nil, udpPort)
|
||||
}
|
||||
|
||||
// Dial implements net.Dialer
|
||||
func (sam *PrimarySession) Dial(network, addr string) (net.Conn, error) {
|
||||
ss, err := sam.NewStreamSubSession("dial-" + RandString())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ss.Dial(network, addr)
|
||||
}
|
||||
|
||||
// DialTCP implements x/dialer
|
||||
func (sam *PrimarySession) DialTCP(network string, laddr, raddr net.Addr) (net.Conn, error) {
|
||||
return sam.Dial(network, raddr.String())
|
||||
}
|
||||
|
||||
// DialTCPI2P dials TCP over I2P
|
||||
func (sam *PrimarySession) DialTCPI2P(network string, laddr, raddr string) (net.Conn, error) {
|
||||
return sam.Dial(network, raddr)
|
||||
}
|
||||
|
||||
// DialUDP implements x/dialer
|
||||
func (sam *PrimarySession) DialUDP(network string, laddr, raddr net.Addr) (net.PacketConn, error) {
|
||||
ds, err := sam.NewDatagramSubSession("udp-"+RandString(), 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// DialUDPI2P dials UDP over I2P
|
||||
func (sam *PrimarySession) DialUDPI2P(network, laddr, raddr string) (*DatagramSession, error) {
|
||||
return sam.NewDatagramSubSession("udp-"+RandString(), 0)
|
||||
}
|
||||
|
||||
// Lookup performs name lookup
|
||||
func (s *PrimarySession) Lookup(name string) (a net.Addr, err error) {
|
||||
addr, err := s.sam.Lookup(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Resolve resolves network address
|
||||
func (sam *PrimarySession) Resolve(network, addr string) (net.Addr, error) {
|
||||
return sam.Lookup(addr)
|
||||
}
|
||||
|
||||
// ResolveTCPAddr resolves TCP address
|
||||
func (sam *PrimarySession) ResolveTCPAddr(network, dest string) (net.Addr, error) {
|
||||
return sam.Lookup(dest)
|
||||
}
|
||||
|
||||
// ResolveUDPAddr resolves UDP address
|
||||
func (sam *PrimarySession) ResolveUDPAddr(network, dest string) (net.Addr, error) {
|
||||
return sam.Lookup(dest)
|
||||
}
|
||||
|
||||
// LocalAddr returns local address
|
||||
func (ss *PrimarySession) LocalAddr() net.Addr {
|
||||
return ss.keys.Addr()
|
||||
}
|
||||
|
||||
// From returns from port
|
||||
func (ss *PrimarySession) From() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// To returns to port
|
||||
func (ss *PrimarySession) To() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// SignatureType returns signature type
|
||||
func (ss *PrimarySession) SignatureType() string {
|
||||
return ""
|
||||
}
|
63
raw.go
Normal file
63
raw.go
Normal file
@ -0,0 +1,63 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/go-sam-go/raw"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// RawSession provides raw datagram messaging
|
||||
type RawSession struct {
|
||||
session *raw.RawSession
|
||||
sam *common.SAM
|
||||
}
|
||||
|
||||
// NewRawSession creates a new raw session
|
||||
func (s *SAM) NewRawSession(id string, keys i2pkeys.I2PKeys, options []string, udpPort int) (*RawSession, error) {
|
||||
session, err := raw.NewRawSession(s.sam, id, keys, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RawSession{
|
||||
session: session,
|
||||
sam: s.sam,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read reads one raw datagram
|
||||
func (s *RawSession) Read(b []byte) (n int, err error) {
|
||||
return s.session.Read(b)
|
||||
}
|
||||
|
||||
// WriteTo sends one raw datagram to the destination
|
||||
func (s *RawSession) WriteTo(b []byte, addr i2pkeys.I2PAddr) (n int, err error) {
|
||||
return s.session.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
// Close closes the raw session
|
||||
func (s *RawSession) Close() error {
|
||||
return s.session.Close()
|
||||
}
|
||||
|
||||
// LocalAddr returns the local I2P destination
|
||||
func (s *RawSession) LocalAddr() i2pkeys.I2PAddr {
|
||||
return s.session.Addr()
|
||||
}
|
||||
|
||||
// SetDeadline sets read and write deadlines
|
||||
func (s *RawSession) SetDeadline(t time.Time) error {
|
||||
return s.session.SetDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline sets read deadline
|
||||
func (s *RawSession) SetReadDeadline(t time.Time) error {
|
||||
return s.session.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets write deadline
|
||||
func (s *RawSession) SetWriteDeadline(t time.Time) error {
|
||||
return s.session.SetWriteDeadline(t)
|
||||
}
|
44
resolver.go
Normal file
44
resolver.go
Normal file
@ -0,0 +1,44 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// SAMResolver provides name resolution functionality
|
||||
type SAMResolver struct {
|
||||
*SAM
|
||||
resolver *common.SAMResolver
|
||||
}
|
||||
|
||||
// NewSAMResolver creates a new SAMResolver from existing SAM
|
||||
func NewSAMResolver(parent *SAM) (*SAMResolver, error) {
|
||||
resolver, err := common.NewSAMResolver(parent.sam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SAMResolver{
|
||||
SAM: parent,
|
||||
resolver: resolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewFullSAMResolver creates a new full SAMResolver
|
||||
func NewFullSAMResolver(address string) (*SAMResolver, error) {
|
||||
resolver, err := common.NewFullSAMResolver(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sam := &SAM{sam: resolver.SAM}
|
||||
return &SAMResolver{
|
||||
SAM: sam,
|
||||
resolver: resolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Resolve performs a lookup
|
||||
func (sam *SAMResolver) Resolve(name string) (i2pkeys.I2PAddr, error) {
|
||||
return sam.resolver.Resolve(name)
|
||||
}
|
134
sam3.go
Normal file
134
sam3.go
Normal file
@ -0,0 +1,134 @@
|
||||
// Package sam3 provides a compatibility layer for the go-i2p/sam3 library using go-sam-go as the backend
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// Constants from original sam3
|
||||
const (
|
||||
Sig_NONE = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
|
||||
Sig_DSA_SHA1 = "SIGNATURE_TYPE=DSA_SHA1"
|
||||
Sig_ECDSA_SHA256_P256 = "SIGNATURE_TYPE=ECDSA_SHA256_P256"
|
||||
Sig_ECDSA_SHA384_P384 = "SIGNATURE_TYPE=ECDSA_SHA384_P384"
|
||||
Sig_ECDSA_SHA512_P521 = "SIGNATURE_TYPE=ECDSA_SHA512_P521"
|
||||
Sig_EdDSA_SHA512_Ed25519 = "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"
|
||||
)
|
||||
|
||||
// Predefined option sets (keeping your existing definitions)
|
||||
var (
|
||||
Options_Humongous = []string{
|
||||
"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=3", "outbound.backupQuantity=3",
|
||||
"inbound.quantity=6", "outbound.quantity=6",
|
||||
}
|
||||
|
||||
Options_Large = []string{
|
||||
"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
|
||||
"inbound.quantity=4", "outbound.quantity=4",
|
||||
}
|
||||
|
||||
Options_Wide = []string{
|
||||
"inbound.length=1", "outbound.length=1",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=2", "outbound.backupQuantity=2",
|
||||
"inbound.quantity=3", "outbound.quantity=3",
|
||||
}
|
||||
|
||||
Options_Medium = []string{
|
||||
"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
|
||||
"inbound.quantity=2", "outbound.quantity=2",
|
||||
}
|
||||
|
||||
Options_Default = []string{
|
||||
"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
|
||||
"inbound.backupQuantity=1", "outbound.backupQuantity=1",
|
||||
"inbound.quantity=1", "outbound.quantity=1",
|
||||
}
|
||||
|
||||
Options_Small = []string{
|
||||
"inbound.length=3", "outbound.length=3",
|
||||
"inbound.lengthVariance=1", "outbound.lengthVariance=1",
|
||||
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
|
||||
"inbound.quantity=1", "outbound.quantity=1",
|
||||
}
|
||||
|
||||
Options_Warning_ZeroHop = []string{
|
||||
"inbound.length=0", "outbound.length=0",
|
||||
"inbound.lengthVariance=0", "outbound.lengthVariance=0",
|
||||
"inbound.backupQuantity=0", "outbound.backupQuantity=0",
|
||||
"inbound.quantity=2", "outbound.quantity=2",
|
||||
}
|
||||
)
|
||||
|
||||
// Global variables from original sam3
|
||||
var (
|
||||
PrimarySessionSwitch string = PrimarySessionString()
|
||||
SAM_HOST = getEnv("sam_host", "127.0.0.1")
|
||||
SAM_PORT = getEnv("sam_port", "7656")
|
||||
)
|
||||
|
||||
// SAM represents the main controller for I2P router's SAM bridge
|
||||
type SAM struct {
|
||||
Config SAMEmit
|
||||
sam *common.SAM
|
||||
}
|
||||
|
||||
// NewSAM creates a new controller for the I2P routers SAM bridge
|
||||
func NewSAM(address string) (*SAM, error) {
|
||||
samInstance, err := common.NewSAM(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := NewConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emit := SAMEmit{I2PConfig: *config}
|
||||
|
||||
return &SAM{
|
||||
Config: emit,
|
||||
sam: samInstance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes this sam session
|
||||
func (sam *SAM) Close() error {
|
||||
return sam.sam.Close()
|
||||
}
|
||||
|
||||
// Keys returns the keys associated with this SAM instance
|
||||
func (sam *SAM) Keys() (k *i2pkeys.I2PKeys) {
|
||||
return sam.sam.Keys()
|
||||
}
|
||||
|
||||
// NewKeys creates the I2P-equivalent of an IP address
|
||||
func (sam *SAM) NewKeys(sigType ...string) (i2pkeys.I2PKeys, error) {
|
||||
return sam.sam.NewKeys(sigType...)
|
||||
}
|
||||
|
||||
// ReadKeys reads public/private keys from an io.Reader
|
||||
func (sam *SAM) ReadKeys(r io.Reader) (err error) {
|
||||
return sam.sam.ReadKeys(r)
|
||||
}
|
||||
|
||||
// EnsureKeyfile ensures keyfile exists
|
||||
func (sam *SAM) EnsureKeyfile(fname string) (keys i2pkeys.I2PKeys, err error) {
|
||||
return sam.sam.EnsureKeyfile(fname)
|
||||
}
|
||||
|
||||
// Lookup performs a name lookup
|
||||
func (sam *SAM) Lookup(name string) (i2pkeys.I2PAddr, error) {
|
||||
return sam.sam.Lookup(name)
|
||||
}
|
191
stream.go
Normal file
191
stream.go
Normal file
@ -0,0 +1,191 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-sam-go/common"
|
||||
"github.com/go-i2p/go-sam-go/stream"
|
||||
"github.com/go-i2p/i2pkeys"
|
||||
)
|
||||
|
||||
// StreamSession represents a streaming session
|
||||
type StreamSession struct {
|
||||
Timeout time.Duration
|
||||
Deadline time.Time
|
||||
session *stream.StreamSession
|
||||
sam *common.SAM
|
||||
fromPort string
|
||||
toPort string
|
||||
signatureType string
|
||||
}
|
||||
|
||||
// NewStreamSession creates a new StreamSession
|
||||
func (sam *SAM) NewStreamSession(id string, keys i2pkeys.I2PKeys, options []string) (*StreamSession, error) {
|
||||
session, err := stream.NewStreamSession(sam.sam, id, keys, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StreamSession{
|
||||
session: session,
|
||||
sam: sam.sam,
|
||||
fromPort: "",
|
||||
toPort: "",
|
||||
signatureType: common.SIG_EdDSA_SHA512_Ed25519,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewStreamSessionWithSignature creates a new StreamSession with custom signature
|
||||
func (sam *SAM) NewStreamSessionWithSignature(id string, keys i2pkeys.I2PKeys, options []string, sigType string) (*StreamSession, error) {
|
||||
streamSAM := &stream.SAM{SAM: sam.sam}
|
||||
session, err := streamSAM.NewStreamSessionWithSignature(id, keys, options, sigType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StreamSession{
|
||||
session: session,
|
||||
sam: sam.sam,
|
||||
fromPort: "",
|
||||
toPort: "",
|
||||
signatureType: sigType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewStreamSessionWithSignatureAndPorts creates a new StreamSession with signature and ports
|
||||
func (sam *SAM) NewStreamSessionWithSignatureAndPorts(id, from, to string, keys i2pkeys.I2PKeys, options []string, sigType string) (*StreamSession, error) {
|
||||
streamSAM := &stream.SAM{SAM: sam.sam}
|
||||
session, err := streamSAM.NewStreamSessionWithPorts(id, from, to, keys, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StreamSession{
|
||||
session: session,
|
||||
sam: sam.sam,
|
||||
fromPort: from,
|
||||
toPort: to,
|
||||
signatureType: sigType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ID returns the local tunnel name
|
||||
func (s *StreamSession) ID() string {
|
||||
return s.session.ID()
|
||||
}
|
||||
|
||||
// Keys returns the keys associated with the session
|
||||
func (s *StreamSession) Keys() i2pkeys.I2PKeys {
|
||||
return s.session.Keys()
|
||||
}
|
||||
|
||||
// Addr returns the I2P destination address
|
||||
func (s *StreamSession) Addr() i2pkeys.I2PAddr {
|
||||
return s.session.Keys().Addr()
|
||||
}
|
||||
|
||||
// Close closes the session
|
||||
func (s *StreamSession) Close() error {
|
||||
return s.session.Close()
|
||||
}
|
||||
|
||||
// Listen creates a new stream listener
|
||||
func (s *StreamSession) Listen() (*StreamListener, error) {
|
||||
listener, err := s.session.Listen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StreamListener{
|
||||
listener: listener,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Dial establishes a connection to an address
|
||||
func (s *StreamSession) Dial(n, addr string) (c net.Conn, err error) {
|
||||
dialer := s.session.NewDialer()
|
||||
return dialer.Dial(n, addr)
|
||||
}
|
||||
|
||||
// DialContext establishes a connection with context
|
||||
func (s *StreamSession) DialContext(ctx context.Context, n, addr string) (net.Conn, error) {
|
||||
dialer := s.session.NewDialer()
|
||||
return dialer.DialContext(ctx, n, addr)
|
||||
}
|
||||
|
||||
// DialContextI2P establishes an I2P connection with context
|
||||
func (s *StreamSession) DialContextI2P(ctx context.Context, n, addr string) (*SAMConn, error) {
|
||||
dialer := s.session.NewDialer()
|
||||
conn, err := dialer.DialContext(ctx, n, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SAMConn{conn: conn}, nil
|
||||
}
|
||||
|
||||
// DialI2P dials to an I2P destination
|
||||
func (s *StreamSession) DialI2P(addr i2pkeys.I2PAddr) (*SAMConn, error) {
|
||||
dialer := s.session.NewDialer()
|
||||
conn, err := dialer.DialI2P(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SAMConn{conn: conn}, nil
|
||||
}
|
||||
|
||||
// Lookup performs name lookup
|
||||
func (s *StreamSession) Lookup(name string) (i2pkeys.I2PAddr, error) {
|
||||
return s.sam.Lookup(name)
|
||||
}
|
||||
|
||||
// Read reads data from the stream
|
||||
func (s *StreamSession) Read(buf []byte) (int, error) {
|
||||
return s.session.Read(buf)
|
||||
}
|
||||
|
||||
// Write sends data over the stream
|
||||
func (s *StreamSession) Write(data []byte) (int, error) {
|
||||
return s.session.Write(data)
|
||||
}
|
||||
|
||||
// LocalAddr returns the local address
|
||||
func (s *StreamSession) LocalAddr() net.Addr {
|
||||
return s.session.LocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote address
|
||||
func (s *StreamSession) RemoteAddr() net.Addr {
|
||||
return s.session.RemoteAddr()
|
||||
}
|
||||
|
||||
// SetDeadline sets read and write deadlines
|
||||
func (s *StreamSession) SetDeadline(t time.Time) error {
|
||||
return s.session.SetDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline sets read deadline
|
||||
func (s *StreamSession) SetReadDeadline(t time.Time) error {
|
||||
return s.session.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline sets write deadline
|
||||
func (s *StreamSession) SetWriteDeadline(t time.Time) error {
|
||||
return s.session.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
// From returns the from port
|
||||
func (s *StreamSession) From() string {
|
||||
return s.fromPort
|
||||
}
|
||||
|
||||
// To returns the to port
|
||||
func (s *StreamSession) To() string {
|
||||
return s.toPort
|
||||
}
|
||||
|
||||
// SignatureType returns the signature type
|
||||
func (s *StreamSession) SignatureType() string {
|
||||
return s.signatureType
|
||||
}
|
15
types.go
Normal file
15
types.go
Normal file
@ -0,0 +1,15 @@
|
||||
package sam3
|
||||
|
||||
// Options represents a map of configuration options
|
||||
type Options map[string]string
|
||||
|
||||
// AsList returns options as a list of strings
|
||||
func (opts Options) AsList() (ls []string) {
|
||||
for k, v := range opts {
|
||||
ls = append(ls, k+"="+v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Option is a functional option for SAMEmit
|
||||
type Option func(*SAMEmit) error
|
122
utils.go
Normal file
122
utils.go
Normal file
@ -0,0 +1,122 @@
|
||||
package sam3
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// getEnv gets environment variable with fallback
|
||||
func getEnv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// RandString generates a random string
|
||||
func RandString() string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz"
|
||||
result := make([]byte, 12)
|
||||
for i := range result {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
result[i] = chars[n.Int64()]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// PrimarySessionString returns primary session string
|
||||
func PrimarySessionString() string {
|
||||
return "primary"
|
||||
}
|
||||
|
||||
// SAMDefaultAddr returns default SAM address
|
||||
func SAMDefaultAddr(fallforward string) string {
|
||||
if fallforward != "" {
|
||||
return fallforward
|
||||
}
|
||||
return SAM_HOST + ":" + SAM_PORT
|
||||
}
|
||||
|
||||
// ExtractDest extracts destination from input
|
||||
func ExtractDest(input string) string {
|
||||
parts := strings.Fields(input)
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "DEST=") {
|
||||
return part[5:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtractPairString extracts string value from key=value pair
|
||||
func ExtractPairString(input, value string) string {
|
||||
prefix := value + "="
|
||||
parts := strings.Fields(input)
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, prefix) {
|
||||
return part[len(prefix):]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtractPairInt extracts integer value from key=value pair
|
||||
func ExtractPairInt(input, value string) int {
|
||||
str := ExtractPairString(input, value)
|
||||
if str == "" {
|
||||
return 0
|
||||
}
|
||||
i, _ := strconv.Atoi(str)
|
||||
return i
|
||||
}
|
||||
|
||||
// GenerateOptionString generates option string from slice
|
||||
func GenerateOptionString(opts []string) string {
|
||||
return strings.Join(opts, " ")
|
||||
}
|
||||
|
||||
// IgnorePortError ignores port-related errors
|
||||
func IgnorePortError(err error) error {
|
||||
if err != nil && strings.Contains(err.Error(), "port") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Logging functions
|
||||
var sam3Logger *logrus.Logger
|
||||
|
||||
// InitializeSAM3Logger initializes the logger
|
||||
func InitializeSAM3Logger() {
|
||||
sam3Logger = logrus.New()
|
||||
sam3Logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
// GetSAM3Logger returns the initialized logger
|
||||
func GetSAM3Logger() *logrus.Logger {
|
||||
if sam3Logger == nil {
|
||||
InitializeSAM3Logger()
|
||||
}
|
||||
return sam3Logger
|
||||
}
|
||||
|
||||
// Additional utility functions that may be needed for compatibility
|
||||
func ConvertOptionsToSlice(opts Options) []string {
|
||||
return opts.AsList()
|
||||
}
|
||||
|
||||
func ConvertSliceToOptions(slice []string) Options {
|
||||
opts := make(Options)
|
||||
for _, opt := range slice {
|
||||
parts := strings.SplitN(opt, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
opts[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
Reference in New Issue
Block a user