orator-static-server 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,89 +1,97 @@
1
- const FableServiceProviderBase = require('fable-serviceproviderbase');
1
+ /**
2
+ * Orator Static Server
3
+ *
4
+ * Static file serving for Orator API servers. Handles MIME type detection,
5
+ * route stripping, default files, magic subdomain folder mapping, and
6
+ * Content-Type headers so browsers render HTML instead of downloading it.
7
+ *
8
+ * @license MIT
9
+ *
10
+ * @author Steven Velozo <steven@velozo.com>
11
+ */
12
+
13
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
14
 
3
15
  const libServeStatic = require('serve-static');
4
16
  const libFinalHandler = require('finalhandler');
17
+ const libMime = require('mime');
5
18
 
6
19
  /**
7
- * Fable service that provides a simple static file server.
20
+ * @class OratorStaticServer
21
+ * @extends libFableServiceProviderBase
22
+ *
23
+ * A service provider that manages static file serving routes on an Orator instance.
8
24
  */
9
- class OratorStaticFileService extends FableServiceProviderBase
25
+ class OratorStaticServer extends libFableServiceProviderBase
10
26
  {
11
- /**
12
- * Construct a service instance.
13
- *
14
- * @param {object} pFable The fable instance for the application. Used for logging and settings.
15
- * @param {object} pOptions Custom settings for this service instance.
16
- * @param {string} pServiceHash The hash for this service instance.
17
- *
18
- * @return a static file service instance.
19
- */
20
27
  constructor(pFable, pOptions, pServiceHash)
21
28
  {
22
29
  super(pFable, pOptions, pServiceHash);
23
30
 
24
- if (typeof(this.options.proxyUrl) != 'string' || !this.options.proxyUrl.startsWith('http'))
25
- {
26
- this.log.trace('API proxy url falling back to settings...', { badUrl: this.options.proxyUrl });
27
- this.options.proxyUrl = this.fable.settings.APIProxyUrl;
28
- }
29
-
30
- // By jacking up the log level, the static server will become more and more communicative.
31
- this.logLevel = (`LogLevel` in this.options) ? this.options.LogLevel
32
- : `OratorStaticServerLogLevel` in this.fable.settings ? this.fable.settings.OratorStaticServerLogLevel
33
- : 0;
31
+ this.serviceType = 'OratorStaticServer';
34
32
 
35
- // The magic hosts option will look for the leftmost subdomain and use it as a subfolder.
36
- this.magicHostsEnabled = (`MagicHosts` in this.options) ? this.options.LogLevel
37
- : `OratorStaticServerMagicHosts` in this.fable.settings ? this.fable.settings.OratorStaticServerLogLevel
38
- : false;
33
+ // Keep track of registered static routes for introspection
34
+ this.routes = [];
39
35
 
40
- // The default folder to serve from
41
- this.defaultFolder = (`DefaultFolder` in this.options) ? this.options.DefaultFolder
42
- : (`OratorStaticServerDefaultFolder` in this.fable.settings) ? this.fable.settings.OratorStaticServerDefaultFolder
43
- : false;
36
+ // This is here because libMime has a breaking change from v1 to v2 and the lookup function was update to be getType per https://stackoverflow.com/a/60741078
37
+ // We don't want to introspect properties on this library every single time we need to check mime types.
38
+ // Therefore we are setting this boolean here and using it to branch.
39
+ this.oldLibMime = false;
40
+ if ('lookup' in libMime)
41
+ {
42
+ this.oldLibMime = true;
43
+ }
44
+ }
44
45
 
45
- // Whether or not to auto map the route
46
- this.autoMap = (`AutoMap` in this.options) ? this.options.AutoMap
47
- : (`OratorStaticServerAutoMap` in this.fable.settings) ? this.fable.settings.OratorStaticServerAutoMap
48
- : false;
46
+ /**
47
+ * Set the Content-Type header on a response based on the file name.
48
+ *
49
+ * @param {string} pFileName - The file name (or path) to detect MIME type from.
50
+ * @param {Object} pResponse - The HTTP response object.
51
+ */
52
+ setMimeHeader(pFileName, pResponse)
53
+ {
54
+ let tmpHeader;
49
55
 
50
- this.fable.instantiateServiceProviderIfNotExists('FilePersistence');
56
+ if (this.oldLibMime)
57
+ {
58
+ tmpHeader = libMime.lookup(pFileName);
59
+ }
60
+ else
61
+ {
62
+ tmpHeader = libMime.getType(pFileName);
63
+ }
51
64
 
52
- if (this.autoMap && this.defaultFolder)
65
+ if (!tmpHeader)
53
66
  {
54
- this.log.info(`Auto-mapping static route [/*] to files in ==> [${this.defaultFolder}] default file [index.html]`);
55
- this.addStaticRoute(this.defaultFolder);
67
+ tmpHeader = 'application/octet-stream';
56
68
  }
57
- }
58
69
 
70
+ pResponse.setHeader('Content-Type', tmpHeader);
71
+ }
59
72
 
60
73
  /**
61
- * Brought over from old orator and ported to work in the same way.
74
+ * Add a static file serving route to the Orator instance.
62
75
  *
63
- * @param {object} this.fable.Orator The Orator instance.
64
- * @param {string} pFilePath The path on disk that we are serving files from.
65
- * @param {string?} pDefaultFile (optional) The default file served if no specific file is requested.
66
- * @param {string?} pRoute (optional) The route matcher that will be used. Defaults to everything.
67
- * @param {string?} pRouteStrip (optional) If provided, this prefix will be removed from URL paths before being served.
68
- * @param {object?} pParams (optional) Additional parameters to pass to serve-static.
69
- * @return {boolean} true if the handler was successfully installed, otherwise false.
76
+ * @param {string} pFilePath - The path on disk to serve files from.
77
+ * @param {string} [pDefaultFile='index.html'] - The default file for directory requests.
78
+ * @param {string} [pRoute='/*'] - The route pattern to match.
79
+ * @param {string} [pRouteStrip='/'] - URL prefix to strip before filesystem lookup.
80
+ * @param {object} [pParams={}] - Additional parameters passed to serve-static.
81
+ * @returns {boolean} true if the route was successfully installed.
70
82
  */
71
83
  addStaticRoute(pFilePath, pDefaultFile, pRoute, pRouteStrip, pParams)
72
84
  {
73
- if (!'Orator' in this.fable)
74
- {
75
- this.fable.log.error('Orator must be initialized before adding a static route.');
76
- return false;
77
- }
78
- if (!'serviceServer' in this.fable.Orator)
85
+ if (!this.fable.Orator)
79
86
  {
80
- this.fable.log.error('Orator must have a service server initialized before adding a static route.');
87
+ this.log.error('OratorStaticServer requires an Orator instance to be registered with Fable.');
81
88
  return false;
82
89
  }
90
+
83
91
  if (typeof(pFilePath) !== 'string')
84
92
  {
85
- this.fable.log.error('A file path must be passed in as part of the server.');
86
- return false;
93
+ this.fable.log.error('A file path must be passed in as part of the server.');
94
+ return false;
87
95
  }
88
96
 
89
97
  // Default to just serving from root
@@ -93,19 +101,45 @@ class OratorStaticFileService extends FableServiceProviderBase
93
101
  // Default to serving index.html
94
102
  const tmpDefaultFile = (typeof(pDefaultFile) === 'undefined') ? 'index.html' : pDefaultFile;
95
103
 
104
+ let tmpOrator = this.fable.Orator;
105
+
96
106
  this.fable.log.info('Orator mapping static route to files: '+tmpRoute+' ==> '+pFilePath+' '+tmpDefaultFile);
97
107
 
98
- // Add the route
99
- this.fable.Orator.serviceServer.get(tmpRoute, (pRequest, pResponse, fNext) =>
108
+ // Ensure FilePersistence is available for the magic subdomain subfolder check
109
+ if (!this.fable.FilePersistence)
100
110
  {
101
- let servePath = pFilePath;
111
+ this.fable.serviceManager.instantiateServiceProvider('FilePersistence');
112
+ }
102
113
 
103
- if (this.magicHostsEnabled)
104
- {
114
+ // Try the service server's built-in serveStatic first (e.g. restify's serveStaticFiles plugin).
115
+ // This handles MIME types, caching headers, and streaming correctly without our manual intervention.
116
+ if (typeof(tmpOrator.serviceServer.serveStatic) === 'function')
117
+ {
118
+ let tmpServeStaticOptions = Object.assign({ directory: pFilePath, default: tmpDefaultFile }, pParams);
119
+ if (tmpOrator.serviceServer.serveStatic(tmpRoute, tmpServeStaticOptions))
120
+ {
121
+ this.routes.push(
122
+ {
123
+ filePath: pFilePath,
124
+ defaultFile: tmpDefaultFile,
125
+ route: tmpRoute,
126
+ routeStrip: tmpRouteStrip,
127
+ params: pParams || {}
128
+ });
129
+ return true;
130
+ }
131
+ }
132
+
133
+ // Fall back to the serve-static library approach (used by the IPC service server and other
134
+ // service servers that don't have a built-in serveStatic implementation).
135
+ tmpOrator.serviceServer.get(tmpRoute,
136
+ (pRequest, pResponse, fNext) =>
137
+ {
105
138
  // See if there is a magic subdomain put at the beginning of a request.
106
139
  // If there is, then we need to see if there is a subfolder and add that to the file path
107
140
  let tmpHostSet = pRequest.headers.host.split('.');
108
141
  let tmpPotentialSubfolderMagicHost = false;
142
+ let servePath = pFilePath;
109
143
  // Check if there are more than one host in the host header (this will be 127 a lot)
110
144
  if (tmpHostSet.length > 1)
111
145
  {
@@ -113,44 +147,45 @@ class OratorStaticFileService extends FableServiceProviderBase
113
147
  }
114
148
  if (tmpPotentialSubfolderMagicHost)
115
149
  {
116
- // Check if the subfolder exists
150
+ // Check if the subfolder exists -- this is only one dimensional for now
117
151
  let tmpPotentialSubfolder = servePath + tmpPotentialSubfolderMagicHost;
118
152
  if (this.fable.FilePersistence.libFS.existsSync(tmpPotentialSubfolder))
119
153
  {
120
154
  // If it does, then we need to add it to the file path
121
155
  servePath = `${tmpPotentialSubfolder}/`;
122
- if (this.logLevel > 1)
123
- {
124
- this.fable.log.trace(`Orator static magic mapped subdomain ${tmpPotentialSubfolderMagicHost}, altering servepath to [${servePath}]`);
125
- }
126
156
  }
127
157
  }
128
- }
129
-
130
- pRequest.url = pRequest.url.split('?')[0].substr(tmpRouteStrip.length) || '/';
131
- pRequest.path = function()
132
- {
133
- return pRequest.url;
134
- };
158
+ pRequest.url = pRequest.url.split('?')[0].substr(tmpRouteStrip.length) || '/';
159
+ pRequest.path = function()
160
+ {
161
+ return pRequest.url;
162
+ };
135
163
 
136
- if (this.logLevel > 0)
137
- {
138
- this.fable.log.trace(`Static request from host [${pRequest.headers.host}] URL [${pRequest.url}]`,
139
- {
140
- Host: pRequest.headers.host,
141
- UserAgent: pRequest.headers['user-agent'],
142
- Method: pRequest.method,
143
- ClientInterface: {Family: pRequest.connection.remoteFamily, Address: pRequest.connection.remoteAddress, Port: pRequest.connection.remotePort},
144
- ServerInterface: {Family: pRequest.connection.localFamily, Address: pRequest.connection.localAddress, Port: pRequest.connection.localPort},
145
- URL: pRequest.url
146
- }
147
- );
148
- }
149
- const tmpServe = libServeStatic(servePath, Object.assign({ index: tmpDefaultFile }, pParams));
150
- tmpServe(pRequest, pResponse, libFinalHandler(pRequest, pResponse));
151
- });
164
+ // When the URL is a directory (e.g. '/' or '/docs/'), use the default file for MIME detection
165
+ // so the browser gets text/html instead of application/octet-stream
166
+ let tmpMimeTarget = pRequest.url;
167
+ if (tmpMimeTarget.endsWith('/') || tmpMimeTarget.indexOf('.') < 0)
168
+ {
169
+ tmpMimeTarget = tmpDefaultFile;
170
+ }
171
+ this.setMimeHeader(tmpMimeTarget, pResponse);
172
+
173
+ const tmpServe = libServeStatic(servePath, Object.assign({ index: tmpDefaultFile }, pParams));
174
+ tmpServe(pRequest, pResponse, libFinalHandler(pRequest, pResponse));
175
+ // TODO: This may break things if a post request send handler is setup...
176
+ //fNext();
177
+ });
178
+
179
+ this.routes.push(
180
+ {
181
+ filePath: pFilePath,
182
+ defaultFile: tmpDefaultFile,
183
+ route: tmpRoute,
184
+ routeStrip: tmpRouteStrip,
185
+ params: pParams || {}
186
+ });
152
187
  return true;
153
188
  }
154
189
  }
155
190
 
156
- module.exports = OratorStaticFileService;
191
+ module.exports = OratorStaticServer;