/**
 *  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;

import org.apache.kerby.kerberos.kerb.KrbCodec;
import org.apache.kerby.kerberos.kerb.KrbErrorCode;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.server.request.AsRequest;
import org.apache.kerby.kerberos.kerb.server.request.KdcRequest;
import org.apache.kerby.kerberos.kerb.server.request.TgsRequest;
import org.apache.kerby.kerberos.kerb.type.KerberosTime;
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.KrbMessageType;
import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
import org.apache.kerby.kerberos.kerb.type.kdc.AsReq;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcReq;
import org.apache.kerby.kerberos.kerb.type.kdc.TgsReq;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;

/**
 * KDC handler to process client requests. Currently only one realm is supported.
 */
public class KdcHandler {
    private static final Logger LOG = LoggerFactory.getLogger(KdcHandler.class);
    private final KdcContext kdcContext;

    /**
     * Constructor with kdc context.
     *
     * @param kdcContext kdc context
     */
    public KdcHandler(KdcContext kdcContext) {
        this.kdcContext = kdcContext;
    }

    /**
     * Process the client request message.
     *
     * @throws org.apache.kerby.kerberos.kerb.KrbException e
     * @param receivedMessage The client request message
     * @param  isTcp whether the protocol is tcp
     * @param remoteAddress Address from remote side
     * @return The response message
     */
    public ByteBuffer handleMessage(ByteBuffer receivedMessage, boolean isTcp,
                                    InetAddress remoteAddress) throws KrbException {
        KrbMessage krbRequest;
        KdcRequest kdcRequest = null;
        KrbMessage krbResponse;

        ByteBuffer message = receivedMessage.duplicate();

        try {
            krbRequest = KrbCodec.decodeMessage(receivedMessage);
        } catch (IOException e) {
            LOG.error("Krb decoding message failed", e);
            KrbError error = createValidationError(KrbErrorCode.KRB_AP_ERR_MSG_TYPE,
                    e.getMessage(),
                    null, kdcContext.getKdcRealm(),
                    null, null);
            return encodeKrbMessage(error, isTcp);
        }

        KrbMessageType messageType = krbRequest.getMsgType();
        if (messageType == KrbMessageType.TGS_REQ || messageType
                == KrbMessageType.AS_REQ) {
            KdcReq kdcReq = (KdcReq) krbRequest;
            String realm = getRequestRealm(kdcReq);
            if (realm == null || !kdcContext.getKdcRealm().equals(realm)) {
                LOG.error("Invalid realm from kdc request: " + realm);
                KrbError error = createValidationError(KrbErrorCode.WRONG_REALM,
                        "Invalid realm from kdc request: " + realm,
                        realm, kdcContext.getKdcRealm(),
                        kdcReq.getReqBody().getCname(), kdcReq.getReqBody().getSname());
                return encodeKrbMessage(error, isTcp);
            }

            if (messageType == KrbMessageType.TGS_REQ) {
                kdcRequest = new TgsRequest((TgsReq) kdcReq, kdcContext);
            } else {
                kdcRequest = new AsRequest((AsReq) kdcReq, kdcContext);
            }
        } else {
            LOG.error("Invalid message type: " + messageType);
            KrbError error = createValidationError(KrbErrorCode.KRB_AP_ERR_MSG_TYPE,
                    "Invalid message type: " + messageType,
                    null, kdcContext.getKdcRealm(),
                    null, null);
            return encodeKrbMessage(error, isTcp);
        }

        // For checksum
        if (kdcRequest == null) {
            throw new KrbException("Kdc request is null.");
        }
        kdcRequest.setReqPackage(message);
        if (remoteAddress == null) {
            throw new KrbException("Remote address is null, not available.");
        }
        kdcRequest.setClientAddress(remoteAddress);
        kdcRequest.isTcp(isTcp);

        try {
            kdcRequest.process();
            krbResponse = kdcRequest.getReply();
        } catch (Throwable e) {
            if (e instanceof KdcRecoverableException) {
                krbResponse = handleRecoverableException(
                        (KdcRecoverableException) e, kdcRequest);
            } else {
                KrbError krbError = new KrbError();
                krbError.setStime(KerberosTime.now());
                krbError.setSusec(100);

                KrbErrorCode errorCode = KrbErrorCode.UNKNOWN_ERR;
                if (e instanceof KrbException && ((KrbException) e).getKrbErrorCode() != null) {
                    errorCode = ((KrbException) e).getKrbErrorCode();
                }
                krbError.setErrorCode(errorCode);
                krbError.setCrealm(kdcContext.getKdcRealm());
                if (kdcRequest.getClientPrincipal() != null) {
                    krbError.setCname(kdcRequest.getClientPrincipal());
                }
                krbError.setRealm(kdcContext.getKdcRealm());
                if (kdcRequest.getServerPrincipal() != null) {
                    krbError.setSname(kdcRequest.getServerPrincipal());
                } else {
                    PrincipalName serverPrincipal = kdcRequest.getKdcReq().getReqBody().getSname();
                    serverPrincipal.setRealm(kdcRequest.getKdcReq().getReqBody().getRealm());
                    krbError.setSname(serverPrincipal);
                }
                if (KrbErrorCode.KRB_AP_ERR_BAD_INTEGRITY.equals(errorCode)) {
                    krbError.setEtext("PREAUTH_FAILED");
                } else {
                    krbError.setEtext(e.getMessage());
                }
                krbResponse = krbError;
            }
        }

        return encodeKrbMessage(krbResponse, isTcp);
    }

    private ByteBuffer encodeKrbMessage(KrbMessage krbMessage, boolean isTcp) throws KrbException {
        int bodyLen = krbMessage.encodingLength();
        ByteBuffer responseMessage;
        if (isTcp) {
            responseMessage = ByteBuffer.allocate(bodyLen + 4);
            responseMessage.putInt(bodyLen);
        } else {
            responseMessage = ByteBuffer.allocate(bodyLen);
        }
        KrbCodec.encode(krbMessage, responseMessage);
        responseMessage.flip();

        return responseMessage;
    }

    private KrbError createValidationError(KrbErrorCode errorCode, String errorText, String cRealm, String realm,
                                           PrincipalName cName, PrincipalName sName) {
        KrbError krbError = new KrbError();
        krbError.setStime(KerberosTime.now());
        krbError.setSusec(100);
        krbError.setErrorCode(errorCode);
        krbError.setEtext(errorText);
        krbError.setCrealm(cRealm);
        krbError.setRealm(realm);
        krbError.setCname(cName);
        krbError.setSname(sName);
        return krbError;
    }

    /**
     * Process the recoverable exception.
     *
     * @param e The exception return by kdc
     * @param kdcRequest kdc request
     * @return The KrbError
     */
    private KrbMessage handleRecoverableException(KdcRecoverableException e,
                                                  KdcRequest kdcRequest)
            throws KrbException {
        LOG.info("KRB error occurred while processing request: "
                + e.getMessage());

        KrbError error = e.getKrbError();
        error.setStime(KerberosTime.now());
        error.setSusec(100);
        error.setErrorCode(e.getKrbError().getErrorCode());
        error.setRealm(kdcContext.getKdcRealm());
        if (kdcRequest != null) {
            error.setSname(kdcRequest.getKdcReq().getReqBody().getCname());
        } else {
            error.setSname(new PrincipalName("NONE"));
        }
        error.setEtext(e.getMessage());
        return error;
    }

    /**
     * Get request realm.
     * @param kdcReq kdc request
     * @return realm
     */
    private String getRequestRealm(KdcReq kdcReq) {
        String realm = kdcReq.getReqBody().getRealm();
        if (realm == null && kdcReq.getReqBody().getCname() != null) {
            realm = kdcReq.getReqBody().getCname().getRealm();
        }

        return realm;
    }
}
