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