Monday, November 22, 2021

Alter user during authentication using DSAPI

I had a need to alter user during web-authentication process on fly (skipping password validation). Initially the task looked impossible but I managed to solve it using DSAPI filter.
Though the solution looks quite unsecure it could be very useful in some cases (by high level administrators) who needs to 'signin' as a user in their organization to do some checks.

Here are few most important snippets how to do that:

1. Subscribe for the event kFilterAuthenticate

That means that our dsapi filter only intercepts one specific event: kFilterAuthenticate), as there are other 10-15 other events which we do not wanna touch.

EXPORT unsigned int FilterInit(FilterInitData* filterInitData) {
	STATUS   error = NOERROR;

	filterInitData->appFilterVersion = kInterfaceVersion;
	filterInitData->eventFlags = kFilterAuthenticate;

	// other logic
	// ...
}

2. Catch the authenticate event and process it

Get our event and associate it with a C function

EXPORT unsigned int HttpFilterProc(FilterContext* context, unsigned int eventType, void* eventData) {
	/* Include only those events we want to handle */
	switch (eventType) {
	case kFilterAuthenticate:
		return Authenticate(context, (FilterAuthenticate *) eventData);
	default:
		break;
	}

   return kFilterNotHandled;
}	// end HttpFilterProc

3. Finally set a desired username

Below I only show the key moment - replace user name with another name

unsigned int Authenticate(FilterContext* context, FilterAuthenticate* authData) {
	/* logic that calculate username  */
    // .................................
    // char[] fullName = "CN=T5 Tester5/O=DmytroDev";
    // .................................

	/* Copy the canonical name for this user that dsapi requires.  */
	strncpy ((char *)authData->authName, fullName, authData->authNameSize);
	authData->authNameSize = strlen(alterAuthToken);
	authData->authType = kAuthenticBasic;
	authData->foundInCache = TRUE;

	return kFilterHandledEvent;
}

In order to improve security I have built an application on Domino side that generates tokens which have to be set in cookie and then DSAPI filter reads the cookie and get username from database. Tokens could be generated only by certain people are will be deleted by schedule agents after some time.



On the screenshot below you can see that I signed in as a "T5 Tester5" using my custom token AlterAuthToken while I am Anonymous.



Friday, November 12, 2021

Clear database replication history programatically

Recently I had a need to make a solution that can periodically clean replication history for list of databases.

Native LotusScript/Java classes do not allow that, but there is an C API for that.

Here is a cross platform solution (works for Windows/Linux)

Declare

Public Const W32_LIB = {nnotes.dll}
Public Const LINUX_LIB = {libnotes.so}

Declare Function W32_NSFDbOpen Lib W32_LIB Alias {NSFDbOpen} (ByVal dbName As String, hDb As Long) As Integer
Declare Function W32_NSFDbClose Lib W32_LIB Alias {NSFDbClose} (ByVal hDb As Long) As Integer
Declare Function W32_NSFDbClearReplHistory Lib W32_LIB Alias {NSFDbClearReplHistory} (ByVal hDb As Long, flags As Integer) As Integer

Declare Function LINUX_NSFDbOpen Lib LINUX_LIB Alias {NSFDbOpen} (ByVal dbName As String, hDb As Long) As Integer
Declare Function LINUX_NSFDbClose Lib LINUX_LIB Alias {NSFDbClose} (ByVal hDb As Long) As Integer
Declare Function LINUX_NSFDbClearReplHistory Lib LINUX_LIB Alias {NSFDbClearReplHistory} (ByVal hDb As Long, flags As Integer) As Integer

Using C API functions

// get a handler to database
If IS_WINDOWS Then
	rc = W32_NSFDbOpen(Server & "!!" & FileName, hDb)
Else
	rc = LINUX_NSFDbOpen(Server & "!!" & FileName, hDb)
End If

// clear replication history
If IS_WINDOWS Then
	rc = W32_NSFDbClearReplHistory(hDb, 0)
Else
	rc = LINUX_NSFDbClearReplHistory(hDb, 0)
End If

// close datababase (be sure you always close hDb if you opened it, otherwise memory leak).
If IS_WINDOWS Then
	rc = W32_NSFDbClose(hDb)
Else
	rc = LINUX_NSFDbClose(hDb)
End If

Be sure that you always close hDb handler if you opened the database, otherwise it would lead to memory leak)

See the full solution on GitHub: DominoReplicationHistoryCleaner

Thursday, April 08, 2021

Checking if database is encrypted with LotusScript (C API)

Since it's not possible to identify encryption status and level using native LotusScript/Java classes here is a way to do that. The solution is based on Notes CAPI (within LotusScript) but it works for both Linux/Windows environment.

I will omit NSFDbOpen and NSFDbClose since it's easy to find out and focus instead on the main function: NSFDbLocalSecInfoGetLocal.

Declaration

Const NNOTES ="nnotes.dll"
Const LIBNOTES ="libnotes.so"

Declare Public Function WIN_NSFDbLocalSecInfoGetLocal Lib NNOTES Alias "NSFDbLocalSecInfoGetLocal"(ByVal hDb As Long, state As Long, strength As Long) As Integer
Declare Public Function LIN_NSFDbLocalSecInfoGetLocal Lib LIBNOTES Alias "NSFDbLocalSecInfoGetLocal"(ByVal hDb As Long, state As Long, strength As Long) As integer

Function check encryption status

public Function NSFDbLocalSecInfoGetLocal(hDB As Long, state As Long, strength As long) As Integer
 If isDefined("WINDOWS") Then
  NSFDbLocalSecInfoGetLocal = WIN_NSFDbLocalSecInfoGetLocal(hDb, state, strength)
 ElseIf isDefined("LINUX") Then
  NSFDbLocalSecInfoGetLocal = LIN_NSFDbLocalSecInfoGetLocal(hDb, state, strength)
 End If
End Function

Example how to use it

Private Function calcEncryption(database As NotesDatabase, doc As notesdocument)
 Dim sDb As String
 Dim hDb As Long
 Dim state As Long
 Dim encrypt As Long
 Dim rc As Integer

 sDb = database.server & "!!" & database.filepath

 rc = NSFDbOpen(sDb, hDb)
 If rc <> 0 Then Exit function

 rc = NSFDbLocalSecInfoGetLocal(hDB, state, encrypt)
 If rc <> 0 Then
  Error 9001, "Impossible to read encryption. Error code: " & CStr(rc)
 End If

 rc = NSFDbClose(hDb)
End Function
  • state: 0 (not encrypted), 1 (encrypted) or 2 (will be encrypted after compact)
  • encrypt: 1 (easy), 2 (middle), 3 (strong)

Wednesday, January 20, 2021

How to post attachments using form to agent

I have a form with some text fields and I also needed to send attachments within same form.

Form is printed by agent and is processed by another agent written in LotusScript.

I spent some time working on solution and here it is.

The idea is to convert selected files to base64 on client side and then post data on submission and agent that process submission will conver base64 to file.

Here is a form, note that we run some logic when files are added

<form name="formName" method="post" action="agentName?openagent">
<input name="title" value="xxx">
<input type="file" name="files" multiple onchange="toBase64()">
</form>

Here is how we convert selected files to base64 and how we results as text fields to form (JS is not optimal, it can be done without jQuery)

function toBase64() {
  var files = document.querySelector('input[type=file]').files;

  var form = $("form");
  form.find("input[name^='filebase64']").remove(); // replace

  function readAndSave(file, index) {
    var reader = new FileReader();
	
    reader.addEventListener("load", function() {
      form.append("");
      form.append("");
    }, false);

    reader.readAsDataURL(file);
  }

  if (files) {
    [].forEach.call(files, readAndSave);
  }
}

Once form is submitted we have to read base64 items and convert them to file. There are at least 2 solutions: pure LS or Java/LS2J

a) LotusScript using NotesMIMEHeader
Private Function saveBase64AsFile(base64 As String, filePath As string) As Boolean
	On Error GoTo ErrorHandler

	Dim stub As NotesDocument
	Dim stream As NotesStream
	Dim item As NotesMIMEEntity
	Dim header As NotesMIMEHeader
	Dim emb As NotesEmbeddedObject
	Dim fileName As String
	Dim contentType As string
	Dim base64File As String

	fileName = StrRightBack(filePath, "\")
	contentType = StrRight(Strleft(base64, ";"), ":")
	base64File = StrRight(Base64, ",")
	
	Call scriptLog.loginfo(fileName)
	Call scriptLog.loginfo(contentType)
	
	Set stub = db.Createdocument()
	Set item = stub.CreateMIMEEntity("Body")
	Set header = item.createHeader("Content-Disposition")
	Call header.setHeaderVal({attachment; filename="} & fileName & {"})

	Set stream = app.NotesSession.CreateStream()
	Call stream.WriteText(base64File)
	Call item.SetContentFromText(stream, contentType, ENC_BASE64)

	Call stream.Truncate
	Call stream.Close
	Call stub.Closemimeentities(True)

	Set emb = stub.Getattachment(fileName)
	Call emb.Extractfile(filePath)
	
	Exit Function
ErrorHandler:
	Error Err, Error
End Function
b) Java with LS2J using native classses.
import java.util.Base64;
import java.io.IOException;
import java.nio.file.*;

public class Base64ToFile{

	public boolean convert(String base64String, String filePath) {
		try {
			byte[] decodedImg = Base64.getDecoder().decode(base64String.getBytes());
			Path destinationFile = Paths.get(filePath);
			Files.write(destinationFile, decodedImg);
			return true;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}
}
UseLSX "*javacon"
Use "Base64ToFile"

Class Base64ToFile
	Private jSession As JavaSession
	Private jClass As Javaclass
	Private jObject As JavaObject
	Private jError As JavaError
	
	Sub New()
		Set jSession = New JavaSession
		Set jClass = jSession.GetClass("Base64ToFile")
		Set jObject = jClass.Createobject()
	End Sub
	
	Public Function convert(base64 As String, filePath As String) As Boolean
		convert = jObject.convert(base64, filePath)
	End Function
End Class

Tuesday, October 20, 2020

JavaServerAddin in Domino - constructor and schedule

In this article we will improve our java addin 'DemoAddin' with following features:

  1. Constructor that accepts parameters when we load java addin from console.
  2. We will schedule output to console amount of registered person in names.nsf (every 33 seconds).
  3. Define destructor.
  4. Load (with parameter) and Unload addin from console

Constructor

We will define 2 constructor, one that can accept parameters and one in case if we load addin without any parameters. It is pretty obvious how it works.

// we expect our first parameter is dedicated for secondsElapsed
public DemoAddin(String[] args) {
	this.secondsElapsed = Integer.parseInt(args[0]);
}

// constructor if no parameters
public DemoAddin() {}

Means if we run command like below it will run using constructor with parameters

load runjava org.demo.DemoAddin 33

Schedule worker

There is a method (it works but deprecated, however I have not found what should be use instead). The snippet below run constant loop, open names.nsf, read amount of users in the view People and output it to console. The main line here is this.addInRunning(), it keeps loop running forever (until we change it to this.stopAddin() or unload addin from console)

session = NotesFactory.createSession();
String server = session.getServerName();

while (this.addInRunning()) {
	/* gives control to other task in non preemptive os*/
	OSPreemptOccasionally();

	if (this.AddInHasSecondsElapsed(secondsElapsed)) {
		ab = session.getDatabase(server, "names.nsf");
		long count = ab.getView("People").getAllEntries().getCount();
		logMessage("Count of persons: " + Long.toString(count));
		ab.recycle();
	}
}

Destructor

Keep in mind that we need to be careful with Notes object, we have to release memory (recycle) after we no longer use them. So it's a good idea to create own terminate method that release memory for all Notes object you delcared and use it when addin unloads. There is also built-in method finalize so you can put code there, but I prefer to have own method and use it in places I need

private void terminate() {
	try {
		if (this.ab != null) {
			this.ab.recycle();
		}
		if (this.session != null) {
			this.session.recycle();
		}

		logMessage("UNLOADED (OK)");
	} catch (NotesException e) {
		logMessage("UNLOADED (**FAILED**)");
	}
}

Load (with parameter) and Unload addin from console

In order to run addin with parameter simply add it after name of Addin, use space as a separator when you need more than 1 parameter

load runjava org.demo.DemoAddin 33
[1098:0002-23A0] 10/20/2020 11:15:00 AM  JVM: Java Virtual Machine initialized.
[1098:0002-23A0] 10/20/2020 11:15:00 AM  RunJava: Started org/demo/DemoAddin Java task.
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: version             2
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: build date          2020-10-19 11:00 CET
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: java                1.8
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: seconds elapsed     33
[1098:0004-3984] 10/20/2020 11:15:33 AM  DemoAddin: Count of persons: 11
[1098:0004-3984] 10/20/2020 11:16:06 AM  DemoAddin: Count of persons: 11
[1098:0004-3984] 10/20/2020 11:16:39 AM  DemoAddin: Count of persons: 11
[1098:0004-3984] 10/20/2020 11:17:12 AM  DemoAddin: Count of persons: 11

When you want to unload addin using console here is a command

tell runjava unload org.demo.DemoAddin

And you should be see confirmation on console if everything went fine

[1098:0004-3984] 10/20/2020 11:26:55 AM  DemoAddin: UNLOADED (OK)
[1098:0002-23A0] 10/20/2020 11:26:55 AM  RunJava: Finalized org/demo/DemoAddin Java task.
[1098:0002-23A0] 10/20/2020 11:26:56 AM  RunJava shutdown.

If you are interested in this topic I can recommend at least two more sources NSFTools.com JavaAddinTest and AndyBrunner / Domino-JAddin or wait for new articles in my blog :-). Also feel free to ask questions if you are uncertain.

Full version of DemoAddin class is hosted on github: DominoDemoAddin

All articles in series
  1. JavaServerAddin in Domino - introduction
  2. JavaServerAddin in Domino - constructor and schedule