Browse Source

Key mishmatch warning, new serialization handler

pull/780/head
philon- 11 months ago
parent
commit
1c5bafe8de
  1. 79
      packages/web/src/core/stores/nodeDBStore/index.ts
  2. 8
      packages/web/src/core/stores/nodeDBStore/types.ts
  3. 105
      packages/web/src/core/stores/utils/indexDB.ts
  4. 27
      packages/web/src/core/subscriptions.ts

79
packages/web/src/core/stores/nodeDBStore/index.ts

@ -4,7 +4,7 @@ import { Protobuf, type Types } from "@meshtastic/core";
import { produce } from "immer";
import { create as createStore } from "zustand";
import { persist } from "zustand/middleware";
import type { NodeError, ProcessPacketParams } from "./types";
import type { NodeError, NodeErrorType, ProcessPacketParams } from "./types";
const CURRENT_STORE_VERSION = 0;
@ -22,7 +22,7 @@ export interface NodeDB {
updateFavorite: (nodeNum: number, isFavorite: boolean) => void;
updateIgnore: (nodeNum: number, isIgnored: boolean) => void;
setNodeNum: (nodeNum: number) => void;
setNodeError: (nodeNum: number, error: string) => void;
setNodeError: (nodeNum: number, error: NodeErrorType) => void;
clearNodeError: (nodeNum: number) => void;
getNodesLength: () => number;
@ -58,17 +58,16 @@ type NodeDBPersisted = {
nodeDBs: Map<number, NodeDBData>;
};
function mergeIntoCurrent(target: NodeDB, incoming: NodeDB) {
for (const [n, info] of incoming.nodeMap) {
if (!target.nodeMap.has(n)) {
target.nodeMap.set(n, info);
}
export function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
if (a.byteLength !== b.byteLength) {
return false;
}
for (const [n, err] of incoming.nodeErrors) {
if (!target.nodeErrors.has(n)) {
target.nodeErrors.set(n, err);
for (let i = 0; i < a.byteLength; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
function nodeDBFactory(
@ -79,7 +78,7 @@ function nodeDBFactory(
): NodeDB {
const nodeMap = data?.nodeMap ?? new Map<number, Protobuf.Mesh.NodeInfo>();
const nodeErrors = data?.nodeErrors ?? new Map<number, NodeError>();
let myNodeNum = data?.myNodeNum;
const myNodeNum = data?.myNodeNum;
return {
id,
@ -205,26 +204,30 @@ function nodeDBFactory(
setNodeNum: (nodeNum) =>
set(
produce<PrivateNodeDBState>((draft) => {
console.debug(
`Setting myNodeNum for nodeDB with id: ${id} to ${nodeNum}`,
);
const current = draft.nodeDBs.get(id);
if (!current) {
const newDB = draft.nodeDBs.get(id);
if (!newDB) {
throw new Error(`No nodeDB found for id: ${id}`);
}
// There might already be an entry for this nodeNum, so we need to merge
// it into the current nodeDB.
myNodeNum = nodeNum;
current.myNodeNum = nodeNum;
newDB.myNodeNum = nodeNum;
for (const [k, other] of draft.nodeDBs) {
if (k === id) {
for (const [key, oldDB] of draft.nodeDBs) {
if (key === id) {
continue;
}
if (other.myNodeNum === nodeNum) {
mergeIntoCurrent(current, other); // current wins on conflicts
draft.nodeDBs.delete(k);
if (oldDB.myNodeNum === nodeNum) {
// The new DB is typically empty when nodenum is set, so we can safely copy over from the old DB
// otherwise, discard the old DB completely
if (newDB.nodeMap.size === 0) {
newDB.nodeMap = oldDB.nodeMap;
newDB.nodeErrors = oldDB.nodeErrors;
} else {
console.error(
`NodeDB with id: ${id} already has nodes, not merging with old DB`,
);
}
draft.nodeDBs.delete(key);
}
}
}),
@ -384,7 +387,12 @@ export const useNodeDBStore = createStore<PrivateNodeDBState>()(
if (!state) {
return;
}
console.debug("NodeDBStore: Rehydrating state", state);
console.debug(
"NodeDBStore: Rehydrating state with ",
state.nodeDBs.size,
" nodeDBs -",
state.nodeDBs,
);
useNodeDBStore.setState(
produce<PrivateNodeDBState>((draft) => {
@ -392,15 +400,18 @@ export const useNodeDBStore = createStore<PrivateNodeDBState>()(
for (const [id, data] of (
draft.nodeDBs as unknown as Map<number, NodeDBData>
).entries()) {
rebuilt.set(
id,
nodeDBFactory(
if (data.myNodeNum !== undefined) {
// Only rebuild if there is a nodenum set otherwise orphan dbs will acumulate
rebuilt.set(
id,
useNodeDBStore.getState,
useNodeDBStore.setState,
data,
),
);
nodeDBFactory(
id,
useNodeDBStore.getState,
useNodeDBStore.setState,
data,
),
);
}
}
draft.nodeDBs = rebuilt;
}),

8
packages/web/src/core/stores/nodeDBStore/types.ts

@ -1,6 +1,10 @@
import type { Protobuf } from "@meshtastic/core";
type NodeErrorType = Protobuf.Mesh.Routing_Error | "MISMATCH_PKI";
type NodeError = {
node: number;
error: string;
error: NodeErrorType;
};
type ProcessPacketParams = {
@ -9,4 +13,4 @@ type ProcessPacketParams = {
time: number;
};
export type { NodeError, ProcessPacketParams };
export type { NodeError, ProcessPacketParams, NodeErrorType };

105
packages/web/src/core/stores/utils/indexDB.ts

@ -17,40 +17,91 @@ export const zustandIndexDBStorage: StateStorage = {
},
};
type SerializedMap<K = unknown, V = unknown> = {
__dataType: "Map";
value: Array<[K, V]>;
type AnyRecord = Record<string, unknown>;
type Envelope<Tag extends string = string, V = unknown> = {
__datatype: Tag;
value: V;
};
type JsonReplacer = (key: string, value: unknown) => unknown;
const replacer: JsonReplacer = (_, value) => {
if (value instanceof Map) {
const map = value as Map<unknown, unknown>;
const serialized: SerializedMap = {
__dataType: "Map",
value: Array.from(map.entries()),
};
return serialized;
}
return value;
interface Handler<T, Tag extends string, Encoded> {
tag: Tag;
test(val: unknown): val is T;
serialize(val: T): Encoded;
revive(encoded: Encoded): T;
}
function isObject(x: unknown): x is AnyRecord {
return typeof x === "object" && x !== null;
}
function isEnvelope(x: unknown): x is Envelope {
return isObject(x) && typeof x.__datatype === "string" && "value" in x;
}
// Map handler
type SerializedMap<K = unknown, V = unknown> = Array<[K, V]>;
const mapHandler: Handler<Map<unknown, unknown>, "Map", SerializedMap> = {
tag: "Map",
test: (val): val is Map<unknown, unknown> => val instanceof Map,
serialize: (map) => Array.from(map.entries()),
revive: (pairs) => new Map(pairs),
};
type JsonReviver = (key: string, value: unknown) => unknown;
function isSerializedMap(value: unknown): value is SerializedMap {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
// Uint8Array handler
const uint8ArrayHandler: Handler<Uint8Array, "Uint8Array", number[]> = {
tag: "Uint8Array",
test: (val): val is Uint8Array => val instanceof Uint8Array,
serialize: (uint8array) => Array.from(uint8array),
revive: (arr) => new Uint8Array(arr),
};
const defaultHandlers = [mapHandler, uint8ArrayHandler] as const;
function makeJson<H extends Handler<unknown, string, unknown>>(
handlers: readonly H[],
) {
const byTag = new Map<H["tag"], H>();
for (const handler of handlers) {
byTag.set(handler.tag, handler);
}
const potentialMap = value as Partial<SerializedMap>;
return potentialMap.__dataType === "Map" && Array.isArray(potentialMap.value);
const replacer = (_: string, value: unknown): unknown => {
for (const handler of handlers) {
if (handler.test(value)) {
const encoded = handler.serialize(value as never);
const envelope: Envelope<typeof handler.tag, typeof encoded> = {
__datatype: handler.tag,
value: encoded,
};
return envelope;
}
}
return value;
};
const reviver = (_: string, value: unknown): unknown => {
if (isEnvelope(value)) {
const handler = byTag.get(value.__datatype);
if (handler) {
return handler.revive(
(value as Envelope<H["tag"], unknown>).value as never,
);
}
}
return value;
};
return { replacer, reviver };
}
const reviver: JsonReviver = (_, value) => {
if (isSerializedMap(value)) {
return new Map(value.value);
}
return value;
};
export function createStorageWithMapSupport<T>(): PersistStorage<T> {
export function createStorageWithMapSupport<
T,
H extends Handler<unknown, string, unknown> = never,
>(extraHandlers: readonly H[] = [] as const): PersistStorage<T> {
const { replacer, reviver } = makeJson([
...defaultHandlers,
...extraHandlers,
]);
return {
getItem: async (name): Promise<StorageValue<T> | null> => {
const str = await zustandIndexDBStorage.getItem(name);

27
packages/web/src/core/subscriptions.ts

@ -7,6 +7,7 @@ import {
type NodeDB,
} from "@core/stores";
import { type MeshDevice, Protobuf } from "@meshtastic/core";
import { fromByteArray } from "base64-js";
export const subscribeAll = (
device: Device,
@ -72,6 +73,22 @@ export const subscribeAll = (
connection.events.onNodeInfoPacket.subscribe((nodeInfo) => {
const nodeWithUser = ensureDefaultUser(nodeInfo);
if (
nodeWithUser.num !== myNodeNum && // Ignore my own node
nodeDB.getNode(nodeWithUser.num)?.user?.publicKey !== undefined && // Only flag if the old key is not undefined
fromByteArray(
nodeDB.getNode(nodeWithUser.num)?.user?.publicKey ?? new Uint8Array(), // ... and equal to the new key
) !== fromByteArray(nodeWithUser.user?.publicKey ?? new Uint8Array())
) {
console.warn(
`Node ${nodeWithUser.num} has a different public key than expected`,
);
nodeDB.setNodeError(nodeWithUser.num, "MISMATCH_PKI");
// TODO: Handle this error case properly (refactor PKI dialog?)
}
nodeDB.addNode(nodeWithUser);
});
@ -131,21 +148,21 @@ export const subscribeAll = (
if (routingPacket.data.variant.case === "errorReason") {
switch (routingPacket.data.variant.value) {
case Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT:
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
console.error(`Routing Error: $routingPacket.data.variant.value`);
break;
case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
console.error(`Routing Error: $routingPacket.data.variant.value`);
nodeDB.setNodeError(
routingPacket.from,
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value],
routingPacket?.data?.variant?.value,
);
device.setDialogOpen("refreshKeys", true);
break;
case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY:
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
console.error(`Routing Error: $routingPacket.data.variant.value`);
nodeDB.setNodeError(
routingPacket.from,
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value],
routingPacket?.data?.variant?.value,
);
device.setDialogOpen("refreshKeys", true);
break;

Loading…
Cancel
Save