node-red-contrib-hik-media-buffer 1.1.108 → 1.1.109
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/hik-media-buffer.js +18 -46
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -97,7 +97,7 @@ module.exports = function(RED) {
|
|
|
97
97
|
const referenceTime = new Date();
|
|
98
98
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
99
99
|
|
|
100
|
-
// Costruiamo le date giornaliere
|
|
100
|
+
// Costruiamo le date giornaliere identiche a quelle viste su Chrome
|
|
101
101
|
const pad = (num) => String(num).padStart(2, '0');
|
|
102
102
|
const year = referenceTime.getFullYear();
|
|
103
103
|
const month = pad(referenceTime.getMonth() + 1);
|
|
@@ -106,7 +106,7 @@ module.exports = function(RED) {
|
|
|
106
106
|
const startTime = `${year}-${month}-${day}T00:00:00 01:00`;
|
|
107
107
|
const endTime = `${year}-${month}-${day}T23:59:59 01:00`;
|
|
108
108
|
|
|
109
|
-
node.status({fill:"yellow", shape:"dot", text:`
|
|
109
|
+
node.status({fill:"yellow", shape:"dot", text:`Invio query protetta...`});
|
|
110
110
|
|
|
111
111
|
let output = {
|
|
112
112
|
tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
|
|
@@ -117,38 +117,13 @@ module.exports = function(RED) {
|
|
|
117
117
|
let fileDaCancellare = [];
|
|
118
118
|
|
|
119
119
|
try {
|
|
120
|
-
// ---
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/Security/sessionLogin/capabilities?format=json`,
|
|
125
|
-
timeout: 5000
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Estraiamo il cookie "WebSession" dagli header di risposta
|
|
129
|
-
const responseHeaders = loginRes.headers['set-cookie'] || [];
|
|
130
|
-
let webSessionValue = '';
|
|
131
|
-
let cookieKeyName = 'WebSession';
|
|
132
|
-
|
|
133
|
-
for (let cookie of responseHeaders) {
|
|
134
|
-
if (cookie.includes('WebSession')) {
|
|
135
|
-
const matchCookie = cookie.match(/(WebSession_[a-f0-9]+|WebSession)=([^;]+)/i);
|
|
136
|
-
if (matchCookie) {
|
|
137
|
-
cookieKeyName = matchCookie[1];
|
|
138
|
-
webSessionValue = matchCookie[2];
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Se l'NVR non ha sputato il cookie, ne generiamo uno fittizio in MD5 basandoci sul timestamp
|
|
144
|
-
// perché in molti firmware basta che l'header esista ed abbia lo stesso valore del cookie!
|
|
145
|
-
if (!webSessionValue) {
|
|
146
|
-
webSessionValue = require('crypto').createHash('md5').update(String(Date.now())).digest('hex');
|
|
147
|
-
}
|
|
120
|
+
// --- GENERAZIONE HASH DI SESSIONE LOCALE ---
|
|
121
|
+
// Generiamo un MD5 casuale per bypassare il controllo di uguaglianza tra Cookie e Sessiontag
|
|
122
|
+
const fakeSessionToken = require('crypto').createHash('md5').update(String(Math.random() + Date.now())).digest('hex');
|
|
123
|
+
const cookieKeyName = `WebSession_${require('crypto').createHash('md5').update(node.host).digest('hex').substring(0, 10)}`;
|
|
148
124
|
|
|
149
|
-
node.warn(`[DEBUG SESSION]
|
|
125
|
+
node.warn(`[DEBUG SESSION] Simulazione Header -> ${cookieKeyName}=${fakeSessionToken}`);
|
|
150
126
|
|
|
151
|
-
// --- STEP 2: COSTRUZIONE QUERY DIRETTA CON DATI DI CHROME ---
|
|
152
127
|
const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
153
128
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
154
129
|
return v.toString(16).toUpperCase();
|
|
@@ -170,20 +145,17 @@ module.exports = function(RED) {
|
|
|
170
145
|
const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
|
|
171
146
|
fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
|
|
172
147
|
|
|
173
|
-
// --- STEP 3: LANCE DIRETTO VIA CURL CON I TRE ASSI NELLA MANICA ---
|
|
174
|
-
// 1. Content-Type form-urlencoded (anche se mandiamo un JSON, come fa Chrome!)
|
|
175
|
-
// 2. Cookie WebSession inserito a mano
|
|
176
|
-
// 3. sessiontag identico al valore del cookie
|
|
177
148
|
const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
|
|
178
149
|
|
|
150
|
+
// Compiliamo il comando cURL inserendo gli header emersi dall'analisi di Chrome
|
|
151
|
+
// Usiamo sia il content-type form-urlencoded sia la coppia speculare cookie/sessiontag
|
|
179
152
|
const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" ` +
|
|
180
153
|
`-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" ` +
|
|
181
154
|
`-H "X-Requested-With: XMLHttpRequest" ` +
|
|
182
|
-
`-H "sessiontag: ${
|
|
183
|
-
`-b "${cookieKeyName}=${
|
|
155
|
+
`-H "sessiontag: ${fakeSessionToken}" ` +
|
|
156
|
+
`-b "${cookieKeyName}=${fakeSessionToken}; updatePlugin=false" ` +
|
|
184
157
|
`-d "@${tempJsonPath}" "${targetUrl}"`;
|
|
185
158
|
|
|
186
|
-
node.warn(`[DEBUG JSON] Invio query protetta con token di sessione...`);
|
|
187
159
|
let jsonResponseRaw = await new Promise((resolve) => { exec(curlCommand, (err, stdout) => resolve(err ? null : stdout)); });
|
|
188
160
|
|
|
189
161
|
try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
|
|
@@ -198,19 +170,19 @@ module.exports = function(RED) {
|
|
|
198
170
|
const matches = searchResult?.EventSearchResult?.matchList || [];
|
|
199
171
|
|
|
200
172
|
if (matches.length === 0) {
|
|
201
|
-
node.warn(`[DEBUG JSON] Zero match trovati
|
|
173
|
+
node.warn(`[DEBUG JSON] Zero match trovati nell'archivio protetto.`);
|
|
202
174
|
updateNodeStatus();
|
|
203
175
|
return;
|
|
204
176
|
}
|
|
205
177
|
|
|
206
|
-
// Se
|
|
178
|
+
// Se superiamo il blocco, estraiamo i file storici
|
|
207
179
|
const matchItem = matches[0];
|
|
208
180
|
const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
|
|
209
181
|
|
|
210
182
|
if (playbackUri) {
|
|
211
|
-
node.warn(`[DEBUG JSON]
|
|
183
|
+
node.warn(`[DEBUG JSON] File individuato: ${playbackUri}`);
|
|
212
184
|
|
|
213
|
-
// --- DOWNLOAD FOTO
|
|
185
|
+
// --- DOWNLOAD FOTO REGISTRATA ---
|
|
214
186
|
let pictureUrlPath = null;
|
|
215
187
|
const urlParamsMatch = playbackUri.match(/\?(.*)/);
|
|
216
188
|
if (urlParamsMatch) {
|
|
@@ -220,7 +192,7 @@ module.exports = function(RED) {
|
|
|
220
192
|
|
|
221
193
|
if (pictureUrlPath) {
|
|
222
194
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
223
|
-
const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" -b "${cookieKeyName}=${
|
|
195
|
+
const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" -b "${cookieKeyName}=${fakeSessionToken}" -H "sessiontag: ${fakeSessionToken}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
|
|
224
196
|
await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
|
|
225
197
|
if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
|
|
226
198
|
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
@@ -228,7 +200,7 @@ module.exports = function(RED) {
|
|
|
228
200
|
}
|
|
229
201
|
}
|
|
230
202
|
|
|
231
|
-
// --- DOWNLOAD VIDEO
|
|
203
|
+
// --- DOWNLOAD VIDEO REGISTRATO VIA FFMPEG ---
|
|
232
204
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
233
205
|
const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
|
|
234
206
|
const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
|
|
@@ -251,7 +223,7 @@ module.exports = function(RED) {
|
|
|
251
223
|
}
|
|
252
224
|
|
|
253
225
|
} catch (e) {
|
|
254
|
-
node.error(`Errore
|
|
226
|
+
node.error(`Errore query speculare: ${e.message}`);
|
|
255
227
|
}
|
|
256
228
|
updateNodeStatus();
|
|
257
229
|
}
|