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 +}