main.ts 5.1 KB

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