Initial engine.io/websocketrpc port
[KiwiIRC.git] / server / httphandler.js
1 var url = require('url'),
2 fs = require('fs'),
3 crypto = require('crypto'),
4 node_static = require('node-static'),
5 _ = require('lodash'),
6 config = require('./configuration.js');
7
8
9 // Cache for settings.json
10 var cached_settings = {
11 debug: {
12 hash: '',
13 settings: ''
14 },
15 production: {
16 hash: '',
17 settings: ''
18 }
19 };
20
21 // Clear the settings cache when the settings change
22 config.on('loaded', function () {
23 cached_settings.debug.settings = cached_settings.production.settings = '';
24 cached_settings.debug.hash = cached_settings.production.hash = '';
25 });
26
27
28
29
30 var HttpHandler = function (config) {
31 var public_html = config.public_html || 'client/';
32 this.file_server = new node_static.Server(public_html);
33 };
34
35 module.exports.HttpHandler = HttpHandler;
36
37
38
39 HttpHandler.prototype.serve = function (request, response) {
40 // The incoming requests base path (ie. /kiwiclient)
41 var base_path = global.config.http_base_path || '/kiwi',
42 base_path_regex;
43
44 // Trim of any trailing slashes
45 if (base_path.substr(base_path.length - 1) === '/') {
46 base_path = base_path.substr(0, base_path.length - 1);
47 }
48
49 // Build the regex to match the base_path
50 base_path_regex = base_path.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
51
52 // Any asset request to head into the asset dir
53 request.url = request.url.replace(base_path + '/assets/', '/assets/');
54 console.log('request url:', request.url);
55
56 // Any requests for /client to load the index file
57 if (request.url.match(new RegExp('^' + base_path_regex + '([/$]|$)', 'i'))) {
58 request.url = '/';
59 }
60
61 // If the 'magic' translation is requested, figure out the best language to use from
62 // the Accept-Language HTTP header. If nothing is suitible, fallback to our en-gb default translation
63 if (request.url.substr(0, 16) === '/assets/locales/') {
64 if (request.url === '/assets/locales/magic.json') {
65 return serveMagicLocale.call(this, request, response);
66 } else {
67 response.setHeader('Content-Language', request.url.substr(16, request.url.indexOf('.') - 16));
68 }
69 } else if (request.url.substr(0, 21) === '/assets/settings.json') {
70 return serveSettings.call(this, request, response);
71 }
72
73 this.file_server.serve(request, response, function (err) {
74 if (err) {
75 response.writeHead(err.status, err.headers);
76 response.end();
77 }
78 });
79 };
80
81
82 /**
83 * Handle the /assets/locales/magic.json request
84 * Find the closest translation we have for the language
85 * set in the browser.
86 **/
87 var serveMagicLocale = function (request, response) {
88 var that = this,
89 default_locale_id = 'en-gb';
90
91 if (request.headers['accept-language']) {
92 fs.readdir('client/assets/locales', function (err, files) {
93 var available = [],
94 i = 0,
95 langs = request.headers['accept-language'].split(','), // Example: en-gb,en;q=0.5
96 found_locale = default_locale_id;
97
98 // Get a list of the available translations we have
99 files.forEach(function (file) {
100 if (file.substr(-5) === '.json') {
101 available.push(file.slice(0, -5));
102 }
103 });
104
105 // Sanitise the browsers accepted languages and the qualities
106 for (i = 0; i < langs.length; i++) {
107 langs[i]= langs[i].split(';q=');
108 langs[i][0] = langs[i][0].toLowerCase();
109 langs[i][1] = (typeof langs[i][1] === 'string') ? parseFloat(langs[i][1]) : 1.0;
110 }
111
112 // Sort the accepted languages by quality
113 langs.sort(function (a, b) {
114 return b[1] - a[1];
115 });
116
117 // Serve the first language we have a translation for
118 for (i = 0; i < langs.length; i++) {
119 if (langs[i][0] === '*') {
120 break;
121 } else if (_.contains(available, langs[i][0])) {
122 found_locale = langs[i][0];
123 break;
124 }
125 }
126
127 return serveLocale.call(that, request, response, found_locale);
128 });
129 } else {
130
131 // No accept-language specified in the request so send the default
132 return serveLocale.call(this, request, response, default_locale_id);
133 }
134
135 };
136
137
138 /**
139 * Send a locale to the browser
140 */
141 var serveLocale = function (request, response, locale_id) {
142 this.file_server.serveFile('/assets/locales/' + locale_id + '.json', 200, {
143 Vary: 'Accept-Language',
144 'Content-Language': locale_id
145 }, request, response);
146 };
147
148
149 /**
150 * Handle the settings.json request
151 */
152 function serveSettings(request, response) {
153 var referrer_url,
154 debug = false,
155 settings;
156
157 // Check the referrer for a debug option
158 if (request.headers['referer']) {
159 referrer_url = url.parse(request.headers['referer'], true);
160 if (referrer_url.query && referrer_url.query.debug) {
161 debug = true;
162 }
163 }
164
165 settings = cached_settings[debug ? 'debug' : 'production'];
166
167 // Generate the settings if we don't have them cached as yet
168 if (settings.settings === '') {
169 generateSettings(request, debug, function (err, settings) {
170 if (err) {
171 response.statusCode = 500;
172 response.end();
173 } else {
174 sendSettings.call(this, request, response, settings);
175 }
176 });
177
178 } else {
179 sendSettings.call(this, request, response, settings);
180 }
181 }
182
183
184 /**
185 * Send the the settings to the browser
186 */
187 function sendSettings(request, response, settings) {
188 if (request.headers['if-none-match'] && request.headers['if-none-match'] === settings.hash) {
189 response.writeHead(304, 'Not Modified');
190 return response.end();
191 }
192
193 response.writeHead(200, {
194 'ETag': settings.hash,
195 'Content-Type': 'application/json'
196 });
197 response.end(settings.settings);
198 }
199
200
201 /**
202 * Generate a settings object for the client.
203 * Settings include available translations, default client config, etc
204 */
205 function generateSettings(request, debug, callback) {
206 var vars = {
207 server_settings: {},
208 client_plugins: [],
209 translations: [],
210 scripts: [
211 [
212 'libs/lodash.min.js'
213 ],
214 'libs/backbone.min.js',
215 'libs/jed.js'
216 ]
217 };
218
219 if (debug) {
220 vars.scripts = vars.scripts.concat([
221 'src/app.js',
222 [
223 'src/models/application.js',
224 'src/models/gateway.js'
225 ],
226 [
227 'src/models/newconnection.js',
228 'src/models/panellist.js',
229 'src/models/networkpanellist.js',
230 'src/models/panel.js',
231 'src/models/member.js',
232 'src/models/memberlist.js',
233 'src/models/network.js'
234 ],
235
236 [
237 'src/models/query.js',
238 'src/models/channel.js',
239 'src/models/server.js',
240 'src/models/applet.js'
241 ],
242
243 [
244 'src/applets/settings.js',
245 'src/applets/chanlist.js',
246 'src/applets/scripteditor.js'
247 ],
248
249 [
250 'src/models/pluginmanager.js',
251 'src/models/datastore.js',
252 'src/helpers/utils.js'
253 ],
254
255 // Some views extend these, so make sure they're loaded beforehand
256 [
257 'src/views/panel.js'
258 ],
259
260 [
261 'src/views/channel.js',
262 'src/views/applet.js',
263 'src/views/application.js',
264 'src/views/apptoolbar.js',
265 'src/views/controlbox.js',
266 'src/views/favicon.js',
267 'src/views/mediamessage.js',
268 'src/views/member.js',
269 'src/views/memberlist.js',
270 'src/views/menubox.js',
271 'src/views/networktabs.js',
272 'src/views/nickchangebox.js',
273 'src/views/resizehandler.js',
274 'src/views/serverselect.js',
275 'src/views/statusmessage.js',
276 'src/views/tabs.js',
277 'src/views/topicbar.js',
278 'src/views/userbox.js'
279 ]
280 ]);
281 } else {
282 vars.scripts.push('kiwi.min.js');
283 }
284
285 // Any restricted server mode set?
286 if (config.get().restrict_server) {
287 vars.server_settings = {
288 connection: {
289 server: config.get().restrict_server,
290 port: config.get().restrict_server_port || 6667,
291 ssl: config.get().restrict_server_ssl,
292 channel: config.get().restrict_server_channel,
293 nick: config.get().restrict_server_nick,
294 allow_change: false
295 }
296 };
297 }
298
299 // Any client default settings?
300 if (config.get().client) {
301 vars.server_settings.client = config.get().client;
302 }
303
304 // Any client plugins?
305 if (config.get().client_plugins && config.get().client_plugins.length > 0) {
306 vars.client_plugins = config.get().client_plugins;
307 }
308
309 // Get a list of available translations
310 fs.readFile(__dirname + '/../client/assets/src/translations/translations.json', function (err, translations) {
311 if (err) {
312 return callback(err);
313 }
314
315 var translation_files;
316 translations = JSON.parse(translations);
317 fs.readdir(__dirname + '/../client/assets/src/translations/', function (err, pofiles) {
318 var hash, settings;
319 if (err) {
320 return callback(err);
321 }
322
323 pofiles.forEach(function (file) {
324 var locale = file.slice(0, -3);
325 if ((file.slice(-3) === '.po') && (locale !== 'template')) {
326 vars.translations.push({tag: locale, language: translations[locale]});
327 }
328 });
329
330 settings = cached_settings[debug?'debug':'production'];
331 settings.settings = JSON.stringify(vars);
332 settings.hash = crypto.createHash('md5').update(settings.settings).digest('hex');
333
334 return callback(null, settings);
335 });
336 });
337 }
338