~samwhited/xmpp

5a3b4b1a95ad2794ed079fe37d1d9eaf2ebde532 — Sam Whited 5 years ago 6d94689
Factor xmpp connections out into their own package
5 files changed, 259 insertions(+), 59 deletions(-)

M client/client.go
A conn/doc.go
A conn/options.go
A conn/xmppconn.go
A conn/xmppconn_test.go
M client/client.go => client/client.go +4 -59
@@ 5,10 5,7 @@
package client

import (
	"net"
	"strconv"
	"time"

	"bitbucket.org/mellium/xmpp/conn"
	"bitbucket.org/mellium/xmpp/jid"
	"bitbucket.org/mellium/xmpp/stream"
)


@@ 18,14 15,9 @@ import (
type Client struct {
	options
	jid    *jid.JID
	conn   net.Conn
	conn   *conn.XMPPConn
	input  stream.Stream
	output stream.Stream

	// DNS Cache
	cname   string
	addrs   []*net.SRV
	srvtime time.Time
}

// New creates a new XMPP client with the given options.


@@ 37,59 29,12 @@ func New(j *jid.JID, opts ...Option) *Client {
}

// Connect establishes a connection with the server.
func (c *Client) Connect(password string) error {
func (c *Client) Connect(password string) (err error) {

	c.options.log.Printf("Establishing C2S connection to %s…\n", c.jid.Domainpart())

	// If the cache has expired, lookup SRV records again.
	if c.srvtime.Add(c.options.srvExpiration).Before(time.Now()) {
		if err := c.LookupSRV(); err != nil {
			return err
		}
	}

	// Try dialing all of the SRV records we know about, breaking as soon as the
	// connection is established.
	var err error
	for _, addr := range c.addrs {
		if conn, e := c.options.dialer.Dial(
			"tcp", net.JoinHostPort(
				addr.Target, strconv.FormatUint(uint64(addr.Port), 10),
			),
		); e != nil {
			err = e
			continue
		} else {
			err = nil
			c.conn = conn
			break
		}
	}
	if err != nil {
		return err
	}
	c.conn, err = conn.Dial("xmpp-client", c.jid)

	c.output = stream.New(c.jid.Domain(), c.jid)

	return nil
}

// LookupSRV fetches and caches any xmpp-client SRV records associated with the
// domain name in the clients JID. It is called automatically when a client
// attempts to establish a connection, but can be called manually to force the
// cache to update. If an expiration time is set for the records, LookupSRV
// resets the timeout.
func (c *Client) LookupSRV() error {
	c.options.log.Printf("Refreshing SRV record cache for %s…\n", c.jid.Domainpart())

	if cname, addrs, err := net.LookupSRV(
		"xmpp-client", "tcp", c.jid.Domainpart(),
	); err != nil {
		return err
	} else {
		c.addrs = addrs
		c.cname = cname
	}
	c.srvtime = time.Now()
	return nil
}

A conn/doc.go => conn/doc.go +11 -0
@@ 0,0 1,11 @@
// Copyright 2016 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

// Package conn is a generic connection handler for client-to-server (C2S) and
// server-to-server (S2S) connections between XMPP endpoints. The conn package
// merely facilitates creating the underlying connections over which XMPP will
// be routed, and does not handle any XML itself.
//
// Be advised: This API is still unstable and is subject to change.
package conn // import "bitbucket.org/mellium/xmpp/conn"

A conn/options.go => conn/options.go +93 -0
@@ 0,0 1,93 @@
// Copyright 2016 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package conn

import (
	"crypto/tls"
	"io/ioutil"
	"log"
	"net"
	"time"

	"bitbucket.org/mellium/xmpp/jid"
)

// Option's can be used to configure the connection.
type Option func(*options)
type options struct {
	log           *log.Logger
	tlsConfig     *tls.Config
	srvExpiration time.Duration
	dialer        net.Dialer
	network       string
	raddr         *jid.JID
}

func getOpts(laddr *jid.JID, o ...Option) (res options) {
	for _, f := range o {
		f(&res)
	}

	// Log to /dev/null by default.
	if res.log == nil {
		res.log = log.New(ioutil.Discard, "", log.LstdFlags)
	}
	if res.network == "" {
		res.network = "tcp"
	}
	if res.raddr == nil {
		res.raddr = laddr.Domain()
	}
	return
}

// The Logger option can be provided to have the connection log debug messages.
func Logger(logger *log.Logger) Option {
	return func(o *options) {
		o.log = logger
	}
}

// The Remote option specifies an endpoint in the XMPP network that we should
// establish the connection to. By default, the domain part of the local
// addresses JID is used.
func Remote(addr *jid.JID) Option {
	return func(o *options) {
		o.raddr = addr
	}
}

// The TLS option fully configures the TLS connection options including the
// certificate chains used, cipher suites, etc.
func TLS(config *tls.Config) Option {
	return func(o *options) {
		o.tlsConfig = config
	}
}

// The SRVExpiration option sets the duration for which the client will cache
// DNS SRV records. The default is 0 (no caching).
func SRVExpiration(exp time.Duration) Option {
	return func(o *options) {
		o.srvExpiration = exp
	}
}

// The Dialer option can be used to configure properties of the connection to
// the XMPP server including the timeout, local address, whether dualstack
// networking is enabled, etc.
func Dialer(dialer net.Dialer) Option {
	return func(o *options) {
		o.dialer = dialer
	}
}

// The network to connect with. Nothing is guaranteed to work if this is not set
// to TCP (the default).
func Network(net string) Option {
	return func(o *options) {
		o.network = net
	}
}

A conn/xmppconn.go => conn/xmppconn.go +140 -0
@@ 0,0 1,140 @@
// Copyright 2016 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package conn

import (
	"net"
	"strconv"
	"time"

	"bitbucket.org/mellium/xmpp/jid"
)

type XMPPConn struct {
	opts     options
	conn     net.Conn
	conntype string
	laddr    *jid.JID

	// DNS Cache
	cname   string
	addrs   []*net.SRV
	srvtime time.Time
}

// Dial creates a server-to-server or client-to-server connection to a remote
// endpoint. By default, it connects to the domain part of the given local
// address. The conntype should be either "xmpp-client" for C2S connections, or
// "xmpp-server" for S2S connections.
func Dial(conntype string, laddr *jid.JID, opts ...Option) (*XMPPConn, error) {

	c := &XMPPConn{
		opts:     getOpts(laddr, opts...),
		conntype: conntype,
		laddr:    laddr,
	}

	// If the cache has expired, lookup SRV records again.
	if c.srvtime.Add(c.opts.srvExpiration).Before(time.Now()) {
		if err := c.lookupSRV(); err != nil {
			return nil, err
		}
	}

	// Try dialing all of the SRV records we know about, breaking as soon as the
	// connection is established.
	var err error
	for _, addr := range c.addrs {
		if conn, e := c.opts.dialer.Dial(
			c.opts.network, net.JoinHostPort(
				addr.Target, strconv.FormatUint(uint64(addr.Port), 10),
			),
		); e != nil {
			err = e
			continue
		} else {
			err = nil
			c.conn = conn
			break
		}
	}
	if err != nil {
		return nil, err
	}

	return c, nil
}

// lookupSRV fetches and caches any xmpp-client or xmpp-server SRV records
// associated with the domain name in the clients JID. It is called
// automatically when a client attempts to establish a connection, but can be
// called manually to force the cache to update. If an expiration time is set
// for the records, lookupSRV resets the timeout.
func (c *XMPPConn) lookupSRV() error {
	if cname, addrs, err := net.LookupSRV(
		string(c.conntype), "tcp", c.opts.raddr.Domainpart(),
	); err != nil {
		return err
	} else {
		c.addrs = addrs
		c.cname = cname
	}
	c.srvtime = time.Now()
	return nil
}

// Read reads data from the connection.
func (c *XMPPConn) Read(b []byte) (n int, err error) {
	return c.conn.Read(b)
}

// Write writes data to the connection.
func (c *XMPPConn) Write(b []byte) (n int, err error) {
	return c.conn.Write(b)
}

// Close closes the connection.
// Any blocked Read or Write operations will be unblocked and return errors.
func (c *XMPPConn) Close() error {
	return c.conn.Close()
}

// LocalAddr returns the local network address as a JID.
func (c *XMPPConn) LocalAddr() net.Addr {
	return c.laddr
}

// RemoteAddr returns the remote network address as a JID.
func (c *XMPPConn) RemoteAddr() net.Addr {
	return c.opts.raddr
}

// SetDeadline sets the read and write deadlines associated with the connection.
// It is equivalent to calling both SetReadDeadline and SetWriteDeadline.
//
// A deadline is an absolute time after which I/O operations fail with a timeout
// (see type Error) instead of blocking. The deadline applies to all future I/O,
// not just the immediately following call to Read or Write.
//
// An idle timeout can be implemented by repeatedly extending the deadline after
// successful Read or Write calls.
//
// A zero value for t means I/O operations will not time out.
func (c *XMPPConn) SetDeadline(t time.Time) error {
	return c.conn.SetDeadline(t)
}

// SetReadDeadline sets the deadline for future Read calls. A zero value for t
// means Read will not time out.
func (c *XMPPConn) SetReadDeadline(t time.Time) error {
	return c.conn.SetReadDeadline(t)
}

// SetWriteDeadline sets the deadline for future Write calls. Even if write
// times out, it may return n > 0, indicating that some of the data was
// successfully written. A zero value for t means Write will not time out.
func (c *XMPPConn) SetWriteDeadline(t time.Time) error {
	return c.conn.SetWriteDeadline(t)
}

A conn/xmppconn_test.go => conn/xmppconn_test.go +11 -0
@@ 0,0 1,11 @@
// Copyright 2016 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package conn

import (
	"net"
)

var _ net.Conn = (*XMPPConn)(nil)