UNPKG

hfs

Version:
162 lines (161 loc) 7.86 kB
"use strict"; // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.someSecurity = exports.cloudflareDetected = exports.headRequests = exports.gzipper = exports.sessionDuration = void 0; exports.getProxyDetected = getProxyDetected; exports.failAllowNet = failAllowNet; const koa_compress_1 = __importDefault(require("koa-compress")); const const_1 = require("./const"); const misc_1 = require("./misc"); const stream_1 = require("stream"); const block_1 = require("./block"); const perm_1 = require("./perm"); const connections_1 = require("./connections"); const auth_1 = require("./auth"); const zlib_1 = require("zlib"); const listen_1 = require("./listen"); const config_1 = require("./config"); const koa_session_1 = __importDefault(require("koa-session")); const index_1 = require("./index"); const events_1 = __importDefault(require("./events")); const forceHttps = (0, config_1.defineConfig)('force_https', true); (0, config_1.defineConfig)('ignore_proxies', false); const allowAuthorizationHeader = (0, config_1.defineConfig)('authorization_header', true); exports.sessionDuration = (0, config_1.defineConfig)('session_duration', Number(process.env.SESSION_DURATION) || misc_1.DAY / 1000, v => v * 1000); exports.gzipper = (0, koa_compress_1.default)({ threshold: 2048, gzip: { flush: zlib_1.constants.Z_SYNC_FLUSH }, deflate: { flush: zlib_1.constants.Z_SYNC_FLUSH }, br: false, // disable brotli filter(type) { return /text|javascript|style/i.test(type); }, }); const headRequests = async (ctx, next) => { const head = ctx.method === 'HEAD'; if (head) ctx.method = 'GET'; // let other middlewares work, so we can collect the size at the end await next(); if (!head || ctx.body === undefined) return; const { length, status } = ctx.response; if (ctx.body) ctx.body = stream_1.Readable.from(''); // empty the body for this is a HEAD request. Using Readable avoids koa from trying to set length to 0 ctx.status = status; if (length) ctx.response.length = length; }; exports.headRequests = headRequests; let proxyDetected; const someSecurity = (ctx, next) => { ctx.request.ip = (0, connections_1.normalizeIp)(ctx.ip); const ss = ctx.session; if ((ss === null || ss === void 0 ? void 0 : ss.username) && !(ss === null || ss === void 0 ? void 0 : ss[misc_1.ALLOW_SESSION_IP_CHANGE])) if (!ss.ip) ss.ip = ctx.ip; else if (ss.ip !== ctx.ip) { delete ss.username; ss.ip = ctx.ip; } try { if ((0, misc_1.dirTraversal)(decodeURI(ctx.path))) return ctx.status = const_1.HTTP_FOOL; if (!ctx.state.skipFilters && (0, block_1.applyBlock)(ctx.socket, ctx.ip)) return; if (ctx.get('X-Forwarded-For') // we have some dev-proxies to ignore && !(const_1.DEV && [process.env.FRONTEND_PROXY, process.env.ADMIN_PROXY].includes(ctx.get('X-Forwarded-port')))) { proxyDetected = ctx; ctx.state.whenProxyDetected = new Date(); } if (ctx.get('cf-ray')) exports.cloudflareDetected = new Date(); } catch (_a) { return ctx.status = const_1.HTTP_FOOL; } if (!ctx.secure && forceHttps.get() && (0, listen_1.getHttpsWorkingPort)() && !(0, misc_1.isLocalHost)(ctx)) { const { URL } = ctx; URL.protocol = 'https'; URL.port = (0, listen_1.getHttpsWorkingPort)(); ctx.status = 307; // this ensures the client doesn't switch to a simpler GET request return ctx.redirect(URL.href); } return next(); }; exports.someSecurity = someSecurity; // limited to http proxies function getProxyDetected() { if ((proxyDetected === null || proxyDetected === void 0 ? void 0 : proxyDetected.state.whenProxyDetected) < Date.now() - misc_1.DAY) // detection is reset after a day proxyDetected = undefined; return proxyDetected && { from: proxyDetected.socket.remoteAddress, for: proxyDetected.get('X-Forwarded-For') }; } const prepareState = async (ctx, next) => { var _a, _b; if ((_a = ctx.session) === null || _a === void 0 ? void 0 : _a.username) { if (ctx.session.ts < auth_1.invalidateSessionBefore.get(ctx.session.username)) delete ctx.session.username; ctx.session.maxAge = exports.sessionDuration.compiled(); } // calculate these once and for all ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket); const a = ctx.state.account = await urlLogin() || await getHttpAccount() || (0, perm_1.getAccount)((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username, false); if (a && (!(0, perm_1.accountCanLogin)(a) || failAllowNet(ctx, a))) // enforce allow_net also after login ctx.state.account = undefined; ctx.state.revProxyPath = ctx.get('x-forwarded-prefix'); (0, connections_1.updateConnectionForCtx)(ctx); await next(); function urlLogin() { const { login } = ctx.query; if (!login) return; const [u, p] = (0, misc_1.splitAt)(':', String(login)); ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length - 1)); // redirect to hide credentials return u && (0, auth_1.clearTextLogin)(ctx, u, p, 'url'); } function getHttpAccount() { var _a, _b; const b64 = allowAuthorizationHeader.get() && ((_a = ctx.get('authorization')) === null || _a === void 0 ? void 0 : _a.split(' ')[1]); if (!b64) return; try { const [u, p] = atob(b64).split(':'); if (!u || u === ((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username)) return; // providing credentials, but not needed return (0, auth_1.clearTextLogin)(ctx, u, p || '', 'header'); } catch (_c) { } } }; exports.prepareState = prepareState; function failAllowNet(ctx, a) { var _a, _b; const cached = (_a = ctx.session) === null || _a === void 0 ? void 0 : _a.allowNet; // won't reflect changes until session is terminated const mask = cached !== null && cached !== void 0 ? cached : (0, perm_1.getFromAccount)(a || '', a => a.allow_net); if (!cached && mask && ((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username)) ctx.session.allowNet = mask; // must be deleted on logout by setLoggedIn const ret = mask && !(0, misc_1.netMatches)(ctx.ip, mask, true); if (ret) console.debug("login failed: allow_net"); return ret; } const paramsDecoder = async (ctx, next) => { ctx.state.params = ctx.method === 'POST' && ctx.originalUrl.startsWith(const_1.API_URI) && ((0, misc_1.tryJson)(await (0, misc_1.stream2string)(ctx.req)) || {}); await next(); }; exports.paramsDecoder = paramsDecoder; // once https cookie is created, http cannot do the same. The solution is to use 2 different cookies. // But koa-session doesn't support 2 cookies, so I made this hacky solution: keep track of the options object, to modify the key at run-time. let internalSessionMw; let options; events_1.default.once('app', () => // wait for app to be defined internalSessionMw = (0, koa_session_1.default)(options = { signed: true, rolling: true, sameSite: 'lax' }, index_1.app)); const sessionMiddleware = (ctx, next) => { options.key = 'hfs_' + ctx.protocol; return internalSessionMw(ctx, next); }; exports.sessionMiddleware = sessionMiddleware;