2018-01-01 09:12:33 -08:00
|
|
|
package xmpp // import "fluux.io/xmpp"
|
2016-01-06 07:51:12 -08:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/xml"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"strings"
|
2016-02-17 04:45:39 -08:00
|
|
|
"time"
|
2016-01-06 07:51:12 -08:00
|
|
|
)
|
|
|
|
|
2016-02-15 02:05:44 -08:00
|
|
|
// Client is the main structure use to connect as a client on an XMPP
|
|
|
|
// server.
|
2016-01-06 07:51:12 -08:00
|
|
|
type Client struct {
|
|
|
|
// Store user defined options
|
|
|
|
options Options
|
|
|
|
// Session gather data that can be accessed by users of this library
|
|
|
|
Session *Session
|
2017-10-21 04:58:58 -07:00
|
|
|
// TCP level connection / can be replaced by a TLS session after starttls
|
2016-01-06 07:51:12 -08:00
|
|
|
conn net.Conn
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Setting up the client / Checking the parameters
|
|
|
|
*/
|
|
|
|
|
2016-02-13 08:01:06 -08:00
|
|
|
// NewClient generates a new XMPP client, based on Options passed as parameters.
|
|
|
|
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
|
|
|
|
// Default the port to 5222.
|
|
|
|
// TODO: better options checks
|
2016-01-06 07:51:12 -08:00
|
|
|
func NewClient(options Options) (c *Client, err error) {
|
|
|
|
// TODO: If option address is nil, use the Jid domain to compose the address
|
|
|
|
if options.Address, err = checkAddress(options.Address); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if options.Password == "" {
|
|
|
|
err = errors.New("missing password")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c = new(Client)
|
|
|
|
c.options = options
|
|
|
|
|
|
|
|
// Parse JID
|
|
|
|
if c.options.parsedJid, err = NewJid(c.options.Jid); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-02-17 04:45:39 -08:00
|
|
|
if c.options.ConnectTimeout == 0 {
|
|
|
|
c.options.ConnectTimeout = 15 // 15 second as default
|
|
|
|
}
|
2016-01-06 07:51:12 -08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-04 13:39:34 -07:00
|
|
|
// TODO Pass JID to be able to add default address based on JID, if
|
|
|
|
// addr is empty
|
2016-01-06 07:51:12 -08:00
|
|
|
func checkAddress(addr string) (string, error) {
|
|
|
|
var err error
|
|
|
|
hostport := strings.Split(addr, ":")
|
|
|
|
if len(hostport) > 2 {
|
|
|
|
err = errors.New("too many colons in xmpp server address")
|
|
|
|
return addr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Address is composed of two parts, we are good
|
|
|
|
if len(hostport) == 2 && hostport[1] != "" {
|
|
|
|
return addr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Port was not passed, we append XMPP default port:
|
|
|
|
return strings.Join([]string{hostport[0], "5222"}, ":"), err
|
|
|
|
}
|
|
|
|
|
2016-02-13 08:01:06 -08:00
|
|
|
// Connect triggers actual TCP connection, based on previously defined parameters.
|
2016-01-06 07:51:12 -08:00
|
|
|
func (c *Client) Connect() (*Session, error) {
|
|
|
|
var tcpconn net.Conn
|
|
|
|
var err error
|
2016-02-17 04:45:39 -08:00
|
|
|
|
|
|
|
// TODO: Refactor = abstract retry loop in capped exponential back-off function
|
|
|
|
var try = 0
|
|
|
|
var success bool
|
|
|
|
for try <= c.options.Retry || !success {
|
|
|
|
if tcpconn, err = net.DialTimeout("tcp", c.options.Address, time.Duration(c.options.ConnectTimeout)*time.Second); err == nil {
|
|
|
|
success = true
|
|
|
|
}
|
|
|
|
try++
|
|
|
|
}
|
|
|
|
if err != nil {
|
2016-01-06 07:51:12 -08:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-02-17 04:45:39 -08:00
|
|
|
// Connection is ok, we now open XMPP session
|
2016-01-06 07:51:12 -08:00
|
|
|
c.conn = tcpconn
|
|
|
|
if c.conn, c.Session, err = NewSession(c.conn, c.options); err != nil {
|
|
|
|
return c.Session, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// We're connected and can now receive and send messages.
|
|
|
|
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
2016-01-06 08:08:51 -08:00
|
|
|
// TODO: Do we always want to send initial presence automatically ?
|
|
|
|
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
2016-01-06 07:51:12 -08:00
|
|
|
fmt.Fprintf(c.Session.socketProxy, "<presence/>")
|
|
|
|
|
|
|
|
return c.Session, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) recv(receiver chan<- interface{}) (err error) {
|
|
|
|
for {
|
2018-01-13 09:50:17 -08:00
|
|
|
val, err := next(c.Session.decoder)
|
2016-01-06 07:51:12 -08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
receiver <- val
|
|
|
|
val = nil
|
|
|
|
}
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
|
2016-02-13 08:01:06 -08:00
|
|
|
// Recv abstracts receiving preparsed XMPP packets from a channel.
|
|
|
|
// Channel allow client to receive / dispatch packets in for range loop.
|
2016-01-06 07:51:12 -08:00
|
|
|
func (c *Client) Recv() <-chan interface{} {
|
|
|
|
ch := make(chan interface{})
|
|
|
|
go c.recv(ch)
|
|
|
|
return ch
|
|
|
|
}
|
|
|
|
|
2018-01-26 00:55:39 -08:00
|
|
|
// Send marshalls XMPP stanza and sends it to the server.
|
|
|
|
func (c *Client) Send(packet Packet) error {
|
|
|
|
data, err := xml.Marshal(packet)
|
|
|
|
if err != nil {
|
|
|
|
return errors.New("cannot marshal packet " + err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := fmt.Fprintf(c.conn, string(data)); err != nil {
|
|
|
|
return errors.New("cannot send packet " + err.Error())
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendRaw sends an XMPP stanza as a string to the server.
|
|
|
|
// It can be invalid XML or XMPP content. In that case, the server will
|
|
|
|
// disconnect the client. It is up to the user of this method to
|
|
|
|
// carefully craft the XML content to produce valid XMPP.
|
|
|
|
func (c *Client) SendRaw(packet string) error {
|
2018-01-12 10:08:47 -08:00
|
|
|
fmt.Fprintf(c.Session.socketProxy, packet) // TODO handle errors
|
2016-01-06 07:51:12 -08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func xmlEscape(s string) string {
|
|
|
|
var b bytes.Buffer
|
|
|
|
xml.Escape(&b, []byte(s))
|
|
|
|
return b.String()
|
|
|
|
}
|