BACKEND DONE FOR ALL APPS
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { SocketCommunicatorBase } from "./socket_communicator/socket_communicator_base";
|
||||
|
||||
interface Connection {
|
||||
communicator: SocketCommunicatorBase;
|
||||
}
|
||||
|
||||
export class ConnectionManager {
|
||||
private readonly connections: { [key: string]: Connection };
|
||||
|
||||
constructor() {
|
||||
this.connections = {};
|
||||
}
|
||||
|
||||
// Adds a new communicator, keyed by both IP and port
|
||||
addConnection(ip: string, port: number, communicator: SocketCommunicatorBase): void {
|
||||
const key = `${ip}:${port}`;
|
||||
|
||||
// Store the communicator along with the client's public and private keys
|
||||
this.connections[key] = {
|
||||
communicator
|
||||
};
|
||||
|
||||
console.log(`Added communicator for ${ip}:${port}, generated public and private keys.`);
|
||||
}
|
||||
|
||||
// Removes a communicator based on IP and port
|
||||
removeCommunicator(ip: string, port: number): void {
|
||||
const key = `${ip}:${port}`;
|
||||
if (this.connections[key]) {
|
||||
delete this.connections[key];
|
||||
console.log(`Removed communicator for ${ip}:${port}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieves a communicator based on IP and port
|
||||
getCommunicator(ip: string, port: number): SocketCommunicatorBase | null {
|
||||
const key = `${ip}:${port}`;
|
||||
return this.connections[key] ? this.connections[key].communicator : null;
|
||||
}
|
||||
|
||||
// Checks if a communicator exists for a given IP and port
|
||||
communicatorExists(ip: string, port: number): boolean {
|
||||
const key = `${ip}:${port}`;
|
||||
return this.connections[key] !== undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
export interface ParsedMessage {
|
||||
operationCode: string;
|
||||
metaInfo?: { [key: string]: any };
|
||||
fileContent?: Buffer;
|
||||
}
|
||||
|
||||
export class MessageHandler {
|
||||
// Format the message with operationCode, guid, metaInfo, and fileContent (Base64 for fileContent)
|
||||
static formatMessage(
|
||||
operationCode: string,
|
||||
metaInfo?: { [key: string]: any },
|
||||
fileContent?: Buffer
|
||||
): string {
|
||||
let message = `${operationCode}\n`; // First part: operationCode and guid
|
||||
|
||||
if (metaInfo && Object.keys(metaInfo).length > 0) {
|
||||
message += `${JSON.stringify(metaInfo)}\n`; // Add metaInfo
|
||||
}
|
||||
|
||||
if (fileContent && fileContent.length > 0) {
|
||||
message += fileContent.toString('base64'); // Convert buffer to Base64 for fileContent
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// Parse the incoming message (convert Base64 back to Buffer if fileContent is present)
|
||||
static parseMessage(msg: string): ParsedMessage {
|
||||
const parts = msg.split('\n'); // Split by \n (operationCode, metaInfo, and fileContent are on separate lines)
|
||||
|
||||
// First part should always be the operation code
|
||||
const operationCode = parts[0]?.trim();
|
||||
if (!operationCode) {
|
||||
throw new Error('Missing operation code in the message');
|
||||
}
|
||||
|
||||
let metaInfo: { [key: string]: any } | undefined = undefined;
|
||||
let fileContent: Buffer | undefined = undefined;
|
||||
|
||||
// Parse the metaInfo (JSON object) if present
|
||||
if (parts[1]) {
|
||||
try {
|
||||
metaInfo = JSON.parse(parts[1].trim());
|
||||
} catch (err) {
|
||||
console.error('Invalid metaInfo JSON format:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Base64 string back to Buffer for fileContent if present
|
||||
if (parts[2]) {
|
||||
fileContent = Buffer.from(parts[2].trim(), 'base64');
|
||||
}
|
||||
|
||||
return {
|
||||
operationCode,
|
||||
metaInfo,
|
||||
fileContent,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate if the parsed message contains an operation code
|
||||
static validateMessage(parsedMessage: ParsedMessage | null): boolean {
|
||||
return !!parsedMessage?.operationCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { UdpClient} from './udp/udp_client';
|
||||
export { TcpClient } from './tcp/tcp_client'
|
||||
@@ -0,0 +1,37 @@
|
||||
export let operationCodes = {
|
||||
HEARTBEAT: 'HEARTBEAT',
|
||||
ALIVE: 'ALIVE',
|
||||
SET_PUBLIC_KEY: 'SET_PUBLIC_KEY',
|
||||
SET_AES_KEY: 'SET_AES_KEY',
|
||||
RESET_DATABASE: 'RESET_DATABASE',
|
||||
OK: 'OK',
|
||||
ERR: 'ERR',
|
||||
END: 'END',
|
||||
UNKNOWN_COMMAND: 'UNKNOWN_COMMAND',
|
||||
|
||||
LOGIN: 'LOGIN',
|
||||
SIGN_UP: 'SIGN_UP',
|
||||
RESET_PASSWORD: 'RESET_PASSWORD',
|
||||
|
||||
GET_DEPARTMENTS: 'GET_DEPARTMENTS',
|
||||
CREATE_DEPARTMENT: 'CREATE_DEPARTMENT',
|
||||
MODIFY_DEPARTMENT: 'MODIFY_DEPARTMENT',
|
||||
DELETE_DEPARTMENT: 'DELETE_DEPARTMENT',
|
||||
|
||||
GET_USERS: 'GET_USERS',
|
||||
DELETE_USER: 'DELETE_USER',
|
||||
FIND_KEY_BY_USER_ID: 'FIND_KEY_BY_USER_ID',
|
||||
FIND_BY_EMAIL: 'FIND_BY_EMAIL',
|
||||
MODIFY_USER: 'MODIFY_USER',
|
||||
|
||||
GET_USER_INFORMATION: 'GET_USER_INFORMATION',
|
||||
CLEAR_BACKUP: 'CLEAR_BACKUP',
|
||||
BACKUP_FILE: 'BACKUP_FILE',
|
||||
SHARE_FILE: 'SHARE_FILE',
|
||||
CLEAR_DEPARTMENT: 'CLEAR_DEPARTMENT',
|
||||
DEPARTMENT_FILE: 'DEPARTMENT_FILE',
|
||||
IS_BACKUP_CREATED: 'IS_BACKUP_CREATED',
|
||||
GET_BACKUP_STRUCTURE: 'GET_BACKUP_STRUCTURE',
|
||||
REQ_FILE_FROM_BACKUP: 'REQ_FILE_FROM_BACKUP',
|
||||
DECRYPT_AND_SAVE_OLD_BACKUP_FILE: 'DECRYPT_AND_SAVE_OLD_BACKUP_FILE',
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ParsedMessage } from '../message_handler';
|
||||
import { OperationHandler } from './operation_handler';
|
||||
import { OperationPlugin } from './operation_plugin';
|
||||
|
||||
export abstract class OperationBase implements OperationPlugin {
|
||||
// Shared operation codes
|
||||
public static readonly operationCodes = {
|
||||
OK: 'OK',
|
||||
ERR: 'ERR',
|
||||
END: 'END',
|
||||
};
|
||||
|
||||
// Default handler for OK operation
|
||||
public static handleOk(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
console.log('OK operation received');
|
||||
return parsedMessage; // Typically, you would just acknowledge with OK, returning as-is
|
||||
}
|
||||
|
||||
// Default handler for ERR operation
|
||||
public static handleErr(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
console.log('ERR operation received: ', parsedMessage.metaInfo?.message || 'No error details provided');
|
||||
return parsedMessage; // Typically, you would log the error and return
|
||||
}
|
||||
|
||||
// Default handler for END operation
|
||||
public static handleEnd(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
console.log('END operation received');
|
||||
return {
|
||||
operationCode: OperationBase.operationCodes.END,
|
||||
metaInfo: { message: 'Connection ended.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Register the common OK, ERR, and END handlers
|
||||
public static registerCommonOperations(operationHandler: OperationHandler): void {
|
||||
operationHandler.registerHandler(OperationBase.operationCodes.OK, OperationBase.handleOk);
|
||||
operationHandler.registerHandler(OperationBase.operationCodes.ERR, OperationBase.handleErr);
|
||||
operationHandler.registerHandler(OperationBase.operationCodes.END, OperationBase.handleEnd);
|
||||
}
|
||||
|
||||
// Abstract register method that will be implemented by subclasses
|
||||
public abstract register(operationHandler: OperationHandler): void;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// operation_handler.ts
|
||||
import { ParsedMessage, MessageHandler } from '../message_handler';
|
||||
import { OperationPlugin } from './operation_plugin';
|
||||
|
||||
type OperationHandlerFunction = (parsedMessage: ParsedMessage) => ParsedMessage;
|
||||
|
||||
export class OperationHandler {
|
||||
private static instance: OperationHandler;
|
||||
private handlers: { [operationCode: string]: OperationHandlerFunction } = {};
|
||||
|
||||
private constructor() {
|
||||
// Register only the unknown command handler on initialization
|
||||
this.registerHandler('UNKNOWN_COMMAND', this.handleUnknownCommand);
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
public static getInstance(): OperationHandler {
|
||||
if (!OperationHandler.instance) {
|
||||
OperationHandler.instance = new OperationHandler();
|
||||
}
|
||||
return OperationHandler.instance;
|
||||
}
|
||||
|
||||
// Register a handler for a specific operation code
|
||||
public registerHandler(operationCode: string, handler: OperationHandlerFunction): void {
|
||||
this.handlers[operationCode] = handler;
|
||||
}
|
||||
|
||||
// Handle operation request
|
||||
public handleOperation(rawMessage: string): ParsedMessage {
|
||||
// Parse and validate the message
|
||||
const parsedMessage = MessageHandler.parseMessage(rawMessage);
|
||||
if (!parsedMessage || !MessageHandler.validateMessage(parsedMessage)) {
|
||||
return this.handleUnknownCommand(parsedMessage);
|
||||
}
|
||||
|
||||
// Dispatch the handler for the given operation code
|
||||
const handler = this.handlers[parsedMessage.operationCode];
|
||||
if (handler) {
|
||||
return handler(parsedMessage);
|
||||
} else {
|
||||
return this.handleUnknownCommand(parsedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Default handler for unknown commands
|
||||
private handleUnknownCommand(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
return {
|
||||
operationCode: 'UNKNOWN_COMMAND',
|
||||
metaInfo: { message: 'Unknown command received.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Plugin system: Load plugins to register handlers
|
||||
public loadPlugin(plugin: OperationPlugin): void {
|
||||
plugin.register(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// operation_plugin.ts
|
||||
import { OperationHandler } from './operation_handler';
|
||||
|
||||
export interface OperationPlugin {
|
||||
register(operationHandler: OperationHandler): void;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ParsedMessage } from '../message_handler';
|
||||
import { OperationBase } from '../operations_base/operation_base';
|
||||
import { OperationHandler } from '../operations_base/operation_handler';
|
||||
import os from 'node:os';
|
||||
|
||||
export class GeneralOperations extends OperationBase {
|
||||
public static readonly operationCodes = {
|
||||
...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END)
|
||||
HEARTBEAT: 'HEARTBEAT',
|
||||
ALIVE: 'ALIVE',
|
||||
SET_PUBLIC_KEY: 'SET_PUBLIC_KEY',
|
||||
SET_AES_KEY: 'SET_AES_KEY',
|
||||
};
|
||||
|
||||
// Handle heartbeat operation
|
||||
public static handleHeartbeat(): ParsedMessage {
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
let ipAddress = 'Unknown';
|
||||
|
||||
for (const iface of Object.values(networkInterfaces)) {
|
||||
// @ts-ignore
|
||||
for (const address of iface) {
|
||||
if (address.family === 'IPv4' && !address.internal) {
|
||||
ipAddress = address.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ipAddress !== 'Unknown') break;
|
||||
}
|
||||
|
||||
return {
|
||||
operationCode: GeneralOperations.operationCodes.ALIVE,
|
||||
metaInfo: { ipAddress },
|
||||
};
|
||||
}
|
||||
|
||||
// Handle public key exchange
|
||||
public static handlePublicKey(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
const clientPublicKey = parsedMessage.metaInfo?.publicKey;
|
||||
if (clientPublicKey) {
|
||||
return {
|
||||
operationCode: GeneralOperations.operationCodes.SET_PUBLIC_KEY,
|
||||
metaInfo: { publicKey: clientPublicKey },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
operationCode: GeneralOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'No public key provided.' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle AES key exchange
|
||||
public static handleAESKey(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
const aesKey = parsedMessage.metaInfo?.aesKey;
|
||||
const aesIv = parsedMessage.metaInfo?.aesIv;
|
||||
if (aesKey && aesIv) {
|
||||
return {
|
||||
operationCode: GeneralOperations.operationCodes.SET_AES_KEY,
|
||||
metaInfo: { aesKey: aesKey, aesIv: aesIv },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
operationCode: GeneralOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'No AES key provided.' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register general operations with the OperationHandler
|
||||
public register(operationHandler: OperationHandler): void {
|
||||
// Register specific handlers for the general operations
|
||||
operationHandler.registerHandler(GeneralOperations.operationCodes.HEARTBEAT, GeneralOperations.handleHeartbeat);
|
||||
operationHandler.registerHandler(GeneralOperations.operationCodes.SET_PUBLIC_KEY, GeneralOperations.handlePublicKey);
|
||||
operationHandler.registerHandler(GeneralOperations.operationCodes.SET_AES_KEY, GeneralOperations.handleAESKey); // Register AES key handler
|
||||
|
||||
// Register common operations inherited from the base class
|
||||
OperationBase.registerCommonOperations(operationHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
import { ParsedMessage } from '../message_handler';
|
||||
import { OperationBase } from '../operations_base/operation_base';
|
||||
import { OperationHandler } from '../operations_base/operation_handler';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { FileEncryptor } from '../../helpers/file_encryptor';
|
||||
|
||||
const LOCK_FILE_EXTENSION = '.lock';
|
||||
|
||||
export class UserToUserOperations extends OperationBase {
|
||||
public static readonly operationCodes = {
|
||||
...OperationBase.operationCodes, // Inherit common operation codes (OK, ERR, END)
|
||||
GET_USER_INFORMATION: 'GET_USER_INFORMATION',
|
||||
BACKUP_FILE: 'BACKUP_FILE',
|
||||
CLEAR_BACKUP: 'CLEAR_BACKUP',
|
||||
SHARE_FILE: 'SHARE_FILE',
|
||||
CLEAR_DEPARTMENT: 'CLEAR_DEPARTMENT',
|
||||
DEPARTMENT_FILE: 'DEPARTMENT_FILE',
|
||||
IS_BACKUP_CREATED: 'IS_BACKUP_CREATED',
|
||||
GET_BACKUP_STRUCTURE: 'GET_BACKUP_STRUCTURE',
|
||||
REQ_FILE_FROM_BACKUP: 'REQ_FILE_FROM_BACKUP',
|
||||
DECRYPT_AND_SAVE_OLD_BACKUP_FILE: 'DECRYPT_AND_SAVE_OLD_BACKUP_FILE',
|
||||
};
|
||||
|
||||
// Utility function to pause execution (sleep)
|
||||
static sleep(ms: number): void {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < ms) {
|
||||
// Busy wait loop (not optimal but fine for this short duration)
|
||||
}
|
||||
}
|
||||
|
||||
// Read JSON file with a lock mechanism
|
||||
static readJsonSync(filePath: string): any {
|
||||
const lockFilePath = `${filePath}${LOCK_FILE_EXTENSION}`;
|
||||
const absolutePath = path.resolve(filePath);
|
||||
|
||||
try {
|
||||
// Loop until the lock file is removed by another process
|
||||
while (fs.existsSync(lockFilePath)) {
|
||||
console.log(`Waiting for lock file to be released: ${lockFilePath}`);
|
||||
UserToUserOperations.sleep(100);
|
||||
}
|
||||
|
||||
// Create lock file to signal this process is working on the file
|
||||
fs.writeFileSync(lockFilePath, ''); // Create the lock file
|
||||
|
||||
// Read and parse the JSON file
|
||||
const fileContents = fs.readFileSync(absolutePath, 'utf-8');
|
||||
const parsedJson = JSON.parse(fileContents);
|
||||
|
||||
// Once processing is done, delete the lock file
|
||||
fs.unlinkSync(lockFilePath); // Remove the lock file
|
||||
|
||||
return parsedJson; // Return the parsed JSON data
|
||||
} catch (error) {
|
||||
console.error(`Error reading or parsing JSON from ${filePath}:`, error);
|
||||
|
||||
// Ensure the lock file is removed even in case of an error
|
||||
if (fs.existsSync(lockFilePath)) {
|
||||
fs.unlinkSync(lockFilePath);
|
||||
}
|
||||
|
||||
return null; // Return null or throw error based on preference
|
||||
}
|
||||
}
|
||||
|
||||
// Handle user information retrieval
|
||||
public static handleGetUserInformation(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
const pathToUserJson = path.join(__dirname, '..', '..', 'json_files', 'userConfig.json');
|
||||
const userInfo = UserToUserOperations.readJsonSync(pathToUserJson);
|
||||
|
||||
if (userInfo) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: userInfo.user_info,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Error fetching user information.' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file reception and saving
|
||||
public static handleBackupFile(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
// Ensure metaInfo and fileContent are available
|
||||
if (!parsedMessage.metaInfo || !parsedMessage.fileContent) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing file content or meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const { userName, relativeFilePath } = parsedMessage.metaInfo;
|
||||
|
||||
if (!userName || !relativeFilePath) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing user name or file path information.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Base directory where backups will be stored
|
||||
const baseBackupDir = path.join(__dirname, '..', '..', 'backups');
|
||||
|
||||
// Full path where the file will be stored (under the user's directory)
|
||||
const fullFilePath = path.join(baseBackupDir, userName, relativeFilePath);
|
||||
|
||||
try {
|
||||
// Ensure the directory structure exists (create directories if they don't exist)
|
||||
const dirPath = path.dirname(fullFilePath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the file content to the correct path
|
||||
fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64');
|
||||
|
||||
console.log(`File saved successfully: ${fullFilePath}`);
|
||||
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { message: `File saved successfully: ${relativeFilePath}` },
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`Error saving file: ${error.message}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: `Error saving file: ${error.message}` },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle clearing all backups for a user
|
||||
public static handleClearBackup(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
// Ensure the userName is available in metaInfo
|
||||
if (!parsedMessage.metaInfo || !parsedMessage.metaInfo.userName) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing user name in meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const { userName } = parsedMessage.metaInfo;
|
||||
|
||||
// Base directory where backups are stored
|
||||
const baseBackupDir = path.join(__dirname, '..', '..', 'backups');
|
||||
const userBackupDir = path.join(baseBackupDir, userName);
|
||||
|
||||
try {
|
||||
// Check if the user's backup directory exists
|
||||
if (fs.existsSync(userBackupDir)) {
|
||||
// Recursively delete the user's backup directory
|
||||
fs.rmSync(userBackupDir, { recursive: true, force: true });
|
||||
console.log(`Backup cleared successfully for user: ${userName}`);
|
||||
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { message: `Backup cleared successfully for user: ${userName}` },
|
||||
};
|
||||
} else {
|
||||
console.warn(`Backup directory not found for user: ${userName}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: `Backup directory not found for user: ${userName}` },
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Error clearing backup for user: ${userName} - ${error.message}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: `Error clearing backup for user: ${userName}` },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sharing file operation
|
||||
public static handleShareFile(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
// Ensure metaInfo and fileContent are available
|
||||
if (!parsedMessage.metaInfo || !parsedMessage.fileContent) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing file content or meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const { userName, relativeFilePath } = parsedMessage.metaInfo;
|
||||
|
||||
if (!userName || !relativeFilePath) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing user name or file path information.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Path to the application.json to read the shareDirectory field
|
||||
const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json');
|
||||
const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson);
|
||||
|
||||
// Corrected the condition
|
||||
if (!appInfo || !appInfo.shareDirectory) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Error retrieving share directory from application.json.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Get the share directory path
|
||||
const shareDirectory = appInfo.shareDirectory.path;
|
||||
|
||||
// Full path where the file will be stored (under the user's directory in the shared folder)
|
||||
const fullFilePath = path.join(shareDirectory, userName, relativeFilePath);
|
||||
|
||||
try {
|
||||
// Ensure the directory structure exists (create directories if they don't exist)
|
||||
const dirPath = path.dirname(fullFilePath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the file content to the correct path
|
||||
fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64');
|
||||
|
||||
console.log(`File shared successfully: ${fullFilePath}`);
|
||||
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { message: `File shared successfully: ${relativeFilePath}` },
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`Error sharing file: ${error.message}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: `Error sharing file: ${error.message}` },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static handleClearDepartment(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
// Ensure the userName is available in metaInfo
|
||||
if (!parsedMessage.metaInfo || !parsedMessage.metaInfo.userName) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing user name in meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const { userName } = parsedMessage.metaInfo;
|
||||
|
||||
const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json');
|
||||
const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson);
|
||||
|
||||
// Corrected the condition
|
||||
if (!appInfo || !appInfo.departmentDirectory) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Error retrieving share directory from application.json.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Get the share directory path
|
||||
const baseDepartmentDir = appInfo.departmentDirectory.path;
|
||||
const userDepartmentDir = path.join(baseDepartmentDir, userName);
|
||||
|
||||
try {
|
||||
// Check if the user's backup directory exists
|
||||
if (fs.existsSync(userDepartmentDir)) {
|
||||
// Recursively delete the user's backup directory
|
||||
fs.rmSync(userDepartmentDir, { recursive: true, force: true });
|
||||
console.log(`Backup cleared successfully for user: ${userName}`);
|
||||
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { message: `Backup cleared successfully for user: ${userName}`},
|
||||
};
|
||||
} else {
|
||||
console.warn(`Backup directory not found for user: ${userName}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: `Backup directory not found for user: ${userName}`},
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Error clearing backup for user: ${userName} - ${error.message}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: `Error clearing backup for user: ${userName}`},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static handleDepartmentFile(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
// Ensure metaInfo and fileContent are available
|
||||
if (!parsedMessage.metaInfo || !parsedMessage.fileContent) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing file content or meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const { userName, relativeFilePath } = parsedMessage.metaInfo;
|
||||
|
||||
if (!userName || !relativeFilePath) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing user name or file path information.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Path to the application.json to read the shareDirectory field
|
||||
const pathToApplicationJson = path.join(__dirname, '..', '..', 'json_files', 'application.json');
|
||||
const appInfo = UserToUserOperations.readJsonSync(pathToApplicationJson);
|
||||
|
||||
// Corrected the condition
|
||||
if (!appInfo || !appInfo.departmentDirectory) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Error retrieving share directory from application.json.' },
|
||||
};
|
||||
}
|
||||
|
||||
// Get the share directory path
|
||||
const departmentDirectory = appInfo.departmentDirectory.path;
|
||||
|
||||
// Full path where the file will be stored (under the user's directory in the shared folder)
|
||||
const fullFilePath = path.join(departmentDirectory, userName, relativeFilePath);
|
||||
|
||||
try {
|
||||
// Ensure the directory structure exists (create directories if they don't exist)
|
||||
const dirPath = path.dirname(fullFilePath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the file content to the correct path
|
||||
fs.writeFileSync(fullFilePath, Buffer.from(parsedMessage.fileContent as Buffer), 'base64');
|
||||
|
||||
console.log(`File shared successfully: ${fullFilePath}`);
|
||||
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { message: `File shared successfully: ${relativeFilePath}`},
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`Error sharing file: ${error.message}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: {message: `Error sharing file: ${error.message}`},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a backup has been created for a user
|
||||
public static handleIsBackupCreated(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
if(!parsedMessage.metaInfo) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing metaInfo.' },
|
||||
};
|
||||
}
|
||||
|
||||
const {name} = parsedMessage.metaInfo;
|
||||
if (!name) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing name in meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const baseBackupDir = path.join(__dirname, '..', '..', 'backups');
|
||||
const userBackupDir = path.join(baseBackupDir, name);
|
||||
|
||||
console.log(userBackupDir);
|
||||
|
||||
const exists = fs.existsSync(userBackupDir);
|
||||
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { backupExists: exists },
|
||||
};
|
||||
}
|
||||
|
||||
// Get the structure of the backup directory for a user
|
||||
public static handleGetBackupStructure(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
if(!parsedMessage.metaInfo) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing metaInfo.' },
|
||||
};
|
||||
}
|
||||
|
||||
const {name} = parsedMessage.metaInfo;
|
||||
if (!name) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing name in meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const baseBackupDir = path.join(__dirname, '..', '..', 'backups');
|
||||
const userBackupDir = path.join(baseBackupDir, name);
|
||||
|
||||
if (!fs.existsSync(userBackupDir)) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { structure: {} }, // Return empty structure if directory doesn't exist
|
||||
};
|
||||
}
|
||||
|
||||
const fileStructure = UserToUserOperations.buildDirectoryStructure(userBackupDir);
|
||||
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { structure: fileStructure },
|
||||
};
|
||||
}
|
||||
|
||||
// Build the directory structure recursively
|
||||
private static buildDirectoryStructure(directoryPath: string): any {
|
||||
const structure: any = {};
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
structure[file] = UserToUserOperations.buildDirectoryStructure(filePath); // Recursive for subdirectories
|
||||
} else {
|
||||
structure[file] = path.relative(directoryPath, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
// Handle file request from backup directory
|
||||
public static handleReqFileFromBackup(parsedMessage: ParsedMessage): ParsedMessage {
|
||||
if(!parsedMessage.metaInfo) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing metaInfo.' },
|
||||
};
|
||||
}
|
||||
|
||||
const { name, relativeFilePath } = parsedMessage.metaInfo;
|
||||
|
||||
if (!name || !relativeFilePath) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'Missing name or file path in meta information.' },
|
||||
};
|
||||
}
|
||||
|
||||
const baseBackupDir = path.join(__dirname, '..', '..', 'backups');
|
||||
const userBackupDir = path.join(baseBackupDir, name);
|
||||
const fullFilePath = path.join(userBackupDir, relativeFilePath);
|
||||
|
||||
if (!fs.existsSync(fullFilePath)) {
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: 'File not found in backup.' },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(fullFilePath, 'base64');
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.OK,
|
||||
metaInfo: { relativeFilePath },
|
||||
fileContent: Buffer.from(fileContent, 'base64')
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`Error reading file: ${error.message}`);
|
||||
return {
|
||||
operationCode: UserToUserOperations.operationCodes.ERR,
|
||||
metaInfo: { message: `Error reading file: ${error.message}`},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register user-to-user operations with the OperationHandler
|
||||
public register(operationHandler: OperationHandler): void {
|
||||
// Register specific handlers for user-to-user operations
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.GET_USER_INFORMATION, UserToUserOperations.handleGetUserInformation);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.BACKUP_FILE, UserToUserOperations.handleBackupFile);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.CLEAR_BACKUP, UserToUserOperations.handleClearBackup);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.SHARE_FILE, UserToUserOperations.handleShareFile);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.DEPARTMENT_FILE, UserToUserOperations.handleDepartmentFile);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.CLEAR_DEPARTMENT, UserToUserOperations.handleClearDepartment);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.IS_BACKUP_CREATED, UserToUserOperations.handleIsBackupCreated);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.GET_BACKUP_STRUCTURE, UserToUserOperations.handleGetBackupStructure);
|
||||
operationHandler.registerHandler(UserToUserOperations.operationCodes.REQ_FILE_FROM_BACKUP, UserToUserOperations.handleReqFileFromBackup);
|
||||
|
||||
// Register common operations inherited from the base class
|
||||
OperationBase.registerCommonOperations(operationHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ParsedMessage } from '../message_handler';
|
||||
import { OperationHandler } from '../operations_base/operation_handler';
|
||||
|
||||
export abstract class SocketCommunicatorBase {
|
||||
protected readonly ip: string;
|
||||
protected readonly port: number;
|
||||
protected readonly operationHandler: OperationHandler;
|
||||
protected handlerResult: ParsedMessage | null;
|
||||
|
||||
protected constructor(ip: string, port: number, operationHandler: OperationHandler) {
|
||||
this.ip = ip;
|
||||
this.port = port;
|
||||
this.operationHandler = operationHandler
|
||||
this.handlerResult = null;
|
||||
}
|
||||
|
||||
// Getter for the handler result
|
||||
getHandlerResult(): ParsedMessage | null {
|
||||
return this.handlerResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { Socket } from 'net';
|
||||
import { createCipheriv, createDecipheriv } from 'crypto';
|
||||
import { MessageHandler, ParsedMessage } from '../message_handler';
|
||||
import { SocketCommunicatorBase } from './socket_communicator_base';
|
||||
import { OperationHandler } from '../operations_base/operation_handler';
|
||||
import {constants, publicDecrypt} from "node:crypto";
|
||||
import {operationCodes} from "../operation_codes";
|
||||
|
||||
const END_OF_MESSAGE = '<EOM>'; // Define a unique marker for end of message
|
||||
|
||||
export class TcpClientCommunicator extends SocketCommunicatorBase {
|
||||
private readonly socket: Socket;
|
||||
private aesKey: Buffer | null;
|
||||
private aesIv: Buffer | null;
|
||||
private messageBuffer: string;
|
||||
private serverPublicKey: string | null;
|
||||
private isAesKeySetFlag: boolean;
|
||||
|
||||
constructor(socket: Socket, ip: string, port: number, operationHandler: OperationHandler) {
|
||||
super(ip, port, operationHandler);
|
||||
this.socket = socket;
|
||||
this.aesKey = null;
|
||||
this.aesIv = null;
|
||||
this.serverPublicKey = null;
|
||||
this.messageBuffer = ''; // Buffer for message reassembly
|
||||
this.isAesKeySetFlag = false;
|
||||
}
|
||||
|
||||
setServerPublicKey(publicKey: string): void {
|
||||
this.serverPublicKey = publicKey;
|
||||
console.log('Server public key set.');
|
||||
}
|
||||
|
||||
// Set the AES key when received
|
||||
setAesKey(aesKey: string, aesIv: string): void {
|
||||
this.aesKey = Buffer.from(aesKey, 'base64');
|
||||
this.aesIv = Buffer.from(aesIv, 'base64');
|
||||
console.log('AES key set.');
|
||||
}
|
||||
|
||||
// Encrypt a message with AES
|
||||
private encryptWithAes(message: string): string {
|
||||
if (!this.aesKey || !this.aesIv) {
|
||||
throw new Error('AES key or IV is not set.');
|
||||
}
|
||||
const cipher = createCipheriv('aes-256-cbc', this.aesKey, this.aesIv);
|
||||
let encrypted = cipher.update(message, 'utf-8');
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
return encrypted.toString('base64');
|
||||
}
|
||||
|
||||
private decryptWithAes(encryptedMessage: string): string {
|
||||
if (!this.aesKey || !this.aesIv) {
|
||||
throw new Error('AES key or IV is not set.');
|
||||
}
|
||||
const decipher = createDecipheriv('aes-256-cbc', this.aesKey, this.aesIv);
|
||||
let decrypted = decipher.update(Buffer.from(encryptedMessage, 'base64'));
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString('utf-8');
|
||||
}
|
||||
|
||||
private decryptWithRsa(message: string): string {
|
||||
if (!this.serverPublicKey) {
|
||||
throw new Error('Server public key not set.');
|
||||
}
|
||||
try {
|
||||
const encryptedMessage = Buffer.from(message.toString(), 'base64');
|
||||
// Decrypt the message using the server's public key
|
||||
const decrypted = publicDecrypt(
|
||||
{
|
||||
key: this.serverPublicKey,
|
||||
padding: constants.RSA_PKCS1_PADDING, // Matching padding for decryption
|
||||
},
|
||||
encryptedMessage
|
||||
);
|
||||
return decrypted.toString('utf-8');
|
||||
} catch (error) {
|
||||
console.error('RSA decryption failed:', error);
|
||||
throw new Error('Failed to decrypt RSA message.');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming chunks of data
|
||||
async handleIncomingChunk(data: Buffer): Promise<void> {
|
||||
const incomingMessage = data.toString();
|
||||
this.messageBuffer += incomingMessage;
|
||||
|
||||
// Check if the message ends with END_OF_MESSAGE
|
||||
if (this.messageBuffer.endsWith(END_OF_MESSAGE)) {
|
||||
// Remove the END_OF_MESSAGE marker and process the message
|
||||
const completeMessage = this.messageBuffer.slice(0, -END_OF_MESSAGE.length);
|
||||
|
||||
this.handleIncomingMessage(completeMessage);
|
||||
|
||||
// Clear the message buffer after processing
|
||||
this.messageBuffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Send a chunked message over the socket
|
||||
async sendChunkedMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise<void> {
|
||||
const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent);
|
||||
let outgoingMessage: string;
|
||||
|
||||
// Encrypt the message with AES if available
|
||||
if (this.aesKey && this.aesIv) {
|
||||
outgoingMessage = this.encryptWithAes(message);
|
||||
} else {
|
||||
outgoingMessage = message // Send plain text if AES is not set
|
||||
}
|
||||
|
||||
// Append the end marker to the message
|
||||
outgoingMessage += END_OF_MESSAGE;
|
||||
|
||||
await this.writeToSocket(outgoingMessage);
|
||||
}
|
||||
|
||||
// Write message to socket
|
||||
private writeToSocket(message: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket.write(message, (err: any) => {
|
||||
if (err) {
|
||||
console.error('Error sending message over TCP:', err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle incoming message (decrypted if AES is set)
|
||||
handleIncomingMessage(incomingMessage: string): void {
|
||||
let messageToProcess = incomingMessage;
|
||||
if (this.aesKey && this.aesIv) {
|
||||
messageToProcess = this.decryptWithAes(incomingMessage);
|
||||
}else if(this.serverPublicKey){
|
||||
messageToProcess = this.decryptWithRsa(incomingMessage);
|
||||
}
|
||||
|
||||
console.log(`\n\nComplete Message:\n${messageToProcess}\n\n`);
|
||||
|
||||
const result = this.operationHandler.handleOperation(messageToProcess);
|
||||
|
||||
if(result.operationCode === operationCodes.SET_AES_KEY){
|
||||
this.isAesKeySetFlag = true;
|
||||
this.setAesKey(result.metaInfo?.aesKey, result.metaInfo?.aesIv);
|
||||
return;
|
||||
}
|
||||
|
||||
if(result.operationCode === operationCodes.SET_PUBLIC_KEY) {
|
||||
this.setServerPublicKey(result.metaInfo?.publicKey);
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlerResult = result
|
||||
}
|
||||
|
||||
// Check if AES key is set
|
||||
isAesKeySet(): boolean {
|
||||
return this.isAesKeySetFlag;
|
||||
}
|
||||
|
||||
// Get handler result for operation handling
|
||||
getHandlerResult(): ParsedMessage | null {
|
||||
return this.handlerResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Socket } from 'net';
|
||||
import { privateEncrypt, privateDecrypt, generateKeyPairSync, createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
||||
import { MessageHandler, ParsedMessage } from '../message_handler';
|
||||
import { SocketCommunicatorBase } from './socket_communicator_base';
|
||||
import { OperationHandler } from '../operations_base/operation_handler';
|
||||
import {constants} from "node:crypto";
|
||||
|
||||
const END_OF_MESSAGE = '<EOM>'; // Define a unique marker for end of message
|
||||
|
||||
export class TcpServerCommunicator extends SocketCommunicatorBase {
|
||||
private readonly socket: Socket;
|
||||
private privateKey: string | null;
|
||||
private publicKey: string | null;
|
||||
private aesKey: Buffer | null;
|
||||
private aesIv: Buffer | null;
|
||||
private messageBuffer: string;
|
||||
|
||||
constructor(socket: Socket, ip: string, port: number, operationHandler: OperationHandler) {
|
||||
super(ip, port, operationHandler);
|
||||
this.socket = socket;
|
||||
this.privateKey = null;
|
||||
this.publicKey = null; // Client public key will be set later
|
||||
this.aesKey = null;
|
||||
this.aesIv = null;
|
||||
this.messageBuffer = ''; // Initialize the message buffer
|
||||
this.generateKeyPair(); // Generate RSA key pair for encryption
|
||||
}
|
||||
|
||||
// Generate RSA key pair (public and private keys)
|
||||
generateKeyPair(): void {
|
||||
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
||||
});
|
||||
this.privateKey = privateKey;
|
||||
this.publicKey = publicKey;
|
||||
console.log('RSA key pair generated.');
|
||||
}
|
||||
|
||||
// Send the server's public key to the client
|
||||
async sendPublicKey(): Promise<void> {
|
||||
if (!this.publicKey) {
|
||||
throw new Error('Public key is not available. Please generate RSA key pair.');
|
||||
}
|
||||
|
||||
const message = MessageHandler.formatMessage('SET_PUBLIC_KEY', { publicKey: this.publicKey });
|
||||
await this.writeToSocket(message + END_OF_MESSAGE);
|
||||
console.log('Public key sent to client.');
|
||||
}
|
||||
|
||||
// Generate AES key and IV, then send them to the client
|
||||
async sendAesKey(): Promise<void> {
|
||||
this.aesKey = randomBytes(32); // 256-bit AES key
|
||||
this.aesIv = randomBytes(16); // AES IV
|
||||
|
||||
const aesKeyBase64 = this.aesKey.toString('base64');
|
||||
const aesIvBase64 = this.aesIv.toString('base64');
|
||||
|
||||
const formattedMessage = MessageHandler.formatMessage('SET_AES_KEY', { aesKey: aesKeyBase64, aesIv: aesIvBase64 });
|
||||
const encryptedMessage = this.encryptWithRsa(Buffer.from(formattedMessage)).toString('base64');
|
||||
|
||||
await this.writeToSocket(encryptedMessage + END_OF_MESSAGE);
|
||||
console.log('AES key and IV sent to client.');
|
||||
}
|
||||
|
||||
// Encrypt a message with the server's private key (RSA encryption)
|
||||
private encryptWithRsa(message: Buffer): Buffer {
|
||||
const bufferMessage = Buffer.isBuffer(message) ? message : Buffer.from(message);
|
||||
if (!this.privateKey) throw new Error('Server private key not set.');
|
||||
|
||||
return privateEncrypt(
|
||||
{
|
||||
key: this.privateKey,
|
||||
padding: constants.RSA_PKCS1_PADDING, // PKCS1 padding
|
||||
},
|
||||
bufferMessage
|
||||
);
|
||||
}
|
||||
|
||||
// Decrypt AES-encrypted messages
|
||||
private decryptWithAes(encryptedMessage: string): string {
|
||||
if (!this.aesKey || !this.aesIv) {
|
||||
throw new Error('AES key not set.');
|
||||
}
|
||||
|
||||
const decipher = createDecipheriv('aes-256-cbc', this.aesKey, this.aesIv);
|
||||
let decrypted = decipher.update(Buffer.from(encryptedMessage, 'base64'));
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString('utf-8');
|
||||
}
|
||||
|
||||
// Encrypt a message with AES
|
||||
private encryptWithAes(message: string): string {
|
||||
if (!this.aesKey || !this.aesIv) {
|
||||
throw new Error('AES key or IV is not set.');
|
||||
}
|
||||
const cipher = createCipheriv('aes-256-cbc', this.aesKey, this.aesIv);
|
||||
let encrypted = cipher.update(message, 'utf-8');
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
return encrypted.toString('base64');
|
||||
}
|
||||
|
||||
// Handle incoming chunks of data
|
||||
async handleIncomingChunk(data: Buffer): Promise<void> {
|
||||
const incomingMessage = data.toString();
|
||||
this.messageBuffer += incomingMessage;
|
||||
|
||||
// Check if the message ends with END_OF_MESSAGE
|
||||
if (this.messageBuffer.endsWith(END_OF_MESSAGE)) {
|
||||
// Remove the END_OF_MESSAGE marker and process the message
|
||||
const completeMessage = this.messageBuffer.slice(0, -END_OF_MESSAGE.length);
|
||||
|
||||
console.log(`\n\nComplete Message:\n${completeMessage}\n\n`);
|
||||
|
||||
this.handleIncomingMessage(completeMessage);
|
||||
|
||||
// Clear the message buffer after processing
|
||||
this.messageBuffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Send chunked message
|
||||
async sendChunkedMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise<void> {
|
||||
const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent);
|
||||
let outgoingMessage: string;
|
||||
|
||||
if (this.aesKey && this.aesIv) {
|
||||
outgoingMessage = this.encryptWithAes(message);
|
||||
} else {
|
||||
outgoingMessage = message;
|
||||
}
|
||||
|
||||
outgoingMessage += END_OF_MESSAGE; // Append end-of-message marker
|
||||
await this.writeToSocket(outgoingMessage);
|
||||
}
|
||||
|
||||
// Handle incoming message (decrypt with AES if available)
|
||||
handleIncomingMessage(incomingMessage: string): void {
|
||||
let messageToProcess = incomingMessage;
|
||||
|
||||
if (this.aesKey && this.aesIv) {
|
||||
messageToProcess = this.decryptWithAes(incomingMessage);
|
||||
}
|
||||
|
||||
this.handlerResult = this.operationHandler.handleOperation(messageToProcess);
|
||||
}
|
||||
|
||||
// Write message to socket
|
||||
private writeToSocket(message: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket.write(message, (err: any) => {
|
||||
if (err) {
|
||||
console.error('Error sending message over TCP:', err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Socket as UdpSocket } from 'dgram';
|
||||
import { MessageHandler } from '../message_handler';
|
||||
import { SocketCommunicatorBase } from './socket_communicator_base';
|
||||
import {OperationHandler} from "../operations_base/operation_handler";
|
||||
|
||||
export class UdpSocketCommunicator extends SocketCommunicatorBase {
|
||||
private readonly socket: UdpSocket;
|
||||
|
||||
constructor(socket: UdpSocket, ip: string, port: number, operationHandler: OperationHandler) {
|
||||
super(ip, port, operationHandler);
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
// Handle incoming message (no decryption needed for UDP)
|
||||
handleIncomingMessage(incomingMessage: string): void {
|
||||
this.handlerResult = this.operationHandler.handleOperation(incomingMessage);
|
||||
}
|
||||
|
||||
// Send plain message over UDP (metaInfo as JSON and fileContent as Buffer)
|
||||
async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise<void> {
|
||||
const message = MessageHandler.formatMessage(operationCode, metaInfo, fileContent);
|
||||
|
||||
await this.sendUdpMessage(message);
|
||||
}
|
||||
|
||||
// Helper method to wrap socket.send in a Promise for async/await support
|
||||
private sendUdpMessage(message: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket.send(message, this.port, this.ip, (err: any) => {
|
||||
if (err) {
|
||||
console.error('Error sending UDP message:', err);
|
||||
return reject(err);
|
||||
}
|
||||
console.log(`Plain message sent to ${this.ip}:${this.port} (UDP)`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import net, { Socket } from 'net';
|
||||
import { TcpClientCommunicator } from '../socket_communicator/tcp_client_communicator';
|
||||
import { OperationHandler } from '../operations_base/operation_handler';
|
||||
import { operationCodes } from "../operation_codes";
|
||||
import { GeneralOperations } from "../operations_custom/general_operations";
|
||||
import { UserToUserOperations } from "../operations_custom/user_to_user_operations";
|
||||
import { ParsedMessage } from "../message_handler";
|
||||
|
||||
export class TcpClient {
|
||||
private readonly tcp_port: number;
|
||||
private socket: Socket | null;
|
||||
private communicator: TcpClientCommunicator | null;
|
||||
private readonly operationHandler: OperationHandler;
|
||||
private lastResult: ParsedMessage | null;
|
||||
|
||||
constructor(tcp_port: number) {
|
||||
this.tcp_port = tcp_port;
|
||||
this.socket = null;
|
||||
this.communicator = null;
|
||||
this.operationHandler = OperationHandler.getInstance();
|
||||
this.lastResult = null;
|
||||
|
||||
this.operationHandler.loadPlugin(new GeneralOperations());
|
||||
this.operationHandler.loadPlugin(new UserToUserOperations());
|
||||
}
|
||||
|
||||
// Open a TCP socket connection
|
||||
openSocket(ip: string): void {
|
||||
this.socket = new net.Socket();
|
||||
|
||||
this.socket.connect(this.tcp_port, ip, () => {
|
||||
console.log(`Client connected to server at ${ip}:${this.tcp_port}`);
|
||||
this.communicator = new TcpClientCommunicator(this.socket as Socket, ip, this.tcp_port, this.operationHandler);
|
||||
});
|
||||
|
||||
this.socket.on('error', (err) => {
|
||||
console.error(`Connection error to server at ${ip}:${this.tcp_port}: ${err.message}`);
|
||||
});
|
||||
|
||||
this.socket.on('data', async (data: Buffer) => {
|
||||
if (this.communicator) {
|
||||
await this.communicator.handleIncomingChunk(data); // Let the communicator handle the chunks
|
||||
this.lastResult = this.communicator.getHandlerResult();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('close', () => {
|
||||
console.log(`Connection closed: ${ip}:${this.tcp_port}`);
|
||||
this.lastResult = null; // Clear the last result on socket close
|
||||
});
|
||||
}
|
||||
|
||||
// Close the socket connection
|
||||
closeSocket(): void {
|
||||
if (this.socket) {
|
||||
this.socket.end();
|
||||
this.socket = null;
|
||||
this.communicator = null;
|
||||
this.lastResult = null; // Clear the last result on close
|
||||
console.log('Client socket connection closed.');
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message with operationCode, metaInfo, and fileContent in chunks
|
||||
async sendMessage(operationCode: string, metaInfo?: { [key: string]: any }, fileContent?: Buffer): Promise<boolean> {
|
||||
if (!this.communicator || !this.isAesKeySet()) {
|
||||
console.error('Communicator not initialized or AES key not set.');
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.communicator.sendChunkedMessage(operationCode, metaInfo, fileContent); // Send message via communicator
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if AES key is set
|
||||
isAesKeySet(): boolean {
|
||||
if(!this.communicator) return false;
|
||||
return this.communicator?.isAesKeySet()
|
||||
}
|
||||
|
||||
// Check if the message is received (based on if lastResult is available)
|
||||
isMessageReceived(): boolean {
|
||||
console.log('Is message received?');
|
||||
console.log(this.lastResult);
|
||||
console.log(this.lastResult !== null);
|
||||
return this.lastResult !== null;
|
||||
}
|
||||
|
||||
// Get the last result (and clear it after returning)
|
||||
getLastResult(): ParsedMessage | null {
|
||||
const result = this.lastResult;
|
||||
this.lastResult = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if the socket is still connected
|
||||
isSocketConnected(): boolean {
|
||||
return this.socket !== null && !this.socket.destroyed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import net, { Socket } from 'net';
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
import { ConnectionManager } from "../connection_manager";
|
||||
import { TcpServerCommunicator } from "../socket_communicator/tcp_server_communicator";
|
||||
import { GeneralOperations } from "../operations_custom/general_operations";
|
||||
import { OperationHandler } from "../operations_base/operation_handler";
|
||||
import { UserToUserOperations } from "../operations_custom/user_to_user_operations";
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, './config/.env') });
|
||||
|
||||
export class TcpServer {
|
||||
private readonly connectionManager: ConnectionManager;
|
||||
private readonly operationHandler: OperationHandler;
|
||||
private readonly port: number;
|
||||
private readonly host: string;
|
||||
|
||||
constructor(host: string, port: number) {
|
||||
this.connectionManager = new ConnectionManager();
|
||||
this.operationHandler = OperationHandler.getInstance();
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
|
||||
this.operationHandler.loadPlugin(new GeneralOperations());
|
||||
this.operationHandler.loadPlugin(new UserToUserOperations());
|
||||
}
|
||||
|
||||
// Start the TCP server
|
||||
public start(): void {
|
||||
const tcpServer = net.createServer();
|
||||
|
||||
// Handle incoming connections
|
||||
tcpServer.on('connection', (socket: Socket) => {
|
||||
const ip = socket.remoteAddress || 'unknown';
|
||||
const port = socket.remotePort || 0;
|
||||
|
||||
const clientId = `${ip}:${port}`; // Use IP and port to identify the client
|
||||
console.log(`Client connected: ${clientId}`);
|
||||
|
||||
const tcpCommunicator = new TcpServerCommunicator(socket, ip, port, this.operationHandler);
|
||||
this.connectionManager.addConnection(ip, port, tcpCommunicator);
|
||||
|
||||
// Generate RSA key pair and start key exchange
|
||||
tcpCommunicator.generateKeyPair();
|
||||
tcpCommunicator.sendPublicKey()
|
||||
.then(() => tcpCommunicator.sendAesKey())
|
||||
.then(() => console.log('Public key and AES key sent successfully.'))
|
||||
.catch(err => {
|
||||
console.error(`Error during key exchange with client ${clientId}:`, err);
|
||||
socket.end(); // Close the connection in case of any error
|
||||
});
|
||||
|
||||
// Handle incoming data in chunks
|
||||
socket.on('data', async (data: Buffer) => {
|
||||
await this.handleData(data, ip, port);
|
||||
});
|
||||
|
||||
// Handle client disconnect
|
||||
socket.on('end', () => {
|
||||
console.log(`Client disconnected: ${clientId}`);
|
||||
this.connectionManager.removeCommunicator(ip, port);
|
||||
});
|
||||
|
||||
// Handle socket errors
|
||||
socket.on('error', (err: Error) => {
|
||||
console.error(`Error from client ${clientId}: ${err.message}`);
|
||||
this.connectionManager.removeCommunicator(ip, port);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle server errors
|
||||
tcpServer.on('error', (err: Error) => {
|
||||
console.error(`TCP server error: ${err.message}`);
|
||||
});
|
||||
|
||||
// Start listening for connections
|
||||
tcpServer.listen(this.port, this.host, () => {
|
||||
console.log(`TCP server listening on ${this.host}:${this.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle incoming data from a client
|
||||
private async handleData(data: Buffer, ip: string, port: number): Promise<void> {
|
||||
const clientId = `${ip}:${port}`;
|
||||
|
||||
// Retrieve the communicator associated with this connection
|
||||
const communicator = this.connectionManager.getCommunicator(ip, port) as TcpServerCommunicator;
|
||||
if (!communicator) {
|
||||
console.error(`No communicator found for ${clientId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle incoming chunked message via communicator
|
||||
await communicator.handleIncomingChunk(data);
|
||||
|
||||
// Fetch and process result if available
|
||||
const handlerResult = communicator.getHandlerResult();
|
||||
if (handlerResult) {
|
||||
try {
|
||||
await communicator.sendChunkedMessage(
|
||||
handlerResult.operationCode,
|
||||
handlerResult.metaInfo,
|
||||
handlerResult.fileContent
|
||||
);
|
||||
console.log(`Response sent to ${clientId}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to send response to ${clientId}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import dgram from 'dgram';
|
||||
import ping from 'ping';
|
||||
import { OperationHandler } from '../operations_base/operation_handler';
|
||||
import { MessageHandler } from '../message_handler';
|
||||
import { GeneralOperations } from "../operations_custom/general_operations";
|
||||
import { operationCodes } from "../operation_codes";
|
||||
import os from 'os';
|
||||
|
||||
export class UdpClient {
|
||||
private udpSocket: dgram.Socket;
|
||||
private readonly port: number;
|
||||
private operationHandler: OperationHandler;
|
||||
|
||||
constructor(port: number) {
|
||||
this.port = port;
|
||||
this.udpSocket = dgram.createSocket('udp4');
|
||||
this.operationHandler = OperationHandler.getInstance();
|
||||
this.operationHandler.loadPlugin(new GeneralOperations());
|
||||
}
|
||||
|
||||
// Send heartbeat and get clients that respond with ALIVE, excluding the host machine's IP addresses
|
||||
async getAliveClients(): Promise<string[]> {
|
||||
const subnet = this.getSubnet();
|
||||
const ipRange = this.getIPRange(subnet);
|
||||
|
||||
// Get local machine's IP addresses to exclude
|
||||
const localIPs = this.getLocalIPs();
|
||||
|
||||
// First, filter active IPs that respond to ping
|
||||
const activeIps = await this.filterActiveIps(ipRange);
|
||||
|
||||
// Send heartbeat to each active IP and keep only those that respond with ALIVE
|
||||
const aliveClients: string[] = [];
|
||||
for (const ip of activeIps) {
|
||||
if (!localIPs.includes(ip)) {
|
||||
const result = await this.sendHeartbeat(ip);
|
||||
if (result.found) {
|
||||
aliveClients.push(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aliveClients; // Return the list of IPs that responded with ALIVE, excluding the host machine
|
||||
}
|
||||
|
||||
// Get local IP addresses of the host machine (excluding loopback)
|
||||
private getLocalIPs(): string[] {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const localIPs: string[] = [];
|
||||
|
||||
Object.values(interfaces).forEach((iface) => {
|
||||
iface?.forEach((address) => {
|
||||
if (address.family === 'IPv4' && !address.internal) {
|
||||
localIPs.push(address.address);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return localIPs;
|
||||
}
|
||||
|
||||
// Send heartbeat to an IP
|
||||
private async sendHeartbeat(ip: string): Promise<{ found: boolean }> {
|
||||
return new Promise((resolve) => {
|
||||
const heartbeatCode = operationCodes.HEARTBEAT;
|
||||
const heartbeatMessage = MessageHandler.formatMessage(heartbeatCode);
|
||||
|
||||
this.udpSocket.send(heartbeatMessage, this.port, ip, (err) => {
|
||||
if (err) {
|
||||
resolve({ found: false });
|
||||
} else {
|
||||
const timeout = setTimeout(() => {
|
||||
this.dropConnection(ip);
|
||||
resolve({ found: false });
|
||||
}, 1500);
|
||||
|
||||
this.udpSocket.once('message', (msg, rinfo) => {
|
||||
if (rinfo.address === ip) {
|
||||
clearTimeout(timeout);
|
||||
const parsedMessage = MessageHandler.parseMessage(msg.toString());
|
||||
if (parsedMessage?.operationCode === operationCodes.ALIVE) {
|
||||
resolve({ found: true });
|
||||
} else {
|
||||
resolve({ found: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Drop connection for a specific IP
|
||||
private dropConnection(ip: string): void {
|
||||
try {
|
||||
this.udpSocket.removeAllListeners('message');
|
||||
} catch (err: any) {
|
||||
console.error(`Error dropping connection to ${ip}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the subnet (e.g., 192.168.1)
|
||||
private getSubnet(): string {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const iface of Object.values(interfaces)) {
|
||||
for (const address of iface || []) {
|
||||
if (address.family === 'IPv4' && !address.internal) {
|
||||
return address.address.split('.').slice(0, 3).join('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get IP range (assuming /24 subnet)
|
||||
private getIPRange(subnet: string): string[] {
|
||||
const ipRange = [];
|
||||
for (let i = 1; i < 255; i++) {
|
||||
ipRange.push(`${subnet}.${i}`);
|
||||
}
|
||||
return ipRange;
|
||||
}
|
||||
|
||||
// Filter only active IPs by pinging each IP in the range
|
||||
private async filterActiveIps(ipRange: string[]): Promise<string[]> {
|
||||
const activeIps: string[] = [];
|
||||
|
||||
const pingPromises = ipRange.map(ip => ping.promise.probe(ip, { timeout: 1 }));
|
||||
|
||||
const pingResults = await Promise.all(pingPromises);
|
||||
|
||||
for (const result of pingResults) {
|
||||
if (result.alive) {
|
||||
activeIps.push(result.host);
|
||||
}
|
||||
}
|
||||
|
||||
return activeIps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import dgram, { RemoteInfo } from 'dgram';
|
||||
import { UdpSocketCommunicator } from "../socket_communicator/udp_socket_communicator";
|
||||
import { OperationHandler } from "../operations_base/operation_handler";
|
||||
import { GeneralOperations} from "../operations_custom/general_operations";
|
||||
|
||||
export class UdpServer {
|
||||
private readonly udpServer: dgram.Socket;
|
||||
private readonly operationHandler: OperationHandler;
|
||||
private readonly port: number;
|
||||
private readonly host: string;
|
||||
|
||||
constructor(host: string, port: number) {
|
||||
this.udpServer = dgram.createSocket('udp4');
|
||||
this.operationHandler = OperationHandler.getInstance();
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
|
||||
// Load only the GeneralOperations into the operation handler
|
||||
this.operationHandler.loadPlugin(new GeneralOperations());
|
||||
}
|
||||
|
||||
// Start the UDP server
|
||||
public start(): void {
|
||||
this.udpServer.on('message', this.handleUdpMessages.bind(this));
|
||||
this.udpServer.on('error', this.handleError);
|
||||
this.udpServer.on('listening', this.handleListening.bind(this));
|
||||
|
||||
// Bind the server to the UDP port and host
|
||||
this.udpServer.bind(this.port, this.host);
|
||||
}
|
||||
|
||||
// Handle incoming UDP messages
|
||||
private async handleUdpMessages(msg: Buffer, rinfo: RemoteInfo): Promise<void> {
|
||||
const ip = rinfo.address;
|
||||
const port = rinfo.port;
|
||||
|
||||
console.log(`Received message from ${ip}:${port}`);
|
||||
|
||||
// Create a temporary communicator for the incoming message
|
||||
const communicator = new UdpSocketCommunicator(this.udpServer, ip, port, this.operationHandler);
|
||||
|
||||
// Process the incoming message using the communicator
|
||||
communicator.handleIncomingMessage(msg.toString());
|
||||
|
||||
const communicatorResult = communicator.getHandlerResult();
|
||||
if (communicatorResult) {
|
||||
// Send response back to the client using the temporary communicator
|
||||
await communicator.sendMessage(communicatorResult.operationCode, communicatorResult.metaInfo);
|
||||
} else {
|
||||
console.error(`No handler result for ${ip}:${port}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle UDP server errors
|
||||
private handleError(err: Error): void {
|
||||
console.error(`UDP server error:\n${err.stack}`);
|
||||
}
|
||||
|
||||
// Handle when the UDP server starts listening
|
||||
private handleListening(): void {
|
||||
const address = this.udpServer.address();
|
||||
console.log(`UDP server listening on ${address.address}:${address.port}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user