Merge pull request #4 from FluuxIO/component
Add initial support for XMPP components
This commit is contained in:
commit
ceeb51ce0e
49
auth.go
49
auth.go
|
@ -32,37 +32,72 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
|
||||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
|
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
|
||||||
|
|
||||||
// Next message should be either success or failure.
|
// Next message should be either success or failure.
|
||||||
name, val, err := next(decoder)
|
val, err := next(decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v := val.(type) {
|
switch v := val.(type) {
|
||||||
case *saslSuccess:
|
case SASLSuccess:
|
||||||
case *saslFailure:
|
case SASLFailure:
|
||||||
// v.Any is type of sub-element in failure, which gives a description of what failed.
|
// v.Any is type of sub-element in failure, which gives a description of what failed.
|
||||||
return errors.New("auth failure: " + v.Any.Local)
|
return errors.New("auth failure: " + v.Any.Local)
|
||||||
default:
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// XMPP Packet Parsing
|
|
||||||
type saslMechanisms struct {
|
type saslMechanisms struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
||||||
Mechanism []string `xml:"mechanism"`
|
Mechanism []string `xml:"mechanism"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type saslSuccess struct {
|
// ============================================================================
|
||||||
|
// SASLSuccess
|
||||||
|
|
||||||
|
type SASLSuccess struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
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"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
||||||
Any xml.Name // error reason is a subelement
|
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 {
|
type auth struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
||||||
Mechanism string `xml:"mecanism,attr"`
|
Mechanism string `xml:"mecanism,attr"`
|
||||||
|
|
|
@ -108,7 +108,7 @@ func (c *Client) Connect() (*Session, error) {
|
||||||
|
|
||||||
func (c *Client) recv(receiver chan<- interface{}) (err error) {
|
func (c *Client) recv(receiver chan<- interface{}) (err error) {
|
||||||
for {
|
for {
|
||||||
_, val, err := next(c.Session.decoder)
|
val, err := next(c.Session.decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ func (c *Client) Recv() <-chan interface{} {
|
||||||
|
|
||||||
// Send sends message text.
|
// Send sends message text.
|
||||||
func (c *Client) Send(packet string) error {
|
func (c *Client) Send(packet string) error {
|
||||||
fmt.Fprintf(c.Session.socketProxy, packet)
|
fmt.Fprintf(c.Session.socketProxy, packet) // TODO handle errors
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
iq := &ClientIQ{}
|
iq := &IQ{}
|
||||||
// Decode element into pointer storage
|
// Decode element into pointer storage
|
||||||
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
||||||
t.Errorf("cannot decode bind iq: %s", err)
|
t.Errorf("cannot decode bind iq: %s", err)
|
||||||
|
|
32
cmd/xmpp_component/xmpp_component.go
Normal file
32
cmd/xmpp_component/xmpp_component.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,9 +31,9 @@ func main() {
|
||||||
// Iterator to receive packets coming from our XMPP connection
|
// Iterator to receive packets coming from our XMPP connection
|
||||||
for packet := range client.Recv() {
|
for packet := range client.Recv() {
|
||||||
switch packet := packet.(type) {
|
switch packet := packet.(type) {
|
||||||
case *xmpp.ClientMessage:
|
case *xmpp.Message:
|
||||||
fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
|
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())
|
client.Send(reply.XMPPFormat())
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
||||||
|
|
|
@ -40,11 +40,11 @@ func main() {
|
||||||
for packet := range client.Recv() {
|
for packet := range client.Recv() {
|
||||||
|
|
||||||
switch packet := packet.(type) {
|
switch packet := packet.(type) {
|
||||||
case *xmpp.ClientMessage:
|
case *xmpp.Message:
|
||||||
processMessage(client, p, packet)
|
processMessage(client, p, packet)
|
||||||
case *xmpp.ClientIQ:
|
case *xmpp.IQ:
|
||||||
processIq(client, p, packet)
|
processIq(client, p, packet)
|
||||||
case *xmpp.ClientPresence:
|
case *xmpp.Presence:
|
||||||
// Do nothing with received presence
|
// Do nothing with received presence
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
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, " ")
|
command := strings.Trim(packet.Body, " ")
|
||||||
if command == "stop" {
|
if command == "stop" {
|
||||||
p.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) {
|
switch payload := packet.Payload.(type) {
|
||||||
// We support IOT Control IQ
|
// We support IOT Control IQ
|
||||||
case *iot.ControlSet:
|
case *iot.ControlSet:
|
||||||
|
@ -76,7 +76,7 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientIQ) {
|
||||||
|
|
||||||
playSCURL(p, url)
|
playSCURL(p, url)
|
||||||
setResponse := new(iot.ControlSetResponse)
|
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())
|
client.Send(reply.XMPPFormat())
|
||||||
// TODO add Soundclound artist / title retrieval
|
// TODO add Soundclound artist / title retrieval
|
||||||
sendUserTune(client, "Radiohead", "Spectre")
|
sendUserTune(client, "Radiohead", "Spectre")
|
||||||
|
|
116
component.go
Normal file
116
component.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
|
||||||
|
|
||||||
|
// 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, "<handshake>%s</handshake>", 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
|
||||||
|
}
|
18
component_test.go
Normal file
18
component_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
42
iq.go
42
iq.go
|
@ -7,22 +7,38 @@ import (
|
||||||
"fluux.io/xmpp/iot"
|
"fluux.io/xmpp/iot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// info/query
|
// ============================================================================
|
||||||
type ClientIQ struct {
|
// IQ Packet
|
||||||
XMLName xml.Name `xml:"jabber:client iq"`
|
|
||||||
Packet
|
type IQ struct { // Info/Query
|
||||||
|
XMLName xml.Name `xml:"iq"`
|
||||||
|
PacketAttrs
|
||||||
Payload IQPayload `xml:",omitempty"`
|
Payload IQPayload `xml:",omitempty"`
|
||||||
RawXML string `xml:",innerxml"`
|
RawXML string `xml:",innerxml"`
|
||||||
// TODO We need to support detecting the IQ namespace / Query packet
|
// TODO We need to support detecting the IQ namespace / Query packet
|
||||||
// Error clientError
|
// 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 {
|
type IQPayload interface {
|
||||||
IsIQPayload()
|
IsIQPayload()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing for IQs
|
// 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
|
iq.XMLName = start.Name
|
||||||
// Extract IQ attributes
|
// Extract IQ attributes
|
||||||
for _, attr := range start.Attr {
|
for _, attr := range start.Attr {
|
||||||
|
@ -59,7 +75,8 @@ func (iq *ClientIQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
p = new(bindBind)
|
p = new(bindBind)
|
||||||
case "urn:xmpp:iot:control set":
|
case "urn:xmpp:iot:control set":
|
||||||
p = new(iot.ControlSet)
|
p = new(iot.ControlSet)
|
||||||
// TODO: Add a default Type that passes RawXML
|
default:
|
||||||
|
p = new(Node)
|
||||||
}
|
}
|
||||||
if p != nil {
|
if p != nil {
|
||||||
err = d.DecodeElement(p, &tt)
|
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.
|
// XMPPFormat returns the string representation of the XMPP packet.
|
||||||
// TODO: Should I simply rely on xml.Marshal ?
|
// TODO: Should I simply rely on xml.Marshal ?
|
||||||
func (iq *ClientIQ) XMPPFormat() string {
|
func (iq *IQ) XMPPFormat() string {
|
||||||
if iq.Payload != nil {
|
if iq.Payload != nil {
|
||||||
var payload []byte
|
var payload []byte
|
||||||
var err error
|
var err error
|
||||||
|
@ -98,3 +115,14 @@ func (iq *ClientIQ) XMPPFormat() string {
|
||||||
iq.To, iq.Type, iq.Id,
|
iq.To, iq.Type, iq.Id,
|
||||||
iq.RawXML)
|
iq.RawXML)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Genery IQ Node
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Content []byte `xml:",innerxml"`
|
||||||
|
Nodes []Node `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Node) IsIQPayload() {}
|
||||||
|
|
|
@ -10,14 +10,14 @@ func TestUnmarshalIqs(t *testing.T) {
|
||||||
//var cs1 = new(iot.ControlSet)
|
//var cs1 = new(iot.ControlSet)
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
iqString string
|
iqString string
|
||||||
parsedIQ ClientIQ
|
parsedIQ IQ
|
||||||
}{
|
}{
|
||||||
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>", ClientIQ{XMLName: xml.Name{Space: "", Local: "iq"}, Packet: Packet{To: "test@localhost", Type: "set", Id: "1"}}},
|
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>", IQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}},
|
||||||
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", ClientIQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, Packet: Packet{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
|
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", 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 {
|
for _, test := range tests {
|
||||||
var parsedIQ = new(ClientIQ)
|
var parsedIQ = new(IQ)
|
||||||
err := xml.Unmarshal([]byte(test.iqString), parsedIQ)
|
err := xml.Unmarshal([]byte(test.iqString), parsedIQ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unmarshal(%s) returned error", test.iqString)
|
t.Errorf("Unmarshal(%s) returned error", test.iqString)
|
||||||
|
|
32
message.go
32
message.go
|
@ -5,20 +5,36 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// XMPP Packet Parsing
|
// ============================================================================
|
||||||
type ClientMessage struct {
|
// Message Packet
|
||||||
XMLName xml.Name `xml:"jabber:client message"`
|
|
||||||
Packet
|
type Message struct {
|
||||||
|
XMLName xml.Name `xml:"message"`
|
||||||
|
PacketAttrs
|
||||||
Subject string `xml:"subject,omitempty"`
|
Subject string `xml:"subject,omitempty"`
|
||||||
Body string `xml:"body,omitempty"`
|
Body string `xml:"body,omitempty"`
|
||||||
Thread string `xml:"thread,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("<message to='%s' type='chat' xml:lang='en'>"+
|
return fmt.Sprintf("<message to='%s' type='chat' xml:lang='en'>"+
|
||||||
"<body>%s</body></message>",
|
"<body>%s</body></message>",
|
||||||
message.To,
|
msg.To,
|
||||||
xmlEscape(message.Body))
|
xmlEscape(msg.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Func new message to create an empty message structure without the XML tag matching elements
|
||||||
|
|
13
ns.go
13
ns.go
|
@ -1,10 +1,11 @@
|
||||||
package xmpp // import "fluux.io/xmpp"
|
package xmpp // import "fluux.io/xmpp"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NSStream = "http://etherx.jabber.org/streams"
|
NSStream = "http://etherx.jabber.org/streams"
|
||||||
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
||||||
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||||
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||||
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||||
NSClient = "jabber:client"
|
NSClient = "jabber:client"
|
||||||
|
NSComponent = "jabber:component:accept"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package xmpp // import "fluux.io/xmpp"
|
package xmpp // import "fluux.io/xmpp"
|
||||||
|
|
||||||
// Packet represents the root default structure for an XMPP packet.
|
type Packet interface {
|
||||||
type Packet struct {
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacketAttrs represents the common structure for base XMPP packets.
|
||||||
|
type PacketAttrs struct {
|
||||||
Id string `xml:"id,attr,omitempty"`
|
Id string `xml:"id,attr,omitempty"`
|
||||||
From string `xml:"from,attr,omitempty"`
|
From string `xml:"from,attr,omitempty"`
|
||||||
To string `xml:"to,attr,omitempty"`
|
To string `xml:"to,attr,omitempty"`
|
||||||
|
|
95
parser.go
95
parser.go
|
@ -8,11 +8,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reads and checks the opening XMPP stream element.
|
// 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
|
// - 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
|
// - 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
|
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
|
||||||
// getting through the authentication process.
|
// getting through the authentication process.
|
||||||
|
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
|
||||||
func initDecoder(p *xml.Decoder) (sessionID string, err error) {
|
func initDecoder(p *xml.Decoder) (sessionID string, err error) {
|
||||||
for {
|
for {
|
||||||
var t xml.Token
|
var t xml.Token
|
||||||
|
@ -59,38 +60,76 @@ func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan XML token stream for next element and save into val.
|
// next scans XML token stream for next element and then assign a structure to decode
|
||||||
// If val == nil, allocate new element based on proto map.
|
// that elements.
|
||||||
// Either way, return val.
|
// TODO Use an interface to return packets interface xmppDecoder
|
||||||
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
|
func next(p *xml.Decoder) (Packet, error) {
|
||||||
// Read start element to find out what type we want.
|
// Read start element to find out how we want to parse the XMPP packet
|
||||||
se, err := nextStart(p)
|
se, err := nextStart(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xml.Name{}, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put it in an interface and allocate one.
|
// TODO: general case = Parse IQ / presence / message => split SASL Stream and component cases
|
||||||
var nv interface{}
|
switch se.Name.Space {
|
||||||
switch se.Name.Space + " " + se.Name.Local {
|
case NSStream:
|
||||||
// TODO: general case = Parse IQ / presence / message => split SASL case
|
return decodeStream(p, se)
|
||||||
case nsSASL + " success":
|
case nsSASL:
|
||||||
nv = &saslSuccess{}
|
return decodeSASL(p, se)
|
||||||
case nsSASL + " failure":
|
case NSClient:
|
||||||
nv = &saslFailure{}
|
return decodeClient(p, se)
|
||||||
case NSClient + " message":
|
case NSComponent:
|
||||||
nv = &ClientMessage{}
|
return decodeComponent(p, se)
|
||||||
case NSClient + " presence":
|
|
||||||
nv = &ClientPresence{}
|
|
||||||
case NSClient + " iq":
|
|
||||||
nv = &ClientIQ{}
|
|
||||||
default:
|
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 + "/>")
|
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type iq struct {
|
type iq struct {
|
||||||
XMLName xml.Name `xml:"jabber:client iq"`
|
XMLName xml.Name `xml:"jabber:client iq"`
|
||||||
C pubSub // c for "contains"
|
C pubSub // c for "contains"
|
||||||
xmpp.Packet // Rename h for "header" ?
|
xmpp.PacketAttrs // Rename h for "header" ?
|
||||||
}
|
}
|
||||||
|
|
||||||
type pubSub struct {
|
type pubSub struct {
|
||||||
|
@ -68,7 +68,7 @@ type Tune struct {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func (t *Tune) XMPPFormat() (s string) {
|
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)
|
return string(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
24
presence.go
24
presence.go
|
@ -2,12 +2,28 @@ package xmpp // import "fluux.io/xmpp"
|
||||||
|
|
||||||
import "encoding/xml"
|
import "encoding/xml"
|
||||||
|
|
||||||
// XMPP Packet Parsing
|
// ============================================================================
|
||||||
type ClientPresence struct {
|
// Presence Packet
|
||||||
XMLName xml.Name `xml:"jabber:client presence"`
|
|
||||||
Packet
|
type Presence struct {
|
||||||
|
XMLName xml.Name `xml:"presence"`
|
||||||
|
PacketAttrs
|
||||||
Show string `xml:"show,attr,omitempty"` // away, chat, dnd, xa
|
Show string `xml:"show,attr,omitempty"` // away, chat, dnd, xa
|
||||||
Status string `xml:"status,attr,omitempty"`
|
Status string `xml:"status,attr,omitempty"`
|
||||||
Priority string `xml:"priority,attr,omitempty"`
|
Priority string `xml:"priority,attr,omitempty"`
|
||||||
//Error *clientError
|
//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
|
||||||
|
}
|
||||||
|
|
|
@ -157,7 +157,7 @@ func (s *Session) bind(o Options) {
|
||||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), nsBind)
|
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), nsBind)
|
||||||
}
|
}
|
||||||
|
|
||||||
var iq ClientIQ
|
var iq IQ
|
||||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
||||||
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
||||||
return
|
return
|
||||||
|
@ -180,7 +180,7 @@ func (s *Session) rfc3921Session(o Options) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var iq ClientIQ
|
var iq IQ
|
||||||
if s.Features.Session.optional.Local != "" {
|
if s.Features.Session.optional.Local != "" {
|
||||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), nsSession)
|
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), nsSession)
|
||||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
||||||
|
|
33
stream.go
33
stream.go
|
@ -1,8 +1,12 @@
|
||||||
package xmpp // import "fluux.io/xmpp"
|
package xmpp // import "fluux.io/xmpp"
|
||||||
|
|
||||||
import "encoding/xml"
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// StreamFeatures Packet
|
||||||
|
|
||||||
// XMPP Packet Parsing
|
|
||||||
type streamFeatures struct {
|
type streamFeatures struct {
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
||||||
StartTLS tlsStartTLS
|
StartTLS tlsStartTLS
|
||||||
|
@ -13,6 +17,31 @@ type streamFeatures struct {
|
||||||
Any []xml.Name `xml:",any"`
|
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 {
|
type Caps struct {
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
|
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
|
||||||
Hash string `xml:"hash,attr"`
|
Hash string `xml:"hash,attr"`
|
||||||
|
|
Loading…
Reference in a new issue