package xmpp import ( "crypto/tls" "encoding/xml" "errors" "fmt" "log" ) // Config structure used to create a new XMPP client connection. type ClientConfig struct { // Don't upgrade the connection to TLS, even if the server supports it. If // the server *requires* TLS then this option is ignored. NoTLS bool // Skip verification of the server's certificate chain. Probably only // useful during development. InsecureSkipVerify bool } // Create a client XMPP over the stream. func NewClientXMPP(stream *Stream, jid JID, password string, config *ClientConfig) (*XMPP, error) { if config == nil { config = &ClientConfig{} } for { if err := startClient(stream, jid); err != nil { return nil, err } // Read features. f := new(features) if err := stream.Decode(f, nil); err != nil { return nil, err } // TLS? if f.StartTLS != nil && (f.StartTLS.Required != nil || !config.NoTLS) { log.Println("Start TLS") if err := startTLS(stream, config); err != nil { return nil, err } continue // Restart } // Authentication if f.Mechanisms != nil { log.Println("Authenticating") if err := authenticate(stream, f.Mechanisms.Mechanisms, jid.Node, password); err != nil { return nil, err } continue // Restart } // Bind resource. if f.Bind != nil { log.Println("Binding resource.") boundJID, err := bindResource(stream, jid) if err != nil { return nil, err } jid = boundJID } // Session. if f.Session != nil { log.Println("Establishing session.") if err := establishSession(stream, jid.Domain); err != nil { return nil, err } } break } return newXMPP(jid, stream), nil } func startClient(stream *Stream, jid JID) error { start := xml.StartElement{ xml.Name{"stream", "stream"}, []xml.Attr{ xml.Attr{xml.Name{"", "xmlns"}, nsClient}, xml.Attr{xml.Name{"xmlns", "stream"}, nsStreams}, xml.Attr{xml.Name{"", "from"}, jid.Full()}, xml.Attr{xml.Name{"", "to"}, jid.Domain}, xml.Attr{xml.Name{"", "version"}, "1.0"}, }, } rstart, err := stream.SendStart(&start) if err != nil { return err } if rstart.Name != (xml.Name{nsStreams, "stream"}) { return fmt.Errorf("unexpected start element: %s", rstart.Name) } return nil } func startTLS(stream *Stream, config *ClientConfig) error { if err := stream.Send(&tlsStart{}); err != nil { return err } p := tlsProceed{} if err := stream.Decode(&p, nil); err != nil { return err } tlsConfig := tls.Config{InsecureSkipVerify: config.InsecureSkipVerify, ServerName: stream.config.ConnectionDomain} return stream.UpgradeTLS(&tlsConfig) } type tlsStart struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"` } type tlsProceed struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"` } func authenticate(stream *Stream, mechanisms []string, user, password string) error { for _, handler := range authHandlers { if !stringSliceContains(mechanisms, handler.Mechanism) { continue } if err := handler.Fn(stream, user, password); err == nil { log.Printf("Authentication (%s) successful", handler.Mechanism) return nil } } return errors.New("no supported SASL mechanism found") } type authHandler struct { Mechanism string Fn func(*Stream, string, string) error } var authHandlers = []authHandler{ {"PLAIN", authenticatePlain}, } func authenticatePlain(stream *Stream, user, password string) error { auth := saslAuth{Mechanism: "PLAIN", Text: saslEncodePlain(user, password)} if err := stream.Send(&auth); err != nil { return err } return authenticateResponse(stream) } func authenticateResponse(stream *Stream) error { se, err := stream.Next() if err != nil { return err } switch se.Name.Local { case "success": if err := stream.Skip(); err != nil { return err } return nil case "failure": f := new(saslFailure) if err := stream.Decode(f, se); err != nil { return err } return fmt.Errorf("Authentication failed: %s", f.Reason.Local) default: return fmt.Errorf("Unexpected: %s", se.Name) } } type saslAuth struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"` Mechanism string `xml:"mechanism,attr"` Text string `xml:",chardata"` } func bindResource(stream *Stream, jid JID) (JID, error) { req := IQ{ID: UUID4(), Type: "set"} if jid.Resource == "" { req.PayloadEncode(bindIQ{}) } else { req.PayloadEncode(bindIQ{Resource: jid.Resource}) } if err := stream.Send(req); err != nil { return JID{}, err } resp := IQ{} err := stream.Decode(&resp, nil) if err != nil { return JID{}, err } bindResp := bindIQ{} resp.PayloadDecode(&bindResp) boundJID, err := ParseJID(bindResp.JID) return boundJID, nil } type bindIQ struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` Resource string `xml:"resource,omitempty"` JID string `xml:"jid,omitempty"` } func establishSession(stream *Stream, domain string) error { req := IQ{ID: UUID4(), Type: "set", To: domain} req.PayloadEncode(&session{}) if err := stream.Send(req); err != nil { return err } resp := IQ{} if err := stream.Decode(&resp, nil); err != nil { return err } else if resp.Error != nil { return resp.Error } return nil } func stringSliceContains(l []string, m string) bool { for _, i := range l { if i == m { return true } } return false } type features struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` StartTLS *tlsStartTLS `xml:"starttls"` Mechanisms *mechanisms `xml:"mechanisms"` Bind *bind `xml:"bind"` Session *session `xml:"session"` } type session struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` } type bind struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` Required *required `xml:"required"` } type mechanisms struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` Mechanisms []string `xml:"mechanism"` } type tlsStartTLS struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"` Required *required `xml:"required"` } type required struct{} type saslFailure struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"` Reason xml.Name `xml:",any"` } // BUG(matt): authentication incorrectly reports, "No supported SASL mechanism // found", for authentication attemtps that fail due to invalid credentials.