Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Nov 15, 2024
1 parent a2a0d37 commit 89329d0
Show file tree
Hide file tree
Showing 16 changed files with 108 additions and 67 deletions.
2 changes: 1 addition & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ module.exports = (grunt) => {
;(function defineApiEnvars () {
const API_PORT = Number.parseInt(grunt.option('port') ?? process.env.API_PORT ?? '8000', 10)

if (Number.isNaN(API_PORT) || API_PORT < 8000 || API_PORT > 65535) {
if (Number.isNaN(API_PORT) || API_PORT < 1024 || API_PORT > 65535) {
throw new RangeError(`Invalid API_PORT value: ${API_PORT}.`)
}
process.env.API_PORT = String(API_PORT)
Expand Down
2 changes: 2 additions & 0 deletions backend/pubsub.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,10 @@ const defaultSocketEventHandlers = {

for (const channelID of socket.subscriptions) {
// Remove this socket from the channel subscribers.
console.error('@@@@CHID', channelID)
server.subscribersByChannelID[channelID].delete(socket)
}
console.error('@@@@CLOSED PUBSUB', [...socket.subscriptions])
socket.subscriptions.clear()
},

Expand Down
42 changes: 28 additions & 14 deletions backend/push.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { aes128gcm } from '@exact-realty/rfc8188/encodings'
import encrypt from '@exact-realty/rfc8188/encrypt'
import { aes128gcm } from '@apeleghq/rfc8188/encodings'
import encrypt from '@apeleghq/rfc8188/encrypt'
// $FlowFixMe[missing-export]
import { webcrypto as crypto } from 'crypto' // Needed for Node 18 and under
import rfc8291Ikm from './rfc8291Ikm.js'
Expand Down Expand Up @@ -118,6 +118,7 @@ export const pushServerActionhandlers: any = {
[PUSH_SERVER_ACTION_TYPE.SEND_PUBLIC_KEY] () {
const socket = this
socket.send(createMessage(REQUEST_TYPE.PUSH_ACTION, { type: PUSH_SERVER_ACTION_TYPE.SEND_PUBLIC_KEY, data: getVapidPublicKey() }))
console.error('@@@@SENT PK')
},
async [PUSH_SERVER_ACTION_TYPE.STORE_SUBSCRIPTION] (payload) {
const socket = this
Expand Down Expand Up @@ -156,6 +157,7 @@ export const pushServerActionhandlers: any = {
socket.subscriptions?.forEach(channelID => {
server.pushSubscriptions[subscriptionId].subscriptions.add(channelID)
})
console.error('@@@@ADD PUSH SUBSCRIPTION')
},
[PUSH_SERVER_ACTION_TYPE.DELETE_SUBSCRIPTION] (payload) {
const socket = this
Expand All @@ -173,12 +175,7 @@ const encryptPayload = async (subcription: Object, data: any) => {
const readableStream = new Response(data).body
const [asPublic, IKM] = await subcription.encryptionKeys

return encrypt(aes128gcm, readableStream, 32768, asPublic, IKM)
}

export const postEvent = async (subscription: Object, event: any): Promise<void> => {
const authorization = await vapidAuthorization(subscription.endpoint)
const body = await encryptPayload(subscription, JSON.stringify(event)).then(async (bodyStream) => {
return encrypt(aes128gcm, readableStream, 32768, asPublic, IKM).then(async (bodyStream) => {
const chunks = []
const reader = bodyStream.getReader()
for (;;) {
Expand All @@ -188,20 +185,37 @@ export const postEvent = async (subscription: Object, event: any): Promise<void>
}
return Buffer.concat(chunks)
})
}

export const postEvent = async (subscription: Object, event: any): Promise<void> => {
const authorization = await vapidAuthorization(subscription.endpoint)
const body = event
? await encryptPayload(subscription, JSON.stringify(event))
: undefined

/* if (body) {
body[body.length - 2] = 0
} */

const req = await fetch(subscription.endpoint, {
method: 'POST',
headers: [
['authorization', authorization],
['content-encoding', 'aes128gcm'],
[
'content-type',
'application/octet-stream'
],
// ["push-receipt", ""],
...(body
? [['content-encoding', 'aes128gcm'],
[
'content-type',
'application/octet-stream'
]
]
: []),
// ['push-receipt', ''],
['ttl', '60']
],
body
}).then(async (req) => {
console.error('@@@@postEvent', body, subscription.endpoint, req.status, await req.text())
return req
})

if (!req.ok) {
Expand Down
7 changes: 6 additions & 1 deletion backend/rfc8291Ikm.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export default async (uaPublic: Uint8Array, salt: SharedArrayBuffer): Promise<[A
const info = new Uint8Array(infoString.byteLength + uaPublic.byteLength + asPublic.byteLength)
info.set(infoString, 0)
info.set(uaPublic, infoString.byteLength)
info.set(asPublic, infoString.byteLength + uaPublic.byteLength)
info.set(
new Uint8Array(asPublic),
infoString.byteLength + uaPublic.byteLength
)

const IKM = await crypto.subtle.deriveBits(
{
Expand All @@ -75,6 +78,8 @@ export default async (uaPublic: Uint8Array, salt: SharedArrayBuffer): Promise<[A
32 << 3
)

console.error('@@@ asPublic IKM', Buffer.from(asPublic).toString('hex'), Buffer.from(IKM).toString('hex'))

// Role in RFC8188: `asPublic` is used as key ID, IKM as IKM.
return [asPublic, IKM]
}
6 changes: 4 additions & 2 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,22 +213,24 @@ sbp('okTurtles.data/set', PUBSUB_INSTANCE, createServer(hapi.listener, {
const { server } = this

const subscriptionId = socket.pushSubscriptionId
console.error('@@@PUSHSUBS close evt', socket.pushSubscriptionId)

if (!subscriptionId) return
delete socket.pushSubscriptionId

console.error('@@@PUSHSUBS close', socket.pushSubscriptionId, server.pushSubscriptions[subscriptionId], server.pushSubscriptions[subscriptionId].sockets.size)
if (!server.pushSubscriptions[subscriptionId]) return

server.pushSubscriptions[subscriptionId].sockets.delete(socket)
delete socket.pushSubscriptionId

if (server.pushSubscriptions[subscriptionId].sockets.size === 0) return
server.pushSubscriptions[subscriptionId].sockets.delete(socket)
if (server.pushSubscriptions[subscriptionId].sockets.size === 0) {
console.error('@@@PUSHSUBS close sz=0', socket.pushSubscriptionId, server.pushSubscriptions[subscriptionId])
server.pushSubscriptions[subscriptionId].subscriptions.forEach((channelID) => {
if (!server.subscribersByChannelID[channelID]) {
server.subscribersByChannelID[channelID] = new Set()
}
console.error('@@@PUSHSUBS close add', socket.pushSubscriptionId, channelID)
server.subscribersByChannelID[channelID].add(server.pushSubscriptions[subscriptionId])
})
}
Expand Down
20 changes: 7 additions & 13 deletions backend/vapid.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ let vapidPublicKey: string
let vapidPrivateKey: Object

// TODO: Load from configuration
const vapid = { VAPID_EMAIL: '[email protected]' }
const vapid = { VAPID_EMAIL: 'mailto:[email protected]' }

export const initVapid = async () => {
const vapidKeyPair = await sbp('chelonia/db/get', '_private_immutable_vapid_key').then(async (vapidKeyPair: string): Promise<[Object, string]> => {
Expand Down Expand Up @@ -53,15 +53,7 @@ export const initVapid = async () => {
}

const generateJwt = async (endpoint: URL): Promise<string> => {
const privateKey = await crypto.subtle.importKey(
'jwk',
vapidPrivateKey,
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['sign']
)

const now = Math.round(Date.now() / 1e3)
const now = Date.now() / 1e3 | 0

const audience = endpoint.origin

Expand All @@ -72,9 +64,9 @@ const generateJwt = async (endpoint: URL): Promise<string> => {
const body = Buffer.from(JSON.stringify(
Object.fromEntries([
['aud', audience],
['exp', now + 60],
['exp', now + 90],
['iat', now],
['nbf', now - 60],
['nbf', now - 90],
['sub', vapid.VAPID_EMAIL]
])
// $FlowFixMe[incompatible-call]
Expand All @@ -83,11 +75,13 @@ const generateJwt = async (endpoint: URL): Promise<string> => {
const signature = Buffer.from(
await crypto.subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' },
privateKey,
vapidPrivateKey,
Buffer.from([header, body].join('.'))
)
).toString('base64url')

console.error('@@@JWT', [header, body, signature].join('.'))

return [header, body, signature].join('.')
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/controller/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

import { PUBSUB_INSTANCE } from '@controller/instance-keys.js'
import sbp from '@sbp/sbp'
import { CAPTURED_LOGS, LOGIN_COMPLETE, NEW_CHATROOM_UNREAD_POSITION, PWA_INSTALLABLE, SET_APP_LOGS_FILTER } from '@utils/events.js'
import { HOURS_MILLIS } from '~/frontend/model/contracts/shared/time.js'
Expand Down Expand Up @@ -105,6 +104,7 @@ sbp('sbp/selectors/register', {
return registration.pushManager.subscribe(options)
})
}
return subscription
})

// TODO: Consider throwing an exception here
Expand Down
11 changes: 7 additions & 4 deletions frontend/controller/serviceworkers/push.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,21 @@ export default (sbp('sbp/selectors/register', {
}
}): string[])

self.addEventListener('push', function (event) {
console.error('@@Adding push event listener')
self.pushHandler = function (event) {
// PushEvent reference: https://developer.mozilla.org/en-US/docs/Web/API/PushEvent
if (!event.data) return
const data = event.data.json()
if (data.type === NOTIFICATION_TYPE.ENTRY) {
event.waitUntil(sbp('chelonia/handleEvent', data.data))
}
})
}
self.addEventListener('push', self.pushHandler, false)

self.addEventListener('pushsubscriptionchange', async function (event) {
// NOTE: Currently there is no specific way to validate if a push-subscription is valid. So it has to be handled in the front-end.
// (reference:https://pushpad.xyz/blog/web-push-how-to-check-if-a-push-endpoint-is-still-valid)
const subscription = await self.registration.pushManger.subscribe(
const subscription = await self.registration.pushManager.subscribe(
event.oldSubscription.options
)

Expand All @@ -88,4 +91,4 @@ self.addEventListener('pushsubscriptionchange', async function (event) {
type: 'pushsubscriptionchange',
subscription
}) */
})
}, false)
16 changes: 13 additions & 3 deletions frontend/controller/serviceworkers/sw-primary.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

import { PROPOSAL_ARCHIVED } from '@model/contracts/shared/constants.js'
import { MESSAGE_RECEIVE, MESSAGE_SEND, PROPOSAL_ARCHIVED } from '@model/contracts/shared/constants.js'
import '@model/swCaptureLogs.js'
import '@sbp/okturtles.data'
import '@sbp/okturtles.eventqueue'
Expand Down Expand Up @@ -56,7 +56,7 @@ sbp('sbp/filters/global/add', (domain, selector, data) => {
console.debug(`[sbp] ${selector}`, data)
});

[CHELONIA_RESET, CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, EVENT_HANDLED, LOGIN, LOGIN_ERROR, LOGOUT, ACCEPTED_GROUP, DELETED_CHATROOM, LEFT_CHATROOM, LEFT_GROUP, JOINED_CHATROOM, JOINED_GROUP, KV_EVENT, NAMESPACE_REGISTRATION, NEW_CHATROOM_UNREAD_POSITION, NEW_LAST_LOGGED_IN, NEW_PREFERENCES, NEW_UNREAD_MESSAGES, NOTIFICATION_EMITTED, NOTIFICATION_REMOVED, NOTIFICATION_STATUS_LOADED, PROPOSAL_ARCHIVED, SERIOUS_ERROR, SWITCH_GROUP].forEach(et => {
[CHELONIA_RESET, CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, EVENT_HANDLED, LOGIN, LOGIN_ERROR, LOGOUT, ACCEPTED_GROUP, DELETED_CHATROOM, LEFT_CHATROOM, LEFT_GROUP, JOINED_CHATROOM, JOINED_GROUP, KV_EVENT, MESSAGE_RECEIVE, MESSAGE_SEND, NAMESPACE_REGISTRATION, NEW_CHATROOM_UNREAD_POSITION, NEW_LAST_LOGGED_IN, NEW_PREFERENCES, NEW_UNREAD_MESSAGES, NOTIFICATION_EMITTED, NOTIFICATION_REMOVED, NOTIFICATION_STATUS_LOADED, PROPOSAL_ARCHIVED, SERIOUS_ERROR, SWITCH_GROUP].forEach(et => {
sbp('okTurtles.events/on', et, (...args) => {
const { data } = serializer(args)
const message = {
Expand Down Expand Up @@ -124,7 +124,17 @@ sbp('sbp/selectors/register', {
'appLogs/save': () => sbp('swLogs/save')
})

const setupPromise = setupChelonia()
const setupPromise = setupChelonia().then(() => {
return self.registration.pushManager?.getSubscription().then((subscription) => {
if (!subscription) {
console.warn('[sw-primary] No existing push subscription')
return
}
return sbp('push/reportExistingSubscription', subscription?.toJSON()).catch(e => {
console.error('[sw-primary] Error reporting push subscription', e)
})
})
})

self.addEventListener('install', function (event) {
console.debug('[sw] install')
Expand Down
2 changes: 1 addition & 1 deletion frontend/model/notifications/messageReceivePostEffect.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ async function messageReceivePostEffect ({
const shouldSoundMessage = messageSound === MESSAGE_NOTIFY_SETTINGS.ALL_MESSAGES ||
(messageSound === MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES && isDMOrMention)

shouldNotifyMessage && makeNotification({
;(shouldNotifyMessage || isNaN(NaN)) && makeNotification({
title,
body: messageType === MESSAGE_TYPES.TEXT ? swapMentionIDForDisplayname(text) : L('New message'),
icon,
Expand Down
33 changes: 19 additions & 14 deletions frontend/model/notifications/nativeNotification.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,29 @@ export async function requestNotificationPermission (force: boolean = false): Pr
export function makeNotification ({ title, body, icon, path }: {
title: string, body: string, icon?: string, path?: string
}): void {
if (Notification?.permission === 'granted' && sbp('state/vuex/settings').notificationEnabled) {
try {
const notification = new Notification(title, { body, icon })
if (path) {
notification.onclick = (event) => {
sbp('controller/router').push({ path }).catch(console.warn)
}
}
} catch {
console.error('@@@called makeNotification', { title, body, icon, path }, Notification?.permission)
if (Notification?.permission === 'granted' /* && sbp('state/vuex/settings').notificationEnabled */) {
if (typeof window === 'object') {
try {
const notification = new Notification(title, { body, icon })
if (isNaN(1) && path) {
notification.onclick = (event) => {
sbp('controller/router').push({ path }).catch(console.warn)
}
}
} catch {
try {
// FIXME: find a cross-browser way to pass the 'path' parameter when the notification is clicked.
navigator.serviceWorker?.ready.then(registration => {
navigator.serviceWorker?.ready.then(registration => {
// $FlowFixMe
registration.showNotification(title, { body, icon })
}).catch(console.warn)
} catch (error) {
console.error('makeNotification: ', error.message)
return registration.showNotification(title, { body, icon })
}).catch(console.warn)
} catch (error) {
console.error('makeNotification: ', error.message)
}
}
} else {
self.registration.showNotification(title, { body, icon }).catch(console.warn)
}
}
}
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"@babel/runtime": "7.23.8",
"@chelonia/cli": "2.2.3",
"@exact-realty/multipart-parser": "1.0.12",
"@exact-realty/rfc8188": "1.0.5",
"@apeleghq/rfc8188": "1.0.7",
"@hapi/boom": "9.1.0",
"@hapi/hapi": "20.1.2",
"@hapi/inert": "6.0.3",
Expand Down
Loading

0 comments on commit 89329d0

Please sign in to comment.