/***********************************************************
Copyright (C) 2025 VeriSign, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

http://www.verisign.com/nds/naming/namestore/techdocs.html
***********************************************************/
package com.verisign.epp.rootzone.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

import com.verisign.epp.codec.domain.EPPDomainCreateCmd;
import com.verisign.epp.codec.domain.EPPDomainInfoCmd;
import com.verisign.epp.codec.domain.EPPDomainInfoResp;
import com.verisign.epp.codec.domain.EPPDomainTransferCmd;
import com.verisign.epp.codec.domain.EPPDomainUpdateCmd;
import com.verisign.epp.codec.gen.EPPAuthInfo;
import com.verisign.epp.codec.gen.EPPCodec;
import com.verisign.epp.codec.gen.EPPCodecComponent;
import com.verisign.epp.codec.gen.EPPLoginCmd;
import com.verisign.epp.codec.gen.EPPMessage;
import com.verisign.epp.exception.EPPException;
import com.verisign.epp.pool.parser.EPPSchemaCachingParserPool;
import com.verisign.epp.pool.transformer.EPPTransformerPool;
import com.verisign.epp.util.EPPSendReceiveLogger;
import com.verisign.epp.util.EPPXMLByteArray;
import com.verisign.epp.util.EPPXMLStream;

/**
 * A concrete {@link EPPSendReceiveLogger} that logs the messages in secure form
 * to the com.verisign.epp.util.EPPXMLStream category. This logging
 * category is used for backward compatibility to logging configurations when
 * the {@link com.verisign.epp.util.EPPXMLStream} logged the packets directly.
 * Sensitive attributes of messages will be masked with the string "MASKED".
 */
public class EPPSecureSendReceiveLogger implements EPPSendReceiveLogger {

	/**
	 * String used for the attribute value of a masked attribute.
	 */
	private static final String ATTR_MASKED = "MASKED";

	/** Category for logging */
	private static Logger cat = LoggerFactory.getLogger(EPPSecureSendReceiveLogger.class);

	private static Logger packetCat = LoggerFactory.getLogger(EPPXMLStream.class.getName() + ".packet");

	/**
	 * Regular expression used in {@link #maskString(String) to make pwType and
	 * pwAuthInfoType XML parser errors.
	 */
	static Pattern xmlTypeErrorPattern = Pattern.compile("^(.*Value ').+(' with .*type ')(pwType|pwAuthInfoType)('.*)$");

	/**
	 * Log the raw sending of a message. No filtering or alterations are done to
	 * the attributes of the message.
	 * 
	 * @param aPacket
	 *           Packet to send. This may be <code>null</code> if the packet has
	 *           not been encoded yet.
	 * @param aMessage
	 *           The message that is being sent, which could be any concrete
	 *           <code>EPPMessage</code>, including a
	 *           {@link com.verisign.epp.codec.gen.EPPCommand}, an
	 *           {@link com.verisign.epp.codec.gen.EPPGreeting}, an
	 *           {@link com.verisign.epp.codec.gen.EPPHello}, or an
	 *           {@link com.verisign.epp.codec.gen.EPPResponse}. This may be
	 *           <code>null</code> if the <code>EPPMessage</code> is not
	 *           available.
	 */
	@Override
	public void logSend(byte[] aPacket, final EPPMessage aMessage) {
		cat.debug("logSend(byte[], EPPMessage): enter");

		EPPMessage theMessage = (EPPMessage) this.maskMessage(aMessage);

		if (theMessage == aMessage) {
			this.log("writePacket() : Sending [", aPacket, theMessage);
		}
		else {
			// Must encode log from theMessage
			this.log("writePacket() : Sending [", null, theMessage);
		}

		cat.debug("logSend(byte[], EPPMessage): exit");
	}

	/**
	 * Log the raw receiving of a message. No filtering or alterations are done
	 * to the attributes of the message.
	 * 
	 * @param aPacket
	 *           Packet received. This may be <code>null</code> if the packet is
	 *           not available.
	 * @param aMessage
	 *           The message received, which could be any concrete
	 *           <code>EPPMessage</code>, including a
	 *           {@link com.verisign.epp.codec.gen.EPPCommand}, an
	 *           {@link com.verisign.epp.codec.gen.EPPGreeting}, an
	 *           {@link com.verisign.epp.codec.gen.EPPHello}, or an
	 *           {@link com.verisign.epp.codec.gen.EPPResponse}. This may be
	 *           <code>null</code> if the <code>EPPMessage</code> is not
	 *           available.
	 */
	@Override
	public void logReceive(byte[] aPacket, final EPPMessage aMessage) {
		cat.debug("logReceive(byte[], EPPMessage): enter");

		EPPMessage theMessage = (EPPMessage) this.maskMessage(aMessage);

		if (theMessage == aMessage) {
			this.log("decodePacket() : Received [", aPacket, theMessage);
		}
		else {
			// Must encode log from theMessage
			this.log("decodePacket() : Received [", null, theMessage);
		}

		cat.debug("logReceive(byte[], EPPMessage): exit");
	}

	/**
	 * Mask a general string of sensitive information. Specific string regular
	 * expressions are checked and matching strings are masked by this method for
	 * inclusion in exceptions and logs.
	 * 
	 * @param aString
	 *           <code>String</code> to scan for masking
	 * 
	 * @return Masked <code>String</code>.
	 */
	@Override
	public String maskString(String aString) {
		cat.debug("maskString(String): enter");
		if (aString == null) {
			return aString;
		}

		String maskedString = aString;

		// Mask pwType or pwAuthInfoType value XML errors
		Matcher theMatcher = xmlTypeErrorPattern.matcher(aString);
		if (theMatcher.matches()) {
			maskedString = theMatcher.group(1) + ATTR_MASKED + theMatcher.group(2) + theMatcher.group(3)
			      + theMatcher.group(4);
		}

		cat.debug("maskString(String): exit");
		return maskedString;
	}

	/**
	 * Mask the message by cloning the message and by masking specific message
	 * attributes.
	 * 
	 * @param aMessage
	 *           Message to mask. If <code>null</code>, <code>null</code> will be
	 *           returned.
	 * @return Masked message if instance is different from <code>aMessage</code>
	 *         . <code>aMessage</code> is returned if no filtering was done.
	 */
	@Override
	public EPPCodecComponent maskMessage(final EPPCodecComponent aMessage) {
		cat.debug("maskMessage(EPPCodecComponent): enter");

		if (aMessage == null) {
			return aMessage;
		}

		EPPMessage theMessage = null;

		// Message that needs to be filtered?
		if ((aMessage instanceof EPPLoginCmd) || (aMessage instanceof EPPDomainCreateCmd)
		      || (aMessage instanceof EPPDomainUpdateCmd) || (aMessage instanceof EPPDomainTransferCmd)
		      || (aMessage instanceof EPPDomainInfoCmd) || (aMessage instanceof EPPDomainInfoResp)) {
			// Clone message
			try {
				theMessage = (EPPMessage) aMessage.clone();
			}
			catch (CloneNotSupportedException ex) {
				cat.error("Exception cloning " + aMessage.getClass().getName() + ": " + ex);
				return aMessage;
			}

		}
		else {
			cat.debug("maskMessage(EPPCodecComponent): exit - no masking needed");
			return aMessage;
		}

		// ------------------------------
		// Login
		// ------------------------------

		// Login Command?
		if (theMessage instanceof EPPLoginCmd) {

			cat.debug(EPPLoginCmd.class.getName() + " attributes masked");

			EPPLoginCmd theLoginCmd = (EPPLoginCmd) theMessage;

			// Mask the login password
			theLoginCmd.setPassword(ATTR_MASKED);

			// Mask the new login password
			if (theLoginCmd.hasNewPassword()) {
				theLoginCmd.setNewPassword(ATTR_MASKED);
			}

			// Login Security Extension v1_0
			if (theLoginCmd.hasExtension(com.verisign.epp.codec.loginsec.v1_0.EPPLoginSec.class)) {
				cat.debug("com.verisign.epp.codec.loginsec.v1_0.EPPLoginSec attributes masked");

				com.verisign.epp.codec.loginsec.v1_0.EPPLoginSec theExt = (com.verisign.epp.codec.loginsec.v1_0.EPPLoginSec) theLoginCmd
				      .getExtension(com.verisign.epp.codec.loginsec.v1_0.EPPLoginSec.class);

				// Mask the login password
				if (theExt.hasPassword()) {
					theExt.setPassword(ATTR_MASKED);
				}

				// Mask the new login password
				if (theExt.hasNewPassword()) {
					theExt.setNewPassword(ATTR_MASKED);
				}

			}

		}

		// ------------------------------
		// Domain commands / responses
		// ------------------------------

		// Domain Create Command
		if (theMessage instanceof EPPDomainCreateCmd) {
			cat.debug(EPPDomainCreateCmd.class.getName() + " attributes masked");

			EPPDomainCreateCmd theDomainCreateCmd = (EPPDomainCreateCmd) theMessage;

			// Mask auth info
			theDomainCreateCmd.setAuthInfo(new EPPAuthInfo(ATTR_MASKED));
		}

		// Domain Update Command
		if (theMessage instanceof EPPDomainUpdateCmd) {
			cat.debug(EPPDomainUpdateCmd.class.getName() + " attributes masked");

			EPPDomainUpdateCmd theDomainUpdateCmd = (EPPDomainUpdateCmd) theMessage;

			// Mask auth info
			if (theDomainUpdateCmd.getChange() != null && theDomainUpdateCmd.getChange().getAuthInfo() != null) {
				theDomainUpdateCmd.getChange().setAuthInfo(new EPPAuthInfo(ATTR_MASKED));
			}

		}

		// Domain Transfer Command
		if (theMessage instanceof EPPDomainTransferCmd) {
			cat.debug(EPPDomainTransferCmd.class.getName() + " attributes masked");

			EPPDomainTransferCmd theDomainTransferCmd = (EPPDomainTransferCmd) theMessage;

			// Mask auth info
			if (theDomainTransferCmd.getAuthInfo() != null) {
				theDomainTransferCmd.setAuthInfo(new EPPAuthInfo(ATTR_MASKED));
			}

		}

		// Domain Info Command
		if (theMessage instanceof EPPDomainInfoCmd) {
			cat.debug(EPPDomainInfoCmd.class.getName() + " attributes masked");

			// Mask auth info
			((EPPDomainInfoCmd) theMessage).setAuthInfo(new EPPAuthInfo(ATTR_MASKED));
		}

		// Domain Info Response
		if (theMessage instanceof EPPDomainInfoResp) {
			cat.debug(EPPDomainInfoResp.class.getName() + " attributes masked");

			// Mask auth info
			((EPPDomainInfoResp) theMessage).setAuthInfo(new EPPAuthInfo(ATTR_MASKED));
		}


		cat.debug("maskMessage(EPPCodecComponent): exit - message masked");
		return theMessage;

	}

	/**
	 * Writes the log given a log prefix set by either
	 * {@link #logReceive(byte[], EPPMessage)} or
	 * {@link #logSend(byte[], EPPMessage)}, the packet, and the message object.
	 * A non-<code>null</code> packet or message must be passed. It's most
	 * efficient to pass the packet over the message object, since the message
	 * object needed to be encoded into a packet.
	 * 
	 * @param logPrefix
	 *           Prefix to start the packet log with.
	 * @param aPacket
	 *           Packet to log if available; <code>null</code> otherwise.
	 * @param aMessage
	 *           Message object to log if available; <code>null</code> otherwise.
	 */
	private void log(String logPrefix, byte[] aPacket, final EPPMessage aMessage) {
		cat.debug("log(byte[], EPPMessage): enter");

		// If debug logging is not enabled, immediately return.
		if (!packetCat.isDebugEnabled())
			return;

		// Packet provided?
		if (aPacket != null) {
			packetCat.debug(logPrefix + new String(aPacket) + "]");
		}
		// Message provided?
		else if (aMessage != null) {

			try {
				Document theDoc = EPPCodec.getInstance().encode(aMessage);

				EPPXMLByteArray theByteArray = new EPPXMLByteArray(EPPSchemaCachingParserPool.getInstance().getPool(),
				      EPPTransformerPool.getInstance().getPool());
				byte[] thePacket = theByteArray.encode(theDoc);

				packetCat.debug(logPrefix + new String(thePacket) + "]");
			}
			catch (EPPException ex) {
				cat.error("log(byte[], EPPMessage); Error encoding message: " + ex);
				return;
			}

		}
		// Neither packet or message provided
		else {
			cat.error("log(byte[], EPPMessage); Both aPacket and aMessage are null");
		}

		cat.debug("log(byte[], EPPMessage): exit");
	}

}
