Skip to content

Commit

Permalink
add UserIdentityToken.UserName support
Browse files Browse the repository at this point in the history
  • Loading branch information
erossignon committed Aug 18, 2023
1 parent 0954cb9 commit 4cfb4ae
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
- run: npm ci
- run: npm install node-red @types/node-red mocha -D
- run: npm run build
- run: npm run mocha
- run: npm run mocha
3 changes: 3 additions & 0 deletions source/nodes/OpcUa-Endpoint2/OpcUa-Endpoint2.html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ RED.nodes.registerType<OpcUaEndpoint2NodeProperties, CRED>("OpcUa-Endpoint2", {
securityPolicy: { value: "None" },

userIdentityType: { value: "Anonymous" },
// to use when userIdentityType is UserName
userName: { value: "" },
password: { value: "", type: "password" },

// to use when userIdentityType is Certificate
userCertificate: { value: "" },
Expand Down
2 changes: 2 additions & 0 deletions source/nodes/OpcUa-Endpoint2/OpcUa-Endpoint2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const nodeInit: NodeInitializer = (RED): void => {
this.securityMode = config.securityMode;
///
this.userIdentityType = config.userIdentityType;
this.userName = config.userName;
this.password = config.password;
this.userCertificate = config.userCertificate;
this.userPrivatekey = config.userPrivatekey;

Expand Down
4 changes: 4 additions & 0 deletions source/nodes/OpcUa-Endpoint2/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface OpcUaEndpoint2Attributes {
// User Identity
userIdentityType: UserTokenTypeStr;

// User Identity: UserName
userName: string;
password: string;

// User Identity: X509 Certificate
userCertificate: string;
userPrivatekey: string;
Expand Down
31 changes: 30 additions & 1 deletion source/nodes/tools/connection_tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ limitations under the License.

import { Node } from "node-red";
import { ClientSession, OPCUAClient } from "node-opcua-client";
import { convertPEMtoDER } from "node-opcua-crypto";
import { OpcUaEndpoint2Node } from "../../nodes/OpcUa-Endpoint2/shared";
import {
ClientSubscription,
ClientSubscriptionOptions,
MessageSecurityMode,
SecurityPolicy,
UserIdentityInfo,
UserTokenType,
coerceSecurityPolicy,
} from "node-opcua";

Expand Down Expand Up @@ -52,6 +55,25 @@ interface Resolver {
const pendingConnection = new Map<string, Resolver[]>();

const doDebug = false;

const makeUserIdentityInfo = (endpointNode: OpcUaEndpoint2Node): UserIdentityInfo => {
switch (endpointNode.userIdentityType) {
case "UserName":
return {
type: UserTokenType.UserName,
userName: endpointNode.userName,
password: endpointNode.password,
};
case "Certificate":
return {
type: UserTokenType.Certificate,
certificateData: convertPEMtoDER(endpointNode.userCertificate),
privateKey: "", // convertPEMtoDER(endpointNode.userPrivatekey),
};
case "Anonymous":
return { type: UserTokenType.Anonymous };
}
};
export async function get_opcua_endpoint(endpointNode: OpcUaEndpoint2Node, requestingNode?: Node): Promise<GetOpcUAEndpointResult> {
const endpointId = endpointNode.id;
let connection = connections.get(endpointId);
Expand Down Expand Up @@ -96,12 +118,15 @@ export async function get_opcua_endpoint(endpointNode: OpcUaEndpoint2Node, reque
nodeToNotify.warn("backoff " + attempt + " " + delay);
});
client.on("connection_reestablished", () => {
doDebug && console.log("connection_reestablished");
nodeToNotify.warn("connection_reestablished");
});
client.on("start_reconnection", () => {
doDebug && console.log("start_reconnection");
nodeToNotify.warn("start_reconnection");
});
client.on("close", () => {
doDebug && console.log("client close");
nodeToNotify.warn("client close");
});

Expand All @@ -113,7 +138,10 @@ export async function get_opcua_endpoint(endpointNode: OpcUaEndpoint2Node, reque
};
try {
await client.connect(endpointNode.endpoint);
const session = await client.createSession();

const userIdentityInfo = makeUserIdentityInfo(endpointNode);

const session = await client.createSession(userIdentityInfo);
connections.set(endpointId, connection);

session.on("session_restored", () => {
Expand Down Expand Up @@ -151,6 +179,7 @@ export async function get_opcua_endpoint(endpointNode: OpcUaEndpoint2Node, reque
fulfill(result);
return result;
} catch (err) {
await client.disconnect();
doDebug && console.error(err);
endpointNode.warn((err as any).message);
const result = { connection: null, error: (err as any).message };
Expand Down
76 changes: 59 additions & 17 deletions source/tests/OpcUaClient2/OpcUaClient2.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import helper, { TestFlowsItem } from "node-red-node-test-helper";
import helper, { TestFlowsItem, TestFlows } from "node-red-node-test-helper";
import { Node, NodeDef } from "node-red";

// import { describe, it, beforeEach, afterEach } from "mocha";
Expand All @@ -7,10 +7,14 @@ import sinon from "sinon";
import OpcUaClient2 from "../../nodes/OpcUa-Client2/OpcUa-Client2";
import OpcUaEndpoint2 from "../../nodes/OpcUa-Endpoint2/OpcUa-Endpoint2";
import { Status, toNodeStatus } from "../../nodes/tools/to_node_status";
import { OpcUaClient2Node, OpcUaClient2NodeDef } from "../../nodes/OpcUa-Client2/shared";
import { OpcUaClient2Attributes, OpcUaClient2Node, OpcUaClient2NodeDef } from "../../nodes/OpcUa-Client2/shared";
import { OpcUaEndpoint2Attributes, OpcUaEndpoint2Node, OpcUaEndpoint2NodeDef } from "../../nodes/OpcUa-Endpoint2/shared";
import { DataValue, OPCUAServer, StatusCodes, s } from "node-opcua";

interface T1 extends Partial<Omit<OpcUaClient2NodeDef, "id" | "type">>, TestFlowsItem {}
interface T2 extends Partial<Omit<OpcUaEndpoint2NodeDef, "id" | "type">>, TestFlowsItem {}
type T = T1 | T2;

console.log("don't forget to run npm install ~/projects/node-red/packages/node_modules/node-red --no-save");

type FlowsItem = TestFlowsItem<OpcUaClient2NodeDef | OpcUaEndpoint2NodeDef>;
Expand Down Expand Up @@ -83,7 +87,7 @@ describe("OpcUa-Client2 node", function () {
isValidUser: (userName: string, password: string) => {
return userName === "user1" && password === "password1";
},
}
},
});
await server.initialize();

Expand Down Expand Up @@ -197,7 +201,7 @@ describe("OpcUa-Client2 node", function () {
helperNode.on("input", (msg: any) => {
doDebug && console.log(msg);
try {
const statusFunc = opcuaNode.status as any;
const statusFunc = opcuaNode.status as sinon.SinonSpy;
statusFunc.callCount.should.equal(4);

// connecting stage
Expand Down Expand Up @@ -299,7 +303,7 @@ describe("OpcUa-Client2 node", function () {
await new Promise((resolve) => setTimeout(resolve, 6000));

try {
const statusFunc = opcuaNode.status as any;
const statusFunc = opcuaNode.status as sinon.SinonSpy;
statusFunc.callCount.should.equal(2);

// ------------------------------ First call
Expand All @@ -312,7 +316,7 @@ describe("OpcUa-Client2 node", function () {
statusFunc.getCall(1).args[0].fill.should.eql("red");
statusFunc.getCall(1).args[0].shape.should.eql("dot");

const errorFunc = opcuaNode.error as any;
const errorFunc = opcuaNode.error as sinon.SinonSpy;;
errorFunc.callCount.should.equal(1, " Expecting 1 error message on console");
console.log(errorFunc.getCall(0).args[0]);
// console.log(errorFunc.getCall(1).args[0]);
Expand All @@ -332,7 +336,7 @@ describe("OpcUa-Client2 node", function () {
doDebug && console.log(msg);

try {
const statusFunc = opcuaNode.status as any;
const statusFunc = opcuaNode.status as sinon.SinonSpy;
statusFunc.callCount.should.equal(6);

// ------------------------------ First call
Expand Down Expand Up @@ -366,7 +370,7 @@ describe("OpcUa-Client2 node", function () {
statusFunc.getCall(5).args[0].fill.should.eql("green");
statusFunc.getCall(5).args[0].shape.should.eql("ring");

const errorFunc = opcuaNode.error as any;
const errorFunc = opcuaNode.error as sinon.SinonSpy;
errorFunc.callCount.should.equal(1, " Expecting 1 error message on console");
console.log(errorFunc.getCall(0).args[0]);
// console.log(errorFunc.getCall(1).args[0]);
Expand Down Expand Up @@ -442,17 +446,38 @@ describe("OpcUa-Client2 node", function () {
});
});

const waitFirstInput = async (opcuaNode: Node, helperNode: any) => {


const waitFirstInput = async (opcuaNode: Node, helperNode: any, timeout: number = 5000) => {
return await new Promise((resolve, reject) => {
let resolved = false;
const timerId = setTimeout(() => {
!resolved && reject(new Error("Timeout waiting for first input"));
resolved= true;
}, timeout);

helperNode.on("input", (msg: unknown) => {
clearTimeout(timerId);
doDebug && console.log(msg);
resolve(msg);
!resolved && resolve(msg);
resolved = true;
});
opcuaNode.receive({ topic: "ns=0;i=2258" });
});
};

const waitTimeOut = async (opcuaNode: Node, helperNode: any, timeout: number) => {
return await new Promise<void>((resolve, reject) => {
helperNode.on("input", (msg: unknown) => {
doDebug && console.log(msg);
reject(new Error("Not expecting a message"));
});
opcuaNode.receive({ topic: "ns=0;i=2258" });
(async () => {
await new Promise((resolve) => setTimeout(resolve, timeout));
resolve();
})();
});
};

it("endpoint-security-1: Sign & Basic256Sha256 with anonymous Identity", async () => {
const flows = [
{
Expand Down Expand Up @@ -483,15 +508,17 @@ describe("OpcUa-Client2 node", function () {
// console.log(msg);
(msg as {}).should.have.property("$dataValue");
});

it("endpoint-security-2: Username Password, with valid credentials", async () => {
const flows = [
const a: T[] = [];
const flows: T[] = [
{
id: "e1",
type: "OpcUa-Endpoint2",
name: "endpoint1",
endpoint: serverEndpoint,
userIdentityType: "UserName",
user: "user1",
userName: "user1",
password: "password1",
},
{
Expand Down Expand Up @@ -534,9 +561,24 @@ describe("OpcUa-Client2 node", function () {
await helper.load([OpcUaEndpoint2, OpcUaClient2], flows);
const helperNode = helper.getNode("h1");
const opcuaNode = helper.getNode("n1") as OpcUaClient2Node;
const msg = await waitFirstInput(opcuaNode, helperNode);
console.log(msg);
(msg as {}).should.have.property("$dataValue");
await waitTimeOut(opcuaNode, helperNode, 1000);

const statusFunc = opcuaNode.status as sinon.SinonSpy;
statusFunc.callCount.should.equal(2);

// ------------------------------ First call
// connecting stage
statusFunc.getCall(0).args[0].should.eql(toNodeStatus(Status.Connecting, "connecting"));
statusFunc.getCall(0).args[0].fill.should.eql("blue");
statusFunc.getCall(0).args[0].shape.should.eql("dot");

statusFunc.getCall(1).args[0].should.eql(toNodeStatus(Status.ConnectionFailed, "failed"));
statusFunc.getCall(1).args[0].fill.should.eql("red");
statusFunc.getCall(1).args[0].shape.should.eql("dot");

const errorFunc = opcuaNode.error as sinon.SinonSpy;
errorFunc.callCount.should.equal(1);
errorFunc.getCall(0).args[0].should.match(/BadIdentityTokenInvalid/);
});
});
});

0 comments on commit 4cfb4ae

Please sign in to comment.