node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
1,248 lines (1,068 loc) • 45 kB
text/typescript
/* eslint-disable max-statements */
/**
* @module node-opcua-server
*/
// tslint:disable:no-console
import { EventEmitter } from "events";
import net from "net";
import { Server, Socket } from "net";
import chalk from "chalk";
import async from "async";
import { assert } from "node-opcua-assert";
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
import { Certificate, PrivateKey, makeSHA1Thumbprint, split_der } from "node-opcua-crypto/web";
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
import { getFullyQualifiedDomainName, resolveFullyQualifiedDomainName } from "node-opcua-hostname";
import {
fromURI,
MessageSecurityMode,
SecurityPolicy,
ServerSecureChannelLayer,
ServerSecureChannelParent,
toURI,
IServerSessionBase,
Message
} from "node-opcua-secure-channel";
import { UserTokenType } from "node-opcua-service-endpoints";
import { EndpointDescription } from "node-opcua-service-endpoints";
import { ApplicationDescription } from "node-opcua-service-endpoints";
import { UserTokenPolicyOptions } from "node-opcua-types";
import { IHelloAckLimits } from "node-opcua-transport";
import { IChannelData } from "./i_channel_data";
import { ISocketData } from "./i_socket_data";
const debugLog = make_debugLog(__filename);
const errorLog = make_errorLog(__filename);
const warningLog = make_warningLog(__filename);
const doDebug = checkDebugFlag(__filename);
const default_transportProfileUri = "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary";
function extractSocketData(socket: net.Socket, reason: string): ISocketData {
const { bytesRead, bytesWritten, remoteAddress, remoteFamily, remotePort, localAddress, localPort } = socket;
const data: ISocketData = {
bytesRead,
bytesWritten,
localAddress,
localPort,
remoteAddress,
remoteFamily,
remotePort,
timestamp: new Date(),
reason
};
return data;
}
function extractChannelData(channel: ServerSecureChannelLayer): IChannelData {
const {
channelId,
clientCertificate,
securityMode,
securityPolicy,
timeout,
transactionsCount
} = channel;
const channelData: IChannelData = {
channelId,
clientCertificate,
securityMode,
securityPolicy,
timeout,
transactionsCount
};
return channelData;
}
function dumpChannelInfo(channels: ServerSecureChannelLayer[]): void {
function d(s: IServerSessionBase) {
return `[ status=${s.status} lastSeen=${s.clientLastContactTime.toFixed(0)}ms sessionName=${s.sessionName} timeout=${
s.sessionTimeout
} ]`;
}
function dumpChannel(channel: ServerSecureChannelLayer): void {
console.log("------------------------------------------------------");
console.log(" channelId = ", channel.channelId);
console.log(" timeout = ", channel.timeout);
console.log(" remoteAddress = ", channel.remoteAddress);
console.log(" remotePort = ", channel.remotePort);
console.log("");
console.log(" bytesWritten = ", channel.bytesWritten);
console.log(" bytesRead = ", channel.bytesRead);
console.log(" sessions = ", Object.keys(channel.sessionTokens).length);
console.log(Object.values(channel.sessionTokens).map(d).join("\n"));
const socket = (channel as any).transport?._socket;
if (!socket) {
console.log(" SOCKET IS CLOSED");
}
}
for (const channel of channels) {
dumpChannel(channel);
}
console.log("------------------------------------------------------");
}
const emptyCertificate = Buffer.alloc(0);
const emptyPrivateKey = null as any as PrivateKey;
let OPCUAServerEndPointCounter = 0;
export interface OPCUAServerEndPointOptions {
/**
* the tcp port
*/
port: number;
/**
* the tcp host
*/
host?: string;
/**
* the DER certificate chain
*/
certificateChain: Certificate;
/**
* privateKey
*/
privateKey: PrivateKey;
certificateManager: OPCUACertificateManager;
/**
* the default secureToken lifetime @default=60000
*/
defaultSecureTokenLifetime?: number;
/**
* the maximum number of connection allowed on the TCP server socket
* @default 20
*/
maxConnections?: number;
/**
* the timeout for the TCP HEL/ACK transaction (in ms)
* @default 30000
*/
timeout?: number;
serverInfo: ApplicationDescription;
objectFactory?: any;
transportSettings?: IServerTransportSettings;
}
export interface IServerTransportSettings {
adjustTransportLimits: (hello: IHelloAckLimits) => IHelloAckLimits;
}
export interface EndpointDescriptionParams {
restricted?: boolean;
allowUnsecurePassword?: boolean;
resourcePath?: string;
alternateHostname?: string[];
hostname: string;
securityPolicies: SecurityPolicy[];
userTokenTypes: UserTokenType[];
}
export interface AddStandardEndpointDescriptionsParam {
allowAnonymous?: boolean;
disableDiscovery?: boolean;
securityModes?: MessageSecurityMode[];
restricted?: boolean;
allowUnsecurePassword?: boolean;
resourcePath?: string;
alternateHostname?: string[];
hostname?: string;
securityPolicies?: SecurityPolicy[];
userTokenTypes?: UserTokenType[];
}
function getUniqueName(name: string, collection: { [key: string]: number }) {
if (collection[name]) {
let counter = 0;
while (collection[name + "_" + counter.toString()]) {
counter++;
}
name = name + "_" + counter.toString();
collection[name] = 1;
return name;
} else {
collection[name] = 1;
return name;
}
}
interface ServerSecureChannelLayerPriv extends ServerSecureChannelLayer {
_unpreregisterChannelEvent?: () => void;
}
/**
* OPCUAServerEndPoint a Server EndPoint.
* A sever end point is listening to one port
* note:
* see OPCUA Release 1.03 part 4 page 108 7.1 ApplicationDescription
*/
export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureChannelParent {
/**
* the tcp port
*/
public port: number;
public host: string | undefined;
public certificateManager: OPCUACertificateManager;
public defaultSecureTokenLifetime: number;
public maxConnections: number;
public timeout: number;
public bytesWrittenInOldChannels: number;
public bytesReadInOldChannels: number;
public transactionsCountOldChannels: number;
public securityTokenCountOldChannels: number;
public serverInfo: ApplicationDescription;
public objectFactory: any;
public _on_new_channel?: (channel: ServerSecureChannelLayer) => void;
public _on_close_channel?: (channel: ServerSecureChannelLayer) => void;
public _on_connectionRefused?: (socketData: any) => void;
public _on_openSecureChannelFailure?: (socketData: any, channelData: any) => void;
private _certificateChain: Certificate;
private _privateKey: PrivateKey;
private _channels: { [key: string]: ServerSecureChannelLayer };
private _server?: Server;
private _endpoints: EndpointDescription[];
private _listen_callback?: (err?: Error) => void;
private _started = false;
private _counter = OPCUAServerEndPointCounter++;
private _policy_deduplicator: { [key: string]: number } = {};
private transportSettings?: IServerTransportSettings;
constructor(options: OPCUAServerEndPointOptions) {
super();
assert(!Object.prototype.hasOwnProperty.call(options, "certificate"), "expecting a certificateChain instead");
assert(Object.prototype.hasOwnProperty.call(options, "certificateChain"), "expecting a certificateChain");
assert(Object.prototype.hasOwnProperty.call(options, "privateKey"));
this.certificateManager = options.certificateManager;
options.port = options.port || 0;
this.port = parseInt(options.port.toString(), 10);
this.host = options.host;
assert(typeof this.port === "number");
this._certificateChain = options.certificateChain;
this._privateKey = options.privateKey;
this._channels = {};
this.defaultSecureTokenLifetime = options.defaultSecureTokenLifetime || 600000;
this.maxConnections = options.maxConnections || 20;
this.timeout = options.timeout || 30000;
this._server = undefined;
this._setup_server();
this._endpoints = [];
this.objectFactory = options.objectFactory;
this.bytesWrittenInOldChannels = 0;
this.bytesReadInOldChannels = 0;
this.transactionsCountOldChannels = 0;
this.securityTokenCountOldChannels = 0;
this.serverInfo = options.serverInfo;
assert(this.serverInfo !== null && typeof this.serverInfo === "object");
this.transportSettings = options.transportSettings;
}
public dispose(): void {
this._certificateChain = emptyCertificate;
this._privateKey = emptyPrivateKey;
assert(Object.keys(this._channels).length === 0, "OPCUAServerEndPoint channels must have been deleted");
this._channels = {};
this.serverInfo = new ApplicationDescription({});
this._endpoints = [];
assert(this._endpoints.length === 0, "endpoints must have been deleted");
this._endpoints = [];
this._server = undefined;
this._listen_callback = undefined;
this.removeAllListeners();
}
public toString(): string {
const txt =
" end point" +
this._counter +
" port = " +
this.port +
" l = " +
this._endpoints.length +
" " +
makeSHA1Thumbprint(this.getCertificateChain()).toString("hex")
return txt;
}
public getChannels(): ServerSecureChannelLayer[] {
return Object.values(this._channels);
}
/**
* Returns the X509 DER form of the server certificate
*/
public getCertificate(): Certificate {
return split_der(this.getCertificateChain())[0];
}
/**
* Returns the X509 DER form of the server certificate
*/
public getCertificateChain(): Certificate {
return this._certificateChain;
}
/**
* the private key
*/
public getPrivateKey(): PrivateKey {
return this._privateKey;
}
/**
* The number of active channel on this end point.
*/
public get currentChannelCount(): number {
return Object.keys(this._channels).length;
}
/**
*/
public getEndpointDescription(
securityMode: MessageSecurityMode,
securityPolicy: SecurityPolicy,
endpointUrl: string | null
): EndpointDescription | null {
const endpoints = this.endpointDescriptions();
const arr = endpoints.filter(matching_endpoint.bind(this, securityMode, securityPolicy, endpointUrl));
if (endpointUrl && endpointUrl.length > 0 && !(arr.length === 0 || arr.length === 1)) {
errorLog("Several matching endpoints have been found : ");
for (const a of arr) {
errorLog(" ", a.endpointUrl, MessageSecurityMode[securityMode], securityPolicy);
}
}
return arr.length === 0 ? null : arr[0];
}
public addEndpointDescription(
securityMode: MessageSecurityMode,
securityPolicy: SecurityPolicy,
options: EndpointDescriptionParams
): void {
// istanbul ignore next
if (securityMode === MessageSecurityMode.None && securityPolicy !== SecurityPolicy.None) {
throw new Error(" invalid security ");
}
// istanbul ignore next
if (securityMode !== MessageSecurityMode.None && securityPolicy === SecurityPolicy.None) {
throw new Error(" invalid security ");
}
//
// resource Path is a string added at the end of the url such as "/UA/Server"
const resourcePath = (options.resourcePath || "").replace(/\\/g, "/");
assert(resourcePath.length === 0 || resourcePath.charAt(0) === "/", "resourcePath should start with /");
const hostname = options.hostname || getFullyQualifiedDomainName();
const endpointUrl = `opc.tcp://${hostname}:${this.port}${resourcePath}`;
const endpoint_desc = this.getEndpointDescription(securityMode, securityPolicy, endpointUrl);
// istanbul ignore next
if (endpoint_desc) {
throw new Error(" endpoint already exist");
}
const userTokenTypes = options.userTokenTypes;
// now build endpointUrl
this._endpoints.push(
_makeEndpointDescription(
{
collection: this._policy_deduplicator,
hostname,
server: this.serverInfo,
serverCertificateChain: this.getCertificateChain(),
securityMode,
securityPolicy,
allowUnsecurePassword: options.allowUnsecurePassword,
resourcePath: options.resourcePath,
restricted: !!options.restricted,
securityPolicies: options.securityPolicies || [],
userTokenTypes
},
this
)
);
}
public addRestrictedEndpointDescription(options: EndpointDescriptionParams): void {
options = { ...options };
options.restricted = true;
return this.addEndpointDescription(MessageSecurityMode.None, SecurityPolicy.None, options);
}
public addStandardEndpointDescriptions(options?: AddStandardEndpointDescriptionsParam): void {
options = options || {};
options.securityModes = options.securityModes || defaultSecurityModes;
options.securityPolicies = options.securityPolicies || defaultSecurityPolicies;
options.userTokenTypes = options.userTokenTypes || defaultUserTokenTypes;
options.allowAnonymous = options.allowAnonymous === undefined ? true : options.allowAnonymous;
// make sure we do not have anonymous
if (!options.allowAnonymous) {
options.userTokenTypes = options.userTokenTypes.filter((r) => r !== UserTokenType.Anonymous);
}
const defaultHostname = options.hostname || getFullyQualifiedDomainName();
let hostnames: string[] = [defaultHostname];
options.alternateHostname = options.alternateHostname || [];
if (typeof options.alternateHostname === "string") {
options.alternateHostname = [options.alternateHostname];
}
// remove duplicates if any (uniq)
hostnames = [...new Set(hostnames.concat(options.alternateHostname))];
for (const alternateHostname of hostnames) {
const optionsE: EndpointDescriptionParams = {
hostname: alternateHostname,
securityPolicies: options.securityPolicies,
userTokenTypes: options.userTokenTypes,
allowUnsecurePassword: options.allowUnsecurePassword,
alternateHostname: options.alternateHostname,
resourcePath: options.resourcePath
};
if (options.securityModes.indexOf(MessageSecurityMode.None) >= 0) {
this.addEndpointDescription(MessageSecurityMode.None, SecurityPolicy.None, optionsE);
} else {
if (!options.disableDiscovery) {
this.addRestrictedEndpointDescription(optionsE);
}
}
for (const securityMode of options.securityModes) {
if (securityMode === MessageSecurityMode.None) {
continue;
}
for (const securityPolicy of options.securityPolicies) {
if (securityPolicy === SecurityPolicy.None) {
continue;
}
this.addEndpointDescription(securityMode, securityPolicy, optionsE);
}
}
}
}
/**
* returns the list of end point descriptions.
*/
public endpointDescriptions(): EndpointDescription[] {
return this._endpoints;
}
/**
*/
public listen(callback: (err?: Error) => void): void {
assert(typeof callback === "function");
assert(!this._started, "OPCUAServerEndPoint is already listening");
this._listen_callback = callback;
this._server!.on("error", (err: Error) => {
debugLog(chalk.red.bold(" error") + " port = " + this.port, err);
this._started = false;
this._end_listen(err);
});
this._server!.on("listening", () => {
debugLog("server is listening");
});
const listenOptions: net.ListenOptions = {
port: this.port,
host: this.host
};
this._server!.listen(
listenOptions,
/*"::",*/ (err?: Error) => {
// 'listening' listener
debugLog(chalk.green.bold("LISTENING TO PORT "), this.port, "err ", err);
assert(!err, " cannot listen to port ");
this._started = true;
if (!this.port) {
const add = this._server!.address()!;
this.port = typeof add !== "string" ? add.port : this.port;
}
this._end_listen();
}
);
}
public killClientSockets(callback: (err?: Error) => void): void {
for (const channel of this.getChannels()) {
const hacked_channel = channel as any;
if (hacked_channel.transport && hacked_channel.transport._socket) {
// hacked_channel.transport._socket.close();
hacked_channel.transport._socket.destroy();
hacked_channel.transport._socket.emit("error", new Error("EPIPE"));
}
}
callback();
}
public suspendConnection(callback: (err?: Error) => void): void {
if (!this._started) {
return callback(new Error("Connection already suspended !!"));
}
// Stops the server from accepting new connections and keeps existing connections.
// (note from nodejs doc: This function is asynchronous, the server is finally closed
// when all connections are ended and the server emits a 'close' event.
// The optional callback will be called once the 'close' event occurs.
// Unlike that event, it will be called with an Error as its only argument
// if the server was not open when it was closed.
this._server!.close(() => {
this._started = false;
debugLog("Connection has been closed !" + this.port);
});
this._started = false;
callback();
}
public restoreConnection(callback: (err?: Error) => void): void {
this.listen(callback);
}
public abruptlyInterruptChannels(): void {
for (const channel of Object.values(this._channels)) {
channel.abruptlyInterrupt();
}
}
/**
*/
public shutdown(callback: (err?: Error) => void): void {
debugLog("OPCUAServerEndPoint#shutdown ");
if (this._started) {
// make sure we don't accept new connection any more ...
this.suspendConnection(() => {
// shutdown all opened channels ...
const _channels = Object.values(this._channels);
async.each(
_channels,
(channel: ServerSecureChannelLayer, callback1: (err?: Error) => void) => {
this.shutdown_channel(channel, callback1);
},
(err?: Error | null) => {
/* istanbul ignore next */
if (!(Object.keys(this._channels).length === 0)) {
errorLog(" Bad !");
}
assert(Object.keys(this._channels).length === 0, "channel must have unregistered themselves");
callback(err || undefined);
}
);
});
} else {
callback();
}
}
/**
*/
public start(callback: (err?: Error) => void): void {
assert(typeof callback === "function");
this.listen(callback);
}
public get bytesWritten(): number {
const channels = Object.values(this._channels);
return (
this.bytesWrittenInOldChannels +
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
return accumulated + channel.bytesWritten;
}, 0)
);
}
public get bytesRead(): number {
const channels = Object.values(this._channels);
return (
this.bytesReadInOldChannels +
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
return accumulated + channel.bytesRead;
}, 0)
);
}
public get transactionsCount(): number {
const channels = Object.values(this._channels);
return (
this.transactionsCountOldChannels +
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
return accumulated + channel.transactionsCount;
}, 0)
);
}
public get securityTokenCount(): number {
const channels = Object.values(this._channels);
return (
this.securityTokenCountOldChannels +
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
return accumulated + channel.securityTokenCount;
}, 0)
);
}
public get activeChannelCount(): number {
return Object.keys(this._channels).length;
}
private _dump_statistics() {
this._server!.getConnections((err: Error | null, count: number) => {
debugLog(chalk.cyan("CONCURRENT CONNECTION = "), count);
});
debugLog(chalk.cyan("MAX CONNECTIONS = "), this._server!.maxConnections);
}
private _setup_server() {
assert(!this._server);
this._server = net.createServer({ pauseOnConnect: true }, this._on_client_connection.bind(this));
// xx console.log(" Server with max connections ", self.maxConnections);
this._server.maxConnections = this.maxConnections + 1; // plus one extra
this._listen_callback = undefined;
this._server
.on("connection", (socket: NodeJS.Socket) => {
// istanbul ignore next
if (doDebug) {
this._dump_statistics();
debugLog("server connected with : " + (socket as any).remoteAddress + ":" + (socket as any).remotePort);
}
})
.on("close", () => {
debugLog("server closed : all connections have ended");
})
.on("error", (err: Error) => {
// this could be because the port is already in use
debugLog(chalk.red.bold("server error: "), err.message);
});
}
private _on_client_connection(socket: Socket) {
// a client is attempting a connection on the socket
socket.setNoDelay(true);
debugLog("OPCUAServerEndPoint#_on_client_connection", this._started);
if (!this._started) {
debugLog(
chalk.bgWhite.cyan(
"OPCUAServerEndPoint#_on_client_connection " +
"SERVER END POINT IS PROBABLY SHUTTING DOWN !!! - Connection is refused"
)
);
socket.end();
return;
}
const deny_connection = () => {
console.log(
chalk.bgWhite.cyan(
"OPCUAServerEndPoint#_on_client_connection " +
"The maximum number of connection has been reached - Connection is refused"
)
);
const reason = "maxConnections reached (" + this.maxConnections + ")";
const socketData = extractSocketData(socket, reason);
this.emit("connectionRefused", socketData);
socket.end();
socket.destroy();
};
const establish_connection = () => {
const nbConnections = Object.keys(this._channels).length;
if (nbConnections >= this.maxConnections) {
warningLog(
" nbConnections ",
nbConnections,
" self._server.maxConnections",
this._server!.maxConnections,
this.maxConnections
);
deny_connection();
return;
}
debugLog("OPCUAServerEndPoint._on_client_connection successful => New Channel");
const channel = new ServerSecureChannelLayer({
defaultSecureTokenLifetime: this.defaultSecureTokenLifetime,
// objectFactory: this.objectFactory,
parent: this,
timeout: this.timeout,
adjustTransportLimits: this.transportSettings?.adjustTransportLimits
});
debugLog("channel Timeout = >", channel.timeout);
socket.resume();
this._preregisterChannel(channel);
channel.init(socket, (err?: Error) => {
this._un_pre_registerChannel(channel);
debugLog(chalk.yellow.bold("Channel#init done"), err);
if (err) {
const reason = "openSecureChannel has Failed " + err.message;
const socketData = extractSocketData(socket, reason);
const channelData = extractChannelData(channel);
this.emit("openSecureChannelFailure", socketData, channelData);
socket.end();
socket.destroy();
} else {
debugLog("server receiving a client connection");
this._registerChannel(channel);
}
});
channel.on("message", (message: Message) => {
// forward
this.emit("message", message, channel, this);
});
};
// Each SecureChannel exists until it is explicitly closed or until the last token has expired and the overlap
// period has elapsed. A Server application should limit the number of SecureChannels.
// To protect against misbehaving Clients and denial of service attacks, the Server shall close the oldest
// SecureChannel that has no Session assigned before reaching the maximum number of supported SecureChannels.
this._prevent_DDOS_Attack(establish_connection, deny_connection);
}
private _preregisterChannel(channel: ServerSecureChannelLayer) {
// _preregisterChannel is used to keep track of channel for which
// that are in early stage of the hand shaking process.
// e.g HEL/ACK and OpenSecureChannel may not have been received yet
// as they will need to be interrupted when OPCUAServerEndPoint is closed
assert(this._started, "OPCUAServerEndPoint must be started");
assert(!Object.prototype.hasOwnProperty.call(this._channels, channel.hashKey), " channel already preregistered!");
const channelPriv = <ServerSecureChannelLayerPriv>channel;
this._channels[channel.hashKey] = channelPriv;
channelPriv._unpreregisterChannelEvent = () => {
debugLog("Channel received an abort event during the preregistration phase");
this._un_pre_registerChannel(channel);
channel.dispose();
};
channel.on("abort", channelPriv._unpreregisterChannelEvent);
}
private _un_pre_registerChannel(channel: ServerSecureChannelLayer) {
if (!this._channels[channel.hashKey]) {
debugLog("Already un preregistered ?", channel.hashKey);
return;
}
delete this._channels[channel.hashKey];
const channelPriv = <ServerSecureChannelLayerPriv>channel;
if (typeof channelPriv._unpreregisterChannelEvent === "function") {
channel.removeListener("abort", channelPriv._unpreregisterChannelEvent!);
channelPriv._unpreregisterChannelEvent = undefined;
}
}
/**
* @private
*/
private _registerChannel(channel: ServerSecureChannelLayer) {
if (this._started) {
debugLog(chalk.red("_registerChannel = "), "channel.hashKey = ", channel.hashKey);
assert(!this._channels[channel.hashKey]);
this._channels[channel.hashKey] = channel;
/**
* @event newChannel
* @param channel
*/
this.emit("newChannel", channel);
channel.on("abort", () => {
this._unregisterChannel(channel);
});
} else {
debugLog("OPCUAServerEndPoint#_registerChannel called when end point is shutdown !");
debugLog(" -> channel will be forcefully terminated");
channel.close(() => {
channel.dispose();
});
}
}
/**
*/
private _unregisterChannel(channel: ServerSecureChannelLayer): void {
debugLog("_un-registerChannel channel.hashKey", channel.hashKey);
if (!Object.prototype.hasOwnProperty.call(this._channels, channel.hashKey)) {
return;
}
assert(Object.prototype.hasOwnProperty.call(this._channels, channel.hashKey), "channel is not registered");
/**
* @event closeChannel
* @param channel
*/
this.emit("closeChannel", channel);
// keep trace of statistics data from old channel for our own accumulated stats.
this.bytesWrittenInOldChannels += channel.bytesWritten;
this.bytesReadInOldChannels += channel.bytesRead;
this.transactionsCountOldChannels += channel.transactionsCount;
delete this._channels[channel.hashKey];
// istanbul ignore next
if (doDebug) {
this._dump_statistics();
debugLog("un-registering channel - Count = ", this.currentChannelCount);
}
/// channel.dispose();
}
private _end_listen(err?: Error) {
if (!this._listen_callback) return;
assert(typeof this._listen_callback === "function");
this._listen_callback!(err);
this._listen_callback = undefined;
}
/**
* shutdown_channel
* @param channel
* @param inner_callback
*/
private shutdown_channel(channel: ServerSecureChannelLayer, inner_callback: (err?: Error) => void) {
assert(typeof inner_callback === "function");
channel.once("close", () => {
// xx console.log(" ON CLOSED !!!!");
});
channel.close(() => {
this._unregisterChannel(channel);
setImmediate(inner_callback);
});
}
/**
* @private
*/
private _prevent_DDOS_Attack(establish_connection: () => void, deny_connection: () => void) {
const nbConnections = this.activeChannelCount;
if (nbConnections >= this.maxConnections) {
// istanbul ignore next
errorLog(chalk.bgRed.white("PREVENTING DDOS ATTACK => maxConnection =" + this.maxConnections));
const unused_channels: ServerSecureChannelLayer[] = this.getChannels().filter((channel1: ServerSecureChannelLayer) => {
return !channel1.hasSession;
});
if (unused_channels.length === 0) {
doDebug && console.log(
this.getChannels()
.map(({ status, isOpened, hasSession }) => `${status} ${isOpened} ${hasSession}\n`)
.join(" ")
);
// all channels are in used , we cannot get any
errorLog(`All channels are in used ! we cannot cancel any ${this.getChannels().length}`);
// istanbul ignore next
if (doDebug) {
console.log(" - all channels are used !!!!");
false && dumpChannelInfo(this.getChannels());
}
setTimeout(deny_connection, 1000);
return;
}
// istanbul ignore next
if (doDebug) {
console.log(
" - Unused channels that can be clobbered",
unused_channels.map((channel1: ServerSecureChannelLayer) => channel1.hashKey).join(" ")
);
}
const channel = unused_channels[0];
errorLog(`${unused_channels.length} : Forcefully closing oldest channel that have no session: ${channel.hashKey}`);
channel.close(() => {
// istanbul ignore next
if (doDebug) {
console.log(" _ Unused channel has been closed ", channel.hashKey);
}
this._unregisterChannel(channel);
establish_connection();
});
} else {
setImmediate(establish_connection);
}
}
}
interface MakeEndpointDescriptionOptions {
/**
* @default default hostname (default value will be full qualified domain name)
*/
hostname: string;
serverCertificateChain: Certificate;
/**
*
*/
securityMode: MessageSecurityMode;
/**
*
*/
securityPolicy: SecurityPolicy;
securityLevel?: number;
server: ApplicationDescription;
/*
{
applicationUri: string;
applicationName: LocalizedTextOptions;
applicationType: ApplicationType;
gatewayServerUri: string;
discoveryProfileUri: string;
discoveryUrls: string[];
};
*/
resourcePath?: string;
// allow un-encrypted password in userNameIdentity
allowUnsecurePassword?: boolean; // default false
/**
* onlyCertificateLessConnection
*/
onlyCertificateLessConnection?: boolean;
restricted: boolean;
collection: { [key: string]: number };
securityPolicies: SecurityPolicy[];
userTokenTypes: UserTokenType[];
/**
*
* default value: false;
*
* note: setting noUserIdentityTokens=true is useful for pure local discovery servers
*/
noUserIdentityTokens?: boolean;
}
export interface EndpointDescriptionEx extends EndpointDescription {
_parent: OPCUAServerEndPoint;
restricted: boolean;
}
function estimateSecurityLevel(securityMode: MessageSecurityMode, securityPolicy: SecurityPolicy): number {
if (securityMode === MessageSecurityMode.None) {
return 1;
}
let offset = 100;
if (securityMode === MessageSecurityMode.SignAndEncrypt) {
offset = 200;
}
switch (securityPolicy) {
case SecurityPolicy.Basic128:
case SecurityPolicy.Basic128Rsa15:
case SecurityPolicy.Basic192:
return 2; // deprecated => low
case SecurityPolicy.Basic192Rsa15:
return 3; // deprecated => low
case SecurityPolicy.Basic256:
return 4; // deprecated => low
case SecurityPolicy.Basic256Rsa15:
return 4 + offset;
case SecurityPolicy.Aes128_Sha256_RsaOaep:
return 5 + offset;
case SecurityPolicy.Basic256Sha256:
return 6 + offset;
case SecurityPolicy.Aes256_Sha256_RsaPss:
return 7 + offset;
default:
case SecurityPolicy.None:
return 1;
}
}
/**
* @private
*/
function _makeEndpointDescription(options: MakeEndpointDescriptionOptions, parent: OPCUAServerEndPoint): EndpointDescriptionEx {
assert(Object.prototype.hasOwnProperty.call(options, "serverCertificateChain"));
assert(!Object.prototype.hasOwnProperty.call(options, "serverCertificate"));
assert(!!options.securityMode); // s.MessageSecurityMode
assert(!!options.securityPolicy);
assert(options.server !== null && typeof options.server === "object");
assert(!!options.hostname && typeof options.hostname === "string");
assert(typeof options.restricted === "boolean");
const u = (n: string) => getUniqueName(n, options.collection);
options.securityLevel =
options.securityLevel === undefined
? estimateSecurityLevel(options.securityMode, options.securityPolicy)
: options.securityLevel;
assert(isFinite(options.securityLevel), "expecting a valid securityLevel");
const securityPolicyUri = toURI(options.securityPolicy);
const userIdentityTokens: UserTokenPolicyOptions[] = [];
const registerIdentity2 = (tokenType: UserTokenType, securityPolicy: SecurityPolicy, name: string) => {
return registerIdentity({
policyId: u(name),
tokenType,
issuedTokenType: null,
issuerEndpointUrl: null,
securityPolicyUri: securityPolicy
});
};
const registerIdentity = (r: UserTokenPolicyOptions) => {
const tokenType = r.tokenType === undefined ? UserTokenType.Invalid : r.tokenType;
const securityPolicy = (r.securityPolicyUri || "") as SecurityPolicy;
if (!securityPolicy && options.userTokenTypes.indexOf(tokenType) >= 0) {
userIdentityTokens.push(r);
return;
}
if (options.securityPolicies.indexOf(securityPolicy) >= 0 && options.userTokenTypes.indexOf(tokenType) >= 0) {
userIdentityTokens.push(r);
}
};
if (!options.noUserIdentityTokens) {
if (options.securityPolicy === SecurityPolicy.None) {
if (options.allowUnsecurePassword) {
registerIdentity({
policyId: u("username_unsecure"),
tokenType: UserTokenType.UserName,
issuedTokenType: null,
issuerEndpointUrl: null,
securityPolicyUri: null
});
}
const onlyCertificateLessConnection =
options.onlyCertificateLessConnection === undefined ? false : options.onlyCertificateLessConnection;
if (!onlyCertificateLessConnection) {
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Basic256, "username_basic256");
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Basic128Rsa15, "username_basic128Rsa15");
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Basic256Sha256, "username_basic256Sha256");
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Aes128_Sha256_RsaOaep, "username_aes128Sha256RsaOaep");
// X509
registerIdentity2(UserTokenType.Certificate, SecurityPolicy.Basic256, "certificate_basic256");
registerIdentity2(UserTokenType.Certificate, SecurityPolicy.Basic128Rsa15, "certificate_basic128Rsa15");
registerIdentity2(UserTokenType.Certificate, SecurityPolicy.Basic256Sha256, "certificate_basic256Sha256");
registerIdentity2(
UserTokenType.Certificate,
SecurityPolicy.Aes128_Sha256_RsaOaep,
"certificate_aes128Sha256RsaOaep"
);
}
} else {
// note:
// when channel session security is not "None",
// userIdentityTokens can be left to null.
// in this case this mean that secure policy will be the same as connection security policy
// istanbul ignore next
if (process.env.NODEOPCUA_SERVER_EMULATE_SIEMENS) {
// However, for some reason SIEMENS plc requires that password get encrypted even though
// the secure channel is also encrypted ....
// you can set the NODEOPCUA_SERVER_EMULATE_SIEMENS env variable to simulate this behavior
const registerIdentity3 = (tokenType: UserTokenType, securityPolicy: SecurityPolicy, name: string) => {
const identity = {
policyId: u(name),
tokenType,
issuedTokenType: null,
issuerEndpointUrl: null,
securityPolicyUri: securityPolicy
};
userIdentityTokens.push(identity);
};
registerIdentity3(UserTokenType.UserName, SecurityPolicy.Basic256, "username_basic256");
registerIdentity3(UserTokenType.UserName, SecurityPolicy.Basic128Rsa15, "username_basic128Rsa15");
registerIdentity3(UserTokenType.UserName, SecurityPolicy.Basic256Sha256, "username_basic256Sha256");
} else {
registerIdentity({
policyId: u("usernamePassword"),
tokenType: UserTokenType.UserName,
issuedTokenType: null,
issuerEndpointUrl: null,
securityPolicyUri: null
});
registerIdentity({
policyId: u("certificateX509"),
tokenType: UserTokenType.Certificate,
issuedTokenType: null,
issuerEndpointUrl: null,
securityPolicyUri: null
});
}
}
registerIdentity({
policyId: u("anonymous"),
tokenType: UserTokenType.Anonymous,
issuedTokenType: null,
issuerEndpointUrl: null,
securityPolicyUri: null
});
}
// return the endpoint object
const endpoint = new EndpointDescription({
endpointUrl: "<to be evaluated at run time>", // options.endpointUrl,
server: undefined, // options.server,
serverCertificate: options.serverCertificateChain,
securityMode: options.securityMode,
securityPolicyUri,
userIdentityTokens,
securityLevel: options.securityLevel,
transportProfileUri: default_transportProfileUri
}) as EndpointDescriptionEx;
endpoint._parent = parent;
// endpointUrl is dynamic as port number may be adjusted
// when the tcp socket start listening
(endpoint as any).__defineGetter__("endpointUrl", () => {
const port = endpoint._parent.port;
const resourcePath = options.resourcePath || "";
const hostname = options.hostname;
const endpointUrl = `opc.tcp://${hostname}:${port}${resourcePath}`;
return resolveFullyQualifiedDomainName(endpointUrl);
});
endpoint.server = options.server;
endpoint.restricted = options.restricted;
return endpoint;
}
/**
* return true if the end point matches security mode and policy
* @param endpoint
* @param securityMode
* @param securityPolicy
* @internal
*
*/
function matching_endpoint(
securityMode: MessageSecurityMode,
securityPolicy: SecurityPolicy,
endpointUrl: string | null,
endpoint: EndpointDescription
): boolean {
assert(endpoint instanceof EndpointDescription);
const endpoint_securityPolicy = fromURI(endpoint.securityPolicyUri);
if (endpointUrl && endpoint.endpointUrl! !== endpointUrl) {
return false;
}
return endpoint.securityMode === securityMode && endpoint_securityPolicy === securityPolicy;
}
const defaultSecurityModes = [MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt];
const defaultSecurityPolicies = [
// now deprecated Basic128Rs15 shall be disabled by default
// see https://profiles.opcfoundation.org/profile/1532
// SecurityPolicy.Basic128Rsa15,
// now deprecated Basic256 shall be disabled by default
// see https://profiles.opcfoundation.org/profile/2062
// SecurityPolicy.Basic256,
// xx UNUSED!! SecurityPolicy.Basic192Rsa15,
// xx UNUSED!! SecurityPolicy.Basic256Rsa15,
SecurityPolicy.Basic256Sha256,
SecurityPolicy.Aes128_Sha256_RsaOaep,
SecurityPolicy.Aes256_Sha256_RsaPss
];
const defaultUserTokenTypes = [
UserTokenType.Anonymous,
UserTokenType.UserName,
UserTokenType.Certificate
// NOT USED YET : UserTokenType.IssuedToken
];