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 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
@@ -4,21 +4,18 @@ module.exports = function(req){
4
4
  const src = req.header;
5
5
  const out = {};
6
6
 
7
- for (let i = 0; i < src.length; i++) {
8
- const buf = src[i];
7
+ for(let i = 0; i < src.length; i++){
8
+ const [buf, split] = src[i];
9
9
 
10
- let j = 0;
11
- while (buf[j] !== 58) j++;
12
- const key = buf.toString('ascii', 0, j);
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
- const val = buf.toString('ascii', j, buf.length);
17
-
18
- if(out[key]){
19
- out[key].push(val);
20
- }else{
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 = 0;
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
- while(p < len && !(chunk[p] === 13 && chunk[p+1] === 10)) p++;
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 0x436F6E74:{ // "Cont"
57
- if(h[8] === 76 && h[14] === 58 && h[15] === 32){ // Content-Length:
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] === 84 && h[12] === 58 && h[13] === 32){ // Content-Type:
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 0x486F7374:{ // "Host"
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 0x436F6E6E:{ // "Conn"
85
- // Connection
94
+ case 0x636F6E6E:{ // "conn"
95
+ // connection
86
96
  if(h[10] === 58 && h[11] === 32){
87
- if(h[12] === 107 && h.length === 22){ // Connection: keep-alive
88
- connection = 1
89
- }else if(h[12] === 85 && h.length === 19){ // Connection: Upgrade
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\nX-Powered-By: r938-upcore/${version}\r\n`
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.CONNECTION_UPGRADE = Buffer.from('Connection: Upgrade\r\nUpgrade: websocket\r\n');
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('Content-Length: ');
24
- module.exports.CONTENT_TYPE_PLAIN = Buffer.from('Content-Type: text/plain; charset=utf-8\r\n');
25
- module.exports.CONTENT_TYPE_HTML = Buffer.from('Content-Type: text/html; charset=utf-8\r\n');
26
- module.exports.CONTENT_TYPE_JSON = Buffer.from('Content-Type: application/json; charset=utf-8\r\n');
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');
@@ -28,10 +28,8 @@ module.exports = function(req, filePath, parameter, error){
28
28
  parameter?.[key] ?? ''
29
29
  );
30
30
 
31
- req
32
- .set('Content-Type', type)
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 checkHeader = require('./checkHeader');
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 = checkHeader(chunk);
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.CONNECTION_UPGRADE + httpHeader.CRLF);
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('Set-Cookie', text.join('; '));
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('Sec-WebSocket-Key');
326
- let protocol = req.get('Sec-WebSocket-Protocol');
327
- let version = parseInt(req.get('Sec-WebSocket-Version'));
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
- .set('Sec-WebSocket-Accept', acceptKey);
352
+ req.response.header.push(`sec-websocket-accept: ${acceptKey}\r\n`);
349
353
 
350
354
  if(protocol){
351
- req.set('Sec-WebSocket-Protocol', protocol);
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.code(415).set('Content-Length', 79).set('Content-Type', 'application/json').http('{"status":false,"err":"Request Content-Type does not match the expected type."}');
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.code(400).set('Content-Length', 78).set('Content-Type', 'application/json').http('{"status":false,"err":"The request does not satisfy the required conditions."}');
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.code(415).set('Content-Length', 61).set('Content-Type', 'application/json').http('{"status":false,"err":"Request body must be a valid object."}');
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.code(400).set('Content-Length', 54).set('Content-Type', 'application/json').http('{"status":false,"err":"Invalid format for parameter."}');
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.code(400).set('Content-Length', 54).set('Content-Type', 'application/json').http('{"status":false,"err":"Missing or invalid parameter."}');
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.set('Location', location)
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.set('Content-Disposition', `attachment; filename="${op.download || path.basename(op.file)}"`);
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
- .code(status)
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upcore-tcp",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Upcore Web framework support http websocket",
5
5
  "keywords": [
6
6
  "upcore",