UNPKG

s7-server

Version:
409 lines 14.7 kB
"use strict"; /** * Connection Manager for S7 Protocol * Handles client connections, PDU negotiation, and connection state */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectionManager = void 0; const events_1 = require("events"); // Internal modules const iso_tcp_1 = require("./iso-tcp"); const s7_pdu_1 = require("./s7-pdu"); class ConnectionManager extends events_1.EventEmitter { constructor(maxClients = 8, maxPDUSize = 2048) { super(); this.connections = new Map(); this.clientCounter = 0; this.maxClients = maxClients; this.maxPDUSize = maxPDUSize; } /** * Handle new client connection */ async handleNewConnection(socket) { const clientId = `client_${++this.clientCounter}`; const address = socket.remoteAddress || 'unknown'; const port = socket.remotePort || 0; // Check if we can accept more clients if (this.connections.size >= this.maxClients) { this.emit('clientRejected', { address, port, reason: 'Max clients reached' }); socket.destroy(); return null; } // Create connection info const connectionInfo = { id: clientId, socket, address, port, state: iso_tcp_1.ConnectionState.DISCONNECTED, params: { srcTSAP: 0x0100, dstTSAP: 0x0100, pduSize: 1024, maxOutstanding: 1, maxOutstandingCalled: 1 }, sequenceNumber: 0, lastActivity: new Date(), negotiatedPDU: 1024 }; this.connections.set(clientId, connectionInfo); // Set up socket event handlers this.setupSocketHandlers(clientId, socket); this.emit('clientAdded', this.getClientInfo(clientId)); return clientId; } /** * Handle ISO-TCP connection request */ async handleConnectionRequest(clientId, data) { const connection = this.connections.get(clientId); if (!connection) { return false; } try { const parsed = iso_tcp_1.ISOTCPProtocol.parseConnectionPDU(data); if (!parsed) { this.emit('malformedPDU', { clientId, error: 'Invalid connection PDU' }); return false; } const { header, params } = parsed; if (header.pduType !== 0xe0) { // Connection Request this.emit('malformedPDU', { clientId, error: 'Expected connection request' }); return false; } // Validate TSAP parameters if (!iso_tcp_1.ISOTCPProtocol.validateTSAP(params.srcTSAP, params.dstTSAP)) { this.emit('malformedPDU', { clientId, error: 'Invalid TSAP parameters' }); return false; } // Update connection parameters connection.params = params; connection.state = iso_tcp_1.ConnectionState.CONNECTING; // Send connection confirm const confirmPDU = iso_tcp_1.ISOTCPProtocol.createConnectionConfirm(params); connection.socket.write(confirmPDU); connection.state = iso_tcp_1.ConnectionState.CONNECTED; connection.lastActivity = new Date(); this.emit('connectionEstablished', this.getClientInfo(clientId)); return true; } catch (error) { this.emit('connectionError', { clientId, error }); return false; } } /** * Handle S7 PDU negotiation */ async handlePDUNegotiation(clientId, data) { const connection = this.connections.get(clientId); if (!connection) { return false; } try { const header = s7_pdu_1.S7PDUParser.parseRequestHeader(data); if (!header) { this.emit('malformedPDU', { clientId, error: 'Invalid request header' }); return false; } const request = s7_pdu_1.S7PDUParser.parseNegotiateRequest(data); if (!request) { this.emit('malformedPDU', { clientId, error: 'Invalid negotiate request' }); return false; } // Calculate negotiated PDU size const negotiatedPDU = iso_tcp_1.ISOTCPProtocol.calculatePDUSize(request.PDULength, this.maxPDUSize); connection.negotiatedPDU = negotiatedPDU; connection.sequenceNumber = header.Sequence; // Use sequence from header // Send negotiate response const response = s7_pdu_1.S7PDUParser.createNegotiateResponse(header.Sequence, negotiatedPDU); const isoResponse = iso_tcp_1.ISOTCPProtocol.createDataTransfer(response); connection.socket.write(isoResponse); connection.lastActivity = new Date(); this.emit('pduNegotiated', { clientId, requestedPDU: request.PDULength, negotiatedPDU }); return true; } catch (error) { this.emit('negotiateError', { clientId, error }); return false; } } /** * Handle S7 data request */ async handleS7Request(clientId, data) { const connection = this.connections.get(clientId); if (!connection) { return false; } try { // Check for PDU negotiation request first const negotiateRequest = s7_pdu_1.S7PDUParser.parseNegotiateRequest(data); if (negotiateRequest) { return await this.handlePDUNegotiation(clientId, data); } // Parse S7 request const readRequest = s7_pdu_1.S7PDUParser.parseReadRequest(data); if (readRequest) { // Use sequence number from the S7 header, not from address const header = s7_pdu_1.S7PDUParser.parseRequestHeader(data); if (header) { connection.sequenceNumber = header.Sequence; } this.emit('readRequest', { clientId, request: readRequest }); return true; } const writeRequest = s7_pdu_1.S7PDUParser.parseWriteRequest(data); if (writeRequest) { // Use sequence number from the S7 header, not from address const header = s7_pdu_1.S7PDUParser.parseRequestHeader(data); if (header) { connection.sequenceNumber = header.Sequence; } this.emit('writeRequest', { clientId, request: writeRequest }); return true; } const controlRequest = s7_pdu_1.S7PDUParser.parseControlRequest(data); if (controlRequest) { this.emit('controlRequest', { clientId, request: controlRequest }); return true; } this.emit('unknownRequest', { clientId, data }); return false; } catch (error) { this.emit('requestError', { clientId, error }); return false; } } /** * Send S7 response to client */ async sendS7Response(clientId, response) { const connection = this.connections.get(clientId); if (!connection || connection.state !== iso_tcp_1.ConnectionState.CONNECTED) { return false; } try { const isoResponse = iso_tcp_1.ISOTCPProtocol.createDataTransfer(response); connection.socket.write(isoResponse); connection.lastActivity = new Date(); return true; } catch (error) { this.emit('sendError', { clientId, error }); return false; } } /** * Disconnect client */ async disconnectClient(clientId) { const connection = this.connections.get(clientId); if (!connection) { return; } try { // Send disconnect request if connected if (connection.state === iso_tcp_1.ConnectionState.CONNECTED) { const disconnectPDU = iso_tcp_1.ISOTCPProtocol.createDataTransfer(Buffer.alloc(0)); connection.socket.write(disconnectPDU); } connection.state = iso_tcp_1.ConnectionState.DISCONNECTING; connection.socket.destroy(); this.connections.delete(clientId); this.emit('clientDisconnected', this.getClientInfo(clientId)); } catch (error) { this.emit('disconnectError', { clientId, error }); } } /** * Disconnect all clients */ async disconnectAllClients() { const clientIds = Array.from(this.connections.keys()); for (const clientId of clientIds) { await this.disconnectClient(clientId); } } /** * Get client information */ getClientInfo(clientId) { const connection = this.connections.get(clientId); if (!connection) { return null; } return { id: connection.id, address: connection.address, port: connection.port, connected: connection.state === iso_tcp_1.ConnectionState.CONNECTED, lastActivity: connection.lastActivity, negotiatedPDU: connection.negotiatedPDU }; } /** * Get all connected clients */ getAllClients() { return Array.from(this.connections.values()) .map(conn => this.getClientInfo(conn.id)) .filter(info => info !== null); } /** * Get connected clients count */ getClientsCount() { return this.connections.size; } /** * Check if client is connected */ isClientConnected(clientId) { const connection = this.connections.get(clientId); return connection?.state === iso_tcp_1.ConnectionState.CONNECTED; } /** * Get client's negotiated PDU size */ getClientPDUSize(clientId) { const connection = this.connections.get(clientId); return connection?.negotiatedPDU || 1024; } /** * Setup socket event handlers */ setupSocketHandlers(clientId, socket) { let buffer = Buffer.alloc(0); socket.on('data', (data) => { const connection = this.connections.get(clientId); if (!connection) { return; } connection.lastActivity = new Date(); buffer = Buffer.concat([buffer, data]); // Process complete PDUs const { pdus, remaining } = iso_tcp_1.ISOTCPProtocol.extractPDUs(buffer); buffer = Buffer.from(remaining); for (const pdu of pdus) { this.handlePDU(clientId, pdu); } }); socket.on('close', () => { this.handleClientDisconnect(clientId); }); socket.on('error', (error) => { this.emit('clientException', { clientId, error }); this.handleClientDisconnect(clientId); }); socket.on('timeout', () => { this.emit('clientTimeout', { clientId }); this.handleClientDisconnect(clientId); }); } /** * Handle incoming PDU */ async handlePDU(clientId, pdu) { try { const tpkt = iso_tcp_1.ISOTCPProtocol.parseTPKTHeader(pdu); if (!tpkt) { this.emit('malformedPDU', { clientId, error: 'Invalid TPKT header' }); return; } const cotpData = pdu.slice(4); const parsed = iso_tcp_1.ISOTCPProtocol.parseConnectionPDU(cotpData); if (parsed) { // This is a connection PDU await this.handleConnectionRequest(clientId, cotpData); return; } const dataTransfer = iso_tcp_1.ISOTCPProtocol.parseDataTransfer(cotpData); if (dataTransfer) { // This is a data transfer PDU await this.handleS7Request(clientId, dataTransfer.payload); return; } this.emit('unknownPDU', { clientId, pdu }); } catch (error) { this.emit('pduError', { clientId, error }); } } /** * Handle client disconnect */ handleClientDisconnect(clientId) { const connection = this.connections.get(clientId); if (connection) { connection.state = iso_tcp_1.ConnectionState.DISCONNECTED; const clientInfo = this.getClientInfo(clientId); this.connections.delete(clientId); this.emit('clientDisconnected', clientInfo); } } /** * Extract sequence number from address (simplified) */ extractSequenceFromAddress(addr) { // This is a simplified implementation // In a real implementation, you'd extract the sequence from the address return addr.readUInt8(0); } /** * Cleanup inactive connections */ cleanupInactiveConnections(timeoutMs = 300000) { // 5 minutes default const now = new Date(); const inactiveClients = []; for (const [clientId, connection] of this.connections) { const inactiveTime = now.getTime() - connection.lastActivity.getTime(); if (inactiveTime > timeoutMs) { inactiveClients.push(clientId); } } for (const clientId of inactiveClients) { this.disconnectClient(clientId); } if (inactiveClients.length > 0) { this.emit('clientsDropped', { count: inactiveClients.length }); } } /** * Update server configuration */ updateConfig(maxClients, maxPDUSize) { this.maxClients = maxClients; this.maxPDUSize = maxPDUSize; } /** * Get connection by client ID */ getConnection(clientId) { return this.connections.get(clientId); } /** * Get connection statistics */ getStatistics() { const connectedClients = Array.from(this.connections.values()).filter(conn => conn.state === iso_tcp_1.ConnectionState.CONNECTED).length; return { totalConnections: this.connections.size, connectedClients, maxClients: this.maxClients, maxPDUSize: this.maxPDUSize }; } } exports.ConnectionManager = ConnectionManager; //# sourceMappingURL=connection-manager.js.map