/**
 *  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.kerby.kerberos.kerb.server.request;

import org.apache.kerby.kerberos.kerb.KrbCodec;
import org.apache.kerby.kerberos.kerb.KrbConstant;
import org.apache.kerby.kerberos.kerb.KrbErrorCode;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.common.EncryptionUtil;
import org.apache.kerby.kerberos.kerb.common.KrbUtil;
import org.apache.kerby.kerberos.kerb.crypto.CheckSumHandler;
import org.apache.kerby.kerberos.kerb.crypto.EncryptionHandler;
import org.apache.kerby.kerberos.kerb.crypto.fast.FastUtil;
import org.apache.kerby.kerberos.kerb.request.KrbIdentity;
import org.apache.kerby.kerberos.kerb.server.KdcContext;
import org.apache.kerby.kerberos.kerb.server.KdcRecoverableException;
import org.apache.kerby.kerberos.kerb.server.preauth.KdcFastContext;
import org.apache.kerby.kerberos.kerb.server.preauth.PreauthContext;
import org.apache.kerby.kerberos.kerb.server.preauth.PreauthHandler;
import org.apache.kerby.kerberos.kerb.type.ap.ApReq;
import org.apache.kerby.kerberos.kerb.type.ap.Authenticator;
import org.apache.kerby.kerberos.kerb.type.base.AuthToken;
import org.apache.kerby.kerberos.kerb.type.base.CheckSum;
import org.apache.kerby.kerberos.kerb.type.base.EncryptedData;
import org.apache.kerby.kerberos.kerb.type.base.EncryptionKey;
import org.apache.kerby.kerberos.kerb.type.base.EncryptionType;
import org.apache.kerby.kerberos.kerb.type.base.EtypeInfo;
import org.apache.kerby.kerberos.kerb.type.base.EtypeInfo2;
import org.apache.kerby.kerberos.kerb.type.base.EtypeInfo2Entry;
import org.apache.kerby.kerberos.kerb.type.base.EtypeInfoEntry;
import org.apache.kerby.kerberos.kerb.type.base.KeyUsage;
import org.apache.kerby.kerberos.kerb.type.base.KrbError;
import org.apache.kerby.kerberos.kerb.type.base.KrbMessage;
import org.apache.kerby.kerberos.kerb.type.base.MethodData;
import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
import org.apache.kerby.kerberos.kerb.type.fast.ArmorType;
import org.apache.kerby.kerberos.kerb.type.fast.KrbFastArmor;
import org.apache.kerby.kerberos.kerb.type.fast.KrbFastArmoredReq;
import org.apache.kerby.kerberos.kerb.type.fast.KrbFastReq;
import org.apache.kerby.kerberos.kerb.type.fast.PaFxFastRequest;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcOption;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcOptions;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcRep;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcReq;
import org.apache.kerby.kerberos.kerb.type.pa.PaData;
import org.apache.kerby.kerberos.kerb.type.pa.PaDataEntry;
import org.apache.kerby.kerberos.kerb.type.pa.PaDataType;
import org.apache.kerby.kerberos.kerb.type.ticket.EncTicketPart;
import org.apache.kerby.kerberos.kerb.type.ticket.Ticket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.List;

public abstract class KdcRequest {

    private static final Logger LOG = LoggerFactory.getLogger(KdcRequest.class);
    private final KdcReq kdcReq;
    private final KdcContext kdcContext;

    private Ticket ticket;
    private boolean isPreAuthenticated;
    private KdcRep reply;
    private InetAddress clientAddress;
    private boolean isTcp = true;
    private EncryptionType encryptionType;
    private EncryptionKey clientKey;
    private KrbIdentity clientEntry;
    private KrbIdentity serverEntry;
    private EncryptionKey serverKey;
    private KrbIdentity tgsEntry;
    private PreauthContext preauthContext;
    private KdcFastContext fastContext;
    private PrincipalName clientPrincipal;
    private PrincipalName serverPrincipal;
    private byte[] innerBodyout;
    private AuthToken token;
    private boolean isToken = false;
    private boolean isPkinit = false;
    private boolean isAnonymous = false;
    private EncryptionKey sessionKey;
    private ByteBuffer reqPackage;
    private boolean isHttps = false;
    private boolean isCrossRealm = false;
    private String remoteRealm = null;

    /**
     * Get session key.
     *
     * @return session key
     */
    public EncryptionKey getSessionKey() {
        return sessionKey;
    }

    /**
     * Set session key.
     * @param sessionKey The session key
     */
    public void setSessionKey(EncryptionKey sessionKey) {
        this.sessionKey = sessionKey;
    }

    /**
     * kdc request constructor
     * @param kdcReq kdc request
     * @param kdcContext kdc context
     */
    public KdcRequest(KdcReq kdcReq, KdcContext kdcContext) {
        this.kdcReq = kdcReq;
        this.kdcContext = kdcContext;

        this.preauthContext = kdcContext.getPreauthHandler()
                .preparePreauthContext(this);
        this.fastContext = new KdcFastContext();
    }

    /**
     * Get kdc context.
     *
     * @return kdc context
     */
    public KdcContext getKdcContext() {
        return kdcContext;
    }

    /**
     * Get KdcReq.
     *
     * @return kdc request
     */
    public KdcReq getKdcReq() {
        return kdcReq;
    }

    /**
     * Get preauth context.
     *
     * @return preauthentication context.
     */
    public PreauthContext getPreauthContext() {
        return preauthContext;
    }

    /**
     * Process the kdcrequest from client and issue the ticket.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e.
     */
    public void process() throws KrbException {
        checkVersion();
        checkTgsEntry();
        if (isPreauthRequired()) {
            kdcFindFast();
        }
        checkEncryptionType();

        if (PreauthHandler.isToken(getKdcReq().getPaData())) {
            isToken = true;
            if (isPreauthRequired()) {
                preauth();
            }
            checkClient();
            checkServer();
        } else {
            if (PreauthHandler.isPkinit(getKdcReq().getPaData())) {
                isPkinit = true;
            }
            checkClient();
            checkServer();
            if (isPreauthRequired()) {
                preauth();
            }
        }
        checkPolicy();
        issueTicket();
        makeReply();
    }

    /**
     * Check the tgs entry.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e.
     */
    private void checkTgsEntry() throws KrbException {
        KrbIdentity tgsEntry = getEntry(getTgsPrincipal().getName());
        setTgsEntry(tgsEntry);
    }

    /**
     * Find the fast from padata.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    private void kdcFindFast() throws KrbException {

        PaData paData = getKdcReq().getPaData();
        if (paData != null) {
            for (PaDataEntry paEntry : paData.getElements()) {
                if (paEntry.getPaDataType() == PaDataType.FX_FAST) {
                    LOG.info("Found fast padata and starting to process it.");

                    PaFxFastRequest paFxFastRequest = null;
                    try {
                        paFxFastRequest = KrbCodec.decode(paEntry.getPaDataValue(), PaFxFastRequest.class);
                    } catch (KrbException e) {
                        String errMessage = "Decode PaFxFastRequest failed. " + e.getMessage();
                        LOG.error(errMessage);
                        throw new KrbException(errMessage);
                    }
                    KrbFastArmoredReq fastArmoredReq = paFxFastRequest.getFastArmoredReq();
                    if (fastArmoredReq == null) {
                        return;
                    }

                    KrbFastArmor fastArmor = fastArmoredReq.getArmor();
                    if (fastArmor == null) {
                        return;
                    }

                    try {
                        armorApRequest(fastArmor);
                    } catch (KrbException e) {
                        String errMessage = "Get armor key failed. " + e.getMessage();
                        LOG.error(errMessage);
                        throw new KrbException(errMessage);
                    }

                    if (getArmorKey() == null) {
                        return;
                    }
                    EncryptedData encryptedData = fastArmoredReq.getEncryptedFastReq();
                    KrbFastReq fastReq;
                    try {
                        fastReq = KrbCodec.decode(
                            EncryptionHandler.decrypt(encryptedData, getArmorKey(), KeyUsage.FAST_ENC),
                            KrbFastReq.class);
                    } catch (KrbException e) {
                        String errMessage = "Decode KrbFastReq failed. " + e.getMessage();
                        LOG.error(errMessage);
                        throw new KrbException(errMessage);
                    }
                    try {
                        innerBodyout = KrbCodec.encode(fastReq.getKdcReqBody());
                    } catch (KrbException e) {
                        String errMessage = "Encode KdcReqBody failed. " + e.getMessage();
                        LOG.error(errMessage);
                        throw new KrbException(errMessage);
                    }


                    // TODO: get checksumed data in stream
                    CheckSum checkSum = fastArmoredReq.getReqChecksum();
                    if (checkSum == null) {
                        LOG.warn("Checksum is empty.");
                        throw new KrbException(KrbErrorCode.KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED);
                    }

                    byte[] reqBody;
                    try {
                        reqBody = KrbCodec.encode(getKdcReq().getReqBody());
                    } catch (KrbException e) {
                        String errMessage = "Encode the ReqBody failed. " + e.getMessage();
                        LOG.error(errMessage);
                        throw new KrbException(errMessage);
                    }
                    boolean success;
                    try {
                        success = CheckSumHandler.verifyWithKey(checkSum, reqBody,
                            getArmorKey().getKeyData(), KeyUsage.FAST_REQ_CHKSUM);
                    } catch (KrbException e) {
                        String errMessage = "Verify the KdcReqBody failed. " + e.getMessage();
                        LOG.error(errMessage);
                        throw new KrbException(errMessage);
                    }
                    if (!success) {
                        LOG.error("Verify the KdcReqBody failed.");
                        throw new KrbException("Verify the KdcReqBody failed. ");
                    }
                }
            }
        }
    }

    /**
     * Get the armor key.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     * @param fastArmor krb fast armor
     */
    private void armorApRequest(KrbFastArmor fastArmor) throws KrbException {
        if (fastArmor.getArmorType() == ArmorType.ARMOR_AP_REQUEST) {
            ApReq apReq;
            try {
                apReq = KrbCodec.decode(fastArmor.getArmorValue(), ApReq.class);
            } catch (KrbException e) {
                String errMessage = "Decode ApReq failed. " + e.getMessage();
                LOG.error(errMessage);
                throw new KrbException(errMessage);
            }

            Ticket ticket = apReq.getTicket();
            EncryptionType encType = ticket.getEncryptedEncPart().getEType();
            EncryptionKey tgsKey = getTgsEntry().getKeys().get(encType);
            if (ticket.getTktvno() != KrbConstant.KRB_V5) {
                LOG.error(KrbErrorCode.KRB_AP_ERR_BADVERSION.getMessage());
                throw new KrbException(KrbErrorCode.KRB_AP_ERR_BADVERSION);
            }

            EncTicketPart encPart = null;
            try {
                encPart = EncryptionUtil.unseal(ticket.getEncryptedEncPart(),
                    tgsKey, KeyUsage.KDC_REP_TICKET, EncTicketPart.class);
            } catch (KrbException e) {
                String errMessage = "Unseal EncTicketPart failed. " + e.getMessage();
                LOG.error(errMessage);
                throw new KrbException(errMessage);
            }
            ticket.setEncPart(encPart);

            EncryptionKey encKey = ticket.getEncPart().getKey();
            setSessionKey(encKey);

            Authenticator authenticator = null;
            try {
                authenticator = EncryptionUtil.unseal(apReq.getEncryptedAuthenticator(),
                    encKey, KeyUsage.AP_REQ_AUTH, Authenticator.class);
            } catch (KrbException e) {
                String errMessage = "Unseal Authenticator failed. " + e.getMessage();
                LOG.error(errMessage);
                throw new KrbException(errMessage);
            }

            EncryptionKey armorKey = null;
            try {
                armorKey = FastUtil.cf2(authenticator.getSubKey(), "subkeyarmor",
                    encKey, "ticketarmor");
            } catch (KrbException e) {
                String errMessage = "Create armor key failed. " + e.getMessage();
                LOG.error(errMessage);
                throw new KrbException(errMessage);
            }

            setArmorKey(armorKey);
        }
    }

    /**
     * Get tgs entry.
     *
     * @return TGS entry
     */
    public KrbIdentity getTgsEntry() {
        return tgsEntry;
    }

    /**
     * Set tgs entry .
     *
     * @param tgsEntry TGS entry
     */
    public void setTgsEntry(KrbIdentity tgsEntry) {
        this.tgsEntry = tgsEntry;
    }

    /**
     * Get whether is tcp.
     *
     * @return whether is tcp
     */
    public boolean isTcp() {
        return isTcp;
    }

    /**
     * Set use tcp.
     *
     * @param isTcp set kdc request though TCP protocol or not
     */
    public void isTcp(boolean isTcp) {
        this.isTcp = isTcp;
    }

    /**
     * Get the reply message.
     *
     * @return reply
     */
    public KrbMessage getReply() {
        return reply;
    }

    /**
     * Set kdc reply.
     *
     * @param reply reply
     */
    public void setReply(KdcRep reply) {
        this.reply = reply;
    }

    /**
     * Get client address.
     *
     * @return client address
     */
    public InetAddress getClientAddress() {
        return clientAddress;
    }

    /**
     * Set client address.
     *
     * @param clientAddress client address
     */
    public void setClientAddress(InetAddress clientAddress) {
        this.clientAddress = clientAddress;
    }

    /**
     * Get encryption type.
     *
     * @return encryption type
     */
    public EncryptionType getEncryptionType() {
        return encryptionType;
    }

    /**
     * Set encryption type.
     *
     * @param encryptionType encryption type
     */
    public void setEncryptionType(EncryptionType encryptionType) {
        this.encryptionType = encryptionType;
    }

    /**
     * Get ticket.
     *
     * @return ticket
     */
    public Ticket getTicket() {
        return ticket;
    }

    /**
     * Set ticket.
     *
     * @param ticket ticket
     */
    public void setTicket(Ticket ticket) {
        this.ticket = ticket;
    }

    /**
     * Get whether pre-authenticated.
     *
     * @return whether preauthenticated
     */
    public boolean isPreAuthenticated() {
        return isPreAuthenticated;
    }

    /**
     * Set whether pre-authenticated.
     *
     * @param isPreAuthenticated whether is preauthenticated
     */
    public void setPreAuthenticated(boolean isPreAuthenticated) {
        this.isPreAuthenticated = isPreAuthenticated;
    }

    /**
     * Get server entry.
     *
     * @return server entry
     */
    public KrbIdentity getServerEntry() {
        return serverEntry;
    }

    /**
     * Set server entry.
     *
     * @param serverEntry server entry
     */
    public void setServerEntry(KrbIdentity serverEntry) {
        this.serverEntry = serverEntry;
    }

    /**
     * Get client entry.
     *
     * @return client entry
     */
    public KrbIdentity getClientEntry() {
        return clientEntry;
    }

    /**
     * Set client entry.
     *
     * @param clientEntry client entry
     */
    public void setClientEntry(KrbIdentity clientEntry) {
        this.clientEntry = clientEntry;
    }

    /**
     * Get client key with entryption type.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     * @param encType encryption type
     * @return encryption key
     */
    public EncryptionKey getClientKey(EncryptionType encType) throws KrbException {
        return getClientEntry().getKey(encType);
    }

    /**
     * Get client key.
     *
     * @return client key
     */
    public EncryptionKey getClientKey() {
        return clientKey;
    }

    /**
     * Set client key.
     *
     * @param clientKey client key
     */
    public void setClientKey(EncryptionKey clientKey) {
        this.clientKey = clientKey;
    }

    /**
     * Get server key.
     * @return The server key
     */
    public EncryptionKey getServerKey() {
        return serverKey;
    }

    /**
     * Set server key.
     *
     * @param serverKey server key
     */
    public void setServerKey(EncryptionKey serverKey) {
        this.serverKey = serverKey;
    }

    /**
     * Get tgs principal name.
     *
     * @return principal name
     */
    public PrincipalName getTgsPrincipal() {
        return KrbUtil.makeTgsPrincipal(kdcContext.getKdcRealm());
    }

    public PrincipalName getCrossRealmTgsPrincipal(String remoteRealm) {
        return KrbUtil.makeCrossRealmPrincipal(kdcContext.getKdcRealm(), remoteRealm);
    }

    public KrbIdentity getCrossRealmTgsEntry(String remoteRealm) throws KrbException {
        PrincipalName tgsPrincipal = getCrossRealmTgsPrincipal(remoteRealm);
        KrbIdentity tgsEntry = null;
        if (tgsPrincipal != null) {
            tgsEntry = getEntry(tgsPrincipal.getName());
        }
        return tgsEntry;
    }

    public boolean checkCrossRealm(String remoteRealm) throws KrbException {
        if (remoteRealm != null && kdcContext.getKdcRealm() != null) {
            isCrossRealm = !(kdcContext.getKdcRealm().equals(remoteRealm));
            this.remoteRealm = remoteRealm;
            return isCrossRealm;
        } else {
            throw new KrbException("Missing the realm.");
        }
    }

    public boolean isCrossRealm() {
        return isCrossRealm;
    }

    public String getRemoteRealm() {
        return this.remoteRealm;
    }

    /**
     * Make reply.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected abstract void makeReply() throws KrbException;

    /**
     * Check Version.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected void checkVersion() throws KrbException {
        KdcReq request = getKdcReq();

        int kerberosVersion = request.getPvno();
        if (kerberosVersion != KrbConstant.KRB_V5) {
            LOG.warn("Kerberos version: " + kerberosVersion + " should equal to "
                    + KrbConstant.KRB_V5);
            throw new KrbException(KrbErrorCode.KDC_ERR_BAD_PVNO);
        }
    }

    /**
     * Check policy.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected void checkPolicy() throws KrbException {
        KrbIdentity entry = getClientEntry();

        // if we can not get the client entry, maybe it is token preauth, ignore it.
        if (entry != null) {
            if (entry.isDisabled()) {
                LOG.warn("Client entry " + entry.getPrincipalName() + " is disabled.");
                throw new KrbException(KrbErrorCode.KDC_ERR_CLIENT_REVOKED);
            }
            if (entry.isLocked()) {
                LOG.warn("Client entry " + entry.getPrincipalName() + " is expired.");
                throw new KrbException(KrbErrorCode.KDC_ERR_CLIENT_REVOKED);
            }
            if (entry.getExpireTime().lessThan(new Date().getTime())) {
                throw new KrbException(KrbErrorCode.KDC_ERR_CLIENT_REVOKED);
            }
        } else {
            LOG.info("Client entry is empty, token preauth or cross realm.");
        }
    }

    /**
     * Check client.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected abstract void checkClient() throws KrbException;

    /**
     * Do the preatuh.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected void preauth() throws KrbException {
        KdcReq request = getKdcReq();

        PaData preAuthData = request.getPaData();

        if (isAnonymous && !isPkinit) {
            LOG.info("Need PKINIT.");
            KrbError krbError = makePreAuthenticationError(kdcContext, request,
                KrbErrorCode.KDC_ERR_PREAUTH_REQUIRED, true);
            throw new KdcRecoverableException(krbError);
        }

        if (preAuthData == null || preAuthData.isEmpty()) {
            LOG.info("The preauth data is empty.");
            KrbError krbError = makePreAuthenticationError(kdcContext, request,
                KrbErrorCode.KDC_ERR_PREAUTH_REQUIRED, false);
            throw new KdcRecoverableException(krbError);
        } else {
            getPreauthHandler().verify(this, preAuthData);
        }

        setPreAuthenticated(true);
    }

    /**
     * Set whether preauth required.
     * @param preauthRequired whether preauthentication required
     */
    protected void setPreauthRequired(boolean preauthRequired) {
        preauthContext.setPreauthRequired(preauthRequired);
    }

    /**
     * Get whether preauth required.
     * @return whether preauthentication is required
     */
    protected boolean isPreauthRequired() {
        return preauthContext.isPreauthRequired();
    }

    /**
     * Get preauth handler.
     * @return preauthencation handler
     */
    protected PreauthHandler getPreauthHandler() {
        return kdcContext.getPreauthHandler();
    }

    /**
     * Check encryption type.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected void checkEncryptionType() throws KrbException {
        List<EncryptionType> requestedTypes = getKdcReq().getReqBody().getEtypes();

        EncryptionType bestType = EncryptionUtil.getBestEncryptionType(requestedTypes,
                kdcContext.getConfig().getEncryptionTypes());

        if (bestType == null) {
            LOG.error("Can't get the best encryption type.");
            throw new KrbException(KrbErrorCode.KDC_ERR_ETYPE_NOSUPP);
        }

        setEncryptionType(bestType);
    }

    /**
     * Do some authenticate.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected void authenticate() throws KrbException {
        checkEncryptionType();
        checkPolicy();
    }

    /**
     * Issue ticket.
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    protected abstract void issueTicket() throws KrbException;

    /**
     * Check server.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     */
    private void checkServer() throws KrbException {
        KdcReq request = getKdcReq();

        PrincipalName principal = request.getReqBody().getSname();
        String serverRealm = request.getReqBody().getRealm();
        if (serverRealm == null || serverRealm.isEmpty()) {
            LOG.info("Can't get the server realm from request, and try to get from kdcContext.");
            serverRealm = kdcContext.getKdcRealm();
        }
        principal.setRealm(serverRealm);

        KrbIdentity serverEntry = getEntry(principal.getName());
        if (serverEntry == null) {
            LOG.error("Principal: " + principal.getName() + " is not known");
            throw new KrbException(KrbErrorCode.KDC_ERR_S_PRINCIPAL_UNKNOWN);
        }

        setServerEntry(serverEntry);
        for (EncryptionType encType : request.getReqBody().getEtypes()) {
            if (serverEntry.getKeys().containsKey(encType)) {
                EncryptionKey serverKey = serverEntry.getKeys().get(encType);
                setServerKey(serverKey);
                break;
            }
        }
    }

    /**
     * Make preauthentication error.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     * @param kdcContext kdc context
     * @param request kdc request
     * @param errorCode krb error code
     * @return The krb error reply to client
     */
    protected KrbError makePreAuthenticationError(KdcContext kdcContext, KdcReq request,
                                                  KrbErrorCode errorCode, boolean pkinit)
            throws KrbException {
        List<EncryptionType> encryptionTypes = kdcContext.getConfig().getEncryptionTypes();
        List<EncryptionType> clientEtypes = request.getReqBody().getEtypes();
        boolean isNewEtype = true;

        EtypeInfo2 eTypeInfo2 = new EtypeInfo2();

        EtypeInfo eTypeInfo = new EtypeInfo();

        for (EncryptionType encryptionType : encryptionTypes) {
            if (clientEtypes.contains(encryptionType)) {
                if (!isNewEtype) {
                    EtypeInfoEntry etypeInfoEntry = new EtypeInfoEntry();
                    etypeInfoEntry.setEtype(encryptionType);
                    etypeInfoEntry.setSalt(null);
                    eTypeInfo.add(etypeInfoEntry);
                }

                EtypeInfo2Entry etypeInfo2Entry = new EtypeInfo2Entry();
                etypeInfo2Entry.setEtype(encryptionType);
                eTypeInfo2.add(etypeInfo2Entry);
            }
        }

        byte[] encTypeInfo = null;
        byte[] encTypeInfo2 = null;
        if (!isNewEtype) {
            encTypeInfo = KrbCodec.encode(eTypeInfo);
        }
        encTypeInfo2 = KrbCodec.encode(eTypeInfo2);

        MethodData methodData = new MethodData();
        //methodData.add(new PaDataEntry(PaDataType.ENC_TIMESTAMP, null));
        if (!isNewEtype) {
            methodData.add(new PaDataEntry(PaDataType.ETYPE_INFO, encTypeInfo));
        }
        methodData.add(new PaDataEntry(PaDataType.ETYPE_INFO2, encTypeInfo2));

        if (pkinit) {
            methodData.add(new PaDataEntry(PaDataType.PK_AS_REQ, "empty".getBytes()));
            methodData.add(new PaDataEntry(PaDataType.PK_AS_REP, "empty".getBytes()));
        }

        KrbError krbError = new KrbError();
        krbError.setErrorCode(errorCode);
        byte[] encodedData = KrbCodec.encode(methodData);
        krbError.setEdata(encodedData);

        return krbError;
    }

    /**
     * Get identity entry with principal name.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     * @param principal Principal
     * @return krb identity entry
     */
    protected KrbIdentity getEntry(String principal) throws KrbException {
        return kdcContext.getIdentityService().getIdentity(principal);
    }

    /**
     * Get request body.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     * @return request body
     */
    protected ByteBuffer getRequestBody() throws KrbException {
        return null;
    }

    /**
     * Get armor key.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     * @return armor key
     */
    public EncryptionKey getArmorKey() throws KrbException {
        return fastContext.getArmorKey();
    }

    /**
     * Set armor key.
     *
     * @param armorKey armor key
     */
    protected void setArmorKey(EncryptionKey armorKey) {
        fastContext.setArmorKey(armorKey);
    }

    /**
     * Get client principal.
     * @return client principal
     */
    public PrincipalName getClientPrincipal() {
        return clientPrincipal;
    }

    /**
     * Set client principal.
     * @param clientPrincipal client principal
     */
    public void setClientPrincipal(PrincipalName clientPrincipal) {
        this.clientPrincipal = clientPrincipal;
    }

    /**
     * Get server principal.
     * @return server principal
     */
    public PrincipalName getServerPrincipal() {
        return serverPrincipal;
    }

    /**
     * Set server principal.
     * @param serverPrincipal server principal
     */
    public void setServerPrincipal(PrincipalName serverPrincipal) {
        this.serverPrincipal = serverPrincipal;
    }

    /**
     * Get innerbodyout.
     * @return inner body out
     */
    protected byte[] getInnerBodyout() {
        return innerBodyout;
    }

    /**
     * Get whether kdc request with token.
     * @return whether isToken
     */
    protected boolean isToken() {
        return isToken;
    }

    public boolean isHttps() {
        return isHttps;
    }

    public void setHttps(boolean https) {
        isHttps = https;
    }

    /**
     * Set auth token.
     * @param authToken The auth token
     */
    public void setToken(AuthToken authToken) {
        this.token = authToken;
    }

    /**
     * Get auth token.
     * @return token
     */
    protected AuthToken getToken() {
        return token;
    }

    protected boolean isPkinit() {
        return isPkinit;
    }

    public boolean isAnonymous() {
        return getKdcOptions().isFlagSet(KdcOption.REQUEST_ANONYMOUS);
    }

    public KdcOptions getKdcOptions() {
        return kdcReq.getReqBody().getKdcOptions();
    }

    public void setReqPackage(ByteBuffer reqPackage) {
        this.reqPackage = reqPackage;
    }

    public ByteBuffer getReqPackage() {
        return this.reqPackage;
    }
}
