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