diff --git a/prisma/migrations/20210830210159_zws/migration.sql b/prisma/migrations/20210830210159_zws/migration.sql new file mode 100644 index 0000000..05795e5 --- /dev/null +++ b/prisma/migrations/20210830210159_zws/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - A unique constraint covering the columns `[imageId]` on the table `InvisibleImage` will be added. If there are existing duplicate values, this will fail. + - Added the required column `imageId` to the `InvisibleImage` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_id_fkey"; + +-- DropIndex +DROP INDEX "InvisibleImage_id_unique"; + +-- AlterTable +CREATE SEQUENCE "invisibleimage_id_seq"; +ALTER TABLE "InvisibleImage" ADD COLUMN "imageId" INTEGER NOT NULL, +ALTER COLUMN "id" SET DEFAULT nextval('invisibleimage_id_seq'), +ADD PRIMARY KEY ("id"); +ALTER SEQUENCE "invisibleimage_id_seq" OWNED BY "InvisibleImage"."id"; + +-- CreateIndex +CREATE UNIQUE INDEX "InvisibleImage_imageId_unique" ON "InvisibleImage"("imageId"); + +-- AddForeignKey +ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b940de3..44bbafb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,10 +49,11 @@ model Image { } model InvisibleImage { - id Int - image Image @relation(fields: [id], references: [id]) - + id Int @id @default(autoincrement()) invis String @unique + + imageId Int + image Image @relation(fields: [imageId], references: [id]) } model Url { diff --git a/server/index.js b/server/index.js index 4c28096..f521949 100644 --- a/server/index.js +++ b/server/index.js @@ -64,40 +64,46 @@ function shouldUseYarn() { const parts = req.url.split('/'); if (!parts[2] || parts[2] === '') return; - const data = await getFile(config.uploader.directory, parts[2]); - if (!data) { - app.render404(req, res); + let image = await prisma.image.findFirst({ + where: { + OR: { file: parts[2] }, + OR: { invisible: { invis: decodeURI(parts[2]) } } + }, + select: { + mimetype: true, + id: true, + file: true, + invisible: true + } + }); + + if (!image) { + const data = await getFile(config.uploader.directory, parts[2]); + if (!data) return app.render404(req, res); + + const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream'; + res.setHeader('Content-Type', mimetype); + res.end(data); } else { - let image = await prisma.image.findFirst({ - where: { - OR: { - file: parts[2], - }, - OR: { - invisible: { - invis: decodeURI(parts[2]) - } - } - } - }); if (image) { + const data = await getFile(config.uploader.directory, image.file); + if (!data) return app.render404(req, res); + await prisma.image.update({ - where: { - id: image.id, - }, - data: { - views: { - increment: 1 - } - } + where: { id: image.id }, + data: { views: { increment: 1 } } }); res.setHeader('Content-Type', image.mimetype); + res.end(data); } else { + const data = await getFile(config.uploader.directory, parts[2]); + if (!data) return app.render404(req, res); + const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream'; res.setHeader('Content-Type', mimetype); + res.end(data); } - res.end(data); } } else { handle(req, res); diff --git a/src/components/pages/Manage.tsx b/src/components/pages/Manage.tsx index c0b7717..d5914f2 100644 --- a/src/components/pages/Manage.tsx +++ b/src/components/pages/Manage.tsx @@ -84,8 +84,8 @@ export default function Manage() { const [severity, setSeverity] = useState('success'); const [message, setMessage] = useState('Saved'); - const genShareX = withEmbed => { - let config = { + const genShareX = (withEmbed: boolean = false, withZws: boolean = false) => { + const config = { Version: '13.2.1', Name: 'Zipline', DestinationType: 'ImageUploader, TextUploader', @@ -93,15 +93,17 @@ export default function Manage() { RequestURL: `${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api/upload`, Headers: { Authorization: user?.token, - ...(withEmbed && {Embed: 'true'}) + ...(withEmbed && {Embed: 'true'}), + ...(withZws && {ZWS: 'true'}) }, URL: '$json:url$', Body: 'MultipartFormData', FileFormName: 'file' }; - var pseudoElement = document.createElement('a'); - pseudoElement.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t'))); - pseudoElement.setAttribute('download', `zipline${withEmbed ? '_embed' : ''}.sxcu`); + + const pseudoElement = document.createElement('a'); + pseudoElement.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t'))); + pseudoElement.setAttribute('download', `zipline${withEmbed ? '_embed' : ''}${withZws ? '_zws' : ''}.sxcu`); pseudoElement.style.display = 'none'; document.body.appendChild(pseudoElement); pseudoElement.click(); @@ -245,6 +247,7 @@ export default function Manage() { ShareX Config + ); } diff --git a/src/lib/util.ts b/src/lib/util.ts index c075471..6a0e7c9 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,8 +1,9 @@ -import { createHmac, timingSafeEqual } from 'crypto'; +import { createHmac, randomBytes, timingSafeEqual } from 'crypto'; import { hash, verify } from 'argon2'; import { readdir, stat } from 'fs/promises'; import { join } from 'path'; import prisma from './prisma'; +import { InvisibleImage } from '@prisma/client'; export async function hashPassword(s: string): Promise { return await hash(s); @@ -89,13 +90,14 @@ export function bytesToRead(bytes: number) { } export function createInvisURL(length: number) { + // some parts from https://github.com/tycrek/ass/blob/master/generators/lengthGen.js const invisibleCharset = ['\u200B', '\u2060', '\u200C', '\u200D']; - for (var i = 0, output = ''; i <= length; ++i) output += invisibleCharset[Math.floor(Math.random() * 4)]; - return output; + + return [...randomBytes(length)].map((byte) => invisibleCharset[Number(byte) % invisibleCharset.length]).join('').slice(1).concat(invisibleCharset[0]); } export function createInvis(length: number, imageId: number) { - const retry = async () => { + const retry = async (): Promise => { const invis = createInvisURL(length); const existing = await prisma.invisibleImage.findUnique({ @@ -109,7 +111,7 @@ export function createInvis(length: number, imageId: number) { const inv = await prisma.invisibleImage.create({ data: { invis, - id: imageId + imageId } }); diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index 7d48eb7..161e50f 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -2,7 +2,7 @@ import multer from 'multer'; import prisma from 'lib/prisma'; import zconfig from 'lib/config'; import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline'; -import { randomChars } from 'lib/util'; +import { createInvis, randomChars } from 'lib/util'; import { writeFile } from 'fs/promises'; import { join } from 'path'; import Logger from 'lib/logger'; @@ -26,8 +26,9 @@ async function handler(req: NextApiReq, res: NextApiRes) { const ext = req.file.originalname.split('.').pop(); if (zconfig.uploader.disabled_extentions.includes(ext)) return res.error('disabled extension recieved: ' + ext); - const rand = randomChars(zconfig.uploader.length); + + let invis; const image = await prisma.image.create({ data: { file: `${rand}.${ext}`, @@ -35,13 +36,15 @@ async function handler(req: NextApiReq, res: NextApiRes) { userId: user.id } }); + + if (req.headers.zws) invis = await createInvis(zconfig.uploader.length, image.id); await writeFile(join(process.cwd(), zconfig.uploader.directory, image.file), req.file.buffer); Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`); return res.json({ - url: `${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${req.headers.embed ? zconfig.uploader.embed_route : zconfig.uploader.route}/${image.file}` + url: `${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${req.headers.embed ? zconfig.uploader.embed_route : zconfig.uploader.route}/${invis ? invis.invis : image.file}` }); }