diff --git a/package-lock.json b/package-lock.json index abc0dcc..6df6368 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "compression": "^1.7.4", "dotenv": "^16.4.5", "express": "^4.19.2", + "forwarded-parse": "^2.1.2", + "is-ip": "^5.0.1", "is-mobile": "^4.0.0", "nipplejs": "^0.10.2", "socket.io": "^4.7.5", @@ -1858,6 +1860,31 @@ "node": ">=6" } }, + "node_modules/clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-regexp/node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1973,6 +2000,17 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -3194,6 +3232,11 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -3230,6 +3273,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-amd-module-type": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-5.0.1.tgz", @@ -3687,6 +3741,17 @@ "node": ">=10.13.0" } }, + "node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3752,6 +3817,21 @@ "node": ">=8" } }, + "node_modules/is-ip": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz", + "integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==", + "dependencies": { + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-mobile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", @@ -5767,6 +5847,22 @@ "node": ">=14" } }, + "node_modules/super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "dependencies": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5884,6 +5980,20 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 1538518..3f167aa 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "compression": "^1.7.4", "dotenv": "^16.4.5", "express": "^4.19.2", + "forwarded-parse": "^2.1.2", + "is-ip": "^5.0.1", "is-mobile": "^4.0.0", "nipplejs": "^0.10.2", "socket.io": "^4.7.5", diff --git a/server/index.ts b/server/index.ts index 4fcd869..e5edee9 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2,6 +2,7 @@ import { clampMagnitude } from "../common/vector"; import { env } from "./env"; import { disconnectPlayer, joinPlayer } from "./logic/logic"; import { server } from "./server"; +import { getClientIp } from "./socket-utils"; import { getLogger, initializeLogger } from "./utils/logger"; import { getPlayer, getRoom } from "./world"; @@ -24,11 +25,13 @@ async function startServer() { }); socket.on("requestJoin", async (event, callback) => { + const clientIp = getClientIp(socket); + getLogger().stats({ name: "user join request", unit: "count", extra: { - player_ip: socket.handshake.address, + player_ip: clientIp, }, value: 1, }); @@ -42,7 +45,7 @@ async function startServer() { getLogger().info( `User ${socket.id} with name "${event.username}" joined room ${room.id}`, { - player_ip: socket.handshake.address, + player_ip: clientIp, player_id: player.id, room_id: `${room.id}`, }, diff --git a/server/socket-utils.ts b/server/socket-utils.ts new file mode 100644 index 0000000..c41e9a7 --- /dev/null +++ b/server/socket-utils.ts @@ -0,0 +1,43 @@ +import forwardedParse from "forwarded-parse"; +import { isIP } from "is-ip"; +import { Socket } from "socket.io"; + +/** + * Given a socket, returns the IP address of the client. + * + * This function will try to find the most accurate IP address, based on headers and other data. + */ +export function getClientIp(socket: Socket): string { + return ( + checkedIp(getXForwardedFor(socket)) || + checkedIp(getForwarded(socket)) || + checkedIp(getCloudflareHeaders(socket)) || + socket.handshake.address + ); +} + +function checkedIp(ip: string | undefined) { + return ip && isIP(ip) ? ip : undefined; +} + +function getXForwardedFor(socket: Socket) { + return [socket.handshake.headers["x-forwarded-for"] ?? []] + .flat() + .map((forwardedFor) => forwardedFor.split(",")[0]?.trim()) + .filter(Boolean)[0]; +} + +function getForwarded(socket: Socket) { + try { + const forwardedHeader = socket.handshake.headers["forwarded"]; + return forwardedHeader + ? forwardedParse(forwardedHeader)[0]?.for + : undefined; + } catch { + return undefined; + } +} + +function getCloudflareHeaders(socket: Socket) { + return socket.handshake.headers["cf-connecting-ip"]?.[0]; +}