go-xmpp/component.go

221 lines
6.0 KiB
Go
Raw Permalink Normal View History

package xmpp // import "gosrc.io/xmpp"
2018-01-11 13:15:54 -08:00
import (
"crypto/sha1"
"encoding/hex"
2018-01-11 14:00:59 -08:00
"encoding/xml"
"errors"
2018-01-11 13:15:54 -08:00
"fmt"
2018-01-11 14:00:59 -08:00
"io"
2018-01-11 13:15:54 -08:00
"net"
"time"
)
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
type ComponentOptions struct {
// =================================
// Component Connection Info
// Domain is the XMPP server subdomain that the component will handle
Domain string
// Secret is the "password" used by the XMPP server to secure component access
Secret string
// Address is the XMPP Host and port to connect to. Host is of
// the form 'serverhost:port' i.e "localhost:8888"
Address string
// =================================
// Component discovery
// Component human readable name, that will be shown in XMPP discovery
Name string
// Typical categories and types: https://xmpp.org/registrar/disco-categories.html
Category string
Type string
// =================================
// Communication with developer client / StreamManager
// Packet channel
RecvChannel chan Packet
// Track and broadcast connection state
EventManager
}
2018-01-11 13:15:54 -08:00
// Component implements an XMPP extension allowing to extend XMPP server
// using external components. Component specifications are defined
// in XEP-0114, XEP-0355 and XEP-0356.
type Component struct {
ComponentOptions
2018-01-11 13:15:54 -08:00
// TCP level connection
conn net.Conn
2018-01-11 14:00:59 -08:00
// read / write
socketProxy io.ReadWriter // TODO
2018-01-11 14:00:59 -08:00
decoder *xml.Decoder
2018-01-11 13:15:54 -08:00
}
func NewComponent(opts ComponentOptions) (*Component, error) {
2019-06-09 03:48:22 -07:00
c := Component{ComponentOptions: opts}
// Create a default channel that developers can override
c.RecvChannel = make(chan Packet)
2019-06-09 03:48:22 -07:00
return &c, nil
}
2018-01-25 14:16:55 -08:00
// Connect triggers component connection to XMPP server component port.
// TODO: Failed handshake should be a permanent error
func (c *Component) Connect() error {
2018-01-11 13:15:54 -08:00
var conn net.Conn
var err error
if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil {
2018-01-11 13:15:54 -08:00
return err
}
2018-01-11 14:00:59 -08:00
c.conn = conn
2018-01-11 13:15:54 -08:00
2018-01-12 09:14:41 -08:00
// 1. Send stream open tag
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, NSComponent, NSStream); err != nil {
2018-01-12 09:14:41 -08:00
return errors.New("cannot send stream open " + err.Error())
2018-01-11 14:00:59 -08:00
}
c.decoder = xml.NewDecoder(conn)
2018-01-12 09:14:41 -08:00
// 2. Initialize xml decoder and extract streamID from reply
2018-01-11 14:00:59 -08:00
streamId, err := initDecoder(c.decoder)
if err != nil {
2018-01-12 09:14:41 -08:00
return errors.New("cannot init decoder " + err.Error())
2018-01-11 13:15:54 -08:00
}
2018-01-12 09:14:41 -08:00
// 3. Authentication
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
return errors.New("cannot send handshake " + err.Error())
2018-01-11 14:00:59 -08:00
}
2018-01-12 09:14:41 -08:00
// 4. Check server response for authentication
2018-01-13 09:50:17 -08:00
val, err := next(c.decoder)
2018-01-11 14:00:59 -08:00
if err != nil {
return err
}
switch v := val.(type) {
case StreamError:
2018-01-12 09:14:41 -08:00
return errors.New("handshake failed " + v.Error.Local)
case Handshake:
// Start the receiver go routine
go c.recv()
2018-01-12 09:14:41 -08:00
return nil
2018-01-11 14:00:59 -08:00
default:
return errors.New("expecting handshake result, got " + v.Name())
2018-01-11 14:00:59 -08:00
}
2018-01-12 09:14:41 -08:00
}
2018-01-11 14:00:59 -08:00
func (c *Component) 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 *Component) 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 *Component) Recv() <-chan Packet {
return c.RecvChannel
}
func (c *Component) recv() (err error) {
for {
val, err := next(c.decoder)
if err != nil {
c.updateState(StateDisconnected)
return err
}
// Handle stream errors
switch p := val.(type) {
case StreamError:
c.RecvChannel <- val
close(c.RecvChannel)
c.streamError(p.Error.Local, p.Text)
return errors.New("stream error: " + p.Error.Local)
}
c.RecvChannel <- val
}
2018-01-12 10:08:47 -08:00
}
2018-01-26 00:55:39 -08:00
// Send marshalls XMPP stanza and sends it to the server.
2018-01-17 09:47:34 -08:00
func (c *Component) 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
}
2018-01-26 00:55:39 -08: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 component. It is up to the user of this method to
// carefully craft the XML content to produce valid XMPP.
func (c *Component) SendRaw(packet string) error {
2019-06-09 03:21:20 -07:00
var err error
_, err = fmt.Fprintf(c.conn, packet)
return err
2018-01-26 00:55:39 -08:00
}
2018-01-25 14:16:55 -08:00
// handshake generates an authentication token based on StreamID and shared secret.
func (c *Component) handshake(streamId string) string {
// 1. Concatenate the Stream ID received from the server with the shared secret.
concatStr := streamId + c.Secret
// 2. Hash the concatenated string according to the SHA1 algorithm, i.e., SHA1( concat (sid, password)).
h := sha1.New()
h.Write([]byte(concatStr))
hash := h.Sum(nil)
// 3. Ensure that the hash output is in hexadecimal format, not binary or base64.
// 4. Convert the hash output to all lowercase characters.
encodedStr := hex.EncodeToString(hash)
return encodedStr
}
2018-01-12 09:14:41 -08:00
// ============================================================================
2018-01-25 14:16:55 -08:00
// Handshake Stanza
2018-01-12 09:14:41 -08:00
2018-01-25 14:16:55 -08:00
// Handshake is a stanza used by XMPP components to authenticate on XMPP
// component port.
2018-01-12 09:14:41 -08:00
type Handshake struct {
XMLName xml.Name `xml:"jabber:component:accept handshake"`
2018-01-25 14:16:55 -08:00
// TODO Add handshake value with test for proper serialization
// Value string `xml:",innerxml"`
2018-01-11 13:15:54 -08:00
}
2018-01-13 09:50:17 -08:00
func (Handshake) Name() string {
return "component:handshake"
}
2018-01-25 14:16:55 -08:00
// Handshake decoding wrapper
2018-01-13 09:50:17 -08:00
type handshakeDecoder struct{}
var handshake handshakeDecoder
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
var packet Handshake
err := p.DecodeElement(&packet, &se)
return packet, err
}
2019-06-09 04:02:58 -07:00
/*
TODO: Add support for discovery management directly in component
TODO: Support multiple identities on disco info
TODO: Support returning features on disco info
*/