feat: full server-side validation

This commit is contained in:
cha0s 2020-07-22 04:38:49 -05:00
parent cc6d4e6250
commit 7f55a32f67
12 changed files with 121 additions and 45 deletions

View File

@ -17,7 +17,7 @@ export const validateSubreddit = (name) => {
return !!name.match(/^[A-Za-z0-9][A-Za-z0-9_]{2,20}$/i); return !!name.match(/^[A-Za-z0-9][A-Za-z0-9_]{2,20}$/i);
}; };
export const validateUsername = (name) => name.match(/^[\w-]{3,20}/); export const validateUsername = (name) => !!name.match(/^[\w-]{3,20}$/);
export const validateChannel = (channel) => { export const validateChannel = (channel) => {
if (!channel) { if (!channel) {

View File

@ -1,10 +1,15 @@
import {validateChannel} from '~/common/channel';
import AddFavorite from '~/common/packets/add-favorite.packet'; import AddFavorite from '~/common/packets/add-favorite.packet';
import {allModels} from '~/server/models/registrar'; import ValidationError from './validation-error';
export default { export default {
Packet: AddFavorite, Packet: AddFavorite,
validator: async () => true, validator: async ({data: channel}) => {
if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
},
responder: async (packet, socket) => { responder: async (packet, socket) => {
const {req} = socket; const {req} = socket;
await req.user.createFavorite({channel: packet.data}); await req.user.createFavorite({channel: packet.data});

View File

@ -10,10 +10,10 @@ export default {
limiter: {points: 20, duration: 60}, limiter: {points: 20, duration: 60},
validator: async ({data: {nameOrStatus}}) => { validator: async ({data: {nameOrStatus}}) => {
if (!validateUsername(nameOrStatus)) { if (!validateUsername(nameOrStatus)) {
throw new ValidationError('Invalid username'); throw new ValidationError({code: 400, reason: 'Malformed username'});
} }
}, },
responder: async (packet, socket) => { responder: async ({data: {nameOrStatus}}, socket) => {
const {req} = socket; const {req} = socket;
const { const {
Friendship, Friendship,
@ -21,8 +21,8 @@ export default {
} = allModels(); } = allModels();
const adderId = req.user.id; const adderId = req.user.id;
const user = ( const user = (
await User.findOne({where: {redditUsername: packet.data.nameOrStatus}}) await User.findOne({where: {redditUsername: nameOrStatus}})
|| await User.create({redditUsername: packet.data.nameOrStatus}) || await User.create({redditUsername: nameOrStatus})
); );
const addeeId = user.id; const addeeId = user.id;
let friendship = await Friendship.findOne({where: {adderId: addeeId, addeeId: adderId}}); let friendship = await Friendship.findOne({where: {adderId: addeeId, addeeId: adderId}});

View File

@ -7,9 +7,16 @@ import {allModels} from '~/server/models/registrar';
import {removeFavoritedUser} from './remove-favorite'; import {removeFavoritedUser} from './remove-favorite';
import ValidationError from './validation-error';
export default { export default {
Packet: Block, Packet: Block,
validator: async () => true, validator: async ({data: id}) => {
const {User} = allModels();
if (!await User.count({where: {id}})) {
throw new ValidationError({code: 400, reason: 'No such user'});
}
},
responder: async (packet, socket) => { responder: async (packet, socket) => {
const {req} = socket; const {req} = socket;
const id = packet.data; const id = packet.data;

View File

@ -3,6 +3,7 @@ import {promisify} from 'util';
import {ServerSocket} from '@avocado/net/server/socket'; import {ServerSocket} from '@avocado/net/server/socket';
import {validateChannel} from '~/common/channel';
import Join from '~/common/packets/join.packet'; import Join from '~/common/packets/join.packet';
import { import {
@ -10,6 +11,8 @@ import {
channelUsers, channelUsers,
} from '~/server/entry'; } from '~/server/entry';
import ValidationError from './validation-error';
export const userJoin = async (channel, socket) => { export const userJoin = async (channel, socket) => {
const userId = '/r/anonymous' === channel ? 0 : socket.handshake.userId; const userId = '/r/anonymous' === channel ? 0 : socket.handshake.userId;
const users = await channelUsers(socket.handshake, channel); const users = await channelUsers(socket.handshake, channel);
@ -21,10 +24,13 @@ export const userJoin = async (channel, socket) => {
export default { export default {
Packet: Join, Packet: Join,
validator: async () => true, validator: async () => {
responder: async (packet, socket) => { if (!validateChannel()) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
},
responder: async ({data: channel}, socket) => {
const {req} = socket; const {req} = socket;
const {channel} = packet.data;
await userJoin(channel, socket.socket); await userJoin(channel, socket.socket);
return channelState(req, channel); return channelState(req, channel);
}, },

View File

@ -1,10 +1,13 @@
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import {promisify} from 'util'; import {promisify} from 'util';
import {validateChannel} from '~/common/channel';
import Leave from '~/common/packets/leave.packet'; import Leave from '~/common/packets/leave.packet';
import {channelUserCounts} from '~/server/entry'; import {channelUserCounts} from '~/server/entry';
import ValidationError from './validation-error';
export const userLeave = async (channel, socket) => { export const userLeave = async (channel, socket) => {
const userId = '/r/anonymous' === channel ? 0 : socket.req.userId; const userId = '/r/anonymous' === channel ? 0 : socket.req.userId;
await promisify(socket.leave.bind(socket))(channel); await promisify(socket.leave.bind(socket))(channel);
@ -16,9 +19,10 @@ export const userLeave = async (channel, socket) => {
export default { export default {
Packet: Leave, Packet: Leave,
validator: async () => true, validator: async () => {
responder: async (packet, socket) => { if (!validateChannel()) {
const {channel} = packet.data; throw new ValidationError({code: 400, reason: 'Malformed channel'});
return userLeave(channel, socket); }
}, },
responder: async ({data: channel}, socket) => userLeave(channel, socket),
}; };

View File

@ -1,19 +1,28 @@
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import {parseChannel} from '~/common/channel'; import {parseChannel, validateChannel} from '~/common/channel';
import Message from '~/common/packets/message.packet'; import Message from '~/common/packets/message.packet';
import {allModels} from '~/server/models/registrar'; import {allModels} from '~/server/models/registrar';
import ValidationError from './validation-error';
export default { export default {
Packet: Message, Packet: Message,
limiter: {points: 10, duration: 15}, limiter: {points: 10, duration: 15},
validator: async () => true, validator: async ({data: {channel, message}}) => {
responder: async (packet, socket, fn) => { if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
if (message.length > 1024) {
throw new ValidationError({code: 400, reason: 'Your message was a bit too long'});
}
},
responder: async ({data}, socket) => {
const {req} = socket; const {req} = socket;
const {pubClient} = req.adapter; const {pubClient} = req.adapter;
const {User} = allModels(); const {User} = allModels();
const {channel, message} = packet.data; const {channel, message} = data;
const {name, type} = parseChannel(`/chat${channel}`); const {name, type} = parseChannel(`/chat${channel}`);
const other = await User.findOne({where: {redditUsername: name}}); const other = await User.findOne({where: {redditUsername: name}});
const owner = '/r/anonymous' === channel ? 0 : req.userId; const owner = '/r/anonymous' === channel ? 0 : req.userId;
@ -26,7 +35,7 @@ export default {
const key = `${serverChannel}:messages:${uuid}`; const key = `${serverChannel}:messages:${uuid}`;
('u' === type ? [`/user/${other.id}`, `/user/${req.userId}`] : [channel]).forEach((room) => ( ('u' === type ? [`/user/${other.id}`, `/user/${req.userId}`] : [channel]).forEach((room) => (
socket.to(room, new Message({ socket.to(room, new Message({
...packet.data, ...data,
channel: 'r' === type channel: 'r' === type
? channel ? channel
: `/u/${username === room.substr(3) ? name : username}`, : `/u/${username === room.substr(3) ? name : username}`,

View File

@ -1,34 +1,37 @@
import {validateChannel} from '~/common/channel';
import RemoveFavorite from '~/common/packets/remove-favorite.packet'; import RemoveFavorite from '~/common/packets/remove-favorite.packet';
import {allModels} from '~/server/models/registrar'; import {allModels} from '~/server/models/registrar';
import ValidationError from './validation-error';
export const removeFavoritedUser = async (socket, user, other) => { export const removeFavoritedUser = async (socket, user, other) => {
const {Favorite} = allModels(); const {Favorite} = allModels();
const favorites = await user.getFavorites(); const favorite = await Favorite.findOne(
const toRemove = favorites.find(({channel}) => channel === `/u/${other.redditUsername}`); {where: {channel: `/u/${other.redditUsername}`, user_id: user.id}},
if (toRemove) { );
await Favorite.destroy({ if (favorite) {
where: { await Favorite.destroy({where: {id: favorite.id}});
id: toRemove.id,
},
});
socket.to(`/user/${user.id}`, new RemoveFavorite(`/u/${other.redditUsername}`)); socket.to(`/user/${user.id}`, new RemoveFavorite(`/u/${other.redditUsername}`));
} }
}; };
export default { export default {
Packet: RemoveFavorite, Packet: RemoveFavorite,
validator: async () => true, validator: async ({data: channel}, {req: {user}}) => {
const {Favorite} = allModels();
if (!validateChannel()) {
throw new ValidationError({code: 400, reason: 'Malformed channel.'});
}
if (0 === await Favorite.count({where: {user_id: user.id, channel}})) {
throw new ValidationError({code: 400, reason: 'No such favorite existed.'});
}
},
responder: async (packet, socket) => { responder: async (packet, socket) => {
const {req} = socket; const {req} = socket;
const {Favorite} = allModels(); const {Favorite} = allModels();
const favorites = await req.user.getFavorites(); const favorite = await Favorite.findOne({where: {channel: packet.data, user_id: req.user.id}});
const toRemove = favorites.find(({channel}) => channel === packet.data); await Favorite.destroy({where: {id: favorite.id}});
await Favorite.destroy({
where: {
id: toRemove.id,
},
});
socket.to(`/user/${req.userId}`, packet); socket.to(`/user/${req.userId}`, packet);
}, },
}; };

View File

@ -6,12 +6,26 @@ import {allModels} from '~/server/models/registrar';
import {removeFavoritedUser} from './remove-favorite'; import {removeFavoritedUser} from './remove-favorite';
import ValidationError from './validation-error';
export default { export default {
Packet: RemoveFriend, Packet: RemoveFriend,
validator: async () => true, validator: async ({data: id}, {req: {userId}}) => {
responder: async (packet, socket) => { const {Friendship} = allModels();
const hasFriendship = !!await Friendship.count({
where: {
[Op.or]: [
{[Op.and]: [{addeeId: userId}, {adderId: id}]},
{[Op.and]: [{addeeId: id}, {adderId: userId}]},
],
},
});
if (!hasFriendship) {
throw new ValidationError({code: 400, reason: 'Malformed friendship.'});
}
},
responder: async ({data: id}, socket) => {
const {req} = socket; const {req} = socket;
const id = packet.data;
const {Friendship, User} = allModels(); const {Friendship, User} = allModels();
await Friendship.destroy({ await Friendship.destroy({
where: { where: {
@ -24,7 +38,9 @@ export default {
socket.to(`/user/${id}`, new RemoveFriend(req.userId)); socket.to(`/user/${id}`, new RemoveFriend(req.userId));
socket.to(`/user/${req.userId}`, new RemoveFriend(id)); socket.to(`/user/${req.userId}`, new RemoveFriend(id));
const user = await User.findByPk(id); const user = await User.findByPk(id);
removeFavoritedUser(socket, user, req.user); return Promise.all([
removeFavoritedUser(socket, req.user, user); removeFavoritedUser(socket, user, req.user),
removeFavoritedUser(socket, req.user, user),
]);
}, },
}; };

View File

@ -2,15 +2,28 @@ import Unblock from '~/common/packets/unblock.packet';
import {allModels} from '~/server/models/registrar'; import {allModels} from '~/server/models/registrar';
import ValidationError from './validation-error';
export default { export default {
Packet: Unblock, Packet: Unblock,
validator: async () => true, validator: async ({data: blocked}, {req: {userId}}) => {
responder: async (packet, socket) => { const {Block: BlockModel} = allModels();
const hasBlock = !!await BlockModel.count({
where: {
blocked,
user_id: userId,
},
});
if (!hasBlock) {
throw new ValidationError({code: 400, reason: "Wasn't blocking."});
}
},
responder: async ({data: blocked}, socket) => {
const {req} = socket; const {req} = socket;
const {Block: BlockModel} = allModels(); const {Block: BlockModel} = allModels();
await BlockModel.destroy({ await BlockModel.destroy({
where: { where: {
blocked: packet.data, blocked,
user_id: req.userId, user_id: req.userId,
}, },
}); });

View File

@ -1 +1,9 @@
export default class ValidationError extends Error {} export default class ValidationError extends Error {
constructor(...args) {
const [payload, ...after] = args;
super(...after);
this.payload = payload;
}
}

View File

@ -10,6 +10,7 @@ import createLimiter from './limiter';
import * as PacketHandlers from './packet'; import * as PacketHandlers from './packet';
import {userJoin} from './packet/join'; import {userJoin} from './packet/join';
import {userLeave} from './packet/leave'; import {userLeave} from './packet/leave';
import ValidationError from './packet/validation-error';
import passport from './passport'; import passport from './passport';
import createRedisClient from './redis'; import createRedisClient from './redis';
import session from './session'; import session from './session';
@ -72,6 +73,10 @@ export function createSocketServer(httpServer) {
fn(undefined, await responder(packet, socket)); fn(undefined, await responder(packet, socket));
} }
catch (error) { catch (error) {
if (error instanceof ValidationError) {
fn(error.payload);
return;
}
if (error instanceof Error) { if (error instanceof Error) {
fn({code: 500}); fn({code: 500});
throw error; throw error;