Properly implemented the connection history logger

master
WolverinDEV 2021-01-08 23:51:26 +01:00
parent 1e2c20641b
commit d2503aa4e6
6 changed files with 486 additions and 38 deletions

View File

@ -40,6 +40,7 @@ import {SelectedClientInfo} from "./SelectedClientInfo";
import {SideBarManager} from "tc-shared/SideBarManager";
import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog";
import {PlaylistManager} from "tc-shared/music/PlaylistManager";
import {connectionHistory} from "tc-shared/connectionlog/History";
export enum InputHardwareState {
MISSING,
@ -138,6 +139,7 @@ export class ConnectionHandler {
connection_state: ConnectionState = ConnectionState.UNCONNECTED;
serverConnection: AbstractServerConnection;
currentConnectId: number; /* Id used for the connection history */
fileManager: FileManager;
@ -263,7 +265,7 @@ export class ConnectionHandler {
this.autoReconnectAttempt = parameters.auto_reconnect_attempt || false;
this.handleDisconnect(DisconnectReason.REQUESTED);
let server_address: ServerAddress = {
let resolvedAddress: ServerAddress = {
host: "",
port: -1
};
@ -271,22 +273,22 @@ export class ConnectionHandler {
let _v6_end = addr.indexOf(']');
let idx = addr.lastIndexOf(':');
if(idx != -1 && idx > _v6_end) {
server_address.port = parseInt(addr.substr(idx + 1));
server_address.host = addr.substr(0, idx);
resolvedAddress.port = parseInt(addr.substr(idx + 1));
resolvedAddress.host = addr.substr(0, idx);
} else {
server_address.host = addr;
server_address.port = 9987;
resolvedAddress.host = addr;
resolvedAddress.port = 9987;
}
}
logInfo(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port);
logInfo(LogCategory.CLIENT, tr("Start connection to %s:%d"), resolvedAddress.host, resolvedAddress.port);
this.log.log("connection.begin", {
address: {
server_hostname: server_address.host,
server_port: server_address.port
server_hostname: resolvedAddress.host,
server_port: resolvedAddress.port
},
client_nickname: parameters.nickname
});
this.channelTree.initialiseHead(addr, server_address);
this.channelTree.initialiseHead(addr, resolvedAddress);
if(parameters.password && !parameters.password.hashed){
try {
@ -302,28 +304,28 @@ export class ConnectionHandler {
}
if(parameters.password) {
connection_log.update_address_password({
hostname: server_address.host,
port: server_address.port
hostname: resolvedAddress.host,
port: resolvedAddress.port
}, parameters.password.password);
}
const original_address = {host: server_address.host, port: server_address.port};
if(server_address.host === "localhost") {
server_address.host = "127.0.0.1";
} else if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
const originalAddress = {host: resolvedAddress.host, port: resolvedAddress.port};
if(resolvedAddress.host === "localhost") {
resolvedAddress.host = "127.0.0.1";
} else if(dns.supported() && !resolvedAddress.host.match(Regex.IP_V4) && !resolvedAddress.host.match(Regex.IP_V6)) {
const id = ++this.connectAttemptId;
this.log.log("connection.hostname.resolve", {});
try {
const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any;
const resolved = await dns.resolve_address(resolvedAddress, { timeout: 5000 }) || {} as any;
if(id != this.connectAttemptId)
return; /* we're old */
server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host;
server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port;
resolvedAddress.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : resolvedAddress.host;
resolvedAddress.port = typeof(resolved.target_port) === "number" ? resolved.target_port : resolvedAddress.port;
this.log.log("connection.hostname.resolved", {
address: {
server_port: server_address.port,
server_hostname: server_address.host
server_port: resolvedAddress.port,
server_hostname: resolvedAddress.host
}
});
} catch(error) {
@ -334,14 +336,22 @@ export class ConnectionHandler {
}
}
await this.serverConnection.connect(server_address, new HandshakeHandler(profile, parameters));
if(user_action) {
this.currentConnectId = await connectionHistory.logConnectionAttempt(originalAddress.host, originalAddress.port);
} else {
this.currentConnectId = -1;
}
await this.serverConnection.connect(resolvedAddress, new HandshakeHandler(profile, parameters));
setTimeout(() => {
const connected = this.serverConnection.connected();
if(user_action && connected) {
connection_log.log_connect({
hostname: original_address.host,
port: original_address.port
hostname: originalAddress.host,
port: originalAddress.port
});
/* TODO: Log successful connect/update the server unique id: attemptId */
}
}, 50);
}
@ -491,6 +501,7 @@ export class ConnectionHandler {
private _certificate_modal: Modal;
handleDisconnect(type: DisconnectReason, data: any = {}) {
this.connectAttemptId++;
this.currentConnectId = -1;
let auto_reconnect = false;
switch (type) {

View File

@ -0,0 +1,435 @@
import {LogCategory, logError, logWarn} from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize";
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {server_connections} from "tc-shared/ConnectionManager";
import {ServerProperties} from "tc-shared/tree/Server";
const kUnknownServerUniqueId = "unknown";
export type ConnectionHistoryEntry = {
id: number,
timestamp: number,
targetHost: string,
targetPort: number,
serverUniqueId: string | typeof kUnknownServerUniqueId
};
export type ConnectionHistoryServerEntry = {
firstConnectTimestamp: number,
firstConnectId: number,
lastConnectTimestamp: number,
lastConnectId: number,
}
export type ConnectionHistoryServerInfo = {
name: string,
iconId: number,
/* These properties are only available upon server variable retrieval */
clientsOnline: number | -1,
clientsMax: number | -1,
passwordProtected: boolean
}
export class ConnectionHistory {
private database: IDBDatabase;
constructor() { }
async initializeDatabase() {
const openRequest = indexedDB.open("connection-log", 1);
openRequest.onupgradeneeded = event => {
const database = openRequest.result;
switch (event.oldVersion) {
case 0:
if(!database.objectStoreNames.contains("attempt-history")) {
const store = database.createObjectStore("attempt-history", { keyPath: "id", autoIncrement: true });
store.createIndex("timestamp", "timestamp", { unique: false });
store.createIndex("targetHost", "targetHost", { unique: false });
store.createIndex("targetPort", "targetPort", { unique: false });
store.createIndex("serverUniqueId", "serverUniqueId", { unique: false });
}
if(!database.objectStoreNames.contains("server-info")) {
const store = database.createObjectStore("server-info", { keyPath: "uniqueId" });
store.createIndex("firstConnectTimestamp", "firstConnectTimestamp", { unique: false });
store.createIndex("firstConnectId", "firstConnectId", { unique: false });
store.createIndex("lastConnectTimestamp", "lastConnectTimestamp", { unique: false });
store.createIndex("lastConnectId", "lastConnectId", { unique: false });
store.createIndex("name", "name", { unique: false });
store.createIndex("iconId", "iconId", { unique: false });
store.createIndex("clientsOnline", "clientsOnline", { unique: false });
store.createIndex("clientsMax", "clientsMax", { unique: false });
store.createIndex("passwordProtected", "passwordProtected", { unique: false });
}
/* fall through wanted */
case 1:
break;
default:
throw tra("connection log database has an invalid version: {}", event.oldVersion);
}
};
this.database = await new Promise<IDBDatabase>((resolve, reject) => {
openRequest.onblocked = () => reject(tr("Failed to open the connection log database"));
openRequest.onerror = () => {
logError(LogCategory.GENERAL, tr("Failed to open the client connection log database: %o"), openRequest.error);
reject(openRequest.error.message);
};
openRequest.onsuccess = () => resolve(openRequest.result);
});
}
/**
* Register a new connection attempt.
* @param targetHost
* @param targetPort
* @return Returns a unique connect attempt identifier id which could be later used to set the unique server id.
*/
async logConnectionAttempt(targetHost: string, targetPort: number) : Promise<number> {
if(!this.database) {
return;
}
const transaction = this.database.transaction(["attempt-history"], "readwrite");
const store = transaction.objectStore("attempt-history");
const id = await new Promise<IDBValidKey>((resolve, reject) => {
const insert = store.put({
timestamp: Date.now(),
targetHost: targetHost,
targetPort: targetPort,
serverUniqueId: kUnknownServerUniqueId
});
insert.onsuccess = () => resolve(insert.result);
insert.onerror = () => reject(insert.error);
});
if(typeof id !== "number") {
logError(LogCategory.GENERAL, tr("Received invalid idb key type which isn't a number: %o"), id);
throw tr("invalid idb key returned");
}
return id;
}
private async resolveDatabaseServerInfo(serverUniqueId: string) : Promise<IDBCursorWithValue | null> {
const transaction = this.database.transaction(["server-info"], "readwrite");
const store = transaction.objectStore("server-info");
return await new Promise<IDBCursorWithValue | null>((resolve, reject) => {
const cursor = store.openCursor(serverUniqueId);
cursor.onsuccess = () => resolve(cursor.result);
cursor.onerror = () => reject(cursor.error);
});
}
private async updateDatabaseServerInfo(serverUniqueId: string, updateCallback: (databaseValue) => void) {
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
if(entry) {
const newValue = Object.assign({}, entry.value);
updateCallback(newValue);
await new Promise((resolve, reject) => {
const update = entry.update(newValue);
update.onsuccess = resolve;
update.onerror = () => reject(update.error);
});
} else {
const transaction = this.database.transaction(["server-info"], "readwrite");
const store = transaction.objectStore("server-info");
const value = {
uniqueId: serverUniqueId,
firstConnectTimestamp: 0,
firstConnectId: -1,
lastConnectTimestamp: 0,
lastConnectId: -1,
name: tr("unknown"),
iconId: 0,
clientsOnline: -1,
clientsMax: -1,
passwordProtected: false
};
updateCallback(value);
await new Promise((resolve, reject) => {
const insert = store.put(value);
insert.onsuccess = resolve;
insert.onerror = () => reject(insert.error);
});
}
}
/**
* Update the connection attempts target server id.
* @param connectionAttemptId
* @param serverUniqueId
*/
async updateConnectionServerUniqueId(connectionAttemptId: number, serverUniqueId: string) {
if(!this.database) {
return;
}
const transaction = this.database.transaction(["attempt-history"], "readwrite");
const store = transaction.objectStore("attempt-history");
let connectAttemptInfo;
{
const entry = await new Promise<IDBCursorWithValue | null>((resolve, reject) => {
const cursor = store.openCursor(connectionAttemptId);
cursor.onsuccess = () => resolve(cursor.result);
cursor.onerror = () => reject(cursor.error);
});
if(!entry) {
throw tr("missing connection attempt");
}
if(entry.value.serverUniqueId === serverUniqueId) {
logWarn(LogCategory.GENERAL, tr("updateConnectionServerUniqueId(...) has been called twice"));
return;
} else if(entry.value.serverUniqueId !== kUnknownServerUniqueId) {
throw tr("connection attempt has already a server unique id set");
}
const newValue = connectAttemptInfo = Object.assign({}, entry.value);
newValue.serverUniqueId = serverUniqueId;
await new Promise((resolve, reject) => {
const update = entry.update(newValue);
update.onsuccess = resolve;
update.onerror = () => reject(update.error);
});
}
await this.updateDatabaseServerInfo(serverUniqueId, databaseValue => {
if(databaseValue.firstConnectTimestamp === 0) {
databaseValue.firstConnectTimestamp = connectAttemptInfo.timestamp;
databaseValue.firstConnectId = connectAttemptInfo.id;
}
databaseValue.lastConnectTimestamp = connectAttemptInfo.timestamp;
databaseValue.lastConnectId = connectAttemptInfo.id;
});
}
/**
* Update the server info of the given server.
* @param serverUniqueId
* @param info
*/
async updateServerInfo(serverUniqueId: string, info: ConnectionHistoryServerInfo) {
if(!this.database) {
return;
}
await this.updateDatabaseServerInfo(serverUniqueId, databaseValue => {
databaseValue.name = info.name;
databaseValue.iconId = info.iconId;
databaseValue.clientsOnline = info.clientsOnline;
databaseValue.clientsMax = info.clientsMax;
});
}
/**
* Query the server info of a given server unique id
* @param serverUniqueId
*/
async queryServerInfo(serverUniqueId: string) : Promise<(ConnectionHistoryServerInfo & ConnectionHistoryServerEntry) | undefined> {
if(!this.database) {
return undefined;
}
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
if(!entry) {
return;
}
const value = entry.value;
return {
firstConnectId: value.firstConnectId,
firstConnectTimestamp: value.firstConnectTimestamp,
lastConnectId: value.lastConnectId,
lastConnectTimestamp: value.lastConnectTimestamp,
name: value.name,
iconId: value.iconId,
clientsOnline: value.clientsOnline,
clientsMax: value.clientsMax,
passwordProtected: value.passwordProtected
};
}
/**
* Query the last connected addresses/servers.
* @param maxUniqueServers
*/
async lastConnectedServers(maxUniqueServers: number) : Promise<ConnectionHistoryEntry[]> {
if(!this.database) {
return [];
}
const result: ConnectionHistoryEntry[] = [];
const transaction = this.database.transaction(["attempt-history"], "readwrite");
const store = transaction.objectStore("attempt-history");
const cursor = store.index("timestamp").openCursor(undefined, "prev");
while(result.length < maxUniqueServers) {
const entry = await new Promise<IDBCursorWithValue | null>((resolve, reject) => {
cursor.onsuccess = () => resolve(cursor.result);
cursor.onerror = () => reject(cursor.error);
});
if(!entry) {
break;
}
const parsedEntry = {
id: entry.value.id,
timestamp: entry.value.timestamp,
targetHost: entry.value.targetHost,
targetPort: entry.value.targetPort,
serverUniqueId: entry.value.serverUniqueId,
} as ConnectionHistoryEntry;
entry.continue();
if(parsedEntry.serverUniqueId !== kUnknownServerUniqueId) {
if(result.findIndex(entry => entry.serverUniqueId === parsedEntry.serverUniqueId) !== -1) {
continue;
}
} else {
if(result.findIndex(entry => {
return entry.targetHost === parsedEntry.targetHost && entry.targetPort === parsedEntry.targetPort;
}) !== -1) {
continue;
}
}
result.push(parsedEntry);
}
return result;
}
}
const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
"virtualserver_icon_id",
"virtualserver_name",
"virtualserver_flag_password",
"virtualserver_maxclients",
"virtualserver_clientsonline",
"virtualserver_flag_password"
];
class ConnectionHistoryUpdateListener {
private readonly history: ConnectionHistory;
private listenerHandlerManager: (() => void)[];
private listenerConnectionHandler: {[key: string]: (() => void)[]} = {};
constructor(history: ConnectionHistory) {
this.history = history;
this.listenerHandlerManager = [];
this.listenerHandlerManager.push(server_connections.events().on("notify_handler_created", event => {
this.registerConnectionHandler(event.handler);
}));
this.listenerHandlerManager.push(server_connections.events().on("notify_handler_deleted", event => {
this.listenerConnectionHandler[event.handler.handlerId]?.forEach(callback => callback());
delete this.listenerConnectionHandler[event.handler.handlerId];
}));
}
destroy() {
this.listenerHandlerManager.forEach(callback => callback());
Object.values(this.listenerConnectionHandler).forEach(callbacks => callbacks.forEach(callback => callback()));
this.listenerConnectionHandler = {};
}
private registerConnectionHandler(handler: ConnectionHandler) {
handler.channelTree.server.events.on("notify_properties_updated", event => {
if("virtualserver_unique_identifier" in event.updated_properties) {
if(handler.currentConnectId > 0) {
this.history.updateConnectionServerUniqueId(handler.currentConnectId, event.server_properties.virtualserver_unique_identifier)
.catch(error => {
logError(LogCategory.GENERAL, tr("Failed to update connect server unique id: %o"), error);
})
}
}
for(const key of kConnectServerInfoUpdatePropertyKeys) {
if(key in event.updated_properties) {
this.history.updateServerInfo(event.server_properties.virtualserver_unique_identifier, {
name: event.server_properties.virtualserver_name,
iconId: event.server_properties.virtualserver_icon_id,
clientsMax: event.server_properties.virtualserver_maxclients,
clientsOnline: event.server_properties.virtualserver_clientsonline,
passwordProtected: event.server_properties.virtualserver_flag_password
}).catch(error => {
logError(LogCategory.GENERAL, tr("Failed to update connect server info: %o"), error);
});
break;
}
}
});
}
}
export let connectionHistory: ConnectionHistory;
let historyInfoListener: ConnectionHistoryUpdateListener;
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
priority: 0,
name: "Chat history setup",
function: async () => {
if(!('indexedDB' in window)) {
loader.critical_error(tr("Missing Indexed DB support"));
throw tr("Missing Indexed DB support");
}
connectionHistory = new ConnectionHistory();
try {
await connectionHistory.initializeDatabase();
} catch (error) {
logError(LogCategory.GENERAL, tr("Failed to initialize connection history database: %o"), error);
logError(LogCategory.GENERAL, tr("Do not saving the connection attempts."));
return;
}
historyInfoListener = new ConnectionHistoryUpdateListener(connectionHistory);
(window as any).connectionHistory = connectionHistory;
}
});

View File

@ -206,8 +206,9 @@ async function doOpenDatabase(forceUpgrade: boolean) {
function doInitializeUser(uniqueId: string, database: IDBDatabase) {
const storeId = clientUniqueId2StoreName(uniqueId);
if(database.objectStoreNames.contains(storeId))
if(database.objectStoreNames.contains(storeId)) {
return;
}
const store = database.createObjectStore(storeId, { keyPath: "databaseId", autoIncrement: true });

View File

@ -124,7 +124,7 @@ export interface ServerAddress {
export interface ServerEvents extends ChannelTreeEntryEvents {
notify_properties_updated: {
updated_properties: {[Key in keyof ServerProperties]: ServerProperties[Key]};
updated_properties: Partial<ServerProperties>;
server_properties: ServerProperties
}
}
@ -267,24 +267,26 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Server update properties", entries);
}
let updatedProperties: Partial<ServerProperties> = {};
let update_bookmarks = false;
for(let variable of variables) {
JSON.map_field_to(this.properties, variable.value, variable.key);
if(!JSON.map_field_to(this.properties, variable.value, variable.key)) {
/* The value has not been updated */
continue;
}
updatedProperties[variable.key] = variable.value;
if(variable.key == "virtualserver_icon_id") {
/* For more detail lookup client::updateVariables and client_icon_id!
* ATTENTION: This is required!
*/
this.properties.virtualserver_icon_id = variable.value as any >>> 0;
update_bookmarks = true;
}
}
{
let properties = {};
for(const property of variables)
properties[property.key] = this.properties[property.key];
this.events.fire("notify_properties_updated", { updated_properties: properties as any, server_properties: this.properties });
}
this.events.fire("notify_properties_updated", {
updated_properties: updatedProperties,
server_properties: this.properties
});
if(update_bookmarks) {
const bmarks = bookmarks.bookmarks_flat()
.filter(e => e.server_properties.server_address === this.remote_address.host && e.server_properties.server_port == this.remote_address.port)

View File

@ -4,7 +4,6 @@ import {LogCategory} from "../../log";
import * as loader from "tc-loader";
import {createModal} from "../../ui/elements/Modal";
import {ConnectionProfile, defaultConnectProfile, findConnectProfile, availableConnectProfiles} from "../../profiles/ConnectionProfile";
import {KeyCode} from "../../PPTListener";
import * as i18nc from "../../i18n/country";
import {spawnSettingsModal} from "../../ui/modal/ModalSettings";
import {server_connections} from "tc-shared/ConnectionManager";