From cf520b079743ec95d085a439d00b841c253c564a Mon Sep 17 00:00:00 2001 From: xengineering Date: Mon, 3 Jul 2023 21:10:51 +0200 Subject: Rename encoder_decoder.go to xml.go This is way shorter and serves the same purpose: It reflects the responsibility of encoding and decoding XML. The encoderDecoder struct should be removed soon. --- xmpp/xml.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 xmpp/xml.go (limited to 'xmpp/xml.go') diff --git a/xmpp/xml.go b/xmpp/xml.go new file mode 100644 index 0000000..b0ea77b --- /dev/null +++ b/xmpp/xml.go @@ -0,0 +1,80 @@ +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 + } + } + } +} -- cgit v1.2.3-70-g09d2 From 2fade1039c1842f08b30da5c95b5542b57e38ec6 Mon Sep 17 00:00:00 2001 From: xengineering Date: Mon, 3 Jul 2023 22:17:15 +0200 Subject: Move xml.Encoder to session struct The encoderDecoder sub-struct of the session struct should be removed in little steps. This is the first one. --- xmpp/jid.go | 2 +- xmpp/presence.go | 2 +- xmpp/sasl.go | 2 +- xmpp/session.go | 6 ++++++ xmpp/streams.go | 4 ++-- xmpp/xml.go | 12 +++--------- 6 files changed, 14 insertions(+), 14 deletions(-) (limited to 'xmpp/xml.go') diff --git a/xmpp/jid.go b/xmpp/jid.go index 332073b..83772fd 100644 --- a/xmpp/jid.go +++ b/xmpp/jid.go @@ -81,7 +81,7 @@ func (s *session) sendBind() { inner.Bind.Xmlns = "urn:ietf:params:xml:ns:xmpp-bind" inner.Bind.Resource.Content = "limox-" + fmt.Sprintf("%08x", rand.Uint32()) - err := s.ed.tx.EncodeElement(inner, start) + err := s.tx.EncodeElement(inner, start) if err != nil { log.Println("Could not encode ressource binding!") } diff --git a/xmpp/presence.go b/xmpp/presence.go index 4adae6e..e2b1841 100644 --- a/xmpp/presence.go +++ b/xmpp/presence.go @@ -13,7 +13,7 @@ func (s *session) sendPresence() { []xml.Attr{}, } - err := s.ed.tx.EncodeElement(presence{}, start) + err := s.tx.EncodeElement(presence{}, start) if err != nil { log.Println("Could not encode presence!") return diff --git a/xmpp/sasl.go b/xmpp/sasl.go index 24edc9a..0c13f36 100644 --- a/xmpp/sasl.go +++ b/xmpp/sasl.go @@ -23,7 +23,7 @@ func (s *session) sasl() { inner.Payload = make([]byte, base64.StdEncoding.EncodedLen(len(data))) base64.StdEncoding.Encode(inner.Payload, data) - err := s.ed.tx.EncodeElement(inner, start) + err := s.tx.EncodeElement(inner, start) if err != nil { log.Println("Could not encode SASL PLAIN element!") } diff --git a/xmpp/session.go b/xmpp/session.go index a43e4f4..b4a8fab 100644 --- a/xmpp/session.go +++ b/xmpp/session.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/xml" + "io" "log" ) @@ -18,6 +19,7 @@ type session struct { out chan<- any transport *tls.Conn ed encoderDecoder + tx *xml.Encoder rx chan xml.Token resourceReq string } @@ -49,6 +51,10 @@ func (s *session) run() { go s.ed.run() defer func() { s.ed.terminator <- true }() + lw := logger{"[TX] "} + w := io.MultiWriter(s.transport, lw) + s.tx = xml.NewEncoder(w) + s.out <- SessionConnect{} runStreamPair(s) diff --git a/xmpp/streams.go b/xmpp/streams.go index 87df86a..5ba4c1d 100644 --- a/xmpp/streams.go +++ b/xmpp/streams.go @@ -47,7 +47,7 @@ func openStream(s *session) xml.EndElement { } end := start.End() - err := s.ed.encodeToken(start) + err := s.encodeToken(start) if err != nil { log.Println("Could not encode stream start!") } @@ -86,7 +86,7 @@ func syncStreams(s *session) { } func closeStream(s *session, end xml.EndElement) { - err := s.ed.encodeToken(end) + err := s.encodeToken(end) if err != nil { log.Println("Could not encode stream end!") } diff --git a/xmpp/xml.go b/xmpp/xml.go index b0ea77b..f547210 100644 --- a/xmpp/xml.go +++ b/xmpp/xml.go @@ -9,7 +9,6 @@ import ( type encoderDecoder struct { session *session - tx *xml.Encoder rx *xml.Decoder terminator chan bool } @@ -19,11 +18,6 @@ func newEncoderDecoder(s *session) 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) @@ -31,7 +25,7 @@ func newEncoderDecoder(s *session) encoderDecoder { return ed } -func (ed *encoderDecoder) encodeToken(t xml.Token) error { +func (s *session) encodeToken(t xml.Token) error { var err error defer func() { if err != nil { @@ -39,11 +33,11 @@ func (ed *encoderDecoder) encodeToken(t xml.Token) error { } }() - err = ed.tx.EncodeToken(t) + err = s.tx.EncodeToken(t) if err != nil { return err } - err = ed.tx.Flush() + err = s.tx.Flush() if err != nil { return err } -- cgit v1.2.3-70-g09d2 From 3efcd60f8ebdc962d5be85003cc8c59a2b43e610 Mon Sep 17 00:00:00 2001 From: xengineering Date: Mon, 3 Jul 2023 22:28:01 +0200 Subject: Remove encoderDecoder struct completely This was not really necessary because it was all related to the xmpp.session and should thus be implemented there. Using the context package further reduced the complexity for cancelation. --- xmpp/session.go | 9 ++++---- xmpp/xml.go | 69 ++++++++++++++++++++++++--------------------------------- 2 files changed, 34 insertions(+), 44 deletions(-) (limited to 'xmpp/xml.go') diff --git a/xmpp/session.go b/xmpp/session.go index b4a8fab..6a5e646 100644 --- a/xmpp/session.go +++ b/xmpp/session.go @@ -1,6 +1,7 @@ package xmpp import ( + "context" "crypto/tls" "crypto/x509" "encoding/xml" @@ -18,7 +19,6 @@ type session struct { in chan any out chan<- any transport *tls.Conn - ed encoderDecoder tx *xml.Encoder rx chan xml.Token resourceReq string @@ -47,9 +47,10 @@ func (s *session) run() { } 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) diff --git a/xmpp/xml.go b/xmpp/xml.go index f547210..14c6637 100644 --- a/xmpp/xml.go +++ b/xmpp/xml.go @@ -1,57 +1,26 @@ package xmpp import ( + "context" + "crypto/tls" "encoding/xml" "errors" "io" "log" ) -type encoderDecoder struct { - session *session - rx *xml.Decoder - terminator chan bool -} - -func newEncoderDecoder(s *session) encoderDecoder { - ed := encoderDecoder{} - - ed.session = s +func runRx(ctx context.Context, chn chan xml.Token, conn *tls.Conn) { - lr := logger{"[RX] "} - r := io.TeeReader(s.transport, lr) - ed.rx = xml.NewDecoder(r) + l := logger{"[RX] "} + r := io.TeeReader(conn, l) + d := xml.NewDecoder(r) - return ed -} - -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 -} - -func (ed *encoderDecoder) run() { for { select { - case <-ed.terminator: + case <-ctx.Done(): return default: - t, err := ed.rx.Token() + t, err := d.Token() if t != nil && err == nil { switch t.(type) { case xml.ProcInst: @@ -59,7 +28,7 @@ func (ed *encoderDecoder) run() { case xml.Comment: default: c := xml.CopyToken(t) - ed.session.rx <- c + chn <- c } } if err != nil { @@ -72,3 +41,23 @@ func (ed *encoderDecoder) run() { } } } + +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 +} -- cgit v1.2.3-70-g09d2 From ea98ee187477051444bbf548757af6336d333862 Mon Sep 17 00:00:00 2001 From: xengineering Date: Tue, 4 Jul 2023 12:58:13 +0200 Subject: Re-implement stream open and close This is more suitable for the new RX concept. --- xmpp/session.go | 21 ++++++++++++++++++--- xmpp/streams.go | 53 +++++++++++++++-------------------------------------- xmpp/xml.go | 19 ++++++++++++------- 3 files changed, 45 insertions(+), 48 deletions(-) (limited to 'xmpp/xml.go') diff --git a/xmpp/session.go b/xmpp/session.go index f18fd2a..4be3386 100644 --- a/xmpp/session.go +++ b/xmpp/session.go @@ -39,8 +39,6 @@ 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 @@ -55,8 +53,25 @@ func (s *session) run() { lw := logger{"[TX] "} w := io.MultiWriter(s.transport, lw) s.tx = xml.NewEncoder(w) + defer s.tx.Close() + + openStream(s.tx, s.jid) + defer closeStream(s.tx) - s.out <- SessionConnect{} + s.out <- SessionConnect{} // TODO this should be sent after initial presence + defer func() { s.out <- SessionDisconnect{} }() + + for { + select { + case e := <-s.rx: + log.Print(e) + case signal := <-s.in: + switch signal.(type) { + case SessionShouldDisconnect: + return + } + } + } } func (s *session) startTransport() error { diff --git a/xmpp/streams.go b/xmpp/streams.go index 388be4d..b9c0cb7 100644 --- a/xmpp/streams.go +++ b/xmpp/streams.go @@ -5,62 +5,39 @@ import ( "log" ) -func openStream(s *session) xml.EndElement { +func openStream(e *xml.Encoder, jid string) { 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{"", "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"}, }, } - end := start.End() - err := s.encodeToken(start) + err := e.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) - } + err = e.Flush() + if err != nil { + log.Println("Could not flush after stream start!") } } -func closeStream(s *session, end xml.EndElement) { - err := s.encodeToken(end) +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!") + } } func streamFeaturesHandler(s *session, e []xml.Token) { diff --git a/xmpp/xml.go b/xmpp/xml.go index 14c6637..36d7eb1 100644 --- a/xmpp/xml.go +++ b/xmpp/xml.go @@ -22,13 +22,18 @@ func runRx(ctx context.Context, chn chan xml.Token, conn *tls.Conn) { default: t, err := d.Token() if t != nil && err == nil { - switch t.(type) { - case xml.ProcInst: - case xml.Directive: - case xml.Comment: - default: - c := xml.CopyToken(t) - chn <- c + 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, getRoutingTable()) + } + case xml.EndElement: + if e.Name.Local == "stream" { + // TODO end complete session + return + } } } if err != nil { -- cgit v1.2.3-70-g09d2 From 92534f5af88b42665ad44f2495fe5dfb116d3406 Mon Sep 17 00:00:00 2001 From: xengineering Date: Tue, 4 Jul 2023 13:48:34 +0200 Subject: First working version of new RX concept This uses xml.Decoder.DecodeElement() which makes parsing way easier. This first step is just able to parse stream features partially. --- xmpp/router.go | 38 ++++++++------------------------------ xmpp/session.go | 4 ++-- xmpp/streams.go | 33 +++++++++++++++++++-------------- xmpp/xml.go | 4 ++-- 4 files changed, 31 insertions(+), 48 deletions(-) (limited to 'xmpp/xml.go') diff --git a/xmpp/router.go b/xmpp/router.go index 1e21c9b..d437b28 100644 --- a/xmpp/router.go +++ b/xmpp/router.go @@ -2,7 +2,6 @@ package xmpp import ( "encoding/xml" - "log" ) // routingTable is a data structure which contains routing information for XML @@ -12,7 +11,7 @@ import ( // entry of the routingTable. type routingTable []struct { name xml.Name - handler func(*session, []xml.Token) + handler func(s *xml.StartElement, d *xml.Decoder, c chan<- any) } // getRoutingTable returns the routing table used in @@ -23,40 +22,19 @@ type routingTable []struct { 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}, +// {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 +func route(s *xml.StartElement, d *xml.Decoder, c chan<- any, t routingTable) { + for _, v := range t { + if v.name == (*s).Name { + v.handler(s, d, c) } } - - log.Println("Could not route XML element") } diff --git a/xmpp/session.go b/xmpp/session.go index 4be3386..6abc343 100644 --- a/xmpp/session.go +++ b/xmpp/session.go @@ -20,7 +20,7 @@ type session struct { out chan<- any transport *tls.Conn tx *xml.Encoder - rx chan xml.Token + rx chan any resourceReq string } @@ -31,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() diff --git a/xmpp/streams.go b/xmpp/streams.go index b9c0cb7..9c90554 100644 --- a/xmpp/streams.go +++ b/xmpp/streams.go @@ -40,20 +40,6 @@ func closeStream(e *xml.Encoder) { } } -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 @@ -77,3 +63,22 @@ func iqHandler(s *session, e []xml.Token) { } } } + +type streamFeatures struct { + Mechanisms struct { + Items []struct { + Type string `xml:",innerxml"` + } `xml:"mechanism"` + } `xml:"mechanisms"` +} + +func streamFeaturesHandler(s *xml.StartElement, d *xml.Decoder, c chan<- any) { + e := streamFeatures{} + + err := d.DecodeElement(&e, s) + if err != nil { + log.Printf("Could not decode stream features: %v\n", err) + } + + c <- e +} diff --git a/xmpp/xml.go b/xmpp/xml.go index 36d7eb1..470a2ef 100644 --- a/xmpp/xml.go +++ b/xmpp/xml.go @@ -9,7 +9,7 @@ import ( "log" ) -func runRx(ctx context.Context, chn chan xml.Token, conn *tls.Conn) { +func runRx(ctx context.Context, chn chan<- any, conn *tls.Conn) { l := logger{"[RX] "} r := io.TeeReader(conn, l) @@ -27,7 +27,7 @@ func runRx(ctx context.Context, chn chan xml.Token, conn *tls.Conn) { if e.Name.Local == "stream" { // new server-side stream TODO what to do with this info? } else { -// route(&e, &d, chn, getRoutingTable()) + route(&e, d, chn, getRoutingTable()) } case xml.EndElement: if e.Name.Local == "stream" { -- cgit v1.2.3-70-g09d2 From 3a5e9fce02264eeed884b15781593479fda9296a Mon Sep 17 00:00:00 2001 From: xengineering Date: Tue, 4 Jul 2023 14:25:21 +0200 Subject: Rework routing completely --- xmpp/router.go | 40 ---------------------------------------- xmpp/routing.go | 21 +++++++++++++++++++++ xmpp/streams.go | 11 ----------- xmpp/xml.go | 2 +- 4 files changed, 22 insertions(+), 52 deletions(-) delete mode 100644 xmpp/router.go create mode 100644 xmpp/routing.go (limited to 'xmpp/xml.go') diff --git a/xmpp/router.go b/xmpp/router.go deleted file mode 100644 index d437b28..0000000 --- a/xmpp/router.go +++ /dev/null @@ -1,40 +0,0 @@ -package xmpp - -import ( - "encoding/xml" -) - -// 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(s *xml.StartElement, d *xml.Decoder, c chan<- any) -} - -// 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 *xml.StartElement, d *xml.Decoder, c chan<- any, t routingTable) { - for _, v := range t { - if v.name == (*s).Name { - v.handler(s, d, c) - } - } -} diff --git a/xmpp/routing.go b/xmpp/routing.go new file mode 100644 index 0000000..e8aa4ed --- /dev/null +++ b/xmpp/routing.go @@ -0,0 +1,21 @@ +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`}: + data := streamFeatures{} + err := d.DecodeElement(&data, s) + if err != nil { + log.Printf("Could not decode stream features: %v\n", err) + } else { + log.Print(data) // TODO + } + default: + d.Skip() + } +} diff --git a/xmpp/streams.go b/xmpp/streams.go index b7cde3e..3aca8a2 100644 --- a/xmpp/streams.go +++ b/xmpp/streams.go @@ -67,14 +67,3 @@ func iqHandler(s *session, e []xml.Token) { type streamFeatures struct { SaslMechanisms []string `xml:"mechanisms>mechanism"` } - -func streamFeaturesHandler(s *xml.StartElement, d *xml.Decoder, c chan<- any) { - e := streamFeatures{} - - err := d.DecodeElement(&e, s) - if err != nil { - log.Printf("Could not decode stream features: %v\n", err) - } else { - c <- e - } -} diff --git a/xmpp/xml.go b/xmpp/xml.go index 470a2ef..e6fccee 100644 --- a/xmpp/xml.go +++ b/xmpp/xml.go @@ -27,7 +27,7 @@ func runRx(ctx context.Context, chn chan<- any, conn *tls.Conn) { if e.Name.Local == "stream" { // new server-side stream TODO what to do with this info? } else { - route(&e, d, chn, getRoutingTable()) + route(&e, d, chn) } case xml.EndElement: if e.Name.Local == "stream" { -- cgit v1.2.3-70-g09d2