@jnode/server 1.0.6 → 2.0.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.
package/src/server.js CHANGED
@@ -1,55 +1,216 @@
1
1
  /*
2
- JustServer/server.js
2
+ @jnode/server/server.js
3
+ v2
3
4
 
4
5
  Simple web server package for Node.js.
5
6
 
6
- by JustNode Dev Team / JustApple
7
+ by JustApple
7
8
  */
8
9
 
9
- //load node packages
10
+ // dependencies
10
11
  const http = require('http');
11
12
  const https = require('https');
12
- const { domainToUnicode } = require('url');
13
+ const http2 = require('http2');
14
+ const path = require('path');
15
+ const stream = require('stream');
13
16
  const EventEmitter = require('events');
14
17
 
15
- //load classes and functions
16
- const processMap = require('./map.js');
17
-
18
- //http server
18
+ // server class
19
19
  class Server extends EventEmitter {
20
- constructor(map = {}, useHttps = false, options) {
21
- super();
22
-
23
- this.map = map; //server map
24
- this.server = (useHttps ? https : http).createServer(options); //create server
25
-
26
- //on normal request
27
- this.server.on('request', (req, res) => {
28
- //emit event
29
- this.emit('request', req, res);
30
-
31
- //parse url
32
- const url = new URL(req.url, 'http://localhost');
33
-
34
- //create path
35
- let p = url.pathname.split('/').map((e) => decodeURIComponent(e));
36
- p[0] = req.headers.host; //set root path to host
37
-
38
- //process map
39
- processMap(req, res, this.map, p, {
40
- url: url,
41
- default: {},
42
- time: Date.now(),
43
- emitError: (...err) => { this.emit('error', ...err); }
44
- });
45
- });
46
- }
47
-
48
- //listen a port to receive requests
49
- listen(port, cb) {
50
- return this.server.listen(port, cb);
51
- }
20
+ constructor(router, options = {}) {
21
+ super();
22
+
23
+ this.router = router;
24
+ this.options = options;
25
+
26
+ // start a server
27
+ this.server = options.enableHTTP2 ?
28
+ ((options.key && options.cert) ?
29
+ http2.createSecureServer(options) :
30
+ http2.createServer(options)) :
31
+ ((options.key && options.cert) ?
32
+ https.createServer(options) :
33
+ http.createServer(options));
34
+
35
+ // request listener
36
+ // HTTP/2 also use the same event with the node:http2 Compatibility API
37
+ this.server.on('request', async (req, res) => {
38
+ let url;
39
+
40
+ try {
41
+ url = new URL(req.url, `http${options.key && options.cert ? 's' : ''}://${req.headers.host || req.headers[':authority']}`);
42
+ } catch (e) {
43
+ this.emit('warn', e);
44
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
45
+ res.end('400 Bad Request', 'utf8');
46
+ return;
47
+ }
48
+
49
+ // the context object, mainly for handlers
50
+ const ctx = {
51
+ req, res, url,
52
+ server: this,
53
+ path: url.pathname,
54
+ host: url.hostname,
55
+ method: req.method,
56
+ headers: req.headers,
57
+ identity: { address: req.socket.remoteAddress, port: req.socket.remotePort },
58
+ params: Object.fromEntries(url.searchParams.entries()),
59
+ body: req
60
+ };
61
+
62
+ // the environment object, mainly for routers
63
+ const env = {
64
+ path: url.pathname.split('/').slice(1).map((i) => { try { return decodeURIComponent(i); } catch { return i; } }),
65
+ pathPointer: 0,
66
+ host: url.hostname.split('.').filter(Boolean).reverse(),
67
+ hostPointer: 0,
68
+ codeHandlers: this.options.codeHandlers || {},
69
+ i: 0
70
+ };
71
+
72
+ let handler;
73
+ try {
74
+ handler = await Server.route(this.router, env, ctx, this.options);
75
+ if (handler === undefined || handler === null) handler = 404;
76
+ } catch (e) { // error while routing
77
+ this.emit('e', e, env, ctx);
78
+ handler = 500;
79
+ }
80
+
81
+ await Server.handle(handler, env, ctx, this.options);
82
+ });
83
+ }
84
+
85
+ // route
86
+ static async route(router, env = {}, ctx = {}, options = {}) {
87
+ let r = router;
88
+ if (!env.i) env.i = 0;
89
+
90
+ while (typeof r?.route === 'function') {
91
+ env.i++;
92
+
93
+ if (env.i > (options.maxRoutingSteps || 50)) return 508; // 508 Loop Detected
94
+
95
+ r = await r.route(env, ctx);
96
+ }
97
+
98
+ return r;
99
+ }
100
+
101
+ // handle
102
+ static async handle(handler, env = {}, ctx = {}, options = {}) {
103
+ try {
104
+ if (typeof handler?.handle === 'function') { // handler
105
+ await handler.handle(ctx, env);
106
+ } else if (typeof handler === 'function') { // function
107
+ await handler(ctx, env);
108
+ } else if (typeof handler === 'string') { // string
109
+ ctx.res.writeHead(200, {
110
+ 'Content-Type': 'text/plain; charset=utf-8',
111
+ 'Content-Length': Buffer.byteLength(handler, 'utf8')
112
+ });
113
+ ctx.res.end(handler, 'utf8');
114
+ } else if (handler instanceof Uint8Array) { // buffer
115
+ ctx.res.writeHead(200, {
116
+ 'Content-Type': 'application/octet-stream',
117
+ 'Content-Length': handler.length
118
+ });
119
+ ctx.res.end(handler);
120
+ } else if (stream.isReadable(handler)) { // stream
121
+ ctx.res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
122
+ await stream.promises.pipeline(handler, ctx.res);
123
+ } else if (typeof handler === 'number') { // status code
124
+ const code = handler;
125
+ handler = env.codeHandlers[code];
126
+
127
+ // handlers without code handlers
128
+ if (typeof handler?.handle === 'function') { // handler
129
+ await handler.handle(ctx, env);
130
+ } else if (typeof handler === 'function') { // function
131
+ await handler(ctx, env);
132
+ } else if (typeof handler === 'string') { // string
133
+ ctx.res.writeHead(code, {
134
+ 'Content-Type': 'text/plain; charset=utf-8',
135
+ 'Content-Length': Buffer.byteLength(handler, 'utf8')
136
+ });
137
+ ctx.res.end(handler, 'utf8');
138
+ } else if (handler instanceof Uint8Array) { // buffer
139
+ ctx.res.writeHead(code, {
140
+ 'Content-Type': 'application/octet-stream',
141
+ 'Content-Length': handler.length
142
+ });
143
+ ctx.res.end(handler);
144
+ } else if (stream.isReadable(handler)) { // stream
145
+ ctx.res.writeHead(code, { 'Content-Type': 'application/octet-stream' });
146
+ await stream.promises.pipeline(handler, ctx.res);
147
+ } else { // use default status code response
148
+ try { ctx.res.writeHead(code, { 'Content-Type': 'text/plain; charset=utf-8' }); } catch { }
149
+ try { ctx.res.end(`${code} ${http.STATUS_CODES[code] || 'Unknown'}`, 'utf8'); } catch { }
150
+ }
151
+ } else { // invalid handler
152
+ throw new Error('JNS: Invalid handler returned from router.');
153
+ }
154
+ } catch (e) { // error while handling
155
+ if (typeof e !== 'number') ctx.server.emit('e', e, env, ctx);
156
+
157
+ try {
158
+ const code = (typeof e === 'number') ? e : 500;
159
+ handler = env.codeHandlers[code];
160
+
161
+ // handlers without code handlers
162
+ if (typeof handler?.handle === 'function') { // handler
163
+ await handler.handle(ctx, env);
164
+ } else if (typeof handler === 'function') { // function
165
+ await handler(ctx, env);
166
+ } else if (typeof handler === 'string') { // string
167
+ ctx.res.writeHead(code, {
168
+ 'Content-Type': 'text/plain; charset=utf-8',
169
+ 'Content-Length': Buffer.byteLength(handler, 'utf8')
170
+ });
171
+ ctx.res.end(handler, 'utf8');
172
+ } else if (handler instanceof Uint8Array) { // buffer
173
+ ctx.res.writeHead(code, {
174
+ 'Content-Type': 'application/octet-stream',
175
+ 'Content-Length': handler.length
176
+ });
177
+ ctx.res.end(handler);
178
+ } else if (stream.isReadable(handler)) { // stream
179
+ ctx.res.writeHead(code, { 'Content-Type': 'application/octet-stream' });
180
+ await stream.promises.pipeline(handler, ctx.res);
181
+ } else { // use default status code response
182
+ ctx.res.writeHead(code, { 'Content-Type': 'text/plain; charset=utf-8' });
183
+ ctx.res.end(`${code} ${http.STATUS_CODES[code] || 'Unknown'}`, 'utf8');
184
+ }
185
+ } catch (e) { // error while handling the error while handling :)
186
+ ctx.server.emit('warn', e, env, ctx);
187
+ try { ctx.res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }); } catch { }
188
+ try { ctx.res.end('500 Internal Server Error', 'utf8'); } catch { }
189
+ }
190
+ }
191
+ }
192
+
193
+ listen(...args) {
194
+ this.server.listen(...args);
195
+ }
196
+
197
+ close(...args) {
198
+ this.server.close(...args);
199
+ }
200
+
201
+ // throw an error event
202
+ throw(...args) {
203
+ this.emit('e', ...args);
204
+ }
205
+ }
206
+
207
+ // create server
208
+ function createServer(router, options = {}) {
209
+ return new Server(router, options);
52
210
  }
53
211
 
54
- //export
55
- module.exports = Server;
212
+ // export
213
+ module.exports = {
214
+ Server,
215
+ createServer
216
+ };
package/src/error.js DELETED
@@ -1,32 +0,0 @@
1
- /*
2
- JustServer/error.js
3
-
4
- Simple web server package for Node.js.
5
-
6
- by JustNode Dev Team / JustApple
7
- */
8
-
9
- //load classes and functions
10
- const processFinal = require('./final.js');
11
-
12
- //process HandleObject error
13
- async function processError(req, res, map, p, e) {
14
- try {
15
- //500 error
16
- return await processFinal(req, res, map['!500'] ?? e.default['!500'] ?? { //default error response
17
- '+STATUS': 500,
18
- 'TEXT': '500 Internal server error.'
19
- }, p, e);
20
- } catch (err) {
21
- //last error response
22
- try { res.writeHead(500, { 'Content-Type': 'text/plain' }); } catch { }
23
- try { res.end('500 Internal server error.'); } catch { }
24
-
25
- //emit error
26
- e.emitError(err);
27
- return;
28
- }
29
- }
30
-
31
- //export
32
- module.exports = processError;
package/src/final.js DELETED
@@ -1,129 +0,0 @@
1
- /*
2
- JustServer/final.js
3
-
4
- Simple web server package for Node.js.
5
-
6
- by JustNode Dev Team / JustApple
7
- */
8
-
9
- //load node packages
10
- const path = require('path');
11
- const fs = require('fs');
12
- const fsPromises = fs.promises;
13
-
14
- //load mime types
15
- const mimeTypes = require('./mime.json');
16
-
17
- //process FinalObject
18
- async function processFinal(req, res, map, p, e) {
19
- //firewall
20
- if (
21
- (map['+FIREWALL'] ?? e.default['+FIREWALL']) &&
22
- !(await (map['+FIREWALL'] ?? e.default['+FIREWALL'])(req, res, map, p, e))
23
- ) return;
24
-
25
- //custom function
26
- if (map['FUNCTION']) return map['FUNCTION'](req, res, map, p, e);
27
-
28
- //text response (include empty text)
29
- if (map['TEXT'] !== undefined) {
30
- res.writeHead(map['+STATUS'] ?? 200, {
31
- 'Content-Type': 'text/plain; charset=UTF-8',
32
- 'Content-Length': Buffer.byteLength(map['TEXT']),
33
- ...map['+HEADERS']
34
- });
35
- res.end(map['TEXT'], 'utf8');
36
- return;
37
- }
38
-
39
- //buffer (binary) response
40
- if (map['BUFFER']) {
41
- res.writeHead(map['+STATUS'] ?? 200, {
42
- 'Content-Type': 'application/octet-stream',
43
- 'Content-Length': map['BUFFER'].length,
44
- ...map['+HEADERS']
45
- });
46
- res.end(map['BUFFER']);
47
- return;
48
- }
49
-
50
- //file response
51
- if (map['FILE']) {
52
- let fileSize;
53
-
54
- //get file size and check file exists
55
- try {
56
- fileSize = (await fsPromises.stat(map['FILE'])).size;
57
- } catch (err) { return '!404'; } //return error
58
-
59
- //write head
60
- res.writeHead(map['+STATUS'] ?? 200, {
61
- 'Content-Type': mimeTypes[path.extname(map['FILE'])] ?? 'application/octet-stream',
62
- 'Content-Length': fileSize,
63
- ...map['+HEADERS']
64
- });
65
-
66
- //stream response
67
- const readStream = fs.createReadStream(map['FILE']);
68
- readStream.pipe(res);
69
-
70
- //wait for stream
71
- return await new Promise((resolve, reject) => {
72
- readStream.on('error', reject);
73
- readStream.on('end', resolve);
74
- });
75
- }
76
-
77
- //folder
78
- if (map['FOLDER']) {
79
- //get file path
80
- const file = path.resolve(map['FOLDER'], p.map((e) => encodeURIComponent(e)).join('/'));
81
-
82
- //check file path
83
- if (path.relative(path.resolve(map['FOLDER']), file).startsWith('..')) return '!403';
84
-
85
- //check hidden file
86
- if (
87
- path.basename(file).startsWith('.') &&
88
- !(map['__ALLOW_HIDDEN_FILE'] || e.default['__ALLOW_HIDDEN_FILE'])
89
- ) return '!404';
90
-
91
- let fileSize;
92
-
93
- //get file size and check file exists
94
- try {
95
- const stat = await fsPromises.stat(file);
96
- if (stat.isDirectory()) return '!404'; //return error for directory
97
- fileSize = stat.size;
98
- } catch (err) { return '!404'; } //return error
99
-
100
- //write head
101
- res.writeHead(map['+STATUS'] ?? 200, {
102
- 'Content-Type': mimeTypes[path.extname(file)] ?? 'application/octet-stream',
103
- 'Content-Length': fileSize,
104
- ...map['+HEADERS']
105
- });
106
-
107
- //stream response
108
- const readStream = fs.createReadStream(file);
109
- readStream.pipe(res);
110
-
111
- //wait for stream
112
- return await new Promise((resolve, reject) => {
113
- readStream.on('error', reject);
114
- readStream.on('end', resolve);
115
- });
116
- }
117
-
118
- //only headers or status code
119
- if (map['+STATUS'] || map['+HEADERS']) {
120
- res.writeHead(map['+STATUS'] ?? 200, map['+HEADERS']);
121
- res.end();
122
- return;
123
- }
124
-
125
- return '!404';
126
- }
127
-
128
- //export
129
- module.exports = processFinal;
package/src/handle.js DELETED
@@ -1,88 +0,0 @@
1
- /*
2
- JustServer/handle.js
3
-
4
- Simple web server package for Node.js.
5
-
6
- by JustNode Dev Team / JustApple
7
- */
8
-
9
- //load classes and functions
10
- const processFinal = require('./final.js');
11
- const processError = require('./error.js');
12
-
13
- //error handler function
14
- async function handleStatus(req, res, map, p, e, status) {
15
- let statusCode;
16
- let defaultMap;
17
-
18
- switch (status) {
19
- case '!404':
20
- statusCode = 404;
21
- defaultMap = e.default['!404'] ?? {
22
- '+STATUS': 404,
23
- 'TEXT': '404 Page not found.'
24
- };
25
- break;
26
- case '!403':
27
- statusCode = 403;
28
- defaultMap = e.default['!403'] ?? {
29
- '+STATUS': 403,
30
- 'TEXT': '403 Forbidden.'
31
- };
32
- break;
33
- default:
34
- return status; //if status is not a special error, return it directly
35
- }
36
-
37
- return await processFinal(req, res, map[status] ?? defaultMap, p, e);
38
- }
39
-
40
- //process function with error handling
41
- async function safeProcessFinal(req, res, map, p, e, finalObject) {
42
- try {
43
- let status = await processFinal(req, res, finalObject, p, e);
44
-
45
- //handle special status codes
46
- status = await handleStatus(req, res, map, p, e, status);
47
-
48
- //fall into loop error
49
- if ((typeof status === 'string') && status.startsWith('!')) {
50
- throw new Error('Process may fall into infinity loop.');
51
- }
52
- return status;
53
- } catch (err) {
54
- //emit error
55
- e.emitError(err);
56
-
57
- //internal server error
58
- return processError(req, res, map, p, e);
59
- }
60
- }
61
-
62
- //process HandleObject
63
- async function processHandle(req, res, map, p, e) {
64
- //protocol upgrade
65
- if (
66
- req.headers.connection &&
67
- req.headers.connection.toLowerCase() === 'upgrade' &&
68
- map['^' + req.headers.upgrade]
69
- ) {
70
- return await safeProcessFinal(req, res, map, p, e, map['^' + req.headers.upgrade]);
71
- }
72
-
73
- //method check
74
- if (map['@' + req.method]) {
75
- return await safeProcessFinal(req, res, map, p, e, map['@' + req.method]);
76
- }
77
-
78
- //get final object by function
79
- if (map['>']) {
80
- return await safeProcessFinal(req, res, map, p, e, await map['>'](req, res, map, p, e));
81
- }
82
-
83
- //process final
84
- return await safeProcessFinal(req, res, map, p, e, map);
85
- }
86
-
87
- //export
88
- module.exports = processHandle;
package/src/map.js DELETED
@@ -1,50 +0,0 @@
1
- /*
2
- JustServer/map.js
3
-
4
- Simple web server package for Node.js.
5
-
6
- by JustNode Dev Team / JustApple
7
- */
8
-
9
- //load classes and functions
10
- const processHandle = require('./handle.js');
11
- const processFinal = require('./final.js');
12
- const processError = require('./error.js');
13
-
14
- //process MapObject
15
- async function processMap(req, res, map, p, e) {
16
- //overwrite default handle object
17
- Object.assign(e.default, map['#']);
18
-
19
- //path ends, process HandleObject
20
- if (p.length === 0) return await processHandle(req, res, map, p, e);
21
-
22
- //specific path
23
- if (map['/' + p[0]]) return await processMap(req, res, map['/' + p.shift()], p, e);
24
-
25
- //any path
26
- if (map['*']) {
27
- p.shift(); //remove first one
28
- return await processMap(req, res, map['*'], p, e);
29
- }
30
-
31
- //any path following
32
- if (map['**']) return await processHandle(req, res, map['**'], p, e);
33
-
34
- //page not found
35
- try {
36
- return await processFinal(req, res, map['!404'] ?? e.default['!404'] ?? { //default error response
37
- '+STATUS': 404,
38
- 'TEXT': '404 Page not found.'
39
- }, p, e);
40
- } catch (err) {
41
- //emit error
42
- e.emitError(err);
43
-
44
- //internal server error
45
- return processError(req, res, map, p, e);
46
- }
47
- }
48
-
49
- //export
50
- module.exports = processMap;