1 2 3 4 5importScripts( 6 'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js' 7); 8 9self.addEventListener("install", event => { 10 console.log("installing service worker") 11}); 12 13self.addEventListener('activate', event => { 14 console.log("activating service worker") 15}); 16 17 18function buildNotification(comment, ...comments) { 19 let allComments = [comment]; 20 for (let i = 0; i < comments.length; i++) { 21 let subComments = comments[i]; 22 for (let j = 0; j < subComments.length; j++) { 23 let subComment = subComments[j]; 24 if (subComment.data.data.id == comment.data.data.id) { 25 allComments.push(comment); 26 allComments = allComments.slice(1); 27 } else { 28 allComments.push(subComment); 29 } 30 } 31 } 32 if (allComments.length == 1) { 33 return allComments[0] 34 } 35 let currentUser = ""; 36 let response_urls = []; 37 let groupedComments = []; 38 for (let i = allComments.length - 1; i >= 0; i--) { 39 let currentComment = allComments[i]; 40 if (currentUser != currentComment.data.data.user) { 41 currentUser = currentComment.data.data.user; 42 groupedComments.push({user: currentUser, comments: []}) 43 let dateCreated = new Date(currentComment.data.data.date_created); 44 } 45 groupedComments[groupedComments.length-1].comments.push(currentComment) 46 if (currentComment.data.response_url) { 47 response_urls.push(currentComment.data.response_url); 48 } 49 } 50 51 body = 'Latest comments:' 52 for (let i = groupedComments.length - 1; i >= 0; i--) { 53 let userComments = groupedComments[i].comments 54 let dateCreated = new Date(userComments[userComments.length-1].data.data.date_created); 55 body += `\n${groupedComments[i].user} at ${dateCreated.toLocaleTimeString()}`; 56 for (let j = 0; j < userComments.length; j++) { 57 body += `\n ${userComments[j].body}`; 58 } 59 } 60 61 let notificationData = { 62 body: body, 63 icon: comment.icon ? comment.icon : "https://www.clm-community.eu/media/static/images/logos/logo_160x160.png", 64 tag: comment.tag, 65 requireInteraction: true, 66 renotify: true, 67 actions: comment.actions, 68 data: { 69 url: comment.data.url, 70 comments: allComments, 71 response_urls: response_urls, 72 subject: `${allComments.length} new comments in #${comment.data.data.channel.channel_id}: ${comment.data.data.channel.display_name}` 73 } 74 } 75 return notificationData; 76} 77 78function runningInAndroidChrome() { 79 // check if we are running in android chrome 80 return navigator.userAgent.match(/(chrome)/ig) && navigator.userAgent.match(/(android)/ig) 81} 82 83function runningInChrome() { 84 // check if we are running in android chrome 85 return navigator.userAgent.match(/(chrome)/ig) 86} 87 88self.addEventListener('push', function(event) { 89 90 var payload = event.data.text(); 91 92 let data = JSON.parse(payload); 93 94 let tag = data.channel ? "channel-" + data.channel.channel_id : data.id; 95 let icon = data.icon ? data.icon : "https://www.clm-community.eu/media/static/images/logos/logo_160x160.png"; 96 97 if ((!data.unread) || (data.action == "delete")) { 98 99 event.waitUntil( 100 self.registration.getNotifications().then( 101 function(allNotifications){ 102 return self.registration.getNotifications({tag: tag}).then( 103 function(notifications){ 104 if (allNotifications.length > notifications.length) { 105 notifications.forEach(n => n.close()); 106 let notification = allNotifications.filter(n => n.tag != tag)[0] 107 notification.data.notificationData.renotify = false; 108 return self.registration.showNotification( 109 notification.data.subject, notification.data.notificationData 110 ) 111 } else if (notifications.length > 0) { 112 if (runningInAndroidChrome()) { 113 // keep the notification to avoid unwanted popups 114 let notification = notifications[0]; 115 notifications.forEach(n => n.close()); 116 notification.data.notificationData.tag = "dismiss"; 117 notification.data.notificationData.renotify = false;
118 notification.data.notificationData.silent = true; 119 return self.registration.showNotification( 120 notification.data.subject, notification.data.notificationData 121 ) 122 } else { 123 notifications.forEach(n => n.close()); 124 } 125 } else { 126 if (runningInAndroidChrome()) { 127 return self.registration.showNotification( 128 "", {tag: "dismiss", icon: icon, silent: true} 129 ) 130 } 131 } 132 } 133 ) 134 } 135 ) 136 ) 137 } else if (data.unread) { 138 // Retrieve a list of the clients of this service worker. 139 event.waitUntil( 140 self.clients.matchAll().then(function(clientList) { 141 // Check if there's at least one focused client. 142 143 var notificationData = { 144 body: data.body, 145 icon: icon, 146 tag: tag, 147 requireInteraction: true, 148 actions: [], 149 renotify: true, 150 data: { 151 url: data.url ? data.url: self.location.origin, 152 subject: data.subject, 153 data: data 154 } 155 } 156 notificationData.data.notificationData = notificationData; 157 if (data.response_url) { 158 notificationData.actions.push({ 159 action: "markAsRead", 160 title: "Mark as read" 161 }); 162 notificationData.actions.push({ 163 action: "delete", 164 title: "Delete notification" 165 }); 166 notificationData.data.response_url = data.response_url; 167 } 168 169 var focused = clientList.some(function(client) { 170 return client.focused; 171 }); 172 173 if (!focused) { 174 if ((data.action == "create") || (data.action == "update")) { 175 return self.registration.getNotifications({tag: notificationData.tag}).then( 176 (notifications) => { 177 notificationData = buildNotification( 178 notificationData, ...notifications.map(n => n.data.comments ? n.data.comments.map(c => c.data.notificationData) : [n.data.notificationData]) 179 ) 180 if ((!runningInAndroidChrome()) && (runningInChrome())) { 181 // close the notifications because the actions in desktop 182 // chrome will not appear again otherwise 183 notifications.forEach(n => n.close()); 184 } 185 return self.registration.showNotification( 186 notificationData.data.subject, notificationData 187 ).then( 188 () => self.registration.getNotifications({tag: "dismiss"}) 189 ).then( 190 notifications => notifications.forEach(n => n.close()) 191 ) 192 } 193 ) 194 } 195 } 196 }) 197 ) 198 } 199}); 200 201// Register event listener for the 'notificationclick' event. 202self.addEventListener('notificationclick', function(event) { 203 if (event.action) { 204 var body = {}; 205 var method = "PATCH"; 206 if (event.action == "markAsRead") { 207 body.unread = false; 208 } else if (event.action == "delete") { 209 method = "DELETE"; 210 } 211 if (event.notification.data.response_urls) { 212 event.waitUntil( 213 Promise.all( 214 event.notification.data.response_urls.map( 215 uri => fetch(uri, { 216 method: method, 217 headers: { 218 'Content-Type': 'application/json' 219 }, 220 body: JSON.stringify(body) 221 }) 222 ) 223 ).then((response) => { 224 event.notification.close(); 225 }).catch(error => { 226 console.error(error); 227 }) 228 ) 229 } else { 230 event.waitUntil( 231 fetch(event.notification.data.response_url, { 232 method: method, 233 headers: { 234 'Content-Type': 'application/json' 235 }, 236 body: JSON.stringify(body) 237 }).then((response) => { 238 event.notification.close(); 239 }).catch(error => { 240 console.error(error); 241 }) 242 ) 243 } 244 } else { 245 event.waitUntil( 246 // Retrieve a list of the clients of this service worker. 247 self.clients.matchAll({type: "window"}).then(function(clientList) { 248 // If there is at least one client, focus it. 249 if (clientList.length > 0) { 250 event.notification.close(); 251 clientList[0].navigate(event.notification.data.url); 252 return clientList[0].focus(); 253 } 254 255 // Otherwise, open a new page.
256 return self.clients.openWindow(event.notification.data.url); 257 }) 258 ); 259 } 260}); 261 262 263 264importScripts('/static/js/e2ee/utils.js'); 265importScripts('https://cdn.jsdelivr.net/npm/idb-keyval@6/dist/umd.js'); 266 267 268async function handleMasterKeyPost({ url, request, event, params }) { 269 270 let body = await request.json(); 271 272 let salt = crypto.getRandomValues(new Uint8Array(16)) 273 274 return generateKeyFromPassword(body.password, salt).then( 275 async (wrappingKey) => { 276 let masterKey = new MasterKey( 277 request.headers, 278 "/e2ee/rest/" 279 ) 280 return masterKey.uploadPublicKeys().then( 281 async (response) => { 282 if (response.status == 201) { 283 return masterKey.uploadPrivateKeys( 284 "/e2ee/rest/master_keysecrets/", 285 wrappingKey, 286 "POST", 287 { 288 salt: arrayBufferToBase64(salt), 289 identifier: body.identifier 290 } 291 ) 292 } else { 293 return response; 294 } 295 } 296 ).then( 297 async (response) => { 298 if (response.status == 201) { 299 let sessionKey = await generateAESKey(["wrapKey", "unwrapKey"]); 300 301 return idbKeyval.set("sessionKey", sessionKey).then( 302 async () => masterKey.uploadPrivateKeys( 303 '/e2ee/rest/sessionkeys/', 304 sessionKey, 305 "POST" 306 ) 307 ) 308 } else { 309 return response 310 } 311 } 312 ) 313 } 314 ) 315 316} 317 318 319async function handleMasterKeySecretPost({ url, request, event, params }) { 320 321 let body = await request.json(); 322 323 let salt = crypto.getRandomValues(new Uint8Array(16)) 324 325 return generateKeyFromPassword(body.password, salt).then( 326 async (wrappingKey) => { 327 let masterKey = new MasterKeyFromSession( 328 request.headers, 329 "/e2ee/rest/" 330 ); 331 return masterKey.uploadPrivateKeys( 332 "/e2ee/rest/master_keysecrets/", 333 wrappingKey, 334 "POST", 335 { 336 salt: arrayBufferToBase64(salt), 337 identifier: body.identifier 338 } 339 ) 340 } 341 ) 342} 343 344 345async function handleSessionKeyPost({ url, request, event, params }) { 346 347 var masterKey; 348 349 // get the public key and encrypt the sessionKey 350 let body = await request.json(); 351 let sessionKey = await generateAESKey(["wrapKey", "unwrapKey"]); 352 353 if (body.ignore) { 354 return fetch( 355 url, { 356 method: "POST", 357 headers: request.headers, 358 body: JSON.stringify({ignore: true}) 359 } 360 ) 361 } 362 363 return idbKeyval.set("sessionKey", sessionKey).then( 364 async function () { 365 if (body.password) { 366 masterKey = new MasterKeyFromPassword( 367 request.headers, 368 "/e2ee/rest/", 369 body.password, 370 body.uuid 371 ) 372 373 return masterKey.uploadPrivateKeys( 374 url, sessionKey, "POST" 375 ) 376 } else { 377 // publish encrypted session secret to the server 378 masterKey = new MasterKeyFromSession( 379 request.headers, 380 "/e2ee/rest/" 381 ); 382 return masterKey.wrapAESKey(sessionKey).then( 383 async (sessionSecret) => { 384 385 return fetch( 386 url, 387 { 388 method: 'POST', 389 headers: request.headers, 390 body: JSON.stringify({ 391 session_secret: arrayBufferToBase64(sessionSecret) 392 }) 393 } 394 ).then(async function (response) { 395 let randomNumber = twoRandomDigits(); 396 return fetch("/static/js/e2ee/passwords.json").then( 397 async passwords => new Response( 398 JSON.stringify({ 399 verificationNumber: await getVerificationNumber( 400 randomNumber, passwords[randomNumber], sessionSecret 401 ), 402 session_secret: arrayBufferToBase64(sessionSecret) 403 }), 404 { 405 headers: response.headers, 406 status: response.status, 407 statusText: response.statusText 408 } 409 410 ) 411 ) 412 }) 413 } 414 ) 415 } 416 } 417 ) 418} 419 420async function handleForeignSessionKeyPatch({ url, request, event, params }) { 421 422 let body = await request.json(); 423 424 // first validate the verificationNumber and then create the secret 425 426 return fetch("/static/js/e2ee/passwords.json").then( 427 async function (passwords) { 428 429 let requestValid = await verifyRequest( 430 body.verificationNumber, passwords, base64ToArrayBuffer(body.session_secret) 431 ) 432 if (!requestValid) { 433 return new Response( 434 '{"error": "Invalid verification number"}', 435 {statusCode: 400, statusText: "Bad Request"} 436 ) 437 } 438 439 var masterKey; 440 441 if (body.password) { 442 masterKey = new MasterKeyFromPassword( 443 request.headers, 444 "/e2ee/rest/", 445 body.password, 446 body.uuid 447 ); 448 } else { 449 masterKey = new MasterKeyFromSession( 450 request.headers, 451 "/e2ee/rest/" 452 ); 453 } 454 455 return masterKey.unwrapSessionKey( 456 body.session_secret 457 ).then( 458 async (sessionKey) => { 459 return masterKey.uploadPrivateKeys( 460 url, sessionKey, "PATCH", { session_secret: null } 461 ) 462 } 463 ) 464 } 465 ) 466} 467 468async function handleEncryptionPost({ url, request, event, params }) { 469 let body = await request.json(); 470 471 if (typeof (body.message) === "undefined") { 472 return new Response( 473 '{"error": "No message to encrypt."}', 474 {statusCode: 400, statusText: "Bad Request"} 475 ) 476 } 477 478 let encryptionKey; 479 if (body.encryption_key) { 480 encryptionKey = new ExistingEncryptionKey( 481 request.headers, "/e2ee/rest/", body.encryption_key 482 ); 483 484 } else { 485 encryptionKey = new EncryptionKey( 486 request.headers, "/e2ee/rest/" 487 ); 488 await encryptionKey.createKey(); 489 } 490 491 body = await encryptionKey.encryptAndSignString( 492 body.message 493 ); 494 body.encryption_key = encryptionKey.uuid; 495 496 return new Response( 497 JSON.stringify(body), 498 {statusCode: 200, statusText: "OK"} 499 ) 500} 501 502async function handleDecryptionPost({ url, request, event, params }) { 503 let body = await request.json(); 504 505 if (typeof (body.message) === "undefined") { 506 return new Response( 507 '{"error": "No message to decrypt."}', 508 { statusCode: 400, statusText: "Bad Request" } 509 ) 510 } else if (typeof (body.encryption_key) === "undefined") { 511 return new Response( 512 '{"error": "No encryption key specified."}', 513 { statusCode: 400, statusText: "Bad Request" } 514 ) 515 } 516 517 let encryptionKey = new ExistingEncryptionKey( 518 request.headers, "/e2ee/rest/", body.encryption_key 519 ); 520 521 return encryptionKey.decryptAndVerfiyString( 522 body.message, body.signature, body.signed_by 523 ).then( 524 message => new Response( 525 JSON.stringify({ message: message }), 526 {statusCode: 200, statusText: "OK"} 527 ) 528 ).catch((err) => new Response( 529 JSON.stringify(err), 530 {statusCode: 400, statusText: "Bad Request"} 531 )) 532} 533 534 535workbox.routing.registerRoute( 536 new RegExp('/e2ee/rest/master_keys/$'), 537 handleMasterKeyPost, 538 "POST" 539); 540 541 542workbox.routing.registerRoute( 543 new RegExp('/e2ee/rest/master_keysecrets/$'), 544 handleMasterKeySecretPost, 545 "POST" 546); 547 548 549workbox.routing.registerRoute( 550 new RegExp('/e2ee/rest/sessionkeys/$'), 551 handleSessionKeyPost, 552 "POST" 553); 554 555workbox.routing.registerRoute( 556 new RegExp('/e2ee/rest/sessionkeys/.+/$'), 557 handleForeignSessionKeyPatch, 558 "PATCH" 559); 560 561workbox.routing.registerRoute( 562 new RegExp('/e2ee/rest/encrypt/'), 563 handleEncryptionPost, 564 "POST" 565); 566 567workbox.routing.registerRoute( 568 new RegExp('/e2ee/rest/decrypt/'), 569 handleDecryptionPost, 570 "POST" 571); 572
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.