SPB Git forge

spb/admin-ka

Public
41commits 1branches 0releases
172.9 MBsize
maindefault branch
19 days agolast push
JavaScript 65.5% Python 17.8% CSS 13% HTML 3.7%

ajout de Groupe·Ka au sélecteur de projets du chat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 20, 2026) parent 733d288

32 changed files +6,069 −2

deleted .gitignore +0 −2
@@ -1,2 +0,0 @@
1 node_modules/
2 data/
added data/audit.jsonl +53 −0
@@ -0,0 +1,53 @@
1 +{"ts":"2026-08-20T00:09:46.342Z","kind":"login_fail","ip":"70.25.239.68"}
2 +{"ts":"2026-08-20T00:09:46.590Z","kind":"login_ok","ip":"70.25.239.68"}
3 +{"ts":"2026-08-20T00:14:18.159Z","kind":"login_ok","ip":"70.25.239.68"}
4 +{"ts":"2026-08-20T00:15:11.290Z","kind":"prompt","chatId":"3f52ef5c2d20f901","project":"/Users/simon-pierreboucher","node":"M3U96a","dir":"/Users/simon-pierreboucher","mode":"auto","prompt":"Réponds uniquement: PING-OK"}
5 +{"ts":"2026-08-20T00:15:13.001Z","kind":"result","chatId":"3f52ef5c2d20f901","subtype":"success","cost":0.07691825000000001,"duration_ms":1462,"turns":1}
6 +{"ts":"2026-08-20T00:15:32.184Z","kind":"prompt","chatId":"a605e4af3ff4ef68","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","mode":"safe","prompt":"Exécute la commande git log --oneline -1 avec l outil Bash puis rapporte la sortie exacte. Ne fais rien d autre."}
7 +{"ts":"2026-08-20T00:15:36.459Z","kind":"tool_use","chatId":"a605e4af3ff4ef68","tool":"Bash","input":"{\"command\":\"git log --oneline -1\",\"description\":\"Show last commit\"}"}
8 +{"ts":"2026-08-20T00:15:38.505Z","kind":"result","chatId":"a605e4af3ff4ef68","subtype":"success","cost":0.050321950000000004,"duration_ms":5467,"turns":2}
9 +{"ts":"2026-08-20T00:16:03.126Z","kind":"prompt","chatId":"a02996ca9e96e72a","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","mode":"safe","prompt":"Exécute exactement cette commande avec l outil Bash: touch /tmp/adminka-perm-test-2026 && echo CREATED. Rapporte la sortie."}
10 +{"ts":"2026-08-20T00:16:07.586Z","kind":"tool_use","chatId":"a02996ca9e96e72a","tool":"Bash","input":"{\"command\":\"touch /tmp/adminka-perm-test-2026 && echo CREATED\",\"description\":\"Create test file in /tmp and echo CREATED\"}"}
11 +{"ts":"2026-08-20T00:16:12.089Z","kind":"result","chatId":"a02996ca9e96e72a","subtype":"success","cost":0.052153599999999994,"duration_ms":7840,"turns":2}
12 +{"ts":"2026-08-20T00:19:02.513Z","kind":"login_ok","ip":"::ffff:127.0.0.1"}
13 +{"ts":"2026-08-20T00:19:02.681Z","kind":"login_ok","ip":"70.25.239.68"}
14 +{"ts":"2026-08-20T00:19:54.997Z","kind":"login_ok","ip":"2605:b100:122:c0a8:b11f:f73b:fe9d:bc14"}
15 +{"ts":"2026-08-20T00:21:54.323Z","kind":"prompt","chatId":"cd7a17fc2311a9bb","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","mode":"safe","prompt":"Execute exactement cette commande avec l outil Bash: touch /tmp/adminka-perm-test-2026 && echo CREATED. Rapporte la sortie. Aucun fichier de projet modifie donc pas de commit."}
16 +{"ts":"2026-08-20T00:21:58.202Z","kind":"tool_use","chatId":"cd7a17fc2311a9bb","tool":"Bash","input":"{\"command\":\"touch /tmp/adminka-perm-test-2026 && echo CREATED\",\"description\":\"Create test file in /tmp\"}"}
17 +{"ts":"2026-08-20T00:22:00.695Z","kind":"result","chatId":"cd7a17fc2311a9bb","subtype":"success","cost":0.05067714999999999,"duration_ms":5529,"turns":2}
18 +{"ts":"2026-08-20T00:23:10.934Z","kind":"prompt","chatId":"81465e6bdd155087","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","mode":"safe","prompt":"Execute exactement cette commande avec l outil Bash: touch /tmp/adminka-perm-test-2026 && echo CREATED. Rapporte la sortie."}
19 +{"ts":"2026-08-20T00:23:14.693Z","kind":"tool_use","chatId":"81465e6bdd155087","tool":"Bash","input":"{\"command\":\"touch /tmp/adminka-perm-test-2026 && echo CREATED\",\"description\":\"Create test file in /tmp\"}"}
20 +{"ts":"2026-08-20T00:23:14.704Z","kind":"permission_request","chatId":"81465e6bdd155087","tool":"Bash","input":"{\"command\":\"touch /tmp/adminka-perm-test-2026 && echo CREATED\",\"description\":\"Create test file in /tmp\"}"}
21 +{"ts":"2026-08-20T00:23:14.770Z","kind":"permission_response","chatId":"81465e6bdd155087","requestId":"9f46fa5841aade3c","allow":true}
22 +{"ts":"2026-08-20T00:23:16.945Z","kind":"result","chatId":"81465e6bdd155087","subtype":"success","cost":0.036749699999999996,"duration_ms":5149,"turns":2}
23 +{"ts":"2026-08-20T00:23:46.614Z","kind":"prompt","chatId":"6b89119c0416877c","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","mode":"safe","prompt":"Mémorise le mot de passe secret BANANE-42. Réponds uniquement: mémorisé."}
24 +{"ts":"2026-08-20T00:23:55.311Z","kind":"result","chatId":"6b89119c0416877c","subtype":"success","cost":0.0320148,"duration_ms":7829,"turns":1}
25 +{"ts":"2026-08-20T00:23:55.928Z","kind":"prompt","chatId":"6b89119c0416877c","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","mode":"safe","prompt":"Quel est le mot de passe secret que je viens de te donner ? Réponds uniquement le mot."}
26 +{"ts":"2026-08-20T00:23:59.032Z","kind":"result","chatId":"6b89119c0416877c","subtype":"success","cost":0.008705250000000001,"duration_ms":2670,"turns":1}
27 +{"ts":"2026-08-20T00:26:41.877Z","kind":"login_ok","ip":"2605:b100:122:c0a8:b11f:f73b:fe9d:bc14"}
28 +{"ts":"2026-08-20T00:54:46.997Z","kind":"prompt","chatId":"a3c0c1ae7ff9ae41","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","mode":"safe","prompt":"Allo"}
29 +{"ts":"2026-08-20T00:54:50.449Z","kind":"result","chatId":"a3c0c1ae7ff9ae41","subtype":"success","cost":0.041230199999999995,"duration_ms":2486,"turns":1}
30 +{"ts":"2026-08-20T01:55:35.435Z","kind":"prompt","chatId":"aec35de8e2dfa1ce","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","model":"opus","permMode":"bypass","prompt":"Quel modèle es-tu et dans quel répertoire de travail es-tu ? Vérifie avec pwd via Bash et donne aussi le nom du repo git. Réponds en 2 lignes."}
31 +{"ts":"2026-08-20T01:55:39.139Z","kind":"tool_use","chatId":"aec35de8e2dfa1ce","tool":"Bash","input":"{\"command\":\"pwd && basename $(git rev-parse --show-toplevel)\",\"description\":\"Show working directory and git repo name\"}"}
32 +{"ts":"2026-08-20T01:55:41.690Z","kind":"result","chatId":"aec35de8e2dfa1ce","subtype":"success","cost":0.115017,"duration_ms":5552,"turns":2}
33 +{"ts":"2026-08-20T01:55:58.883Z","kind":"prompt","chatId":"aec35de8e2dfa1ce","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","model":"haiku","permMode":"bypass","prompt":"Redis-moi quel modèle tu es maintenant, et rappelle en un mot ce que je t ai demandé au tour précédent."}
34 +{"ts":"2026-08-20T01:56:02.883Z","kind":"result","chatId":"aec35de8e2dfa1ce","subtype":"success","cost":0.015552400000000001,"duration_ms":3759,"turns":1}
35 +{"ts":"2026-08-20T01:56:27.868Z","kind":"prompt","chatId":"aec35de8e2dfa1ce","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","model":"opus","permMode":"bypass","prompt":"Cite mot pour mot la toute première question que je t ai posée dans cette conversation."}
36 +{"ts":"2026-08-20T01:56:29.892Z","kind":"result","chatId":"aec35de8e2dfa1ce","subtype":"success","cost":0.020039,"duration_ms":1777,"turns":1}
37 +{"ts":"2026-08-20T01:56:52.618Z","kind":"prompt","chatId":"55675f90d1d9eb7f","project":"sorti-ka@M3U96a","node":"M3U96a","dir":"/Users/simon-pierreboucher/apps/sorti-ka","model":"opus","permMode":"bypass","prompt":"Exécute avec Bash exactement: sleep 90 && echo TERMINE-APRES-90S — puis réponds uniquement: FINI."}
38 +{"ts":"2026-08-20T01:56:55.922Z","kind":"tool_use","chatId":"55675f90d1d9eb7f","tool":"Bash","input":"{\"command\":\"sleep 90 && echo TERMINE-APRES-90S\",\"description\":\"Sleep 90 seconds then echo\",\"timeout\":100000}"}
39 +{"ts":"2026-08-20T01:56:58.785Z","kind":"tool_use","chatId":"55675f90d1d9eb7f","tool":"Bash","input":"{\"command\":\"sleep 90 && echo TERMINE-APRES-90S\",\"description\":\"Sleep 90 seconds then echo in background\",\"run_in_background\":true}"}
40 +{"ts":"2026-08-20T01:57:01.018Z","kind":"tool_use","chatId":"55675f90d1d9eb7f","tool":"ToolSearch","input":"{\"query\":\"select:Monitor\",\"max_results\":1}"}
41 +{"ts":"2026-08-20T01:57:09.054Z","kind":"tool_use","chatId":"55675f90d1d9eb7f","tool":"Monitor","input":"{\"command\":\"until grep -q TERMINE-APRES-90S /private/tmp/claude-501/-Users-simon-pierreboucher-apps-sorti-ka/8f7fb976-2d34-4eb5-bf9c-80d1ba388e7d/tasks/be9tfey63.output 2>/dev/null; do sleep 2; done; echo DONE\",\"description\":\"wait for sleep-90 background command to finish\",\"timeout_ms\":120000,\"persistent\":false}"}
42 +{"ts":"2026-08-20T01:57:10.472Z","kind":"result","chatId":"55675f90d1d9eb7f","subtype":"success","cost":0.43205750000000004,"duration_ms":17578,"turns":5}
43 +{"ts":"2026-08-20T01:58:35.144Z","kind":"prompt","chatId":"c6bf4c6c75817784","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","model":"opus","permMode":"acceptEdits","prompt":"Crée le fichier docs/admin-ka-e2e.md avec ce contenu exact: ligne 1 \"# Test E2E Admin-Ka\", ligne 3 \"Validation du pilotage Claude Code via administration-ka.com — 2026-08-19.\" Puis git add docs/admin-ka-e2e.md, git commit -m \"docs: test E2E console Admin-Ka\" et git push origin main. Fichier de documentation seulement: NE redémarre PAS pm2, NE rebuild PAS."}
44 +{"ts":"2026-08-20T01:58:39.222Z","kind":"tool_use","chatId":"c6bf4c6c75817784","tool":"Bash","input":"{\"command\":\"ls /Users/simon-pierreboucher/apps/lou-ka/docs 2>/dev/null || echo \\\"docs dir does not exist\\\"\",\"description\":\"Check if docs directory exists\"}"}
45 +{"ts":"2026-08-20T01:58:41.972Z","kind":"tool_use","chatId":"c6bf4c6c75817784","tool":"Write","input":"{\"file_path\":\"/Users/simon-pierreboucher/apps/lou-ka/docs/admin-ka-e2e.md\",\"content\":\"# Test E2E Admin-Ka\\n\\nValidation du pilotage Claude Code via administration-ka.com — 2026-08-19.\\n\"}"}
46 +{"ts":"2026-08-20T01:58:44.232Z","kind":"tool_use","chatId":"c6bf4c6c75817784","tool":"Bash","input":"{\"command\":\"git add docs/admin-ka-e2e.md && git commit -m \\\"docs: test E2E console Admin-Ka\\\" && git push origin main\",\"description\":\"Commit and push the docs file\"}"}
47 +{"ts":"2026-08-20T01:58:44.290Z","kind":"permission_request","chatId":"c6bf4c6c75817784","tool":"Bash","input":"{\"command\":\"git add docs/admin-ka-e2e.md && git commit -m \\\"docs: test E2E console Admin-Ka\\\" && git push origin main\",\"description\":\"Commit and push the docs file\"}"}
48 +{"ts":"2026-08-20T01:58:44.354Z","kind":"permission_response","chatId":"c6bf4c6c75817784","requestId":"13397bb8a4894ac9","allow":true,"always":true}
49 +{"ts":"2026-08-20T01:58:46.639Z","kind":"result","chatId":"c6bf4c6c75817784","subtype":"success","cost":0.12456475,"duration_ms":10774,"turns":4}
50 +{"ts":"2026-08-20T02:00:25.939Z","kind":"prompt","chatId":"352c98e9e9d31975","project":"lou-ka@M3U96b","node":"M3U96b","dir":"~/apps/lou-ka","model":"fable","permMode":"bypass","prompt":"Quel modèle es-tu exactement (ID) ? Une ligne."}
51 +{"ts":"2026-08-20T02:00:31.512Z","kind":"result","chatId":"352c98e9e9d31975","subtype":"success","cost":0.10990075,"duration_ms":4957,"turns":1}
52 +{"ts":"2026-08-20T04:16:58.224Z","kind":"prompt","chatId":"21d6c1690bd84a05","project":"crea-ka@M3U96b","node":"M3U96b","dir":"~/apps/crea-ka","model":"fable","permMode":"default","prompt":"Allo"}
53 +{"ts":"2026-08-20T04:17:05.971Z","kind":"result","chatId":"21d6c1690bd84a05","subtype":"success","cost":0.15040725000000002,"duration_ms":6960,"turns":1}
added data/backend.err.log +0 −0
added data/backend.log +9 −0
@@ -0,0 +1,9 @@
1 +admin-ka backend sur http://127.0.0.1:3300
2 +admin-ka backend sur http://0.0.0.0:3300
3 +admin-ka backend sur port 3300 (dual-stack)
4 +admin-ka backend sur port 3300 (dual-stack)
5 +admin-ka backend sur port 3300 (dual-stack)
6 +admin-ka backend sur port 3300 (dual-stack)
7 +admin-ka v2 sur port 3300 (dual-stack)
8 +admin-ka v2 sur port 3300 (dual-stack)
9 +admin-ka v2 sur port 3300 (dual-stack)
added data/chats.json +21 −0
@@ -0,0 +1,21 @@
1 +[
2 + {
3 + "id": "21d6c1690bd84a05",
4 + "title": "Allo",
5 + "projectId": "crea-ka@M3U96b",
6 + "projectName": "Créa·Ka",
7 + "node": "M3U96b",
8 + "dir": "~/apps/crea-ka",
9 + "model": "fable",
10 + "permMode": "default",
11 + "claudeSessionId": "1a808aed-e9c3-4913-bef8-698d375e8789",
12 + "state": "idle",
13 + "totalCost": 0.15040725000000002,
14 + "turns": 1,
15 + "contextTokens": 39717,
16 + "alwaysAllow": [],
17 + "created": 1787199418217,
18 + "updated": 1787199426469,
19 + "lastModel": "claude-fable-5"
20 + }
21 +]
\ No newline at end of file
added data/config.json +5 −0
@@ -0,0 +1,5 @@
1 +{
2 + "salt": "c4b136cc7ecf11de11392c0e94754234",
3 + "passwordHash": "936110daf78afb40f2732f201199db9dd178942daad54823949e22db79e0b77f9e88f2b4dff589d3d14ecc0bf94576aa195925c3f9ccda7c7f3158ff05fffb40",
4 + "port": 3300
5 +}
\ No newline at end of file
added data/eco.sqlite +0 −0

Binary file not shown.

added data/ngrok.err.log +0 −0
added data/ngrok.log +325 −0
@@ -0,0 +1,325 @@
1 +t=2026-08-19T20:09:21-0400 lvl=info msg="no configuration paths supplied"
2 +t=2026-08-19T20:09:21-0400 lvl=info msg="using configuration at default config path" path="/Users/simon-pierreboucher/Library/Application Support/ngrok/ngrok.yml"
3 +t=2026-08-19T20:09:21-0400 lvl=info msg="open config file" path="/Users/simon-pierreboucher/Library/Application Support/ngrok/ngrok.yml" err=nil
4 +t=2026-08-19T20:09:21-0400 lvl=info msg="FIPS 140 mode" enabled=false
5 +t=2026-08-19T20:09:21-0400 lvl=warn msg="can't bind default web address, trying alternatives" obj=web addr=127.0.0.1:4040
6 +t=2026-08-19T20:09:21-0400 lvl=info msg="starting web service" obj=web addr=127.0.0.1:40413 allow_hosts=[]
7 +t=2026-08-19T20:09:21-0400 lvl=info msg="client session established" obj=tunnels.session
8 +t=2026-08-19T20:09:21-0400 lvl=info msg="tunnel session started" obj=tunnels.session
9 +t=2026-08-19T20:09:21-0400 lvl=info msg="update available" obj=updater
10 +t=2026-08-19T20:09:21-0400 lvl=info msg="started tunnel" obj=tunnels name=command_line addr=http://localhost:3300 url=https://www.administration-ka.com
11 +t=2026-08-19T20:09:46-0400 lvl=info msg="join connections" obj=join id=980a6d7c2d3b l=127.0.0.1:3300 r=70.25.239.68:60788
12 +t=2026-08-19T20:09:46-0400 lvl=info msg="join connections" obj=join id=5c1f696a3a66 l=127.0.0.1:3300 r=70.25.239.68:60789
13 +t=2026-08-19T20:09:46-0400 lvl=info msg="join connections" obj=join id=46fe0fc01d6f l=127.0.0.1:3300 r=70.25.239.68:60790
14 +t=2026-08-19T20:09:46-0400 lvl=info msg="join connections" obj=join id=3bdd2daa6599 l=127.0.0.1:3300 r=70.25.239.68:60791
15 +t=2026-08-19T20:09:46-0400 lvl=info msg="join connections" obj=join id=da42bfe0036b l=127.0.0.1:3300 r=70.25.239.68:60792
16 +t=2026-08-19T20:10:55-0400 lvl=info msg="join connections" obj=join id=d5e06e3670a1 l=127.0.0.1:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58680
17 +t=2026-08-19T20:10:55-0400 lvl=info msg="join connections" obj=join id=b44c284a2628 l=127.0.0.1:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58680
18 +t=2026-08-19T20:10:55-0400 lvl=info msg="join connections" obj=join id=c7fc1f1d3815 l=127.0.0.1:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58680
19 +t=2026-08-19T20:10:55-0400 lvl=info msg="join connections" obj=join id=9db72a834f55 l=127.0.0.1:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58680
20 +t=2026-08-19T20:10:55-0400 lvl=info msg="join connections" obj=join id=448679569283 l=127.0.0.1:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58680
21 +t=2026-08-19T20:10:56-0400 lvl=info msg="join connections" obj=join id=9b4580c6a9d4 l=127.0.0.1:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58681
22 +t=2026-08-19T20:12:33-0400 lvl=info msg="join connections" obj=join id=898faed82b2a l=127.0.0.1:3300 r=70.25.239.68:61178
23 +t=2026-08-19T20:12:33-0400 lvl=info msg="join connections" obj=join id=a2850fd431a7 l=127.0.0.1:3300 r=70.25.239.68:61178
24 +t=2026-08-19T20:12:33-0400 lvl=info msg="join connections" obj=join id=50c2f8aff620 l=127.0.0.1:3300 r=70.25.239.68:61178
25 +t=2026-08-19T20:12:33-0400 lvl=info msg="join connections" obj=join id=6689908fa5b8 l=127.0.0.1:3300 r=70.25.239.68:61178
26 +t=2026-08-19T20:12:33-0400 lvl=info msg="join connections" obj=join id=4907f98b3412 l=127.0.0.1:3300 r=70.25.239.68:61178
27 +t=2026-08-19T20:14:18-0400 lvl=info msg="join connections" obj=join id=966c9fd6bd2a l=127.0.0.1:3300 r=70.25.239.68:61178
28 +t=2026-08-19T20:14:18-0400 lvl=info msg="join connections" obj=join id=f850e915d8df l=127.0.0.1:3300 r=70.25.239.68:61180
29 +t=2026-08-19T20:14:43-0400 lvl=info msg="join connections" obj=join id=f0278a8f8868 l=127.0.0.1:3300 r=70.25.239.68:61182
30 +t=2026-08-19T20:15:05-0400 lvl=info msg="join connections" obj=join id=2e09cfdeb763 l=127.0.0.1:3300 r=70.25.239.68:61178
31 +t=2026-08-19T20:15:05-0400 lvl=info msg="join connections" obj=join id=ed38b337a25a l=127.0.0.1:3300 r=70.25.239.68:61178
32 +t=2026-08-19T20:15:06-0400 lvl=info msg="join connections" obj=join id=501220e8b873 l=127.0.0.1:3300 r=70.25.239.68:61185
33 +t=2026-08-19T20:15:11-0400 lvl=info msg="join connections" obj=join id=c10c89a96eee l=127.0.0.1:3300 r=76.70.60.50:49847
34 +t=2026-08-19T20:15:32-0400 lvl=info msg="join connections" obj=join id=e6cabb676952 l=127.0.0.1:3300 r=76.70.60.50:49969
35 +t=2026-08-19T20:16:03-0400 lvl=info msg="join connections" obj=join id=1ccabdbb46e7 l=127.0.0.1:3300 r=76.70.60.50:50066
36 +t=2026-08-19T20:16:22-0400 lvl=info msg="join connections" obj=join id=b94bb62f7425 l=127.0.0.1:3300 r=70.25.239.68:61178
37 +t=2026-08-19T20:16:39-0400 lvl=info msg="join connections" obj=join id=5b7063cfde4f l=127.0.0.1:3300 r=74.7.230.51:44558
38 +t=2026-08-19T20:16:40-0400 lvl=info msg="join connections" obj=join id=381c043b7b39 l=127.0.0.1:3300 r=74.7.242.32:41364
39 +t=2026-08-19T20:17:20-0400 lvl=info msg="join connections" obj=join id=6b9c1bd836f0 l=[::1]:3300 r=70.25.239.68:61194
40 +t=2026-08-19T20:18:11-0400 lvl=info msg="join connections" obj=join id=cf5f66bce1c8 l=[::1]:3300 r=70.25.239.68:61178
41 +t=2026-08-19T20:18:11-0400 lvl=info msg="join connections" obj=join id=d3de1b52cb3f l=[::1]:3300 r=70.25.239.68:61178
42 +t=2026-08-19T20:18:11-0400 lvl=info msg="join connections" obj=join id=d57e107f07e6 l=[::1]:3300 r=70.25.239.68:61198
43 +t=2026-08-19T20:19:01-0400 lvl=info msg="join connections" obj=join id=6a0b2df6e4b0 l=[::1]:3300 r=70.25.239.68:61201
44 +t=2026-08-19T20:19:02-0400 lvl=info msg="join connections" obj=join id=b6d71e5db46a l=[::1]:3300 r=70.25.239.68:61044
45 +t=2026-08-19T20:19:17-0400 lvl=info msg="join connections" obj=join id=f61b26ca5d4d l=[::1]:3300 r=70.25.239.68:61178
46 +t=2026-08-19T20:19:17-0400 lvl=info msg="join connections" obj=join id=d4e6708ce744 l=[::1]:3300 r=70.25.239.68:61178
47 +t=2026-08-19T20:19:18-0400 lvl=info msg="join connections" obj=join id=e4150d486b1a l=[::1]:3300 r=70.25.239.68:61203
48 +t=2026-08-19T20:19:46-0400 lvl=info msg="join connections" obj=join id=35c2512ad178 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
49 +t=2026-08-19T20:19:46-0400 lvl=info msg="join connections" obj=join id=33618c8ce7f2 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
50 +t=2026-08-19T20:19:46-0400 lvl=info msg="join connections" obj=join id=a85b93644124 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
51 +t=2026-08-19T20:19:46-0400 lvl=info msg="join connections" obj=join id=487f1f07757c l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
52 +t=2026-08-19T20:19:46-0400 lvl=info msg="join connections" obj=join id=e6d554cf2a8c l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
53 +t=2026-08-19T20:19:54-0400 lvl=info msg="join connections" obj=join id=d8d75780fb1f l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
54 +t=2026-08-19T20:19:55-0400 lvl=info msg="join connections" obj=join id=5fa348af28aa l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58947
55 +t=2026-08-19T20:20:01-0400 lvl=info msg="join connections" obj=join id=e0afa0f6fb8c l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
56 +t=2026-08-19T20:20:07-0400 lvl=info msg="join connections" obj=join id=a7bd0c32dbb1 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
57 +t=2026-08-19T20:20:07-0400 lvl=info msg="join connections" obj=join id=1fe4ee89a34c l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58949
58 +t=2026-08-19T20:20:17-0400 lvl=info msg="join connections" obj=join id=2fa2930858a3 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
59 +t=2026-08-19T20:20:17-0400 lvl=info msg="join connections" obj=join id=3d5cb3abf09d l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58937
60 +t=2026-08-19T20:20:18-0400 lvl=info msg="join connections" obj=join id=41778aa549bc l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58951
61 +t=2026-08-19T20:21:11-0400 lvl=info msg="join connections" obj=join id=71517d3ba713 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58952
62 +t=2026-08-19T20:21:11-0400 lvl=info msg="join connections" obj=join id=9b7bf784f172 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58952
63 +t=2026-08-19T20:21:11-0400 lvl=info msg="join connections" obj=join id=a578d46dd2a6 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58958
64 +t=2026-08-19T20:21:13-0400 lvl=info msg="join connections" obj=join id=52e81ec32b77 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58959
65 +t=2026-08-19T20:21:13-0400 lvl=info msg="join connections" obj=join id=eba92f75b634 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:58961
66 +t=2026-08-19T20:21:44-0400 lvl=info msg="join connections" obj=join id=7983c0607377 l=[::1]:3300 r=70.25.239.68:61212
67 +t=2026-08-19T20:21:54-0400 lvl=info msg="join connections" obj=join id=a9c04db4a702 l=[::1]:3300 r=76.70.60.50:51559
68 +t=2026-08-19T20:23:09-0400 lvl=info msg="join connections" obj=join id=ccbadafce458 l=[::1]:3300 r=70.25.239.68:61215
69 +t=2026-08-19T20:23:10-0400 lvl=info msg="join connections" obj=join id=511151556827 l=[::1]:3300 r=76.70.60.50:51868
70 +t=2026-08-19T20:23:46-0400 lvl=info msg="join connections" obj=join id=d96aabf566e9 l=[::1]:3300 r=76.70.60.50:51989
71 +t=2026-08-19T20:24:00-0400 lvl=info msg="join connections" obj=join id=d000143fd634 l=[::1]:3300 r=70.25.239.68:61153
72 +t=2026-08-19T20:24:00-0400 lvl=info msg="join connections" obj=join id=dc709ba7a1ea l=[::1]:3300 r=70.25.239.68:61154
73 +t=2026-08-19T20:24:00-0400 lvl=info msg="join connections" obj=join id=a8b0cf15bb58 l=[::1]:3300 r=70.25.239.68:61155
74 +t=2026-08-19T20:24:00-0400 lvl=info msg="join connections" obj=join id=238209481dab l=[::1]:3300 r=70.25.239.68:61156
75 +t=2026-08-19T20:24:00-0400 lvl=info msg="join connections" obj=join id=416cec98c6f8 l=[::1]:3300 r=70.25.239.68:61157
76 +t=2026-08-19T20:26:33-0400 lvl=info msg="join connections" obj=join id=c3ca6fc3d898 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59109
77 +t=2026-08-19T20:26:33-0400 lvl=info msg="join connections" obj=join id=97fd39d6279c l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59110
78 +t=2026-08-19T20:26:33-0400 lvl=info msg="join connections" obj=join id=3144d5ecc517 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59109
79 +t=2026-08-19T20:26:33-0400 lvl=info msg="join connections" obj=join id=fda1b5685aaf l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59109
80 +t=2026-08-19T20:26:33-0400 lvl=info msg="join connections" obj=join id=bb3c94e2c887 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59110
81 +t=2026-08-19T20:26:41-0400 lvl=info msg="join connections" obj=join id=a972348ca626 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59109
82 +t=2026-08-19T20:26:42-0400 lvl=info msg="join connections" obj=join id=1b6a44e6ae48 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59113
83 +t=2026-08-19T20:26:50-0400 lvl=info msg="join connections" obj=join id=5251de2cc125 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59109
84 +t=2026-08-19T20:27:36-0400 lvl=info msg="join connections" obj=join id=ef07095e8c87 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59118
85 +t=2026-08-19T20:30:29-0400 lvl=info msg="join connections" obj=join id=2dcf5956dc4f l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59165
86 +t=2026-08-19T20:33:37-0400 lvl=info msg="join connections" obj=join id=2a382cc497af l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59252
87 +t=2026-08-19T20:35:01-0400 lvl=info msg="join connections" obj=join id=3ee68693eadd l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59259
88 +t=2026-08-19T20:35:17-0400 lvl=info msg="join connections" obj=join id=aad867620c1f l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59264
89 +t=2026-08-19T20:35:17-0400 lvl=info msg="join connections" obj=join id=e2c437bd2af1 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59264
90 +t=2026-08-19T20:35:17-0400 lvl=info msg="join connections" obj=join id=e7037481d67b l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59264
91 +t=2026-08-19T20:35:18-0400 lvl=info msg="join connections" obj=join id=f3e024c4d121 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59266
92 +t=2026-08-19T20:35:20-0400 lvl=info msg="join connections" obj=join id=d7f49ecb8491 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59267
93 +t=2026-08-19T20:35:20-0400 lvl=info msg="join connections" obj=join id=1f94b4239ecb l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59267
94 +t=2026-08-19T20:35:20-0400 lvl=info msg="join connections" obj=join id=5f1e4aaa5900 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59267
95 +t=2026-08-19T20:36:05-0400 lvl=info msg="join connections" obj=join id=1840b60abc91 l=[::1]:3300 r=76.70.60.50:52253
96 +t=2026-08-19T20:36:05-0400 lvl=info msg="join connections" obj=join id=39978d0df1b1 l=[::1]:3300 r=76.70.60.50:52253
97 +t=2026-08-19T20:36:05-0400 lvl=info msg="join connections" obj=join id=0675d310ee49 l=[::1]:3300 r=76.70.60.50:52253
98 +t=2026-08-19T20:36:05-0400 lvl=info msg="join connections" obj=join id=0441920f031d l=[::1]:3300 r=76.70.60.50:52253
99 +t=2026-08-19T20:36:05-0400 lvl=info msg="join connections" obj=join id=ad2bb07c72c7 l=[::1]:3300 r=76.70.60.50:52253
100 +t=2026-08-19T20:36:11-0400 lvl=info msg="join connections" obj=join id=e4f01289a400 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59334
101 +t=2026-08-19T20:37:55-0400 lvl=info msg="join connections" obj=join id=26a7a8124c46 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59477
102 +t=2026-08-19T20:38:34-0400 lvl=info msg="join connections" obj=join id=cc87f89405c0 l=[::1]:3300 r=76.70.60.50:52255
103 +t=2026-08-19T20:38:34-0400 lvl=info msg="join connections" obj=join id=29f427844065 l=[::1]:3300 r=76.70.60.50:52255
104 +t=2026-08-19T20:40:01-0400 lvl=info msg="join connections" obj=join id=7589e76cd06b l=[::1]:3300 r=216.73.217.52:24133
105 +t=2026-08-19T20:40:32-0400 lvl=info msg="join connections" obj=join id=c41da4e0fd39 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59637
106 +t=2026-08-19T20:52:43-0400 lvl=info msg="join connections" obj=join id=a1c5936ba614 l=[::1]:3300 r=168.100.9.46:51230
107 +t=2026-08-19T20:54:38-0400 lvl=info msg="join connections" obj=join id=ba6958a28f23 l=[::1]:3300 r=184.151.190.153:59714
108 +t=2026-08-19T20:55:06-0400 lvl=info msg="join connections" obj=join id=ac42a620c819 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59945
109 +t=2026-08-19T20:55:30-0400 lvl=info msg="join connections" obj=join id=dd2492d3e89b l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59949
110 +t=2026-08-19T20:55:59-0400 lvl=info msg="join connections" obj=join id=a4d3f7e1724b l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59973
111 +t=2026-08-19T20:57:40-0400 lvl=info msg="join connections" obj=join id=d37e9c52a598 l=[::1]:3300 r=[2605:b100:122:c0a8:b11f:f73b:fe9d:bc14]:59984
112 +t=2026-08-19T21:13:35-0400 lvl=info msg="join connections" obj=join id=99a6b45518cb l=[::1]:3300 r=67.215.237.243:15001
113 +t=2026-08-19T21:21:09-0400 lvl=info msg="join connections" obj=join id=38ffabd533b9 l=[::1]:3300 r=76.70.60.50:52371
114 +t=2026-08-19T21:21:10-0400 lvl=info msg="join connections" obj=join id=95aa98ab08cd l=[::1]:3300 r=76.70.60.50:52371
115 +t=2026-08-19T21:21:10-0400 lvl=info msg="join connections" obj=join id=8c28ce4993f6 l=[::1]:3300 r=76.70.60.50:52371
116 +t=2026-08-19T21:21:10-0400 lvl=info msg="join connections" obj=join id=939c5e7192cd l=[::1]:3300 r=76.70.60.50:52371
117 +t=2026-08-19T21:21:10-0400 lvl=info msg="join connections" obj=join id=79cf54ebe3ed l=[::1]:3300 r=76.70.60.50:52371
118 +t=2026-08-19T21:26:47-0400 lvl=info msg="join connections" obj=join id=1fde653346ac l=[::1]:3300 r=95.181.238.176:60486
119 +t=2026-08-19T21:26:47-0400 lvl=info msg="join connections" obj=join id=a94f1ca60038 l=[::1]:3300 r=188.214.125.44:38248
120 +t=2026-08-19T21:26:47-0400 lvl=info msg="join connections" obj=join id=57c6a5fff414 l=[::1]:3300 r=158.173.101.49:49914
121 +t=2026-08-19T21:26:48-0400 lvl=info msg="join connections" obj=join id=26ebdb20fbe1 l=[::1]:3300 r=45.154.153.3:49918
122 +t=2026-08-19T21:26:48-0400 lvl=info msg="join connections" obj=join id=c6510d5a1891 l=[::1]:3300 r=191.96.168.41:45206
123 +t=2026-08-19T21:26:48-0400 lvl=info msg="join connections" obj=join id=81d2d29cf0cc l=[::1]:3300 r=45.132.194.11:47250
124 +t=2026-08-19T21:26:48-0400 lvl=info msg="join connections" obj=join id=fc693b0c9835 l=[::1]:3300 r=154.47.30.5:49884
125 +t=2026-08-19T21:27:22-0400 lvl=info msg="join connections" obj=join id=045a389c786b l=[::1]:3300 r=62.210.198.226:49734
126 +t=2026-08-19T21:27:24-0400 lvl=info msg="join connections" obj=join id=24cb416fd483 l=[::1]:3300 r=62.210.198.226:50897
127 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=d81277cb17d9 l=[::1]:3300 r=62.210.198.226:50897
128 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=d91f73084ed2 l=[::1]:3300 r=62.210.198.226:50897
129 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=de81fc49ba71 l=[::1]:3300 r=62.210.198.226:50897
130 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=17cbe828e617 l=[::1]:3300 r=62.210.198.226:50897
131 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=279f56968814 l=[::1]:3300 r=62.210.198.226:45102
132 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=26174e1522dd l=[::1]:3300 r=62.210.198.226:45102
133 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=a669475a9657 l=[::1]:3300 r=62.210.198.226:45102
134 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=a065a2dde61d l=[::1]:3300 r=62.210.198.226:45102
135 +t=2026-08-19T21:27:25-0400 lvl=info msg="join connections" obj=join id=5af7c33f6ae0 l=[::1]:3300 r=62.210.198.226:45102
136 +t=2026-08-19T21:27:30-0400 lvl=info msg="join connections" obj=join id=4720db600eaf l=[::1]:3300 r=62.210.198.226:56340
137 +t=2026-08-19T21:27:31-0400 lvl=info msg="join connections" obj=join id=db453b8760ac l=[::1]:3300 r=62.210.198.226:50068
138 +t=2026-08-19T21:27:31-0400 lvl=info msg="join connections" obj=join id=b34e608242b6 l=[::1]:3300 r=62.210.198.226:44116
139 +t=2026-08-19T21:27:31-0400 lvl=info msg="join connections" obj=join id=bf12975cc54a l=[::1]:3300 r=62.210.198.226:50072
140 +t=2026-08-19T21:38:02-0400 lvl=info msg="join connections" obj=join id=989be1349e22 l=[::1]:3300 r=216.73.217.52:42025
141 +t=2026-08-19T21:40:19-0400 lvl=info msg="join connections" obj=join id=c2679743dfdc l=[::1]:3300 r=216.73.217.52:27053
142 +t=2026-08-19T21:41:26-0400 lvl=info msg="join connections" obj=join id=9c4f11751482 l=[::1]:3300 r=216.73.217.52:55060
143 +t=2026-08-19T21:41:29-0400 lvl=info msg="join connections" obj=join id=84fadf170ee4 l=[::1]:3300 r=216.73.217.52:4879
144 +t=2026-08-19T21:41:29-0400 lvl=info msg="join connections" obj=join id=f09fffad840b l=[::1]:3300 r=216.73.217.52:4879
145 +t=2026-08-19T21:43:25-0400 lvl=info msg="join connections" obj=join id=e37e9ac3f827 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51950
146 +t=2026-08-19T21:43:25-0400 lvl=info msg="join connections" obj=join id=049a5350fc00 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51950
147 +t=2026-08-19T21:43:25-0400 lvl=info msg="join connections" obj=join id=2f47f53d40ef l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51952
148 +t=2026-08-19T21:44:59-0400 lvl=info msg="join connections" obj=join id=0b49a3ce6c0e l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51953
149 +t=2026-08-19T21:44:59-0400 lvl=info msg="join connections" obj=join id=74aa49f6e541 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51953
150 +t=2026-08-19T21:45:00-0400 lvl=info msg="join connections" obj=join id=1cb5ad7a1381 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51955
151 +t=2026-08-19T21:45:02-0400 lvl=info msg="join connections" obj=join id=58c621506d55 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51957
152 +t=2026-08-19T21:47:58-0400 lvl=info msg="join connections" obj=join id=69544b6ba010 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51986
153 +t=2026-08-19T21:47:59-0400 lvl=info msg="join connections" obj=join id=b0d8d06e4139 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51986
154 +t=2026-08-19T21:47:59-0400 lvl=info msg="join connections" obj=join id=fbae22152165 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51987
155 +t=2026-08-19T21:47:59-0400 lvl=info msg="join connections" obj=join id=825ba5a90cf3 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51989
156 +t=2026-08-19T21:48:11-0400 lvl=info msg="join connections" obj=join id=4c535204cba4 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51994
157 +t=2026-08-19T21:48:13-0400 lvl=info msg="join connections" obj=join id=3e451ec923bf l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
158 +t=2026-08-19T21:48:13-0400 lvl=info msg="join connections" obj=join id=c6cf93cae865 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
159 +t=2026-08-19T21:48:14-0400 lvl=info msg="join connections" obj=join id=8d72a0d3b2ec l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52001
160 +t=2026-08-19T21:48:26-0400 lvl=info msg="join connections" obj=join id=0561f40a3839 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
161 +t=2026-08-19T21:48:36-0400 lvl=info msg="join connections" obj=join id=3ec3e709268c l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
162 +t=2026-08-19T21:48:45-0400 lvl=info msg="join connections" obj=join id=9e197c2031ab l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
163 +t=2026-08-19T21:48:45-0400 lvl=info msg="join connections" obj=join id=8b5685b5d140 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
164 +t=2026-08-19T21:48:45-0400 lvl=info msg="join connections" obj=join id=f2365b890332 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52003
165 +t=2026-08-19T21:48:53-0400 lvl=info msg="join connections" obj=join id=1fcd97b169b8 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
166 +t=2026-08-19T21:48:53-0400 lvl=info msg="join connections" obj=join id=b807fd4d185b l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:51999
167 +t=2026-08-19T21:48:53-0400 lvl=info msg="join connections" obj=join id=f54674471fd0 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52005
168 +t=2026-08-19T21:48:55-0400 lvl=info msg="join connections" obj=join id=9bc293a0e30f l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52007
169 +t=2026-08-19T21:50:21-0400 lvl=info msg="join connections" obj=join id=914b31df9a61 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52035
170 +t=2026-08-19T21:50:21-0400 lvl=info msg="join connections" obj=join id=1ea4a6498aaf l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52035
171 +t=2026-08-19T21:50:21-0400 lvl=info msg="join connections" obj=join id=3abc7a449bba l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52037
172 +t=2026-08-19T21:50:23-0400 lvl=info msg="join connections" obj=join id=b4ac45e1a39c l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52040
173 +t=2026-08-19T21:50:26-0400 lvl=info msg="join connections" obj=join id=66c789ecca85 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52043
174 +t=2026-08-19T21:51:43-0400 lvl=info msg="join connections" obj=join id=4fe95d2d7042 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52047
175 +t=2026-08-19T21:52:43-0400 lvl=info msg="join connections" obj=join id=b4552c5714fa l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52073
176 +t=2026-08-19T21:53:18-0400 lvl=info msg="join connections" obj=join id=dc0e934ed2bd l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52084
177 +t=2026-08-19T21:54:12-0400 lvl=info msg="join connections" obj=join id=4aba30031d49 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52377
178 +t=2026-08-19T21:54:30-0400 lvl=info msg="join connections" obj=join id=13a47c276b60 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52383
179 +t=2026-08-19T21:54:36-0400 lvl=info msg="join connections" obj=join id=e801cbc8a0f4 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52384
180 +t=2026-08-19T21:54:36-0400 lvl=info msg="join connections" obj=join id=604bf8fadbae l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52384
181 +t=2026-08-19T21:54:36-0400 lvl=info msg="join connections" obj=join id=67ba18d9e7bb l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52386
182 +t=2026-08-19T21:54:46-0400 lvl=info msg="join connections" obj=join id=94616a930659 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52387
183 +t=2026-08-19T21:54:46-0400 lvl=info msg="join connections" obj=join id=0dfbccb8c3d6 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52387
184 +t=2026-08-19T21:54:47-0400 lvl=info msg="join connections" obj=join id=db6ce9ea5881 l=[::1]:3300 r=70.25.239.68:61380
185 +t=2026-08-19T21:54:55-0400 lvl=info msg="join connections" obj=join id=bda461a22eed l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52392
186 +t=2026-08-19T21:55:35-0400 lvl=info msg="join connections" obj=join id=6417e0202d87 l=[::1]:3300 r=76.70.60.50:54151
187 +t=2026-08-19T21:55:58-0400 lvl=info msg="join connections" obj=join id=9e2a734125ff l=[::1]:3300 r=76.70.60.50:54280
188 +t=2026-08-19T21:56:27-0400 lvl=info msg="join connections" obj=join id=df4a75ce5c7f l=[::1]:3300 r=76.70.60.50:54412
189 +t=2026-08-19T21:56:52-0400 lvl=info msg="join connections" obj=join id=199eedad2c07 l=[::1]:3300 r=76.70.60.50:54478
190 +t=2026-08-19T21:57:43-0400 lvl=info msg="join connections" obj=join id=132eb0da7cfc l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52404
191 +t=2026-08-19T21:57:46-0400 lvl=info msg="join connections" obj=join id=837528564b9f l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52406
192 +t=2026-08-19T21:57:46-0400 lvl=info msg="join connections" obj=join id=5826ebba1769 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52406
193 +t=2026-08-19T21:57:46-0400 lvl=info msg="join connections" obj=join id=ac722a7a1f87 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52407
194 +t=2026-08-19T21:57:46-0400 lvl=info msg="join connections" obj=join id=225ca92b4854 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52409
195 +t=2026-08-19T21:58:06-0400 lvl=info msg="join connections" obj=join id=567ac0f902fb l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52406
196 +t=2026-08-19T21:58:10-0400 lvl=info msg="join connections" obj=join id=28d839db5411 l=[::1]:3300 r=76.70.60.50:54850
197 +t=2026-08-19T21:58:35-0400 lvl=info msg="join connections" obj=join id=56e6de71eb26 l=[::1]:3300 r=76.70.60.50:54915
198 +t=2026-08-19T21:58:59-0400 lvl=info msg="join connections" obj=join id=da34b55080fa l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52423
199 +t=2026-08-19T21:59:00-0400 lvl=info msg="join connections" obj=join id=b7736ce4f9d2 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52426
200 +t=2026-08-19T22:00:23-0400 lvl=info msg="join connections" obj=join id=885fbf944200 l=[::1]:3300 r=70.25.239.68:61387
201 +t=2026-08-19T22:00:25-0400 lvl=info msg="join connections" obj=join id=4a0cb0b6b8d9 l=[::1]:3300 r=76.70.60.50:55389
202 +t=2026-08-19T22:00:31-0400 lvl=info msg="join connections" obj=join id=cfc9a7485d7a l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52436
203 +t=2026-08-19T22:00:32-0400 lvl=info msg="join connections" obj=join id=32c84474834e l=[::1]:3300 r=70.25.239.68:63336
204 +t=2026-08-19T22:00:41-0400 lvl=info msg="join connections" obj=join id=584ea3eba612 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52441
205 +t=2026-08-19T22:00:48-0400 lvl=info msg="join connections" obj=join id=1e4aa6df079d l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52443
206 +t=2026-08-19T22:00:53-0400 lvl=info msg="join connections" obj=join id=1e43c5248adb l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52447
207 +t=2026-08-19T22:02:50-0400 lvl=info msg="join connections" obj=join id=483f92c2c0e2 l=[::1]:3300 r=70.25.239.68:63392
208 +t=2026-08-19T22:04:05-0400 lvl=info msg="join connections" obj=join id=6a1cf6f5c1f9 l=[::1]:3300 r=70.25.239.68:63430
209 +t=2026-08-19T22:04:06-0400 lvl=info msg="join connections" obj=join id=ee3a56157880 l=[::1]:3300 r=70.25.239.68:63439
210 +t=2026-08-19T22:04:33-0400 lvl=info msg="join connections" obj=join id=190558b97605 l=[::1]:3300 r=70.25.239.68:63454
211 +t=2026-08-19T22:04:33-0400 lvl=info msg="join connections" obj=join id=ac0228440018 l=[::1]:3300 r=70.25.239.68:63455
212 +t=2026-08-19T22:04:33-0400 lvl=info msg="join connections" obj=join id=5f31718130e2 l=[::1]:3300 r=70.25.239.68:63456
213 +t=2026-08-19T22:04:33-0400 lvl=info msg="join connections" obj=join id=4373318ebd36 l=[::1]:3300 r=70.25.239.68:63457
214 +t=2026-08-19T22:04:34-0400 lvl=info msg="join connections" obj=join id=bf29ac1c79dc l=[::1]:3300 r=70.25.239.68:63458
215 +t=2026-08-19T22:04:34-0400 lvl=info msg="join connections" obj=join id=13a60f98b172 l=[::1]:3300 r=70.25.239.68:63459
216 +t=2026-08-19T22:04:34-0400 lvl=info msg="join connections" obj=join id=2a934ff11959 l=[::1]:3300 r=70.25.239.68:63460
217 +t=2026-08-19T22:04:34-0400 lvl=info msg="join connections" obj=join id=e53564ead02b l=[::1]:3300 r=70.25.239.68:63461
218 +t=2026-08-19T22:04:35-0400 lvl=info msg="join connections" obj=join id=381591839ed1 l=[::1]:3300 r=70.25.239.68:63462
219 +t=2026-08-19T22:04:35-0400 lvl=info msg="join connections" obj=join id=7d4626114a07 l=[::1]:3300 r=70.25.239.68:63463
220 +t=2026-08-19T22:04:35-0400 lvl=info msg="join connections" obj=join id=65da02325afa l=[::1]:3300 r=70.25.239.68:63464
221 +t=2026-08-19T22:04:35-0400 lvl=info msg="join connections" obj=join id=9b1bf7b0cd54 l=[::1]:3300 r=70.25.239.68:63465
222 +t=2026-08-19T22:04:35-0400 lvl=info msg="join connections" obj=join id=b134cab4d853 l=[::1]:3300 r=70.25.239.68:63466
223 +t=2026-08-19T22:04:35-0400 lvl=info msg="join connections" obj=join id=c255f283eb1b l=[::1]:3300 r=70.25.239.68:63467
224 +t=2026-08-19T22:04:36-0400 lvl=info msg="join connections" obj=join id=c74aa187453a l=[::1]:3300 r=70.25.239.68:63468
225 +t=2026-08-19T22:04:36-0400 lvl=info msg="join connections" obj=join id=fc0b38050874 l=[::1]:3300 r=70.25.239.68:63469
226 +t=2026-08-19T22:08:34-0400 lvl=info msg="join connections" obj=join id=bda40b869978 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52502
227 +t=2026-08-19T22:08:42-0400 lvl=info msg="join connections" obj=join id=dc767205f0d3 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52504
228 +t=2026-08-19T22:09:38-0400 lvl=info msg="join connections" obj=join id=07d704a82bdc l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52513
229 +t=2026-08-19T22:09:49-0400 lvl=info msg="join connections" obj=join id=c0f4019ad697 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52515
230 +t=2026-08-19T22:12:33-0400 lvl=info msg="join connections" obj=join id=5e5c5347cc91 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52537
231 +t=2026-08-19T22:12:34-0400 lvl=info msg="join connections" obj=join id=cf09795ece3f l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52540
232 +t=2026-08-19T22:13:00-0400 lvl=info msg="join connections" obj=join id=bed5aaa493f2 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52545
233 +t=2026-08-19T22:13:16-0400 lvl=info msg="join connections" obj=join id=b5e2f2f6569d l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52549
234 +t=2026-08-19T22:13:16-0400 lvl=info msg="join connections" obj=join id=9e15e747c50b l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52549
235 +t=2026-08-19T22:13:20-0400 lvl=info msg="join connections" obj=join id=d368faf8cfef l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:52553
236 +t=2026-08-19T22:21:47-0400 lvl=info msg="join connections" obj=join id=20c785244377 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:47378
237 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=8d2900b01396 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46243
238 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=6c434aeb57f4 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46243
239 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=8a5f3d955e63 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46243
240 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=ef93e21c210e l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46243
241 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=9df67c3643d9 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46243
242 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=9cc13103861e l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46276
243 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=5aed8f8cf0f5 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46276
244 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=f1b2b0bc10a6 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46276
245 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=4895672234ff l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46276
246 +t=2026-08-19T22:21:51-0400 lvl=info msg="join connections" obj=join id=ab8d1e9a8013 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:46276
247 +t=2026-08-19T22:21:57-0400 lvl=info msg="join connections" obj=join id=4a6d53caa3d4 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:51356
248 +t=2026-08-19T22:21:58-0400 lvl=info msg="join connections" obj=join id=6a7cb4e462dd l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:39912
249 +t=2026-08-19T22:21:58-0400 lvl=info msg="join connections" obj=join id=a34eb4eabeab l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:34354
250 +t=2026-08-19T22:21:58-0400 lvl=info msg="join connections" obj=join id=bbd51c5969d7 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:feea:e65d]:51362
251 +t=2026-08-19T22:22:12-0400 lvl=info msg="join connections" obj=join id=6bc36f78ee80 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:60456
252 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=f5b96abcd83f l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:36231
253 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=9de387db4a29 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:33566
254 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=112e3b0df8ed l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:36231
255 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=374dc8f030cd l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:36231
256 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=7efb9e81ae6e l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:36231
257 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=28eaf499ffbe l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:36231
258 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=1a83132fafe1 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:33566
259 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=1b0d57125f3e l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:33566
260 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=ffb381b9e1ed l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:33566
261 +t=2026-08-19T22:22:14-0400 lvl=info msg="join connections" obj=join id=5dc0b6685362 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:33566
262 +t=2026-08-19T22:22:19-0400 lvl=info msg="join connections" obj=join id=49f169e29d78 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:33578
263 +t=2026-08-19T22:22:30-0400 lvl=info msg="join connections" obj=join id=6c2ef35c118e l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:36720
264 +t=2026-08-19T22:22:30-0400 lvl=info msg="join connections" obj=join id=f68d59a5433b l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:47560
265 +t=2026-08-19T22:22:30-0400 lvl=info msg="join connections" obj=join id=5125086bba58 l=[::1]:3300 r=[2001:bc8:701:51:da5e:d3ff:fe49:a574]:45360
266 +t=2026-08-19T22:27:24-0400 lvl=info msg="join connections" obj=join id=d51f258e903b l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53033
267 +t=2026-08-19T22:31:12-0400 lvl=info msg="join connections" obj=join id=726cff0fc2b4 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53144
268 +t=2026-08-19T22:32:18-0400 lvl=info msg="join connections" obj=join id=6d06cf756852 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53237
269 +t=2026-08-19T22:32:19-0400 lvl=info msg="join connections" obj=join id=433871de66bd l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53237
270 +t=2026-08-19T22:32:19-0400 lvl=info msg="join connections" obj=join id=c462a4209aff l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53238
271 +t=2026-08-19T22:32:19-0400 lvl=info msg="join connections" obj=join id=0b0d34cc534c l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53240
272 +t=2026-08-19T22:32:21-0400 lvl=info msg="join connections" obj=join id=945e649fbe3b l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53245
273 +t=2026-08-19T22:33:39-0400 lvl=info msg="join connections" obj=join id=397819b2dda0 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53250
274 +t=2026-08-19T22:33:41-0400 lvl=info msg="join connections" obj=join id=b3664f4650a8 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53253
275 +t=2026-08-19T22:35:57-0400 lvl=info msg="join connections" obj=join id=9f2228e1e020 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53261
276 +t=2026-08-19T22:36:02-0400 lvl=info msg="join connections" obj=join id=15d98ddf375c l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53263
277 +t=2026-08-19T22:36:11-0400 lvl=info msg="join connections" obj=join id=3fef013ea81d l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53263
278 +t=2026-08-19T22:36:29-0400 lvl=info msg="join connections" obj=join id=ce61870e5f6c l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53263
279 +t=2026-08-19T22:54:50-0400 lvl=info msg="join connections" obj=join id=65373a9dccbd l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53337
280 +t=2026-08-19T22:54:50-0400 lvl=info msg="join connections" obj=join id=aa97c7ee2835 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53337
281 +t=2026-08-19T22:54:50-0400 lvl=info msg="join connections" obj=join id=7d3e834533a0 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53339
282 +t=2026-08-19T23:00:10-0400 lvl=info msg="join connections" obj=join id=d26143d16bef l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53379
283 +t=2026-08-19T23:00:10-0400 lvl=info msg="join connections" obj=join id=455af43b0e28 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53380
284 +t=2026-08-19T23:00:10-0400 lvl=info msg="join connections" obj=join id=0fef13e0d4cb l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53380
285 +t=2026-08-19T23:00:10-0400 lvl=info msg="join connections" obj=join id=df9ecd519d2c l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53383
286 +t=2026-08-19T23:05:54-0400 lvl=info msg="join connections" obj=join id=c38949d258c7 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53416
287 +t=2026-08-19T23:06:12-0400 lvl=info msg="join connections" obj=join id=74b84426409f l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53418
288 +t=2026-08-19T23:07:38-0400 lvl=info msg="join connections" obj=join id=dda8a89c4b42 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53427
289 +t=2026-08-19T23:07:39-0400 lvl=info msg="join connections" obj=join id=b52434469f58 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53430
290 +t=2026-08-19T23:25:58-0400 lvl=info msg="join connections" obj=join id=612f510852e3 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53543
291 +t=2026-08-19T23:25:59-0400 lvl=info msg="join connections" obj=join id=60e1852d7e19 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53546
292 +t=2026-08-19T23:48:37-0400 lvl=info msg="join connections" obj=join id=9e14c1d18f72 l=[::1]:3300 r=46.232.235.133:7264
293 +t=2026-08-19T23:48:37-0400 lvl=info msg="join connections" obj=join id=f52a812c1b4f l=[::1]:3300 r=46.232.235.133:7280
294 +t=2026-08-19T23:48:37-0400 lvl=info msg="join connections" obj=join id=fd0fa28f0e3a l=[::1]:3300 r=46.232.235.133:7294
295 +t=2026-08-19T23:48:38-0400 lvl=info msg="join connections" obj=join id=db53824ec136 l=[::1]:3300 r=46.232.235.133:20336
296 +t=2026-08-19T23:48:38-0400 lvl=info msg="join connections" obj=join id=9ad84d1bc1e1 l=[::1]:3300 r=46.232.235.133:58660
297 +t=2026-08-19T23:48:38-0400 lvl=info msg="join connections" obj=join id=af3ac03b5984 l=[::1]:3300 r=46.232.235.133:58666
298 +t=2026-08-20T00:15:41-0400 lvl=info msg="join connections" obj=join id=1b8f0d107d32 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53885
299 +t=2026-08-20T00:15:41-0400 lvl=info msg="join connections" obj=join id=c1dce6c247ee l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53886
300 +t=2026-08-20T00:15:41-0400 lvl=info msg="join connections" obj=join id=c0368d030769 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53886
301 +t=2026-08-20T00:15:41-0400 lvl=info msg="join connections" obj=join id=a9e6335a3636 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53891
302 +t=2026-08-20T00:19:13-0400 lvl=info msg="join connections" obj=join id=a781d17689fe l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53909
303 +t=2026-08-20T00:19:57-0400 lvl=info msg="join connections" obj=join id=83f279ccd37f l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53922
304 +t=2026-08-20T00:20:23-0400 lvl=info msg="join connections" obj=join id=3b9c89253542 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53924
305 +t=2026-08-20T00:22:21-0400 lvl=info msg="join connections" obj=join id=d556db1553ac l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53957
306 +t=2026-08-20T00:22:21-0400 lvl=info msg="join connections" obj=join id=3bbcb0a5454a l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53957
307 +t=2026-08-20T00:22:21-0400 lvl=info msg="join connections" obj=join id=ad28a02f3aa3 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53959
308 +t=2026-08-20T00:23:16-0400 lvl=info msg="join connections" obj=join id=b505cc4fc218 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53964
309 +t=2026-08-20T00:24:34-0400 lvl=info msg="join connections" obj=join id=e04c1591949d l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53968
310 +t=2026-08-20T00:24:39-0400 lvl=info msg="join connections" obj=join id=3eea74dd9688 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53970
311 +t=2026-08-20T00:24:47-0400 lvl=info msg="join connections" obj=join id=dfd40879566c l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53970
312 +t=2026-08-20T00:24:57-0400 lvl=info msg="join connections" obj=join id=8dbd2456f840 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53970
313 +t=2026-08-20T00:24:57-0400 lvl=info msg="join connections" obj=join id=108b8cc3e104 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53970
314 +t=2026-08-20T00:24:57-0400 lvl=info msg="join connections" obj=join id=aad32ee538ae l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53970
315 +t=2026-08-20T00:24:57-0400 lvl=info msg="join connections" obj=join id=3a1991e355bf l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53970
316 +t=2026-08-20T00:24:57-0400 lvl=info msg="join connections" obj=join id=df32ca24efb0 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:53970
317 +t=2026-08-20T00:24:57-0400 lvl=info msg="join connections" obj=join id=0e521e95b109 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54059
318 +t=2026-08-20T00:25:03-0400 lvl=info msg="join connections" obj=join id=f2a00f734278 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54060
319 +t=2026-08-20T00:25:03-0400 lvl=info msg="join connections" obj=join id=28aa5e958664 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54061
320 +t=2026-08-20T00:25:03-0400 lvl=info msg="join connections" obj=join id=3fd131907557 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54061
321 +t=2026-08-20T00:25:03-0400 lvl=info msg="join connections" obj=join id=3412aa770323 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54061
322 +t=2026-08-20T00:38:11-0400 lvl=info msg="join connections" obj=join id=83adadeccbc3 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54342
323 +t=2026-08-20T00:38:39-0400 lvl=info msg="join connections" obj=join id=f7755b441375 l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54373
324 +t=2026-08-20T00:38:39-0400 lvl=info msg="join connections" obj=join id=274ae66881de l=[::1]:3300 r=[2605:b100:121:7aaf:a44c:a261:eb80:fba]:54376
325 +t=2026-08-20T01:02:03-0400 lvl=info msg="join connections" obj=join id=504deb98fb4c l=[::1]:3300 r=70.25.239.68:61599
added data/sessions.json +1 −0
@@ -0,0 +1 @@
1 +{"d08eba390f7e302448b3ad0904841b180b7a97c4be547185d6b383f341f5b651":{"created":1787184586590,"last":1787184586861},"bf96e101ba362b5964767484aaf148081bf85dbaefafb6396a346223f3f4bbfc":{"created":1787184858159,"last":1787191223674},"1729725f9065ea67c047e062f2dce440e44660763199c5cd81f614c1bb669069":{"created":1787185142512,"last":1787185142512},"58389ecc05a5ef3d63f28c27d46026a1184e275de3ddc9ae375dfc7f6564e191":{"created":1787185142681,"last":1787191476301},"3dffbd143a2cc043fde94e4a5b9c93a395f09d814797c04cabd1be8832d2e062":{"created":1787185194996,"last":1787199741938}}
\ No newline at end of file
added data/transcripts/21d6c1690bd84a05.jsonl +8 −0
@@ -0,0 +1,8 @@
1 +{"type":"user_prompt","text":"Allo","ts":1787199418219}
2 +{"type":"status","state":"running"}
3 +{"type":"claude","event":{"type":"system","subtype":"init","cwd":"/Users/simon-pierreboucher/apps/crea-ka","session_id":"1a808aed-e9c3-4913-bef8-698d375e8789","tools":["Task","AskUserQuestion","Bash","CronCreate","CronDelete","CronList","Edit","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Glob","Grep","Monitor","NotebookEdit","PushNotification","Read","RemoteTrigger","ScheduleWakeup","Skill","TaskOutput","TaskStop","TodoWrite","ToolSearch","WebFetch","WebSearch","Write"],"mcp_servers":[{"name":"adminka","status":"connected"}],"model":"claude-fable-5","permissionMode":"default","slash_commands":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api","clear","compact","context","cost","heapdump","init","review","security-review","insights","team-onboarding"],"apiKeySource":"ANTHROPIC_API_KEY","claude_code_version":"2.1.117","output_style":"default","agents":["Explore","general-purpose","Plan","statusline-setup"],"skills":["update-config","debug","simplify","batch","fewer-permission-prompts","loop","schedule","claude-api"],"plugins":[],"uuid":"b6371561-ada7-4736-b07c-7b339cef27d3","memory_paths":{"auto":"/Users/simon-pierreboucher/.claude/projects/-Users-simon-pierreboucher-apps-crea-ka/memory/"},"fast_mode_state":"off"}}
4 +{"type":"claude","event":{"type":"system","subtype":"status","status":"requesting","uuid":"5033ddbf-fdac-42fd-a18c-31ad87c78c4c","session_id":"1a808aed-e9c3-4913-bef8-698d375e8789"}}
5 +{"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeDMWHgBmF1HqDGXubBtY","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"","signature":"CAIS3gIKpAEIEBgCKkC316Sy+OTuBVYoIgZuUsMrkEVoa7m9pMW1lNwVODFFdod5/ftcpvPEqsCL8FVYp9gBbIpKVoQv+R1IPV/XOzx5Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okNmZjYzhmODMtMWY5Yy00ZDMzLWJhMDctNGQ0ZGYzN2Q2N2Y1chD0eFf7D17nbHkCPBxNVtIaiAEBqAHA/5nUBhIMqQ1Wc1+Fi07SJqJPGgweovy1bvKmD82eK4kiMCls4n81u7qJv7VenCk7jjVpqwOuXKGKPgGJIkHOZBKD5TowDl6I/OOrgUcz9YmepCpngnb/GcFbt+wvRpxTEzn+1XQhTPEY0A/6FUJ3JZT3+G+byJpqUktUocjsPEsHzUKLIxEaGLUzUNPOdzloQDeVK41Gsny4hXrdGlCKuTYWss/rmacTS5LqawikGziOBbkasMoGb/auaxgB"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":39715,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":39715,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"1a808aed-e9c3-4913-bef8-698d375e8789","uuid":"16e03e27-d007-4bab-987e-b27f34c9adb9"}}
6 +{"type":"claude","event":{"type":"assistant","message":{"model":"claude-fable-5","id":"msg_011CeDMWHgBmF1HqDGXubBtY","type":"message","role":"assistant","content":[{"type":"text","text":"Allo ! Je suis sur M3U96b, dans le repo **crea-ka** (`~/apps/crea-ka`). Qu'est-ce que je peux faire pour toi ?"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":39715,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":39715,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"global"},"context_management":null},"parent_tool_use_id":null,"session_id":"1a808aed-e9c3-4913-bef8-698d375e8789","uuid":"9e733460-f754-49d5-b4bf-04ab1d28c2fe"}}
7 +{"type":"claude","event":{"type":"result","subtype":"success","is_error":false,"api_error_status":null,"duration_ms":6960,"duration_api_ms":6935,"num_turns":1,"result":"Allo ! Je suis sur M3U96b, dans le repo **crea-ka** (`~/apps/crea-ka`). Qu'est-ce que je peux faire pour toi ?","stop_reason":"end_turn","session_id":"1a808aed-e9c3-4913-bef8-698d375e8789","total_cost_usd":0.15040725000000002,"usage":{"input_tokens":2,"cache_creation_input_tokens":39715,"cache_read_input_tokens":0,"output_tokens":98,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":39715},"inference_geo":"","iterations":[{"input_tokens":2,"output_tokens":98,"cache_read_input_tokens":0,"cache_creation_input_tokens":39715,"cache_creation":{"ephemeral_5m_input_tokens":39715,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-fable-5":{"inputTokens":2,"outputTokens":98,"cacheReadInputTokens":0,"cacheCreationInputTokens":39715,"webSearchRequests":0,"costUSD":0.15040725000000002,"contextWindow":200000,"maxOutputTokens":32000}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"029e0311-11f4-4729-a3a1-6022a5eff3c2"}}
8 +{"type":"status","state":"idle"}
added node_modules/.package-lock.json +29 −0
@@ -0,0 +1,29 @@
1 +{
2 + "name": "admin-ka",
3 + "version": "1.0.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "node_modules/ws": {
8 + "version": "8.21.3",
9 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
10 + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
11 + "license": "MIT",
12 + "engines": {
13 + "node": ">=10.0.0"
14 + },
15 + "peerDependencies": {
16 + "bufferutil": "^4.0.1",
17 + "utf-8-validate": ">=5.0.2"
18 + },
19 + "peerDependenciesMeta": {
20 + "bufferutil": {
21 + "optional": true
22 + },
23 + "utf-8-validate": {
24 + "optional": true
25 + }
26 + }
27 + }
28 + }
29 +}
added node_modules/ws/LICENSE +20 −0
@@ -0,0 +1,20 @@
1 +Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
2 +Copyright (c) 2013 Arnout Kazemier and contributors
3 +Copyright (c) 2016 Luigi Pinca and contributors
4 +
5 +Permission is hereby granted, free of charge, to any person obtaining a copy of
6 +this software and associated documentation files (the "Software"), to deal in
7 +the Software without restriction, including without limitation the rights to
8 +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 +the Software, and to permit persons to whom the Software is furnished to do so,
10 +subject to the following conditions:
11 +
12 +The above copyright notice and this permission notice shall be included in all
13 +copies or substantial portions of the Software.
14 +
15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
added node_modules/ws/README.md +548 −0
@@ -0,0 +1,548 @@
1 +# ws: a Node.js WebSocket library
2 +
3 +[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws)
4 +[![CI](https://img.shields.io/github/actions/workflow/status/websockets/ws/ci.yml?branch=master&label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
5 +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws)
6 +
7 +ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and
8 +server implementation.
9 +
10 +Passes the quite extensive Autobahn test suite: [server][server-report],
11 +[client][client-report].
12 +
13 +**Note**: This module does not work in the browser. The client in the docs is a
14 +reference to a backend with the role of a client in the WebSocket communication.
15 +Browser clients must use the native
16 +[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
17 +object. To make the same code work seamlessly on Node.js and the browser, you
18 +can use one of the many wrappers available on npm, like
19 +[isomorphic-ws](https://github.com/heineiuo/isomorphic-ws).
20 +
21 +## Table of Contents
22 +
23 +- [Protocol support](#protocol-support)
24 +- [Installing](#installing)
25 + - [Opt-in for performance](#opt-in-for-performance)
26 + - [Legacy opt-in for performance](#legacy-opt-in-for-performance)
27 +- [API docs](#api-docs)
28 +- [WebSocket compression](#websocket-compression)
29 +- [Usage examples](#usage-examples)
30 + - [Sending and receiving text data](#sending-and-receiving-text-data)
31 + - [Sending binary data](#sending-binary-data)
32 + - [Simple server](#simple-server)
33 + - [External HTTP/S server](#external-https-server)
34 + - [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server)
35 + - [Client authentication](#client-authentication)
36 + - [Server broadcast](#server-broadcast)
37 + - [Round-trip time](#round-trip-time)
38 + - [Use the Node.js streams API](#use-the-nodejs-streams-api)
39 + - [Other examples](#other-examples)
40 +- [FAQ](#faq)
41 + - [How to get the IP address of the client?](#how-to-get-the-ip-address-of-the-client)
42 + - [How to detect and close broken connections?](#how-to-detect-and-close-broken-connections)
43 + - [How to connect via a proxy?](#how-to-connect-via-a-proxy)
44 +- [Changelog](#changelog)
45 +- [License](#license)
46 +
47 +## Protocol support
48 +
49 +- **HyBi drafts 07-12** (Use the option `protocolVersion: 8`)
50 +- **HyBi drafts 13-17** (Current default, alternatively option
51 + `protocolVersion: 13`)
52 +
53 +## Installing
54 +
55 +```
56 +npm install ws
57 +```
58 +
59 +### Opt-in for performance
60 +
61 +[bufferutil][] is an optional module that can be installed alongside the ws
62 +module:
63 +
64 +```
65 +npm install --save-optional bufferutil
66 +```
67 +
68 +This is a binary addon that improves the performance of certain operations such
69 +as masking and unmasking the data payload of the WebSocket frames. Prebuilt
70 +binaries are available for the most popular platforms, so you don't necessarily
71 +need to have a C++ compiler installed on your machine.
72 +
73 +To force ws to not use bufferutil, use the
74 +[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) environment variable. This
75 +can be useful to enhance security in systems where a user can put a package in
76 +the package search path of an application of another user, due to how the
77 +Node.js resolver algorithm works.
78 +
79 +#### Legacy opt-in for performance
80 +
81 +If you are running on an old version of Node.js (prior to v18.14.0), ws also
82 +supports the [utf-8-validate][] module:
83 +
84 +```
85 +npm install --save-optional utf-8-validate
86 +```
87 +
88 +This contains a binary polyfill for [`buffer.isUtf8()`][].
89 +
90 +To force ws not to use utf-8-validate, use the
91 +[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable.
92 +
93 +## API docs
94 +
95 +See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and
96 +utility functions.
97 +
98 +## WebSocket compression
99 +
100 +ws supports the [permessage-deflate extension][permessage-deflate] which enables
101 +the client and server to negotiate a compression algorithm and its parameters,
102 +and then selectively apply it to the data payloads of each WebSocket message.
103 +
104 +The extension is disabled by default on the server and enabled by default on the
105 +client. It adds a significant overhead in terms of performance and memory
106 +consumption so we suggest to enable it only if it is really needed.
107 +
108 +Note that Node.js has a variety of issues with high-performance compression,
109 +where increased concurrency, especially on Linux, can lead to [catastrophic
110 +memory fragmentation][node-zlib-bug] and slow performance. If you intend to use
111 +permessage-deflate in production, it is worthwhile to set up a test
112 +representative of your workload and ensure Node.js/zlib will handle it with
113 +acceptable performance and memory usage.
114 +
115 +Tuning of permessage-deflate can be done via the options defined below. You can
116 +also use `zlibDeflateOptions` and `zlibInflateOptions`, which is passed directly
117 +into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs].
118 +
119 +See [the docs][ws-server-options] for more options.
120 +
121 +```js
122 +import WebSocket, { WebSocketServer } from 'ws';
123 +
124 +const wss = new WebSocketServer({
125 + port: 8080,
126 + perMessageDeflate: {
127 + zlibDeflateOptions: {
128 + // See zlib defaults.
129 + chunkSize: 1024,
130 + memLevel: 7,
131 + level: 3
132 + },
133 + zlibInflateOptions: {
134 + chunkSize: 10 * 1024
135 + },
136 + // Other options settable:
137 + clientNoContextTakeover: true, // Defaults to negotiated value.
138 + serverNoContextTakeover: true, // Defaults to negotiated value.
139 + serverMaxWindowBits: 10, // Defaults to negotiated value.
140 + // Below options specified as default values.
141 + concurrencyLimit: 10, // Limits zlib concurrency for perf.
142 + threshold: 1024 // Size (in bytes) below which messages
143 + // should not be compressed if context takeover is disabled.
144 + }
145 +});
146 +```
147 +
148 +The client will only use the extension if it is supported and enabled on the
149 +server. To always disable the extension on the client, set the
150 +`perMessageDeflate` option to `false`.
151 +
152 +```js
153 +import WebSocket from 'ws';
154 +
155 +const ws = new WebSocket('ws://www.host.com/path', {
156 + perMessageDeflate: false
157 +});
158 +```
159 +
160 +## Usage examples
161 +
162 +### Sending and receiving text data
163 +
164 +```js
165 +import WebSocket from 'ws';
166 +
167 +const ws = new WebSocket('ws://www.host.com/path');
168 +
169 +ws.on('error', console.error);
170 +
171 +ws.on('open', function open() {
172 + ws.send('something');
173 +});
174 +
175 +ws.on('message', function message(data) {
176 + console.log('received: %s', data);
177 +});
178 +```
179 +
180 +### Sending binary data
181 +
182 +```js
183 +import WebSocket from 'ws';
184 +
185 +const ws = new WebSocket('ws://www.host.com/path');
186 +
187 +ws.on('error', console.error);
188 +
189 +ws.on('open', function open() {
190 + const array = new Float32Array(5);
191 +
192 + for (var i = 0; i < array.length; ++i) {
193 + array[i] = i / 2;
194 + }
195 +
196 + ws.send(array);
197 +});
198 +```
199 +
200 +### Simple server
201 +
202 +```js
203 +import { WebSocketServer } from 'ws';
204 +
205 +const wss = new WebSocketServer({ port: 8080 });
206 +
207 +wss.on('connection', function connection(ws) {
208 + ws.on('error', console.error);
209 +
210 + ws.on('message', function message(data) {
211 + console.log('received: %s', data);
212 + });
213 +
214 + ws.send('something');
215 +});
216 +```
217 +
218 +### External HTTP/S server
219 +
220 +```js
221 +import { createServer } from 'https';
222 +import { readFileSync } from 'fs';
223 +import { WebSocketServer } from 'ws';
224 +
225 +const server = createServer({
226 + cert: readFileSync('/path/to/cert.pem'),
227 + key: readFileSync('/path/to/key.pem')
228 +});
229 +const wss = new WebSocketServer({ server });
230 +
231 +wss.on('connection', function connection(ws) {
232 + ws.on('error', console.error);
233 +
234 + ws.on('message', function message(data) {
235 + console.log('received: %s', data);
236 + });
237 +
238 + ws.send('something');
239 +});
240 +
241 +server.listen(8080);
242 +```
243 +
244 +### Multiple servers sharing a single HTTP/S server
245 +
246 +```js
247 +import { createServer } from 'http';
248 +import { WebSocketServer } from 'ws';
249 +
250 +const server = createServer();
251 +const wss1 = new WebSocketServer({ noServer: true });
252 +const wss2 = new WebSocketServer({ noServer: true });
253 +
254 +wss1.on('connection', function connection(ws) {
255 + ws.on('error', console.error);
256 +
257 + // ...
258 +});
259 +
260 +wss2.on('connection', function connection(ws) {
261 + ws.on('error', console.error);
262 +
263 + // ...
264 +});
265 +
266 +server.on('upgrade', function upgrade(request, socket, head) {
267 + const { pathname } = new URL(request.url, 'wss://base.url');
268 +
269 + if (pathname === '/foo') {
270 + wss1.handleUpgrade(request, socket, head, function done(ws) {
271 + wss1.emit('connection', ws, request);
272 + });
273 + } else if (pathname === '/bar') {
274 + wss2.handleUpgrade(request, socket, head, function done(ws) {
275 + wss2.emit('connection', ws, request);
276 + });
277 + } else {
278 + socket.destroy();
279 + }
280 +});
281 +
282 +server.listen(8080);
283 +```
284 +
285 +### Client authentication
286 +
287 +```js
288 +import { createServer } from 'http';
289 +import { WebSocketServer } from 'ws';
290 +
291 +function onSocketError(err) {
292 + console.error(err);
293 +}
294 +
295 +const server = createServer();
296 +const wss = new WebSocketServer({ noServer: true });
297 +
298 +wss.on('connection', function connection(ws, request, client) {
299 + ws.on('error', console.error);
300 +
301 + ws.on('message', function message(data) {
302 + console.log(`Received message ${data} from user ${client}`);
303 + });
304 +});
305 +
306 +server.on('upgrade', function upgrade(request, socket, head) {
307 + socket.on('error', onSocketError);
308 +
309 + // This function is not defined on purpose. Implement it with your own logic.
310 + authenticate(request, function next(err, client) {
311 + if (err || !client) {
312 + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
313 + socket.destroy();
314 + return;
315 + }
316 +
317 + socket.removeListener('error', onSocketError);
318 +
319 + wss.handleUpgrade(request, socket, head, function done(ws) {
320 + wss.emit('connection', ws, request, client);
321 + });
322 + });
323 +});
324 +
325 +server.listen(8080);
326 +```
327 +
328 +Also see the provided [example][session-parse-example] using `express-session`.
329 +
330 +### Server broadcast
331 +
332 +A client WebSocket broadcasting to all connected WebSocket clients, including
333 +itself.
334 +
335 +```js
336 +import WebSocket, { WebSocketServer } from 'ws';
337 +
338 +const wss = new WebSocketServer({ port: 8080 });
339 +
340 +wss.on('connection', function connection(ws) {
341 + ws.on('error', console.error);
342 +
343 + ws.on('message', function message(data, isBinary) {
344 + wss.clients.forEach(function each(client) {
345 + if (client.readyState === WebSocket.OPEN) {
346 + client.send(data, { binary: isBinary });
347 + }
348 + });
349 + });
350 +});
351 +```
352 +
353 +A client WebSocket broadcasting to every other connected WebSocket clients,
354 +excluding itself.
355 +
356 +```js
357 +import WebSocket, { WebSocketServer } from 'ws';
358 +
359 +const wss = new WebSocketServer({ port: 8080 });
360 +
361 +wss.on('connection', function connection(ws) {
362 + ws.on('error', console.error);
363 +
364 + ws.on('message', function message(data, isBinary) {
365 + wss.clients.forEach(function each(client) {
366 + if (client !== ws && client.readyState === WebSocket.OPEN) {
367 + client.send(data, { binary: isBinary });
368 + }
369 + });
370 + });
371 +});
372 +```
373 +
374 +### Round-trip time
375 +
376 +```js
377 +import WebSocket from 'ws';
378 +
379 +const ws = new WebSocket('wss://websocket-echo.com/');
380 +
381 +ws.on('error', console.error);
382 +
383 +ws.on('open', function open() {
384 + console.log('connected');
385 + ws.send(Date.now());
386 +});
387 +
388 +ws.on('close', function close() {
389 + console.log('disconnected');
390 +});
391 +
392 +ws.on('message', function message(data) {
393 + console.log(`Round-trip time: ${Date.now() - data} ms`);
394 +
395 + setTimeout(function timeout() {
396 + ws.send(Date.now());
397 + }, 500);
398 +});
399 +```
400 +
401 +### Use the Node.js streams API
402 +
403 +```js
404 +import WebSocket, { createWebSocketStream } from 'ws';
405 +
406 +const ws = new WebSocket('wss://websocket-echo.com/');
407 +
408 +const duplex = createWebSocketStream(ws, { encoding: 'utf8' });
409 +
410 +duplex.on('error', console.error);
411 +
412 +duplex.pipe(process.stdout);
413 +process.stdin.pipe(duplex);
414 +```
415 +
416 +### Other examples
417 +
418 +For a full example with a browser client communicating with a ws server, see the
419 +examples folder.
420 +
421 +Otherwise, see the test cases.
422 +
423 +## FAQ
424 +
425 +### How to get the IP address of the client?
426 +
427 +The remote IP address can be obtained from the raw socket.
428 +
429 +```js
430 +import { WebSocketServer } from 'ws';
431 +
432 +const wss = new WebSocketServer({ port: 8080 });
433 +
434 +wss.on('connection', function connection(ws, req) {
435 + const ip = req.socket.remoteAddress;
436 +
437 + ws.on('error', console.error);
438 +});
439 +```
440 +
441 +When the server runs behind a proxy like NGINX, the de-facto standard is to use
442 +the `X-Forwarded-For` header.
443 +
444 +```js
445 +wss.on('connection', function connection(ws, req) {
446 + const ip = req.headers['x-forwarded-for'].split(',')[0].trim();
447 +
448 + ws.on('error', console.error);
449 +});
450 +```
451 +
452 +### How to detect and close broken connections?
453 +
454 +Sometimes, the link between the server and the client can be interrupted in a
455 +way that keeps both the server and the client unaware of the broken state of the
456 +connection (e.g. when pulling the cord).
457 +
458 +In these cases, ping messages can be used as a means to verify that the remote
459 +endpoint is still responsive.
460 +
461 +```js
462 +import { WebSocketServer } from 'ws';
463 +
464 +function heartbeat() {
465 + this.isAlive = true;
466 +}
467 +
468 +const wss = new WebSocketServer({ port: 8080 });
469 +
470 +wss.on('connection', function connection(ws) {
471 + ws.isAlive = true;
472 + ws.on('error', console.error);
473 + ws.on('pong', heartbeat);
474 +});
475 +
476 +const interval = setInterval(function ping() {
477 + wss.clients.forEach(function each(ws) {
478 + if (ws.isAlive === false) return ws.terminate();
479 +
480 + ws.isAlive = false;
481 + ws.ping();
482 + });
483 +}, 30000);
484 +
485 +wss.on('close', function close() {
486 + clearInterval(interval);
487 +});
488 +```
489 +
490 +Pong messages are automatically sent in response to ping messages as required by
491 +the spec.
492 +
493 +Just like the server example above, your clients might as well lose connection
494 +without knowing it. You might want to add a ping listener on your clients to
495 +prevent that. A simple implementation would be:
496 +
497 +```js
498 +import WebSocket from 'ws';
499 +
500 +function heartbeat() {
501 + clearTimeout(this.pingTimeout);
502 +
503 + // Use `WebSocket#terminate()`, which immediately destroys the connection,
504 + // instead of `WebSocket#close()`, which waits for the close timer.
505 + // Delay should be equal to the interval at which your server
506 + // sends out pings plus a conservative assumption of the latency.
507 + this.pingTimeout = setTimeout(() => {
508 + this.terminate();
509 + }, 30000 + 1000);
510 +}
511 +
512 +const client = new WebSocket('wss://websocket-echo.com/');
513 +
514 +client.on('error', console.error);
515 +client.on('open', heartbeat);
516 +client.on('ping', heartbeat);
517 +client.on('close', function clear() {
518 + clearTimeout(this.pingTimeout);
519 +});
520 +```
521 +
522 +### How to connect via a proxy?
523 +
524 +Use a custom `http.Agent` implementation like [https-proxy-agent][] or
525 +[socks-proxy-agent][].
526 +
527 +## Changelog
528 +
529 +We're using the GitHub [releases][changelog] for changelog entries.
530 +
531 +## License
532 +
533 +[MIT](LICENSE)
534 +
535 +[`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input
536 +[bufferutil]: https://github.com/websockets/bufferutil
537 +[changelog]: https://github.com/websockets/ws/releases
538 +[client-report]: http://websockets.github.io/ws/autobahn/clients/
539 +[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
540 +[node-zlib-bug]: https://github.com/nodejs/node/issues/8871
541 +[node-zlib-deflaterawdocs]:
542 + https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options
543 +[permessage-deflate]: https://tools.ietf.org/html/rfc7692
544 +[server-report]: http://websockets.github.io/ws/autobahn/servers/
545 +[session-parse-example]: ./examples/express-session-parse
546 +[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
547 +[utf-8-validate]: https://github.com/websockets/utf-8-validate
548 +[ws-server-options]: ./doc/ws.md#new-websocketserveroptions-callback
added node_modules/ws/browser.js +8 −0
@@ -0,0 +1,8 @@
1 +'use strict';
2 +
3 +module.exports = function () {
4 + throw new Error(
5 + 'ws does not work in the browser. Browser clients must use the native ' +
6 + 'WebSocket object'
7 + );
8 +};
added node_modules/ws/index.js +22 −0
@@ -0,0 +1,22 @@
1 +'use strict';
2 +
3 +const createWebSocketStream = require('./lib/stream');
4 +const extension = require('./lib/extension');
5 +const PerMessageDeflate = require('./lib/permessage-deflate');
6 +const Receiver = require('./lib/receiver');
7 +const Sender = require('./lib/sender');
8 +const subprotocol = require('./lib/subprotocol');
9 +const WebSocket = require('./lib/websocket');
10 +const WebSocketServer = require('./lib/websocket-server');
11 +
12 +WebSocket.createWebSocketStream = createWebSocketStream;
13 +WebSocket.extension = extension;
14 +WebSocket.PerMessageDeflate = PerMessageDeflate;
15 +WebSocket.Receiver = Receiver;
16 +WebSocket.Sender = Sender;
17 +WebSocket.Server = WebSocketServer;
18 +WebSocket.subprotocol = subprotocol;
19 +WebSocket.WebSocket = WebSocket;
20 +WebSocket.WebSocketServer = WebSocketServer;
21 +
22 +module.exports = WebSocket;
added node_modules/ws/lib/buffer-util.js +131 −0
@@ -0,0 +1,131 @@
1 +'use strict';
2 +
3 +const { EMPTY_BUFFER } = require('./constants');
4 +
5 +const FastBuffer = Buffer[Symbol.species];
6 +
7 +/**
8 + * Merges an array of buffers into a new buffer.
9 + *
10 + * @param {Buffer[]} list The array of buffers to concat
11 + * @param {Number} totalLength The total length of buffers in the list
12 + * @return {Buffer} The resulting buffer
13 + * @public
14 + */
15 +function concat(list, totalLength) {
16 + if (list.length === 0) return EMPTY_BUFFER;
17 + if (list.length === 1) return list[0];
18 +
19 + const target = Buffer.allocUnsafe(totalLength);
20 + let offset = 0;
21 +
22 + for (let i = 0; i < list.length; i++) {
23 + const buf = list[i];
24 + target.set(buf, offset);
25 + offset += buf.length;
26 + }
27 +
28 + if (offset < totalLength) {
29 + return new FastBuffer(target.buffer, target.byteOffset, offset);
30 + }
31 +
32 + return target;
33 +}
34 +
35 +/**
36 + * Masks a buffer using the given mask.
37 + *
38 + * @param {Buffer} source The buffer to mask
39 + * @param {Buffer} mask The mask to use
40 + * @param {Buffer} output The buffer where to store the result
41 + * @param {Number} offset The offset at which to start writing
42 + * @param {Number} length The number of bytes to mask.
43 + * @public
44 + */
45 +function _mask(source, mask, output, offset, length) {
46 + for (let i = 0; i < length; i++) {
47 + output[offset + i] = source[i] ^ mask[i & 3];
48 + }
49 +}
50 +
51 +/**
52 + * Unmasks a buffer using the given mask.
53 + *
54 + * @param {Buffer} buffer The buffer to unmask
55 + * @param {Buffer} mask The mask to use
56 + * @public
57 + */
58 +function _unmask(buffer, mask) {
59 + for (let i = 0; i < buffer.length; i++) {
60 + buffer[i] ^= mask[i & 3];
61 + }
62 +}
63 +
64 +/**
65 + * Converts a buffer to an `ArrayBuffer`.
66 + *
67 + * @param {Buffer} buf The buffer to convert
68 + * @return {ArrayBuffer} Converted buffer
69 + * @public
70 + */
71 +function toArrayBuffer(buf) {
72 + if (buf.length === buf.buffer.byteLength) {
73 + return buf.buffer;
74 + }
75 +
76 + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length);
77 +}
78 +
79 +/**
80 + * Converts `data` to a `Buffer`.
81 + *
82 + * @param {*} data The data to convert
83 + * @return {Buffer} The buffer
84 + * @throws {TypeError}
85 + * @public
86 + */
87 +function toBuffer(data) {
88 + toBuffer.readOnly = true;
89 +
90 + if (Buffer.isBuffer(data)) return data;
91 +
92 + let buf;
93 +
94 + if (data instanceof ArrayBuffer) {
95 + buf = new FastBuffer(data);
96 + } else if (ArrayBuffer.isView(data)) {
97 + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength);
98 + } else {
99 + buf = Buffer.from(data);
100 + toBuffer.readOnly = false;
101 + }
102 +
103 + return buf;
104 +}
105 +
106 +module.exports = {
107 + concat,
108 + mask: _mask,
109 + toArrayBuffer,
110 + toBuffer,
111 + unmask: _unmask
112 +};
113 +
114 +/* istanbul ignore else */
115 +if (!process.env.WS_NO_BUFFER_UTIL) {
116 + try {
117 + const bufferUtil = require('bufferutil');
118 +
119 + module.exports.mask = function (source, mask, output, offset, length) {
120 + if (length < 48) _mask(source, mask, output, offset, length);
121 + else bufferUtil.mask(source, mask, output, offset, length);
122 + };
123 +
124 + module.exports.unmask = function (buffer, mask) {
125 + if (buffer.length < 32) _unmask(buffer, mask);
126 + else bufferUtil.unmask(buffer, mask);
127 + };
128 + } catch (e) {
129 + // Continue regardless of the error.
130 + }
131 +}
added node_modules/ws/lib/constants.js +19 −0
@@ -0,0 +1,19 @@
1 +'use strict';
2 +
3 +const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments'];
4 +const hasBlob = typeof Blob !== 'undefined';
5 +
6 +if (hasBlob) BINARY_TYPES.push('blob');
7 +
8 +module.exports = {
9 + BINARY_TYPES,
10 + CLOSE_TIMEOUT: 30000,
11 + EMPTY_BUFFER: Buffer.alloc(0),
12 + GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
13 + hasBlob,
14 + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'),
15 + kListener: Symbol('kListener'),
16 + kStatusCode: Symbol('status-code'),
17 + kWebSocket: Symbol('websocket'),
18 + NOOP: () => {}
19 +};
added node_modules/ws/lib/event-target.js +292 −0
@@ -0,0 +1,292 @@
1 +'use strict';
2 +
3 +const { kForOnEventAttribute, kListener } = require('./constants');
4 +
5 +const kCode = Symbol('kCode');
6 +const kData = Symbol('kData');
7 +const kError = Symbol('kError');
8 +const kMessage = Symbol('kMessage');
9 +const kReason = Symbol('kReason');
10 +const kTarget = Symbol('kTarget');
11 +const kType = Symbol('kType');
12 +const kWasClean = Symbol('kWasClean');
13 +
14 +/**
15 + * Class representing an event.
16 + */
17 +class Event {
18 + /**
19 + * Create a new `Event`.
20 + *
21 + * @param {String} type The name of the event
22 + * @throws {TypeError} If the `type` argument is not specified
23 + */
24 + constructor(type) {
25 + this[kTarget] = null;
26 + this[kType] = type;
27 + }
28 +
29 + /**
30 + * @type {*}
31 + */
32 + get target() {
33 + return this[kTarget];
34 + }
35 +
36 + /**
37 + * @type {String}
38 + */
39 + get type() {
40 + return this[kType];
41 + }
42 +}
43 +
44 +Object.defineProperty(Event.prototype, 'target', { enumerable: true });
45 +Object.defineProperty(Event.prototype, 'type', { enumerable: true });
46 +
47 +/**
48 + * Class representing a close event.
49 + *
50 + * @extends Event
51 + */
52 +class CloseEvent extends Event {
53 + /**
54 + * Create a new `CloseEvent`.
55 + *
56 + * @param {String} type The name of the event
57 + * @param {Object} [options] A dictionary object that allows for setting
58 + * attributes via object members of the same name
59 + * @param {Number} [options.code=0] The status code explaining why the
60 + * connection was closed
61 + * @param {String} [options.reason=''] A human-readable string explaining why
62 + * the connection was closed
63 + * @param {Boolean} [options.wasClean=false] Indicates whether or not the
64 + * connection was cleanly closed
65 + */
66 + constructor(type, options = {}) {
67 + super(type);
68 +
69 + this[kCode] = options.code === undefined ? 0 : options.code;
70 + this[kReason] = options.reason === undefined ? '' : options.reason;
71 + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean;
72 + }
73 +
74 + /**
75 + * @type {Number}
76 + */
77 + get code() {
78 + return this[kCode];
79 + }
80 +
81 + /**
82 + * @type {String}
83 + */
84 + get reason() {
85 + return this[kReason];
86 + }
87 +
88 + /**
89 + * @type {Boolean}
90 + */
91 + get wasClean() {
92 + return this[kWasClean];
93 + }
94 +}
95 +
96 +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true });
97 +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true });
98 +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true });
99 +
100 +/**
101 + * Class representing an error event.
102 + *
103 + * @extends Event
104 + */
105 +class ErrorEvent extends Event {
106 + /**
107 + * Create a new `ErrorEvent`.
108 + *
109 + * @param {String} type The name of the event
110 + * @param {Object} [options] A dictionary object that allows for setting
111 + * attributes via object members of the same name
112 + * @param {*} [options.error=null] The error that generated this event
113 + * @param {String} [options.message=''] The error message
114 + */
115 + constructor(type, options = {}) {
116 + super(type);
117 +
118 + this[kError] = options.error === undefined ? null : options.error;
119 + this[kMessage] = options.message === undefined ? '' : options.message;
120 + }
121 +
122 + /**
123 + * @type {*}
124 + */
125 + get error() {
126 + return this[kError];
127 + }
128 +
129 + /**
130 + * @type {String}
131 + */
132 + get message() {
133 + return this[kMessage];
134 + }
135 +}
136 +
137 +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true });
138 +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true });
139 +
140 +/**
141 + * Class representing a message event.
142 + *
143 + * @extends Event
144 + */
145 +class MessageEvent extends Event {
146 + /**
147 + * Create a new `MessageEvent`.
148 + *
149 + * @param {String} type The name of the event
150 + * @param {Object} [options] A dictionary object that allows for setting
151 + * attributes via object members of the same name
152 + * @param {*} [options.data=null] The message content
153 + */
154 + constructor(type, options = {}) {
155 + super(type);
156 +
157 + this[kData] = options.data === undefined ? null : options.data;
158 + }
159 +
160 + /**
161 + * @type {*}
162 + */
163 + get data() {
164 + return this[kData];
165 + }
166 +}
167 +
168 +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true });
169 +
170 +/**
171 + * This provides methods for emulating the `EventTarget` interface. It's not
172 + * meant to be used directly.
173 + *
174 + * @mixin
175 + */
176 +const EventTarget = {
177 + /**
178 + * Register an event listener.
179 + *
180 + * @param {String} type A string representing the event type to listen for
181 + * @param {(Function|Object)} handler The listener to add
182 + * @param {Object} [options] An options object specifies characteristics about
183 + * the event listener
184 + * @param {Boolean} [options.once=false] A `Boolean` indicating that the
185 + * listener should be invoked at most once after being added. If `true`,
186 + * the listener would be automatically removed when invoked.
187 + * @public
188 + */
189 + addEventListener(type, handler, options = {}) {
190 + for (const listener of this.listeners(type)) {
191 + if (
192 + !options[kForOnEventAttribute] &&
193 + listener[kListener] === handler &&
194 + !listener[kForOnEventAttribute]
195 + ) {
196 + return;
197 + }
198 + }
199 +
200 + let wrapper;
201 +
202 + if (type === 'message') {
203 + wrapper = function onMessage(data, isBinary) {
204 + const event = new MessageEvent('message', {
205 + data: isBinary ? data : data.toString()
206 + });
207 +
208 + event[kTarget] = this;
209 + callListener(handler, this, event);
210 + };
211 + } else if (type === 'close') {
212 + wrapper = function onClose(code, message) {
213 + const event = new CloseEvent('close', {
214 + code,
215 + reason: message.toString(),
216 + wasClean: this._closeFrameReceived && this._closeFrameSent
217 + });
218 +
219 + event[kTarget] = this;
220 + callListener(handler, this, event);
221 + };
222 + } else if (type === 'error') {
223 + wrapper = function onError(error) {
224 + const event = new ErrorEvent('error', {
225 + error,
226 + message: error.message
227 + });
228 +
229 + event[kTarget] = this;
230 + callListener(handler, this, event);
231 + };
232 + } else if (type === 'open') {
233 + wrapper = function onOpen() {
234 + const event = new Event('open');
235 +
236 + event[kTarget] = this;
237 + callListener(handler, this, event);
238 + };
239 + } else {
240 + return;
241 + }
242 +
243 + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute];
244 + wrapper[kListener] = handler;
245 +
246 + if (options.once) {
247 + this.once(type, wrapper);
248 + } else {
249 + this.on(type, wrapper);
250 + }
251 + },
252 +
253 + /**
254 + * Remove an event listener.
255 + *
256 + * @param {String} type A string representing the event type to remove
257 + * @param {(Function|Object)} handler The listener to remove
258 + * @public
259 + */
260 + removeEventListener(type, handler) {
261 + for (const listener of this.listeners(type)) {
262 + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) {
263 + this.removeListener(type, listener);
264 + break;
265 + }
266 + }
267 + }
268 +};
269 +
270 +module.exports = {
271 + CloseEvent,
272 + ErrorEvent,
273 + Event,
274 + EventTarget,
275 + MessageEvent
276 +};
277 +
278 +/**
279 + * Call an event listener
280 + *
281 + * @param {(Function|Object)} listener The listener to call
282 + * @param {*} thisArg The value to use as `this`` when calling the listener
283 + * @param {Event} event The event to pass to the listener
284 + * @private
285 + */
286 +function callListener(listener, thisArg, event) {
287 + if (typeof listener === 'object' && listener.handleEvent) {
288 + listener.handleEvent.call(listener, event);
289 + } else {
290 + listener.call(thisArg, event);
291 + }
292 +}
added node_modules/ws/lib/extension.js +203 −0
@@ -0,0 +1,203 @@
1 +'use strict';
2 +
3 +const { tokenChars } = require('./validation');
4 +
5 +/**
6 + * Adds an offer to the map of extension offers or a parameter to the map of
7 + * parameters.
8 + *
9 + * @param {Object} dest The map of extension offers or parameters
10 + * @param {String} name The extension or parameter name
11 + * @param {(Object|Boolean|String)} elem The extension parameters or the
12 + * parameter value
13 + * @private
14 + */
15 +function push(dest, name, elem) {
16 + if (dest[name] === undefined) dest[name] = [elem];
17 + else dest[name].push(elem);
18 +}
19 +
20 +/**
21 + * Parses the `Sec-WebSocket-Extensions` header into an object.
22 + *
23 + * @param {String} header The field value of the header
24 + * @return {Object} The parsed object
25 + * @public
26 + */
27 +function parse(header) {
28 + const offers = Object.create(null);
29 + let params = Object.create(null);
30 + let mustUnescape = false;
31 + let isEscaping = false;
32 + let inQuotes = false;
33 + let extensionName;
34 + let paramName;
35 + let start = -1;
36 + let code = -1;
37 + let end = -1;
38 + let i = 0;
39 +
40 + for (; i < header.length; i++) {
41 + code = header.charCodeAt(i);
42 +
43 + if (extensionName === undefined) {
44 + if (end === -1 && tokenChars[code] === 1) {
45 + if (start === -1) start = i;
46 + } else if (
47 + i !== 0 &&
48 + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
49 + ) {
50 + if (end === -1 && start !== -1) end = i;
51 + } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
52 + if (start === -1) {
53 + throw new SyntaxError(`Unexpected character at index ${i}`);
54 + }
55 +
56 + if (end === -1) end = i;
57 + const name = header.slice(start, end);
58 + if (code === 0x2c) {
59 + push(offers, name, params);
60 + params = Object.create(null);
61 + } else {
62 + extensionName = name;
63 + }
64 +
65 + start = end = -1;
66 + } else {
67 + throw new SyntaxError(`Unexpected character at index ${i}`);
68 + }
69 + } else if (paramName === undefined) {
70 + if (end === -1 && tokenChars[code] === 1) {
71 + if (start === -1) start = i;
72 + } else if (code === 0x20 || code === 0x09) {
73 + if (end === -1 && start !== -1) end = i;
74 + } else if (code === 0x3b || code === 0x2c) {
75 + if (start === -1) {
76 + throw new SyntaxError(`Unexpected character at index ${i}`);
77 + }
78 +
79 + if (end === -1) end = i;
80 + push(params, header.slice(start, end), true);
81 + if (code === 0x2c) {
82 + push(offers, extensionName, params);
83 + params = Object.create(null);
84 + extensionName = undefined;
85 + }
86 +
87 + start = end = -1;
88 + } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) {
89 + paramName = header.slice(start, i);
90 + start = end = -1;
91 + } else {
92 + throw new SyntaxError(`Unexpected character at index ${i}`);
93 + }
94 + } else {
95 + //
96 + // The value of a quoted-string after unescaping must conform to the
97 + // token ABNF, so only token characters are valid.
98 + // Ref: https://tools.ietf.org/html/rfc6455#section-9.1
99 + //
100 + if (isEscaping) {
101 + if (tokenChars[code] !== 1) {
102 + throw new SyntaxError(`Unexpected character at index ${i}`);
103 + }
104 + if (start === -1) start = i;
105 + else if (!mustUnescape) mustUnescape = true;
106 + isEscaping = false;
107 + } else if (inQuotes) {
108 + if (tokenChars[code] === 1) {
109 + if (start === -1) start = i;
110 + } else if (code === 0x22 /* '"' */ && start !== -1) {
111 + inQuotes = false;
112 + end = i;
113 + } else if (code === 0x5c /* '\' */) {
114 + isEscaping = true;
115 + } else {
116 + throw new SyntaxError(`Unexpected character at index ${i}`);
117 + }
118 + } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
119 + inQuotes = true;
120 + } else if (end === -1 && tokenChars[code] === 1) {
121 + if (start === -1) start = i;
122 + } else if (start !== -1 && (code === 0x20 || code === 0x09)) {
123 + if (end === -1) end = i;
124 + } else if (code === 0x3b || code === 0x2c) {
125 + if (start === -1) {
126 + throw new SyntaxError(`Unexpected character at index ${i}`);
127 + }
128 +
129 + if (end === -1) end = i;
130 + let value = header.slice(start, end);
131 + if (mustUnescape) {
132 + value = value.replace(/\\/g, '');
133 + mustUnescape = false;
134 + }
135 + push(params, paramName, value);
136 + if (code === 0x2c) {
137 + push(offers, extensionName, params);
138 + params = Object.create(null);
139 + extensionName = undefined;
140 + }
141 +
142 + paramName = undefined;
143 + start = end = -1;
144 + } else {
145 + throw new SyntaxError(`Unexpected character at index ${i}`);
146 + }
147 + }
148 + }
149 +
150 + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) {
151 + throw new SyntaxError('Unexpected end of input');
152 + }
153 +
154 + if (end === -1) end = i;
155 + const token = header.slice(start, end);
156 + if (extensionName === undefined) {
157 + push(offers, token, params);
158 + } else {
159 + if (paramName === undefined) {
160 + push(params, token, true);
161 + } else if (mustUnescape) {
162 + push(params, paramName, token.replace(/\\/g, ''));
163 + } else {
164 + push(params, paramName, token);
165 + }
166 + push(offers, extensionName, params);
167 + }
168 +
169 + return offers;
170 +}
171 +
172 +/**
173 + * Builds the `Sec-WebSocket-Extensions` header field value.
174 + *
175 + * @param {Object} extensions The map of extensions and parameters to format
176 + * @return {String} A string representing the given object
177 + * @public
178 + */
179 +function format(extensions) {
180 + return Object.keys(extensions)
181 + .map((extension) => {
182 + let configurations = extensions[extension];
183 + if (!Array.isArray(configurations)) configurations = [configurations];
184 + return configurations
185 + .map((params) => {
186 + return [extension]
187 + .concat(
188 + Object.keys(params).map((k) => {
189 + let values = params[k];
190 + if (!Array.isArray(values)) values = [values];
191 + return values
192 + .map((v) => (v === true ? k : `${k}=${v}`))
193 + .join('; ');
194 + })
195 + )
196 + .join('; ');
197 + })
198 + .join(', ');
199 + })
200 + .join(', ');
201 +}
202 +
203 +module.exports = { format, parse };
added node_modules/ws/lib/limiter.js +55 −0
@@ -0,0 +1,55 @@
1 +'use strict';
2 +
3 +const kDone = Symbol('kDone');
4 +const kRun = Symbol('kRun');
5 +
6 +/**
7 + * A very simple job queue with adjustable concurrency. Adapted from
8 + * https://github.com/STRML/async-limiter
9 + */
10 +class Limiter {
11 + /**
12 + * Creates a new `Limiter`.
13 + *
14 + * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
15 + * to run concurrently
16 + */
17 + constructor(concurrency) {
18 + this[kDone] = () => {
19 + this.pending--;
20 + this[kRun]();
21 + };
22 + this.concurrency = concurrency || Infinity;
23 + this.jobs = [];
24 + this.pending = 0;
25 + }
26 +
27 + /**
28 + * Adds a job to the queue.
29 + *
30 + * @param {Function} job The job to run
31 + * @public
32 + */
33 + add(job) {
34 + this.jobs.push(job);
35 + this[kRun]();
36 + }
37 +
38 + /**
39 + * Removes a job from the queue and runs it if possible.
40 + *
41 + * @private
42 + */
43 + [kRun]() {
44 + if (this.pending === this.concurrency) return;
45 +
46 + if (this.jobs.length) {
47 + const job = this.jobs.shift();
48 +
49 + this.pending++;
50 + job(this[kDone]);
51 + }
52 + }
53 +}
54 +
55 +module.exports = Limiter;
added node_modules/ws/lib/permessage-deflate.js +530 −0
@@ -0,0 +1,530 @@
1 +'use strict';
2 +
3 +const zlib = require('zlib');
4 +
5 +const bufferUtil = require('./buffer-util');
6 +const Limiter = require('./limiter');
7 +const { kStatusCode } = require('./constants');
8 +
9 +const FastBuffer = Buffer[Symbol.species];
10 +const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
11 +const kPerMessageDeflate = Symbol('permessage-deflate');
12 +const kTotalLength = Symbol('total-length');
13 +const kCallback = Symbol('callback');
14 +const kBuffers = Symbol('buffers');
15 +const kError = Symbol('error');
16 +
17 +//
18 +// We limit zlib concurrency, which prevents severe memory fragmentation
19 +// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913
20 +// and https://github.com/websockets/ws/issues/1202
21 +//
22 +// Intentionally global; it's the global thread pool that's an issue.
23 +//
24 +let zlibLimiter;
25 +
26 +/**
27 + * permessage-deflate implementation.
28 + */
29 +class PerMessageDeflate {
30 + /**
31 + * Creates a PerMessageDeflate instance.
32 + *
33 + * @param {Object} [options] Configuration options
34 + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support
35 + * for, or request, a custom client window size
36 + * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/
37 + * acknowledge disabling of client context takeover
38 + * @param {Number} [options.concurrencyLimit=10] The number of concurrent
39 + * calls to zlib
40 + * @param {Boolean} [options.isServer=false] Create the instance in either
41 + * server or client mode
42 + * @param {Number} [options.maxPayload=0] The maximum allowed message length
43 + * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
44 + * use of a custom server window size
45 + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
46 + * disabling of server context takeover
47 + * @param {Number} [options.threshold=1024] Size (in bytes) below which
48 + * messages should not be compressed if context takeover is disabled
49 + * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on
50 + * deflate
51 + * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
52 + * inflate
53 + */
54 + constructor(options) {
55 + this._options = options || {};
56 + this._threshold =
57 + this._options.threshold !== undefined ? this._options.threshold : 1024;
58 + this._maxPayload = this._options.maxPayload | 0;
59 + this._isServer = !!this._options.isServer;
60 + this._deflate = null;
61 + this._inflate = null;
62 +
63 + this.params = null;
64 +
65 + if (!zlibLimiter) {
66 + const concurrency =
67 + this._options.concurrencyLimit !== undefined
68 + ? this._options.concurrencyLimit
69 + : 10;
70 + zlibLimiter = new Limiter(concurrency);
71 + }
72 + }
73 +
74 + /**
75 + * @type {String}
76 + */
77 + static get extensionName() {
78 + return 'permessage-deflate';
79 + }
80 +
81 + /**
82 + * Create an extension negotiation offer.
83 + *
84 + * @return {Object} Extension parameters
85 + * @public
86 + */
87 + offer() {
88 + const params = {};
89 +
90 + if (this._options.serverNoContextTakeover) {
91 + params.server_no_context_takeover = true;
92 + }
93 + if (this._options.clientNoContextTakeover) {
94 + params.client_no_context_takeover = true;
95 + }
96 + if (this._options.serverMaxWindowBits) {
97 + params.server_max_window_bits = this._options.serverMaxWindowBits;
98 + }
99 + if (this._options.clientMaxWindowBits) {
100 + params.client_max_window_bits = this._options.clientMaxWindowBits;
101 + } else if (this._options.clientMaxWindowBits == null) {
102 + params.client_max_window_bits = true;
103 + }
104 +
105 + return params;
106 + }
107 +
108 + /**
109 + * Accept an extension negotiation offer/response.
110 + *
111 + * @param {Array} configurations The extension negotiation offers/reponse
112 + * @return {Object} Accepted configuration
113 + * @public
114 + */
115 + accept(configurations) {
116 + configurations = this.normalizeParams(configurations);
117 +
118 + this.params = this._isServer
119 + ? this.acceptAsServer(configurations)
120 + : this.acceptAsClient(configurations);
121 +
122 + return this.params;
123 + }
124 +
125 + /**
126 + * Releases all resources used by the extension.
127 + *
128 + * @public
129 + */
130 + cleanup() {
131 + if (this._inflate) {
132 + this._inflate.close();
133 + this._inflate = null;
134 + }
135 +
136 + if (this._deflate) {
137 + const callback = this._deflate[kCallback];
138 +
139 + this._deflate.close();
140 + this._deflate = null;
141 +
142 + if (callback) {
143 + callback(
144 + new Error(
145 + 'The deflate stream was closed while data was being processed'
146 + )
147 + );
148 + }
149 + }
150 + }
151 +
152 + /**
153 + * Accept an extension negotiation offer.
154 + *
155 + * @param {Array} offers The extension negotiation offers
156 + * @return {Object} Accepted configuration
157 + * @private
158 + */
159 + acceptAsServer(offers) {
160 + const opts = this._options;
161 + const accepted = offers.find((params) => {
162 + if (
163 + (opts.serverNoContextTakeover === false &&
164 + params.server_no_context_takeover) ||
165 + (params.server_max_window_bits &&
166 + (opts.serverMaxWindowBits === false ||
167 + (typeof opts.serverMaxWindowBits === 'number' &&
168 + opts.serverMaxWindowBits > params.server_max_window_bits))) ||
169 + (typeof opts.clientMaxWindowBits === 'number' &&
170 + (typeof params.client_max_window_bits === 'number'
171 + ? opts.clientMaxWindowBits > params.client_max_window_bits
172 + : !params.client_max_window_bits))
173 + ) {
174 + return false;
175 + }
176 +
177 + return true;
178 + });
179 +
180 + if (!accepted) {
181 + throw new Error('None of the extension offers can be accepted');
182 + }
183 +
184 + if (opts.serverNoContextTakeover) {
185 + accepted.server_no_context_takeover = true;
186 + }
187 + if (opts.clientNoContextTakeover) {
188 + accepted.client_no_context_takeover = true;
189 + }
190 + if (typeof opts.serverMaxWindowBits === 'number') {
191 + accepted.server_max_window_bits = opts.serverMaxWindowBits;
192 + }
193 + if (typeof opts.clientMaxWindowBits === 'number') {
194 + accepted.client_max_window_bits = opts.clientMaxWindowBits;
195 + } else if (
196 + accepted.client_max_window_bits === true ||
197 + opts.clientMaxWindowBits === false
198 + ) {
199 + delete accepted.client_max_window_bits;
200 + }
201 +
202 + return accepted;
203 + }
204 +
205 + /**
206 + * Accept the extension negotiation response.
207 + *
208 + * @param {Array} response The extension negotiation response
209 + * @return {Object} Accepted configuration
210 + * @private
211 + */
212 + acceptAsClient(response) {
213 + const params = response[0];
214 +
215 + if (
216 + this._options.clientNoContextTakeover === false &&
217 + params.client_no_context_takeover
218 + ) {
219 + throw new Error('Unexpected parameter "client_no_context_takeover"');
220 + }
221 +
222 + if (!params.client_max_window_bits) {
223 + if (typeof this._options.clientMaxWindowBits === 'number') {
224 + params.client_max_window_bits = this._options.clientMaxWindowBits;
225 + }
226 + } else if (
227 + this._options.clientMaxWindowBits === false ||
228 + (typeof this._options.clientMaxWindowBits === 'number' &&
229 + params.client_max_window_bits > this._options.clientMaxWindowBits)
230 + ) {
231 + throw new Error(
232 + 'Unexpected or invalid parameter "client_max_window_bits"'
233 + );
234 + }
235 +
236 + return params;
237 + }
238 +
239 + /**
240 + * Normalize parameters.
241 + *
242 + * @param {Array} configurations The extension negotiation offers/reponse
243 + * @return {Array} The offers/response with normalized parameters
244 + * @private
245 + */
246 + normalizeParams(configurations) {
247 + configurations.forEach((params) => {
248 + Object.keys(params).forEach((key) => {
249 + let value = params[key];
250 +
251 + if (value.length > 1) {
252 + throw new Error(`Parameter "${key}" must have only a single value`);
253 + }
254 +
255 + value = value[0];
256 +
257 + if (key === 'client_max_window_bits') {
258 + if (value !== true) {
259 + const num = +value;
260 + if (!Number.isInteger(num) || num < 8 || num > 15) {
261 + throw new TypeError(
262 + `Invalid value for parameter "${key}": ${value}`
263 + );
264 + }
265 + value = num;
266 + } else if (!this._isServer) {
267 + throw new TypeError(
268 + `Invalid value for parameter "${key}": ${value}`
269 + );
270 + }
271 + } else if (key === 'server_max_window_bits') {
272 + const num = +value;
273 + if (!Number.isInteger(num) || num < 8 || num > 15) {
274 + throw new TypeError(
275 + `Invalid value for parameter "${key}": ${value}`
276 + );
277 + }
278 + value = num;
279 + } else if (
280 + key === 'client_no_context_takeover' ||
281 + key === 'server_no_context_takeover'
282 + ) {
283 + if (value !== true) {
284 + throw new TypeError(
285 + `Invalid value for parameter "${key}": ${value}`
286 + );
287 + }
288 + } else {
289 + throw new Error(`Unknown parameter "${key}"`);
290 + }
291 +
292 + params[key] = value;
293 + });
294 + });
295 +
296 + return configurations;
297 + }
298 +
299 + /**
300 + * Decompress data. Concurrency limited.
301 + *
302 + * @param {Buffer} data Compressed data
303 + * @param {Boolean} fin Specifies whether or not this is the last fragment
304 + * @param {Function} callback Callback
305 + * @public
306 + */
307 + decompress(data, fin, callback) {
308 + zlibLimiter.add((done) => {
309 + this._decompress(data, fin, (err, result) => {
310 + done();
311 + callback(err, result);
312 + });
313 + });
314 + }
315 +
316 + /**
317 + * Compress data. Concurrency limited.
318 + *
319 + * @param {(Buffer|String)} data Data to compress
320 + * @param {Boolean} fin Specifies whether or not this is the last fragment
321 + * @param {Function} callback Callback
322 + * @public
323 + */
324 + compress(data, fin, callback) {
325 + zlibLimiter.add((done) => {
326 + this._compress(data, fin, (err, result) => {
327 + done();
328 + callback(err, result);
329 + });
330 + });
331 + }
332 +
333 + /**
334 + * Decompress data.
335 + *
336 + * @param {Buffer} data Compressed data
337 + * @param {Boolean} fin Specifies whether or not this is the last fragment
338 + * @param {Function} callback Callback
339 + * @private
340 + */
341 + _decompress(data, fin, callback) {
342 + const endpoint = this._isServer ? 'client' : 'server';
343 +
344 + if (!this._inflate) {
345 + const key = `${endpoint}_max_window_bits`;
346 + const windowBits =
347 + typeof this.params[key] !== 'number'
348 + ? zlib.Z_DEFAULT_WINDOWBITS
349 + : this.params[key];
350 +
351 + this._inflate = zlib.createInflateRaw({
352 + ...this._options.zlibInflateOptions,
353 + windowBits
354 + });
355 + this._inflate[kPerMessageDeflate] = this;
356 + this._inflate[kTotalLength] = 0;
357 + this._inflate[kBuffers] = [];
358 + this._inflate.on('error', inflateOnError);
359 + this._inflate.on('data', inflateOnData);
360 + }
361 +
362 + this._inflate[kCallback] = callback;
363 +
364 + this._inflate.write(data);
365 + if (fin) this._inflate.write(TRAILER);
366 +
367 + this._inflate.flush(() => {
368 + const err = this._inflate[kError];
369 +
370 + if (err) {
371 + this._inflate.close();
372 + this._inflate = null;
373 + callback(err);
374 + return;
375 + }
376 +
377 + const data = bufferUtil.concat(
378 + this._inflate[kBuffers],
379 + this._inflate[kTotalLength]
380 + );
381 +
382 + if (this._inflate._readableState.endEmitted) {
383 + this._inflate.close();
384 + this._inflate = null;
385 + } else {
386 + this._inflate[kTotalLength] = 0;
387 + this._inflate[kBuffers] = [];
388 +
389 + if (fin && this.params[`${endpoint}_no_context_takeover`]) {
390 + this._inflate.reset();
391 + }
392 + }
393 +
394 + callback(null, data);
395 + });
396 + }
397 +
398 + /**
399 + * Compress data.
400 + *
401 + * @param {(Buffer|String)} data Data to compress
402 + * @param {Boolean} fin Specifies whether or not this is the last fragment
403 + * @param {Function} callback Callback
404 + * @private
405 + */
406 + _compress(data, fin, callback) {
407 + const endpoint = this._isServer ? 'server' : 'client';
408 +
409 + if (!this._deflate) {
410 + const key = `${endpoint}_max_window_bits`;
411 + const windowBits =
412 + typeof this.params[key] !== 'number'
413 + ? zlib.Z_DEFAULT_WINDOWBITS
414 + : this.params[key];
415 +
416 + this._deflate = zlib.createDeflateRaw({
417 + ...this._options.zlibDeflateOptions,
418 + windowBits
419 + });
420 +
421 + this._deflate[kTotalLength] = 0;
422 + this._deflate[kBuffers] = [];
423 +
424 + this._deflate.on('data', deflateOnData);
425 + }
426 +
427 + this._deflate[kCallback] = callback;
428 +
429 + this._deflate.write(data);
430 + this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
431 + if (!this._deflate) {
432 + //
433 + // The deflate stream was closed while data was being processed.
434 + //
435 + return;
436 + }
437 +
438 + let data = bufferUtil.concat(
439 + this._deflate[kBuffers],
440 + this._deflate[kTotalLength]
441 + );
442 +
443 + if (fin) {
444 + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4);
445 + }
446 +
447 + //
448 + // Ensure that the callback will not be called again in
449 + // `PerMessageDeflate#cleanup()`.
450 + //
451 + this._deflate[kCallback] = null;
452 +
453 + this._deflate[kTotalLength] = 0;
454 + this._deflate[kBuffers] = [];
455 +
456 + if (fin && this.params[`${endpoint}_no_context_takeover`]) {
457 + this._deflate.reset();
458 + }
459 +
460 + callback(null, data);
461 + });
462 + }
463 +}
464 +
465 +module.exports = PerMessageDeflate;
466 +
467 +/**
468 + * The listener of the `zlib.DeflateRaw` stream `'data'` event.
469 + *
470 + * @param {Buffer} chunk A chunk of data
471 + * @private
472 + */
473 +function deflateOnData(chunk) {
474 + this[kBuffers].push(chunk);
475 + this[kTotalLength] += chunk.length;
476 +}
477 +
478 +/**
479 + * The listener of the `zlib.InflateRaw` stream `'data'` event.
480 + *
481 + * @param {Buffer} chunk A chunk of data
482 + * @private
483 + */
484 +function inflateOnData(chunk) {
485 + this[kTotalLength] += chunk.length;
486 +
487 + if (
488 + this[kPerMessageDeflate]._maxPayload < 1 ||
489 + this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload
490 + ) {
491 + this[kBuffers].push(chunk);
492 + return;
493 + }
494 +
495 + this[kError] = new RangeError('Max payload size exceeded');
496 + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH';
497 + this[kError][kStatusCode] = 1009;
498 + this.removeListener('data', inflateOnData);
499 +
500 + //
501 + // The choice to employ `zlib.reset()` over `zlib.close()` is dictated by the
502 + // fact that in Node.js versions prior to 13.10.0, the callback for
503 + // `zlib.flush()` is not called if `zlib.close()` is used. Utilizing
504 + // `zlib.reset()` ensures that either the callback is invoked or an error is
505 + // emitted.
506 + //
507 + this.reset();
508 +}
509 +
510 +/**
511 + * The listener of the `zlib.InflateRaw` stream `'error'` event.
512 + *
513 + * @param {Error} err The emitted error
514 + * @private
515 + */
516 +function inflateOnError(err) {
517 + //
518 + // There is no need to call `Zlib#close()` as the handle is automatically
519 + // closed when an error is emitted.
520 + //
521 + this[kPerMessageDeflate]._inflate = null;
522 +
523 + if (this[kError]) {
524 + this[kCallback](this[kError]);
525 + return;
526 + }
527 +
528 + err[kStatusCode] = 1007;
529 + this[kCallback](err);
530 +}
added node_modules/ws/lib/receiver.js +743 −0
@@ -0,0 +1,743 @@
1 +'use strict';
2 +
3 +const { Writable } = require('stream');
4 +
5 +const PerMessageDeflate = require('./permessage-deflate');
6 +const {
7 + BINARY_TYPES,
8 + EMPTY_BUFFER,
9 + kStatusCode,
10 + kWebSocket
11 +} = require('./constants');
12 +const { concat, toArrayBuffer, unmask } = require('./buffer-util');
13 +const { isValidStatusCode, isValidUTF8 } = require('./validation');
14 +
15 +const FastBuffer = Buffer[Symbol.species];
16 +
17 +const GET_INFO = 0;
18 +const GET_PAYLOAD_LENGTH_16 = 1;
19 +const GET_PAYLOAD_LENGTH_64 = 2;
20 +const GET_MASK = 3;
21 +const GET_DATA = 4;
22 +const INFLATING = 5;
23 +const DEFER_EVENT = 6;
24 +
25 +/**
26 + * HyBi Receiver implementation.
27 + *
28 + * @extends Writable
29 + */
30 +class Receiver extends Writable {
31 + /**
32 + * Creates a Receiver instance.
33 + *
34 + * @param {Object} [options] Options object
35 + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether
36 + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
37 + * multiple times in the same tick
38 + * @param {String} [options.binaryType=nodebuffer] The type for binary data
39 + * @param {Object} [options.extensions] An object containing the negotiated
40 + * extensions
41 + * @param {Boolean} [options.isServer=false] Specifies whether to operate in
42 + * client or server mode
43 + * @param {Number} [options.maxBufferedChunks=0] The maximum number of
44 + * buffered data chunks
45 + * @param {Number} [options.maxFragments=0] The maximum number of message
46 + * fragments
47 + * @param {Number} [options.maxPayload=0] The maximum allowed message length
48 + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
49 + * not to skip UTF-8 validation for text and close messages
50 + */
51 + constructor(options = {}) {
52 + super();
53 +
54 + this._allowSynchronousEvents =
55 + options.allowSynchronousEvents !== undefined
56 + ? options.allowSynchronousEvents
57 + : true;
58 + this._binaryType = options.binaryType || BINARY_TYPES[0];
59 + this._extensions = options.extensions || {};
60 + this._isServer = !!options.isServer;
61 + this._maxBufferedChunks = options.maxBufferedChunks | 0;
62 + this._maxFragments = options.maxFragments | 0;
63 + this._maxPayload = options.maxPayload | 0;
64 + this._skipUTF8Validation = !!options.skipUTF8Validation;
65 + this[kWebSocket] = undefined;
66 +
67 + this._bufferedBytes = 0;
68 + this._buffers = [];
69 +
70 + this._compressed = false;
71 + this._payloadLength = 0;
72 + this._mask = undefined;
73 + this._fragmented = 0;
74 + this._masked = false;
75 + this._fin = false;
76 + this._opcode = 0;
77 +
78 + this._totalPayloadLength = 0;
79 + this._messageLength = 0;
80 + this._numFragments = 0;
81 + this._fragments = [];
82 +
83 + this._errored = false;
84 + this._loop = false;
85 + this._state = GET_INFO;
86 + }
87 +
88 + /**
89 + * Implements `Writable.prototype._write()`.
90 + *
91 + * @param {Buffer} chunk The chunk of data to write
92 + * @param {String} encoding The character encoding of `chunk`
93 + * @param {Function} cb Callback
94 + * @private
95 + */
96 + _write(chunk, encoding, cb) {
97 + if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
98 +
99 + if (
100 + this._maxBufferedChunks > 0 &&
101 + this._buffers.length >= this._maxBufferedChunks
102 + ) {
103 + cb(
104 + this.createError(
105 + RangeError,
106 + 'Too many buffered chunks',
107 + false,
108 + 1008,
109 + 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
110 + )
111 + );
112 + return;
113 + }
114 +
115 + this._bufferedBytes += chunk.length;
116 + this._buffers.push(chunk);
117 + this.startLoop(cb);
118 + }
119 +
120 + /**
121 + * Consumes `n` bytes from the buffered data.
122 + *
123 + * @param {Number} n The number of bytes to consume
124 + * @return {Buffer} The consumed bytes
125 + * @private
126 + */
127 + consume(n) {
128 + this._bufferedBytes -= n;
129 +
130 + if (n === this._buffers[0].length) return this._buffers.shift();
131 +
132 + if (n < this._buffers[0].length) {
133 + const buf = this._buffers[0];
134 + this._buffers[0] = new FastBuffer(
135 + buf.buffer,
136 + buf.byteOffset + n,
137 + buf.length - n
138 + );
139 +
140 + return new FastBuffer(buf.buffer, buf.byteOffset, n);
141 + }
142 +
143 + const dst = Buffer.allocUnsafe(n);
144 +
145 + do {
146 + const buf = this._buffers[0];
147 + const offset = dst.length - n;
148 +
149 + if (n >= buf.length) {
150 + dst.set(this._buffers.shift(), offset);
151 + } else {
152 + dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
153 + this._buffers[0] = new FastBuffer(
154 + buf.buffer,
155 + buf.byteOffset + n,
156 + buf.length - n
157 + );
158 + }
159 +
160 + n -= buf.length;
161 + } while (n > 0);
162 +
163 + return dst;
164 + }
165 +
166 + /**
167 + * Starts the parsing loop.
168 + *
169 + * @param {Function} cb Callback
170 + * @private
171 + */
172 + startLoop(cb) {
173 + this._loop = true;
174 +
175 + do {
176 + switch (this._state) {
177 + case GET_INFO:
178 + this.getInfo(cb);
179 + break;
180 + case GET_PAYLOAD_LENGTH_16:
181 + this.getPayloadLength16(cb);
182 + break;
183 + case GET_PAYLOAD_LENGTH_64:
184 + this.getPayloadLength64(cb);
185 + break;
186 + case GET_MASK:
187 + this.getMask();
188 + break;
189 + case GET_DATA:
190 + this.getData(cb);
191 + break;
192 + case INFLATING:
193 + case DEFER_EVENT:
194 + this._loop = false;
195 + return;
196 + }
197 + } while (this._loop);
198 +
199 + if (!this._errored) cb();
200 + }
201 +
202 + /**
203 + * Reads the first two bytes of a frame.
204 + *
205 + * @param {Function} cb Callback
206 + * @private
207 + */
208 + getInfo(cb) {
209 + if (this._bufferedBytes < 2) {
210 + this._loop = false;
211 + return;
212 + }
213 +
214 + const buf = this.consume(2);
215 +
216 + if ((buf[0] & 0x30) !== 0x00) {
217 + const error = this.createError(
218 + RangeError,
219 + 'RSV2 and RSV3 must be clear',
220 + true,
221 + 1002,
222 + 'WS_ERR_UNEXPECTED_RSV_2_3'
223 + );
224 +
225 + cb(error);
226 + return;
227 + }
228 +
229 + const compressed = (buf[0] & 0x40) === 0x40;
230 +
231 + if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
232 + const error = this.createError(
233 + RangeError,
234 + 'RSV1 must be clear',
235 + true,
236 + 1002,
237 + 'WS_ERR_UNEXPECTED_RSV_1'
238 + );
239 +
240 + cb(error);
241 + return;
242 + }
243 +
244 + this._fin = (buf[0] & 0x80) === 0x80;
245 + this._opcode = buf[0] & 0x0f;
246 + this._payloadLength = buf[1] & 0x7f;
247 +
248 + if (this._opcode === 0x00) {
249 + if (compressed) {
250 + const error = this.createError(
251 + RangeError,
252 + 'RSV1 must be clear',
253 + true,
254 + 1002,
255 + 'WS_ERR_UNEXPECTED_RSV_1'
256 + );
257 +
258 + cb(error);
259 + return;
260 + }
261 +
262 + if (!this._fragmented) {
263 + const error = this.createError(
264 + RangeError,
265 + 'invalid opcode 0',
266 + true,
267 + 1002,
268 + 'WS_ERR_INVALID_OPCODE'
269 + );
270 +
271 + cb(error);
272 + return;
273 + }
274 +
275 + this._opcode = this._fragmented;
276 + } else if (this._opcode === 0x01 || this._opcode === 0x02) {
277 + if (this._fragmented) {
278 + const error = this.createError(
279 + RangeError,
280 + `invalid opcode ${this._opcode}`,
281 + true,
282 + 1002,
283 + 'WS_ERR_INVALID_OPCODE'
284 + );
285 +
286 + cb(error);
287 + return;
288 + }
289 +
290 + this._compressed = compressed;
291 + } else if (this._opcode > 0x07 && this._opcode < 0x0b) {
292 + if (!this._fin) {
293 + const error = this.createError(
294 + RangeError,
295 + 'FIN must be set',
296 + true,
297 + 1002,
298 + 'WS_ERR_EXPECTED_FIN'
299 + );
300 +
301 + cb(error);
302 + return;
303 + }
304 +
305 + if (compressed) {
306 + const error = this.createError(
307 + RangeError,
308 + 'RSV1 must be clear',
309 + true,
310 + 1002,
311 + 'WS_ERR_UNEXPECTED_RSV_1'
312 + );
313 +
314 + cb(error);
315 + return;
316 + }
317 +
318 + if (
319 + this._payloadLength > 0x7d ||
320 + (this._opcode === 0x08 && this._payloadLength === 1)
321 + ) {
322 + const error = this.createError(
323 + RangeError,
324 + `invalid payload length ${this._payloadLength}`,
325 + true,
326 + 1002,
327 + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
328 + );
329 +
330 + cb(error);
331 + return;
332 + }
333 + } else {
334 + const error = this.createError(
335 + RangeError,
336 + `invalid opcode ${this._opcode}`,
337 + true,
338 + 1002,
339 + 'WS_ERR_INVALID_OPCODE'
340 + );
341 +
342 + cb(error);
343 + return;
344 + }
345 +
346 + if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
347 + this._masked = (buf[1] & 0x80) === 0x80;
348 +
349 + if (this._isServer) {
350 + if (!this._masked) {
351 + const error = this.createError(
352 + RangeError,
353 + 'MASK must be set',
354 + true,
355 + 1002,
356 + 'WS_ERR_EXPECTED_MASK'
357 + );
358 +
359 + cb(error);
360 + return;
361 + }
362 + } else if (this._masked) {
363 + const error = this.createError(
364 + RangeError,
365 + 'MASK must be clear',
366 + true,
367 + 1002,
368 + 'WS_ERR_UNEXPECTED_MASK'
369 + );
370 +
371 + cb(error);
372 + return;
373 + }
374 +
375 + if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
376 + else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
377 + else this.haveLength(cb);
378 + }
379 +
380 + /**
381 + * Gets extended payload length (7+16).
382 + *
383 + * @param {Function} cb Callback
384 + * @private
385 + */
386 + getPayloadLength16(cb) {
387 + if (this._bufferedBytes < 2) {
388 + this._loop = false;
389 + return;
390 + }
391 +
392 + this._payloadLength = this.consume(2).readUInt16BE(0);
393 + this.haveLength(cb);
394 + }
395 +
396 + /**
397 + * Gets extended payload length (7+64).
398 + *
399 + * @param {Function} cb Callback
400 + * @private
401 + */
402 + getPayloadLength64(cb) {
403 + if (this._bufferedBytes < 8) {
404 + this._loop = false;
405 + return;
406 + }
407 +
408 + const buf = this.consume(8);
409 + const num = buf.readUInt32BE(0);
410 +
411 + //
412 + // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
413 + // if payload length is greater than this number.
414 + //
415 + if (num > Math.pow(2, 53 - 32) - 1) {
416 + const error = this.createError(
417 + RangeError,
418 + 'Unsupported WebSocket frame: payload length > 2^53 - 1',
419 + false,
420 + 1009,
421 + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
422 + );
423 +
424 + cb(error);
425 + return;
426 + }
427 +
428 + this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
429 + this.haveLength(cb);
430 + }
431 +
432 + /**
433 + * Payload length has been read.
434 + *
435 + * @param {Function} cb Callback
436 + * @private
437 + */
438 + haveLength(cb) {
439 + if (this._payloadLength && this._opcode < 0x08) {
440 + this._totalPayloadLength += this._payloadLength;
441 + if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
442 + const error = this.createError(
443 + RangeError,
444 + 'Max payload size exceeded',
445 + false,
446 + 1009,
447 + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
448 + );
449 +
450 + cb(error);
451 + return;
452 + }
453 + }
454 +
455 + if (this._masked) this._state = GET_MASK;
456 + else this._state = GET_DATA;
457 + }
458 +
459 + /**
460 + * Reads mask bytes.
461 + *
462 + * @private
463 + */
464 + getMask() {
465 + if (this._bufferedBytes < 4) {
466 + this._loop = false;
467 + return;
468 + }
469 +
470 + this._mask = this.consume(4);
471 + this._state = GET_DATA;
472 + }
473 +
474 + /**
475 + * Reads data bytes.
476 + *
477 + * @param {Function} cb Callback
478 + * @private
479 + */
480 + getData(cb) {
481 + let data = EMPTY_BUFFER;
482 +
483 + if (this._payloadLength) {
484 + if (this._bufferedBytes < this._payloadLength) {
485 + this._loop = false;
486 + return;
487 + }
488 +
489 + data = this.consume(this._payloadLength);
490 +
491 + if (
492 + this._masked &&
493 + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0
494 + ) {
495 + unmask(data, this._mask);
496 + }
497 + }
498 +
499 + if (this._opcode > 0x07) {
500 + this.controlMessage(data, cb);
501 + return;
502 + }
503 +
504 + if (this._maxFragments > 0 && ++this._numFragments > this._maxFragments) {
505 + const error = this.createError(
506 + RangeError,
507 + 'Too many message fragments',
508 + false,
509 + 1008,
510 + 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
511 + );
512 +
513 + cb(error);
514 + return;
515 + }
516 +
517 + if (this._compressed) {
518 + this._state = INFLATING;
519 + this.decompress(data, cb);
520 + return;
521 + }
522 +
523 + if (data.length) {
524 + //
525 + // This message is not compressed so its length is the sum of the payload
526 + // length of all fragments.
527 + //
528 + this._messageLength = this._totalPayloadLength;
529 + this._fragments.push(data);
530 + }
531 +
532 + this.dataMessage(cb);
533 + }
534 +
535 + /**
536 + * Decompresses data.
537 + *
538 + * @param {Buffer} data Compressed data
539 + * @param {Function} cb Callback
540 + * @private
541 + */
542 + decompress(data, cb) {
543 + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
544 +
545 + perMessageDeflate.decompress(data, this._fin, (err, buf) => {
546 + if (err) return cb(err);
547 +
548 + if (buf.length) {
549 + this._messageLength += buf.length;
550 + if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
551 + const error = this.createError(
552 + RangeError,
553 + 'Max payload size exceeded',
554 + false,
555 + 1009,
556 + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
557 + );
558 +
559 + cb(error);
560 + return;
561 + }
562 +
563 + this._fragments.push(buf);
564 + }
565 +
566 + this.dataMessage(cb);
567 + if (this._state === GET_INFO) this.startLoop(cb);
568 + });
569 + }
570 +
571 + /**
572 + * Handles a data message.
573 + *
574 + * @param {Function} cb Callback
575 + * @private
576 + */
577 + dataMessage(cb) {
578 + if (!this._fin) {
579 + this._state = GET_INFO;
580 + return;
581 + }
582 +
583 + const messageLength = this._messageLength;
584 + const fragments = this._fragments;
585 +
586 + this._totalPayloadLength = 0;
587 + this._messageLength = 0;
588 + this._fragmented = 0;
589 + this._numFragments = 0;
590 + this._fragments = [];
591 +
592 + if (this._opcode === 2) {
593 + let data;
594 +
595 + if (this._binaryType === 'nodebuffer') {
596 + data = concat(fragments, messageLength);
597 + } else if (this._binaryType === 'arraybuffer') {
598 + data = toArrayBuffer(concat(fragments, messageLength));
599 + } else if (this._binaryType === 'blob') {
600 + data = new Blob(fragments);
601 + } else {
602 + data = fragments;
603 + }
604 +
605 + if (this._allowSynchronousEvents) {
606 + this.emit('message', data, true);
607 + this._state = GET_INFO;
608 + } else {
609 + this._state = DEFER_EVENT;
610 + setImmediate(() => {
611 + this.emit('message', data, true);
612 + this._state = GET_INFO;
613 + this.startLoop(cb);
614 + });
615 + }
616 + } else {
617 + const buf = concat(fragments, messageLength);
618 +
619 + if (!this._skipUTF8Validation && !isValidUTF8(buf)) {
620 + const error = this.createError(
621 + Error,
622 + 'invalid UTF-8 sequence',
623 + true,
624 + 1007,
625 + 'WS_ERR_INVALID_UTF8'
626 + );
627 +
628 + cb(error);
629 + return;
630 + }
631 +
632 + if (this._state === INFLATING || this._allowSynchronousEvents) {
633 + this.emit('message', buf, false);
634 + this._state = GET_INFO;
635 + } else {
636 + this._state = DEFER_EVENT;
637 + setImmediate(() => {
638 + this.emit('message', buf, false);
639 + this._state = GET_INFO;
640 + this.startLoop(cb);
641 + });
642 + }
643 + }
644 + }
645 +
646 + /**
647 + * Handles a control message.
648 + *
649 + * @param {Buffer} data Data to handle
650 + * @return {(Error|RangeError|undefined)} A possible error
651 + * @private
652 + */
653 + controlMessage(data, cb) {
654 + if (this._opcode === 0x08) {
655 + if (data.length === 0) {
656 + this._loop = false;
657 + this.emit('conclude', 1005, EMPTY_BUFFER);
658 + this.end();
659 + } else {
660 + const code = data.readUInt16BE(0);
661 +
662 + if (!isValidStatusCode(code)) {
663 + const error = this.createError(
664 + RangeError,
665 + `invalid status code ${code}`,
666 + true,
667 + 1002,
668 + 'WS_ERR_INVALID_CLOSE_CODE'
669 + );
670 +
671 + cb(error);
672 + return;
673 + }
674 +
675 + const buf = new FastBuffer(
676 + data.buffer,
677 + data.byteOffset + 2,
678 + data.length - 2
679 + );
680 +
681 + if (!this._skipUTF8Validation && !isValidUTF8(buf)) {
682 + const error = this.createError(
683 + Error,
684 + 'invalid UTF-8 sequence',
685 + true,
686 + 1007,
687 + 'WS_ERR_INVALID_UTF8'
688 + );
689 +
690 + cb(error);
691 + return;
692 + }
693 +
694 + this._loop = false;
695 + this.emit('conclude', code, buf);
696 + this.end();
697 + }
698 +
699 + this._state = GET_INFO;
700 + return;
701 + }
702 +
703 + if (this._allowSynchronousEvents) {
704 + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);
705 + this._state = GET_INFO;
706 + } else {
707 + this._state = DEFER_EVENT;
708 + setImmediate(() => {
709 + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);
710 + this._state = GET_INFO;
711 + this.startLoop(cb);
712 + });
713 + }
714 + }
715 +
716 + /**
717 + * Builds an error object.
718 + *
719 + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor
720 + * @param {String} message The error message
721 + * @param {Boolean} prefix Specifies whether or not to add a default prefix to
722 + * `message`
723 + * @param {Number} statusCode The status code
724 + * @param {String} errorCode The exposed error code
725 + * @return {(Error|RangeError)} The error
726 + * @private
727 + */
728 + createError(ErrorCtor, message, prefix, statusCode, errorCode) {
729 + this._loop = false;
730 + this._errored = true;
731 +
732 + const err = new ErrorCtor(
733 + prefix ? `Invalid WebSocket frame: ${message}` : message
734 + );
735 +
736 + Error.captureStackTrace(err, this.createError);
737 + err.code = errorCode;
738 + err[kStatusCode] = statusCode;
739 + return err;
740 + }
741 +}
742 +
743 +module.exports = Receiver;
added node_modules/ws/lib/sender.js +607 −0
@@ -0,0 +1,607 @@
1 +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */
2 +
3 +'use strict';
4 +
5 +const { Duplex } = require('stream');
6 +const { randomFillSync } = require('crypto');
7 +const {
8 + types: { isUint8Array }
9 +} = require('util');
10 +
11 +const PerMessageDeflate = require('./permessage-deflate');
12 +const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants');
13 +const { isBlob, isValidStatusCode } = require('./validation');
14 +const { mask: applyMask, toBuffer } = require('./buffer-util');
15 +
16 +const kByteLength = Symbol('kByteLength');
17 +const maskBuffer = Buffer.alloc(4);
18 +const RANDOM_POOL_SIZE = 8 * 1024;
19 +let randomPool;
20 +let randomPoolPointer = RANDOM_POOL_SIZE;
21 +
22 +const DEFAULT = 0;
23 +const DEFLATING = 1;
24 +const GET_BLOB_DATA = 2;
25 +
26 +/**
27 + * HyBi Sender implementation.
28 + */
29 +class Sender {
30 + /**
31 + * Creates a Sender instance.
32 + *
33 + * @param {Duplex} socket The connection socket
34 + * @param {Object} [extensions] An object containing the negotiated extensions
35 + * @param {Function} [generateMask] The function used to generate the masking
36 + * key
37 + */
38 + constructor(socket, extensions, generateMask) {
39 + this._extensions = extensions || {};
40 +
41 + if (generateMask) {
42 + this._generateMask = generateMask;
43 + this._maskBuffer = Buffer.alloc(4);
44 + }
45 +
46 + this._socket = socket;
47 +
48 + this._firstFragment = true;
49 + this._compress = false;
50 +
51 + this._bufferedBytes = 0;
52 + this._queue = [];
53 + this._state = DEFAULT;
54 + this.onerror = NOOP;
55 + this[kWebSocket] = undefined;
56 + }
57 +
58 + /**
59 + * Frames a piece of data according to the HyBi WebSocket protocol.
60 + *
61 + * @param {(Buffer|String)} data The data to frame
62 + * @param {Object} options Options object
63 + * @param {Boolean} [options.fin=false] Specifies whether or not to set the
64 + * FIN bit
65 + * @param {Function} [options.generateMask] The function used to generate the
66 + * masking key
67 + * @param {Boolean} [options.mask=false] Specifies whether or not to mask
68 + * `data`
69 + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking
70 + * key
71 + * @param {Number} options.opcode The opcode
72 + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
73 + * modified
74 + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
75 + * RSV1 bit
76 + * @return {(Buffer|String)[]} The framed data
77 + * @public
78 + */
79 + static frame(data, options) {
80 + let mask;
81 + let merge = false;
82 + let offset = 2;
83 + let skipMasking = false;
84 +
85 + if (options.mask) {
86 + mask = options.maskBuffer || maskBuffer;
87 +
88 + if (options.generateMask) {
89 + options.generateMask(mask);
90 + } else {
91 + if (randomPoolPointer === RANDOM_POOL_SIZE) {
92 + /* istanbul ignore else */
93 + if (randomPool === undefined) {
94 + //
95 + // This is lazily initialized because server-sent frames must not
96 + // be masked so it may never be used.
97 + //
98 + randomPool = Buffer.alloc(RANDOM_POOL_SIZE);
99 + }
100 +
101 + randomFillSync(randomPool, 0, RANDOM_POOL_SIZE);
102 + randomPoolPointer = 0;
103 + }
104 +
105 + mask[0] = randomPool[randomPoolPointer++];
106 + mask[1] = randomPool[randomPoolPointer++];
107 + mask[2] = randomPool[randomPoolPointer++];
108 + mask[3] = randomPool[randomPoolPointer++];
109 + }
110 +
111 + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0;
112 + offset = 6;
113 + }
114 +
115 + let dataLength;
116 +
117 + if (typeof data === 'string') {
118 + if (
119 + (!options.mask || skipMasking) &&
120 + options[kByteLength] !== undefined
121 + ) {
122 + dataLength = options[kByteLength];
123 + } else {
124 + data = Buffer.from(data);
125 + dataLength = data.length;
126 + }
127 + } else {
128 + dataLength = data.length;
129 + merge = options.mask && options.readOnly && !skipMasking;
130 + }
131 +
132 + let payloadLength = dataLength;
133 +
134 + if (dataLength >= 65536) {
135 + offset += 8;
136 + payloadLength = 127;
137 + } else if (dataLength > 125) {
138 + offset += 2;
139 + payloadLength = 126;
140 + }
141 +
142 + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset);
143 +
144 + target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
145 + if (options.rsv1) target[0] |= 0x40;
146 +
147 + target[1] = payloadLength;
148 +
149 + if (payloadLength === 126) {
150 + target.writeUInt16BE(dataLength, 2);
151 + } else if (payloadLength === 127) {
152 + target[2] = target[3] = 0;
153 + target.writeUIntBE(dataLength, 4, 6);
154 + }
155 +
156 + if (!options.mask) return [target, data];
157 +
158 + target[1] |= 0x80;
159 + target[offset - 4] = mask[0];
160 + target[offset - 3] = mask[1];
161 + target[offset - 2] = mask[2];
162 + target[offset - 1] = mask[3];
163 +
164 + if (skipMasking) return [target, data];
165 +
166 + if (merge) {
167 + applyMask(data, mask, target, offset, dataLength);
168 + return [target];
169 + }
170 +
171 + applyMask(data, mask, data, 0, dataLength);
172 + return [target, data];
173 + }
174 +
175 + /**
176 + * Sends a close message to the other peer.
177 + *
178 + * @param {Number} [code] The status code component of the body
179 + * @param {(String|Buffer)} [data] The message component of the body
180 + * @param {Boolean} [mask=false] Specifies whether or not to mask the message
181 + * @param {Function} [cb] Callback
182 + * @public
183 + */
184 + close(code, data, mask, cb) {
185 + let buf;
186 +
187 + if (code === undefined) {
188 + buf = EMPTY_BUFFER;
189 + } else if (typeof code !== 'number' || !isValidStatusCode(code)) {
190 + throw new TypeError('First argument must be a valid error code number');
191 + } else if (data === undefined || !data.length) {
192 + buf = Buffer.allocUnsafe(2);
193 + buf.writeUInt16BE(code, 0);
194 + } else {
195 + const length = Buffer.byteLength(data);
196 +
197 + if (length > 123) {
198 + throw new RangeError('The message must not be greater than 123 bytes');
199 + }
200 +
201 + buf = Buffer.allocUnsafe(2 + length);
202 + buf.writeUInt16BE(code, 0);
203 +
204 + if (typeof data === 'string') {
205 + buf.write(data, 2);
206 + } else if (isUint8Array(data)) {
207 + buf.set(data, 2);
208 + } else {
209 + throw new TypeError('Second argument must be a string or a Uint8Array');
210 + }
211 + }
212 +
213 + const options = {
214 + [kByteLength]: buf.length,
215 + fin: true,
216 + generateMask: this._generateMask,
217 + mask,
218 + maskBuffer: this._maskBuffer,
219 + opcode: 0x08,
220 + readOnly: false,
221 + rsv1: false
222 + };
223 +
224 + if (this._state !== DEFAULT) {
225 + this.enqueue([this.dispatch, buf, false, options, cb]);
226 + } else {
227 + this.sendFrame(Sender.frame(buf, options), cb);
228 + }
229 + }
230 +
231 + /**
232 + * Sends a ping message to the other peer.
233 + *
234 + * @param {*} data The message to send
235 + * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
236 + * @param {Function} [cb] Callback
237 + * @public
238 + */
239 + ping(data, mask, cb) {
240 + let byteLength;
241 + let readOnly;
242 +
243 + if (typeof data === 'string') {
244 + byteLength = Buffer.byteLength(data);
245 + readOnly = false;
246 + } else if (isBlob(data)) {
247 + byteLength = data.size;
248 + readOnly = false;
249 + } else {
250 + data = toBuffer(data);
251 + byteLength = data.length;
252 + readOnly = toBuffer.readOnly;
253 + }
254 +
255 + if (byteLength > 125) {
256 + throw new RangeError('The data size must not be greater than 125 bytes');
257 + }
258 +
259 + const options = {
260 + [kByteLength]: byteLength,
261 + fin: true,
262 + generateMask: this._generateMask,
263 + mask,
264 + maskBuffer: this._maskBuffer,
265 + opcode: 0x09,
266 + readOnly,
267 + rsv1: false
268 + };
269 +
270 + if (isBlob(data)) {
271 + if (this._state !== DEFAULT) {
272 + this.enqueue([this.getBlobData, data, false, options, cb]);
273 + } else {
274 + this.getBlobData(data, false, options, cb);
275 + }
276 + } else if (this._state !== DEFAULT) {
277 + this.enqueue([this.dispatch, data, false, options, cb]);
278 + } else {
279 + this.sendFrame(Sender.frame(data, options), cb);
280 + }
281 + }
282 +
283 + /**
284 + * Sends a pong message to the other peer.
285 + *
286 + * @param {*} data The message to send
287 + * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
288 + * @param {Function} [cb] Callback
289 + * @public
290 + */
291 + pong(data, mask, cb) {
292 + let byteLength;
293 + let readOnly;
294 +
295 + if (typeof data === 'string') {
296 + byteLength = Buffer.byteLength(data);
297 + readOnly = false;
298 + } else if (isBlob(data)) {
299 + byteLength = data.size;
300 + readOnly = false;
301 + } else {
302 + data = toBuffer(data);
303 + byteLength = data.length;
304 + readOnly = toBuffer.readOnly;
305 + }
306 +
307 + if (byteLength > 125) {
308 + throw new RangeError('The data size must not be greater than 125 bytes');
309 + }
310 +
311 + const options = {
312 + [kByteLength]: byteLength,
313 + fin: true,
314 + generateMask: this._generateMask,
315 + mask,
316 + maskBuffer: this._maskBuffer,
317 + opcode: 0x0a,
318 + readOnly,
319 + rsv1: false
320 + };
321 +
322 + if (isBlob(data)) {
323 + if (this._state !== DEFAULT) {
324 + this.enqueue([this.getBlobData, data, false, options, cb]);
325 + } else {
326 + this.getBlobData(data, false, options, cb);
327 + }
328 + } else if (this._state !== DEFAULT) {
329 + this.enqueue([this.dispatch, data, false, options, cb]);
330 + } else {
331 + this.sendFrame(Sender.frame(data, options), cb);
332 + }
333 + }
334 +
335 + /**
336 + * Sends a data message to the other peer.
337 + *
338 + * @param {*} data The message to send
339 + * @param {Object} options Options object
340 + * @param {Boolean} [options.binary=false] Specifies whether `data` is binary
341 + * or text
342 + * @param {Boolean} [options.compress=false] Specifies whether or not to
343 + * compress `data`
344 + * @param {Boolean} [options.fin=false] Specifies whether the fragment is the
345 + * last one
346 + * @param {Boolean} [options.mask=false] Specifies whether or not to mask
347 + * `data`
348 + * @param {Function} [cb] Callback
349 + * @public
350 + */
351 + send(data, options, cb) {
352 + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
353 + let opcode = options.binary ? 2 : 1;
354 + let rsv1 = options.compress;
355 +
356 + let byteLength;
357 + let readOnly;
358 +
359 + if (typeof data === 'string') {
360 + byteLength = Buffer.byteLength(data);
361 + readOnly = false;
362 + } else if (isBlob(data)) {
363 + byteLength = data.size;
364 + readOnly = false;
365 + } else {
366 + data = toBuffer(data);
367 + byteLength = data.length;
368 + readOnly = toBuffer.readOnly;
369 + }
370 +
371 + if (this._firstFragment) {
372 + this._firstFragment = false;
373 + if (
374 + rsv1 &&
375 + perMessageDeflate &&
376 + perMessageDeflate.params[
377 + perMessageDeflate._isServer
378 + ? 'server_no_context_takeover'
379 + : 'client_no_context_takeover'
380 + ]
381 + ) {
382 + rsv1 = byteLength >= perMessageDeflate._threshold;
383 + }
384 + this._compress = rsv1;
385 + } else {
386 + rsv1 = false;
387 + opcode = 0;
388 + }
389 +
390 + if (options.fin) this._firstFragment = true;
391 +
392 + const opts = {
393 + [kByteLength]: byteLength,
394 + fin: options.fin,
395 + generateMask: this._generateMask,
396 + mask: options.mask,
397 + maskBuffer: this._maskBuffer,
398 + opcode,
399 + readOnly,
400 + rsv1
401 + };
402 +
403 + if (isBlob(data)) {
404 + if (this._state !== DEFAULT) {
405 + this.enqueue([this.getBlobData, data, this._compress, opts, cb]);
406 + } else {
407 + this.getBlobData(data, this._compress, opts, cb);
408 + }
409 + } else if (this._state !== DEFAULT) {
410 + this.enqueue([this.dispatch, data, this._compress, opts, cb]);
411 + } else {
412 + this.dispatch(data, this._compress, opts, cb);
413 + }
414 + }
415 +
416 + /**
417 + * Gets the contents of a blob as binary data.
418 + *
419 + * @param {Blob} blob The blob
420 + * @param {Boolean} [compress=false] Specifies whether or not to compress
421 + * the data
422 + * @param {Object} options Options object
423 + * @param {Boolean} [options.fin=false] Specifies whether or not to set the
424 + * FIN bit
425 + * @param {Function} [options.generateMask] The function used to generate the
426 + * masking key
427 + * @param {Boolean} [options.mask=false] Specifies whether or not to mask
428 + * `data`
429 + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking
430 + * key
431 + * @param {Number} options.opcode The opcode
432 + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
433 + * modified
434 + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
435 + * RSV1 bit
436 + * @param {Function} [cb] Callback
437 + * @private
438 + */
439 + getBlobData(blob, compress, options, cb) {
440 + this._bufferedBytes += options[kByteLength];
441 + this._state = GET_BLOB_DATA;
442 +
443 + blob
444 + .arrayBuffer()
445 + .then((arrayBuffer) => {
446 + if (this._socket.destroyed) {
447 + const err = new Error(
448 + 'The socket was closed while the blob was being read'
449 + );
450 +
451 + //
452 + // `callCallbacks` is called in the next tick to ensure that errors
453 + // that might be thrown in the callbacks behave like errors thrown
454 + // outside the promise chain.
455 + //
456 + process.nextTick(callCallbacks, this, err, cb);
457 + return;
458 + }
459 +
460 + this._bufferedBytes -= options[kByteLength];
461 + const data = toBuffer(arrayBuffer);
462 +
463 + if (!compress) {
464 + this._state = DEFAULT;
465 + this.sendFrame(Sender.frame(data, options), cb);
466 + this.dequeue();
467 + } else {
468 + this.dispatch(data, compress, options, cb);
469 + }
470 + })
471 + .catch((err) => {
472 + //
473 + // `onError` is called in the next tick for the same reason that
474 + // `callCallbacks` above is.
475 + //
476 + process.nextTick(onError, this, err, cb);
477 + });
478 + }
479 +
480 + /**
481 + * Dispatches a message.
482 + *
483 + * @param {(Buffer|String)} data The message to send
484 + * @param {Boolean} [compress=false] Specifies whether or not to compress
485 + * `data`
486 + * @param {Object} options Options object
487 + * @param {Boolean} [options.fin=false] Specifies whether or not to set the
488 + * FIN bit
489 + * @param {Function} [options.generateMask] The function used to generate the
490 + * masking key
491 + * @param {Boolean} [options.mask=false] Specifies whether or not to mask
492 + * `data`
493 + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking
494 + * key
495 + * @param {Number} options.opcode The opcode
496 + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
497 + * modified
498 + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
499 + * RSV1 bit
500 + * @param {Function} [cb] Callback
501 + * @private
502 + */
503 + dispatch(data, compress, options, cb) {
504 + if (!compress) {
505 + this.sendFrame(Sender.frame(data, options), cb);
506 + return;
507 + }
508 +
509 + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
510 +
511 + this._bufferedBytes += options[kByteLength];
512 + this._state = DEFLATING;
513 + perMessageDeflate.compress(data, options.fin, (_, buf) => {
514 + if (this._socket.destroyed) {
515 + const err = new Error(
516 + 'The socket was closed while data was being compressed'
517 + );
518 +
519 + callCallbacks(this, err, cb);
520 + return;
521 + }
522 +
523 + this._bufferedBytes -= options[kByteLength];
524 + this._state = DEFAULT;
525 + options.readOnly = false;
526 + this.sendFrame(Sender.frame(buf, options), cb);
527 + this.dequeue();
528 + });
529 + }
530 +
531 + /**
532 + * Executes queued send operations.
533 + *
534 + * @private
535 + */
536 + dequeue() {
537 + while (this._state === DEFAULT && this._queue.length) {
538 + const params = this._queue.shift();
539 +
540 + this._bufferedBytes -= params[3][kByteLength];
541 + Reflect.apply(params[0], this, params.slice(1));
542 + }
543 + }
544 +
545 + /**
546 + * Enqueues a send operation.
547 + *
548 + * @param {Array} params Send operation parameters.
549 + * @private
550 + */
551 + enqueue(params) {
552 + this._bufferedBytes += params[3][kByteLength];
553 + this._queue.push(params);
554 + }
555 +
556 + /**
557 + * Sends a frame.
558 + *
559 + * @param {(Buffer | String)[]} list The frame to send
560 + * @param {Function} [cb] Callback
561 + * @private
562 + */
563 + sendFrame(list, cb) {
564 + if (list.length === 2) {
565 + this._socket.cork();
566 + this._socket.write(list[0]);
567 + this._socket.write(list[1], cb);
568 + this._socket.uncork();
569 + } else {
570 + this._socket.write(list[0], cb);
571 + }
572 + }
573 +}
574 +
575 +module.exports = Sender;
576 +
577 +/**
578 + * Calls queued callbacks with an error.
579 + *
580 + * @param {Sender} sender The `Sender` instance
581 + * @param {Error} err The error to call the callbacks with
582 + * @param {Function} [cb] The first callback
583 + * @private
584 + */
585 +function callCallbacks(sender, err, cb) {
586 + if (typeof cb === 'function') cb(err);
587 +
588 + for (let i = 0; i < sender._queue.length; i++) {
589 + const params = sender._queue[i];
590 + const callback = params[params.length - 1];
591 +
592 + if (typeof callback === 'function') callback(err);
593 + }
594 +}
595 +
596 +/**
597 + * Handles a `Sender` error.
598 + *
599 + * @param {Sender} sender The `Sender` instance
600 + * @param {Error} err The error
601 + * @param {Function} [cb] The first pending callback
602 + * @private
603 + */
604 +function onError(sender, err, cb) {
605 + callCallbacks(sender, err, cb);
606 + sender.onerror(err);
607 +}
added node_modules/ws/lib/stream.js +161 −0
@@ -0,0 +1,161 @@
1 +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^WebSocket$" }] */
2 +'use strict';
3 +
4 +const WebSocket = require('./websocket');
5 +const { Duplex } = require('stream');
6 +
7 +/**
8 + * Emits the `'close'` event on a stream.
9 + *
10 + * @param {Duplex} stream The stream.
11 + * @private
12 + */
13 +function emitClose(stream) {
14 + stream.emit('close');
15 +}
16 +
17 +/**
18 + * The listener of the `'end'` event.
19 + *
20 + * @private
21 + */
22 +function duplexOnEnd() {
23 + if (!this.destroyed && this._writableState.finished) {
24 + this.destroy();
25 + }
26 +}
27 +
28 +/**
29 + * The listener of the `'error'` event.
30 + *
31 + * @param {Error} err The error
32 + * @private
33 + */
34 +function duplexOnError(err) {
35 + this.removeListener('error', duplexOnError);
36 + this.destroy();
37 + if (this.listenerCount('error') === 0) {
38 + // Do not suppress the throwing behavior.
39 + this.emit('error', err);
40 + }
41 +}
42 +
43 +/**
44 + * Wraps a `WebSocket` in a duplex stream.
45 + *
46 + * @param {WebSocket} ws The `WebSocket` to wrap
47 + * @param {Object} [options] The options for the `Duplex` constructor
48 + * @return {Duplex} The duplex stream
49 + * @public
50 + */
51 +function createWebSocketStream(ws, options) {
52 + let terminateOnDestroy = true;
53 +
54 + const duplex = new Duplex({
55 + ...options,
56 + autoDestroy: false,
57 + emitClose: false,
58 + objectMode: false,
59 + writableObjectMode: false
60 + });
61 +
62 + ws.on('message', function message(msg, isBinary) {
63 + const data =
64 + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg;
65 +
66 + if (!duplex.push(data)) ws.pause();
67 + });
68 +
69 + ws.once('error', function error(err) {
70 + if (duplex.destroyed) return;
71 +
72 + // Prevent `ws.terminate()` from being called by `duplex._destroy()`.
73 + //
74 + // - If the `'error'` event is emitted before the `'open'` event, then
75 + // `ws.terminate()` is a noop as no socket is assigned.
76 + // - Otherwise, the error is re-emitted by the listener of the `'error'`
77 + // event of the `Receiver` object. The listener already closes the
78 + // connection by calling `ws.close()`. This allows a close frame to be
79 + // sent to the other peer. If `ws.terminate()` is called right after this,
80 + // then the close frame might not be sent.
81 + terminateOnDestroy = false;
82 + duplex.destroy(err);
83 + });
84 +
85 + ws.once('close', function close() {
86 + if (duplex.destroyed) return;
87 +
88 + duplex.push(null);
89 + });
90 +
91 + duplex._destroy = function (err, callback) {
92 + if (ws.readyState === ws.CLOSED) {
93 + callback(err);
94 + process.nextTick(emitClose, duplex);
95 + return;
96 + }
97 +
98 + let called = false;
99 +
100 + ws.once('error', function error(err) {
101 + called = true;
102 + callback(err);
103 + });
104 +
105 + ws.once('close', function close() {
106 + if (!called) callback(err);
107 + process.nextTick(emitClose, duplex);
108 + });
109 +
110 + if (terminateOnDestroy) ws.terminate();
111 + };
112 +
113 + duplex._final = function (callback) {
114 + if (ws.readyState === ws.CONNECTING) {
115 + ws.once('open', function open() {
116 + duplex._final(callback);
117 + });
118 + return;
119 + }
120 +
121 + // If the value of the `_socket` property is `null` it means that `ws` is a
122 + // client websocket and the handshake failed. In fact, when this happens, a
123 + // socket is never assigned to the websocket. Wait for the `'error'` event
124 + // that will be emitted by the websocket.
125 + if (ws._socket === null) return;
126 +
127 + if (ws._socket._writableState.finished) {
128 + callback();
129 + if (duplex._readableState.endEmitted) duplex.destroy();
130 + } else {
131 + ws._socket.once('finish', function finish() {
132 + // `duplex` is not destroyed here because the `'end'` event will be
133 + // emitted on `duplex` after this `'finish'` event. The EOF signaling
134 + // `null` chunk is, in fact, pushed when the websocket emits `'close'`.
135 + callback();
136 + });
137 + ws.close();
138 + }
139 + };
140 +
141 + duplex._read = function () {
142 + if (ws.isPaused) ws.resume();
143 + };
144 +
145 + duplex._write = function (chunk, encoding, callback) {
146 + if (ws.readyState === ws.CONNECTING) {
147 + ws.once('open', function open() {
148 + duplex._write(chunk, encoding, callback);
149 + });
150 + return;
151 + }
152 +
153 + ws.send(chunk, callback);
154 + };
155 +
156 + duplex.on('end', duplexOnEnd);
157 + duplex.on('error', duplexOnError);
158 + return duplex;
159 +}
160 +
161 +module.exports = createWebSocketStream;
added node_modules/ws/lib/subprotocol.js +62 −0
@@ -0,0 +1,62 @@
1 +'use strict';
2 +
3 +const { tokenChars } = require('./validation');
4 +
5 +/**
6 + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names.
7 + *
8 + * @param {String} header The field value of the header
9 + * @return {Set} The subprotocol names
10 + * @public
11 + */
12 +function parse(header) {
13 + const protocols = new Set();
14 + let start = -1;
15 + let end = -1;
16 + let i = 0;
17 +
18 + for (i; i < header.length; i++) {
19 + const code = header.charCodeAt(i);
20 +
21 + if (end === -1 && tokenChars[code] === 1) {
22 + if (start === -1) start = i;
23 + } else if (
24 + i !== 0 &&
25 + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
26 + ) {
27 + if (end === -1 && start !== -1) end = i;
28 + } else if (code === 0x2c /* ',' */) {
29 + if (start === -1) {
30 + throw new SyntaxError(`Unexpected character at index ${i}`);
31 + }
32 +
33 + if (end === -1) end = i;
34 +
35 + const protocol = header.slice(start, end);
36 +
37 + if (protocols.has(protocol)) {
38 + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
39 + }
40 +
41 + protocols.add(protocol);
42 + start = end = -1;
43 + } else {
44 + throw new SyntaxError(`Unexpected character at index ${i}`);
45 + }
46 + }
47 +
48 + if (start === -1 || end !== -1) {
49 + throw new SyntaxError('Unexpected end of input');
50 + }
51 +
52 + const protocol = header.slice(start, i);
53 +
54 + if (protocols.has(protocol)) {
55 + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
56 + }
57 +
58 + protocols.add(protocol);
59 + return protocols;
60 +}
61 +
62 +module.exports = { parse };
added node_modules/ws/lib/validation.js +152 −0
@@ -0,0 +1,152 @@
1 +'use strict';
2 +
3 +const { isUtf8 } = require('buffer');
4 +
5 +const { hasBlob } = require('./constants');
6 +
7 +//
8 +// Allowed token characters:
9 +//
10 +// '!', '#', '$', '%', '&', ''', '*', '+', '-',
11 +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
12 +//
13 +// tokenChars[32] === 0 // ' '
14 +// tokenChars[33] === 1 // '!'
15 +// tokenChars[34] === 0 // '"'
16 +// ...
17 +//
18 +// prettier-ignore
19 +const tokenChars = [
20 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
21 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
22 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47
23 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
24 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
25 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95
26 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
27 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127
28 +];
29 +
30 +/**
31 + * Checks if a status code is allowed in a close frame.
32 + *
33 + * @param {Number} code The status code
34 + * @return {Boolean} `true` if the status code is valid, else `false`
35 + * @public
36 + */
37 +function isValidStatusCode(code) {
38 + return (
39 + (code >= 1000 &&
40 + code <= 1014 &&
41 + code !== 1004 &&
42 + code !== 1005 &&
43 + code !== 1006) ||
44 + (code >= 3000 && code <= 4999)
45 + );
46 +}
47 +
48 +/**
49 + * Checks if a given buffer contains only correct UTF-8.
50 + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by
51 + * Markus Kuhn.
52 + *
53 + * @param {Buffer} buf The buffer to check
54 + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false`
55 + * @public
56 + */
57 +function _isValidUTF8(buf) {
58 + const len = buf.length;
59 + let i = 0;
60 +
61 + while (i < len) {
62 + if ((buf[i] & 0x80) === 0) {
63 + // 0xxxxxxx
64 + i++;
65 + } else if ((buf[i] & 0xe0) === 0xc0) {
66 + // 110xxxxx 10xxxxxx
67 + if (
68 + i + 1 === len ||
69 + (buf[i + 1] & 0xc0) !== 0x80 ||
70 + (buf[i] & 0xfe) === 0xc0 // Overlong
71 + ) {
72 + return false;
73 + }
74 +
75 + i += 2;
76 + } else if ((buf[i] & 0xf0) === 0xe0) {
77 + // 1110xxxx 10xxxxxx 10xxxxxx
78 + if (
79 + i + 2 >= len ||
80 + (buf[i + 1] & 0xc0) !== 0x80 ||
81 + (buf[i + 2] & 0xc0) !== 0x80 ||
82 + (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong
83 + (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF)
84 + ) {
85 + return false;
86 + }
87 +
88 + i += 3;
89 + } else if ((buf[i] & 0xf8) === 0xf0) {
90 + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
91 + if (
92 + i + 3 >= len ||
93 + (buf[i + 1] & 0xc0) !== 0x80 ||
94 + (buf[i + 2] & 0xc0) !== 0x80 ||
95 + (buf[i + 3] & 0xc0) !== 0x80 ||
96 + (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong
97 + (buf[i] === 0xf4 && buf[i + 1] > 0x8f) ||
98 + buf[i] > 0xf4 // > U+10FFFF
99 + ) {
100 + return false;
101 + }
102 +
103 + i += 4;
104 + } else {
105 + return false;
106 + }
107 + }
108 +
109 + return true;
110 +}
111 +
112 +/**
113 + * Determines whether a value is a `Blob`.
114 + *
115 + * @param {*} value The value to be tested
116 + * @return {Boolean} `true` if `value` is a `Blob`, else `false`
117 + * @private
118 + */
119 +function isBlob(value) {
120 + return (
121 + hasBlob &&
122 + typeof value === 'object' &&
123 + typeof value.arrayBuffer === 'function' &&
124 + typeof value.type === 'string' &&
125 + typeof value.stream === 'function' &&
126 + (value[Symbol.toStringTag] === 'Blob' ||
127 + value[Symbol.toStringTag] === 'File')
128 + );
129 +}
130 +
131 +module.exports = {
132 + isBlob,
133 + isValidStatusCode,
134 + isValidUTF8: _isValidUTF8,
135 + tokenChars
136 +};
137 +
138 +if (isUtf8) {
139 + module.exports.isValidUTF8 = function (buf) {
140 + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf);
141 + };
142 +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) {
143 + try {
144 + const isValidUTF8 = require('utf-8-validate');
145 +
146 + module.exports.isValidUTF8 = function (buf) {
147 + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf);
148 + };
149 + } catch (e) {
150 + // Continue regardless of the error.
151 + }
152 +}
added node_modules/ws/lib/websocket-server.js +562 −0
@@ -0,0 +1,562 @@
1 +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */
2 +
3 +'use strict';
4 +
5 +const EventEmitter = require('events');
6 +const http = require('http');
7 +const { Duplex } = require('stream');
8 +const { createHash } = require('crypto');
9 +
10 +const extension = require('./extension');
11 +const PerMessageDeflate = require('./permessage-deflate');
12 +const subprotocol = require('./subprotocol');
13 +const WebSocket = require('./websocket');
14 +const { CLOSE_TIMEOUT, GUID, kWebSocket } = require('./constants');
15 +
16 +const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
17 +
18 +const RUNNING = 0;
19 +const CLOSING = 1;
20 +const CLOSED = 2;
21 +
22 +/**
23 + * Class representing a WebSocket server.
24 + *
25 + * @extends EventEmitter
26 + */
27 +class WebSocketServer extends EventEmitter {
28 + /**
29 + * Create a `WebSocketServer` instance.
30 + *
31 + * @param {Object} options Configuration options
32 + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether
33 + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
34 + * multiple times in the same tick
35 + * @param {Boolean} [options.autoPong=true] Specifies whether or not to
36 + * automatically send a pong in response to a ping
37 + * @param {Number} [options.backlog=511] The maximum length of the queue of
38 + * pending connections
39 + * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
40 + * track clients
41 + * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
42 + * wait for the closing handshake to finish after `websocket.close()` is
43 + * called
44 + * @param {Function} [options.handleProtocols] A hook to handle protocols
45 + * @param {String} [options.host] The hostname where to bind the server
46 + * @param {Number} [options.maxBufferedChunks=262144] The maximum number of
47 + * buffered data chunks
48 + * @param {Number} [options.maxFragments=16384] The maximum number of message
49 + * fragments
50 + * @param {Number} [options.maxPayload=104857600] The maximum allowed message
51 + * size
52 + * @param {Boolean} [options.noServer=false] Enable no server mode
53 + * @param {String} [options.path] Accept only connections matching this path
54 + * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
55 + * permessage-deflate
56 + * @param {Number} [options.port] The port where to bind the server
57 + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
58 + * server to use
59 + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
60 + * not to skip UTF-8 validation for text and close messages
61 + * @param {Function} [options.verifyClient] A hook to reject connections
62 + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket`
63 + * class to use. It must be the `WebSocket` class or class that extends it
64 + * @param {Function} [callback] A listener for the `listening` event
65 + */
66 + constructor(options, callback) {
67 + super();
68 +
69 + options = {
70 + allowSynchronousEvents: true,
71 + autoPong: true,
72 + maxBufferedChunks: 256 * 1024,
73 + maxFragments: 16 * 1024,
74 + maxPayload: 100 * 1024 * 1024,
75 + skipUTF8Validation: false,
76 + perMessageDeflate: false,
77 + handleProtocols: null,
78 + clientTracking: true,
79 + closeTimeout: CLOSE_TIMEOUT,
80 + verifyClient: null,
81 + noServer: false,
82 + backlog: null, // use default (511 as implemented in net.js)
83 + server: null,
84 + host: null,
85 + path: null,
86 + port: null,
87 + WebSocket,
88 + ...options
89 + };
90 +
91 + if (
92 + (options.port == null && !options.server && !options.noServer) ||
93 + (options.port != null && (options.server || options.noServer)) ||
94 + (options.server && options.noServer)
95 + ) {
96 + throw new TypeError(
97 + 'One and only one of the "port", "server", or "noServer" options ' +
98 + 'must be specified'
99 + );
100 + }
101 +
102 + if (options.port != null) {
103 + this._server = http.createServer((req, res) => {
104 + const body = http.STATUS_CODES[426];
105 +
106 + res.writeHead(426, {
107 + 'Content-Length': body.length,
108 + 'Content-Type': 'text/plain'
109 + });
110 + res.end(body);
111 + });
112 + this._server.listen(
113 + options.port,
114 + options.host,
115 + options.backlog,
116 + callback
117 + );
118 + } else if (options.server) {
119 + this._server = options.server;
120 + }
121 +
122 + if (this._server) {
123 + const emitConnection = this.emit.bind(this, 'connection');
124 +
125 + this._removeListeners = addListeners(this._server, {
126 + listening: this.emit.bind(this, 'listening'),
127 + error: this.emit.bind(this, 'error'),
128 + upgrade: (req, socket, head) => {
129 + this.handleUpgrade(req, socket, head, emitConnection);
130 + }
131 + });
132 + }
133 +
134 + if (options.perMessageDeflate === true) options.perMessageDeflate = {};
135 + if (options.clientTracking) {
136 + this.clients = new Set();
137 + this._shouldEmitClose = false;
138 + }
139 +
140 + this.options = options;
141 + this._state = RUNNING;
142 + }
143 +
144 + /**
145 + * Returns the bound address, the address family name, and port of the server
146 + * as reported by the operating system if listening on an IP socket.
147 + * If the server is listening on a pipe or UNIX domain socket, the name is
148 + * returned as a string.
149 + *
150 + * @return {(Object|String|null)} The address of the server
151 + * @public
152 + */
153 + address() {
154 + if (this.options.noServer) {
155 + throw new Error('The server is operating in "noServer" mode');
156 + }
157 +
158 + if (!this._server) return null;
159 + return this._server.address();
160 + }
161 +
162 + /**
163 + * Stop the server from accepting new connections and emit the `'close'` event
164 + * when all existing connections are closed.
165 + *
166 + * @param {Function} [cb] A one-time listener for the `'close'` event
167 + * @public
168 + */
169 + close(cb) {
170 + if (this._state === CLOSED) {
171 + if (cb) {
172 + this.once('close', () => {
173 + cb(new Error('The server is not running'));
174 + });
175 + }
176 +
177 + process.nextTick(emitClose, this);
178 + return;
179 + }
180 +
181 + if (cb) this.once('close', cb);
182 +
183 + if (this._state === CLOSING) return;
184 + this._state = CLOSING;
185 +
186 + if (this.options.noServer || this.options.server) {
187 + if (this._server) {
188 + this._removeListeners();
189 + this._removeListeners = this._server = null;
190 + }
191 +
192 + if (this.clients) {
193 + if (!this.clients.size) {
194 + process.nextTick(emitClose, this);
195 + } else {
196 + this._shouldEmitClose = true;
197 + }
198 + } else {
199 + process.nextTick(emitClose, this);
200 + }
201 + } else {
202 + const server = this._server;
203 +
204 + this._removeListeners();
205 + this._removeListeners = this._server = null;
206 +
207 + //
208 + // The HTTP/S server was created internally. Close it, and rely on its
209 + // `'close'` event.
210 + //
211 + server.close(() => {
212 + emitClose(this);
213 + });
214 + }
215 + }
216 +
217 + /**
218 + * See if a given request should be handled by this server instance.
219 + *
220 + * @param {http.IncomingMessage} req Request object to inspect
221 + * @return {Boolean} `true` if the request is valid, else `false`
222 + * @public
223 + */
224 + shouldHandle(req) {
225 + if (this.options.path) {
226 + const index = req.url.indexOf('?');
227 + const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
228 +
229 + if (pathname !== this.options.path) return false;
230 + }
231 +
232 + return true;
233 + }
234 +
235 + /**
236 + * Handle a HTTP Upgrade request.
237 + *
238 + * @param {http.IncomingMessage} req The request object
239 + * @param {Duplex} socket The network socket between the server and client
240 + * @param {Buffer} head The first packet of the upgraded stream
241 + * @param {Function} cb Callback
242 + * @public
243 + */
244 + handleUpgrade(req, socket, head, cb) {
245 + socket.on('error', socketOnError);
246 +
247 + const key = req.headers['sec-websocket-key'];
248 + const upgrade = req.headers.upgrade;
249 + const version = +req.headers['sec-websocket-version'];
250 +
251 + if (req.method !== 'GET') {
252 + const message = 'Invalid HTTP method';
253 + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
254 + return;
255 + }
256 +
257 + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
258 + const message = 'Invalid Upgrade header';
259 + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
260 + return;
261 + }
262 +
263 + if (key === undefined || !keyRegex.test(key)) {
264 + const message = 'Missing or invalid Sec-WebSocket-Key header';
265 + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
266 + return;
267 + }
268 +
269 + if (version !== 13 && version !== 8) {
270 + const message = 'Missing or invalid Sec-WebSocket-Version header';
271 + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, {
272 + 'Sec-WebSocket-Version': '13, 8'
273 + });
274 + return;
275 + }
276 +
277 + if (!this.shouldHandle(req)) {
278 + abortHandshake(socket, 400);
279 + return;
280 + }
281 +
282 + const secWebSocketProtocol = req.headers['sec-websocket-protocol'];
283 + let protocols = new Set();
284 +
285 + if (secWebSocketProtocol !== undefined) {
286 + try {
287 + protocols = subprotocol.parse(secWebSocketProtocol);
288 + } catch (err) {
289 + const message = 'Invalid Sec-WebSocket-Protocol header';
290 + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
291 + return;
292 + }
293 + }
294 +
295 + const secWebSocketExtensions = req.headers['sec-websocket-extensions'];
296 + const extensions = {};
297 +
298 + if (
299 + this.options.perMessageDeflate &&
300 + secWebSocketExtensions !== undefined
301 + ) {
302 + const perMessageDeflate = new PerMessageDeflate({
303 + ...this.options.perMessageDeflate,
304 + isServer: true,
305 + maxPayload: this.options.maxPayload
306 + });
307 +
308 + try {
309 + const offers = extension.parse(secWebSocketExtensions);
310 +
311 + if (offers[PerMessageDeflate.extensionName]) {
312 + perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
313 + extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
314 + }
315 + } catch (err) {
316 + const message =
317 + 'Invalid or unacceptable Sec-WebSocket-Extensions header';
318 + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
319 + return;
320 + }
321 + }
322 +
323 + //
324 + // Optionally call external client verification handler.
325 + //
326 + if (this.options.verifyClient) {
327 + const info = {
328 + origin:
329 + req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
330 + secure: !!(req.socket.authorized || req.socket.encrypted),
331 + req
332 + };
333 +
334 + if (this.options.verifyClient.length === 2) {
335 + this.options.verifyClient(info, (verified, code, message, headers) => {
336 + if (!verified) {
337 + return abortHandshake(socket, code || 401, message, headers);
338 + }
339 +
340 + this.completeUpgrade(
341 + extensions,
342 + key,
343 + protocols,
344 + req,
345 + socket,
346 + head,
347 + cb
348 + );
349 + });
350 + return;
351 + }
352 +
353 + if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
354 + }
355 +
356 + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
357 + }
358 +
359 + /**
360 + * Upgrade the connection to WebSocket.
361 + *
362 + * @param {Object} extensions The accepted extensions
363 + * @param {String} key The value of the `Sec-WebSocket-Key` header
364 + * @param {Set} protocols The subprotocols
365 + * @param {http.IncomingMessage} req The request object
366 + * @param {Duplex} socket The network socket between the server and client
367 + * @param {Buffer} head The first packet of the upgraded stream
368 + * @param {Function} cb Callback
369 + * @throws {Error} If called more than once with the same socket
370 + * @private
371 + */
372 + completeUpgrade(extensions, key, protocols, req, socket, head, cb) {
373 + //
374 + // Destroy the socket if the client has already sent a FIN packet.
375 + //
376 + if (!socket.readable || !socket.writable) return socket.destroy();
377 +
378 + if (socket[kWebSocket]) {
379 + throw new Error(
380 + 'server.handleUpgrade() was called more than once with the same ' +
381 + 'socket, possibly due to a misconfiguration'
382 + );
383 + }
384 +
385 + if (this._state > RUNNING) return abortHandshake(socket, 503);
386 +
387 + const digest = createHash('sha1')
388 + .update(key + GUID)
389 + .digest('base64');
390 +
391 + const headers = [
392 + 'HTTP/1.1 101 Switching Protocols',
393 + 'Upgrade: websocket',
394 + 'Connection: Upgrade',
395 + `Sec-WebSocket-Accept: ${digest}`
396 + ];
397 +
398 + const ws = new this.options.WebSocket(null, undefined, this.options);
399 +
400 + if (protocols.size) {
401 + //
402 + // Optionally call external protocol selection handler.
403 + //
404 + const protocol = this.options.handleProtocols
405 + ? this.options.handleProtocols(protocols, req)
406 + : protocols.values().next().value;
407 +
408 + if (protocol) {
409 + headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
410 + ws._protocol = protocol;
411 + }
412 + }
413 +
414 + if (extensions[PerMessageDeflate.extensionName]) {
415 + const params = extensions[PerMessageDeflate.extensionName].params;
416 + const value = extension.format({
417 + [PerMessageDeflate.extensionName]: [params]
418 + });
419 + headers.push(`Sec-WebSocket-Extensions: ${value}`);
420 + ws._extensions = extensions;
421 + }
422 +
423 + //
424 + // Allow external modification/inspection of handshake headers.
425 + //
426 + this.emit('headers', headers, req);
427 +
428 + socket.write(headers.concat('\r\n').join('\r\n'));
429 + socket.removeListener('error', socketOnError);
430 +
431 + ws.setSocket(socket, head, {
432 + allowSynchronousEvents: this.options.allowSynchronousEvents,
433 + maxBufferedChunks: this.options.maxBufferedChunks,
434 + maxFragments: this.options.maxFragments,
435 + maxPayload: this.options.maxPayload,
436 + skipUTF8Validation: this.options.skipUTF8Validation
437 + });
438 +
439 + if (this.clients) {
440 + this.clients.add(ws);
441 + ws.on('close', () => {
442 + this.clients.delete(ws);
443 +
444 + if (this._shouldEmitClose && !this.clients.size) {
445 + process.nextTick(emitClose, this);
446 + }
447 + });
448 + }
449 +
450 + cb(ws, req);
451 + }
452 +}
453 +
454 +module.exports = WebSocketServer;
455 +
456 +/**
457 + * Add event listeners on an `EventEmitter` using a map of <event, listener>
458 + * pairs.
459 + *
460 + * @param {EventEmitter} server The event emitter
461 + * @param {Object.<String, Function>} map The listeners to add
462 + * @return {Function} A function that will remove the added listeners when
463 + * called
464 + * @private
465 + */
466 +function addListeners(server, map) {
467 + for (const event of Object.keys(map)) server.on(event, map[event]);
468 +
469 + return function removeListeners() {
470 + for (const event of Object.keys(map)) {
471 + server.removeListener(event, map[event]);
472 + }
473 + };
474 +}
475 +
476 +/**
477 + * Emit a `'close'` event on an `EventEmitter`.
478 + *
479 + * @param {EventEmitter} server The event emitter
480 + * @private
481 + */
482 +function emitClose(server) {
483 + server._state = CLOSED;
484 + server.emit('close');
485 +}
486 +
487 +/**
488 + * Handle socket errors.
489 + *
490 + * @private
491 + */
492 +function socketOnError() {
493 + this.destroy();
494 +}
495 +
496 +/**
497 + * Close the connection when preconditions are not fulfilled.
498 + *
499 + * @param {Duplex} socket The socket of the upgrade request
500 + * @param {Number} code The HTTP response status code
501 + * @param {String} [message] The HTTP response body
502 + * @param {Object} [headers] Additional HTTP response headers
503 + * @private
504 + */
505 +function abortHandshake(socket, code, message, headers) {
506 + //
507 + // The socket is writable unless the user destroyed or ended it before calling
508 + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user
509 + // error. Handling this does not make much sense as the worst that can happen
510 + // is that some of the data written by the user might be discarded due to the
511 + // call to `socket.end()` below, which triggers an `'error'` event that in
512 + // turn causes the socket to be destroyed.
513 + //
514 + message = message || http.STATUS_CODES[code];
515 + headers = {
516 + Connection: 'close',
517 + 'Content-Type': 'text/html',
518 + 'Content-Length': Buffer.byteLength(message),
519 + ...headers
520 + };
521 +
522 + socket.once('finish', socket.destroy);
523 +
524 + socket.end(
525 + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
526 + Object.keys(headers)
527 + .map((h) => `${h}: ${headers[h]}`)
528 + .join('\r\n') +
529 + '\r\n\r\n' +
530 + message
531 + );
532 +}
533 +
534 +/**
535 + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least
536 + * one listener for it, otherwise call `abortHandshake()`.
537 + *
538 + * @param {WebSocketServer} server The WebSocket server
539 + * @param {http.IncomingMessage} req The request object
540 + * @param {Duplex} socket The socket of the upgrade request
541 + * @param {Number} code The HTTP response status code
542 + * @param {String} message The HTTP response body
543 + * @param {Object} [headers] The HTTP response headers
544 + * @private
545 + */
546 +function abortHandshakeOrEmitwsClientError(
547 + server,
548 + req,
549 + socket,
550 + code,
551 + message,
552 + headers
553 +) {
554 + if (server.listenerCount('wsClientError')) {
555 + const err = new Error(message);
556 + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
557 +
558 + server.emit('wsClientError', err, socket, req);
559 + } else {
560 + abortHandshake(socket, code, message, headers);
561 + }
562 +}
added node_modules/ws/lib/websocket.js +1407 −0
@@ -0,0 +1,1407 @@
1 +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$", "caughtErrors": "none" }] */
2 +
3 +'use strict';
4 +
5 +const EventEmitter = require('events');
6 +const https = require('https');
7 +const http = require('http');
8 +const net = require('net');
9 +const tls = require('tls');
10 +const { randomBytes, createHash } = require('crypto');
11 +const { Duplex, Readable } = require('stream');
12 +const { URL } = require('url');
13 +
14 +const PerMessageDeflate = require('./permessage-deflate');
15 +const Receiver = require('./receiver');
16 +const Sender = require('./sender');
17 +const { isBlob } = require('./validation');
18 +
19 +const {
20 + BINARY_TYPES,
21 + CLOSE_TIMEOUT,
22 + EMPTY_BUFFER,
23 + GUID,
24 + kForOnEventAttribute,
25 + kListener,
26 + kStatusCode,
27 + kWebSocket,
28 + NOOP
29 +} = require('./constants');
30 +const {
31 + EventTarget: { addEventListener, removeEventListener }
32 +} = require('./event-target');
33 +const { format, parse } = require('./extension');
34 +const { toBuffer } = require('./buffer-util');
35 +
36 +const kAborted = Symbol('kAborted');
37 +const protocolVersions = [8, 13];
38 +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
39 +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
40 +
41 +/**
42 + * Class representing a WebSocket.
43 + *
44 + * @extends EventEmitter
45 + */
46 +class WebSocket extends EventEmitter {
47 + /**
48 + * Create a new `WebSocket`.
49 + *
50 + * @param {(String|URL)} address The URL to which to connect
51 + * @param {(String|String[])} [protocols] The subprotocols
52 + * @param {Object} [options] Connection options
53 + */
54 + constructor(address, protocols, options) {
55 + super();
56 +
57 + this._binaryType = BINARY_TYPES[0];
58 + this._closeCode = 1006;
59 + this._closeFrameReceived = false;
60 + this._closeFrameSent = false;
61 + this._closeMessage = EMPTY_BUFFER;
62 + this._closeTimer = null;
63 + this._errorEmitted = false;
64 + this._extensions = {};
65 + this._paused = false;
66 + this._protocol = '';
67 + this._readyState = WebSocket.CONNECTING;
68 + this._receiver = null;
69 + this._sender = null;
70 + this._socket = null;
71 +
72 + if (address !== null) {
73 + this._bufferedAmount = 0;
74 + this._isServer = false;
75 + this._redirects = 0;
76 +
77 + if (protocols === undefined) {
78 + protocols = [];
79 + } else if (!Array.isArray(protocols)) {
80 + if (typeof protocols === 'object' && protocols !== null) {
81 + options = protocols;
82 + protocols = [];
83 + } else {
84 + protocols = [protocols];
85 + }
86 + }
87 +
88 + initAsClient(this, address, protocols, options);
89 + } else {
90 + this._autoPong = options.autoPong;
91 + this._closeTimeout = options.closeTimeout;
92 + this._isServer = true;
93 + }
94 + }
95 +
96 + /**
97 + * For historical reasons, the custom "nodebuffer" type is used by the default
98 + * instead of "blob".
99 + *
100 + * @type {String}
101 + */
102 + get binaryType() {
103 + return this._binaryType;
104 + }
105 +
106 + set binaryType(type) {
107 + if (!BINARY_TYPES.includes(type)) return;
108 +
109 + this._binaryType = type;
110 +
111 + //
112 + // Allow to change `binaryType` on the fly.
113 + //
114 + if (this._receiver) this._receiver._binaryType = type;
115 + }
116 +
117 + /**
118 + * @type {Number}
119 + */
120 + get bufferedAmount() {
121 + if (!this._socket) return this._bufferedAmount;
122 +
123 + return this._socket._writableState.length + this._sender._bufferedBytes;
124 + }
125 +
126 + /**
127 + * @type {String}
128 + */
129 + get extensions() {
130 + return Object.keys(this._extensions).join();
131 + }
132 +
133 + /**
134 + * @type {Boolean}
135 + */
136 + get isPaused() {
137 + return this._paused;
138 + }
139 +
140 + /**
141 + * @type {Function}
142 + */
143 + /* istanbul ignore next */
144 + get onclose() {
145 + return null;
146 + }
147 +
148 + /**
149 + * @type {Function}
150 + */
151 + /* istanbul ignore next */
152 + get onerror() {
153 + return null;
154 + }
155 +
156 + /**
157 + * @type {Function}
158 + */
159 + /* istanbul ignore next */
160 + get onopen() {
161 + return null;
162 + }
163 +
164 + /**
165 + * @type {Function}
166 + */
167 + /* istanbul ignore next */
168 + get onmessage() {
169 + return null;
170 + }
171 +
172 + /**
173 + * @type {String}
174 + */
175 + get protocol() {
176 + return this._protocol;
177 + }
178 +
179 + /**
180 + * @type {Number}
181 + */
182 + get readyState() {
183 + return this._readyState;
184 + }
185 +
186 + /**
187 + * @type {String}
188 + */
189 + get url() {
190 + return this._url;
191 + }
192 +
193 + /**
194 + * Set up the socket and the internal resources.
195 + *
196 + * @param {Duplex} socket The network socket between the server and client
197 + * @param {Buffer} head The first packet of the upgraded stream
198 + * @param {Object} options Options object
199 + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether
200 + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
201 + * multiple times in the same tick
202 + * @param {Function} [options.generateMask] The function used to generate the
203 + * masking key
204 + * @param {Number} [options.maxBufferedChunks=0] The maximum number of
205 + * buffered data chunks
206 + * @param {Number} [options.maxFragments=0] The maximum number of message
207 + * fragments
208 + * @param {Number} [options.maxPayload=0] The maximum allowed message size
209 + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
210 + * not to skip UTF-8 validation for text and close messages
211 + * @private
212 + */
213 + setSocket(socket, head, options) {
214 + const receiver = new Receiver({
215 + allowSynchronousEvents: options.allowSynchronousEvents,
216 + binaryType: this.binaryType,
217 + extensions: this._extensions,
218 + isServer: this._isServer,
219 + maxBufferedChunks: options.maxBufferedChunks,
220 + maxFragments: options.maxFragments,
221 + maxPayload: options.maxPayload,
222 + skipUTF8Validation: options.skipUTF8Validation
223 + });
224 +
225 + const sender = new Sender(socket, this._extensions, options.generateMask);
226 +
227 + this._receiver = receiver;
228 + this._sender = sender;
229 + this._socket = socket;
230 +
231 + receiver[kWebSocket] = this;
232 + sender[kWebSocket] = this;
233 + socket[kWebSocket] = this;
234 +
235 + receiver.on('conclude', receiverOnConclude);
236 + receiver.on('drain', receiverOnDrain);
237 + receiver.on('error', receiverOnError);
238 + receiver.on('message', receiverOnMessage);
239 + receiver.on('ping', receiverOnPing);
240 + receiver.on('pong', receiverOnPong);
241 +
242 + sender.onerror = senderOnError;
243 +
244 + //
245 + // These methods may not be available if `socket` is just a `Duplex`.
246 + //
247 + if (socket.setTimeout) socket.setTimeout(0);
248 + if (socket.setNoDelay) socket.setNoDelay();
249 +
250 + if (head.length > 0) socket.unshift(head);
251 +
252 + socket.on('close', socketOnClose);
253 + socket.on('data', socketOnData);
254 + socket.on('end', socketOnEnd);
255 + socket.on('error', socketOnError);
256 +
257 + this._readyState = WebSocket.OPEN;
258 + this.emit('open');
259 + }
260 +
261 + /**
262 + * Emit the `'close'` event.
263 + *
264 + * @private
265 + */
266 + emitClose() {
267 + if (!this._socket) {
268 + this._readyState = WebSocket.CLOSED;
269 + this.emit('close', this._closeCode, this._closeMessage);
270 + return;
271 + }
272 +
273 + if (this._extensions[PerMessageDeflate.extensionName]) {
274 + this._extensions[PerMessageDeflate.extensionName].cleanup();
275 + }
276 +
277 + this._receiver.removeAllListeners();
278 + this._readyState = WebSocket.CLOSED;
279 + this.emit('close', this._closeCode, this._closeMessage);
280 + }
281 +
282 + /**
283 + * Start a closing handshake.
284 + *
285 + * +----------+ +-----------+ +----------+
286 + * - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
287 + * | +----------+ +-----------+ +----------+ |
288 + * +----------+ +-----------+ |
289 + * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING
290 + * +----------+ +-----------+ |
291 + * | | | +---+ |
292 + * +------------------------+-->|fin| - - - -
293 + * | +---+ | +---+
294 + * - - - - -|fin|<---------------------+
295 + * +---+
296 + *
297 + * @param {Number} [code] Status code explaining why the connection is closing
298 + * @param {(String|Buffer)} [data] The reason why the connection is
299 + * closing
300 + * @public
301 + */
302 + close(code, data) {
303 + if (this.readyState === WebSocket.CLOSED) return;
304 + if (this.readyState === WebSocket.CONNECTING) {
305 + const msg = 'WebSocket was closed before the connection was established';
306 + abortHandshake(this, this._req, msg);
307 + return;
308 + }
309 +
310 + if (this.readyState === WebSocket.CLOSING) {
311 + if (
312 + this._closeFrameSent &&
313 + (this._closeFrameReceived || this._receiver._writableState.errorEmitted)
314 + ) {
315 + this._socket.end();
316 + }
317 +
318 + return;
319 + }
320 +
321 + this._readyState = WebSocket.CLOSING;
322 + this._sender.close(code, data, !this._isServer, (err) => {
323 + //
324 + // This error is handled by the `'error'` listener on the socket. We only
325 + // want to know if the close frame has been sent here.
326 + //
327 + if (err) return;
328 +
329 + this._closeFrameSent = true;
330 +
331 + if (
332 + this._closeFrameReceived ||
333 + this._receiver._writableState.errorEmitted
334 + ) {
335 + this._socket.end();
336 + }
337 + });
338 +
339 + setCloseTimer(this);
340 + }
341 +
342 + /**
343 + * Pause the socket.
344 + *
345 + * @public
346 + */
347 + pause() {
348 + if (
349 + this.readyState === WebSocket.CONNECTING ||
350 + this.readyState === WebSocket.CLOSED
351 + ) {
352 + return;
353 + }
354 +
355 + this._paused = true;
356 + this._socket.pause();
357 + }
358 +
359 + /**
360 + * Send a ping.
361 + *
362 + * @param {*} [data] The data to send
363 + * @param {Boolean} [mask] Indicates whether or not to mask `data`
364 + * @param {Function} [cb] Callback which is executed when the ping is sent
365 + * @public
366 + */
367 + ping(data, mask, cb) {
368 + if (this.readyState === WebSocket.CONNECTING) {
369 + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
370 + }
371 +
372 + if (typeof data === 'function') {
373 + cb = data;
374 + data = mask = undefined;
375 + } else if (typeof mask === 'function') {
376 + cb = mask;
377 + mask = undefined;
378 + }
379 +
380 + if (typeof data === 'number') data = data.toString();
381 +
382 + if (this.readyState !== WebSocket.OPEN) {
383 + sendAfterClose(this, data, cb);
384 + return;
385 + }
386 +
387 + if (mask === undefined) mask = !this._isServer;
388 + this._sender.ping(data || EMPTY_BUFFER, mask, cb);
389 + }
390 +
391 + /**
392 + * Send a pong.
393 + *
394 + * @param {*} [data] The data to send
395 + * @param {Boolean} [mask] Indicates whether or not to mask `data`
396 + * @param {Function} [cb] Callback which is executed when the pong is sent
397 + * @public
398 + */
399 + pong(data, mask, cb) {
400 + if (this.readyState === WebSocket.CONNECTING) {
401 + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
402 + }
403 +
404 + if (typeof data === 'function') {
405 + cb = data;
406 + data = mask = undefined;
407 + } else if (typeof mask === 'function') {
408 + cb = mask;
409 + mask = undefined;
410 + }
411 +
412 + if (typeof data === 'number') data = data.toString();
413 +
414 + if (this.readyState !== WebSocket.OPEN) {
415 + sendAfterClose(this, data, cb);
416 + return;
417 + }
418 +
419 + if (mask === undefined) mask = !this._isServer;
420 + this._sender.pong(data || EMPTY_BUFFER, mask, cb);
421 + }
422 +
423 + /**
424 + * Resume the socket.
425 + *
426 + * @public
427 + */
428 + resume() {
429 + if (
430 + this.readyState === WebSocket.CONNECTING ||
431 + this.readyState === WebSocket.CLOSED
432 + ) {
433 + return;
434 + }
435 +
436 + this._paused = false;
437 + if (!this._receiver._writableState.needDrain) this._socket.resume();
438 + }
439 +
440 + /**
441 + * Send a data message.
442 + *
443 + * @param {*} data The message to send
444 + * @param {Object} [options] Options object
445 + * @param {Boolean} [options.binary] Specifies whether `data` is binary or
446 + * text
447 + * @param {Boolean} [options.compress] Specifies whether or not to compress
448 + * `data`
449 + * @param {Boolean} [options.fin=true] Specifies whether the fragment is the
450 + * last one
451 + * @param {Boolean} [options.mask] Specifies whether or not to mask `data`
452 + * @param {Function} [cb] Callback which is executed when data is written out
453 + * @public
454 + */
455 + send(data, options, cb) {
456 + if (this.readyState === WebSocket.CONNECTING) {
457 + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
458 + }
459 +
460 + if (typeof options === 'function') {
461 + cb = options;
462 + options = {};
463 + }
464 +
465 + if (typeof data === 'number') data = data.toString();
466 +
467 + if (this.readyState !== WebSocket.OPEN) {
468 + sendAfterClose(this, data, cb);
469 + return;
470 + }
471 +
472 + const opts = {
473 + binary: typeof data !== 'string',
474 + mask: !this._isServer,
475 + compress: true,
476 + fin: true,
477 + ...options
478 + };
479 +
480 + if (!this._extensions[PerMessageDeflate.extensionName]) {
481 + opts.compress = false;
482 + }
483 +
484 + this._sender.send(data || EMPTY_BUFFER, opts, cb);
485 + }
486 +
487 + /**
488 + * Forcibly close the connection.
489 + *
490 + * @public
491 + */
492 + terminate() {
493 + if (this.readyState === WebSocket.CLOSED) return;
494 + if (this.readyState === WebSocket.CONNECTING) {
495 + const msg = 'WebSocket was closed before the connection was established';
496 + abortHandshake(this, this._req, msg);
497 + return;
498 + }
499 +
500 + if (this._socket) {
501 + this._readyState = WebSocket.CLOSING;
502 + this._socket.destroy();
503 + }
504 + }
505 +}
506 +
507 +/**
508 + * @constant {Number} CONNECTING
509 + * @memberof WebSocket
510 + */
511 +Object.defineProperty(WebSocket, 'CONNECTING', {
512 + enumerable: true,
513 + value: readyStates.indexOf('CONNECTING')
514 +});
515 +
516 +/**
517 + * @constant {Number} CONNECTING
518 + * @memberof WebSocket.prototype
519 + */
520 +Object.defineProperty(WebSocket.prototype, 'CONNECTING', {
521 + enumerable: true,
522 + value: readyStates.indexOf('CONNECTING')
523 +});
524 +
525 +/**
526 + * @constant {Number} OPEN
527 + * @memberof WebSocket
528 + */
529 +Object.defineProperty(WebSocket, 'OPEN', {
530 + enumerable: true,
531 + value: readyStates.indexOf('OPEN')
532 +});
533 +
534 +/**
535 + * @constant {Number} OPEN
536 + * @memberof WebSocket.prototype
537 + */
538 +Object.defineProperty(WebSocket.prototype, 'OPEN', {
539 + enumerable: true,
540 + value: readyStates.indexOf('OPEN')
541 +});
542 +
543 +/**
544 + * @constant {Number} CLOSING
545 + * @memberof WebSocket
546 + */
547 +Object.defineProperty(WebSocket, 'CLOSING', {
548 + enumerable: true,
549 + value: readyStates.indexOf('CLOSING')
550 +});
551 +
552 +/**
553 + * @constant {Number} CLOSING
554 + * @memberof WebSocket.prototype
555 + */
556 +Object.defineProperty(WebSocket.prototype, 'CLOSING', {
557 + enumerable: true,
558 + value: readyStates.indexOf('CLOSING')
559 +});
560 +
561 +/**
562 + * @constant {Number} CLOSED
563 + * @memberof WebSocket
564 + */
565 +Object.defineProperty(WebSocket, 'CLOSED', {
566 + enumerable: true,
567 + value: readyStates.indexOf('CLOSED')
568 +});
569 +
570 +/**
571 + * @constant {Number} CLOSED
572 + * @memberof WebSocket.prototype
573 + */
574 +Object.defineProperty(WebSocket.prototype, 'CLOSED', {
575 + enumerable: true,
576 + value: readyStates.indexOf('CLOSED')
577 +});
578 +
579 +[
580 + 'binaryType',
581 + 'bufferedAmount',
582 + 'extensions',
583 + 'isPaused',
584 + 'protocol',
585 + 'readyState',
586 + 'url'
587 +].forEach((property) => {
588 + Object.defineProperty(WebSocket.prototype, property, { enumerable: true });
589 +});
590 +
591 +//
592 +// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes.
593 +// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface
594 +//
595 +['open', 'error', 'close', 'message'].forEach((method) => {
596 + Object.defineProperty(WebSocket.prototype, `on${method}`, {
597 + enumerable: true,
598 + get() {
599 + for (const listener of this.listeners(method)) {
600 + if (listener[kForOnEventAttribute]) return listener[kListener];
601 + }
602 +
603 + return null;
604 + },
605 + set(handler) {
606 + for (const listener of this.listeners(method)) {
607 + if (listener[kForOnEventAttribute]) {
608 + this.removeListener(method, listener);
609 + break;
610 + }
611 + }
612 +
613 + if (typeof handler !== 'function') return;
614 +
615 + this.addEventListener(method, handler, {
616 + [kForOnEventAttribute]: true
617 + });
618 + }
619 + });
620 +});
621 +
622 +WebSocket.prototype.addEventListener = addEventListener;
623 +WebSocket.prototype.removeEventListener = removeEventListener;
624 +
625 +module.exports = WebSocket;
626 +
627 +/**
628 + * Initialize a WebSocket client.
629 + *
630 + * @param {WebSocket} websocket The client to initialize
631 + * @param {(String|URL)} address The URL to which to connect
632 + * @param {Array} protocols The subprotocols
633 + * @param {Object} [options] Connection options
634 + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether any
635 + * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple
636 + * times in the same tick
637 + * @param {Boolean} [options.autoPong=true] Specifies whether or not to
638 + * automatically send a pong in response to a ping
639 + * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to wait
640 + * for the closing handshake to finish after `websocket.close()` is called
641 + * @param {Function} [options.finishRequest] A function which can be used to
642 + * customize the headers of each http request before it is sent
643 + * @param {Boolean} [options.followRedirects=false] Whether or not to follow
644 + * redirects
645 + * @param {Function} [options.generateMask] The function used to generate the
646 + * masking key
647 + * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
648 + * handshake request
649 + * @param {Number} [options.maxBufferedChunks=262144] The maximum number of
650 + * buffered data chunks
651 + * @param {Number} [options.maxFragments=16384] The maximum number of message
652 + * fragments
653 + * @param {Number} [options.maxPayload=104857600] The maximum allowed message
654 + * size
655 + * @param {Number} [options.maxRedirects=10] The maximum number of redirects
656 + * allowed
657 + * @param {String} [options.origin] Value of the `Origin` or
658 + * `Sec-WebSocket-Origin` header
659 + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable
660 + * permessage-deflate
661 + * @param {Number} [options.protocolVersion=13] Value of the
662 + * `Sec-WebSocket-Version` header
663 + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
664 + * not to skip UTF-8 validation for text and close messages
665 + * @private
666 + */
667 +function initAsClient(websocket, address, protocols, options) {
668 + const opts = {
669 + allowSynchronousEvents: true,
670 + autoPong: true,
671 + closeTimeout: CLOSE_TIMEOUT,
672 + protocolVersion: protocolVersions[1],
673 + maxBufferedChunks: 256 * 1024,
674 + maxFragments: 16 * 1024,
675 + maxPayload: 100 * 1024 * 1024,
676 + skipUTF8Validation: false,
677 + perMessageDeflate: true,
678 + followRedirects: false,
679 + maxRedirects: 10,
680 + ...options,
681 + socketPath: undefined,
682 + hostname: undefined,
683 + protocol: undefined,
684 + timeout: undefined,
685 + method: 'GET',
686 + host: undefined,
687 + path: undefined,
688 + port: undefined
689 + };
690 +
691 + websocket._autoPong = opts.autoPong;
692 + websocket._closeTimeout = opts.closeTimeout;
693 +
694 + if (!protocolVersions.includes(opts.protocolVersion)) {
695 + throw new RangeError(
696 + `Unsupported protocol version: ${opts.protocolVersion} ` +
697 + `(supported versions: ${protocolVersions.join(', ')})`
698 + );
699 + }
700 +
701 + let parsedUrl;
702 +
703 + if (address instanceof URL) {
704 + parsedUrl = address;
705 + } else {
706 + try {
707 + parsedUrl = new URL(address);
708 + } catch {
709 + throw new SyntaxError(`Invalid URL: ${address}`);
710 + }
711 + }
712 +
713 + if (parsedUrl.protocol === 'http:') {
714 + parsedUrl.protocol = 'ws:';
715 + } else if (parsedUrl.protocol === 'https:') {
716 + parsedUrl.protocol = 'wss:';
717 + }
718 +
719 + websocket._url = parsedUrl.href;
720 +
721 + const isSecure = parsedUrl.protocol === 'wss:';
722 + const isIpcUrl = parsedUrl.protocol === 'ws+unix:';
723 + let invalidUrlMessage;
724 +
725 + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) {
726 + invalidUrlMessage =
727 + 'The URL\'s protocol must be one of "ws:", "wss:", ' +
728 + '"http:", "https:", or "ws+unix:"';
729 + } else if (isIpcUrl && !parsedUrl.pathname) {
730 + invalidUrlMessage = "The URL's pathname is empty";
731 + } else if (parsedUrl.hash) {
732 + invalidUrlMessage = 'The URL contains a fragment identifier';
733 + }
734 +
735 + if (invalidUrlMessage) {
736 + const err = new SyntaxError(invalidUrlMessage);
737 +
738 + if (websocket._redirects === 0) {
739 + throw err;
740 + } else {
741 + emitErrorAndClose(websocket, err);
742 + return;
743 + }
744 + }
745 +
746 + const defaultPort = isSecure ? 443 : 80;
747 + const key = randomBytes(16).toString('base64');
748 + const request = isSecure ? https.request : http.request;
749 + const protocolSet = new Set();
750 + let perMessageDeflate;
751 +
752 + opts.createConnection =
753 + opts.createConnection || (isSecure ? tlsConnect : netConnect);
754 + opts.defaultPort = opts.defaultPort || defaultPort;
755 + opts.port = parsedUrl.port || defaultPort;
756 + opts.host = parsedUrl.hostname.startsWith('[')
757 + ? parsedUrl.hostname.slice(1, -1)
758 + : parsedUrl.hostname;
759 + opts.headers = {
760 + ...opts.headers,
761 + 'Sec-WebSocket-Version': opts.protocolVersion,
762 + 'Sec-WebSocket-Key': key,
763 + Connection: 'Upgrade',
764 + Upgrade: 'websocket'
765 + };
766 + opts.path = parsedUrl.pathname + parsedUrl.search;
767 + opts.timeout = opts.handshakeTimeout;
768 +
769 + if (opts.perMessageDeflate) {
770 + perMessageDeflate = new PerMessageDeflate({
771 + ...opts.perMessageDeflate,
772 + isServer: false,
773 + maxPayload: opts.maxPayload
774 + });
775 + opts.headers['Sec-WebSocket-Extensions'] = format({
776 + [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
777 + });
778 + }
779 + if (protocols.length) {
780 + for (const protocol of protocols) {
781 + if (
782 + typeof protocol !== 'string' ||
783 + !subprotocolRegex.test(protocol) ||
784 + protocolSet.has(protocol)
785 + ) {
786 + throw new SyntaxError(
787 + 'An invalid or duplicated subprotocol was specified'
788 + );
789 + }
790 +
791 + protocolSet.add(protocol);
792 + }
793 +
794 + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(',');
795 + }
796 + if (opts.origin) {
797 + if (opts.protocolVersion < 13) {
798 + opts.headers['Sec-WebSocket-Origin'] = opts.origin;
799 + } else {
800 + opts.headers.Origin = opts.origin;
801 + }
802 + }
803 + if (parsedUrl.username || parsedUrl.password) {
804 + opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
805 + }
806 +
807 + if (isIpcUrl) {
808 + const parts = opts.path.split(':');
809 +
810 + opts.socketPath = parts[0];
811 + opts.path = parts[1];
812 + }
813 +
814 + let req;
815 +
816 + if (opts.followRedirects) {
817 + if (websocket._redirects === 0) {
818 + websocket._originalIpc = isIpcUrl;
819 + websocket._originalSecure = isSecure;
820 + websocket._originalHostOrSocketPath = isIpcUrl
821 + ? opts.socketPath
822 + : parsedUrl.host;
823 +
824 + const headers = options && options.headers;
825 +
826 + //
827 + // Shallow copy the user provided options so that headers can be changed
828 + // without mutating the original object.
829 + //
830 + options = { ...options, headers: {} };
831 +
832 + if (headers) {
833 + for (const [key, value] of Object.entries(headers)) {
834 + options.headers[key.toLowerCase()] = value;
835 + }
836 + }
837 + } else if (websocket.listenerCount('redirect') === 0) {
838 + const isSameHost = isIpcUrl
839 + ? websocket._originalIpc
840 + ? opts.socketPath === websocket._originalHostOrSocketPath
841 + : false
842 + : websocket._originalIpc
843 + ? false
844 + : parsedUrl.host === websocket._originalHostOrSocketPath;
845 +
846 + if (!isSameHost || (websocket._originalSecure && !isSecure)) {
847 + //
848 + // Match curl 7.77.0 behavior and drop the following headers. These
849 + // headers are also dropped when following a redirect to a subdomain.
850 + //
851 + delete opts.headers.authorization;
852 + delete opts.headers.cookie;
853 +
854 + if (!isSameHost) delete opts.headers.host;
855 +
856 + opts.auth = undefined;
857 + }
858 + }
859 +
860 + //
861 + // Match curl 7.77.0 behavior and make the first `Authorization` header win.
862 + // If the `Authorization` header is set, then there is nothing to do as it
863 + // will take precedence.
864 + //
865 + if (opts.auth && !options.headers.authorization) {
866 + options.headers.authorization =
867 + 'Basic ' + Buffer.from(opts.auth).toString('base64');
868 + }
869 +
870 + req = websocket._req = request(opts);
871 +
872 + if (websocket._redirects) {
873 + //
874 + // Unlike what is done for the `'upgrade'` event, no early exit is
875 + // triggered here if the user calls `websocket.close()` or
876 + // `websocket.terminate()` from a listener of the `'redirect'` event. This
877 + // is because the user can also call `request.destroy()` with an error
878 + // before calling `websocket.close()` or `websocket.terminate()` and this
879 + // would result in an error being emitted on the `request` object with no
880 + // `'error'` event listeners attached.
881 + //
882 + websocket.emit('redirect', websocket.url, req);
883 + }
884 + } else {
885 + req = websocket._req = request(opts);
886 + }
887 +
888 + if (opts.timeout) {
889 + req.on('timeout', () => {
890 + abortHandshake(websocket, req, 'Opening handshake has timed out');
891 + });
892 + }
893 +
894 + req.on('error', (err) => {
895 + if (req === null || req[kAborted]) return;
896 +
897 + req = websocket._req = null;
898 + emitErrorAndClose(websocket, err);
899 + });
900 +
901 + req.on('response', (res) => {
902 + const location = res.headers.location;
903 + const statusCode = res.statusCode;
904 +
905 + if (
906 + location &&
907 + opts.followRedirects &&
908 + statusCode >= 300 &&
909 + statusCode < 400
910 + ) {
911 + if (++websocket._redirects > opts.maxRedirects) {
912 + abortHandshake(websocket, req, 'Maximum redirects exceeded');
913 + return;
914 + }
915 +
916 + req.abort();
917 +
918 + let addr;
919 +
920 + try {
921 + addr = new URL(location, address);
922 + } catch (e) {
923 + const err = new SyntaxError(`Invalid URL: ${location}`);
924 + emitErrorAndClose(websocket, err);
925 + return;
926 + }
927 +
928 + initAsClient(websocket, addr, protocols, options);
929 + } else if (!websocket.emit('unexpected-response', req, res)) {
930 + abortHandshake(
931 + websocket,
932 + req,
933 + `Unexpected server response: ${res.statusCode}`
934 + );
935 + }
936 + });
937 +
938 + req.on('upgrade', (res, socket, head) => {
939 + websocket.emit('upgrade', res);
940 +
941 + //
942 + // The user may have closed the connection from a listener of the
943 + // `'upgrade'` event.
944 + //
945 + if (websocket.readyState !== WebSocket.CONNECTING) return;
946 +
947 + req = websocket._req = null;
948 +
949 + const upgrade = res.headers.upgrade;
950 +
951 + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
952 + abortHandshake(websocket, socket, 'Invalid Upgrade header');
953 + return;
954 + }
955 +
956 + const digest = createHash('sha1')
957 + .update(key + GUID)
958 + .digest('base64');
959 +
960 + if (res.headers['sec-websocket-accept'] !== digest) {
961 + abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header');
962 + return;
963 + }
964 +
965 + const serverProt = res.headers['sec-websocket-protocol'];
966 + let protError;
967 +
968 + if (serverProt !== undefined) {
969 + if (!protocolSet.size) {
970 + protError = 'Server sent a subprotocol but none was requested';
971 + } else if (!protocolSet.has(serverProt)) {
972 + protError = 'Server sent an invalid subprotocol';
973 + }
974 + } else if (protocolSet.size) {
975 + protError = 'Server sent no subprotocol';
976 + }
977 +
978 + if (protError) {
979 + abortHandshake(websocket, socket, protError);
980 + return;
981 + }
982 +
983 + if (serverProt) websocket._protocol = serverProt;
984 +
985 + const secWebSocketExtensions = res.headers['sec-websocket-extensions'];
986 +
987 + if (secWebSocketExtensions !== undefined) {
988 + if (!perMessageDeflate) {
989 + const message =
990 + 'Server sent a Sec-WebSocket-Extensions header but no extension ' +
991 + 'was requested';
992 + abortHandshake(websocket, socket, message);
993 + return;
994 + }
995 +
996 + let extensions;
997 +
998 + try {
999 + extensions = parse(secWebSocketExtensions);
1000 + } catch (err) {
1001 + const message = 'Invalid Sec-WebSocket-Extensions header';
1002 + abortHandshake(websocket, socket, message);
1003 + return;
1004 + }
1005 +
1006 + const extensionNames = Object.keys(extensions);
1007 +
1008 + if (
1009 + extensionNames.length !== 1 ||
1010 + extensionNames[0] !== PerMessageDeflate.extensionName
1011 + ) {
1012 + const message = 'Server indicated an extension that was not requested';
1013 + abortHandshake(websocket, socket, message);
1014 + return;
1015 + }
1016 +
1017 + try {
1018 + perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
1019 + } catch (err) {
1020 + const message = 'Invalid Sec-WebSocket-Extensions header';
1021 + abortHandshake(websocket, socket, message);
1022 + return;
1023 + }
1024 +
1025 + websocket._extensions[PerMessageDeflate.extensionName] =
1026 + perMessageDeflate;
1027 + }
1028 +
1029 + websocket.setSocket(socket, head, {
1030 + allowSynchronousEvents: opts.allowSynchronousEvents,
1031 + generateMask: opts.generateMask,
1032 + maxBufferedChunks: opts.maxBufferedChunks,
1033 + maxFragments: opts.maxFragments,
1034 + maxPayload: opts.maxPayload,
1035 + skipUTF8Validation: opts.skipUTF8Validation
1036 + });
1037 + });
1038 +
1039 + if (opts.finishRequest) {
1040 + opts.finishRequest(req, websocket);
1041 + } else {
1042 + req.end();
1043 + }
1044 +}
1045 +
1046 +/**
1047 + * Emit the `'error'` and `'close'` events.
1048 + *
1049 + * @param {WebSocket} websocket The WebSocket instance
1050 + * @param {Error} The error to emit
1051 + * @private
1052 + */
1053 +function emitErrorAndClose(websocket, err) {
1054 + websocket._readyState = WebSocket.CLOSING;
1055 + //
1056 + // The following assignment is practically useless and is done only for
1057 + // consistency.
1058 + //
1059 + websocket._errorEmitted = true;
1060 + websocket.emit('error', err);
1061 + websocket.emitClose();
1062 +}
1063 +
1064 +/**
1065 + * Create a `net.Socket` and initiate a connection.
1066 + *
1067 + * @param {Object} options Connection options
1068 + * @return {net.Socket} The newly created socket used to start the connection
1069 + * @private
1070 + */
1071 +function netConnect(options) {
1072 + options.path = options.socketPath;
1073 + return net.connect(options);
1074 +}
1075 +
1076 +/**
1077 + * Create a `tls.TLSSocket` and initiate a connection.
1078 + *
1079 + * @param {Object} options Connection options
1080 + * @return {tls.TLSSocket} The newly created socket used to start the connection
1081 + * @private
1082 + */
1083 +function tlsConnect(options) {
1084 + options.path = undefined;
1085 +
1086 + if (!options.servername && options.servername !== '') {
1087 + options.servername = net.isIP(options.host) ? '' : options.host;
1088 + }
1089 +
1090 + return tls.connect(options);
1091 +}
1092 +
1093 +/**
1094 + * Abort the handshake and emit an error.
1095 + *
1096 + * @param {WebSocket} websocket The WebSocket instance
1097 + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to
1098 + * abort or the socket to destroy
1099 + * @param {String} message The error message
1100 + * @private
1101 + */
1102 +function abortHandshake(websocket, stream, message) {
1103 + websocket._readyState = WebSocket.CLOSING;
1104 +
1105 + const err = new Error(message);
1106 + Error.captureStackTrace(err, abortHandshake);
1107 +
1108 + if (stream.setHeader) {
1109 + stream[kAborted] = true;
1110 + stream.abort();
1111 +
1112 + if (stream.socket && !stream.socket.destroyed) {
1113 + //
1114 + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if
1115 + // called after the request completed. See
1116 + // https://github.com/websockets/ws/issues/1869.
1117 + //
1118 + stream.socket.destroy();
1119 + }
1120 +
1121 + process.nextTick(emitErrorAndClose, websocket, err);
1122 + } else {
1123 + stream.destroy(err);
1124 + stream.once('error', websocket.emit.bind(websocket, 'error'));
1125 + stream.once('close', websocket.emitClose.bind(websocket));
1126 + }
1127 +}
1128 +
1129 +/**
1130 + * Handle cases where the `ping()`, `pong()`, or `send()` methods are called
1131 + * when the `readyState` attribute is `CLOSING` or `CLOSED`.
1132 + *
1133 + * @param {WebSocket} websocket The WebSocket instance
1134 + * @param {*} [data] The data to send
1135 + * @param {Function} [cb] Callback
1136 + * @private
1137 + */
1138 +function sendAfterClose(websocket, data, cb) {
1139 + if (data) {
1140 + const length = isBlob(data) ? data.size : toBuffer(data).length;
1141 +
1142 + //
1143 + // The `_bufferedAmount` property is used only when the peer is a client and
1144 + // the opening handshake fails. Under these circumstances, in fact, the
1145 + // `setSocket()` method is not called, so the `_socket` and `_sender`
1146 + // properties are set to `null`.
1147 + //
1148 + if (websocket._socket) websocket._sender._bufferedBytes += length;
1149 + else websocket._bufferedAmount += length;
1150 + }
1151 +
1152 + if (cb) {
1153 + const err = new Error(
1154 + `WebSocket is not open: readyState ${websocket.readyState} ` +
1155 + `(${readyStates[websocket.readyState]})`
1156 + );
1157 + process.nextTick(cb, err);
1158 + }
1159 +}
1160 +
1161 +/**
1162 + * The listener of the `Receiver` `'conclude'` event.
1163 + *
1164 + * @param {Number} code The status code
1165 + * @param {Buffer} reason The reason for closing
1166 + * @private
1167 + */
1168 +function receiverOnConclude(code, reason) {
1169 + const websocket = this[kWebSocket];
1170 +
1171 + websocket._closeFrameReceived = true;
1172 + websocket._closeMessage = reason;
1173 + websocket._closeCode = code;
1174 +
1175 + if (websocket._socket[kWebSocket] === undefined) return;
1176 +
1177 + websocket._socket.removeListener('data', socketOnData);
1178 + process.nextTick(resume, websocket._socket);
1179 +
1180 + if (code === 1005) websocket.close();
1181 + else websocket.close(code, reason);
1182 +}
1183 +
1184 +/**
1185 + * The listener of the `Receiver` `'drain'` event.
1186 + *
1187 + * @private
1188 + */
1189 +function receiverOnDrain() {
1190 + const websocket = this[kWebSocket];
1191 +
1192 + if (!websocket.isPaused) websocket._socket.resume();
1193 +}
1194 +
1195 +/**
1196 + * The listener of the `Receiver` `'error'` event.
1197 + *
1198 + * @param {(RangeError|Error)} err The emitted error
1199 + * @private
1200 + */
1201 +function receiverOnError(err) {
1202 + const websocket = this[kWebSocket];
1203 +
1204 + if (websocket._socket[kWebSocket] !== undefined) {
1205 + websocket._socket.removeListener('data', socketOnData);
1206 +
1207 + //
1208 + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See
1209 + // https://github.com/websockets/ws/issues/1940.
1210 + //
1211 + process.nextTick(resume, websocket._socket);
1212 +
1213 + websocket.close(err[kStatusCode]);
1214 + }
1215 +
1216 + if (!websocket._errorEmitted) {
1217 + websocket._errorEmitted = true;
1218 + websocket.emit('error', err);
1219 + }
1220 +}
1221 +
1222 +/**
1223 + * The listener of the `Receiver` `'finish'` event.
1224 + *
1225 + * @private
1226 + */
1227 +function receiverOnFinish() {
1228 + this[kWebSocket].emitClose();
1229 +}
1230 +
1231 +/**
1232 + * The listener of the `Receiver` `'message'` event.
1233 + *
1234 + * @param {Buffer|ArrayBuffer|Buffer[])} data The message
1235 + * @param {Boolean} isBinary Specifies whether the message is binary or not
1236 + * @private
1237 + */
1238 +function receiverOnMessage(data, isBinary) {
1239 + this[kWebSocket].emit('message', data, isBinary);
1240 +}
1241 +
1242 +/**
1243 + * The listener of the `Receiver` `'ping'` event.
1244 + *
1245 + * @param {Buffer} data The data included in the ping frame
1246 + * @private
1247 + */
1248 +function receiverOnPing(data) {
1249 + const websocket = this[kWebSocket];
1250 +
1251 + if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP);
1252 + websocket.emit('ping', data);
1253 +}
1254 +
1255 +/**
1256 + * The listener of the `Receiver` `'pong'` event.
1257 + *
1258 + * @param {Buffer} data The data included in the pong frame
1259 + * @private
1260 + */
1261 +function receiverOnPong(data) {
1262 + this[kWebSocket].emit('pong', data);
1263 +}
1264 +
1265 +/**
1266 + * Resume a readable stream
1267 + *
1268 + * @param {Readable} stream The readable stream
1269 + * @private
1270 + */
1271 +function resume(stream) {
1272 + stream.resume();
1273 +}
1274 +
1275 +/**
1276 + * The `Sender` error event handler.
1277 + *
1278 + * @param {Error} The error
1279 + * @private
1280 + */
1281 +function senderOnError(err) {
1282 + const websocket = this[kWebSocket];
1283 +
1284 + if (websocket.readyState === WebSocket.CLOSED) return;
1285 + if (websocket.readyState === WebSocket.OPEN) {
1286 + websocket._readyState = WebSocket.CLOSING;
1287 + setCloseTimer(websocket);
1288 + }
1289 +
1290 + //
1291 + // `socket.end()` is used instead of `socket.destroy()` to allow the other
1292 + // peer to finish sending queued data. There is no need to set a timer here
1293 + // because `CLOSING` means that it is already set or not needed.
1294 + //
1295 + this._socket.end();
1296 +
1297 + if (!websocket._errorEmitted) {
1298 + websocket._errorEmitted = true;
1299 + websocket.emit('error', err);
1300 + }
1301 +}
1302 +
1303 +/**
1304 + * Set a timer to destroy the underlying raw socket of a WebSocket.
1305 + *
1306 + * @param {WebSocket} websocket The WebSocket instance
1307 + * @private
1308 + */
1309 +function setCloseTimer(websocket) {
1310 + websocket._closeTimer = setTimeout(
1311 + websocket._socket.destroy.bind(websocket._socket),
1312 + websocket._closeTimeout
1313 + );
1314 +}
1315 +
1316 +/**
1317 + * The listener of the socket `'close'` event.
1318 + *
1319 + * @private
1320 + */
1321 +function socketOnClose() {
1322 + const websocket = this[kWebSocket];
1323 +
1324 + this.removeListener('close', socketOnClose);
1325 + this.removeListener('data', socketOnData);
1326 + this.removeListener('end', socketOnEnd);
1327 +
1328 + websocket._readyState = WebSocket.CLOSING;
1329 +
1330 + //
1331 + // The close frame might not have been received or the `'end'` event emitted,
1332 + // for example, if the socket was destroyed due to an error. Ensure that the
1333 + // `receiver` stream is closed after writing any remaining buffered data to
1334 + // it. If the readable side of the socket is in flowing mode then there is no
1335 + // buffered data as everything has been already written. If instead, the
1336 + // socket is paused, any possible buffered data will be read as a single
1337 + // chunk.
1338 + //
1339 + if (
1340 + !this._readableState.endEmitted &&
1341 + !websocket._closeFrameReceived &&
1342 + !websocket._receiver._writableState.errorEmitted &&
1343 + this._readableState.length !== 0
1344 + ) {
1345 + const chunk = this.read(this._readableState.length);
1346 +
1347 + websocket._receiver.write(chunk);
1348 + }
1349 +
1350 + websocket._receiver.end();
1351 +
1352 + this[kWebSocket] = undefined;
1353 +
1354 + clearTimeout(websocket._closeTimer);
1355 +
1356 + if (
1357 + websocket._receiver._writableState.finished ||
1358 + websocket._receiver._writableState.errorEmitted
1359 + ) {
1360 + websocket.emitClose();
1361 + } else {
1362 + websocket._receiver.on('error', receiverOnFinish);
1363 + websocket._receiver.on('finish', receiverOnFinish);
1364 + }
1365 +}
1366 +
1367 +/**
1368 + * The listener of the socket `'data'` event.
1369 + *
1370 + * @param {Buffer} chunk A chunk of data
1371 + * @private
1372 + */
1373 +function socketOnData(chunk) {
1374 + if (!this[kWebSocket]._receiver.write(chunk)) {
1375 + this.pause();
1376 + }
1377 +}
1378 +
1379 +/**
1380 + * The listener of the socket `'end'` event.
1381 + *
1382 + * @private
1383 + */
1384 +function socketOnEnd() {
1385 + const websocket = this[kWebSocket];
1386 +
1387 + websocket._readyState = WebSocket.CLOSING;
1388 + websocket._receiver.end();
1389 + this.end();
1390 +}
1391 +
1392 +/**
1393 + * The listener of the socket `'error'` event.
1394 + *
1395 + * @private
1396 + */
1397 +function socketOnError() {
1398 + const websocket = this[kWebSocket];
1399 +
1400 + this.removeListener('error', socketOnError);
1401 + this.on('error', NOOP);
1402 +
1403 + if (websocket) {
1404 + websocket._readyState = WebSocket.CLOSING;
1405 + this.destroy();
1406 + }
1407 +}
added node_modules/ws/package.json +74 −0
@@ -0,0 +1,74 @@
1 +{
2 + "name": "ws",
3 + "version": "8.21.3",
4 + "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5 + "keywords": [
6 + "HyBi",
7 + "Push",
8 + "RFC-6455",
9 + "WebSocket",
10 + "WebSockets",
11 + "real-time"
12 + ],
13 + "homepage": "https://github.com/websockets/ws",
14 + "bugs": "https://github.com/websockets/ws/issues",
15 + "repository": {
16 + "type": "git",
17 + "url": "git+https://github.com/websockets/ws.git"
18 + },
19 + "author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
20 + "license": "MIT",
21 + "main": "index.js",
22 + "exports": {
23 + ".": {
24 + "browser": "./browser.js",
25 + "import": "./wrapper.mjs",
26 + "require": "./index.js"
27 + },
28 + "./package.json": "./package.json"
29 + },
30 + "browser": "browser.js",
31 + "engines": {
32 + "node": ">=10.0.0"
33 + },
34 + "files": [
35 + "browser.js",
36 + "index.js",
37 + "lib/*.js",
38 + "wrapper.mjs"
39 + ],
40 + "scripts": {
41 + "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
42 + "integration": "mocha --throw-deprecation test/*.integration.js",
43 + "lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
44 + },
45 + "peerDependencies": {
46 + "bufferutil": "^4.0.1",
47 + "utf-8-validate": ">=5.0.2"
48 + },
49 + "peerDependenciesMeta": {
50 + "bufferutil": {
51 + "optional": true
52 + },
53 + "utf-8-validate": {
54 + "optional": true
55 + }
56 + },
57 + "devDependencies": {
58 + "@eslint/js": "^10.0.1",
59 + "benchmark": "^2.1.4",
60 + "bufferutil": "^4.0.1",
61 + "eslint": "^10.0.1",
62 + "eslint-config-prettier": "^10.0.1",
63 + "eslint-plugin-prettier": "^5.0.0",
64 + "globals": "^17.0.0",
65 + "mocha": "^8.4.0",
66 + "nyc": "^15.0.0",
67 + "prettier": "^3.0.0",
68 + "utf-8-validate": "^6.0.0"
69 + },
70 + "allowScripts": {
71 + "bufferutil": true,
72 + "utf-8-validate": true
73 + }
74 +}
added node_modules/ws/wrapper.mjs +21 −0
@@ -0,0 +1,21 @@
1 +import createWebSocketStream from './lib/stream.js';
2 +import extension from './lib/extension.js';
3 +import PerMessageDeflate from './lib/permessage-deflate.js';
4 +import Receiver from './lib/receiver.js';
5 +import Sender from './lib/sender.js';
6 +import subprotocol from './lib/subprotocol.js';
7 +import WebSocket from './lib/websocket.js';
8 +import WebSocketServer from './lib/websocket-server.js';
9 +
10 +export {
11 + createWebSocketStream,
12 + extension,
13 + PerMessageDeflate,
14 + Receiver,
15 + Sender,
16 + subprotocol,
17 + WebSocket,
18 + WebSocketServer
19 +};
20 +
21 +export default WebSocket;
modified server/server.js +1 −0
@@ -133,6 +133,7 @@ function readTranscript(chatId) {
133 133 // ---------- projets (12 apps Groupe KA) ----------
134 134 const SELF_NODE = 'M3U96a';
135 135 const PROJECTS = [
136 + { id: 'groupe-ka@M3U96b', name: 'Groupe·Ka', node: 'M3U96b', dir: '~/apps/groupe-ka' },
136 137 { id: 'lou-ka@M3U96b', name: 'Lou·Ka', node: 'M3U96b', dir: '~/apps/lou-ka' },
137 138 { id: 'immo-ka@M4M64a', name: 'Immo·Ka', node: 'M4M64a', dir: '~/apps/immo-ka' },
138 139 { id: 'vrai-prix@M3U96a', name: 'Vrai-Prix', node: null, dir: path.join(HOME, 'apps', 'vrai-prix') },
139 140