# 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;
          }
       }

    }