UNPKG

8.08 kBJavaScriptView Raw
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"use strict";
11
12/**
13 * Module dependencies.
14 * @private
15 */
16
17const EventEmitter = require("events").EventEmitter;
18const http = require("http");
19
20const debug = require("debug")("connect:dispatcher");
21const finalhandler = require("finalhandler");
22const parseUrl = require("parseurl");
23
24// ########################################################################
25// Helpers
26// ########################################################################
27
28// Merge object's properties
29// From: https://www.npmjs.com/package/utils-merge
30const 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
41const 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, ...]);
59const defer = (typeof setImmediate === "function")
60 ? setImmediate
61 : function(func) { process.nextTick(func.bind.apply(func, arguments)) };
62
63// Private module variables.
64const env = process.env.NODE_ENV || "development";
65const core = {};
66
67/**
68 * Create a new connect server.
69 *
70 * @return {function}
71 * @public
72 */
73
74function 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
116core.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
162core.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
260core.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
270function 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
304function 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
315function 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
333module.exports = createServer;