~samwhited/xmpp

1ae4e93cf98dad5040582648a66e8d26f8652374 — Sam Whited 5 years ago faa9acf
Use Conn as the session and remove Session
7 files changed, 210 insertions(+), 101 deletions(-)

M config.go
M conn.go
M dial.go
A features.go
M lookup.go
D session.go
A starttls.go
M config.go => config.go +27 -12
@@ 12,11 12,6 @@ import (
	"mellium.im/xmpp/jid"
)

// StreamFeatures represents a list of handlers for starting XMPP stream
// features (eg. STARTTLS). While the feature is being negotiated, the given
// function has complete control over the XML stream and the session.
type StreamFeatures map[xml.Name]func(e xml.Encoder, d xml.Decoder)

// Config represents the configuration of an XMPP session.
type Config struct {
	// An XMPP server address.


@@ 31,18 26,38 @@ type Config struct {
	// TLS config for STARTTLS.
	TLSConfig *tls.Config

	Features StreamFeatures
	// True if this is a server-to-server session.
	S2S bool

	// The supported stream features.
	Features map[xml.Name]StreamFeature
}

// NewClientConfig constructs a new client-to-server session configuration with
// sane defaults.
func NewClientConfig(origin *jid.JID) *Config {
	return &Config{
		Location: origin.Domain(),
		Origin:   origin,
		Version:  internal.DefaultVersion,

		Features: map[xml.Name]StreamFeature{
		// TODO
		},
	}
}

// NewConfig constructs a new session configuration with some sane defaults. The
// resulting config supports features to auth against most XMPP servers off the
// shelf.
func NewConfig(server, origin *jid.JID) *Config {
// NewServerConfig constructs a new server-to-server session configuration with
// sane defaults.
func NewServerConfig(location, origin *jid.JID) *Config {
	return &Config{
		Location: server,
		Location: location,
		Origin:   origin,
		S2S:      true,
		Version:  internal.DefaultVersion,

		Features: StreamFeatures{},
		Features: map[xml.Name]StreamFeature{
		// TODO
		},
	}
}

M conn.go => conn.go +68 -9
@@ 5,20 5,65 @@
package xmpp

import (
	"encoding/xml"
	"io"
	"net"
	"time"
)

// SessionState represents the current state of an XMPP session. For a
// description of each bit, see the various SessionState typed constants.
type SessionState int8

const (
	// Indicates that the underlying connection has been secured. For instance,
	// after STARTTLS has been performed or if an already secure connection is
	// being used such as websockets over HTTPS.
	Secure SessionState = 1 << iota

	// Indicates that the session has been authenticated via SASL.
	Authn

	"mellium.im/xmpp/jid"
	// Indicates that an XMPP resource has been bound.
	Bind

	// Indicates that the session is fully negotiated and that XMPP stanzas may be
	// sent and received.
	Ready

	// Indicates that the session's streams must be restarted. This bit will
	// trigger an automatic restart and will be flipped back to off as soon as the
	// stream is restarted.
	StreamRestartRequired
)

type stream struct {
	encoder *xml.Encoder
	decoder *xml.Decoder
}

func newStream(rw io.ReadWriter) stream {
	return stream{
		encoder: xml.NewEncoder(rw),
		decoder: xml.NewDecoder(rw),
	}
}

// A Conn represents an XMPP connection that can perform SRV lookups for a given
// server and connect to the correct ports.
type Conn struct {
	conn net.Conn
	config   *Config
	conn     net.Conn
	in, out  stream
	network  string
	rwc      io.ReadWriteCloser
	state    SessionState
	received bool
}

	network string
	raddr   *jid.JID
	laddr   *jid.JID
// Config returns the connections config.
func (conn *Conn) Config() *Config {
	return conn.config
}

// Read reads data from the connection.


@@ 37,14 82,28 @@ func (c *Conn) Close() error {
	return c.conn.Close()
}

// LocalAddr returns the local network address as a JID.
// State returns the current state of the session.
func (conn *Conn) State() SessionState {
	return conn.state
}

// LocalAddr returns the Origin address for initiated connections, or the
// Location for received connections.
func (c *Conn) LocalAddr() net.Addr {
	return c.laddr
	if c.received {
		return c.config.Location
	}

	return c.config.Origin
}

// RemoteAddr returns the remote network address as a JID.
// RemoteAddr returns the Location address for initiated connections, or the
// Origin address for received connections.
func (c *Conn) RemoteAddr() net.Addr {
	return c.raddr
	if c.received {
		return c.config.Origin
	}
	return c.config.Location
}

// SetDeadline sets the read and write deadlines associated with the connection.

M dial.go => dial.go +52 -33
@@ 13,26 13,40 @@ import (
	"mellium.im/xmpp/jid"
)

// DialClient connects to the address on the named network with a
// client-to-server (c2s) connection.
// DialClient discovers and connects to the address on the named network that
// services the given local address with a client-to-server (c2s) connection.
//
// For a description of the network and addr arguments see the Dialer.Dial
// method.
func DialClient(ctx context.Context, network string, addr *jid.JID) (*Conn, error) {
// laddr is the clients origin address. The remote address is taken from the
// origins domain part or from the domains SRV records. For a description of the
// ctx and network arguments, see the Dial function.
func DialClient(ctx context.Context, network string, laddr *jid.JID) (*Conn, error) {
	var d Dialer
	d.Service = "xmpp-client"
	return d.Dial(ctx, network, addr)
	return d.DialClient(ctx, network, laddr)
}

// DialServer connects to the address on the named network with a
// server-to-server (s2s) connection.
//
// For a description of the network and addr arguments see the Dialer.Dial
// method.
func DialServer(ctx context.Context, network string, addr *jid.JID) (*Conn, error) {
// raddr is the remote servers address and laddr is the local servers origin
// address. For a description of the ctx and network arguments, see the Dial
// function.
func DialServer(ctx context.Context, network string, raddr, laddr *jid.JID) (*Conn, error) {
	var d Dialer
	d.Service = "xmpp-server"
	return d.Dial(ctx, network, addr)
	return d.DialServer(ctx, network, raddr, laddr)
}

// Dial connects to the address on the named network using the provided config.
//
// The context must be non-nil. If the context expires before the connection is
// complete, an error is returned. Once successfully connected, any expiration
// of the context will not affect the connection.
//
// Network may be any of the network types supported by net.Dial, but you almost
// certainly want to use one of the tcp connection types ("tcp", "tcp4", or
// "tcp6").
func Dial(ctx context.Context, network string, config *Config) (*Conn, error) {
	var d Dialer
	return d.Dial(ctx, network, config)
}

// A Dialer contains options for connecting to an XMPP address.


@@ 42,10 56,6 @@ func DialServer(ctx context.Context, network string, addr *jid.JID) (*Conn, erro
// the DialClient function.
type Dialer struct {
	net.Dialer

	// Service is the connection type that the dialer will create (either
	// xmpp-client or xmpp-server).
	Service string
}

// Copied from the net package in the standard library. Copyright The Go


@@ 78,27 88,36 @@ func (d *Dialer) deadline(ctx context.Context, now time.Time) (earliest time.Tim
	return minNonzeroTime(earliest, d.Deadline)
}

func (d *Dialer) connType() string {
	if d.Service == "" {
		return "xmpp-client"
	} else {
		return d.Service
func connType(config *Config) string {
	if config.S2S {
		return "xmpp-server"
	}
	return "xmpp-client"
}

// Dial connects to the address on the named network using the provided context.
// DialClient discovers and connects to the address on the named network that
// services the given local address with a client-to-server (c2s) connection.
//
// The context must be non-nil. If the context expires before the connection is
// complete, an error is returned. Once successfully connected, any expiration
// of the context will not affect the connection.
// For a description of the arguments see the DialClient function.
func (d *Dialer) DialClient(ctx context.Context, network string, laddr *jid.JID) (*Conn, error) {
	c := NewClientConfig(laddr)
	return d.Dial(ctx, network, c)
}

// DialServer connects to the address on the named network with a
// server-to-server (s2s) connection.
//
// Network may be any of the network types supported by net.Dial, but you almost
// certainly want to use one of the tcp connection types ("tcp", "tcp4", or
// "tcp6"). The address is the local address that you want to make a connection
// for, and the remote address is taken from the JIDs domainpart (@example.com)
// or from the domains SRV records.
// For a description of the arguments see the DialServer function.
func (d *Dialer) DialServer(ctx context.Context, network string, raddr, laddr *jid.JID) (*Conn, error) {
	c := NewServerConfig(raddr, laddr)
	return d.Dial(ctx, network, c)
}

// Dial connects to the address on the named network using the provided config.
//
// For a description of the arguments see the Dial function.
func (d *Dialer) Dial(
	ctx context.Context, network string, addr *jid.JID) (*Conn, error) {
	ctx context.Context, network string, config *Config) (*Conn, error) {
	if ctx == nil {
		panic("xmpp.Dial: nil context")
	}


@@ 125,11 144,11 @@ func (d *Dialer) Dial(
	}

	c := &Conn{
		laddr:   addr,
		config:  config,
		network: network,
	}

	addrs, err := lookupService(d.connType(), addr.Domain())
	addrs, err := lookupService(connType(config), c.RemoteAddr())
	if err != nil {
		return nil, err
	}

A features.go => features.go +31 -0
@@ 0,0 1,31 @@
// 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 xmpp

import (
	"encoding/xml"

	"golang.org/x/net/context"
)

// A StreamFeature represents a feature that may be selected during stream
// negotiation.
type StreamFeature struct {
	// A function that will take over the session temporarily while negotiating
	// the feature. If StreamRestart is true, the stream will be restarted
	// automatically if Handler does not return an error. SessionState represents
	// the state bits that should be flipped on successful negotiation of the
	// feature. For instance, if this feature upgrades the connection to a
	// TLS connection and performs mutual TLS authentication to log in the user
	// this would be set to Authn|Secure|StreamRestartRequired, but if it does not
	// authenticate the connection it would return Secure|StreamRestartRequired.
	Handler func(ctx context.Context, conn *Conn) (SessionState, error)

	// The XML name of the feature in the <stream:feature/> list.
	Name xml.Name

	// True if negotiating the feature is required.
	Required bool
}

M lookup.go => lookup.go +9 -3
@@ 43,8 43,12 @@ var (
// returns addresses from SRV records or the default domain (as a fake SRV
// record) if no real records exist. Service should be one of "xmpp-client" or
// "xmpp-server".
func lookupService(service string, addr *jid.JID) (addrs []*net.SRV, err error) {
	_, addrs, err = net.LookupSRV(service, "tcp", addr.Domainpart())
func lookupService(service string, addr net.Addr) (addrs []*net.SRV, err error) {
	switch j := addr.(type) {
	case *jid.JID:
		addr = j.Domain()
	}
	_, addrs, err = net.LookupSRV(service, "tcp", addr.String())

	// RFC 6230 §3.2.1
	//    3.  If a response is received, it will contain one or more


@@ 67,12 71,14 @@ func lookupService(service string, addr *jid.JID) (addrs []*net.SRV, err error) 
			p = 5222
		case "xmpp-server":
			p = 5269
		case "xmpp-bosh":
			p = 5280
		default:
			return nil, err
		}
	}
	addrs = []*net.SRV{{
		Target: addr.Domainpart(),
		Target: addr.String(),
		Port:   uint16(p),
	}}
	return addrs, nil

D session.go => session.go +0 -44
@@ 1,44 0,0 @@
// 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 xmpp

import (
	"encoding/xml"
	"io"
)

// Session represents an XMPP session that can be bound to a connection and
// handles the underlying XML streams. Because sessions are independant of
// connections, the same Session may survive across reconnects and may even be
// switched between different types of connections.
type Session struct {
	in  stream
	out stream

	config *Config
}

// NewSession creates a new XMPP session over the given ReadWriteCloser and
// attempts to authenticate.
func NewSession(config *Config, rwc io.ReadWriteCloser) (*Session, error) {
	return &Session{
		in:  newStream(rwc),
		out: newStream(rwc),

		config: config,
	}, nil
}

type stream struct {
	encoder *xml.Encoder
	decoder *xml.Decoder
}

func newStream(rw io.ReadWriter) stream {
	return stream{
		encoder: xml.NewEncoder(rw),
		decoder: xml.NewDecoder(rw),
	}
}

A starttls.go => starttls.go +23 -0
@@ 0,0 1,23 @@
// 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 xmpp

import (
	"encoding/xml"

	"golang.org/x/net/context"
)

// StartTLS returns a new stream feature that can be used for negotiating TLS.
func StartTLS(required bool) StreamFeature {
	return StreamFeature{
		Handler: func(ctx context.Context, conn *Conn) (state SessionState, err error) {
			state = Secure | StreamRestartRequired
			return
		},
		Name:     xml.Name{Local: "starttls", Space: "urn:ietf:params:xml:ns:xmpp-tls"},
		Required: true,
	}
}