main.ts 5.7 KB

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