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 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
@@ -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;
@@ -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.CONNECTION_UPGRADE + httpHeader.CRLF);
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('Set-Cookie', text.join('; '));
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('Sec-WebSocket-Key');
326
- let protocol = req.get('Sec-WebSocket-Protocol');
327
- let version = parseInt(req.get('Sec-WebSocket-Version'));
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
- .set('Sec-WebSocket-Accept', acceptKey);
357
+ req.response.header.push(`sec-websocket-accept: ${acceptKey}\r\n`);
349
358
 
350
359
  if(protocol){
351
- req.set('Sec-WebSocket-Protocol', protocol);
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.code(415).set('Content-Length', 79).set('Content-Type', 'application/json').http('{"status":false,"err":"Request Content-Type does not match the expected type."}');
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.code(400).set('Content-Length', 78).set('Content-Type', 'application/json').http('{"status":false,"err":"The request does not satisfy the required conditions."}');
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.code(415).set('Content-Length', 61).set('Content-Type', 'application/json').http('{"status":false,"err":"Request body must be a valid object."}');
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.code(400).set('Content-Length', 54).set('Content-Type', 'application/json').http('{"status":false,"err":"Invalid format for parameter."}');
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.code(400).set('Content-Length', 54).set('Content-Type', 'application/json').http('{"status":false,"err":"Missing or invalid parameter."}');
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.set('Location', location)
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.set('Content-Disposition', `attachment; filename="${op.download || path.basename(op.file)}"`);
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
- .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.3",
3
+ "version": "0.0.5",
4
4
  "description": "Upcore Web framework support http websocket",
5
5
  "keywords": [
6
6
  "upcore",