1package xmpp
2
3import (
4	"encoding/xml"
5	"io"
6	"net"
7	"strings"
8
9	"github.com/coyim/coyim/tls"
10	"github.com/coyim/coyim/xmpp/data"
11	"github.com/coyim/coyim/xmpp/interfaces"
12
13	"golang.org/x/net/proxy"
14)
15
16// A dialer connects and authenticates to an XMPP server
17type dialer struct {
18	// JID represents the user's "bare JID" as specified in RFC 6120
19	JID string
20
21	// password used to authenticate to the server
22	password string
23
24	// serverAddress associates a particular FQDN with the origin domain specified by the JID.
25	serverAddress string
26
27	// proxy configures a proxy used to connect to the server
28	proxy proxy.Dialer
29
30	// config configures the XMPP protocol
31	config data.Config
32
33	verifier       tls.Verifier
34	tlsConnFactory tls.Factory
35}
36
37// DialerFactory returns a new xmpp dialer
38func DialerFactory(verifier tls.Verifier, connFactory tls.Factory) interfaces.Dialer {
39	return &dialer{verifier: verifier, tlsConnFactory: connFactory}
40}
41
42func (d *dialer) SetJID(v string) {
43	d.JID = v
44}
45
46func (d *dialer) SetServerAddress(v string) {
47	d.serverAddress = v
48}
49
50func (d *dialer) SetPassword(v string) {
51	d.password = v
52}
53
54func (d *dialer) SetProxy(v proxy.Dialer) {
55	d.proxy = v
56}
57
58func (d *dialer) SetConfig(v data.Config) {
59	d.config = v
60}
61
62func (d *dialer) Config() data.Config {
63	return d.config
64}
65
66func (d *dialer) ServerAddress() string {
67	return d.serverAddress
68}
69
70func (d *dialer) hasCustomServer() bool {
71	return d.serverAddress != ""
72}
73
74func (d *dialer) getJIDLocalpart() string {
75	parts := strings.SplitN(d.JID, "@", 2)
76	return parts[0]
77}
78
79func (d *dialer) getJIDDomainpart() string {
80	//TODO: remove any existing resourcepart although our doc says it is a bare JID (without resourcepart) but it would be nice
81	parts := strings.SplitN(d.JID, "@", 2)
82	return parts[1]
83}
84
85// GetServer returns the "hardcoded" server chosen if available, otherwise returns the domainpart from the JID. The server contains port information
86func (d *dialer) GetServer() string {
87	if d.hasCustomServer() {
88		return d.serverAddress
89	}
90
91	return d.getFallbackServer()
92}
93
94func (d *dialer) getFallbackServer() string {
95	return net.JoinHostPort(d.getJIDDomainpart(), "5222")
96}
97
98// RegisterAccount registers an account on the server. The formCallback is used to handle XMPP forms.
99func (d *dialer) RegisterAccount(formCallback data.FormCallback) (interfaces.Conn, error) {
100	//TODO: notify in case the feature is not supported
101	d.config.CreateCallback = formCallback
102	return d.Dial()
103}
104
105// Dial creates a new connection to an XMPP server with the given proxy
106// and authenticates as the given user.
107func (d *dialer) Dial() (interfaces.Conn, error) {
108	// Starting an XMPP connection comprises two parts:
109	// - Opening a transport channel (TCP)
110	// - Opening an XML stream over the transport channel
111
112	// RFC 6120, section 3
113	conn, err := d.newTCPConn()
114	if err != nil {
115		return nil, err
116	}
117
118	// RFC 6120, section 4
119	return d.setupStream(conn)
120}
121
122// RFC 6120, Section 4.2
123func (d *dialer) setupStream(conn net.Conn) (interfaces.Conn, error) {
124	c := newConn()
125	c.config = d.config
126	c.originDomain = d.getJIDDomainpart()
127	d.bindTransport(c, conn)
128
129	if err := d.negotiateStreamFeatures(c, conn); err != nil {
130		return nil, err
131	}
132
133	go c.watchKeepAlive(conn)
134	go c.watchPings()
135
136	return c, nil
137}
138
139// RFC 6120, section 4.3.2
140func (d *dialer) negotiateStreamFeatures(c interfaces.Conn, conn net.Conn) error {
141	if err := c.SendInitialStreamHeader(); err != nil {
142		return err
143	}
144
145	// STARTTLS MUST be the first feature to be negotiated
146	if err := d.negotiateSTARTTLS(c, conn); err != nil {
147		return err
148	}
149
150	if registered, err := d.negotiateInBandRegistration(c); err != nil || registered {
151		return err
152	}
153
154	// SASL negotiation. RFC 6120, section 6
155	if err := d.negotiateSASL(c); err != nil {
156		return err
157	}
158
159	//TODO: negotiate other features
160
161	return nil
162}
163
164func (d *dialer) bindTransport(c interfaces.Conn, conn net.Conn) {
165	c.SetInOut(makeInOut(conn, d.config))
166	c.SetRawOut(conn)
167	c.SetKeepaliveOut(&timeoutableConn{conn, keepaliveTimeout})
168	c.SetServerAddress(d.serverAddress)
169}
170
171func makeInOut(conn io.ReadWriter, config data.Config) (in *xml.Decoder, out io.Writer) {
172	if config.InLog != nil {
173		in = xml.NewDecoder(io.TeeReader(conn, config.InLog))
174	} else {
175		in = xml.NewDecoder(conn)
176	}
177
178	if config.OutLog != nil {
179		out = io.MultiWriter(conn, config.OutLog)
180	} else {
181		out = conn
182	}
183
184	return
185}
186