-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.js
5626 lines (4796 loc) · 278 KB
/
index.js
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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'use strict';
// ****************** start of EOS settings
// name and version
const packagejson = require('./package.json');
const PLUGIN_NAME = packagejson.name;
const PLATFORM_NAME = packagejson.platformname;
const PLUGIN_VERSION = packagejson.version;
// required node modules
const fs = require('fs');
const fsPromises = require('fs').promises;
const path = require('path');
const debug = require('debug')('eosstb'); // https://github.com/debug-js/debug
// good example of debug usage https://github.com/mqttjs/MQTT.js/blob/main/lib/client.js
const semver = require('semver') // https://github.com/npm/node-semver
const mqtt = require('mqtt'); // https://github.com/mqttjs
const qs = require('qs'); // https://github.com/ljharb/qs
const WebSocket = require('ws'); // https://github.com/websockets/ws for the mqtt websocket
// needed for sso logon with pkce OAuth 2.0
const {randomBytes, createHash} = require("node:crypto");
// axios-cookiejar-support v2.0.2 syntax
const { wrapper: axiosCookieJarSupport } = require('axios-cookiejar-support'); // as of axios-cookiejar-support v2.0.x, see https://github.com/3846masa/axios-cookiejar-support/blob/main/MIGRATION.md
const tough = require('tough-cookie');
const cookieJar = new tough.CookieJar();
const axios = require('axios') //.default; // https://github.com/axios/axios
axios.defaults.xsrfCookieName = undefined; // change xsrfCookieName: 'XSRF-TOKEN' to xsrfCookieName: undefined, we do not want this default,
const axiosWS = axios.create({
jar: cookieJar, //added in axios-cookiejar-support v2.0.x, see https://github.com/3846masa/axios-cookiejar-support/blob/main/MIGRATION.md
});
// remove default header in axios that causes trouble with Telenet
delete axiosWS.defaults.headers.common["Accept"];
delete axiosWS.defaults.headers.common;
axiosWS.defaults.headers.post = {}; // ensure no default post header, upsets some logon routines
// setup the cookieJar support with axiosWS
axiosCookieJarSupport(axiosWS);
// ++++++++++++++++++++++++++++++++++++++++++++
// config start
// ++++++++++++++++++++++++++++++++++++++++++++
// base url varies by country
// without any trailing /
// refer https://github.com/Sholofly/lghorizon-python/blob/features/telenet/lghorizon/const.py
const countryBaseUrlArray = {
//https://spark-prod-be.gnp.cloud.telenet.tv/be/en/config-service/conf/web/backoffice.json
'be-fr': 'https://spark-prod-be.gnp.cloud.telenet.tv', // changed 15.06.2024, be still needs 2 x language variangs: be-fr and be-nl
'be-nl': 'https://spark-prod-be.gnp.cloud.telenet.tv', // changed 15.06.2024, be still needs 2 x language variangs: be-fr and be-nl
// https://spark-prod-ch.gnp.cloud.sunrisetv.ch/ch/en/config-service/conf/web/backoffice.json
//'ch': 'https://prod.spark.sunrisetv.ch',
'ch': 'https://spark-prod-ch.gnp.cloud.sunrisetv.ch', // verified 14.01.2024
'gb': 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com', // verified 14.01.2024
'ie': 'https://spark-prod-ie.gnp.cloud.virginmediatv.ie', // verified 14.01.2024
'nl': 'https://prod.spark.ziggogo.tv', // verified 14.01.2024
//'pl': 'https://prod.spark.upctv.pl',
'pl': 'https://spark-prod-pl.gnp.cloud.upctv.pl', // verified 14.01.2024
//'sk': 'https://prod.spark.upctv.sk',
'sk': 'https://spark-prod-sk.gnp.cloud.upctv.sk', // verified 14.01.2024
};
// mqtt endpoints varies by country, unchanged after backend change on 13.10.2022
/*
const mqttUrlArray = {
'be-fr': 'wss://obomsg.prod.be.horizon.tv/mqtt',
'be-nl': 'wss://obomsg.prod.be.horizon.tv/mqtt',
'ch': 'wss://messagebroker-prod-ch.gnp.cloud.dmdsdp.com/mqtt', // from 11.02.2024
'gb': 'wss://obomsg.prod.gb.horizon.tv/mqtt',
'ie': 'wss://obomsg.prod.ie.horizon.tv/mqtt',
'nl': 'wss://obomsg.prod.nl.horizon.tv/mqtt',
'pl': 'wss://obomsg.prod.pl.horizon.tv/mqtt',
'sk': 'wss://obomsg.prod.sk.horizon.tv/mqtt'
};*/
// openid logon url used in Telenet.be Belgium for be-nl and be-fr sessions
const BE_AUTH_URL = 'https://login.prd.telenet.be/openid/login.do';
// oidc logon url used in VirginMedia for gb sessions
// still in use after logon session changes on 13.10.2022 for other countries
// the url that worked in v1.7: 'https://web-api-prod-obo.horizon.tv/oesp/v4/GB/eng/web'
// this may also work: https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/authorization
const GB_AUTH_OESP_URL = 'https://web-api-prod-obo.horizon.tv/oesp/v4/GB/eng/web';
// https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true
const GB_AUTH_URL = 'https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true';
// general constants
const NO_INPUT_ID = 99; // an input id that does not exist. Must be > 0 as a uint32 is expected. inteder
const NO_CHANNEL_ID = 'ID_UNKNOWN'; // id for a channel not in the channel list, string
const NO_CHANNEL_NAME = 'UNKNOWN'; // name for a channel not in the channel list
const MAX_INPUT_SOURCES = 95; // max input services. Default = 95. Cannot be more than 96 (100 - all other services)
const SESSION_WATCHDOG_INTERVAL_MS = 15000; // session watchdog interval in millisec. Default = 15000 (15s)
const MASTER_CHANNEL_LIST_VALID_FOR_S = 1800; // master channel list stays valid for 1800s (30min) from last refresh from July 2023. Triggers reauthentication rpocess as well
const MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S = 60; // master channel list refresh check interval, in seconds. Default = 60 (1mim) from July 2023
const SETTOPBOX_NAME_MINLEN = 3; // min len of the set-top box name
const SETTOPBOX_NAME_MAXLEN = 14; // max len of the set-top box name
// state constants. Need to add an array for any characteristic that is not an array, or the array is not contiguous
const sessionState = { DISCONNECTED: 0, LOADING: 1, LOGGING_IN: 2, AUTHENTICATING: 3, VERIFYING: 4, AUTHENTICATED: 5, CONNECTED: 6 }; // custom
const powerStateName = ["OFF", "ON"]; // custom
const recordingState = { IDLE: 0, ONGOING_NDVR: 1, ONGOING_LOCALDVR: 2 }; // custom
const statusActiveName = ["NOT_ACTIVE", "ACTIVE"]; // ccustom, haracteristic is boolean, not an array
Object.freeze(sessionState);
Object.freeze(powerStateName);
Object.freeze(recordingState);
Object.freeze(statusActiveName);
// exec spawns child process to run a bash script
var exec = require("child_process").exec;
const { waitForDebugger } = require('inspector');
const { ENGINE_METHOD_CIPHERS } = require('constants');
const { LOADIPHLPAPI } = require('dns');
const { connected } = require('process');
var PLUGIN_ENV = ''; // controls the development environment, appended to UUID to make unique device when developing
// variables for session and all devices
let mqttClient = {};
let mqttClientId = '';
let mqttUsername;
let currentSessionState;
let isShuttingDown = false; // to handle reboots cleanly
let Accessory, Characteristic, Service, Categories, UUID;
// make a randon id of the desired length
function makeId(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
// format an id to conform with the web client ids
// 32 char, lower case, formatted as follows:
// "d3e9aa58-6ddc-4c1a-b6a4-8fc1526c6f19"
// 12345678-9012-3456-7890-123456789012
// 1------8 9-12-1316-1720-21--------32
function makeFormattedId(length) {
let id = '';
let result = '';
let characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
id += characters.charAt(Math.floor(Math.random() * charactersLength));
}
// expects 32 char id length
result = result + id.substring(0, 8) + '-';
result = result + id.substring(8, 12) + '-';
result = result + id.substring(12, 16) + '-';
result = result + id.substring(16, 20) + '-';
result = result + id.substring(20, id.length);
return result;
};
// get unix timestamp in seconds
function getTimestampInSeconds() {
return Math.floor(Date.now() / 1000)
};
// transform current media state of 0,1,2,4,5 to 1,2,3,4,5 to work with Object.keys
function currentMediaStateName(currentMediaState) {
let i = (currentMediaState + 1); // get the bew index
if (i > 3) { i=i-1 }; // modify if > 3 to get 1,2,3,4,5
return Object.keys(Characteristic.CurrentMediaState)[i];
};
// clean a name so it is acceptable for HomeKit
function cleanNameForHomeKit(name) {
// HomeKit does not allow non-alphanumeric characters apart from [ .,-]
// Use only alphanumeric, space, and apostrophe characters.
// Start and end with an alphabetic or numeric character.
// Don't include emojis.
// https://developer.apple.com/design/human-interface-guidelines/homekit/overview/setup/
// [^A-Za-zÀ-ÖØ-öø-ÿ0-9 .,-] allows all accented characters
// https://stackoverflow.com/questions/6664582/regex-accent-insensitive
// HomeKit however displays all these characters, so allow them
let result = name;
// replace + with plus, for 3+ HD in CH
//result.replace('+', 'plus');
//console.log("cleanNameForHomeKit after replacing + [%s]", result);
// replace unwanted characters with whitespace
//result = result.replace(/[^0-9A-Za-zÀ-ÖØ-öø-ÿ .,-]/gi, ' ');
// for now just replace forward slash with whitespace
result = result.replace('/', ' ');
//console.log("cleanNameForHomeKit after replace [%s]", result);
// replace any double whitespace with single whitespace
//while(result.indexOf(' ')!=-1) { result.replace(' ',' '); }
//console.log("cleanNameForHomeKit after replacing double whitespace [%s]", result);
// trim to remove resultant leading and trailing whitespace
result = result.trim();
//ensure ends with a non-alphanumric character
// testing shows
// OK ending with .
// Not OK ending with ,-
// append . if not ending in a alpha-numeric character
/*
if (RegExp(/[^0-9A-Za-zÀ-ÖØ-öø-ÿ.]\z/gi).test(result)) {
console.log("cleanNameForHomeKit last char not allowed, appending .");
result = result + '.'; // append a .
}
*/
//console.log("cleanNameForHomeKit result [%s]", result);
return result;
}
// wait function
const wait=ms => new Promise(resolve => setTimeout(resolve, ms));
// wait function with promise
async function waitprom(ms) {
return new Promise((resolve) => {
setTimeout(() => { resolve(ms) }, ms )
})
}
// generate PKCE code verifier pair for OAuth 2.0
function generatePKCEPair() {
const NUM_OF_BYTES = 22; // Total of 44 characters (1 Byte = 2 char) (standard states that: 43 chars <= verifier <= 128 chars)
const HASH_ALG = "sha256";
const code_verifier = randomBytes(NUM_OF_BYTES).toString('hex');
const code_verifier_hash = createHash(HASH_ALG).update(code_verifier).digest('base64');
const code_challenge = code_verifier_hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // Clean base64 to make it URL safe
return {verifier: code_verifier, code_challenge}
}
// ++++++++++++++++++++++++++++++++++++++++++++
// config end
// ++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++
// platform setup
// ++++++++++++++++++++++++++++++++++++++++++++
module.exports = (api) => {
Accessory = api.platformAccessory;
Characteristic = api.hap.Characteristic;
Service = api.hap.Service;
Categories = api.hap.Categories;
UUID = api.hap.uuid;
const isDynamicPlatform = true;
api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, stbPlatform, isDynamicPlatform);
};
class stbPlatform {
// build the platform. Runs once on restart
// All platform-specifie code goes in this class
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.stbDevices = []; // store stbDevice in this.stbDevices
this.masterChannelList = [];
// show some useful version info
this.log.info('%s v%s, node %s, homebridge v%s', packagejson.name, packagejson.version, process.version, this.api.serverVersion)
// only load if configured and mandatory items exist. Homebridge checks for platform itself, and name is not critical
if (!this.config) { this.log.warn('%s config missing. Initialization aborted.', PLUGIN_NAME); return; }
const configWarningText = '%s config incomplete: "{configItemName}" missing. Initialization aborted.';
if (!this.config.country) { this.log.warn( configWarningText.replace('{configItemName}','country'), PLUGIN_NAME); return; }
if (!this.config.username) { this.log.warn( configWarningText.replace('{configItemName}','username'), PLUGIN_NAME); return; }
if (!this.config.password) { this.log.warn( configWarningText.replace('{configItemName}','password'), PLUGIN_NAME); return; }
// session flags
currentSessionState = sessionState.DISCONNECTED;
mqttClient.connected = false;
this.sessionWatchdogRunning = false;
this.watchdogCounter = 0;
this.mqttClientConnecting = false;
this.currentStatusFault = null;
/*
this.inputsFile = this.storagePath + '/' + 'inputs_' + this.host.split('.').join('');
this.customInputsFile = this.storagePath + '/' + 'customInputs_' + this.host.split('.').join('');
this.devInfoFile = this.storagePath + '/' + 'devInfo_' + this.host.split('.').join('');
*/
/*
* Platforms should wait until the "didFinishLaunching" event has fired before registering any new accessories.
*/
//this.api.on('didFinishLaunching', () => {
this.api.on('didFinishLaunching', async () => {
if (this.config.debugLevel > 2) { this.log.warn('API event: didFinishLaunching'); }
debug('stbPlatform:apievent :: didFinishLaunching')
// call the session watchdog once to create the session initially
setTimeout(this.sessionWatchdog.bind(this),500); // wait 500ms then call this.sessionWatchdog
// the session watchdog creates a session when none exists, and recreates one if the session ever fails due to internet failure or anything else
if ((this.config.watchdogDisabled || false) == true) {
this.log.warn('WARNING: Session watchdog disabled')
} else {
this.checkSessionInterval = setInterval(this.sessionWatchdog.bind(this),SESSION_WATCHDOG_INTERVAL_MS);
}
// check for a channel list update every MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S seconds
this.checkChannelListInterval = setInterval(() => {
// check if master channel list has expired. If it has, refresh auth token, then refresh channel list
if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: checkChannelListInterval Start'); }
if (this.masterChannelListExpiryDate <= Date.now()) {
// must check and refresh auth token before each call to refresh master channel list
this.refreshAccessToken()
.then(response => {
if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: refreshAccessToken completed OK'); }
return this.refreshMasterChannelList()
})
.then(response => {
if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: refreshMasterChannelList completed OK'); }
return true
})
.catch(error => {
if (error.code) {
this.log.warn('stbPlatform: checkChannelListInterval Error', (error.syscall || '') + ' ' + (error.code || '') + ' ' + (error.config.url || error.hostname || ''));
} else {
this.log.warn('stbPlatform: checkChannelListInterval Error', error);
}
})
if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: checkChannelListInterval end'); }
}
}, MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S * 1000 ) // need to pass ms
debug('stbPlatform:apievent :: didFinishLaunching end of code block')
//this.log('stbPlatform: end of code block');
});
/*
* "shutdown" event is fired when homebridge shuts down
*/
this.api.on('shutdown', () => {
debug('stbPlatform:apievent :: shutdown')
if (this.config.debugLevel > 2) { this.log.warn('API event: shutdown'); }
isShuttingDown = true;
this.endMqttSession()
.then(() => {
this.log('Goodbye');
}
);
debug('stbPlatform :apievent :: shutdown end of code block')
});
} // end of constructor
// test wait function with promise
async testprom() {
return new Promise((resolve, reject) => {
this.log('testprom: in the testprom async function')
resolve('testprom response: some success text in the class') // must have a resolve to return something
})
}
/**
* REQUIRED - Homebridge will call the "configureAccessory" method once for every cached accessory restored
* This is called BEFORE the didFinishLaunching event
*/
configureAccessory(accessory) {
// Note: Applies only to accesories linked to the Bridge. Does not apply to ExternalAccessories
this.log("configurePlatformAccessory %s", accessory.displayName);
this.accessories.push(accessory);
}
removeAccessory(accessory) {
// Note: Applies only to accesories linked to the Bridge. Does not apply to ExternalAccessories
this.log('removeAccessory');
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
// persist config to disc
persistConfig(deviceId, jsonData) {
// we want to save channel names and visibilityState
// storage path, constant
const filename = path.join(this.api.user.storagePath(), 'persist', 'AccessoryInfo.' + PLATFORM_NAME + '.' + deviceId + '.json');
this.log("filename", filename)
//this.log("jsonData", jsonData)
var jsonString = JSON.stringify(jsonData);
this.log("jsonString", jsonString)
// write to file
fs.writeFile(filename, jsonString, function(err) {
if (err) {
this.log('persistConfig', err);
}
});
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// START session handler (web)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// the awesome watchDog
async sessionWatchdog(callback) {
// the session watchdog creates a session when none exists, and creates stbDevices when none exist
// runs every few seconds.
// If session exists or is still being connected: Exit immediately
// If no session exists: prepares the session, then prepares the device
this.watchdogCounter++; // increment global counter by 1
let watchdogInstance = 'sessionWatchdog(' + this.watchdogCounter + ')'; // set a log prefix for this instance of the watchdog to allow differentiation in the logs
let statusOverview = '';
callback = true;
//this.log('++++ SESSION WATCHDOG STARTED ++++');
// standard debugging
let debugPrefix='\x1b[33msessionWatchdog :: ' // 33=yellow
debug(debugPrefix + 'started')
if (this.config.debugLevel > 2) { this.log.warn('%s: Started watchdog instance %s', watchdogInstance, this.watchdogCounter); }
//robustness: if session state ever gets disconnected due to session creation problems, ensure the mqtt status is always disconnected
if (currentSessionState == sessionState.DISCONNECTED) {
this.mqttClientConnecting = false;
}
if (this.config.debugLevel > 0) {
statusOverview = statusOverview + ' sessionState=' + Object.keys(sessionState)[currentSessionState]
statusOverview = statusOverview + ' mqttClient.connected=' + mqttClient.connected
statusOverview = statusOverview + ' sessionWatchdogRunning=' + this.sessionWatchdogRunning
}
// exit if shutting down
if (isShuttingDown) {
if (this.config.debugLevel > 2) { this.log.warn(watchdogInstance + ': Homebridge is shutting down, exiting %s without action', watchdogInstance); }
return;
}
// exit if a previous session is still running
if (this.sessionWatchdogRunning) {
if (this.config.debugLevel > 2) { this.log.warn(watchdogInstance + ': Previous sessionWatchdog still working, exiting %s without action', watchdogInstance); }
return;
// as we are called regularly by setInterval, check connection status and exit without action if required
} else if (currentSessionState == sessionState.CONNECTED) {
// session is connected, check mqtt state
if (mqttClient.connected) {
if (this.config.debugLevel > 2) { this.log.warn(watchdogInstance + ': Session and mqtt connected, exiting %s without action', watchdogInstance); }
return;
} else if (this.mqttClientConnecting) {
if (this.config.debugLevel > 2) { this.log.warn(watchdogInstance + ': Session connected but mqtt still connecting, exiting %s without action', watchdogInstance); }
return;
} else {
if (this.config.debugLevel > 2) { this.log.warn(watchdogInstance + ': Session connected but mqtt not connected, %s will try to reconnect mqtt now...', watchdogInstance); }
}
} else if (currentSessionState != sessionState.DISCONNECTED) {
// session is not disconnected, meaning it is between connected and disconnected, ie: a connection is in progress
if (this.config.debugLevel > 2) { this.log.warn(watchdogInstance + ': Session still connecting, exiting %s without action', watchdogInstance); }
return;
} else {
// session is not connected and is not in a state between connected and disconnected, so it is disconnected. ContinuecurrentMediaStateName(
if (this.config.debugLevel > 2) { this.log.warn(watchdogInstance + ': Session and mqtt not connected, %s will try to connect now...', watchdogInstance); }
}
// the watchdog will now attempt to reconnect the session. Flag that the watchdog is running
this.sessionWatchdogRunning = true;
if (this.config.debugLevel > 2) { this.log.warn('%s: Status: sessionWatchdogRunning=%s', watchdogInstance, this.sessionWatchdogRunning); }
// detect if running on development environment
// customStoragePath: 'C:\\Users\\jochen\\.homebridge'
if ( this.api.user.customStoragePath.includes( 'jochen' ) ) { PLUGIN_ENV = ' DEV' }
if (PLUGIN_ENV) { this.log.debug('%s: %s running in %s environment with debugLevel %s', watchdogInstance, PLUGIN_NAME, PLUGIN_ENV.trim(), (this.config || {}).debugLevel || 0); }
// if session does not exist, create the session, passing the country value
let errorTitle;
if (currentSessionState == sessionState.DISCONNECTED ) {
this.log('Session %s. Starting session connection process', Object.keys(sessionState)[currentSessionState]);
if (this.config.debugLevel > 2) { this.log.warn('%s: Attempting to create session', watchdogInstance); }
// asnyc startup sequence with chain of promises
this.log.debug('%s: ++++ step 1: calling config service', watchdogInstance)
errorTitle = 'Failed to get config';
debug(debugPrefix + 'calling getConfig')
await this.getConfig(this.config.country.toLowerCase()) // returns config, stores config in this.config
.then((session) => {
this.log.debug('%s: ++++++ step 2: config was retrieved', watchdogInstance)
this.log.debug('%s: ++++++ step 2: calling createSession with country code %s ', watchdogInstance, this.config.country.toLowerCase())
this.log('Creating session...');
errorTitle = 'Failed to create session';
debug(debugPrefix + 'calling createSession')
return this.createSession(this.config.country.toLowerCase()) // returns householdId, stores session in this.session
})
.then((sessionHouseholdId) => {
this.log.debug('%s: ++++++ step 3: session was created, connected to sessionHouseholdId %s', watchdogInstance, sessionHouseholdId)
this.log.debug('%s: ++++++ step 3: calling getPersonalizationData with sessionHouseholdId %s ', watchdogInstance, sessionHouseholdId)
this.log('Discovering platform...');
errorTitle = 'Failed to discover platform';
debug(debugPrefix + 'calling getPersonalizationData')
return this.getPersonalizationData(this.session.householdId) // returns customer object, with devices and profiles, stores object in this.customer
})
.then((objCustomer) => {
this.log.debug('%s: ++++++ step 4: personalization data was retrieved, customerId %s customerStatus %s', watchdogInstance, objCustomer.customerId, objCustomer.customerStatus)
this.log.debug('%s: ++++++ step 4: calling getEntitlements with customerId %s ', watchdogInstance, objCustomer.customerId)
debug(debugPrefix + 'calling getEntitlements')
return this.getEntitlements(this.customer.customerId) // returns customer object
})
.then((objEntitlements) => {
this.log.debug('%s: ++++++ step 5: entitlements data was retrieved, objEntitlements.token %s', watchdogInstance, objEntitlements.token)
this.log.debug('%s: ++++++ step 5: calling refreshMasterChannelList', watchdogInstance)
debug(debugPrefix + 'calling refreshMasterChannelList')
return this.refreshMasterChannelList() // returns entitlements object
})
.then((objChannels) => {
this.log.debug('%s: ++++++ step 6: masterchannelList data was retrieved, channels found: %s', watchdogInstance, objChannels.length)
// Recording needs entitlements of PVR or LOCALDVR
const pvrFeatureFound = this.entitlements.features.find(feature => (feature === 'PVR' || feature === 'LOCALDVR'));
this.log.debug('%s: ++++++ step 6: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
if (pvrFeatureFound) {
this.log.debug('%s: ++++++ step 6: calling getRecordingState with householdId %s', watchdogInstance, this.session.householdId)
this.getRecordingState(this.session.householdId) // returns true when successful
}
return true
})
.then((objRecordingStateFound) => {
this.log.debug('%s: ++++++ step 7: recording state data was retrieved, objRecordingStateFound: %s', watchdogInstance, objRecordingStateFound)
// Recording needs entitlements of PVR or LOCALDVR
const pvrFeatureFound = this.entitlements.features.find(feature => (feature === 'PVR' || feature === 'LOCALDVR'));
this.log.debug('%s: ++++++ step 7: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
if (pvrFeatureFound) {
this.log.debug('%s: ++++++ step 7: calling getRecordingBookings with householdId %s', watchdogInstance, this.session.householdId)
this.getRecordingBookings(this.session.householdId) // returns true when successful
}
return true
})
.then((objRecordingBookingsFound) => {
this.log.debug('%s: ++++++ step 8: recording bookings data was retrieved, objRecordingBookingsFound: %s', watchdogInstance, objRecordingBookingsFound)
this.log.debug('%s: ++++++ step 8: calling discoverDevices', watchdogInstance)
errorTitle = 'Failed to discover devices';
debug(debugPrefix + 'calling discoverDevices')
return this.discoverDevices() // returns stbDevices object
})
.then((objStbDevices) => {
this.log('Discovery completed');
this.log.debug('%s: ++++++ step 9: devices found:', watchdogInstance, this.devices.length)
this.log.debug('%s: ++++++ step 9: calling getMqttToken', watchdogInstance)
errorTitle = 'Failed to start mqtt session';
debug(debugPrefix + 'calling getMqttToken')
return this.getMqttToken(this.session.username, this.session.accessToken, this.session.householdId);
})
.then((mqttToken) => {
this.log.debug('%s: ++++++ step 10: getMqttToken token was retrieved, token %s', watchdogInstance, mqttToken)
this.log.debug('%s: ++++++ step 10: start mqtt client', watchdogInstance)
debug(debugPrefix + 'calling statMqttClient')
return this.statMqttClient(this, this.session.householdId, mqttToken); // returns true
})
.catch(errorReason => {
// log any errors and set the currentSessionState
this.log.warn(errorTitle + ' - %s', errorReason);
currentSessionState = sessionState.DISCONNECTED;
this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
return true
});
debug(debugPrefix + 'end of promise chain')
this.log.debug('%s: ++++++ End of promise chain', watchdogInstance)
//this.log.debug('%s: ++++ create session promise chain completed', watchdogInstance)
}
if (this.config.debugLevel > 2) { this.log.warn('%s: Exiting sessionWatchdog', watchdogInstance,); }
debug(debugPrefix + 'exiting sessionWatchdog')
//this.log('Exiting sessionWatchdog')
this.sessionWatchdogRunning = false;
return true
}
// discover all devices
async discoverDevices() {
return new Promise((resolve, reject) => {
this.log('Discovering devices...');
// show feedback for devices found
if (!this.devices || !this.devices[0].settings) {
this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
this.sessionWatchdogRunning = false;
//this.log('Failed to find any devices. The backend systems may be down, or you have no supported devices on your customer account')
reject('No devices found. The backend systems may be down, or you have no supported devices on your customer account')
} else {
// at least one device found
var logText = "Found %s device";
if (this.devices.length > 1) { logText = logText + "s"; }
this.log(logText, this.devices.length);
// user config tip showing all found devices
// display only when no config.devices not found
this.log.debug('Showing config tip...');
let tipText = '', deviceFoundInConfig = true;
for (let i = 0; i < this.devices.length; i++) {
if (!tipText == '') { tipText = tipText + ',\n'; }
tipText = tipText + ' {\n';
tipText = tipText + ' "deviceId": "' + this.devices[i].deviceId + '",\n';
tipText = tipText + ' "name": "' + this.devices[i].settings.deviceFriendlyName + '"\n';
tipText = tipText + ' }';
if (this.config.devices) {
let configDeviceIndex = this.config.devices.findIndex(devConfig => devConfig.deviceId == this.devices[i].deviceId);
if (configDeviceIndex == -1) {
this.log("Device not found in config: %s", this.devices[i].deviceId);
deviceFoundInConfig = false;
}
} else {
deviceFoundInConfig = false;
}
}
if (!deviceFoundInConfig) {
this.log('Config tip: Add these lines to your Homebridge ' + PLATFORM_NAME + ' config if you wish to customise your device config: \n"devices": [\n' + tipText + '\n]');
}
// setup/restore each device in turn as an accessory, as we can only setup the accessory after the session is created and the physicalDevices are retrieved
this.log.debug("Finding devices in cache...");
for (let i = 0; i < this.devices.length; i++) {
// setup each device (runs once per device)
const deviceName = this.devices[i].settings.deviceFriendlyName;
this.log("Device %s: %s %s", i+1, deviceName, this.devices[i].deviceId);
// generate a constant uuid that will never change over the life of the accessory
const uuid = this.api.hap.uuid.generate(this.devices[i].deviceId + PLUGIN_ENV);
// check if the accessory already exists, create if it does not
// a stbDevice contains various data: HomeKit accessory, EOS platform, EOS device, EOS profile
let foundStbDevice = this.stbDevices.find(stbDevice => (stbDevice.accessory || {}).UUID === uuid)
if (!foundStbDevice) {
this.log("Device %s: Not found in cache, creating new accessory for %s", i+1, this.devices[i].deviceId);
// create the accessory
// constructor(log, config, api, device, customer, entitlements, platform) {
this.log("Setting up device %s of %s: %s", i+1, this.devices.length, deviceName);
//let newStbDevice = new stbDevice(this.log, this.config, this.api, this.devices[i], this.customer, this.entitlements, this);
// simplified the call by removing customer and entitlements as they are part of platform anyway
let newStbDevice = new stbDevice(this.log, this.config, this.api, this.devices[i], this);
this.stbDevices.push(newStbDevice);
} else {
this.log("Device found in cache: [%s] %s", foundStbDevice.name, foundStbDevice.deviceId);
}
};
resolve(this.stbDevices); // resolve the promise with the stbDevices object
}
//this.log.debug('discoverDevices: end of code block')
})
}
// get a new access token
async refreshAccessToken() {
return new Promise((resolve, reject) => {
// exit immediately if access token has not expired
if (this.session.accessTokenExpiry > Date.now()) {
if (this.config.debugLevel >= 1) { this.log.warn('refreshAccessToken: Access token has not expired yet. Next refresh will occur after %s', this.session.accessTokenExpiry.toLocaleString()); }
resolve(true);
return
}
if (this.config.debugLevel >= 1) { this.log.warn('refreshAccessToken: Access token has expired at %s. Requesting refresh', this.session.accessTokenExpiry.toLocaleString()); }
// needed to suppress the XSRF-TOKEN which upsets the auth refresh
axiosWS.defaults.xsrfCookieName = undefined; // change xsrfCookieName: 'XSRF-TOKEN' to xsrfCookieName: undefined, we do not want this default,
const axiosConfig = {
method: 'POST',
// https://prod.spark.sunrisetv.ch/auth-service/v1/authorization/refresh
//url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/authorization/refresh',
url: this.configsvc.authorizationService.URL + '/v1/authorization/refresh',
headers: {
"accept": "*/*", // mandatory
"content-type": "application/json; charset=UTF-8", // mandatory
'x-oesp-username': this.session.username,
"x-tracking-id": this.customer.hashedCustomerId, // hashed customer id
},
jar: cookieJar,
data: {
refreshToken: this.session.refreshToken,
username: this.config.username
}
};
if (this.config.debugLevel >=1) { this.log.warn('refreshAccessToken: Post auth refresh request to',axiosConfig.url); }
axiosWS(axiosConfig)
.then(response => {
if (this.config.debugLevel >= 2) {
this.log('refreshAccessToken: auth refresh response:',response.status, response.statusText);
this.log('refreshAccessToken: response data (saved to this.session):');
this.log(response.data);
//this.log(response.headers);
}
this.session = response.data;
// add an expiry date for the access token: 2 min (120000ms) after created date
this.session.accessTokenExpiry = new Date(new Date().getTime() + 2*60000);
// check if householdId exists, if so, we have authenticated ok
if (this.session.householdId) { currentSessionState = sessionState.AUTHENTICATED; }
this.log.debug('Session username:', this.session.username);
this.log.debug('Session householdId:', this.session.householdId);
this.log.debug('Session accessToken:', this.session.accessToken);
this.log.debug('Session accessTokenExpiry:', this.session.accessTokenExpiry);
this.log.debug('Session refreshToken:', this.session.refreshToken);
this.log.debug('Session refreshTokenExpiry:', this.session.refreshTokenExpiry);
// Robustness: Observed that new APLSTB Apollo box on NL did not always return username during session logon, so store username from settings if missing
if (this.session.username == '') {
this.log.debug('Session username empty, setting to %s', this.config.username);
this.session.username = this.config.username;
} else {
this.log.debug('Session username exists: %s', this.session.username);
}
currentSessionState = sessionState.CONNECTED;
this.currentStatusFault = Characteristic.StatusFault.NO_FAULT;
resolve(this.session.householdId) // resolve the promise with the householdId
})
.catch(error => {
this.log.debug('refreshAccessToken: error:', error);
reject(error); // reject the promise and return the error
});
})
}
// select the right session to create
async createSession(country) {
return new Promise((resolve, reject) => {
this.currentStatusFault = Characteristic.StatusFault.NO_FAULT;
//switch using authmethod with backup of country
switch(this.config.authmethod || this.config.country) {
case 'D': // OAuth 2.0 with PKCE
this.getSessionOAuth2Pkce()
.then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
.catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
break;
case 'be-nl': case 'be-fr': case 'B':
this.getSessionBE()
.then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
.catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
break;
case 'gb': case 'C':
this.getSessionGB()
.then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
.catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
break;
default: // ch, nl, ie, at, method A
this.getSession()
.then((getSessionResponse) => { resolve(getSessionResponse); }) // resolve with the getSessionResponse for the promise
.catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
}
})
}
// get session for OAuth 2.0 PKCE (special logon sequence)
getSessionOAuth2Pkce() {
return new Promise((resolve, reject) => {
this.log('Creating %s OAuth 2.0 PKCE session...',PLATFORM_NAME);
this.log.warn('++++ PLEASE NOTE: This is current test code with lots of debugging. Do not expect it to work yet. ++++');
currentSessionState = sessionState.LOADING;
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// axios interceptors to log request and response for debugging
// works on all following requests in this sub
/*
axiosWS.interceptors.request.use(req => {
this.log.warn('+++INTERCEPTED BEFORE HTTP REQUEST COOKIEJAR:\n', cookieJar.getCookies(req.url));
this.log.warn('+++INTERCEPTOR HTTP REQUEST:',
'\nMethod:', req.method, '\nURL:', req.url,
'\nBaseURL:', req.baseURL, '\nHeaders:', req.headers,
'\nParams:', req.params, '\nData:', req.data
);
this.log.warn(req);
return req; // must return request
});
axiosWS.interceptors.response.use(res => {
this.log.warn('+++INTERCEPTED HTTP RESPONSE:', res.status, res.statusText,
'\nHeaders:', res.headers,
'\nUrl:', res.url,
//'\nData:', res.data,
'\nLast Request:', res.request
);
//this.log.warn(res);
this.log('+++INTERCEPTED AFTER HTTP RESPONSE COOKIEJAR:');
if (cookieJar) { this.log(cookieJar); }// watch out for empty cookieJar
return res; // must return response
});
*/
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// good description of PKCE
// https://www.authlete.com/developers/pkce/
// creake a PKCE code pair and save it
this.pkcePair = generatePKCEPair();
//this.log('PKCE pair:', pkcePair);
// Step 1: # get authentication details
// Recorded sequence step 1: https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true
// const GB_AUTH_OESP_URL = 'https://web-api-prod-obo.horizon.tv/oesp/v4/GB/eng/web';
// https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/auth-service/v1/sso/authorization?code_challenge=aHsoE2kJlwA4qGOcx1OCH7i__1bBdV1l6yLOKUvW24U&language=en
let apiAuthorizationUrl = this.configsvc.authorizationService.URL + '/v1/sso/authorization?'
+ 'code_challenge=' + this.pkcePair.code_challenge
+ '&language=en';
this.log('Step 1 of 7: get authentication details');
if (this.config.debugLevel > 1) { this.log.warn('Step 1 of 7: get authentication details from',apiAuthorizationUrl); }
axiosWS.get(apiAuthorizationUrl)
.then(response => {
this.log('Step 1 of 7: response:',response.status, response.statusText);
this.log('Step 1 of 7: response.data',response.data);
// get the data we need for further steps
let auth = response.data;
let authState = auth.state;
let authAuthorizationUri = auth.authorizationUri;
let authValidtyToken = auth.validityToken;
this.log('Step 1 of 7: results: authState',authState);
this.log('Step 1 of 7: results: authAuthorizationUri',authAuthorizationUri);
this.log('Step 1 of 7: results: authValidtyToken',authValidtyToken);
// Step 2: # follow authorizationUri to get AUTH cookie (ULM-JSESSIONID)
this.log('Step 2 of 7: get AUTH cookie');
this.log.debug('Step 2 of 7: get AUTH cookie ULM-JSESSIONID from',authAuthorizationUri);
axiosWS.get(authAuthorizationUri, {
jar: cookieJar,
// unsure what minimum headers will here
headers: {
Accept: 'application/json, text/plain, */*'
//Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
}, })
.then(response => {
this.log('Step 2 of 7: response:',response.status, response.statusText);
this.log.warn('Step 2 of 7 response.data',response.data); // an html logon page
// Step 3: # login
this.log('Step 3 of 7: logging in with username %s', this.config.username);
currentSessionState = sessionState.LOGGING_IN;
// we want to POST to
// 'https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true';
// see https://auth0.com/intro-to-iam/what-is-openid-connect-oidc
const GB_AUTH_URL = 'https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true';
this.log.debug('Step 3 of 7: POST request will contain this data: {"username":"' + this.config.username + '","credential":"' + this.config.password + '"}');
axiosWS(GB_AUTH_URL,{
//axiosWS('https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true',{
jar: cookieJar,
// However, since v2.0, axios-cookie-jar will always ignore invalid cookies. See https://github.com/3846masa/axios-cookiejar-support/blob/main/MIGRATION.md
data: '{"username":"' + this.config.username + '","credential":"' + this.config.password + '"}',
method: "POST",
// minimum headers are "accept": "*/*", "content-type": "application/json; charset=UTF-8",
headers: {
"accept": "*/*", // mandatory
"content-type": "application/json; charset=UTF-8", // mandatory
},
maxRedirects: 0, // do not follow redirects
validateStatus: function (status) {
return ((status >= 200 && status < 300) || status == 302) ; // allow 302 redirect as OK. GB returns 200
},
})
.then(response => {
this.log('Step 3 of 7: response:',response.status, response.statusText);
this.log.warn('Step 3 of 7: response.headers:',response.headers);
// responds with a userId, this will need to be used somewhere...
this.log.warn('Step 3 of 7: response.data:',response.data); // { userId: 28786528, runtimeId: 79339515 }
var url = response.headers['x-redirect-location'] // must be lowercase
if (!url) { // robustness: fail if url missing
this.log.warn('getSessionGB: Step 3: x-redirect-location url empty!');
currentSessionState = sessionState.DISCONNECTED;
this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
return false;
}
//location is h??=... if success
//location is https?? if not authorised
//location is https:... error=session_expired if session has expired
if (url.indexOf('authentication_error=true') > 0 ) { // >0 if found
//this.log.warn('Step 3 of 7: Unable to login: wrong credentials');
reject('Step 3 of 7: Unable to login: wrong credentials'); // reject the promise and return the error
} else if (url.indexOf('error=session_expired') > 0 ) { // >0 if found
//this.log.warn('Step 3 of 7: Unable to login: session expired');
cookieJar.removeAllCookies(); // remove all the locally cached cookies
reject('Step 3 of 7: Unable to login: session expired'); // reject the promise and return the error
} else {
this.log.debug('Step 3 of 7: login successful');
// Step 4: # follow redirect url
this.log('Step 4 of 7: follow redirect url');
axiosWS.get(url,{
jar: cookieJar,
maxRedirects: 0, // do not follow redirects
validateStatus: function (status) {
return ((status >= 200 && status < 300) || status == 302) ; // allow 302 redirect as OK
},
})
.then(response => {
this.log('Step 4 of 7: response:',response.status, response.statusText);
this.log.warn('Step 4 of 7: response.headers.location:',response.headers.location); // is https://www.telenet.be/nl/login_success_code=... if success
this.log.warn('Step 4 of 7: response.data:',response.data);
url = response.headers.location;
if (!url) { // robustness: fail if url missing
this.log.warn('getSessionGB: Step 4 of 7 location url empty!');
currentSessionState = sessionState.DISCONNECTED;
this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
return false;
}
// look for login_success?code=
if (url.indexOf('login_success?code=') < 0 ) { // <0 if not found
//this.log.warn('Step 4 of 7: Unable to login: wrong credentials');
reject('Step 4 of 7: Unable to login: wrong credentials'); // reject the promise and return the error
} else if (url.indexOf('error=session_expired') > 0 ) {
//this.log.warn('Step 4 of 7: Unable to login: session expired');
cookieJar.removeAllCookies(); // remove all the locally cached cookies
reject('Step 4 of 7: Unable to login: session expired'); // reject the promise and return the error
} else {
// Step 5: # obtain authorizationCode
this.log('Step 5 of 7: extract authorizationCode');
/*
url = response.headers.location;
if (!url) { // robustness: fail if url missing
this.log.warn('getSessionGB: Step 5: location url empty!');
currentSessionState = sessionState.DISCONNECTED;
this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
return false;
}
*/
var codeMatches = url.match(/code=(?:[^&]+)/g)[0].split('=');
var authorizationCode = codeMatches[1];
if (codeMatches.length !== 2 ) { // length must be 2 if code found
this.log.warn('Step 5 of 7: Unable to extract authorizationCode');
} else {
this.log('Step 5 of 7: authorizationCode OK');
this.log.debug('Step 5 of 7: authorizationCode:',authorizationCode);
// Step 6: # authorize again
this.log('Step 6 of 7: post auth data with valid code');