@lumiastream/tapo-cove 3.12.1 → 3.13.0
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/CHANGELOG.md +13 -0
- package/dist/index.d.mts +8 -9
- package/dist/index.d.ts +8 -9
- package/dist/index.js +56 -52
- package/dist/index.mjs +56 -52
- package/package.json +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [3.13.0](https://github.com/lumiastream/rgb/compare/v3.12.2...v3.13.0) (2024-02-29)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- move from fetch work to axios ([07e5dac](https://github.com/lumiastream/rgb/commit/07e5dac313cf1f00845a29918c1f352201eb3a34))
|
|
11
|
+
- tapo does not need to decrypt response ([36f7b4c](https://github.com/lumiastream/rgb/commit/36f7b4c5bc4a9d7553c1090a0c08a9f76fe9ae19))
|
|
12
|
+
|
|
13
|
+
## [3.12.2](https://github.com/lumiastream/rgb/compare/v3.12.1...v3.12.2) (2023-10-26)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- tapo cookie error ([5aa2093](https://github.com/lumiastream/rgb/commit/5aa20933547b1782bbd27ea321c3c75dfe046992))
|
|
18
|
+
|
|
6
19
|
## [3.12.1](https://github.com/lumiastream/rgb/compare/v3.12.0...v3.12.1) (2023-10-26)
|
|
7
20
|
|
|
8
21
|
### Bug Fixes
|
package/dist/index.d.mts
CHANGED
|
@@ -97,14 +97,13 @@ declare class TapoApi {
|
|
|
97
97
|
email: string;
|
|
98
98
|
password: string;
|
|
99
99
|
}) => Promise<string>;
|
|
100
|
-
setup: (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}) => Promise<Map<string, DeviceSession>>;
|
|
100
|
+
setup: (devices: Array<{
|
|
101
|
+
id: string;
|
|
102
|
+
host: string;
|
|
103
|
+
}>, logger?: any) => Promise<{
|
|
104
|
+
responses: PromiseSettledResult<void>[];
|
|
105
|
+
devices: Map<string, DeviceSession>;
|
|
106
|
+
}>;
|
|
108
107
|
sendState: (config: {
|
|
109
108
|
device: {
|
|
110
109
|
id: string;
|
|
@@ -121,7 +120,7 @@ declare class TapoApi {
|
|
|
121
120
|
power: boolean;
|
|
122
121
|
}) => void;
|
|
123
122
|
private sessionPost;
|
|
124
|
-
needsNewHandshake(): boolean;
|
|
123
|
+
needsNewHandshake(devSession: any): boolean;
|
|
125
124
|
private handshake;
|
|
126
125
|
private firstHandshake;
|
|
127
126
|
private secondHandshake;
|
package/dist/index.d.ts
CHANGED
|
@@ -97,14 +97,13 @@ declare class TapoApi {
|
|
|
97
97
|
email: string;
|
|
98
98
|
password: string;
|
|
99
99
|
}) => Promise<string>;
|
|
100
|
-
setup: (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}) => Promise<Map<string, DeviceSession>>;
|
|
100
|
+
setup: (devices: Array<{
|
|
101
|
+
id: string;
|
|
102
|
+
host: string;
|
|
103
|
+
}>, logger?: any) => Promise<{
|
|
104
|
+
responses: PromiseSettledResult<void>[];
|
|
105
|
+
devices: Map<string, DeviceSession>;
|
|
106
|
+
}>;
|
|
108
107
|
sendState: (config: {
|
|
109
108
|
device: {
|
|
110
109
|
id: string;
|
|
@@ -121,7 +120,7 @@ declare class TapoApi {
|
|
|
121
120
|
power: boolean;
|
|
122
121
|
}) => void;
|
|
123
122
|
private sessionPost;
|
|
124
|
-
needsNewHandshake(): boolean;
|
|
123
|
+
needsNewHandshake(devSession: any): boolean;
|
|
125
124
|
private handshake;
|
|
126
125
|
private firstHandshake;
|
|
127
126
|
private secondHandshake;
|
package/dist/index.js
CHANGED
|
@@ -85,8 +85,8 @@ var base64Decode = (data) => {
|
|
|
85
85
|
// src/shared/helpers.ts
|
|
86
86
|
var throwErrorIfFound = (responseData) => {
|
|
87
87
|
const errorCode = responseData["error_code"];
|
|
88
|
-
console.debug("[Tapo] errorCode: ", errorCode);
|
|
89
88
|
if (errorCode) {
|
|
89
|
+
console.debug("[Tapo] errorCode: ", errorCode);
|
|
90
90
|
switch (errorCode) {
|
|
91
91
|
case 0:
|
|
92
92
|
return;
|
|
@@ -389,26 +389,27 @@ var _TapoApi = class _TapoApi {
|
|
|
389
389
|
this._token = response.data.result.token;
|
|
390
390
|
return this._token;
|
|
391
391
|
});
|
|
392
|
-
this.setup = (
|
|
392
|
+
this.setup = (devices, logger) => __async(this, null, function* () {
|
|
393
393
|
const promises = devices.map((device) => __async(this, null, function* () {
|
|
394
|
-
const deviceSession = yield this.handshake(device.host);
|
|
394
|
+
const deviceSession = yield this.handshake(device.host, void 0, false, logger);
|
|
395
395
|
this._devices.set(device.id, deviceSession);
|
|
396
396
|
return;
|
|
397
397
|
}));
|
|
398
|
+
let responses = [];
|
|
398
399
|
try {
|
|
399
|
-
|
|
400
|
+
responses = yield Promise.allSettled(promises);
|
|
400
401
|
} catch (err) {
|
|
401
402
|
console.error("tapo setup err: ", err);
|
|
402
403
|
}
|
|
403
|
-
return this._devices;
|
|
404
|
+
return { responses, devices: this._devices };
|
|
404
405
|
});
|
|
405
406
|
// Change state using lightstate
|
|
406
407
|
this.sendState = (config) => __async(this, null, function* () {
|
|
407
|
-
const
|
|
408
|
-
if (!
|
|
408
|
+
const devSession = this._devices.get(config.device.id);
|
|
409
|
+
if (!devSession) {
|
|
409
410
|
return Promise.resolve(false);
|
|
410
411
|
}
|
|
411
|
-
yield this.handshake(
|
|
412
|
+
yield this.handshake(devSession == null ? void 0 : devSession.ip, devSession, false);
|
|
412
413
|
let shouldWait = false;
|
|
413
414
|
if (config.fetchConfig && config.fetchConfig.shouldWait) {
|
|
414
415
|
shouldWait = true;
|
|
@@ -419,18 +420,14 @@ var _TapoApi = class _TapoApi {
|
|
|
419
420
|
params: values
|
|
420
421
|
// terminalUUID: uuidv4(),
|
|
421
422
|
});
|
|
422
|
-
const requestData =
|
|
423
|
-
const response = yield this.sessionPost(
|
|
423
|
+
const requestData = devSession.cipher.encrypt(deviceRequest);
|
|
424
|
+
const response = yield this.sessionPost(devSession.ip, "/request", requestData.encrypted, "arraybuffer", devSession.Cookie, {
|
|
424
425
|
seq: requestData.seq.toString()
|
|
425
426
|
});
|
|
426
427
|
if (response.status !== 200) {
|
|
427
428
|
throw new Error("[KLAP] Request failed");
|
|
428
429
|
}
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
response,
|
|
432
|
-
body: data
|
|
433
|
-
};
|
|
430
|
+
return true;
|
|
434
431
|
});
|
|
435
432
|
this.sendPower = (config) => {
|
|
436
433
|
this.sendState({ device: config.device, state: new LightState({ on: config.power }) });
|
|
@@ -447,112 +444,120 @@ var _TapoApi = class _TapoApi {
|
|
|
447
444
|
// Helpers
|
|
448
445
|
sessionPost(deviceIp, path, payload, responseType, cookie, params) {
|
|
449
446
|
return __async(this, null, function* () {
|
|
447
|
+
var _a;
|
|
448
|
+
const headers = {
|
|
449
|
+
Accept: "*/*",
|
|
450
|
+
"Content-Type": "application/octet-stream"
|
|
451
|
+
};
|
|
452
|
+
if (cookie) {
|
|
453
|
+
if ((_a = process == null ? void 0 : process.versions) == null ? void 0 : _a.electron) {
|
|
454
|
+
headers.BypassCookie = cookie;
|
|
455
|
+
} else {
|
|
456
|
+
headers.Cookie = cookie;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
450
459
|
return import_axios.default.post(`http://${deviceIp}/app${path}`, payload, {
|
|
451
460
|
responseType,
|
|
452
461
|
params,
|
|
453
|
-
headers
|
|
454
|
-
Accept: "*/*",
|
|
455
|
-
"Content-Type": "application/octet-stream"
|
|
456
|
-
}, cookie && {
|
|
457
|
-
Cookie: cookie
|
|
458
|
-
}),
|
|
462
|
+
headers,
|
|
459
463
|
httpAgent: new import_http.default.Agent({
|
|
460
464
|
keepAlive: false
|
|
461
465
|
})
|
|
462
466
|
});
|
|
463
467
|
});
|
|
464
468
|
}
|
|
465
|
-
needsNewHandshake() {
|
|
466
|
-
if (!
|
|
469
|
+
needsNewHandshake(devSession) {
|
|
470
|
+
if (!devSession) {
|
|
467
471
|
return true;
|
|
468
472
|
}
|
|
469
|
-
if (!
|
|
473
|
+
if (!devSession.cipher) {
|
|
470
474
|
return true;
|
|
471
475
|
}
|
|
472
|
-
if (
|
|
476
|
+
if (devSession.IsExpired) {
|
|
473
477
|
return true;
|
|
474
478
|
}
|
|
475
|
-
if (!
|
|
479
|
+
if (!devSession.Cookie) {
|
|
476
480
|
return true;
|
|
477
481
|
}
|
|
478
482
|
return false;
|
|
479
483
|
}
|
|
480
|
-
handshake(deviceIp, force = false) {
|
|
484
|
+
handshake(deviceIp, devSession, force = false, logger) {
|
|
481
485
|
return __async(this, null, function* () {
|
|
482
|
-
if (!this.needsNewHandshake() && !force) {
|
|
486
|
+
if (!this.needsNewHandshake(devSession) && !force) {
|
|
483
487
|
return;
|
|
484
488
|
}
|
|
485
|
-
const { localSeed, remoteSeed, authHash } = yield this.firstHandshake(deviceIp);
|
|
486
|
-
return yield this.secondHandshake(deviceIp, localSeed, remoteSeed, authHash);
|
|
489
|
+
const { localSeed, remoteSeed, authHash, deviceSession } = yield this.firstHandshake(deviceIp, void 0, logger);
|
|
490
|
+
return yield this.secondHandshake(deviceSession, deviceIp, localSeed, remoteSeed, authHash, logger);
|
|
487
491
|
});
|
|
488
492
|
}
|
|
489
|
-
firstHandshake(deviceIp, seed) {
|
|
493
|
+
firstHandshake(deviceIp, seed, logger) {
|
|
490
494
|
return __async(this, null, function* () {
|
|
491
495
|
var _a;
|
|
492
496
|
const localSeed = seed ? seed : import_crypto4.default.randomBytes(16);
|
|
493
497
|
const handshake1Result = yield this.sessionPost(deviceIp, "/handshake1", localSeed, "arraybuffer");
|
|
498
|
+
logger == null ? void 0 : logger.debug("handshake1Result: ", handshake1Result);
|
|
494
499
|
if (handshake1Result.status !== 200) {
|
|
495
500
|
throw new Error("Handshake1 failed");
|
|
496
501
|
}
|
|
497
502
|
if (handshake1Result.headers["content-length"] !== "48") {
|
|
498
503
|
throw new Error("Handshake1 failed due to invalid content length");
|
|
499
504
|
}
|
|
500
|
-
const cookie = (_a = handshake1Result.headers["set-cookie"]) == null ? void 0 : _a[0];
|
|
501
|
-
const data = handshake1Result.data;
|
|
505
|
+
const cookie = handshake1Result.headers["bypass-cookie"] || ((_a = handshake1Result.headers["set-cookie"]) == null ? void 0 : _a[0]);
|
|
506
|
+
const data = Buffer.from(new Uint8Array(handshake1Result.data));
|
|
502
507
|
const [cookieValue, timeout] = cookie.split(";");
|
|
503
508
|
const timeoutValue = timeout.split("=").pop();
|
|
504
|
-
|
|
509
|
+
const deviceSession = new DeviceSession(timeoutValue, deviceIp, cookieValue);
|
|
505
510
|
const remoteSeed = data.subarray(0, 16);
|
|
506
511
|
const serverHash = data.subarray(16);
|
|
507
|
-
|
|
512
|
+
logger == null ? void 0 : logger.debug("[KLAP] First handshake decoded successfully:\nRemote Seed:", remoteSeed.toString("hex"), "\nServer Hash:", serverHash.toString("hex"), "\nCookie:", cookieValue);
|
|
508
513
|
const localHash = this.hashAuth(this._config.rawEmail, this._config.rawPassword);
|
|
509
514
|
const localAuthHash = this.sha256(Buffer.concat([localSeed, remoteSeed, localHash]));
|
|
510
515
|
if (Buffer.compare(localAuthHash, serverHash) === 0) {
|
|
511
|
-
|
|
516
|
+
logger == null ? void 0 : logger.debug("[KLAP] Local auth hash matches server hash");
|
|
512
517
|
return {
|
|
513
518
|
localSeed,
|
|
514
519
|
remoteSeed,
|
|
515
|
-
authHash: localHash
|
|
520
|
+
authHash: localHash,
|
|
521
|
+
deviceSession
|
|
516
522
|
};
|
|
517
523
|
}
|
|
518
524
|
const emptyHash = this.sha256(Buffer.concat([localSeed, remoteSeed, this.hashAuth("", "")]));
|
|
519
525
|
if (Buffer.compare(emptyHash, serverHash) === 0) {
|
|
520
|
-
|
|
526
|
+
logger == null ? void 0 : logger.debug("[KLAP] [WARN] Empty auth hash matches server hash");
|
|
521
527
|
return {
|
|
522
528
|
localSeed,
|
|
523
529
|
remoteSeed,
|
|
524
|
-
authHash: emptyHash
|
|
530
|
+
authHash: emptyHash,
|
|
531
|
+
deviceSession
|
|
525
532
|
};
|
|
526
533
|
}
|
|
527
534
|
const testHash = this.sha256(Buffer.concat([localSeed, remoteSeed, this.hashAuth(_TapoApi.TP_TEST_USER, _TapoApi.TP_TEST_PASSWORD)]));
|
|
528
535
|
if (Buffer.compare(testHash, serverHash) === 0) {
|
|
529
|
-
|
|
536
|
+
logger == null ? void 0 : logger.debug("[KLAP] [WARN] Test auth hash matches server hash");
|
|
530
537
|
return {
|
|
531
538
|
localSeed,
|
|
532
539
|
remoteSeed,
|
|
533
|
-
authHash: testHash
|
|
540
|
+
authHash: testHash,
|
|
541
|
+
deviceSession
|
|
534
542
|
};
|
|
535
543
|
}
|
|
536
|
-
this.session = void 0;
|
|
537
544
|
throw new Error("Failed to verify server hash");
|
|
538
545
|
});
|
|
539
546
|
}
|
|
540
|
-
secondHandshake(deviceIp, localSeed, remoteSeed, authHash) {
|
|
547
|
+
secondHandshake(devSession, deviceIp, localSeed, remoteSeed, authHash, logger) {
|
|
541
548
|
return __async(this, null, function* () {
|
|
542
549
|
const localAuthHash = this.sha256(Buffer.concat([remoteSeed, localSeed, authHash]));
|
|
543
550
|
try {
|
|
544
|
-
const handshake2Result = yield this.sessionPost(deviceIp, "/handshake2", localAuthHash, "text",
|
|
551
|
+
const handshake2Result = yield this.sessionPost(deviceIp, "/handshake2", localAuthHash, "text", devSession.Cookie);
|
|
545
552
|
if (handshake2Result.status === 200) {
|
|
546
|
-
|
|
547
|
-
const deviceSession =
|
|
548
|
-
this.session = deviceSession;
|
|
553
|
+
logger == null ? void 0 : logger.debug("[KLAP] Second handshake successful");
|
|
554
|
+
const deviceSession = devSession.completeHandshake(deviceIp, new KlapCipher(localSeed, remoteSeed, authHash));
|
|
549
555
|
return deviceSession;
|
|
550
556
|
}
|
|
551
|
-
|
|
557
|
+
logger.warn("[KLAP] Second handshake failed", handshake2Result.data);
|
|
552
558
|
} catch (e) {
|
|
553
|
-
|
|
559
|
+
logger.error("[KLAP] Second handshake failed:", e.response.data || e.message);
|
|
554
560
|
}
|
|
555
|
-
this.session = void 0;
|
|
556
561
|
return void 0;
|
|
557
562
|
});
|
|
558
563
|
}
|
|
@@ -611,7 +616,7 @@ var discover = (config) => __async(void 0, null, function* () {
|
|
|
611
616
|
throwErrorIfFound(response.data);
|
|
612
617
|
const devices = [];
|
|
613
618
|
const localDevices = yield (0, import_local_devices.default)({ skipNameResolution: true });
|
|
614
|
-
(_a = response.data.result) == null ? void 0 : _a.deviceList.
|
|
619
|
+
(_a = response.data.result) == null ? void 0 : _a.deviceList.forEach((deviceInfo) => __async(void 0, null, function* () {
|
|
615
620
|
var _a2, _b;
|
|
616
621
|
if (!deviceInfo.ip) {
|
|
617
622
|
const findableMac = deviceInfo.deviceMac.replace(/:/g, "").toUpperCase();
|
|
@@ -622,7 +627,6 @@ var discover = (config) => __async(void 0, null, function* () {
|
|
|
622
627
|
return;
|
|
623
628
|
}
|
|
624
629
|
}
|
|
625
|
-
console.log("deviceInfo: ", deviceInfo);
|
|
626
630
|
switch (deviceInfo.deviceType) {
|
|
627
631
|
case "IOT.SMARTBULB":
|
|
628
632
|
case "SMART.TAPOBULB": {
|
package/dist/index.mjs
CHANGED
|
@@ -53,8 +53,8 @@ var base64Decode = (data) => {
|
|
|
53
53
|
// src/shared/helpers.ts
|
|
54
54
|
var throwErrorIfFound = (responseData) => {
|
|
55
55
|
const errorCode = responseData["error_code"];
|
|
56
|
-
console.debug("[Tapo] errorCode: ", errorCode);
|
|
57
56
|
if (errorCode) {
|
|
57
|
+
console.debug("[Tapo] errorCode: ", errorCode);
|
|
58
58
|
switch (errorCode) {
|
|
59
59
|
case 0:
|
|
60
60
|
return;
|
|
@@ -357,26 +357,27 @@ var _TapoApi = class _TapoApi {
|
|
|
357
357
|
this._token = response.data.result.token;
|
|
358
358
|
return this._token;
|
|
359
359
|
});
|
|
360
|
-
this.setup = (
|
|
360
|
+
this.setup = (devices, logger) => __async(this, null, function* () {
|
|
361
361
|
const promises = devices.map((device) => __async(this, null, function* () {
|
|
362
|
-
const deviceSession = yield this.handshake(device.host);
|
|
362
|
+
const deviceSession = yield this.handshake(device.host, void 0, false, logger);
|
|
363
363
|
this._devices.set(device.id, deviceSession);
|
|
364
364
|
return;
|
|
365
365
|
}));
|
|
366
|
+
let responses = [];
|
|
366
367
|
try {
|
|
367
|
-
|
|
368
|
+
responses = yield Promise.allSettled(promises);
|
|
368
369
|
} catch (err) {
|
|
369
370
|
console.error("tapo setup err: ", err);
|
|
370
371
|
}
|
|
371
|
-
return this._devices;
|
|
372
|
+
return { responses, devices: this._devices };
|
|
372
373
|
});
|
|
373
374
|
// Change state using lightstate
|
|
374
375
|
this.sendState = (config) => __async(this, null, function* () {
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
376
|
+
const devSession = this._devices.get(config.device.id);
|
|
377
|
+
if (!devSession) {
|
|
377
378
|
return Promise.resolve(false);
|
|
378
379
|
}
|
|
379
|
-
yield this.handshake(
|
|
380
|
+
yield this.handshake(devSession == null ? void 0 : devSession.ip, devSession, false);
|
|
380
381
|
let shouldWait = false;
|
|
381
382
|
if (config.fetchConfig && config.fetchConfig.shouldWait) {
|
|
382
383
|
shouldWait = true;
|
|
@@ -387,18 +388,14 @@ var _TapoApi = class _TapoApi {
|
|
|
387
388
|
params: values
|
|
388
389
|
// terminalUUID: uuidv4(),
|
|
389
390
|
});
|
|
390
|
-
const requestData =
|
|
391
|
-
const response = yield this.sessionPost(
|
|
391
|
+
const requestData = devSession.cipher.encrypt(deviceRequest);
|
|
392
|
+
const response = yield this.sessionPost(devSession.ip, "/request", requestData.encrypted, "arraybuffer", devSession.Cookie, {
|
|
392
393
|
seq: requestData.seq.toString()
|
|
393
394
|
});
|
|
394
395
|
if (response.status !== 200) {
|
|
395
396
|
throw new Error("[KLAP] Request failed");
|
|
396
397
|
}
|
|
397
|
-
|
|
398
|
-
return {
|
|
399
|
-
response,
|
|
400
|
-
body: data
|
|
401
|
-
};
|
|
398
|
+
return true;
|
|
402
399
|
});
|
|
403
400
|
this.sendPower = (config) => {
|
|
404
401
|
this.sendState({ device: config.device, state: new LightState({ on: config.power }) });
|
|
@@ -415,112 +412,120 @@ var _TapoApi = class _TapoApi {
|
|
|
415
412
|
// Helpers
|
|
416
413
|
sessionPost(deviceIp, path, payload, responseType, cookie, params) {
|
|
417
414
|
return __async(this, null, function* () {
|
|
415
|
+
var _a;
|
|
416
|
+
const headers = {
|
|
417
|
+
Accept: "*/*",
|
|
418
|
+
"Content-Type": "application/octet-stream"
|
|
419
|
+
};
|
|
420
|
+
if (cookie) {
|
|
421
|
+
if ((_a = process == null ? void 0 : process.versions) == null ? void 0 : _a.electron) {
|
|
422
|
+
headers.BypassCookie = cookie;
|
|
423
|
+
} else {
|
|
424
|
+
headers.Cookie = cookie;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
418
427
|
return axios.post(`http://${deviceIp}/app${path}`, payload, {
|
|
419
428
|
responseType,
|
|
420
429
|
params,
|
|
421
|
-
headers
|
|
422
|
-
Accept: "*/*",
|
|
423
|
-
"Content-Type": "application/octet-stream"
|
|
424
|
-
}, cookie && {
|
|
425
|
-
Cookie: cookie
|
|
426
|
-
}),
|
|
430
|
+
headers,
|
|
427
431
|
httpAgent: new http.Agent({
|
|
428
432
|
keepAlive: false
|
|
429
433
|
})
|
|
430
434
|
});
|
|
431
435
|
});
|
|
432
436
|
}
|
|
433
|
-
needsNewHandshake() {
|
|
434
|
-
if (!
|
|
437
|
+
needsNewHandshake(devSession) {
|
|
438
|
+
if (!devSession) {
|
|
435
439
|
return true;
|
|
436
440
|
}
|
|
437
|
-
if (!
|
|
441
|
+
if (!devSession.cipher) {
|
|
438
442
|
return true;
|
|
439
443
|
}
|
|
440
|
-
if (
|
|
444
|
+
if (devSession.IsExpired) {
|
|
441
445
|
return true;
|
|
442
446
|
}
|
|
443
|
-
if (!
|
|
447
|
+
if (!devSession.Cookie) {
|
|
444
448
|
return true;
|
|
445
449
|
}
|
|
446
450
|
return false;
|
|
447
451
|
}
|
|
448
|
-
handshake(deviceIp, force = false) {
|
|
452
|
+
handshake(deviceIp, devSession, force = false, logger) {
|
|
449
453
|
return __async(this, null, function* () {
|
|
450
|
-
if (!this.needsNewHandshake() && !force) {
|
|
454
|
+
if (!this.needsNewHandshake(devSession) && !force) {
|
|
451
455
|
return;
|
|
452
456
|
}
|
|
453
|
-
const { localSeed, remoteSeed, authHash } = yield this.firstHandshake(deviceIp);
|
|
454
|
-
return yield this.secondHandshake(deviceIp, localSeed, remoteSeed, authHash);
|
|
457
|
+
const { localSeed, remoteSeed, authHash, deviceSession } = yield this.firstHandshake(deviceIp, void 0, logger);
|
|
458
|
+
return yield this.secondHandshake(deviceSession, deviceIp, localSeed, remoteSeed, authHash, logger);
|
|
455
459
|
});
|
|
456
460
|
}
|
|
457
|
-
firstHandshake(deviceIp, seed) {
|
|
461
|
+
firstHandshake(deviceIp, seed, logger) {
|
|
458
462
|
return __async(this, null, function* () {
|
|
459
463
|
var _a;
|
|
460
464
|
const localSeed = seed ? seed : crypto4.randomBytes(16);
|
|
461
465
|
const handshake1Result = yield this.sessionPost(deviceIp, "/handshake1", localSeed, "arraybuffer");
|
|
466
|
+
logger == null ? void 0 : logger.debug("handshake1Result: ", handshake1Result);
|
|
462
467
|
if (handshake1Result.status !== 200) {
|
|
463
468
|
throw new Error("Handshake1 failed");
|
|
464
469
|
}
|
|
465
470
|
if (handshake1Result.headers["content-length"] !== "48") {
|
|
466
471
|
throw new Error("Handshake1 failed due to invalid content length");
|
|
467
472
|
}
|
|
468
|
-
const cookie = (_a = handshake1Result.headers["set-cookie"]) == null ? void 0 : _a[0];
|
|
469
|
-
const data = handshake1Result.data;
|
|
473
|
+
const cookie = handshake1Result.headers["bypass-cookie"] || ((_a = handshake1Result.headers["set-cookie"]) == null ? void 0 : _a[0]);
|
|
474
|
+
const data = Buffer.from(new Uint8Array(handshake1Result.data));
|
|
470
475
|
const [cookieValue, timeout] = cookie.split(";");
|
|
471
476
|
const timeoutValue = timeout.split("=").pop();
|
|
472
|
-
|
|
477
|
+
const deviceSession = new DeviceSession(timeoutValue, deviceIp, cookieValue);
|
|
473
478
|
const remoteSeed = data.subarray(0, 16);
|
|
474
479
|
const serverHash = data.subarray(16);
|
|
475
|
-
|
|
480
|
+
logger == null ? void 0 : logger.debug("[KLAP] First handshake decoded successfully:\nRemote Seed:", remoteSeed.toString("hex"), "\nServer Hash:", serverHash.toString("hex"), "\nCookie:", cookieValue);
|
|
476
481
|
const localHash = this.hashAuth(this._config.rawEmail, this._config.rawPassword);
|
|
477
482
|
const localAuthHash = this.sha256(Buffer.concat([localSeed, remoteSeed, localHash]));
|
|
478
483
|
if (Buffer.compare(localAuthHash, serverHash) === 0) {
|
|
479
|
-
|
|
484
|
+
logger == null ? void 0 : logger.debug("[KLAP] Local auth hash matches server hash");
|
|
480
485
|
return {
|
|
481
486
|
localSeed,
|
|
482
487
|
remoteSeed,
|
|
483
|
-
authHash: localHash
|
|
488
|
+
authHash: localHash,
|
|
489
|
+
deviceSession
|
|
484
490
|
};
|
|
485
491
|
}
|
|
486
492
|
const emptyHash = this.sha256(Buffer.concat([localSeed, remoteSeed, this.hashAuth("", "")]));
|
|
487
493
|
if (Buffer.compare(emptyHash, serverHash) === 0) {
|
|
488
|
-
|
|
494
|
+
logger == null ? void 0 : logger.debug("[KLAP] [WARN] Empty auth hash matches server hash");
|
|
489
495
|
return {
|
|
490
496
|
localSeed,
|
|
491
497
|
remoteSeed,
|
|
492
|
-
authHash: emptyHash
|
|
498
|
+
authHash: emptyHash,
|
|
499
|
+
deviceSession
|
|
493
500
|
};
|
|
494
501
|
}
|
|
495
502
|
const testHash = this.sha256(Buffer.concat([localSeed, remoteSeed, this.hashAuth(_TapoApi.TP_TEST_USER, _TapoApi.TP_TEST_PASSWORD)]));
|
|
496
503
|
if (Buffer.compare(testHash, serverHash) === 0) {
|
|
497
|
-
|
|
504
|
+
logger == null ? void 0 : logger.debug("[KLAP] [WARN] Test auth hash matches server hash");
|
|
498
505
|
return {
|
|
499
506
|
localSeed,
|
|
500
507
|
remoteSeed,
|
|
501
|
-
authHash: testHash
|
|
508
|
+
authHash: testHash,
|
|
509
|
+
deviceSession
|
|
502
510
|
};
|
|
503
511
|
}
|
|
504
|
-
this.session = void 0;
|
|
505
512
|
throw new Error("Failed to verify server hash");
|
|
506
513
|
});
|
|
507
514
|
}
|
|
508
|
-
secondHandshake(deviceIp, localSeed, remoteSeed, authHash) {
|
|
515
|
+
secondHandshake(devSession, deviceIp, localSeed, remoteSeed, authHash, logger) {
|
|
509
516
|
return __async(this, null, function* () {
|
|
510
517
|
const localAuthHash = this.sha256(Buffer.concat([remoteSeed, localSeed, authHash]));
|
|
511
518
|
try {
|
|
512
|
-
const handshake2Result = yield this.sessionPost(deviceIp, "/handshake2", localAuthHash, "text",
|
|
519
|
+
const handshake2Result = yield this.sessionPost(deviceIp, "/handshake2", localAuthHash, "text", devSession.Cookie);
|
|
513
520
|
if (handshake2Result.status === 200) {
|
|
514
|
-
|
|
515
|
-
const deviceSession =
|
|
516
|
-
this.session = deviceSession;
|
|
521
|
+
logger == null ? void 0 : logger.debug("[KLAP] Second handshake successful");
|
|
522
|
+
const deviceSession = devSession.completeHandshake(deviceIp, new KlapCipher(localSeed, remoteSeed, authHash));
|
|
517
523
|
return deviceSession;
|
|
518
524
|
}
|
|
519
|
-
|
|
525
|
+
logger.warn("[KLAP] Second handshake failed", handshake2Result.data);
|
|
520
526
|
} catch (e) {
|
|
521
|
-
|
|
527
|
+
logger.error("[KLAP] Second handshake failed:", e.response.data || e.message);
|
|
522
528
|
}
|
|
523
|
-
this.session = void 0;
|
|
524
529
|
return void 0;
|
|
525
530
|
});
|
|
526
531
|
}
|
|
@@ -579,7 +584,7 @@ var discover = (config) => __async(void 0, null, function* () {
|
|
|
579
584
|
throwErrorIfFound(response.data);
|
|
580
585
|
const devices = [];
|
|
581
586
|
const localDevices = yield findLocalDevices({ skipNameResolution: true });
|
|
582
|
-
(_a = response.data.result) == null ? void 0 : _a.deviceList.
|
|
587
|
+
(_a = response.data.result) == null ? void 0 : _a.deviceList.forEach((deviceInfo) => __async(void 0, null, function* () {
|
|
583
588
|
var _a2, _b;
|
|
584
589
|
if (!deviceInfo.ip) {
|
|
585
590
|
const findableMac = deviceInfo.deviceMac.replace(/:/g, "").toUpperCase();
|
|
@@ -590,7 +595,6 @@ var discover = (config) => __async(void 0, null, function* () {
|
|
|
590
595
|
return;
|
|
591
596
|
}
|
|
592
597
|
}
|
|
593
|
-
console.log("deviceInfo: ", deviceInfo);
|
|
594
598
|
switch (deviceInfo.deviceType) {
|
|
595
599
|
case "IOT.SMARTBULB":
|
|
596
600
|
case "SMART.TAPOBULB": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumiastream/tapo-cove",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "GPL",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
"lint": "tsc"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@lumiastream/fetch-cove": "^3.12.0",
|
|
15
14
|
"@lumiastream/lumia-rgb-types": "^3.11.0",
|
|
16
|
-
"@lumiastream/lumia-rgb-utils": "^3.
|
|
17
|
-
"axios": "
|
|
15
|
+
"@lumiastream/lumia-rgb-utils": "^3.13.0",
|
|
16
|
+
"axios": "*",
|
|
18
17
|
"crypto": "1.0.1",
|
|
19
18
|
"local-devices": "^4.0.0"
|
|
20
19
|
},
|
|
@@ -24,5 +23,5 @@
|
|
|
24
23
|
"tsup": "*",
|
|
25
24
|
"typescript": "*"
|
|
26
25
|
},
|
|
27
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "66456bcf21aaf0d90f63020ad705d029c78aa737"
|
|
28
27
|
}
|