UNPKG

8.85 kBJavaScriptView Raw
1/**
2 * @license
3 * MOST Web Framework 2.0 Codename Blueshift
4 * Copyright (c) 2017, THEMOST LP All rights reserved
5 *
6 * Use of this source code is governed by an BSD-3-Clause license that can be
7 * found in the LICENSE file at https://themost.io/license
8 */
9
10var HttpNotFoundError = require('@themost/common/errors').HttpNotFoundError;
11var HttpServerError = require('@themost/common/errors').HttpServerError;
12var HttpForbiddenError = require('@themost/common/errors').HttpForbiddenError;
13var TraceUtils = require('@themost/common/utils').TraceUtils;
14var fs = require('fs');
15var url = require('url');
16var path = require("path");
17var crypto = require('crypto');
18var _ = require('lodash');
19/**
20 * Static File Handler
21 * @class
22 * @implements ValidateRequestHandler
23 * @implements MapRequestHandler
24 * @implements PostMapRequestHandler
25 * @implements ProcessRequestHandler
26 * @param {string=} rootDir
27 * @constructor
28 */
29function StaticHandler(rootDir) {
30 this.rootDir = rootDir;
31}
32
33StaticHandler.prototype.validateRequest = function(context, callback) {
34 callback = callback || function() {};
35 callback.call(context);
36};
37
38/*
39 * Maps the current request handler with the underlying HTTP request.
40 * */
41StaticHandler.prototype.mapRequest = function(context, callback)
42{
43 callback = callback || function() {};
44 try {
45 if (typeof this.rootDir !== 'string') {
46 return callback();
47 }
48 if (_.isEmpty(this.rootDir)) {
49 return callback();
50 }
51 //get file path
52 var uri = url.parse(context.request.url);
53 var filePath = path.join(this.rootDir ,uri.pathname);
54 fs.exists(filePath, function(exists) {
55 if (!exists) {
56 callback(null);
57 }
58 else {
59 fs.stat(filePath, function(err, stats) {
60 if (err) {
61 callback(err);
62 }
63 else {
64 //if file exists
65 if (stats && stats.isFile()) {
66 //set request current handler
67 context.request.currentHandler = new StaticHandler();
68 //set current execution path
69 context.request.currentExecutionPath = filePath;
70 //set file stats
71 context.request.currentExecutionFileStats = stats;
72 }
73 callback(null);
74 }
75 });
76 }
77 });
78 } catch (e) {
79 callback(e);
80 }
81};
82
83/**
84 *
85 * @param {HttpContext} context
86 * @param {string} executionPath
87 * @param {Function} callback
88 */
89StaticHandler.prototype.unmodifiedRequest = function(context, executionPath, callback) {
90 try {
91 var requestETag = context.request.headers['if-none-match'];
92 if (typeof requestETag === 'undefined' || requestETag === null) {
93 callback(null, false);
94 return;
95 }
96 fs.exists(executionPath, function(exists) {
97 try {
98 if (exists) {
99 fs.stat(executionPath, function(err, stats) {
100 if (err) {
101 callback(err);
102 }
103 else {
104 if (!stats.isFile()) {
105 callback(null, false);
106 }
107 else {
108 //validate if-none-match
109 var md5 = crypto.createHash('md5');
110 md5.update(stats.mtime.toString());
111 var responseETag = md5.digest('base64');
112 return callback(null, (requestETag===responseETag));
113 }
114 }
115 });
116 }
117 else {
118 callback(null, false);
119 }
120 }
121 catch (err) {
122 TraceUtils.error(err);
123 callback(null, false);
124 }
125 });
126 }
127 catch (err) {
128 TraceUtils.error(err);
129 callback(null, false);
130 }
131};
132
133StaticHandler.prototype.preflightRequest = function(context, callback) {
134 try {
135
136 if (context && (context.request.currentHandler instanceof StaticHandler)) {
137 var headerNames = context.response["_headerNames"] || { };
138 if (typeof headerNames["access-control-allow-origin"] === 'undefined') {
139 if (context.request.headers.origin) {
140 context.response.setHeader("Access-Control-Allow-Origin", context.request.headers.origin);
141 }
142 else {
143 context.response.setHeader("Access-Control-Allow-Origin", "*");
144 }
145 }
146 if (typeof headerNames["access-control-allow-headers"] === 'undefined')
147 context.response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Content-Language, Accept, Accept-Language, Authorization");
148 if (typeof headerNames["access-control-allow-credentials"] === 'undefined')
149 context.response.setHeader("Access-Control-Allow-Credentials", "true");
150 if (typeof headerNames["access-control-allow-methods"] === 'undefined')
151 context.response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
152 }
153 return callback();
154 }
155 catch(e) {
156 callback(e);
157 }
158
159};
160
161StaticHandler.prototype.postMapRequest = function(context, callback) {
162 return StaticHandler.prototype.preflightRequest.call(this, context, callback);
163};
164
165/**
166 * @param {HttpContext} context
167 * @param {Function} callback
168 */
169StaticHandler.prototype.processRequest = function(context, callback)
170{
171 callback = callback || function() {};
172 try {
173 if (context.is('OPTIONS')) {
174 //do nothing
175 return callback();
176 }
177 //get current execution path and validate once again file presence and MIME type
178 var stats = context.request.currentExecutionFileStats;
179 if (typeof stats === 'undefined' || stats === null) {
180 callback(new HttpServerError('Invalid request handler.'));
181 return;
182 }
183 if (!stats.isFile()) {
184 return callback(new HttpNotFoundError());
185 }
186 else {
187 //get if-none-match header
188 var requestETag = context.request.headers['if-none-match'];
189 //generate responseETag
190 var md5 = crypto.createHash('md5');
191 md5.update(stats.mtime.toString());
192 var responseETag = md5.digest('base64');
193 if (requestETag)
194 if (requestETag===responseETag) {
195 //context.response.writeHead(304, { 'Last-Modified':stats.mtime.toUTCString() });
196 context.response.writeHead(304, { });
197 context.response.end();
198 return callback.call(context);
199 }
200 //get file extension
201 var extensionName = path.extname(context.request.currentExecutionPath);
202 //get MIME collection
203 var mimes = context.getApplication().getConfiguration().mimes;
204 var contentType = null, contentEncoding=null;
205 //find MIME type by extension
206 var mime =mimes.filter(function(x) { return x.extension===extensionName; })[0];
207 if (mime) {
208 contentType = mime.type;
209 if (mime.encoding)
210 contentEncoding = mime.encoding;
211 }
212 //throw exception (MIME not found or access denied)
213 if (contentType===null) {
214 callback(new HttpForbiddenError())
215 }
216 else {
217 //create stream
218 var source = fs.createReadStream(context.request.currentExecutionPath);
219 //write headers
220 context.response.writeHead(200, {'Content-Type': contentType + (contentEncoding ? ';charset=' + contentEncoding : ''), 'ETag' : responseETag});
221 //response file
222 source.pipe(context.response);
223 //handle end
224 source.on('end', function() {
225 callback();
226 });
227 //handle error
228 source.on('error', function(err) {
229 callback(err);
230 });
231 }
232 }
233 }
234 catch (e) {
235 callback.call(context, e);
236 }
237};
238
239
240if (typeof exports !== 'undefined') {
241 module.exports.StaticHandler = StaticHandler;
242 module.exports.createInstance = function() {
243 return new StaticHandler();
244 };
245}
246
247