#include "uwsgi.h"

extern struct uwsgi_server uwsgi;

#define SNMP_SEQUENCE	0x30
#define SNMP_INTEGER	0x02
#define SNMP_STRING		0x04
#define SNMP_NULL	0x05
#define SNMP_GET	0xA0
#define SNMP_RES	0xA2
#define SNMP_OID	0x06

#define SNMP_WATERMARK (127-8)

/* 1.3.6.1.4.1.35156.17.X.X */
#define SNMP_UWSGI_BASE "\x2B\x06\x01\x04\x01\x82\x92\x54\x11"

static int get_snmp_integer(uint8_t *, uint64_t *);

static uint64_t get_uwsgi_snmp_value(uint64_t, uint8_t *);
static uint64_t get_uwsgi_custom_snmp_value(uint64_t, uint8_t *);

static uint8_t snmp_int_to_snmp(uint64_t, uint8_t, uint8_t *);

static ssize_t build_snmp_response(uint8_t, uint8_t, uint8_t *, int, uint8_t *, uint8_t *, uint8_t *);
static ssize_t build_snmp_metric_response(int64_t, uint8_t, uint8_t *, int, uint8_t *, uint8_t *, uint8_t *);

void manage_snmp(int fd, uint8_t * buffer, int size, struct sockaddr_in *client_addr) {

	uint16_t asnlen;
	uint16_t oidlen;

	uint8_t oid_part[2];

	int ptrdelta;

	uint8_t *ptr = buffer, *seq1, *seq2, *seq3;

	uint8_t community_len;

	uint64_t snmp_int = 0;
	uint64_t request_id = 0;
	uint64_t version = 0;


	// KISS for memory management
	if (size > SNMP_WATERMARK)
		return;
	ptr++;


	// check total sequence size
	if (*ptr > SNMP_WATERMARK || *ptr < 13)
		return;
	ptr++;

#ifdef UWSGI_DEBUG
	uwsgi_debug("SNMP packet size: %d\n", size);
#endif

	// check snmp version
	if (*ptr != SNMP_INTEGER)
		return;
	ptr++;

	ptrdelta = get_snmp_integer(ptr, &version);
	if (version > 2)
		return;
	ptr += ptrdelta;

#ifdef UWSGI_DEBUG
	uwsgi_debug("SNMP version: %d\n", version);
#endif

	// check for community string (this must be set from the api using uwsgi.snmp_community or with --snmp-community arg)
	if (*ptr != SNMP_STRING)
		return;
	ptr++;

	community_len = *ptr;

	if (community_len > 72 || community_len < 1)
		return;
	ptr++;

	// check for community string
	if (strlen(uwsgi.shared->snmp_community) != community_len)
		return;
	if (memcmp(ptr, uwsgi.shared->snmp_community, community_len))
		return;

#ifdef UWSGI_DEBUG
	uwsgi_debug("SNMP community: %.*s\n", community_len, ptr);
#endif

	ptr += community_len;

	// check for get request
	if (*ptr != SNMP_GET)
		return;

	*ptr = SNMP_RES;
	ptr++;
	seq1 = ptr;


	if (*ptr != ((size - community_len) - 9))
		return;
	ptr++;

	// get request_id
	if (*ptr != SNMP_INTEGER)
		return;
	ptr++;

	ptrdelta = get_snmp_integer(ptr, &request_id);

	if (ptrdelta <= 0)
		return;

#ifdef UWSGI_DEBUG
	uwsgi_debug("SNMP request id: %d %p\n", request_id, ptr);
#endif

	// check here
	if (ptr + ptrdelta >= buffer + size)
		return;
	ptr += ptrdelta;


	// get error
	if (*ptr != SNMP_INTEGER)
		return;

	ptr++;
	snmp_int = 0;
	ptrdelta = get_snmp_integer(ptr, &snmp_int);

	if (ptrdelta <= 0)
		return;
	if (ptr + ptrdelta >= buffer + size)
		return;
	if (snmp_int != 0)
		return;

	ptr += ptrdelta;

	// get index
	if (*ptr != SNMP_INTEGER)
		return;
	ptr++;
	snmp_int = 0;
	ptrdelta = get_snmp_integer(ptr, &snmp_int);
	if (ptrdelta <= 0)
		return;
	if (ptr + ptrdelta >= buffer + size)
		return;

	if (snmp_int != 0)
		return;
	ptr += ptrdelta;

#ifdef UWSGI_DEBUG
	uwsgi_debug("SNMP int [1]: %d\n", snmp_int);
#endif

	// check for sequence
	if (*ptr != SNMP_SEQUENCE)
		return;
	ptr++;


	if (*ptr > SNMP_WATERMARK)
		return;
	seq2 = ptr;
	ptr++;

	// now the interesting stuff: OID management
	if (*ptr != SNMP_SEQUENCE)
		return;
	ptr++;

	// check for normal OID uWSGI size: |1.3|.6|.1|.4|.1.|35156|.17|.1/2|.x| + OID_NULL
	asnlen = *ptr;
	if (asnlen < 15)
		return;
	seq3 = ptr;
	ptr++;

#ifdef UWSGI_DEBUG
	uwsgi_debug("SNMP ASN len: %d\n", asnlen);
#endif

	// is it an OID ?
	if (*ptr != SNMP_OID)
		return;
	ptr++;

	oidlen = *ptr;
	if (oidlen < 11)
		return;
	ptr++;

	// and now parse the OID !!!
	if (memcmp(ptr, SNMP_UWSGI_BASE, 9))
		return;

	ptr += 9;

	oid_part[0] = *ptr;

	// old-style SNMP metrics
	if (oid_part[0] == 1 || oid_part[0] == 2) {
		ptr++;

		oid_part[1] = *ptr;
		if (oid_part[1] < 1 || oid_part[1] > 100)
			return;
		ptr++;

		// check for null
		if (memcmp((char *) ptr, "\x05\x00", 2))
			return;
		ptr += 2;
		size = build_snmp_response(oid_part[0], oid_part[1], buffer, size, seq1, seq2, seq3);
	}
	// metrics subsystem
	else {
		size_t metric_asn_len = oidlen - 9;
		char *metric_asn = (char *) ptr;
		struct uwsgi_metric *um = uwsgi_metric_find_by_asn(metric_asn, metric_asn_len);
		if (!um) return;
		uwsgi_rlock(uwsgi.metrics_lock);
		int64_t value = *um->value;
		uwsgi_rwunlock(uwsgi.metrics_lock);
		size = build_snmp_metric_response(value, um->type, buffer, size, seq1, seq2, seq3);
	}


	if (size > 0) {
		if (sendto(fd, buffer, size, 0, (struct sockaddr *) client_addr, sizeof(struct sockaddr_in)) < 0) {
			uwsgi_error("sendto()");
		}
	}
}

static uint64_t get_uwsgi_snmp_value(uint64_t val, uint8_t * oid_t) {

	val--;

	if (uwsgi.shared->snmp_gvalue[val].type) {
		*oid_t = uwsgi.shared->snmp_gvalue[val].type;
		return *uwsgi.shared->snmp_gvalue[val].val;
	}

	*oid_t = SNMP_NULL;
	return 0;
}

static uint64_t get_uwsgi_custom_snmp_value(uint64_t val, uint8_t * oid_t) {

	val--;
	uwsgi_wlock(uwsgi.snmp_lock);
	if (uwsgi.shared->snmp_value[val].type) {
		*oid_t = uwsgi.shared->snmp_value[val].type;
		uwsgi_rwunlock(uwsgi.snmp_lock);
		return uwsgi.shared->snmp_value[val].val;
	}

	uwsgi_rwunlock(uwsgi.snmp_lock);
	*oid_t = SNMP_NULL;
	return 0;
}

static int get_snmp_integer(uint8_t * ptr, uint64_t * val) {

	uint16_t tlen;
	int i, j;

	uint8_t *cval = (uint8_t *) val;

	tlen = *ptr;

	if (tlen > 4)
		return -1;

#ifdef UWSGI_DEBUG
	uwsgi_debug("SNMP get integer TLEN %d %p\n", tlen, ptr);
#endif

	j = 0;
#ifdef __BIG_ENDIAN__
	for (i = 0; i < tlen; i++) {
#else
	for (i = tlen - 1; i >= 0; i--) {
#endif
		cval[j] = ptr[1 + i];
		j++;
	}

	return tlen + 1;
}

static uint8_t snmp_int_to_snmp(uint64_t snmp_val, uint8_t oid_type, uint8_t * buffer) {
	uint8_t tlen;
	int i, j;
	uint8_t *ptr = (uint8_t *) & snmp_val;
	int32_t val32 = (int32_t) snmp_val;

	// check for counter, counter64 or gauge

	if (oid_type == SNMP_COUNTER64) {
		tlen = 8;
	}
	else if (oid_type == SNMP_NULL || oid_type == 0) {
		tlen = 0;
	}
	else {
		tlen = 4;
		ptr = (uint8_t *) &val32;
	}

	buffer[0] = tlen;

	j = 1;
#ifdef __BIG_ENDIAN__
	for (i = 0; i < tlen; i++) {
#else
	for (i = tlen - 1; i >= 0; i--) {
#endif
		buffer[j] = ptr[i];
		j++;
	}

	return tlen + 1;
}

static ssize_t build_snmp_response(uint8_t oid1, uint8_t oid2, uint8_t * buffer, int size, uint8_t * seq1, uint8_t * seq2, uint8_t * seq3) {
	uint64_t snmp_val;
	uint8_t oid_sz;
	uint8_t oid_type;

	if (oid1 == 1) {
		snmp_val = get_uwsgi_snmp_value(oid2, &oid_type);
	}
	else if (oid1 == 2) {
		snmp_val = get_uwsgi_custom_snmp_value(oid2, &oid_type);
	}
	else {
		return -1;
	}

	buffer[size - 2] = oid_type;
	oid_sz = snmp_int_to_snmp(snmp_val, oid_type, buffer + (size - 1));

	if (oid_sz < 1)
		return -1;

	oid_sz--;

	buffer[1] += oid_sz;
	*seq1 += oid_sz;
	*seq2 += oid_sz;
	*seq3 += oid_sz;

	return size + oid_sz;

}

static ssize_t build_snmp_metric_response(int64_t value, uint8_t type, uint8_t * buffer, int size, uint8_t * seq1, uint8_t * seq2, uint8_t * seq3) {
        uint8_t oid_sz;

	if (type == UWSGI_METRIC_GAUGE) {
        	buffer[size - 2] = SNMP_GAUGE;
        	oid_sz = snmp_int_to_snmp(value, SNMP_GAUGE, buffer + (size - 1));
	}
	else {
		buffer[size - 2] = SNMP_COUNTER64;
        	oid_sz = snmp_int_to_snmp(value, SNMP_COUNTER64, buffer + (size - 1));
	}

        if (oid_sz < 1)
                return -1;

        oid_sz--;

        buffer[1] += oid_sz;
        *seq1 += oid_sz;
        *seq2 += oid_sz;
        *seq3 += oid_sz;

        return size + oid_sz;

}

void uwsgi_opt_snmp(char *opt, char *value, void *foobar) {
	uwsgi.snmp = 1;
	if (value) {
		uwsgi.snmp_addr = value;
		uwsgi.master_process = 1;
	}

}

void uwsgi_opt_snmp_community(char *opt, char *value, void *foobar) {
	uwsgi.snmp = 1;
	uwsgi.snmp_community = value;
}

int uwsgi_setup_snmp(void) {
	int snmp_fd = -1;
	int i;
	if (uwsgi.snmp) {
		if (uwsgi.snmp_community) {
			if (strlen(uwsgi.snmp_community) > 72) {
				uwsgi_log("*** warning the supplied SNMP community string will be truncated to 72 chars ***\n");
				memcpy(uwsgi.shared->snmp_community, uwsgi.snmp_community, 72);
			}
			else {
				memcpy(uwsgi.shared->snmp_community, uwsgi.snmp_community, strlen(uwsgi.snmp_community) + 1);
			}
		}

		uwsgi.shared->snmp_gvalue[0].type = SNMP_COUNTER64;
		uwsgi.shared->snmp_gvalue[0].val = &uwsgi.workers[0].requests;

		for (i = 0; i < uwsgi.numproc; i++) {
			uwsgi.shared->snmp_gvalue[30 + i].type = SNMP_COUNTER64;
			uwsgi.shared->snmp_gvalue[30 + i].val = &uwsgi.workers[i + 1].requests;
		}

		if (uwsgi.snmp_addr) {
			snmp_fd = bind_to_udp(uwsgi.snmp_addr, 0, 0);
			if (snmp_fd < 0) {
				uwsgi_log("unable to bind to udp socket. SNMP service will be disabled.\n");
			}
			else {
				uwsgi_log("SNMP server enabled on %s\n", uwsgi.snmp_addr);
				event_queue_add_fd_read(uwsgi.master_queue, snmp_fd);
			}
		}
		else {
			uwsgi_log("SNMP agent enabled.\n");
		}

	}

	return snmp_fd;
}

void uwsgi_master_manage_snmp(int snmp_fd) {
        struct sockaddr_in udp_client;
        socklen_t udp_len = sizeof(udp_client);
        ssize_t rlen = recvfrom(snmp_fd, uwsgi.wsgi_req->buffer, uwsgi.buffer_size, 0, (struct sockaddr *) &udp_client, &udp_len);

        if (rlen < 0) {
                uwsgi_error("recvfrom()");
        }
        else if (rlen > 0) {
                manage_snmp(snmp_fd, (uint8_t *) uwsgi.wsgi_req->buffer, rlen, &udp_client);
        }
}

