0
|
1 'use strict';
|
|
2
|
|
3 var Lightstring = {
|
|
4 NS: {},
|
|
5 stanza: {},
|
|
6 };
|
|
7
|
|
8 Lightstring.Connection = function (aURL) {
|
|
9 var parser = new DOMParser();
|
|
10 var serializer = new XMLSerializer();
|
|
11 this.handlers = {};
|
|
12 this.iqid = 1024;
|
|
13 this.getNewId = function() {
|
|
14 this.iqid++;
|
|
15 return 'sendiq:'+this.iqid;
|
|
16 };
|
|
17 this.parse = function(str) {
|
|
18 return parser.parseFromString(str, 'text/xml').documentElement;
|
|
19 };
|
|
20 this.serialize = function(elm) {
|
|
21 return serializer.serializeToString(elm);
|
|
22 };
|
|
23 this.connect = function(jid, password) {
|
|
24 this.domain = jid.split('@')[1];
|
|
25 this.node = jid.split('@')[0];
|
|
26 this.jid = jid;
|
|
27 this.password = password;
|
|
28 if(typeof WebSocket === 'undefined')
|
|
29 this.socket = new MozWebSocket(aURL);
|
|
30 else
|
|
31 this.socket = new WebSocket(aURL);
|
|
32
|
|
33 var that = this;
|
|
34 this.socket.addEventListener('open', function() {
|
|
35 that.emit('open');
|
|
36 that.send("<stream:stream to='"+that.domain+"' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' />");
|
|
37 });
|
|
38 this.socket.addEventListener('error', function(err) {
|
|
39 that.emit('error');
|
|
40 });
|
|
41 this.socket.addEventListener('close', function(close) {
|
|
42 that.emit('close');
|
|
43 that.emit('disconnected');
|
|
44 });
|
|
45 this.socket.addEventListener('message', function(e) {
|
|
46 that.emit('XMLInput', e.data);
|
|
47 var elm = that.parse(e.data);
|
|
48 that.emit('DOMInput', elm);
|
|
49 that.emit(elm.tagName, elm);
|
|
50
|
|
51 if((elm.tagName === 'iq'))
|
|
52 that.emit(elm.getAttribute('id'), elm);
|
|
53 });
|
|
54 };
|
|
55
|
|
56
|
|
57
|
|
58 this.send = function(stanza, callback) {
|
|
59 //FIXME support for E4X
|
|
60 //~ if(typeof stanza === 'xml') {
|
|
61 //~ stanza = stanza.toXMLString();
|
|
62 //~ }
|
|
63 if(stanza.cnode) {
|
|
64 stanza = stanza.toString();
|
|
65 //~ console.log(typeof stanza);
|
|
66 }
|
|
67 if(typeof stanza === 'string') {
|
|
68 var str = stanza;
|
|
69 var elm = this.parse(stanza);
|
|
70 }
|
|
71 else {
|
|
72 var elm = stanza;
|
|
73 var str = this.serialize(stanza);
|
|
74 }
|
|
75
|
|
76
|
|
77 if(elm.tagName === 'iq') {
|
|
78 var id = elm.getAttribute('id');
|
|
79 if(!id) {
|
|
80 elm.setAttribute('id', this.getNewId())
|
|
81 str = this.serialize(elm)
|
|
82 }
|
|
83 if(callback) this.on(elm.getAttribute('id'), callback);
|
|
84 }
|
|
85
|
|
86
|
|
87 this.socket.send(str);
|
|
88 this.emit('XMLOutput', str);
|
|
89 this.emit('DOMOutput', elm);
|
|
90 };
|
|
91 this.disconnect = function() {
|
|
92 this.send('</stream:stream>');
|
|
93 this.emit('disconnected');
|
|
94 this.socket.close();
|
|
95 };
|
|
96 //FIXME Callbacks sucks, better idea?
|
|
97 this.emit = function(name, data) {
|
|
98 var handlers = this.handlers[name];
|
|
99 if(!handlers)
|
|
100 return;
|
|
101
|
|
102 //FIXME Better idea than passing the context as argument?
|
|
103 for(var i=0; i<handlers.length; i++)
|
|
104 handlers[i](data, this);
|
|
105
|
|
106 if(name.match('sendiq:'))
|
|
107 delete this.handlers[name];
|
|
108 };
|
|
109 this.on = function(name, callback) {
|
|
110 if(!this.handlers[name])
|
|
111 this.handlers[name] = [];
|
|
112 this.handlers[name].push(callback);
|
|
113 };
|
|
114 //FIXME do this!
|
|
115 this.once = function(name, callback) {
|
|
116 if(!this.handlers[name])
|
|
117 this.handlers[name] = [];
|
|
118 this.handlers[name].push(callback);
|
|
119 };
|
|
120 //Internal
|
|
121 this.on('stream:features', function(stanza, that) {
|
|
122 var nodes = stanza.querySelectorAll('mechanism');
|
|
123 //SASL/Auth features
|
|
124 if(nodes.length > 0) {
|
|
125 that.emit('mechanisms', stanza);
|
|
126 var mechanisms = {};
|
|
127 for(var i=0; i<nodes.length; i++)
|
|
128 mechanisms[nodes[i].textContent] = true;
|
|
129
|
|
130
|
|
131 //FIXME support SCRAM-SHA1 && allow specify method preferences
|
|
132 if('DIGEST-MD5' in mechanisms)
|
|
133 that.send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>");
|
|
134 else if('PLAIN' in mechanisms) {
|
|
135 var token = btoa(that.jid + "\u0000" + that.jid.split('@')[0] + "\u0000" + that.password);
|
|
136 that.send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"+token+"</auth>");
|
|
137 }
|
|
138 }
|
|
139 //XMPP features
|
|
140 else {
|
|
141 that.emit('features', stanza);
|
|
142 //Bind http://xmpp.org/rfcs/rfc3920.html#bind
|
|
143 that.send("<iq type='set' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>", function() {
|
|
144 //Session http://xmpp.org/rfcs/rfc3921.html#session
|
|
145 that.send("<iq type='set' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>", function() {
|
|
146 that.emit('connected');
|
|
147 });
|
|
148 });
|
|
149 }
|
|
150 });
|
|
151 //Internal
|
|
152 this.on('success', function(stanza, that) {
|
|
153 that.send("<stream:stream to='"+that.domain+"' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' />");
|
|
154 });
|
|
155 //Internal
|
|
156 this.on('challenge', function(stanza, that) {
|
|
157 //FIXME this is mostly Strophe code
|
|
158
|
|
159 function _quote(str) {
|
|
160 return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
161 };
|
|
162
|
|
163 var challenge = atob(stanza.textContent);
|
|
164
|
|
165 var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/;
|
|
166
|
|
167 var cnonce = MD5.hexdigest(Math.random() * 1234567890);
|
|
168 var realm = "";
|
|
169 var host = null;
|
|
170 var nonce = "";
|
|
171 var qop = "";
|
|
172 var matches;
|
|
173
|
|
174 while (challenge.match(attribMatch)) {
|
|
175 matches = challenge.match(attribMatch);
|
|
176 challenge = challenge.replace(matches[0], "");
|
|
177 matches[2] = matches[2].replace(/^"(.+)"$/, "$1");
|
|
178 switch (matches[1]) {
|
|
179 case "realm":
|
|
180 realm = matches[2];
|
|
181 break;
|
|
182 case "nonce":
|
|
183 nonce = matches[2];
|
|
184 break;
|
|
185 case "qop":
|
|
186 qop = matches[2];
|
|
187 break;
|
|
188 case "host":
|
|
189 host = matches[2];
|
|
190 break;
|
|
191 }
|
|
192 }
|
|
193
|
|
194 var digest_uri = "xmpp/" + that.domain;
|
|
195 if (host !== null) {
|
|
196 digest_uri = digest_uri + "/" + host;
|
|
197 }
|
|
198
|
|
199 var A1 = MD5.hash(that.node +
|
|
200 ":" + realm + ":" + that.password) +
|
|
201 ":" + nonce + ":" + cnonce;
|
|
202 var A2 = 'AUTHENTICATE:' + digest_uri;
|
|
203
|
|
204 var responseText = "";
|
|
205 responseText += 'username=' + _quote(that.node) + ',';
|
|
206 responseText += 'realm=' + _quote(realm) + ',';
|
|
207 responseText += 'nonce=' + _quote(nonce) + ',';
|
|
208 responseText += 'cnonce=' + _quote(cnonce) + ',';
|
|
209 responseText += 'nc="00000001",';
|
|
210 responseText += 'qop="auth",';
|
|
211 responseText += 'digest-uri=' + _quote(digest_uri) + ',';
|
|
212 responseText += 'response=' + _quote(
|
|
213 MD5.hexdigest(MD5.hexdigest(A1) + ":" +
|
|
214 nonce + ":00000001:" +
|
|
215 cnonce + ":auth:" +
|
|
216 MD5.hexdigest(A2))) + ',';
|
|
217 responseText += 'charset="utf-8"';
|
|
218
|
|
219 that.send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"+btoa(responseText)+"</response>");
|
|
220 });
|
|
221 };
|