From b17c2efdc1d2ed47deb82ea27a0270b5a1de157a Mon Sep 17 00:00:00 2001 From: Sheena Artrip Date: Sun, 14 Feb 2016 00:27:00 -0500 Subject: [PATCH] Initial commit of XEP-0114 implementation --- LICENSE | 21 ++++++++++ README.md | 46 ++++++++++++++++++++++ cmd/echo-component/main.go | 61 +++++++++++++++++++++++++++++ component.go | 78 ++++++++++++++++++++++++++++++++++++++ handshake.go | 66 ++++++++++++++++++++++++++++++++ header.go | 7 ++++ iq.go | 17 +++++++++ message.go | 43 +++++++++++++++++++++ options.go | 36 ++++++++++++++++++ presence.go | 28 ++++++++++++++ read.go | 67 ++++++++++++++++++++++++++++++++ 11 files changed, 470 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/echo-component/main.go create mode 100644 component.go create mode 100644 handshake.go create mode 100644 header.go create mode 100644 iq.go create mode 100644 message.go create mode 100644 options.go create mode 100644 presence.go create mode 100644 read.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96d152d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Sheena Artrip + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..29863c8 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# go-xco + +Library for building XMPP/Jabber ( XEP-0114 ) components in golang. + +## Usage: + +import ( + "github.com/sheenobu/go-xco" +) + +func main(){ + + opts := &xco.Options{ + Name: Name, + SharedSecret: SharedSecret, + Address: Address, + } + + c, err := opts.NewComponent() + if err != nil { + panic(err) + } + + // Uppercase Echo Component + c.MessageHandler = func(c *xco.Component, msg *xco.Message) error { + m := xco.Message{ + Header: xco.Header{ + From: msg.To, + To: msg.From, + ID: msg.ID, + }, + Subject: msg.Subject, + Thread: msg.Thread, + Type: msg.Type, + Body: strings.ToUpper(msg.Body), + XMLName: msg.XMLName, + } + + return c.Send(m) + } + + c.Run() +} + + + diff --git a/cmd/echo-component/main.go b/cmd/echo-component/main.go new file mode 100644 index 0000000..914dc37 --- /dev/null +++ b/cmd/echo-component/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "flag" + "os" + "strings" + + "github.com/sheenobu/go-xco" +) + +var Name string +var SharedSecret string +var Address string + +func init() { + flag.StringVar(&Name, "name", "", "Name of Component") + flag.StringVar(&SharedSecret, "secret", "", "Shared Secret between server and component") + flag.StringVar(&Address, "address", "", "Hostname:port address of XMPP component listener") +} + +func main() { + + flag.Parse() + + if Name == "" || SharedSecret == "" || Address == "" { + flag.Usage() + os.Exit(-1) + return + } + + opts := &xco.Options{ + Name: Name, + SharedSecret: SharedSecret, + Address: Address, + } + + c, err := opts.NewComponent() + if err != nil { + panic(err) + } + + // Uppercase Echo Component + c.MessageHandler = func(c *xco.Component, msg *xco.Message) error { + m := xco.Message{ + Header: xco.Header{ + From: msg.To, + To: msg.From, + ID: msg.ID, + }, + Subject: msg.Subject, + Thread: msg.Thread, + Type: msg.Type, + Body: strings.ToUpper(msg.Body), + XMLName: msg.XMLName, + } + + return c.Send(m) + } + + c.Run() +} diff --git a/component.go b/component.go new file mode 100644 index 0000000..6e18cea --- /dev/null +++ b/component.go @@ -0,0 +1,78 @@ +package xco + +import ( + "encoding/xml" + "fmt" + "net" + "os" + + "golang.org/x/net/context" +) + +type stateFn func() (stateFn, error) + +// A Component is an instance of a Jabber Component (XEP-0114) +type Component struct { + MessageHandler MessageHandler + PresenceHandler PresenceHandler + IqHandler IqHandler + + ctx context.Context + cancelFn context.CancelFunc + + conn net.Conn + dec *xml.Decoder + enc *xml.Encoder + + stateFn stateFn + + sharedSecret string + name string +} + +func (c *Component) dial(o *Options) error { + conn, err := net.Dial("tcp", o.Address) + if err != nil { + return err + } + + c.MessageHandler = noOpMessageHandler + c.PresenceHandler = noOpPresenceHandler + c.IqHandler = noOpIqHandler + + c.conn = conn + c.name = o.Name + c.sharedSecret = o.SharedSecret + c.dec = xml.NewDecoder(conn) + c.enc = xml.NewEncoder(conn) + c.stateFn = c.handshakeState + + return nil +} + +func (c *Component) Close() { + c.cancelFn() +} + +func (c *Component) Run() { + + defer func() { + c.conn.Close() + }() + + var err error + + for { + if c.stateFn == nil { + return + } + c.stateFn, err = c.stateFn() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + } + } +} + +func (c *Component) Send(i interface{}) error { + return c.enc.Encode(i) +} diff --git a/handshake.go b/handshake.go new file mode 100644 index 0000000..9063812 --- /dev/null +++ b/handshake.go @@ -0,0 +1,66 @@ +package xco + +import ( + "crypto/sha1" + "encoding/hex" + "encoding/xml" + "errors" + "fmt" +) + +func (c *Component) handshakeState() (stateFn, error) { + + if _, err := c.conn.Write([]byte(fmt.Sprintf(` + `, c.name))); err != nil { + return nil, err + } + + for { + t, err := c.dec.Token() + if err != nil { + return nil, err + } + + stream, ok := t.(xml.StartElement) + if !ok { + continue + } + + var id string + + for _, a := range stream.Attr { + if a.Name.Local == "id" { + id = a.Value + } + } + + if id == "" { + return nil, errors.New("Unable to find ID in stream response") + } + + handshakeInput := id + c.sharedSecret + handshake := sha1.Sum([]byte(handshakeInput)) + hexHandshake := hex.EncodeToString(handshake[:]) + if _, err := c.conn.Write([]byte(fmt.Sprintf("%s", hexHandshake))); err != nil { + return nil, err + } + + //TODO: separate each step into a state + + // get handshake response + t, err = c.dec.Token() + if err != nil { + return nil, err + } + + t, err = c.dec.Token() + if err != nil { + return nil, err + } + + return c.readLoopState, nil + } +} diff --git a/header.go b/header.go new file mode 100644 index 0000000..719f761 --- /dev/null +++ b/header.go @@ -0,0 +1,7 @@ +package xco + +type Header struct { + ID string `xml:"id,attr"` + From string `xml:"from,attr"` //TODO: make address type + To string `xml:"to,attr"` //TODO: make address type +} diff --git a/iq.go b/iq.go new file mode 100644 index 0000000..b51fa2a --- /dev/null +++ b/iq.go @@ -0,0 +1,17 @@ +package xco + +type Iq struct { + Header + + Type string `xml:"type,attr"` + + Content string `xml:",innerxml"` + + XMLName string `xml:"iq"` +} + +type IqHandler func(c *Component, iq *Iq) error + +func noOpIqHandler(c *Component, iq *Iq) error { + return nil +} diff --git a/message.go b/message.go new file mode 100644 index 0000000..cddc6fd --- /dev/null +++ b/message.go @@ -0,0 +1,43 @@ +package xco + +import "encoding/xml" + +// MessageType defines the constants for the types of messages within XEP-0114 +type MessageType string + +const ( + + // CHAT defines the chat message type + CHAT = MessageType("chat") + + // ERROR defines the error message type + ERROR = MessageType("error") + + // GROUPCHAT defines the group chat message type + GROUPCHAT = MessageType("groupchat") + + // HEADLINE defines the headline message type + HEADLINE = MessageType("headline") + + // NORMAL defines the normal message type + NORMAL = MessageType("normal") +) + +// A Message is an incoming or outgoing Component message +type Message struct { + Header + Type MessageType `xml:"type,attr,omitempty"` + + Subject string `xml:"subject,omitempty"` + Body string `xml:"body"` + Thread string `xml:"thread,omitempty"` + + XMLName xml.Name +} + +// A MessageHandler handles a message +type MessageHandler func(*Component, *Message) error + +func noOpMessageHandler(c *Component, m *Message) error { + return nil +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..2ad6d2f --- /dev/null +++ b/options.go @@ -0,0 +1,36 @@ +package xco + +import "golang.org/x/net/context" + +// Options define the series of options required to build a component +type Options struct { + + // Name defines the component name + Name string + + // SharedSecret is the secret shared between the server and component + SharedSecret string + + // Address is the address of the XMPP server + Address string + + // The (optional) parent context + Context context.Context +} + +// NewComponent creates a new component from the given options +func (opts *Options) NewComponent() (*Component, error) { + + if opts.Context == nil { + opts.Context = context.Background() + } + + var c Component + c.ctx, c.cancelFn = context.WithCancel(opts.Context) + + if err := c.dial(opts); err != nil { + return nil, err + } + + return &c, nil +} diff --git a/presence.go b/presence.go new file mode 100644 index 0000000..464fa6d --- /dev/null +++ b/presence.go @@ -0,0 +1,28 @@ +package xco + +const ( + SUBSCRIBE = "subscribe" + SUBSCRIBED = "subscribed" + UNSUBSCRIBE = "unsubscribe" + UNSUBSCRIBED = "unsubscribed" + UNAVAILABLE = "unavailable" + PROBE = "probe" +) + +type Presence struct { + Header + + Show string `xml:"show"` + Status string `xml:"status"` + Priority byte `xml:"priority"` + + Type string `xml:"type"` + + XMLName string `xml:"presence"` +} + +type PresenceHandler func(c *Component, p *Presence) error + +func noOpPresenceHandler(c *Component, p *Presence) error { + return nil +} diff --git a/read.go b/read.go new file mode 100644 index 0000000..1c3a5bd --- /dev/null +++ b/read.go @@ -0,0 +1,67 @@ +package xco + +import "encoding/xml" + +func (c *Component) readLoopState() (stateFn, error) { + + t, err := c.dec.Token() + if err != nil { + return nil, err + } + + if st, ok := t.(xml.StartElement); ok { + + if st.Name.Local == "message" { + var m Message + if err := c.dec.DecodeElement(&m, &st); err != nil { + return nil, err + } + + if err := c.MessageHandler(c, &m); err != nil { + return nil, err + } + } else if st.Name.Local == "presence" { + var p Presence + if err := c.dec.DecodeElement(&p, &st); err != nil { + return nil, err + } + + if err := c.PresenceHandler(c, &p); err != nil { + return nil, err + } + } else if st.Name.Local == "iq" { + + var iq Iq + if err := c.dec.DecodeElement(&iq, &st); err != nil { + return nil, err + } + + if err := c.IqHandler(c, &iq); err != nil { + return nil, err + } + + /* + if iq.Type == "get" { + + iqResp := &Iq{ + Header: Header{ + From: iq.To, + To: iq.From, + ID: iq.ID, + }, + Type: "result", + Content: ` + + Sheena Artrip + + `, + } + + c.enc.Encode(&iqResp) + } + */ + } + } + + return c.readLoopState, nil +}