| 1 | #!/usr/bin/env node |
| 2 | // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt |
| 3 | |
| 4 | import Koa from 'koa' |
| 5 | import mount from 'koa-mount' |
| 6 | import './consoleLog' |
| 7 | import { apiMiddleware } from './apiMiddleware' |
| 8 | import { API_URI, DEV, VERSION } from './const' |
| 9 | import { frontEndApis } from './frontEndApis' |
| 10 | import { logMw } from './log' |
| 11 | import { pluginsMiddleware } from './plugins' |
| 12 | import { throttler } from './throttler' |
| 13 | import { headRequests, gzipper, someSecurity, prepareState, paramsDecoder, sessionMiddleware } from './middlewares' |
| 14 | import { serveSharedFiles, guiFilesMiddleware } from './serveGuiAndSharedFiles' |
| 15 | import { webdav } from './webdav' |
| 16 | import './listen' |
| 17 | import './commands' |
| 18 | import { adminApis } from './adminApis' |
| 19 | import { defineConfig, Version } from './config' |
| 20 | import { ok } from 'assert' |
| 21 | import _ from 'lodash' |
| 22 | import { httpStream } from './misc' |
| 23 | import { randomBytes } from 'node:crypto' |
| 24 | import { selfCheckMiddleware } from './selfCheck' |
| 25 | import { acmeMiddleware } from './acme' |
| 26 | import './geo' |
| 27 | import { geoFilter } from './geo' |
| 28 | import { rootsMiddleware } from './roots' |
| 29 | import events from './events' |
| 30 | import { trackIpsMw } from './ips' |
| 31 | import './outboundProxy' |
| 32 | |
| 33 | ok(_.intersection(Object.keys(frontEndApis), Object.keys(adminApis)).length === 0) // they share same endpoints, don't clash |
| 34 | |
| 35 | if (new Version(process.versions.node).olderThan('18.15.0')) { |
| 36 | console.error("Node.js 18.15+ is required, please update") |
| 37 | process.exit(2) |
| 38 | } |
| 39 | |
| 40 | process.title = 'HFS ' + VERSION |
| 41 | httpStream.defaultUA = 'HFS' |
| 42 | const keys = process.env.COOKIE_SIGN_KEYS?.split(',') |
| 43 | || [randomBytes(32).toString('base64url')] // cookie signatures need randomness that cannot be reconstructed from observable Math.random() output |
| 44 | export const app = new Koa({ keys }) |
| 45 | app.use(sessionMiddleware) |
| 46 | .use(selfCheckMiddleware) |
| 47 | .use(acmeMiddleware) |
| 48 | .use(prepareState) |
| 49 | .use(geoFilter) |
| 50 | .use(trackIpsMw) |
| 51 | .use(gzipper) |
| 52 | .use(paramsDecoder) // must be done before plugins, so they can manipulate params |
| 53 | .use(headRequests) |
| 54 | .use(rootsMiddleware) |
| 55 | .use(logMw) |
| 56 | .use(someSecurity) |
| 57 | .use(throttler) |
| 58 | .use(pluginsMiddleware) |
| 59 | .use(mount(API_URI, apiMiddleware({ ...frontEndApis, ...adminApis }))) |
| 60 | .use(guiFilesMiddleware) |
| 61 | .use(webdav) |
| 62 | .use(serveSharedFiles) |
| 63 | .on('error', errorHandler) |
| 64 | events.emit('app', app) |
| 65 | |
| 66 | function errorHandler(err: Error & { code?: string, path?: string }) { |
| 67 | const { code } = err |
| 68 | if (DEV && code === 'ENOENT' && err.path?.endsWith('sockjs-node')) return // spam out dev stuff |
| 69 | if (code === 'ECANCELED' || code === 'ECONNRESET' || code === 'ECONNABORTED' || code === 'EPIPE' |
| 70 | || code === 'ERR_STREAM_WRITE_AFTER_END' // happens disconnecting uploads, don't care |
| 71 | || code === 'ERR_STREAM_PREMATURE_CLOSE' // happens when many files are sent (not locally), but I checked that the files are written completely. Introduced after node18.5.0 and is thrown by pipeline() used by PUT method handler. |
| 72 | || code === 'ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC' // tls peers/proxies can abort encrypted streams after headers are already sent |
| 73 | || code?.startsWith('HPE')) return // malformed client/probe HTTP parser errors, not internal failures |
| 74 | console.error('Server error', err) |
| 75 | } |
| 76 | |
| 77 | process.on('uncaughtException', (err: any) => { |
| 78 | if (err.syscall !== 'watch' && err.code !== 'ECONNRESET' && err.code !== 'EIO') // EIO seems to happen when the terminal is closed |
| 79 | try { console.error("Uncaught:", err) } |
| 80 | catch {} // in case we are writing to a closed terminal, we may throw with "write eio at afterwritedispatched", causing an infinite loop |
| 81 | }) |
| 82 | // this warning is scaring users, and has been removed in node 20.12.0 https://github.com/nodejs/node/pull/51204 |
| 83 | const original = process.emitWarning |
| 84 | process.emitWarning = warn => String(warn).startsWith('An error event has already been emitted') || original.call(process, warn) |
| 85 | |
| 86 | defineConfig('proxies', 0).sub(n => { |
| 87 | app.proxy = n > 0 |
| 88 | app.maxIpsCount = n |
| 89 | }) |