feat: headless mode

This commit is contained in:
diced 2022-12-01 09:27:14 -08:00
parent 231f734fd5
commit 6f3081cb8e
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
7 changed files with 1749 additions and 848 deletions

View file

@ -102,6 +102,8 @@ export interface ConfigFeatures {
oauth_registration: boolean; oauth_registration: boolean;
user_registration: boolean; user_registration: boolean;
headless: boolean;
} }
export interface ConfigOAuth { export interface ConfigOAuth {

View file

@ -138,6 +138,8 @@ export default function readConfig() {
map('FEATURES_OAUTH_REGISTRATION', 'boolean', 'features.oauth_registration'), map('FEATURES_OAUTH_REGISTRATION', 'boolean', 'features.oauth_registration'),
map('FEATURES_USER_REGISTRATION', 'boolean', 'features.user_registration'), map('FEATURES_USER_REGISTRATION', 'boolean', 'features.user_registration'),
map('FEATURES_HEADLESS', 'boolean', 'features.headless'),
map('CHUNKS_MAX_SIZE', 'human-to-byte', 'chunks.max_size'), map('CHUNKS_MAX_SIZE', 'human-to-byte', 'chunks.max_size'),
map('CHUNKS_CHUNKS_SIZE', 'human-to-byte', 'chunks.chunks_size'), map('CHUNKS_CHUNKS_SIZE', 'human-to-byte', 'chunks.chunks_size'),

View file

@ -166,12 +166,14 @@ const validator = s.object({
invites_length: s.number.default(6), invites_length: s.number.default(6),
oauth_registration: s.boolean.default(false), oauth_registration: s.boolean.default(false),
user_registration: s.boolean.default(false), user_registration: s.boolean.default(false),
headless: s.boolean.default(false),
}) })
.default({ .default({
invites: false, invites: false,
invites_length: 6, invites_length: 6,
oauth_registration: false, oauth_registration: false,
user_registration: false, user_registration: false,
headless: false,
}), }),
chunks: s chunks: s
.object({ .object({

View file

@ -1,4 +1,5 @@
import { OauthProviders } from '@prisma/client'; import { OauthProviders } from '@prisma/client';
import config from 'lib/config';
import Logger from 'lib/logger'; import Logger from 'lib/logger';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { createToken } from 'lib/util'; import { createToken } from 'lib/util';
@ -30,6 +31,12 @@ export const withOAuth =
const logger = Logger.get(`oauth::${provider}`); const logger = Logger.get(`oauth::${provider}`);
function oauthError(error: string) { function oauthError(error: string) {
if (config.features.headless)
return res.json({
error,
provider,
});
return res.redirect(`/oauth_error?error=${error}&provider=${provider}`); return res.redirect(`/oauth_error?error=${error}&provider=${provider}`);
} }

View file

@ -157,6 +157,18 @@ export const withZipline =
req.user = async () => { req.user = async () => {
try { try {
const authHeader = req.headers.authorization;
if (authHeader) {
const user = await prisma.user.findFirst({
where: {
token: authHeader,
},
include: { oauth: true },
});
if (user) return user;
}
const userId = req.getCookie('user'); const userId = req.getCookie('user');
if (!userId) return null; if (!userId) return null;

View file

@ -81,12 +81,17 @@ async function start() {
const handle = nextServer.getRequestHandler(); const handle = nextServer.getRequestHandler();
const router = Router({ const router = Router({
defaultRoute: (req, res) => { defaultRoute: (req, res) => {
if (config.features.headless) {
const url = req.url.toLowerCase();
if (!url.startsWith('/api') || url === '/api') return notFound(req, res, nextServer);
}
handle(req, res); handle(req, res);
}, },
}); });
router.on('GET', '/favicon.ico', async (req, res) => { router.on('GET', '/favicon.ico', async (req, res) => {
if (!existsSync('./public/favicon.ico')) return nextServer.render404(req, res); if (!existsSync('./public/favicon.ico')) return notFound(req, res, nextServer);
const favicon = createReadStream('./public/favicon.ico'); const favicon = createReadStream('./public/favicon.ico');
res.setHeader('Content-Type', 'image/x-icon'); res.setHeader('Content-Type', 'image/x-icon');
@ -95,14 +100,14 @@ async function start() {
}); });
router.on('GET', `${config.urls.route}/:id`, async (req, res, params) => { router.on('GET', `${config.urls.route}/:id`, async (req, res, params) => {
if (params.id === '') return nextServer.render404(req, res as ServerResponse); if (params.id === '') return notFound(req, res, nextServer);
const url = await prisma.url.findFirst({ const url = await prisma.url.findFirst({
where: { where: {
OR: [{ id: params.id }, { vanity: params.id }, { invisible: { invis: decodeURI(params.id) } }], OR: [{ id: params.id }, { vanity: params.id }, { invisible: { invis: decodeURI(params.id) } }],
}, },
}); });
if (!url) return nextServer.render404(req, res as ServerResponse); if (!url) return notFound(req, res, nextServer);
const nUrl = await prisma.url.update({ const nUrl = await prisma.url.update({
where: { where: {
@ -122,7 +127,7 @@ async function start() {
Logger.get('url').debug(`url deleted due to max views ${JSON.stringify(nUrl)}`); Logger.get('url').debug(`url deleted due to max views ${JSON.stringify(nUrl)}`);
return nextServer.render404(req, res as ServerResponse); return notFound(req, res, nextServer);
} }
return redirect(res, url.destination); return redirect(res, url.destination);
@ -132,8 +137,9 @@ async function start() {
'GET', 'GET',
config.uploader.route === '/' ? '/:id' : `${config.uploader.route}/:id`, config.uploader.route === '/' ? '/:id' : `${config.uploader.route}/:id`,
async (req, res, params) => { async (req, res, params) => {
if (params.id === '') return nextServer.render404(req, res as ServerResponse); if (params.id === '') return notFound(req, res, nextServer);
else if (params.id === 'dashboard') return nextServer.render(req, res as ServerResponse, '/dashboard'); else if (params.id === 'dashboard' && !config.features.headless)
return nextServer.render(req, res as ServerResponse, '/dashboard');
const image = await prisma.image.findFirst({ const image = await prisma.image.findFirst({
where: { where: {
@ -144,7 +150,7 @@ async function start() {
if (!image) return rawFile(req, res, nextServer, params.id); if (!image) return rawFile(req, res, nextServer, params.id);
else { else {
const failed = await preFile(image, prisma); const failed = await preFile(image, prisma);
if (failed) return nextServer.render404(req, res as ServerResponse); if (failed) return notFound(req, res, nextServer);
if (image.password || image.embed || image.mimetype.startsWith('text/')) if (image.password || image.embed || image.mimetype.startsWith('text/'))
redirect(res, `/view/${image.file}`); redirect(res, `/view/${image.file}`);
@ -156,7 +162,7 @@ async function start() {
); );
router.on('GET', '/r/:id', async (req, res, params) => { router.on('GET', '/r/:id', async (req, res, params) => {
if (params.id === '') return nextServer.render404(req, res as ServerResponse); if (params.id === '') return notFound(req, res, nextServer);
const image = await prisma.image.findFirst({ const image = await prisma.image.findFirst({
where: { where: {
@ -167,13 +173,17 @@ async function start() {
if (!image) await rawFile(req, res, nextServer, params.id); if (!image) await rawFile(req, res, nextServer, params.id);
else { else {
const failed = await preFile(image, prisma); const failed = await preFile(image, prisma);
if (failed) return nextServer.render404(req, res as ServerResponse); if (failed) return notFound(req, res, nextServer);
if (image.password) { if (image.password) {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.statusCode = 403; res.statusCode = 403;
return res.end( return res.end(
JSON.stringify({ error: "can't view a raw file that has a password", url: `/view/${image.file}` }) JSON.stringify({
error: "can't view a raw file that has a password",
url: `/view/${image.file}`,
code: 403,
})
); );
} else await rawFile(req, res, nextServer, params.id); } else await rawFile(req, res, nextServer, params.id);
} }
@ -202,7 +212,11 @@ async function start() {
http.listen(config.core.port, config.core.host ?? '0.0.0.0'); http.listen(config.core.port, config.core.host ?? '0.0.0.0');
logger.info(`started ${dev ? 'development' : 'production'} zipline@${version} server`); logger.info(
`started ${dev ? 'development' : 'production'} zipline@${version} server${
config.features.headless ? ' (headless)' : ''
}`
);
stats(prisma); stats(prisma);
clearInvites(prisma); clearInvites(prisma);
@ -211,6 +225,16 @@ async function start() {
setInterval(() => stats(prisma), config.core.stats_interval * 1000); setInterval(() => stats(prisma), config.core.stats_interval * 1000);
} }
async function notFound(req: IncomingMessage, res: ServerResponse, nextServer: NextServer) {
if (config.features.headless) {
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify({ error: 'not found', url: req.url, code: 404 }));
} else {
return notFound(req, res, nextServer);
}
}
async function preFile(file: Image, prisma: PrismaClient) { async function preFile(file: Image, prisma: PrismaClient) {
if (file.expires_at && file.expires_at < new Date()) { if (file.expires_at && file.expires_at < new Date()) {
await datasource.delete(file.file); await datasource.delete(file.file);
@ -242,7 +266,7 @@ async function postFile(file: Image, prisma: PrismaClient) {
async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: NextServer, id: string) { async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: NextServer, id: string) {
const data = await datasource.get(id); const data = await datasource.get(id);
if (!data) return nextServer.render404(req, res as ServerResponse); if (!data) return notFound(req, res as ServerResponse, nextServer);
const mimetype = await guess(extname(id)); const mimetype = await guess(extname(id));
const size = await datasource.size(id); const size = await datasource.size(id);
@ -253,7 +277,7 @@ async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: N
data.pipe(res); data.pipe(res);
data.on('error', (e) => { data.on('error', (e) => {
logger.debug(`error while serving raw file ${id}: ${e}`); logger.debug(`error while serving raw file ${id}: ${e}`);
nextServer.render404(req, res as ServerResponse); notFound(req, res as ServerResponse, nextServer);
}); });
data.on('end', () => res.end()); data.on('end', () => res.end());
} }
@ -269,7 +293,7 @@ async function fileDb(
if (Object.keys(exts).includes(ext)) return handle(req, res as ServerResponse); if (Object.keys(exts).includes(ext)) return handle(req, res as ServerResponse);
const data = await datasource.get(image.file); const data = await datasource.get(image.file);
if (!data) return nextServer.render404(req, res as ServerResponse); if (!data) return notFound(req, res as ServerResponse, nextServer);
const size = await datasource.size(image.file); const size = await datasource.size(image.file);
@ -279,7 +303,7 @@ async function fileDb(
data.pipe(res); data.pipe(res);
data.on('error', (e) => { data.on('error', (e) => {
logger.debug(`error while serving raw file ${image.file}: ${e}`); logger.debug(`error while serving raw file ${image.file}: ${e}`);
nextServer.render404(req, res as ServerResponse); notFound(req, res as ServerResponse, nextServer);
}); });
data.on('end', () => res.end()); data.on('end', () => res.end());
} }

2518
yarn.lock

File diff suppressed because it is too large Load diff