BSD and expact license modified
[KiwiIRC.git] / server / httphandler.js
1 var url = require('url'),
2 fs = require('fs'),
3 node_static = require('node-static'),
4 Negotiator = require('negotiator'),
5 winston = require('winston'),
6 SettingsGenerator = require('./settingsgenerator.js'),
7 Stats = require('./stats.js');
8
9
10
11 // Cached list of available translations
12 var cached_available_locales = null;
13
14
15
16 var HttpHandler = function (config) {
17 var public_http = config.public_http || 'client/';
18 this.file_server = new node_static.Server(public_http);
19
20 if (!cached_available_locales) {
21 updateLocalesCache();
22 }
23 };
24
25 module.exports.HttpHandler = HttpHandler;
26
27
28
29 HttpHandler.prototype.serve = function (request, response) {
30 // The incoming requests base path (ie. /kiwiclient)
31 var base_path, base_check,
32 whitelisted_folders = ['/assets', '/src'],
33 is_whitelisted_folder = false;
34
35 // Trim off any trailing slashes from the base_path
36 base_path = global.config.http_base_path || '';
37 if (base_path.substr(base_path.length - 1) === '/') {
38 base_path = base_path.substr(0, base_path.length - 1);
39 }
40
41 // Normalise the URL + remove query strings to compare against the base_path
42 base_check = request.url.split('?')[0];
43 if (base_check.substr(base_check.length - 1) !== '/') {
44 base_check += '/';
45 }
46
47 // Normalise the URL we use by removing the base path
48 if (base_check.indexOf(base_path + '/') === 0) {
49 request.url = request.url.replace(base_path, '');
50
51 } else if (base_check !== '/') {
52 // We don't handle requests outside of the base path and not /, so just 404
53 response.writeHead(404);
54 response.write('Not Found');
55 response.end();
56 return;
57 }
58
59 // Map any whitelisted folders to the local directories
60 whitelisted_folders.forEach(function(folder) {
61 if (request.url.indexOf(folder) === 0) {
62 is_whitelisted_folder = true;
63 }
64 });
65
66 // Any requests not for whitelisted assets returns the index page
67 if (!is_whitelisted_folder) {
68 request.url = '/index.html';
69 }
70
71 if (request.url === '/index.html') {
72 Stats.incr('http.homepage');
73 }
74
75 // If the 'magic' translation is requested, figure out the best language to use from
76 // the Accept-Language HTTP header. If nothing is suitible, fallback to our en-gb default translation
77 if (request.url.substr(0, 16) === '/assets/locales/') {
78 if (request.url === '/assets/locales/magic.json') {
79 return serveMagicLocale.call(this, request, response);
80 } else {
81 response.setHeader('Content-Language', request.url.substr(16, request.url.indexOf('.') - 16));
82 }
83 } else if (request.url.substr(0, 21) === '/assets/settings.json') {
84 return serveSettings.call(this, request, response);
85 }
86
87 this.file_server.serve(request, response, function (err) {
88 if (err) {
89 response.writeHead(err.status, err.headers);
90 response.end();
91 }
92 });
93 };
94
95
96
97
98 /**
99 * Cache the available locales we have so we don't read the same directory for each request
100 **/
101 function updateLocalesCache() {
102 cached_available_locales = [];
103
104 fs.readdir(global.config.public_http + '/assets/locales', function (err, files) {
105 if (err) {
106 if (err.code === 'ENOENT') {
107 winston.error('No locale files could be found at ' + err.path);
108 } else {
109 winston.error('Error reading locales.', err);
110 }
111 }
112
113 (files || []).forEach(function (file) {
114 if (file.substr(-5) === '.json') {
115 cached_available_locales.push(file.slice(0, -5));
116 }
117 });
118 });
119 }
120
121
122
123 /**
124 * Handle the /assets/locales/magic.json request
125 * Find the closest translation we have for the language
126 * set in the browser.
127 **/
128 function serveMagicLocale(request, response) {
129 var default_locale_id = 'en-gb',
130 found_locale, negotiator;
131
132 if (!request.headers['accept-language']) {
133 // No accept-language specified in the request so send the default
134 found_locale = default_locale_id;
135
136 } else {
137 negotiator = new Negotiator(request);
138 found_locale = negotiator.language(cached_available_locales);
139
140 // If a locale couldn't be negotiated, use the default
141 found_locale = found_locale || default_locale_id;
142 }
143
144 // Send a locale to the browser
145 this.file_server.serveFile('/assets/locales/' + found_locale + '.json', 200, {
146 Vary: 'Accept-Language',
147 'Content-Language': found_locale
148 }, request, response);
149 }
150
151
152
153 /**
154 * Handle the settings.json request
155 */
156 function serveSettings(request, response) {
157 var referrer_url,
158 debug = false;
159
160 // Check the referrer for a debug option
161 if (request.headers.referer) {
162 referrer_url = url.parse(request.headers.referer, true);
163 if (referrer_url.query && referrer_url.query.debug) {
164 debug = true;
165 }
166 }
167
168 SettingsGenerator.get(debug, function(err, settings) {
169 if (err) {
170 winston.error('Error generating settings', err);
171 response.writeHead(500, 'Internal Server Error');
172 return response.end();
173 }
174
175 if (request.headers['if-none-match'] && request.headers['if-none-match'] === settings.hash) {
176 response.writeHead(304, 'Not Modified');
177 return response.end();
178 }
179
180 response.writeHead(200, {
181 'ETag': settings.hash,
182 'Content-Type': 'application/json'
183 });
184 response.end(settings.settings);
185 });
186 }