92329b48e6
XMPP and WebSocket transports require different open and close stanzas. To handle this the responsibility handling those and creating the XML decoder is moved to the Transport.
239 lines
5.4 KiB
Go
239 lines
5.4 KiB
Go
package xmpp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"gosrc.io/xmpp/stanza"
|
|
)
|
|
|
|
type Session struct {
|
|
// Session info
|
|
BindJid string // Jabber ID as provided by XMPP server
|
|
StreamId string
|
|
SMState SMState
|
|
Features stanza.StreamFeatures
|
|
TlsEnabled bool
|
|
lastPacketId int
|
|
|
|
// read / write
|
|
transport Transport
|
|
|
|
// error management
|
|
err error
|
|
}
|
|
|
|
func NewSession(transport Transport, o Config, state SMState) (*Session, error) {
|
|
s := new(Session)
|
|
s.transport = transport
|
|
s.SMState = state
|
|
s.init(o)
|
|
|
|
s.startTlsIfSupported(o)
|
|
|
|
if s.err != nil {
|
|
return nil, NewConnError(s.err, true)
|
|
}
|
|
|
|
if !transport.IsSecure() && !o.Insecure {
|
|
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
|
return nil, NewConnError(err, true)
|
|
}
|
|
|
|
if s.TlsEnabled {
|
|
s.reset(o)
|
|
}
|
|
|
|
// auth
|
|
s.auth(o)
|
|
s.reset(o)
|
|
|
|
// attempt resumption
|
|
if s.resume(o) {
|
|
return s, s.err
|
|
}
|
|
|
|
// otherwise, bind resource and 'start' XMPP session
|
|
s.bind(o)
|
|
s.rfc3921Session(o)
|
|
|
|
// Enable stream management if supported
|
|
s.EnableStreamManagement(o)
|
|
|
|
return s, s.err
|
|
}
|
|
|
|
func (s *Session) PacketId() string {
|
|
s.lastPacketId++
|
|
return fmt.Sprintf("%x", s.lastPacketId)
|
|
}
|
|
|
|
func (s *Session) init(o Config) {
|
|
s.Features = s.open(o.parsedJid.Domain)
|
|
}
|
|
|
|
func (s *Session) reset(o Config) {
|
|
if s.err != nil {
|
|
return
|
|
}
|
|
s.Features = s.open(o.parsedJid.Domain)
|
|
}
|
|
|
|
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
|
// extract stream features
|
|
if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil {
|
|
s.err = errors.New("stream open decode features: " + s.err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *Session) startTlsIfSupported(o Config) {
|
|
if s.err != nil {
|
|
return
|
|
}
|
|
|
|
if !s.transport.DoesStartTLS() {
|
|
if !o.Insecure {
|
|
s.err = errors.New("Transport does not support starttls")
|
|
}
|
|
return
|
|
}
|
|
|
|
if _, ok := s.Features.DoesStartTLS(); ok {
|
|
fmt.Fprintf(s.transport, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
|
|
|
var k stanza.TLSProceed
|
|
if s.err = s.transport.GetDecoder().DecodeElement(&k, nil); s.err != nil {
|
|
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
|
return
|
|
}
|
|
|
|
s.err = s.transport.StartTLS()
|
|
|
|
if s.err == nil {
|
|
s.TlsEnabled = true
|
|
}
|
|
return
|
|
}
|
|
|
|
// If we do not allow cleartext connections, make it explicit that server do not support starttls
|
|
if !o.Insecure {
|
|
s.err = errors.New("XMPP server does not advertise support for starttls")
|
|
}
|
|
}
|
|
|
|
func (s *Session) auth(o Config) {
|
|
if s.err != nil {
|
|
return
|
|
}
|
|
|
|
s.err = authSASL(s.transport, s.transport.GetDecoder(), s.Features, o.parsedJid.Node, o.Credential)
|
|
}
|
|
|
|
// Attempt to resume session using stream management
|
|
func (s *Session) resume(o Config) bool {
|
|
if !s.Features.DoesStreamManagement() {
|
|
return false
|
|
}
|
|
if s.SMState.Id == "" {
|
|
return false
|
|
}
|
|
|
|
fmt.Fprintf(s.transport, "<resume xmlns='%s' h='%d' previd='%s'/>",
|
|
stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id)
|
|
|
|
var packet stanza.Packet
|
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
|
if s.err == nil {
|
|
switch p := packet.(type) {
|
|
case stanza.SMResumed:
|
|
if p.PrevId != s.SMState.Id {
|
|
s.err = errors.New("session resumption: mismatched id")
|
|
s.SMState = SMState{}
|
|
return false
|
|
}
|
|
return true
|
|
case stanza.SMFailed:
|
|
default:
|
|
s.err = errors.New("unexpected reply to SM resume")
|
|
}
|
|
}
|
|
s.SMState = SMState{}
|
|
return false
|
|
}
|
|
|
|
func (s *Session) bind(o Config) {
|
|
if s.err != nil {
|
|
return
|
|
}
|
|
|
|
// Send IQ message asking to bind to the local user name.
|
|
var resource = o.parsedJid.Resource
|
|
if resource != "" {
|
|
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
|
s.PacketId(), stanza.NSBind, resource)
|
|
} else {
|
|
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
|
}
|
|
|
|
var iq stanza.IQ
|
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
|
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
|
return
|
|
}
|
|
|
|
// TODO Check all elements
|
|
switch payload := iq.Payload.(type) {
|
|
case *stanza.Bind:
|
|
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
|
|
default:
|
|
s.err = errors.New("iq bind result missing")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// After the bind, if the session is not optional (as per old RFC 3921), we send the session open iq.
|
|
func (s *Session) rfc3921Session(o Config) {
|
|
if s.err != nil {
|
|
return
|
|
}
|
|
|
|
var iq stanza.IQ
|
|
// We only negotiate session binding if it is mandatory, we skip it when optional.
|
|
if !s.Features.Session.IsOptional() {
|
|
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
|
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
|
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enable stream management, with session resumption, if supported.
|
|
func (s *Session) EnableStreamManagement(o Config) {
|
|
if s.err != nil {
|
|
return
|
|
}
|
|
if !s.Features.DoesStreamManagement() {
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(s.transport, "<enable xmlns='%s' resume='true'/>", stanza.NSStreamManagement)
|
|
|
|
var packet stanza.Packet
|
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
|
if s.err == nil {
|
|
switch p := packet.(type) {
|
|
case stanza.SMEnabled:
|
|
s.SMState = SMState{Id: p.Id}
|
|
case stanza.SMFailed:
|
|
// TODO: Store error in SMState, for later inspection
|
|
default:
|
|
s.err = errors.New("unexpected reply to SM enable")
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|