Anne Lefebvre commited on
Commit
c4ca913
·
unverified ·
2 Parent(s): b46dc23 58ad246

Merge pull request #26 from AnujPanthri/server-comments

Browse files
server/src/helpers.ts CHANGED
@@ -1,6 +1,12 @@
1
  import * as crypto from "crypto";
2
  import basex from "base-x";
3
 
 
 
 
 
 
 
4
  export function generateSessionToken(length: number): string {
5
  if (length < 1) throw new Error("invalid length. length must be greater than 0");
6
  const base31Tokens = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
 
1
  import * as crypto from "crypto";
2
  import basex from "base-x";
3
 
4
+ /**
5
+ * Generates a random, cryptographically secure token using uppercase, readable characters.
6
+ * Characters that are used: ABCDEFGHJKMNPQRSTUVWXYZ23456789
7
+ * @param {number} length - length of the token
8
+ * @returns {string}
9
+ */
10
  export function generateSessionToken(length: number): string {
11
  if (length < 1) throw new Error("invalid length. length must be greater than 0");
12
  const base31Tokens = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
server/src/index.ts CHANGED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
  import express, { Application, Request, Response } from "express";
2
  import * as http from "http";
3
  import * as WebSocket from "ws";
@@ -7,6 +12,7 @@ import WebSocketWrapper from "./socket/WebSocketAdapter";
7
  import pino from "pino";
8
  import path from "path";
9
 
 
10
  (() => {
11
  const args = process.argv;
12
  if (args.includes("--silent")) {
@@ -16,6 +22,10 @@ import path from "path";
16
  return main(true);
17
  })();
18
 
 
 
 
 
19
  function main(logging: boolean = true) {
20
  const port: number = parseInt(process.env.PORT as string, 10) || 3000;
21
  const logger = pino({
@@ -35,8 +45,10 @@ function main(logging: boolean = true) {
35
  const server = http.createServer(app);
36
  const wss = new WebSocket.Server({ server });
37
 
 
38
  app.use(express.static(path.join(__dirname, "../../client/build/web")));
39
 
 
40
  app.get("*", (_req: Request, res: Response) => {
41
  res.sendFile(path.join(__dirname + "/../../client/build/web/index.html"));
42
  });
 
1
+ /*
2
+ Created by Anne Lefebvre and Anuj Panthri - 2023
3
+ Handles the HTTP file serving and the WebSocket connect for HapticTouch
4
+ */
5
+
6
  import express, { Application, Request, Response } from "express";
7
  import * as http from "http";
8
  import * as WebSocket from "ws";
 
12
  import pino from "pino";
13
  import path from "path";
14
 
15
+ // If started with --silent, then logging will be disabled.
16
  (() => {
17
  const args = process.argv;
18
  if (args.includes("--silent")) {
 
22
  return main(true);
23
  })();
24
 
25
+ /**
26
+ * Starts HTTP(S) and WS server, initializes HapticLinkServer and handles routing.
27
+ * @param {boolean} logging - Defaults true
28
+ */
29
  function main(logging: boolean = true) {
30
  const port: number = parseInt(process.env.PORT as string, 10) || 3000;
31
  const logger = pino({
 
45
  const server = http.createServer(app);
46
  const wss = new WebSocket.Server({ server });
47
 
48
+ // The entire build/web directory is statically served.
49
  app.use(express.static(path.join(__dirname, "../../client/build/web")));
50
 
51
+ // Catch all. If we want to add pages later, then we should probably change this.
52
  app.get("*", (_req: Request, res: Response) => {
53
  res.sendFile(path.join(__dirname + "/../../client/build/web/index.html"));
54
  });
server/src/socket/hapticLinkServer.ts CHANGED
@@ -5,9 +5,15 @@ import { WebSocketInterface } from "./WebSocketAdapter";
5
 
6
  type RouteHandler<T> = (context: Context<T>) => void;
7
 
 
8
  export interface Route<T> {
 
9
  name: string;
 
 
10
  schema: ZodSchema<T>;
 
 
11
  handler: RouteHandler<T>;
12
  }
13
 
@@ -25,6 +31,7 @@ export class User {
25
  }
26
  }
27
 
 
28
  export interface Context<T> {
29
  ws: WebSocketInterface;
30
  user: User;
@@ -33,8 +40,13 @@ export interface Context<T> {
33
  }
34
 
35
  export class HapticLinkServer {
 
36
  routes: { [key: string]: Route<any> };
 
 
37
  rooms: { [key: string]: Room };
 
 
38
  users: Map<WebSocketInterface, User>;
39
 
40
  constructor() {
@@ -43,6 +55,11 @@ export class HapticLinkServer {
43
  this.rooms = {};
44
  }
45
 
 
 
 
 
 
46
  removeUser(ws: WebSocketInterface): boolean {
47
  const user = this.users.get(ws);
48
  if (!user) return false;
@@ -52,6 +69,13 @@ export class HapticLinkServer {
52
  return true;
53
  }
54
 
 
 
 
 
 
 
 
55
  addRoute<T>(name: string, schema: ZodSchema<T>, handler: RouteHandler<T>): boolean {
56
  if (name in this.routes) {
57
  return false;
@@ -66,6 +90,14 @@ export class HapticLinkServer {
66
  return true;
67
  }
68
 
 
 
 
 
 
 
 
 
69
  handleRoute(ws: WebSocketInterface, message: string) {
70
  // Parse JSON
71
  let payload: any;
@@ -75,19 +107,23 @@ export class HapticLinkServer {
75
  return ws.send(JSON.stringify({ error: "message not in JSON format" }));
76
  }
77
 
 
78
  if (typeof payload != "object" || !Object.keys(payload).includes("route")) {
79
  return ws.send(JSON.stringify({ error: "missing route" }));
80
  }
81
 
 
82
  if (!(payload.route in this.routes)) {
83
  return ws.send(JSON.stringify({ error: "route not found" }));
84
  }
85
 
86
  const route = this.routes[payload.route];
 
87
  delete payload.route;
88
 
89
  let user: User;
90
 
 
91
  if (this.users.has(ws)) {
92
  user = this.users.get(ws)!;
93
  } else {
@@ -96,6 +132,7 @@ export class HapticLinkServer {
96
  user = newUser;
97
  }
98
 
 
99
  let context: Context<any> = {
100
  ws,
101
  payload,
@@ -107,6 +144,7 @@ export class HapticLinkServer {
107
  return ws.send(JSON.stringify({ error: "invalid route" }));
108
  }
109
 
 
110
  if (route.schema.safeParse(payload).success) {
111
  route.handler(context);
112
  } else {
 
5
 
6
  type RouteHandler<T> = (context: Context<T>) => void;
7
 
8
+
9
  export interface Route<T> {
10
+ // Name of route
11
  name: string;
12
+
13
+ // ZodSchema used to validate request body
14
  schema: ZodSchema<T>;
15
+
16
+ // Handler Function. To be called after request body is validated
17
  handler: RouteHandler<T>;
18
  }
19
 
 
31
  }
32
  }
33
 
34
+ // Context holds all available information for any given request
35
  export interface Context<T> {
36
  ws: WebSocketInterface;
37
  user: User;
 
40
  }
41
 
42
  export class HapticLinkServer {
43
+ // Router map
44
  routes: { [key: string]: Route<any> };
45
+
46
+ // All rooms are stored in this map
47
  rooms: { [key: string]: Room };
48
+
49
+ // All users are stored in this map and should be dropped when their connection does.
50
  users: Map<WebSocketInterface, User>;
51
 
52
  constructor() {
 
55
  this.rooms = {};
56
  }
57
 
58
+ /**
59
+ * Removes user from registered users
60
+ * @param {WebSocketInterface} ws
61
+ * @returns {boolean} true/false whether the user was successfully removed
62
+ */
63
  removeUser(ws: WebSocketInterface): boolean {
64
  const user = this.users.get(ws);
65
  if (!user) return false;
 
69
  return true;
70
  }
71
 
72
+ /**
73
+ * Registers a route with router
74
+ * @param {string} name The name of the route which be used by the client to identify the route.
75
+ * @param {ZodSchema<T>} schema A ZodSchema to match the body of the WS message
76
+ * @param {RouteHandler<T>} handler A handler function that should have a matching schema
77
+ * @returns
78
+ */
79
  addRoute<T>(name: string, schema: ZodSchema<T>, handler: RouteHandler<T>): boolean {
80
  if (name in this.routes) {
81
  return false;
 
90
  return true;
91
  }
92
 
93
+ /**
94
+ * Compares message param with names of registered routes
95
+ * If route exists, validates the schema and then calls
96
+ * the handler function with the formatted payload
97
+ * @param {WebSocketInterface} ws
98
+ * @param {string} message WebSocket message
99
+ * @returns
100
+ */
101
  handleRoute(ws: WebSocketInterface, message: string) {
102
  // Parse JSON
103
  let payload: any;
 
107
  return ws.send(JSON.stringify({ error: "message not in JSON format" }));
108
  }
109
 
110
+ // Check if message includes route
111
  if (typeof payload != "object" || !Object.keys(payload).includes("route")) {
112
  return ws.send(JSON.stringify({ error: "missing route" }));
113
  }
114
 
115
+ // Check if route is registered
116
  if (!(payload.route in this.routes)) {
117
  return ws.send(JSON.stringify({ error: "route not found" }));
118
  }
119
 
120
  const route = this.routes[payload.route];
121
+ // Removes route from body
122
  delete payload.route;
123
 
124
  let user: User;
125
 
126
+ // Get user or create new one
127
  if (this.users.has(ws)) {
128
  user = this.users.get(ws)!;
129
  } else {
 
132
  user = newUser;
133
  }
134
 
135
+ // Create context
136
  let context: Context<any> = {
137
  ws,
138
  payload,
 
144
  return ws.send(JSON.stringify({ error: "invalid route" }));
145
  }
146
 
147
+ // Validates message body and calls handler
148
  if (route.schema.safeParse(payload).success) {
149
  route.handler(context);
150
  } else {
server/src/socket/room.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { generateSessionToken } from "../helpers";
2
  import { User } from "./hapticLinkServer";
3
 
 
4
  export interface UserData {
5
  username: string;
6
  id: string;
@@ -12,18 +13,34 @@ export class Room {
12
  static roomIdLength: number = 6;
13
  id: string;
14
  users: User[];
 
 
 
 
 
15
  constructor(roomId?: string) {
16
  this.id = roomId || generateSessionToken(Room.roomIdLength);
17
  this.users = [];
18
  }
19
 
 
 
 
 
 
 
 
20
  addUser(user: User) {
21
- // Generate random roomId if one wasn't created.
22
  if (this.users.includes(user)) return;
23
  this.users.push(user);
24
  this.updateRoom();
25
  }
26
 
 
 
 
 
 
27
  removeUserById(id: string): boolean {
28
  const oldSize = this.users.length;
29
  this.users = this.users.filter((user) => user.id != id);
@@ -31,13 +48,19 @@ export class Room {
31
  return oldSize > this.users.length;
32
  }
33
 
 
 
 
 
 
34
  hasUser(id: string): boolean {
35
  return this.users.some((user) => user.id == id);
36
  }
37
 
38
  /**
39
- * @param message Message to send to users
40
- * @param filter Array of user ID to ignore when sending
 
41
  **/
42
  broadcast(message: string, filter: string[] = []) {
43
  this.users.forEach((user) => {
@@ -46,6 +69,10 @@ export class Room {
46
  });
47
  }
48
 
 
 
 
 
49
  updateRoom() {
50
  const usersData: UserData[] = [];
51
  this.users.forEach((user) => {
 
1
  import { generateSessionToken } from "../helpers";
2
  import { User } from "./hapticLinkServer";
3
 
4
+ // User data that is safe to reveal to users
5
  export interface UserData {
6
  username: string;
7
  id: string;
 
13
  static roomIdLength: number = 6;
14
  id: string;
15
  users: User[];
16
+
17
+ /**
18
+ * @constructor
19
+ * @param roomId Optional, a secure one will be generated
20
+ */
21
  constructor(roomId?: string) {
22
  this.id = roomId || generateSessionToken(Room.roomIdLength);
23
  this.users = [];
24
  }
25
 
26
+ /**
27
+ * Add a user to room, broadcasts message to all room users
28
+ * when user if user is successfully added. Won't add user
29
+ * if they are alreadu part of it
30
+ * @param {User} user
31
+ * @returns
32
+ */
33
  addUser(user: User) {
 
34
  if (this.users.includes(user)) return;
35
  this.users.push(user);
36
  this.updateRoom();
37
  }
38
 
39
+ /**
40
+ * Removed user from an ID
41
+ * @param {string} id User's ID
42
+ * @returns {boolean} true/false whether the user was deleted or not
43
+ */
44
  removeUserById(id: string): boolean {
45
  const oldSize = this.users.length;
46
  this.users = this.users.filter((user) => user.id != id);
 
48
  return oldSize > this.users.length;
49
  }
50
 
51
+ /**
52
+ * Used to check if a room contains a user
53
+ * @param {string} id User's ID
54
+ * @returns {boolean} true/false whether the room has that user
55
+ */
56
  hasUser(id: string): boolean {
57
  return this.users.some((user) => user.id == id);
58
  }
59
 
60
  /**
61
+ * Broadcasts a message to every user in room
62
+ * @param {string} message Message to send to users
63
+ * @param {string[]} filter Array of user ID to ignore when sending
64
  **/
65
  broadcast(message: string, filter: string[] = []) {
66
  this.users.forEach((user) => {
 
69
  });
70
  }
71
 
72
+ /**
73
+ * Broadcasts the user list of the current room.
74
+ * Should be used when a user is added or removed.
75
+ */
76
  updateRoom() {
77
  const usersData: UserData[] = [];
78
  this.users.forEach((user) => {
server/src/socket/routes.ts CHANGED
@@ -6,6 +6,10 @@ import { SendVibrationHandler, SendVibrationSchema } from "./routes/send_touch";
6
  import { SetUsernameHandler, SetUsernameSchema } from "./routes/set_username";
7
  import { TestConnnectionSchema, TestConnectionHandler } from "./routes/test_connection";
8
 
 
 
 
 
9
  export function registerRoutes(router: HapticLinkServer) {
10
  router.addRoute("test_connection", TestConnnectionSchema, TestConnectionHandler);
11
  router.addRoute("join_room", JoinRoomSchema, JoinRoomHandler);
 
6
  import { SetUsernameHandler, SetUsernameSchema } from "./routes/set_username";
7
  import { TestConnnectionSchema, TestConnectionHandler } from "./routes/test_connection";
8
 
9
+ /**
10
+ * Registers all the routes for Haptic Link
11
+ * @param {HapticLinkServer} router
12
+ */
13
  export function registerRoutes(router: HapticLinkServer) {
14
  router.addRoute("test_connection", TestConnnectionSchema, TestConnectionHandler);
15
  router.addRoute("join_room", JoinRoomSchema, JoinRoomHandler);