2019-06-18 17:28:30 +03:00
|
|
|
package xmpp
|
2016-01-06 18:51:12 +03:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/xml"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-09-27 17:30:12 +03:00
|
|
|
"io"
|
2016-01-06 18:51:12 +03:00
|
|
|
"net"
|
2016-02-17 15:45:39 +03:00
|
|
|
"time"
|
2019-06-26 18:14:52 +03:00
|
|
|
|
|
|
|
"gosrc.io/xmpp/stanza"
|
2016-01-06 18:51:12 +03:00
|
|
|
)
|
|
|
|
|
2019-06-06 12:58:50 +03:00
|
|
|
//=============================================================================
|
2019-06-08 19:09:22 +03:00
|
|
|
// EventManager
|
2018-09-26 18:26:14 +03:00
|
|
|
|
2019-06-06 12:58:50 +03:00
|
|
|
// ConnState represents the current connection state.
|
|
|
|
type ConnState = uint8
|
2018-09-26 18:26:14 +03:00
|
|
|
|
2019-06-06 12:58:50 +03:00
|
|
|
// This is a the list of events happening on the connection that the
|
|
|
|
// client can be notified about.
|
|
|
|
const (
|
|
|
|
StateDisconnected ConnState = iota
|
|
|
|
StateConnected
|
|
|
|
StateSessionEstablished
|
2019-06-08 12:15:51 +03:00
|
|
|
StateStreamError
|
2019-06-06 12:58:50 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
2019-06-08 12:15:51 +03:00
|
|
|
StreamError string
|
2019-07-31 19:47:30 +03:00
|
|
|
SMState SMState
|
|
|
|
}
|
|
|
|
|
|
|
|
// SMState holds Stream Management information regarding the session that can be
|
|
|
|
// used to resume session after disconnect
|
|
|
|
type SMState struct {
|
|
|
|
// Stream Management ID
|
|
|
|
Id string
|
|
|
|
// Inbound stanza count
|
|
|
|
Inbound uint
|
|
|
|
// TODO Store location for IP affinity
|
|
|
|
// TODO Store max and timestamp, to check if we should retry resumption or not
|
2018-09-26 18:26:14 +03:00
|
|
|
}
|
|
|
|
|
2019-06-06 12:58:50 +03:00
|
|
|
// 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
|
2018-09-26 18:26:14 +03:00
|
|
|
}
|
|
|
|
|
2019-06-06 12:58:50 +03:00
|
|
|
func (em EventManager) updateState(state ConnState) {
|
|
|
|
em.CurrentState = state
|
|
|
|
if em.Handler != nil {
|
|
|
|
em.Handler(Event{State: em.CurrentState})
|
|
|
|
}
|
2018-09-26 18:26:14 +03:00
|
|
|
}
|
|
|
|
|
2019-07-31 19:47:30 +03:00
|
|
|
func (em EventManager) disconnected(state SMState) {
|
|
|
|
em.CurrentState = StateDisconnected
|
|
|
|
if em.Handler != nil {
|
|
|
|
em.Handler(Event{State: em.CurrentState, SMState: state})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-08 12:15:51 +03:00
|
|
|
func (em EventManager) streamError(error, desc string) {
|
|
|
|
em.CurrentState = StateStreamError
|
|
|
|
if em.Handler != nil {
|
|
|
|
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-26 18:26:14 +03:00
|
|
|
// Client
|
|
|
|
// ============================================================================
|
|
|
|
|
2018-02-14 00:07:15 +03:00
|
|
|
// Client is the main structure used to connect as a client on an XMPP
|
2016-02-15 13:05:44 +03:00
|
|
|
// server.
|
2016-01-06 18:51:12 +03:00
|
|
|
type Client struct {
|
2019-06-06 12:58:50 +03:00
|
|
|
// Store user defined options and states
|
2018-09-26 17:25:04 +03:00
|
|
|
config Config
|
2016-01-06 18:51:12 +03:00
|
|
|
// Session gather data that can be accessed by users of this library
|
|
|
|
Session *Session
|
2017-10-21 14:58:58 +03:00
|
|
|
// TCP level connection / can be replaced by a TLS session after starttls
|
2019-10-06 20:37:56 +03:00
|
|
|
transport Transport
|
2019-06-18 13:34:25 +03:00
|
|
|
// Router is used to dispatch packets
|
|
|
|
router *Router
|
2019-06-06 12:58:50 +03:00
|
|
|
// Track and broadcast connection state
|
|
|
|
EventManager
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Setting up the client / Checking the parameters
|
|
|
|
*/
|
|
|
|
|
2018-09-26 17:25:04 +03:00
|
|
|
// NewClient generates a new XMPP client, based on Config passed as parameters.
|
2018-09-23 19:43:46 +03: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 19:01:06 +03:00
|
|
|
// Default the port to 5222.
|
2019-06-18 13:34:25 +03:00
|
|
|
func NewClient(config Config, r *Router) (c *Client, err error) {
|
2019-06-26 10:04:03 +03:00
|
|
|
// Parse JID
|
|
|
|
if config.parsedJid, err = NewJid(config.Jid); err != nil {
|
|
|
|
err = errors.New("missing jid")
|
2019-06-07 16:23:23 +03:00
|
|
|
return nil, NewConnError(err, true)
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|
2019-06-26 13:29:39 +03:00
|
|
|
|
2019-10-01 11:59:55 +03:00
|
|
|
if config.Credential.secret == "" {
|
|
|
|
err = errors.New("missing credential")
|
2019-06-07 16:23:23 +03:00
|
|
|
return nil, NewConnError(err, true)
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|
2019-06-26 13:29:39 +03:00
|
|
|
|
2019-07-28 01:19:32 +03:00
|
|
|
// Fallback to jid domain
|
2019-06-26 10:04:03 +03:00
|
|
|
if config.Address == "" {
|
|
|
|
config.Address = config.parsedJid.Domain
|
2019-07-27 19:22:04 +03:00
|
|
|
|
2019-07-28 01:19:32 +03:00
|
|
|
// Fetch SRV DNS-Entries
|
2019-07-18 02:27:11 +03:00
|
|
|
_, srvEntries, err := net.LookupSRV("xmpp-client", "tcp", config.parsedJid.Domain)
|
2019-07-27 19:22:04 +03:00
|
|
|
|
2019-07-18 02:27:11 +03:00
|
|
|
if err == nil && len(srvEntries) > 0 {
|
2019-07-28 01:19:32 +03:00
|
|
|
// If we found matching DNS records, use the entry with highest weight
|
2019-07-18 02:27:11 +03:00
|
|
|
bestSrv := srvEntries[0]
|
|
|
|
for _, srv := range srvEntries {
|
|
|
|
if srv.Priority <= bestSrv.Priority && srv.Weight >= bestSrv.Weight {
|
|
|
|
bestSrv = srv
|
2019-07-27 19:22:04 +03:00
|
|
|
config.Address = ensurePort(srv.Target, int(srv.Port))
|
2019-07-18 02:27:11 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-06-26 10:04:03 +03:00
|
|
|
}
|
2019-06-26 13:29:39 +03:00
|
|
|
config.Address = ensurePort(config.Address, 5222)
|
2016-01-06 18:51:12 +03:00
|
|
|
|
2019-06-07 13:16:58 +03:00
|
|
|
c = new(Client)
|
|
|
|
c.config = config
|
2019-06-18 13:34:25 +03:00
|
|
|
c.router = r
|
2019-10-06 20:37:56 +03:00
|
|
|
c.transport = &XMPPTransport{}
|
2019-06-07 13:16:58 +03:00
|
|
|
|
2018-09-26 17:25:04 +03:00
|
|
|
if c.config.ConnectTimeout == 0 {
|
|
|
|
c.config.ConnectTimeout = 15 // 15 second as default
|
2016-02-17 15:45:39 +03:00
|
|
|
}
|
2019-06-06 12:58:50 +03:00
|
|
|
|
2016-01-06 18:51:12 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-02-13 19:01:06 +03:00
|
|
|
// Connect triggers actual TCP connection, based on previously defined parameters.
|
2019-07-31 19:47:30 +03:00
|
|
|
// Connect simply triggers resumption, with an empty session state.
|
2019-06-08 19:09:22 +03:00
|
|
|
func (c *Client) Connect() error {
|
2019-07-31 19:47:30 +03:00
|
|
|
var state SMState
|
|
|
|
return c.Resume(state)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
|
|
|
// state.
|
|
|
|
func (c *Client) Resume(state SMState) error {
|
2016-01-06 18:51:12 +03:00
|
|
|
var err error
|
2016-02-17 15:45:39 +03:00
|
|
|
|
2019-10-06 20:37:56 +03:00
|
|
|
err = c.transport.Connect(c.config.Address, c.config)
|
2016-02-17 15:45:39 +03:00
|
|
|
if err != nil {
|
2019-06-08 19:09:22 +03:00
|
|
|
return err
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|
2019-06-06 12:58:50 +03:00
|
|
|
c.updateState(StateConnected)
|
2016-01-06 18:51:12 +03:00
|
|
|
|
2019-06-08 19:52:19 +03:00
|
|
|
// Client is ok, we now open XMPP session
|
2019-10-06 20:37:56 +03:00
|
|
|
if c.Session, err = NewSession(c.transport, c.config, state); err != nil {
|
2019-06-08 19:09:22 +03:00
|
|
|
return err
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|
2019-06-06 12:58:50 +03:00
|
|
|
c.updateState(StateSessionEstablished)
|
2016-01-06 18:51:12 +03:00
|
|
|
|
2019-07-31 19:47:30 +03:00
|
|
|
// Start the keepalive go routine
|
|
|
|
keepaliveQuit := make(chan struct{})
|
2019-10-06 20:37:56 +03:00
|
|
|
go keepalive(c.transport, keepaliveQuit)
|
2019-07-31 19:47:30 +03:00
|
|
|
// Start the receiver go routine
|
|
|
|
state = c.Session.SMState
|
|
|
|
go c.recv(state, keepaliveQuit)
|
|
|
|
|
2016-01-06 18:51:12 +03:00
|
|
|
// 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 19:08:51 +03: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 ?
|
2019-06-29 11:45:25 +03:00
|
|
|
fmt.Fprintf(c.Session.streamLogger, "<presence/>")
|
2016-01-06 18:51:12 +03:00
|
|
|
|
2019-06-08 19:09:22 +03:00
|
|
|
return err
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|
|
|
|
|
2019-06-06 12:58:50 +03:00
|
|
|
func (c *Client) Disconnect() {
|
|
|
|
_ = c.SendRaw("</stream:stream>")
|
|
|
|
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
2019-10-06 20:37:56 +03:00
|
|
|
conn := c.transport
|
2019-09-06 11:28:49 +03:00
|
|
|
if conn != nil {
|
|
|
|
_ = conn.Close()
|
|
|
|
}
|
2019-06-06 12:58:50 +03:00
|
|
|
}
|
|
|
|
|
2019-06-08 19:09:22 +03:00
|
|
|
func (c *Client) SetHandler(handler EventHandler) {
|
|
|
|
c.Handler = handler
|
|
|
|
}
|
|
|
|
|
2019-06-11 16:29:08 +03:00
|
|
|
// Send marshals XMPP stanza and sends it to the server.
|
2019-06-26 18:14:52 +03:00
|
|
|
func (c *Client) Send(packet stanza.Packet) error {
|
2019-10-06 20:37:56 +03:00
|
|
|
conn := c.transport
|
2019-06-26 16:58:42 +03:00
|
|
|
if conn == nil {
|
|
|
|
return errors.New("client is not connected")
|
|
|
|
}
|
|
|
|
|
2018-01-26 11:55:39 +03:00
|
|
|
data, err := xml.Marshal(packet)
|
|
|
|
if err != nil {
|
|
|
|
return errors.New("cannot marshal packet " + err.Error())
|
|
|
|
}
|
|
|
|
|
2019-09-27 17:30:12 +03:00
|
|
|
return c.sendWithWriter(c.Session.streamLogger, data)
|
2018-01-26 11:55:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2019-10-06 20:37:56 +03:00
|
|
|
conn := c.transport
|
2019-06-26 16:58:42 +03:00
|
|
|
if conn == nil {
|
|
|
|
return errors.New("client is not connected")
|
|
|
|
}
|
|
|
|
|
2019-09-27 17:30:12 +03:00
|
|
|
return c.sendWithWriter(c.Session.streamLogger, []byte(packet))
|
2019-06-29 11:45:25 +03:00
|
|
|
}
|
|
|
|
|
2019-09-27 17:30:12 +03:00
|
|
|
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
2019-06-06 12:58:50 +03:00
|
|
|
var err error
|
2019-09-27 17:30:12 +03:00
|
|
|
_, err = writer.Write(packet)
|
2019-06-06 12:58:50 +03:00
|
|
|
return err
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|
|
|
|
|
2019-06-11 16:29:08 +03:00
|
|
|
// ============================================================================
|
|
|
|
// Go routines
|
|
|
|
|
|
|
|
// Loop: Receive data from server
|
2019-07-31 19:47:30 +03:00
|
|
|
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) {
|
2019-06-11 16:29:08 +03:00
|
|
|
for {
|
2019-06-26 18:14:52 +03:00
|
|
|
val, err := stanza.NextPacket(c.Session.decoder)
|
2019-06-11 16:29:08 +03:00
|
|
|
if err != nil {
|
|
|
|
close(keepaliveQuit)
|
2019-07-31 19:47:30 +03:00
|
|
|
c.disconnected(state)
|
2019-06-11 16:29:08 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle stream errors
|
|
|
|
switch packet := val.(type) {
|
2019-06-26 18:14:52 +03:00
|
|
|
case stanza.StreamError:
|
2019-06-24 12:13:25 +03:00
|
|
|
c.router.route(c, val)
|
2019-06-18 13:34:25 +03:00
|
|
|
close(keepaliveQuit)
|
2019-06-11 16:29:08 +03:00
|
|
|
c.streamError(packet.Error.Local, packet.Text)
|
|
|
|
return errors.New("stream error: " + packet.Error.Local)
|
2019-07-31 19:47:30 +03:00
|
|
|
// Process Stream management nonzas
|
|
|
|
case stanza.SMRequest:
|
|
|
|
answer := stanza.SMAnswer{XMLName: xml.Name{
|
|
|
|
Space: stanza.NSStreamManagement,
|
|
|
|
Local: "a",
|
|
|
|
}, H: state.Inbound}
|
|
|
|
c.Send(answer)
|
|
|
|
default:
|
|
|
|
state.Inbound++
|
2019-06-11 16:29:08 +03:00
|
|
|
}
|
|
|
|
|
2019-06-24 12:13:25 +03:00
|
|
|
c.router.route(c, val)
|
2019-06-11 16:29:08 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2019-10-06 20:37:56 +03:00
|
|
|
func keepalive(transport Transport, quit <-chan struct{}) {
|
2019-06-11 16:29:08 +03:00
|
|
|
// TODO: Make keepalive interval configurable
|
|
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
2019-10-06 20:37:56 +03:00
|
|
|
if n, err := fmt.Fprintf(transport, "\n"); err != nil || n != 1 {
|
|
|
|
// When keep alive fails, we force close the transportection. In all cases, the recv will also fail.
|
2019-06-11 16:29:08 +03:00
|
|
|
ticker.Stop()
|
2019-10-06 20:37:56 +03:00
|
|
|
_ = transport.Close()
|
2019-06-11 16:29:08 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
case <-quit:
|
|
|
|
ticker.Stop()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2016-01-06 18:51:12 +03:00
|
|
|
}
|