core: add server-loader (#816)

This commit is contained in:
undefined authored and GitHub committed 2024-06-03 19:29:41 +08:00
1 parent 3302480348
commit 97720efb65
57 files changed
+1328 -1131

No files matched your search

+6 -41
View File
@@ -1,11 +1,10 @@
import { PassThrough } from 'stream';
import type { Next } from 'koa';
import { cloneDeep, omit, pick } from 'lodash';
import { cloneDeep, omit } from 'lodash';
import type { KoaContext } from '@hydrooj/framework';
import { randomPick } from '@hydrooj/utils';
import { PERM } from '../../model/builtin';
import * as system from '../../model/system';
import token from '../../model/token';
import type { HydroRequest, HydroResponse, KoaContext } from '../server';
export interface UiContextBase {
cdn_prefix: string;
@@ -21,41 +20,6 @@ export const UiContextBase: UiContextBase = {
export default async (ctx: KoaContext, next: Next) => {
// Base Layer
const { domainId, domainInfo } = ctx;
const [xff, xhost] = system.getMany(['server.xff', 'server.xhost']);
const request: HydroRequest = {
method: ctx.request.method.toLowerCase(),
host: ctx.request.headers[xhost?.toLowerCase() || ''] as string || ctx.request.host,
hostname: ctx.request.hostname,
ip: ctx.request.headers[xff?.toLowerCase() || ''] as string || ctx.request.ip,
headers: ctx.request.headers,
cookies: ctx.cookies,
...pick(ctx, ['query', 'path', 'params', 'originalPath', 'querystring']),
body: ctx.request.body,
files: ctx.request.files as any,
referer: ctx.request.headers.referer || '',
json: (ctx.request.headers.accept || '').includes('application/json'),
websocket: ctx.request.headers.upgrade === 'websocket',
};
request.ip = request.ip.split(',')[0].trim();
const response: HydroResponse = {
body: {},
type: '',
status: null,
template: null,
redirect: null,
attachment: (name, streamOrBuffer) => {
ctx.attachment(name);
if (streamOrBuffer instanceof Buffer) {
response.body = null;
ctx.body = streamOrBuffer;
} else {
response.body = null;
ctx.body = streamOrBuffer.pipe(new PassThrough());
}
},
addHeader: (name: string, value: string) => ctx.set(name, value),
disposition: null,
};
const args = {
domainId, ...ctx.params, ...ctx.query, ...ctx.request.body, __start: Date.now(),
};
@@ -68,9 +32,9 @@ export default async (ctx: KoaContext, next: Next) => {
}
UiContext.domainId = domainId;
UiContext.domain = domainInfo;
ctx.HydroContext = {
request, response, UiContext, domain: domainInfo, user: null, args,
};
ctx.HydroContext.UiContext = UiContext;
ctx.HydroContext.domain = domainInfo;
ctx.HydroContext.args = args;
const header = ctx.request.headers.authorization;
const sid = header
? header.split(' ')[1] // Accept bearer token
@@ -78,6 +42,7 @@ export default async (ctx: KoaContext, next: Next) => {
const session = sid ? await token.get(sid instanceof Array ? sid[0] : sid, token.TYPE_SESSION) : null;
ctx.session = Object.create(session || { uid: 0, scope: PERM.PERM_ALL.toString() });
await next();
const request = ctx.HydroContext.request;
const ua = request.headers['user-agent'] || '';
if (!ctx.session.uid && system.get('server.ignoreUA').replace(/\r/g, '').split('\n').filter((i) => i && ua.includes(i)).length) return;
const expireSeconds = ctx.session.save
@@ -1,7 +1,7 @@
import { KoaContext } from '@hydrooj/framework';
import BlackListModel from '../../model/blacklist';
import DomainModel from '../../model/domain';
import * as system from '../../model/system';
import { KoaContext } from '../server';
export default async (ctx: KoaContext, next) => {
const forceDomain = /^\/d\/([^/]+)\//.exec(ctx.request.path);
@@ -1,78 +0,0 @@
import { ObjectId } from 'mongodb';
import avatar from '../../lib/avatar';
import serializer from '../../lib/serializer';
import { PERM } from '../../model/builtin';
import * as system from '../../model/system';
import type { KoaContext } from '../server';
export default (router, logger) => async (ctx: KoaContext, next) => {
const { request, response } = ctx.HydroContext;
ctx.renderHTML = (templateName, args) => {
const user = ctx.HydroContext.user;
const UserContext: any = {
...(user || {}),
avatar: avatar(user?.avatar || '', 128),
viewLang: ctx.translate('__id'),
};
const type = templateName.split('.')[1];
const engine = global.Hydro.module.render[type]
|| (() => JSON.stringify(args, serializer({
showDisplayName: user?.hasPerm(PERM.PERM_VIEW_DISPLAYNAME),
})));
return engine(templateName, {
handler: ctx.handler,
UserContext,
url: ctx.getUrl,
_: ctx.translate,
...args,
});
};
ctx.render = async (name, args) => {
response.body = await ctx.renderHTML(name, args);
response.type = 'text/html';
};
ctx.getUrl = (name: string, ...kwargsList: Record<string, any>[]) => {
if (name === '#') return '#';
let res = '#';
const args: any = {};
const query: any = {};
for (const kwargs of kwargsList) {
for (const key in kwargs) {
if (kwargs[key] instanceof ObjectId) args[key] = kwargs[key].toHexString();
else args[key] = kwargs[key].toString().replace(/\//g, '%2F');
}
for (const key in kwargs.query || {}) {
if (query[key] instanceof ObjectId) query[key] = kwargs.query[key].toHexString();
else query[key] = kwargs.query[key].toString();
}
}
try {
const { anchor } = args;
let withDomainId = args.domainId || false;
const domainId = ctx.HydroContext.args.domainId;
const host = ctx.HydroContext.domain?.host;
if (domainId !== 'system' && (
!request.host
|| (host instanceof Array
? (!host.includes(request.host))
: request.host !== host)
)) withDomainId ||= domainId;
res = router.url(name, args, { query }).toString();
if (anchor) res = `${res}#${anchor}`;
if (withDomainId) res = `/d/${withDomainId}${res}`;
} catch (e) {
logger.warn(e.message);
logger.info('%s %o', name, args);
if (!e.message.includes('Expected') || !e.message.includes('to match')) logger.info('%s', e.stack);
}
return res;
};
ctx.translate = (str: string) => {
if (!str) return '';
const lang = ctx.HydroContext.user?.viewLang || ctx.session?.viewLang;
return lang
? str.toString().translate(lang, ...ctx.acceptsLanguages())
: str.toString().translate(...ctx.acceptsLanguages(), system.get('server.language'));
};
await next();
};
@@ -1,75 +0,0 @@
import { errorMessage } from '@hydrooj/utils/lib/utils';
import { SystemError, UserFacingError } from '../../error';
import serializer from '../../lib/serializer';
import { PERM } from '../../model/builtin';
import type { KoaContext } from '../server';
export default (logger) => async (ctx: KoaContext, next) => {
const { request, response } = ctx.HydroContext;
try {
await next();
const { UiContext, user, args } = ctx.HydroContext;
if (response.redirect) {
response.body ||= {};
response.body.url = response.redirect;
}
if (!response.type) {
if (response.pjax && args.pjax) {
const html = await ctx.renderHTML(response.pjax, response.body);
response.body = { fragments: [{ html }] };
response.type = 'application/json';
} else if (
request.json || response.redirect
|| request.query.noTemplate || !response.template) {
// Send raw data
try {
if (typeof response.body === 'object') {
response.body.UiContext = UiContext;
response.body.UserContext = user;
}
response.body = JSON.stringify(response.body, serializer({ showDisplayName: user?.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) }));
} catch (e) {
response.body = new SystemError('Serialize failure', e.message);
}
response.type = 'application/json';
} else if (response.template) {
const s = response.template.split('.');
let templateName = `${s[0]}.${args.domainId}.${s[1]}`;
if (!global.Hydro.ui.template[templateName]) templateName = response.template;
await ctx.render(templateName, response.body || {});
}
}
if (response.disposition) ctx.set('Content-Disposition', response.disposition);
if (response.etag) {
ctx.set('ETag', response.etag);
ctx.set('Cache-Control', 'public');
}
} catch (err) {
const error = errorMessage(err);
response.status = error instanceof UserFacingError ? error.code : 500;
if (request.json) response.body = { error };
else {
try {
await ctx.render(error instanceof UserFacingError ? 'error.html' : 'bsod.html', { UserFacingError, error });
} catch (e) {
logger.error(e);
// this.response.body.error = {};
}
}
} finally {
if (response.etag && request.headers['if-none-match'] === response.etag) {
ctx.response.status = 304;
} else if (response.redirect && !request.json) {
ctx.response.type = 'application/octet-stream';
ctx.response.status = 302;
ctx.redirect(response.redirect);
} else if (response.body) {
ctx.body = response.body;
ctx.response.status = response.status || 200;
ctx.response.type = response.type
|| (request.json
? 'application/json'
: ctx.response.type);
}
}
};
+1 -1
View File
@@ -1,7 +1,7 @@
import type { KoaContext } from '@hydrooj/framework';
import { NotFoundError } from '../../error';
import { PERM } from '../../model/builtin';
import UserModel from '../../model/user';
import type { KoaContext } from '../server';
export default async (ctx: KoaContext, next) => {
// User Layer