1 | /*!
|
2 | * connect
|
3 | * Copyright(c) 2010 Sencha Inc.
|
4 | * Copyright(c) 2011 TJ Holowaychuk
|
5 | * Copyright(c) 2015 Douglas Christopher Wilson
|
6 | * Copyright(c) 2020 Ilya A. Zimnovich
|
7 | * MIT Licensed
|
8 | */
|
9 |
|
10 | ;
|
11 |
|
12 | /**
|
13 | * Module dependencies.
|
14 | * @private
|
15 | */
|
16 |
|
17 | const EventEmitter = require("events").EventEmitter;
|
18 | const http = require("http");
|
19 |
|
20 | const debug = require("debug")("connect:dispatcher");
|
21 | const finalhandler = require("finalhandler");
|
22 | const parseUrl = require("parseurl");
|
23 |
|
24 | // ########################################################################
|
25 | // Helpers
|
26 | // ########################################################################
|
27 |
|
28 | // Merge object's properties
|
29 | // From: https://www.npmjs.com/package/utils-merge
|
30 | const merge = function(dst, src) {
|
31 | if (dst && src) {
|
32 | for (const key in src) {
|
33 | dst[key] = src[key];
|
34 | }
|
35 | }
|
36 | return dst;
|
37 | };
|
38 |
|
39 | // Merge objects using descriptors
|
40 | // From: https://www.npmjs.com/package/merge-descriptors
|
41 | const mixin = function(dst, src) {
|
42 | if (dst && src) {
|
43 | Object.getOwnPropertyNames(src).forEach(function(name) {
|
44 | if (!dst.hasOwnProperty(name)) {
|
45 | const descriptor = Object.getOwnPropertyDescriptor(src, name);
|
46 | Object.defineProperty(dst, name, descriptor);
|
47 | }
|
48 | });
|
49 | }
|
50 | return dst;
|
51 | };
|
52 |
|
53 | // This method is used to break up long running operations and run
|
54 | // a callback function immediately after the node.js has completed
|
55 | // other operations such as events and display updates.
|
56 | //
|
57 | // Syntax:
|
58 | // - var immediateID = setImmediate(func, [param1, param2, ...]);
|
59 | const defer = (typeof setImmediate === "function")
|
60 | ? setImmediate
|
61 | : function(func) { process.nextTick(func.bind.apply(func, arguments)) };
|
62 |
|
63 | // Private module variables.
|
64 | const env = process.env.NODE_ENV || "development";
|
65 | const core = {};
|
66 |
|
67 | /**
|
68 | * Create a new connect server.
|
69 | *
|
70 | * @return {function}
|
71 | * @public
|
72 | */
|
73 |
|
74 | function createServer()
|
75 | {
|
76 | const app = function (req, res, next) {
|
77 | app.handle(req, res, next);
|
78 | }
|
79 |
|
80 | merge(app, EventEmitter.prototype);
|
81 | merge(app, core);
|
82 |
|
83 | // expose the prototype that will get set on requests
|
84 | // app.request = Object.create(req, {
|
85 | // app: { configurable: true, enumerable: true, writable: true, value: app }
|
86 | // });
|
87 |
|
88 | // expose the prototype that will get set on responses
|
89 | // app.response = Object.create(res, {
|
90 | // app: { configurable: true, enumerable: true, writable: true, value: app }
|
91 | // });
|
92 |
|
93 | app.route = "/";
|
94 | app.stack = [];
|
95 |
|
96 | return app;
|
97 | }
|
98 |
|
99 | /**
|
100 | * Utilize the given middleware `handle` to the given `route`,
|
101 | * defaulting to _/_. This "route" is the mount-point for the
|
102 | * middleware, when given a value other than _/_ the middleware
|
103 | * is only effective when that segment is present in the request's
|
104 | * pathname.
|
105 | *
|
106 | * For example if we were to mount a function at _/admin_, it would
|
107 | * be invoked on _/admin_, and _/admin/settings_, however it would
|
108 | * not be invoked for _/_, or _/posts_.
|
109 | *
|
110 | * @param {String|Function|Server} route, callback or server
|
111 | * @param {Function|Server} callback or server
|
112 | * @return {Server} for chaining
|
113 | * @public
|
114 | */
|
115 |
|
116 | core.use = function use(route, handler)
|
117 | {
|
118 | // no route is given
|
119 | // default route to "/"
|
120 | if (typeof route !== "string") {
|
121 | handler = route;
|
122 | route = "/";
|
123 | }
|
124 |
|
125 | // wrap sub-apps
|
126 | // (objects with method "handle")
|
127 | if (typeof handler.handle === "function") {
|
128 | let server = handler;
|
129 | server.route = route;
|
130 | handler = function (req, res, next) {
|
131 | server.handle(req, res, next);
|
132 | };
|
133 | }
|
134 |
|
135 | // wrap vanilla http.Servers
|
136 | if (handler instanceof http.Server) {
|
137 | handler = handler.listeners("request")[0];
|
138 | }
|
139 |
|
140 | // strip trailing slash
|
141 | if (route[route.length - 1] === "/") {
|
142 | route = route.slice(0, -1);
|
143 | }
|
144 |
|
145 | // add the middleware
|
146 | debug("use %s %s", route || "/", handler.name || "anonymous");
|
147 | this.stack.push({
|
148 | route: route,
|
149 | handler: handler
|
150 | });
|
151 |
|
152 | return this;
|
153 | };
|
154 |
|
155 | /**
|
156 | * Handle server requests, punting them down
|
157 | * the middleware stack.
|
158 | *
|
159 | * @private
|
160 | */
|
161 |
|
162 | core.handle = function handle(req, res, out)
|
163 | {
|
164 | var index = 0; // index of current handler
|
165 | var protohost = getProtohost(req.url) || "";
|
166 | var removed = "";
|
167 | var slashAdded = false;
|
168 | var stack = this.stack;
|
169 |
|
170 | // final function handler
|
171 | var done = out || finalhandler(req, res, {
|
172 | env: env,
|
173 | onerror: logerror
|
174 | });
|
175 |
|
176 | // store the original URL
|
177 | req.originalUrl = req.originalUrl || req.url;
|
178 |
|
179 | function next(err)
|
180 | {
|
181 | if (slashAdded) {
|
182 | req.url = req.url.substr(1);
|
183 | slashAdded = false;
|
184 | }
|
185 |
|
186 | if (removed.length !== 0) {
|
187 | req.url = protohost + removed + req.url.substr(protohost.length);
|
188 | removed = "";
|
189 | }
|
190 |
|
191 | // next callback
|
192 | var layer = stack[index++];
|
193 |
|
194 | // all done
|
195 | if (!layer) {
|
196 | defer(done, err);
|
197 | return;
|
198 | }
|
199 |
|
200 | // route data
|
201 | var path = parseUrl(req).pathname || "/";
|
202 | var route = layer.route;
|
203 |
|
204 | // skip this layer if the route doesn't match
|
205 | if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
|
206 | return next(err);
|
207 | }
|
208 |
|
209 | // skip if route match does not border "/", ".", or end
|
210 | var c = path.length > route.length && path[route.length];
|
211 | if (c && c !== "/" && c !== ".") {
|
212 | return next(err);
|
213 | }
|
214 |
|
215 | // trim off the part of the url that matches the route
|
216 | if (route.length !== 0 && route !== "/") {
|
217 | removed = route;
|
218 | req.url = protohost + req.url.substr(protohost.length + removed.length);
|
219 |
|
220 | // ensure leading slash
|
221 | if (!protohost && req.url[0] !== "/") {
|
222 | req.url = "/" + req.url;
|
223 | slashAdded = true;
|
224 | }
|
225 | }
|
226 |
|
227 | // call the layer handle
|
228 | call(layer.handler, route, err, req, res, next);
|
229 | }
|
230 |
|
231 | next();
|
232 | };
|
233 |
|
234 | /**
|
235 | * Listen for connections.
|
236 | *
|
237 | * This method takes the same arguments
|
238 | * as node's `http.Server#listen()`.
|
239 | *
|
240 | * HTTP and HTTPS:
|
241 | *
|
242 | * If you run your application both as HTTP
|
243 | * and HTTPS you may wrap them individually,
|
244 | * since your Connect "server" is really just
|
245 | * a JavaScript `Function`.
|
246 | *
|
247 | * var connect = require("connect")
|
248 | * , http = require("http")
|
249 | * , https = require("https");
|
250 | *
|
251 | * var app = connect();
|
252 | *
|
253 | * http.createServer(app).listen(80);
|
254 | * https.createServer(options, app).listen(443);
|
255 | *
|
256 | * @return {http.Server}
|
257 | * @api public
|
258 | */
|
259 |
|
260 | core.listen = function listen() {
|
261 | let server = http.createServer(this);
|
262 | return server.listen.apply(server, arguments);
|
263 | };
|
264 |
|
265 | /**
|
266 | * Invoke a route handle.
|
267 | * @private
|
268 | */
|
269 |
|
270 | function call(handle, route, err, req, res, next)
|
271 | {
|
272 | var arity = handle.length;
|
273 | var error = err;
|
274 | var hasError = Boolean(err);
|
275 |
|
276 | debug("%s %s : %s", handle.name || "<anonymous>", route, req.originalUrl);
|
277 |
|
278 | try {
|
279 | if (hasError && arity === 4) {
|
280 | // error-handling middleware
|
281 | handle(err, req, res, next);
|
282 | return;
|
283 | } else if (!hasError && arity < 4) {
|
284 | // request-handling middleware
|
285 | handle(req, res, next);
|
286 | return;
|
287 | }
|
288 | } catch (e) {
|
289 | // replace the error
|
290 | error = e;
|
291 | }
|
292 |
|
293 | // continue
|
294 | next(error);
|
295 | }
|
296 |
|
297 | /**
|
298 | * Log error using console.error.
|
299 | *
|
300 | * @param {Error} err
|
301 | * @private
|
302 | */
|
303 |
|
304 | function logerror(err) {
|
305 | if (env !== "test") console.error(err.stack || err.toString());
|
306 | }
|
307 |
|
308 | /**
|
309 | * Get get protocol + host for a URL.
|
310 | *
|
311 | * @param {string} url
|
312 | * @private
|
313 | */
|
314 |
|
315 | function getProtohost(url)
|
316 | {
|
317 | if (url.length === 0 || url[0] === "/") {
|
318 | return undefined;
|
319 | }
|
320 |
|
321 | var fqdnIndex = url.indexOf("://")
|
322 |
|
323 | return fqdnIndex !== -1 && url.lastIndexOf("?", fqdnIndex) === -1
|
324 | ? url.substr(0, url.indexOf("/", 3 + fqdnIndex))
|
325 | : undefined;
|
326 | }
|
327 |
|
328 | /**
|
329 | * Module exports.
|
330 | * @public
|
331 | */
|
332 |
|
333 | module.exports = createServer;
|