/*
 * Copyright 1998-2007 by egnite Software GmbH
 * Copyright 1988, 1989, 1991, 1992 by Carnegie Mellon University
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * For additional information see http://www.ethernut.de/
 */

#include <sys/types.h>

#include <stdlib.h>
#include <string.h>
#ifndef WIN32
#include <memdebug.h>
#include <arpa/inet.h>
#endif

#include <pro/snmp_config.h>
#include <pro/snmp.h>
#include <pro/snmp_api.h>
#include <pro/snmp_auth.h>
#include <pro/snmp_mib.h>
#include <pro/snmp_agent.h>

/*!
 * \addtogroup xgSNMP
 */
/*@{*/

/*
 * Using this as a global had been derived from the original CMU code.
 * It is very ugly (shiffer), but may require some effort to transform
 * it into something local.
 */
static uint8_t *packet_end;

static void SetVariable(const uint8_t * var_val, uint8_t var_val_type, uint8_t * statP, size_t statLen)
{
    size_t buffersize = 1000;

    switch (var_val_type) {
    case ASN_INTEGER:
    case ASN_COUNTER:
    case ASN_GAUGE:
    case ASN_TIMETICKS:
        AsnIntegerParse(var_val, &buffersize, &var_val_type, (long *) statP);
        break;
    case ASN_OCTET_STR:
    case ASN_IPADDRESS:
    case ASN_OPAQUE:
        AsnOctetStringParse(var_val, &buffersize, &var_val_type, statP, &statLen);
        break;
    case ASN_OBJECT_ID:
        AsnOidParse(var_val, &buffersize, &var_val_type, (OID *) statP, &statLen);
        break;
    }
}

/*!
 * \brief Parse a list of variables.
 *
 * \param data       Pointer to the start of the list.
 * \param length     Contains the number of valid bytes following the
 *                   start of the list.
 * \param out_data   Pointer to the output buffer.
 * \param out_length Number of bytes available in the output buffer.
 * \param index      Error index.
 * \param msgtype    Type of the incoming packet.
 * \param action     Action to perform, either SNMP_ACT_RESERVE1,
 *                   SNMP_ACT_RESERVE2, SNMP_ACT_COMMIT, SNMP_ACT_ACTION
 *                   or SNMP_ACT_FREE.
 *
 * \return 0 on success. Otherwise an error code is returned.
 *
 */
static int SnmpVarListParse(SNMP_SESSION * sess, const uint8_t * data, size_t length, uint8_t * out_data, size_t out_length,
                            long *index, int msgtype, int action)
{
    OID var_name[MAX_OID_LEN];
    size_t var_name_len;
    size_t var_val_len;
    uint8_t var_val_type;
    uint8_t *var_val;
    uint8_t statType;
    uint8_t *statP;
    size_t statLen;
    uint16_t acl;
    int exact, err;
    WMETHOD *wmethod;
    uint8_t *headerP;
    uint8_t *var_list_start;
    size_t dummyLen;
    int noSuchObject = 0;

    exact = (msgtype != SNMP_MSG_GETNEXT);
    /* Check if the list starts with a sequence header and get its length. */
    if ((data = AsnSequenceParse(data, &length, ASN_SEQUENCE | ASN_CONSTRUCTOR)) == NULL) {
        return SNMP_PARSE_ERROR;
    }

    /* Build ASN header. */
    headerP = out_data;
    if ((out_data = AsnSequenceBuild(out_data, &out_length, ASN_SEQUENCE | ASN_CONSTRUCTOR, 0)) == NULL) {
        return SNMP_BUILD_ERROR;
    }
    var_list_start = out_data;

    *index = 1;
    while (length > 0) {
        /* Get name and ASN1 encoded value of the next variable. */
        var_name_len = MAX_OID_LEN;
        if ((data = SnmpVarParse(data, &length, var_name, &var_name_len, &var_val_type, &var_val, &var_val_len)) == NULL) {
            return SNMP_PARSE_ERROR;
        }

        /* Now attempt to retrieve the variable on the local entity. */
        statP = SnmpMibFind(var_name, &var_name_len, &statType, &statLen, &acl, exact, &wmethod, &noSuchObject);

        /* Check access. */
        if (msgtype == SNMP_MSG_SET) {
            /* Make sure we have write access. */
            if (acl != ACL_RWRITE) {
                return sess->sess_version == SNMP_VERSION_1 ? SNMP_ERR_NOSUCHNAME : SNMP_ERR_NOTWRITABLE;
            }
            if (wmethod == NULL) {
                if (statP == NULL) {
                    return sess->sess_version == SNMP_VERSION_1 ? SNMP_ERR_NOSUCHNAME : SNMP_ERR_NOCREATION;
                }
                /* Check if the type and value is consistent with this entity's variable. */
                if (var_val_len > statLen || var_val_type != statType) {
                    return sess->sess_version == SNMP_VERSION_1 ? SNMP_ERR_BADVALUE : SNMP_ERR_WRONGTYPE;
                }
                /* Actually do the set if necessary. */
                if (action == SNMP_ACT_COMMIT) {
                    SetVariable(var_val, var_val_type, statP, statLen);
                }
            } else {
                err = (*wmethod) (action, var_val, var_val_type, var_val_len, var_name, var_name_len);

                /*
                 * Map the SNMPv2 error codes to SNMPv1 error codes (RFC 2089).
                 */
                if (err && sess->sess_version == SNMP_VERSION_1) {
                    switch (err) {
                    case SNMP_ERR_WRONGVALUE:
                    case SNMP_ERR_WRONGENCODING:
                    case SNMP_ERR_WRONGTYPE:
                    case SNMP_ERR_WRONGLENGTH:
                    case SNMP_ERR_INCONSISTENTVALUE:
                        err = SNMP_ERR_BADVALUE;
                        break;
                    case SNMP_ERR_NOACCESS:
                    case SNMP_ERR_NOTWRITABLE:
                    case SNMP_ERR_NOCREATION:
                    case SNMP_ERR_INCONSISTENTNAME:
                    case SNMP_ERR_AUTHORIZATIONERROR:
                        err = SNMP_ERR_NOSUCHNAME;
                        break;
                    default:
                        err = SNMP_ERR_GENERR;
                        break;
                    }
                    return err;
                }
            }
        } else {
            /* Retrieve the value and place it into the outgoing packet. */
            if (statP == NULL) {
                statLen = 0;
                if (exact) {
                    if (noSuchObject) {
                        statType = SNMP_NOSUCHOBJECT;
                    } else {
                        statType = SNMP_NOSUCHINSTANCE;
                    }
                } else {
                    statType = SNMP_ENDOFMIBVIEW;
                }
            }
            out_data = SnmpVarBuild(out_data, &out_length, var_name, var_name_len, statType, statP, statLen);
            if (out_data == NULL) {
                return SNMP_ERR_TOOBIG;
            }
        }
        (*index)++;
    }

    if (msgtype != SNMP_MSG_SET) {
        /*
         * Save a pointer to the end of the packet and
         * rebuild header with the actual lengths
         */
        packet_end = out_data;
        dummyLen = packet_end - var_list_start;
        if (AsnSequenceBuild(headerP, &dummyLen, ASN_SEQUENCE | ASN_CONSTRUCTOR, dummyLen) == NULL) {
            return SNMP_ERR_TOOBIG;
        }
    }
    *index = 0;

    return 0;
}

/*!
 * \brief Clone input packet.
 *
 * Creates a packet identical to the input packet, except for the error
 * status and the error index which are set according to the specified
 * parameters.
 *
 * \return 0 upon success and -1 upon failure.
 */
static int SnmpCreateIdentical(SNMP_SESSION * sess, const uint8_t * snmp_in, uint8_t * snmp_out, size_t snmp_length, long errstat,
                               long errindex)
{
    uint8_t *data;
    uint8_t type;
    long dummy;
    size_t length;
    size_t headerLength;
    uint8_t *headerPtr;
    const uint8_t *reqidPtr;
    uint8_t *errstatPtr;
    uint8_t *errindexPtr;
    const uint8_t *varListPtr;

    /* Copy packet contents. */
    memcpy(snmp_out, snmp_in, snmp_length);
    length = snmp_length;
    if ((headerPtr = (uint8_t *) SnmpAuthParse(snmp_out, &length, sess->sess_id, &sess->sess_id_len, &dummy)) == NULL) {
        return -1;
    }
    sess->sess_id[sess->sess_id_len] = 0;

    if ((reqidPtr = AsnHeaderParse(headerPtr, &length, (uint8_t *) & dummy)) == NULL) {
        return -1;
    }
    headerLength = length;

    /* Request id. */
    if ((errstatPtr = (uint8_t *) AsnIntegerParse(reqidPtr, &length, &type, &dummy)) == NULL) {
        return -1;
    }
    /* Error status. */
    if ((errindexPtr = (uint8_t *) AsnIntegerParse(errstatPtr, &length, &type, &dummy)) == NULL) {
        return -1;
    }
    /* Error index. */
    if ((varListPtr = AsnIntegerParse(errindexPtr, &length, &type, &dummy)) == NULL) {
        return -1;
    }

    if ((data = AsnHeaderBuild(headerPtr, &headerLength, SNMP_MSG_RESPONSE, headerLength)) == NULL) {
        return -1;
    }
    length = snmp_length;
    type = (uint8_t) (ASN_UNIVERSAL | ASN_PRIMITIVE | ASN_INTEGER);
    if ((data = AsnIntegerBuild(errstatPtr, &length, type, &errstat)) != errindexPtr) {
        return -1;
    }
    if ((data = AsnIntegerBuild(errindexPtr, &length, type, &errindex)) != varListPtr) {
        return -1;
    }
    packet_end = snmp_out + snmp_length;

    return 0;
}

/*!
 * \brief Parse incoming and create outgoing packet.
 *
 * \param in_data  Pointer to the incoming packet.
 * \param in_len   Number of valid bytes in the incoming packet.
 * \param out_data Pointer to a buffer for the outgoing packet.
 * \param out_len  Pointer to the variable that receives the number of
 *                 bytes in the outgoing packet.
 * \param out_len  Pointer to a variable which contains the size of the
 *                 output buffer on entry. On exit, it is returned
 *                 as the number of valid bytes in the output buffer.
 *
 * \return 0 upon success and -1 upon failure.
 */
int SnmpAgentProcessRequest(SNMP_SESSION * sess, const uint8_t * in_data, size_t in_len, uint8_t * out_data, size_t * out_len)
{
    long zero = 0;
    uint8_t msgtype;
    uint8_t type;
    long reqid;
    long errstat;
    long errindex;
    long dummyindex;
    uint8_t *out_auth;
    uint8_t *out_header;
    uint8_t *out_reqid;
    const uint8_t *data;
    size_t len;

    SnmpStatsInc(SNMP_STAT_INPKTS);

    /* Get version and community from the packet header. */
    len = in_len;
    sess->sess_id_len = sizeof(sess->sess_id) - 1;
    if ((data = SnmpAuthParse(in_data, &len, sess->sess_id, &sess->sess_id_len, &sess->sess_version)) == NULL) {
        SnmpStatsInc(SNMP_STAT_INASNPARSEERRS);
        return -1;
    }

    /* Check authentication. */
    if (sess->sess_version == SNMP_VERSION_1 || sess->sess_version == SNMP_VERSION_2C) {
        if (SnmpCommunityFind((char *) sess->sess_id, &sess->sess_read_view, &sess->sess_write_view)) {
            /* TODO: Create SNMPv2 report. */
            SnmpStatsInc(SNMP_STAT_INBADCOMMUNITYNAMES);
            return -1;
        }
    } else {
        /* Unsupported SNMP version. */
        SnmpStatsInc(SNMP_STAT_INBADVERSIONS);
        return -1;
    }

    /* Parse request header and check type. */
    if ((data = AsnHeaderParse(data, &len, &msgtype)) == NULL) {
        SnmpStatsInc(SNMP_STAT_INASNPARSEERRS);
        return -1;
    }
    if (msgtype == SNMP_MSG_GETBULK) {
        /* SNMPv2 bulk requests are not yet supported. */
        return -1;
    } else if (msgtype != SNMP_MSG_GET && msgtype != SNMP_MSG_GETNEXT && msgtype != SNMP_MSG_SET) {
        /* Bad request type. */
        return -1;
    }

    /* Parse request ID. */
    if ((data = AsnIntegerParse(data, &len, &type, &reqid)) == NULL) {
        SnmpStatsInc(SNMP_STAT_INASNPARSEERRS);
        return -1;
    }
    /* Parse error status. */
    if ((data = AsnIntegerParse(data, &len, &type, &errstat)) == NULL) {
        SnmpStatsInc(SNMP_STAT_INASNPARSEERRS);
        return -1;
    }
    /* Parse error index. */
    if ((data = AsnIntegerParse(data, &len, &type, &errindex)) == NULL) {
        SnmpStatsInc(SNMP_STAT_INASNPARSEERRS);
        return -1;
    }

    /*
     * Now start cobbling together what is known about the output packet.
     * The final lengths are not known now, so they will have to be
     * recomputed later.
     */
    out_auth = out_data;
    if ((out_header = SnmpAuthBuild(sess, out_auth, out_len, 0)) == NULL) {
        return -1;
    }
    if ((out_reqid = AsnSequenceBuild(out_header, out_len, SNMP_MSG_RESPONSE, 0)) == NULL) {
        return -1;
    }
    /* Return identical request ID. */
    type = (uint8_t) (ASN_UNIVERSAL | ASN_PRIMITIVE | ASN_INTEGER);
    if ((out_data = AsnIntegerBuild(out_reqid, out_len, type, &reqid)) == NULL) {
        return -1;
    }
    /* Assume that error status will be zero. */
    if ((out_data = AsnIntegerBuild(out_data, out_len, type, &zero)) == NULL) {
        return -1;
    }
    /* Assume that error index will be zero. */
    if ((out_data = AsnIntegerBuild(out_data, out_len, type, &zero)) == NULL) {
        return -1;
    }

    /*
     * Walk through the list of variables and retrieve each one,
     * placing its value in the output packet.
     *
     * TODO: Handle bulk requests.
     */
    errstat = SnmpVarListParse(sess, data, len, out_data, *out_len, &errindex, msgtype, SNMP_ACT_RESERVE1);

    /*
     * Sets require 3 to 4 passes through the var_op_list. The first two
     * passes verify that all types, lengths, and values are valid and
     * may reserve resources and the third does the set and a fourth
     * executes any actions. Then the identical GET RESPONSE packet is
     * returned.
     *
     * If either of the first two passes returns an error, another pass
     * is made so that any reserved resources can be freed.
     */
    if (msgtype == SNMP_MSG_SET) {
        if (errstat == 0) {
            errstat = SnmpVarListParse(sess, data, len, out_data, *out_len, &errindex, msgtype, SNMP_ACT_RESERVE2);
        }
        if (errstat == 0) {
            errstat = SnmpVarListParse(sess, data, len, out_data, *out_len, &dummyindex, msgtype, SNMP_ACT_COMMIT);
            SnmpVarListParse(sess, data, len, out_data, *out_len, &dummyindex, msgtype, errstat ? SNMP_ACT_FREE : SNMP_ACT_ACTION);
            if (SnmpCreateIdentical(sess, in_data, out_auth, in_len, 0L, 0L)) {
                return -1;
            }
            *out_len = packet_end - out_auth;
            return 0;
        } else {
            SnmpVarListParse(sess, data, len, out_data, *out_len, &dummyindex, msgtype, SNMP_ACT_FREE);
        }
    }

    if (errstat) {
        /* Create an error response. */
        if (SnmpCreateIdentical(sess, in_data, out_auth, in_len, errstat, errindex)) {
            return -1;
        }
        *out_len = packet_end - out_auth;
        return 0;
    }

    /*
     * Re-encode the headers with the real lengths.
     */
    *out_len = packet_end - out_header;
    out_data = AsnSequenceBuild(out_header, out_len, SNMP_MSG_RESPONSE, packet_end - out_reqid);
    if (out_data != out_reqid) {
        return -1;
    }
    *out_len = packet_end - out_auth;
    out_data = SnmpAuthBuild(sess, out_auth, out_len, packet_end - out_header);
    *out_len = packet_end - out_auth;

    return 0;
}

/*!
 * \brief Run SNMP agent.
 *
 * Normally runs in an endless loop, which is only left in case of an error.
 *
 * \param sock UDP socket to use.
 *
 * \return Always -1.
 */
int SnmpAgent(SOCKET sock)
{
    int rc = -1;
    size_t out_len;
    uint8_t *in_data = malloc(SNMP_MAX_LEN);
    uint8_t *out_data = malloc(SNMP_MAX_LEN);
    SNMP_SESSION *sess = malloc(sizeof(SNMP_SESSION));
#ifdef WIN32
    struct sockaddr_in from;
    int fromlen;
#else
    uint32_t raddr;
    uint16_t rport;
#endif

    if (in_data && out_data && sess) {
        for (;;) {
#ifdef WIN32
            fromlen = sizeof(struct sockaddr);
            rc = recvfrom(sock, (char *) in_data, SNMP_MAX_LEN, 0, (struct sockaddr *)&from, &fromlen);
#else
            rc = NutUdpReceiveFrom(sock, &raddr, &rport, in_data, SNMP_MAX_LEN, 0);
#endif
            if (rc < 0) {
                break;
            }
            out_len = SNMP_MAX_LEN;
            memset(sess, 0, sizeof(SNMP_SESSION));
            if (SnmpAgentProcessRequest(sess, in_data, (size_t) rc, out_data, &out_len) == 0) {
#ifdef WIN32
                if (sendto(sock, (char *) out_data, (int)out_len, 0, (struct sockaddr *)&from, sizeof(from)) >= 0) {
#else
                if (NutUdpSendTo(sock, raddr, rport, out_data, out_len) == 0) {
#endif
                    SnmpStatsInc(SNMP_STAT_OUTPKTS);
                }
            }
        }
    } else {
        rc = -1;
    }
    if (in_data) {
        free(in_data);
    }
    if (out_data) {
        free(out_data);
    }
    if (sess) {
        free(sess);
    }
    return rc;
}

/*@}*/
