main.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import SERVERS from './servers.json' with { type: 'json' };
  2. import { crypto } from "https://deno.land/std@0.224.0/crypto/mod.ts";
  3. import { encodeHex } from "https://deno.land/std@0.224.0/encoding/hex.ts"
  4. import { red, green, yellow } from "https://deno.land/std@0.115.1/fmt/colors.ts";
  5. const SECRET = Deno.env.get("TINYSCALE_SECRET");
  6. const _port = Deno.env.get("PORT");
  7. const PORT = _port ? parseInt(_port) : undefined;
  8. const VERSION = 'v2.0.0'
  9. if (SECRET === undefined)
  10. throw "No `TINYSCALE_SECRET` set. tinyscale will exit.";
  11. if (SERVERS.length === 0)
  12. throw "There are no servers listed in `servers.json`";
  13. const date = () => new Date().toLocaleTimeString('de');
  14. class Server {
  15. host: string;
  16. #secret: string;
  17. static listServer: Server[] = [];
  18. static #currentServerIndex = 0;
  19. static get currentServer() {
  20. return Server.listServer[Server.#currentServerIndex];
  21. }
  22. constructor(host: string, secret: string) {
  23. this.host = host;
  24. this.#secret = secret;
  25. this.test().then(_ => Server.listServer.push(this)).catch(_ => Deno.exit(1));
  26. }
  27. static getNextServer() {
  28. Server.#currentServerIndex++;
  29. if (Server.#currentServerIndex === Server.listServer.length)
  30. Server.#currentServerIndex = 0;
  31. return Server.listServer[Server.#currentServerIndex];
  32. }
  33. static async hashCreate(params: string) {
  34. const hash = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(params));
  35. return encodeHex(hash);
  36. }
  37. static splitURL(url: URL) {
  38. // entferne `/bigbluebutton/api/`
  39. const call = url.pathname.slice(19);
  40. const params = url.search.replace('?', '').replace(/[?&]?checksum.*$/, '');
  41. return { call, params }
  42. }
  43. static async checkAuthenticated(url: URL) {
  44. const checksum = url.searchParams.get('checksum');
  45. if (checksum === null)
  46. return false;
  47. const { call, params } = Server.splitURL(url);
  48. const hash = await Server.hashCreate(`${call}${params}${SECRET}`);
  49. return hash === checksum;
  50. }
  51. async urlCreate(call: string, params = "") {
  52. const hash = await Server.hashCreate(`${call}${params}${this.#secret}`);
  53. return new URL(`${this.host}/${call}?${params}${params === '' ? '' : '&'}checksum=${hash}`);
  54. }
  55. async getResponse(query: string, params = "") {
  56. const url = await this.urlCreate(query, params);
  57. const res = await fetch(url);
  58. if (!res.ok)
  59. throw "Connection error. Please check your host configuration";
  60. const body = await res.text();
  61. return body;
  62. }
  63. async test() {
  64. try {
  65. const res = await this.getResponse('getMeetings');
  66. const ok = res.includes('SUCCESS');
  67. if (!ok)
  68. throw Error;
  69. console.log(`${this.host} is ${green('ok')}`);
  70. } catch (e) {
  71. console.log(`${this.host} is ${red('misconfigured. Please check your secret in servers.json')}`);
  72. throw e;
  73. }
  74. }
  75. }
  76. const queue: Map<string, Promise<unknown>> = new Map();
  77. for (const server of SERVERS) {
  78. new Server(server.host, server.secret);
  79. }
  80. Deno.serve({ port: PORT,
  81. onListen({ port, hostname }) {
  82. console.log(green(`Starting tinyscale ${VERSION} on Deno ${Deno.version.deno}`));
  83. console.log(`Your secret is set to ${green(SECRET)}`);
  84. console.log(`API available at ${green(`http://${hostname}:${port}/bigbluebutton/api/`)}`);
  85. console.log();
  86. console.log(`Running tests on ${SERVERS.length} host${SERVERS.length === 1 ? '':'s'}:`);
  87. }}, async (req) => {
  88. const url = new URL(req.url);
  89. const { call, params } = Server.splitURL(url);
  90. const { promise, resolve } = Promise.withResolvers();
  91. let log = `${date()} `;
  92. // voicemail if just looking for a life sign from the server
  93. if (url.pathname === '/bigbluebutton/api/' || url.pathname === '/bigbluebutton/')
  94. return new Response("<response><returncode>SUCCESS</returncode><version>2.0</version></response>");
  95. // check the checksum and fail if not true
  96. const authenticated = await Server.checkAuthenticated(url);
  97. if (!authenticated) {
  98. log = log + red(`401: ${url.pathname}`);
  99. return new Response("<response><returncode>FAILED</returncode><messageKey>checksumError</messageKey><message>Checksums do not match</message></response>",
  100. { status: 401, headers: { "content-type": "text/xml" } });
  101. }
  102. // if there's a meeting/recording id, find the server which has it
  103. let selectedServer: Server | undefined;
  104. const meetingID = url.searchParams.get('meetingID');
  105. const recordingID = url.searchParams.get('recordingID');
  106. if (meetingID !== null) {
  107. if (call === 'create') {
  108. // if there's a request for the same room creation, wait for it
  109. if (queue.has(meetingID)) {
  110. console.log(`Race pending for meeting-ID: ${red(meetingID)}`);
  111. await queue.get(meetingID);
  112. }
  113. queue.set(meetingID, promise);
  114. }
  115. for (const server of Server.listServer) {
  116. const meetings = await server.getResponse('getMeetings');
  117. if (meetings.includes(meetingID)) {
  118. selectedServer = server;
  119. break;
  120. }
  121. }
  122. }
  123. else if (recordingID !== null)
  124. for (const server of Server.listServer) {
  125. const recordings = await server.getResponse('getRecordings');
  126. if (recordings.includes(recordingID)) {
  127. selectedServer = server;
  128. break
  129. }
  130. }
  131. log = log + `${green(`${call}`)} found, reply with`;
  132. if (call === 'create' && selectedServer === undefined && meetingID !== null) {
  133. selectedServer = Server.getNextServer();
  134. console.log(date()+' '+green('create')+' '+yellow('not found')+", opening a new room on "+green(selectedServer.host));
  135. const body = await selectedServer.getResponse('create', params);
  136. resolve(meetingID);
  137. queue.delete(meetingID);
  138. return new Response(body, { headers: { "content-type": "text/xml" } });
  139. } else if (call === 'create' && meetingID !== null) {
  140. resolve(meetingID);
  141. queue.delete(meetingID);
  142. }
  143. if (selectedServer === undefined)
  144. selectedServer = Server.currentServer;
  145. log = log + ' ' + selectedServer.host;
  146. console.log(log);
  147. // return the new URL to the real BBB server
  148. const newURL = await selectedServer.urlCreate(call, params);
  149. return Response.redirect(newURL);
  150. })