hfs
Version:
HTTP File Server
162 lines (161 loc) • 7.86 kB
JavaScript
// 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;
;