From 3a7ecf21ccbac974253fb016b36d7fc2794753ed Mon Sep 17 00:00:00 2001 From: HF Date: Thu, 14 May 2020 01:27:27 +0200 Subject: [PATCH] start to move set pixel request to websocket remove killing of old websocket after 30min catch binary ws package errors change CoolDownPacket to ms stop messing with void bot fix reddit login image alt-text force subrender on pixel set to avoid seemingly freeze excape redex characters in username --- src/actions/index.js | 216 ++++++++++++++---------- src/actions/types.js | 12 +- src/client.js | 34 ++-- src/components/Chat.jsx | 5 +- src/components/ReCaptcha.jsx | 23 ++- src/components/UserAreaModal.jsx | 4 +- src/controls/PixelPainterControls.js | 13 +- src/core/ChatProvider.js | 13 -- src/core/draw.js | 214 +++++++++++++++++++++-- src/core/isProxy.js | 2 +- src/core/utils.js | 22 ++- src/data/models/RedisCanvas.js | 25 ++- src/reducers/user.js | 26 +-- src/routes/api/captcha.js | 85 ++++++++++ src/routes/api/index.js | 9 +- src/routes/api/pixel.js | 4 +- src/socket/APISocketServer.js | 6 +- src/socket/ProtocolClient.js | 14 +- src/socket/SocketServer.js | 187 ++++++++++++++------ src/socket/packets/CoolDownPacket.js | 12 +- src/socket/packets/PixelReturn.js | 27 +++ src/socket/packets/PixelUpdate.js | 46 ----- src/socket/packets/PixelUpdateClient.js | 40 +++++ src/socket/packets/PixelUpdateServer.js | 37 ++++ src/socket/websockets.js | 2 +- src/store/analytics.js | 28 --- src/store/audio.js | 3 +- src/store/configureStore.js | 2 - src/store/protocolClientHook.js | 11 ++ src/store/rendererHook.js | 6 + src/store/track.js | 46 ----- src/ui/ChunkRGB3D.js | 2 +- src/ui/Renderer3D.js | 30 +++- 33 files changed, 828 insertions(+), 378 deletions(-) create mode 100644 src/routes/api/captcha.js create mode 100644 src/socket/packets/PixelReturn.js delete mode 100644 src/socket/packets/PixelUpdate.js create mode 100644 src/socket/packets/PixelUpdateClient.js create mode 100644 src/socket/packets/PixelUpdateServer.js delete mode 100644 src/store/analytics.js delete mode 100644 src/store/track.js diff --git a/src/actions/index.js b/src/actions/index.js index 52ff9a2..e1f3eb7 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -163,19 +163,15 @@ export function selectCanvas(canvasId: number): Action { }; } -export function placePixel(coordinates: Cell, color: ColorIndex): Action { +export function placedPixel(): Action { return { type: 'PLACE_PIXEL', - coordinates, - color, }; } -export function pixelWait(coordinates: Cell, color: ColorIndex): Action { +export function pixelWait(): Action { return { type: 'PIXEL_WAIT', - coordinates, - color, }; } @@ -230,92 +226,120 @@ export function notify(notification: string) { }; } -export function requestPlacePixel( - canvasId: number, - coordinates: Cell, +let pixelTimeout = null; + +export function tryPlacePixel( + i: number, + j: number, + offset: number, color: ColorIndex, - token: ?string = null, ): ThunkAction { - const [x, y, z] = coordinates; - return async (dispatch) => { - const body = JSON.stringify({ - cn: canvasId, - x, - y, - z, - clr: color, - token, + pixelTimeout = Date.now() + 5000; + await dispatch(setPlaceAllowed(false)); + + // TODO: + // this is for resending after captcha returned + // window is ugly, put it into redux or something + window.pixel = { + i, + j, + offset, + color, + }; + + dispatch({ + type: 'REQUEST_PLACE_PIXEL', + i, + j, + offset, + color, }); - - dispatch(setPlaceAllowed(false)); - try { - const response = await fetch('/api/pixel', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body, - // https://github.com/github/fetch/issues/349 - credentials: 'include', - }); - const { - success, - waitSeconds, - coolDownSeconds, - errors, - errorTitle, - } = await response.json(); - - if (waitSeconds) { - dispatch(setWait(waitSeconds * 1000)); - } - const coolDownNotify = Math.round(coolDownSeconds); - if (coolDownSeconds) { - dispatch(notify(coolDownNotify)); - } - if (response.ok) { - if (success) { - dispatch(placePixel(coordinates, color)); - } else { - dispatch(pixelWait(coordinates, color)); - } - return; - } - - if (response.status === 422) { - window.pixel = { canvasId, coordinates, color }; - window.grecaptcha.execute(); - return; - } - - dispatch(pixelFailure()); - dispatch(sweetAlert( - (errorTitle || `Error ${response.status}`), - errors[0].msg, - 'error', - 'OK', - )); - } finally { - dispatch(setPlaceAllowed(true)); - } }; } -export function tryPlacePixel( - coordinates: Cell, - color: ?ColorIndex = null, +export function receivePixelReturn( + retCode: number, + wait: number, + coolDownSeconds: number, ): ThunkAction { - return (dispatch, getState) => { - const state = getState(); - const { - canvasId, - } = state.canvas; - const selectedColor = (color === undefined || color === null) - ? state.canvas.selectedColor - : color; + return (dispatch) => { + try { + if (wait) { + dispatch(setWait(wait)); + } + if (coolDownSeconds) { + dispatch(notify(coolDownSeconds)); + } - dispatch(requestPlacePixel(canvasId, coordinates, selectedColor)); + let errorTitle = null; + let msg = null; + switch (retCode) { + case 0: + dispatch(placedPixel()); + break; + case 1: + errorTitle = 'Invalid Canvas'; + msg = 'This canvas doesn\'t exist'; + break; + case 2: + errorTitle = 'Invalid Coordinates'; + msg = 'x out of bounds'; + break; + case 3: + errorTitle = 'Invalid Coordinates'; + msg = 'y out of bounds'; + break; + case 4: + errorTitle = 'Invalid Coordinates'; + msg = 'z out of bounds'; + break; + case 5: + errorTitle = 'Wrong Color'; + msg = 'Invalid color selected'; + break; + case 6: + errorTitle = 'Just for registered Users'; + msg = 'You have to be logged in to place on this canvas'; + break; + case 7: + errorTitle = 'Place more :)'; + // eslint-disable-next-line max-len + msg = 'You can not access this canvas yet. You need to place more pixels'; + break; + case 8: + errorTitle = 'Oww noo'; + msg = 'This pixel is protected.'; + break; + case 9: + // pixestack used up + dispatch(pixelWait()); + break; + case 10: + // captcha + window.grecaptcha.execute(); + break; + case 11: + errorTitle = 'No Proxies Allowed :('; + msg = 'You are using a Proxy.'; + break; + default: + errorTitle = 'Weird'; + msg = 'Couldn\'t set Pixel'; + } + if (msg) { + dispatch(pixelFailure()); + dispatch(sweetAlert( + (errorTitle || `Error ${retCode}`), + msg, + 'error', + 'OK', + )); + } + } finally { + pixelTimeout = null; + dispatch(setPlaceAllowed(true)); + } }; } @@ -428,11 +452,11 @@ export function receiveBigChunkFailure(center: Cell, error: Error): Action { } export function receiveCoolDown( - waitSeconds: number, + wait: number, ): Action { return { type: 'RECEIVE_COOLDOWN', - waitSeconds, + wait, }; } @@ -565,14 +589,28 @@ function endCoolDown(): Action { function getPendingActions(state): Array { const actions = []; + const now = Date.now(); const { wait } = state.user; - if (wait === null || wait === undefined) return actions; - const coolDown = wait - Date.now(); + const coolDown = wait - now; - if (coolDown > 0) actions.push(setCoolDown(coolDown)); - else actions.push(endCoolDown()); + if (wait !== null && wait !== undefined) { + if (coolDown > 0) actions.push(setCoolDown(coolDown)); + else actions.push(endCoolDown()); + } + + if (pixelTimeout && now > pixelTimeout) { + actions.push(pixelFailure()); + pixelTimeout = null; + actions.push(setPlaceAllowed(true)); + actions.push(sweetAlert( + 'Error :(', + 'Didn\'t get an answer from pixelplanet. Maybe try to refresh?', + 'error', + 'OK', + )); + } return actions; } diff --git a/src/actions/types.js b/src/actions/types.js index 66ebbfc..d1abd80 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -32,13 +32,20 @@ export type Action = | { type: 'SET_HOVER', hover: Cell } | { type: 'UNSET_HOVER' } | { type: 'SET_WAIT', wait: ?number } + | { type: 'RECEIVE_COOLDOWN', wait: number } | { type: 'SET_MOBILE', mobile: boolean } | { type: 'COOLDOWN_END' } | { type: 'COOLDOWN_SET', coolDown: number } | { type: 'SELECT_COLOR', color: ColorIndex } | { type: 'SELECT_CANVAS', canvasId: number } - | { type: 'PLACE_PIXEL', coordinates: Cell, color: ColorIndex, wait: string } - | { type: 'PIXEL_WAIT', coordinates: Cell, color: ColorIndex, wait: string } + | { type: 'REQUEST_PLACE_PIXEL', + i: number, + j: number, + offset: number, + color: ColorIndex, + } + | { type: 'PLACE_PIXEL' } + | { type: 'PIXEL_WAIT' } | { type: 'PIXEL_FAILURE' } | { type: 'SET_VIEW_COORDINATES', view: Cell } | { type: 'SET_SCALE', scale: number, zoompoint: Cell } @@ -46,7 +53,6 @@ export type Action = | { type: 'PRE_LOADED_BIG_CHUNK', center: Cell } | { type: 'RECEIVE_BIG_CHUNK', center: Cell } | { type: 'RECEIVE_BIG_CHUNK_FAILURE', center: Cell, error: Error } - | { type: 'RECEIVE_COOLDOWN', waitSeconds: number } | { type: 'RECEIVE_PIXEL_UPDATE', i: number, j: number, diff --git a/src/client.js b/src/client.js index 6ddb9b4..1222fbe 100644 --- a/src/client.js +++ b/src/client.js @@ -1,5 +1,6 @@ /* @flow */ +// eslint-disable-next-line no-unused-vars import fetch from 'isomorphic-fetch'; // TODO put in the beggining with webpack! import './styles/font.css'; @@ -8,14 +9,15 @@ import './styles/font.css'; import onKeyPress from './controls/keypress'; import { receivePixelUpdate, - receiveCoolDown, fetchMe, fetchStats, initTimer, urlChange, receiveOnline, + receiveCoolDown, receiveChatMessage, receiveChatHistory, + receivePixelReturn, setMobile, } from './actions'; import store from './ui/store'; @@ -35,8 +37,13 @@ function init() { }) => { store.dispatch(receivePixelUpdate(i, j, offset, color)); }); - ProtocolClient.on('cooldownPacket', (waitSeconds) => { - store.dispatch(receiveCoolDown(waitSeconds)); + ProtocolClient.on('pixelReturn', ({ + retCode, wait, coolDownSeconds, + }) => { + store.dispatch(receivePixelReturn(retCode, wait, coolDownSeconds)); + }); + ProtocolClient.on('cooldownPacket', (coolDown) => { + store.dispatch(receiveCoolDown(coolDown)); }); ProtocolClient.on('onlineCounter', ({ online }) => { store.dispatch(receiveOnline(online)); @@ -74,27 +81,6 @@ function init() { store.dispatch(fetchStats()); setInterval(() => { store.dispatch(fetchStats()); }, 300000); - - // mess with void bot :) - function ayylmao() { - let cnt = 0; - for (let i = 0; i < document.body.children.length; i += 1) { - const node = document.body.children[i]; - if (node.nodeName === 'SCRIPT' && node.src === '') { - cnt += 1; - } - } - if (cnt > 1) { - document.body.style.setProperty( - '-webkit-transform', 'rotate(-180deg)', - null, - ); - fetch('https://assets.pixelplanet.fun/iamabot'); - window.fetch = () => true; - } - } - ayylmao(); - setInterval(ayylmao, 120000); } init(); diff --git a/src/components/Chat.jsx b/src/components/Chat.jsx index 36c1fe7..6fdbe79 100644 --- a/src/components/Chat.jsx +++ b/src/components/Chat.jsx @@ -18,6 +18,9 @@ import ProtocolClient from '../socket/ProtocolClient'; import { saveSelection, restoreSelection } from '../utils/storeSelection'; import splitChatMessage from '../core/chatMessageFilter'; +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} const Chat = ({ chatMessages, @@ -50,7 +53,7 @@ const Chat = ({ useEffect(() => { const regExp = (ownName) - ? new RegExp(`(^|\\s+)(@${ownName})(\\s+|$)`, 'g') + ? new RegExp(`(^|\\s+)(@${escapeRegExp(ownName)})(\\s+|$)`, 'g') : null; setNameRegExp(regExp); }, [ownName]); diff --git a/src/components/ReCaptcha.jsx b/src/components/ReCaptcha.jsx index 0f6303b..e301d79 100644 --- a/src/components/ReCaptcha.jsx +++ b/src/components/ReCaptcha.jsx @@ -8,13 +8,28 @@ import React from 'react'; import store from '../ui/store'; -import { requestPlacePixel } from '../actions'; +import { tryPlacePixel } from '../actions'; -function onCaptcha(token: string) { - const { canvasId, coordinates, color } = window.pixel; +async function onCaptcha(token: string) { + const body = JSON.stringify({ + token, + }); + await fetch('/api/captcha', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body, + // https://github.com/github/fetch/issues/349 + credentials: 'include', + }); + + const { + i, j, offset, color, + } = window.pixel; + store.dispatch(tryPlacePixel(i, j, offset, color)); - store.dispatch(requestPlacePixel(canvasId, coordinates, color, token)); window.grecaptcha.reset(); } // https://stackoverflow.com/questions/41717304/recaptcha-google-data-callback-with-angularjs diff --git a/src/components/UserAreaModal.jsx b/src/components/UserAreaModal.jsx index 303f6d1..86ba7fd 100644 --- a/src/components/UserAreaModal.jsx +++ b/src/components/UserAreaModal.jsx @@ -65,7 +65,7 @@ const LogInArea = ({ register, forgotPassword, me }) => ( style={logoStyle} width={32} src={`${window.assetserver}/vklogo.svg`} - alt="vk" + alt="VK" /> @@ -73,7 +73,7 @@ const LogInArea = ({ register, forgotPassword, me }) => ( style={logoStyle} width={32} src={`${window.assetserver}/redditlogo.svg`} - alt="vk" + alt="Reddit" />

or register here:

diff --git a/src/controls/PixelPainterControls.js b/src/controls/PixelPainterControls.js index 2589cb9..ff1228e 100644 --- a/src/controls/PixelPainterControls.js +++ b/src/controls/PixelPainterControls.js @@ -24,6 +24,8 @@ import { } from '../actions'; import { screenToWorld, + getChunkOfPixel, + getOffsetOfPixel, } from '../core/utils'; let store = null; @@ -145,11 +147,14 @@ export function initControls(renderer, viewport: HTMLCanvasElement, curStore) { if (!placeAllowed) return; - // dirty trick: to fetch only before multiple 3 AND on user action - // if (pixelsPlaced % 3 === 0) requestAds(); - if (selectedColor !== renderer.getColorIndexOfPixel(...cell)) { - store.dispatch(tryPlacePixel(cell)); + const { canvasSize } = state.canvas; + const [i, j] = getChunkOfPixel(canvasSize, ...cell); + const offset = getOffsetOfPixel(canvasSize, ...cell); + store.dispatch(tryPlacePixel( + i, j, offset, + selectedColor, + )); } }); diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js index 6fa594f..e3d9c8c 100644 --- a/src/core/ChatProvider.js +++ b/src/core/ChatProvider.js @@ -7,11 +7,6 @@ import User from '../data/models/User'; import webSockets from '../socket/websockets'; import { CHAT_CHANNELS } from './constants'; -import { cheapDetector } from './isProxy'; -import { - USE_PROXYCHECK, -} from './config'; - export class ChatProvider { /* @@ -93,14 +88,6 @@ export class ChatProvider { } } - if (USE_PROXYCHECK && user.ip && await cheapDetector(user.ip)) { - logger.info( - `${name} / ${user.ip} tried to send chat message with proxy`, - ); - return 'You can not send chat messages with a proxy'; - } - - for (let i = 0; i < this.substitutes.length; i += 1) { const subsitute = this.substitutes[i]; message = message.replace(subsitute.regexp, subsitute.replace); diff --git a/src/core/draw.js b/src/core/draw.js index 8626f65..0543558 100644 --- a/src/core/draw.js +++ b/src/core/draw.js @@ -4,24 +4,30 @@ import { using } from 'bluebird'; import type { User } from '../data/models'; import { redlock } from '../data/redis'; -import { getChunkOfPixel, getOffsetOfPixel } from './utils'; +import { + getChunkOfPixel, + getOffsetOfPixel, + getPixelFromChunkOffset, +} from './utils'; import webSockets from '../socket/websockets'; -import logger from './logger'; +import logger, { pixelLogger } from './logger'; import RedisCanvas from '../data/models/RedisCanvas'; // eslint-disable-next-line import/no-unresolved import canvases from './canvases.json'; -import { THREE_CANVAS_HEIGHT } from './constants'; +import { THREE_CANVAS_HEIGHT, THREE_TILE_SIZE, TILE_SIZE } from './constants'; /** * * @param canvasId + * @param canvasId + * @param color * @param x * @param y - * @param color + * @param z optional, if given its 3d canvas */ -export function setPixel( +export function setPixelByCoords( canvasId: number, color: ColorIndex, x: number, @@ -37,6 +43,150 @@ export function setPixel( /** * + * By Offset is prefered on server side + * @param canvasId + * @param i Chunk coordinates + * @param j + * @param offset Offset of pixel withing chunk + */ +export function setPixelByOffset( + canvasId: number, + color: ColorIndex, + i: number, + j: number, + offset: number, +) { + RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId); + webSockets.broadcastPixel(canvasId, i, j, offset, color); +} + +/** + * + * By Offset is prefered on server side + * This gets used by websocket pixel placing requests + * @param user user that can be registered, but doesn't have to + * @param canvasId + * @param i Chunk coordinates + * @param j + * @param offset Offset of pixel withing chunk + */ +export async function drawByOffset( + user: User, + canvasId: number, + color: ColorIndex, + i: number, + j: number, + offset: number, +): Promise { + let wait = 0; + let coolDown = 0; + let retCode = 0; + + logger.info(`Got request for ${canvasId} ${i} ${j} ${offset} ${color}`); + + const canvas = canvases[canvasId]; + if (!canvas) { + // canvas doesn't exist + return { + wait, + coolDown, + retCode: 1, + }; + } + const { size: canvasSize, v: is3d } = canvas; + + try { + const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE; + if (i >= canvasSize / tileSize) { + // x out of bounds + throw new Error(2); + } + if (j >= canvasSize / tileSize) { + // y out of bounds + throw new Error(3); + } + const maxSize = (is3d) ? tileSize * tileSize * THREE_CANVAS_HEIGHT + : tileSize * tileSize; + if (offset >= maxSize) { + // z out of bounds or weird stuff + throw new Error(4); + } + if (color >= canvas.colors.length) { + // color out of bounds + throw new Error(5); + } + + if (canvas.req !== -1) { + if (user.id === null) { + // not logged in + throw new Error(6); + } + const totalPixels = await user.getTotalPixels(); + if (totalPixels < canvas.req) { + // not enough pixels placed yet + throw new Error(7); + } + } + + const setColor = await RedisCanvas.getPixelByOffset(canvasId, i, j, offset); + if (setColor & 0x80 + /* 3D Canvas Minecraft Avatars */ + // && x >= 96 && x <= 128 && z >= 35 && z <= 100 + // 96 - 128 on x + // 32 - 128 on z + || (canvas.v && i === 19 && j >= 17 && j < 20 && !user.isAdmin()) + ) { + // protected pixel + throw new Error(8); + } + + coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd; + if (user.isAdmin()) { + coolDown = 0.0; + } + + const now = Date.now(); + wait = await user.getWait(canvasId); + if (!wait) wait = now; + wait += coolDown; + const waitLeft = wait - now; + if (waitLeft > canvas.cds) { + // cooldown stack used + wait = waitLeft - coolDown; + coolDown = canvas.cds - waitLeft; + throw new Error(9); + } + + setPixelByOffset(canvasId, color, i, j, offset); + + user.setWait(waitLeft, canvasId); + if (canvas.ranked) { + user.incrementPixelcount(); + } + wait = waitLeft; + } catch (e) { + retCode = parseInt(e.message, 10); + if (Number.isNaN(retCode)) { + throw e; + } + } + + const [x, y, z] = getPixelFromChunkOffset(i, j, offset, canvasSize, is3d); + // eslint-disable-next-line max-len + pixelLogger.info(`${user.ip} ${user.id} ${canvasId} ${x} ${y} ${z} ${color} ${retCode}`); + + return { + wait, + coolDown, + retCode, + }; +} + + +/** + * + * Old version of draw that returns explicit error messages + * used for http json api/pixel, used with coordinates * @param user * @param canvasId * @param x @@ -44,7 +194,7 @@ export function setPixel( * @param color * @returns {Promise.} */ -async function draw( +export async function drawByCoords( user: User, canvasId: number, color: ColorIndex, @@ -178,7 +328,7 @@ async function draw( }; } - setPixel(canvasId, color, x, y, z); + setPixelByCoords(canvasId, color, x, y, z); user.setWait(waitLeft, canvasId); if (canvas.ranked) { @@ -191,18 +341,20 @@ async function draw( }; } + /** * This function is a wrapper for draw. It fixes race condition exploits * It permits just placing one pixel at a time per user. * * @param user * @param canvasId + * @param color * @param x * @param y - * @param color + * @param z (optional for 3d canvas) * @returns {Promise.} */ -function drawSafe( +export function drawSafeByCoords( user: User, canvasId: number, color: ColorIndex, @@ -211,7 +363,7 @@ function drawSafe( z: number = null, ): Promise { if (user.isAdmin()) { - return draw(user, canvasId, color, x, y, z); + return drawByCoords(user, canvasId, color, x, y, z); } // can just check for one unique occurence, @@ -223,7 +375,7 @@ function drawSafe( using( redlock.disposer(`locks:${userId}`, 5000, logger.error), async () => { - const ret = await draw(user, canvasId, color, x, y, z); + const ret = await drawByCoords(user, canvasId, color, x, y, z); resolve(ret); }, ); // <-- unlock is automatically handled by bluebird @@ -231,6 +383,42 @@ function drawSafe( } -export const drawUnsafe = draw; +/** + * This function is a wrapper for draw. It fixes race condition exploits + * It permits just placing one pixel at a time per user. + * + * @param user + * @param canvasId + * @param color + * @param i Chunk coordinates + * @param j + * @param offset Offset of pixel withing chunk + * @returns {Promise.} + */ +export function drawSafeByOffset( + user: User, + canvasId: number, + color: ColorIndex, + i: number, + j: number, + offset: number, +): Promise { + if (user.isAdmin()) { + return drawByOffset(user, canvasId, color, i, j, offset); + } -export default drawSafe; + // can just check for one unique occurence, + // we use ip, because id for logged out users is + // always null + const userId = user.ip; + + return new Promise((resolve) => { + using( + redlock.disposer(`locks:${userId}`, 5000, logger.error), + async () => { + const ret = await drawByOffset(user, canvasId, color, i, j, offset); + resolve(ret); + }, + ); // <-- unlock is automatically handled by bluebird + }); +} diff --git a/src/core/isProxy.js b/src/core/isProxy.js index 7d7bd15..ed6a4fd 100644 --- a/src/core/isProxy.js +++ b/src/core/isProxy.js @@ -162,7 +162,7 @@ async function withoutCache(f, ip) { let lock = 4; const checking = []; async function withCache(f, ip) { - if (!ip) return true; + if (!ip || ip === '0.0.0.1') return true; // get from cache, if there const ipKey = getIPv6Subnet(ip); const key = `isprox:${ipKey}`; diff --git a/src/core/utils.js b/src/core/utils.js index c2a2fb7..be9d130 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -38,6 +38,8 @@ export function clamp(n: number, min: number, max: number): number { return Math.max(min, Math.min(n, max)); } +// z is assumed to be height here +// in ui and rendeer, y is height export function getChunkOfPixel( canvasSize: number, x: number, @@ -109,18 +111,26 @@ export function getIdFromObject(obj: Object, ident: string): number { return null; } +// z is returned as height here +// in ui and rendeer, y is height export function getPixelFromChunkOffset( i: number, j: number, offset: number, canvasSize: number, + is3d: boolean = false, ): Cell { - const cx = mod(offset, TILE_SIZE); - const cy = Math.floor(offset / TILE_SIZE); - const devOffset = canvasSize / 2 / TILE_SIZE; - const x = ((i - devOffset) * TILE_SIZE) + cx; - const y = ((j - devOffset) * TILE_SIZE) + cy; - return [x, y]; + const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE; + const cx = offset % tileSize; + const off = offset - cx; + let cy = off % (tileSize * tileSize); + const z = (is3d) ? (off - cy) / tileSize / tileSize : null; + cy /= tileSize; + + const devOffset = canvasSize / 2 / tileSize; + const x = ((i - devOffset) * tileSize) + cx; + const y = ((j - devOffset) * tileSize) + cy; + return [x, y, z]; } export function getCellInsideChunk( diff --git a/src/data/models/RedisCanvas.js b/src/data/models/RedisCanvas.js index f9fc591..d5f3069 100644 --- a/src/data/models/RedisCanvas.js +++ b/src/data/models/RedisCanvas.js @@ -94,16 +94,13 @@ class RedisCanvas { static async getPixelIfExists( canvasId: number, - x: number, - y: number, - z: number = null, + i: number, + j: number, + offset: number, ): Promise { // 1st bit -> protected or not // 2nd bit -> unused // rest (6 bits) -> index of color - const canvasSize = canvases[canvasId].size; - const [i, j] = getChunkOfPixel(canvasSize, x, y, z); - const offset = getOffsetOfPixel(canvasSize, x, y, z); const args = [ `ch:${canvasId}:${i}:${j}`, 'GET', @@ -116,13 +113,27 @@ class RedisCanvas { return color; } + static async getPixelByOffset( + canvasId: number, + i: number, + j: number, + offset: number, + ): Promise { + const clr = RedisCanvas.getPixelIfExists(canvasId, i, j, offset); + return (clr == null) ? 0 : clr; + } + static async getPixel( canvasId: number, x: number, y: number, z: number = null, ): Promise { - const clr = RedisCanvas.getPixelIfExists(canvasId, x, y, z); + const canvasSize = canvases[canvasId].size; + const [i, j] = getChunkOfPixel(canvasSize, x, y, z); + const offset = getOffsetOfPixel(canvasSize, x, y, z); + + const clr = RedisCanvas.getPixelIfExists(canvasId, i, j, offset); return (clr == null) ? 0 : clr; } } diff --git a/src/reducers/user.js b/src/reducers/user.js index 4598161..87bfd8f 100644 --- a/src/reducers/user.js +++ b/src/reducers/user.js @@ -64,7 +64,7 @@ export default function user( const { coolDown } = action; return { ...state, - coolDown, + coolDown: coolDown || null, }; } @@ -96,6 +96,18 @@ export default function user( }; } + case 'RECEIVE_COOLDOWN': { + const { wait: duration } = action; + const wait = duration + ? new Date(Date.now() + duration) + : null; + return { + ...state, + wait, + coolDown: null, + }; + } + case 'SET_MOBILE': { const { mobile: isOnMobile } = action; return { @@ -150,18 +162,6 @@ export default function user( }; } - case 'RECEIVE_COOLDOWN': { - const { waitSeconds } = action; - const wait = waitSeconds - ? new Date(Date.now() + waitSeconds * 1000) - : null; - return { - ...state, - wait, - coolDown: null, - }; - } - case 'RECEIVE_ME': { const { name, diff --git a/src/routes/api/captcha.js b/src/routes/api/captcha.js new file mode 100644 index 0000000..8b43847 --- /dev/null +++ b/src/routes/api/captcha.js @@ -0,0 +1,85 @@ +/* + * This is just for verifying captcha tokens, + * the actual notification that a captcha is needed is sent + * with the pixel return answer when sending apixel on websocket + * + * @flow + */ +import type { Request, Response } from 'express'; + +import logger from '../../core/logger'; +import redis from '../../data/redis'; +import verifyCaptcha from '../../utils/recaptcha'; + +import { + RECAPTCHA_SECRET, + RECAPTCHA_TIME, +} from '../../core/config'; + +const TTL_CACHE = RECAPTCHA_TIME * 60; // seconds + +export default async (req: Request, res: Response) => { + if (!RECAPTCHA_SECRET) { + res.status(200) + .json({ + errors: [{ + msg: + 'No need for a captcha here', + }], + }); + return; + } + + const user = req.user || req.noauthUser; + const { ip } = user; + + try { + const { token } = req.body; + if (!token) { + res.status(400) + .json({ errors: [{ msg: 'No token given' }] }); + return; + } + + const key = `human:${ip}`; + + const ttl: number = await redis.ttlAsync(key); + if (ttl > 0) { + res.status(400) + .json({ + errors: [{ + msg: + 'Why would you even want to solve a captcha?', + }], + }); + return; + } + + if (!await verifyCaptcha(token, ip)) { + logger.info(`CAPTCHA ${ip} failed his captcha`); + res.status(422) + .json({ + errors: [{ + msg: + 'You failed your captcha', + }], + }); + return; + } + + // save to cache + await redis.setAsync(key, 'y', 'EX', TTL_CACHE); + + res.status(200) + .json({ success: true }); + } catch (error) { + logger.error('checkHuman', error); + res.status(500) + .json({ + errors: [{ + msg: + 'Server error occured', + }], + }); + } +}; diff --git a/src/routes/api/index.js b/src/routes/api/index.js index d857843..ba9143b 100644 --- a/src/routes/api/index.js +++ b/src/routes/api/index.js @@ -13,7 +13,8 @@ import { getIPFromRequest, getIPv6Subnet } from '../../utils/ip'; import me from './me'; import mctp from './mctp'; -import pixel from './pixel'; +// import pixel from './pixel'; +import captcha from './captcha'; import auth from './auth'; import ranking from './ranking'; import history from './history'; @@ -63,7 +64,11 @@ router.use((err, req, res, next) => { * rate limiting should occure outside, * with nginx or whatever */ -router.post('/pixel', pixel); +/* api pixel got deactivated in favor of websocket */ +/* keeping it still here to enable it again if needed */ +// router.post('/pixel', pixel); + +router.post('/captcha', captcha); /* * passport authenticate diff --git a/src/routes/api/pixel.js b/src/routes/api/pixel.js index 982bae6..086843a 100644 --- a/src/routes/api/pixel.js +++ b/src/routes/api/pixel.js @@ -5,7 +5,7 @@ import type { Request, Response } from 'express'; -import draw from '../../core/draw'; +import { drawSafeByCoords } from '../../core/draw'; import { blacklistDetector, cheapDetector, @@ -197,7 +197,7 @@ async function place(req: Request, res: Response) { const { errorTitle, error, success, waitSeconds, coolDownSeconds, - } = await draw(user, cn, clr, x, y, z); + } = await drawSafeByCoords(user, cn, clr, x, y, z); logger.log('debug', success); if (success) { diff --git a/src/socket/APISocketServer.js b/src/socket/APISocketServer.js index ed45d63..8e68d26 100644 --- a/src/socket/APISocketServer.js +++ b/src/socket/APISocketServer.js @@ -15,7 +15,7 @@ import WebSocketEvents from './WebSocketEvents'; import webSockets from './websockets'; import { getIPFromRequest } from '../utils/ip'; import Minecraft from '../core/minecraft'; -import { drawUnsafe, setPixel } from '../core/draw'; +import { drawByCoords, setPixelByCoords } from '../core/draw'; import logger from '../core/logger'; import { APISOCKET_KEY } from '../core/config'; import chatProvider from '../core/ChatProvider'; @@ -192,7 +192,7 @@ class APISocketServer extends WebSocketEvents { if (clr < 0 || clr > 32) return; // be aware that user null has no cd if (!minecraftid && !ip) { - setPixel('0', clr, x, y); + setPixelByCoords('0', clr, x, y); ws.send(JSON.stringify(['retpxl', null, null, true, 0, 0])); return; } @@ -200,7 +200,7 @@ class APISocketServer extends WebSocketEvents { user.ip = ip; const { error, success, waitSeconds, coolDownSeconds, - } = await drawUnsafe(user, '0', clr, x, y, null); + } = await drawByCoords(user, '0', clr, x, y, null); ws.send(JSON.stringify([ 'retpxl', (minecraftid) || ip, diff --git a/src/socket/ProtocolClient.js b/src/socket/ProtocolClient.js index 7e22ddb..0d8453d 100644 --- a/src/socket/ProtocolClient.js +++ b/src/socket/ProtocolClient.js @@ -9,7 +9,8 @@ import EventEmitter from 'events'; import CoolDownPacket from './packets/CoolDownPacket'; -import PixelUpdate from './packets/PixelUpdate'; +import PixelUpdate from './packets/PixelUpdateClient'; +import PixelReturn from './packets/PixelReturn'; import OnlineCounter from './packets/OnlineCounter'; import RegisterCanvas from './packets/RegisterCanvas'; import RegisterChunk from './packets/RegisterChunk'; @@ -130,6 +131,14 @@ class ProtocolClient extends EventEmitter { if (~pos) chunks.splice(pos, 1); } + requestPlacePixel( + i, j, offset, + color, + ) { + const buffer = PixelUpdate.dehydrate(i, j, offset, color); + this.sendWhenReady(buffer); + } + requestChatHistory() { const buffer = RequestChatHistory.dehydrate(); if (this.isConnected) this.ws.send(buffer); @@ -186,6 +195,9 @@ class ProtocolClient extends EventEmitter { case PixelUpdate.OP_CODE: this.emit('pixelUpdate', PixelUpdate.hydrate(data)); break; + case PixelReturn.OP_CODE: + this.emit('pixelReturn', PixelReturn.hydrate(data)); + break; case OnlineCounter.OP_CODE: this.emit('onlineCounter', OnlineCounter.hydrate(data)); break; diff --git a/src/socket/SocketServer.js b/src/socket/SocketServer.js index 3a2f925..f25400e 100644 --- a/src/socket/SocketServer.js +++ b/src/socket/SocketServer.js @@ -8,13 +8,15 @@ import Counter from '../utils/Counter'; import RateLimiter from '../utils/RateLimiter'; import { getIPFromRequest } from '../utils/ip'; +import CoolDownPacket from './packets/CoolDownPacket'; +import PixelUpdate from './packets/PixelUpdateServer'; +import PixelReturn from './packets/PixelReturn'; import RegisterCanvas from './packets/RegisterCanvas'; import RegisterChunk from './packets/RegisterChunk'; import RegisterMultipleChunks from './packets/RegisterMultipleChunks'; import DeRegisterChunk from './packets/DeRegisterChunk'; import DeRegisterMultipleChunks from './packets/DeRegisterMultipleChunks'; import RequestChatHistory from './packets/RequestChatHistory'; -import CoolDownPacket from './packets/CoolDownPacket'; import ChangedMe from './packets/ChangedMe'; import OnlineCounter from './packets/OnlineCounter'; @@ -22,6 +24,14 @@ import chatProvider, { ChatProvider } from '../core/ChatProvider'; import authenticateClient from './verifyClient'; import WebSocketEvents from './WebSocketEvents'; import webSockets from './websockets'; +import { drawSafeByOffset } from '../core/draw'; + +import redis from '../data/redis'; +import { cheapDetector, blacklistDetector } from '../core/isProxy'; +import { + USE_PROXYCHECK, + RECAPTCHA_SECRET, +} from '../core/config'; const ipCounter: Counter = new Counter(); @@ -32,10 +42,11 @@ function heartbeat() { async function verifyClient(info, done) { const { req } = info; + const { headers } = req; // Limiting socket connections per ip const ip = await getIPFromRequest(req); - logger.info(`Got ws request from ${ip}`); + logger.info(`Got ws request from ${ip} via ${headers.origin}`); if (ipCounter.get(ip) > 50) { logger.info(`Client ${ip} has more than 50 connections open.`); return done(false); @@ -81,7 +92,7 @@ class SocketServer extends WebSocketEvents { ws.user = user; ws.name = (user.regUser) ? user.regUser.name : null; ws.rateLimiter = new RateLimiter(20, 15, true); - const ip = await getIPFromRequest(req); + SocketServer.checkIfProxy(ws); if (ws.name) { ws.send(`"${ws.name}"`); @@ -91,6 +102,7 @@ class SocketServer extends WebSocketEvents { online: this.wss.clients.size || 0, })); + const ip = await getIPFromRequest(req); ws.on('error', (e) => { logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`); }); @@ -111,9 +123,15 @@ class SocketServer extends WebSocketEvents { this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this); this.ping = this.ping.bind(this); - this.killOld = this.killOld.bind(this); - setInterval(this.killOld, 10 * 60 * 1000); + /* + * i don't tink that we really need that, it just stresses the server + * with lots of reconnects at once, the overhead of having a few idle + * connections isn't too bad in comparison + */ + // this.killOld = this.killOld.bind(this); + // setInterval(this.killOld, 10 * 60 * 1000); + setInterval(this.onlineCounterBroadcast, 10 * 1000); // https://github.com/websockets/ws#how-to-detect-and-close-broken-connections setInterval(this.ping, 45 * 1000); @@ -207,7 +225,7 @@ class SocketServer extends WebSocketEvents { const now = Date.now(); this.wss.clients.forEach((ws) => { const lifetime = now - ws.startDate; - if (lifetime > 30 * 60 * 1000) ws.terminate(); + if (lifetime > 30 * 60 * 1000 && Math.random() < 0.3) ws.terminate(); }); } @@ -227,6 +245,16 @@ class SocketServer extends WebSocketEvents { webSockets.broadcastOnlineCounter(online); } + static async checkIfProxy(ws) { + const { ip } = ws.user; + if (USE_PROXYCHECK && ip && await cheapDetector(ip)) { + return true; + } if (await blacklistDetector(ip)) { + return true; + } + return false; + } + static async onTextMessage(text, ws) { try { let message; @@ -247,6 +275,7 @@ class SocketServer extends WebSocketEvents { message = message.trim(); if (ws.name && message) { + const { user } = ws; const waitLeft = ws.rateLimiter.tick(); if (waitLeft) { ws.send(JSON.stringify([ @@ -258,8 +287,22 @@ class SocketServer extends WebSocketEvents { ])); return; } + // check proxy + if (await SocketServer.checkIfProxy(ws)) { + logger.info( + `${ws.name} / ${user.ip} tried to send chat message with proxy`, + ); + ws.send(JSON.stringify([ + 'info', + 'You can not send chat messages with a proxy', + 'il', + channelId, + ])); + return; + } + // const errorMsg = await chatProvider.sendMessage( - ws.user, + user, message, channelId, ); @@ -286,7 +329,7 @@ class SocketServer extends WebSocketEvents { logger.info('Got empty message or message from unidentified ws'); } } catch { - logger.info('Got invalid ws message'); + logger.info('Got invalid ws text message'); } } @@ -294,52 +337,98 @@ class SocketServer extends WebSocketEvents { if (buffer.byteLength === 0) return; const opcode = buffer[0]; - switch (opcode) { - case RegisterCanvas.OP_CODE: { - const canvasId = RegisterCanvas.hydrate(buffer); - if (ws.canvasId !== null && ws.canvasId !== canvasId) { - this.deleteAllChunks(ws); + try { + switch (opcode) { + case PixelUpdate.OP_CODE: { + const { canvasId, user } = ws; + if (canvasId === null) { + return; + } + const { ip } = user; + // check if captcha needed + if (RECAPTCHA_SECRET) { + const key = `human:${ip}`; + const ttl: number = await redis.ttlAsync(key); + if (ttl <= 0) { + // need captcha + logger.info(`CAPTCHA ${ip} / ${ws.name} got captcha`); + ws.send(PixelReturn.dehydrate(10, 0, 0)); + break; + } + } + // (re)check for Proxy + if (await SocketServer.checkIfProxy(ws)) { + ws.send(PixelReturn.dehydrate(11, 0, 0)); + break; + } + // receive pixels here + const { + i, j, offset, + color, + } = PixelUpdate.hydrate(buffer); + const { + wait, + coolDown, + retCode, + } = await drawSafeByOffset( + ws.user, + ws.canvasId, + color, + i, j, offset, + ); + logger.info(`send: ${wait}, ${coolDown}, ${retCode}`); + ws.send(PixelReturn.dehydrate(retCode, wait, coolDown)); + break; } - ws.canvasId = canvasId; - const wait = await ws.user.getWait(canvasId); - const waitSeconds = (wait) ? Math.ceil((wait - Date.now()) / 1000) : 0; - ws.send(CoolDownPacket.dehydrate(waitSeconds)); - break; - } - case RegisterChunk.OP_CODE: { - const chunkid = RegisterChunk.hydrate(buffer); - this.pushChunk(chunkid, ws); - break; - } - case RegisterMultipleChunks.OP_CODE: { - this.deleteAllChunks(ws); - let posu = 2; - while (posu < buffer.length) { - const chunkid = buffer[posu++] | buffer[posu++] << 8; + case RegisterCanvas.OP_CODE: { + const canvasId = RegisterCanvas.hydrate(buffer); + if (ws.canvasId !== null && ws.canvasId !== canvasId) { + this.deleteAllChunks(ws); + } + ws.canvasId = canvasId; + const wait = await ws.user.getWait(canvasId); + const waitMs = (wait) ? wait - Date.now() : 0; + ws.send(CoolDownPacket.dehydrate(waitMs)); + break; + } + case RegisterChunk.OP_CODE: { + const chunkid = RegisterChunk.hydrate(buffer); this.pushChunk(chunkid, ws); + break; } - break; - } - case DeRegisterChunk.OP_CODE: { - const chunkidn = DeRegisterChunk.hydrate(buffer); - this.deleteChunk(chunkidn, ws); - break; - } - case DeRegisterMultipleChunks.OP_CODE: { - let posl = 2; - while (posl < buffer.length) { - const chunkid = buffer[posl++] | buffer[posl++] << 8; - this.deleteChunk(chunkid, ws); + case RegisterMultipleChunks.OP_CODE: { + this.deleteAllChunks(ws); + let posu = 2; + while (posu < buffer.length) { + const chunkid = buffer[posu++] | buffer[posu++] << 8; + this.pushChunk(chunkid, ws); + } + break; } - break; + case DeRegisterChunk.OP_CODE: { + const chunkidn = DeRegisterChunk.hydrate(buffer); + this.deleteChunk(chunkidn, ws); + break; + } + case DeRegisterMultipleChunks.OP_CODE: { + let posl = 2; + while (posl < buffer.length) { + const chunkid = buffer[posl++] | buffer[posl++] << 8; + this.deleteChunk(chunkid, ws); + } + break; + } + case RequestChatHistory.OP_CODE: { + const history = JSON.stringify(chatProvider.history); + ws.send(history); + break; + } + default: + break; } - case RequestChatHistory.OP_CODE: { - const history = JSON.stringify(chatProvider.history); - ws.send(history); - break; - } - default: - break; + } catch (e) { + logger.info('Got invalid ws binary message'); + throw e; } } diff --git a/src/socket/packets/CoolDownPacket.js b/src/socket/packets/CoolDownPacket.js index 21c55ae..4bdb207 100644 --- a/src/socket/packets/CoolDownPacket.js +++ b/src/socket/packets/CoolDownPacket.js @@ -6,16 +6,12 @@ const OP_CODE = 0xC2; export default { OP_CODE, hydrate(data: DataView) { - // SERVER (Client) - const waitSeconds = data.getUint16(1); - return waitSeconds; + return data.getUint32(1); }, - dehydrate(waitSeconds): Buffer { - // CLIENT (Sender) - const buffer = Buffer.allocUnsafe(1 + 2); + dehydrate(wait): Buffer { + const buffer = Buffer.allocUnsafe(1 + 4); buffer.writeUInt8(OP_CODE, 0); - - buffer.writeUInt16BE(waitSeconds, 1); + buffer.writeUInt32BE(wait, 1); return buffer; }, }; diff --git a/src/socket/packets/PixelReturn.js b/src/socket/packets/PixelReturn.js new file mode 100644 index 0000000..44002b7 --- /dev/null +++ b/src/socket/packets/PixelReturn.js @@ -0,0 +1,27 @@ +/* @flow */ + + +const OP_CODE = 0xC3; + +export default { + OP_CODE, + hydrate(data: DataView) { + const retCode = data.getUint8(1); + const wait = data.getUint32(2); + const coolDownSeconds = data.getInt16(6); + return { + retCode, + wait, + coolDownSeconds, + }; + }, + dehydrate(retCode, wait, coolDown): Buffer { + const buffer = Buffer.allocUnsafe(1 + 1 + 4 + 1 + 2); + buffer.writeUInt8(OP_CODE, 0); + buffer.writeUInt8(retCode, 1); + buffer.writeUInt32BE(wait, 2); + const coolDownSeconds = Math.round(coolDown / 1000); + buffer.writeInt16BE(coolDownSeconds, 6); + return buffer; + }, +}; diff --git a/src/socket/packets/PixelUpdate.js b/src/socket/packets/PixelUpdate.js deleted file mode 100644 index 9d4cc2a..0000000 --- a/src/socket/packets/PixelUpdate.js +++ /dev/null @@ -1,46 +0,0 @@ -/* @flow */ - - -import type { ColorIndex } from '../../core/Palette'; - -type PixelUpdatePacket = { - x: number, - y: number, - color: ColorIndex, -}; - -const OP_CODE = 0xC1; // Chunk Update - -export default { - OP_CODE, - hydrate(data: DataView): PixelUpdatePacket { - // CLIENT - const i = data.getUint8(1); - const j = data.getUint8(2); - const offset = (data.getUint8(3) << 16) | data.getUint16(4); - const color = data.getUint8(6); - // const offset = data.getUint16(5); - // const color = data.getUint8(7); - return { - i, j, offset, color, - }; - }, - dehydrate(i, j, offset, color): Buffer { - // SERVER - if (!process.env.BROWSER) { - const buffer = Buffer.allocUnsafe(1 + 1 + 1 + 1 + 2 + 1); - buffer.writeUInt8(OP_CODE, 0); - - buffer.writeUInt8(i, 1); - buffer.writeUInt8(j, 2); - buffer.writeUInt8(offset >>> 16, 3); - buffer.writeUInt16BE(offset & 0x00FFFF, 4); - buffer.writeUInt8(color, 6); - // buffer.writeUInt16BE(offset, 5); - // buffer.writeUInt8(color, 7); - - return buffer; - } - return null; - }, -}; diff --git a/src/socket/packets/PixelUpdateClient.js b/src/socket/packets/PixelUpdateClient.js new file mode 100644 index 0000000..79d5206 --- /dev/null +++ b/src/socket/packets/PixelUpdateClient.js @@ -0,0 +1,40 @@ +/* @flow */ + + +import type { ColorIndex } from '../../core/Palette'; + +type PixelUpdatePacket = { + x: number, + y: number, + color: ColorIndex, +}; + +const OP_CODE = 0xC1; + +export default { + OP_CODE, + hydrate(data: DataView): PixelUpdatePacket { + const i = data.getUint8(1); + const j = data.getUint8(2); + const offset = (data.getUint8(3) << 16) | data.getUint16(4); + const color = data.getUint8(6); + return { + i, j, offset, color, + }; + }, + + dehydrate(i, j, offset, color): Buffer { + const buffer = new ArrayBuffer(1 + 1 + 1 + 1 + 2 + 1); + const view = new DataView(buffer); + view.setUint8(0, OP_CODE); + + view.setUint8(1, i); + view.setUint8(2, j); + view.setUint8(3, offset >>> 16); + view.setUint16(4, offset & 0x00FFFF); + view.setUint8(6, color); + + return buffer; + }, + +}; diff --git a/src/socket/packets/PixelUpdateServer.js b/src/socket/packets/PixelUpdateServer.js new file mode 100644 index 0000000..6d0ce47 --- /dev/null +++ b/src/socket/packets/PixelUpdateServer.js @@ -0,0 +1,37 @@ +/* @flow */ + + +import type { ColorIndex } from '../../core/Palette'; + +type PixelUpdatePacket = { + x: number, + y: number, + color: ColorIndex, +}; + +const OP_CODE = 0xC1; + +export default { + OP_CODE, + hydrate(data: Buffer): PixelUpdatePacket { + const i = data.readUInt8(1); + const j = data.readUInt8(2); + const offset = (data.readUInt8(3) << 16) | data.readUInt16BE(4); + const color = data.readUInt8(6); + return { + i, j, offset, color, + }; + }, + dehydrate(i, j, offset, color): Buffer { + const buffer = Buffer.allocUnsafe(1 + 1 + 1 + 1 + 2 + 1); + buffer.writeUInt8(OP_CODE, 0); + + buffer.writeUInt8(i, 1); + buffer.writeUInt8(j, 2); + buffer.writeUInt8(offset >>> 16, 3); + buffer.writeUInt16BE(offset & 0x00FFFF, 4); + buffer.writeUInt8(color, 6); + + return buffer; + }, +}; diff --git a/src/socket/websockets.js b/src/socket/websockets.js index c49b6fb..62fbb59 100644 --- a/src/socket/websockets.js +++ b/src/socket/websockets.js @@ -6,7 +6,7 @@ */ import OnlineCounter from './packets/OnlineCounter'; -import PixelUpdate from './packets/PixelUpdate'; +import PixelUpdate from './packets/PixelUpdateServer'; class WebSockets { diff --git a/src/store/analytics.js b/src/store/analytics.js deleted file mode 100644 index 03fe3f7..0000000 --- a/src/store/analytics.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright 2016 Facebook, Inc. - * - * You are hereby granted a non-exclusive, worldwide, royalty-free license to - * use, copy, modify, and distribute this software in source code or binary - * form for use in connection with the web services and APIs provided by - * Facebook. - * - * As with any software that integrates with the Facebook platform, your use - * of this software is subject to the Facebook Developer Principles and - * Policies [http://developers.facebook.com/policy/]. This copyright notice - * shall be included in all copies or substantial portions of the software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE - */ - -import track from './track'; - -export default () => (next) => (action) => { - track(action); - return next(action); -}; diff --git a/src/store/audio.js b/src/store/audio.js index b87f0af..9c66fdc 100644 --- a/src/store/audio.js +++ b/src/store/audio.js @@ -105,8 +105,7 @@ export default (store) => (next) => (action) => { case 'PLACE_PIXEL': { if (mute) break; - const { color } = action; - const { palette } = state.canvas; + const { palette, selectedColor: color } = state.canvas; const colorsAmount = palette.colors.length; const clrFreq = 100 + Math.log(color / colorsAmount + 1) * 300; diff --git a/src/store/configureStore.js b/src/store/configureStore.js index d8956c7..1ac4f8c 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -10,7 +10,6 @@ import swal from './sweetAlert'; import protocolClientHook from './protocolClientHook'; import rendererHook from './rendererHook'; // import ads from './ads'; -// import analytics from './analytics'; import array from './array'; import promise from './promise'; import notifications from './notifications'; @@ -41,7 +40,6 @@ const store = createStore( protocolClientHook, rendererHook, // ads, - // analytics, logger, ), ), diff --git a/src/store/protocolClientHook.js b/src/store/protocolClientHook.js index 18bb0ef..3827ee7 100644 --- a/src/store/protocolClientHook.js +++ b/src/store/protocolClientHook.js @@ -30,6 +30,17 @@ export default (store) => (next) => (action) => { break; } + case 'REQUEST_PLACE_PIXEL': { + const { + i, j, offset, color, + } = action; + ProtocolClient.requestPlacePixel( + i, j, offset, + color, + ); + break; + } + default: // nothing } diff --git a/src/store/rendererHook.js b/src/store/rendererHook.js index 2797e0d..00b1f4e 100644 --- a/src/store/rendererHook.js +++ b/src/store/rendererHook.js @@ -47,6 +47,12 @@ export default (store) => (next) => (action) => { break; } + case 'SET_PLACE_ALLOWED': { + const renderer = getRenderer(); + renderer.forceNextSubRender = true; + break; + } + case 'TOGGLE_HISTORICAL_VIEW': case 'SET_SCALE': { const { diff --git a/src/store/track.js b/src/store/track.js deleted file mode 100644 index 07d96b6..0000000 --- a/src/store/track.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright 2016 Facebook, Inc. - * - * You are hereby granted a non-exclusive, worldwide, royalty-free license to - * use, copy, modify, and distribute this software in source code or binary - * form for use in connection with the web services and APIs provided by - * Facebook. - * - * As with any software that integrates with the Facebook platform, your use - * of this software is subject to the Facebook Developer Principles and - * Policies [http://developers.facebook.com/policy/]. This copyright notice - * shall be included in all copies or substantial portions of the software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE - * - * @flow - */ - -import type { Action } from '../actions/types'; - - -export default function track(action: Action): void { - if (typeof window.ga === 'undefined') return; - - switch (action.type) { - case 'PLACE_PIXEL': { - const [x, y] = action.coordinates; - window.ga('send', { - hitType: 'event', - eventCategory: 'Place', - eventAction: action.color, - eventLabel: `${x},${y}`, - }); - break; - } - - default: - // nothing - } -} diff --git a/src/ui/ChunkRGB3D.js b/src/ui/ChunkRGB3D.js index bcc12ab..283b4bd 100644 --- a/src/ui/ChunkRGB3D.js +++ b/src/ui/ChunkRGB3D.js @@ -87,7 +87,7 @@ class Chunk { timestamp: number; constructor(palette, key, xc, zc) { - this.array = [0, xc, zc]; + this.cell = [0, xc, zc]; this.key = key; this.palette = palette; this.timestamp = Date.now(); diff --git a/src/ui/Renderer3D.js b/src/ui/Renderer3D.js index 38e6224..c582e8f 100644 --- a/src/ui/Renderer3D.js +++ b/src/ui/Renderer3D.js @@ -12,6 +12,7 @@ import VoxelPainterControls from '../controls/VoxelPainterControls'; import ChunkLoader from './ChunkLoader3D'; import { getChunkOfPixel, + getOffsetOfPixel, } from '../core/utils'; import { THREE_TILE_SIZE, @@ -458,6 +459,21 @@ class Renderer { } } + placeVoxel(x: number, y: number, z: number, color: number = null) { + const { + store, + } = this; + const state = store.getState(); + const { + canvasSize, + selectedColor, + } = state.canvas; + const chClr = (color === null) ? selectedColor : color; + const [i, j] = getChunkOfPixel(canvasSize, x, y, z); + const offset = getOffsetOfPixel(canvasSize, x, y, z); + store.dispatch(tryPlacePixel(i, j, offset, chClr)); + } + multiTapEnd() { const { store, @@ -486,7 +502,7 @@ class Renderer { if (this.rollOverMesh.position.y < 0) { return; } - store.dispatch(tryPlacePixel([px, py, pz])); + this.placeVoxel(px, py, pz); break; } case 2: { @@ -512,8 +528,8 @@ class Renderer { return; } if (target.clone().sub(camera.position).length() <= 50) { - const cell = target.toArray(); - store.dispatch(tryPlacePixel(cell, 0)); + const [x, y, z] = target.toArray(); + this.placeVoxel(x, y, z, 0); } } break; @@ -602,8 +618,8 @@ class Renderer { .addScalar(0.5) .floor(); if (target.clone().sub(camera.position).length() < 120) { - const cell = target.toArray(); - store.dispatch(tryPlacePixel(cell)); + const [x, y, z] = target.toArray(); + this.placeVoxel(x, y, z); } } else if (button === 1) { // middle mouse button @@ -635,8 +651,8 @@ class Renderer { return; } if (target.clone().sub(camera.position).length() < 120) { - const cell = target.toArray(); - store.dispatch(tryPlacePixel(cell, 0)); + const [x, y, z] = target.toArray(); + this.placeVoxel(x, y, z, 0); } } }