diff --git a/auth.go b/auth.go index 846dd33..3b4b754 100644 --- a/auth.go +++ b/auth.go @@ -32,37 +32,72 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password fmt.Fprintf(socket, "%s", nsSASL, enc) // Next message should be either success or failure. - name, val, err := next(decoder) + val, err := next(decoder) if err != nil { return err } switch v := val.(type) { - case *saslSuccess: - case *saslFailure: + case SASLSuccess: + case SASLFailure: // v.Any is type of sub-element in failure, which gives a description of what failed. return errors.New("auth failure: " + v.Any.Local) default: - return errors.New("expected success or failure, got " + name.Local + " in " + name.Space) + return errors.New("expected SASL success or failure, got " + v.Name()) } return err } -// XMPP Packet Parsing type saslMechanisms struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` Mechanism []string `xml:"mechanism"` } -type saslSuccess struct { +// ============================================================================ +// SASLSuccess + +type SASLSuccess struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"` } -type saslFailure struct { +func (SASLSuccess) Name() string { + return "sasl:success" +} + +type saslSuccessDecoder struct{} + +var saslSuccess saslSuccessDecoder + +func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) { + var packet SASLSuccess + err := p.DecodeElement(&packet, &se) + return packet, err +} + +// ============================================================================ +// SASLFailure + +type SASLFailure struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"` Any xml.Name // error reason is a subelement } +func (SASLFailure) Name() string { + return "sasl:failure" +} + +type saslFailureDecoder struct{} + +var saslFailure saslFailureDecoder + +func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) { + var packet SASLFailure + err := p.DecodeElement(&packet, &se) + return packet, err +} + +// ============================================================================ + type auth struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"` Mechanism string `xml:"mecanism,attr"` diff --git a/client.go b/client.go index 689720e..defa642 100644 --- a/client.go +++ b/client.go @@ -108,7 +108,7 @@ func (c *Client) Connect() (*Session, error) { func (c *Client) recv(receiver chan<- interface{}) (err error) { for { - _, val, err := next(c.Session.decoder) + val, err := next(c.Session.decoder) if err != nil { return err } @@ -128,7 +128,7 @@ func (c *Client) Recv() <-chan interface{} { // Send sends message text. func (c *Client) Send(packet string) error { - fmt.Fprintf(c.Session.socketProxy, packet) + fmt.Fprintf(c.Session.socketProxy, packet) // TODO handle errors return nil } diff --git a/client_test.go b/client_test.go index a6746d2..2746041 100644 --- a/client_test.go +++ b/client_test.go @@ -163,7 +163,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { return } - iq := &ClientIQ{} + iq := &IQ{} // Decode element into pointer storage if err = decoder.DecodeElement(&iq, &se); err != nil { t.Errorf("cannot decode bind iq: %s", err) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go new file mode 100644 index 0000000..7b794c2 --- /dev/null +++ b/cmd/xmpp_component/xmpp_component.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + + "fluux.io/xmpp" +) + +func main() { + component := xmpp.Component{Host: "mqtt.localhost", Secret: "mypass"} + component.Connect("localhost:8888") + + for { + packet, err := component.ReadPacket() + if err != nil { + fmt.Println("read error", err) + return + } + + switch p := packet.(type) { + case xmpp.IQ: + switch inner := p.Payload.(type) { + case *xmpp.Node: + fmt.Println("Node:", inner.XMLName.Space, inner.XMLName.Local) + default: + fmt.Println("default") + } + default: + fmt.Println("Packet unhandled packet:", packet) + } + } +} diff --git a/cmd/xmpp_echo/xmpp_echo.go b/cmd/xmpp_echo/xmpp_echo.go index 69067be..d092508 100644 --- a/cmd/xmpp_echo/xmpp_echo.go +++ b/cmd/xmpp_echo/xmpp_echo.go @@ -31,9 +31,9 @@ func main() { // Iterator to receive packets coming from our XMPP connection for packet := range client.Recv() { switch packet := packet.(type) { - case *xmpp.ClientMessage: + case *xmpp.Message: fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From) - reply := xmpp.ClientMessage{Packet: xmpp.Packet{To: packet.From}, Body: packet.Body} + reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body} client.Send(reply.XMPPFormat()) default: fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet) diff --git a/cmd/xmpp_jukebox/xmpp_jukebox.go b/cmd/xmpp_jukebox/xmpp_jukebox.go index 0591b4b..a67d1fe 100644 --- a/cmd/xmpp_jukebox/xmpp_jukebox.go +++ b/cmd/xmpp_jukebox/xmpp_jukebox.go @@ -40,11 +40,11 @@ func main() { for packet := range client.Recv() { switch packet := packet.(type) { - case *xmpp.ClientMessage: + case *xmpp.Message: processMessage(client, p, packet) - case *xmpp.ClientIQ: + case *xmpp.IQ: processIq(client, p, packet) - case *xmpp.ClientPresence: + case *xmpp.Presence: // Do nothing with received presence default: fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet) @@ -52,7 +52,7 @@ func main() { } } -func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientMessage) { +func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.Message) { command := strings.Trim(packet.Body, " ") if command == "stop" { p.Stop() @@ -62,7 +62,7 @@ func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientMe } } -func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientIQ) { +func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) { switch payload := packet.Payload.(type) { // We support IOT Control IQ case *iot.ControlSet: @@ -76,7 +76,7 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientIQ) { playSCURL(p, url) setResponse := new(iot.ControlSetResponse) - reply := xmpp.ClientIQ{Packet: xmpp.Packet{To: packet.From, Type: "result", Id: packet.Id}, Payload: setResponse} + reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: setResponse} client.Send(reply.XMPPFormat()) // TODO add Soundclound artist / title retrieval sendUserTune(client, "Radiohead", "Spectre") diff --git a/component.go b/component.go new file mode 100644 index 0000000..6474660 --- /dev/null +++ b/component.go @@ -0,0 +1,116 @@ +package xmpp + +import ( + "crypto/sha1" + "encoding/hex" + "encoding/xml" + "errors" + "fmt" + "io" + "net" + "time" +) + +const componentStreamOpen = "" + +// Component implements an XMPP extension allowing to extend XMPP server +// using external components. Component specifications are defined +// in XEP-0114, XEP-0355 and XEP-0356. +type Component struct { + Host string + Secret string + + // TCP level connection + conn net.Conn + + // read / write + socketProxy io.ReadWriter // TODO + decoder *xml.Decoder +} + +// handshake generates an authentication token based on StreamID and shared secret. +func (c *Component) handshake(streamId string) string { + // 1. Concatenate the Stream ID received from the server with the shared secret. + concatStr := streamId + c.Secret + + // 2. Hash the concatenated string according to the SHA1 algorithm, i.e., SHA1( concat (sid, password)). + h := sha1.New() + h.Write([]byte(concatStr)) + hash := h.Sum(nil) + + // 3. Ensure that the hash output is in hexadecimal format, not binary or base64. + // 4. Convert the hash output to all lowercase characters. + encodedStr := hex.EncodeToString(hash) + + return encodedStr +} + +// TODO Helper to prepare connection string +func (c *Component) Connect(connStr string) error { + var conn net.Conn + var err error + if conn, err = net.DialTimeout("tcp", connStr, time.Duration(5)*time.Second); err != nil { + return err + } + c.conn = conn + + // 1. Send stream open tag + if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Host, NSComponent, NSStream); err != nil { + return errors.New("cannot send stream open " + err.Error()) + } + c.decoder = xml.NewDecoder(conn) + + // 2. Initialize xml decoder and extract streamID from reply + streamId, err := initDecoder(c.decoder) + if err != nil { + return errors.New("cannot init decoder " + err.Error()) + } + + // 3. Authentication + if _, err := fmt.Fprintf(conn, "%s", c.handshake(streamId)); err != nil { + return errors.New("cannot send handshake " + err.Error()) + } + + // 4. Check server response for authentication + val, err := next(c.decoder) + if err != nil { + return err + } + + switch v := val.(type) { + case *StreamError: + return errors.New("handshake failed " + v.Error.Local) + case *Handshake: + return nil + default: + return errors.New("unexpected packet, got " + v.Name()) + } + panic("unreachable") +} + +// ReadPacket reads next incoming XMPP packet +// TODO use defined interface Packet +func (c *Component) ReadPacket() (Packet, error) { + return next(c.decoder) +} + +// ============================================================================ +// Handshake Packet + +type Handshake struct { + XMLName xml.Name `xml:"jabber:component:accept handshake"` +} + +func (Handshake) Name() string { + return "component:handshake" +} + +type handshakeDecoder struct{} + +var handshake handshakeDecoder + +func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) { + var packet Handshake + err := p.DecodeElement(&packet, &se) + return packet, err +} diff --git a/component_test.go b/component_test.go new file mode 100644 index 0000000..76e3849 --- /dev/null +++ b/component_test.go @@ -0,0 +1,18 @@ +package xmpp + +import "testing" + +func TestHandshake(t *testing.T) { + c := Component{ + Host: "test.localhost", + Secret: "mypass", + } + + streamID := "1263952298440005243" + expected := "c77e2ef0109fbbc5161e83b51629cd1353495332" + + result := c.handshake(streamID) + if result != expected { + t.Errorf("incorrect handshake calculation '%s' != '%s'", result, expected) + } +} diff --git a/iq.go b/iq.go index f737e82..63667f1 100644 --- a/iq.go +++ b/iq.go @@ -7,22 +7,38 @@ import ( "fluux.io/xmpp/iot" ) -// info/query -type ClientIQ struct { - XMLName xml.Name `xml:"jabber:client iq"` - Packet +// ============================================================================ +// IQ Packet + +type IQ struct { // Info/Query + XMLName xml.Name `xml:"iq"` + PacketAttrs Payload IQPayload `xml:",omitempty"` RawXML string `xml:",innerxml"` // TODO We need to support detecting the IQ namespace / Query packet // Error clientError } +func (IQ) Name() string { + return "iq" +} + +type iqDecoder struct{} + +var iq iqDecoder + +func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) { + var packet IQ + err := p.DecodeElement(&packet, &se) + return packet, err +} + type IQPayload interface { IsIQPayload() } // UnmarshalXML implements custom parsing for IQs -func (iq *ClientIQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { iq.XMLName = start.Name // Extract IQ attributes for _, attr := range start.Attr { @@ -59,7 +75,8 @@ func (iq *ClientIQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { p = new(bindBind) case "urn:xmpp:iot:control set": p = new(iot.ControlSet) - // TODO: Add a default Type that passes RawXML + default: + p = new(Node) } if p != nil { err = d.DecodeElement(p, &tt) @@ -80,7 +97,7 @@ func (iq *ClientIQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // XMPPFormat returns the string representation of the XMPP packet. // TODO: Should I simply rely on xml.Marshal ? -func (iq *ClientIQ) XMPPFormat() string { +func (iq *IQ) XMPPFormat() string { if iq.Payload != nil { var payload []byte var err error @@ -98,3 +115,14 @@ func (iq *ClientIQ) XMPPFormat() string { iq.To, iq.Type, iq.Id, iq.RawXML) } + +// ============================================================================ +// Genery IQ Node + +type Node struct { + XMLName xml.Name + Content []byte `xml:",innerxml"` + Nodes []Node `xml:",any"` +} + +func (*Node) IsIQPayload() {} diff --git a/iq_test.go b/iq_test.go index b8859c0..9042ebc 100644 --- a/iq_test.go +++ b/iq_test.go @@ -10,14 +10,14 @@ func TestUnmarshalIqs(t *testing.T) { //var cs1 = new(iot.ControlSet) var tests = []struct { iqString string - parsedIQ ClientIQ + parsedIQ IQ }{ - {"", ClientIQ{XMLName: xml.Name{Space: "", Local: "iq"}, Packet: Packet{To: "test@localhost", Type: "set", Id: "1"}}}, - //{"", ClientIQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, Packet: Packet{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}}, + {"", IQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}}, + //{"", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}}, } for _, test := range tests { - var parsedIQ = new(ClientIQ) + var parsedIQ = new(IQ) err := xml.Unmarshal([]byte(test.iqString), parsedIQ) if err != nil { t.Errorf("Unmarshal(%s) returned error", test.iqString) diff --git a/message.go b/message.go index dbcd0ad..be3d758 100644 --- a/message.go +++ b/message.go @@ -5,20 +5,36 @@ import ( "fmt" ) -// XMPP Packet Parsing -type ClientMessage struct { - XMLName xml.Name `xml:"jabber:client message"` - Packet +// ============================================================================ +// Message Packet + +type Message struct { + XMLName xml.Name `xml:"message"` + PacketAttrs Subject string `xml:"subject,omitempty"` Body string `xml:"body,omitempty"` Thread string `xml:"thread,omitempty"` } -// TODO: Func new message to create an empty message structure without the XML tag matching elements +func (Message) Name() string { + return "message" +} -func (message *ClientMessage) XMPPFormat() string { +type messageDecoder struct{} + +var message messageDecoder + +func (messageDecoder) decode(p *xml.Decoder, se xml.StartElement) (Message, error) { + var packet Message + err := p.DecodeElement(&packet, &se) + return packet, err +} + +func (msg *Message) XMPPFormat() string { return fmt.Sprintf(""+ "%s", - message.To, - xmlEscape(message.Body)) + msg.To, + xmlEscape(msg.Body)) } + +// TODO: Func new message to create an empty message structure without the XML tag matching elements diff --git a/ns.go b/ns.go index 9b3bdac..9cd162c 100644 --- a/ns.go +++ b/ns.go @@ -1,10 +1,11 @@ package xmpp // import "fluux.io/xmpp" const ( - NSStream = "http://etherx.jabber.org/streams" - nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" - nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl" - nsBind = "urn:ietf:params:xml:ns:xmpp-bind" - nsSession = "urn:ietf:params:xml:ns:xmpp-session" - NSClient = "jabber:client" + NSStream = "http://etherx.jabber.org/streams" + nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" + nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl" + nsBind = "urn:ietf:params:xml:ns:xmpp-bind" + nsSession = "urn:ietf:params:xml:ns:xmpp-session" + NSClient = "jabber:client" + NSComponent = "jabber:component:accept" ) diff --git a/packet.go b/packet.go index a896440..ca42d6c 100644 --- a/packet.go +++ b/packet.go @@ -1,7 +1,11 @@ package xmpp // import "fluux.io/xmpp" -// Packet represents the root default structure for an XMPP packet. -type Packet struct { +type Packet interface { + Name() string +} + +// PacketAttrs represents the common structure for base XMPP packets. +type PacketAttrs struct { Id string `xml:"id,attr,omitempty"` From string `xml:"from,attr,omitempty"` To string `xml:"to,attr,omitempty"` diff --git a/parser.go b/parser.go index 39165cb..aec306c 100644 --- a/parser.go +++ b/parser.go @@ -8,11 +8,12 @@ import ( ) // Reads and checks the opening XMPP stream element. -// It returns a stream structure containing: +// TODO It returns a stream structure containing: // - Host: You can check the host against the host you were expecting to connect to // - Id: the Stream ID is a temporary shared secret used for some hash calculation. It is also used by ProcessOne // reattach features (allowing to resume an existing stream at the point the connection was interrupted, without // getting through the authentication process. +// TODO We should handle stream error from XEP-0114 ( or ) func initDecoder(p *xml.Decoder) (sessionID string, err error) { for { var t xml.Token @@ -59,38 +60,76 @@ func nextStart(p *xml.Decoder) (xml.StartElement, error) { panic("unreachable") } -// Scan XML token stream for next element and save into val. -// If val == nil, allocate new element based on proto map. -// Either way, return val. -func next(p *xml.Decoder) (xml.Name, interface{}, error) { - // Read start element to find out what type we want. +// next scans XML token stream for next element and then assign a structure to decode +// that elements. +// TODO Use an interface to return packets interface xmppDecoder +func next(p *xml.Decoder) (Packet, error) { + // Read start element to find out how we want to parse the XMPP packet se, err := nextStart(p) if err != nil { - return xml.Name{}, nil, err + return nil, err } - // Put it in an interface and allocate one. - var nv interface{} - switch se.Name.Space + " " + se.Name.Local { - // TODO: general case = Parse IQ / presence / message => split SASL case - case nsSASL + " success": - nv = &saslSuccess{} - case nsSASL + " failure": - nv = &saslFailure{} - case NSClient + " message": - nv = &ClientMessage{} - case NSClient + " presence": - nv = &ClientPresence{} - case NSClient + " iq": - nv = &ClientIQ{} + // TODO: general case = Parse IQ / presence / message => split SASL Stream and component cases + switch se.Name.Space { + case NSStream: + return decodeStream(p, se) + case nsSASL: + return decodeSASL(p, se) + case NSClient: + return decodeClient(p, se) + case NSComponent: + return decodeComponent(p, se) default: - return xml.Name{}, nil, errors.New("unexpected XMPP message " + + return nil, errors.New("unknown namespace " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} + +func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) { + switch se.Name.Local { + case "error": + return streamError.decode(p, se) + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} + +func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) { + switch se.Name.Local { + case "success": + return saslSuccess.decode(p, se) + case "failure": + return saslFailure.decode(p, se) + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} + +func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) { + switch se.Name.Local { + case "message": + return message.decode(p, se) + case "presence": + return presence.decode(p, se) + case "iq": + return iq.decode(p, se) + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} + +func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) { + switch se.Name.Local { + case "handshake": + return handshake.decode(p, se) + case "iq": + return iq.decode(p, se) + default: + return nil, errors.New("unexpected XMPP packet " + se.Name.Space + " <" + se.Name.Local + "/>") } - - // Decode element into pointer storage - if err = p.DecodeElement(nv, &se); err != nil { - return xml.Name{}, nil, err - } - return se.Name, nv, err } diff --git a/pep/user_tune.go b/pep/user_tune.go index 846dda0..70b4b47 100644 --- a/pep/user_tune.go +++ b/pep/user_tune.go @@ -7,9 +7,9 @@ import ( ) type iq struct { - XMLName xml.Name `xml:"jabber:client iq"` - C pubSub // c for "contains" - xmpp.Packet // Rename h for "header" ? + XMLName xml.Name `xml:"jabber:client iq"` + C pubSub // c for "contains" + xmpp.PacketAttrs // Rename h for "header" ? } type pubSub struct { @@ -68,7 +68,7 @@ type Tune struct { */ func (t *Tune) XMPPFormat() (s string) { - packet, _ := xml.Marshal(iq{Packet: xmpp.Packet{Id: "tunes", Type: "set"}, C: pubSub{Publish: publish{Node: "http://jabber.org/protocol/tune", Item: item{Tune: *t}}}}) + packet, _ := xml.Marshal(iq{PacketAttrs: xmpp.PacketAttrs{Id: "tunes", Type: "set"}, C: pubSub{Publish: publish{Node: "http://jabber.org/protocol/tune", Item: item{Tune: *t}}}}) return string(packet) } diff --git a/presence.go b/presence.go index 21cfd77..6d9eaed 100644 --- a/presence.go +++ b/presence.go @@ -2,12 +2,28 @@ package xmpp // import "fluux.io/xmpp" import "encoding/xml" -// XMPP Packet Parsing -type ClientPresence struct { - XMLName xml.Name `xml:"jabber:client presence"` - Packet +// ============================================================================ +// Presence Packet + +type Presence struct { + XMLName xml.Name `xml:"presence"` + PacketAttrs Show string `xml:"show,attr,omitempty"` // away, chat, dnd, xa Status string `xml:"status,attr,omitempty"` Priority string `xml:"priority,attr,omitempty"` //Error *clientError } + +func (Presence) Name() string { + return "presence" +} + +type presenceDecoder struct{} + +var presence presenceDecoder + +func (presenceDecoder) decode(p *xml.Decoder, se xml.StartElement) (Presence, error) { + var packet Presence + err := p.DecodeElement(&packet, &se) + return packet, err +} diff --git a/session.go b/session.go index 6012dad..dc47ffe 100644 --- a/session.go +++ b/session.go @@ -157,7 +157,7 @@ func (s *Session) bind(o Options) { fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsBind) } - var iq ClientIQ + var iq IQ if s.err = s.decoder.Decode(&iq); s.err != nil { s.err = errors.New("error decoding iq bind result: " + s.err.Error()) return @@ -180,7 +180,7 @@ func (s *Session) rfc3921Session(o Options) { return } - var iq ClientIQ + var iq IQ if s.Features.Session.optional.Local != "" { fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsSession) if s.err = s.decoder.Decode(&iq); s.err != nil { diff --git a/stream.go b/stream.go index 9138c2f..3c6afd6 100644 --- a/stream.go +++ b/stream.go @@ -1,8 +1,12 @@ package xmpp // import "fluux.io/xmpp" -import "encoding/xml" +import ( + "encoding/xml" +) + +// ============================================================================ +// StreamFeatures Packet -// XMPP Packet Parsing type streamFeatures struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` StartTLS tlsStartTLS @@ -13,6 +17,31 @@ type streamFeatures struct { Any []xml.Name `xml:",any"` } +// ============================================================================ +// StreamError Packet + +type StreamError struct { + XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"` + Error xml.Name `xml:",any"` +} + +func (StreamError) Name() string { + return "stream:error" +} + +type streamErrorDecoder struct{} + +var streamError streamErrorDecoder + +func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) { + var packet StreamError + err := p.DecodeElement(&packet, &se) + return packet, err +} + +// ============================================================================ +// Caps subElement + type Caps struct { XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"` Hash string `xml:"hash,attr"`