servers.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import fs from 'node:fs/promises'
  2. import http from 'node:http'
  3. import https from 'node:https'
  4. import { ApolloServer } from '@apollo/server'
  5. import { expressMiddleware } from '@as-integrations/express5'
  6. import { isEmpty } from 'lodash-es'
  7. import { Server as IoServer } from 'socket.io'
  8. import { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault } from '@apollo/server/plugin/landingPage/default'
  9. import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'
  10. import { initSchema } from '../graph/index.mjs'
  11. export default {
  12. graph: null,
  13. http: null,
  14. https: null,
  15. ws: null,
  16. connections: new Map(),
  17. le: null,
  18. /**
  19. * Initialize HTTP Server
  20. */
  21. async initHTTP () {
  22. WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
  23. this.http = http.createServer(WIKI.app)
  24. this.http.on('error', (error) => {
  25. if (error.syscall !== 'listen') {
  26. throw error
  27. }
  28. switch (error.code) {
  29. case 'EACCES':
  30. WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
  31. return process.exit(1)
  32. case 'EADDRINUSE':
  33. WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
  34. return process.exit(1)
  35. default:
  36. throw error
  37. }
  38. })
  39. this.http.on('listening', () => {
  40. WIKI.logger.info('HTTP Server: [ RUNNING ]')
  41. })
  42. this.http.on('connection', conn => {
  43. const connKey = `http:${conn.remoteAddress}:${conn.remotePort}`
  44. this.connections.set(connKey, conn)
  45. conn.on('close', () => {
  46. this.connections.delete(connKey)
  47. })
  48. })
  49. },
  50. /**
  51. * Start HTTP Server
  52. */
  53. async startHTTP () {
  54. this.http.listen(WIKI.config.port, WIKI.config.bindIP)
  55. },
  56. /**
  57. * Initialize HTTPS Server
  58. */
  59. async initHTTPS () {
  60. if (WIKI.config.ssl.provider === 'letsencrypt') {
  61. this.le = require('./letsencrypt')
  62. await this.le.init()
  63. }
  64. WIKI.logger.info(`HTTPS Server on port: [ ${WIKI.config.ssl.port} ]`)
  65. const tlsOpts = {}
  66. try {
  67. if (WIKI.config.ssl.format === 'pem') {
  68. tlsOpts.key = WIKI.config.ssl.inline ? WIKI.config.ssl.key : await fs.readFile(WIKI.config.ssl.key, 'utf-8')
  69. tlsOpts.cert = WIKI.config.ssl.inline ? WIKI.config.ssl.cert : await fs.readFile(WIKI.config.ssl.cert, 'utf-8')
  70. } else {
  71. tlsOpts.pfx = WIKI.config.ssl.inline ? WIKI.config.ssl.pfx : await fs.readFile(WIKI.config.ssl.pfx, 'utf-8')
  72. }
  73. if (!isEmpty(WIKI.config.ssl.passphrase)) {
  74. tlsOpts.passphrase = WIKI.config.ssl.passphrase
  75. }
  76. if (!isEmpty(WIKI.config.ssl.dhparam)) {
  77. tlsOpts.dhparam = WIKI.config.ssl.dhparam
  78. }
  79. } catch (err) {
  80. WIKI.logger.error('Failed to setup HTTPS server parameters:')
  81. WIKI.logger.error(err)
  82. return process.exit(1)
  83. }
  84. this.https = https.createServer(tlsOpts, WIKI.app)
  85. this.https.listen(WIKI.config.ssl.port, WIKI.config.bindIP)
  86. this.https.on('error', (error) => {
  87. if (error.syscall !== 'listen') {
  88. throw error
  89. }
  90. switch (error.code) {
  91. case 'EACCES':
  92. WIKI.logger.error('Listening on port ' + WIKI.config.ssl.port + ' requires elevated privileges!')
  93. return process.exit(1)
  94. case 'EADDRINUSE':
  95. WIKI.logger.error('Port ' + WIKI.config.ssl.port + ' is already in use!')
  96. return process.exit(1)
  97. default:
  98. throw error
  99. }
  100. })
  101. this.https.on('listening', () => {
  102. WIKI.logger.info('HTTPS Server: [ RUNNING ]')
  103. })
  104. this.https.on('connection', conn => {
  105. const connKey = `https:${conn.remoteAddress}:${conn.remotePort}`
  106. this.connections.set(connKey, conn)
  107. conn.on('close', () => {
  108. this.connections.delete(connKey)
  109. })
  110. })
  111. },
  112. /**
  113. * Start HTTPS Server
  114. */
  115. async startHTTPS () {
  116. this.https.listen(WIKI.config.ssl.port, WIKI.config.bindIP)
  117. },
  118. /**
  119. * Start GraphQL Server
  120. */
  121. async startGraphQL () {
  122. const graphqlSchema = await initSchema()
  123. this.graph = new ApolloServer({
  124. schema: graphqlSchema,
  125. allowBatchedHttpRequests: true,
  126. csrfPrevention: true,
  127. cache: 'bounded',
  128. plugins: [
  129. process.env.NODE_ENV === 'production'
  130. ? ApolloServerPluginLandingPageProductionDefault({
  131. footer: false
  132. })
  133. : ApolloServerPluginLandingPageLocalDefault({
  134. footer: false,
  135. embed: {
  136. endpointIsEditable: false,
  137. runTelemetry: false
  138. }
  139. })
  140. // ApolloServerPluginDrainHttpServer({ httpServer: this.http })
  141. // ...(this.https && ApolloServerPluginDrainHttpServer({ httpServer: this.https }))
  142. ]
  143. })
  144. await this.graph.start()
  145. WIKI.app.use(graphqlUploadExpress({
  146. maxFileSize: WIKI.config.security.uploadMaxFileSize,
  147. maxFiles: WIKI.config.security.uploadMaxFiles
  148. }))
  149. WIKI.app.use('/_graphql', expressMiddleware(this.graph, {
  150. context: ({ req, res }) => ({ req, res })
  151. }))
  152. },
  153. /**
  154. * Start Socket.io WebSocket Server
  155. */
  156. async initWebSocket () {
  157. if (this.https) {
  158. this.ws = new IoServer(this.https, {
  159. path: '/_ws/',
  160. serveClient: false
  161. })
  162. WIKI.logger.info('WebSocket Server attached to HTTPS Server [ OK ]')
  163. } else {
  164. this.ws = new IoServer(this.http, {
  165. path: '/_ws/',
  166. serveClient: false,
  167. cors: true // TODO: dev only, replace with app settings once stable
  168. })
  169. WIKI.logger.info('WebSocket Server attached to HTTP Server [ OK ]')
  170. }
  171. },
  172. /**
  173. * Close all active connections
  174. */
  175. closeConnections (mode = 'all') {
  176. for (const [key, conn] of this.connections) {
  177. if (mode !== 'all' && key.indexOf(`${mode}:`) !== 0) {
  178. continue
  179. }
  180. conn.destroy()
  181. this.connections.delete(key)
  182. }
  183. if (mode === 'all') {
  184. this.connections.clear()
  185. }
  186. },
  187. /**
  188. * Stop all servers
  189. */
  190. async stopServers () {
  191. this.closeConnections()
  192. if (this.http) {
  193. await new Promise(resolve => this.http.close(resolve))
  194. this.http = null
  195. }
  196. if (this.https) {
  197. await new Promise(resolve => this.https.close(resolve))
  198. this.https = null
  199. }
  200. this.graph = null
  201. },
  202. /**
  203. * Restart Server
  204. */
  205. async restartServer (srv = 'https') {
  206. this.closeConnections(srv)
  207. switch (srv) {
  208. case 'http':
  209. if (this.http) {
  210. await new Promise(resolve => this.http.close(resolve))
  211. this.http = null
  212. }
  213. this.initHTTP()
  214. this.startHTTP()
  215. break
  216. case 'https':
  217. if (this.https) {
  218. await new Promise(resolve => this.https.close(resolve))
  219. this.https = null
  220. }
  221. this.initHTTPS()
  222. this.startHTTPS()
  223. break
  224. default:
  225. throw new Error('Cannot restart server: Invalid designation')
  226. }
  227. }
  228. }