/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.internal.cache.tier.sockets;

import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_CLIENT_AUTHENTICATOR;
import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_CLIENT_AUTH_INIT;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;

import org.apache.geode.DataSerializer;
import org.apache.geode.LogWriter;
import org.apache.geode.annotations.VisibleForTesting;
import org.apache.geode.annotations.internal.MutableForTesting;
import org.apache.geode.cache.CacheClosedException;
import org.apache.geode.cache.client.PoolFactory;
import org.apache.geode.cache.client.ServerRefusedConnectionException;
import org.apache.geode.cache.client.internal.ClientSideHandshakeImpl;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.ClassLoadUtils;
import org.apache.geode.internal.HeapDataOutputStream;
import org.apache.geode.internal.cache.tier.ConnectionProxy;
import org.apache.geode.internal.logging.InternalLogWriter;
import org.apache.geode.internal.security.CallbackInstantiator;
import org.apache.geode.internal.security.Credentials;
import org.apache.geode.internal.security.SecurityService;
import org.apache.geode.internal.serialization.KnownVersion;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.security.AuthInitialize;
import org.apache.geode.security.AuthenticationExpiredException;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.AuthenticationRequiredException;
import org.apache.geode.security.Authenticator;
import org.apache.geode.security.GemFireSecurityException;

public abstract class Handshake {
  private static final Logger logger = LogService.getLogger();

  protected static final byte REPLY_OK = (byte) 59;

  protected static final byte REPLY_REFUSED = (byte) 60;

  protected static final byte REPLY_INVALID = (byte) 61;

  protected static final byte REPLY_EXCEPTION_AUTHENTICATION_REQUIRED = (byte) 62;

  protected static final byte REPLY_EXCEPTION_AUTHENTICATION_FAILED = (byte) 63;

  protected static final byte REPLY_EXCEPTION_DUPLICATE_DURABLE_CLIENT = (byte) 64;

  protected static final byte REPLY_WAN_CREDENTIALS = (byte) 65;

  protected static final byte REPLY_AUTH_NOT_REQUIRED = (byte) 66;

  public static final byte REPLY_SERVER_IS_LOCATOR = (byte) 67;
  /**
   * Test hook for client version support
   *
   * @since GemFire 5.7
   */
  @MutableForTesting
  protected static KnownVersion currentClientVersion = ConnectionProxy.VERSION;

  protected SecurityService securityService;

  /** This is used as part of our hash, so it must be a stable value */
  protected abstract byte getReplyCode();

  protected int clientReadTimeout = PoolFactory.DEFAULT_READ_TIMEOUT;

  protected DistributedSystem system;

  protected ClientProxyMembershipID id;

  protected Properties credentials;

  protected EncryptorImpl encryptor;

  // Security mode flags

  /** No credentials being sent */
  public static final byte CREDENTIALS_NONE = (byte) 0;

  /** Credentials being sent without encryption on the wire */
  public static final byte CREDENTIALS_NORMAL = (byte) 1;

  /** Credentials being sent with Diffie-Hellman key encryption */
  public static final byte CREDENTIALS_DHENCRYPT = (byte) 2;

  public static final byte SECURITY_MULTIUSER_NOTIFICATIONCHANNEL = (byte) 3;

  public static final String PUBLIC_KEY_FILE_PROP = "security-client-kspath";

  public static final String PUBLIC_KEY_PASSWD_PROP = "security-client-kspasswd";

  public static final String PRIVATE_KEY_FILE_PROP = "security-server-kspath";

  public static final String PRIVATE_KEY_ALIAS_PROP = "security-server-ksalias";

  public static final String PRIVATE_KEY_PASSWD_PROP = "security-server-kspasswd";

  /** @since GemFire 5.7 */
  public static final byte CONFLATION_DEFAULT = 0;
  /** @since GemFire 5.7 */
  public static final byte CONFLATION_ON = 1;
  /** @since GemFire 5.7 */
  public static final byte CONFLATION_OFF = 2;
  /** @since GemFire 5.7 */
  protected byte clientConflation = CONFLATION_DEFAULT;

  /**
   * @since GemFire 6.0.3 List of per client property override bits.
   */
  protected byte[] overrides;

  /**
   * Test hooks for per client conflation
   *
   * @since GemFire 5.7
   */
  public static final byte clientConflationForTesting = 0;
  public static final boolean setClientConflationForTesting = false;

  /** Constructor used for subclasses */
  protected Handshake() {}

  /**
   * Clone a HandShake to be used in creating other connections
   */
  protected Handshake(Handshake handshake) {
    clientConflation = handshake.clientConflation;
    clientReadTimeout = handshake.clientReadTimeout;
    credentials = handshake.credentials;
    overrides = handshake.overrides;
    system = handshake.system;
    id = handshake.id;
    securityService = handshake.securityService;
    encryptor = new EncryptorImpl(handshake.encryptor);
  }

  protected void setClientConflation(byte value) {
    clientConflation = value;
    switch (clientConflation) {
      case CONFLATION_DEFAULT:
      case CONFLATION_OFF:
      case CONFLATION_ON:
        break;
      default:
        throw new IllegalArgumentException("Illegal clientConflation");
    }
  }

  protected byte[] getOverrides() {
    return overrides;
  }

  protected void setOverrides(byte[] values) {
    byte override = values[0];
    setClientConflation(((byte) (override & 0x03)));
  }

  // used by CacheClientNotifier's handshake reading code
  public static byte[] extractOverrides(byte[] values) {
    byte override = values[0];
    byte[] overrides = new byte[1];
    for (int item = 0; item < overrides.length; item++) {
      overrides[item] = (byte) (override & 0x03);
      override = (byte) (override >>> 2);
    }
    return overrides;
  }

  /**
   * This method writes what readCredential() method expects to read. (Note the use of singular
   * credential). It is similar to writeCredentials(), except that it doesn't write
   * credential-properties.
   *
   * This is only used by the {@link ClientSideHandshakeImpl}.
   */
  protected byte writeCredential(DataOutputStream dos, DataInputStream dis, String authInit,
      boolean isNotification, DistributedMember member, HeapDataOutputStream heapdos)
      throws IOException, GemFireSecurityException {

    if (!encryptor.isEnabled()) {
      heapdos.writeByte(CREDENTIALS_NORMAL);
      encryptor.setAppSecureMode(CREDENTIALS_NORMAL);
      heapdos.flush();
      dos.write(heapdos.toByteArray());
      dos.flush();
      return -1;
    }
    byte acceptanceCode = -1;
    acceptanceCode = encryptor.writeEncryptedCredential(dos, dis, heapdos);
    if (acceptanceCode != REPLY_OK && acceptanceCode != REPLY_AUTH_NOT_REQUIRED) {
      // Ignore the useless data
      dis.readByte();
      dis.readInt();
      if (!isNotification) {
        DataSerializer.readByteArray(dis);
      }
      readMessage(dis, dos, acceptanceCode, member);
    }
    dos.flush();
    return acceptanceCode;
  }

  public void writeCredentials(DataOutputStream dos, DataInputStream dis, Properties p_credentials,
      boolean isNotification, DistributedMember member)
      throws IOException, GemFireSecurityException {
    HeapDataOutputStream hdos = new HeapDataOutputStream(32, KnownVersion.CURRENT);
    try {
      writeCredentials(dos, dis, p_credentials, isNotification, member, hdos);
    } finally {
      hdos.close();
    }
  }

  /**
   * This assumes that authentication is the last piece of info in handshake
   */
  public void writeCredentials(DataOutputStream dos, DataInputStream dis, Properties p_credentials,
      boolean isNotification, DistributedMember member, HeapDataOutputStream heapdos)
      throws IOException, GemFireSecurityException {

    if (p_credentials == null) {
      // No credentials indicator
      heapdos.writeByte(CREDENTIALS_NONE);
      heapdos.flush();
      dos.write(heapdos.toByteArray());
      dos.flush();
      return;
    }

    if (!encryptor.isEnabled()) {
      // Normal credentials without encryption indicator
      heapdos.writeByte(CREDENTIALS_NORMAL);
      DataSerializer.writeProperties(p_credentials, heapdos);
      heapdos.flush();
      dos.write(heapdos.toByteArray());
      dos.flush();
      return;
    }

    byte acceptanceCode = encryptor.writeEncryptedCredentials(dos, dis, p_credentials, heapdos);
    if (acceptanceCode != REPLY_OK && acceptanceCode != REPLY_AUTH_NOT_REQUIRED) {
      // Ignore the useless data
      dis.readByte();
      dis.readInt();
      if (!isNotification) {
        DataSerializer.readByteArray(dis);
      }
      readMessage(dis, dos, acceptanceCode, member);
    }
    dos.flush();
  }

  /**
   * Throws AuthenticationRequiredException if authentication is required but there are no
   * credentials.
   */
  static void throwIfMissingRequiredCredentials(boolean requireAuthentication,
      boolean hasCredentials) {
    if (requireAuthentication && !hasCredentials) {
      throw new AuthenticationRequiredException(
          "No security credentials are provided");
    }
  }

  // This assumes that authentication is the last piece of info in handshake
  Properties readCredential(DataInputStream dis, DataOutputStream dos, DistributedSystem system)
      throws GemFireSecurityException, IOException {

    Properties credentials = null;
    boolean requireAuthentication = securityService.isClientSecurityRequired();
    try {
      byte secureMode = dis.readByte();
      throwIfMissingRequiredCredentials(requireAuthentication, secureMode != CREDENTIALS_NONE);
      if (secureMode == CREDENTIALS_NORMAL) {
        encryptor.setAppSecureMode(CREDENTIALS_NORMAL);
      } else if (secureMode == CREDENTIALS_DHENCRYPT) {
        encryptor.readEncryptedCredentials(dis, dos, system, requireAuthentication);
      }
    } catch (IOException ex) {
      throw ex;
    } catch (GemFireSecurityException ex) {
      throw ex;
    } catch (Exception ex) {
      throw new AuthenticationFailedException(
          "Failure in reading credentials", ex);
    }
    return credentials;
  }


  protected void readMessage(DataInputStream dis, DataOutputStream dos, byte acceptanceCode,
      DistributedMember member) throws IOException, AuthenticationRequiredException,
      AuthenticationFailedException, ServerRefusedConnectionException {

    String message = dis.readUTF();
    if (message.length() == 0 && acceptanceCode != REPLY_WAN_CREDENTIALS) {
      return; // success
    }

    switch (acceptanceCode) {
      case REPLY_EXCEPTION_AUTHENTICATION_REQUIRED:
        throw new AuthenticationRequiredException(message);
      case REPLY_EXCEPTION_AUTHENTICATION_FAILED:
        throw new AuthenticationFailedException(message);
      case REPLY_EXCEPTION_DUPLICATE_DURABLE_CLIENT:
        throw new ServerRefusedConnectionException(member, message);
      case REPLY_WAN_CREDENTIALS:
        checkIfAuthenticWanSite(dis, dos, member);
        break;
      default:
        throw new ServerRefusedConnectionException(member, message);
    }
  }

  public boolean isOK() {
    return getReplyCode() == REPLY_OK;
  }

  public void setClientReadTimeout(int clientReadTimeout) {
    this.clientReadTimeout = clientReadTimeout;
  }

  public int getClientReadTimeout() {
    return clientReadTimeout;
  }

  /**
   * Indicates whether some other object is "equal to" this one.
   *
   * @param other the reference object with which to compare.
   * @return true if this object is the same as the obj argument; false otherwise.
   */
  @Override
  public boolean equals(Object other) {
    if (other == this) {
      return true;
    }
    if (!(other instanceof Handshake)) {
      return false;
    }
    final Handshake that = (Handshake) other;

    return id.isSameDSMember(that.id) && getReplyCode() == that.getReplyCode();
  }

  @Override
  public int hashCode() {
    final int mult = 37;

    int result = id.hashCode();
    result = mult * result + getReplyCode();

    return result;
  }

  @Override
  public String toString() {
    StringBuilder buf =
        new StringBuilder().append("HandShake@").append(System.identityHashCode(this))
            .append(" code: ").append(getReplyCode());
    if (id != null) {
      buf.append(" identity: ");
      buf.append(id);
    }
    return buf.toString();
  }

  public ClientProxyMembershipID getMembershipId() {
    return id;
  }

  public static Properties getCredentials(String authInitMethod, Properties securityProperties,
      DistributedMember server, boolean isPeer, LogWriter logWriter,
      LogWriter securityLogWriter) throws AuthenticationRequiredException {

    Properties credentials = null;
    // if no authInit, Try to extract the credentials directly from securityProps
    if (StringUtils.isBlank(authInitMethod)) {
      return Credentials.getCredentials(securityProperties);
    }

    // if authInit exists
    try {
      AuthInitialize auth =
          CallbackInstantiator.getObjectOfType(authInitMethod, AuthInitialize.class);
      auth.init(logWriter, securityLogWriter);
      try {
        credentials = auth.getCredentials(securityProperties, server, isPeer);
      } finally {
        auth.close();
      }
    } catch (GemFireSecurityException ex) {
      throw ex;
    } catch (Exception ex) {
      throw new AuthenticationRequiredException(
          String.format("Failed to acquire AuthInitialize method %s",
              authInitMethod),
          ex);
    }
    return credentials;
  }

  protected Properties getCredentials(DistributedMember member) {
    String authInitMethod = system.getProperties().getProperty(SECURITY_CLIENT_AUTH_INIT);
    return getCredentials(authInitMethod, system.getSecurityProperties(), member, false,
        system.getLogWriter(),
        system.getSecurityLogWriter());
  }

  /**
   * this static method is used by CacheClientNotifier in registerClient when creating a queue for a
   * client. This assumes that authentication is the last piece of info in handshake
   */
  public static Properties readCredentials(DataInputStream dis, DataOutputStream dos,
      DistributedSystem system, SecurityService securityService)
      throws GemFireSecurityException, IOException {

    boolean requireAuthentication = securityService.isClientSecurityRequired();
    Properties credentials = null;
    try {
      byte secureMode = dis.readByte();
      throwIfMissingRequiredCredentials(requireAuthentication, secureMode != CREDENTIALS_NONE);
      if (secureMode == CREDENTIALS_NORMAL) {
        if (requireAuthentication) {
          credentials = DataSerializer.readProperties(dis);
        } else {
          DataSerializer.readProperties(dis); // ignore the credentials
        }
      } else if (secureMode == CREDENTIALS_DHENCRYPT) {
        credentials = EncryptorImpl.getDecryptedCredentials(dis, dos, system, requireAuthentication,
            credentials);
      } else if (secureMode == SECURITY_MULTIUSER_NOTIFICATIONCHANNEL) {
        // hitesh there will be no credential CCP will get credential(Principal) using
        // ServerConnection..
        logger.debug("readCredential where multiuser mode creating callback connection");
      }
    } catch (IOException ex) {
      throw ex;
    } catch (GemFireSecurityException ex) {
      throw ex;
    } catch (Exception ex) {
      throw new AuthenticationFailedException(
          "Failure in reading credentials", ex);
    }
    return credentials;
  }

  /**
   * this could return either a Subject or a Principal depending on if it's integrated security or
   * not
   */
  public static Object verifyCredentials(String authenticatorMethod, Properties credentials,
      Properties securityProperties, InternalLogWriter logWriter,
      InternalLogWriter securityLogWriter, DistributedMember member,
      SecurityService securityService)
      throws AuthenticationRequiredException, AuthenticationFailedException, CacheClosedException {

    if (!AcceptorImpl.isAuthenticationRequired()) {
      return null;
    }
    return authenticate(authenticatorMethod, credentials, securityProperties, logWriter,
        securityLogWriter,
        member, securityService);
  }

  @VisibleForTesting
  static Object authenticate(String authenticatorMethod, Properties credentials,
      Properties securityProperties, InternalLogWriter logWriter,
      InternalLogWriter securityLogWriter, DistributedMember member,
      SecurityService securityService) {
    Authenticator auth = null;
    try {
      if (securityService.isIntegratedSecurity()) {
        return securityService.login(credentials);
      } else {
        Method instanceGetter = ClassLoadUtils.methodFromName(authenticatorMethod);
        auth = (Authenticator) instanceGetter.invoke(null, (Object[]) null);
        auth.init(securityProperties, logWriter, securityLogWriter);
        return auth.authenticate(credentials, member);
      }
    } catch (AuthenticationFailedException | AuthenticationExpiredException
        | CacheClosedException ex) {
      throw ex;
    } catch (Exception ex) {
      throw new AuthenticationFailedException(ex.getMessage(), ex);
    } finally {
      if (auth != null) {
        auth.close();
      }
    }
  }

  public Object verifyCredentials()
      throws AuthenticationRequiredException, AuthenticationFailedException {

    String methodName = system.getProperties().getProperty(SECURITY_CLIENT_AUTHENTICATOR);
    return verifyCredentials(methodName, credentials, system.getSecurityProperties(),
        (InternalLogWriter) system.getLogWriter(),
        (InternalLogWriter) system.getSecurityLogWriter(), id.getDistributedMember(),
        securityService);
  }

  private void checkIfAuthenticWanSite(DataInputStream dis, DataOutputStream dos,
      DistributedMember member) throws GemFireSecurityException, IOException {

    if (credentials == null) {
      return;
    }
    String authenticator = system.getProperties().getProperty(SECURITY_CLIENT_AUTHENTICATOR);
    Properties peerWanProps = readCredentials(dis, dos, system, securityService);
    verifyCredentials(authenticator, peerWanProps, system.getSecurityProperties(),
        (InternalLogWriter) system.getLogWriter(),
        (InternalLogWriter) system.getSecurityLogWriter(), member, securityService);
  }
}
