From 0ca11a0ec494d9b069f96fac41da69adf1aa59e7 Mon Sep 17 00:00:00 2001 From: cha0s Date: Wed, 22 Jul 2020 03:08:00 -0500 Subject: [PATCH] feat: limiting, with message impl --- package.json | 1 + src/client/chat--message.jsx | 15 ++++++++++++--- src/client/chat--message.scss | 15 +++++++++++++++ src/client/store/effects.js | 29 ++++++++++++++++++++++++++--- src/common/state/chat.js | 4 ++++ src/server/limiter.js | 7 +++++++ src/server/packet/add-friend.js | 2 +- src/server/packet/message.js | 1 + src/server/sockets.js | 30 ++++++++++++++++++++++++++---- yarn.lock | 5 +++++ 10 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 src/server/limiter.js diff --git a/package.json b/package.json index 65a205a..5d5fc7d 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "passport-local": "^1.0.0", "passport-reddit": "0.2.4", "prop-types": "^15", + "rate-limiter-flexible": "2.1.9", "react": "16.8.6", "react-dom": "16.8.6", "react-hot-loader": "^4.12.21", diff --git a/src/client/chat--message.jsx b/src/client/chat--message.jsx index c9528ad..320c223 100644 --- a/src/client/chat--message.jsx +++ b/src/client/chat--message.jsx @@ -9,7 +9,15 @@ import {useSelector} from 'react-redux'; import {usernameSelector} from '~/common/state/usernames'; export default function ChatMessage(props) { - const {message: {owner, message, timestamp}, isShort} = props; + const { + message: { + owner, + message, + timestamp, + rejected, + }, + isShort, + } = props; const username = useSelector((state) => usernameSelector(state, owner)); const dtf = new Intl.DateTimeFormat(undefined, { hour: '2-digit', @@ -26,12 +34,12 @@ export default function ChatMessage(props) { ); return (
{ !isShort && (
-
{username}
+
{-1 === owner ? '! System !' : username}
{$messageTime}
) @@ -50,6 +58,7 @@ ChatMessage.propTypes = { isShort: PropTypes.bool.isRequired, message: PropTypes.shape({ message: PropTypes.string, + rejected: PropTypes.bool, timestamp: PropTypes.number, owner: PropTypes.number, }).isRequired, diff --git a/src/client/chat--message.scss b/src/client/chat--message.scss index c309d15..3c3f58d 100644 --- a/src/client/chat--message.scss +++ b/src/client/chat--message.scss @@ -1,6 +1,14 @@ +@import '~/client/scss/colors.scss'; + .chat--message { margin-top: 0.75rem; padding: 0.25rem 1rem; + &.rejected { + background-color: rgba(200, 0, 0, 0.2); + } + &.system { + background-color: rgba(200, 0, 0, 0.2); + } } .chat--message, .chat--message * { @@ -43,6 +51,9 @@ header { font-family: Caladea, 'Times New Roman', Times, serif; font-weight: bold; margin-bottom: 0.25rem; + .system & { + color: lighten($color-active, 30%); + } } .chat--messageTime { @@ -57,6 +68,10 @@ header { .chat--messageText { font-size: 0.9rem; + .rejected & { + color: red; + text-decoration: line-through; + } } .chat--messageMarkdown * { diff --git a/src/client/store/effects.js b/src/client/store/effects.js index 1994e57..fa96999 100644 --- a/src/client/store/effects.js +++ b/src/client/store/effects.js @@ -1,3 +1,5 @@ +import {v4 as uuidv4} from 'uuid'; + import AddFavorite from '../../common/packets/add-favorite.packet'; import AddFriend from '~/common/packets/add-friend.packet'; import Block from '~/common/packets/block.packet'; @@ -13,6 +15,7 @@ import { join, joined, leave, + rejectMessage, submitJoin, submitLeave, submitMessage, @@ -68,7 +71,7 @@ const effects = { const state = getState(); const userId = idSelector(state); const hasFriendship = !!payload.addeeId; - socket.send(new AddFriend({nameOrStatus: payload.nameOrStatus}), ({error, id}) => { + socket.send(new AddFriend({nameOrStatus: payload.nameOrStatus}), (error, id) => { if (error) { return; } @@ -88,7 +91,7 @@ const effects = { }, [submitJoin]: ({dispatch}, {payload}) => { const {channel} = payload; - socket.send(new Join(payload), ({messages, users}) => { + socket.send(new Join(payload), (error, {messages, users}) => { dispatch(join({channel, messages, users})); }); }, @@ -98,7 +101,27 @@ const effects = { }, [submitMessage]: ({dispatch}, {payload}) => { dispatch(addMessage(payload)); - socket.send(new Message(payload), ([timestamp, current]) => { + socket.send(new Message(payload), (error, result) => { + if (error) { + switch (error.code) { + case 429: { + dispatch(rejectMessage(payload.uuid)); + dispatch(addMessage({ + ...payload, + message: [ + 'You are sending too many messages.', + `Try again in ${error.ttr} second${1 === error.ttr ? '' : 's'}.`, + ].join(' '), + owner: -1, + uuid: uuidv4(), + })); + break; + } + default: + } + return; + } + const [timestamp, current] = result; dispatch(confirmMessage({current, previous: payload.uuid, timestamp})); }); }, diff --git a/src/common/state/chat.js b/src/common/state/chat.js index b52bd77..7f22655 100644 --- a/src/common/state/chat.js +++ b/src/common/state/chat.js @@ -70,6 +70,9 @@ const slice = createSlice({ const {users} = channels[channel]; users.splice(users.indexOf(id), 1); }, + rejectMessage: ({messages}, {payload: uuid}) => { + messages[uuid].rejected = true; + }, removeMessage: (state, {payload: {channel, uuid}}) => { delete state.messages[uuid]; const {messages} = state.channels[channel]; @@ -89,6 +92,7 @@ export const { joined, leave, left, + rejectMessage, removeMessage, submitJoin, submitLeave, diff --git a/src/server/limiter.js b/src/server/limiter.js new file mode 100644 index 0000000..0d99b78 --- /dev/null +++ b/src/server/limiter.js @@ -0,0 +1,7 @@ +import {RateLimiterRedis} from 'rate-limiter-flexible'; + +import createRedisClient from './redis'; + +const storeClient = createRedisClient(); + +export default (options) => new RateLimiterRedis({...options, storeClient}); diff --git a/src/server/packet/add-friend.js b/src/server/packet/add-friend.js index 6d0baf8..1943d2c 100644 --- a/src/server/packet/add-friend.js +++ b/src/server/packet/add-friend.js @@ -33,6 +33,6 @@ export default { nameOrStatus: friendship.status, })); }); - fn({id: addeeId}); + fn(addeeId); }, }; diff --git a/src/server/packet/message.js b/src/server/packet/message.js index 827a9d8..af357f6 100644 --- a/src/server/packet/message.js +++ b/src/server/packet/message.js @@ -7,6 +7,7 @@ import {allModels} from '~/server/models/registrar'; export default { Packet: Message, + limiter: {points: 10, duration: 15}, validator: () => true, responder: async (packet, socket, fn) => { const {req} = socket; diff --git a/src/server/sockets.js b/src/server/sockets.js index 57cf571..9991ff0 100644 --- a/src/server/sockets.js +++ b/src/server/sockets.js @@ -6,6 +6,7 @@ import {joinChannel, parseChannel} from '~/common/channel'; import {channelsToHydrate} from '~/server/entry'; +import createLimiter from './limiter'; import * as PacketHandlers from './packet'; import {userJoin} from './packet/join'; import {userLeave} from './packet/leave'; @@ -20,8 +21,14 @@ const packetHandlers = new Map(); export function createSocketServer(httpServer) { Object.keys(PacketHandlers).forEach((key) => { - const {Packet, responder, validator} = PacketHandlers[key]; - packetHandlers.set(Packet, {responder, validator}); + const { + Packet, + limiter: limiterConfig, + responder, + validator, + } = PacketHandlers[key]; + const limiter = limiterConfig && createLimiter({keyPrefix: Packet.name, ...limiterConfig}); + packetHandlers.set(Packet, {limiter, responder, validator}); }); const socketServer = new SocketServer(httpServer, {adapter}); socketServer.io.use(socketSession(session())); @@ -56,8 +63,23 @@ export function createSocketServer(httpServer) { }); socketServer.on('connect', (socket) => { socket.on('packet', async (packet, fn) => { - const {responder} = packetHandlers.get(packet.constructor); - responder(packet, socket, fn); + const {limiter, responder} = packetHandlers.get(packet.constructor); + if (limiter) { + try { + await limiter.consume(socket.id); + responder(packet, socket, (...args) => fn(undefined, ...args)); + } + catch (error) { + if (error instanceof Error) { + fn({code: 500}); + throw error; + } + fn({code: 429, ttr: Math.round(error.msBeforeNext / 1000) || 1}); + } + } + else { + responder(packet, socket, (...args) => fn(undefined, ...args)); + } }); socket.on('disconnecting', async () => { Object.keys(socket.socket.rooms).forEach((room) => { diff --git a/yarn.lock b/yarn.lock index 298e1db..06e075b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7561,6 +7561,11 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://npm.i12e.cha0s.io/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +rate-limiter-flexible@2.1.9: + version "2.1.9" + resolved "https://npm.i12e.cha0s.io/rate-limiter-flexible/-/rate-limiter-flexible-2.1.9.tgz#d137ffc874b8ea47d2baafe13a79474da52f8b6f" + integrity sha512-ueIXEHLZZqDBetuzyMbtSQ1Gh6Y5rw8ULoNuGA7L3xZ6njPIc2oM0ZlmsY9rS8rPU4yvdw7lk6MLbWU7WTNfnQ== + raw-body@2.4.0: version "2.4.0" resolved "https://npm.i12e.cha0s.io/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"