SessionRestHandler.java
/*
** Module : SessionRestHandler.java
** Abstract : Collection of security administration workers and session state
**
** Copyright (c) 2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 RNC 20250108 Created initial version.
** 002 OM 20250320 SessionHandler was renamed to SessionRestHandler. The common REST features were
** extracted to AdminRestHandler which also handles the authentication.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.admin.server;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.type.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.util.logging.*;
import org.eclipse.jetty.server.*;
import javax.servlet.http.*;
import java.io.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.logging.*;
/**
* A handler for administration of session resources using REST APIs.
*/
public class SessionRestHandler
extends BaseAdminRestHandler
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(SessionRestHandler.class);
/** The singleton which handles tenant REST requests. */
private static SessionRestHandler instance;
/** The singleton instance for session management. */
private final RouterSessionManager sm;
/**
* Constructs a new handler for administration of session resources using REST APIs.
*
* @param ah
* The WebAuthHandler used for authorisation.
*/
protected SessionRestHandler(WebAuthHandler ah)
{
super("/admin/sessions", ah);
sm = (RouterSessionManager) RouterSessionManager.get();
}
/**
* Tries to initialize this REST handler. It does this by reading the directory and creating the
* {@code DomainRestHandler} according to the settings.
*
* @param ah
* The authentication handler which will be used for authorisation.
*
* @return An instance of {@link BaseAdminRestHandler} on success or {@code null} if the directory is not
* configured with a REST handler for tenant administration.
*/
public static SessionRestHandler initialize(WebAuthHandler ah)
{
if (instance == null)
{
instance = new SessionRestHandler(ah);
}
return instance;
}
/**
* Implement this method for handling the GET requests. The result should be written directly to
* {@code response}'s writer.
*
* @param target
* The relative target of the request. From the absolute URL, the context prefix was eliminated.
* @param body
* The payload of the request (optional).
* @param response
* The response as the {@link Response} object or a wrapper of that request.
* @param token
* The authentication token.
*/
@Override
protected void handleGet(String target, String body, HttpServletResponse response, String token)
{
if (target.indexOf('/') != -1)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
Runnable task;
switch (target)
{
case "": // /admin/sessions/ GET the list of all sessions
List<Integer> filter = null;
if (!body.isEmpty())
{
Map<String, Object> jsonMap = null;
try
{
jsonMap = new ObjectMapper().readValue(body, new TypeReference<Map<String, Object>>() {});
}
catch (JsonProcessingException e)
{
LOG.log(Level.WARNING, "Invalid request body in GET:/admin/sessions: " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (jsonMap.size() != 1 ||
!jsonMap.containsKey("sessions") ||
!(jsonMap.get("sessions") instanceof List))
{
LOG.log(Level.WARNING, "Unknown format in request body for GET:/admin/sessions.");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
filter = (List) jsonMap.get("sessions");
}
List<SessionInfo[]> sessions = new ArrayList<>(1);
task = () -> {
AdminServerImpl.setTargetLive(true, false, true);
sessions.add(AdminServerImpl.getSessionList());
};
wsm.executeInContext(token, task, true);
try
{
response.getWriter().write(sessionsToJSON(sessions.get(0), filter));
response.setStatus(HttpServletResponse.SC_OK);
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
return;
case "lock": // /admin/sessions/lock GET the [lock] status
try
{
response.getWriter().write("{\"lock\":" + sm.getSessionLock() + "}");
response.setStatus(HttpServletResponse.SC_OK);
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
return;
default: // / admin/sessions/{session-id} - GET a specific active session
int sessionID;
try
{
sessionID = Integer.parseInt(target);
}
catch (NumberFormatException e)
{
LOG.log(Level.WARNING, "The session ID is not a valid integer: " + target);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
SessionInfo[] session = new SessionInfo[1];
task = () -> {
AdminServerImpl.setTargetLive(true, false, true);
session[0] = AdminServerImpl.getSessionById(sessionID);
};
wsm.executeInContext(token, task, true);
try
{
response.getWriter().write(sessionsToJSON(session, null));
response.setStatus(HttpServletResponse.SC_OK);
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
}
}
/**
* Implement this method for handling the PUT requests. The result should be written directly to
* {@code response}'s writer.
*
* @param target
* The relative target of the request. From the absolute URL, the context prefix was eliminated.
* @param body
* The payload of the request (optional).
* @param response
* The response as the {@link Response} object or a wrapper of that request.
* @param token
* The authentication token.
*/
@Override
protected void handlePut(String target, String body, HttpServletResponse response, String token)
{
// /admin/sessions/lock
if (!"lock".equals(target))
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
Boolean lock = (Boolean) extractFirstLevelValue(body, "lock", Boolean.class);
if (lock == null)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
try
{
sm.setSessionLock(lock);
response.getWriter().write("{\"lock\":" + sm.getSessionLock() + "}");
response.setStatus(HttpServletResponse.SC_OK);
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
}
/**
* Implement this method for handling the DELETE requests. The result should be written directly to
* {@code response}'s writer.
*
* @param target
* The relative target of the request. From the absolute URL, the context prefix was eliminated.
* @param body
* The payload of the request (optional).
* @param response
* The response as the {@link Response} object or a wrapper of that request.
* @param token
* The authentication token.
*/
@Override
protected void handleDelete(String target, String body, HttpServletResponse response, String token)
{
if (target.indexOf('/') != -1)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (target.isEmpty()) // /admin/sessions - DELETE sessions specified in body or ALL if body is empty
{
if (body.isEmpty())
{
Runnable task;
boolean[] lockStatus = new boolean[1];
boolean oldLockStatus = sm.getSessionLock();
if (!oldLockStatus)
{
// try to lock the session launching first
task = () -> {
AdminServerImpl.setTargetLive(true, false, true);
lockStatus[0] = sm.setSessionLock(true);
};
wsm.executeInContext(token, task, true);
if (!lockStatus[0])
{
LOG.log(Level.WARNING, "Could not lock session launching!");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
}
try
{
List<SessionInfo[]> sessionList = new ArrayList<>();
task = () -> {
AdminServerImpl.setTargetLive(true, false, true);
sessionList.add(AdminServerImpl.getSessionList());
};
wsm.executeInContext(token, task, true);
if (sessionList.get(0) == null)
{
LOG.log(Level.WARNING, "Insufficient rights to retrieve sessions in " +
"DELETE:/admin/sessions/all");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
SessionInfo[] sessions = sessionList.get(0);
List<Integer> withSuccess = new ArrayList<>(sessions.length);
task = () -> {
AdminServerImpl.setTargetLive(true, false, true);
for (SessionInfo session : sessions)
{
if (handleSessionClose(session.id))
{
withSuccess.add(session.id);
}
}
};
wsm.executeInContext(token, task, true);
response.getWriter().write(createDeleteResponse(withSuccess));
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
finally
{
if (!oldLockStatus)
{
// now try to unlock the session launching
task = () -> {
AdminServerImpl.setTargetLive(true, false, true);
lockStatus[0] = sm.setSessionLock(false);
};
wsm.executeInContext(token, task, true);
if (lockStatus[0])
{
LOG.log(Level.WARNING, "Could not unlock session launching!");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
}
else
{
Map<String, Object> jsonMap;
try
{
jsonMap = new ObjectMapper().readValue(body, new TypeReference<Map<String, Object>>() {});
}
catch (JsonProcessingException e)
{
LOG.log(Level.WARNING, "Invalid request body in DELETE:/admin/sessions: " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (jsonMap == null ||
jsonMap.size() != 1 ||
!jsonMap.containsKey("sessions") ||
!(jsonMap.get("sessions") instanceof List))
{
LOG.log(Level.WARNING, "Unknown format in request body for DELETE:/admin/sessions.");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
List<Object> sessions = (List<Object>) jsonMap.get("sessions");
List<Integer> withSuccess = new ArrayList<>(sessions.size());
Runnable task = () ->
{
AdminServerImpl.setTargetLive(true, false, true);
for (Object session : sessions)
{
if (!(session instanceof Integer))
{
continue;
}
if (handleSessionClose((int) session))
{
withSuccess.add((int) session);
}
}
};
wsm.executeInContext(token, task, true);
try
{
response.getWriter().write(createDeleteResponse(withSuccess));
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
}
}
else
{
int sessionID;
try
{
sessionID = Integer.parseInt(target);
}
catch (NumberFormatException e)
{
LOG.log(Level.WARNING, "The session ID is not a valid integer: " + target);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
boolean[] ok = new boolean[1];
Runnable task = () -> {
AdminServerImpl.setTargetLive(true, false, true);
ok[0] = handleSessionClose(sessionID);
};
wsm.executeInContext(token, task, true);
try
{
response.getWriter().write(
createDeleteResponse(ok[0] ? Collections.singletonList(sessionID) : Collections.emptyList()));
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
/**
* List all the sessions from the given list in a JSON format.
*
* @param sessions
* The list with the sessions.
* @param filter
* Filter the response only to specified sessions.
*
* @return A string in JSON format with the sessions' information.
*/
private String sessionsToJSON(SessionInfo[] sessions, List<Integer> filter)
{
int numberOfSessions = sessions == null ? 0 : sessions.length;
if (numberOfSessions == 0 || sessions[0] == null)
{
return "{\n\"sessions\":[]\n}";
}
ObjectMapper objectMapper = mapper.get();
ObjectNode root = objectMapper.createObjectNode();
ArrayNode children = objectMapper.createArrayNode();
for (int i = 0; i < numberOfSessions; i++)
{
SessionInfo session = sessions[i];
if (filter == null || filter.contains(session.id))
{
children.add(createSessionNode(session));
}
}
root.set("sessions", children);
return root.toString();
}
/**
* Method which will create a custom representation of a session.
*
* @param session
* The session's info.
*
* @return The session-specific JSON node, or {@code null} if the session is null.
*/
private JsonNode createSessionNode(SessionInfo session)
{
if (session == null)
{
return null;
}
ZonedDateTime dateTime = Instant.ofEpochMilli(session.logonTime).atZone(ZoneId.systemDefault());
ObjectMapper objectMapper = mapper.get();
ArrayNode programTrace = objectMapper.createArrayNode();
AdminServerImpl.convertProgramTrace(session.stacktrace, programTrace);
ObjectNode sessionNode = objectMapper.createObjectNode();
sessionNode.put("logonTime", dateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
sessionNode.put("accountDescription", session.name);
sessionNode.put("description", session.descr);
sessionNode.put("osUser", session.osUser);
sessionNode.put("sessionDescription", session.sessionDescription);
sessionNode.put("sessionTokenID", session.id == -1 ? null : String.valueOf(session.id));
sessionNode.put("sessionUUID", session.uuid);
sessionNode.put("relatedSessionUUID", session.relatedSessionUuid);
sessionNode.putArray("programTrace").addAll(programTrace);
sessionNode.put("clientWebPort", session.httpPort == -1 ? "" : String.valueOf(session.httpPort));
sessionNode.put("clientPID", session.pid == -1 ? null : String.valueOf(session.pid));
sessionNode.put("browserSocket", session.browserWebSocket);
sessionNode.put("clientSocket", session.remoteNet);
sessionNode.put("serverSocket", session.localNet);
sessionNode.put("secure", session.secure);
sessionNode.put("cacheGeneration", session.cacheGen == -1 ? null : String.valueOf(session.cacheGen));
return sessionNode;
}
/**
* Creates a custom JSON string response which represents a list with all the
* sessions which were successfully terminated by a {@code DELETE} request.
*
* @param sessionID
* The list with the session IDs which were terminated.
*
* @return The custom {@code DELETE} response string.
*/
private String createDeleteResponse(List<Integer> sessionID)
{
ObjectMapper objectMapper = new ObjectMapper();
ArrayNode children = objectMapper.createArrayNode();
for (Integer integer : sessionID)
{
children.add(integer);
}
ObjectNode root = objectMapper.createObjectNode();
root.set("terminated", children);
return root.toString();
}
/**
* Handles the closing of a session by verifying if no error was thrown.
*
* @param sessionID
* The session which needs to be terminated.
*
* @return {@code true} if the session was closed successfully, {@code false} otherwise.
*
*/
private boolean handleSessionClose(int sessionID)
{
try
{
AdminServerImpl.terminateSession(sessionID);
return true;
}
catch (RuntimeException e)
{
LOG.log(Level.WARNING, "Could not terminate session " + sessionID + ": " + e.getMessage());
return false;
}
}
}