From afb046b3b66e0e4b259f02fbf60e1743d90f4a72 Mon Sep 17 00:00:00 2001 From: hippoz <10706925-hippoz@users.noreply.gitlab.com> Date: Sat, 25 Feb 2023 02:35:12 +0200 Subject: [PATCH] add buffer support to rpc and add profile pictures --- .gitignore | 1 + frontend/src/components/Message.svelte | 74 +++-- frontend/src/components/MessageInput.svelte | 4 +- .../src/components/PresenceSidebar.svelte | 19 +- frontend/src/components/Sidebar.svelte | 6 +- frontend/src/components/UserTopBar.svelte | 8 - frontend/src/components/UserView.svelte | 38 +++ .../src/components/overlays/Settings.svelte | 27 +- frontend/src/gateway.js | 21 +- frontend/src/request.js | 37 ++- frontend/src/storage.js | 5 + frontend/src/stores.js | 17 +- frontend/src/styles/global.css | 15 +- package.json | 4 +- src/database/init.ts | 5 +- src/database/templates.ts | 8 +- src/gateway/gatewaypayloadtype.ts | 2 + src/gateway/index.ts | 44 ++- src/impl.ts | 1 + src/routes/api/v1/rpc.ts | 4 +- src/routes/matrix/index.ts | 3 +- src/rpc/apis/channels.ts | 14 +- src/rpc/apis/messages.ts | 8 +- src/rpc/apis/users.ts | 111 +++++++- src/rpc/rpc.ts | 79 ++++- src/server.ts | 1 + src/serverconfig.ts | 7 +- src/types/gatewaypresence.d.ts | 1 + src/types/user.d.ts | 7 +- uploads/avatar/.gitkeep | 0 yarn.lock | 269 +++++++++++++++++- 31 files changed, 728 insertions(+), 112 deletions(-) delete mode 100644 frontend/src/components/UserTopBar.svelte create mode 100644 frontend/src/components/UserView.svelte create mode 100644 uploads/avatar/.gitkeep diff --git a/.gitignore b/.gitignore index b9062f9..014cf2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ dist/ frontend-new/ +uploads/avatar/*.webp .env diff --git a/frontend/src/components/Message.svelte b/frontend/src/components/Message.svelte index 4af1b53..143282e 100644 --- a/frontend/src/components/Message.svelte +++ b/frontend/src/components/Message.svelte @@ -1,5 +1,6 @@ + + diff --git a/frontend/src/gateway.js b/frontend/src/gateway.js index 18a01d7..e1e040c 100644 --- a/frontend/src/gateway.js +++ b/frontend/src/gateway.js @@ -32,7 +32,9 @@ export const GatewayPayloadType = { TypingStart: 130, - PresenceUpdate: 140 + PresenceUpdate: 140, + + UserUpdate: 150, } export const GatewayEventType = { @@ -164,8 +166,17 @@ export default { return true; }, - send(data) { - return this.ws.send(JSON.stringify(data)); + send(jsonPayload, binaryData=null) { + const jsonString = JSON.stringify(jsonPayload); + if (binaryData) { + const dataBlob = new Blob([jsonString, "\n", binaryData], { + type: "application/octet-stream" + }); + console.log(dataBlob); + this.ws.send(dataBlob); + } else { + this.ws.send(jsonString); + } }, dispatch(event, payload) { const eventHandlers = this.handlers.get(event); @@ -195,14 +206,14 @@ export default { this.handlers.delete(event); } }, - sendRPCRequest(calls, isSignal) { + sendRPCRequest(calls, isSignal, binaryData=null) { return new Promise((resolve, _reject) => { this.waitingSerials.set(this.serial, resolve); this.send({ t: isSignal ? GatewayPayloadType.RPCSignal : GatewayPayloadType.RPCRequest, d: calls, s: this.serial - }); + }, binaryData); this.serial++; }); }, diff --git a/frontend/src/request.js b/frontend/src/request.js index 08912f4..6f5a45c 100644 --- a/frontend/src/request.js +++ b/frontend/src/request.js @@ -10,17 +10,18 @@ export const methods = { loginUser: method(1, false), getUserSelf: withCacheable(method(2, true)), promoteUserSelf: method(3, true), - createChannel: method(4, true), - updateChannelName: method(5, true), - deleteChannel: method(6, true), - getChannel: withCacheable(method(7, true)), - getChannels: withCacheable(method(8, true)), - createChannelMessage: method(9, true), - getChannelMessages: withCacheable(method(10, true)), - putChannelTyping: method(11, true), - deleteMessage: method(12, true), - updateMessageContent: method(13, true), - getMessage: withCacheable(method(14, true)) + putUserAvatar: method(4, true), + createChannel: method(5, true), + updateChannelName: method(6, true), + deleteChannel: method(7, true), + getChannel: withCacheable(method(8, true)), + getChannels: withCacheable(method(9, true)), + createChannelMessage: method(10, true), + getChannelMessages: withCacheable(method(11, true)), + putChannelTyping: method(12, true), + deleteMessage: method(13, true), + updateMessageContent: method(14, true), + getMessage: withCacheable(method(15, true)) }; export function compatibleFetch(endpoint, options) { @@ -122,3 +123,17 @@ export async function remoteSignal(method, ...args) { _isSignal: true }, ...args); } + +export async function remoteBlobUpload({methodId, requiresAuthentication, _isSignal=false}, blob) { + const calls = [[methodId, [0, blob.size]]]; + if (requiresAuthentication && gateway.authenticated) { + const replies = await gateway.sendRPCRequest(calls, _isSignal, blob); + const ok = Array.isArray(replies) && !(replies[0] && replies[0].code); + return { + json: ok ? replies[0] : null, + ok + }; + } else { + return { json: null, ok: false }; + } +} diff --git a/frontend/src/storage.js b/frontend/src/storage.js index 43d795b..a6bf829 100644 --- a/frontend/src/storage.js +++ b/frontend/src/storage.js @@ -1,6 +1,7 @@ const defaults = { "server:apiBase": `${window.location.origin || ""}/api/v1`, "server:gatewayBase": `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/gateway`, + "server:avatarsBase": `${window.location.origin || ""}/uploads/avatar`, "auth:token": "", "ui:doAnimations": true, "ui:theme": "dark", @@ -89,3 +90,7 @@ export function init() { export function apiRoute(fragment) { return `${getItem("server:apiBase")}/${fragment}` } + +export function avatarUrl(avatarId, size) { + return `${getItem("server:avatarsBase")}/${avatarId}_${size}.webp`; +} diff --git a/frontend/src/stores.js b/frontend/src/stores.js index ff38751..0854a8c 100644 --- a/frontend/src/stores.js +++ b/frontend/src/stores.js @@ -205,6 +205,14 @@ class UserInfoStore extends Store { this.value = user; this.updated(); }); + + gateway.subscribe(GatewayEventType.UserUpdate, (user) => { + console.log(user); + if (this.value && this.value.id === user.id) { + this.value = user; + this.updated(); + } + }); } } @@ -247,7 +255,6 @@ class MessageStore extends Store { } if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000 && message.author_id === previous.author_id && message._effectiveAuthor === previous._effectiveAuthor && message._viaBadge === previous._viaBadge) { message._clumped = true; - previous._hasChildren = true; } if (!previous || (previous._createdAtDateString !== message._createdAtDateString && !message._aboveDateMarker)) { message._aboveDateMarker = new Intl.DateTimeFormat(getItem("ui:locale"), { month: "long", day: "numeric", year: "numeric" }).format(message._createdAtDate); @@ -556,6 +563,14 @@ class PresenceStore extends Store { gateway.subscribe(GatewayEventType.PresenceUpdate, (data) => { this.ingestPresenceUpdate(data); }); + + gateway.subscribe(GatewayEventType.UserUpdate, (data) => { + const entry = this.entryIndexByUserId(data.id); + if (entry === -1) return; + this.value[entry].user.username = data.username; + this.value[entry].user.avatar = data.avatar; + this.updated(); + }); } } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index dba748c..6ddfe5b 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -143,8 +143,7 @@ body { justify-content: left; width: 100%; padding: var(--space-sm); - flex-grow: 0; - flex-shrink: 0; + height: 56px; } .top-bar-heading { @@ -413,6 +412,7 @@ body { .h4 {font-size: var(--h4)} .h5 {font-size: var(--h5)} .text-small {font-size: var(--h6)} +.text-bold {font-weight: 700} /* sidebar */ @@ -484,6 +484,7 @@ body { .sidebar-button.selected { color: var(--foreground-color-1); background-color: var(--background-color-2); + font-weight: 400; } .sidebar-button.selected .icon-button { @@ -495,6 +496,16 @@ body { color: var(--foreground-special-color-1); } +.material-icons-outlined.circled-icon { + display: block; + width: 32px; + height: 32px; + border-radius: 50%; + text-align: center; + line-height: 32px; + background-color: var(--background-color-3); +} + /* badges */ .user-badge { diff --git a/package.json b/package.json index ad32ac4..9eb55b7 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,17 @@ "express-validator": "^6.14.2", "jsonwebtoken": "^8.5.1", "pg": "^8.8.0", + "sharp": "^0.31.3", "ws": "^8.8.1" }, "devDependencies": { - "@types/cors": "^2.8.12", "@types/bcrypt": "^5.0.0", + "@types/cors": "^2.8.12", "@types/express": "^4.17.13", "@types/jsonwebtoken": "^8.5.9", "@types/node": "^18.7.13", "@types/pg": "^8.6.5", + "@types/sharp": "^0.31.1", "@types/ws": "^8.5.3", "typescript": "^4.8.2" } diff --git a/src/database/init.ts b/src/database/init.ts index 1f8f8ab..78f3354 100644 --- a/src/database/init.ts +++ b/src/database/init.ts @@ -27,7 +27,10 @@ export default async function databaseInit() { ); `, ` - ALTER TABLE messages ADD COLUMN nick_username VARCHAR(64) DEFAULT ''; + ALTER TABLE messages ADD COLUMN nick_username VARCHAR(64) DEFAULT NULL; + `, + ` + ALTER TABLE users ADD COLUMN avatar VARCHAR(48) DEFAULT NULL; ` ]; diff --git a/src/database/templates.ts b/src/database/templates.ts index f6644da..f6e329c 100644 --- a/src/database/templates.ts +++ b/src/database/templates.ts @@ -1,4 +1,4 @@ -export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1"; -export const getMessagesByChannelFirstPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT ${limit}`; -export const getMessagesByChannelPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`; -export const getMessagesByChannelAfterPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id > $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`; +export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1"; +export const getMessagesByChannelFirstPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT ${limit}`; +export const getMessagesByChannelPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`; +export const getMessagesByChannelAfterPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id > $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`; diff --git a/src/gateway/gatewaypayloadtype.ts b/src/gateway/gatewaypayloadtype.ts index f64e3e9..2df692b 100644 --- a/src/gateway/gatewaypayloadtype.ts +++ b/src/gateway/gatewaypayloadtype.ts @@ -18,6 +18,8 @@ export enum GatewayPayloadType { TypingStart = 130, PresenceUpdate = 140, + + UserUpdate = 150, } export enum GatewayPresenceStatus { diff --git a/src/gateway/index.ts b/src/gateway/index.ts index f33944b..b5bbd45 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -8,6 +8,7 @@ import { GatewayPayload } from "../types/gatewaypayload"; import { GatewayPayloadType, GatewayPresenceStatus } from "./gatewaypayloadtype"; import { GatewayPresenceEntry } from "../types/gatewaypresence"; import { processMethodBatch } from "../rpc/rpc"; +import { maxGatewayJsonStringByteLength, maxGatewayJsonStringLength, maxGatewayPayloadByteLength } from "../serverconfig"; const GATEWAY_BATCH_INTERVAL = 50000; const GATEWAY_PING_INTERVAL = 40000; @@ -195,6 +196,7 @@ function getPresenceEntryForConnection(ws: WebSocket, status: GatewayPresenceSta user: { id: ws.state.user.id, username: ws.state.user.username, + avatar: ws.state.user.avatar }, status }; @@ -296,18 +298,44 @@ export default function(server: Server) { } }); - ws.on("message", async (rawData, isBinary) => { - if (isBinary) { - return closeWithBadPayload(ws, "Binary messages are not supported"); - } - + ws.on("message", async (rawData: Buffer, isBinary) => { ws.state.messagesSinceLastCheck++; if (ws.state.messagesSinceLastCheck > MAX_CLIENT_MESSAGES_PER_BATCH) { return closeWithError(ws, gatewayErrors.FLOODING); } + + if (rawData.byteLength >= maxGatewayPayloadByteLength) { + return closeWithError(ws, gatewayErrors.PAYLOAD_TOO_LARGE); + } + + let stringData: string; + let binaryStream: Buffer | null = null; + if (isBinary) { + // Binary frames are used in order combine our text data (JSON) with binary data. + // This is especially useful for calling RPC methods that, for example, upload files. + // The format is: [json payload]\n[begin binary stream] + + let jsonSlice; + let jsonOffset = -1; + for (let i = 0; i < maxGatewayJsonStringByteLength; i++) { + if (rawData.readUInt8(i) === 0x0A) { + // hit newline + jsonSlice = rawData.subarray(0, i); + jsonOffset = i + 1; + break; + } + } + if (!jsonSlice) { + return closeWithBadPayload(ws, "Did not find newline to delimit JSON from binary stream. JSON payload may be too large, or newline may be missing."); + } + + binaryStream = rawData.subarray(jsonOffset, rawData.byteLength); + stringData = jsonSlice.toString(); + } else { + stringData = rawData.toString(); + } - const stringData = rawData.toString(); - if (stringData.length > 4500) { + if (stringData.length > maxGatewayJsonStringLength) { return closeWithError(ws, gatewayErrors.PAYLOAD_TOO_LARGE); } @@ -416,7 +444,7 @@ export default function(server: Server) { } // RPCSignal is like RPCRequest however it does not send RPC method output unless there is an error - processMethodBatch(ws.state.user, payload.d, (payload.t === GatewayPayloadType.RPCSignal ? true : false)).then((results) => { + processMethodBatch(ws.state.user, payload.d, (payload.t === GatewayPayloadType.RPCSignal ? true : false), binaryStream).then((results) => { sendPayload(ws, { t: GatewayPayloadType.RPCResponse, d: results, diff --git a/src/impl.ts b/src/impl.ts index 9b0c5ed..5ddd662 100644 --- a/src/impl.ts +++ b/src/impl.ts @@ -17,6 +17,7 @@ export default async function sendMessage(user: User, channelId: number, optimis channel_id: channelId, author_id: authorId, author_username: user.username, + author_avatar: user.avatar, created_at: createdAt, nick_username: nickUsername }; diff --git a/src/routes/api/v1/rpc.ts b/src/routes/api/v1/rpc.ts index 2c36d40..5690429 100644 --- a/src/routes/api/v1/rpc.ts +++ b/src/routes/api/v1/rpc.ts @@ -9,7 +9,7 @@ router.post( "/", authenticateRoute(false), async (req, res) => { - res.json(await processMethodBatch(req.authenticated ? req.user : null, req.body)); + res.json(await processMethodBatch(req.authenticated ? req.user : null, req.body, false, null)); } ); @@ -27,7 +27,7 @@ router.get( } catch(O_o) { return res.json({ ...errors.BAD_REQUEST, detail: "Bad 'calls': failed to parse as JSON" }); } - res.json(await processMethodBatch(req.authenticated ? req.user : null, callJson)); + res.json(await processMethodBatch(req.authenticated ? req.user : null, callJson, false, null)); } ); diff --git a/src/routes/matrix/index.ts b/src/routes/matrix/index.ts index 5f1afda..d932c1e 100644 --- a/src/routes/matrix/index.ts +++ b/src/routes/matrix/index.ts @@ -361,7 +361,8 @@ router.get( res.json(await buildSyncPayload({ id: 3, username: "test", - is_superuser: true + is_superuser: true, + avatar: null }, cursors, !isInitial, client, channels)); }; diff --git a/src/rpc/apis/channels.ts b/src/rpc/apis/channels.ts index 83752c2..402c66a 100644 --- a/src/rpc/apis/channels.ts +++ b/src/rpc/apis/channels.ts @@ -1,5 +1,5 @@ import express from "express"; -import { channelNameRegex, method, number, string, unsignedNumber, withOptional, withRegexp } from "../rpc"; +import { channelNameRegex, method, int, string, uint, withOptional, withRegexp } from "../rpc"; import { query } from "../../database"; import { getMessagesByChannelFirstPage, getMessagesByChannelPage } from "../../database/templates"; import { errors } from "../../errors"; @@ -36,7 +36,7 @@ method( method( "updateChannelName", - [unsignedNumber(), withRegexp(channelNameRegex, string(1, 32))], + [uint(), withRegexp(channelNameRegex, string(1, 32))], async (user: User, id: number, name: string) => { const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { @@ -68,7 +68,7 @@ method( method( "deleteChannel", - [unsignedNumber()], + [uint()], async (user: User, id: number) => { const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { @@ -94,7 +94,7 @@ method( method( "getChannel", - [unsignedNumber()], + [uint()], async (_user: User, id: number) => { const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]); if (!result || result.rowCount < 1) { @@ -117,7 +117,7 @@ method( method( "createChannelMessage", - [unsignedNumber(), string(1, 4000), withOptional(unsignedNumber()), withOptional(string(1, 64))], + [uint(), string(1, 4000), withOptional(uint()), withOptional(string(1, 64))], async (user: User, id: number, content: string, optimistic_id: number | null, nick_username: string | null) => { return await sendMessage(user, id, optimistic_id, content, nick_username); } @@ -125,7 +125,7 @@ method( method( "getChannelMessages", - [unsignedNumber(), withOptional(number(5, 100)), withOptional(unsignedNumber())], + [uint(), withOptional(int(5, 100)), withOptional(uint())], async (_user: User, channelId: number, count: number | null, before: number | null) => { let limit = count ?? 25; @@ -145,7 +145,7 @@ method( method( "putChannelTyping", - [unsignedNumber()], + [uint()], async (user: User, channelId: number) => { dispatch(`channel:${channelId}`, { t: GatewayPayloadType.TypingStart, diff --git a/src/rpc/apis/messages.ts b/src/rpc/apis/messages.ts index 4ee2b8d..c07a2e5 100644 --- a/src/rpc/apis/messages.ts +++ b/src/rpc/apis/messages.ts @@ -1,4 +1,4 @@ -import { method, string, unsignedNumber } from "./../rpc"; +import { method, string, uint } from "./../rpc"; import { query } from "../../database"; import { getMessageById } from "../../database/templates"; import { errors } from "../../errors"; @@ -7,7 +7,7 @@ import { GatewayPayloadType } from "../../gateway/gatewaypayloadtype"; method( "deleteMessage", - [unsignedNumber()], + [uint()], async (user: User, id: number) => { const permissionCheckResult = await query("SELECT author_id, channel_id FROM messages WHERE id = $1", [id]); if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { @@ -36,7 +36,7 @@ method( method( "updateMessageContent", - [unsignedNumber(), string(1, 4000)], + [uint(), string(1, 4000)], async (user: User, id: number, content: string) => { const permissionCheckResult = await query(getMessageById, [id]); if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { @@ -67,7 +67,7 @@ method( method( "getMessage", - [unsignedNumber()], + [uint()], async (user: User, id: number) => { const result = await query(getMessageById, [id]); if (!result || result.rowCount < 1) { diff --git a/src/rpc/apis/users.ts b/src/rpc/apis/users.ts index df3a2d7..71e2382 100644 --- a/src/rpc/apis/users.ts +++ b/src/rpc/apis/users.ts @@ -2,9 +2,16 @@ import { errors } from "../../errors"; import { query } from "../../database"; import { compare, hash, hashSync } from "bcrypt"; import { getPublicUserObject, loginAttempt } from "../../auth"; -import { method, methodButWarningDoesNotAuthenticate, string, usernameRegex, withRegexp } from "./../rpc"; +import { bufferSlice, method, methodButWarningDoesNotAuthenticate, string, usernameRegex, withRegexp } from "./../rpc"; +import sharp from "sharp"; +import path from "path"; +import { randomBytes } from "crypto"; +import { unlink } from "fs/promises"; +import { GatewayPayloadType } from "../../gateway/gatewaypayloadtype"; +import { dispatch } from "../../gateway"; const superuserKey = process.env.SUPERUSER_KEY ? hashSync(process.env.SUPERUSER_KEY, 10) : null; +const avatarUploadDirectory = process.env.AVATAR_UPLOADS_DIR ?? "./uploads/avatar"; methodButWarningDoesNotAuthenticate( "createUser", @@ -23,7 +30,7 @@ methodButWarningDoesNotAuthenticate( } const hashedPassword = await hash(password, 10); - const insertedUser = await query("INSERT INTO users(username, password, is_superuser) VALUES ($1, $2, $3) RETURNING id, username, is_superuser", [username, hashedPassword, false]); + const insertedUser = await query("INSERT INTO users(username, password, is_superuser, avatar) VALUES ($1, $2, $3, $4) RETURNING id, username, is_superuser, avatar", [username, hashedPassword, false, null]); if (!insertedUser || insertedUser.rowCount < 1) { return errors.GOT_NO_DATABASE_DATA; } @@ -74,3 +81,103 @@ method( return errors.BAD_REQUEST_KEY; } ) + + +const checkMagic = (buffer: Buffer, magic: number[]) => { + for (let i = 0; i < magic.length; i++) { + try { + if (buffer.readUint8(i) !== magic[i]) { + return false; + } + } catch(O_o) { + return false; + } + } + return true; +}; + + +const profilePictureSizes = [ + 16, 28, 32, 64, 80, 128, 256 +]; + +method( + "putUserAvatar", + [bufferSlice()], + async (user: User, buffer: Buffer) => { + if (buffer.byteLength >= 3145728) { + // buffer exceeds 3MiB + return { ...errors.BAD_REQUEST, detail: "Uploaded file exceeds 3MiB limit." }; + } + + // TODO: maybe get rid of this entirely and give buffer directly to `sharp`? + const supportedFormatMagic = [ + [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // PNG + [0xFF, 0xD8, 0xFF], // JPEG + [0x52, 0x49, 0x46, 0x46] // WebP + ]; + let isSupported = false; + for (let i = 0; i < supportedFormatMagic.length; i++) { + if (checkMagic(buffer, supportedFormatMagic[i])) { + isSupported = true; + break; + } + } + + if (!isSupported) { + return { ...errors.BAD_REQUEST, detail: "Unsupported file format. Supported file formats are: png, jpeg, webp." }; + } + + const avatarId = randomBytes(8).toString("hex"); + + const promises = new Array(profilePictureSizes.length); + const filenames = new Array(profilePictureSizes.length); + for (let i = 0; i < profilePictureSizes.length; i++) { + filenames[i] = `${avatarId}_${profilePictureSizes[i]}.webp`; + promises[i] = sharp(buffer, { limitInputPixels: 1000 * 1000 }) + .resize(profilePictureSizes[i], profilePictureSizes[i], { fit: "cover" }) + .timeout({ seconds: 3 }) + .toFile(path.resolve(path.join(avatarUploadDirectory, filenames[i]))); + } + + try { + await Promise.all(promises); + } catch(O_o) { + console.error("rpc: putUserAvatar: error while processing and saving images", O_o); + console.error("rpc: putUserAvatar: removing all processed images due to error above"); + for (let i = 0; i < filenames.length; i++) { + try { + await unlink(path.resolve(path.join(avatarUploadDirectory, filenames[i]))); + } catch(o_0) { + console.error("rpc: putUserAvatar: error while removing files (upon error)", o_0); + } + } + return errors.INTERNAL_ERROR; + } + + // Now, if the user has an existing avatar, we will remove it + if (user.avatar) { + for (let i = 0; i < profilePictureSizes.length; i++) { + try { + await unlink(path.resolve(path.join(avatarUploadDirectory, `${user.avatar}_${profilePictureSizes[i]}.webp`))); + } catch(o_0) { + console.error("rpc: putUserAvatar: error while removing files (removing old avatar)", o_0); + } + } + } + + const updateUserResult = await query("UPDATE users SET avatar = $1 WHERE id = $2", [avatarId, user.id]); + if (!updateUserResult || updateUserResult.rowCount < 1) { + return errors.GOT_NO_DATABASE_DATA; + } + + user.avatar = avatarId; + + dispatch("*", { + t: GatewayPayloadType.UserUpdate, + d: getPublicUserObject(user), + }); + + return filenames; + } +); diff --git a/src/rpc/rpc.ts b/src/rpc/rpc.ts index 51cd46b..39b7a3e 100644 --- a/src/rpc/rpc.ts +++ b/src/rpc/rpc.ts @@ -1,21 +1,28 @@ import { errors } from "../errors"; +import { maxBufferByteLength } from "../serverconfig"; export const alphanumericRegex = new RegExp(/^[a-z0-9]+$/i); export const usernameRegex = new RegExp(/^[a-z0-9_]+$/i); export const channelNameRegex = new RegExp(/^[a-z0-9_\- ]+$/i); const defaultStringMaxLength = 3000; +const defaultMaxBufferLength = maxBufferByteLength; -export const unsignedNumber = (): RPCArgument => ({ type: RPCArgumentType.Number, minValue: 0 }); -export const number = (minValue?: number, maxValue?: number): RPCArgument => ({ type: RPCArgumentType.Number, minValue, maxValue }); +export const uint = (): RPCArgument => ({ type: RPCArgumentType.Integer, minValue: 0 }); +export const int = (minValue?: number, maxValue?: number): RPCArgument => ({ type: RPCArgumentType.Integer, minValue, maxValue }); export const string = (minLength = 0, maxLength = defaultStringMaxLength): RPCArgument => ({ type: RPCArgumentType.String, minLength, maxLength }); +export const bufferSlice = (minLength = 0, maxLength = defaultMaxBufferLength) => ({ type: RPCArgumentType.Buffer, minLength, maxLength }); export const withRegexp = (regexp: RegExp, arg: RPCArgument): RPCArgument => ({ minLength: 0, maxLength: defaultStringMaxLength, ...arg, regexp }); export const withOptional = (arg: RPCArgument): RPCArgument => ({ ...arg, isOptional: true }); +const isInt = (val: any) => typeof val === "number" && Number.isSafeInteger(val); +const isUint = (val: any) => (isInt(val) && val >= 0); + enum RPCArgumentType { - Number, - String + Integer, + String, + Buffer } interface RPCArgument { @@ -23,8 +30,8 @@ interface RPCArgument { isOptional?: boolean // strings - minLength?: number - maxLength?: number + minLength?: number // also used for buffer + maxLength?: number // also used for buffer regexp?: RegExp // numbers @@ -53,7 +60,7 @@ export const methodButWarningDoesNotAuthenticate = (name: string, args: RPCArgum return method(name, args, func, false); }; -export const userInvokeMethod = async (user: User | null, methodId: number, args: any[]) => { +export const userInvokeMethod = async (user: User | null, methodId: number, args: any[], buffer: Buffer | null) => { const methodData = methods.get(methodId); if (!methodData) return { ...errors.BAD_REQUEST, @@ -75,16 +82,16 @@ export const userInvokeMethod = async (user: User | null, methodId: number, args continue; } switch (schema.type) { - case RPCArgumentType.Number: { - if (typeof argument !== "number") { - validationErrors.push({ index: i, msg: `Expected type number, got type ${typeof argument}.` }); + case RPCArgumentType.Integer: { + if (!isInt(argument)) { + validationErrors.push({ index: i, msg: `Expected integer.` }); continue; } if (schema.minValue !== undefined && argument < schema.minValue) { - validationErrors.push({ index: i, msg: `Provided number is below minimum value of ${schema.minValue}.` }); + validationErrors.push({ index: i, msg: `Provided integer is below minimum value of ${schema.minValue}.` }); } if (schema.maxValue !== undefined && argument > schema.maxValue) { - validationErrors.push({ index: i, msg: `Provided number is above maximum value of ${schema.maxValue}.` }); + validationErrors.push({ index: i, msg: `Provided integer is above maximum value of ${schema.maxValue}.` }); } break; } @@ -102,6 +109,43 @@ export const userInvokeMethod = async (user: User | null, methodId: number, args } break; } + case RPCArgumentType.Buffer: { + if (!buffer) { + validationErrors.push({ index: i, msg: "RPC method expects buffer, however no buffer was provided." }); + continue; + } + // the argument should be an array of this format: [byteOffset: number, byteLength: number] + if (!Array.isArray(argument) || argument.length !== 2 || !isUint(argument[0]) || !isUint(argument[1])) { + validationErrors.push({ index: i, msg: "Expected argument to be an array of format '[byteOffset: uint, byteLength: uint]'." }); + continue; + } + + + // TODO: since slices, can overlap and multiple RPC calls can be done in a single request, + // it makes it possible for someone to send a buffer within the allowed limit, however + // actually tell the server to upload it over and over. We should fix this by adding a + // quota per user. + const [byteOffset, byteLength]: number[] = argument; + const end = byteOffset + byteLength; + + if ((schema.minLength !== undefined && byteLength < schema.minLength) || (schema.maxLength !== undefined && byteLength > schema.maxLength)) { + validationErrors.push({ index: i, msg: `Buffer size must be between ${schema.minLength} and ${schema.maxLength} bytes.` }); + continue; + } + + if (end > buffer.byteLength) { + validationErrors.push({ index: i, msg: "Provided slice exceeds buffer boundaries." }); + continue; + } + + const slice = buffer.subarray(byteOffset, end); + if (slice.byteLength !== byteLength) { + validationErrors.push({ index: i, msg: "Provided slice is invalid." }); + continue; + } + + args[i] = slice; + } break; } } @@ -121,7 +165,7 @@ export const userInvokeMethod = async (user: User | null, methodId: number, args } }; -export const processMethodBatch = async (user: User | null, calls: any, ignoreNonErrors = false) => { +export const processMethodBatch = async (user: User | null, calls: any, ignoreNonErrors = false, buffer: Buffer | null) => { if (!Array.isArray(calls) || !calls.length || calls.length > 5) { return { ...errors.BAD_REQUEST, @@ -129,6 +173,13 @@ export const processMethodBatch = async (user: User | null, calls: any, ignoreNo }; } + if (buffer && buffer.byteLength >= maxBufferByteLength) { + return { + ...errors.BAD_REQUEST, + detail: `Provided buffer is larger than maximum of ${maxBufferByteLength} bytes.` + }; + } + const responses = new Array(calls.length); const promises = new Array(calls.length); calls.forEach((call, index) => { @@ -140,7 +191,7 @@ export const processMethodBatch = async (user: User | null, calls: any, ignoreNo return; } - const promise = userInvokeMethod(user, call[0], call.slice(1, call.length)); + const promise = userInvokeMethod(user, call[0], call.slice(1, call.length), buffer); promise.then(value => { if (ignoreNonErrors && !value.code) { responses[index] = null; diff --git a/src/server.ts b/src/server.ts index 1abe7bf..2b799c3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,6 +9,7 @@ const ENABLE_MATRIX_LAYER = false; export default function(app: Application) { app.use(json()); app.use("/api/v1/rpc", rpcRouter); + app.use("/uploads", express.static("uploads")); app.use("/", express.static("frontend/public")); if (ENABLE_MATRIX_LAYER) { app.use("/", matrixRouter); diff --git a/src/serverconfig.ts b/src/serverconfig.ts index 6964224..d42e6ab 100644 --- a/src/serverconfig.ts +++ b/src/serverconfig.ts @@ -1,5 +1,10 @@ export default { superuserRequirement: { createChannel: false - }, + } }; + +export const maxBufferByteLength = 8388608; // 8MiB +export const maxGatewayJsonStringLength = 4500; +export const maxGatewayJsonStringByteLength = maxGatewayJsonStringLength * 2; // https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto#buffer_sizing +export const maxGatewayPayloadByteLength = maxBufferByteLength + maxGatewayJsonStringByteLength + 4; diff --git a/src/types/gatewaypresence.d.ts b/src/types/gatewaypresence.d.ts index d2eb0ff..fee95dc 100644 --- a/src/types/gatewaypresence.d.ts +++ b/src/types/gatewaypresence.d.ts @@ -4,6 +4,7 @@ export interface GatewayPresenceEntry { user: { username: string, id: number, + avatar: string | null }, bridgesTo?: string, privacy?: string, diff --git a/src/types/user.d.ts b/src/types/user.d.ts index 160f52a..2617e10 100644 --- a/src/types/user.d.ts +++ b/src/types/user.d.ts @@ -1,6 +1,7 @@ interface User { - password?: string, - username: string, - id: number, + password?: string + username: string + id: number is_superuser: boolean + avatar: string | null } diff --git a/uploads/avatar/.gitkeep b/uploads/avatar/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/yarn.lock b/yarn.lock index 7727008..972aca4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -112,6 +112,13 @@ "@types/mime" "^1" "@types/node" "*" +"@types/sharp@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.31.1.tgz#db768461455dbcf9ff11d69277fd70564483c4df" + integrity sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag== + dependencies: + "@types/node" "*" + "@types/ws@^8.5.3": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -167,6 +174,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + bcrypt@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" @@ -175,6 +187,15 @@ bcrypt@^5.0.1: "@mapbox/node-pre-gyp" "^1.0.0" node-addon-api "^3.1.0" +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + body-parser@1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" @@ -211,6 +232,14 @@ buffer-writer@2.0.0: resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -224,16 +253,49 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.2" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + color-support@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -288,6 +350,18 @@ debug@4: dependencies: ms "2.1.2" +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -303,7 +377,7 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -detect-libc@^2.0.0: +detect-libc@^2.0.0, detect-libc@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== @@ -335,6 +409,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -345,6 +426,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + express-validator@^6.14.2: version "6.14.2" resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.14.2.tgz#6147893f7bec0e14162c3a88b3653121afc4678f" @@ -413,6 +499,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -454,6 +545,11 @@ get-intrinsic@^1.0.2: has "^1.0.3" has-symbols "^1.0.3" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + glob@^7.1.3: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -509,6 +605,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -517,16 +618,26 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -651,6 +762,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + minimatch@^3.0.4: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -658,6 +774,11 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimist@^1.2.0, minimist@^1.2.3: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass@^3.0.0: version "3.1.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" @@ -673,6 +794,11 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -693,16 +819,33 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +node-abi@^3.3.0: + version "3.33.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.33.0.tgz#8b23a0cec84e1c5f5411836de6a9b84bccf26e7f" + integrity sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog== + dependencies: + semver "^7.3.5" + node-addon-api@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -744,7 +887,7 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -once@^1.3.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -844,6 +987,24 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prebuild-install@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -852,6 +1013,14 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + qs@6.10.3: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" @@ -874,6 +1043,25 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62" + integrity sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -917,6 +1105,13 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -956,6 +1151,20 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sharp@^0.31.3: + version "0.31.3" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.31.3.tgz#60227edc5c2be90e7378a210466c99aefcf32688" + integrity sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg== + dependencies: + color "^4.2.3" + detect-libc "^2.0.1" + node-addon-api "^5.0.0" + prebuild-install "^7.1.1" + semver "^7.3.8" + simple-get "^4.0.1" + tar-fs "^2.1.1" + tunnel-agent "^0.6.0" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -970,6 +1179,27 @@ signal-exit@^3.0.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0, simple-get@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + split2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" @@ -1003,6 +1233,32 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +tar-fs@^2.0.0, tar-fs@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -1025,6 +1281,13 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"