sprucehttp_sjs 2.0.0 → 2.2.0

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.
Files changed (4) hide show
  1. package/README.md +82 -12
  2. package/index.js +126 -18
  3. package/package.json +1 -1
  4. package/types.ts +156 -0
package/README.md CHANGED
@@ -10,6 +10,7 @@ A module for responding to requests within SpruceHTTP in an SJS file.
10
10
  - [writeData(data)](#writedatadata)
11
11
  - [writeDataAsync(data)](#writedataasyncdata)
12
12
  - [clearResponse()](#clearresponse)
13
+ - [end()](#end)
13
14
  - [siteConfig](#siteconfig)
14
15
  - [requestIP](#requestip)
15
16
  - [method](#method)
@@ -23,6 +24,9 @@ A module for responding to requests within SpruceHTTP in an SJS file.
23
24
  - [body](#body)
24
25
  - [bodyStream([options])](#bodystreamoptions)
25
26
  - [mTlsPeerCertificate](#mtlspeercertificate)
27
+ - [requestObject](#requestobject)
28
+ - [updateConfig(newConfig)](#updateconfignewconfig)
29
+ - [modifyRequest(newRequest)](#modifyrequestnewrequest)
26
30
 
27
31
  ## Documentation:
28
32
  Importing the module:
@@ -35,8 +39,10 @@ Sets the response into stream mode. This cannot be undone for the duration of th
35
39
 
36
40
  Stream mode means that responses are not buffered by the webserver, and thus are sent immediately as they're received from the SJS script. This means that the status line and headers _**must**_ be sent first before the data, and the ``\r\n`` after the headers must be sent by the script as well.
37
41
 
42
+ Returns a Promise that resolves when the server acknowledges the switch to stream mode.
43
+
38
44
  ```js
39
- sjs.streamMode(); // Set the response to stream mode.
45
+ await sjs.streamMode(); // Set the response to stream mode.
40
46
  ```
41
47
 
42
48
  ### ``writeStatusLine(statusCode[, reasonPhrase])``:
@@ -45,14 +51,16 @@ sjs.streamMode(); // Set the response to stream mode.
45
51
 
46
52
  Writes the status line to the response.
47
53
 
54
+ Returns a Promise that resolves when the server acknowledges the status line.
55
+
48
56
  To send a 200 "OK" response, without needing to specify the reason phrase.
49
57
  ```js
50
- sjs.writeStatusLine(200);
58
+ await sjs.writeStatusLine(200);
51
59
  ```
52
60
 
53
61
  To send a 418 "I'm a teapot" response, with a specified reason phrase.
54
62
  ```js
55
- sjs.writeStatusLine(418, "I'm a teapot");
63
+ await sjs.writeStatusLine(418, "I'm a teapot");
56
64
  ```
57
65
 
58
66
  ### ``writeHeader(name, value)``:
@@ -61,9 +69,11 @@ sjs.writeStatusLine(418, "I'm a teapot");
61
69
 
62
70
  Writes a header to the response.
63
71
 
72
+ Returns a Promise that resolves when the server acknowledges the header.
73
+
64
74
  To write the "Content-Type: text/html" header.
65
75
  ```js
66
- sjs.writeHeader("Content-Type", "text/html");
76
+ await sjs.writeHeader("Content-Type", "text/html");
67
77
  ```
68
78
 
69
79
  ### ``writeData(data)``:
@@ -89,6 +99,8 @@ sjs.writeData(fs.readFileSync("image.png", 'binary'));
89
99
 
90
100
  Writes data (the body) to the response asynchronously. When not in stream mode, the Content-Length header is automatically updated.
91
101
 
102
+ Returns a Promise that resolves when the data has been written.
103
+
92
104
  Writes text data to the body.
93
105
  ```js
94
106
  await sjs.writeDataAsync("Hello, world!");
@@ -104,23 +116,40 @@ await sjs.writeDataAsync(fs.readFileSync("image.png", 'binary'));
104
116
  Clears the currently written response and resets the status line, headers, and body.
105
117
  This is useful when you want to send a different response than the one you have already written.
106
118
 
119
+ Returns a Promise that resolves when the response has been cleared.
120
+
107
121
  This function does not work in stream mode.
108
122
 
109
123
  ```js
110
- sjs.writeStatusLine(418, "I'm a teapot");
111
- sjs.writeHeader("Content-Type", "text/html");
112
- sjs.writeData("<h1>I'm a teapot</h1>");
124
+ await sjs.writeStatusLine(418, "I'm a teapot");
125
+ await sjs.writeHeader("Content-Type", "text/html");
126
+ await sjs.writeData("<h1>I'm a teapot</h1>");
113
127
  // Be serious
114
- sjs.clearResponse();
128
+ await sjs.clearResponse();
115
129
 
116
- sjs.writeStatusLine(200);
117
- sjs.writeHeader("Content-Type", "text/html");
118
- sjs.writeData("<h1>I'm <i>not</i> a teapot</h1>");
130
+ await sjs.writeStatusLine(200);
131
+ await sjs.writeHeader("Content-Type", "text/html");
132
+ await sjs.writeData("<h1>I'm <i>not</i> a teapot</h1>");
133
+ ```
134
+
135
+ ### ``end()``:
136
+ Ends the response. Responses are automatically ended when the SJS script exits, but this function allows you to end the response early. This is useful if you want to stop sending data before the script has finished executing.
137
+
138
+ Returns a Promise that resolves when the response has been ended.
139
+
140
+ ```js
141
+ await sjs.writeStatusLine(200);
142
+ await sjs.writeHeader("Content-Type", "text/html");
143
+ await sjs.writeData("<h1>Hello, world!</h1>");
144
+ await sjs.end(); // End the response early
145
+ // Any further writes will be ignored
119
146
  ```
120
147
 
121
148
  ### ``siteConfig``:
122
149
  Returns the site-specific configuration set in your config file.
123
150
 
151
+ Returns an object representing the site configuration.
152
+
124
153
  ```js
125
154
  console.log(sjs.siteConfig);
126
155
  /*
@@ -148,7 +177,7 @@ Returns the requestor's IP address.
148
177
 
149
178
  ```js
150
179
  console.log(sjs.requestIP);
151
- // ::ffff:127.0.0.1
180
+ // ::ffff:198.51.100.163
152
181
  ```
153
182
 
154
183
  ### ``method``:
@@ -257,4 +286,45 @@ Returns details about the mTLS peer's certificate, if present.
257
286
  ```js
258
287
  console.log(sjs.mTlsPeerCertificate.subject.CN);
259
288
  // sprucehttp.com
289
+ ```
290
+
291
+ ### ``requestObject``:
292
+ Returns the full request object.
293
+
294
+ ```js
295
+ console.log(sjs.requestObject);
296
+ // {
297
+ // "version": "HTTP/1.1",
298
+ // ... rest of request object ...
299
+ // }
300
+ ```
301
+
302
+ ### ``updateConfig(newConfig)``:
303
+ - ``newConfig:`` object: The new site configuration to set.
304
+
305
+ Updates the site-specific configuration for the request. You must provide a complete configuration object.
306
+
307
+ This is only effective during a pre-request hook.
308
+
309
+ Returns a Promise that resolves when the configuration has been updated.
310
+
311
+ ```js
312
+ sjs.updateConfig({
313
+ // New configuration object
314
+ });
315
+ ```
316
+
317
+ ### ``modifyRequest(newRequest)``:
318
+ - ``newRequest:`` object: The new request object to set.
319
+
320
+ Modifies the request object for the request. You must provide a complete request object.
321
+
322
+ This is only effective during a pre-request hook.
323
+
324
+ Returns a Promise that resolves when the request has been modified.
325
+
326
+ ```js
327
+ sjs.modifyRequest({
328
+ // New request object
329
+ });
260
330
  ```
package/index.js CHANGED
@@ -1,16 +1,37 @@
1
1
  // This is still CommonJS to support both the old and new versions of the module system.
2
2
  // eslint-disable-next-line @typescript-eslint/no-require-imports
3
- const fs = require("fs");
3
+ const fs = require("node:fs");
4
+
5
+ /**
6
+ * Wait until the server tells us that it has processed the last sent message.
7
+ *
8
+ * @returns {Promise<void>} A Promise that resolves when the server has processed the last message.
9
+ */
10
+ function waitUntilProcessed() {
11
+ return new Promise(function (resolve) {
12
+ process.once("message", function (message) {
13
+ if (message === "ready") {
14
+ resolve();
15
+ }
16
+ });
17
+ });
18
+ }
4
19
 
5
20
  module.exports = {
6
21
  /**
7
22
  * Sets the response into stream mode.
8
23
  *
9
24
  * Stream mode means that responses are not buffered by the webserver.
25
+ * @returns {Promise<void>} A Promise that resolves when the server acknowledges the switch to stream mode.
10
26
  */
11
27
  streamMode() {
12
- process.send({
13
- type: "streamMode"
28
+ return new Promise(function (resolve) {
29
+ process.send({
30
+ type: "streamMode"
31
+ }, async () => {
32
+ await waitUntilProcessed();
33
+ resolve();
34
+ });
14
35
  });
15
36
  },
16
37
 
@@ -18,12 +39,18 @@ module.exports = {
18
39
  * Writes the status line to the response.
19
40
  * @param {number} statusCode The HTTP status code to send.
20
41
  * @param {string} [reasonPhrase] The reason phrase to send.
42
+ * @return {Promise<void>} A Promise that resolves when the server acknowledges the status line.
21
43
  */
22
44
  writeStatusLine(statusCode, reasonPhrase) {
23
- process.send({
24
- type: "status",
25
- statusCode,
26
- reasonPhrase
45
+ return new Promise(function (resolve) {
46
+ process.send({
47
+ type: "status",
48
+ statusCode,
49
+ reasonPhrase
50
+ }, async () => {
51
+ await waitUntilProcessed();
52
+ resolve();
53
+ });
27
54
  });
28
55
  },
29
56
 
@@ -31,12 +58,18 @@ module.exports = {
31
58
  * Writes a header to the response.
32
59
  * @param {string} name The name of the header to send.
33
60
  * @param {string} value The value of the header to send.
61
+ * @return {Promise<void>} A Promise that resolves when the server acknowledges the header.
34
62
  */
35
63
  writeHeader(name, value) {
36
- process.send({
37
- type: "header",
38
- name,
39
- value
64
+ return new Promise(function (resolve) {
65
+ process.send({
66
+ type: "header",
67
+ name,
68
+ value
69
+ }, async () => {
70
+ await waitUntilProcessed();
71
+ resolve();
72
+ });
40
73
  });
41
74
  },
42
75
 
@@ -45,7 +78,8 @@ module.exports = {
45
78
  *
46
79
  * When not in stream mode, the Content-Length header is automatically updated.
47
80
  * @param {string | Buffer} data The data to send.
48
- * @deprecated Use writeDataAsync instead. Data cannot be reliably sent synchronously.
81
+ * @returns {void}
82
+ * @deprecated Use writeDataAsync instead. Data cannot be reliably sent synchronously. Will be removed in a future major version.
49
83
  */
50
84
  writeData(data) {
51
85
  (async () => { await this.writeDataAsync(data); })();
@@ -54,8 +88,11 @@ module.exports = {
54
88
  /**
55
89
  * Writes data (the body) to the response asynchronously.
56
90
  *
91
+ * Will be renamed to writeData in a future major version.
92
+ *
57
93
  * When not in stream mode, the Content-Length header is automatically updated.
58
94
  * @param {string | Buffer} data The data to send.
95
+ * @returns {Promise<void>} A Promise that resolves when the data has been written.
59
96
  */
60
97
  writeDataAsync(data) {
61
98
  // eslint-disable-next-line no-async-promise-executor
@@ -74,7 +111,10 @@ module.exports = {
74
111
  process.send({
75
112
  type: "data",
76
113
  data: chunk
77
- }, resolve2);
114
+ }, async () => {
115
+ await waitUntilProcessed();
116
+ resolve2();
117
+ });
78
118
  });
79
119
  }
80
120
  resolve();
@@ -86,17 +126,38 @@ module.exports = {
86
126
  * This is useful when you want to send a different response than the one you have already written.
87
127
  *
88
128
  * This function does not work in stream mode.
129
+ * @returns {Promise<void>} A Promise that resolves when the response has been cleared.
89
130
  */
90
131
  clearResponse() {
91
- process.send({
92
- type: "clear"
132
+ return new Promise(function (resolve) {
133
+ process.send({
134
+ type: "clear"
135
+ }, async () => {
136
+ await waitUntilProcessed();
137
+ resolve();
138
+ });
139
+ });
140
+ },
141
+
142
+ /**
143
+ * Ends the response. This will not terminate the SJS script; it will continue to run until completion.
144
+ * @returns {Promise<void>}
145
+ */
146
+ end() {
147
+ return new Promise(function (resolve) {
148
+ process.send({
149
+ type: "end"
150
+ }, async () => {
151
+ await waitUntilProcessed();
152
+ resolve();
153
+ });
93
154
  });
94
155
  },
95
156
 
96
157
  /**
97
158
  * Returns the site-specific configuration.
98
159
  *
99
- * @returns {{[key: string]: string}} The site-specific configuration.
160
+ * @returns {import("./types").SiteConfig} The site-specific configuration.
100
161
  */
101
162
  get siteConfig() {
102
163
  return JSON.parse(process.env.siteConfig);
@@ -201,7 +262,7 @@ module.exports = {
201
262
  /**
202
263
  * Returns the body of the request as an fs.ReadStream.
203
264
  *
204
- * @param {BufferEncoding | ReadStreamOptions} [options] The options to pass to fs.createReadStream.
265
+ * @param {BufferEncoding | fs.ReadStreamOptions} [options] The options to pass to fs.createReadStream.
205
266
  * @returns {fs.ReadStream} A ReadStream of the body of the request.
206
267
  */
207
268
  bodyStream(options) {
@@ -215,7 +276,7 @@ module.exports = {
215
276
  /**
216
277
  * Returns details about the mTLS peer's certificate, if present.
217
278
  *
218
- * @returns {PeerCertificate}
279
+ * @returns {import("node:tls").PeerCertificate}
219
280
  */
220
281
  get mTlsPeerCertificate() {
221
282
  let retVal = undefined;
@@ -225,5 +286,52 @@ module.exports = {
225
286
  if (retVal.pubkey) retVal.pubkey = Buffer.from(retVal.pubkey, "base64");
226
287
  }
227
288
  return retVal;
289
+ },
290
+
291
+ /**
292
+ * Returns the full request object.
293
+ *
294
+ * @returns {import("./types").HTTPRequest} The full request object.
295
+ */
296
+ get requestObject() {
297
+ return JSON.parse(process.env.requestObject);
298
+ },
299
+
300
+ /**
301
+ * Updates the site-specific configuration for the request. You must provide a complete configuration object.
302
+ *
303
+ * This is only effective during a pre-request hook.
304
+ * @param {import("./types").SiteConfig} newConfig The new site configuration to set.
305
+ * @return {Promise<void>} A Promise that resolves when the configuration has been updated.
306
+ */
307
+ updateConfig(newConfig) {
308
+ return new Promise(function (resolve) {
309
+ process.send({
310
+ type: "siteconfigmodify",
311
+ siteConfig: newConfig
312
+ }, async () => {
313
+ await waitUntilProcessed();
314
+ resolve();
315
+ });
316
+ });
317
+ },
318
+
319
+ /**
320
+ * Modifies the request object. You must provide a complete request object.
321
+ *
322
+ * This is only effective during a pre-request hook.
323
+ * @param {import("./types").HTTPRequest} newRequest The new request object to set.
324
+ * @return {Promise<void>} A Promise that resolves when the request has been modified.
325
+ */
326
+ modifyRequest(newRequest) {
327
+ return new Promise(function (resolve) {
328
+ process.send({
329
+ type: "requestmodify",
330
+ request: newRequest
331
+ }, async () => {
332
+ await waitUntilProcessed();
333
+ resolve();
334
+ });
335
+ });
228
336
  }
229
337
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprucehttp_sjs",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "A module for responding to requests within SpruceHTTP in an SJS file",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/types.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { TlsOptions } from "node:tls";
2
+
3
+ /**
4
+ * Represents a site configuration.
5
+ * @property {string} type The type of configuration.
6
+ * @property {string} location The path to the root of the site.
7
+ * @property {string[]} [indexList] The list of files to check for when serving a directory.
8
+ * @property {boolean} [upgradeInsecure] Require HTTPS for all requests.
9
+ * @property {boolean} [directoryListing] Whether to serve directory listings.
10
+ * @property {number} [maxBodySize] The maximum body size in bytes to accept.
11
+ * @property {boolean} [sjs] Whether to execute SJS files.
12
+ * @property {Record<string, string>} [headers] The headers to send with the response.
13
+ * @property {TlsOptions} [ssl] The TLS options to use.
14
+ * @property {boolean} [trustProxy] Whether the site trusts the proxy (X-Forwarded-For).
15
+ * @property {boolean} [compress] Whether to compress the response.
16
+ * @property {string[]} [mimesToCompress] The list of MIME types to compress.
17
+ * @property {boolean} [excludeFromLogging] Whether to exclude this site from access logs.
18
+ * @property {string[]} [preRequestHooks] An array of paths to pre-request hook scripts for this site.
19
+ */
20
+ interface LocalSiteConfig {
21
+ type: "local";
22
+ location: string;
23
+ indexList: string[];
24
+ upgradeInsecure: boolean;
25
+ directoryListing: boolean;
26
+ maxBodySize: number;
27
+ sjs: boolean;
28
+ headers: Record<string, string>;
29
+ ssl: TlsOptions;
30
+ trustProxy: boolean;
31
+ compress: boolean;
32
+ mimesToCompress: string[];
33
+ excludeFromLogging: boolean;
34
+ preRequestHooks: string[];
35
+ }
36
+
37
+ /**
38
+ * Represents a remote site configuration.
39
+ * @property {string} type The type of configuration.
40
+ * @property {string | string[]} location The URL or URLs of the target server. The request path will be appended to this. If multiple locations are provided, one will be chosen at random.
41
+ * @property {boolean} upgradeInsecure Require HTTPS for all requests.
42
+ * @property {number} maxBodySize The maximum body size in bytes to accept.
43
+ * @property {Record<string, string>} headers The headers to send with the response.
44
+ * @property {TlsOptions} ssl The TLS options to use when connecting to the target.
45
+ * @property {boolean} trustProxy Whether the site trusts the proxy (X-Forwarded-For).
46
+ * @property {boolean} preserveHost Whether to preserve the original host header (true), or rewrite it (false; default)
47
+ * @property {boolean} allowInsecure Whether to allow insecure TLS connections to the target (self-signed certificates, etc).
48
+ * @property {string} errorPagesLocation The location to find custom error pages for this site.
49
+ * @property {boolean} excludeFromLogging Whether to exclude this site from access logs.
50
+ */
51
+ interface RemoteSiteConfig {
52
+ type: "remote";
53
+ location: string | string[];
54
+ upgradeInsecure: boolean;
55
+ maxBodySize: number;
56
+ headers: Record<string, string>;
57
+ ssl: TlsOptions;
58
+ trustProxy: boolean;
59
+ preserveHost: boolean;
60
+ allowInsecure: boolean;
61
+ errorPagesLocation: string;
62
+ excludeFromLogging: boolean;
63
+ }
64
+
65
+ type SiteConfig = LocalSiteConfig | RemoteSiteConfig;
66
+
67
+ enum HTTP_VERSION {
68
+ HTTP0_9 = "HTTP/0.9",
69
+ HTTP1_0 = "HTTP/1.0",
70
+ HTTP1_1 = "HTTP/1.1",
71
+ HTTP2_0 = "HTTP/2.0",
72
+ HTTP3_0 = "HTTP/3.0"
73
+ }
74
+
75
+ enum HTTP_METHOD {
76
+ GET = "GET",
77
+ HEAD = "HEAD",
78
+ POST = "POST",
79
+ PUT = "PUT",
80
+ DELETE = "DELETE",
81
+ CONNECT = "CONNECT",
82
+ OPTIONS = "OPTIONS",
83
+ TRACE = "TRACE",
84
+ PATCH = "PATCH",
85
+ OTHER = "OTHER"
86
+ }
87
+
88
+ type HTTPRequest = {
89
+ /**
90
+ * The HTTP version.
91
+ */
92
+ version: HTTP_VERSION;
93
+
94
+ /**
95
+ * The HTTP method.
96
+ */
97
+ method: HTTP_METHOD;
98
+
99
+ /**
100
+ * The path of the request.
101
+ */
102
+ path: string;
103
+
104
+ /**
105
+ * The parameters of the request.
106
+ */
107
+ params: Record<string, string>;
108
+
109
+ /**
110
+ * The headers of the request.
111
+ */
112
+ headers: Record<string, string>;
113
+
114
+ /**
115
+ * The body of the request.
116
+ * This parameter is a path to a temporary file containing the body.
117
+ */
118
+ body?: string;
119
+
120
+ /**
121
+ * The session ID.
122
+ */
123
+ session?: string;
124
+
125
+ /**
126
+ * The remote family (IPv4 or IPv6).
127
+ */
128
+ remoteFamily: string;
129
+
130
+ /**
131
+ * The remote address.
132
+ */
133
+ remoteAddress: string;
134
+
135
+ /**
136
+ * The remote port.
137
+ */
138
+ remotePort: number;
139
+
140
+ /**
141
+ * The protocol used (http or https).
142
+ */
143
+ protocol: "http" | "https";
144
+
145
+ /**
146
+ * Whether the connection should be kept alive.
147
+ */
148
+ keepAlive: boolean;
149
+
150
+ /**
151
+ * Request ID for logging and tracing.
152
+ */
153
+ requestId: string;
154
+ }
155
+
156
+ export { SiteConfig, HTTPRequest };