mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
refactor(fingerprint): start taking fingerprint snapshots in new table
This commit is contained in:
committed by
Owen Schwartz
parent
adf3d0347b
commit
1f077d7ec2
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, olms } from "@server/db";
|
||||
import { clients, fingerprints } from "@server/db";
|
||||
import { clients, currentFingerprint } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -30,7 +30,10 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.leftJoin(
|
||||
currentFingerprint,
|
||||
eq(olms.olmId, currentFingerprint.olmId)
|
||||
)
|
||||
.limit(1);
|
||||
return res;
|
||||
} else if (niceId && orgId) {
|
||||
@@ -39,7 +42,10 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
.from(clients)
|
||||
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.leftJoin(
|
||||
currentFingerprint,
|
||||
eq(olms.olmId, currentFingerprint.olmId)
|
||||
)
|
||||
.limit(1);
|
||||
return res;
|
||||
}
|
||||
@@ -125,24 +131,25 @@ export async function getClient(
|
||||
// Replace name with device name if OLM exists
|
||||
let clientName = client.clients.name;
|
||||
if (client.olms) {
|
||||
const model = client.fingerprints?.deviceModel || null;
|
||||
const model = client.currentFingerprint?.deviceModel || null;
|
||||
clientName = getUserDeviceName(model, client.clients.name);
|
||||
}
|
||||
|
||||
// Build fingerprint data if available
|
||||
const fingerprintData = client.fingerprints
|
||||
const fingerprintData = client.currentFingerprint
|
||||
? {
|
||||
username: client.fingerprints.username || null,
|
||||
hostname: client.fingerprints.hostname || null,
|
||||
platform: client.fingerprints.platform || null,
|
||||
osVersion: client.fingerprints.osVersion || null,
|
||||
kernelVersion: client.fingerprints.kernelVersion || null,
|
||||
arch: client.fingerprints.arch || null,
|
||||
deviceModel: client.fingerprints.deviceModel || null,
|
||||
serialNumber: client.fingerprints.serialNumber || null,
|
||||
firstSeen: client.fingerprints.firstSeen || null,
|
||||
lastSeen: client.fingerprints.lastSeen || null
|
||||
}
|
||||
username: client.currentFingerprint.username || null,
|
||||
hostname: client.currentFingerprint.hostname || null,
|
||||
platform: client.currentFingerprint.platform || null,
|
||||
osVersion: client.currentFingerprint.osVersion || null,
|
||||
kernelVersion:
|
||||
client.currentFingerprint.kernelVersion || null,
|
||||
arch: client.currentFingerprint.arch || null,
|
||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||
lastSeen: client.currentFingerprint.lastSeen || null
|
||||
}
|
||||
: null;
|
||||
|
||||
const data: GetClientResponse = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
sites,
|
||||
userClients,
|
||||
clientSitesAssociationsCache,
|
||||
fingerprints
|
||||
currentFingerprint
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -143,20 +143,20 @@ function queryClients(
|
||||
olmArchived: olms.archived,
|
||||
archived: clients.archived,
|
||||
blocked: clients.blocked,
|
||||
deviceModel: fingerprints.deviceModel,
|
||||
fingerprintPlatform: fingerprints.platform,
|
||||
fingerprintOsVersion: fingerprints.osVersion,
|
||||
fingerprintKernelVersion: fingerprints.kernelVersion,
|
||||
fingerprintArch: fingerprints.arch,
|
||||
fingerprintSerialNumber: fingerprints.serialNumber,
|
||||
fingerprintUsername: fingerprints.username,
|
||||
fingerprintHostname: fingerprints.hostname
|
||||
deviceModel: currentFingerprint.deviceModel,
|
||||
fingerprintPlatform: currentFingerprint.platform,
|
||||
fingerprintOsVersion: currentFingerprint.osVersion,
|
||||
fingerprintKernelVersion: currentFingerprint.kernelVersion,
|
||||
fingerprintArch: currentFingerprint.arch,
|
||||
fingerprintSerialNumber: currentFingerprint.serialNumber,
|
||||
fingerprintUsername: currentFingerprint.username,
|
||||
fingerprintHostname: currentFingerprint.hostname
|
||||
})
|
||||
.from(clients)
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(users, eq(clients.userId, users.userId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
|
||||
.where(and(...conditions));
|
||||
}
|
||||
|
||||
|
||||
132
server/routers/olm/fingerprintingUtils.ts
Normal file
132
server/routers/olm/fingerprintingUtils.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { currentFingerprint, db, fingerprintSnapshots, Olm } from "@server/db";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
function fingerprintHash(fp: any): string {
|
||||
const canonical = {
|
||||
username: fp.username ?? null,
|
||||
hostname: fp.hostname ?? null,
|
||||
platform: fp.platform ?? null,
|
||||
osVersion: fp.osVersion ?? null,
|
||||
kernelVersion: fp.kernelVersion ?? null,
|
||||
arch: fp.arch ?? null,
|
||||
deviceModel: fp.deviceModel ?? null,
|
||||
serialNumber: fp.serialNumber ?? null,
|
||||
platformFingerprint: fp.platformFingerprint ?? null
|
||||
};
|
||||
|
||||
return encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(JSON.stringify(canonical)))
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleFingerprintInsertion(olm: Olm, fingerprint: any) {
|
||||
if (!fingerprint || !olm.olmId || Object.keys(fingerprint).length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = fingerprintHash(fingerprint);
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const [current] = await db
|
||||
.select()
|
||||
.from(currentFingerprint)
|
||||
.where(eq(currentFingerprint.olmId, olm.olmId))
|
||||
.limit(1);
|
||||
|
||||
if (!current) {
|
||||
const [inserted] = await db
|
||||
.insert(currentFingerprint)
|
||||
.values({
|
||||
olmId: olm.olmId,
|
||||
firstSeen: now,
|
||||
lastSeen: now,
|
||||
|
||||
username: fingerprint.username,
|
||||
hostname: fingerprint.hostname,
|
||||
platform: fingerprint.platform,
|
||||
osVersion: fingerprint.osVersion,
|
||||
kernelVersion: fingerprint.kernelVersion,
|
||||
arch: fingerprint.arch,
|
||||
deviceModel: fingerprint.deviceModel,
|
||||
serialNumber: fingerprint.serialNumber,
|
||||
platformFingerprint: fingerprint.platformFingerprint
|
||||
})
|
||||
.returning();
|
||||
|
||||
await db.insert(fingerprintSnapshots).values({
|
||||
fingerprintId: inserted.fingerprintId,
|
||||
|
||||
username: fingerprint.username,
|
||||
hostname: fingerprint.hostname,
|
||||
platform: fingerprint.platform,
|
||||
osVersion: fingerprint.osVersion,
|
||||
kernelVersion: fingerprint.kernelVersion,
|
||||
arch: fingerprint.arch,
|
||||
deviceModel: fingerprint.deviceModel,
|
||||
serialNumber: fingerprint.serialNumber,
|
||||
platformFingerprint: fingerprint.platformFingerprint,
|
||||
|
||||
hash,
|
||||
collectedAt: now
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get most recent snapshot hash
|
||||
const [latestSnapshot] = await db
|
||||
.select({ hash: fingerprintSnapshots.hash })
|
||||
.from(fingerprintSnapshots)
|
||||
.where(eq(fingerprintSnapshots.fingerprintId, current.fingerprintId))
|
||||
.orderBy(desc(fingerprintSnapshots.collectedAt))
|
||||
.limit(1);
|
||||
|
||||
const changed = !latestSnapshot || latestSnapshot.hash !== hash;
|
||||
|
||||
if (changed) {
|
||||
// Insert snapshot if it has changed
|
||||
await db.insert(fingerprintSnapshots).values({
|
||||
fingerprintId: current.fingerprintId,
|
||||
|
||||
username: fingerprint.username,
|
||||
hostname: fingerprint.hostname,
|
||||
platform: fingerprint.platform,
|
||||
osVersion: fingerprint.osVersion,
|
||||
kernelVersion: fingerprint.kernelVersion,
|
||||
arch: fingerprint.arch,
|
||||
deviceModel: fingerprint.deviceModel,
|
||||
serialNumber: fingerprint.serialNumber,
|
||||
platformFingerprint: fingerprint.platformFingerprint,
|
||||
|
||||
hash,
|
||||
collectedAt: now
|
||||
});
|
||||
|
||||
// Update current fingerprint fully
|
||||
await db
|
||||
.update(currentFingerprint)
|
||||
.set({
|
||||
lastSeen: now,
|
||||
|
||||
username: fingerprint.username,
|
||||
hostname: fingerprint.hostname,
|
||||
platform: fingerprint.platform,
|
||||
osVersion: fingerprint.osVersion,
|
||||
kernelVersion: fingerprint.kernelVersion,
|
||||
arch: fingerprint.arch,
|
||||
deviceModel: fingerprint.deviceModel,
|
||||
serialNumber: fingerprint.serialNumber,
|
||||
platformFingerprint: fingerprint.platformFingerprint
|
||||
})
|
||||
.where(eq(currentFingerprint.fingerprintId, current.fingerprintId));
|
||||
} else {
|
||||
// No change, so only bump lastSeen
|
||||
await db
|
||||
.update(currentFingerprint)
|
||||
.set({ lastSeen: now })
|
||||
.where(eq(currentFingerprint.fingerprintId, current.fingerprintId));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { olms, clients, fingerprints } from "@server/db";
|
||||
import { olms, clients, currentFingerprint } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -66,16 +66,14 @@ export async function getUserOlm(
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(and(eq(olms.userId, userId), eq(olms.olmId, olmId)))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.leftJoin(
|
||||
currentFingerprint,
|
||||
eq(olms.olmId, currentFingerprint.olmId)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result.olms) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Olm not found"
|
||||
)
|
||||
);
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Olm not found"));
|
||||
}
|
||||
|
||||
const olm = result.olms;
|
||||
@@ -98,12 +96,13 @@ export async function getUserOlm(
|
||||
}
|
||||
|
||||
// Replace name with device name
|
||||
const model = result.fingerprints?.deviceModel || null;
|
||||
const model = result.currentFingerprint?.deviceModel || null;
|
||||
const newName = getUserDeviceName(model, olm.name);
|
||||
|
||||
const responseData = blocked !== undefined
|
||||
? { ...olm, name: newName, blocked }
|
||||
: { ...olm, name: newName };
|
||||
const responseData =
|
||||
blocked !== undefined
|
||||
? { ...olm, name: newName, blocked }
|
||||
: { ...olm, name: newName };
|
||||
|
||||
return response(res, {
|
||||
data: responseData,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
|
||||
import { clientPostureSnapshots, db, fingerprints } from "@server/db";
|
||||
import { clientPostureSnapshots, db, currentFingerprint } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { clients, olms, Olm } from "@server/db";
|
||||
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
||||
@@ -11,6 +11,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { sendOlmSyncMessage } from "./sync";
|
||||
import { OlmErrorCodes } from "./error";
|
||||
import { handleFingerprintInsertion } from "./fingerprintingUtils";
|
||||
|
||||
// Track if the offline checker interval is running
|
||||
let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
||||
@@ -173,15 +174,25 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
||||
}
|
||||
|
||||
// get the version
|
||||
logger.debug(`handleOlmPingMessage: About to get config version for olmId: ${olm.olmId}`);
|
||||
logger.debug(
|
||||
`handleOlmPingMessage: About to get config version for olmId: ${olm.olmId}`
|
||||
);
|
||||
const configVersion = await getClientConfigVersion(olm.olmId);
|
||||
logger.debug(`handleOlmPingMessage: Got config version: ${configVersion} (type: ${typeof configVersion})`);
|
||||
logger.debug(
|
||||
`handleOlmPingMessage: Got config version: ${configVersion} (type: ${typeof configVersion})`
|
||||
);
|
||||
|
||||
if (configVersion == null || configVersion === undefined) {
|
||||
logger.debug(`handleOlmPingMessage: could not get config version from server for olmId: ${olm.olmId}`);
|
||||
logger.debug(
|
||||
`handleOlmPingMessage: could not get config version from server for olmId: ${olm.olmId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (message.configVersion != null && configVersion != null && configVersion != message.configVersion) {
|
||||
if (
|
||||
message.configVersion != null &&
|
||||
configVersion != null &&
|
||||
configVersion != message.configVersion
|
||||
) {
|
||||
logger.debug(
|
||||
`handleOlmPingMessage: Olm ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
|
||||
);
|
||||
@@ -204,55 +215,14 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
||||
.set({ archived: false })
|
||||
.where(eq(olms.olmId, olm.olmId));
|
||||
}
|
||||
|
||||
await handleFingerprintInsertion(olm, fingerprint);
|
||||
} catch (error) {
|
||||
logger.error("Error handling ping message", { error });
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (fingerprint && olm.olmId) {
|
||||
const [existingFingerprint] = await db
|
||||
.select()
|
||||
.from(fingerprints)
|
||||
.where(eq(fingerprints.olmId, olm.olmId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingFingerprint) {
|
||||
await db.insert(fingerprints).values({
|
||||
olmId: olm.olmId,
|
||||
firstSeen: now,
|
||||
lastSeen: now,
|
||||
|
||||
username: fingerprint.username,
|
||||
hostname: fingerprint.hostname,
|
||||
platform: fingerprint.platform,
|
||||
osVersion: fingerprint.osVersion,
|
||||
kernelVersion: fingerprint.kernelVersion,
|
||||
arch: fingerprint.arch,
|
||||
deviceModel: fingerprint.deviceModel,
|
||||
serialNumber: fingerprint.serialNumber,
|
||||
platformFingerprint: fingerprint.platformFingerprint
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(fingerprints)
|
||||
.set({
|
||||
lastSeen: now,
|
||||
|
||||
username: fingerprint.username,
|
||||
hostname: fingerprint.hostname,
|
||||
platform: fingerprint.platform,
|
||||
osVersion: fingerprint.osVersion,
|
||||
kernelVersion: fingerprint.kernelVersion,
|
||||
arch: fingerprint.arch,
|
||||
deviceModel: fingerprint.deviceModel,
|
||||
serialNumber: fingerprint.serialNumber,
|
||||
platformFingerprint: fingerprint.platformFingerprint
|
||||
})
|
||||
.where(eq(fingerprints.olmId, olm.olmId));
|
||||
}
|
||||
}
|
||||
|
||||
if (postures && olm.clientId) {
|
||||
await db.insert(clientPostureSnapshots).values({
|
||||
clientId: olm.clientId,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { clientPostureSnapshots, db, fingerprints, orgs } from "@server/db";
|
||||
import {
|
||||
clientPostureSnapshots,
|
||||
db,
|
||||
currentFingerprint,
|
||||
orgs
|
||||
} from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import {
|
||||
clients,
|
||||
@@ -48,12 +53,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
if (fingerprint) {
|
||||
const [existingFingerprint] = await db
|
||||
.select()
|
||||
.from(fingerprints)
|
||||
.where(eq(fingerprints.olmId, olm.olmId))
|
||||
.from(currentFingerprint)
|
||||
.where(eq(currentFingerprint.olmId, olm.olmId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingFingerprint) {
|
||||
await db.insert(fingerprints).values({
|
||||
await db.insert(currentFingerprint).values({
|
||||
olmId: olm.olmId,
|
||||
firstSeen: now,
|
||||
lastSeen: now,
|
||||
@@ -75,16 +80,16 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
existingFingerprint.platform !== fingerprint.platform ||
|
||||
existingFingerprint.osVersion !== fingerprint.osVersion ||
|
||||
existingFingerprint.kernelVersion !==
|
||||
fingerprint.kernelVersion ||
|
||||
fingerprint.kernelVersion ||
|
||||
existingFingerprint.arch !== fingerprint.arch ||
|
||||
existingFingerprint.deviceModel !== fingerprint.deviceModel ||
|
||||
existingFingerprint.serialNumber !== fingerprint.serialNumber ||
|
||||
existingFingerprint.platformFingerprint !==
|
||||
fingerprint.platformFingerprint;
|
||||
fingerprint.platformFingerprint;
|
||||
|
||||
if (hasChanges) {
|
||||
await db
|
||||
.update(fingerprints)
|
||||
.update(currentFingerprint)
|
||||
.set({
|
||||
lastSeen: now,
|
||||
username: fingerprint.username,
|
||||
@@ -97,7 +102,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
serialNumber: fingerprint.serialNumber,
|
||||
platformFingerprint: fingerprint.platformFingerprint
|
||||
})
|
||||
.where(eq(fingerprints.olmId, olm.olmId));
|
||||
.where(eq(currentFingerprint.olmId, olm.olmId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { db, fingerprints } from "@server/db";
|
||||
import { db, currentFingerprint } from "@server/db";
|
||||
import { olms } from "@server/db";
|
||||
import { eq, count, desc } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -104,13 +104,16 @@ export async function listUserOlms(
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.userId, userId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.leftJoin(
|
||||
currentFingerprint,
|
||||
eq(olms.olmId, currentFingerprint.olmId)
|
||||
)
|
||||
.orderBy(desc(olms.dateCreated))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const userOlms = list.map((item) => {
|
||||
const model = item.fingerprints?.deviceModel || null;
|
||||
const model = item.currentFingerprint?.deviceModel || null;
|
||||
const newName = getUserDeviceName(model, item.olms.name);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, fingerprints, olms } from "@server/db";
|
||||
import { db, currentFingerprint, olms } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
@@ -55,18 +55,24 @@ export async function recoverOlmWithFingerprint(
|
||||
const result = await db
|
||||
.select({
|
||||
olm: olms,
|
||||
fingerprint: fingerprints
|
||||
fingerprint: currentFingerprint
|
||||
})
|
||||
.from(olms)
|
||||
.innerJoin(fingerprints, eq(fingerprints.olmId, olms.olmId))
|
||||
.innerJoin(
|
||||
currentFingerprint,
|
||||
eq(currentFingerprint.olmId, olms.olmId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(olms.userId, userId),
|
||||
eq(olms.archived, false),
|
||||
eq(fingerprints.platformFingerprint, platformFingerprint)
|
||||
eq(
|
||||
currentFingerprint.platformFingerprint,
|
||||
platformFingerprint
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(fingerprints.lastSeen);
|
||||
.orderBy(currentFingerprint.lastSeen);
|
||||
|
||||
if (!result || result.length == 0) {
|
||||
return next(
|
||||
|
||||
Reference in New Issue
Block a user