├── .gitignore ├── LICENSE ├── README.md ├── address.go ├── address_test.go ├── cmd └── echo-component │ ├── .gitignore │ └── main.go ├── component.go ├── disco.go ├── error.go ├── error_test.go ├── go.mod ├── handshake.go ├── header.go ├── header_test.go ├── iq.go ├── logger.go ├── message.go ├── message_test.go ├── options.go ├── presence.go ├── read.go ├── vcard.go └── xep-0184.go /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheenobu/go-xco/9f71bc564eae64ef8faf9d494a5aea31e7b3aa81/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sheena Artrip 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-xco 2 | 3 | [![GoDoc](https://godoc.org/github.com/sheenobu/go-xco?status.svg)](https://godoc.org/github.com/sheenobu/go-xco) 4 | 5 | Library for building XMPP/Jabber ([XEP-0114](http://xmpp.org/extensions/xep-0114.html)) components in golang. 6 | 7 | ## XEPs 8 | 9 | Additional XEPs have been added through forks and cherry-picked into 10 | a downstream branch, then merged into master. If you've got additional XEPs to add, create a pull 11 | request or issue to list it. 12 | 13 | * [all] XEP-0114 - XMPP Components 14 | * [v0.3.x] XEP-0172 - User Nicknames [mndrix/go-xco](https://github.com/mndrix/go-xco) 15 | * [v0.3.x] XEP-0184 - Message Delivery Receipt [mndrix/go-xco](https://github.com/mndrix/go-xco) 16 | * [v0.3.x] XEP-0030 - Service Discovery [mndrix/go-xco](https://github.com/mndrix/go-xco) 17 | * [v0.3.x] XEP-0054 - vCard [mndrix/go-xco](https://github.com/mndrix/go-xco) 18 | 19 | ## Usage: 20 | 21 | import ( 22 | "github.com/sheenobu/go-xco" 23 | ) 24 | 25 | func main(){ 26 | 27 | opts := xco.Options{ 28 | Name: Name, 29 | SharedSecret: SharedSecret, 30 | Address: Address, 31 | } 32 | 33 | c, err := xco.NewComponent(opts) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | // Uppercase Echo Component 39 | c.MessageHandler = xco.BodyResponseHandler(func(msg *xco.Message) (string, error) { 40 | return strings.ToUpper(msg.Body), nil 41 | }) 42 | 43 | c.Run() 44 | } 45 | 46 | ## Raw Usage 47 | 48 | The various handlers take the arguments of Component and either Message, Iq, Presence, etc. 49 | 50 | You can work with the messages directly without a helper function: 51 | 52 | // Uppercase Echo Component 53 | c.MessageHandler = func(c *xco.Component, msg *xco.Message) error { 54 | resp := xco.Message{ 55 | Header: xco.Header{ 56 | From: msg.To, 57 | To: msg.From, 58 | ID: msg.ID, 59 | }, 60 | Subject: msg.Subject, 61 | Thread: msg.Thread, 62 | Type: msg.Type, 63 | Body: strings.ToUpper(msg.Body), 64 | XMLName: msg.XMLName, 65 | } 66 | 67 | return c.Send(&resp) 68 | } 69 | 70 | -------------------------------------------------------------------------------- /address.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Address is an XMPP JID address 13 | type Address struct { 14 | LocalPart string 15 | DomainPart string 16 | ResourcePart string 17 | } 18 | 19 | // ParseAddress parses the address from the given string 20 | func ParseAddress(s string) (Address, error) { 21 | var addr Address 22 | err := addr.parse(s) 23 | return addr, err 24 | } 25 | 26 | // Equals compares the given address 27 | func (a *Address) Equals(o *Address) bool { 28 | return (a == o) || ((a != nil && o != nil) && (a.LocalPart == o.LocalPart && a.DomainPart == o.DomainPart && a.ResourcePart == o.ResourcePart)) 29 | } 30 | 31 | // String formats the address as an XMPP JID 32 | func (a *Address) String() string { 33 | buf := bytes.NewBufferString("") 34 | if a.LocalPart != "" { 35 | buf.WriteString(a.LocalPart) 36 | buf.WriteString("@") 37 | } 38 | 39 | buf.WriteString(a.DomainPart) 40 | 41 | if a.ResourcePart != "" { 42 | buf.WriteString("/") 43 | buf.WriteString(a.ResourcePart) 44 | } 45 | 46 | return buf.String() 47 | } 48 | 49 | // Bare returns a copy of this address with the resource part made 50 | // blank. 51 | func (a *Address) Bare() *Address { 52 | b := *a 53 | b.ResourcePart = "" 54 | return &b 55 | } 56 | 57 | // UnmarshalXMLAttr marks the Address struct as being able to be parsed as an XML attribute 58 | func (a *Address) UnmarshalXMLAttr(attr xml.Attr) error { 59 | return a.parse(attr.Value) 60 | } 61 | 62 | // MarshalXMLAttr marks the Address struct as being able to be written as an XML attribute 63 | func (a *Address) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { 64 | if a == nil { 65 | return xml.Attr{}, nil 66 | } 67 | errs := a.validate() 68 | if len(errs) != 0 { 69 | return xml.Attr{}, &multiError{ 70 | errs: errs, 71 | mainError: errors.Errorf( 72 | "Malformed Address for Attribute %s", name), 73 | } 74 | } 75 | 76 | return xml.Attr{ 77 | Name: name, 78 | Value: a.String(), 79 | }, nil 80 | } 81 | 82 | func (a *Address) validate() []error { 83 | 84 | var errs []error 85 | if a != nil && a.LocalPart != "" && a.DomainPart == "" { 86 | errs = append(errs, errors.New("Domain is empty")) 87 | } 88 | 89 | return errs 90 | } 91 | 92 | func (a *Address) parse(s string) error { 93 | 94 | // normalization 95 | 96 | s = strings.TrimSpace(s) 97 | 98 | if len(s) == 0 { 99 | return nil //errors.New("Address is empty") 100 | } 101 | 102 | // parsing 103 | 104 | domainStart := 0 105 | domainEnd := len(s) 106 | 107 | if idx := strings.IndexAny(s, "@"); idx != -1 { 108 | a.LocalPart = s[0:idx] 109 | domainStart = idx + 1 110 | } 111 | 112 | if idx := strings.IndexAny(s, "/"); idx != -1 { 113 | a.ResourcePart = s[idx+1:] 114 | domainEnd = idx 115 | } 116 | 117 | if domainStart != domainEnd { 118 | a.DomainPart = s[domainStart:domainEnd] 119 | } 120 | 121 | // validation 122 | 123 | errs := a.validate() 124 | 125 | if a.LocalPart == "" && domainStart != 0 { 126 | errs = append(errs, errors.New("Localpart is empty")) 127 | } 128 | 129 | if a.ResourcePart == "" && domainEnd != len(s) { 130 | errs = append(errs, errors.New("Resourcepart is empty")) 131 | } 132 | 133 | if len(errs) == 1 { 134 | return errs[0] 135 | } else if len(errs) > 1 { 136 | return fmt.Errorf("Multiple errors: %v", errs) 137 | } 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /address_test.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "encoding/xml" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | var nilAddress = Address{} 11 | var withDomain = Address{DomainPart: "example.com"} 12 | 13 | var parseAddressTests = []struct { 14 | Input string 15 | Output Address 16 | Error string 17 | }{ 18 | {"", nilAddress, ""}, 19 | {" ", nilAddress, ""}, 20 | {"@example.com", withDomain, "Localpart is empty"}, 21 | {"example.com/", withDomain, "Resourcepart is empty"}, 22 | {"@example.com/", withDomain, "Multiple errors: [Localpart is empty Resourcepart is empty]"}, 23 | {"@/", nilAddress, "Multiple errors: [Localpart is empty Resourcepart is empty]"}, 24 | 25 | {"example.com", Address{"", "example.com", ""}, ""}, 26 | {"hello@example.com", Address{"hello", "example.com", ""}, ""}, 27 | {"example.com/home", Address{"", "example.com", "home"}, ""}, 28 | {"hello@example.com/home", Address{"hello", "example.com", "home"}, ""}, 29 | 30 | {"goodbye@example.com/home", Address{"goodbye", "example.com", "home"}, ""}, 31 | } 32 | 33 | func TestParseAddress(t *testing.T) { 34 | for _, pat := range parseAddressTests { 35 | addr, err := ParseAddress(pat.Input) 36 | matches := addr.Equals(&pat.Output) && (err == nil && pat.Error == "" || err.Error() == pat.Error) 37 | if !matches { 38 | t.Errorf("ParseAddress(%s) => {%s,%v}, expected {%s,%s}", 39 | pat.Input, addr, err, pat.Output, pat.Error) 40 | } 41 | } 42 | } 43 | 44 | var stringAddressTests = []struct { 45 | Input *Address 46 | Output string 47 | }{ 48 | {&Address{"", "example.com", ""}, "example.com"}, 49 | {&Address{"hello", "example.com", ""}, "hello@example.com"}, 50 | {&Address{"", "example.com", "home"}, "example.com/home"}, 51 | {&Address{"hello", "example.com", "home"}, "hello@example.com/home"}, 52 | {&Address{"goodbye", "example.com", "home"}, "goodbye@example.com/home"}, 53 | } 54 | 55 | func TestStringAddress(t *testing.T) { 56 | for _, sat := range stringAddressTests { 57 | out := sat.Input.String() 58 | matches := out == sat.Output 59 | if !matches { 60 | t.Errorf("%v.String() => {%s}, expected {%s}", 61 | sat.Input, out, sat.Output) 62 | } 63 | } 64 | } 65 | 66 | func attrEquals(a *xml.Attr, b *xml.Attr) bool { 67 | if a == b { 68 | return true 69 | } 70 | if a == nil && b != nil { 71 | return false 72 | } 73 | if a != nil && b == nil { 74 | return false 75 | } 76 | 77 | if a.Name.Space != b.Name.Space { 78 | return false 79 | } 80 | if a.Name.Local != b.Name.Local { 81 | return false 82 | } 83 | if a.Value != b.Value { 84 | return false 85 | } 86 | 87 | return true 88 | } 89 | 90 | var marshallXMLAddressTests = []struct { 91 | Input *Address 92 | Output xml.Attr 93 | Error string 94 | }{ 95 | {nil, xml.Attr{}, ""}, 96 | {&Address{"", "example.com", ""}, 97 | xml.Attr{Name: xml.Name{}, Value: "example.com"}, ""}, 98 | {&Address{"asdf", "", ""}, 99 | xml.Attr{}, "[Domain is empty]"}, 100 | } 101 | 102 | func TestMarshallXMLAddress(t *testing.T) { 103 | for _, mat := range marshallXMLAddressTests { 104 | out, err := mat.Input.MarshalXMLAttr(xml.Name{}) 105 | matches := attrEquals(&out, &mat.Output) && (err == nil && mat.Error == "" || errors.Cause(err).Error() == mat.Error) 106 | if !matches { 107 | t.Errorf("{%s}.MarshalXMLAttr({}) => {%s,%v}, expected {%v,%s}", 108 | mat.Input, out, errors.Cause(err), mat.Output, mat.Error) 109 | } 110 | } 111 | } 112 | 113 | var bareAddressTests = []struct { 114 | Input string 115 | Output string 116 | }{ 117 | {"example.com", "example.com"}, 118 | {"hello@example.com", "hello@example.com"}, 119 | {"example.com/home", "example.com"}, 120 | {"hello@example.com/home", "hello@example.com"}, 121 | {"goodbye@example.com/home", "goodbye@example.com"}, 122 | } 123 | 124 | func TestBareAddress(t *testing.T) { 125 | for _, test := range bareAddressTests { 126 | a, err := ParseAddress(test.Input) 127 | if err != nil { 128 | t.Errorf("Invalid address: %s", test.Input) 129 | continue 130 | } 131 | 132 | if a.Bare().String() != test.Output { 133 | t.Errorf("%v.Bare() => {%s}, expected {%s}", 134 | test.Input, a.Bare(), test.Output) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cmd/echo-component/.gitignore: -------------------------------------------------------------------------------- 1 | echo-component 2 | -------------------------------------------------------------------------------- /cmd/echo-component/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "strings" 7 | 8 | "github.com/sheenobu/go-xco" 9 | ) 10 | 11 | var Name string 12 | var SharedSecret string 13 | var Address string 14 | 15 | func init() { 16 | flag.StringVar(&Name, "name", "", "Name of Component") 17 | flag.StringVar(&SharedSecret, "secret", "", "Shared Secret between server and component") 18 | flag.StringVar(&Address, "address", "", "Hostname:port address of XMPP component listener") 19 | } 20 | 21 | func main() { 22 | 23 | flag.Parse() 24 | 25 | if Name == "" || SharedSecret == "" || Address == "" { 26 | flag.Usage() 27 | os.Exit(-1) 28 | return 29 | } 30 | 31 | opts := xco.Options{ 32 | Name: Name, 33 | SharedSecret: SharedSecret, 34 | Address: Address, 35 | } 36 | 37 | c, err := xco.NewComponent(opts) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // Uppercase Echo Component 43 | c.MessageHandler = xco.BodyResponseHandler(func(msg *xco.Message) (string, error) { 44 | return strings.ToUpper(msg.Body), nil 45 | }) 46 | 47 | c.PresenceHandler = xco.AlwaysOnlinePresenceHandler 48 | 49 | if err := c.Run(); err != nil { 50 | panic(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /component.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "encoding/xml" 5 | "log" 6 | "net" 7 | 8 | "github.com/pkg/errors" 9 | 10 | "context" 11 | ) 12 | 13 | type stateFn func() (stateFn, error) 14 | 15 | // A Component is an instance of a Jabber Component (XEP-0114) 16 | type Component struct { 17 | MessageHandler MessageHandler 18 | DiscoInfoHandler DiscoInfoHandler 19 | PresenceHandler PresenceHandler 20 | IqHandler IqHandler 21 | UnknownHandler UnknownElementHandler 22 | 23 | ctx context.Context 24 | cancelFn context.CancelFunc 25 | 26 | conn net.Conn 27 | dec *xml.Decoder 28 | enc *xml.Encoder 29 | log *log.Logger 30 | 31 | stateFn stateFn 32 | 33 | sharedSecret string 34 | name string 35 | } 36 | 37 | func (c *Component) init(o Options) error { 38 | conn, err := net.Dial("tcp", o.Address) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | c.MessageHandler = noOpMessageHandler 44 | c.DiscoInfoHandler = noOpDiscoInfoHandler 45 | c.PresenceHandler = noOpPresenceHandler 46 | c.IqHandler = noOpIqHandler 47 | c.UnknownHandler = noOpUnknownHandler 48 | 49 | c.conn = conn 50 | c.name = o.Name 51 | c.sharedSecret = o.SharedSecret 52 | if o.Logger == nil { 53 | c.dec = xml.NewDecoder(conn) 54 | c.enc = xml.NewEncoder(conn) 55 | } else { 56 | c.log = o.Logger 57 | c.dec = xml.NewDecoder(newReadLogger(c.log, conn)) 58 | c.enc = xml.NewEncoder(newWriteLogger(c.log, conn)) 59 | } 60 | c.stateFn = c.handshakeState 61 | 62 | return nil 63 | } 64 | 65 | // Close closes the Component 66 | func (c *Component) Close() { 67 | if c == nil { 68 | return 69 | } 70 | c.cancelFn() 71 | } 72 | 73 | // Run runs the component handlers loop and waits for it to finish 74 | func (c *Component) Run() (err error) { 75 | 76 | defer func() { 77 | c.conn.Close() 78 | }() 79 | 80 | for { 81 | if c.stateFn == nil { 82 | return nil 83 | } 84 | c.stateFn, err = c.stateFn() 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | } 90 | 91 | // A Sender is an interface which allows sending of arbitrary objects 92 | // as XML to an XMPP server. 93 | type Sender interface { 94 | Send(i interface{}) error 95 | } 96 | 97 | // Send sends the given pointer struct by serializing it to XML. 98 | func (c *Component) Send(i interface{}) error { 99 | return errors.Wrap(c.enc.Encode(i), "Error encoding object to XML") 100 | } 101 | 102 | // Write implements the io.Writer interface to allow direct writing to the XMPP connection 103 | func (c *Component) Write(b []byte) (int, error) { 104 | return c.conn.Write(b) 105 | } 106 | -------------------------------------------------------------------------------- /disco.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import "encoding/xml" 4 | 5 | // DiscoInfoHandler handles an incoming service discovery request. 6 | // The target entity is described by iq.To. This function's first 7 | // return value is a slice of identities for the target entity. The 8 | // second is a slice of features offered by the target entity. 9 | // 10 | // You don't have to include a feature indicating support for service 11 | // discovery, one is automatically added for you. If you return 0 12 | // identity elements, service discovery is disabled. 13 | type DiscoInfoHandler func(c *Component, iq *Iq) ([]DiscoIdentity, []DiscoFeature, error) 14 | 15 | func noOpDiscoInfoHandler(c *Component, iq *Iq) ([]DiscoIdentity, []DiscoFeature, error) { 16 | return nil, nil, nil 17 | } 18 | 19 | const discoInfoSpace = `http://jabber.org/protocol/disco#info` 20 | 21 | // DiscoInfoQuery represents a service discovery info query message. 22 | // See section 3.1 in XEP-0030. 23 | type DiscoInfoQuery struct { 24 | Identities []DiscoIdentity 25 | Features []DiscoFeature 26 | 27 | XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` 28 | } 29 | 30 | // returns true if this is a valid service discovery info query that 31 | // meets the requirements of XEP-0030 section 3.1 32 | func (q *DiscoInfoQuery) isValid() bool { 33 | return len(q.Identities) == 0 && 34 | len(q.Features) == 0 && 35 | q.XMLName.Local == "query" && 36 | q.XMLName.Space == discoInfoSpace 37 | } 38 | 39 | // IsDiscoInfo returns true if an iq stanza is a service discovery 40 | // info query. 41 | func (iq *Iq) IsDiscoInfo() bool { 42 | if iq.Type == "get" { 43 | var disco DiscoInfoQuery 44 | err := xml.Unmarshal([]byte(iq.Content), &disco) 45 | return err == nil && disco.isValid() 46 | } 47 | return false 48 | } 49 | 50 | // DiscoIdentity represents an identity element in a response to a 51 | // service discovery info query. 52 | type DiscoIdentity struct { 53 | // Category is a mandatory description of the category of this 54 | // identity. Category often contains values like "conference", 55 | // "directory", "gateway", "server", "client", etc. 56 | // 57 | // See the category registry at http://xmpp.org/registrar/disco-categories.html 58 | Category string `xml:"category,attr"` 59 | 60 | // Type is a mandatory description of the type of this identity. 61 | // The type goes together with the Category to help requesting 62 | // entities know which services are offered. 63 | // 64 | // For example, if Category is "gateway" then Type might be "msn" 65 | // or "aim". See the type registry at http://xmpp.org/registrar/disco-categories.html 66 | Type string `xml:"type,attr"` 67 | 68 | // Name is an optional natural language name for the entity 69 | // described by this identity. 70 | Name string `xml:"name,attr,omitempty"` 71 | 72 | XMLName string `xml:"identity"` 73 | } 74 | 75 | // DiscoFeature represents a feature element in a response to a 76 | // service discovery info query. 77 | // 78 | // See the registry of features at http://xmpp.org/registrar/disco-features.html 79 | type DiscoFeature struct { 80 | // Var is a mandatory protocol namespace offered by the entity. 81 | Var string `xml:"var,attr"` 82 | 83 | XMLName string `xml:"feature"` 84 | } 85 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Error is the error sent over XMPP 11 | type Error struct { 12 | XMLName xml.Name 13 | 14 | Code string `xml:"code,omitempty,attr"` 15 | Type string `xml:"type,omitempty,attr"` 16 | } 17 | 18 | func (e *Error) String() string { 19 | return fmt.Sprintf("Error{code='%s' type='%s'}", e.Code, e.Type) 20 | } 21 | 22 | type multiError struct { 23 | errs []error 24 | mainError error 25 | } 26 | 27 | func (m *multiError) Error() string { 28 | return fmt.Sprintf("%s: %s", m.mainError.Error(), m.errs) 29 | } 30 | 31 | func (m *multiError) Errors() []error { 32 | return m.errs 33 | } 34 | 35 | func (m *multiError) Cause() error { 36 | //FIXME: should "cause" be the mainError or the group of errors? 37 | return errors.Errorf("%s", m.errs) 38 | } 39 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import "testing" 4 | 5 | type manyerrors interface { 6 | Errors() []error 7 | } 8 | 9 | func TestErrorCast(t *testing.T) { 10 | var err error = &multiError{} 11 | e, ok := err.(manyerrors) 12 | if !ok { 13 | t.Errorf("can't cast multiError to Errors interface") 14 | } 15 | e.Errors() 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sheenobu/go-xco 2 | 3 | require github.com/pkg/errors v0.8.1 4 | -------------------------------------------------------------------------------- /handshake.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "encoding/xml" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func sendStreamStart(w io.Writer, name string) (err error) { 14 | _, err = fmt.Fprintf(w, ``, name) 15 | err = errors.Wrapf(err, "failed to write stream start for %s", name) 16 | return 17 | } 18 | 19 | func sendHandshake(w io.Writer, id string, sharedSecret string) (err error) { 20 | handshakeInput := id + sharedSecret 21 | handshake := sha1.Sum([]byte(handshakeInput)) 22 | hexHandshake := hex.EncodeToString(handshake[:]) 23 | _, err = fmt.Fprintf(w, "%s", hexHandshake) 24 | err = errors.Wrapf(err, "failed to write handshake for %s", id) 25 | return 26 | } 27 | 28 | func findStreamID(stream *xml.StartElement) (id string, err error) { 29 | 30 | for _, a := range stream.Attr { 31 | if a.Name.Local == "id" { 32 | id = a.Value 33 | } 34 | } 35 | 36 | if id == "" { 37 | err = errors.New("Unable to find ID in stream response") 38 | return 39 | } 40 | 41 | return 42 | } 43 | 44 | func (c *Component) handshakeState() (st stateFn, err error) { 45 | 46 | if err = sendStreamStart(c.conn, c.name); err != nil { 47 | err = errors.Wrapf(err, "Error sending streamStart") 48 | return 49 | } 50 | 51 | for { 52 | var t xml.Token 53 | if t, err = c.dec.Token(); err != nil { 54 | return 55 | } 56 | 57 | stream, ok := t.(xml.StartElement) 58 | if !ok { 59 | continue 60 | } 61 | 62 | var id string 63 | id, err = findStreamID(&stream) 64 | if err != nil { 65 | err = errors.Wrapf(err, "Failed to find ID attribute in stream response") 66 | return 67 | } 68 | 69 | if err = sendHandshake(c.conn, id, c.sharedSecret); err != nil { 70 | err = errors.Wrapf(err, "Failed to send handshake") 71 | return 72 | } 73 | 74 | //TODO: separate each step into a state 75 | 76 | // get handshake response, but ignore it 77 | _, err = c.dec.Token() 78 | if err != nil { 79 | return 80 | } 81 | 82 | _, err = c.dec.Token() 83 | if err != nil { 84 | return 85 | } 86 | 87 | st = c.readLoopState 88 | return 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | // Header contains the common fields for every XEP-0114 message 4 | type Header struct { 5 | ID string `xml:"id,attr,omitempty"` 6 | From *Address `xml:"from,attr,omitempty"` 7 | To *Address `xml:"to,attr,omitempty"` 8 | } 9 | -------------------------------------------------------------------------------- /header_test.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "testing" 7 | ) 8 | 9 | const headerBody = ` 10 | 14 | 15 | ` 16 | 17 | func TestReadHeader(t *testing.T) { 18 | input := bytes.NewReader([]byte(headerBody)) 19 | dec := xml.NewDecoder(input) 20 | 21 | var h Header 22 | 23 | err := dec.Decode(&h) 24 | if err != nil { 25 | t.Errorf("Unexpected error parsing message header: %s", err) 26 | return 27 | } 28 | 29 | if s := h.From.String(); s != "goodbye@example.com/home" { 30 | t.Errorf("Expected from string to be 'goodbye@example.com/home', is '%s'", s) 31 | } 32 | 33 | if s := h.To.String(); s != "hello@example.com" { 34 | t.Errorf("Expected from string to be 'hello@example.com', is '%s'", s) 35 | } 36 | 37 | if h.From.DomainPart != "example.com" { 38 | t.Errorf("domain part equals %s, expected %s", "example.com", h.From.DomainPart) 39 | } 40 | 41 | if h.ID != "asdf" { 42 | t.Errorf("Expected ID to be 'asdf', is '%s'", h.ID) 43 | } 44 | 45 | } 46 | 47 | func TestWriteHeader(t *testing.T) { 48 | b := bytes.NewBuffer([]byte("")) 49 | enc := xml.NewEncoder(b) 50 | 51 | var h Header 52 | 53 | err := enc.Encode(&h) 54 | if err != nil { 55 | t.Errorf("Unexpected error encoding message header: %s", err) 56 | return 57 | } 58 | 59 | //h.From.DomainPart = "example.com" 60 | //h.From.ResourcePart = "home" 61 | 62 | h.To = &Address{} 63 | h.To.LocalPart = "goodbye" 64 | h.To.DomainPart = "example.com" 65 | h.To.ResourcePart = "home" 66 | 67 | err = enc.Encode(&h) 68 | if err != nil { 69 | t.Errorf("Unexpected error encoding message header: %s", err) 70 | return 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /iq.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | // Iq represents an info/query message 4 | type Iq struct { 5 | Header 6 | 7 | Type string `xml:"type,attr"` 8 | 9 | Content string `xml:",innerxml"` 10 | 11 | Vcard *Vcard `xml:"vcard-temp vCard,omitempty"` 12 | 13 | XMLName string `xml:"iq"` 14 | } 15 | 16 | // IqHandler handles an incoming Iq (info/query) request 17 | type IqHandler func(c *Component, iq *Iq) error 18 | 19 | func noOpIqHandler(c *Component, iq *Iq) error { 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "io" 5 | "log" 6 | ) 7 | 8 | type writeLogger struct { 9 | log *log.Logger 10 | w io.Writer 11 | } 12 | 13 | func (l *writeLogger) Write(p []byte) (n int, err error) { 14 | n, err = l.w.Write(p) 15 | if err == nil { 16 | l.log.Printf("|> %s", p[0:n]) 17 | } else { 18 | l.log.Printf("|> %s: %v", p[0:n], err) 19 | } 20 | return 21 | } 22 | 23 | // newWriteLogger returns a writer that behaves like w except that it 24 | // logs the string written. 25 | func newWriteLogger(log *log.Logger, w io.Writer) io.Writer { 26 | return &writeLogger{log, w} 27 | } 28 | 29 | type readLogger struct { 30 | log *log.Logger 31 | r io.Reader 32 | } 33 | 34 | func (l *readLogger) Read(p []byte) (n int, err error) { 35 | n, err = l.r.Read(p) 36 | if err == nil { 37 | l.log.Printf("<| %s", p[0:n]) 38 | } else { 39 | l.log.Printf("<| %s: %v", p[0:n], err) 40 | } 41 | return 42 | } 43 | 44 | // newReadLogger returns a reader that behaves like r except that it 45 | // logs the string read. 46 | func newReadLogger(log *log.Logger, r io.Reader) io.Reader { 47 | return &readLogger{log, r} 48 | } 49 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // MessageType defines the constants for the types of messages within XEP-0114 10 | type MessageType string 11 | 12 | const ( 13 | 14 | // CHAT defines the chat message type 15 | CHAT = MessageType("chat") 16 | 17 | // ERROR defines the error message type 18 | ERROR = MessageType("error") 19 | 20 | // GROUPCHAT defines the group chat message type 21 | GROUPCHAT = MessageType("groupchat") 22 | 23 | // HEADLINE defines the headline message type 24 | HEADLINE = MessageType("headline") 25 | 26 | // NORMAL defines the normal message type 27 | NORMAL = MessageType("normal") 28 | ) 29 | 30 | // A Message is an incoming or outgoing Component message 31 | type Message struct { 32 | Header 33 | Type MessageType `xml:"type,attr,omitempty"` 34 | 35 | Subject string `xml:"subject,omitempty"` 36 | Body string `xml:"body,omitempty"` 37 | Error *Error `xml:"error"` 38 | Thread string `xml:"thread,omitempty"` 39 | Content string `xml:",innerxml"` // allow arbitrary content 40 | 41 | // XEP-0184 message delivery receipts 42 | ReceiptRequest *xml.Name `xml:"urn:xmpp:receipts request,omitempty"` 43 | ReceiptAck *ReceiptAck `xml:"urn:xmpp:receipts received,omitempty"` 44 | 45 | // XEP-0172 User nicknames 46 | Nick string `xml:"http://jabber.org/protocol/nick nick,omitempty"` 47 | 48 | XMLName xml.Name `xml:"message"` 49 | } 50 | 51 | // A MessageHandler handles an incoming message 52 | type MessageHandler func(*Component, *Message) error 53 | 54 | func noOpMessageHandler(c *Component, m *Message) error { 55 | return nil 56 | } 57 | 58 | // BodyResponseHandler builds a simple request-response style function which returns the body 59 | // of the response message 60 | func BodyResponseHandler(fn func(*Message) (string, error)) MessageHandler { 61 | return func(c *Component, m *Message) error { 62 | 63 | body, err := fn(m) 64 | if err != nil { 65 | return err 66 | } 67 | resp := m.Response() 68 | resp.Body = body 69 | return errors.Wrap(c.Send(resp), "Error sending message response") 70 | } 71 | } 72 | 73 | // Response returns a new message representing a response to this 74 | // message. The To and From attributes of the header are reversed to 75 | // indicate the new origin. 76 | func (m *Message) Response() *Message { 77 | resp := &Message{ 78 | Header: Header{ 79 | From: m.To, 80 | To: m.From, 81 | ID: m.ID, 82 | }, 83 | Subject: m.Subject, 84 | Thread: m.Thread, 85 | Type: m.Type, 86 | XMLName: m.XMLName, 87 | } 88 | 89 | return resp 90 | } 91 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestWriteMessage(t *testing.T) { 11 | b := bytes.NewBuffer([]byte("")) 12 | enc := xml.NewEncoder(b) 13 | 14 | var h Message 15 | 16 | err := enc.Encode(&h) 17 | if err != nil { 18 | t.Errorf("Unexpected error encoding message header: %s", err) 19 | return 20 | } 21 | 22 | h.From = &Address{} 23 | h.From.DomainPart = "example.com" 24 | h.From.ResourcePart = "home" 25 | 26 | h.To = &Address{} 27 | h.To.LocalPart = "goodbye" 28 | h.To.DomainPart = "example.com" 29 | h.To.ResourcePart = "home" 30 | 31 | h.Content = "" 32 | h.Body = "hello" 33 | 34 | err = enc.Encode(&h) 35 | if err != nil { 36 | t.Errorf("Unexpected error encoding message header: %s", err) 37 | return 38 | } 39 | 40 | fmt.Println(b.String()) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "log" 5 | 6 | "context" 7 | ) 8 | 9 | // Options define the series of options required to build a component 10 | type Options struct { 11 | 12 | // Name defines the component name 13 | Name string 14 | 15 | // SharedSecret is the secret shared between the server and component 16 | SharedSecret string 17 | 18 | // Address is the address of the XMPP server 19 | Address string 20 | 21 | // The (optional) parent context 22 | Context context.Context 23 | 24 | // Logger is an optional logger to which to send raw XML stanzas 25 | // sent and received. It's primarily intended for debugging and 26 | // development. 27 | Logger *log.Logger 28 | } 29 | 30 | // NewComponent creates a new component from the given options 31 | func NewComponent(opts Options) (*Component, error) { 32 | 33 | if opts.Context == nil { 34 | opts.Context = context.Background() 35 | } 36 | 37 | var c Component 38 | c.ctx, c.cancelFn = context.WithCancel(opts.Context) 39 | 40 | if err := c.init(opts); err != nil { 41 | return nil, err 42 | } 43 | 44 | return &c, nil 45 | } 46 | -------------------------------------------------------------------------------- /presence.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | const ( 8 | 9 | // SUBSCRIBE represents the subscribe Presence message type 10 | SUBSCRIBE = "subscribe" 11 | 12 | // SUBSCRIBED represents the subscribed Presence message type 13 | SUBSCRIBED = "subscribed" 14 | 15 | // UNSUBSCRIBE represents the unsubsribe Presence message type 16 | UNSUBSCRIBE = "unsubscribe" 17 | 18 | // UNSUBSCRIBED represents the unsubsribed Presence message type 19 | UNSUBSCRIBED = "unsubscribed" 20 | 21 | // UNAVAILABLE represents the unavailable Presence message type 22 | UNAVAILABLE = "unavailable" 23 | 24 | // PROBE represents the probe Presence message type 25 | PROBE = "probe" 26 | ) 27 | 28 | // Presence represents a message identifying whether an entity is available and the subscription requests/responses for the entity 29 | type Presence struct { 30 | Header 31 | 32 | Show string `xml:"show,omitempty"` 33 | Status string `xml:"status,omitempty"` 34 | Priority byte `xml:"priority,omitempty"` 35 | 36 | Type string `xml:"type,attr,omitempty"` 37 | 38 | // XEP-0172 User nicknames 39 | Nick string `xml:"http://jabber.org/protocol/nick nick,omitempty"` 40 | 41 | XMLName string `xml:"presence"` 42 | } 43 | 44 | // PresenceHandler handles incoming presence requests 45 | type PresenceHandler func(c *Component, p *Presence) error 46 | 47 | func noOpPresenceHandler(c *Component, p *Presence) error { 48 | return nil 49 | } 50 | 51 | // AlwaysOnlinePresenceHandler always returns "subscribed" to any presence requests 52 | func AlwaysOnlinePresenceHandler(c *Component, p *Presence) error { 53 | resp := &Presence{ 54 | Header: Header{ 55 | From: p.To, 56 | To: p.From, 57 | ID: p.ID, 58 | }, 59 | Type: "subscribed", 60 | } 61 | 62 | return errors.Wrap(c.Send(resp), "Error sending always online presence") 63 | } 64 | 65 | // ToAddressPresenceHandler calls the function with the To address 66 | func ToAddressPresenceHandler(fn func(subject Address) error) PresenceHandler { 67 | return func(c *Component, p *Presence) error { 68 | return fn(*p.To) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /read.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | import "encoding/xml" 4 | 5 | // UnknownElementHandler handles unknown XML entities sent through XMPP 6 | type UnknownElementHandler func(*Component, *xml.StartElement) error 7 | 8 | func noOpUnknownHandler(c *Component, x *xml.StartElement) error { 9 | return nil 10 | } 11 | 12 | func (c *Component) readLoopState() (stateFn, error) { 13 | 14 | t, err := c.dec.Token() 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | if st, ok := t.(xml.StartElement); ok { 20 | 21 | if st.Name.Local == "message" { 22 | var m Message 23 | if err := c.dec.DecodeElement(&m, &st); err != nil { 24 | return nil, err 25 | } 26 | 27 | if err := c.MessageHandler(c, &m); err != nil { 28 | return nil, err 29 | } 30 | } else if st.Name.Local == "presence" { 31 | var p Presence 32 | if err := c.dec.DecodeElement(&p, &st); err != nil { 33 | return nil, err 34 | } 35 | 36 | if err := c.PresenceHandler(c, &p); err != nil { 37 | return nil, err 38 | } 39 | } else if st.Name.Local == "iq" { 40 | 41 | var iq Iq 42 | if err := c.dec.DecodeElement(&iq, &st); err != nil { 43 | return nil, err 44 | } 45 | 46 | // recognize XEP-0030 service discovery info queries 47 | if iq.IsDiscoInfo() { 48 | return c.discoInfo(&iq) 49 | } 50 | 51 | // handle all other iq stanzas 52 | if err := c.IqHandler(c, &iq); err != nil { 53 | return nil, err 54 | } 55 | } else { 56 | if err := c.UnknownHandler(c, &st); err != nil { 57 | return nil, err 58 | } 59 | } 60 | } 61 | 62 | return c.readLoopState, nil 63 | } 64 | 65 | func (c *Component) discoInfo(iq *Iq) (stateFn, error) { 66 | ids, features, err := c.DiscoInfoHandler(c, iq) 67 | if err != nil { 68 | return nil, err 69 | } 70 | if len(ids) < 1 { 71 | return c.readLoopState, nil 72 | } 73 | 74 | features = append(features, DiscoFeature{ 75 | Var: discoInfoSpace, 76 | }) 77 | query := DiscoInfoQuery{ 78 | Identities: ids, 79 | Features: features, 80 | } 81 | queryContent, err := xml.Marshal(query) 82 | if err != nil { 83 | return nil, err 84 | } 85 | resp := &Iq{ 86 | Header: Header{ 87 | From: iq.To, 88 | To: iq.From, 89 | ID: iq.ID, 90 | }, 91 | Type: "result", 92 | Content: string(queryContent), 93 | XMLName: iq.XMLName, 94 | } 95 | if err := c.Send(resp); err != nil { 96 | return nil, err 97 | } 98 | 99 | return c.readLoopState, nil 100 | } 101 | -------------------------------------------------------------------------------- /vcard.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | // Vcard represents details about a XEP-0054 vCard. 4 | type Vcard struct { 5 | FullName string `xml:"FN,omitempty"` 6 | } 7 | -------------------------------------------------------------------------------- /xep-0184.go: -------------------------------------------------------------------------------- 1 | package xco 2 | 3 | // ReceiptAck represents an acknowledgement that the message with ID 4 | // has been received. 5 | type ReceiptAck struct { 6 | ID string `xml:"id,attr"` 7 | } 8 | --------------------------------------------------------------------------------