scrypted-kasa 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/camera/rtsp.ts +38 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrypted-kasa",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "TP-Link Kasa plugin for Scrypted — cameras (video, two-way audio, spotlight, siren, status LED), plugs/outlets, wall switches, dimmers, and smart bulbs (on/off, brightness, color, color temperature).",
|
|
5
5
|
"author": "Sergey Vilgelm <sergey@vilgelm.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/camera/rtsp.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface RtspHandlerOptions {
|
|
|
27
27
|
interface RtspRequest {
|
|
28
28
|
method: string;
|
|
29
29
|
uri: string;
|
|
30
|
-
headers:
|
|
30
|
+
headers: Map<string, string>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Minimal RTSP server-side handler for ONE TCP client. Supports the methods Scrypted (and
|
|
@@ -85,7 +85,7 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
|
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
const handleRequest = (req: RtspRequest) => {
|
|
88
|
-
const cseq = req.headers
|
|
88
|
+
const cseq = req.headers.get('cseq') || '0';
|
|
89
89
|
switch (req.method) {
|
|
90
90
|
case 'OPTIONS':
|
|
91
91
|
respond(cseq, 200, 'OK', {
|
|
@@ -98,7 +98,7 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
|
|
|
98
98
|
return;
|
|
99
99
|
|
|
100
100
|
case 'SETUP': {
|
|
101
|
-
const transport = req.headers
|
|
101
|
+
const transport = req.headers.get('transport') || '';
|
|
102
102
|
const m = /interleaved=(\d+)-(\d+)/i.exec(transport);
|
|
103
103
|
if (!m) {
|
|
104
104
|
// We only support TCP-interleaved transport. UDP would require allocating
|
|
@@ -153,6 +153,12 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
|
|
|
153
153
|
};
|
|
154
154
|
|
|
155
155
|
const HEADER_TERMINATOR = Buffer.from('\r\n\r\n');
|
|
156
|
+
// Caps on accumulated buffer size. Server is localhost-only, but a misbehaving or
|
|
157
|
+
// malicious client could trickle bytes without ever sending the header terminator,
|
|
158
|
+
// or advertise a giant Content-Length, forcing `buffered` to grow unbounded via
|
|
159
|
+
// Buffer.concat. Real RTSP requests for the methods we accept are well under 1 KB.
|
|
160
|
+
const RTSP_MAX_HEADER_BYTES = 8 * 1024;
|
|
161
|
+
const RTSP_MAX_BODY_BYTES = 4 * 1024;
|
|
156
162
|
client.on('data', (chunk: Buffer) => {
|
|
157
163
|
buffered = buffered.length === 0 ? chunk : Buffer.concat([buffered, chunk]);
|
|
158
164
|
// Parse messages until we run out of complete ones in the buffer.
|
|
@@ -168,21 +174,44 @@ export function handleRtspClient(client: net.Socket, opts: RtspHandlerOptions):
|
|
|
168
174
|
continue;
|
|
169
175
|
}
|
|
170
176
|
const headerEndIdx = buffered.indexOf(HEADER_TERMINATOR);
|
|
177
|
+
// Apply the cap whether or not the terminator has arrived: a single oversized
|
|
178
|
+
// chunk that contains the terminator would otherwise slip past a check that
|
|
179
|
+
// only fires while we're still waiting for it.
|
|
180
|
+
const headerLen = headerEndIdx < 0 ? buffered.length : headerEndIdx;
|
|
181
|
+
if (headerLen > RTSP_MAX_HEADER_BYTES) {
|
|
182
|
+
opts.console.warn('rtsp: header section exceeded cap, dropping client');
|
|
183
|
+
client.destroy();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
171
186
|
if (headerEndIdx < 0) return;
|
|
172
187
|
const headerStr = buffered.subarray(0, headerEndIdx).toString('utf8');
|
|
173
188
|
const headerLines = headerStr.split('\r\n');
|
|
174
189
|
const requestLine = headerLines.shift() || '';
|
|
175
190
|
const [method, uri] = requestLine.split(' ');
|
|
176
|
-
//
|
|
177
|
-
// crafted header name like `__proto__`. Practical risk is tiny — the server
|
|
178
|
-
// only listens on localhost — but
|
|
179
|
-
const headers =
|
|
191
|
+
// Map (not a plain object) so a malicious peer can't reach Object.prototype via
|
|
192
|
+
// a crafted header name like `__proto__`. Practical risk is tiny — the server
|
|
193
|
+
// only listens on localhost — but Map is the pattern CodeQL accepts.
|
|
194
|
+
const headers = new Map<string, string>();
|
|
180
195
|
for (const line of headerLines) {
|
|
181
196
|
const colon = line.indexOf(':');
|
|
182
197
|
if (colon < 0) continue;
|
|
183
|
-
headers
|
|
198
|
+
headers.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
|
|
199
|
+
}
|
|
200
|
+
// Strict validation: parseInt('1abc', 10) === 1, which would let us buffer
|
|
201
|
+
// 1 body byte and then desync the parser on the rest. Require a pure decimal
|
|
202
|
+
// string before trusting the length.
|
|
203
|
+
const rawContentLength = headers.get('content-length');
|
|
204
|
+
if (rawContentLength !== undefined && !/^\d+$/.test(rawContentLength)) {
|
|
205
|
+
opts.console.warn('rtsp: invalid content-length', rawContentLength);
|
|
206
|
+
client.destroy();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const contentLength = rawContentLength ? parseInt(rawContentLength, 10) : 0;
|
|
210
|
+
if (contentLength > RTSP_MAX_BODY_BYTES) {
|
|
211
|
+
opts.console.warn('rtsp: oversized content-length', rawContentLength);
|
|
212
|
+
client.destroy();
|
|
213
|
+
return;
|
|
184
214
|
}
|
|
185
|
-
const contentLength = parseInt(headers['content-length'] || '0', 10);
|
|
186
215
|
const totalLen = headerEndIdx + HEADER_TERMINATOR.length + contentLength;
|
|
187
216
|
if (buffered.length < totalLen) return;
|
|
188
217
|
// We currently ignore request bodies — none of the methods we handle need one.
|