upcore-tcp 0.0.3 → 0.0.5
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/README.md +43 -1
- package/lib/headerDecode.js +10 -13
- package/lib/{checkHeader.js → headerRead.js} +22 -12
- package/lib/httpHeader.js +16 -6
- package/lib/parameterFile.js +2 -4
- package/lib/server.js +44 -18
- package/lib/streamFile.js +2 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ app.listen();
|
|
|
103
103
|
- Response: `req.code()`, `req.set()`, `req.http()`, `req.send()`, `req.html()`, `req.json()`, `req.redirect()`, `req.file()`, `req.error()`
|
|
104
104
|
- Request: `req.get(headerKey)`, `req.look(queryKeys)`, `req.u(queryKey)`, `req.load()`, `req.check()`, `req.q()`
|
|
105
105
|
- Cookie: `req.gcookie()`, `req.scookie()`, `req.rcookie()`
|
|
106
|
+
- EventSource (SSE): `req.event(start, close)`
|
|
106
107
|
- Middleware flow: `req.next()`
|
|
107
108
|
- Validation: `req.test({ type, data, core })`
|
|
108
109
|
|
|
@@ -263,6 +264,7 @@ app.routes('./routes');
|
|
|
263
264
|
- ถ้าชื่อไฟล์มี `@` มากกว่า 1 ตัว (เช่น `get@a@b.js`) ระบบจะไม่ map route ไฟล์นั้น
|
|
264
265
|
|
|
265
266
|
ตัวอย่างชื่อไฟล์ -> endpoint:
|
|
267
|
+
- `get@.js` => `GET /`
|
|
266
268
|
- `get@home-admin.js` => `GET /home/admin`
|
|
267
269
|
- `get@user-$id.js` => `GET /user/:id`
|
|
268
270
|
- `post@auth-login.js` => `POST /auth/login`
|
|
@@ -305,8 +307,48 @@ app.websocket('/ws', async (req) => {
|
|
|
305
307
|
|
|
306
308
|
// ws://127.0.0.1/ws?uid=upcore
|
|
307
309
|
```
|
|
310
|
+
|
|
311
|
+
### 14) EventSource (SSE) ด้วย `req.event`
|
|
312
|
+
```js
|
|
313
|
+
app.get('/event', (req) => {
|
|
314
|
+
let timer;
|
|
315
|
+
|
|
316
|
+
req.event(() => {
|
|
317
|
+
req.write('event: ready\n');
|
|
318
|
+
req.write('data: {"status":"connected"}\n\n');
|
|
319
|
+
|
|
320
|
+
timer = setInterval(() => {
|
|
321
|
+
req.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
|
|
322
|
+
}, 2000);
|
|
323
|
+
}, () => {
|
|
324
|
+
clearInterval(timer); // เรียกเมื่อ client ปิดการเชื่อมต่อ
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
ตัวอย่างฝั่ง client:
|
|
330
|
+
```js
|
|
331
|
+
const source = new EventSource('/event');
|
|
332
|
+
|
|
333
|
+
source.addEventListener('ready', (e) => {
|
|
334
|
+
console.log('ready:', e.data);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
source.onmessage = (e) => {
|
|
338
|
+
console.log('message:', JSON.parse(e.data));
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
source.onerror = () => {
|
|
342
|
+
source.close();
|
|
343
|
+
};
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
รูปแบบข้อมูล SSE ที่ต้องส่ง:
|
|
347
|
+
- แต่ละ message ควรลงท้ายด้วย `\n\n`
|
|
348
|
+
- ใช้ `req.write(...)` ส่งข้อมูลตามฟอร์แมต `event:` และ `data:`
|
|
349
|
+
|
|
308
350
|
## หมายเหตุสำคัญ
|
|
309
351
|
- ค่า `Host` ต้องตรงกับ domain ที่ลง route (หรือใช้ `*` ถ้าต้องการรับทุก host)
|
|
310
352
|
- ถ้า `req.load(...)` ประเภทไม่ตรงกับ `Content-Type` จะตอบ 400
|
|
311
353
|
- `req.test(...)` จะตอบ error อัตโนมัติเมื่อ type/format ไม่ผ่าน
|
|
312
|
-
- ถ้าใช้ `app.routes(dir)` โฟลเดอร์ต้องมีไฟล์ `.js` ตาม naming convention
|
|
354
|
+
- ถ้าใช้ `app.routes(dir)` โฟลเดอร์ต้องมีไฟล์ `.js` ตาม naming convention
|
package/lib/headerDecode.js
CHANGED
|
@@ -4,21 +4,18 @@ module.exports = function(req){
|
|
|
4
4
|
const src = req.header;
|
|
5
5
|
const out = {};
|
|
6
6
|
|
|
7
|
-
for
|
|
8
|
-
const buf = src[i];
|
|
7
|
+
for(let i = 0; i < src.length; i++){
|
|
8
|
+
const [buf, split] = src[i];
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
j++;
|
|
14
|
-
if (buf[j] === 32) j++;
|
|
10
|
+
if(buf[split + 1] === 32){
|
|
11
|
+
const key = buf.toString('ascii', 0, split);
|
|
12
|
+
const val = buf.toString('ascii', split + 2, buf.length);
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
out[key] = [val];
|
|
14
|
+
if(out[key]){
|
|
15
|
+
out[key].push(val);
|
|
16
|
+
}else{
|
|
17
|
+
out[key] = [val];
|
|
18
|
+
}
|
|
22
19
|
}
|
|
23
20
|
}
|
|
24
21
|
|
|
@@ -37,7 +37,7 @@ module.exports = function(chunk){
|
|
|
37
37
|
let port = false;
|
|
38
38
|
let length = 0;
|
|
39
39
|
let type = false;
|
|
40
|
-
let connection =
|
|
40
|
+
let connection = 1;
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
while(p + 3 < len){
|
|
@@ -48,21 +48,31 @@ module.exports = function(chunk){
|
|
|
48
48
|
break;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
let check = true;
|
|
52
|
+
let split = -1;
|
|
53
|
+
while(p < len && !(chunk[p] === 13 && chunk[p + 1] === 10)){
|
|
54
|
+
if(chunk[p] == 58){
|
|
55
|
+
split = p;
|
|
56
|
+
check = false;
|
|
57
|
+
}else if(check && chunk[p] >= 65 && chunk[p] <= 90){
|
|
58
|
+
chunk[p] = chunk[p] + 32;
|
|
59
|
+
}
|
|
60
|
+
p++;
|
|
61
|
+
}
|
|
52
62
|
|
|
53
63
|
const h = chunk.subarray(lineStart, p);
|
|
54
64
|
|
|
55
65
|
switch((h[0] << 24) | (h[1] << 16) | (h[2] << 8) | h[3]){
|
|
56
|
-
case
|
|
57
|
-
if(h[8] ===
|
|
66
|
+
case 0x636F6E74:{ // "cont"
|
|
67
|
+
if(h[8] === 108 && h[14] === 58 && h[15] === 32){ // content-length:
|
|
58
68
|
length = parseInt(h.subarray(16));
|
|
59
69
|
break;
|
|
60
|
-
}else if(h[8] ===
|
|
70
|
+
}else if(h[8] === 116 && h[12] === 58 && h[13] === 32){ // content-type:
|
|
61
71
|
type = h.subarray(14);
|
|
62
72
|
break;
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
|
-
case
|
|
75
|
+
case 0x686F7374:{ // "host"
|
|
66
76
|
if(h[4] === 58 && h[5] === 32){
|
|
67
77
|
let colon = -1;
|
|
68
78
|
for(let i = h.length - 1; i > 5; i--){
|
|
@@ -81,19 +91,19 @@ module.exports = function(chunk){
|
|
|
81
91
|
break;
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
|
-
case
|
|
85
|
-
//
|
|
94
|
+
case 0x636F6E6E:{ // "conn"
|
|
95
|
+
// connection
|
|
86
96
|
if(h[10] === 58 && h[11] === 32){
|
|
87
|
-
if(h[12] ===
|
|
88
|
-
connection =
|
|
89
|
-
}else if(h[12] === 85 && h.length === 19){ //
|
|
97
|
+
if(h[12] === 99 && h.length === 17){ // connection: close
|
|
98
|
+
connection = 0
|
|
99
|
+
}else if(h[12] === 85 && h.length === 19){ // connection: Upgrade
|
|
90
100
|
connection = 2
|
|
91
101
|
}
|
|
92
102
|
break;
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
default:{
|
|
96
|
-
header.push(h);
|
|
106
|
+
header.push([h, split - lineStart]);
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
|
package/lib/httpHeader.js
CHANGED
|
@@ -5,9 +5,14 @@ const version = package.version;
|
|
|
5
5
|
let CACHE_HTTP = {};
|
|
6
6
|
for(let x in http.STATUS_CODES){
|
|
7
7
|
CACHE_HTTP[x] = Buffer.from(
|
|
8
|
-
`HTTP/1.1 ${x} ${http.STATUS_CODES[x]}\r\
|
|
8
|
+
`HTTP/1.1 ${x} ${http.STATUS_CODES[x]}\r\nx-powered-by: r938-upcore/${version}\r\n`
|
|
9
9
|
);
|
|
10
10
|
}
|
|
11
|
+
let CONNECTION = {
|
|
12
|
+
0:Buffer.from('connection: close\r\n'),
|
|
13
|
+
1:Buffer.from('connection: keep-alive\r\n'),
|
|
14
|
+
2:Buffer.from('connection: Upgrade\r\nupgrade: websocket\r\n'),
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
module.exports = (code)=>{
|
|
13
18
|
if(CACHE_HTTP[code]){
|
|
@@ -17,10 +22,15 @@ module.exports = (code)=>{
|
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
module.exports.
|
|
25
|
+
module.exports.connection = (state)=>{
|
|
26
|
+
return CONNECTION[state] || '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
module.exports.CRLF = Buffer.from('\r\n');
|
|
22
31
|
module.exports.CRLF2 = Buffer.from('\r\n\r\n');
|
|
23
|
-
module.exports.CONTENT_LENGTH = Buffer.from('
|
|
24
|
-
module.exports.CONTENT_TYPE_PLAIN = Buffer.from('
|
|
25
|
-
module.exports.CONTENT_TYPE_HTML = Buffer.from('
|
|
26
|
-
module.exports.CONTENT_TYPE_JSON = Buffer.from('
|
|
32
|
+
module.exports.CONTENT_LENGTH = Buffer.from('content-length: ');
|
|
33
|
+
module.exports.CONTENT_TYPE_PLAIN = Buffer.from('content-Type: text/plain; charset=utf-8\r\n');
|
|
34
|
+
module.exports.CONTENT_TYPE_HTML = Buffer.from('content-Type: text/html; charset=utf-8\r\n');
|
|
35
|
+
module.exports.CONTENT_TYPE_JSON = Buffer.from('content-Type: application/json; charset=utf-8\r\n');
|
|
36
|
+
module.exports.CONTENT_TYPE_EVENT_STREAM = Buffer.from('content-Type: text/event-stream\r\n');
|
package/lib/parameterFile.js
CHANGED
|
@@ -28,10 +28,8 @@ module.exports = function(req, filePath, parameter, error){
|
|
|
28
28
|
parameter?.[key] ?? ''
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
req
|
|
32
|
-
.
|
|
33
|
-
.set('Content-Length', Buffer.byteLength(data))
|
|
34
|
-
.http(data);
|
|
31
|
+
req.response.header.push(`content-type: ${type}\r\n`);
|
|
32
|
+
req.http(data);
|
|
35
33
|
});
|
|
36
34
|
}else{
|
|
37
35
|
error(400);
|
package/lib/server.js
CHANGED
|
@@ -7,7 +7,7 @@ const version = package.version;
|
|
|
7
7
|
|
|
8
8
|
const routeContext = require('./routeContext');
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const headerRead = require('./headerRead');
|
|
11
11
|
|
|
12
12
|
const headerDecode = require('./headerDecode');
|
|
13
13
|
const cookieDecode = require('./cookieDecode');
|
|
@@ -120,13 +120,17 @@ module.exports = class{
|
|
|
120
120
|
.setTimeout(this.config.timeout)
|
|
121
121
|
.on('data', (chunk)=>{
|
|
122
122
|
if(focus == 1){
|
|
123
|
-
req =
|
|
123
|
+
req = headerRead(chunk);
|
|
124
124
|
if(req === false){
|
|
125
125
|
socket.destroy();
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
switch(req.connection){
|
|
130
|
+
case 0:{
|
|
131
|
+
socket.setKeepAlive(false, 0);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
130
134
|
case 2:{
|
|
131
135
|
socket.setTimeout(0);
|
|
132
136
|
focus = 3;
|
|
@@ -204,6 +208,11 @@ module.exports = class{
|
|
|
204
208
|
header:[],
|
|
205
209
|
};
|
|
206
210
|
|
|
211
|
+
req.gets = ()=>{
|
|
212
|
+
headerDecode(req);
|
|
213
|
+
|
|
214
|
+
return req.header;
|
|
215
|
+
}
|
|
207
216
|
req.get = (key)=>{
|
|
208
217
|
headerDecode(req);
|
|
209
218
|
|
|
@@ -221,11 +230,11 @@ module.exports = class{
|
|
|
221
230
|
}
|
|
222
231
|
req.http = (data)=>{
|
|
223
232
|
if(data !== undefined){
|
|
224
|
-
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.CONTENT_LENGTH + Buffer.byteLength(data) + httpHeader.CRLF2 + data);
|
|
233
|
+
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.connection(req.connection) + httpHeader.CONTENT_LENGTH + Buffer.byteLength(data) + httpHeader.CRLF2 + data);
|
|
225
234
|
}else if(req.connection === 2){
|
|
226
|
-
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.
|
|
235
|
+
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.connection(req.connection) + httpHeader.CRLF);
|
|
227
236
|
}else{
|
|
228
|
-
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.CRLF);
|
|
237
|
+
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.connection(req.connection) + httpHeader.CRLF);
|
|
229
238
|
}
|
|
230
239
|
}
|
|
231
240
|
req.socket = (call)=>{
|
|
@@ -258,7 +267,7 @@ module.exports = class{
|
|
|
258
267
|
}
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
|
-
req.set
|
|
270
|
+
req.response.header.push(`set-cookie: ${text.join('; ')}\r\n`);
|
|
262
271
|
|
|
263
272
|
return req;
|
|
264
273
|
}
|
|
@@ -322,9 +331,9 @@ module.exports = class{
|
|
|
322
331
|
});
|
|
323
332
|
|
|
324
333
|
req.start = async(uid)=>{
|
|
325
|
-
let seckey = req.get('
|
|
326
|
-
let protocol = req.get('
|
|
327
|
-
let version = parseInt(req.get('
|
|
334
|
+
let seckey = req.get('sec-websocket-key');
|
|
335
|
+
let protocol = req.get('sec-websocket-protocol');
|
|
336
|
+
let version = parseInt(req.get('sec-websocket-version'));
|
|
328
337
|
|
|
329
338
|
if(version !== 13){
|
|
330
339
|
req.error(400);
|
|
@@ -345,10 +354,10 @@ module.exports = class{
|
|
|
345
354
|
req.userIn();
|
|
346
355
|
|
|
347
356
|
req.code(101)
|
|
348
|
-
.
|
|
357
|
+
req.response.header.push(`sec-websocket-accept: ${acceptKey}\r\n`);
|
|
349
358
|
|
|
350
359
|
if(protocol){
|
|
351
|
-
req.
|
|
360
|
+
req.response.header.push(`sec-websocket-protocol: ${protocol}\r\n`);
|
|
352
361
|
}
|
|
353
362
|
|
|
354
363
|
req.http();
|
|
@@ -449,14 +458,16 @@ module.exports = class{
|
|
|
449
458
|
(type == 'formdata' && !TYPE_TEST.startsWith('multipart/form-data')) ||
|
|
450
459
|
(type == 'wwwform' && !TYPE_TEST.startsWith('application/x-www-form-urlencoded'))
|
|
451
460
|
){
|
|
452
|
-
req.
|
|
461
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
462
|
+
req.code(415).http('{"status":false,"err":"Request Content-Type does not match the expected type."}');
|
|
453
463
|
return false;
|
|
454
464
|
}
|
|
455
465
|
|
|
456
466
|
if(typeof core == 'object'){
|
|
457
467
|
for(let id in core){
|
|
458
468
|
if(req[id] !== core[id]){
|
|
459
|
-
req.
|
|
469
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
470
|
+
req.code(400).http('{"status":false,"err":"The request does not satisfy the required conditions."}');
|
|
460
471
|
return false;
|
|
461
472
|
}
|
|
462
473
|
}
|
|
@@ -464,7 +475,8 @@ module.exports = class{
|
|
|
464
475
|
|
|
465
476
|
if(typeof data == 'object'){
|
|
466
477
|
if(typeof req.body != 'object'){
|
|
467
|
-
req.
|
|
478
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
479
|
+
req.code(415).http('{"status":false,"err":"Request body must be a valid object."}');
|
|
468
480
|
return false;
|
|
469
481
|
}
|
|
470
482
|
|
|
@@ -474,13 +486,15 @@ module.exports = class{
|
|
|
474
486
|
let value = type == 'json' ? req.body[id] : req.q(id);
|
|
475
487
|
let test = checkType(this.TYPE_TREE[t] || {}, value);
|
|
476
488
|
if(!test){
|
|
477
|
-
req.
|
|
489
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
490
|
+
req.code(400).http('{"status":false,"err":"Invalid format for parameter."}');
|
|
478
491
|
return false;
|
|
479
492
|
}
|
|
480
493
|
|
|
481
494
|
parameter[id] = value;
|
|
482
495
|
}else if(r === true){
|
|
483
|
-
req.
|
|
496
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
497
|
+
req.code(400).http('{"status":false,"err":"Missing or invalid parameter."}');
|
|
484
498
|
return false;
|
|
485
499
|
}
|
|
486
500
|
}
|
|
@@ -547,7 +561,7 @@ module.exports = class{
|
|
|
547
561
|
}
|
|
548
562
|
|
|
549
563
|
req.code(status)
|
|
550
|
-
req.
|
|
564
|
+
req.response.header.push(`location: ${location}\r\n`);
|
|
551
565
|
req.http();
|
|
552
566
|
socket.destroy();
|
|
553
567
|
}
|
|
@@ -562,7 +576,7 @@ module.exports = class{
|
|
|
562
576
|
}
|
|
563
577
|
}
|
|
564
578
|
if(op.download){
|
|
565
|
-
req.
|
|
579
|
+
req.response.header.push(`content-disposition: attachment; filename="${op.download || path.basename(op.file)}"\r\n`);
|
|
566
580
|
}
|
|
567
581
|
|
|
568
582
|
if(op.parameter){
|
|
@@ -603,6 +617,18 @@ module.exports = class{
|
|
|
603
617
|
req.error(400);
|
|
604
618
|
}
|
|
605
619
|
}
|
|
620
|
+
req.event = (start, close)=>{
|
|
621
|
+
if(typeof start == 'function' && typeof close == 'function'){
|
|
622
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_EVENT_STREAM);
|
|
623
|
+
|
|
624
|
+
req.http();
|
|
625
|
+
socket.pause();
|
|
626
|
+
|
|
627
|
+
req.userOut = close;
|
|
628
|
+
|
|
629
|
+
start(socket);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
606
632
|
}
|
|
607
633
|
}
|
|
608
634
|
handle(req, socket){
|
package/lib/streamFile.js
CHANGED
|
@@ -38,15 +38,8 @@ module.exports = function(req, filePath, error){
|
|
|
38
38
|
|
|
39
39
|
const contentLength = end - start + 1;
|
|
40
40
|
|
|
41
|
-
req
|
|
42
|
-
.
|
|
43
|
-
.set('Content-Type', type)
|
|
44
|
-
.set('Accept-Ranges', 'bytes')
|
|
45
|
-
.set('Content-Length', contentLength)
|
|
46
|
-
|
|
47
|
-
if(status === 206){
|
|
48
|
-
req.set('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
49
|
-
}
|
|
41
|
+
req.code(status)
|
|
42
|
+
req.response.header.push((status === 206 ? `content-range: bytes ${start}-${end}/${size}\r\n` : '') + `content-type: ${type}\r\naccept-ranges: bytes\r\ncontent-length: ${contentLength}\r\n`);
|
|
50
43
|
req.http();
|
|
51
44
|
|
|
52
45
|
const stream = fs.createReadStream(filePath, { start, end });
|