Back to godoc.org
mellium.im/xmpp

Package xmpp

v0.15.0
Latest Go to latest

The latest major version is .

Published: Feb 28, 2020 | License: BSD-2-Clause | Module: mellium.im/xmpp

Overview

Package xmpp provides functionality from the Extensible Messaging and Presence Protocol, sometimes known as "Jabber".

It is subdivided into several packages; this package provides functionality for establishing an XMPP session, feature negotiation (including an API for defining your own stream features), and low-level connection and stream manipulation. The jid package provides an implementation of the XMPP address format.

Session Negotiation

To create an XMPP session, most users will want to call DialClientSession or DialServerSession to create a client-to-server (c2s) or server-to-server (s2s) connection respectively. These methods use sane defaults to dial a TCP connection and perform stream negotiation.

session, err := xmpp.DialClientSession(
    context.TODO(), addr,
    xmpp.BindResource(),
    xmpp.StartTLS(true, nil),
    xmpp.SASL("", pass, sasl.ScramSha1Plus, sasl.ScramSha1, sasl.Plain),
)

If control over DNS or HTTP-based service discovery is desired, the user can use the dial package to create a dial.Dialer or use dial.Client (c2s) or dial.Server (s2s). To use the resulting connection, or to use something other than a TCP connection (eg. to communicate over a Unix domain socket, an in-memory pipe, etc.) the connection can be passed to NewClientSession or NewServerSession.

conn, err := dial.Client(context.TODO(), "tcp", addr)
…
session, err := xmpp.NewClientSession(
    context.TODO(), addr, conn, false,
    xmpp.BindResource(),
    xmpp.StartTLS(true, nil),
    xmpp.SASL("", pass, sasl.ScramSha1Plus, sasl.ScramSha1, sasl.Plain),
)

If complete control over the session establishment process is needed the NegotiateSession function can be used to support custom session negotiation protocols or to customize options around the default negotiator by using the NewNegotiator function.

session, err := xmpp.NegotiateSession(
    context.TODO(), addr.Domain(), addr, conn,
    xmpp.NewNegotiator(xmpp.StreamConfig{
        Lang: "en",
        …
    }),
)

The default Negotiator and related functions use a list of StreamFeature's to negotiate the state of the session. Implementations of the most common features (StartTLS, SASL-based authentication, and resource binding) are provided. Custom stream features may be created using the StreamFeature struct. Stream features defined in this module are safe for concurrent use by multiple goroutines and for efficiency should only be created once and re-used.

Handling Stanzas

Unlike HTTP, the XMPP protocol is asynchronous, meaning that both clients and servers can accept and send requests at any time and responses are not required or may be received out of order. This is accomplished with two XML streams: an input stream and an output stream. To receive XML on the input stream, Session implements the xml.TokenReader interface defined in encoding/xml; this allows session to be wrapped with xml.NewTokenDecoder. To send XML on the output stream, Session has a TokenEncoder method that returns a token encoder that holds a lock on the output stream until it is closed. The session may also buffer writes and has a Flush method which will write any buffered XML to the underlying connection.

Writing individual XML tokens can be tedious and error prone. The stanza package contains functions and structs that aid in the construction of message, presence and info/query (IQ) elements which have special semantics in XMPP and are known as "stanzas". These can be sent with the Send, SendElement, SendIQ, and SendIQElement methods.

// Send initial presence to let the server know we want to receive messages.
_, err = session.Send(context.TODO(), stanza.WrapPresence(jid.JID{}, stanza.AvailablePresence, nil))

For SendIQ to correctly handle IQ responses, and to make the common case of polling for incoming XML on the input stream—and possibly writing to the output stream in response—easier, we need a long running goroutine. Session includes the Serve method for starting this processing.

Serve provides a Handler with access to the stream but prevents it from advancing the stream beyond the current element and always advances the stream to the end of the element when the handler returns (even if the handler did not consume the entire element).

err := session.Serve(xmpp.HandlerFunc(func(t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
    d := xml.NewTokenDecoder(t)

    // Ignore anything that's not a message.
    if start.Name.Local != "message" {
        return nil
    }

    msg := struct {
        stanza.Message
        Body string `xml:"body"`
    }{}
    err := d.DecodeElement(&msg, start)
    …
    if msg.Body != "" {
        log.Println("Got message: %q", msg.Body)
    }
}))

Be Advised

This API is unstable and subject to change.

Example (Echobot)

Code:

package main

import (
	"context"
	"crypto/tls"
	"encoding/xml"
	"io"
	"log"

	"mellium.im/sasl"
	"mellium.im/xmlstream"
	"mellium.im/xmpp"
	"mellium.im/xmpp/jid"
	"mellium.im/xmpp/stanza"
)

const (
	login = "echo@example.net"
	pass  = "just an example don't hardcode passwords"
)

// MessageBody is a message stanza that contains a body. It is normally used for
// chat messages.
type MessageBody struct {
	stanza.Message
	Body string `xml:"body"`
}

func main() {
	j := jid.MustParse(login)
	s, err := xmpp.DialClientSession(
		context.TODO(), j,
		xmpp.BindResource(),
		xmpp.StartTLS(true, &tls.Config{
			ServerName: j.Domain().String(),
		}),
		xmpp.SASL("", pass, sasl.ScramSha1Plus, sasl.ScramSha1, sasl.Plain),
	)
	if err != nil {
		log.Printf("Error establishing a session: %q", err)
		return
	}
	defer func() {
		log.Println("Closing session…")
		if err := s.Close(); err != nil {
			log.Printf("Error closing session: %q", err)
		}
		log.Println("Closing conn…")
		if err := s.Conn().Close(); err != nil {
			log.Printf("Error closing connection: %q", err)
		}
	}()

	// Send initial presence to let the server know we want to receive messages.
	err = s.Send(context.TODO(), stanza.Presence{Type: stanza.AvailablePresence}.Wrap(nil))
	if err != nil {
		log.Printf("Error sending initial presence: %q", err)
		return
	}

	s.Serve(xmpp.HandlerFunc(func(t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
		d := xml.NewTokenDecoder(t)

		// Ignore anything that's not a message. In a real system we'd want to at
		// least respond to IQs.
		if start.Name.Local != "message" {
			return nil
		}

		msg := MessageBody{}
		err = d.DecodeElement(&msg, start)
		if err != nil && err != io.EOF {
			log.Printf("Error decoding message: %q", err)
			return nil
		}

		// Don't reflect messages unless they are chat messages and actually have a
		// body.
		// In a real world situation we'd probably want to respond to IQs, at least.
		if msg.Body == "" || msg.Type != stanza.ChatMessage {
			return nil
		}

		reply := MessageBody{
			Message: stanza.Message{
				To: msg.From.Bare(),
			},
			Body: msg.Body,
		}
		log.Printf("Replying to message %q from %s with body %q", msg.ID, reply.To, reply.Body)
		err = t.Encode(reply)
		if err != nil {
			log.Printf("Error responding to message %q: %q", msg.ID, err)
		}
		return nil
	}))
}

Index

Examples

Variables

var (
	ErrInputStreamClosed  = errors.New("xmpp: attempted to read token from closed stream")
	ErrOutputStreamClosed = errors.New("xmpp: attempted to write token to closed stream")
)

Errors returned by the XMPP package.

type Handler

type Handler interface {
	HandleXMPP(t xmlstream.TokenReadEncoder, start *xml.StartElement) error
}

A Handler triggers events or responds to incoming elements in an XML stream.

type HandlerFunc

type HandlerFunc func(t xmlstream.TokenReadEncoder, start *xml.StartElement) error

The HandlerFunc type is an adapter to allow the use of ordinary functions as XMPP handlers. If f is a function with the appropriate signature, HandlerFunc(f) is a Handler that calls f.

func (HandlerFunc) HandleXMPP

func (f HandlerFunc) HandleXMPP(t xmlstream.TokenReadEncoder, start *xml.StartElement) error

HandleXMPP calls f(t, start).

type Negotiator

type Negotiator func(ctx context.Context, session *Session, data interface{}) (mask SessionState, rw io.ReadWriter, cache interface{}, err error)

Negotiator is a function that can be passed to NegotiateSession to perform custom session negotiation. This can be used for creating custom stream initialization logic that does not use XMPP feature negotiation such as the connection mechanism described in XEP-0114: Jabber Component Protocol. Normally NewClientSession or NewServerSession should be used instead.

If a Negotiator is passed into NegotiateSession it will be called repeatedly until a mask is returned with the Ready bit set. Each time Negotiator is called any bits set in the state mask that it returns will be set on the session state and any cache value that is returned will be passed back in during the next iteration. If a new io.ReadWriter is returned, it is set as the session's underlying io.ReadWriter and the internal session state (encoders, decoders, etc.) will be reset.

func NewNegotiator

func NewNegotiator(cfg StreamConfig) Negotiator

NewNegotiator creates a Negotiator that uses a collection of StreamFeatures to negotiate an XMPP client-to-server (c2s) or server-to-server (s2s) session. If StartTLS is one of the supported stream features, the Negotiator attempts to negotiate it whether the server advertises support or not.

type Session

type Session struct {
	// contains filtered or unexported fields
}

A Session represents an XMPP session comprising an input and an output XML stream.

func DialClientSession

func DialClientSession(ctx context.Context, origin jid.JID, features ...StreamFeature) (*Session, error)

DialClientSession uses a default dialer to create a TCP connection and attempts to negotiate an XMPP session over it.

If the provided context is canceled after stream negotiation is complete it has no effect on the session.

func DialServerSession

func DialServerSession(ctx context.Context, location, origin jid.JID, features ...StreamFeature) (*Session, error)

DialServerSession uses a default dialer to create a TCP connection and attempts to negotiate an XMPP session over it.

If the provided context is canceled after stream negotiation is complete it has no effect on the session.

func NegotiateSession

func NegotiateSession(ctx context.Context, location, origin jid.JID, rw io.ReadWriter, received bool, negotiate Negotiator) (*Session, error)

NegotiateSession creates an XMPP session using a custom negotiate function. Calling NegotiateSession with a nil Negotiator panics.

For more information see the Negotiator type.

func NewClientSession

func NewClientSession(ctx context.Context, origin jid.JID, rw io.ReadWriter, received bool, features ...StreamFeature) (*Session, error)

NewClientSession attempts to use an existing connection (or any io.ReadWriter) to negotiate an XMPP client-to-server session. If the provided context is canceled before stream negotiation is complete an error is returned. After stream negotiation if the context is canceled it has no effect.

func NewServerSession

func NewServerSession(ctx context.Context, location, origin jid.JID, rw io.ReadWriter, received bool, features ...StreamFeature) (*Session, error)

NewServerSession attempts to use an existing connection (or any io.ReadWriter) to negotiate an XMPP server-to-server session. If the provided context is canceled before stream negotiation is complete an error is returned. After stream negotiation if the context is canceled it has no effect.

func (*Session) Close

func (s *Session) Close() error

Close ends the output stream (by sending a closing </stream:stream> token). It does not close the underlying connection. Calling Close() multiple times will only result in one closing </stream:stream> being sent.

func (*Session) Conn

func (s *Session) Conn() net.Conn

Conn returns the Session's backing connection.

This should almost never be read from or written to, but is useful during stream negotiation for wrapping the existing connection in a new layer (eg. compression or TLS).

func (*Session) Encode

func (s *Session) Encode(v interface{}) error

Encode writes the XML encoding of v to the stream.

For more information see "encoding/xml".Encode.

func (*Session) EncodeElement

func (s *Session) EncodeElement(v interface{}, start xml.StartElement) error

EncodeElement writes the XML encoding of v to the stream, using start as the outermost tag in the encoding.

For more information see "encoding/xml".EncodeElement.

func (*Session) Feature

func (s *Session) Feature(namespace string) (data interface{}, ok bool)

Feature checks if a feature with the given namespace was advertised by the server for the current stream. If it was data will be the canonical representation of the feature as returned by the feature's Parse function.

func (*Session) LocalAddr

func (s *Session) LocalAddr() jid.JID

LocalAddr returns the Origin address for initiated connections, or the Location for received connections.

func (*Session) RemoteAddr

func (s *Session) RemoteAddr() jid.JID

RemoteAddr returns the Location address for initiated connections, or the Origin address for received connections.

func (*Session) Send

func (s *Session) Send(ctx context.Context, r xml.TokenReader) error

Send transmits the first element read from the provided token reader.

Send is safe for concurrent use by multiple goroutines.

func (*Session) SendElement

func (s *Session) SendElement(ctx context.Context, r xml.TokenReader, start xml.StartElement) error

SendElement is like Send except that it uses start as the outermost tag in the encoding.

SendElement is safe for concurrent use by multiple goroutines.

func (*Session) SendIQ

func (s *Session) SendIQ(ctx context.Context, r xml.TokenReader) (xmlstream.TokenReadCloser, error)

SendIQ is like Send except that it returns an error if the first token read from the stream is not an Info/Query (IQ) start element and blocks until a response is received.

If the input stream is not being processed (a call to Serve is not running), SendIQ will never receive a response and will block until the provided context is canceled. If the response is non-nil, it does not need to be consumed in its entirety, but it must be closed before stream processing will resume. If the IQ type does not require a response—ie. it is a result or error IQ, meaning that it is a response itself—SendIQElemnt does not block and the response is nil.

If the context is closed before the response is received, SendIQ immediately returns the context error. Any response received at a later time will not be associated with the original request but can still be handled by the Serve handler.

If an error is returned, the response will be nil; the converse is not necessarily true. SendIQ is safe for concurrent use by multiple goroutines.

func (*Session) SendIQElement

func (s *Session) SendIQElement(ctx context.Context, payload xml.TokenReader, iq stanza.IQ) (xmlstream.TokenReadCloser, error)

SendIQElement is like SendIQ except that it wraps the payload in an Info/Query (IQ) element and blocks until a response is received. For more information, see SendIQ.

SendIQElement is safe for concurrent use by multiple goroutines.

func (*Session) Serve

func (s *Session) Serve(h Handler) (err error)

Serve decodes incoming XML tokens from the connection and delegates handling them to h. If an error is returned from the handler and it is of type stanza.Error or stream.Error, the error is marshaled and sent over the XML stream. If any other error type is returned, it is marshaled as an undefined-condition StreamError. If a stream error is received while serving it is not passed to the handler. Instead, Serve unmarshals the error, closes the session, and returns it (h handles stanza level errors, the session handles stream level errors). If serve handles an incoming IQ stanza and the handler does not write a response (an IQ with the same ID and type "result" or "error"), Serve writes an error IQ with a service-unavailable payload.

If the user closes the output stream by calling Close, Serve continues until the input stream is closed by the remote entity as above, or the deadline set by SetCloseDeadline is reached in which case a timeout error is returned. Serve takes a lock on the input and output stream before calling the handler, so the handler should not close over the session or use any of its send methods or a deadlock will occur. After Serve finishes running the handler, it flushes the output stream.

func (*Session) SetCloseDeadline

func (s *Session) SetCloseDeadline(t time.Time) error

SetCloseDeadline sets a deadline for the input stream to be closed by the other side. If the input stream is not closed by the deadline, the input stream is marked as closed and any blocking calls to Serve will return an error. This is normally called just before a call to Close.

func (*Session) State

func (s *Session) State() SessionState

State returns the current state of the session. For more information, see the SessionState type.

func (*Session) TokenReader

func (s *Session) TokenReader() xmlstream.TokenReadCloser

TokenReader returns a new xmlstream.TokenReadCloser that can be used to read raw XML tokens from the session. All other reads and future calls to TokenReader will block until the Close method is called. After the TokenReadCloser has been closed, any future reads will return io.EOF.

func (*Session) TokenWriter

func (s *Session) TokenWriter() xmlstream.TokenWriteFlushCloser

TokenWriter returns a new xmlstream.TokenWriteCloser that can be used to write raw XML tokens to the session. All other writes and future calls to TokenWriter will block until the Close method is called. After the TokenWriteCloser has been closed, any future writes will return io.EOF.

type SessionState

type SessionState uint8

SessionState is a bitmask that represents the current state of an XMPP session. For a description of each bit, see the various SessionState typed constants.

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

	// Authn indicates that the session has been authenticated (probably with
	// SASL).
	Authn

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

	// Received indicates that the session was initiated by a foreign entity.
	Received

	// OutputStreamClosed indicates that the output stream has been closed with a
	// stream end tag.  When set all write operations will return an error even if
	// the underlying TCP connection is still open.
	OutputStreamClosed

	// InputStreamClosed indicates that the input stream has been closed with a
	// stream end tag. When set all read operations will return an error.
	InputStreamClosed
)

type StreamConfig

type StreamConfig struct {
	// The native language of the stream.
	Lang string

	// S2S causes the negotiator to negotiate a server-to-server (s2s) connection.
	S2S bool

	// A list of stream features to attempt to negotiate.
	Features []StreamFeature

	// If set a copy of any reads from the session will be written to TeeIn and
	// any writes to the session will be written to TeeOut (similar to the tee(1)
	// command).
	// This can be used to build an "XML console", but users should be careful
	// since this bypasses TLS and could expose passwords and other sensitve data.
	TeeIn, TeeOut io.Writer
}

StreamConfig contains options for configuring the default Negotiator.

type StreamFeature

type StreamFeature struct {
	// The XML name of the feature in the <stream:feature/> list. If a start
	// element with this name is seen while the connection is reading the features
	// list, it will trigger this StreamFeature's Parse function as a callback.
	Name xml.Name

	// Bits that are required before this feature is advertised. For instance, if
	// this feature should only be advertised after the user is authenticated we
	// might set this to "Authn" or if it should be advertised only after the
	// feature is authenticated and encrypted we might set this to "Authn|Secure".
	Necessary SessionState

	// Bits that must be off for this feature to be advertised. For instance, if
	// this feature should only be advertised before the connection is
	// authenticated (eg. if the feature performs authentication itself), we might
	// set this to "Authn".
	Prohibited SessionState

	// Used to send the feature in a features list for server connections. The
	// start element will have a name that matches the features name and should be
	// used as the outermost tag in the stream (but also may be ignored). List
	// implementations that call e.EncodeToken directly need to call e.Flush when
	// finished to ensure that the XML is written to the underlying writer.
	List func(ctx context.Context, e xmlstream.TokenWriter, start xml.StartElement) (req bool, err error)

	// Used to parse the feature that begins with the given xml start element
	// (which should have a Name that matches this stream feature's Name).
	// Returns whether or not the feature is required, and any data that will be
	// needed if the feature is selected for negotiation (eg. the list of
	// mechanisms if the feature was SASL authentication).
	Parse func(ctx context.Context, r xml.TokenReader, start *xml.StartElement) (req bool, data interface{}, err error)

	// A function that will take over the session temporarily while negotiating
	// the feature. The "mask" SessionState represents the state bits that should
	// be flipped after negotiation of the feature is complete. For instance, if
	// this feature creates a security layer (such as TLS) and performs
	// authentication, mask would be set to Authn|Secure, but if it does not
	// authenticate the connection it would just return Secure. If negotiate
	// returns a new io.ReadWriter (probably wrapping the old session.Conn()) the
	// stream will be restarted automatically after Negotiate returns using the
	// new ReadWriter. If this is an initiated connection and the features List
	// call returned a value, that value is passed to the data parameter when
	// Negotiate is called. For instance, in the case of compression this data
	// parameter might be the list of supported algorithms as a slice of strings
	// (or in whatever format the feature implementation has decided upon).
	Negotiate func(ctx context.Context, session *Session, data interface{}) (mask SessionState, rw io.ReadWriter, err error)
}

A StreamFeature represents a feature that may be selected during stream negotiation, eg. STARTTLS, compression, and SASL authentication are all stream features. Features should be stateless as they may be reused between connection attempts, however, a method for passing state between features exists on the Parse and Negotiate functions.

func BindCustom

func BindCustom(server func(jid.JID, string) (jid.JID, error)) StreamFeature

BindCustom is identical to BindResource when used on a client session, but for server sessions the server function is called to generate the JID that should be returned to the client. If server is nil, BindCustom is identical to BindResource.

The server function is passed the current client JID and the resource requested by the client (or an empty string if a specific resource was not requested). Resources generated by the server function should be random to prevent certain security issues related to guessing resourceparts.

func BindResource

func BindResource() StreamFeature

BindResource is a stream feature that can be used for binding a resource (the name by which an individual client can be addressed) to the stream.

Resource binding is the final feature negotiated when setting up a new session and is required to allow communication with other clients and servers in the network. Resource binding is mandatory-to-negotiate.

If used on a server connection, BindResource generates and assigns random resourceparts, however this default is subject to change.

func SASL

func SASL(identity, password string, mechanisms ...sasl.Mechanism) StreamFeature

SASL returns a stream feature for performing authentication using the Simple Authentication and Security Layer (SASL) as defined in RFC 4422. It panics if no mechanisms are specified. The order in which mechanisms are specified will be the preferred order, so stronger mechanisms should be listed first.

Identity is used when a user wants to act on behalf of another user. For instance, an admin might want to log in as another user to help them troubleshoot an issue. Normally it is left blank and the localpart of the Origin JID is used.

func StartTLS

func StartTLS(required bool, cfg *tls.Config) StreamFeature

StartTLS returns a new stream feature that can be used for negotiating TLS. If cfg is nil, a default configuration is used that uses the domainpart of the sessions local address as the ServerName.

Package Files

BUGs

  • SASL feature does not have security layer byte precision.

  • STARTTLS feature does not have security layer byte precision.

Documentation was rendered with GOOS=linux and GOARCH=amd64.

Jump to identifier

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to identifier