-
Notifications
You must be signed in to change notification settings - Fork 46
/
socketUtility.ts
188 lines (158 loc) · 6.74 KB
/
socketUtility.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// This script contains generalized methods for working with websocket objects.
// @ts-ignore
import { ensureJSONString } from '../utility/JSONUtils.js';
// @ts-ignore
import jsutil from '../../client/scripts/esm/util/jsutil.js';
// Type Definitions ---------------------------------------------------------------------------
import type { IncomingMessage } from 'http'; // Used for the socket upgrade http request TYPE
import type WebSocket from 'ws';
/** The socket object that contains all properties a normal socket has,
* plus an additional `metadata` property that we define ourselves. */
interface CustomWebSocket extends WebSocket {
/** Our custom-entered information about this websocket.
* To my knowledge (Naviary), the `metadata` property isn't already in use. */
metadata: {
/** What subscription lists they are subscribed to. Possible: "invites" / "game" */
subscriptions: {
/** Whether they are subscribed to the invites list. */
invites?: boolean;
/** Will be defined if they are subscribed to, or in, a game. */
game?: {
/** The id of the game they're in. @type {string} */
id: string;
/** The color they are playing as. @type {string} */
color: string;
};
};
/** The parsed cookie object, this will contain the 'browser-id' cookie if they are not signed in */
cookies: {
/** This is ALWAYS present, even if signed in! */
'browser-id'?: string;
/** Their preferred language. For example, 'en-US'. This is determined by their `i18next` cookie. */
i18next?: string;
/** Their refresh/session token, if they are signed in. */
jwt?: string;
};
/** The user-agent property of the original websocket upgrade's req.headers */
userAgent?: string;
memberInfo: {
/** True if they are signed in, if not they MUST have a browser-id cookie! */
signedIn: boolean;
user_id?: string;
username?: string;
roles?: string[];
};
/** The id of their websocket. */
id: string;
/** The socket's IP address. */
IP: string;
/** The timeout ID that can be used to cancel the timer that will
* expire the socket connection. This is useful if it closes early. */
clearafter?: NodeJS.Timeout;
/** The timeout ID to cancel the timer that will send an empty
* message to this socket just to verify they are alive and thinking. */
renewConnectionTimeoutID?: NodeJS.Timeout;
};
}
// Functions ---------------------------------------------------------------------------
/**
* Prints the websocket to the console, temporarily removing self-referencing first.
* @param ws - The websocket
*/
function printSocket(ws: CustomWebSocket) { console.log(stringifySocketMetadata(ws)); }
/**
* Simplifies the websocket's metadata and stringifies it.
* @param ws - The websocket object
* @returns The stringified simplified websocket metadata.
*/
function stringifySocketMetadata(ws: CustomWebSocket): string {
// Removes the recursion from the metadata, making it safe to stringify.
const simplifiedMetadata = getSimplifiedMetadata(ws);
return ensureJSONString(simplifiedMetadata, 'Error while stringifying socket metadata:');
}
/**
* Creates a new object with simplified metadata information from the websocket,
* and removes recursion. This can be safely be JSON.stringified() afterward.
* Excludes the stuff like the sendmessage() function and clearafter timer.
*
* BE CAREFUL not to modify the return object, for it will modify the original socket!
* @param ws - The websocket object
* @returns A new object containing simplified metadata.
*/
function getSimplifiedMetadata(ws: CustomWebSocket) {
const metadata = ws.metadata;
// Using Partial takes an existing type and makes all of its properties optional
const metadataCopy: Partial<typeof metadata> = {
memberInfo: jsutil.deepCopyObject(metadata.memberInfo),
cookies: { "browser-id": ws.metadata.cookies['browser-id'], "i18next": ws.metadata.cookies["i18next"]}, // Only copy these 2 cookies, NOT their refresh token!!!
id: metadata.id,
IP: metadata.IP,
subscriptions: jsutil.deepCopyObject(metadata.subscriptions),
};
return metadataCopy;
}
/**
* Returns the owner of the websocket.
* @param ws - The websocket
* @returns An object that contains either the `member` or `browser` property.
*/
function getOwnerFromSocket(ws: CustomWebSocket): { member: string } | { browser: string } {
const metadata = ws.metadata;
if (metadata.memberInfo.signedIn) return { member: ws.metadata.memberInfo.username! };
else return { browser: metadata.cookies['browser-id']! };
}
/**
* Parses cookies from the WebSocket upgrade request headers.
* @param req - The WebSocket upgrade request object
* @returns An object with cookie names as keys and their corresponding values
*/
function getCookiesFromWebsocket(req: IncomingMessage): { [cookieName: string]: string } {
// req.cookies is only defined from our cookie parser for regular requests,
// NOT for websocket upgrade requests! We have to parse them manually!
const rawCookies = req.headers.cookie;
const cookies: { [cookieName: string]: string } = {};
if (!rawCookies) return cookies;
for (const cookie of rawCookies.split(';')) {
const parts = cookie.split('=');
if (parts.length < 2) continue; // Skip if no value part exists
const name = parts[0]!.trim();
const value = parts[1]!.trim();
if (name && value) cookies[name] = value;
}
return cookies;
}
/**
* Reads the IP address attached to the incoming websocket connection request,
* and sets the websocket metadata's `IP` property to that value, then returns that IP.
* @param req - The request object.
* @param ws - The websocket object.
* @returns The IP address of the websocket connection, or `undefined` if not present.
*/
function getIPFromWebsocketUpgradeRequest(req: IncomingMessage): string | undefined {
// Check the headers for the forwarded IP (useful if behind a proxy like Cloudflare)
const clientIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress; // ws._socket.remoteAddress
// If we didn't get a string IP, return undefined
if (typeof clientIP !== 'string') return undefined;
return clientIP;
}
/**
* Extracts the signed-in status and identifier (username or browser ID) from the provided socket.
* @param ws - The socket to extract the data from.
* @returns An object containing the `signedIn` status and `identifier` (either username or browser ID).
*/
function getSignedInAndIdentifierOfSocket(ws: CustomWebSocket) {
const signedIn = ws.metadata.memberInfo.signedIn;
const identifier = signedIn ? ws.metadata.memberInfo.username : ws.metadata.cookies['browser-id'];
return { signedIn, identifier };
}
export default {
printSocket,
stringifySocketMetadata,
getOwnerFromSocket,
getCookiesFromWebsocket,
getIPFromWebsocketUpgradeRequest,
getSignedInAndIdentifierOfSocket,
};
export type {
CustomWebSocket,
};