initial commit
[JIRC.git] / node_modules / websocket / lib / WebSocketRequest.js
CommitLineData
39c8b14f 1/************************************************************************
2 * Copyright 2010-2011 Worlize Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 ***********************************************************************/
16
17var crypto = require('crypto');
18var util = require('util');
19var url = require('url');
20var EventEmitter = require('events').EventEmitter;
21var WebSocketConnection = require('./WebSocketConnection');
22var Constants = require('./Constants');
23
24var headerValueSplitRegExp = /,\s*/;
25var headerParamSplitRegExp = /;\s*/;
26var headerSanitizeRegExp = /[\r\n]/g;
27var separators = [
28 "(", ")", "<", ">", "@",
29 ",", ";", ":", "\\", "\"",
30 "/", "[", "]", "?", "=",
31 "{", "}", " ", String.fromCharCode(9)
32];
33var controlChars = [String.fromCharCode(127) /* DEL */];
34for (var i=0; i < 31; i ++) {
35 /* US-ASCII Control Characters */
36 controlChars.push(String.fromCharCode(i));
37}
38
39var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
40var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;
41var cookieValueDQuoteValidateRegEx = /^"[^"]*"$/;
42var controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g;
43
44var cookieSeparatorRegEx = /; */;
45var cookieCaptureRegEx = /(.*?)=(.*)/;
46
47var httpStatusDescriptions = {
48 100: "Continue",
49 101: "Switching Protocols",
50 200: "OK",
51 201: "Created",
52 203: "Non-Authoritative Information",
53 204: "No Content",
54 205: "Reset Content",
55 206: "Partial Content",
56 300: "Multiple Choices",
57 301: "Moved Permanently",
58 302: "Found",
59 303: "See Other",
60 304: "Not Modified",
61 305: "Use Proxy",
62 307: "Temporary Redirect",
63 400: "Bad Request",
64 401: "Unauthorized",
65 402: "Payment Required",
66 403: "Forbidden",
67 404: "Not Found",
68 406: "Not Acceptable",
69 407: "Proxy Authorization Required",
70 408: "Request Timeout",
71 409: "Conflict",
72 410: "Gone",
73 411: "Length Required",
74 412: "Precondition Failed",
75 413: "Request Entity Too Long",
76 414: "Request-URI Too Long",
77 415: "Unsupported Media Type",
78 416: "Requested Range Not Satisfiable",
79 417: "Expectation Failed",
80 426: "Upgrade Required",
81 500: "Internal Server Error",
82 501: "Not Implemented",
83 502: "Bad Gateway",
84 503: "Service Unavailable",
85 504: "Gateway Timeout",
86 505: "HTTP Version Not Supported"
87};
88
89function WebSocketRequest(socket, httpRequest, serverConfig) {
90 this.socket = socket;
91 this.httpRequest = httpRequest;
92 this.resource = httpRequest.url;
93 this.remoteAddress = socket.remoteAddress;
94 this.serverConfig = serverConfig;
95}
96
97util.inherits(WebSocketRequest, EventEmitter);
98
99WebSocketRequest.prototype.readHandshake = function() {
100 var request = this.httpRequest;
101
102 // Decode URL
103 this.resourceURL = url.parse(this.resource, true);
104
105 this.host = request.headers['host'];
106 if (!this.host) {
107 throw new Error("Client must provide a Host header.");
108 }
109
110 this.key = request.headers['sec-websocket-key'];
111 if (!this.key) {
112 throw new Error("Client must provide a value for Sec-WebSocket-Key.");
113 }
114
115 this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
116 this.websocketVersion = this.webSocketVersion; // Deprecated websocketVersion (proper casing...)
117
118 if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
119 throw new Error("Client must provide a value for Sec-WebSocket-Version.");
120 }
121
122 switch (this.webSocketVersion) {
123 case 8:
124 case 13:
125 break;
126 default:
127 var e = new Error("Unsupported websocket client version: " + this.webSocketVersion +
128 "Only versions 8 and 13 are supported.");
129 e.httpCode = 426;
130 e.headers = {
131 "Sec-WebSocket-Version": "13"
132 };
133 throw e;
134 }
135
136 if (this.webSocketVersion === 13) {
137 this.origin = request.headers['origin'];
138 }
139 else if (this.webSocketVersion === 8) {
140 this.origin = request.headers['sec-websocket-origin'];
141 }
142
143 // Protocol is optional.
144 var protocolString = request.headers['sec-websocket-protocol'];
145 if (protocolString) {
146 this.requestedProtocols = protocolString.toLocaleLowerCase().split(headerValueSplitRegExp);
147 }
148 else {
149 this.requestedProtocols = [];
150 }
151
152 if (request.headers['x-forwarded-for']) {
153 this.remoteAddress = request.headers['x-forwarded-for'].split(', ')[0];
154 }
155
156 // Extensions are optional.
157 var extensionsString = request.headers['sec-websocket-extensions'];
158 this.requestedExtensions = this.parseExtensions(extensionsString);
159
160 // Cookies are optional
161 var cookieString = request.headers['cookie'];
162 this.cookies = this.parseCookies(cookieString);
163};
164
165WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
166 if (!extensionsString || extensionsString.length === 0) {
167 return [];
168 }
169 extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
170 extensions.forEach(function(extension, index, array) {
171 var params = extension.split(headerParamSplitRegExp);
172 var extensionName = params[0];
173 var extensionParams = params.slice(1);
174 extensionParams.forEach(function(rawParam, index, array) {
175 var arr = rawParam.split('=');
176 var obj = {
177 name: arr[0],
178 value: arr[1]
179 };
180 array.splice(index, 1, obj);
181 });
182 var obj = {
183 name: extensionName,
184 params: extensionParams
185 };
186 array.splice(index, 1, obj);
187 });
188 return extensions;
189};
190
191WebSocketRequest.prototype.parseCookies = function(cookieString) {
192 if (!cookieString || cookieString.length === 0) {
193 return [];
194 }
195 var cookies = [];
196 var cookieArray = cookieString.split(cookieSeparatorRegEx);
197
198 cookieArray.forEach(function(cookie) {
199 if (cookie && cookie.length !== 0) {
200 var cookieParts = cookie.match(cookieCaptureRegEx);
201 cookies.push({
202 name: cookieParts[1],
203 value: cookieParts[2]
204 });
205 }
206 });
207 return cookies;
208};
209
210WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
211 // TODO: Handle extensions
212 var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
213
214 connection.webSocketVersion = this.webSocketVersion;
215 connection.websocketVersion = this.webSocketVersion; // deprecated.. proper casing
216 connection.remoteAddress = this.remoteAddress;
217
218 // Create key validation hash
219 var sha1 = crypto.createHash('sha1');
220 sha1.update(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
221 var acceptKey = sha1.digest('base64');
222
223 var response = "HTTP/1.1 101 Switching Protocols\r\n" +
224 "Upgrade: websocket\r\n" +
225 "Connection: Upgrade\r\n" +
226 "Sec-WebSocket-Accept: " + acceptKey + "\r\n";
227
228 if (acceptedProtocol) {
229 // validate protocol
230 for (var i=0; i < acceptedProtocol.length; i++) {
231 var charCode = acceptedProtocol.charCodeAt(i);
232 var character = acceptedProtocol.charAt(i);
233 if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
234 this.reject(500);
235 throw new Error("Illegal character '" + String.fromCharCode(character) + "' in subprotocol.");
236 }
237 }
238 if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
239 this.reject(500);
240 throw new Error("Specified protocol was not requested by the client.");
241 }
242
243 acceptedProtocol = acceptedProtocol.replace(headerSanitizeRegExp, '');
244 response += "Sec-WebSocket-Protocol: " + acceptedProtocol + "\r\n";
245 }
246 if (allowedOrigin) {
247 allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
248 if (this.webSocketVersion === 13) {
249 response += "Origin: " + allowedOrigin + "\r\n";
250 }
251 else if (this.webSocketVersion === 8) {
252 response += "Sec-WebSocket-Origin: " + allowedOrigin + "\r\n";
253 }
254 }
255
256 if (cookies) {
257 if (!Array.isArray(cookies)) {
258 this.reject(500);
259 throw new Error("Value supplied for 'cookies' argument must be an array.");
260 }
261 var seenCookies = {};
262 cookies.forEach(function(cookie) {
263 if (!cookie.name || !cookie.value) {
264 this.reject(500);
265 throw new Error("Each cookie to set must at least provide a 'name' and 'value'");
266 }
267
268 // Make sure there are no \r\n sequences inserted
269 cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
270 cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
271
272 if (seenCookies[cookie.name]) {
273 this.reject(500);
274 throw new Error("You may not specify the same cookie name twice.");
275 }
276 seenCookies[cookie.name] = true;
277
278 // token (RFC 2616, Section 2.2)
279 var invalidChar = cookie.name.match(cookieNameValidateRegEx);
280 if (invalidChar) {
281 this.reject(500);
282 throw new Error("Illegal character " + invalidChar[0] + " in cookie name");
283 }
284
285 // RFC 6265, Section 4.1.1
286 // *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
287 if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
288 invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
289 } else {
290 invalidChar = cookie.value.match(cookieValueValidateRegEx);
291 }
292 if (invalidChar) {
293 this.reject(500);
294 throw new Error("Illegal character " + invalidChar[0] + " in cookie value");
295 }
296
297 var cookieParts = [cookie.name + "=" + cookie.value];
298
299 // RFC 6265, Section 4.1.1
300 // "Path=" path-value | <any CHAR except CTLs or ";">
301 if(cookie.path){
302 invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
303 if (invalidChar) {
304 this.reject(500);
305 throw new Error("Illegal character " + invalidChar[0] + " in cookie path");
306 }
307 cookieParts.push("Path=" + cookie.path);
308 }
309
310 // RFC 6265, Section 4.1.2.3
311 // "Domain=" subdomain
312 if (cookie.domain) {
313 if (typeof(cookie.domain) !== 'string') {
314 this.reject(500);
315 throw new Error("Domain must be specified and must be a string.");
316 }
317 var domain = cookie.domain.toLowerCase();
318 invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
319 if (invalidChar) {
320 this.reject(500);
321 throw new Error("Illegal character " + invalidChar[0] + " in cookie domain");
322 }
323 cookieParts.push("Domain=" + cookie.domain.toLowerCase());
324 }
325
326 // RFC 6265, Section 4.1.1
327 //"Expires=" sane-cookie-date | Force Date object requirement by using only epoch
328 if (cookie.expires) {
329 if (!(cookie.expires instanceof Date)){
330 this.reject(500);
331 throw new Error("Value supplied for cookie 'expires' must be a vaild date object");
332 }
333 cookieParts.push("Expires=" + cookie.expires.toGMTString());
334 }
335
336 // RFC 6265, Section 4.1.1
337 //"Max-Age=" non-zero-digit *DIGIT
338 if (cookie.maxage) {
339 var maxage = cookie.maxage;
340 if (typeof(maxage) === 'string') {
341 maxage = parseInt(maxage, 10);
342 }
343 if (isNaN(maxage) || maxage <= 0 ) {
344 this.reject(500);
345 throw new Error("Value supplied for cookie 'maxage' must be a non-zero number");
346 }
347 maxage = Math.round(maxage);
348 cookieParts.push("Max-Age=" + maxage.toString(10));
349 }
350
351 // RFC 6265, Section 4.1.1
352 //"Secure;"
353 if (cookie.secure) {
354 if (typeof(cookie.secure) !== "boolean") {
355 this.reject(500);
356 throw new Error("Value supplied for cookie 'secure' must be of type boolean");
357 }
358 cookieParts.push("Secure");
359 }
360
361 // RFC 6265, Section 4.1.1
362 //"HttpOnly;"
363 if (cookie.httponly) {
364 if (typeof(cookie.httponly) !== "boolean") {
365 this.reject(500);
366 throw new Error("Value supplied for cookie 'httponly' must be of type boolean");
367 }
368 cookieParts.push("HttpOnly");
369 }
370
371 response += ("Set-Cookie: " + cookieParts.join(';') + "\r\n");
372 }.bind(this));
373 }
374
375 // TODO: handle negotiated extensions
376 // if (negotiatedExtensions) {
377 // response += "Sec-WebSocket-Extensions: " + negotiatedExtensions.join(", ") + "\r\n";
378 // }
379
380 response += "\r\n";
381 try {
382 this.socket.write(response, 'ascii');
383 }
384 catch(e) {
385 if (Constants.DEBUG) {
386 console.log("Error Writing to Socket: " + e.toString());
387 }
388 // Since we have to return a connection object even if the socket is
389 // already dead in order not to break the API, we schedule a 'close'
390 // event on the connection object to occur immediately.
391 process.nextTick(function() {
392 // WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
393 // Third param: Skip sending the close frame to a dead socket
394 connection.drop(1006, "TCP connection lost before handshake completed.", true);
395 });
396 }
397
398 this.emit('requestAccepted', connection);
399
400 return connection;
401};
402
403WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
404 if (typeof(status) !== 'number') {
405 status = 403;
406 }
407 var response = "HTTP/1.1 " + status + " " + httpStatusDescriptions[status] + "\r\n" +
408 "Connection: close\r\n";
409 if (reason) {
410 reason = reason.replace(headerSanitizeRegExp, '');
411 response += "X-WebSocket-Reject-Reason: " + reason + "\r\n";
412 }
413
414 if (extraHeaders) {
415 for (var key in extraHeaders) {
416 var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
417 var sanitizedKey = key.replace(headerSanitizeRegExp, '');
418 response += (sanitizedKey + ": " + sanitizedValue + "\r\n");
419 }
420 }
421
422 response += "\r\n";
423 this.socket.end(response, 'ascii');
424
425 this.emit('requestRejected', this);
426};
427
428module.exports = WebSocketRequest;