From 83642bd04ca01c5225e96278281a8e668dff0cd0 Mon Sep 17 00:00:00 2001 From: Keith Petkus Date: Wed, 4 Mar 2020 01:17:40 -0500 Subject: [PATCH] initial commit --- LICENSE.txt | 13 +++++ README.txt | 6 ++ sambridge/naming.go | 74 +++++++++++++++++++++++++ sambridge/sambridge.go | 96 ++++++++++++++++++++++++++++++++ sambridge/sessionreply.go | 57 +++++++++++++++++++ sambridge/streamreply.go | 58 +++++++++++++++++++ sambridge/utilitycommands.go | 65 ++++++++++++++++++++++ session/master.go | 104 +++++++++++++++++++++++++++++++++++ session/session.go | 51 +++++++++++++++++ session/subsession.go | 77 ++++++++++++++++++++++++++ 10 files changed, 601 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.txt create mode 100644 sambridge/naming.go create mode 100644 sambridge/sambridge.go create mode 100644 sambridge/sessionreply.go create mode 100644 sambridge/streamreply.go create mode 100644 sambridge/utilitycommands.go create mode 100644 session/master.go create mode 100644 session/session.go create mode 100644 session/subsession.go diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5c93f45 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..0eb97fc --- /dev/null +++ b/README.txt @@ -0,0 +1,6 @@ +yagsl +===== + +This is experimental and expect lots of breaking changes. + +Do not use this in production or at all. \ No newline at end of file diff --git a/sambridge/naming.go b/sambridge/naming.go new file mode 100644 index 0000000..a8dc2c6 --- /dev/null +++ b/sambridge/naming.go @@ -0,0 +1,74 @@ +package sambridge + +import ( + "errors" + "fmt" + "strings" + "sync" +) + +const ( + resultOk string = "OK" + invalidKey string = "INVALID_KEY" + keyNotFound string = "KEY_NOT_FOUND" +) + +var ( + errKeyNotFound = errors.New("Key not found") + errUnknownNamingLookup = errors.New("Unknown naming lookup error") +) + +type reply struct { + name string + value string + err error +} + +type namingHandler struct { + incoming chan reply + m sync.Mutex +} + +func (m *SAMBridge) Lookup(name string) (string, error) { + var lookupReply reply + m.newNamingHandler() + + go m.Send(fmt.Sprintf("NAMING LOOKUP NAME=%s\n", name)) + for reply := range m.namingHandler.incoming { + lookupReply = reply + break + } + if lookupReply.err != nil { + return name, lookupReply.err + } + return lookupReply.value, nil +} + +func (m *SAMBridge) namingReply(line string) { + reply := reply{} + + fields := strings.Fields(line) + m.namingHandler.m.Lock() + reply.name = strings.SplitN(fields[3], "=", 2)[1] + switch strings.SplitN(fields[2], "=", 2)[1] { // result + case resultOk: + reply.err = nil + reply.value = strings.SplitN(fields[4], "=", 2)[1] + case invalidKey: + reply.err = errInvalidKey + case keyNotFound: + reply.err = errKeyNotFound + default: + reply.err = errUnknownNamingLookup + } + m.namingHandler.incoming <- reply + m.namingHandler.m.Unlock() +} + +func (m *SAMBridge) newNamingHandler() { + if m.namingHandler == nil { + t := new(namingHandler) + t.incoming = make(chan reply) + m.namingHandler = t + } +} diff --git a/sambridge/sambridge.go b/sambridge/sambridge.go new file mode 100644 index 0000000..bb671cd --- /dev/null +++ b/sambridge/sambridge.go @@ -0,0 +1,96 @@ +package sambridge + +import ( + "bufio" + "errors" + "log" + "net" + "strings" +) + +type SAMBridge struct { + Options SamOptions + Conn net.Conn + namingHandler *namingHandler + destHandler *destHandler + + SessionReply chan SessionReply + StreamReply chan StreamReply +} + +type SamOptions struct { + Address string + User string + Password string +} + +const handshakeReply string = "HELLO VERSION MIN=3.0 MAX=3.3\n" + +func (s *SAMBridge) Start() { + s.Options.Address = "127.0.0.1:7656" + err := s.dialSAMBridge() + if err != nil { + panic(err.Error()) + } + r := bufio.NewReader(s.Conn) + for { + line, _ := r.ReadString('\n') + if line != "" { + log.Printf("DEBUG line: %s", line) + } + if strings.Contains(line, " ") { + first := strings.Fields(line)[0] + switch first { + case "HELLO": + go s.handshakeReply(line) + case "SESSION": + s.sessionReply(line) + case "STREAM": + // TODO: Wait for an "OK" STREAM result and as soon as that happens break out of the loop so we can panic on unknown sam lines + s.streamReply(line) + case "DEST": + s.destReply(line) + case "NAMING": + s.namingReply(line) + case "PING": + go s.pingReply(line) + default: + //panic("Unknown SAM line encountered") + } + } + } +} + +func (s *SAMBridge) dialSAMBridge() error { + c, err := net.Dial("tcp", s.Options.Address) + s.Conn = c + s.sendHandshake() + return err +} + +func (s SAMBridge) Send(line string) error { + if s.Conn != nil { + var sb strings.Builder + sb.WriteString(line) + if !strings.HasSuffix(line, "\n") { + sb.WriteString("\n") + } + w := bufio.NewWriter(s.Conn) + _, err := w.WriteString(sb.String()) + + err = w.Flush() + return err + } + return errors.New("Conn was closed") +} + +func (s SAMBridge) sendHandshake() error { + return s.Send(handshakeReply) +} + +func (s SAMBridge) handshakeReply(reply string) error { + if !strings.HasPrefix(reply, "HELLO REPLY RESULT=OK") { + return errHandshakeFailed + } + return nil +} diff --git a/sambridge/sessionreply.go b/sambridge/sessionreply.go new file mode 100644 index 0000000..57e3eee --- /dev/null +++ b/sambridge/sessionreply.go @@ -0,0 +1,57 @@ +package sambridge + +import ( + "errors" + "strings" +) + +type SessionReply struct { + Destination string + Err error + ID string + Message string +} + +const ( + duplicatedDest = "DUPLICATED_DEST" + duplicatedID = "DUPLICATED_ID" +) + +var ( + errHandshakeFailed = errors.New("Handshake failed") + errInvalidKey = errors.New("invalid key error") + errDuplicatedDest = errors.New("the destination is not a valid private destination key") + errDuplicatedID = errors.New("the destination is already in use") + + errUnknownSession = errors.New("Unknown session error") +) + +func (s SAMBridge) sessionReply(line string) { + reply := SessionReply{} + fields := strings.Fields(line) + + switch strings.SplitN(fields[2], "=", 2)[1] { // result + case resultOk: + reply.Err = nil + if strings.HasPrefix(fields[3], "DESTINATION") { + reply.Destination = strings.SplitN(fields[3], "=", 2)[1] + } + if strings.HasPrefix(fields[3], "ID") { + reply.ID = strings.SplitN(fields[3], "=", 2)[1] + } + if len(fields) > 4 { + if strings.HasPrefix(fields[4], "MESSAGE") { + reply.Message = strings.SplitN(fields[4], "=", 2)[1] + } + } + case duplicatedID: + reply.Err = errDuplicatedID + case duplicatedDest: + reply.Err = errDuplicatedDest + case invalidKey: + reply.Err = errInvalidKey + default: + reply.Err = errUnknownSession + } + s.SessionReply <- reply +} diff --git a/sambridge/streamreply.go b/sambridge/streamreply.go new file mode 100644 index 0000000..e66ef6c --- /dev/null +++ b/sambridge/streamreply.go @@ -0,0 +1,58 @@ +package sambridge + +import ( + "errors" + "log" + "strings" +) + +type StreamReply struct { + Err error + Message string +} + +const ( + invalidID = "INVALID_ID" + i2pErr = "I2P_ERROR" + cantReachPeer = "CANT_REACH_PEER" + timeout = "TIMEOUT" +) + +var ( + errInvalidID = errors.New("invalid ID") + errI2PErr = errors.New("generic I2P error") + errCantReachPeer = errors.New("can't reach peer") + errTimeout = errors.New("timeout") + errUnknownStream = errors.New("unknown stream error") +) + +func (s SAMBridge) streamReply(line string) { + log.Printf("DEBUG STREAMREPLY: %s", line) + reply := StreamReply{} + fields := strings.Fields(line) + + switch strings.SplitN(fields[2], "=", 2)[1] { // result + case resultOk: + reply.Err = nil + case invalidID: + reply.Err = errInvalidID + case i2pErr: + reply.Err = errI2PErr + case cantReachPeer: + reply.Err = errCantReachPeer + case invalidKey: + reply.Err = errInvalidKey + case timeout: + reply.Err = errTimeout + default: + reply.Err = errUnknownStream + } + + if len(fields) > 3 { + if strings.HasPrefix(fields[3], "MESSAGE") { + reply.Message = strings.SplitN(fields[3], "=", 2)[1] + } + } + + s.StreamReply <- reply +} diff --git a/sambridge/utilitycommands.go b/sambridge/utilitycommands.go new file mode 100644 index 0000000..a9077fb --- /dev/null +++ b/sambridge/utilitycommands.go @@ -0,0 +1,65 @@ +package sambridge + +import ( + "errors" + "fmt" + "strings" + "sync" +) + +type DestReply struct { + Pub string + Priv string + err error +} + +type destHandler struct { + incoming chan DestReply + m sync.Mutex +} + +func (m SAMBridge) DestGenerate(signatureType string) (DestReply, error) { + var destReply DestReply + m.newDestHandler() + + if signatureType == "" { + signatureType = "DSA_SHA1" + } + m.Send(fmt.Sprintf("DEST GENERATE SIGNATURE_TYPE=%s", signatureType)) + for reply := range m.destHandler.incoming { + destReply = reply + break + } + if destReply.err != nil { + return destReply, destReply.err + } + return destReply, nil +} + +func (m SAMBridge) destReply(reply string) { + dest := DestReply{} + fields := strings.Fields(reply) + m.destHandler.m.Lock() + if fields[2] != "RESULT=OK" { + dest.err = errors.New(strings.TrimRight(strings.TrimLeft(strings.Join(fields[3:], " "), "MESSAGE=\""), "\"")) + } else { + dest.Pub = strings.TrimPrefix(fields[2], "PUB=") + dest.Priv = strings.TrimPrefix(fields[3], "PRIV=") + dest.err = nil + } + m.destHandler.incoming <- dest + m.destHandler.m.Unlock() +} + +func (s SAMBridge) pingReply(reply string) error { + s.Send(fmt.Sprintf("PONG %s", strings.TrimLeft(reply, "PING "))) + return nil +} + +func (m *SAMBridge) newDestHandler() { + if m.destHandler == nil { + dh := destHandler{} + dh.incoming = make(chan DestReply) + m.destHandler = &dh + } +} diff --git a/session/master.go b/session/master.go new file mode 100644 index 0000000..8ea1e22 --- /dev/null +++ b/session/master.go @@ -0,0 +1,104 @@ +package session + +import ( + "errors" + "fmt" + "log" + "strconv" + "time" + + "yagsl/sambridge" +) + +const masterSessionStyle string = "MASTER" + +var errMasterSessionFailed = errors.New("Failed to start MasterSession") + +type MasterSession struct { + Style string + ID string + Destination string + Transient bool + + Sam *sambridge.SAMBridge + *Session + + Ready chan bool + + Subsessions []Subsession + + // i2cp and streaming options go here +} + +func NewMasterSession(destination string) (MasterSession, error) { + sb := new(sambridge.SAMBridge) + sb.Options.Address = "127.0.0.1:7656" + go sb.Start() + ms := MasterSession{Style: "MASTER"} + ms.Ready = make(chan bool, 1) + if destination == "" { + destination = "TRANSIENT" + ms.Transient = true + } + ms.Sam = sb + ms.Sam.SessionReply = make(chan sambridge.SessionReply) + go ms.newSessionHandler() + + ms.Sam.StreamReply = make(chan sambridge.StreamReply) + go ms.newStreamHandler() + + ms.ID = "master" + strconv.FormatInt(time.Now().UTC().Unix(), 10) + ms.Destination = destination + + time.Sleep(time.Second * 1) + ms.Sam.Send(fmt.Sprintf("SESSION CREATE STYLE=MASTER ID=%s DESTINATION=%s\n", ms.ID, ms.Destination)) + for ready := range ms.Ready { + if ready { + return ms, nil + } + return ms, errMasterSessionFailed + } + return ms, nil +} + +func (ms *MasterSession) NewSubsession(fromPort string, toPort string, listenPort string) Subsession { + ss := Subsession{} + + ss.ID = "sub" + strconv.FormatInt(time.Now().UTC().Unix(), 10) + if listenPort != "" { + ms.Sam.Send(fmt.Sprintf("SESSION ADD STYLE=STREAM ID=%s FROM_PORT=%s LISTEN_PORT=%s SILENT=FALSE\n", ss.ID, fromPort, listenPort)) + } else { + ms.Sam.Send(fmt.Sprintf("SESSION ADD STYLE=STREAM ID=%s FROM_PORT=%s SILENT=FALSE\n", ss.ID, fromPort)) + } + + ms.Subsessions = append(ms.Subsessions, ss) + + return ss +} + +func (ms *MasterSession) newSessionHandler() { + var sessionReply sambridge.SessionReply + for reply := range ms.Sam.SessionReply { + sessionReply = reply + break + } + if sessionReply.Err != nil { + ms.Ready <- false + } + ms.Destination = sessionReply.Destination + ms.Ready <- true +} + +func (ms *MasterSession) newStreamHandler() { + var streamReply sambridge.StreamReply + for reply := range ms.Sam.StreamReply { + streamReply = reply + break + } + if streamReply.Err != nil { + log.Printf("StreamReply err: %s", streamReply.Err.Error()) + } else { + log.Printf("StreamReply success!") + + } +} diff --git a/session/session.go b/session/session.go new file mode 100644 index 0000000..2c09394 --- /dev/null +++ b/session/session.go @@ -0,0 +1,51 @@ +package session + +import ( + "log" + "strings" + "sync" +) + +type Session struct { + Style string + ID string + Destination string + Options SessionOptions +} + +type SessionOptions struct { + Port string + Host string + FromPort string + ToPort string + ListenPort string + ListenProtcol string + Header bool + // i2cp and streaming options go here +} + +type sessionHandler struct { + incoming chan reply + m sync.Mutex +} + +type reply struct { + full string + + name string + value string + err error +} + +func (m *Session) SessionCreate() { +} + +func (s Session) sessionReply(reply string) error { + fields := strings.Fields(reply) + if strings.HasPrefix(fields[1], "RESULT=") { + log.Printf("Session status result: %s", fields[1:]) + } else { + log.Printf("Got SESSION reply: %s", reply) + } + return nil +} diff --git a/session/subsession.go b/session/subsession.go new file mode 100644 index 0000000..d8104c7 --- /dev/null +++ b/session/subsession.go @@ -0,0 +1,77 @@ +package session + +import ( + "errors" + "fmt" + "net" + "time" + "yagsl/sambridge" +) + +type Subsession struct { + *Session + ID string + Style string + Sam *sambridge.SAMBridge + Ready chan (bool) +} + +var ( + errSubSessionFailed = errors.New("the subsession failed :(") +) + +func NewStreamConnect(ID string, destination string, fromPort string) (Subsession, error) { + sb := new(sambridge.SAMBridge) + go sb.Start() + ss := Subsession{ + Style: "STREAM", + ID: ID, + } + ss.Sam = sb + ss.Sam.SessionReply = make(chan sambridge.SessionReply) + go ss.newSessionHandler() + // TODO: Replace this handshake pause with a Ready callback to prevent a data race + time.Sleep(time.Second * 1) + // TODO: Refactor this sam.Send into a stream package w/a streamHandler and it's own connect message in that section + err := ss.Sam.Send(fmt.Sprintf("STREAM CONNECT ID=%s DESTINATION=%s FROM_PORT=%s TO_PORT=0 SILENT=FALSE\n", ID, destination, fromPort)) + return ss, err +} + +func NewStreamAccept(ID string) (Subsession, error) { + sb := new(sambridge.SAMBridge) + go sb.Start() + ss := Subsession{ + Style: "STREAM", + ID: ID, + } + ss.Sam = sb + ss.Sam.SessionReply = make(chan sambridge.SessionReply) + go ss.newSessionHandler() + // TODO: Replace this handshake pause with a Ready callback to prevent a data race + time.Sleep(time.Second * 1) + // TODO: Refactor this sam.Send into a stream package w/a streamHandler and it's own connect message in that section + err := ss.Sam.Send(fmt.Sprintf("STREAM ACCEPT ID=%s SILENT=FALSE\n", ID)) + return ss, err +} + +func (ss *Subsession) newSessionHandler() { + var sessionReply sambridge.SessionReply + for reply := range ss.Sam.SessionReply { + sessionReply = reply + break + } + if sessionReply.Err != nil { + ss.Ready <- false + } + ss.Destination = sessionReply.Destination + ss.Ready <- true +} + +func (ss *Subsession) Stop() error { + ss.Sam.Send(fmt.Sprintf("SESSION REMOVE ID=%s", ss.ID)) + return nil +} + +func (s *Subsession) Dial(network, addr string) (net.Conn, error) { + return s.Sam.Conn, nil +}