Detect self multi-connect, some bug fixes
[vchess.git] / server / sockets.js
1 const url = require('url');
2
3 // Node version in Ubuntu 16.04 does not know about URL class
4 // NOTE: url is already transformed, without ?xxx=yyy... parts
5 function getJsonFromUrl(url)
6 {
7 const query = url.substr(2); //starts with "/?"
8 let result = {};
9 query.split("&").forEach((part) => {
10 const item = part.split("=");
11 result[item[0]] = decodeURIComponent(item[1]);
12 });
13 return result;
14 }
15
16 module.exports = function(wss) {
17 // Associative array page --> sid --> tmpId --> socket
18 // "page" is either "/" for hall or "/game/some_gid" for Game,
19 // tmpId is required if a same user (browser) has different tabs
20 let clients = {};
21 wss.on("connection", (socket, req) => {
22 const query = getJsonFromUrl(req.url);
23 const sid = query["sid"];
24 const tmpId = query["tmpId"];
25 const page = query["page"];
26 const notifyRoom = (page,code,obj={}) => {
27 if (!clients[page])
28 return;
29 Object.keys(clients[page]).forEach(k => {
30 Object.keys(clients[page][k]).forEach(x => {
31 if (k == sid && x == tmpId)
32 return;
33 clients[page][k][x].send(JSON.stringify(Object.assign(
34 {code:code, from:sid}, obj)));
35 });
36 });
37 };
38 const deleteConnexion = () => {
39 if (!clients[page] || !clients[page][sid] || !clients[page][sid][tmpId])
40 return; //job already done
41 delete clients[page][sid][tmpId];
42 if (Object.keys(clients[page][sid]).length == 0)
43 {
44 delete clients[page][sid];
45 if (Object.keys(clients[page]) == 0)
46 delete clients[page];
47 }
48 };
49 const messageListener = (objtxt) => {
50 let obj = JSON.parse(objtxt);
51 if (!!obj.target)
52 {
53 // Check if receiver is connected, because there may be some lag
54 // between a client disconnects and another notice.
55 if (Array.isArray(obj.target))
56 {
57 if (!clients[page][obj.target[0]] ||
58 !clients[page][obj.target[0]][obj.target[1]])
59 {
60 return;
61 }
62 }
63 else if (!clients[page][obj.target])
64 return;
65 }
66 switch (obj.code)
67 {
68 // Wait for "connect" message to notify connection to the room,
69 // because if game loading is slow the message listener might
70 // not be ready too early.
71 case "connect":
72 {
73 notifyRoom(page, "connect");
74 if (page.indexOf("/game/") >= 0)
75 notifyRoom("/", "gconnect", {page:page});
76 break;
77 }
78 case "disconnect":
79 // When page changes:
80 deleteConnexion();
81 if (!clients[page] || !clients[page][sid])
82 {
83 // I effectively disconnected from this page:
84 notifyRoom(page, "disconnect");
85 if (page.indexOf("/game/") >= 0)
86 notifyRoom("/", "gdisconnect", {page:page});
87 }
88 break;
89 case "killme":
90 {
91 // Self multi-connect: manual removal + disconnect
92 const doKill = (pg) => {
93 Object.keys(clients[pg][obj.sid]).forEach(x => {
94 clients[pg][obj.sid][x].send(JSON.stringify({code: "killed"}));
95 });
96 delete clients[pg][obj.sid];
97 };
98 const disconnectFromOtherConnexion = (pg,code,o={}) => {
99 Object.keys(clients[pg]).forEach(k => {
100 if (k != obj.sid)
101 {
102 Object.keys(clients[pg][k]).forEach(x => {
103 clients[pg][k][x].send(JSON.stringify(Object.assign(
104 {code:code, from:obj.sid}, o)));
105 });
106 }
107 });
108 };
109 Object.keys(clients).forEach(pg => {
110 if (!!clients[pg][obj.sid])
111 {
112 doKill(pg);
113 disconnectFromOtherConnexion(pg, "disconnect");
114 if (pg.indexOf("/game/") >= 0)
115 disconnectFromOtherConnexion("/", "gdisconnect", {page:pg});
116 }
117 });
118 break;
119 }
120 case "pollclients": //from Hall or Game
121 {
122 let sockIds = [];
123 Object.keys(clients[page]).forEach(k => {
124 // Avoid polling myself: no new information to get
125 if (k != sid)
126 sockIds.push(k);
127 });
128 socket.send(JSON.stringify({code:"pollclients", sockIds:sockIds}));
129 break;
130 }
131 case "pollclientsandgamers": //from Hall
132 {
133 let sockIds = [];
134 Object.keys(clients["/"]).forEach(k => {
135 // Avoid polling myself: no new information to get
136 if (k != sid)
137 sockIds.push({sid:k});
138 });
139 // NOTE: a "gamer" could also just be an observer
140 Object.keys(clients).forEach(p => {
141 if (p != "/")
142 {
143 Object.keys(clients[p]).forEach(k => {
144 if (k != sid)
145 sockIds.push({sid:k, page:p}); //page needed for gamers
146 });
147 }
148 });
149 socket.send(JSON.stringify({code:"pollclientsandgamers", sockIds:sockIds}));
150 break;
151 }
152
153 // Asking something: from is fully identified,
154 // but the requested resource can be from any tmpId (except current!)
155 case "askidentity":
156 case "asklastate":
157 case "askchallenge":
158 case "askgame":
159 case "askfullgame":
160 {
161 const tmpIds = Object.keys(clients[page][obj.target]);
162 if (obj.target == sid) //targetting myself
163 {
164 const idx_myTmpid = tmpIds.findIndex(x => x == tmpId);
165 if (idx_myTmpid >= 0)
166 tmpIds.splice(idx_myTmpid, 1);
167 }
168 const tmpId_idx = Math.floor(Math.random() * tmpIds.length);
169 clients[page][obj.target][tmpIds[tmpId_idx]].send(
170 JSON.stringify({code:obj.code, from:[sid,tmpId]}));
171 break;
172 }
173
174 // Some Hall events: target all tmpId's (except mine),
175 case "refusechallenge":
176 case "startgame":
177 Object.keys(clients[page][obj.target]).forEach(x => {
178 if (obj.target != sid || x != tmpId)
179 {
180 clients[page][obj.target][x].send(JSON.stringify(
181 {code:obj.code, data:obj.data}));
182 }
183 });
184 break;
185
186 // Notify all room: mostly game events
187 case "newchat":
188 case "newchallenge":
189 case "newgame":
190 case "deletechallenge":
191 case "newmove":
192 case "resign":
193 case "abort":
194 case "drawoffer":
195 case "draw":
196 notifyRoom(page, obj.code, {data:obj.data});
197 break;
198
199 // Passing, relaying something: from isn't needed,
200 // but target is fully identified (sid + tmpId)
201 case "challenge":
202 case "fullgame":
203 case "game":
204 case "identity":
205 case "lastate":
206 clients[page][obj.target[0]][obj.target[1]].send(JSON.stringify(
207 {code:obj.code, data:obj.data}));
208 break;
209 }
210 };
211 const closeListener = () => {
212 // For tab or browser closing:
213 deleteConnexion();
214 };
215 // Update clients object: add new connexion
216 if (!clients[page])
217 clients[page] = {[sid]: {[tmpId]: socket}};
218 else if (!clients[page][sid])
219 clients[page][sid] = {[tmpId]: socket};
220 else
221 clients[page][sid][tmpId] = socket;
222 socket.on("message", messageListener);
223 socket.on("close", closeListener);
224 });
225 }