SPB Git

spb/admin-ka

Public

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

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

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 // ---------- projets (12 apps Groupe KA) ---------- 133 // ---------- projets (12 apps Groupe KA) ----------
134 const SELF_NODE = 'M3U96a'; 134 const SELF_NODE = 'M3U96a';
135 const PROJECTS = [ 135 const PROJECTS = [
136+ { id: 'groupe-ka@M3U96b', name: 'Groupe·Ka', node: 'M3U96b', dir: '~/apps/groupe-ka' },
136 { id: 'lou-ka@M3U96b', name: 'Lou·Ka', node: 'M3U96b', dir: '~/apps/lou-ka' }, 137 { id: 'lou-ka@M3U96b', name: 'Lou·Ka', node: 'M3U96b', dir: '~/apps/lou-ka' },
137 { id: 'immo-ka@M4M64a', name: 'Immo·Ka', node: 'M4M64a', dir: '~/apps/immo-ka' }, 138 { id: 'immo-ka@M4M64a', name: 'Immo·Ka', node: 'M4M64a', dir: '~/apps/immo-ka' },
138 { id: 'vrai-prix@M3U96a', name: 'Vrai-Prix', node: null, dir: path.join(HOME, 'apps', 'vrai-prix') }, 139 { id: 'vrai-prix@M3U96a', name: 'Vrai-Prix', node: null, dir: path.join(HOME, 'apps', 'vrai-prix') },
139 140
Menu

Theme