app.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
  2. const HTTPS_PORT = process.env.PORT || 8080;
  3. const DOCKER_HOST = process.env.DOCKER_HOST;
  4. const DOCKER_PORT = process.env.DOCKER_PORT;
  5. const DOCKER_NETWORK = process.env.DOCKER_NETWORK || 'bridge';
  6. const GAME_CONTAINER_PREFIX = process.env.GAME_CONTAINER_PREFIX || 'monopoly-game-';
  7. const MAX_GAMES = process.env.MAX_GAMES || 500;
  8. const GAME_TTL = process.env.GAME_TTL || 24;
  9. const GAME_IMAGE = process.env.GAME_IMAGE || 'gonzague/monopoly';
  10. let dockerConfig = {socketPath: DOCKER_SOCKET};
  11. if (DOCKER_HOST && DOCKER_PORT) {
  12. dockerConfig = {
  13. host: DOCKER_HOST,
  14. port: DOCKER_PORT
  15. }
  16. }
  17. // HTTPS or not stuff
  18. const key = process.env.HTTP_TLS_KEY || 'key.pem';
  19. const cert = process.env.HTTP_TLS_CERTIFICATE || 'cert.pem';
  20. const USE_HTTPS = process.env.HTTP !== "true";
  21. const fs = require('fs');
  22. const http = USE_HTTPS ? require('https') : require('http');
  23. const serverConfig = USE_HTTPS ? {
  24. key: fs.readFileSync(key),
  25. cert: fs.readFileSync(cert),
  26. } : {}
  27. // imports
  28. const express = require('express');
  29. const app = express();
  30. const {createProxyMiddleware} = require('http-proxy-middleware');
  31. const Docker = require('dockerode');
  32. const {v4: uuidv4} = require('uuid');
  33. const fetch = require("node-fetch");
  34. docker = new Docker(dockerConfig);
  35. /**
  36. * Extracts a game ID from a string
  37. * @param str
  38. * @returns {*|string}
  39. */
  40. function getGameId(str) {
  41. var test = str.match(/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/g);
  42. console.log('getGameid', str, test);
  43. return test[0];
  44. }
  45. /**
  46. * Route reverse proxy url to the related monopoly game
  47. * @param req
  48. * @returns {string}
  49. */
  50. function customRouter(req) {
  51. const seemsWs = !req.protocol;
  52. console.log('request', req.url, getGameId(req.url));
  53. const gameName = GAME_CONTAINER_PREFIX + getGameId(req.url);
  54. let newUrl = 'http://' + gameName + ":8443";
  55. if (seemsWs) {
  56. newUrl.replace('http', 'ws');
  57. }
  58. console.log('routing to', newUrl);
  59. return newUrl;
  60. };
  61. /**
  62. * Rewrites the path of the proxy
  63. * @param path
  64. * @param req
  65. * @returns {string}
  66. */
  67. function rewriteGamePAth(path, req) {
  68. let newPath = path.replace('/game/' + getGameId(req.url), '/').replace('//', '/');
  69. console.log('new Path', newPath);
  70. return newPath;
  71. };
  72. const options = {
  73. target: 'http://localhost:8000',
  74. // ws: true,
  75. router: customRouter,
  76. logLevel: 'debug',
  77. pathRewrite: rewriteGamePAth
  78. };
  79. const myProxy = createProxyMiddleware(options);
  80. /**
  81. * Gets a list of running game by inspecing the containers on the hosts that match the GAME_PREFIX
  82. * @returns {Promise<*>}
  83. */
  84. async function getRunningGames() {
  85. const listContainers = await docker.listContainers();
  86. let games = listContainers.filter(c => c.Names.some(n => n.indexOf(GAME_CONTAINER_PREFIX) !== -1));
  87. return games;
  88. }
  89. /**
  90. * Checks whether a game is expired by calling /stats on the container.
  91. * if the container can't be reached it'll be terminated
  92. * @param container
  93. * @returns {Promise<boolean>}
  94. */
  95. async function isGameExpired(container) {
  96. const gameId = getGameId(container.Names.find(n => n.indexOf(GAME_CONTAINER_PREFIX) !== -1));
  97. const stats = await fetch('http://' + GAME_CONTAINER_PREFIX + gameId + ':8443/stats')
  98. .then(r => r.json())
  99. .catch(e => console.log("Couldn't reach server"));
  100. if (stats && stats.lastActivity) {
  101. const now = Date.now();
  102. return (now - stats.lastActivity) > 1000 * 60 * 60 * GAME_TTL;
  103. } else {
  104. // if we can't get the info, the game is expired
  105. return true;
  106. }
  107. }
  108. /**
  109. * Checks the started games and if expired, the container will be stopped
  110. */
  111. async function retireExpiredGames() {
  112. console.log('Checking running games');
  113. const games = await getRunningGames();
  114. console.log('running games', games.length);
  115. games.forEach(async g => {
  116. let container = docker.getContainer(g.Id);
  117. // container.stop();
  118. if (await isGameExpired(g)) {
  119. console.log('stopping', g.Names);
  120. container.stop()
  121. }
  122. })
  123. }
  124. /**
  125. * Creates a new game
  126. * @param req
  127. * @param res
  128. * @returns the game name
  129. */
  130. async function createNewGame(req, res) {
  131. console.log('creating new game');
  132. const gameName = uuidv4();
  133. const games = await getRunningGames();
  134. if (games.length < MAX_GAMES) {
  135. docker.createContainer({
  136. Image: GAME_IMAGE,
  137. name: GAME_CONTAINER_PREFIX + gameName,
  138. Env: [
  139. "HTTP=true"
  140. ],
  141. HostConfig: {
  142. NetworkMode: DOCKER_NETWORK,
  143. AutoRemove: true
  144. }
  145. }).then(function (container) {
  146. container.start();
  147. });
  148. res.send(gameName);
  149. } else {
  150. res.status(401);
  151. }
  152. }
  153. /**
  154. * Get stats of monopoly manager
  155. * @param req
  156. * @param res
  157. * @returns {Promise<void>}
  158. */
  159. async function getStats(req, res) {
  160. const games = await getRunningGames();
  161. const current = games.length;
  162. res.send(JSON.stringify({
  163. ttl: GAME_TTL,
  164. current: games.length,
  165. max: MAX_GAMES
  166. }));
  167. }
  168. console.log('Pulling ' + GAME_IMAGE)
  169. docker.pull(GAME_IMAGE).then(s => {
  170. console.log('Pull compete, starting server');
  171. //clean games every hour
  172. // setInterval(retireExpiredGames, 3600 * 1000);
  173. setInterval(retireExpiredGames, 60 * 60 * 1000);
  174. // pulling the game every 24 hours
  175. setInterval(() => {
  176. console.log('Pulling ' + GAME_IMAGE)
  177. docker.pull(GAME_IMAGE).then(s => console.log('pull complete'));
  178. }, 1000 * 60 * 60 * 24)
  179. app.use(express.static('static'));
  180. app.get('/new-game', createNewGame);
  181. app.get('/stats', getStats);
  182. app.use('/game/:gameId', myProxy);
  183. const httpsServer = http.createServer(serverConfig, app);
  184. httpsServer.listen(HTTPS_PORT, '0.0.0.0');
  185. httpsServer.on('upgrade', myProxy.upgrade); // <-- subscribe to http 'upgrade'
  186. console.log('Server running. Visit http' + (USE_HTTPS ? "s" : "") + '://localhost:' + HTTPS_PORT + ' in Firefox/Chrome.\n\n\
  187. Some important notes:\n\
  188. * Some browsers or OSs may not allow the webcam to be used by multiple pages at once. You may need to use two different browsers or machines.\n\
  189. * Some browsers may not allow the webcam and microphone to work on a non secure connection\n'
  190. );
  191. }
  192. );