Enable/Disable/Sign agents without opening DDE

A couple of days ago, Thomas Adrian posted a new idea on aha.io Allow Enable/Disable/Sign agents from Domino Administrator without opening DDE.

This is a pretty cool idea, I think. Although there already is a great tool available from Ytria ( agentEZ ), handling of agents should be a part of the core features of Notes / Domino.

Notes / Domino contains some of the requested features and for example, you can enable an agent via adminp. You can simply create a new admin request in the admin4.nsf; if you know, which values to set. Unfortunately, this is not documented and you have to do a lot of try and error before the admin request is processed. The advantage of using adminp would be documentation of who did what.
As a downside, you cannot disable an agent; there is no adminp request for that.

Back in 2007, I already demonstrated, how you can add your own adminp requests to the adminp Delete Group Members Using The Administration Process

After reading Thomas’ idea, I decided to spend some hours on building a Domino server addin that would enable / disable or toggle the status of scheduled agents in an application. The application path, agent name and what to do should be passed as parameters to the addin.

Project “AMgr2” was born.

Over the years, I have created my own Notes cAPI CPP framework. This is still work in progress, as I add new methods and properties when I need them.

The framework is a great help when it comes to RAD in Notes / Domino using c/c++. I tried to name methods and properties as close as possible to what we have in LotusScript or Java to make the resulting source code readable and maintainable. Here is an example how I determine if an agent is of type “scheduled” within my framework

bool cNotesAgent::isScheduled() {
		cNotesDocument doc(db_h, agnt_id, OPEN_NOVERIFYDEFAULT);
		cNotesItemText trigger = doc.getItemText("$AssistTrigger");
		if(!trigger.compare("1")) {return TRUE;} else {return FALSE;} 
		}

Most of the magic happens in the framework, so the source code for AMgr2 is pretty short.

// main.h

#ifndef _MAIN_H_
#define _MAIN_H_

#include <string>

#if defined (_MSC_VER) &amp;&amp; !defined(ND64)
#pragma pack(push, 1)
#endif
#include <global.h>
#include <miscerr.h>
#include <addin.h>
#if defined (_MSC_VER) &amp;&amp; !defined (ND64)
#pragma pack(pop)
#endif

#include "cNotesFramework.h"
#include "cNotesAgent.h"
#include "cmdline.h"

#if defined (W64)
#define HANDLE DHANDLE
#undef NOTEHANDLE
#define NOTEHANDLE DHANDLE
#else
#define DHANDLE HANDLE
#undef NOTEHANDLE
#define NOTEHANDLE HANDLE
#endif

#define ADDIN_STATUS_LINE	"AMgr2"
#define APP_NAME	"AMgr2: "
#undef MSG
#define MSG(fmt) APP_NAME fmt
#define ERROR -1
using namespace std;

#endif

// main.cpp

/*
* Amgr2
*
* eknori at eknori dot de  www.eknori.de FEBRUARY 2019
*
* copyright (c) 2019 Ulrich Krause www.eknori.de
*/

#pragma warning(disable:4005) 

#include "main.h"

STATUS LNPUBLIC AddInMain (HMODULE hModule, int argc, char *argv[]) {

	STATUS				error = NOERROR;
	HANDLE				hStatusLine;
	HANDLE				hStatusLineDesc;
	HMODULE				hMod;

	AddInQueryDefaults (&amp;hMod, &amp;hStatusLine);
	AddInDeleteStatusLine (hStatusLine);
	hStatusLineDesc = AddInCreateStatusLine(ADDIN_STATUS_LINE);
	AddInSetDefaults (hMod, hStatusLineDesc);
	AddInSetStatusText("Initialising");

	CmdLine *cmdline = new CmdLine();

	cmdline->addUsage("  amgr2, V1.0.0.0, (c) 2019, Ulrich Krause\n");
	cmdline->addUsage("Usage: lo amgr2 [options] [flags]\n");

	cmdline->addUsage( "Options:\n" );
	cmdline->addUsage( "-d    --db\t\tdatabase path" );
	cmdline->addUsage( "-a    --agent\t\tagent name" );

	cmdline->addUsage( "" );
	cmdline->addUsage( "Flags:\n" );
	cmdline->addUsage( "-h    --help\t\tPrints this help" );
	cmdline->addUsage( "      --enable\tEnable agent" );
	cmdline->addUsage( "      --disable\tDisable agent" );
	cmdline->addUsage( "      --toggle\tToggle agent status" );

	cmdline->setOption( "db", 'd' );
	cmdline->setOption( "agent", 'a' );

	cmdline->setFlag ( "help", 'h' ); 
	cmdline->setFlag ( "enable");
	cmdline->setFlag ( "disable");
	cmdline->setFlag ( "toggle");

	cmdline->processCommandArgs( argc, argv );

	if( ! cmdline->hasOptions()) {
		cmdline->printUsage2();
		delete cmdline;
		return ERROR;
		}

	if( cmdline->getFlag( "help" ) 
		|| cmdline->getFlag( 'h' ) ) {
			cmdline->printUsage2();
			return NOERROR;
		}

	string file_path;
	if( cmdline->getValue( 'd' ) != NULL  
		|| cmdline->getValue( "db" ) != NULL  ){
			file_path = cmdline->getValue( 'd' );
		} 

	string agent_name;
	if( cmdline->getValue( 'a' ) != NULL  
		|| cmdline->getValue( "agent" ) != NULL  ){
			agent_name = cmdline->getValue( 'a' );
		} 

	cNotesDatabase _NotesDatabase;

	try{

		_NotesDatabase.open(
			file_path.c_str());

		cNotesAgent _NotesAgent(
			_NotesDatabase.h, agent_name.c_str());

		if(_NotesAgent.isScheduled()) {

			if( cmdline->getFlag( "enable" )) {
				_NotesAgent.enable();

				AddInLogMessageText(
					MSG("... agent '%s' in database '%s' has been enabled\n"), 
					NOERROR, 
					_NotesAgent.name.c_str(), 
					_NotesDatabase.filePath().c_str());
				}

			if( cmdline->getFlag( "disable" )) {
				_NotesAgent.disable();

				AddInLogMessageText(
					MSG("... agent '%s' in database '%s' has been disabled\n"), 
					NOERROR, 
					_NotesAgent.name.c_str(), 
					_NotesDatabase.filePath().c_str());
				}

			if( cmdline->getFlag( "toggle" )) {
				if(_NotesAgent.isEnabled()) {
					_NotesAgent.disable();

					AddInLogMessageText(
						MSG("... agent '%s' in database '%s' has been disabled\n"), 
						NOERROR, 
						_NotesAgent.name.c_str(), 
						_NotesDatabase.filePath().c_str());
					}
				else {
					_NotesAgent.enable();

					AddInLogMessageText(
						MSG("... agent '%s' in database '%s' has been enabled\n"), 
						NOERROR, 
						_NotesAgent.name.c_str(), 
						_NotesDatabase.filePath().c_str());
					}
				}

			} else { // is_scheduled

				AddInLogMessageText(
					MSG("... agent '%s' in database '%s' is not a scheduled agent\n"), 
					NOERROR, 
					_NotesAgent.name.c_str(), 
					_NotesDatabase.filePath().c_str());

			}

		} catch (cNotesErr&amp; err) {
			delete cmdline;
			_NotesDatabase.close();

			AddInLogMessageText(
				MSG("Notes Error: %s\n"), 
				NOERROR, err.what());
			return ERROR;

		} catch (...) {
			delete cmdline;
			_NotesDatabase.close();

			AddInLogMessageText("Unexpected Error\n", NOERROR);
			return ERROR;
			}

		delete cmdline;
		_NotesDatabase.close();
		return error;
	}

To build the cmdline parser, I use another framework that I wrote a couple of years ago. I used the Boost.Program_options in a couple of other projects before, but it is a lot of overhead for such a small project like AMgr2.
CmdLine is much smaller. Despite of its simplicity, it is reliable and produces nice help screens.

I have not yet published CmdLine on Github. If you are interested in the source code, send me an email and I will send you the sources.

Putting it all together, we get a 64bit binary amgr2.exe Copy it to your Domino program directory and you are ready to go.

Open the Domino server console and type

lo amgr2 -h  and you should get

  amgr2, V1.0.0.0, (c) 2019, Ulrich Krause

  Usage: lo amgr2 [options] [flags]

  Options:

  -d    --db		database path
  -a    --agent		agent name
  
  Flags:

  -h    --help		Prints this help
        --enable	Enable agent
        --disable	Disable agent
        --toggle	Toggle agent status

To enable a scheduled agent in an application type

lo amgr2 -d names.nsf -a test --enable

[219C:0002-1CE8] 17.02.2019 08:39:56   AMgr2: ... agent 'test' in database 'names.nsf' has been enabled 

Use –disable to disable an agent or –toggle to change the status of an agent accordingly.

AMgr2 only works for scheduled agents. If an agent does not match this criteria, you’ll get the following message on the server console

lo amgr2 -d names.nsf -a test2 --toggle

[1F94:0002-15D8] 17.02.2019 08:47:53   AMgr2: ... agent 'test2' in database 'names.nsf' is not a scheduled agent

You also can build a simple Notes application that scans all applications on a server for scheduled agents.
Next you can use NotesSession.SendConsoleCommand to enable / disable / toggle one or more agents.

Here is some sample code

Sub Click(Source As Button)
	Dim session As New NotesSession
	serverName$ = "serv01/singultus"
	consoleCommand$ = Inputbox$("Type command:", _
	"Send console command")
	consoleReturn$ = session.SendConsoleCommand( _
	serverName$, consoleCommand$)
	Messagebox consoleReturn$,, consoleCommand$
End Sub

This might not be what Thomas asked for, but it is a good starting point. There is a lot room for improvements and enhancements. AMgr2 will sign the agent with the server id. This might not always be intended. It is not rocket science to implement code and add a couple of parameters to the cmdline parse to use a different id for signing. It is more work to make sure, this id is stored in a secure place and cnnot be accessed by any unauthorized person.

For now, this is it.

AMgr2 once again proves that you can do everything with Notes / Domino. The creators gave us tools that let us add functionallity that is not in the core code. OK, I admit that c/c++ is not the preferred programming language for most of the Notes / Domino developers.


SyntaxHighlighter Evolved: @Formula Brush

Submitted another plugin to the WordPress plugin directory.

The plugin adds a new brush to the SyntaxHighlighter Evolved plugin to colorize @formula code.

Example:

_exclude:= 
"$FILE":"$Fonts":"Form":"$UpdatedBy":"$Revisions":
"ID":"ModifiedBy":"AddressInvoiceAppartment";
_fld:=@Trim(@ReplaceSubstring(@DocFields; _exclude; @Nothing));
 
"{\"@unid\":\""
+@Text(@DocumentUniqueID)+"\","
+ @Implode ( @Transform (
_fld; "_fn" ; "\"" + _fn + "\":\"" + 
@Text ( @GetField ( _fn) ) + "\"" ) ; "," ) +
"},"

java.lang.UnsatisfiedLinkError: lotus/domino/local/Database.NcreateDQuery()J

UPDATE 05-FEB-2019: Issue is being tracked under SPR # VRARB94KAQ

When executing db.createDominoQuery(); in a Java agent on the server, I see the following error message on the Domino console:

te amgr run "ec11.nsf" 'dql.java'
[021963:000035-00007F2BE8DFD700] 02/03/2019 05:41:29 AM AMgr: Start executing agent 'dql.java' in 'ec11.nsf'
[021963:000037-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent printing: Version: Release 10.0.1 November 29, 2018
[021963:000037-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent printing: Db Title: singultus's Directory
[021963:000038-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent error: Exception in thread "AgentThread: JavaAgent"
[021963:000039-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent error: java.lang.UnsatisfiedLinkError: lotus/domino/local/Database.NcreateDQuery()J
[021963:000041-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent error: at lotus.domino.local.Database.createDominoQuery(Unknown Source)
[021963:000043-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent error: at JavaAgent.NotesMain(JavaAgent.java:19)
[021963:000045-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent error: at lotus.domino.AgentBase.runNotes(Unknown Source)
[021963:000047-00007F2BE816F700] 02/03/2019 05:41:29 AM Agent Manager: Agent error: at lotus.domino.NotesThread.run(Unknown Source)
[021963:000035-00007F2BE8DFD700] 02/03/2019 05:41:29 AM AMgr: Agent 'dql.java' in 'ec11.nsf' completed execution

According to John Curtis (HCL) “… We haven’t seen this to date.”.

I have tested on different OS.

serv02: 
Red Hat Enterprise Linux Server release 7.4 (Maipo)
Linux serv02.fritz.box 3.10.0-693.el7.x86_64 #1 SMP Thu Jul 6 19:56:57 EDT 2017 x86_64 x86_64 x86_64 GNU/Linux
IBM Domino (r) Server (64 Bit) (Release 10.0.1 for Linux/64) 02/03/2019 05:48:40 AM
serv02 has been upgraded from V10.0
serv03:
CentOS Linux release 7.6.1810 (Core)
Linux serv03.fritz.box 3.10.0-957.1.3.el7.x86_64 #1 SMP Thu Nov 29 14:49:43 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
IBM Domino (r) Server (64 Bit) (Release 10.0.1 for Linux/64) 02/03/2019 05:49:15 AM (Community server)
serv03 is "fresh" install and not upgraded from earlier Domino versions.

Here is the code I used

import lotus.domino.AgentBase;
import lotus.domino.Database;
import lotus.domino.DominoQuery;
import lotus.domino.Session;

public class JavaAgent extends AgentBase {

	public void NotesMain() {

		try {
			Session session = getSession();
			System.out.println("Version: " + session.getNotesVersion());
			
			Database db = null;
			db = session.getDatabase(null, "names.nsf");
			System.out.println("Db Title: " + db.getTitle());
			
			DominoQuery dql = null;
			dql = db.createDominoQuery();

			dql.recycle();
			db.recycle();
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

As it works on Domino 10.0.1 on WINDOWS, I investigated nlsxbe.dll and liblsxbe.so

nlsxbe.dll contains the function in question

Java_lotus_domino_local_Database_NcreateDQuery 0x0000000180017cd0 0x00017cd0 732 (0x2dc) nlsxbe.dll W:\Domino\nlsxbe.dll Exported Function

liblsxbe.so is missing the function. At least, it is not exported.

nm -D liblsxbe.so | grep createD

0000000000096a03 T Java_lotus_domino_local_Database_NcreateDocColl
0000000000095587 T Java_lotus_domino_local_Database_NcreateDocument
000000000012bf7d T Java_lotus_domino_local_DateRange_NrecreateDateRange
00000000000e33e1 T Java_lotus_domino_local_DateTime_NrecreateDateTime
0000000000110ad2 T Java_lotus_domino_local_DbDirectory_NcreateDatabase
00000000000d5d6d T Java_lotus_domino_local_Session_NcreateDateRange
00000000000da091 T Java_lotus_domino_local_Session_NcreateDateTime
00000000000d6bd1 T Java_lotus_domino_local_Session_NcreateDxlExporter
00000000000d6d71 T Java_lotus_domino_local_Session_NcreateDxlImporter
00000000000957a0 T Java_lotus_notes_Database_NcreateDocument
0000000000110d7e T Java_lotus_notes_DbDirectory_NcreateDatabase
00000000000d5f40 T Java_lotus_notes_Session_NcreateDateRange
00000000000da360 T Java_lotus_notes_Session_NcreateDateTime
U _ZN11XmlDocument14createDocumentEv
U _ZN11XmlDocument22cr
eateDocumentFragmentEv

The version and date seem to be OK.

ls -al liblsxbe.so
-rwxr-xr-x. 1 root root 14510400 Nov 29 07:48 liblsxbe.so

Perhaps someone else can confirm this behaviour. I have already created case #TS001863705 with HCL.


SyntaxHighlighter Evolved: LotusScript Brush

SyntaxHighlighter Evolved: LotusScript Brush is a small WordPress plugin that adds support for the LotusScript language to the great SyntaxHighlighter Evolved plugin by Alex Mills.

I have submitted a request to add the plugin to the official WordPress plugin directory. The review process will take some time.

In the meantime, you can start using SyntaxHighlighter Evolved: LotusScript Brush right now.

Download the plugin from here , unpack it and upload to your /wp-content/plugins directory.

Update 04-FEB-2019: The plugin is now available in the WordPress plugin directory.

Next activate the plugin

SyntaxHighlighter Evolved: LotusScript Brush will now be available as a new language in the Block Editor (“Code Languages”)

SyntaxHighlighter Evolved: LotusScript Brush highlights classes, method and properties, keywords, strings, comments and directives.


NotesDominoQuery – Find the needle in the haystack (LS)

Think about a big database with lots of documents in it and you want to find only one particular document. You can do that with FTSearch, or you can use a db.search with some formula.

As of V10.0.x, you also have DQL and the new NotesDominoQuery class.
The class is available in Lotsscript and Java.
I want to demonstrate in this sample, how you can find the needle in the haystack with DQL in Lotusscript.

My database has about 12.500.000 Documents. It is one of our customers database at midpoints. The amount of documents was created by accident. Some call it a bug. Anyway, the database is a good playground.

The code is typical for a LS developer. It initiates objects and stuff, assigns variables like our dqlTerm (line 6 see the similarity to the @formula, you would probably use with db.search? ), does a check, if the target database is open (line 9 ) and also has some basic error handling (17, 20-21). We will come to that later.

I am running the sample on the client. As of today , you cannot run a query client / server. I will show in another post, how you can run the query on the server and work with the results on the client. But that is another story.

public Sub foo() 
	Dim session As New NotesSession
	Dim db As NotesDatabase
	Dim col As NotesDocumentCollection
	Dim dqlTerm As String
	dqlTerm = "form = 'frm.rules.device.rule' And rule_unid = '99A242AAB69B5BB9C1257FFC005DE6C4'"
	
	Set db = session.Getdatabase("","trul-big.nsf", False)
	If db.Isopen Then
		
		Dim dql As NOTESDOMINOQUERY
		Set dql = db.CreateDominoQuery()
		
		Dim parse_result As String
		parse_result = dql.parse(dqlTerm)

		If LCase(parse_result) = "success" Then
			Set col = dql.Execute(dqlTerm)
			MsgBox dql.Explain(dqlTerm)
		Else
			MsgBox parse_result
		End If
		
	End If
End Sub

Before you run the code and try to search such a huge amount of data, you need to set some notes.ini variables.

QUERY_MAX_DOCS_SCANNED=13000000 
QUERY_MAX_VIEW_ENTRIES_SCANNED=13000000

By now, there is no other way to increase the number of documents. There are setters in the NotesDominoQuery class, but those setters are broken. This is a known issue and HCL is working on a solution.

When we now run the code, we will get the following result

It took 51.3 secs to find 110642 documents that use the form “frm.rules.device.rule” and zero to none secs to find 1 document in that resultset.

Would it be faster or slower, if we modify our query to use only the rule_unid field and search over the entire set of documents?

dqlTerm = "rule_unid = '99A242AAB69B5BB9C1257FFC005DE6C4'"

Here is the result

32.2 secs to find the needle in the haystack. Use this kind of single field search, if you are sure that the field “rule_unid” is only used on one form.

But even in the case that the field is used on another form; to filter the resulting NotesDocument collection afterwards is much faster than the AND dqlTerm from the original code.

Maybe, you already have a view in your application where the rule_unid is in a sorted column.

Then we can use a modified dqlTerm to find the document in that view.

Again, let’s change our dqlTerm a little bit

dqlTerm = "'all'.rule_unid = '99A242AAB69B5BB9C1257FFC005DE6C4'"

And here is the result

THAT is pretty cool, isn’t it? 5.6 msecs to find the needle in the haystack.

Can it be even faster? I think no, but let us take another look at the code that does the query.

        Dim parse_result As String
        parse_result = dql.parse(dqlTerm)
 
        If LCase(parse_result) = "success" Then
            Set col = dql.Execute(dqlTerm)
            MsgBox dql.Explain(dqlTerm)
        Else
            MsgBox parse_result
        End If

We can safely remove all of our “Error handling”, that means lines 14-17 and 20-22. Why? Because dql.Excecute(dqlTerm) does the parse before executing the query.

So we end up with the following code

public Sub foo() 
	Dim session As New NotesSession
	Dim db As NotesDatabase
	Dim col As NotesDocumentCollection
	Dim dqlTerm As String
	
	dqlTerm = "'all'.rule_unid = '99A242AAB69B5BB9C1257FFC005DE6C4'"

	Set db = session.Getdatabase("","trul-big.nsf", False)
	If db.Isopen Then
		Dim dql As NOTESDOMINOQUERY
		Set dql = db.CreateDominoQuery()
		Set col = dql.Execute(dqlTerm)
		MsgBox dql.Explain(dqlTerm)
	End If
End Sub

When we run the code, we get

Keep in mind that the numbers vary a litte bit from run to run.

If we now modify our dqlTerm and add an error, we are shown a nice dialog box explaining the error in detail.

That’s all for today, I hope you find this information useful.


Bye, bye, Watson Workspace

I received the following mail from IBM announcing the End of Marketing for all Watson Workspace offerings and ending the Watson Workspace service on 2/28/19.

Watson Workspace Announcement

Tomorrow, Tuesday 15 Jan 2019, IBM will formally announce the End of Marketing for all Watson Workspace offerings and we anticipate ending the Watson Workspace service on 2/28/19. This includes Watson Workspace Essentials, Plus, and the free offering. It is our intent to ensure that you have a clear understanding of your options regarding the closure of Watson Workspace.
While there is no question that Watson Workspace is innovative and agile, it hasn’t resonated with clients or obtained the traction in the marketplace necessary for IBM to continue forward with the service. Despite our best efforts and enthusiasm for these offerings, our decision to withdraw them aligns to IBM’s investment strategy, focused on delivering solutions that deliver measurable value to our customers and business partners.
IBM has stopped accepting new orders for Watson Workspace products, and we will not be adding any new features to the offerings. We have been working with our licensed customers and business partners to provide options for handling subscriptions and contracts once the end of service announcement is published to provide a smooth transition.
Starting tomorrow there will be a banner placed into the UI of Watson Workspace informing users of the timeline for moving off the service. Mobile customers may find details in the release notes of the mobile app. We will also provide access to a tool that will allow you to download and save your conversations and content from Watson Workspace. Please plan accordingly to capture any content you’d like to retain as we work to sunset the service. This option will only be available for a limited time.
Thank you for your support of Watson Workspace and IBM Collaboration Solutions. Please find additional details and answers to commonly asked questions in the FAQ that will be posted in the Watson Workspace banner tomorrow.


IBM Collaboration Services, Offering Management


NotesHttpRequest LotusScript example

NotesHttpRequest is a new LotusScript class in Notes V10.0.1 used to make HTTP requests to web servers.

The example wraps the get(), post(), put() and deleteResource() methods in a LotusScript class.

I have also put together a small Node.js / Express.js project that can be used as test server for the NotesHttpRequest example.

Class HttpRequestWrapper

%REM
	Library 10010.http
	Created Jan 8, 2019 by Ulrich Krause/singultus
%END REM
Option Public
Option Declare

Const BASE_URL_PORT = "http://192.168.178.35:3001/api/"
Const CLASS_CUSTOMER ="customers"

Const CUSTOMER_5 = |{
					"firstname" : "Ulrich",
					"lastname" : "Krause",
					"age" : 59,
					"id" : 5
				}|

Const CUSTOMER_5_UPDATE = |{
					"firstname" : "Heinz Ulrich",
					"lastname" : "Krause",
					"age" : 59,
					"id" : 5
				}|
				
%REM
	Class HttpRequestWrapper
%END REM
Public Class HttpRequestWrapper
	
	Private m_session As NotesSession
	Private m_url As String
	Private m_class As String
	Private m_json As String
	Private m_httpRequest As NOTESHTTPREQUEST
	
	%REM
		Sub New
	%END REM
	Public Sub New()
		Set m_session = New NotesSession
		Set m_httpRequest = me.m_session.CreateHttpRequest()
		
		m_httpRequest.Preferstrings = True
		
		m_url = BASE_URL_PORT
		m_class = CLASS_CUSTOMER
	End Sub
	
	%REM
		Function getApiVersion
	%END REM
	Public sub getApiVersion()
		m_json = m_httpRequest.Get(BASE_URL_PORT)
		MsgBox m_json
	End Sub
	
	%REM
		Sub getAllObj
	%END REM
	Public Sub getAllObj()
		m_json = m_httpRequest.Get(_
		BASE_URL_PORT + CLASS_CUSTOMER)
		
		MsgBox m_json
	End Sub
	
	%REM
		Sub getObjById
	%END REM
	Public Sub getObjById(id As String)
		m_json = m_httpRequest.Get(_
		BASE_URL_PORT + CLASS_CUSTOMER + "/" + id)
		
		MsgBox m_json	
	End Sub
	
	%REM
		Sub createObj
	%END REM
	Public Sub createObj()
		m_json = m_httpRequest.Post(_
		BASE_URL_PORT + CLASS_CUSTOMER,_
		removeCRLF(CUSTOMER_5))
		
		MsgBox m_json	
	End Sub
	
	%REM
		Sub updateObj
	%END REM
	Public Sub updateObj(id As String)
		m_json = m_httpRequest.Put(_
		BASE_URL_PORT + CLASS_CUSTOMER + "/" + id,_
		removeCRLF(CUSTOMER_5_UPDATE))
		
		MsgBox m_json
	End Sub
	
	%REM
		Sub deleteObj
	%END REM
	Public Sub deleteObj(id As String)
		m_json = m_httpRequest.DeleteResource(_
		BASE_URL_PORT + CLASS_CUSTOMER + "/" + id)
		
		MsgBox m_json
	End Sub
End Class

Public Function removeCRLF(json As String) As String
	removeCRLF = Replace(Replace(json, Chr(13), ""),Chr(10),"")
End Function

Sample usage:

Use "10010.http"

Sub Click(Source As Button)
	Dim httpRequestWrapper As New HttpRequestWrapper()

	Call httpRequestWrapper.getApiVersion()
	Call httpRequestWrapper.createObj()
	Call httpRequestWrapper.deleteObj("3")
	Call httpRequestWrapper.getAllObj()
	Call httpRequestWrapper.updateObj("5")
	Call httpRequestWrapper.getObjById("5")
End Sub



Starting Express.js applications with PM2

I recently ran into an issue with PM2. I have created a “helloworld” applications using Express.js.

[root@nodejs projects]# express helloworld
warning: the default view engine will not be jade in future releases
warning: use --view=jade' or--help' for additional options
create : helloworld/
create : helloworld/public/
create : helloworld/public/javascripts/
create : helloworld/public/images/
create : helloworld/public/stylesheets/
create : helloworld/public/stylesheets/style.css
create : helloworld/routes/
create : helloworld/routes/index.js
create : helloworld/routes/users.js
create : helloworld/views/
create : helloworld/views/error.jade
create : helloworld/views/index.jade
create : helloworld/views/layout.jade
create : helloworld/app.js
create : helloworld/package.json
create : helloworld/bin/
create : helloworld/bin/www
change directory:
$ cd helloworld
install dependencies:
$ npm install
run the app:
$ DEBUG=helloworld:* npm start

The application started without issues with npm start.

Next I started the application with PM2

[root@nodejs helloworld]# pm2 start app.js

PM2 displayed the application as “online”, but it constantly restarted and was not accessible.

I found the solution here

[root@nodejs helloworld]# pm2 start bin/www

did the trick for me.


nginx + node.js + CentOS 7 = 502 Bad Gateway

I have setup a new Node.js / Express development environment on a CentOS 7 VM. I ‘ll describe the details in another post later.

To test my setp, I created a new Express application “helloworld”. The application listens on port 3000 and I was able to connect to the application using a browser.

Next, I configured NGINX as reverse proxy to use port 80 to access the helloworld application.

But I got an error

I checked the logs

[root@nodejs ~]# cat /var/log/audit/audit.log | grep nginx | grep denied

and got

type=AVC msg=audit(1546783734.750:239): avc:  denied  { name_connect } for  pid=11084 comm="nginx" dest=3000 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:ntop_port_t:s0 tclass=tcp_socket permissive=0

My best guess was SELinux.I checked, if SELinux was enabled.

[root@nodejs ~]# sestatus

SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Max kernel policy version: 31

Next I checked the settings for httpd.

[root@nodejs ~]# getsebool -a | grep httpd
httpd_anon_write --> off
httpd_builtin_scripting --> on
httpd_can_check_spam --> off
httpd_can_connect_ftp --> off
httpd_can_connect_ldap --> off
httpd_can_connect_mythtv --> off
httpd_can_connect_zabbix --> off
httpd_can_network_connect --> off
httpd_can_network_connect_cobbler --> off
httpd_can_network_connect_db --> off
httpd_can_network_memcache --> off
httpd_can_network_relay --> off
httpd_can_sendmail --> off

So, httpd_can_network_connect was set to “Off”. This blocks the connection from the reverse proxy to the node.js application. As a result, you get the 502 Bad gateway error.

To enable the setting, execute the following command from the shell.

[root@nodejs ~]# setsebool -P httpd_can_network_connect on

You do not need to reboot the machine or SELinux.


ESXi 6.7 Update Failed

I just tried to update my ESXi 6.7 host from ESXi-6.7.0-20181002001-standard (Build 10302608) to ESXi-6.7.0-20181104001-standard (Build 10764712).

Find a list for all available updates and patches here.

The update process in general is straight forward. SSH into your ESXi host and execute the following commands.

esxcli network firewall ruleset set -e true -r httpClient
esxcli software profile update -p ESXi-6.7.0-20181104001-standard -d https://hostupdate.vmware.com/software/VUM/PRODUCTION/main/vmw-depot-index.xml
esxcli network firewall ruleset set -e false -r httpClient

(replace the highlighted version with the version you want to upgrade to )

This time, the update failed.

[InstallationError]
Failed to setup upgrade using esx-update VIB: (None, "Failed to mount tardisk /tmp/esx-update-2123405/esxupdt-2123405 in ramdisk esx-update-2123405: [Errno 1] Operation not permitted: '/tardisks.noauto/esxupdt-2123405'")
vibs = ['VMware_bootbank_esx-update_6.7.0-1.31.10764712']
Please refer to the log file for more details.
[root@esxi:~]

Digging into the logs, I found the following clues

vmkernel.log:
cpu1:2099635)ALERT: VisorFSTar: 1655: Unauthorized attempt to mount a tardisk
cpu1:2099635)VisorFSTar: 2062: Access denied by vmkernel access control policy prevented creating tardisk

esxupdate.log:
esxupdate: 2099635: root: ERROR: File "/build/mts/release/bora-10302608/bora/build/esx/release/vmvisor/sys-boot/lib64/python3.5/shutil.py", line 544, in move
esxupdate: 2099635: root: ERROR: PermissionError: [Errno 1] Operation not permitted: '/tmp/esx-update-2123405 /esxupdt-2123405' -> '/tardisks.noauto/esxupdt-2123405 '

By the way, the esxupdate.log is in HEX format for some reason.

You can either use a HEX-Editor to decode the file, or open it in Visual Studio Code.

You’ll get a warning; just click on “Do you want to open it anyway?

After a couple of try and error, I was able to get the updated VIBs using

[root@esxi:~] esxcli software vib update --depot=https://hostupdate.vmware.com/software/VUM/PRODUCTION/main/vmw-depot-index.xml
Installation Result
Message: The update completed successfully, but the system needs to be rebooted for the changes to be effective.
Reboot Required: true
VIBs Installed: VMware_bootbank_esx-base_6.7.0-1.31.10764712, VMware_bootbank_esx-ui_1.31.0-10201673, VMware_bootbank_esx-update_6.7.0-1.31.10764712, VMware_bootbank_sata-ahci_3.0-28vmw.600.3.107.10474991, VMware_bootbank_vsan_6.7.0-1.31.10720746, VMware_bootbank_vsanhealth_6.7.0-1.31.10720754
VIBs Removed: VMW_bootbank_sata-ahci_3.0-26vmw.670.0.0.8169922, VMware_bootbank_esx-base_6.7.0-1.28.10302608, VMware_bootbank_esx-ui_1.30.0-9946814, VMware_bootbank_esx-update_6.7.0-1.28.10302608, VMware_bootbank_vsan_6.7.0-1.28.10290435, VMware_bootbank_vsanhealth_6.7.0-1.28.10290721
VIBs Skipped: VMW_bootbank_ata-libata-92_3.00.9.2-16vmw.670.0.0.8169922, VMW_bootbank_ata-pata-amd_0.3.10-3vmw.670.0.0.8169922, VMW_bootbank_ata-pata-atiixp_0.4.6-4vmw.670.0.0.8169922, VMW_bootbank_ata-pata-cmd64x_0.2.5-3vmw.670.0.0.8169922, VMW_bootbank_ata-pata-hpt3x2n_0.3.4-3vmw.670.0.0.8169922, VMW_bootbank_ata-pata-pdc2027x_1.0-3vmw.670.0.0.8169922, VMW_bootbank_ata-pata-serverworks_0.4.3-3vmw.670.0.0.8169922, VMW_bootbank_ata-pata-sil680_0.4.8-3vmw.670.0.0.8169922, VMW_bootbank_ata-pata-via_0.3.3-2vmw.670.0.0.8169922, VMW_bootbank_block-cciss_3.6.14-10vmw.670.0.0.8169922, VMW_bootbank_bnxtnet_20.6.101.7-11vmw.670.0.0.8169922, VMW_bootbank_bnxtroce_20.6.101.0-20vmw.670.1.28.10302608, VMW_bootbank_brcmfcoe_11.4.1078.5-11vmw.670.1.28.10302608, VMW_bootbank_char-random_1.0-3vmw.670.0.0.8169922, VMW_bootbank_ehci-ehci-hcd_1.0-4vmw.670.0.0.8169922, VMW_bootbank_elxiscsi_11.4.1174.0-2vmw.670.0.0.8169922, VMW_bootbank_elxnet_11.4.1095.0-5vmw.670.1.28.10302608, VMW_bootbank_hid-hid_1.0-3vmw.670.0.0.8169922, VMW_bootbank_i40en_1.3.1-22vmw.670.1.28.10302608, VMW_bootbank_iavmd_1.2.0.1011-2vmw.670.0.0.8169922, VMW_bootbank_igbn_0.1.0.0-15vmw.670.0.0.8169922, VMW_bootbank_ima-qla4xxx_2.02.18-1vmw.670.0.0.8169922, VMW_bootbank_ipmi-ipmi-devintf_39.1-5vmw.670.1.28.10302608, VMW_bootbank_ipmi-ipmi-msghandler_39.1-5vmw.670.1.28.10302608, VMW_bootbank_ipmi-ipmi-si-drv_39.1-5vmw.670.1.28.10302608, VMW_bootbank_iser_1.0.0.0-1vmw.670.1.28.10302608, VMW_bootbank_ixgben_1.4.1-16vmw.670.1.28.10302608, VMW_bootbank_lpfc_11.4.33.3-11vmw.670.1.28.10302608, VMW_bootbank_lpnic_11.4.59.0-1vmw.670.0.0.8169922, VMW_bootbank_lsi-mr3_7.702.13.00-5vmw.670.1.28.10302608, VMW_bootbank_lsi-msgpt2_20.00.04.00-5vmw.670.1.28.10302608, VMW_bootbank_lsi-msgpt35_03.00.01.00-12vmw.670.1.28.10302608, VMW_bootbank_lsi-msgpt3_16.00.01.00-3vmw.670.1.28.10302608, VMW_bootbank_misc-cnic-register_1.78.75.v60.7-1vmw.670.0.0.8169922, VMW_bootbank_misc-drivers_6.7.0-0.0.8169922, VMW_bootbank_mtip32xx-native_3.9.8-1vmw.670.1.28.10302608, VMW_bootbank_ne1000_0.8.4-1vmw.670.1.28.10302608, VMW_bootbank_nenic_1.0.21.0-1vmw.670.1.28.10302608, VMW_bootbank_net-bnx2_2.2.4f.v60.10-2vmw.670.0.0.8169922, VMW_bootbank_net-bnx2x_1.78.80.v60.12-2vmw.670.0.0.8169922, VMW_bootbank_net-cdc-ether_1.0-3vmw.670.0.0.8169922, VMW_bootbank_net-cnic_1.78.76.v60.13-2vmw.670.0.0.8169922, VMW_bootbank_net-e1000_8.0.3.1-5vmw.670.0.0.8169922, VMW_bootbank_net-e1000e_3.2.2.1-2vmw.670.0.0.8169922, VMW_bootbank_net-enic_2.1.2.38-2vmw.670.0.0.8169922, VMW_bootbank_net-fcoe_1.0.29.9.3-7vmw.670.0.0.8169922, VMW_bootbank_net-forcedeth_0.61-2vmw.670.0.0.8169922, VMW_bootbank_net-igb_5.0.5.1.1-5vmw.670.0.0.8169922, VMW_bootbank_net-ixgbe_3.7.13.7.14iov-20vmw.670.0.0.8169922, VMW_bootbank_net-libfcoe-92_1.0.24.9.4-8vmw.670.0.0.8169922, VMW_bootbank_net-mlx4-core_1.9.7.0-1vmw.670.0.0.8169922, VMW_bootbank_net-mlx4-en_1.9.7.0-1vmw.670.0.0.8169922, VMW_bootbank_net-nx-nic_5.0.621-5vmw.670.0.0.8169922, VMW_bootbank_net-tg3_3.131d.v60.4-2vmw.670.0.0.8169922, VMW_bootbank_net-usbnet_1.0-3vmw.670.0.0.8169922, VMW_bootbank_net-vmxnet3_1.1.3.0-3vmw.670.0.0.8169922, VMW_bootbank_nfnic_4.0.0.14-0vmw.670.1.28.10302608, VMW_bootbank_nhpsa_2.0.22-3vmw.670.1.28.10302608, VMW_bootbank_nmlx4-core_3.17.9.12-1vmw.670.0.0.8169922, VMW_bootbank_nmlx4-en_3.17.9.12-1vmw.670.0.0.8169922, VMW_bootbank_nmlx4-rdma_3.17.9.12-1vmw.670.0.0.8169922, VMW_bootbank_nmlx5-core_4.17.9.12-1vmw.670.0.0.8169922, VMW_bootbank_nmlx5-rdma_4.17.9.12-1vmw.670.0.0.8169922, VMW_bootbank_ntg3_4.1.3.2-1vmw.670.1.28.10302608, VMW_bootbank_nvme_1.2.2.17-1vmw.670.1.28.10302608, VMW_bootbank_nvmxnet3-ens_2.0.0.21-1vmw.670.0.0.8169922, VMW_bootbank_nvmxnet3_2.0.0.29-1vmw.670.1.28.10302608, VMW_bootbank_ohci-usb-ohci_1.0-3vmw.670.0.0.8169922, VMW_bootbank_pvscsi_0.1-2vmw.670.0.0.8169922, VMW_bootbank_qcnic_1.0.2.0.4-1vmw.670.0.0.8169922, VMW_bootbank_qedentv_2.0.6.4-10vmw.670.1.28.10302608, VMW_bootbank_qfle3_1.0.50.11-9vmw.670.0.0.8169922, VMW_bootbank_qfle3f_1.0.25.0.2-14vmw.670.0.0.8169922, VMW_bootbank_qfle3i_1.0.2.3.9-3vmw.670.0.0.8169922, VMW_bootbank_qflge_1.1.0.11-1vmw.670.0.0.8169922, VMW_bootbank_sata-ata-piix_2.12-10vmw.670.0.0.8169922, VMW_bootbank_sata-sata-nv_3.5-4vmw.670.0.0.8169922, VMW_bootbank_sata-sata-promise_2.12-3vmw.670.0.0.8169922, VMW_bootbank_sata-sata-sil24_1.1-1vmw.670.0.0.8169922, VMW_bootbank_sata-sata-sil_2.3-4vmw.670.0.0.8169922, VMW_bootbank_sata-sata-svw_2.3-3vmw.670.0.0.8169922, VMW_bootbank_scsi-aacraid_1.1.5.1-9vmw.670.0.0.8169922, VMW_bootbank_scsi-adp94xx_1.0.8.12-6vmw.670.0.0.8169922, VMW_bootbank_scsi-aic79xx_3.1-6vmw.670.0.0.8169922, VMW_bootbank_scsi-bnx2fc_1.78.78.v60.8-1vmw.670.0.0.8169922, VMW_bootbank_scsi-bnx2i_2.78.76.v60.8-1vmw.670.0.0.8169922, VMW_bootbank_scsi-fnic_1.5.0.45-3vmw.670.0.0.8169922, VMW_bootbank_scsi-hpsa_6.0.0.84-3vmw.670.0.0.8169922, VMW_bootbank_scsi-ips_7.12.05-4vmw.670.0.0.8169922, VMW_bootbank_scsi-iscsi-linux-92_1.0.0.2-3vmw.670.0.0.8169922, VMW_bootbank_scsi-libfc-92_1.0.40.9.3-5vmw.670.0.0.8169922, VMW_bootbank_scsi-megaraid-mbox_2.20.5.1-6vmw.670.0.0.8169922, VMW_bootbank_scsi-megaraid-sas_6.603.55.00-2vmw.670.0.0.8169922, VMW_bootbank_scsi-megaraid2_2.00.4-9vmw.670.0.0.8169922, VMW_bootbank_scsi-mpt2sas_19.00.00.00-2vmw.670.0.0.8169922, VMW_bootbank_scsi-mptsas_4.23.01.00-10vmw.670.0.0.8169922, VMW_bootbank_scsi-mptspi_4.23.01.00-10vmw.670.0.0.8169922, VMW_bootbank_scsi-qla4xxx_5.01.03.2-7vmw.670.0.0.8169922, VMW_bootbank_shim-iscsi-linux-9-2-1-0_6.7.0-0.0.8169922, VMW_bootbank_shim-iscsi-linux-9-2-2-0_6.7.0-0.0.8169922, VMW_bootbank_shim-libata-9-2-1-0_6.7.0-0.0.8169922, VMW_bootbank_shim-libata-9-2-2-0_6.7.0-0.0.8169922, VMW_bootbank_shim-libfc-9-2-1-0_6.7.0-0.0.8169922, VMW_bootbank_shim-libfc-9-2-2-0_6.7.0-0.0.8169922, VMW_bootbank_shim-libfcoe-9-2-1-0_6.7.0-0.0.8169922, VMW_bootbank_shim-libfcoe-9-2-2-0_6.7.0-0.0.8169922, VMW_bootbank_shim-vmklinux-9-2-1-0_6.7.0-0.0.8169922, VMW_bootbank_shim-vmklinux-9-2-2-0_6.7.0-0.0.8169922, VMW_bootbank_shim-vmklinux-9-2-3-0_6.7.0-0.0.8169922, VMW_bootbank_smartpqi_1.0.1.553-12vmw.670.1.28.10302608, VMW_bootbank_uhci-usb-uhci_1.0-3vmw.670.0.0.8169922, VMW_bootbank_usb-storage-usb-storage_1.0-3vmw.670.0.0.8169922, VMW_bootbank_usbcore-usb_1.0-3vmw.670.0.0.8169922, VMW_bootbank_vmkata_0.1-1vmw.670.0.0.8169922, VMW_bootbank_vmkfcoe_1.0.0.1-1vmw.670.1.28.10302608, VMW_bootbank_vmkplexer-vmkplexer_6.7.0-0.0.8169922, VMW_bootbank_vmkusb_0.1-1vmw.670.1.28.10302608, VMW_bootbank_vmw-ahci_1.2.3-1vmw.670.1.28.10302608, VMW_bootbank_xhci-xhci_1.0-3vmw.670.0.0.8169922, VMware_bootbank_cpu-microcode_6.7.0-1.28.10302608, VMware_bootbank_elx-esx-libelxima.so_11.4.1184.0-0.0.8169922, VMware_bootbank_esx-dvfilter-generic-fastpath_6.7.0-0.0.8169922, VMware_bootbank_esx-xserver_6.7.0-0.0.8169922, VMware_bootbank_lsu-hp-hpsa-plugin_2.0.0-16vmw.670.1.28.10302608, VMware_bootbank_lsu-intel-vmd-plugin_1.0.0-2vmw.670.1.28.10302608, VMware_bootbank_lsu-lsi-lsi-mr3-plugin_1.0.0-13vmw.670.1.28.10302608, VMware_bootbank_lsu-lsi-lsi-msgpt3-plugin_1.0.0-8vmw.670.0.0.8169922, VMware_bootbank_lsu-lsi-megaraid-sas-plugin_1.0.0-9vmw.670.0.0.8169922, VMware_bootbank_lsu-lsi-mpt2sas-plugin_2.0.0-7vmw.670.0.0.8169922, VMware_bootbank_lsu-smartpqi-plugin_1.0.0-3vmw.670.1.28.10302608, VMware_bootbank_native-misc-drivers_6.7.0-0.0.8169922, VMware_bootbank_qlnativefc_3.0.1.0-5vmw.670.0.0.8169922, VMware_bootbank_rste_2.0.2.0088-7vmw.670.0.0.8169922, VMware_bootbank_vmware-esx-esxcli-nvme-plugin_1.2.0.34-1.28.10302608, VMware_locker_tools-light_10.3.2.9925305-10176879
[root@esxi:~]


[HomeLab] – Copy VM from one ESXi host to another

I recently decided that it is time to setup a new homelab. The old server is about 10 yrs old. The hardware does not allow any upgrade in CPU and Ram. VMWare ESXi was version 6.5, but I could not upgrade to version 6.7 because the network card was not in the list of supported NICs and so the upgrade failed. Last, but not least, the power consumption was at 200W.

The new HomeLab has the following components

It took less than 30 minutes to assemble the NUC and install VMWare ESXi 6.7. ( + 15 minutes to drive to the local hardware store to grab an USB keyboard once I realized that I would need one for the setup )

Today, I migrated the existing VMs from the old host to the new one.

ESXi does not include VMotion. VMotion costs a lot of money.
I had read some articles which claimed to be best practice. But to be honest, using Veeam or SCP are not, what I consider “best” practice. I tried SCP, but it was so slooooow. Even a 50GB VM was estimated 11 hours to copy. And I have 30 VMs from just a couple of MB to 100GB.

I searched for a better solution. And I finally found it. VMware vCenter Converter Standalone Client.

You simply choose the “source” ESXi instance and select the VM to copy. Next you select the “target” ESXi. You can also choose, if the copy will be automatically updated to the target VM version.

It took only 30 minutes to copy a 50GB VM. Another 100GB VM was copied in 20 minutes.

The whole migration was done in only 5 hours. Not bad, isn’t it.



NotesJsonNavigator.getElementByPointer example

Sample code:

Const white = |{"color": "white", "category": "value","code": {"rgba": [0,0,0,1],"hex": "#FFF"}}|

Public Sub testJsonNavGetElementByPointer()
	Dim session As New NotesSession
	Dim jsnav As NotesJSONNavigator 
	Dim el As NOTESJSONELEMENT
	
	Set jsnav = session.CreateJSONNavigator(removeCRLF(white)) 
	
	'// returns "value"
	Set el = jsnav.Getelementbypointer("/category")
	MsgBox "category: " + el.Value
	
	'// returns "#FFF"
	Set el = jsnav.Getelementbypointer("/code/hex")
	MsgBox "hex: " + el.Value
	
	'// returns the 4th element in the rgba array  "1"
	Set el = jsnav.Getelementbypointer("/code/rgba/3")
	MsgBox "code/rgba/3: " + el.Value
End Sub

General information about JSON pointer: https://tools.ietf.org/html/rfc6901


NotesJsonNavigator.getNthElement does not obey boundries

I stumbled upon an issue with NotesJsonNavigator getNthElement(index) method.

It looks like there is no boundry check, which leads to some inconsitent behaviour and unpredictable results.

Here is the code that I used in my test.

%REM
	Sub testJsonNavGetNthElement
%END REM
Public Sub testJsonNavGetNthElement
	Dim s As New NotesSession 
	Dim jsnav As NotesJSONNavigator
	Dim el As NotesJSONElement
	
	Set jsnav = s.CreateJSONNavigator(|{ "element1" : "value 1", "element2" : "value 2", "element3 ": "value 3" }|)	
	
	Set el = jsnav.GetNthElement(0)
	Stop
	Set el = jsnav.GetNthElement(1)
	Stop
	Set el = jsnav.GetNthElement(2)
	Stop
	Set el = jsnav.GetNthElement(3)
	Stop
	Set el = jsnav.GetNthElement(1000)
	Stop
End Sub

The issue occurs with index < 1 and > upper bound of the array.

Index 0 AND index 1 both return the same value; “value 1“.
Index 1000 returns NULL or nothing.

This is not the expected behaviour. At least I would expect some “out of bounds” error.
Also, it is not clear for me what the base for the index is. Do we start counting at 0 or do we start with 1 ?

According to the documentation, the index is 1-based.


NotesJsonNavigator, NotesJsonElement, NotesJsonArray, NotesJsonObject example

NotesJsonNavigator, NotesJsonElement, NotesJsonArray, NotesJsonObject are new classes in Domino Designer as of Notes V10.0.1. They are not yet documented in the Domino Designer Help, but you can find online documentation following the above links.

The documentation also contains some basic samples.

In this article, I will demonstrate how to use the classes beyond the basic examples.
I’ll also show, how you can use the new NotesHttpRequest class to connect to a server and read and parse view data as JSON.

The first thing you need to know is that the NotesJsonNavigator class does not like CRLF.
When you try to create a new NotesJsonNavigator from the following JSON data

Const colors = |{
  "colors": [
    {
      "color": "black",
      "category": "hue",
      "type": "primary",
      "code": {
        "rgba": [255,255,255,1],
        "hex": "#000"
      }
    },
    {
      "color": "white",
      "category": "value",
      "code": {
        "rgba": [0,0,0,1],
        "hex": "#FFF"
      }
    },
    {
      "color": "red",
      "category": "hue",
      "type": "primary",
      "code": {
        "rgba": [255,0,0,1],
        "hex": "#FF0"
      }
    },
    {
      "color": "blue",
      "category": "hue",
      "type": "primary",
      "code": {
        "rgba": [0,0,255,1],
        "hex": "#00F"
      }
    },
    {
      "color": "yellow",
      "category": "hue",
      "type": "primary",
      "code": {
        "rgba": [255,255,0,1],
        "hex": "#FF0"
      }
    },
    {
      "color": "green",
      "category": "hue",
      "type": "secondary",
      "code": {
        "rgba": [0,255,0,1],
        "hex": "#0F0"
      }
    }
  ]
}|

you will get an error.

You can remove all CRLF from the data using this helper function.

Public Function removeCRLF(json As String) As String
	removeCRLF = Replace(Replace(json, Chr(13), ""),Chr(10),"")
End Function

Let us create the NotesJsonNavigator from NotesSession first.

Dim session As New NotesSession 
Dim jsnav As NotesJSONNavigator 
	
Dim json As String
json = removeCRLF(colors)
Set jsnav = session.CreateJSONNavigator(json)

Now we can count how many different colors we would find in our JSON object

Dim el As NOTESJSONELEMENT
Dim arr As NOTESJSONARRAY
	
Set el = jsnav.GetFirstElement()

Set arr = el.value
	
MsgBox "Elements count: " + CStr(arr.size)

And finally, we want to get the value for the second color in the JSON object.

Although there is a GetNthElement (index) method, this method seems to be buggy or not yet fully implemented. GetNthElement(index) will always return the first element from the JSON object.

So we will use GetFirstElement and GetNextElement to navigate thru the object.

'Set el = arr.Getnthelement(2)
Set el = arr.GetFirstElement()
Set el = arr.GetNextElement()

Dim obj As NOTESJSONOBJECT
Set obj = el.Value
Set el = obj.Getelementbyname("color")
	 
MsgBox "color: " + CStr(el.Value)

The next sample creates a NotesHttpRequest and gets JSON from a view in a Notes application. Then we retrieve the UNID and NoteId from the data returned using the new NotesJson… classes.

%REM
	Library 10010.http
	Created Jan 1, 2019 by Ulrich Krause/singultus
	Description: Comments for Library
%END REM
Option Public
Option Declare

Public Sub httpGet
	Dim Session As New NotesSession        
	Dim ret As String
	Dim URL As String

	Dim user As String
	Dim password As String
	
	Dim httpReq As NotesHTTPRequest
	Set httpReq = session.CreateHttpRequest()
	
	httpReq.Preferstrings = True
	
	user = "firstname.lastname@tld.de"
	password = "pAssw0rd"
	
	URL = "https://yourserver/names.nsf/($certifiers)?readviewentries&amp;outputformat=JSON"

	Call httpReq.Setheaderfield("Authorization", "Basic " + EncodeBase64 (user + ":" + password))

	Dim json As string
	json = httpReq.Get(URL)

	Dim jsnav As NotesJSONNavigator 
	Set jsnav = session.CreateJSONNavigator(removeCRLF(json))

	Dim el As NOTESJSONELEMENT
	Dim arr As NOTESJSONARRAY
	Dim obj As NOTESJSONOBJECT

	Set el = jsnav.GetElementByName("viewentry")

	Set arr = el.value
	Set el = arr.GetFirstElement()
	Set el = arr.GetNextElement()
	Set obj = el.Value
	Set el = obj.Getelementbyname("@unid")

	MsgBox "unid: " + CStr(el.Value)
	Set el = obj.Getelementbyname("@noteid")

	MsgBox "noteid: " + CStr(el.Value)

End Sub

Private Function removeCRLF(json As String) As String
	removeCRLF = Replace(Replace(json, Chr(13), ""),Chr(10),"")
End Function

Private Function EncodeBase64 (StrIn As String) As String
	Dim session As New NotesSession
	Dim stream As NotesStream
	Dim db As NotesDatabase
	Dim doc As NotesDocument
	Dim body As NotesMIMEEntity
	
	Set stream = session.CreateStream
	Call stream.WriteText (StrIn)
	
	Set db = session.CurrentDatabase
	Set doc = db.CreateDocument
	Set body  = doc.CreateMIMEEntity
	
	Call body.SetContentFromText (stream, "", ENC_NONE)
	Call body.EncodeContent (ENC_BASE64)
	
	EncodeBase64 = body.ContentAsText
	
	Call stream.Close
	Set doc = Nothing
End Function


Finding And Fixing Node.js Memory Leaks: A Practical Guide


Fixing memory leaks may not be not the shiniest skill on a CV, but when things go wrong on production, it’s better to be prepared!
After reading this article, you’ll be able to monitor, understand, and debug the memory consumption of a Node.js application.


Kévin Maschtaler

https://marmelab.com/blog/2018/04/03/how-to-track-and-fix-memory-leak-with-nodejs.html

In addition to this very good article , here are a couple of tipps how to enable remote debugging.

node –inspect=192.168.178.133:9229 yourapp.js

This will bind the debugger to a different IP address:port. Otherwise only LOCALHOST will be available

If you are using PM2

pm2 start –node-args=”–inspect=192.168.178.133:9229″ yourapp.js

Then open Chrome browser and navigate to chrome://inspect

Check “Discover network targets” and click “Configure”

Your application is ready for inspect


NotesDominoQuery sample

I have put together a small sample to demonstrate how to use NotesDominoQuery from LotusScript.

I created a new Class DQLWrapper. A little bit over the top, I know.

%REM
	Library 10010.dql
	Created Dec 30, 2018 by Ulrich Krause/singultus
%END REM
Option Declare

%REM
	Class DqlWrapper
%END REM
Public Class DqlWrapper
	
	m_query As String
	m_session As NotesSession
	m_db As NotesDatabase
	m_ndq As NotesDominoQuery
	
	%REM
		Sub New
	%END REM
	Public Sub New(strDbFilePath As String)
		Set me.m_session = New NotesSession
		Set me.m_db = me.m_session.Getdatabase("",strDbFilePath, False)
		If ( me.m_db.Isopen ) then
			Set me.m_ndq = me.m_db.Createdominoquery()
		Else
			' // do some error handling
		End if
	End Sub

	%REM
		Public function executeQuery()
	%END REM	
	Public function executeQuery() As NotesDocumentCollection
		If ( me.m_query <> "" ) then
			Set executeQuery = me.m_ndq.Execute(me.m_query)
		Else
			Set executeQuery = nothing
		End If
	End Function

	%REM
		Public Function explainQuery()
	%END REM	
	Public Function explainQuery() As String
		If ( me.m_query <> "" ) Then
			explainQuery = me.m_ndq.Explain(me.m_query)
		Else
			explainQuery = ""
		End If
		
	End Function
	
	%REM
		Public Function explainQuery()
	%END REM	
	Public Function parseQuery() As String
		If ( me.m_query <> "" ) Then
			parseQuery = me.m_ndq.parse(me.m_query)
		Else
			parseQuery = ""
		End If
		
	End Function	

	%REM
		Property query
	%END REM	
	Public Property Set query As String
		me.m_query = query
	End property
	
End Class

The query itself is executed from an agent that runs on the server. At the moment it is not possible to run a query client/ server.

Here is the code for the agent

%REM
	Agent dql.execute
	Created Dec 30, 2018 by Ulrich Krause/singultus
%END REM
Option Public
Option Declare
Use "10010.dql"

Sub Initialize
	Dim query As String
	Dim col As  NotesDocumentCollection
	query = "firstname = 'Ulrich' And lastname = 'Krause'"
	
	Dim dql As New DQlWrapper("names.nsf")
	dql.query = query
	
	If (  LCase(dql.parseQuery()) ="success" ) Then
		
		Set col = dql.executeQuery()
		MsgBox "QRY returns # docs: " + CStr(col.count)
		
		If ( col.count > 0 ) then
			Dim doc As NotesDocument
			Set doc = col.Getfirstdocument()
			MsgBox "UNID of first doc: " + doc.Universalid
		End if
	Else 
		
		MsgBox dql.explainQuery()
	End If
	
End Sub

You can now start the agent from the server console. You will get the number of documents for this query and the UNID of the first document found.

te amgr run "ec11.nsf" 'dql.execute'
[0DFC:001F-0FFC] 30.12.2018 13:49:10 AMgr: Start executing agent 'dql.execute' in 'ec11.nsf'
[0DFC:001F-0FFC] 30.12.2018 13:49:10 Agent Manager: Agent message: QRY returns # docs: 1
[0DFC:001F-0FFC] 30.12.2018 13:49:10 Agent Manager: Agent message: UNID of first doc: D8436D0F4E546BA3C12573FE0070AE88
[0DFC:001F-0FFC] 30.12.2018 13:49:10 AMgr: Agent 'dql.execute' in 'ec11.nsf' completed execution

If your query contains errors / is not understandable, you will see an output similar like this on your console

[0DFC:0020-11D0] 30.12.2018 13:59:45   Agent Manager: Agent 'dql.execute' error: Domino Query execution error:   Query is not understandable -  syntax error     - processing or expecting operator (=, <, <= …) token syntax    (Call hint: OSCalls::OSLocalAllc, Core call 
0) firstname = 'Ulrich' And lastname IS 'Krause' …………………………….^……….. ****
[0DFC:0020-11D0] 30.12.2018 13:59:45 AMgr: Agent 'dql.execute' in 'ec11.nsf' completed execution


Start node.js application using systemd on RHEL 7

In a test or development environment we can start a node.js application with npm start . But this is not, what we want to do in production.

We need to start the application as soon as the server is up and running.

Here is a quick and easy way to achieve this goal.

Create a new file in /etc/systemd/system ( domino-db.service for example )

[Unit]
Description=domino-node-list sample

[Service]
ExecStart=/git/domino-node-list/app.js
Restart=always
User=root
Group=root
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/git/domino-node-list/

[Install]
WantedBy=multi-user.target

Modify ExecStart and WorkingDirectory to point to the correct path in your installation.

Save domino-db.service and set execution rights to 744.

Check app.js for proper execution right (744).

Edit app.js and add

#!/usr/bin/env node

as the first line in the code.

Enable the service

systemctl enable domino-db

Created symlink from /etc/systemd/system/multi-user.target.wants/domino-db.service to /etc/systemd/system/domino-db.service.

Check with

systemctl status domino-db

● domino-db.service - domino-node-list sample
   Loaded: loaded (/etc/systemd/system/domino-db.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2018-12-30 10:24:04 CET; 4min 15s ago
 Main PID: 7163 (node)
    Tasks: 7
   CGroup: /system.slice/domino-db.service
           └─7163 node /git/domino-node-list/app.js

Dec 30 10:24:04 serv02.fritz.box systemd[1]: Started domino-node-list sample.
Dec 30 10:24:04 serv02.fritz.box systemd[1]: Starting domino-node-list sample...
Dec 30 10:24:05 serv02.fritz.box app.js[7163]: Example app listening at http://:::3000

You can now start and stop the service at any time using

systemctl start domino-db
systemctl stop domino-db

[Solved] – PROTON_SSL=1

This is a follow up to my recent post https://www.eknori.de/2018-12-28/problem-using-proton-with-proton_ssl1-enabled/

Due to my misunderstanding and also a lack in documentation, I was not able to get PROTON with PROTON_SSL=1 and PROTON_AUTHENTICATION=client_certs running.

But thanks to our great community and a little help from Jan Krejcárek (SUTOL), I was finally able to solve the puzzle.

I must admit that I am not an expert in this field. So please bare with me if things are obvious for you. They are (not yet) for me.

My understanding of using TLS/SSL with PROTON was that I could use my existing Let’s Encrypt certificate to communicate via HTTPS instead of HTTP with my server URL.

But this is not the case. TLS/SSL encryption only enables a secured communication between the domino.db module and the PROTON addin sitting on the Domino server.

Here is an image.

The second thing that I got wrong was the fact that the existing certificate and it’s CA would be enough to enable the secured communication. In my first test, I had set PROTON_AUTHENTICATION=anonymous.

So why bother with client certificates if they are not needed at this point?

I followed Jan’s advice to try with a new generated self-signed certificate. The AppDevPack contains shell scripts to create some sample certs and also lets you create a keyring file that can be used with PROTON.

I have modified the make_keyring.sh file a bit because it complained about missing environment variables.

# added UKR, 12/2018
export NOTESDATA=/local/notesdata
export NOTESBIN=/opt/ibm/domino/notes/10000000/linux

I then created the certs and keyring and copied them to the proper location.

The original source code has used

// proton config
const serverConfig = {
    "hostName": "serv02.fritz.box",
    "connection": {
        "port": 3002
    }
}

to define the serverConfig. This must be enhanced, if you are going the secure way.

I created a new module using Jan’s code

const fs = require('fs');
const path = require('path');

/**
 * Internal functions that reads a content of a file to a buffer.
 * @param {string} fileName 
 */
const readFile = fileName => {
        return fs.readFileSync(path.resolve(fileName));
}

/**
 * (c) Jan Krejcárek
 * Creates an object in a structure required for the DominoDB module
 * to initialize o connection to the Domino server.
 * @param {string} serverHostName Host name of the Domino server
 * @param {string} port TCP port number where a Proton task listens for connection requests
 * @param {string} rootCertificatePath Path to the certificate used to establish a TLS connection
 * @param {string} clientCertificatePath Path to the application certificate used to authenticate the application
 * @param {string} clienKeyPath Path to the private key of the application
 */
const config = (serverHostName, port, rootCertificatePath, clientCertificatePath, clienKeyPath) => {
    const rootCertificate = readFile(rootCertificatePath);
    const clientCertificate = readFile(clientCertificatePath);
    const clientKey = readFile(clienKeyPath);
    
    const serverConfig = {
            hostName: serverHostName,
            connection: {
                    port: port,
                    secure: true
            },
            credentials: {
                    rootCertificate,
                    clientCertificate,
                    clientKey
            }    
    };

    return serverConfig;
};

module.exports = config;

All parts in the credentials section are mandatory. Even if you use PROTON_AUTHENTICATION=anonymous with PROTON_SSL=1, you must have certificates and keys for the client as well.

And we can now use this module in app.js

const protonConfig = require('./protonConfigSSL.js');
const serverConfig = protonConfig("serv02.fritz.box", "3002", "./certs/proton-self/ca.crt", "./certs/proton-self/app1.crt", "./certs/proton-self/app1.key");

My PROTON configuration is

[029971:000009-00007F78D8293700] PROTON_AUTHENTICATION=client_cert
[029971:000009-00007F78D8293700] PROTON_KEYFILE=proton-self.kyr
[029971:000009-00007F78D8293700] PROTON_LISTEN_ADDRESS=serv02.fritz.box
[029971:000009-00007F78D8293700] PROTON_LISTEN_PORT=3002
[029971:000009-00007F78D8293700] PROTON_SSL=1
[029971:000009-00007F78D8293700] PROTON_TRACE_REQUEST=0
[029971:000009-00007F78D8293700] PROTON_TRACE_SESSION=0

And after restarting PROTON ( restart task proton ) and starting my application ( npm start ), I was able to open hp.nsf in the browser.

I also found an interesting article by Sven Hasselbach about how to protect PROTON keys.

I hope that this will help others starting with this stuff to save some time.

Recommended reading:

Heiko Voigt: DominoDB and a big NO-NO !

Sven Hasselbach ( response to Heiko’s post ): DominoDB and a big NO-NO?


Problem using PROTON with PROTON_SSL=1 enabled

By default, PROTON supports anonymous access and an insecure connection. This is a good starting point, but if you want to do something more productive, you should at least have PROTON_SSL set to 1 and a valid certificate at hand.

You can also use a self signed certificate, but most, if not all browsers will reject it. I am using a certificate issued by Lets Encrypt. This is the same certificate that I am using on my Domino server to secure HTTPS connections.

My sample node application uses the excellent sample written by Oliver Busse from https://gitlab.com/obusse/domino-node-list
I have changed package.json to use the latest domino-db module

{
"name": "domino-node-list",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@domino/domino-db": "file:vendor/domino-domino-db-1.1.0.tgz",
"ejs": "^2.6.1",
"express": "^4.16.3"
}
}

I also left app.js unchanged

// domino-db
const {
useServer
} = require('@domino/domino-db');

// proton config
const serverConfig = {
"hostName": "127.0.0.1",
"connection": {
"port": 3002
}
}

// domino nsf
const databaseConfig = {
"filePath": "hp.nsf"
};

I created hp.nsf according to the NSF source description in the reporitory.

Needed ports on the host are open

[root@serv02 domino-node-list]# firewall-cmd --list-ports
22/tcp 8585/tcp 1352/tcp 3001/tcp 3002/tcp 3000/tcp 10000/tcp 21/tcp 8443/tcp 443/tcp 80/tcp

1. PROTON_SSL=0

First test with

PROTON_AUTHENTICATION=anonymous
PROTON_LISTEN_ADDRESS=0.0.0.0
PROTON_LISTEN_PORT=3002

12/28/2018 04:36:56.50 AM PROTON> Listening on 0.0.0.0:3002, INSECURE
12/28/2018 04:36:56.50 AM PROTON> Server initialized
12/28/2018 04:36:56.50 AM PROTON> Server allows Anonymous access only.

node app has been started

> domino-node-list@1.0.0 start /git/domino-node-list
> node app.js
Example app listening at http://:::3000

In the browser, I typed http://eknori.blogsite.org:3000/

On the server console, I got:

[013713:000006-00007FCB3C40C700] 12/28/2018 04:39:42.88 AM PROTON> Session::init()
[013713:000006-00007FCB3C40C700] Tokens parsed

[013713:000006-00007FCB3C40C700] order = 1, level = 0, token = {form}, delim { } type = IDENTIFIER, opertype = NOT AN OPERATION, booltype = NONE
[013713:000006-00007FCB3C40C700] order = 2, level = 0, token = {=}, delim { } type = OPERATOR, opertype = EQUALITY, booltype = NONE
[013713:000006-00007FCB3C40C700] order = 3, level = 0, token = {'post'}, delim {'} type = QUOTED STRING, opertype = NOT AN OPERATION, booltype = NONE

[013887:000009-00007F93BA436700] Documents scanned = 4
[013887:000009-00007F93BA436700] Count of docs found = 2

PROTON_SSL=1

Next, I tried with PROTON_SSL=1 ( eknori.kyr uses a valid Lets Encrypt certificate, no client certificates created and configured at this time )

sh con proton*
PROTON_AUTHENTICATION=anonymous
PROTON_KEYFILE=eknori.kyr
PROTON_LISTEN_ADDRESS=0.0.0.0
PROTON_LISTEN_PORT=3002
PROTON_SSL=1
PROTON_TRACE_REQUEST=1
PROTON_TRACE_SESSION=1

lo proton
12/28/2018 04:10:14.55 AM PROTON> Build 0.2.2
12/28/2018 04:10:15.69 AM PROTON> Listening on 0.0.0.0:3002, SSL-ENABLED
12/28/2018 04:10:15.69 AM PROTON> Server initialized
12/28/2018 04:10:15.69 AM PROTON> Server allows Anonymous access only.

sh ta o
PROTON Listening on 0.0.0.0:3002, SSL-ENABLED
HTTP Server Listen for connect requests on TCP Port:80, 443

When I know try to open https://eknori.blogsite.org:3000, I get

Secure Connection Failed
The connection to eknori.blogsite.org:3000 was interrupted while the page was loading.
The page you are trying to view cannot be shown because the authenticity of the received data could not be verified. Please contact the website owners to inform them of this problem.

Request headers (355 B)
Accept text/html,application/xhtml+xm…plication/xml;q=0.9,/;q=0.8
Accept-Encoding gzip, deflate, br
Accept-Language en-US,de;q=0.7,en;q=0.3
Connection keep-alive
DNT 1
Host eknori.blogsite.org:3000
Upgrade-Insecure-Requests 1
User-Agent Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/64.0

I have verified that I can connect to PROTON

[root@serv02 domino-node-list]# openssl s_client -connect eknori.blogsite.org:3002
CONNECTED(00000003)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = eknori.blogsite.org
verify return:1
Certificate chain
0 s:/CN=eknori.blogsite.org
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
i:/O=Digital Signature Trust Co./CN=DST Root CA X3
Server certificate
-----BEGIN CERTIFICATE-----
MIIFkzCCBHugAwIBAgISA02ojw4Nx7YNceZNEQjzQ6HRMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODExMjQwOTI5NDdaFw0x
OTAyMjIwOTI5NDdaMB4xHDAaBgNVBAMTE2Vrbm9yaS5ibG9nc2l0ZS5vcmcwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCF64IGqJ1K6AB4Is7KasQEdaiv
gN5Fh1VUulz8xY/hCshgyIP46weexrU6jpmUE/DUQSuC/YfzpF7wohZrtd+4AHwd
Hu+ICmGh8tGa1KRv2j/Gpu2Cj1h1uXms+qdMBX/fQzQ9pM/8o4PS1NvUkt58dpL0
1zpbyr9ibbfGV1VPE68zCDWYT+S1VhC/sXt09B32zsd+LC5bJgtqbYUa5dghbDnY
G7qLI3ahy/atRMD4+Qdyk4yJqqQO58LMKO/+h9er8JNxMXoDDaT54Sq7owRHm3Vn
EkomPPriIXEizqXd1GwpAYc/uiaOYUorUFnoA40u8Gi2RlTHh2ze6rLzplrnAgMB
AAGjggKdMIICmTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFGUvZj1OYKlDBGAiMref
eMYoSN0dMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUF
BwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNy
eXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNy
eXB0Lm9yZy8wUgYDVR0RBEswSYITZWtub3JpLmJsb2dzaXRlLm9yZ4IPZWtub3Jp
LnNwZG5zLmRlgg9la25vcmkuc3BkbnMuZXWCEGVrbm9yaS5zcGRucy5vcmcwTAYD
VR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYa
aHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEFBgorBgEEAdZ5AgQCBIH2BIHz
APEAdwB0ftqDMa0zEJEhnM4lT0Jwwr/9XkIgCMY3NXnmEHvMVgAAAWdFRKvdAAAE
AwBIMEYCIQCbF0dO0sH/+n0JgzUdKrNQdSXi0icdC/tcgoZJMWbBsQIhALrmWoIq
aOAryVyB5BdiIaCJ4gkQpltzZlMXsf2a8S6EAHYAY/Lbzeg7zCzPC3KEJ1drM6SN
YXePvXWmOLHHaFRL2I0AAAFnRUSr+gAABAMARzBFAiARZMQ/xNmVsD1AtFKhsjP/
JT7Ly1w/4t1lpCc9llWC6gIhAL9AvzdQfP3JSezUftlhbQwUq8pl87yilvM7tF/6
fJm9MA0GCSqGSIb3DQEBCwUAA4IBAQBWBIEnaXToqKlLepvxllJzJRaG4IdtDpHC
BN+bSsf5pCR9a1IOiJURdneLUF2RFYEtK8YOsWhEgpFodC/lEGC7P9AlPwK24/vO
Wyd2jPbCgpyzmp+8Hc6QLhttxz+Pd+MrjKFuYhpFkGqigAZGywiz12wPjhNOwhkI
iLpGENPnfJA/DPD01Zk0/4+toAhayuKqzVVB47W2O88dHB5TpsknGwVAWBAouTpm
hy8mHuVNk0+fwXGn739kLgZlsv95NWLCLIo5InJ5eHSQKgAU9uzRo5fzFXdRgxHk
w+4uKbruLxzi2IXUA214xEycylq49/jXePphfAsPXDvMYtmQRLcx
-----END CERTIFICATE-----
subject=/CN=eknori.blogsite.org
issuer=/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
No client certificate CA names sent
Peer signing digest: SHA256
Server Temp Key: ECDH, P-256, 256 bits
SSL handshake has read 3260 bytes and written 415 bytes
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES256-GCM-SHA384
Session-ID: 49DA29C84DEFE38DAE3B7458F1E59859145DC5C27A27B248E1AC1958175F3BCC
Session-ID-ctx:
Master-Key: EC83FBCB155BEF83E9450C73D6A56A487DE933FC2D1405F3D95E725D9698962378BC65CA8D683F985415DF02D39A266F
Key-Arg : None
Krb5 Principal: None
PSK identity: None
PSK identity hint: None
TLS session ticket lifetime hint: 7200 (seconds)
TLS session ticket:
0000 - b8 b4 e4 5d 80 ec 53 c5-a5 06 8c 23 19 82 2e f2 …]..S….#….
0010 - 4e 66 a9 3a 5c d6 d6 09-b1 cb 69 b2 2e 16 f9 37 Nf.:.….i….7
0020 - 81 b6 db 13 a3 08 76 e0-59 91 38 12 15 0a 43 9e ……v.Y.8…C.
0030 - da df b2 8c d4 57 00 be-ef 77 30 12 77 4e 08 92 …..W…w0.wN..
0040 - 1c d6 ee ca d2 98 41 79-47 c7 b7 69 1e 93 f4 91 ……AyG..i….
0050 - 80 8e 9a 20 f7 88 30 4d-22 3b 96 37 22 6c 1e bd … ..0M";.7"l..
0060 - ca 98 87 3c 25 4c ca 33-c9 07 a3 45 a3 92 51 9c …<%L.3…E..Q.
0070 - a2 60 a6 fa 3f 5d 8f 6e-30 f9 75 4e 8d e1 17 ea .`..?].n0.uN….
0080 - f2 99 c5 74 fb d4 94 38-9d 29 39 8a 01 82 50 1a …t…8.)9…P.
0090 - e0 a2 af 40 44 75 a6 2d-a1 06 c5 9a 50 7a 3c 2e …@Du.-….Pz<.
Start Time: 1545966761 Timeout : 300 (sec)
Verify return code: 0 (ok)
read:errno=0

I also tried a simple https://eknori.blogsite.org:3000. I (expected) result is the following server console output

[013816:000007-00007FBB7D375700] 12/28/2018 04:48:24.89 AM PROTON> GET request without QUERY [src/core/ext/filters/http/server/http_server_filter.c:236]
[013816:000007-00007FBB7D375700] 12/28/2018 04:48:24.89 AM PROTON> Invalid entry in accept encoding metadata: ' deflate'. Ignoring. [src/core/lib/surface/call.c:940]
[013816:000007-00007FBB7D375700] 12/28/2018 04:48:24.89 AM PROTON> Invalid entry in accept encoding metadata: ' br'. Ignoring. [src/core/lib/surface/call.c:940]

So, apparently, connection via external server IP address is possible, but there is a problem when the node app tries to communicate to the PROTON addin.
Unfortunately, there are no error messages; neither on the Domino server console nor in the node app.