# SessionForkManager.java

    /*
     ** Module   : SessionForkManager.java
     ** Abstract : Manager for forking sessions in web
     **
     ** Copyright (c) 2023-2024, Golden Code Development Corporation.
     **
     ** -#- -I- --Date-- ----------------------------Description-----------------------------
     ** 001 GBB 20231108 Initial setup
     ** 002 GBB 20240229 Adding logs for generated and expired tokens. TOKEN_CONFIGS_PAIRS thread-safe.
     ** 003 GBB 20240516 Saving query params when building new forked session url.
     */
    /*
     ** 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.util;

    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.util.logging.*;
    import org.eclipse.jetty.util.*;

    import java.nio.charset.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.*;
    import java.util.logging.*;

    /** Manager for forking sessions in web */
    public class SessionForkManager
    {
       /** Initialization on demand idiom singleton holder */
       private static class SingletonHolder
       {
          /** Singleton instance */
          private static final SessionForkManager INSTANCE = new SessionForkManager();
       }

       /**
        * Static method for access to the singleton instance of the manager
        *
        * @return  The singleton instance of the manager
        */
       public static SessionForkManager getInstance()
       {
          return SingletonHolder.INSTANCE;
       }

       /** The search / path parameter name of the one time token */
       public static final String PARAM_NAME_TOKEN = "fst";

       /** Logger. */
       public static final CentralLogger LOG = CentralLogger.get(SessionForkManager.class);

       /** Counter for tokens. */
       private static final AtomicInteger TOKEN_COUNTER = new AtomicInteger(0);

       /** Lifespan of tokens in seconds. Should be short to prevent session stealing */
       private static final int TOKEN_LIFESPAN_SEC = 30;

       /** Executor Service for scheduled cleanup of expired tokens */
       private static final ScheduledExecutorService EXECUTOR_SERVICE =
          Executors.newSingleThreadScheduledExecutor();

       /** Map of generated tokens and corresponding configs */
       private static final Map<String, ForkedSessionConfigs> TOKEN_CONFIGS_PAIRS = new ConcurrentHashMap<>();

       /**
        * Collects relevant client configs for replicating the original session, generates a one-time token and
        * opens the login page address in browser, passing in the token as parameter.
        *
        * @param    cfgOverrides
        *           Client build options
        * @param    authBlob
        *           Authentication data
        */
       public void forkNewSession(String cfgOverrides, String authBlob)
       {
          ClientExports clientExports = LogicalTerminal.getClient();

          String loginPageAddress = clientExports.getLoginPageAddress();

          ForkedSessionConfigs configs = new ForkedSessionConfigs();
          configs.relatedSessionId = SessionUtils.getRelatedSessionId().toStringMessage();
          configs.cfgOverrides = cfgOverrides == null ? null : Arrays.asList(cfgOverrides.split(" "));
          configs.authBlob = authBlob;
          configs.fwdUser = com.goldencode.p2j.security.SecurityManager.getInstance().getUserId();
          configs.osUser = clientExports.getUserName();
          configs.storageId = clientExports.getStorageId();

          String oneTimeToken = UUID.randomUUID().toString();
          TOKEN_CONFIGS_PAIRS.put(oneTimeToken, configs);

          int tokenSequentialNumber = TOKEN_COUNTER.incrementAndGet();
          LOG.log(Level.FINE, "Token #%s generated.", tokenSequentialNumber);

          EXECUTOR_SERVICE.schedule(() -> {
             ForkedSessionConfigs removedConfig = TOKEN_CONFIGS_PAIRS.remove(oneTimeToken);
             if (removedConfig != null)
             {
                LOG.log(Level.FINE, "Token #%s expired.", tokenSequentialNumber);
             }
          }, TOKEN_LIFESPAN_SEC, TimeUnit.SECONDS);

          String newSessionAddress = loginPageAddress + "?" + PARAM_NAME_TOKEN + "=" + oneTimeToken;

          Map<String, String[]> loginParams = SessionUtils.getLoginParams();
          if (loginParams != null && !loginParams.isEmpty())
          {
             MultiMap<String> queryParams = new MultiMap<>();
             for (String paramName : loginParams.keySet())
             {
                queryParams.addValues(paramName, loginParams.get(paramName));
             }
             String encodedQueryString = UrlEncoded.encode(queryParams, StandardCharsets.UTF_8, false);
             newSessionAddress += "&" + encodedQueryString;
          }

          WebBrowserManager.openURL(newSessionAddress);
       }

       /**
        * Returns the configs corresponding to the token.
        *
        * @param   token
        *          The one time token
        *
        * @return  <code>null</code> if token has expired, otherwise {@link ForkedSessionConfigs}.
        */
       public ForkedSessionConfigs getConfigsForToken(String token)
       {
          return TOKEN_CONFIGS_PAIRS.remove(token);
       }

       /** Class for client configs necessary for replicating the original session */
       public static class ForkedSessionConfigs
       {
          /** The original session ID */
          private String relatedSessionId;

          /** Options for the new client */
          private List<String> cfgOverrides;

          /** Authentication data to be passed in to 4GL */
          private String authBlob;

          /** The FWD user */
          private String fwdUser;

          /** The OS user */
          private String osUser;

          /** The unique id for the client storage. <code>null</code> if SSO not enabled. */
          private String storageId;

          /**
           * Returns {@link #relatedSessionId}.
           *
           * @return See above.
           */
          public String getRelatedSessionId()
          {
             return relatedSessionId;
          }

          /**
           * Returns {@link #cfgOverrides}.
           *
           * @return See above.
           */
          public List<String> getCfgOverrides()
          {
             return cfgOverrides;
          }

          /**
           * Returns {@link #authBlob}.
           *
           * @return See above.
           */
          public String getAuthBlob()
          {
             return authBlob;
          }

          /**
           * Returns {@link #fwdUser}.
           *
           * @return See above.
           */
          public String getFwdUser()
          {
             return fwdUser;
          }

          /**
           * Returns {@link #osUser}.
           *
           * @return See above.
           */
          public String getOsUser()
          {
             return osUser;
          }

          /**
           * Returns {@link #storageId}.
           *
           * @return See above.
           */
          public String getStorageId()
          {
             return storageId;
          }
       }
    }