go-xmpp/client.go

253 lines
6.9 KiB
Go
Raw Normal View History

package xmpp // import "gosrc.io/xmpp"
import (
"encoding/xml"
"errors"
"fmt"
"net"
"strings"
2016-02-17 04:45:39 -08:00
"time"
)
//=============================================================================
// EventManager
// ConnState represents the current connection state.
type ConnState = uint8
// This is a the list of events happening on the connection that the
// client can be notified about.
const (
StateDisconnected ConnState = iota
StateConnected
StateSessionEstablished
StateStreamError
)
// Event is a structure use to convey event changes related to client state. This
// is for example used to notify the client when the client get disconnected.
type Event struct {
State ConnState
Description string
StreamError string
}
// EventHandler is use to pass events about state of the connection to
// client implementation.
type EventHandler func(Event)
type EventManager struct {
// Store current state
CurrentState ConnState
// Callback used to propagate connection state changes
Handler EventHandler
}
func (em EventManager) updateState(state ConnState) {
em.CurrentState = state
if em.Handler != nil {
em.Handler(Event{State: em.CurrentState})
}
}
func (em EventManager) streamError(error, desc string) {
em.CurrentState = StateStreamError
if em.Handler != nil {
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
}
}
// Client
// ============================================================================
2018-02-13 13:07:15 -08:00
// Client is the main structure used to connect as a client on an XMPP
2016-02-15 02:05:44 -08:00
// server.
type Client struct {
// Store user defined options and states
2018-09-26 07:25:04 -07:00
config Config
// 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
conn net.Conn
// Packet channel
RecvChannel chan Packet
// Track and broadcast connection state
EventManager
}
/*
Setting up the client / Checking the parameters
*/
2018-09-26 07:25:04 -07:00
// NewClient generates a new XMPP client, based on Config passed as parameters.
2018-09-23 09:43:46 -07:00
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
2016-02-13 08:01:06 -08:00
// Default the port to 5222.
2018-09-26 07:25:04 -07:00
// TODO: better config checks
func NewClient(config Config) (c *Client, err error) {
// TODO: If option address is nil, use the Jid domain to compose the address
2018-09-26 07:25:04 -07:00
if config.Address, err = checkAddress(config.Address); err != nil {
return nil, NewConnError(err, true)
}
2018-09-26 07:25:04 -07:00
if config.Password == "" {
err = errors.New("missing password")
return nil, NewConnError(err, true)
}
// Parse JID
if config.parsedJid, err = NewJid(config.Jid); err != nil {
err = errors.New("missing jid")
return nil, NewConnError(err, true)
}
c = new(Client)
c.config = config
2018-09-26 07:25:04 -07:00
if c.config.ConnectTimeout == 0 {
c.config.ConnectTimeout = 15 // 15 second as default
2016-02-17 04:45:39 -08:00
}
2019-06-09 03:48:22 -07:00
// Create a default channel that developers can override
c.RecvChannel = make(chan Packet)
return
}
2018-09-23 09:43:46 -07:00
// TODO Pass JID to be able to add default address based on JID, if addr is empty
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.
func (c *Client) Connect() error {
var err error
2016-02-17 04:45:39 -08:00
c.conn, err = net.DialTimeout("tcp", c.config.Address, time.Duration(c.config.ConnectTimeout)*time.Second)
2016-02-17 04:45:39 -08:00
if err != nil {
return err
}
c.updateState(StateConnected)
// Client is ok, we now open XMPP session
2018-09-26 07:25:04 -07:00
if c.conn, c.Session, err = NewSession(c.conn, c.config); err != nil {
return err
}
c.updateState(StateSessionEstablished)
// 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 ?
fmt.Fprintf(c.Session.socketProxy, "<presence/>")
// Start the keepalive go routine
keepaliveQuit := make(chan struct{})
go keepalive(c.conn, keepaliveQuit)
// Start the receiver go routine
go c.recv(keepaliveQuit)
return err
}
func (c *Client) Disconnect() {
_ = c.SendRaw("</stream:stream>")
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
_ = c.conn.Close()
}
func (c *Client) SetHandler(handler EventHandler) {
c.Handler = handler
}
// Recv abstracts receiving preparsed XMPP packets from a channel.
// Channel allow client to receive / dispatch packets in for range loop.
// TODO: Deprecate this function in favor of reading directly from the RecvChannel ?
func (c *Client) Recv() <-chan Packet {
return c.RecvChannel
}
// Send marshals XMPP stanza and sends it to the server.
2018-01-26 00:55:39 -08:00
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 {
var err error
_, err = fmt.Fprintf(c.Session.socketProxy, packet)
return err
}
// ============================================================================
// Go routines
// Loop: Receive data from server
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
for {
val, err := next(c.Session.decoder)
if err != nil {
close(keepaliveQuit)
c.updateState(StateDisconnected)
return err
}
// Handle stream errors
switch packet := val.(type) {
case StreamError:
c.RecvChannel <- val
close(c.RecvChannel)
c.streamError(packet.Error.Local, packet.Text)
return errors.New("stream error: " + packet.Error.Local)
}
c.RecvChannel <- val
}
}
// Loop: send whitespace keepalive to server
// This is use to keep the connection open, but also to detect connection loss
// and trigger proper client connection shutdown.
func keepalive(conn net.Conn, quit <-chan struct{}) {
// TODO: Make keepalive interval configurable
ticker := time.NewTicker(30 * time.Second)
for {
select {
case <-ticker.C:
if n, err := fmt.Fprintf(conn, "\n"); err != nil || n != 1 {
// When keep alive fails, we force close the connection. In all cases, the recv will also fail.
ticker.Stop()
_ = conn.Close()
return
}
case <-quit:
ticker.Stop()
return
}
}
}