node-opcua
Advanced tools
Comparing version 0.0.1 to 0.0.2
var assert = require("assert"); | ||
var util = require("util"); | ||
var Writable = require("stream").Writable; | ||
var Readable = require("stream").Readable; | ||
function BinaryReader(buffer) { | ||
this._buffer = buffer; | ||
Readable.call(this, {}); | ||
} | ||
util.inherits(BinaryReader, Readable); | ||
exports.BinaryReader = BinaryReader; | ||
BinaryReader.prototype._read = function() { | ||
this.push(this._buffer); | ||
this.push(null); | ||
}; | ||
function BinaryStream(data) { | ||
if (data === undefined) { | ||
this.stream = new Buffer(1000); | ||
this._buffer = new Buffer(1024); | ||
} else { | ||
this.stream = new Buffer(data, "binary"); | ||
if (data instanceof Buffer) { | ||
this._buffer = data; | ||
} else { | ||
this._buffer = new Buffer(data, "binary"); | ||
} | ||
} | ||
@@ -17,5 +37,11 @@ this.length = 0; | ||
BinaryStream.prototype.writeUByte = function (value) { | ||
assert(value >= 0 && value < 256); | ||
this._buffer.writeInt8(value, this.length); | ||
this.length += 1; | ||
}; | ||
BinaryStream.prototype.writeByte = function (value) { | ||
assert(value >= 0 && value < 256); | ||
this.stream.writeInt8(value, this.length); | ||
this._buffer.writeInt8(value, this.length); | ||
this.length += 1; | ||
@@ -26,3 +52,3 @@ }; | ||
assert(value >= 0 && value < 256); | ||
this.stream.writeUInt8(value, this.length); | ||
this._buffer.writeUInt8(value, this.length); | ||
this.length += 1; | ||
@@ -32,3 +58,3 @@ }; | ||
BinaryStream.prototype.writeInt16 = function (value) { | ||
this.stream.writeInt16LE(value, this.length); | ||
this._buffer.writeInt16LE(value, this.length); | ||
this.length += 2; | ||
@@ -38,3 +64,3 @@ }; | ||
BinaryStream.prototype.writeInteger = function (value) { | ||
this.stream.writeInt32LE(value, this.length); | ||
this._buffer.writeInt32LE(value, this.length); | ||
this.length += 4; | ||
@@ -44,3 +70,3 @@ }; | ||
BinaryStream.prototype.writeUInt32 = function (value) { | ||
this.stream.writeUInt32LE(value, this.length); | ||
this._buffer.writeUInt32LE(value, this.length); | ||
this.length += 4; | ||
@@ -50,3 +76,3 @@ }; | ||
BinaryStream.prototype.writeUInt16 = function (value) { | ||
this.stream.writeUInt16LE(value, this.length); | ||
this._buffer.writeUInt16LE(value, this.length); | ||
this.length += 2; | ||
@@ -56,3 +82,3 @@ }; | ||
BinaryStream.prototype.writeFloat = function (value) { | ||
this.stream.writeFloatLE(value, this.length); | ||
this._buffer.writeFloatLE(value, this.length); | ||
this.length += 4; | ||
@@ -62,3 +88,3 @@ }; | ||
BinaryStream.prototype.writeDouble = function (value) { | ||
this.stream.writeDoubleLE(value, this.length); | ||
this._buffer.writeDoubleLE(value, this.length); | ||
this.length += 8; | ||
@@ -68,3 +94,3 @@ }; | ||
BinaryStream.prototype.readByte = function () { | ||
var retVal = this.stream.readInt8(this.length); | ||
var retVal = this._buffer.readInt8(this.length); | ||
this.length += 1; | ||
@@ -75,3 +101,3 @@ return retVal; | ||
BinaryStream.prototype.readUInt8 = function () { | ||
var retVal = this.stream.readUInt8(this.length); | ||
var retVal = this._buffer.readUInt8(this.length); | ||
this.length += 1; | ||
@@ -82,3 +108,3 @@ return retVal; | ||
BinaryStream.prototype.readInt16 = function () { | ||
var retVal = this.stream.readInt16LE(this.length); | ||
var retVal = this._buffer.readInt16LE(this.length); | ||
this.length += 2; | ||
@@ -89,3 +115,3 @@ return retVal; | ||
BinaryStream.prototype.readInteger = function () { | ||
var retVal = this.stream.readInt32LE(this.length); | ||
var retVal = this._buffer.readInt32LE(this.length); | ||
this.length += 4; | ||
@@ -96,3 +122,3 @@ return retVal; | ||
BinaryStream.prototype.readUInt16 = function () { | ||
var retVal = this.stream.readUInt16LE(this.length); | ||
var retVal = this._buffer.readUInt16LE(this.length); | ||
this.length += 2; | ||
@@ -103,3 +129,3 @@ return retVal; | ||
BinaryStream.prototype.readUInt32 = function () { | ||
var retVal = this.stream.readUInt32LE(this.length); | ||
var retVal = this._buffer.readUInt32LE(this.length); | ||
this.length += 4; | ||
@@ -110,3 +136,3 @@ return retVal; | ||
BinaryStream.prototype.readFloat = function () { | ||
var retVal = this.stream.readFloatLE(this.length); | ||
var retVal = this._buffer.readFloatLE(this.length); | ||
this.length += 4; | ||
@@ -117,3 +143,3 @@ return retVal; | ||
BinaryStream.prototype.readDouble = function () { | ||
var retVal = this.stream.readDoubleLE(this.length); | ||
var retVal = this._buffer.readDoubleLE(this.length); | ||
this.length += 8; | ||
@@ -123,13 +149,31 @@ return retVal; | ||
BinaryStream.prototype.writeRaw = function (buf,length) { | ||
this._buffer.write(buf,this.length,length); | ||
this.length += length; | ||
}; | ||
BinaryStream.prototype.writeByteStream = function (buf) { | ||
if (!buf) { | ||
this.writeUInt32(0xFFFFFFFF); | ||
return; | ||
} | ||
var bufLen = buf.length; | ||
this.writeUInt32(buf.length); | ||
buf.copy(this.stream, this.length,0,buf.length); | ||
buf.copy(this._buffer, this.length,0,buf.length); | ||
this.length += bufLen; | ||
}; | ||
BinaryStream.prototype.readByteStream = function () { | ||
var bufLen = this.readUInt32(); | ||
if (bufLen === 0xFFFFFFFF) { | ||
return null; | ||
} | ||
if (bufLen === 0) { | ||
return new Buffer(0); | ||
} | ||
var buf = new Buffer(bufLen); | ||
this.stream.copy(buf,0,this.length,this.length+ bufLen); | ||
this._buffer.copy(buf,0,this.length,this.length+ bufLen); | ||
this.length += bufLen; | ||
@@ -140,1 +184,62 @@ return buf; | ||
exports.BinaryStream = BinaryStream; | ||
function BinaryStreamSizeCalculator() { | ||
this.length = 0; | ||
} | ||
BinaryStreamSizeCalculator.prototype.rewind = function () { | ||
this.length = 0; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeByte = function (value) { | ||
this.length += 1; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeUInt8 = function (value) { | ||
this.length += 1; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeInt16 = function (value) { | ||
this.length += 2; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeInteger = function (value) { | ||
this.length += 4; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeUInt32 = function (value) { | ||
this.length += 4; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeUInt16 = function (value) { | ||
this.length += 2; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeFloat = function (value) { | ||
this.length += 4; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeDouble = function (value) { | ||
this.length += 8; | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeByteStream = function (buf) { | ||
if(!buf) { | ||
this.writeUInt32(0); | ||
}else { | ||
this.writeUInt32(buf.length); | ||
this.length += buf.length; | ||
} | ||
}; | ||
BinaryStreamSizeCalculator.prototype.writeRaw = function (buf,length) { | ||
this.length += length; | ||
}; | ||
exports.BinaryStreamSizeCalculator = BinaryStreamSizeCalculator; |
@@ -13,3 +13,3 @@ var BinaryStream = require("./binaryStream").BinaryStream; | ||
} else { | ||
value = stream.stream.toString(encoding = 'binary', stream.length, stream.length + length); | ||
value = stream._buffer.toString('binary', stream.length, stream.length + length); | ||
stream.length += length; | ||
@@ -22,3 +22,3 @@ } | ||
if (value === undefined) { | ||
if (value === undefined || value === null) { | ||
stream.writeInteger(-1); | ||
@@ -28,6 +28,23 @@ return; | ||
stream.writeInteger(value.length); | ||
stream.stream.write(value, stream.length); | ||
stream.length += value.length; | ||
stream.writeRaw(value, value.length); | ||
}; | ||
exports.encodeUInt16 = function (value, stream) { | ||
stream.writeUInt16(value); | ||
}; | ||
exports.decodeUInt16 = function (stream) { | ||
return stream.readUInt16(); | ||
}; | ||
exports.encodeInt16 = function (value, stream) { | ||
stream.writeInt16(value); | ||
}; | ||
exports.decodeInt16 = function (stream) { | ||
return stream.readInt16(); | ||
}; | ||
exports.encodeInt32 = function (value, stream) { | ||
@@ -41,2 +58,3 @@ stream.writeInteger(value); | ||
exports.encodeUInt32 = function (value, stream) { | ||
@@ -56,3 +74,3 @@ stream.writeUInt32(value); | ||
exports.decodeBoolean = function (stream) { | ||
return value = stream.readByte() ? true : false; | ||
return stream.readByte() ? true : false; | ||
}; | ||
@@ -65,5 +83,13 @@ | ||
exports.decodeByte = function (stream) { | ||
return value = stream.readByte(); | ||
return stream.readByte(); | ||
}; | ||
exports.encodeUInt8 = function (value, stream) { | ||
stream.writeUInt8(value); | ||
}; | ||
exports.decodeUInt8 = function (stream) { | ||
return stream.readUInt8(); | ||
}; | ||
exports.encodeFloat = function (value, stream) { | ||
@@ -74,3 +100,3 @@ stream.writeFloat(value); | ||
exports.decodeFloat = function (stream) { | ||
return value = stream.readFloat(); | ||
return stream.readFloat(); | ||
}; | ||
@@ -83,45 +109,80 @@ | ||
exports.decodeDouble = function (stream) { | ||
return value = stream.readDouble(); | ||
return stream.readDouble(); | ||
}; | ||
//xx var int64 = require("int64-native"); | ||
var offset1601 = function () { | ||
var offset_factor_1601 = function () { | ||
// utc = Date.UTC(this.value.getYear(),this.value.getMonth()); // Number of second since January 1970 | ||
var utc1600 = new Date(1601, 1, 1); | ||
var utc1970 = new Date(1970, 1, 1); | ||
var t1970 = utc1970.getTime(); | ||
var t1600 = utc1600.getTime(); | ||
return (t1970 - t1600); | ||
var utc_offset = (new Date().getTimezoneOffset() * 60000); | ||
var utc1600 = new Date(1601, 0, 1); | ||
var t1600 = utc1600.getTime() + utc_offset; | ||
var utc1600_plus_one_day = new Date(1601, 0, 1+1); | ||
var t1600_1d = utc1600_plus_one_day.getTime() + utc_offset; | ||
var factor = (24*60*60*1000)*10000/(t1600_1d-t1600); | ||
var utc1970 = new Date(1970, 0, 1, 0, 0); | ||
var t1970 = utc1970.getTime() + utc_offset; | ||
var offset = -t1600 + t1970; | ||
assert( factor === 10000); | ||
assert( offset === 11644473600000); | ||
return [offset,factor]; | ||
}(); | ||
exports.encodeDateTime = function (date, stream) { | ||
var t = date.getTime(); // number of millisecond since 1/1/70 | ||
assert(new Date(t).getTime() == t); | ||
function dateToHundredNanoSecondFrom1601(date) { | ||
var span = t + offset1601; | ||
var high = Math.floor(span / 0xffffffff); | ||
var low = span % 0xffffffff; | ||
assert( date instanceof Date); | ||
var t = date.getTime(); // number of milliseconds since 1/1/70 | ||
assert(new Date(t).getTime() === t); | ||
var check_value = high * 0xffffffff + low; | ||
assert(check_value === span); | ||
var offset = offset_factor_1601[0]; | ||
var factor = offset_factor_1601[1]; | ||
stream.writeUInt32(high); | ||
stream.writeUInt32(low); | ||
return (t + offset)*factor; | ||
} | ||
}; | ||
exports.dateToHundredNanoSecondFrom1601 = dateToHundredNanoSecondFrom1601; | ||
exports.decodeDateTime = function (stream) { | ||
function hundredNanoSecondFrom1601ToDate(value) { | ||
var offset = offset_factor_1601[0]; | ||
var factor = offset_factor_1601[1]; | ||
value = value/factor - offset; | ||
return new Date(value); | ||
} | ||
var high = stream.readUInt32(); | ||
var low = stream.readUInt32(); | ||
var value = (high * 0xffffffff) + low; | ||
value -= offset1601; | ||
exports.hundredNanoSecondFrom1601ToDate = hundredNanoSecondFrom1601ToDate; | ||
var d = new Date(value); | ||
return d; | ||
// 0123456789012345 | ||
var MAX_INT64 = 0x8FFFFFFFFFFFFFFF; | ||
exports.encodeDateTime = function(date, stream) { | ||
assert(date instanceof Date); | ||
var longValue = dateToHundredNanoSecondFrom1601(date); | ||
assert(longValue>=0 && longValue <= MAX_INT64); | ||
var lo = longValue % 0xFFFFFFFF; | ||
var hi = Math.floor(longValue / 0xFFFFFFFF); | ||
stream.writeUInt32(lo); | ||
stream.writeUInt32(hi); | ||
}; | ||
exports.decodeDateTime = function(stream) { | ||
var lo= stream.readUInt32(); | ||
var hi= stream.readUInt32(); | ||
var value = hi * 0xFFFFFFFF + lo; | ||
return hundredNanoSecondFrom1601ToDate(value); | ||
}; | ||
exports.isValidGUID = function (guid) { | ||
@@ -185,27 +246,120 @@ assert(guid.length == 36) | ||
exports.makeNodeId = function makeNodeId(value,namespace) { | ||
var Enum = require("enum"); | ||
var NodeIdType = new Enum({ | ||
NUMERIC: 0x01, | ||
STRING: 0x02, | ||
GUID: 0x03, | ||
BYTESTRING: 0x04 | ||
}); | ||
exports.NodeIdType = NodeIdType; | ||
var re = new RegExp("\"", 'g'); | ||
function nodeId_toString() { | ||
return JSON.stringify(this).replace(re,""); | ||
} | ||
var makeNodeId = function makeNodeId(value,namespace) { | ||
value = value || 0; | ||
namespace = namespace || 0; | ||
return { value: value, namespace: namespace}; | ||
var identifierType = NodeIdType.NUMERIC; | ||
if (typeof value == "string" ) { | ||
identifierType= NodeIdType.STRING; | ||
} else if ( value instanceof Buffer) { | ||
identifierType= NodeIdType.BYTESTRING; | ||
} | ||
var nodeId = { identifierType: identifierType , value: value, namespace: namespace}; | ||
nodeId.toString = nodeId_toString; | ||
assert(nodeId.hasOwnProperty("identifierType")); | ||
return nodeId; | ||
}; | ||
exports.makeNodeId = makeNodeId; | ||
var EnumNodeIdEncoding = new Enum({ | ||
TwoBytes: 0x00, // A numeric value that fits into the two byte representation. | ||
FourBytes: 0x01, // A numeric value that fits into the four byte representation. | ||
Numeric: 0x02, // A numeric value that does not fit into the two or four byte representations. | ||
String: 0x03, // A String value. | ||
Guid: 0x04, // A Guid value. | ||
ByteString: 0x05, // An opaque (ByteString) value. | ||
NamespaceUriFlag: 0x80, // NamespaceUriFlag on ExpandedNodeId is present | ||
ServerIndexFlag: 0x40 // NamespaceUriFlag on ExpandedNodeId is present | ||
}); | ||
function nodeID_encodingByte(nodeId) | ||
{ | ||
assert(nodeId.hasOwnProperty("identifierType")); | ||
var encodingByte = 0; | ||
if (nodeId.identifierType === NodeIdType.NUMERIC) { | ||
if (nodeId.value < 255 && (!nodeId.namespace) && !nodeId.namespaceUri && !nodeId.serverIndex) { | ||
return EnumNodeIdEncoding.TwoBytes; | ||
} else if (nodeId.value <= 65535 && nodeId.namespace >= 0 && nodeId.namespace <= 255 && !nodeId.namespaceUri && !nodeId.serverIndex) { | ||
return EnumNodeIdEncoding.FourBytes; | ||
} else { | ||
encodingByte = EnumNodeIdEncoding.Numeric; | ||
} | ||
} else if (nodeId.identifierType === NodeIdType.STRING) { | ||
encodingByte = EnumNodeIdEncoding.String; | ||
} else if (nodeId.identifierType === NodeIdType.BYTESTRING ) { | ||
encodingByte = EnumNodeIdEncoding.ByteString; | ||
} else if (nodeId.identifierType === NodeIdType.GUID ) { | ||
encodingByte = EnumNodeIdEncoding.Guid; | ||
} | ||
if (nodeId.hasOwnProperty("namespaceUri") && nodeId.namespaceUri ) { | ||
encodingByte |= EnumNodeIdEncoding.NamespaceUriFlag; | ||
} | ||
if (nodeId.hasOwnProperty("serverIndexFlag") && nodeId.serverIndexFlag ) { | ||
encodingByte |= EnumNodeIdEncoding.ServerIndexFlag; | ||
} | ||
return encodingByte; | ||
} | ||
exports.encodeNodeId = function(nodeId,stream) | ||
{ | ||
if (typeof nodeId.value === 'number') { | ||
if (nodeId.value < 255 && nodeId.namespace == 0) { | ||
// 2 byte encoding | ||
stream.writeByte(0);// encoding byte | ||
var encoding_byte = nodeID_encodingByte(nodeId); | ||
stream.writeByte(encoding_byte.value);// encoding byte | ||
switch(encoding_byte) { | ||
case EnumNodeIdEncoding.TwoBytes: | ||
stream.writeByte(nodeId.value); | ||
} else if (nodeId.value <= 65535 && nodeId.namespace >= 0 && nodeId.namespace <= 255) { | ||
stream.writeByte(1); // encoding byte | ||
stream.writeByte(nodeId.namespace); // encoding byte | ||
break; | ||
case EnumNodeIdEncoding.FourBytes: | ||
stream.writeByte(nodeId.namespace); | ||
stream.writeInt16(nodeId.value); | ||
} else { | ||
throw " cannot encode node id "+ util.inspect(nodeId); | ||
} | ||
} else { | ||
stream.writeByte(3); // UAString | ||
stream.writeInt16(nodeId.namespace); | ||
ec.encodeUAString(nodeId.value,stream); | ||
break; | ||
case EnumNodeIdEncoding.Numeric: | ||
stream.writeInt16(nodeId.namespace); | ||
stream.writeUInt32(nodeId.value); | ||
break; | ||
case EnumNodeIdEncoding.String: | ||
stream.writeInt16(nodeId.namespace); | ||
ec.encodeUAString(nodeId.value,stream); | ||
break; | ||
case EnumNodeIdEncoding.ByteString: | ||
assert(nodeId.value instanceof Buffer); | ||
stream.writeInt16(nodeId.namespace); | ||
ec.encodeByteString(nodeId.value,stream); | ||
break; | ||
default: | ||
throw new Error("encodeNodeId: invalid nodeId { or Not Implemented yet} " + encoding_byte); | ||
break; | ||
} | ||
@@ -215,8 +369,115 @@ | ||
var _decodeNodeId = function(encoding_byte,stream) { | ||
var nodeId = { value: 0, namespace: null}; | ||
var value,namespace; | ||
switch(encoding_byte) { | ||
case EnumNodeIdEncoding.TwoBytes: | ||
value = stream.readByte(); | ||
break; | ||
case EnumNodeIdEncoding.FourBytes: | ||
namespace = stream.readByte(); | ||
value = stream.readInt16(); | ||
break; | ||
case EnumNodeIdEncoding.Numeric: | ||
namespace = stream.readInt16(); | ||
value = stream.readUInt32(stream); | ||
break; | ||
case EnumNodeIdEncoding.String: | ||
namespace = stream.readInt16(); | ||
value = ec.decodeUAString(stream); | ||
break; | ||
case EnumNodeIdEncoding.ByteString: | ||
namespace = stream.readInt16(); | ||
value = ec.decodeByteString(stream); | ||
break; | ||
default: | ||
throw new Error("_decodeNodeId : invalid nodeId { or Not Implemented yet} "+encoding_byte ); | ||
break; | ||
} | ||
return makeNodeId(value,namespace); | ||
}; | ||
exports.decodeNodeId = function(stream) { | ||
var encoding_byte = EnumNodeIdEncoding.get(stream.readByte()); | ||
return _decodeNodeId(encoding_byte,stream); | ||
}; | ||
//------ | ||
// An ExpandedNodeId extends the NodeId structure by allowing the NamespaceUri to be | ||
// explicitly specified instead of using the NamespaceIndex. The NamespaceUri is optional. If it | ||
// is specified then the NamespaceIndex inside the NodeId shall be ignored. | ||
// | ||
// The ExpandedNodeId is encoded by first encoding a NodeId as described in Clause 5 .2.2.9 | ||
// and then encoding NamespaceUri as a String. | ||
// | ||
// An instance of an ExpandedNodeId may still use the NamespaceIndex instead of the | ||
// NamespaceUri. In this case, the NamespaceUri is not encoded in the stream. The presence of | ||
// the NamespaceUri in the stream is indicated by setting the NamespaceUri flag in the encoding | ||
// format byte for the NodeId. | ||
// | ||
// If the NamespaceUri is present then the encoder shall encode the NamespaceIndex as 0 in | ||
// the stream when the NodeId portion is encoded. The unused NamespaceIndex is included in | ||
// the stream for consistency, | ||
// | ||
// An ExpandedNodeId may also have a ServerIndex which is encoded as a UInt32 after the | ||
// NamespaceUri. The ServerIndex flag in the NodeId encoding byte indicates whether the | ||
// ServerIndex is present in the stream. The ServerIndex is omitted if it is equal to zero. | ||
// | ||
exports.makeExpandedNodeId = function makeExpandedNodeId(value,namespace) { | ||
value = parseInt(value) || 0; | ||
namespace = namespace || 0; | ||
serverIndex = 0; | ||
namespaceUri = null; | ||
var nodeId= { | ||
identifierType: NodeIdType.NUMERIC, | ||
value: value, | ||
namespace: namespace, | ||
namespaceUri: namespaceUri, | ||
serverIndex: serverIndex | ||
}; | ||
nodeId.toString = nodeId_toString; | ||
return nodeId; | ||
}; | ||
exports.encodeExpandedNodeId = function(expandedNodeId, stream) { | ||
var encodingByte = nodeID_encodingByte(expandedNodeId); | ||
exports.encodeNodeId(expandedNodeId,stream); | ||
if (encodingByte.has(EnumNodeIdEncoding.NamespaceUriFlag)) { | ||
ec.encodeString(expandedNodeId.namespaceUri,stream); | ||
} | ||
if(encodingByte.has(EnumNodeIdEncoding.ServerIndexFlag)) { | ||
ec.encodeUInt32(expandedNodeId.serverIndex,stream); | ||
} | ||
}; | ||
exports.decodeExpandedNodeId = function(stream) { | ||
var encoding_byte = EnumNodeIdEncoding.get(stream.readByte()); | ||
var expandedNodeId = _decodeNodeId(encoding_byte,stream); | ||
expandedNodeId.namespaceUri = null; | ||
expandedNodeId.serverIndex = 0; | ||
if (encoding_byte.has(EnumNodeIdEncoding.NamespaceUriFlag)) { | ||
expandedNodeId.namespaceUri = ec.encodeString(stream); | ||
} | ||
if(encoding_byte.has(EnumNodeIdEncoding.ServerIndexFlag)) { | ||
expandedNodeId.serverIndex = ec.encodeUInt32(stream); | ||
} | ||
return expandedNodeId; | ||
}; | ||
exports.encodeLocaleId = ec.encodeUAString; | ||
exports.decodeLocaleId = ec.decodeUAString; | ||
exports.validateLocaleId = function( value) { | ||
// TODO : check that localeID is wellformed | ||
exports.decodeLocaleId = ec.decodeUAString; | ||
exports.validateLocaleId = function( /*value*/) { | ||
// TODO : check that localeID is well-formed | ||
// see part 3 $8.4 page 63 | ||
return true; | ||
@@ -223,0 +484,0 @@ }; |
var assert = require("assert"); | ||
var ec = require("./encode_decode"); | ||
var util =require("util"); | ||
var util = require("util"); | ||
require('enum').register(); | ||
var _ = require("underscore"); | ||
var hexDump = require("./utils").hexDump; | ||
var objectNodeIds = require("./opcua_node_ids").Object; | ||
factories = {}; | ||
@@ -12,11 +17,20 @@ _enumerations = {}; | ||
{ name: "UAString" , encode: ec.encodeUAString, decode: ec.decodeUAString , defaultValue: ""}, | ||
{ name: "Byte", encode: ec.encodeByte, decode: ec.decodeByte, defaultValue: 0 }, | ||
{ name: "Integer", encode: ec.encodeInt32, decode: ec.decodeInt32, defaultValue: 0 }, | ||
{ name: "Int32", encode: ec.encodeInt32, decode: ec.decodeInt32, defaultValue: 0 }, | ||
{ name: "UInt32", encode: ec.encodeUInt32, decode: ec.decodeUInt32, defaultValue: 0 }, | ||
{ name: "Double", encode: ec.encodeDouble, decode: ec.decodeDouble, defaultValue: 0.0 }, | ||
{ name: "Float", encode: ec.encodeFloat, decode: ec.decodeFloat, defaultValue: 0.0 }, | ||
{ name: "UAString", encode: ec.encodeUAString, decode: ec.decodeUAString, defaultValue: ""}, | ||
{ name: "String", encode: ec.encodeUAString, decode: ec.decodeUAString, defaultValue: ""}, | ||
{ name: "Byte", encode: ec.encodeByte, decode: ec.decodeByte, defaultValue: 0 }, | ||
{ name: "Integer", encode: ec.encodeInt32, decode: ec.decodeInt32, defaultValue: 0 }, | ||
{ name: "Int32", encode: ec.encodeInt32, decode: ec.decodeInt32, defaultValue: 0 }, | ||
{ name: "UInt32", encode: ec.encodeUInt32, decode: ec.decodeUInt32, defaultValue: 0 }, | ||
{ name: "Int16", encode: ec.encodeInt16, decode: ec.decodeInt16, defaultValue: 0 }, | ||
{ name: "UInt16", encode: ec.encodeUInt16, decode: ec.decodeUInt16, defaultValue: 0 }, | ||
{ name: "Double", encode: ec.encodeDouble, decode: ec.decodeDouble, defaultValue: 0.0 }, | ||
{ name: "Boolean", encode: ec.encodeBoolean, decode: ec.decodeBoolean, defaultValue: false }, | ||
// OPC Unified Architecture, part 3.0 $8.13 page 65 | ||
{ name: "Duration", encode: ec.encodeDouble, decode: ec.decodeDouble, defaultValue: 0.0 }, | ||
{ name: "Float", encode: ec.encodeFloat, decode: ec.decodeFloat, defaultValue: 0.0 }, | ||
// OPC Unified Architecture, part 3.0 $8.26 page 67 | ||
{ name: "UtcTime", encode: ec.encodeDateTime, decode: ec.decodeDateTime, defaultValue: new Date() }, | ||
{ name: "UtcTime", encode: ec.encodeDateTime, decode: ec.decodeDateTime, defaultValue: new Date() }, | ||
@@ -26,21 +40,67 @@ // OPC Unified Architecture, part 4.0 $7.13 | ||
// except for 0, are valid. | ||
{ name: "IntegerId", encode: ec.encodeUInt32, decode: ec.decodeUInt32, defaultValue: 0xFFFFFFFF }, | ||
{ name: "IntegerId", encode: ec.encodeUInt32, decode: ec.decodeUInt32, defaultValue: 0xFFFFFFFF }, | ||
// string in the form "en-US" or "de-DE" or "fr" etc... | ||
{ name: "LocaleId", encode: ec.encodeLocaleId, decode: ec.decodeLocaleId, validate:ec.validateLocaleId, defaultValue: "en" }, | ||
{ name: "LocaleId", encode: ec.encodeLocaleId, decode: ec.decodeLocaleId, validate: ec.validateLocaleId, defaultValue: "en" }, | ||
{ name: "NodeId", encode: ec.encodeNodeId, decode: ec.decodeNodeId, defaultValue: ec.makeNodeId() }, | ||
{ name: "NodeId", encode: ec.encodeNodeId, decode: ec.decodeNodeId, defaultValue: ec.makeNodeId() }, | ||
{ name: "ExpandedNodeId", encode: ec.encodeExpandedNodeId, decode: ec.decodeExpandedNodeId, defaultValue: ec.makeExpandedNodeId() }, | ||
{ name: "ByteString", encode: ec.encodeByteString, decode: ec.decodeByteString, defaultValue: null } | ||
{ name: "ByteString", encode: ec.encodeByteString, decode: ec.decodeByteString, defaultValue: function () { | ||
return new Buffer(0); | ||
} }, | ||
{ name: "ExtensionObject", encode: encodeExtensionObject, decode: decodeExtensionObject, defaultValue: function () { | ||
return null; | ||
} } | ||
]; | ||
var _defaultTypeMap = {}; _defaultType.forEach(function(d) {_defaultTypeMap[d.name] = d;}); | ||
function registerType(name,encodeFunc,decodeFunc,defaultValue) | ||
{ | ||
// OPC-UA Part 6 - $5.2.2.15 ExtensionObject | ||
// AnExtensionObjectis encoded as sequence of bytes prefixed by the NodeIdof its | ||
// DataTypeEncodingand the number of bytes encoded. | ||
function encodeExtensionObject(object, stream) { | ||
if (!object) { | ||
ec.encodeNodeId(object.makeNodeId(0), stream); | ||
stream.writeByte(0x00); // no body is encoded | ||
stream.writeUInt32(0); | ||
} else { | ||
ec.encodeNodeId(object.encodingDefaultBinary, stream); | ||
stream.writeByte(0x01); // 0x01 The body is encoded as a ByteString. | ||
stream.writeUInt32(object.binaryStoreSize()); | ||
object.encode(stream); | ||
} | ||
} | ||
function decodeExtensionObject(stream) { | ||
var nodeId = ec.decodeNodeId(stream); | ||
var encodingType = stream.readByte(); | ||
var length = stream.readUInt32(); | ||
if (nodeId.value == 0 || encodingType == 0) { | ||
return null; | ||
} | ||
var object = exports.constructObject(nodeId); | ||
if (object === null) { | ||
// this object is unknown to us .. | ||
stream.length += length; | ||
return null; | ||
} | ||
object.decode(stream); | ||
return object; | ||
} | ||
var _defaultTypeMap = {}; | ||
_defaultType.forEach(function (d) { | ||
_defaultTypeMap[d.name] = d; | ||
}); | ||
function registerType(name, encodeFunc, decodeFunc, defaultValue) { | ||
assert(_.isFunction(encodeFunc)); | ||
assert(_.isFunction(decodeFunc)); | ||
var obj = { | ||
name: name,encode:encodeFunc,decode:decodeFunc,defaultValue:defaultValue | ||
}; | ||
name: name, encode: encodeFunc, decode: decodeFunc, defaultValue: defaultValue | ||
}; | ||
_defaultType.push(obj); | ||
@@ -51,2 +111,3 @@ _defaultTypeMap[name] = obj; | ||
var constructorMap = {}; | ||
@@ -58,55 +119,226 @@ | ||
} | ||
throw "cannot find " + name + " in "+ util.inspect(_defaultTypeMap); | ||
throw "cannot find " + name + " in " + util.inspect(_defaultTypeMap); | ||
}; | ||
function _encode_by_type(obj,fieldType,stream) | ||
{ | ||
var _t = _defaultTypeMap[fieldType]; | ||
_t.encode(obj,stream); | ||
function _encode_by_type(obj, fieldType, stream) { | ||
try { | ||
var _t = _defaultTypeMap[fieldType]; | ||
_t.encode(obj, stream); | ||
} | ||
catch (err) { | ||
console.error("ERROR in _encode_by_type ".red + "cannot encode " + fieldType + " on " + util.inspect(obj)); | ||
console.error(util.inspect(err)); | ||
console.error(err.stack); | ||
} | ||
} | ||
function _decode_by_type(obj,fieldType,stream) | ||
{ | ||
function _decode_by_type(obj, fieldType, stream) { | ||
var _t = _defaultTypeMap[fieldType]; | ||
obj = _t.decode(stream); | ||
if (!_t.hasOwnProperty("decode") || (!_t.decode instanceof Function)) { | ||
console.error(" _decode_by_type :" , util.inspect(_t), util.inspect(obj)); | ||
} | ||
var obj = _t.decode(stream); | ||
return obj; | ||
} | ||
function _encode_member_(member, fieldType, stream) { | ||
if (_defaultTypeMap[fieldType]) { | ||
function _encode_(obj,objDesc,stream) | ||
{ | ||
objDesc.fields.forEach(function(field) { | ||
_encode_by_type(member, fieldType, stream); | ||
} else if (factories[fieldType]) { | ||
member.encode(stream); | ||
} else if (_enumerations[fieldType]) { | ||
// OPC Unified Architecture, Part 3 page 34 | ||
// Enumerations are always encoded as Int32 on the wire as defined in Part 6. | ||
stream.writeInteger(member.value); | ||
} else { | ||
throw new Error(" Invalid field" + fieldType); | ||
} | ||
} | ||
function _decode_member_(member, fieldType, stream, options) { | ||
var tracer = undefined; | ||
if (options) { | ||
tracer = options.tracer; | ||
} | ||
var cursor_before = stream.length; | ||
if (_defaultTypeMap[fieldType]) { | ||
member = _decode_by_type(member, fieldType, stream); | ||
if (tracer) { | ||
tracer.trace("member", options.name, member, cursor_before, stream.length,fieldType); | ||
} | ||
return member; | ||
} else if (factories[fieldType]) { | ||
//xx console.log(" decoding fieldType=",fieldType); | ||
member.decode(stream, options); | ||
} else if (_enumerations[fieldType]) { | ||
var typedEnum = _enumerations[fieldType].typedEnum; | ||
member = typedEnum.get(stream.readInteger()); | ||
if (tracer) { | ||
tracer.trace("member", options.name, member, cursor_before, stream.length,fieldType); | ||
} | ||
return member; | ||
} else { | ||
throw new Error(" Invalid field" + field.fieldType); | ||
} | ||
return member; | ||
} | ||
function _encode_(obj, objDesc, stream) { | ||
objDesc.fields.forEach(function (field) { | ||
if (obj.hasOwnProperty(field.name)) { | ||
_encode_by_type(obj[field.name],field.fieldType,stream); | ||
var member = obj[field.name]; | ||
var fieldType = field.fieldType; | ||
if (_.isArray(member)) { | ||
stream.writeUInt32(member.length); | ||
member.forEach(function (element) { | ||
_encode_member_(element, fieldType, stream); | ||
}); | ||
} else { | ||
_encode_member_(member, fieldType, stream); | ||
} | ||
} else { | ||
throw new Error(" Missing field " + field.name + " in object " + util.inspect(obj)); | ||
} | ||
}); | ||
} | ||
function _decode_(obj,objDesc,stream) | ||
{ | ||
objDesc.fields.forEach(function(field) { | ||
function _resolve_defaultValue(type_userValue, defaultValue) { | ||
defaultValue = defaultValue || type_userValue; | ||
if (_.isFunction(defaultValue)) { | ||
defaultValue = defaultValue.call(); | ||
} | ||
// this is a default type such as UAString or Integer | ||
return defaultValue; | ||
} | ||
function _build_default_value(fieldType, isArray, defaultValue) { | ||
if (isArray) { | ||
return []; | ||
} | ||
var sub_options, _constructor; | ||
if (_defaultTypeMap[fieldType]) { | ||
var _type = _defaultTypeMap[fieldType]; | ||
return _resolve_defaultValue(_type.defaultValue, defaultValue); | ||
} else if (factories[fieldType]) { | ||
sub_options = sub_options || {}; | ||
_constructor = factories[fieldType]; | ||
return callConstructor(_constructor, defaultValue); | ||
} else if (_enumerations[fieldType]) { | ||
console.error("Missing Enumeration"); | ||
} else { | ||
throw new Error(" Invalid field" + field.fieldType); | ||
} | ||
return null; | ||
} | ||
function _decode_(obj, objDesc, stream, options) { | ||
var tracer = undefined; | ||
if (options) { | ||
tracer = options.tracer; | ||
} | ||
if (tracer) { | ||
tracer.trace("start", options.name + "(" + objDesc.name + ")", stream.length, stream.length); | ||
} | ||
objDesc.fields.forEach(function (field) { | ||
if (obj.hasOwnProperty(field.name)) { | ||
obj[field.name] = _decode_by_type(obj[field.name],field.fieldType,stream); | ||
//xx console.error(" field.name " , field.name); | ||
var member = obj[field.name]; | ||
var fieldType = field.fieldType; | ||
if (_.isArray(member)) { | ||
assert(member.length === 0); | ||
var cursor_before = stream.length; | ||
var nb = stream.readUInt32(); | ||
if (options) { | ||
options.name = field.name + [] | ||
} | ||
if (tracer) { | ||
tracer.trace("start_array", field.name, nb, cursor_before, stream.length); | ||
} | ||
for (var i = 0; i < nb; i++) { | ||
var element = _build_default_value(fieldType, false, field.defaultValue); | ||
element = _decode_member_(element, fieldType, stream, options) || member; | ||
member.push(element); | ||
} | ||
if (tracer) { | ||
tracer.trace("end_array", field.name, stream.length - 4); | ||
} | ||
} else { | ||
if (options) { | ||
options.name = field.name; | ||
} | ||
obj[field.name] = _decode_member_(member, fieldType, stream, options) || member; | ||
} | ||
} | ||
}); | ||
if (tracer) { | ||
tracer.trace("end", objDesc.name, stream.length, stream.length); | ||
} | ||
} | ||
function installEnumProp(obj,name,Enum){ | ||
var private_name ="__"+name; | ||
function installEnumProp(obj, name, Enum) { | ||
var private_name = "__" + name; | ||
obj[private_name] = Enum.enums[0]; | ||
if (typeof(obj[param]) === "function") { | ||
return; // cannot do it !!! | ||
} | ||
if (Object.getOwnPropertyDescriptor(obj, name)) { | ||
return; | ||
} | ||
// create a array of possible value for the enum | ||
param = {}; | ||
var param = {}; | ||
param[name] = { | ||
set : function (value){ | ||
set: function (value) { | ||
if (!(value in Enum )) { | ||
throw "Invalid value provided for Enum " + name+ ": " + value; | ||
throw "Invalid value provided for Enum " + name + ": " + value; | ||
} | ||
this[private_name] = value; | ||
}, | ||
get : function() { | ||
get: function () { | ||
return this[private_name]; | ||
@@ -117,4 +349,4 @@ }, | ||
}; | ||
Object.defineProperties(obj,param); | ||
Object.defineProperty(obj,private_name,{ hidden: true, enumerable: false}); | ||
Object.defineProperties(obj, param); | ||
Object.defineProperty(obj, private_name, { hidden: true, enumerable: false}); | ||
} | ||
@@ -130,6 +362,176 @@ | ||
function _addSimpleField(fieldType, isArray, defaultValue) { | ||
function UAObjectFactoryBuild(_description) | ||
{ | ||
var description =_description; | ||
var _type = _defaultTypeMap[fieldType]; | ||
return _build_default_value(fieldType, isArray, defaultValue); | ||
} | ||
var constructObject = function (kind_of_field, self, field, extra, data) { | ||
var fieldType = field.fieldType; | ||
var fieldName = field.name; | ||
switch (kind_of_field) { | ||
case "enumeration": | ||
var typedEnum = _enumerations[fieldType].typedEnum; | ||
installEnumProp(self, fieldName, typedEnum); | ||
if (field.defaultValue) { | ||
self[fieldName] = _resolve_defaultValue(field.defaultValue); | ||
} | ||
break; | ||
case "basic": | ||
self[fieldName] = _addSimpleField(fieldType, field.isArray, field.defaultValue); | ||
break; | ||
case "complex": | ||
if (field.subtype) { | ||
// this is a synonymous | ||
fieldType = field.subType; | ||
callback("basic", self, fieldName, field); | ||
self[fieldName] = _addSimpleField(fieldType, field.isArray, field.defaultValue); | ||
} else { | ||
if (field.defaultValue == "null") { | ||
self[fieldName] = null; | ||
} else { | ||
// this is a complex type | ||
// find options related to this filed in options | ||
var sub_options = {}; | ||
if (fieldName in data.options) { | ||
sub_options = data.options[fieldName]; | ||
data.sub_option_to_ignore.push(fieldName); | ||
} | ||
self[fieldName] = _build_default_value(fieldType, field.isArray, sub_options); | ||
} | ||
} | ||
break; | ||
default: | ||
throw new Error("internal error kind_of_field"); | ||
} | ||
}; | ||
function r(str) { | ||
return (str + " ").substr(0, 30); | ||
} | ||
var _exploreObject = function (kind_of_field, self, field, extra, data) { | ||
var fieldType = field.fieldType; | ||
var fieldName = field.name; | ||
var padding = data.padding; | ||
var value = self[fieldName]; | ||
var str; | ||
switch (kind_of_field) { | ||
case "enumeration": | ||
var typedEnum = _enumerations[fieldType].typedEnum; | ||
str = r(padding + fieldName, 30) + " " + r(fieldType, 15) + " " + value.key + " ( " + value.value + ")"; | ||
data.lines.push(str); | ||
break; | ||
case "basic": | ||
if (value instanceof Buffer) { | ||
var _hexDump = hexDump(value); | ||
value = "<BUFFER>"; | ||
data.lines.push(r(padding + fieldName, 30) + " " + r(fieldType, 15)); | ||
data.lines.push(_hexDump); | ||
} else { | ||
if (fieldType == "IntegerId" || fieldType == "UInt32") { | ||
value = "" + value + " 0x" + value.toString(16); | ||
} | ||
str = r(padding + fieldName, 30) + " " + r(fieldType, 15) + " " + value; | ||
data.lines.push(str); | ||
} | ||
break; | ||
case "complex": | ||
if (field.subtype) { | ||
// this is a synonymous | ||
fieldType = field.subType; | ||
str = r(padding + fieldName, 30) + " " + r(fieldType, 15) + " " + value; | ||
data.lines.push(str); | ||
} else { | ||
var _new_desc = factories[fieldType].prototype._description; | ||
if (field.isArray) { | ||
data.lines.push(r(padding + fieldName, 30) + r(fieldType, 15) + ': ['); | ||
var i = 0; | ||
value.forEach(function (element) { | ||
var data1 = { padding: padding + " ", lines: []}; | ||
objectVisitor(element, _new_desc, data1, _exploreObject); | ||
data.lines.push(padding + i + ": {"); | ||
data.lines = data.lines.concat(data1.lines); | ||
data.lines.push(padding + "}"); | ||
i++; | ||
}); | ||
data.lines.push(r(padding + "", 30) + "]"); | ||
} else { | ||
data.lines.push(r(padding + fieldName, 30) + r(fieldType, 15) + "{") | ||
var data1 = { padding: padding + " ", lines: []}; | ||
objectVisitor(value, _new_desc, data1, _exploreObject); | ||
data.lines = data.lines.concat(data1.lines); | ||
data.lines.push(padding + "}") | ||
} | ||
} | ||
break; | ||
default: | ||
throw new Error("internal error: unknown kind_of_field"); | ||
} | ||
}; | ||
var explore = function () { | ||
var self = this; | ||
var data = { padding: " ", lines: []}; | ||
data.lines.push("message /*" + this._description.name + "*/ : {"); | ||
objectVisitor(self, self._description, data, _exploreObject); | ||
data.lines.push(" };"); | ||
return data.lines.join("\n"); | ||
}; | ||
var objectVisitor = function (object, description, data, callback) { | ||
var self = object; | ||
assert(description); | ||
description.fields.forEach(function (field) { | ||
var fieldType = field.fieldType; | ||
var fieldName = field.name; | ||
if (fieldType in _enumerations) { | ||
var typedEnum = _enumerations[fieldType].typedEnum; | ||
callback("enumeration", self, field, typedEnum, data); | ||
} else if (fieldType in _defaultTypeMap) { | ||
callback("basic", self, field, null, data); | ||
} else if (fieldType in factories) { | ||
callback("complex", self, field, null, data); | ||
} else { | ||
console.error(description); | ||
console.error("field = ",field); | ||
throw new Error("Invalid field type : " + fieldType + JSON.stringify(field) + " is not a default type nor a registered complex struct"); | ||
} | ||
}); | ||
}; | ||
function UAObjectFactoryBuild(_description) { | ||
var description = _description; | ||
var name = description.name; | ||
@@ -139,7 +541,9 @@ | ||
// create a new Enum | ||
var myEnum = new Enum(description.enumValues); | ||
var typedEnum = new Enum(description.enumValues); | ||
// xx if ( name in enumerations) { throw " already inserted"; } | ||
_enumerations[name] = description; | ||
return myEnum; | ||
_enumerations[name].typedEnum = typedEnum; | ||
return typedEnum; | ||
} | ||
if (description.hasOwnProperty("subtype")) { | ||
@@ -149,4 +553,7 @@ | ||
if (t === undefined) { | ||
throw " "+ util.inspect(description,{color: true}) +" cannot find subtype " + description.subtype; | ||
throw " " + util.inspect(description, {color: true}) + " cannot find subtype " + description.subtype; | ||
} | ||
//xx console.log(util.inspect(description)); | ||
assert(_.isFunction(t.encode)); | ||
assert(_.isFunction(t.decode)); | ||
registerType(name, t.encode, t.decode, t.defaultValue); | ||
@@ -156,6 +563,5 @@ return; | ||
var ClassConstructor = function (options) { | ||
var classConstructor; | ||
classConstructor = function (options) { | ||
assert(this != undefined, " keyword 'new' is required for constructor call"); | ||
var self = this; | ||
@@ -165,86 +571,103 @@ | ||
// construct default properties | ||
var sub_option_to_ignore = []; | ||
if (!description.fields) { | ||
console.log("description",description) | ||
throw new Error("fields missing in description " + description) | ||
} | ||
description.fields.forEach( function(field) { | ||
var fieldType = field.fieldType; | ||
var fieldName = field.name; | ||
var data = { | ||
options: options, | ||
sub_option_to_ignore: [] | ||
}; | ||
objectVisitor(self, description, data, constructObject); | ||
function _addSimpleField(fieldType,isArray,defaultValue) { | ||
_type = _defaultTypeMap[fieldType]; | ||
if(isArray) { | ||
return []; | ||
} else { | ||
defaultValue = defaultValue || _type.defaultValue; | ||
// this is a default type such as UAString or Integer | ||
return defaultValue; | ||
} | ||
for (var option in options) { | ||
if (options.hasOwnProperty(option) && data.sub_option_to_ignore.indexOf(option) == -1) { | ||
assert(this.hasOwnProperty(option), " invalid option specified " + option); | ||
assert(!(this[options] instanceof Object)); // | ||
this[option] = options[option]; | ||
} | ||
} | ||
if ( fieldType in _enumerations ) { | ||
// Prevents code from adding or deleting properties, or changing the descriptors of any property on an object. | ||
// Property values can be changed however. | ||
Object.seal(this); | ||
}; | ||
var typedEnum = new Enum(_enumerations[fieldType].enumValues); | ||
installEnumProp(self,fieldName,typedEnum); | ||
ClassConstructor.prototype.binaryStoreSize = function () { | ||
} else if ( fieldType in _defaultTypeMap) { | ||
var BinaryStreamSizeCalculator = require("../lib/binaryStream").BinaryStreamSizeCalculator; | ||
var stream = new BinaryStreamSizeCalculator(); | ||
this.encode(stream); | ||
return stream.length; | ||
}; | ||
self[fieldName] = _addSimpleField(fieldType,field.isArray, field.defaultValue); | ||
ClassConstructor.prototype.encode = function (stream) { | ||
if (description.encode) { | ||
// use the encode function specified in the description object instead of default one | ||
description.encode(this, stream); | ||
} else { | ||
_encode_(this, description, stream); | ||
} | ||
}; | ||
} else if (fieldType in factories) { | ||
ClassConstructor.prototype.decode = function (stream, options) { | ||
if (description.decode) { | ||
// use the decode function specified in the description object instead of default one | ||
description.decode(this, stream, options); | ||
} else { | ||
_decode_(this, description, stream, options); | ||
} | ||
}; | ||
if (field.subtype) { | ||
// this is a synonymous | ||
fieldType = field.subType; | ||
self[fieldName] = _addSimpleField(fieldType,field.isArray,field.defaultValue); | ||
} else { | ||
// this is a complex type | ||
// find options related to this filed in options | ||
sub_options = {}; | ||
if (fieldName in options) { | ||
sub_options = options[fieldName]; | ||
sub_option_to_ignore.push(fieldName); | ||
} | ||
if(field.isArray) { | ||
self[fieldName] = []; | ||
} else { | ||
_constructor = factories[fieldType]; | ||
self[fieldName] = callConstructor(_constructor, sub_options); | ||
} | ||
} | ||
var id = description.id; | ||
if (!id) { | ||
var encode_name = name + "_Encoding_DefaultBinary"; | ||
id = objectNodeIds[encode_name]; | ||
if (!id) { | ||
throw new Error(" " + name + " has no _Encoding_DefaultBinary id \n" | ||
+" please add a Id field in the structure definition" ); | ||
} | ||
} | ||
var expandedNodeId = ec.makeExpandedNodeId(id); | ||
} else { | ||
throw new Error("Invalid field type : " + fieldType + " is not a default type nor a registered complex struct"); | ||
} | ||
ClassConstructor.prototype.encodingDefaultBinary = expandedNodeId; | ||
ClassConstructor.prototype.constructor.name = name; | ||
ClassConstructor.prototype._description = description; | ||
ClassConstructor.prototype.explore = explore; | ||
}); | ||
if (name in factories) { | ||
throw new Error(" Class " + name + " already in factories"); | ||
} | ||
factories[name] = ClassConstructor; | ||
for (option in options) { | ||
if (options.hasOwnProperty(option) && sub_option_to_ignore.indexOf(option) == -1) { | ||
assert(!(this[options] instanceof Object)); // | ||
this[option] = options[option]; | ||
} | ||
if (expandedNodeId.value != 0) { | ||
if (expandedNodeId.value in constructorMap) { | ||
throw new Error(" Class " + name + " with ID " + expandedNodeId.value + " already in constructorMap"); | ||
} | ||
}; | ||
} | ||
constructorMap[expandedNodeId.value] = ClassConstructor; | ||
return ClassConstructor; | ||
} | ||
var getConstructor = function (expandedId) { | ||
if (!expandedId || !(expandedId.value in constructorMap)) { | ||
return null; | ||
} | ||
return constructorMap[expandedId.value]; | ||
}; | ||
exports.getConstructor = getConstructor; | ||
classConstructor.prototype.encode = function(stream) { | ||
_encode_(this,description,stream); | ||
}; | ||
classConstructor.prototype.decode = function(stream) { | ||
_decode_(this,description,stream); | ||
}; | ||
exports.constructObject = function (expandedId) { | ||
if (!expandedId || !(expandedId.value in constructorMap)) { | ||
throw new Error("constructObject: cannot find constructor for " + expandedId); | ||
} | ||
return new constructorMap[expandedId.value](); | ||
}; | ||
factories[name] = classConstructor; | ||
exports.UAObjectFactoryBuild = UAObjectFactoryBuild; | ||
return classConstructor; | ||
} | ||
exports.UAObjectFactoryBuild = UAObjectFactoryBuild; | ||
var _next_available_id = 0xFFFE0000; | ||
exports.next_available_id = function(){ | ||
_next_available_id +=1; | ||
return _next_available_id; | ||
} |
var assert = require("assert"); | ||
var BinaryStream = require("./binaryStream").BinaryStream; | ||
var ec = require("./encode_decode"); | ||
var s = require("./structures"); | ||
var factories = require("../lib/factories"); | ||
//=========================================================== | ||
function GUID(value) { | ||
// pattern should be | ||
// “72962B91-FA75-4AE6-8D28-B404DC7DAF63” | ||
this.guid = value; | ||
} | ||
GUID.prototype.encode = function (stream) { | ||
ec.encodeGUID(this.guid,stream); | ||
}; | ||
function Enum(possibleValues) { | ||
this.possibleValues = possibleValues; | ||
this.value = possibleValues[0]; | ||
} | ||
Enum.prototype.encode = function (binaryStream) { | ||
}; | ||
function NodeId(name) { | ||
this.namespace = 0; | ||
this.value = name; | ||
} | ||
NodeId.prototype.encode = function (binaryStream) { | ||
ec.encodeNodeId(this,binaryStream); | ||
}; | ||
function LocalizeText() { | ||
this.encodingMask = new Byte(); | ||
this.locale = ""; | ||
this.text = ""; | ||
} | ||
LocalizeText.prototype.encode = function (binaryStream) { | ||
}; | ||
LocalizeText.prototype.decode = function (binaryStream) { | ||
}; | ||
opcua = exports; | ||
@@ -56,9 +13,10 @@ | ||
name: "HelloMessage", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name: "protocolVersion" , fieldType: "Integer" }, | ||
{ name: "receiveBufferSize" , fieldType: "Integer" }, | ||
{ name: "sendBufferSize" , fieldType: "Integer" }, | ||
{ name: "maxMessageSize" , fieldType: "Integer" }, | ||
{ name: "maxChunkCount" , fieldType: "Integer" }, | ||
{ name: "endpointUrl" , fieldType: "UAString"} | ||
{ name: "protocolVersion" , fieldType: "UInt32" , description: "The latest version of the OPC UA TCP protocol supported by the Client"}, | ||
{ name: "receiveBufferSize" , fieldType: "UInt32" , description: "The largest message that the sender can receive."}, | ||
{ name: "sendBufferSize" , fieldType: "UInt32" , description: "The largest message that the sender will send." }, | ||
{ name: "maxMessageSize" , fieldType: "UInt32" , description: "The maximum size for any response message." }, | ||
{ name: "maxChunkCount" , fieldType: "UInt32" , description: "The maximum number of chunks in any response message"}, | ||
{ name: "endpointUrl" , fieldType: "UAString",description: "The URL of the Endpoint which the Client wished to connect to."} | ||
] | ||
@@ -69,8 +27,9 @@ }; | ||
name: "AcknowledgeMessage", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name: "protocolVersion" , fieldType: "Integer" }, | ||
{ name: "receiveBufferSize" , fieldType: "Integer" }, | ||
{ name: "sendBufferSize" , fieldType: "Integer" }, | ||
{ name: "maxMessageSize" , fieldType: "Integer" }, | ||
{ name: "maxChunkCount" , fieldType: "Integer" } | ||
{ name: "protocolVersion" , fieldType: "UInt32" , description: "The latest version of the OPC UA TCP protocol supported by the Server." }, | ||
{ name: "receiveBufferSize" , fieldType: "UInt32" }, | ||
{ name: "sendBufferSize" , fieldType: "UInt32" }, | ||
{ name: "maxMessageSize" , fieldType: "UInt32" , description: "The maximum size for any request message."}, | ||
{ name: "maxChunkCount" , fieldType: "UInt32" , description: "The maximum number of chunks in any request message." } | ||
] | ||
@@ -81,9 +40,9 @@ }; | ||
name: "ErrorMessage", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{name: "Error", fieldType: "UInt32", comment: "The numeric code for the error. This shall be one of the values listed in Table 40." }, | ||
{name: "Reason", fieldType: "String", comment: "A more verbose description of the error.This string shall not be more than 4096 characters." } | ||
{name: "Error", fieldType: "UInt32", description: "The numeric code for the error. This shall be one of the values listed in Table 40." }, | ||
{name: "Reason", fieldType: "String", description: "A more verbose description of the error.This string shall not be more than 4096 characters." } | ||
] | ||
}; | ||
var factories = require("../lib/factories"); | ||
var HelloMessage = factories.UAObjectFactoryBuild(HelloMessage_Description); | ||
@@ -93,3 +52,52 @@ var AcknowledgeMessage = factories.UAObjectFactoryBuild(AcknowledgeMessage_Description); | ||
exports.AcknowledgeMessage = AcknowledgeMessage; | ||
function is_valid_msg_type(msgType) { | ||
assert([ 'HEL', 'ACK', 'ERR', // Connection Layer | ||
'OPN', 'MSG', 'CLO' // OPC Unified Architecture, Part 6 page 36 | ||
].indexOf(msgType) >= 0, "invalid message type " + msgType); | ||
return true; | ||
} | ||
function readMessageHeader(stream) { | ||
assert(stream instanceof BinaryStream); | ||
var msgType = String.fromCharCode(stream.readUInt8()) + | ||
String.fromCharCode(stream.readUInt8()) + | ||
String.fromCharCode(stream.readUInt8()); | ||
var isFinal = String.fromCharCode(stream.readUInt8()); | ||
var length = stream.readUInt32(); | ||
return { msgType: msgType, isFinal: isFinal, length: length }; | ||
} | ||
exports.readMessageHeader = readMessageHeader; | ||
function decodeMessage(stream, className) { | ||
assert(stream instanceof BinaryStream); | ||
assert(className instanceof Function , " expecting a function for " + className); | ||
var header = readMessageHeader(stream); | ||
assert(stream.length == 8); | ||
var obj; | ||
if (header.msgType == "ERR") { | ||
//xx console.log(" received an error"); | ||
obj = new opcua.TCPErrorMessage(); | ||
obj.decode(stream); | ||
return obj; | ||
} else { | ||
obj = new className(); | ||
obj.decode(stream); | ||
return obj; | ||
} | ||
} | ||
/** | ||
@@ -103,66 +111,72 @@ * @name encodeMessage | ||
*/ | ||
encodeMessage = function (msgType, messageContent, stream) { | ||
// console.log(msgType) | ||
// console.log(['HEL','ACK','ERR']); | ||
assert(['HEL', 'ACK', 'ERR'].indexOf(msgType) >= 0); | ||
var writeTCPMessageHeader = function(msgType,chunkType,total_length,stream){ | ||
if (stream instanceof Buffer) { | ||
stream = new BinaryStream(stream) | ||
} | ||
assert(is_valid_msg_type(msgType)); | ||
assert(['A','F','C'].indexOf(chunkType) !== -1); | ||
stream.writeByte(msgType.charCodeAt(0)); | ||
stream.writeByte(msgType.charCodeAt(1)); | ||
stream.writeByte(msgType.charCodeAt(2)); | ||
stream.writeByte(0); // reserved | ||
stream.writeInteger(0); //the length of the message, in bytes. (includes the 8 bytes of the message header) | ||
messageContent.encode(stream); | ||
length = stream.length; | ||
stream.stream.writeUInt32LE(length, 4); | ||
// Chunk type | ||
stream.writeByte(chunkType.charCodeAt(0)); // reserved | ||
stream.writeUInt32(total_length); | ||
}; | ||
exports.writeTCPMessageHeader = writeTCPMessageHeader; | ||
function readMessageHeader(stream) { | ||
var encodeMessage = function (msgType, messageContent, stream) { | ||
assert(stream instanceof opcua.BinaryStream); | ||
//the length of the message, in bytes. (includes the 8 bytes of the message header) | ||
var total_length = messageContent.binaryStoreSize() + 8; | ||
msgType = String.fromCharCode(stream.readByte()) + | ||
String.fromCharCode(stream.readByte()) + | ||
String.fromCharCode(stream.readByte()); | ||
padding = stream.readByte(); | ||
writeTCPMessageHeader(msgType,"F",total_length,stream); | ||
messageContent.encode(stream); | ||
assert(total_length == stream.length , "invalid message size"); | ||
}; | ||
length = stream.readInteger(4); | ||
function packTcpMessage(msgType,encodableObject) { | ||
return { msgType: msgType, length: length }; | ||
} | ||
assert(is_valid_msg_type(msgType)); | ||
function decodeMessage(stream, className) { | ||
assert(stream instanceof opcua.BinaryStream); | ||
var messageChunk = new Buffer(encodableObject.binaryStoreSize() + 8); | ||
// encode encodeableObject in a packet | ||
var stream = new BinaryStream(messageChunk); | ||
encodeMessage(msgType, encodableObject, stream); | ||
return messageChunk; | ||
header = readMessageHeader(stream); | ||
obj = new className(); | ||
obj.decode(stream); | ||
return obj; | ||
} | ||
exports.BinaryStream = BinaryStream; | ||
exports.GUID = GUID; | ||
exports.NodeId = NodeId; | ||
exports.HelloMessage = HelloMessage; | ||
exports.AcknowledgeMessage = AcknowledgeMessage; | ||
// opc.tcp://xleuri11022:51210/UA/SampleServer | ||
function parseEndpointUrl(endpoint_url) | ||
{ | ||
var r = /([a-z.]*):\/\/([a-zA-Z\.\-0-9]*):([0-9]*)(\/.*)/ | ||
try { | ||
var matches = r.exec(endpoint_url); | ||
return { | ||
protocol: matches[1], | ||
hostname: matches[2], | ||
port: parseInt(matches[3]), | ||
address: matches[4] | ||
} | ||
exports.encodeMessage = encodeMessage; | ||
exports.decodeMessage = decodeMessage; | ||
function sendMessage(socket, msgType, msg, callback) { | ||
assert(['HEL', 'ACK', 'ERR'].indexOf(msgType) >= 0); | ||
// encode message in a packet | ||
var stream = new opcua.BinaryStream(); | ||
opcua.encodeMessage(msgType, msg, stream); | ||
s = stream.stream.slice(0, stream.length); | ||
// write data to socket | ||
socket.write(s, callback); | ||
} | ||
catch(err) { | ||
console.log(" invalid endpoint URL " + endpoint_url); | ||
} | ||
return {}; | ||
} | ||
exports.sendMessage = sendMessage; | ||
exports.HelloMessage = HelloMessage; | ||
exports.AcknowledgeMessage = AcknowledgeMessage; | ||
exports.TCPErrorMessage = s.TCPErrorMessage; | ||
exports.decodeMessage = decodeMessage; | ||
//xx exports.encodeMessage = encodeMessage; | ||
exports.packTcpMessage = packTcpMessage; | ||
exports.parseEndpointUrl = parseEndpointUrl; |
@@ -0,82 +1,431 @@ | ||
var util = require("util"); | ||
var EventEmitter = require("events").EventEmitter; | ||
var net = require("net"); | ||
var opcua = require("./nodeopcua"); | ||
var colors = require('colors'); | ||
var util = require('util'); | ||
var read_certificate = require("../lib/crypto_utils").read_certificate; | ||
var crypto = require("crypto"); | ||
var async = require("async"); | ||
var _ = require("underscore"); | ||
var assert= require("assert"); | ||
var ClientSecureChannelLayer =require("./client/client_secure_channel_layer").ClientSecureChannelLayer; | ||
var s = require("./structures"); | ||
var nodeids = require("./opcua_node_ids").DataType; | ||
var ec = require("./encode_decode"); | ||
var debugLog = require("../lib/utils").make_debugLog(__filename); | ||
function makeHeader(options) | ||
{ | ||
var header = new s.RequestHeader(); | ||
header; | ||
return header; | ||
} | ||
exports.makeHeader = makeHeader; | ||
/** | ||
* | ||
* @constructor OPCUAClient | ||
*/ | ||
function OPCUAClient() { | ||
EventEmitter.call(this); | ||
this.protocolVersion = 1; | ||
this._sessions = []; | ||
this._clientNonce = crypto.randomBytes(32); | ||
this._certificate = read_certificate("certificates/client_cert.pem"); | ||
this._server_endpoints =[]; | ||
function OPCUAClient() | ||
} | ||
util.inherits(OPCUAClient, EventEmitter); | ||
/** | ||
* connect OPCUA client to server | ||
* | ||
* @param endpoint_url | ||
* @param callback | ||
*/ | ||
OPCUAClient.prototype.connect = function(endpoint_url, callback) | ||
{ | ||
this._client = new net.Socket(); | ||
assert(_.isFunction(callback), "expecting a callback"); | ||
// Add a 'data' event handler for the client socket | ||
// data is what the server sent to this socket | ||
this._client.on('data', function(data) { | ||
var self = this; | ||
_stream = new opcua.BinaryStream(data); | ||
var acknowledgeMessage = opcua.decodeMessage(_stream,opcua.AcknowledgeMessage); | ||
//xx console.log(util.inspect(acknowledgeMessage,{ colors: true})); | ||
// prevent illegal call to connect | ||
if ( this._secureChannel) { | ||
process.nextTick(function() { callback(new Error("connect already called"),null);}); | ||
return; | ||
} | ||
self._connection_callback = callback; | ||
//todo: make sure endpoint_url exists in the list of endpoints send by the server | ||
async.series([ | ||
//------------------------------------------------- STEP 2 : OpenSecureChannel | ||
function(callback) { | ||
assert( self._secureChannel === undefined ); | ||
self._secureChannel = new ClientSecureChannelLayer(); | ||
self._secureChannel.on("send_chunk",function(message_chunk) { | ||
self.emit("send_chunk",message_chunk); }); | ||
self._secureChannel.on("receive_chunk",function(message_chunk) { | ||
self.emit("receive_chunk",message_chunk); }); | ||
self._secureChannel.on("send_request",function(message) { | ||
self.emit("send_request",message); }); | ||
self._secureChannel.on("receive_response",function(message) { | ||
self.emit("receive_response",message); }); | ||
self._secureChannel.protocolVersion = self.protocolVersion; | ||
self._secureChannel.create(endpoint_url,function(err){ | ||
if (err) { | ||
self._secureChannel = null; | ||
} else { | ||
} | ||
callback(err); | ||
}); | ||
}, | ||
//------------------------------------------------- STEP 3 : GetEndpointsRequest | ||
function(callback) { | ||
self.getEndPointRequest(function(err,endpoints){ | ||
callback(err); | ||
}); | ||
} | ||
], function(err) { | ||
if (err) { | ||
self.disconnect(function() { | ||
if (self._connection_callback) { | ||
setImmediate(self._connection_callback,err); // OK | ||
self._connection_callback = null; | ||
} | ||
}); | ||
self._secureChannel = null; | ||
} else { | ||
if (self._connection_callback) { | ||
setImmediate(self._connection_callback,err); // OK | ||
self._connection_callback = null; | ||
} | ||
} | ||
}); | ||
// Add a 'close' event handler for the client socket | ||
this._client.on('close', function() { | ||
//xx console.log('Connection closed'); | ||
}; | ||
/** | ||
* return the endpoint information from a URI | ||
* @param endpoint_url | ||
*/ | ||
OPCUAClient.prototype.findEndpoint = function(endpoint_url) { | ||
for (var i in this._server_endpoints) { | ||
var endpoint =this._server_endpoints[i]; | ||
if (endpoint.endpointUrl === endpoint_url) { | ||
return endpoint; | ||
} | ||
} | ||
return null; | ||
}; | ||
OPCUAClient.prototype.getEndPointRequest = function(callback) { | ||
var self = this; | ||
// OpenSecureChannel | ||
var request = new s.GetEndpointsRequest( | ||
{ | ||
endpointUrl: self.endpoint_url, | ||
localeIds: [], | ||
requestHeader: { | ||
auditEntryId: null | ||
} | ||
} | ||
); | ||
self._secureChannel.performMessageTransaction(request, s.GetEndpointsResponse,function(err,response){ | ||
if (!err) { | ||
self._server_endpoints = response.endpoints; | ||
callback(null,response.endpoints); | ||
} else { | ||
self._server_endpoints = []; | ||
callback(err,null); | ||
} | ||
}); | ||
} | ||
}; | ||
var OPCUASession = function(client) { | ||
OPCUAClient.prototype.sendMessage = function(msg,callback) | ||
assert(client instanceof OPCUAClient); | ||
this._client = client; | ||
}; | ||
var browse_service = require("./browse_service"); | ||
/** | ||
* session.browse([{ | ||
* nodeId: ec.makeNodeId(85), | ||
* includeSubTypes: true, | ||
* browseDirection: browse_service.BrowseDirection.Both, | ||
* }],function(err,nodes) {} ); | ||
* | ||
* @param nodesToBrowse | ||
* @param callback | ||
*/ | ||
OPCUASession.prototype.browse = function(nodesToBrowse,callback) { | ||
assert(typeof(callback) === "function"); | ||
var request = new browse_service.BrowseRequest({ | ||
}); | ||
nodesToBrowse.forEach(function(node) { | ||
var b = new browse_service.BrowseDescription(node); | ||
b.resultMask = b.resultMask || 63; | ||
request.nodesToBrowse.push(b); | ||
}); | ||
request.requestHeader.authenticationToken = this.authenticationToken; | ||
var self = this; | ||
self._client._secureChannel.performMessageTransaction(request, browse_service.BrowseResponse,function(err,response){ | ||
if(err) { | ||
callback(err,null,response); | ||
} else { | ||
// console.log(JSON.stringify(response.results,null," ").yellow.bold); | ||
callback(null,response.results,response.diagnosticInfos); | ||
} | ||
}); | ||
}; | ||
OPCUASession.prototype.close = function(callback) { | ||
this._client.closeSession(this,callback); | ||
}; | ||
OPCUAClient.prototype._nextSessionName = function() | ||
{ | ||
opcua.sendMessage(this._client,"HEL",msg,callback); | ||
if (!this.___sessionName_counter) { | ||
this.___sessionName_counter = 0; | ||
} | ||
this.___sessionName_counter += 1; | ||
return 'Session' + this.___sessionName_counter; | ||
}; | ||
OPCUAClient.prototype._createSession = function(callback) { | ||
assert(typeof(callback) === "function"); | ||
if (!this._secureChannel) { | ||
callback(new Error(" No secure channel")); | ||
return; | ||
} | ||
var endpoint = this.findEndpoint(this._secureChannel.endpoint_url); | ||
if (!endpoint) { | ||
callback(new Error( " End point must exist " + this._secureChannel.endpoint_url)); | ||
return; | ||
} | ||
this.serverUri = endpoint.server.applicationUri; | ||
this.endpoint_url =this._secureChannel.endpoint_url; | ||
this.endpoint_url = "opc.tcp://localhost:51210/UA/SampleServer"; | ||
var applicationDescription = new s.ApplicationDescription({ | ||
applicationUri: "urn:localhost:application:", | ||
productUri: "http://localhost/application", | ||
applicationName: { text: "MyApplication"}, | ||
applicationType: s.EnumApplicationType.CLIENT, | ||
gatewayServerUri: undefined, | ||
discoveryProfileUri: undefined, | ||
discoveryUrls: [] | ||
}); | ||
assert(this.serverUri," must have a valid server URI"); | ||
assert(this.endpoint_url," must have a valid server endpoint_url"); | ||
var request = new s.CreateSessionRequest({ | ||
clientDescription: applicationDescription, | ||
serverUri: this.serverUri, | ||
endpointUrl: this.endpoint_url, | ||
sessionName: this._nextSessionName(), | ||
clientNonce: this._clientNonce, | ||
clientCertificate: null, //xx this._certificate, | ||
requestedSessionTimeout: 300000, | ||
maxResponseMessageSize: 800000 | ||
}); | ||
// console.log(JSON.stringify(request,null," ")); | ||
var self = this; | ||
self._secureChannel.performMessageTransaction(request, s.CreateSessionResponse,function(err,response){ | ||
if (!err) { | ||
assert( response instanceof s.CreateSessionResponse); | ||
// | ||
// todo: verify SignedSoftwareCertificates and response.serverSignature | ||
// | ||
var session = new OPCUASession(self); | ||
session.name = request.sessionName; | ||
session.sessionId = response.sessionId; | ||
session.authenticationToken = response.authenticationToken; | ||
session.timeout = response.revisedSessionTimeout; | ||
session.serverNonce = response.serverNonce; | ||
session.serverCertificate = response.serverCertificate; | ||
session.serverSignature = response.serverSignature; | ||
callback(null,session); | ||
} else { | ||
callback(err,null); | ||
} | ||
}); | ||
}; | ||
OPCUAClient.prototype.connect = function(host, port , callback) | ||
{ | ||
// see OPCUA Part 4 - $7.35 | ||
OPCUAClient.prototype._activateSession = function(session,callback) { | ||
assert(typeof(callback) === "function"); | ||
if (!this._secureChannel) { | ||
callback(new Error(" No secure channel")); | ||
} | ||
var request = new s.ActivateSessionRequest({ | ||
clientSignature: { algorithm: null, signature: null }, | ||
clientSoftwareCertificates: [ | ||
], | ||
localeIds: [ | ||
], | ||
userIdentityToken: new s.AnonymousIdentityToken({ | ||
policyId: "0" | ||
}), // extension object | ||
userTokenSignature: { | ||
algorithm: null, | ||
signature: null | ||
} | ||
}); | ||
request.requestHeader.authenticationToken = session.authenticationToken; | ||
var self = this; | ||
self._secureChannel.performMessageTransaction(request, s.ActivateSessionResponse,function(err,response){ | ||
if (!err) { | ||
assert( response instanceof s.ActivateSessionResponse); | ||
var serverNonce = response.serverNonce; | ||
var results = response.results; | ||
callback(null,session); | ||
} else { | ||
callback(err,null); | ||
} | ||
}); | ||
}; | ||
/** | ||
* create and activate a new session | ||
* | ||
* @param callback | ||
*/ | ||
OPCUAClient.prototype.createSession = function(callback) { | ||
var self = this; | ||
this._client.connect(port, host, function() { | ||
//xx console.log('Client connected to : ' + host + ':' + port); | ||
self._createSession(function(err,session){ | ||
if(err) { | ||
callback(err); | ||
} else { | ||
self._activateSession(session,function(err,session){ | ||
if (!err) { | ||
self._sessions.push(session); | ||
} | ||
callback(err,session); | ||
}); | ||
} | ||
}); | ||
// Write a message to the socket as soon as the client is connected, | ||
// the server will receive it as message from the client | ||
msg = new opcua.HelloMessage(); | ||
self.sendMessage(msg,function(err) { | ||
}; | ||
if (err) { | ||
OPCUAClient.prototype._closeSession= function(session,callback) { | ||
} else { | ||
callback(); | ||
assert(typeof(callback) === "function"); | ||
if (!this._secureChannel) { | ||
callback(new Error(" No secure channel")); | ||
} | ||
// msg = new OpenSecureChannel(); | ||
// self.sendMessage(msg,function callback(err) { | ||
// callback(); | ||
// }); | ||
} | ||
}); | ||
var request = new s.CloseSessionRequest({ | ||
deleteSubscriptions: true | ||
}); | ||
var self = this; | ||
self._secureChannel.performMessageTransaction(request, s.CreateSessionResponse,function(err,response){ | ||
if(err) { | ||
callback(err,null); | ||
} else { | ||
self._secureChannel.close(callback); | ||
} | ||
}); | ||
}; | ||
OPCUAClient.prototype.disconnect = function(callback) | ||
{ | ||
this._client.end(); | ||
callback(); | ||
/** | ||
* @param callback | ||
* @param session | ||
*/ | ||
OPCUAClient.prototype.closeSession = function(session,callback) { | ||
var self = this; | ||
//todo : send close session on secure channel | ||
var index = this._sessions.indexOf(session); | ||
if (index >=0 ) { | ||
this._sessions.splice(index, 1); | ||
} | ||
self._closeSession(session,function(err){ | ||
callback(err); | ||
}); | ||
}; | ||
/** | ||
* property transactionInProgress | ||
* | ||
* @returns {boolean} true if a transaction has already been initiated and if the client | ||
* is waiting for a reply from the server, false if the client is ready | ||
* to initiate a new transaction with the server. | ||
*/ | ||
OPCUAClient.prototype.__defineGetter__("transactionInProgress" ,function() { | ||
return this._secureChannel.transactionInProgress; | ||
}); | ||
/** | ||
* disconnect client from server | ||
* @param callback | ||
*/ | ||
OPCUAClient.prototype.disconnect = function(callback) { | ||
if (this._secureChannel) { | ||
this._secureChannel.close(callback); | ||
} else { | ||
callback(); | ||
} | ||
}; | ||
exports.OPCUAClient = OPCUAClient; | ||
@@ -1,83 +0,270 @@ | ||
var net = require('net'); | ||
var colors = require('colors'); | ||
var util = require('util'); | ||
var s = require("./structures"); | ||
var StatusCodes = require("./opcua_status_code").StatusCodes; | ||
var assert = require("assert"); | ||
var hexDump = require("../lib/utils").hexDump; | ||
var async = require('async'); | ||
var messageHeaderToString = require("../lib/packet_analyzer").messageHeaderToString; | ||
var debugLog = require("../lib/utils").make_debugLog(__filename); | ||
function OPCUAServer() | ||
{ | ||
var ServerSecureChannelLayer = require("../lib/server/server_secure_channel_layer").ServerSecureChannelLayer; | ||
var _ = require("underscore"); | ||
/** | ||
* OPCUAServerEndPoint a ServerEndPoint, listening to one port | ||
* | ||
* when a client is connecting, a SecureChannelIsCreated | ||
* | ||
* @param server | ||
* @param port | ||
* @constructor | ||
*/ | ||
function OPCUAServerEndPoint(server, port) { | ||
assert(server instanceof OPCUAServer); | ||
var self = this; | ||
self.port = parseInt(port); | ||
self._helloreceived = false; | ||
self.server = server; | ||
self._channels = []; | ||
this._server = net.createServer( | ||
self._server = net.createServer( | ||
function (socket) { | ||
function(socket) { | ||
// a client is attempting a connection on the socket | ||
//'connection' listener | ||
//xx console.log('server connected'); | ||
var channel = new ServerSecureChannelLayer(); | ||
channel.timeout = 100; | ||
var _stream = new Buffer(8192); | ||
channel.init(socket, function (err) { | ||
if (err) { | ||
socket.end(); | ||
} else { | ||
self._registerChannel(channel); | ||
debugLog('server receiving a client connection'); | ||
socket.on('data',function(data) { | ||
} | ||
}); | ||
channel.on("message", function (message) { | ||
debugLog("--------------------------------------------------------".green.bold, message._description.name); | ||
if (message instanceof s.GetEndpointsRequest) { | ||
self._on_GetEndpointsRequest(message, channel); | ||
} else if (message instanceof s.CreateSessionRequest) { | ||
self._on_CreateSessionRequest(message, channel); | ||
} else if (message instanceof s.ActivateSessionRequest) { | ||
self._on_ActivateSessionRequest(message, channel); | ||
} else if (message instanceof s.CloseSessionRequest) { | ||
self._on_CloseSessionRequest(message, channel); | ||
} else { | ||
var errMessage = "UNSUPPORTED MESSAGE !! " + message._description.name; | ||
debugLog(errMessage.red.bold); | ||
self._abortWithError(StatusCodes.Bad_ProtocolVersionUnsupported, errMessage, channel); | ||
} | ||
}); | ||
channel.on("abort",function(){ | ||
// the channel has aborted | ||
self._unregisterChannel(channel); | ||
}); | ||
} | ||
); | ||
self._server.on("connection", function (socket) { | ||
debugLog('server connected with : ' + socket.remoteAddress + ':' + socket.remotePort); | ||
}); | ||
} | ||
_stream = new opcua.BinaryStream(data); | ||
var helloMessage = opcua.decodeMessage(_stream,opcua.HelloMessage); | ||
OPCUAServerEndPoint.prototype.endpointDescription = function () { | ||
var self = this; | ||
//xx console.log(util.inspect(helloMessage,{ colors: true})); | ||
var server = self.server; | ||
var hostname = require("os").hostname(); | ||
// the helloMessage shall only be received once. | ||
self._helloreceived = true; | ||
var acknowledgeMessage = new opcua.AcknowledgeMessage(); | ||
opcua.sendMessage(socket,"ACK",acknowledgeMessage,function callback() { | ||
// return the endpoint object | ||
var endpoint = new s.EndpointDescription({ | ||
endpointUrl: "opc.tcp://" + hostname + ":" + this.port + "/UA/SampleServer", | ||
server: { | ||
applicationUri: "SampleServer", | ||
productUri: "SampleServer", | ||
applicationName: { text: "SampleServer", locale: null }, | ||
applicationType: s.EnumApplicationType.SERVER, | ||
gatewayServerUri: "", | ||
discoveryProfileUri: "", | ||
discoveryUrls: [] | ||
}, | ||
serverCertificate: server.getCertificate(), | ||
securityMode: s.MessageSecurityMode.NONE, | ||
securityPolicyUri: "http://opcfoundation.org/UA/SecurityPolicy#None", | ||
userIdentityTokens: [ | ||
{ | ||
policyId: "0", | ||
tokenType: s.UserIdentityTokenType.ANONYMOUS, | ||
issuedTokenType: "", | ||
issuerEndpointUrl: "", | ||
securityPolicyUri: "" | ||
} | ||
], | ||
transportProfileUri: "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary", | ||
securityLevel: 3 | ||
}); | ||
//xx console.log("send reply"); | ||
}); | ||
return endpoint; | ||
}; | ||
/** | ||
* | ||
* @param channel | ||
* @private | ||
*/ | ||
OPCUAServerEndPoint.prototype._registerChannel = function (channel) { | ||
var self = this; | ||
self._channels[channel.securityToken.secureChannelId] = channel; | ||
}; | ||
}); | ||
OPCUAServerEndPoint.prototype._unregisterChannel = function (channel) { | ||
var self = this; | ||
delete self._channels[channel.securityToken.secureChannelId]; | ||
}; | ||
socket.on('close', function() { | ||
//xx console.log('server disconnected (CLOSE)'); | ||
}); | ||
OPCUAServerEndPoint.prototype.listen = function () { | ||
socket.on('end', function() { | ||
//xx console.log('server disconnected (END)'); | ||
}); | ||
var self = this; | ||
self._started = true; | ||
self._server.listen(self.port, function () { //'listening' listener | ||
debugLog('server bound'); | ||
}); | ||
}; | ||
// socket.write('hello\r\n'); | ||
// socket.pipe(socket); | ||
OPCUAServerEndPoint.prototype.shutdown = function (callback) { | ||
var self = this; | ||
if (self._started) { | ||
self._started = false; | ||
self._server.close(function () { | ||
callback(); | ||
}); | ||
} else { | ||
callback(); | ||
} | ||
}; | ||
self.connected_client_count+=1; | ||
OPCUAServerEndPoint.prototype.start = function (callback) { | ||
this.listen(); | ||
process.nextTick(callback); | ||
}; | ||
} | ||
); | ||
this._server.on("connection",function(socket){ | ||
//xx console.log('CONNECTED: ' + socket.remoteAddress +':'+ socket.remotePort); | ||
}); | ||
/** | ||
* | ||
* @param statusCode | ||
* @param description | ||
* @param channel | ||
* @private | ||
*/ | ||
OPCUAServerEndPoint.prototype._abortWithError = function (statusCode, description, channel) { | ||
channel.send_error_and_abort(statusCode, description); | ||
}; | ||
this.connected_client_count = 0; | ||
} | ||
/** | ||
* | ||
* @param request | ||
* @param channel | ||
* @private | ||
*/ | ||
OPCUAServerEndPoint.prototype._on_GetEndpointsRequest = function (request, channel) { | ||
OPCUAServer.prototype.listen = function(port) | ||
{ | ||
port = parseInt(port); | ||
var self = this; | ||
var server = self.server; | ||
assert(request._description.name == "GetEndpointsRequest"); | ||
this._server.listen(port, function() { //'listening' listener | ||
//xx console.log('server bound'); | ||
var response = new s.GetEndpointsResponse({}); | ||
server.endpoints.forEach(function (endpoint) { | ||
response.endpoints.push(endpoint.endpointDescription()); | ||
}); | ||
channel.send_response("MSG", response); | ||
}; | ||
OPCUAServer.prototype.shutdown = function(callback) | ||
{ | ||
OPCUAServerEndPoint.prototype._on_CreateSessionRequest = function(request,channel) { | ||
var self = this; | ||
var server = self.server; | ||
assert(request._description.name == "CreateSessionRequest"); | ||
var response = new s.CreateSessionResponse({}); | ||
channel.send_response("MSG", response); | ||
}; | ||
OPCUAServerEndPoint.prototype._on_ActivateSessionRequest = function(request,channel) { | ||
var self = this; | ||
var server = self.server; | ||
assert(request._description.name == "ActivateSessionRequest"); | ||
var response = new s.ActivateSessionResponse({}); | ||
channel.send_response("MSG", response); | ||
}; | ||
delete this._server; | ||
callback(); | ||
OPCUAServerEndPoint.prototype._on_CloseSessionRequest = function(request,channel) { | ||
var self = this; | ||
assert(request._description.name == "CloseSessionRequest"); | ||
var response = new s.CloseSessionResponse({}); | ||
channel.send_response("MSG", response); | ||
}; | ||
OPCUAServer = function () { | ||
var self = this; | ||
self.endpoints = []; | ||
self.protocolVersion = 1; | ||
self.connected_client_count = 0; | ||
// add the tcp/ip endpoint with no security | ||
self.endpoints.push(new OPCUAServerEndPoint(this, 65432)); | ||
}; | ||
/** | ||
* Initiate the server by starting all its endpoints | ||
*/ | ||
OPCUAServer.prototype.start = function (done) { | ||
var tasks = []; | ||
this.endpoints.forEach(function (endpoint) { | ||
tasks.push(function (callback) { | ||
endpoint.start(callback); | ||
}); | ||
}); | ||
async.parallel(tasks, done); | ||
}; | ||
OPCUAServer.prototype.shutdown = function (done) { | ||
assert(_.isFunction(done)); | ||
var tasks = []; | ||
this.endpoints.forEach(function (endpoint) { | ||
tasks.push(function (callback) { | ||
debugLog(" shutting down endpoint " + endpoint.endpointDescription().endpointUrl); | ||
endpoint.shutdown(callback); | ||
}); | ||
}); | ||
async.parallel(tasks, function(err) { | ||
done(err); | ||
debugLog("shutdown completed"); | ||
}); | ||
}; | ||
OPCUAServer.prototype.getCertificate = function () { | ||
if (!this.certificate) { | ||
// create fake certificate | ||
var read_certificate = require("../lib/crypto_utils").read_certificate; | ||
this.certificate = read_certificate("certificates/cert.pem"); | ||
} | ||
return this.certificate; | ||
}; | ||
exports.OPCUAServerEndPoint = OPCUAServerEndPoint; | ||
exports.OPCUAServer = OPCUAServer; | ||
// A SecureChannel is a long-running logical connection between a single Client and a single Server. | ||
// This channel maintains a set of keys known only to the Client and Server, which are used to | ||
// authenticate and encrypt Messages sent across the network. The SecureChannel Services allow | ||
// authenticate and encrypt Messages sent across the network. The ClientSecureChannelLayer Services allow | ||
// the Client and Server to securely negotiate the keys to use. | ||
var util = require("util"); | ||
var assert = require("assert"); | ||
var color = require("colors"); | ||
function SecureChannel() | ||
{ | ||
var factories = require("./factories"); | ||
var BinaryStream = require("./binaryStream").BinaryStream; | ||
var s= require("./structures"); | ||
var ec = require("./encode_decode"); | ||
// OPC UA Secure Conversation Message Header : Part 6 page 36 | ||
//Asymmetric algorithms are used to secure the OpenSecureChannel messages. | ||
var AsymmetricAlgorithmSecurityHeader_Description = { | ||
name: "AsymmetricAlgorithmSecurityHeader", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
// length shall not exceed 256 | ||
// The URI of the security policy used to secure the message. | ||
// This field is encoded as a UTF8 string without a null terminator | ||
{ name:"securityPolicyUri" , fieldType:"String" }, | ||
// The X509v3 certificate assigned to the sending application instance. | ||
// This is a DER encoded blob. | ||
// This indicates what private key was used to sign the MessageChunk. | ||
// This field shall be null if the message is not signed. | ||
{ name:"senderCertificate" , fieldType:"ByteString" }, | ||
// The thumbprint of the X509v3 certificate assigned to the receiving application | ||
// The thumbprint is the SHA1 digest of the DER encoded form of the certificate. | ||
// This indicates what public key was used to encrypt the MessageChunk | ||
// This field shall be null if the message is not encrypted. | ||
{ name:"receiverCertificateThumbprint" , fieldType:"ByteString"} | ||
] | ||
}; | ||
AsymmetricAlgorithmSecurityHeader = exports.AsymmetricAlgorithmSecurityHeader = factories.UAObjectFactoryBuild(AsymmetricAlgorithmSecurityHeader_Description); | ||
// Symmetric algorithms are used to secure all messages other than the OpenSecureChannel messages | ||
// OPC UA Secure Conversation Message Header : Part 6 page 37 | ||
var SymmetricAlgorithmSecurityHeader_Description= { | ||
name: "SymmetricAlgorithmSecurityHeader", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
// A unique identifier for the ClientSecureChannelLayer token used to secure the message | ||
// This identifier is returned by the server in an OpenSecureChannel response message. If a | ||
// Server receives a TokenId which it does not recognize it shall return an appropriate | ||
// transport layer error. | ||
{ name: "tokenId" , fieldType: "UInt32" , defaultValue: 0xDEADBEEF } | ||
] | ||
}; | ||
SymmetricAlgorithmSecurityHeader = exports.SymmetricAlgorithmSecurityHeader = factories.UAObjectFactoryBuild(SymmetricAlgorithmSecurityHeader_Description); | ||
var SequenceHeader_Description = { | ||
name: "SequenceHeader", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
// A monotonically increasing sequence number assigned by the sender to each | ||
// MessageChunk sent over the ClientSecureChannelLayer. | ||
{ name: "sequenceNumber", fieldType: "UInt32" }, | ||
// An identifier assigned by the client to OPC UA request Message. All MessageChunks for | ||
// the request and the associated response use the same identifier. | ||
{ name: "requestId", fieldType: "UInt32" } | ||
] | ||
}; | ||
exports.SequenceHeader = SequenceHeader = factories.UAObjectFactoryBuild(SequenceHeader_Description); | ||
var Padding_Description = { | ||
name: "Padding", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
// The number of padding bytes (not including the byte for the PaddingSize). | ||
{ name: "paddingSize", fieldType: "Byte" }, | ||
// Padding added to the end of the message to ensure length of the data to encrypt is an | ||
// integer multiple of the encryption block size. | ||
// The value of each byte of the padding is equal to PaddingSize. | ||
{ name: "padding", fieldType: "Byte*" }, | ||
// The signature for the MessageChunk. | ||
// The signature includes the all headers, all message data, the PaddingSize and the Padding. | ||
{ name: "Signatures", fieldType: "Byte*" } | ||
] | ||
}; | ||
function calculateMaxBodySize() { | ||
// The formula to calculate the amount of padding depends on the amount of data that needs to | ||
// be sent (called BytesToWrite). The sender shall first calculate the maximum amount of space | ||
// available in the MessageChunk (called MaxBodySize) using the following formula: | ||
// MaxBodySize = PlainTextBlockSize * Floor((MessageChunkSize – | ||
// HeaderSize – SignatureSize - 1)/CipherTextBlockSize) – | ||
// SequenceHeaderSize; | ||
} | ||
exports.SecureChannel = SecureChannel(); | ||
var MessageChunkManager = require("./chunk_manager").MessageChunkManager; | ||
var SequenceNumberGenerator = require("./sequence_number_generator").SequenceNumberGenerator; | ||
function makeAlgorithmSecurityHeader(msgType) { | ||
var securityHeader = new AsymmetricAlgorithmSecurityHeader(); | ||
securityHeader.securityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#None"; | ||
securityHeader.senderCertificate = null; | ||
securityHeader.receiverCertificateThumbprint = null; | ||
return securityHeader; | ||
} | ||
function chooseSecurityHeader(msgType) { | ||
var securityHeader = (msgType === "OPN") ? | ||
new AsymmetricAlgorithmSecurityHeader() : | ||
new SymmetricAlgorithmSecurityHeader(); | ||
return securityHeader; | ||
} | ||
exports.chooseSecurityHeader = chooseSecurityHeader; | ||
var SecureMessageChunkManager = function (msgType, options, securityHeader,sequenceNumberGenerator) { | ||
msgType = msgType || "OPN"; | ||
this.chunkSize = options.chunkSize || 8192; | ||
var secureChannelId = options.secureChannelId; | ||
var requestId = options.requestId; | ||
this.sequenceNumberGenerator = sequenceNumberGenerator; | ||
securityHeader = securityHeader || chooseSecurityHeader(msgType); | ||
this.securityHeader = securityHeader; | ||
assert(requestId >0 , "expecting a valid request ID"); | ||
this.sequenceHeader = new SequenceHeader({ requestId: requestId, sequenceNumber: -1}); | ||
var securityHeaderSize = this.securityHeader.binaryStoreSize(); | ||
var sequenceHeaderSize = this.sequenceHeader.binaryStoreSize(); | ||
var extraHeaderSize = securityHeaderSize + sequenceHeaderSize; | ||
MessageChunkManager.call(this, this.chunkSize,msgType,secureChannelId,extraHeaderSize); | ||
assert(this.bodySize > 0); | ||
}; | ||
util.inherits(SecureMessageChunkManager, MessageChunkManager); | ||
SecureMessageChunkManager.prototype.write_header_and_footer = function(finalC,buf,length) { | ||
assert(finalC.length === 1); | ||
// reserve space for header | ||
var self = this; | ||
assert(buf instanceof Buffer); | ||
var bs = new BinaryStream(buf); | ||
// message header -------------------------- | ||
// --------------------------------------------------------------- | ||
// OPC UA Secure Conversation Message Header : Part 6 page 36 | ||
// MessageType Byte[3] | ||
// IsFinal Byte[1] C : intermediate, F: Final , A: Final with Error | ||
// MessageSize UInt32 The length of the MessageChunk, in bytes. This value includes size of the message header. | ||
// SecureChannelId UInt32 A unique identifier for the ClientSecureChannelLayer assigned by the server. | ||
bs.writeUInt8(this.msgType.charCodeAt(0)); | ||
bs.writeUInt8(this.msgType.charCodeAt(1)); | ||
bs.writeUInt8(this.msgType.charCodeAt(2)); | ||
bs.writeUInt8(finalC.charCodeAt(0)); | ||
bs.writeUInt32(length); | ||
bs.writeUInt32(this.secureChannelId); | ||
assert(bs.length === 12 ); | ||
// write Security Header ----------------- | ||
this.securityHeader.encode(bs); | ||
// write Sequence Header ----------------- | ||
this.sequenceHeader.sequenceNumber = this.sequenceNumberGenerator.next(); | ||
this.sequenceHeader.encode(bs); | ||
assert(bs.length === this.sizeOfHeader); | ||
// body + padding already written | ||
// sign | ||
// write signature | ||
// encrypt | ||
}; | ||
/** | ||
* wrap a message body into one or more message_chunks | ||
* ( use this method to build fake data bloc in tests) | ||
* @param message_body | ||
* @param msgType | ||
* @param chunk_size | ||
* @returns {Array} | ||
*/ | ||
function decompose_message_body_in_chunks(message_body,msgType,chunk_size) { | ||
assert(chunk_size>24,"expecting chunk_size"); | ||
assert(msgType.length===3," invalid msgType "+ msgType); | ||
assert(message_body instanceof Buffer && message_body.length >0 , " invalid buffer"); | ||
var sequenceNumberGenerator = new SequenceNumberGenerator(); | ||
var options = { | ||
secureChannelId: 10, | ||
requestId: 36 | ||
}; | ||
var msgChunkManager = new SecureMessageChunkManager(msgType,options,null,sequenceNumberGenerator); | ||
var chunks = []; | ||
msgChunkManager.on("chunk",function(chunk){ | ||
if(chunk) { | ||
assert(chunk.length>0); | ||
chunks.push(chunk); | ||
} | ||
}); | ||
msgChunkManager.write(message_body); | ||
msgChunkManager.end(); | ||
assert(chunks.length>0,"decompose_message_body_in_chunks: must produce at least one chunk"); | ||
return chunks; | ||
} | ||
exports.decompose_message_body_in_chunks = decompose_message_body_in_chunks; | ||
function clone_buffer(buffer) { | ||
var clone = new Buffer(buffer.length); | ||
buffer.copy(clone,0,0); | ||
return clone; | ||
} | ||
exports.clone_buffer = clone_buffer; | ||
function MessageChunker() { | ||
this.sequenceNumberGenerator = new SequenceNumberGenerator(); | ||
} | ||
MessageChunker.prototype.chunkSecureMessage = function(msgType,options,message,messageChunkCallback) { | ||
var chunkStream = require("./chunk_manager").chunkStream; | ||
// calculate message size ( with its encodingDefaultBinary) | ||
var binSize = message.binaryStoreSize() + 4; | ||
var stream = new BinaryStream(binSize); | ||
ec.encodeExpandedNodeId(message.encodingDefaultBinary,stream); | ||
message.encode(stream); | ||
exports.fullbuf = clone_buffer(stream._buffer); | ||
var r = require("stream").Readable(); | ||
r.push(stream._buffer); | ||
r.push(null); | ||
var securityHeader; | ||
if (msgType == "OPN") { | ||
securityHeader = makeAlgorithmSecurityHeader(); | ||
} else { | ||
securityHeader = new SymmetricAlgorithmSecurityHeader({ | ||
tokenId: options.tokenId | ||
}); | ||
} | ||
r.pipe(chunkStream(new SecureMessageChunkManager(msgType, options, securityHeader,this.sequenceNumberGenerator)) | ||
).on("data",function (messageChunk) { | ||
messageChunkCallback(messageChunk); | ||
}).on("finish",function(){ | ||
messageChunkCallback(null); | ||
}); | ||
}; | ||
exports.MessageChunker = MessageChunker; |
@@ -1,4 +0,5 @@ | ||
var factories = require("./factories.js"); | ||
var factories = require("./factories"); | ||
var ec = require("./encode_decode"); | ||
var assert = require("assert"); | ||
//The StatusCode is a 32-bit unsigned integer. The top 16 bits represent the numeric value of the | ||
@@ -14,9 +15,73 @@ //code that shall be used for detecting specific errors or conditions. The bottom 16 bits are bit flags | ||
// TCP Error Message OPC Unified Architecture, Part 6 page 46 | ||
// the server always close the connection after sending the TCPError message | ||
var TCPErrorMessage_Description = { | ||
name: "TCPErrorMessage", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name :"name", fieldType: "UInt32"}, | ||
{ name :"reason", fieldType: "String"} // A more verbose description of the error. | ||
] | ||
}; | ||
exports.TCPErrorMessage = factories.UAObjectFactoryBuild(TCPErrorMessage_Description); | ||
// see Part 3 $8.3 and Part 6 $5.2.213 | ||
var QualifiedName_Description = { | ||
name: "QualifiedName", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name: "namespaceIndex", fieldType: "UInt16" , comment: "The namespace index" }, | ||
{ name: "name", fieldType: "String" , comment: "The name" } | ||
] | ||
}; | ||
exports.QualifiedName = factories.UAObjectFactoryBuild(QualifiedName_Description); | ||
function getLocalizeText_EncodingByte(localizedText) { | ||
var encoding_mask = 0; | ||
if (localizedText.locale) { | ||
encoding_mask +=1; | ||
} | ||
if (localizedText.text) { | ||
encoding_mask +=2; | ||
} | ||
return encoding_mask; | ||
} | ||
// see Part 3 - $8.5 page 63 | ||
var LocalizedText_Description = { | ||
name: "LocalizedText", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name: "text", fieldType: "String" }, | ||
{ name: "locale", fieldType: "LocaleId" } | ||
] | ||
], | ||
// OPCUA Part 6 $ 5.2.2.14 : localizedText have a special encoding | ||
encode: function(localizedText,stream) { | ||
var encoding_mask= getLocalizeText_EncodingByte(localizedText); | ||
ec.encodeByte(encoding_mask,stream); | ||
if ( ( encoding_mask & 0x01) === 0x01) { | ||
ec.encodeUAString(localizedText.locale,stream); | ||
} | ||
if ( ( encoding_mask & 0x02) == 0x02 ) { | ||
ec.encodeUAString(localizedText.text,stream); | ||
} | ||
}, | ||
decode: function(self,stream) { | ||
var encoding_mask = ec.decodeByte(stream); | ||
if ( ( encoding_mask & 0x01) === 0x01) { | ||
self.locale = ec.decodeUAString(stream); | ||
}else { | ||
self.locale = null; | ||
} | ||
if ( ( encoding_mask & 0x02) == 0x02 ) { | ||
self.text = ec.decodeUAString(stream); | ||
} else { | ||
self.text = null; | ||
} | ||
} | ||
}; | ||
@@ -28,2 +93,3 @@ exports.LocalizedText = factories.UAObjectFactoryBuild(LocalizedText_Description); | ||
name: "ExtensibleParameter", | ||
id: factories.next_available_id(), | ||
// The extensible parameter types can only be extended by additional parts of this multi-part | ||
@@ -46,4 +112,6 @@ // specification. | ||
name: "ExtensibleParameterAdditionalHeader", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name: "parameterTypeId", fieldType: "NodeId" } | ||
{ name: "parameterTypeId", fieldType: "NodeId" }, | ||
{ name: "encodingMask" , fieldType: "Byte" } | ||
// TODO: { name: "data"} | ||
@@ -70,3 +138,3 @@ ] | ||
// the Session. The SessionAuthenticationToken type is defined in 7.29. | ||
{ name: "authenticationToken", fieldType: "SessionAuthenticationToken" }, | ||
{ name: "authenticationToken", fieldType: "NodeId" }, | ||
@@ -111,2 +179,3 @@ // The time the Client sent the request. | ||
{ name: "serviceDiagnostics", fieldType:"Byte" , comment: "The diagnostics associated with the ServiceResult." }, | ||
// There is one string in this list for each unique namespace, symbolic identifier, | ||
@@ -147,3 +216,3 @@ // and localized text string contained in all of the diagnostics information | ||
// List of transport profiles that the returned Endpoints shall support. | ||
{ name: "profileUris" , isArray: true, fieldType: "String" }, | ||
{ name: "profileUris" , isArray: true, fieldType: "String" } | ||
@@ -203,3 +272,3 @@ ] | ||
}; | ||
exports.ApplicationType = factories.UAObjectFactoryBuild(EnumApplicationType_Description); | ||
exports.EnumApplicationType = factories.UAObjectFactoryBuild(EnumApplicationType_Description); | ||
@@ -273,3 +342,4 @@ // OPC Unified Architecture, Part 4 $7.1 page 106 | ||
// The application instance Certificate issued to the Server . | ||
{ name: "serverCertificate", fieldType: "ApplicationInstanceCertificate" }, | ||
//xx { name: "serverCertificate", fieldType: "ApplicationInstanceCertificate" }, | ||
{ name: "serverCertificate", fieldType: "ByteString" }, | ||
@@ -319,5 +389,7 @@ // The type of security to apply to the messages. ( see part 4 - $7.14.) | ||
name: "ApplicationInstanceCertificate", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
// An identifier for the version of the Certificate encoding. | ||
{ name: "version", fieldType: "ExtensibleParameterAdditionalHeader" }, | ||
{ name: "version", fieldType: "String" }, | ||
@@ -372,4 +444,15 @@ // A unique identifier for the Certificate assigned by the Issuer. | ||
var EnumSecurityTokenRequestType_Description = { | ||
name:"SecurityTokenRequestType", | ||
isEnum: true, | ||
enumValues: { | ||
ISSUE: 0, // creates a new SecurityToken for a new ClientSecureChannelLayer | ||
RENEW: 1 // creates a new SecurityToken for an existing ClientSecureChannelLayer . | ||
} | ||
}; | ||
exports.SecurityTokenRequestType = factories.UAObjectFactoryBuild(EnumSecurityTokenRequestType_Description); | ||
// see OPCUA.Part4. page 22 | ||
var OpenSecureChannelRequest_Description = { | ||
var OpenSecureChannelRequest_Description_as_per_SPEC_Part4 = { | ||
name: "OpenSecureChannelRequest", | ||
@@ -387,23 +470,60 @@ fields: [ | ||
}; | ||
// or OPCUA.Part6. Release 1.0 6.4.4 Establishing a ClientSecureChannelLayer page 39 | ||
var OpenSecureChannelRequest_Description_as_per_XMLSCHEMA = { | ||
name: "OpenSecureChannelRequest", | ||
fields: [ | ||
{ name: "requestHeader" , fieldType: "RequestHeader" }, | ||
{ name: "clientProtocolVersion", fieldType: "UInt32" , description: "The version of protocol used by the client" }, | ||
{ name: "requestType", fieldType: "SecurityTokenRequestType", description: "Whether the channel is being created or renewed" }, | ||
{ name: "securityMode", fieldType: "EnumMessageSecurityMode" , description: "The security mode to use with the channel." }, | ||
{ name: "clientNonce", fieldType: "ByteString" , description: "A random number generated by the client." }, | ||
{ name: "requestedLifetime", fieldType: "UInt32" , description: "The channel lifetime in milliseconds." } | ||
] | ||
}; | ||
var OpenSecureChannelResponse_Description = { | ||
exports.OpenSecureChannelRequest = factories.UAObjectFactoryBuild(OpenSecureChannelRequest_Description_as_per_XMLSCHEMA); | ||
// OPC Unified Architecture, Part 6 page 36 | ||
var ChannelSecurityToken_Description = { | ||
name: "ChannelSecurityToken", | ||
fields: [ | ||
{ name: "secureChannelId", fieldType: "UInt32" }, | ||
{ name: "tokenId", fieldType: "UInt32" }, | ||
{ name: "createdAt", fieldType: "UtcTime" }, | ||
{ name: "revisedLifeTime", fieldType: "UInt32" } | ||
] | ||
}; | ||
exports.ChannelSecurityToken = factories.UAObjectFactoryBuild(ChannelSecurityToken_Description); | ||
var OpenSecureChannelResponse_Description_as_per_Spec_Part4 = { | ||
name: "OpenSecureChannelResponse", | ||
fields: [ | ||
{ name: "responseHeader", fieldType: "responseHeader" }, | ||
{ name: "responseHeader", fieldType: "ResponseHeader" }, | ||
{ name: "securityToken", fieldType: "ChannelSecurityToken" }, | ||
{ name: "channelId", fieldType: "ByteString" }, | ||
{ name: "tokenId", fieldType: "ByteString" }, | ||
{ name: "createdAt", fieldType: "UTCTime" }, | ||
{ name: "revisedLifetime", fieldType: "Duration" }, | ||
{ name: "serverNonce", fieldType: "ByteString" } | ||
] | ||
}; | ||
var OpenSecureChannelResponse_Description_as_per_Spec_Part6 = { | ||
name: "OpenSecureChannelResponse", | ||
fields: [ | ||
{ name: "responseHeader", fieldType: "ResponseHeader" }, | ||
{ name: "serverProtocolVersion", fieldType: "UInt32" }, | ||
{ name: "securityToken", fieldType: "ChannelSecurityToken" }, | ||
{ name: "serverNonce", fieldType: "ByteString" } | ||
] | ||
}; | ||
exports.OpenSecureChannelResponse = factories.UAObjectFactoryBuild(OpenSecureChannelResponse_Description_as_per_Spec_Part6); | ||
var CloseSecureChannelRequest_Description = { | ||
name: "CloseSecureChannelRequuest", | ||
name: "CloseSecureChannelRequest", | ||
fields: [ | ||
{ name: "requestHeader", fieldType: "RequestHeader" }, | ||
{ name: "secureChannelId", fieldType: "ByteString" }, | ||
{ name: "requestHeader", fieldType: "RequestHeader" } | ||
// { name: "secureChannelId", fieldType: "ByteString" }, | ||
] | ||
}; | ||
// | ||
exports.CloseSecureChannelRequest= factories.UAObjectFactoryBuild(CloseSecureChannelRequest_Description); | ||
@@ -413,10 +533,213 @@ var CloseSecureChannelResponse_Description = { | ||
fields: [ | ||
{ name: "responseHeader", fieldType: "ResponseHeader" }, | ||
{ name: "responseHeader", fieldType: "ResponseHeader" } | ||
] | ||
}; | ||
exports.CloseSecureChannelResponse = factories.UAObjectFactoryBuild(CloseSecureChannelResponse_Description); | ||
var factories = require("./factories"); | ||
var FindServersRequest_Description = { | ||
comment: "A standard header included in all requests sent to a server.", | ||
name: "FindServersRequest", | ||
fields: [ | ||
{ name: "requestHeader" , fieldType: "RequestHeader" }, | ||
{ name: "endpointUrl", fieldType: "String", documentation:"The URL used by the client to send the request." }, | ||
{ name: "localeIds", isArray: true, fieldType: "LocaleId", documentation:"The locales to use when constructing a response." }, | ||
{ name: "serverUris", isArray: true, fieldType: "String", documentation:"The URIs of the servers to return (all servers returned if none specified"} | ||
] | ||
}; | ||
exports.FindServersRequest = factories.UAObjectFactoryBuild(FindServersRequest_Description); | ||
var OpenSecureChannelRequest = factories.UAObjectFactoryBuild(OpenSecureChannelRequest_Description); | ||
var FindServersResponse_Description = { | ||
name: "FindServersResponse", | ||
fields: [ | ||
{ name: "responseHeader" , fieldType: "ResponseHeader" }, | ||
{ name: "servers", isArray: true, fieldType: "ApplicationDescription", documentation:"The servers that met the criteria specified in the request."} | ||
] | ||
}; | ||
exports.FindServersResponse = factories.UAObjectFactoryBuild(FindServersResponse_Description); | ||
var ServiceFault_Description = { | ||
comment: "The response returned by all services when there is a service level error.", | ||
name: "ServiceFault", | ||
fields: [ | ||
{ name: "responseHeader" , fieldType: "ResponseHeader" } | ||
] | ||
}; | ||
exports.ServiceFault = factories.UAObjectFactoryBuild(ServiceFault_Description); | ||
var s2 = require("./session_service"); | ||
for( var name in s2) { exports[name] = s2[name]; } | ||
var SignedSoftwareCertificate_Description = { | ||
name: "SignedSoftwareCertificate", | ||
fields: [ | ||
{ name: "certificateData" , fieldType: "ByteString", documentation:"The data of the certificate." }, | ||
{ name: "signature", fieldType: "ByteString", documentation:"The digital signature."} | ||
] | ||
}; | ||
exports.SignedSoftwareCertificate = factories.UAObjectFactoryBuild(SignedSoftwareCertificate_Description); | ||
var SignatureData_Description = { | ||
name: "SignatureData", | ||
fields: [ | ||
{ name: "algorithm" , fieldType: "String", documentation:"The cryptography algorithm used to create the signature." }, | ||
{ name: "signature", fieldType: "ByteString", documentation:"The digital signature."} | ||
] | ||
}; | ||
exports.SignatureData = factories.UAObjectFactoryBuild(SignatureData_Description); | ||
var DiagnosticInfo_EncodingByte_Description = { | ||
name: "DiagnosticInfo_EncodingByte", | ||
isEnum: true, | ||
enumValues: { | ||
SymbolicId: 0x01, | ||
NamespaceUri: 0x02, | ||
LocalizedText: 0x04, | ||
Locale: 0x08, | ||
AdditionalInfo: 0x10, | ||
InnerStatusCode: 0x20, | ||
InnerDiagnosticInfo: 0x40 | ||
} | ||
}; | ||
var DiagnosticInfo_EncodingByte = exports.DiagnosticInfo_EncodingByte = factories.UAObjectFactoryBuild(DiagnosticInfo_EncodingByte_Description); | ||
// see OPCUA Part 4 $7.8 table 123 | ||
var DiagnosticInfoIdentifier_Description = { | ||
name: "DiagnosticInfoIdentifier", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name: "namespaceUri", fieldType: "Int32" , defaultValue: null }, | ||
{ name: "symbolicId", fieldType: "Int32" , defaultValue: null }, | ||
{ name: "locale", fieldType: "Int32" , defaultValue: null }, | ||
{ name: "localizedText", fieldType: "Int32" , defaultValue: null } | ||
] | ||
}; | ||
exports.DiagnosticInfoIdentifier = factories.UAObjectFactoryBuild(DiagnosticInfoIdentifier_Description); | ||
function getDiagnosticInfoEncodingByte(diagnosticInfo) { | ||
assert(diagnosticInfo); | ||
var encoding_mask= 0; | ||
if (diagnosticInfo.identifier.namespaceUri) { | ||
encoding_mask = set_flag(encoding_mask,DiagnosticInfo_EncodingByte.NamespaceUri); | ||
} | ||
if (diagnosticInfo.identifier.symbolicId) { | ||
encoding_mask = set_flag(encoding_mask,DiagnosticInfo_EncodingByte.SymbolicId); | ||
} | ||
if (diagnosticInfo.identifier.locale) { | ||
encoding_mask = set_flag(encoding_mask,DiagnosticInfo_EncodingByte.Locale); | ||
} | ||
if (diagnosticInfo.identifier.localizedText) { | ||
encoding_mask = set_flag(encoding_mask,DiagnosticInfo_EncodingByte.LocalizedText); | ||
} | ||
if (diagnosticInfo.additionalInfo) { | ||
encoding_mask = set_flag(encoding_mask,DiagnosticInfo_EncodingByte.AdditionalInfo); | ||
} | ||
if (diagnosticInfo.innerStatusCode) { | ||
encoding_mask = set_flag(encoding_mask,DiagnosticInfo_EncodingByte.InnerStatusCode); | ||
} | ||
if (diagnosticInfo.innerDiagnosticInfo !== null) { | ||
encoding_mask = set_flag(encoding_mask,DiagnosticInfo_EncodingByte.InnerDiagnosticInfo); | ||
} | ||
return encoding_mask; | ||
} | ||
var set_flag = require("./utils").set_flag; | ||
var check_flag = require("./utils").check_flag; | ||
exports.set_flag = set_flag; | ||
exports.check_flag = check_flag; | ||
var DiagnosticInfo_Description = { | ||
name: "DiagnosticInfo", | ||
fields: [ | ||
{ name: "identifier", fieldType: "DiagnosticInfoIdentifier"}, | ||
{ name: "additionalInfo", fieldType: "String", defaultValue: null }, | ||
{ name: "innerStatusCode", fieldType: "StatusCode" }, | ||
{ name: "innerDiagnosticInfo", fieldType: "DiagnosticInfo", defaultValue: "null" } | ||
], | ||
id: 25, | ||
encode: function(diagnosticInfo,stream) { | ||
var encoding_mask = getDiagnosticInfoEncodingByte(diagnosticInfo); | ||
// write encoding byte | ||
ec.encodeByte(encoding_mask,stream); | ||
// write symbolic id | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.SymbolicId)) { | ||
ec.encodeInt32(diagnosticInfo.identifier.symbolicId,stream); | ||
} | ||
// write namespace uri | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.NamespaceUri)) { | ||
ec.encodeInt32(diagnosticInfo.identifier.namespaceUri,stream); | ||
} | ||
// write locale | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.Locale)) { | ||
ec.encodeInt32(diagnosticInfo.identifier.locale,stream); | ||
} | ||
// write localized text | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.LocalizedText)) { | ||
ec.encodeInt32(diagnosticInfo.identifier.localizedText,stream); | ||
} | ||
// write additional info | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.AdditionalInfo)) { | ||
ec.encodeUAString(diagnosticInfo.additionalInfo,stream); | ||
} | ||
// write inner status code | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.InnerStatusCode)) { | ||
ec.encodeInt32(diagnosticInfo.innerStatusCode,stream); | ||
} | ||
// write innerDiagnosticInfo | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.InnerDiagnosticInfo)) { | ||
assert(diagnosticInfo.innerDiagnosticInfo!=null,"missing innerDiagnosticInfo"); | ||
diagnosticInfo.innerDiagnosticInfo.encode(stream); | ||
} | ||
}, | ||
decode: function(diagnosticInfo,stream) { | ||
var encoding_mask = ec.decodeByte(stream); | ||
// read symbolic id | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.SymbolicId)) { | ||
diagnosticInfo.identifier.symbolicId = ec.decodeInt32(stream); | ||
} | ||
// read namespace uri | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.NamespaceUri)) { | ||
diagnosticInfo.identifier.namespaceUri = ec.decodeInt32(stream); | ||
} | ||
// read locale | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.Locale)) { | ||
diagnosticInfo.identifier.locale = ec.decodeInt32(stream); | ||
} | ||
// read localized text | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.LocalizedText)) { | ||
diagnosticInfo.identifier.localizedText= ec.decodeInt32(stream); | ||
} | ||
// read additional info | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.AdditionalInfo)) { | ||
diagnosticInfo.additionalInfo = ec.decodeUAString(stream); | ||
} | ||
// read inner status code | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.InnerStatusCode)) { | ||
diagnosticInfo.innerStatusCode = ec.decodeInt32(stream); | ||
} | ||
// read inner status code | ||
if( check_flag(encoding_mask,DiagnosticInfo_EncodingByte.InnerDiagnosticInfo)) { | ||
diagnosticInfo.innerDiagnosticInfo = new exports.DiagnosticInfo({}); | ||
diagnosticInfo.innerDiagnosticInfo.decode(stream); | ||
} | ||
} | ||
}; | ||
exports.DiagnosticInfo = factories.UAObjectFactoryBuild(DiagnosticInfo_Description); | ||
{ | ||
"name": "node-opcua", | ||
"version": "0.0.1", | ||
"description": "pure nodejs OPCUA SDK", | ||
"main": "", | ||
"bin": {}, | ||
"directories": { | ||
"test": "test" | ||
}, | ||
"scripts": { | ||
"test": "mocha test" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/erossignon/node-opcua.git" | ||
}, | ||
"keywords": [ | ||
"OPC UA", | ||
"M2M" | ||
], | ||
"author": "Etienne Rossignon", | ||
"license": "MIT", | ||
"bugs" : { | ||
"url": "git://github.com/erossignon/node-opcua/issues" | ||
}, | ||
"dependencies": { | ||
"int64-native": "~0.2.1", | ||
"should": "~3.0.1", | ||
"colors": "~0.6.2", | ||
"x509": "0.0.7", | ||
"enum": "~0.2.5" | ||
} | ||
"name": "node-opcua", | ||
"version": "0.0.2", | ||
"description": "pure nodejs OPCUA SDK", | ||
"main": "", | ||
"bin": {}, | ||
"directories": { | ||
"test": "test" | ||
}, | ||
"scripts": { | ||
"test": "mocha test -R spec --recursive" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/erossignon/node-opcua.git" | ||
}, | ||
"keywords": [ | ||
"OPC UA", | ||
"M2M" | ||
], | ||
"author": "Etienne Rossignon", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "git://github.com/erossignon/node-opcua/issues" | ||
}, | ||
"devDependencies": { | ||
"mocha": "*", | ||
"should": "*", | ||
"csv": "~0.3.6" | ||
}, | ||
"dependencies": { | ||
"colors": "~0.6.2", | ||
"enum": "~0.2.5", | ||
"async": "~0.2.9", | ||
"underscore": "~1.6.0", | ||
"ignite": "~0.1.8", | ||
"machina": "~0.3.4", | ||
"through2": "~0.4.1", | ||
"node-xml": "~1.0.2", | ||
"node-expat": "~2.1.4", | ||
"tmp": "0.0.23", | ||
"sprintf": "~0.1.3", | ||
"hexy": "~0.2.5", | ||
"optimist": "~0.6.0", | ||
"prettyjson": "~0.11.1", | ||
"treeify": "~1.0.1", | ||
"coveralls": "~2.7.0", | ||
"mocha-lcov-reporter": "0.0.1", | ||
"node-int64": "~0.3.0", | ||
"easy-table": "~0.3.0", | ||
"tracer": "~0.6.2", | ||
"base64-js": "0.0.6" | ||
} | ||
} |
@@ -7,2 +7,10 @@ node-opcua | ||
[![NPM version](https://badge.fury.io/js/node-opcua.png)](http://badge.fury.io/js/node-opcua) | ||
[![Build Status](https://travis-ci.org/erossignon/node-opcua.png?branch=master)](https://travis-ci.org/erossignon/node-opcua) | ||
[![Dependency Status](https://gemnasium.com/erossignon/node-opcua.png)](https://gemnasium.com/erossignon/node-opcua) | ||
[![Coverage Status](https://coveralls.io/repos/erossignon/node-opcua/badge.png)](https://coveralls.io/r/erossignon/node-opcua) | ||
node-opcua is an experimental OPC-UA stack written in NodeJS. | ||
@@ -15,1 +23,21 @@ | ||
Getting started: | ||
================ | ||
$ git clone git://github.com/erossignon/node-opcua.git node-opcua | ||
$ cd node-opcua | ||
$ npm install | ||
$ npm test | ||
[![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=gadz_er&url=https://github.com/erossignon/node-opcua&title=Node-OPCUA&language=nodejs&tags=github&category=software) | ||
[![NPM](https://nodei.co/npm/node-opcua.png?downloads=true&stars=true)](https://nodei.co/npm/node-opcua/) | ||
[![Project Stats](https://www.ohloh.net/p/713850/widgets/project_thin_badge.gif)](https://www.ohloh.net/p/node-opcua) |
var should = require("should"); | ||
var ec = require("../lib/encode_decode"); | ||
var opcua = require("../lib/nodeopcua"); | ||
var BinaryStream = require("../lib/binaryStream").BinaryStream; | ||
describe("testing built-in type encoding",function() { | ||
function test_encode_decode(obj, encode_func, decode_func, expectedLength, verify_buffer_func) { | ||
var binaryStream = new BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
encode_func(obj, binaryStream); | ||
binaryStream.length.should.equal(expectedLength); | ||
it("should encode a boolean as a single byte",function(){ | ||
if (verify_buffer_func) { | ||
verify_buffer_func(binaryStream._buffer); | ||
} | ||
binaryStream.rewind(); | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
var obj_verif = decode_func(binaryStream); | ||
binaryStream.length.should.equal(expectedLength); | ||
ec.encodeBoolean(true,binaryStream); | ||
binaryStream.length.should.equal(1); | ||
if (obj !== undefined) { | ||
obj_verif.should.eql(obj); | ||
binaryStream.rewind(); | ||
var boolValue =ec.decodeBoolean(binaryStream); | ||
boolValue.should.equal(true); | ||
} else { | ||
should.not.exists(obj_verif); | ||
} | ||
} | ||
describe("testing built-in type encoding", function () { | ||
it("should encode and decode a boolean as a single byte", function () { | ||
test_encode_decode(true, ec.encodeBoolean, ec.decodeBoolean, 1); | ||
test_encode_decode(false, ec.encodeBoolean, ec.decodeBoolean, 1); | ||
}); | ||
it("should encode a Integer as a 4 byte stream",function(){ | ||
it("should encode and decode a Integer (4 bytes)", function () { | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
ec.encodeInt32(1000000000,binaryStream); | ||
binaryStream.length.should.equal(4); | ||
test_encode_decode(1000000000, ec.encodeInt32, ec.decodeInt32, 4, function (buffer) { | ||
// should be little endian | ||
buffer.readUInt8(0).should.equal(0x00); | ||
buffer.readUInt8(1).should.equal(0xCA); | ||
buffer.readUInt8(2).should.equal(0x9A); | ||
buffer.readUInt8(3).should.equal(0x3B); | ||
}); | ||
test_encode_decode(-100000000, ec.encodeInt32, ec.decodeInt32, 4); | ||
// should be little endian | ||
//xx console.log(binaryStream.stream.slice(0,4));//toJSON()); | ||
binaryStream.stream.readUInt8(0).should.equal(0x00); | ||
binaryStream.stream.readUInt8(1).should.equal(0xCA); | ||
binaryStream.stream.readUInt8(2).should.equal(0x9A); | ||
binaryStream.stream.readUInt8(3).should.equal(0x3B); | ||
binaryStream.rewind(); | ||
var check_value =ec.decodeInt32(binaryStream); | ||
check_value.should.equal(1000000000); | ||
}); | ||
it("should encode a Floating Point as a 4 byte stream",function(){ | ||
// I EEE-754 | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
it("should encode and decode a Floating Point (4 bytes)", function () { | ||
var value = -6.5; | ||
ec.encodeFloat(value,binaryStream); | ||
// I EEE-754 | ||
test_encode_decode(value, ec.encodeFloat, ec.decodeFloat, 4, function (buffer) { | ||
// should be little endian | ||
buffer.readUInt8(0).should.equal(0x00); | ||
buffer.readUInt8(1).should.equal(0x00); | ||
buffer.readUInt8(2).should.equal(0xD0); | ||
buffer.readUInt8(3).should.equal(0xC0); | ||
}); | ||
binaryStream.length.should.equal(4); | ||
// should be little endian | ||
binaryStream.stream.readUInt8(0).should.equal(0x00); | ||
binaryStream.stream.readUInt8(1).should.equal(0x00); | ||
binaryStream.stream.readUInt8(2).should.equal(0xD0); | ||
binaryStream.stream.readUInt8(3).should.equal(0xC0); | ||
binaryStream.rewind(); | ||
var check_value =ec.decodeFloat(binaryStream); | ||
check_value.should.equal(value); | ||
}); | ||
it("should encode a Double Point as a 8 byte stream",function(){ | ||
it("should encode and decode a Double Point (8 bytes)", function () { | ||
// I EEE-754 | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
var value = -6.5; | ||
ec.encodeDouble(value,binaryStream); | ||
// I EEE-754 | ||
test_encode_decode(value, ec.encodeDouble, ec.decodeDouble, 8, function (buffer) { | ||
// should be little endian | ||
buffer.readUInt8(0).should.equal(0x00); | ||
buffer.readUInt8(1).should.equal(0x00); | ||
buffer.readUInt8(2).should.equal(0x00); | ||
buffer.readUInt8(3).should.equal(0x00); | ||
buffer.readUInt8(4).should.equal(0x00); | ||
buffer.readUInt8(5).should.equal(0x00); | ||
buffer.readUInt8(6).should.equal(0x1a); | ||
buffer.readUInt8(7).should.equal(0xc0); | ||
}); | ||
binaryStream.length.should.equal(8); | ||
// should be little endian | ||
//xx console.log(binaryStream.stream.slice(0,8));//toJSON()); | ||
binaryStream.stream.readUInt8(0).should.equal(0x00); | ||
binaryStream.stream.readUInt8(1).should.equal(0x00); | ||
binaryStream.stream.readUInt8(2).should.equal(0x00); | ||
binaryStream.stream.readUInt8(3).should.equal(0x00); | ||
binaryStream.stream.readUInt8(4).should.equal(0x00); | ||
binaryStream.stream.readUInt8(5).should.equal(0x00); | ||
binaryStream.stream.readUInt8(6).should.equal(0x1a); | ||
binaryStream.stream.readUInt8(7).should.equal(0xc0); | ||
binaryStream.rewind(); | ||
var check_value =ec.decodeDouble(binaryStream); | ||
check_value.should.equal(value); | ||
}); | ||
it("should encode a null string" ,function() { | ||
it("should encode and decode a null string", function () { | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
var value = undefined; | ||
ec.encodeUAString(value,binaryStream); | ||
binaryStream.length.should.equal(4); | ||
binaryStream.stream.readUInt8(0).should.equal(0xff); | ||
binaryStream.stream.readUInt8(1).should.equal(0xff); | ||
binaryStream.stream.readUInt8(2).should.equal(0xff); | ||
binaryStream.stream.readUInt8(3).should.equal(0xff); | ||
test_encode_decode(value, ec.encodeUAString, ec.decodeUAString, 4, function (buffer) { | ||
// should be little endian | ||
buffer.readUInt8(0).should.equal(0xff); | ||
buffer.readUInt8(1).should.equal(0xff); | ||
buffer.readUInt8(2).should.equal(0xff); | ||
buffer.readUInt8(3).should.equal(0xff); | ||
}); | ||
binaryStream.rewind(); | ||
var check_value =ec.decodeUAString(binaryStream); | ||
should.not.exists(check_value); | ||
}); | ||
it("should encode a normal string" ,function() { | ||
it("should encode and decode a normal string", function () { | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
var value = "Hello"; | ||
var value ="Hello"; | ||
ec.encodeUAString(value,binaryStream); | ||
test_encode_decode(value, ec.encodeUAString, ec.decodeUAString, 9, function (buffer) { | ||
// should be little endian | ||
buffer.readUInt8(0).should.equal(0x05); | ||
buffer.readUInt8(1).should.equal(0x00); | ||
buffer.readUInt8(2).should.equal(0x00); | ||
buffer.readUInt8(3).should.equal(0x00); | ||
buffer.readUInt8(4).should.equal('H'.charCodeAt(0)); | ||
buffer.readUInt8(5).should.equal('e'.charCodeAt(0)); | ||
buffer.readUInt8(6).should.equal('l'.charCodeAt(0)); | ||
buffer.readUInt8(7).should.equal('l'.charCodeAt(0)); | ||
buffer.readUInt8(8).should.equal('o'.charCodeAt(0)); | ||
}); | ||
binaryStream.length.should.equal(4 + 5); | ||
// should be little endian | ||
binaryStream.stream.readUInt8(0).should.equal(0x05); | ||
binaryStream.stream.readUInt8(1).should.equal(0x00); | ||
binaryStream.stream.readUInt8(2).should.equal(0x00); | ||
binaryStream.stream.readUInt8(3).should.equal(0x00); | ||
binaryStream.stream.readUInt8(4).should.equal('H'.charCodeAt(0)); | ||
binaryStream.stream.readUInt8(5).should.equal('e'.charCodeAt(0)); | ||
binaryStream.stream.readUInt8(6).should.equal('l'.charCodeAt(0)); | ||
binaryStream.stream.readUInt8(7).should.equal('l'.charCodeAt(0)); | ||
binaryStream.stream.readUInt8(8).should.equal('o'.charCodeAt(0)); | ||
}); | ||
binaryStream.rewind(); | ||
var check_value =ec.decodeUAString(binaryStream); | ||
check_value.should.equal(value); | ||
it("should encode and decode a DateTime", function () { | ||
var value = new Date(2014,0,2,15,0); | ||
test_encode_decode(value, ec.encodeDateTime, ec.decodeDateTime, 8, function (buffer) { | ||
// todo | ||
}); | ||
}); | ||
it("should encode a DateTime" ,function() { | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
it("should encode and decode a GUID", function () { | ||
var value = new Date(); | ||
ec.encodeDateTime(value,binaryStream); | ||
var value = "72962B91-FA75-4AE6-8D28-B404DC7DAF63"; | ||
binaryStream.length.should.equal(8); | ||
test_encode_decode(value, ec.encodeGUID, ec.decodeGUID, 16, function (buffer) { | ||
buffer.readUInt8(0).should.equal(0x91); | ||
buffer.readUInt8(1).should.equal(0x2B); | ||
buffer.readUInt8(2).should.equal(0x96); | ||
buffer.readUInt8(3).should.equal(0x72); | ||
binaryStream.rewind(); | ||
var checked_value = ec.decodeDateTime(binaryStream); | ||
checked_value.should.eql(value); | ||
buffer.readUInt8(4).should.equal(0x75); | ||
buffer.readUInt8(5).should.equal(0xFA); | ||
}); | ||
buffer.readUInt8(6).should.equal(0xE6); | ||
buffer.readUInt8(7).should.equal(0x4A); | ||
it("should encode a GUID" ,function() { | ||
buffer.readUInt8(8).should.equal(0x8D); | ||
buffer.readUInt8(9).should.equal(0x28); | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
buffer.readUInt8(10).should.equal(0xB4); | ||
buffer.readUInt8(11).should.equal(0x04); | ||
buffer.readUInt8(12).should.equal(0xDC); | ||
buffer.readUInt8(13).should.equal(0x7D); | ||
buffer.readUInt8(14).should.equal(0xAF); | ||
buffer.readUInt8(15).should.equal(0x63); | ||
}); | ||
}); | ||
var value = "72962B91-FA75-4AE6-8D28-B404DC7DAF63"; | ||
ec.encodeGUID(value,binaryStream); | ||
binaryStream.length.should.equal(16); | ||
it("should encode and decode a ByteString", function () { | ||
var buf = new Buffer(256); | ||
buf.write("THIS IS MY BUFFER"); | ||
binaryStream.rewind(); | ||
// should be little endian | ||
binaryStream.readUInt8().should.equal(0x91); | ||
binaryStream.readUInt8().should.equal(0x2B); | ||
binaryStream.readUInt8().should.equal(0x96); | ||
binaryStream.readUInt8().should.equal(0x72); | ||
test_encode_decode(buf, ec.encodeByteString, ec.decodeByteString, 256 + 4, function (buffer) { | ||
binaryStream.readUInt8().should.equal(0x75); | ||
binaryStream.readUInt8().should.equal(0xFA); | ||
buffer.readUInt32LE(0).should.equal(256); | ||
}); | ||
//xx check_buf.toString('hex').should.equal(buf.toString('hex')); | ||
binaryStream.readUInt8().should.equal(0xE6); | ||
binaryStream.readUInt8().should.equal(0x4A); | ||
binaryStream.readUInt8().should.equal(0x8D); | ||
binaryStream.readUInt8().should.equal(0x28); | ||
}); | ||
binaryStream.readUInt8().should.equal(0xB4); | ||
binaryStream.readUInt8().should.equal(0x04); | ||
binaryStream.readUInt8().should.equal(0xDC); | ||
binaryStream.readUInt8().should.equal(0x7D); | ||
binaryStream.readUInt8().should.equal(0xAF); | ||
binaryStream.readUInt8().should.equal(0x63); | ||
it("should encode and decode a two byte NodeId", function () { | ||
binaryStream.rewind(); | ||
var checked_value = ec.decodeGUID(binaryStream); | ||
checked_value.should.eql(value); | ||
var nodeId = ec.makeNodeId(25); | ||
nodeId.identifierType.should.eql(ec.NodeIdType.NUMERIC); | ||
test_encode_decode( | ||
nodeId, | ||
ec.encodeNodeId, | ||
ec.decodeNodeId, | ||
2, | ||
function verify_buffer(buffer) { | ||
buffer.readUInt8(0).should.equal(0); | ||
buffer.readUInt8(1).should.equal(25); // namespace | ||
} | ||
); | ||
@@ -194,71 +193,156 @@ }); | ||
it("should encode and decode a ByteString" ,function() { | ||
it("should encode and decode a four byte NodeId", function () { | ||
var buf = new Buffer(256); | ||
buf.write("THIS IS MY BUFFER"); | ||
var nodeId = ec.makeNodeId(258); | ||
nodeId.identifierType.should.eql(ec.NodeIdType.NUMERIC); | ||
test_encode_decode( | ||
nodeId, | ||
ec.encodeNodeId, | ||
ec.decodeNodeId, | ||
4, | ||
function verify_buffer(buffer) { | ||
buffer.readUInt8(0).should.equal(1); | ||
buffer.readUInt8(1).should.equal(0); // namespace | ||
buffer.readUInt16LE(2).should.equal(258); | ||
} | ||
); | ||
}); | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
it("should encode and decode a Numeric NodeId", function () { | ||
ec.encodeByteString(buf,binaryStream); | ||
binaryStream.length.should.equal(256+4); | ||
var nodeId = ec.makeNodeId(545889, 2500); | ||
nodeId.identifierType.should.eql(ec.NodeIdType.NUMERIC); | ||
test_encode_decode( | ||
nodeId, | ||
ec.encodeNodeId, | ||
ec.decodeNodeId, | ||
7 | ||
); | ||
}); | ||
binaryStream.rewind(); | ||
var check_buf = ec.decodeByteString(binaryStream); | ||
check_buf.length.should.equal(buf.length); | ||
it("should encode and decode a String NodeId", function () { | ||
var nodeId = ec.makeNodeId("SomeStuff", 2500); | ||
nodeId.identifierType.should.eql(ec.NodeIdType.STRING); | ||
test_encode_decode( | ||
nodeId, | ||
ec.encodeNodeId, | ||
ec.decodeNodeId, | ||
4 + 9 + 2 + 1 | ||
); | ||
}); | ||
it("should encode a two byte NodeId" ,function() { | ||
// standard binary encoding | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
xit("should encode and decode a Guid NodeId", function () { | ||
// todo | ||
}); | ||
var value = ec.makeNodeId(25); | ||
ec.encodeNodeId(value,binaryStream); | ||
it("should encode and decode a Opaque NodeId", function () { | ||
binaryStream.length.should.equal(2); | ||
binaryStream.stream.readUInt8(0).should.equal(0); | ||
binaryStream.stream.readUInt8(1).should.equal(25); | ||
var value = Buffer(32); | ||
for(var i= 0; i< 32;i++) { value.writeUInt8(i,i); } | ||
var nodeId = ec.makeNodeId(value, 0x1BCD); | ||
nodeId.identifierType.should.equal(ec.NodeIdType.BYTESTRING); | ||
var expectedLength = 1+ 2 + 4 + 32; | ||
test_encode_decode(nodeId, ec.encodeNodeId, ec.decodeNodeId, expectedLength, function (buffer) { | ||
// cod | ||
buffer.readUInt8(0).should.equal(0x05); | ||
// namespace | ||
buffer.readUInt8(1).should.equal(0xCD); | ||
buffer.readUInt8(2).should.equal(0x1B); | ||
// size | ||
buffer.readUInt32LE(3).should.equal(32); | ||
buffer.readUInt8( 7).should.equal(0x00); | ||
buffer.readUInt8( 8).should.equal(0x01); | ||
buffer.readUInt8( 9).should.equal(0x02); | ||
buffer.readUInt8(10).should.equal(0x03); | ||
buffer.readUInt8(11).should.equal(0x04); | ||
buffer.readUInt8(12).should.equal(0x05); | ||
// ... | ||
buffer.readUInt8(38).should.equal( 31); | ||
}); | ||
}); | ||
it("should encode a four byte NodeId" ,function() { | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
var value = ec.makeNodeId(258); | ||
ec.encodeNodeId(value,binaryStream); | ||
it("should encode and decode a Expanded NodeId - TwoBytes", function () { | ||
binaryStream.length.should.equal(4); | ||
binaryStream.stream.readUInt8(0).should.equal(1); | ||
binaryStream.stream.readUInt8(1).should.equal(0); // namespace | ||
binaryStream.stream.readUInt16LE(2).should.equal(258); | ||
test_encode_decode( | ||
ec.makeExpandedNodeId(10), | ||
ec.encodeExpandedNodeId, | ||
ec.decodeExpandedNodeId, | ||
2 | ||
); | ||
}); | ||
it("should encode and decode a Expanded NodeId - FourBytes", function () { | ||
test_encode_decode( | ||
ec.makeExpandedNodeId(32000), | ||
ec.encodeExpandedNodeId, | ||
ec.decodeExpandedNodeId, | ||
4 | ||
); | ||
}); | ||
it("should encode a Numeric NodeId" ,function() { | ||
}); | ||
it("should encode a long NodeId" ,function() { | ||
// standard binary encoding | ||
var binaryStream = new opcua.BinaryStream(); | ||
binaryStream.length.should.equal(0); | ||
}); | ||
var value = new ec.makeNodeId("SomeStuff",2500); | ||
ec.encodeNodeId(value,binaryStream); | ||
binaryStream.stream.readUInt8(0).should.equal(3); | ||
describe("check OPCUA Date Type ", function () { | ||
it("should convert date in 2014 ",function(){ | ||
var date = new Date(2014,0,1); | ||
var nano = ec.dateToHundredNanoSecondFrom1601(date); | ||
var date2 = ec.hundredNanoSecondFrom1601ToDate(nano); | ||
date2.toString().should.equal(date.toString()); | ||
}); | ||
it("should encode a Guid NodeId" ,function() { | ||
it("dateToHundredNanoSecondFrom1601 should return 0 for 1st of January 1601",function(){ | ||
var date = new Date(Date.UTC(1601,0,1,0,0)); | ||
var nano = ec.dateToHundredNanoSecondFrom1601(date); | ||
nano.should.equal(0); | ||
}); | ||
it("should encode a Opaque NodeId" ,function() { | ||
it("dateToHundredNanoSecondFrom1601 should return xx nanos for 2st of January 1601",function(){ | ||
var date = new Date(Date.UTC(1601,0,2,0,0)); | ||
var nano = ec.dateToHundredNanoSecondFrom1601(date); | ||
nano.should.equal(24*60*60*1000*10000); | ||
}); | ||
it("should encode a Expanded NodeId" ,function() { | ||
it("hundredNanoSecondFrom1601ToDate and dateToHundredNanoSecondFrom1601 ",function(){ | ||
var date = new Date(1789,6,14,19,47); | ||
var nano = ec.dateToHundredNanoSecondFrom1601(date); | ||
var date2 = ec.hundredNanoSecondFrom1601ToDate(nano); | ||
date2.toString().should.equal(date.toString()); | ||
}); | ||
it("should decode 92c253d3 0cf7ce01 DateTime as Dec 12, 2013 08:36:09.747317000 ", function () { | ||
var buf = new Buffer(8); | ||
buf.writeUInt32BE(0x92c253d3, 0); | ||
buf.writeUInt32BE(0x0cf7ce01, 4); | ||
buf.readUInt8(0).should.equal(0x92); | ||
buf.readUInt8(1).should.equal(0xc2); | ||
buf.readUInt8(2).should.equal(0x53); | ||
buf.readUInt8(7).should.equal(0x01); | ||
var stream = new BinaryStream(buf); | ||
var date = ec.decodeDateTime(stream); | ||
stream.rewind(); | ||
ec.encodeDateTime(new Date(2013,11,12,9,36,9),stream); | ||
}); | ||
// | ||
// => | ||
}); | ||
@@ -5,4 +5,9 @@ var factories = require("../lib/factories"); | ||
var util = require("util"); | ||
var ec = require("../lib/encode_decode"); | ||
var encode_decode_round_trip_test = require("./utils/encode_decode_round_trip_test").encode_decode_round_trip_test; | ||
var Person_Description = { | ||
id: factories.next_available_id(), | ||
name: "Person", | ||
@@ -18,3 +23,3 @@ fields: [ | ||
var Employee_Description = { | ||
id: factories.next_available_id(), | ||
name: "Employee", | ||
@@ -28,9 +33,57 @@ fields: [ | ||
var Company_Description = { | ||
id: factories.next_available_id(), | ||
name: "Company", | ||
fields: [ | ||
{ name: "name", fieldType: "String" }, | ||
{ name: "employees", isArray: true, fieldType: "Employee" } | ||
] | ||
}; | ||
var Person = factories.UAObjectFactoryBuild(Person_Description); | ||
var Employee = factories.UAObjectFactoryBuild(Employee_Description); | ||
var Company = factories.UAObjectFactoryBuild(Company_Description); | ||
describe("testing object factory", function () { | ||
var ShapeType = factories.UAObjectFactoryBuild( { | ||
name: "EnumShapeType", | ||
isEnum: true, | ||
enumValues: { | ||
CIRCLE: 1, | ||
SQUARE: 2, | ||
RECTANGLE: 3, | ||
HEXAGON: 6 | ||
} | ||
}); | ||
var Color = factories.UAObjectFactoryBuild( { | ||
name: "EnumColor", | ||
isEnum: true, | ||
enumValues: { | ||
RED: 100, | ||
BLUE: 200, | ||
GREEN: 300 | ||
} | ||
}); | ||
var Shape = factories.UAObjectFactoryBuild({ | ||
id: factories.next_available_id(), | ||
name: "Shape", | ||
fields: [ | ||
{ name:"name", fieldType: "String" , defaultValue: function() { return "my shape";} }, | ||
{ name:"shapeType", fieldType: "EnumShapeType" }, | ||
{ name:"color", fieldType: "EnumColor", defaultValue: Color.GREEN }, | ||
{ name:"inner_color", fieldType: "EnumColor", defaultValue: function() { return Color.BLUE; }} | ||
] | ||
}); | ||
describe("Factories: testing object factory", function () { | ||
it("should construct a new object from a simple Class Description", function () { | ||
@@ -75,6 +128,4 @@ | ||
it("should encode and decode a object created from the Factory",function(){ | ||
it("should encode and decode a simple object created from the Factory",function(){ | ||
var Person = factories.UAObjectFactoryBuild(Person_Description); | ||
var person = new Person({lastName:"Joe"}); | ||
@@ -84,10 +135,4 @@ person.age = 50; | ||
var stream = new BinaryStream(); | ||
person.encode(stream); | ||
var person_reloaded = encode_decode_round_trip_test(person); | ||
stream.rewind(); | ||
var person_reloaded = new Person(); | ||
person_reloaded.decode(stream); | ||
person.lastName.should.equal(person_reloaded.lastName); | ||
@@ -97,3 +142,33 @@ person.age.should.equal(person_reloaded.age); | ||
}); | ||
it("should encode and decode a composite object created from the Factory",function(){ | ||
var employee = new Employee({ person: { lastName: "John"}, service: "R&D" }); | ||
encode_decode_round_trip_test(employee); | ||
}); | ||
it("should encode and decode a composite object containing an array",function(){ | ||
var company = new Company({name: "ACME" }); | ||
company.employees.length.should.equal(0); | ||
var employee = new Employee({ person: { lastName: "John"}, service: "R&D" }); | ||
company.employees.push(employee); | ||
company.employees.push(new Employee({ person: { lastName: "Peter"}, service: "R&D" })); | ||
company.employees.length.should.equal(2); | ||
encode_decode_round_trip_test(company); | ||
}); | ||
it("should handle subtype properly",function(){ | ||
@@ -110,2 +185,3 @@ | ||
name: "MyStruct", | ||
id: factories.next_available_id(), | ||
fields: [ | ||
@@ -120,19 +196,6 @@ { name: "value", fieldType: "MyInteger" } | ||
}); | ||
it('should handle enumeration properly',function(){ | ||
var ShapeType = factories.UAObjectFactoryBuild( { | ||
name: "EnumShapeType", | ||
isEnum: true, | ||
enumValues: { | ||
CIRCLE: 1, | ||
SQUARE: 2, | ||
RECTANGLE: 3 | ||
} | ||
}); | ||
var Shape = factories.UAObjectFactoryBuild({ | ||
name: "Shape", | ||
fields: [ | ||
{ name:"shapeType" , fieldType: "EnumShapeType" } | ||
] | ||
}); | ||
@@ -142,3 +205,7 @@ var shape = new Shape(); | ||
shape.shapeType.should.eql(ShapeType.CIRCLE); | ||
shape.name.should.eql("my shape"); | ||
shape.color.should.eql(Color.GREEN); | ||
shape.inner_color.should.eql(Color.BLUE); | ||
shape.shapeType = ShapeType.RECTANGLE; | ||
@@ -152,7 +219,26 @@ shape.shapeType.should.equal(ShapeType.RECTANGLE); | ||
}); | ||
it('should allow enumeration value to be passed in options during construction',function(){ | ||
var shape1 = new Shape({ shapeType: ShapeType.HEXAGON}); | ||
shape1.shapeType.should.eql(ShapeType.HEXAGON); | ||
var shape2 = new Shape({ shapeType: ShapeType.RECTANGLE}); | ||
shape2.shapeType.should.eql(ShapeType.RECTANGLE); | ||
}); | ||
it("should encode and decode a structure containing a enumeration properly",function(){ | ||
var shape = new Shape({name: "yo" , shapeType: ShapeType.HEXAGON , color: Color.BLUE }); | ||
encode_decode_round_trip_test(shape); | ||
}); | ||
}); | ||
describe("testing strong typed enums", function(){ | ||
it('installEnumProp should create a strong typed enum',function(){ | ||
describe("Factories: testing strong typed enums", function(){ | ||
it('installEnumProp should append a member as a strong typed enum',function(){ | ||
var ShapeType = factories.UAObjectFactoryBuild( { | ||
@@ -182,3 +268,107 @@ name: "EnumShapeType", | ||
}); | ||
}); | ||
describe("Factories: testing binaryStoreSize",function(){ | ||
it("should implement binaryStoreSize",function(){ | ||
var shape = new Shape(); | ||
shape.binaryStoreSize().should.be.greaterThan(10); | ||
}); | ||
}); | ||
describe("Factories: testing encodingDefaultBinary and constructObject",function(){ | ||
it("a factory object should have a encodingDefaultBinary",function(){ | ||
var company = new Company({name: "ACME"}); | ||
company.encodingDefaultBinary.should.eql(ec.makeExpandedNodeId(Company_Description.id)); | ||
}); | ||
it("should create a object from a encodingDefaultBinaryId", function() { | ||
var getObjectClassName = require("../lib/utils").getObjectClassName; | ||
var obj = factories.constructObject(ec.makeExpandedNodeId(Company_Description.id)); | ||
should(obj).have.property("_description"); | ||
obj._description.name.should.equal("Company"); | ||
getObjectClassName(obj).should.equal("Object"); | ||
}); | ||
it("should encode and decode a Object containing ByteString",function(done){ | ||
var Blob_Description = { | ||
id: factories.next_available_id(), | ||
name: "FakeBlob", | ||
fields: [ | ||
{ name: "name", fieldType: "String" }, | ||
{ name: "buffer0", fieldType: "ByteString" }, | ||
{ name: "buffer1", fieldType: "ByteString" } | ||
] | ||
}; | ||
var Blob = factories.UAObjectFactoryBuild(Blob_Description); | ||
var blob = new Blob({ buffer0: new Buffer(0), buffer1: new Buffer(1024) }); | ||
encode_decode_round_trip_test(blob); | ||
done(); | ||
}); | ||
it("should pretty print an object ",function(){ | ||
var company = new Company({name: "ACME" }); | ||
var employee = new Employee({ person: { lastName: "John"}, service: "R&D" }); | ||
company.employees.push(employee); | ||
company.employees.push(new Employee({ person: { lastName: "Peter"}, service: "R&D" })); | ||
var str = company.explore(); | ||
// console.log(str); | ||
}); | ||
it("should handle ExtensionObject ",function(){ | ||
var MetaShape_Description = { | ||
id: factories.next_available_id(), | ||
fields: [ | ||
{ name: "name", fieldType: "String" }, | ||
{ name: "shape", fieldType: "ExtensionObject" }, | ||
{ name: "comment", fieldType: "String" } | ||
] | ||
}; | ||
var MetaShape = factories.UAObjectFactoryBuild(MetaShape_Description); | ||
var shape = new MetaShape({ | ||
name: "MyCircle", | ||
shape: new Shape({name: "circle" , shapeType:ShapeType.CIRCLE , color: Color.BLUE}), | ||
comment: "this is a comment" | ||
}); | ||
shape.encodingDefaultBinary.should.eql(ec.makeExpandedNodeId(MetaShape_Description.id)); | ||
var stream = new BinaryStream(shape.binaryStoreSize()); | ||
shape.encode(stream); | ||
var encode_decode_round_trip_test = require("./utils/encode_decode_round_trip_test").encode_decode_round_trip_test; | ||
encode_decode_round_trip_test(shape); | ||
}); | ||
}); |
@@ -1,3 +0,3 @@ | ||
opcua = require("../lib/nodeopcua"); | ||
var opcua = require("../lib/nodeopcua"); | ||
var BinaryStream = require("../lib/binaryStream").BinaryStream; | ||
var should = require("should"); | ||
@@ -13,9 +13,7 @@ | ||
var stream = new opcua.BinaryStream(2000); | ||
opcua.encodeMessage('HEL',helloMessage1,stream); | ||
var message = opcua.packTcpMessage('HEL',helloMessage1); | ||
//xx console.log(helloMessage1); | ||
stream.rewind(); | ||
var stream = new BinaryStream(message); | ||
@@ -34,1 +32,23 @@ var helloMessage2 = opcua.decodeMessage(stream,opcua.HelloMessage); | ||
}); | ||
describe("testing parseEndpointUrl",function(){ | ||
it(" should parse a endpoint ",function(){ | ||
var ep = opcua.parseEndpointUrl("opc.tcp://abcd1234:51210/UA/SampleServer"); | ||
ep.protocol.should.equal("opc.tcp"); | ||
ep.hostname.should.equal("abcd1234"); | ||
ep.port.should.equal(51210); | ||
ep.address.should.equal("/UA/SampleServer"); | ||
}); | ||
it(" should parse this endpoint as well",function(){ | ||
var ep = opcua.parseEndpointUrl("opc.tcp://ABCD12354:51210/UA/SampleServer"); | ||
ep.protocol.should.equal("opc.tcp"); | ||
ep.hostname.should.equal("ABCD12354"); | ||
ep.port.should.equal(51210); | ||
ep.address.should.equal("/UA/SampleServer"); | ||
}); | ||
}); |
@@ -1,34 +0,136 @@ | ||
nodeopcua = require("../lib/nodeopcua"); | ||
OPCUAServer = require("../lib/opcua-server").OPCUAServer; | ||
OPCUAClient = require("../lib/opcua-client").OPCUAClient; | ||
var OPCUAServer = require("../lib/opcua-server").OPCUAServer; | ||
var OPCUAClient = require("../lib/opcua-client").OPCUAClient; | ||
var should = require("should"); | ||
var async = require("async"); | ||
var util = require("util"); | ||
var opcua = require("../lib/nodeopcua"); | ||
var debugLog = require("../lib/utils").make_debugLog(__filename); | ||
function is_valid_endpointUrl(endpointUrl) { | ||
var e = opcua.parseEndpointUrl(endpointUrl); | ||
if (e.hasOwnProperty("hostname")) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
describe("testing basic Client-Server communication",function() { | ||
var server , client; | ||
var endpointUrl ; | ||
beforeEach(function(done){ | ||
server = new OPCUAServer(); | ||
// we will connect to first server end point | ||
endpointUrl = server.endpoints[0].endpointDescription().endpointUrl; | ||
debugLog("endpointUrl",endpointUrl); | ||
is_valid_endpointUrl(endpointUrl).should.equal(true); | ||
server.start(function() { | ||
done(); | ||
}); | ||
client = new OPCUAClient(); | ||
}); | ||
afterEach(function(done){ | ||
server.shutdown(function() { | ||
done(); | ||
}); | ||
}); | ||
it("should start a server and accept a connection",function(done){ | ||
var server = new OPCUAServer(); | ||
server.listen(8345); | ||
server.connected_client_count.should.equal(0); | ||
var client = new OPCUAClient(); | ||
client.protocolVersion = 1; | ||
client.connect("localhost",8345, function callback(){ | ||
async.series([ | ||
function(callback) { | ||
client.connect(endpointUrl,callback); | ||
}, | ||
function(callback) { | ||
client.disconnect(callback); | ||
}, | ||
function(callback) { | ||
server.shutdown(callback); | ||
} | ||
],done); | ||
server.connected_client_count.should.equal(1); | ||
}); | ||
process.nextTick(function() { | ||
client.disconnect(function() { | ||
server.shutdown(function() { | ||
done(); | ||
}) | ||
}) | ||
}); | ||
it("Server should not accept connection, if protocol version is incompatible",function(done){ | ||
client.protocolVersion = 55555; // set a invalid protocol version | ||
server.connected_client_count.should.equal(0); | ||
async.series([ | ||
function(callback) { | ||
debugLog(" connect"); | ||
client.connect(endpointUrl,function(err){ | ||
debugLog(" Error =".yellow.bold,err); | ||
callback(err); | ||
}); | ||
}, | ||
function(callback) { | ||
server.shutdown(callback); | ||
} | ||
],function(err) { | ||
server.connected_client_count.should.equal(0); | ||
debugLog(" error : ", err); | ||
server.shutdown(done); | ||
}); | ||
}); | ||
it("Client shall be able to create a session with a anonymous token",function(done){ | ||
server.connected_client_count.should.equal(0); | ||
var g_session ; | ||
async.series([ | ||
function(callback) { | ||
debugLog(" connect"); | ||
client.connect(endpointUrl,function(err){ | ||
debugLog(" Error =".yellow.bold,err); | ||
callback(err); | ||
}); | ||
}, | ||
function(callback) { | ||
debugLog(" createSession"); | ||
client.createSession(function(err,session){ | ||
g_session = session; | ||
debugLog(" Error =".yellow.bold,err); | ||
client.transactionInProgress.should.equal(false); | ||
callback(err); | ||
}); | ||
client.transactionInProgress.should.equal(true); | ||
}, | ||
function(callback) { | ||
debugLog("closing session"); | ||
g_session.close(callback); | ||
}, | ||
function(callback) { | ||
debugLog("Disconnecting client"); | ||
client.disconnect(callback); | ||
} | ||
],function(err) { | ||
debugLog("finally"); | ||
server.connected_client_count.should.equal(0); | ||
debugLog(" error : ", err); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
@@ -5,2 +5,3 @@ var should = require("should"); | ||
var opcua = require("../lib/nodeopcua"); | ||
var BinaryStream = require("../lib/binaryStream").BinaryStream; | ||
@@ -16,9 +17,12 @@ describe("testing OPCUA structures ",function() { | ||
ltext.locale.should.equal("en-US"); | ||
}); | ||
it("should encode and decode a LocalizeText" , function() { | ||
it("should encode and decode a LocalizeText that have both text and locale" , function() { | ||
var ltext = new s.LocalizedText({text: "HelloWorld" , locale: "en-US"}); | ||
var stream = new opcua.BinaryStream(); | ||
var stream = new BinaryStream(); | ||
stream.length.should.equal(0); | ||
@@ -37,2 +41,3 @@ | ||
ltext_verif.text.should.equal("HelloWorld"); | ||
ltext_verif.locale.should.equal("en-US"); | ||
@@ -42,2 +47,53 @@ | ||
it("should encode and decode a LocalizeText that have text but no locale" , function() { | ||
var ltext = new s.LocalizedText({text: "HelloWorld" , locale: null }); | ||
ltext.should.have.property("locale"); | ||
should(ltext.locale).equal(null); | ||
var stream = new BinaryStream(); | ||
stream.length.should.equal(0); | ||
ltext.encode(stream); | ||
stream.length.should.be.greaterThan(0); | ||
var ltext_verif = new s.LocalizedText(); | ||
stream.rewind(); | ||
ltext_verif.decode(stream); | ||
ltext_verif.text.should.equal("HelloWorld"); | ||
should(ltext_verif.locale).equal(null); | ||
}); | ||
it("should encode and decode a LocalizeText that have no text but a locale" , function() { | ||
var ltext = new s.LocalizedText({text: null , locale: "en-US" }); | ||
ltext.should.have.property("text"); | ||
should(ltext.text).equal(null); | ||
var stream = new BinaryStream(); | ||
stream.length.should.equal(0); | ||
ltext.encode(stream); | ||
stream.length.should.be.greaterThan(0); | ||
var ltext_verif = new s.LocalizedText(); | ||
stream.rewind(); | ||
ltext_verif.decode(stream); | ||
ltext_verif.should.eql(ltext); | ||
ltext_verif.locale.should.equal("en-US"); | ||
ltext_verif.should.have.property("text"); | ||
should(ltext_verif.text).equal(null); | ||
}); | ||
it("should create a RequestHeader",function(){ | ||
@@ -44,0 +100,0 @@ |
var should = require("should"); | ||
var secure_channel = require("../lib/secure_channel_service"); | ||
var MessageBuilder = require("../lib/message_builder").MessageBuilder; | ||
var s = require("../lib/structures"); | ||
var async = require("async"); | ||
var util = require("util"); | ||
var compare_buffers = require("./../lib/utils").compare_buffers; | ||
var debugLog = require("../lib/utils").make_debugLog(__filename); | ||
var clone_buffer = secure_channel.clone_buffer; | ||
var MessageChunker = secure_channel.MessageChunker; | ||
describe("SecureMessageChunkManager",function(){ | ||
it("should reconstruct a valid message when message is received in multiple chunks",function(done){ | ||
// a very large endPointResponse spanning on multiple chunks ... | ||
var endPointResponse = require("./fixtures/fixture_GetEndPointResponse").fixture2; | ||
var requestId = 0x1000; | ||
var chunk_stack = []; | ||
var fullBufferForVerif = null; | ||
async.series([ | ||
function (callback) { | ||
var options = { | ||
requestId: requestId, | ||
tokenId: 1 | ||
}; | ||
endPointResponse.responseHeader.requestHandle = requestId; | ||
var chunker = new MessageChunker(); | ||
chunker.chunkSecureMessage("MSG",options,endPointResponse,function(messageChunk) { | ||
if (messageChunk) { | ||
chunk_stack.push(clone_buffer(messageChunk)); | ||
} else { | ||
fullBufferForVerif = secure_channel.fullbuf; | ||
callback(); | ||
} | ||
}); | ||
}, | ||
function (callback) { | ||
// let verify that each intermediate chunk is marked with "C" and final chunk is marked with "F" | ||
for (var i = 0; i<chunk_stack.length -1;i++) { | ||
String.fromCharCode(chunk_stack[i].readUInt8(3)).should.equal("C"); | ||
} | ||
String.fromCharCode(chunk_stack[i].readUInt8(3)).should.equal("F"); | ||
callback(); | ||
}, | ||
function (callback) { | ||
chunk_stack.length.should.be.greaterThan(0); | ||
// now apply the opposite operation by reconstructing the message from chunk and | ||
// decoding the inner object | ||
// console.log(" message Builder"); | ||
var messageBuilder = new MessageBuilder(); | ||
messageBuilder.on("full_message_body",function(full_message_body){ | ||
compare_buffers(fullBufferForVerif,full_message_body,40); | ||
}).on("message",function(message) { | ||
//xx console.log("message = ", util.inspect(message)); | ||
message.should.eql(endPointResponse); | ||
// check also that requestId has been properly installed by chunkSecureMessage | ||
callback(); | ||
}).on("error",function(errCode){ | ||
callback(new Error("Error : code 0x"+ errCode.toString(16))); | ||
}); | ||
// feed messageBuilder with | ||
chunk_stack.forEach(function(chunk){ | ||
// let simulate a real TCP communication | ||
// where our messageChunk would be split into several packages.... | ||
var l1 = Math.round(chunk.length/3); // arbitrarily split into 2 packets : 1/3 and 2/3 | ||
// just for testing the ability to reassemble data block | ||
var data1 = chunk.slice(0,l1); | ||
var data2 = chunk.slice(l1); | ||
messageBuilder.feed(data1); | ||
messageBuilder.feed(data2); | ||
}); | ||
} | ||
],done); | ||
}); | ||
it("should receive an ERR message",function(done){ | ||
var messageBuilder = new MessageBuilder(); | ||
messageBuilder.on("full_message_body",function(full_message_body){ | ||
debugLog(" On raw Buffer \n" ); | ||
debugLog(require("../lib/utils").hexDump(full_message_body)); | ||
}).on("message",function(message) { | ||
debugLog(" message ", message); | ||
done(); | ||
}).on("error",function(errCode){ | ||
debugLog(" errCode ", errCode); | ||
done(); | ||
}); | ||
var makebuffer_from_trace = require("./utils/makebuffer_from_trace").makebuffer_from_trace; | ||
var packet =makebuffer_from_trace(function(){ | ||
/* | ||
00000000: 4d 53 47 46 64 00 00 00 0c 00 00 00 01 00 00 00 04 00 00 00 03 00 00 00 01 00 8d 01 00 00 00 00 MSGFd........................... | ||
00000020: 00 00 00 00 00 00 00 00 00 00 82 80 24 00 00 00 00 00 00 00 80 01 00 00 00 24 00 00 00 55 6e 65 ............$............$...Une | ||
00000040: 78 70 65 63 74 65 64 20 65 72 72 6f 72 20 70 72 6f 63 65 73 73 69 6e 67 20 72 65 71 75 65 73 74 xpected.error.processing.request | ||
00000060: 2e 00 00 00 .... | ||
*/ | ||
}); | ||
messageBuilder.feed(packet); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
3480995
122
14931
42
21
3
1
15
5
+ Addedasync@~0.2.9
+ Addedbase64-js@0.0.6
+ Addedcoveralls@~2.7.0
+ Addedeasy-table@~0.3.0
+ Addedhexy@~0.2.5
+ Addedignite@~0.1.8
+ Addedmachina@~0.3.4
+ Addedmocha-lcov-reporter@0.0.1
+ Addednode-expat@~2.1.4
+ Addednode-int64@~0.3.0
+ Addednode-xml@~1.0.2
+ Addedoptimist@~0.6.0
+ Addedprettyjson@~0.11.1
+ Addedsprintf@~0.1.3
+ Addedthrough2@~0.4.1
+ Addedtmp@0.0.23
+ Addedtracer@~0.6.2
+ Addedtreeify@~1.0.1
+ Addedunderscore@~1.6.0
+ Addedasync@0.2.10(transitive)
+ Addedaws-sign@0.2.1(transitive)
+ Addedbase64-js@0.0.6(transitive)
+ Addedbindings@1.5.0(transitive)
+ Addedboom@0.3.8(transitive)
+ Addedcombined-stream@0.0.7(transitive)
+ Addedcookie-jar@0.2.0(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addedcoveralls@2.7.1(transitive)
+ Addedcryptiles@0.1.3(transitive)
+ Addeddateformat@1.0.2-1.2.3(transitive)
+ Addeddelayed-stream@0.0.5(transitive)
+ Addedeasy-table@0.3.0(transitive)
+ Addedfile-uri-to-path@1.0.0(transitive)
+ Addedforever-agent@0.2.0(transitive)
+ Addedform-data@0.0.10(transitive)
+ Addedhawk@0.10.2(transitive)
+ Addedhexy@0.2.11(transitive)
+ Addedhoek@0.7.6(transitive)
+ Addedignite@0.1.8(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@0.0.1(transitive)
+ Addedjson-stringify-safe@3.0.0(transitive)
+ Addedlcov-parse@0.0.4(transitive)
+ Addedlodash@2.4.2(transitive)
+ Addedlog-driver@1.2.1(transitive)
+ Addedmachina@0.3.8(transitive)
+ Addedmime@1.2.11(transitive)
+ Addedminimist@0.0.10(transitive)
+ Addedmocha-lcov-reporter@0.0.1(transitive)
+ Addednan@0.6.02.22.0(transitive)
+ Addednode-expat@2.1.4(transitive)
+ Addednode-int64@0.3.3(transitive)
+ Addednode-proxy@1.0.0(transitive)
+ Addednode-uuid@1.4.8(transitive)
+ Addednode-xml@1.0.2(transitive)
+ Addedoauth-sign@0.2.0(transitive)
+ Addedobject-keys@0.4.0(transitive)
+ Addedoptimist@0.6.1(transitive)
+ Addedprettyjson@0.11.1(transitive)
+ Addedqs@0.5.6(transitive)
+ Addedreadable-stream@1.0.34(transitive)
+ Addedrequest@2.16.2(transitive)
+ Addedsntp@0.1.4(transitive)
+ Addedsprintf@0.1.5(transitive)
+ Addedsprintf-js@1.1.3(transitive)
+ Addedstring_decoder@0.10.31(transitive)
+ Addedthrough2@0.4.2(transitive)
+ Addedtinytim@0.1.1(transitive)
+ Addedtmp@0.0.23(transitive)
+ Addedtracer@0.6.2(transitive)
+ Addedtreeify@1.0.1(transitive)
+ Addedtunnel-agent@0.2.0(transitive)
+ Addeduglify-js@1.3.5(transitive)
+ Addedunderscore@1.6.0(transitive)
+ Addedunderscore.string@3.3.6(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedwordwrap@0.0.3(transitive)
+ Addedxtend@2.1.2(transitive)
+ Addedyaml@0.2.3(transitive)
- Removedint64-native@~0.2.1
- Removedshould@~3.0.1
- Removedx509@0.0.7
- Removedint64-native@0.2.1(transitive)
- Removedshould@3.0.1(transitive)
- Removedx509@0.0.7(transitive)