app.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { opine, ErrorRequestHandler, Router, createHash, server, createError, Color, deferred, Deferred } from "./deps.ts";
  2. import { BBB } from './bbb.ts';
  3. const date = () => new Date().toLocaleTimeString('de')
  4. const VERSION = 'v1.5.1'
  5. // give your tinyscale server a secret so it looks like a BBB server
  6. const secret: string = Deno.env.get("TINYSCALE_SECRET") || ""
  7. if (!secret) throw "No secret set for tinyscale"
  8. const tinyscale_strict: boolean = Deno.env.get("TINYSCALE_STRICT") === 'false' ? false : true
  9. console.log(date() + Color.green(` Starting tinyscale ${VERSION} in ${tinyscale_strict ? 'strict':'loose'} mode`))
  10. console.log(`Your secret is set to ${Color.green(secret)}`)
  11. // store your BBB servers in servers.json
  12. const file: string = await Deno.readTextFile('servers.json')
  13. const servers: server[] = JSON.parse(file)
  14. // create an iterator so that we can treat all servers equally
  15. let iterator = servers[Symbol.iterator]();
  16. console.log('Checking servers first …')
  17. console.log(servers)
  18. // check servers for connectivity and if the secret is correct
  19. servers.forEach(async s => {
  20. const hash = createHash("sha1");
  21. hash.update(`getMeetings${s.secret}`)
  22. try {
  23. // throw an error if cannot connect or if secret fails
  24. const res = await fetch(`${s.host}/bigbluebutton/api/getMeetings?checksum=${hash.toString()}`)
  25. if (!res.ok) throw "Connection error. Please check your host configuration"
  26. const body = await res.text()
  27. const ok = body.includes('SUCCESS')
  28. console.log(`${s.host} is ${ok ? Color.green('ok') : Color.red('misconfigured. Please check your secret in servers.json')}`)
  29. if (!ok) throw "Configuration error. Exiting …"
  30. } catch (e) {
  31. // exit tinyscale if an error is encountered in servers.json
  32. console.log(Color.brightRed(e))
  33. Deno.exit(1);
  34. }
  35. })
  36. let current_server: server
  37. get_available_server()
  38. type waiter = Deferred<string>
  39. type queue = Record<string, waiter>
  40. let queue: queue = {}
  41. // pick the next server, using an iterator to cycle through all servers available
  42. function get_available_server(): server {
  43. let candidate = iterator.next()
  44. if (candidate.done) {
  45. iterator = servers[Symbol.iterator]()
  46. candidate = iterator.next()
  47. }
  48. console.log(`Using next server ${Color.green(candidate.value.host)}`)
  49. current_server = candidate.value;
  50. return current_server
  51. }
  52. const router = Router()
  53. // the api itself answering to every call
  54. router.all("/:call", async (req, res, next) => {
  55. const handler = new BBB(req)
  56. console.log(`${date()} New call to ${Color.green(handler.call)}`)
  57. if (!handler.authenticated(secret)) {
  58. console.log(`${Color.red("Rejected incoming call to "+handler.call)}`)
  59. next(createError(401))
  60. return
  61. }
  62. let server: server
  63. try {
  64. server = await handler.find_meeting_id(servers)
  65. } catch (e) {
  66. console.log(`Found no server with Meeting ID ${Color.yellow(handler.meeting_id)}`)
  67. if (handler.call === 'create' && tinyscale_strict) { get_available_server() }
  68. server = current_server
  69. }
  70. console.log(`Redirecting to ${server.host}`)
  71. const redirect = handler.rewritten_query(server)
  72. if (handler.call === 'join') {
  73. res.redirect(redirect)
  74. } else {
  75. try {
  76. const data = await fetch(redirect)
  77. const body = await data.text()
  78. if (handler.call === 'create') {queue[handler.meeting_id].resolve(body);delete queue[handler.meeting_id]}
  79. res.set('Content-Type', 'text/xml');
  80. res.send(body)
  81. } catch (e) {
  82. if (handler.call === 'create') {queue[handler.meeting_id].reject(Error);delete queue[handler.meeting_id]}
  83. next(createError(500))
  84. }
  85. }
  86. });
  87. // the fake answering machine to make sure we are recognized as a proper api
  88. router.get("/", (req, res, next) => {
  89. console.log('sending fake xml response')
  90. res.set('Content-Type', 'text/xml');
  91. res.send(`<response>
  92. <returncode>SUCCESS</returncode>
  93. <version>2.0</version>
  94. </response>`);
  95. })
  96. const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  97. res.setStatus(err.status ?? 500);
  98. res.end();
  99. console.log(`${Color.red(`${res.status}`)} ${req.originalUrl}`)
  100. };
  101. // @ts-ignore
  102. const check_if_create = async (req, res, next) => {
  103. console.log("check if room has been called before and has not yet finished")
  104. const meeting_id = req.query.meetingID
  105. console.log(meeting_id)
  106. const existing_id = queue[meeting_id]
  107. console.log(meeting_id, existing_id)
  108. if (existing_id) {
  109. try {
  110. const existing_res = await existing_id
  111. console.log(existing_res)
  112. res.send(existing_res)
  113. } catch (e) { next() }
  114. } else {
  115. queue[meeting_id] = deferred<string>();
  116. next()
  117. }
  118. }
  119. const app = opine()
  120. .use('/bigbluebutton/api/create', check_if_create)
  121. .use("/bigbluebutton/api", router)
  122. .use((req, res, next) => next(createError(404)))
  123. .use(errorHandler);
  124. export default app;