summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--xmpp/element_buffer.go61
-rw-r--r--xmpp/element_buffer_test.go55
-rw-r--r--xmpp/encoder_decoder.go80
-rw-r--r--xmpp/jid.go67
-rw-r--r--xmpp/presence.go13
-rw-r--r--xmpp/router.go62
-rw-r--r--xmpp/router_test.go70
-rw-r--r--xmpp/routing.go41
-rw-r--r--xmpp/sasl.go64
-rw-r--r--xmpp/session.go40
-rw-r--r--xmpp/stream_pair.go131
-rw-r--r--xmpp/streams.go64
-rw-r--r--xmpp/xml.go68
13 files changed, 245 insertions, 571 deletions
diff --git a/xmpp/element_buffer.go b/xmpp/element_buffer.go
deleted file mode 100644
index 7792db2..0000000
--- a/xmpp/element_buffer.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package xmpp
-
-import (
- "encoding/xml"
-)
-
-// elementBuffer is a struct to store multiple values of type xml.Token until
-// they form a complete XML element with opening and closing tag which is
-// suitable to be passed to an appropriate handler function.
-type elementBuffer struct {
- tokens []xml.Token
- end xml.EndElement
- level int
-}
-
-// newElementBuffer returns a new initialized elementBuffer struct.
-func newElementBuffer() elementBuffer {
- buf := elementBuffer{}
- buf.reset()
- return buf
-}
-
-// FIXME this function needs essential error handling for corner cases!
-//
-// add is able to add a new xml.Token to the buffer. There are some rules
-// checked to ensure a correct and consistent elementBuffer which are checked.
-// If one of these checks fail the token is not added and a corresponding error
-// is returned.
-func (e *elementBuffer) add(t xml.Token) error {
- switch t.(type) {
- case xml.StartElement:
- e.level += 1
- case xml.EndElement:
- e.level -= 1
- }
- e.tokens = append(e.tokens, t)
- return nil
-}
-
-// FIXME isComplete would be true if a stream with only one XML comment is
-// passed to the buffer. This might be unexpected behaviour.
-//
-// isComplete returns true if the buffer contains a slice of XML tokens which
-// form a complete XML element starting with an xml.StartElement and closing
-// with the corresponding xml.EndElement.
-func (e *elementBuffer) isComplete() bool {
- return (len(e.tokens) > 0 && e.level == 0)
-}
-
-// reset returns the content of the buffer as a slice of XML tokens and resets
-// the buffer to the initial state. This function can be used to initialize the
-// elementBuffer struct. In that case the return value can be ignored.
-func (e *elementBuffer) reset() (buf []xml.Token) {
- retval := e.tokens
-
- e.tokens = make([]xml.Token, 0)
- e.end = xml.EndElement{}
- e.level = 0
-
- return retval
-}
diff --git a/xmpp/element_buffer_test.go b/xmpp/element_buffer_test.go
deleted file mode 100644
index af3d5c2..0000000
--- a/xmpp/element_buffer_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package xmpp
-
-import (
- "encoding/xml"
- "strings"
- "testing"
-)
-
-// bufTest is a struct containing a test point for the
-// xengineering.eu/limox/xmpp.elementBuffer. It contains a test XML string
-// which has to be exactly one XML element and an array of indentation levels
-// which have to be checked after each token which is parsed.
-type bufTest struct {
- xml string
- levels []int
-}
-
-func TestElementBuffer(t *testing.T) {
- tests := []bufTest{
- bufTest{`<stream></stream>`, []int{1, 0}},
- bufTest{`<stream/>`, []int{1, 0}},
- bufTest{`<a><b>testing</b></a>`, []int{1, 2, 2, 1, 0}},
- bufTest{`<a><!-- comment --><b>testing</b></a>`, []int{1, 1, 2, 2, 1, 0}},
- bufTest{`<!-- comment --><a><b>testing</b></a>`, []int{0, 1, 2, 2, 1, 0}},
- }
-
- for _, v := range tests {
- r := strings.NewReader(v.xml)
- d := xml.NewDecoder(r)
- b := newElementBuffer()
-
- i := 0
-
- for {
- token, err := d.Token()
- if err != nil {
- if i != len(v.levels) {
- t.Fatalf("Stopped parsing at unexpected index due to error `%v`!\n", err)
- }
- break
- }
-
- err = b.add(token)
- if err != nil {
- t.Fatalf("add(token) failed with error `%v`!\n", err)
- }
-
- if b.level != v.levels[i] {
- t.Fatalf("Indent level of xmpp.elementBuffer %d does not match value given by test data %d!\n", b.level, v.levels[i])
- }
-
- i += 1
- }
- }
-}
diff --git a/xmpp/encoder_decoder.go b/xmpp/encoder_decoder.go
deleted file mode 100644
index b0ea77b..0000000
--- a/xmpp/encoder_decoder.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package xmpp
-
-import (
- "encoding/xml"
- "errors"
- "io"
- "log"
-)
-
-type encoderDecoder struct {
- session *session
- tx *xml.Encoder
- rx *xml.Decoder
- terminator chan bool
-}
-
-func newEncoderDecoder(s *session) encoderDecoder {
- ed := encoderDecoder{}
-
- ed.session = s
-
- lw := logger{"[TX] "}
- w := io.MultiWriter(s.transport, lw)
- ed.tx = xml.NewEncoder(w)
- ed.tx.Indent("", "")
-
- lr := logger{"[RX] "}
- r := io.TeeReader(s.transport, lr)
- ed.rx = xml.NewDecoder(r)
-
- return ed
-}
-
-func (ed *encoderDecoder) encodeToken(t xml.Token) error {
- var err error
- defer func() {
- if err != nil {
- log.Println(err)
- }
- }()
-
- err = ed.tx.EncodeToken(t)
- if err != nil {
- return err
- }
- err = ed.tx.Flush()
- if err != nil {
- return err
- }
-
- return nil
-}
-
-func (ed *encoderDecoder) run() {
- for {
- select {
- case <-ed.terminator:
- return
- default:
- t, err := ed.rx.Token()
- if t != nil && err == nil {
- switch t.(type) {
- case xml.ProcInst:
- case xml.Directive:
- case xml.Comment:
- default:
- c := xml.CopyToken(t)
- ed.session.rx <- c
- }
- }
- if err != nil {
- if errors.Is(err, io.EOF) {
- return
- }
- log.Println(err) // FIXME terminate session on error
- return
- }
- }
- }
-}
diff --git a/xmpp/jid.go b/xmpp/jid.go
index fd0d7ae..9580ad5 100644
--- a/xmpp/jid.go
+++ b/xmpp/jid.go
@@ -41,62 +41,45 @@ func username(jid string) string {
return ""
}
-func hasBind(e []xml.Token) bool {
- bind := xml.Name{`urn:ietf:params:xml:ns:xmpp-bind`, `bind`}
-
- for _, v := range e {
- switch s := v.(type) {
- case xml.StartElement:
- if s.Name == bind {
- return true
- }
- }
- }
-
- return false
+type bindRequest struct {
+ Bind struct {
+ Xmlns string `xml:"xmlns,attr"`
+ Resource struct {
+ Content string `xml:",chardata"`
+ } `xml:"resource"`
+ } `xml:"bind"`
}
func (s *session) sendBind() {
+
s.resourceReq = fmt.Sprintf("%016x", rand.Uint64())
- iqStart := xml.StartElement{
+ start := xml.StartElement{
xml.Name{"jabber:client", "iq"},
[]xml.Attr{
xml.Attr{xml.Name{"", "id"}, s.resourceReq},
xml.Attr{xml.Name{"", "type"}, "set"},
},
}
- iqEnd := iqStart.End()
- bindStart := xml.StartElement{
- xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"},
- []xml.Attr{},
- }
- bindEnd := bindStart.End()
+ inner := bindRequest{}
+ inner.Bind.Xmlns = "urn:ietf:params:xml:ns:xmpp-bind"
+ inner.Bind.Resource.Content = "limox-" + fmt.Sprintf("%08x", rand.Uint32())
- resourceStart := xml.StartElement{
- xml.Name{"", "resource"},
- []xml.Attr{},
- }
- resourceEnd := resourceStart.End()
-
- name := xml.CharData("limox-" + fmt.Sprintf("%08x", rand.Uint32()))
-
- tokens := [...]xml.Token{
- iqStart,
- bindStart,
- resourceStart,
- name,
- resourceEnd,
- bindEnd,
- iqEnd,
+ err := s.tx.EncodeElement(inner, start)
+ if err != nil {
+ log.Println("Could not encode ressource binding!")
}
+}
- for _, v := range tokens {
- err := s.ed.encodeToken(v)
- if err != nil {
- log.Println("Could not encode ressource binding!")
- return
- }
+type iqResponse struct {
+ Jid string `xml:"urn:ietf:params:xml:ns:xmpp-bind bind>jid"`
+}
+
+func handleIqResponse(s *session, i iqResponse) {
+ if i.Jid != "" {
+ s.jid = i.Jid
+ s.sendPresence()
+ return
}
}
diff --git a/xmpp/presence.go b/xmpp/presence.go
index b6ea3b5..00eaa47 100644
--- a/xmpp/presence.go
+++ b/xmpp/presence.go
@@ -10,15 +10,10 @@ func (s *session) sendPresence() {
xml.Name{"", "presence"},
[]xml.Attr{},
}
- end := start.End()
- tokens := [...]xml.Token{start, end}
-
- for _, v := range tokens {
- err := s.ed.encodeToken(v)
- if err != nil {
- log.Println("Could not encode presence!")
- return
- }
+ err := s.tx.EncodeElement(struct{}{}, start)
+ if err != nil {
+ log.Println("Could not encode presence!")
+ return
}
}
diff --git a/xmpp/router.go b/xmpp/router.go
deleted file mode 100644
index 1e21c9b..0000000
--- a/xmpp/router.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package xmpp
-
-import (
- "encoding/xml"
- "log"
-)
-
-// routingTable is a data structure which contains routing information for XML
-// elements. The xml.StartElement at the beginning of an XML element has a name
-// containing the XML namespace and a local name. Based on this compisition
-// which forms the xml.Name the appropriate handler function is defined by each
-// entry of the routingTable.
-type routingTable []struct {
- name xml.Name
- handler func(*session, []xml.Token)
-}
-
-// getRoutingTable returns the routing table used in
-// xengineering.eu/limox/xmpp. Since Go does not allow such a datatype as a
-// constant such a function is a simple yet inefficient approach to guarantee
-// that an unmodified routing table is delivered to each user. A global
-// variable would have the problem that it could be altered during execution.
-func getRoutingTable() routingTable {
- return routingTable{
- {xml.Name{`http://etherx.jabber.org/streams`, `features`}, streamFeaturesHandler},
- {xml.Name{`urn:ietf:params:xml:ns:xmpp-sasl`, `success`}, saslSuccessHandler},
- {xml.Name{`urn:ietf:params:xml:ns:xmpp-sasl`, `failure`}, saslFailureHandler},
- {xml.Name{`jabber:client`, `iq`}, iqHandler},
- }
-}
-
-// route determines the correct handler function for the given XML element by a
-// given routingTable. In addition it executes the determined handler function.
-// If no handler function is found an error message is send via the log module.
-func route(s *session, e []xml.Token, t routingTable) {
- var name xml.Name
-
- // TODO a stronger definition of an XML element (as here
- // https://www.w3schools.com/xml/xml_elements.asp) would define that the
- // first Token of an element is a StartElement token. This would make this
- // code easier.
- escape := false
- for _, token := range e {
- switch s := token.(type) {
- case xml.StartElement:
- name = s.Name
- escape = true
- }
- if escape {
- break
- }
- }
-
- for _, r := range t {
- if name == r.name {
- r.handler(s, e)
- return
- }
- }
-
- log.Println("Could not route XML element")
-}
diff --git a/xmpp/router_test.go b/xmpp/router_test.go
deleted file mode 100644
index b490778..0000000
--- a/xmpp/router_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package xmpp
-
-import (
- "encoding/xml"
- "strings"
- "testing"
-)
-
-// routerTest contains a single test case for the xmpp.router. The XML element
-// is given as a string and the value int describes the expected value of a
-// test variable inside the corresponding unit test. See TestRouter for
-// details.
-type routerTest struct {
- xml string
- value int
-}
-
-// TestRouter tests the xmpp/router.go file. The central functionality is the
-// route() function. To test this a fake routing table is created inside this
-// test with XML element handler function produced by the factory function
-// defined below. each handler simply sets the value of the testpoint variable
-// to the value mentioned by the test point. That way the routing can be
-// validated.
-func TestRouter(t *testing.T) {
- var testpoint int
- var s session
-
- factory := func(tp *int, i int) func(*session, []xml.Token) {
- return func(*session, []xml.Token) {
- *tp = i
- }
- }
-
- tests := []routerTest{
- routerTest{`<a></a>`, 1},
- routerTest{`<b></b>`, 2},
- routerTest{`<c></c>`, 3},
- routerTest{`<b xmlns='https://xengineering.eu'></b>`, 4},
- routerTest{`<a xmlns='https://xengineering.eu'></a>`, 5},
- }
-
- testRouting := routingTable{
- {xml.Name{``, `a`}, factory(&testpoint, 1)},
- {xml.Name{``, `b`}, factory(&testpoint, 2)},
- {xml.Name{``, `c`}, factory(&testpoint, 3)},
- {xml.Name{`https://xengineering.eu`, `b`}, factory(&testpoint, 4)},
- {xml.Name{`https://xengineering.eu`, `a`}, factory(&testpoint, 5)},
- }
-
- for _, v := range tests {
- testpoint = 0
-
- r := strings.NewReader(v.xml)
- d := xml.NewDecoder(r)
- tokens := make([]xml.Token, 0)
- for {
- token, err := d.Token()
- if err != nil {
- break
- }
- tokens = append(tokens, xml.CopyToken(token))
- }
-
- route(&s, tokens, testRouting)
-
- if testpoint != v.value {
- t.Fatalf("XML element was not routed correctly!\n")
- }
- }
-}
diff --git a/xmpp/routing.go b/xmpp/routing.go
new file mode 100644
index 0000000..5cd2040
--- /dev/null
+++ b/xmpp/routing.go
@@ -0,0 +1,41 @@
+package xmpp
+
+import (
+ "encoding/xml"
+ "log"
+)
+
+func route(s *xml.StartElement, d *xml.Decoder, c chan<- any) {
+ switch (*s).Name {
+ case xml.Name{`http://etherx.jabber.org/streams`, `features`}:
+ parse(streamFeatures{}, s, d, c)
+ case xml.Name{`urn:ietf:params:xml:ns:xmpp-sasl`, `success`}:
+ parse(saslSuccess{}, s, d, c)
+ case xml.Name{`jabber:client`, `iq`}:
+ parse(iqResponse{}, s, d, c)
+ default:
+ d.Skip()
+ }
+}
+
+func parse[T any](data T, s *xml.StartElement, d *xml.Decoder, c chan<- any) {
+ err := d.DecodeElement(&data, s)
+ if err != nil {
+ log.Printf("Could not decode stream features: %v\n", err)
+ } else {
+ c <- data
+ }
+}
+
+func handle(s *session, element any) {
+ switch t := element.(type) {
+ case streamFeatures:
+ handleStreamFeatures(s, t)
+ case saslSuccess:
+ handleSaslSuccess(s)
+ case iqResponse:
+ handleIqResponse(s, t)
+ default:
+ log.Printf("Unknown parsed element: %v", t)
+ }
+}
diff --git a/xmpp/sasl.go b/xmpp/sasl.go
index 512fd58..69b536d 100644
--- a/xmpp/sasl.go
+++ b/xmpp/sasl.go
@@ -6,69 +6,31 @@ import (
"log"
)
-func (s *session) sasl() {
- tokens := make([]xml.Token, 0, 3)
+type saslRequest struct {
+ Payload []byte `xml:",chardata"`
+}
+func (s *session) sasl() {
start := xml.StartElement{
xml.Name{"urn:ietf:params:xml:ns:xmpp-sasl", "auth"},
[]xml.Attr{
xml.Attr{xml.Name{"", "mechanism"}, "PLAIN"},
},
}
- tokens = append(tokens, start)
data := []byte("\x00" + username(s.jid) + "\x00" + s.pwd)
- dst := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
- base64.StdEncoding.Encode(dst, data)
- payload := xml.CharData(dst)
- tokens = append(tokens, payload)
-
- end := start.End()
- tokens = append(tokens, end)
-
- for _, t := range tokens {
- err := s.ed.encodeToken(t)
- if err != nil {
- log.Println("Could not encode SASL PLAIN element!")
- return
- }
- }
-}
-
-// hasSaslPlain scans the given stream features XML element for the SASL PLAIN
-// mechanism which is supported by xengineering.eu/limox/xmpp. It returns true
-// if the stream has support for this mechanism and false otherwise.
-func hasSaslPlain(e []xml.Token) bool {
- mechanism := xml.Name{`urn:ietf:params:xml:ns:xmpp-sasl`, `mechanism`}
+ inner := saslRequest{}
+ inner.Payload = make([]byte, base64.StdEncoding.EncodedLen(len(data)))
+ base64.StdEncoding.Encode(inner.Payload, data)
- for i, t := range e {
- switch s := t.(type) {
- case xml.StartElement:
- if s.Name == mechanism {
- if i+1 < len(e) {
- subtype := func() string {
- switch c := e[i+1].(type) {
- case xml.CharData:
- return string(c)
- default:
- return ""
- }
- }()
- if subtype == `PLAIN` {
- return true
- }
- }
- }
- }
+ err := s.tx.EncodeElement(inner, start)
+ if err != nil {
+ log.Println("Could not encode SASL PLAIN element!")
}
-
- return false
}
-func saslSuccessHandler(s *session, e []xml.Token) {
- runStreamPair(s)
-}
+type saslSuccess struct{}
-func saslFailureHandler(s *session, e []xml.Token) {
- log.Println("SASL autentication failed!")
+func handleSaslSuccess(s *session) {
+ openStream(s.tx, s.jid)
}
diff --git a/xmpp/session.go b/xmpp/session.go
index a43e4f4..4dfd76f 100644
--- a/xmpp/session.go
+++ b/xmpp/session.go
@@ -1,9 +1,11 @@
package xmpp
import (
+ "context"
"crypto/tls"
"crypto/x509"
"encoding/xml"
+ "io"
"log"
)
@@ -17,8 +19,8 @@ type session struct {
in chan any
out chan<- any
transport *tls.Conn
- ed encoderDecoder
- rx chan xml.Token
+ tx *xml.Encoder
+ rx chan any
resourceReq string
}
@@ -29,7 +31,7 @@ func StartSession(out chan<- any, jid string, pwd string) (in chan<- any) {
s.pwd = pwd
s.in = make(chan any)
s.out = out
- s.rx = make(chan xml.Token, 0)
+ s.rx = make(chan any, 0)
go s.run()
@@ -37,21 +39,39 @@ func StartSession(out chan<- any, jid string, pwd string) (in chan<- any) {
}
func (s *session) run() {
- defer func() { s.out <- SessionDisconnect{} }()
-
err := s.startTransport()
if err != nil {
return
}
defer s.transport.Close()
- s.ed = newEncoderDecoder(s)
- go s.ed.run()
- defer func() { s.ed.terminator <- true }()
+ ctx, cancel := context.WithCancel(context.Background())
+ cpy := s.rx
+ go runRx(ctx, cpy, s.transport)
+ defer cancel()
+
+ lw := logger{"[TX] "}
+ w := io.MultiWriter(s.transport, lw)
+ s.tx = xml.NewEncoder(w)
+ defer s.tx.Close()
- s.out <- SessionConnect{}
+ openStream(s.tx, s.jid)
+ defer closeStream(s.tx)
- runStreamPair(s)
+ s.out <- SessionConnect{} // TODO this should be sent after initial presence
+ defer func() { s.out <- SessionDisconnect{} }()
+
+ for {
+ select {
+ case e := <-s.rx:
+ handle(s, e)
+ case signal := <-s.in:
+ switch signal.(type) {
+ case SessionShouldDisconnect:
+ return
+ }
+ }
+ }
}
func (s *session) startTransport() error {
diff --git a/xmpp/stream_pair.go b/xmpp/stream_pair.go
deleted file mode 100644
index 87df86a..0000000
--- a/xmpp/stream_pair.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package xmpp
-
-import (
- "encoding/xml"
- "log"
-)
-
-func runStreamPair(s *session) {
- end := openStream(s)
- defer closeStream(s, end)
-
- buf := newElementBuffer()
-
- for {
- select {
- case data := <-s.in:
- switch data.(type) {
- case SessionShouldDisconnect:
- return
- default:
- log.Printf("Unknown data '%d'!\n", data)
- }
- case t := <-s.rx:
- err := buf.add(t)
- if err != nil {
- log.Printf("Could not add XML token to buffer: %v\n", err)
- return
- }
- if buf.isComplete() {
- element := buf.reset()
- route(s, element, getRoutingTable())
- }
- }
- }
-}
-
-func openStream(s *session) xml.EndElement {
- start := xml.StartElement{
- xml.Name{"jabber:client", "stream:stream"},
- []xml.Attr{
- xml.Attr{xml.Name{"", "from"}, s.jid},
- xml.Attr{xml.Name{"", "to"}, domainpart(s.jid)},
- xml.Attr{xml.Name{"", "version"}, "1.0"},
- xml.Attr{xml.Name{"", "xml:lang"}, "en"},
- xml.Attr{xml.Name{"", "xmlns:stream"}, "http://etherx.jabber.org/streams"},
- },
- }
- end := start.End()
-
- err := s.ed.encodeToken(start)
- if err != nil {
- log.Println("Could not encode stream start!")
- }
-
- syncStreams(s)
-
- return end
-}
-
-// syncStreams drops XML tokens from the receiving stream until an
-// xml.StartElement with the local name `stream` is received. If this function
-// is called after opening a new stream in the sending direction it is ensured
-// that both streams directions work on the same stream level and are in sync.
-// Tokens received which are not a stream StartElement are not handled but
-// logged since this should not happen.
-func syncStreams(s *session) {
- for {
- select {
- case data := <-s.in:
- switch data.(type) {
- case SessionShouldDisconnect:
- return
- default:
- log.Printf("Unhandled data '%d' during stream sync!\n", data)
- }
- case t := <-s.rx:
- switch token := t.(type) {
- case xml.StartElement:
- if token.Name.Local == "stream" {
- return
- }
- }
- log.Printf("Unhandled XML token '%v' during stream sync!\n", t)
- }
- }
-}
-
-func closeStream(s *session, end xml.EndElement) {
- err := s.ed.encodeToken(end)
- if err != nil {
- log.Println("Could not encode stream end!")
- }
-}
-
-func streamFeaturesHandler(s *session, e []xml.Token) {
- if hasSaslPlain(e) {
- s.sasl()
- return
- }
-
- if hasBind(e) {
- s.sendBind()
- return
- }
-
- log.Println("Stream has no implemented features!")
-}
-
-func iqHandler(s *session, e []xml.Token) {
- isResult := false
- idMatches := false
-
- result := xml.Attr{xml.Name{"", "type"}, "result"}
- id := xml.Attr{xml.Name{"", "id"}, s.resourceReq}
-
- switch start := e[0].(type) {
- case xml.StartElement:
- for _, v := range start.Attr {
- if v == result {
- isResult = true
- }
- if v == id {
- idMatches = true
- }
- }
-
- if isResult && idMatches {
- s.sendPresence()
- }
- }
-}
diff --git a/xmpp/streams.go b/xmpp/streams.go
new file mode 100644
index 0000000..9f6ffe8
--- /dev/null
+++ b/xmpp/streams.go
@@ -0,0 +1,64 @@
+package xmpp
+
+import (
+ "encoding/xml"
+ "log"
+)
+
+type streamFeatures struct {
+ SaslMechanisms []string `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms>mechanism"`
+ Bind *bool `xml:"urn:ietf:params:xml:ns:xmpp-bind bind,omitempty"`
+}
+
+func handleStreamFeatures(s *session, f streamFeatures) {
+ if len(f.SaslMechanisms) > 0 {
+ for _, v := range f.SaslMechanisms {
+ if v == "PLAIN" {
+ s.sasl()
+ return
+ }
+ }
+ log.Println("No compatible SASL mechanism found!")
+ return
+ }
+
+ if f.Bind != nil {
+ s.sendBind()
+ return
+ }
+}
+
+func openStream(e *xml.Encoder, jid string) {
+ start := xml.StartElement{
+ xml.Name{"jabber:client", "stream:stream"},
+ []xml.Attr{
+ xml.Attr{xml.Name{"", "from"}, jid},
+ xml.Attr{xml.Name{"", "to"}, domainpart(jid)},
+ xml.Attr{xml.Name{"", "version"}, "1.0"},
+ xml.Attr{xml.Name{"", "xml:lang"}, "en"},
+ xml.Attr{xml.Name{"", "xmlns:stream"}, "http://etherx.jabber.org/streams"},
+ },
+ }
+
+ err := e.EncodeToken(start)
+ if err != nil {
+ log.Println("Could not encode stream start!")
+ }
+ err = e.Flush()
+ if err != nil {
+ log.Println("Could not flush after stream start!")
+ }
+}
+
+func closeStream(e *xml.Encoder) {
+ end := xml.EndElement{xml.Name{"jabber:client", "stream:stream"}}
+
+ err := e.EncodeToken(end)
+ if err != nil {
+ log.Println("Could not encode stream end!")
+ }
+ err = e.Flush()
+ if err != nil {
+ log.Println("Could not flush after stream end!")
+ }
+}
diff --git a/xmpp/xml.go b/xmpp/xml.go
new file mode 100644
index 0000000..e6fccee
--- /dev/null
+++ b/xmpp/xml.go
@@ -0,0 +1,68 @@
+package xmpp
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/xml"
+ "errors"
+ "io"
+ "log"
+)
+
+func runRx(ctx context.Context, chn chan<- any, conn *tls.Conn) {
+
+ l := logger{"[RX] "}
+ r := io.TeeReader(conn, l)
+ d := xml.NewDecoder(r)
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ t, err := d.Token()
+ if t != nil && err == nil {
+ switch e := t.(type) {
+ case xml.StartElement:
+ if e.Name.Local == "stream" {
+ // new server-side stream TODO what to do with this info?
+ } else {
+ route(&e, d, chn)
+ }
+ case xml.EndElement:
+ if e.Name.Local == "stream" {
+ // TODO end complete session
+ return
+ }
+ }
+ }
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ return
+ }
+ log.Println(err) // FIXME terminate session on error
+ return
+ }
+ }
+ }
+}
+
+func (s *session) encodeToken(t xml.Token) error {
+ var err error
+ defer func() {
+ if err != nil {
+ log.Println(err)
+ }
+ }()
+
+ err = s.tx.EncodeToken(t)
+ if err != nil {
+ return err
+ }
+ err = s.tx.Flush()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}