-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathconfigure.ts
More file actions
250 lines (207 loc) · 7.98 KB
/
Copy pathconfigure.ts
File metadata and controls
250 lines (207 loc) · 7.98 KB
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import fs from 'node:fs';
import { execSync } from 'node:child_process';
import { fetch } from 'undici';
import jwt from 'jsonwebtoken';
// Log levels supported by MediaMTX - anything else causes MediaMTX to refuse to start
const LOGLEVELS = ['error', 'warn', 'info', 'debug'];
const DEFAULT_LOGLEVEL = 'info';
const LOGLEVEL = resolveLogLevel();
const AUTH_ADDRESS = 'http://127.0.0.1:9995/auth';
// Default CoTURN listening port (coturn-infra coturn.conf listening-port)
const COTURN_PORT = 3478;
const webrtcAdditionalHosts = resolveWebRTCAdditionalHosts();
const webrtcEncryption = configureACMCertificate();
const coturn = await resolveCoturnConfig();
const yaml = generateConfig(
webrtcAdditionalHosts,
webrtcEncryption,
coturn
);
fs.writeFileSync('/mediamtx.yml', yaml);
console.log('ok - MediaMTX configuration written to /mediamtx.yml');
/**
* Resolve the MediaMTX log level from MEDIAMTX_LOGLEVEL, falling back to the
* MediaMTX default of `info`. An unknown value is ignored rather than written
* to the config, as MediaMTX would fail to start with it.
*/
function resolveLogLevel(): string {
const level = (process.env.MEDIAMTX_LOGLEVEL || '').trim().toLowerCase();
if (!level) return DEFAULT_LOGLEVEL;
if (!LOGLEVELS.includes(level)) {
console.error(`warn - Unknown MEDIAMTX_LOGLEVEL "${level}", falling back to ${DEFAULT_LOGLEVEL}`);
return DEFAULT_LOGLEVEL;
}
return level;
}
/**
* Extract the hostname from CLOUDTAK_Config_media_url and merge it
* with any existing MTX_WEBRTCADDITIONALHOSTS value.
*/
function resolveWebRTCAdditionalHosts(): string[] {
const hosts: string[] = [];
if (process.env.MTX_WEBRTCADDITIONALHOSTS) {
hosts.push(...process.env.MTX_WEBRTCADDITIONALHOSTS.split(',').filter(Boolean));
}
if (process.env.CLOUDTAK_Config_media_url) {
try {
const hostname = new URL(process.env.CLOUDTAK_Config_media_url).hostname;
if (hostname && !hosts.includes(hostname)) {
hosts.push(hostname);
}
} catch (err) {
console.error('warn - Failed to parse CLOUDTAK_Config_media_url:', err);
}
}
return hosts;
}
/**
* Fetch the CoTURN configuration from the CloudTAK Config API.
* Returns the TURN/STUN host and shared secret if both `coturn::url` and
* `coturn::secret` are set, otherwise null.
*
* The `coturn::*` keys are admin-only, so we mint a short-lived admin user
* token signed with the shared SigningSecret to read them.
*/
async function resolveCoturnConfig(): Promise<{ host: string; secret: string } | null> {
const apiUrl = process.env.API_URL;
const signingSecret = process.env.SigningSecret;
if (!apiUrl || !signingSecret) return null;
try {
const url = new URL(apiUrl + '/api/config');
url.searchParams.append('keys', 'coturn::url,coturn::secret');
const token = jwt.sign(
{ access: 'admin', email: 'media-infra@cloudtak.internal' },
signingSecret,
{ expiresIn: 120 }
);
const res = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`
}
});
if (!res.ok) {
console.error(`warn - Failed to fetch CoTURN config (${res.status}): ${await res.text()}`);
return null;
}
const body = await res.json() as {
'coturn::url'?: string;
'coturn::secret'?: string;
};
const host = (body['coturn::url'] || '').trim();
const secret = (body['coturn::secret'] || '').trim();
if (!host || !secret) return null;
return { host, secret };
} catch (err) {
console.error('warn - Failed to resolve CoTURN config:', err);
return null;
}
}
/**
* If ACM_CERTIFICATE_ARN is set, export the certificate and key
* to /server.crt and /server.key for WebRTC encryption.
* Returns true if encryption was configured.
*/
function configureACMCertificate(): boolean {
const arn = process.env.ACM_CERTIFICATE_ARN;
if (!arn) return false;
const passphraseFile = '/tmp/acm-passphrase';
const exportFile = '/tmp/acm-certificate.json';
const encryptedKeyFile = '/tmp/server.encrypted.key';
try {
execSync(`openssl rand -base64 48 | tr -d '\\n' > ${passphraseFile}`, { stdio: 'pipe' });
execSync(
`aws acm export-certificate --certificate-arn "${arn}" --passphrase "fileb://${passphraseFile}" --output json > ${exportFile}`,
{ stdio: 'pipe' }
);
const certData = JSON.parse(fs.readFileSync(exportFile, 'utf8'));
const cert = (certData.Certificate || '') + '\n' + (certData.CertificateChain || '');
fs.writeFileSync('/server.crt', cert, { mode: 0o644 });
const encryptedKey = certData.PrivateKey;
fs.writeFileSync(encryptedKeyFile, encryptedKey);
execSync(
`openssl pkcs8 -in ${encryptedKeyFile} -out /server.key -passin "file:${passphraseFile}"`,
{ stdio: 'pipe' }
);
fs.chmodSync('/server.key', 0o600);
} finally {
for (const f of [passphraseFile, exportFile, encryptedKeyFile]) {
try { fs.unlinkSync(f); } catch { /* ignore */ }
}
}
return true;
}
/**
* Read the static mediamtx.yml and apply dynamic overrides.
* Only the following values are overridden:
* - logLevel
* - authHTTPAddress
* - webrtcAdditionalHosts
* - webrtcEncryption (and related server key/cert paths)
* - webrtcICEServers2 (when CoTURN is configured)
*/
function generateConfig(
webrtcAdditionalHosts: string[],
webrtcEncryption: boolean,
coturn: { host: string; secret: string } | null
): string {
let config = fs.readFileSync('/mediamtx.yml', 'utf8');
// Override logLevel
config = config.replace(
/^logLevel: .*/m,
`logLevel: ${LOGLEVEL}`
);
// Override authHTTPAddress
config = config.replace(
/^authHTTPAddress: .*/m,
`authHTTPAddress: ${AUTH_ADDRESS}`
);
// Override webrtcAdditionalHosts
const hostsYAML = webrtcAdditionalHosts.length > 0
? webrtcAdditionalHosts.map((h) => ` - "${h}"`).join('\n')
: ' []';
config = config.replace(
/^webrtcAdditionalHosts:.*$/m,
`webrtcAdditionalHosts:\n${hostsYAML}`
);
// Override webrtcEncryption and related settings
if (webrtcEncryption) {
config = config.replace(
/^webrtcEncryption: .*/m,
'webrtcEncryption: true'
);
// Add server key/cert paths after webrtcEncryption line
config = config.replace(
/^(webrtcEncryption: true)$/m,
`$1\nwebrtcServerKey: /server.key\nwebrtcServerCert: /server.crt`
);
} else {
config = config.replace(
/^webrtcEncryption: .*/m,
'webrtcEncryption: false'
);
// Remove server key/cert paths if present
config = config.replace(/^\s*webrtcServerKey:.*$/m, '');
config = config.replace(/^\s*webrtcServerCert:.*$/m, '');
}
// Override webrtcICEServers2 with the CoTURN server when configured.
// All media is forced through TURN over TCP (?transport=tcp) for reliable
// traversal of restrictive networks. The TURN server requires authentication
// via the shared secret, which uses the MediaMTX "AUTH_SECRET" mechanism (the
// secret is placed in the password field) to match CoTURN's `use-auth-secret`.
if (coturn) {
const turnUrl = `turn:${coturn.host}:${COTURN_PORT}?transport=tcp`;
const iceServersYAML = [
'webrtcICEServers2:',
` - url: stun:${coturn.host}:${COTURN_PORT}`,
` - url: ${JSON.stringify(turnUrl)}`,
' username: AUTH_SECRET',
` password: ${JSON.stringify(coturn.secret)}`
].join('\n');
config = config.replace(
/^webrtcICEServers2:.*(?:\n[ \t]+-.*(?:\n[ \t]+[^-\s].*)*)*/m,
iceServersYAML
);
}
return config;
}