servers.mjs 6.6 KB

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