s7-server
Version:
TypeScript S7 PLC Server Simulator
409 lines • 14.7 kB
JavaScript
"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