Add ping support

This commit is contained in:
Jonas Lochmann
2026-03-23 01:00:00 +01:00
parent 510c61bafd
commit 3aed2010c7
28 changed files with 1085 additions and 16 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -78,6 +78,7 @@ export { UpdateTimelimitRuleAction } from './updatetimelimitrule'
export { UpdateUserFlagsAction } from './updateuserflags'
export { UpdateUserLimitLoginCategory } from './updateuserlimitlogincategory'
export { MarkTaskPendingAction } from './marktaskpendingaction'
export { PingAction } from './ping'
export { DeleteChildTaskAction } from './deletechildtaskaction'
export { UpdateChildTaskAction } from './updatechildtaskaction'
export { ReviewChildTaskAction } from './reviewchildtaskaction'
+55
View File
@@ -0,0 +1,55 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AppLogicAction } from './basetypes'
import { assertIdWithinFamily } from './meta/util'
const actionType = 'PingAction'
export class PingAction extends AppLogicAction {
readonly deviceId: string
readonly event: PingEvent
readonly token: string
constructor ({ deviceId, event, token }: {
deviceId: string
event: PingEvent
token: string
}) {
super()
assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId })
assertIdWithinFamily({ actionType, field: 'token', value: token })
this.deviceId = deviceId
this.event = event
this.token = token
}
static parse = ({ deviceId, event, token }: SerializedPingAction) => (
new PingAction({ deviceId, event, token })
)
}
export interface SerializedPingAction {
type: 'PING'
deviceId: string
event: PingEvent
token: string
}
export type PingEvent = 'ping' | 'pong' | 'clear'
+5 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -24,6 +24,7 @@ import { ForceSyncAction, SerializedForceSyncAction } from '../forcesync'
import { ReplyToKeyRequestAction, SerializedReplyToKeyRequestAction } from '../replytokeyrequest'
import { MarkTaskPendingAction, SerializedMarkTaskPendingAction } from '../marktaskpendingaction'
import { UnknownActionTypeException } from '../meta/exception'
import { PingAction, SerializedPingAction } from '../ping'
import { UpdateInstalledAppsAction, SerializedUpdateInstalledAppsAction } from '../updateinstalledapps'
import { RemoveInstalledAppsAction, SerializedRemoveInstalledAppsAction } from '../removeinstalledapps'
import { SendKeyRequestAction, SerializedSendKeyRequestAction } from '../sendkeyrequest'
@@ -41,6 +42,7 @@ export type SerializedAppLogicAction =
SerializedForceSyncAction |
SerializedReplyToKeyRequestAction |
SerializedMarkTaskPendingAction |
SerializedPingAction |
SerializedUpdateInstalledAppsAction |
SerializedRemoveInstalledAppsAction |
SerializedSendKeyRequestAction |
@@ -65,6 +67,8 @@ export const parseAppLogicAction = (serialized: SerializedAppLogicAction): AppLo
return ReplyToKeyRequestAction.parse(serialized)
} else if (serialized.type === 'MARK_TASK_PENDING') {
return MarkTaskPendingAction.parse(serialized)
} else if (serialized.type === 'PING') {
return PingAction.parse(serialized)
} else if (serialized.type === 'UPDATE_INSTALLED_APPS') {
return UpdateInstalledAppsAction.parse(serialized)
} else if (serialized.type === 'REMOVE_INSTALLED_APPS') {
+62
View File
@@ -1759,6 +1759,41 @@ const definitions = {
"type"
]
},
"SerializedPingAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"PING"
]
},
"deviceId": {
"type": "string"
},
"event": {
"$ref": "#/definitions/PingEvent"
},
"token": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"deviceId",
"event",
"token",
"type"
]
},
"PingEvent": {
"enum": [
"clear",
"ping",
"pong"
],
"type": "string"
},
"SerializedUpdateInstalledAppsAction": {
"type": "object",
"properties": {
@@ -2815,6 +2850,30 @@ const definitions = {
"tempKey"
]
},
"ServerPing": {
"type": "object",
"properties": {
"deviceId": {
"type": "string"
},
"token": {
"type": "string"
},
"type": {
"enum": [
"ping",
"pong"
],
"type": "string"
}
},
"additionalProperties": false,
"required": [
"deviceId",
"token",
"type"
]
},
"ServerDhKey": {
"type": "object",
"properties": {
@@ -3213,6 +3272,9 @@ export const isSerializedAppLogicAction: (value: unknown) => value is Serialized
{
"$ref": "#/definitions/SerializedMarkTaskPendingAction"
},
{
"$ref": "#/definitions/SerializedPingAction"
},
{
"$ref": "#/definitions/SerializedUpdateInstalledAppsAction"
},
+4 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -33,6 +33,7 @@ import { createKeyResponseModel, KeyResponseModelStatic } from './keyresponse'
import { createMailLoginTokenModel, MailLoginTokenModelStatic } from './maillogintoken'
import { createUmzug } from './migration/umzug'
import { createOldDeviceModel, OldDeviceModelStatic } from './olddevice'
import { createPingModel, PingModelStatic } from './ping'
import { createPurchaseModel, PurchaseModelStatic } from './purchase'
import { createSessionDurationModel, SessionDurationModelStatic } from './sessionduration'
import { createTimelimitRuleModel, TimelimitRuleModelStatic } from './timelimitrule'
@@ -61,6 +62,7 @@ export interface Database {
keyResponse: KeyResponseModelStatic
mailLoginToken: MailLoginTokenModelStatic
oldDevice: OldDeviceModelStatic
ping: PingModelStatic
purchase: PurchaseModelStatic
sessionDuration: SessionDurationModelStatic
timelimitRule: TimelimitRuleModelStatic
@@ -97,6 +99,7 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
keyResponse: createKeyResponseModel(sequelize),
mailLoginToken: createMailLoginTokenModel(sequelize),
oldDevice: createOldDeviceModel(sequelize),
ping: createPingModel(sequelize),
purchase: createPurchaseModel(sequelize),
sessionDuration: createSessionDurationModel(sequelize),
timelimitRule: createTimelimitRuleModel(sequelize),
@@ -0,0 +1,67 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
const dialect = sequelize.getDialect()
const isMysql = dialect === 'mysql' || dialect === 'mariadb'
if (isMysql) {
await sequelize.query(
'CREATE TABLE `Pings` ' +
'(`familyId` VARCHAR(10) NOT NULL,' +
'`receiverDeviceId` VARCHAR(6) NOT NULL,' +
'`senderDeviceId` VARCHAR(6) NOT NULL,' +
'`type` INTEGER NOT NULL,' +
'`token` VARCHAR(6) NOT NULL,' +
'PRIMARY KEY (`familyId`, `receiverDeviceId`, `senderDeviceId`, `type`),' +
'FOREIGN KEY (`familyId`, `receiverDeviceId`) REFERENCES `Devices` (`familyId`, `deviceId`) ON UPDATE CASCADE ON DELETE CASCADE,' +
'FOREIGN KEY (`familyId`, `senderDeviceId`) REFERENCES `Devices` (`familyId`, `deviceId`) ON UPDATE CASCADE ON DELETE CASCADE' +
')',
{ transaction }
)
} else {
await sequelize.query(
'CREATE TABLE "Pings" ' +
'("familyId" VARCHAR(10) NOT NULL,' +
'"receiverDeviceId" VARCHAR(6) NOT NULL,' +
'"senderDeviceId" VARCHAR(6) NOT NULL,' +
'"type" INTEGER NOT NULL,' +
'"token" VARCHAR(6) NOT NULL,' +
'PRIMARY KEY ("familyId", "receiverDeviceId", "senderDeviceId", "type"),' +
'FOREIGN KEY ("familyId", "receiverDeviceId") REFERENCES "Devices" ("familyId", "deviceId") ON UPDATE CASCADE ON DELETE CASCADE,' +
'FOREIGN KEY ("familyId", "senderDeviceId") REFERENCES "Devices" ("familyId", "deviceId") ON UPDATE CASCADE ON DELETE CASCADE' +
')',
{ transaction }
)
}
await queryInterface.addIndex('Pings', ['familyId', 'senderDeviceId'], { transaction })
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.dropTable('Pings', { transaction })
})
}
+67
View File
@@ -0,0 +1,67 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { familyIdColumn, idWithinFamilyColumn } from './columns'
import { SequelizeAttributes } from './types'
export interface PingAttributes {
familyId: string
receiverDeviceId: string
senderDeviceId: string
type: number
token: string
}
export const types = {
ping: 1,
pong: 2,
all: [1, 2]
}
export type PingModel = Sequelize.Model<PingAttributes> & PingAttributes
export type PingModelStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): PingModel;
}
export const attributes: SequelizeAttributes<PingAttributes> = {
familyId: {
...familyIdColumn,
primaryKey: true
},
receiverDeviceId: {
...idWithinFamilyColumn,
primaryKey: true
},
senderDeviceId: {
...idWithinFamilyColumn,
primaryKey: true
},
type: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
isIn: [types.all]
},
primaryKey: true
},
token: {
...idWithinFamilyColumn
}
}
export const createPingModel = (sequelize: Sequelize.Sequelize): PingModelStatic => sequelize.define('Ping', attributes) as PingModelStatic
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -23,6 +23,7 @@ import {
FinishKeyRequestAction,
ForceSyncAction,
MarkTaskPendingAction,
PingAction,
ReplyToKeyRequestAction,
RemoveInstalledAppsAction,
SendKeyRequestAction,
@@ -41,6 +42,7 @@ import { dispatchAddUsedTimeVersion2 } from './addusedtime2'
import { dispatchFinishKeyRequestAction } from './finishkeyrequest'
import { dispatchForceSyncAction } from './forcesync'
import { dispatchMarkTaskPendingAction } from './marktaskpendingaction'
import { dispatchPingAction } from './ping'
import { dispatchReplyToKeyRequestAction } from './replytokeyrequest'
import { dispatchSendKeyRequestAction } from './sendkeyrequest'
import { dispatchSignOutAtDevice } from './signoutatdevice'
@@ -67,6 +69,8 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
await dispatchForceSyncAction({ deviceId, action, cache })
} else if (action instanceof MarkTaskPendingAction) {
await dispatchMarkTaskPendingAction({ deviceId, action, cache })
} else if (action instanceof PingAction) {
await dispatchPingAction({ deviceId, action, cache })
} else if (action instanceof ReplyToKeyRequestAction) {
await dispatchReplyToKeyRequestAction({ deviceId, action, cache, eventHandler })
} else if (action instanceof RemoveInstalledAppsAction) {
@@ -0,0 +1,73 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PingAction } from '../../../../action'
import { types as pingTypes } from '../../../../database/ping'
import { Cache } from '../cache'
export async function dispatchPingAction ({ action, cache, deviceId }: {
deviceId: string
action: PingAction
cache: Cache
}) {
// handle insertion
if (action.event === 'ping' || action.event === 'pong') {
let type
if (action.event === 'ping') type = pingTypes.ping
else if (action.event === 'pong') type = pingTypes.pong
else throw new Error()
// delete any previous ping/pong
await cache.database.ping.destroy({
where: {
familyId: cache.familyId,
receiverDeviceId: action.deviceId,
senderDeviceId: deviceId,
type: type
},
transaction: cache.transaction
})
// insert
await cache.database.ping.create({
familyId: cache.familyId,
receiverDeviceId: action.deviceId,
senderDeviceId: deviceId,
type,
token: action.token
}, {
transaction: cache.transaction
})
// notify
cache.incrementTargetedTriggeredSyncLevel(action.deviceId, 2)
}
// handle deleting
if (action.event === 'pong' || action.event === 'clear') {
await cache.database.ping.destroy({
where: {
familyId: cache.familyId,
receiverDeviceId: deviceId,
senderDeviceId: action.deviceId,
token: action.token
},
transaction: cache.transaction
})
}
}
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2024 Jonas Lochmann
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -30,6 +30,7 @@ import { getDeviceDetailList } from './device-detail'
import { getDeviceList } from './device-list'
import { getDeviceDhKeys } from './dh-keys'
import { getFamilyEntry } from './family-entry'
import { getPings } from './pings'
import { getUserList } from './user-list'
import { getKeyRequests } from './key-requests'
import { getKeyResponses } from './key-responses'
@@ -52,13 +53,14 @@ export const generateServerDataStatus = async ({
const doesClientSupportCryptoApps = clientLevel >= 4
const doesClientSupportDh = clientLevel >= 5
const doesClientSupportU2f = clientLevel >= 6
const doesClientSupportPing = clientLevel >= 7
const result: ServerDataStatus = {
fullVersion: config.alwaysPro ? 1 : (
familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0
),
message: await getStatusMessage({ database, transaction }) || undefined,
apiLevel: 8
apiLevel: 9
}
if (familyEntry.deviceListVersion !== clientStatus.devices) {
@@ -160,5 +162,14 @@ export const generateServerDataStatus = async ({
}) || undefined
}
if (doesClientSupportPing) {
result.pings = await getPings({
database,
transaction,
familyEntry,
deviceId
})
}
return result
}
@@ -0,0 +1,52 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { Database } from '../../../database'
import { types as pingTypes } from '../../../database/ping'
import { ServerPing } from '../../../object/serverdatastatus'
import { FamilyEntry } from './family-entry'
export async function getPings ({
database, transaction, familyEntry, deviceId
}: {
database: Database
transaction: Sequelize.Transaction
familyEntry: FamilyEntry
deviceId: string
}): Promise<Array<ServerPing>> {
const savedData = await database.ping.findAll({
where: {
familyId: familyEntry.familyId,
receiverDeviceId: deviceId,
},
attributes: ['senderDeviceId', 'type', 'token'],
transaction
})
return savedData.map((row) => ({
deviceId: row.senderDeviceId,
type: convertType(row.type),
token: row.token
}))
}
function convertType(type: number): 'ping' | 'pong' {
if (type === pingTypes.ping) return 'ping'
else if (type === pingTypes.pong) return 'pong'
else throw new Error()
}
+8 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -34,6 +34,7 @@ export interface ServerDataStatus {
users?: ServerUserList // newUserList
krq?: Array<ServerKeyRequest> // pendingKeyRequests
kr?: Array<ServerKeyResponse> // keyResponses
pings?: Array<ServerPing>
dh?: ServerDhKey // Diffie Hellman
u2f?: U2fData
fullVersion: number // fullVersionUntil
@@ -257,6 +258,12 @@ export interface ServerKeyResponse {
signature: string
}
export interface ServerPing {
deviceId: string
token: string
type: 'ping' | 'pong'
}
export interface ServerDhKey {
v: string // version
k: string // key, base64