upcore-tcp 0.0.2 → 0.0.4
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 +46 -4
- 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 +39 -18
- package/lib/streamFile.js +2 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# upcore
|
|
1
|
+
# upcore-tcp
|
|
2
2
|
|
|
3
3
|
Framework Node.js ขนาดเล็กสำหรับ **HTTP + WebSocket**
|
|
4
4
|
ออกแบบมาให้เร็ว เบา และควบคุม flow ได้เองทั้งหมด
|
|
@@ -34,7 +34,7 @@ Framework Node.js ขนาดเล็กสำหรับ **HTTP + WebSocket*
|
|
|
34
34
|
|
|
35
35
|
## Quick Start
|
|
36
36
|
```js
|
|
37
|
-
const upcore = require('upcore');
|
|
37
|
+
const upcore = require('upcore-tcp');
|
|
38
38
|
|
|
39
39
|
const app = new upcore({
|
|
40
40
|
dev: true,
|
|
@@ -63,7 +63,7 @@ project/
|
|
|
63
63
|
|
|
64
64
|
ตัวอย่าง `app.js`:
|
|
65
65
|
```js
|
|
66
|
-
const upcore = require('upcore');
|
|
66
|
+
const upcore = require('upcore-tcp');
|
|
67
67
|
|
|
68
68
|
const app = new upcore({ port: 3000, dev: true, logger: true });
|
|
69
69
|
|
|
@@ -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;
|
|
@@ -221,11 +225,11 @@ module.exports = class{
|
|
|
221
225
|
}
|
|
222
226
|
req.http = (data)=>{
|
|
223
227
|
if(data !== undefined){
|
|
224
|
-
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.CONTENT_LENGTH + Buffer.byteLength(data) + httpHeader.CRLF2 + data);
|
|
228
|
+
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.connection(req.connection) + httpHeader.CONTENT_LENGTH + Buffer.byteLength(data) + httpHeader.CRLF2 + data);
|
|
225
229
|
}else if(req.connection === 2){
|
|
226
|
-
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.
|
|
230
|
+
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.connection(req.connection) + httpHeader.CRLF);
|
|
227
231
|
}else{
|
|
228
|
-
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.CRLF);
|
|
232
|
+
socket.write(httpHeader(req.response.status) + req.response.header.join('') + httpHeader.connection(req.connection) + httpHeader.CRLF);
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
req.socket = (call)=>{
|
|
@@ -258,7 +262,7 @@ module.exports = class{
|
|
|
258
262
|
}
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
|
-
req.set
|
|
265
|
+
req.response.header.push(`set-cookie: ${text.join('; ')}\r\n`);
|
|
262
266
|
|
|
263
267
|
return req;
|
|
264
268
|
}
|
|
@@ -322,9 +326,9 @@ module.exports = class{
|
|
|
322
326
|
});
|
|
323
327
|
|
|
324
328
|
req.start = async(uid)=>{
|
|
325
|
-
let seckey = req.get('
|
|
326
|
-
let protocol = req.get('
|
|
327
|
-
let version = parseInt(req.get('
|
|
329
|
+
let seckey = req.get('sec-websocket-key');
|
|
330
|
+
let protocol = req.get('sec-websocket-protocol');
|
|
331
|
+
let version = parseInt(req.get('sec-websocket-version'));
|
|
328
332
|
|
|
329
333
|
if(version !== 13){
|
|
330
334
|
req.error(400);
|
|
@@ -345,10 +349,10 @@ module.exports = class{
|
|
|
345
349
|
req.userIn();
|
|
346
350
|
|
|
347
351
|
req.code(101)
|
|
348
|
-
.
|
|
352
|
+
req.response.header.push(`sec-websocket-accept: ${acceptKey}\r\n`);
|
|
349
353
|
|
|
350
354
|
if(protocol){
|
|
351
|
-
req.
|
|
355
|
+
req.response.header.push(`sec-websocket-protocol: ${protocol}\r\n`);
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
req.http();
|
|
@@ -449,14 +453,16 @@ module.exports = class{
|
|
|
449
453
|
(type == 'formdata' && !TYPE_TEST.startsWith('multipart/form-data')) ||
|
|
450
454
|
(type == 'wwwform' && !TYPE_TEST.startsWith('application/x-www-form-urlencoded'))
|
|
451
455
|
){
|
|
452
|
-
req.
|
|
456
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
457
|
+
req.code(415).http('{"status":false,"err":"Request Content-Type does not match the expected type."}');
|
|
453
458
|
return false;
|
|
454
459
|
}
|
|
455
460
|
|
|
456
461
|
if(typeof core == 'object'){
|
|
457
462
|
for(let id in core){
|
|
458
463
|
if(req[id] !== core[id]){
|
|
459
|
-
req.
|
|
464
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
465
|
+
req.code(400).http('{"status":false,"err":"The request does not satisfy the required conditions."}');
|
|
460
466
|
return false;
|
|
461
467
|
}
|
|
462
468
|
}
|
|
@@ -464,7 +470,8 @@ module.exports = class{
|
|
|
464
470
|
|
|
465
471
|
if(typeof data == 'object'){
|
|
466
472
|
if(typeof req.body != 'object'){
|
|
467
|
-
req.
|
|
473
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
474
|
+
req.code(415).http('{"status":false,"err":"Request body must be a valid object."}');
|
|
468
475
|
return false;
|
|
469
476
|
}
|
|
470
477
|
|
|
@@ -474,13 +481,15 @@ module.exports = class{
|
|
|
474
481
|
let value = type == 'json' ? req.body[id] : req.q(id);
|
|
475
482
|
let test = checkType(this.TYPE_TREE[t] || {}, value);
|
|
476
483
|
if(!test){
|
|
477
|
-
req.
|
|
484
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
485
|
+
req.code(400).http('{"status":false,"err":"Invalid format for parameter."}');
|
|
478
486
|
return false;
|
|
479
487
|
}
|
|
480
488
|
|
|
481
489
|
parameter[id] = value;
|
|
482
490
|
}else if(r === true){
|
|
483
|
-
req.
|
|
491
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_JSON);
|
|
492
|
+
req.code(400).http('{"status":false,"err":"Missing or invalid parameter."}');
|
|
484
493
|
return false;
|
|
485
494
|
}
|
|
486
495
|
}
|
|
@@ -547,7 +556,7 @@ module.exports = class{
|
|
|
547
556
|
}
|
|
548
557
|
|
|
549
558
|
req.code(status)
|
|
550
|
-
req.
|
|
559
|
+
req.response.header.push(`location: ${location}\r\n`);
|
|
551
560
|
req.http();
|
|
552
561
|
socket.destroy();
|
|
553
562
|
}
|
|
@@ -562,7 +571,7 @@ module.exports = class{
|
|
|
562
571
|
}
|
|
563
572
|
}
|
|
564
573
|
if(op.download){
|
|
565
|
-
req.
|
|
574
|
+
req.response.header.push(`content-disposition: attachment; filename="${op.download || path.basename(op.file)}"\r\n`);
|
|
566
575
|
}
|
|
567
576
|
|
|
568
577
|
if(op.parameter){
|
|
@@ -603,6 +612,18 @@ module.exports = class{
|
|
|
603
612
|
req.error(400);
|
|
604
613
|
}
|
|
605
614
|
}
|
|
615
|
+
req.event = (start, close)=>{
|
|
616
|
+
if(typeof start == 'function' && typeof close == 'function'){
|
|
617
|
+
req.response.header.push(httpHeader.CONTENT_TYPE_EVENT_STREAM);
|
|
618
|
+
|
|
619
|
+
req.http();
|
|
620
|
+
socket.pause();
|
|
621
|
+
|
|
622
|
+
req.userOut = close;
|
|
623
|
+
|
|
624
|
+
start(socket);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
606
627
|
}
|
|
607
628
|
}
|
|
608
629
|
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 });
|