main
ts 89 lines 3.98 KB
Raw
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 })