summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxengineering <me@xengineering.eu>2023-07-04 22:10:55 +0200
committerxengineering <me@xengineering.eu>2023-07-04 22:15:17 +0200
commit48811e7d2487ebc3db49b8af7e20f57db4ac28f4 (patch)
treea317f8dc44ac9828ae5806e1fa1dee7547118619
parent5570ccd1d6b50042acbf2fad3793afa6dde79ca2 (diff)
parentd9fe0a4360770b1e4b6b4fb3686c3275ad1b6e6e (diff)
downloadlimox-48811e7d2487ebc3db49b8af7e20f57db4ac28f4.tar
limox-48811e7d2487ebc3db49b8af7e20f57db4ac28f4.tar.zst
limox-48811e7d2487ebc3db49b8af7e20f57db4ac28f4.zip
Merge branch 'element-handling'
This moves away from the concept to parse each individual XML token from the token and group them as a []xml.Token slice for further processing. While this is still possible, receiving aswell as sending has switched to define structs with XML tags which can be marshalled and unmarshalled with the xml.Encoder.EncodeElement() and xml.Decoder.DecodeElement() functions. Further documentation can be found at https://pkg.go.dev/encoding/xml#Unmarshal and https://pkg.go.dev/encoding/xml#Marshal. This merge reduces with its changes the number of code lines in the XMPP implementation by around 41 percent!
-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
+}