/* packet-ipdc.c
 * Routines for IP Device Control (SS7 over IP) dissection
 * Copyright Lucent Technologies 2004
 * Josh Bailey <joshbailey@lucent.com> and Ruud Linders <ruud@lucent.com>
 *
 * Using IPDC spec 0.20.2
 *
 * $Id: packet-ipdc.c 14999 2005-07-22 22:56:08Z sahlberg $
 *
 * Ethereal - Network traffic analyzer
 * By Gerald Combs <gerald@ethereal.com>
 * Copyright 1998 Gerald Combs
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <glib.h>
#include <math.h>

#ifdef NEED_SNPRINTF_H
# include "snprintf.h"
#endif

#include <epan/packet.h>
#include "packet-ipdc.h"
#include "packet-tcp.h"
#include <epan/packet.h>
#include <epan/ipproto.h>
#include <epan/prefs.h>

static int proto_ipdc = -1;
static int hf_ipdc_nr = -1;
static int hf_ipdc_ns = -1;
static int hf_ipdc_payload_len = -1;
static int hf_ipdc_protocol_id = -1;
static int hf_ipdc_trans_id_size = -1;
static int hf_ipdc_trans_id = -1;
static int hf_ipdc_message_code = -1;

static gint ett_ipdc = -1;
static gint ett_ipdc_tag = -1;

static gboolean ipdc_desegment = TRUE;
static guint ipdc_port_pref = TCP_PORT_IPDC;
static gboolean new_packet = FALSE;

static dissector_handle_t q931_handle;

void proto_reg_handoff_ipdc(void);


static guint
get_ipdc_pdu_len(tvbuff_t *tvb, int offset)
{
        /* lower 10 bits only */
        guint raw_len = (tvb_get_ntohs(tvb,offset+2) & 0x03FF);
 
        return raw_len + 4;
}

static void
dissect_ipdc_common(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree)
{
	proto_item *ti;
	proto_tree *ipdc_tree;
	proto_item *ipdc_tag;
	proto_tree *tag_tree;
	tvbuff_t *q931_tvb;

	const char *des;
	const char *enum_val = "";
	char tmp_tag_text[IPDC_STR_LEN + 1];
	const value_string *val_ptr;
	guint32	type;
	guint len;
	guint i;
	guint status;
	gshort tag;
	guint32 tmp_tag;

	gshort nr = tvb_get_guint8(tvb,0);
	gshort ns = tvb_get_guint8(tvb,1);
        guint payload_len = get_ipdc_pdu_len(tvb,0);

        gshort protocol_id;
        gshort trans_id_size;
        guint32 trans_id;
        guint16 message_code;
        guint16 offset;

        /* display IPDC protocol ID */
        if (check_col(pinfo->cinfo, COL_PROTOCOL))
                col_set_str(pinfo->cinfo, COL_PROTOCOL, "IPDC");

	/* short frame... */
	if (payload_len < 4)
		return;

	/* clear info column and display send/receive sequence numbers */
	if (check_col(pinfo->cinfo, COL_INFO)) {
		if (new_packet == TRUE) {
			col_clear(pinfo->cinfo, COL_INFO);
			new_packet = FALSE;
		}
		col_append_fstr(pinfo->cinfo, COL_INFO, "r=%u s=%u ",
		nr, ns);
	}

	if (payload_len == 4) {
		if (!tree)
			return;

	        ti = proto_tree_add_item(tree, proto_ipdc, tvb, 0, -1, FALSE);
       		ipdc_tree = proto_item_add_subtree(ti, ett_ipdc);
		proto_tree_add_item(ipdc_tree, hf_ipdc_nr, tvb, 0, 1, nr);
        	proto_tree_add_item(ipdc_tree, hf_ipdc_ns, tvb, 1, 1, ns);
        	proto_tree_add_uint(ipdc_tree, hf_ipdc_payload_len, tvb, 2, 2,
			payload_len);

		return;
	}

	/* IPDC tags present - display message code and trans. ID */
	protocol_id = tvb_get_guint8(tvb,4);
        trans_id_size = TRANS_ID_SIZE_IPDC; /* tvb_get_guint8(tvb,5); */
       	trans_id = tvb_get_ntohl(tvb,6);
       	message_code = tvb_get_ntohs(tvb,6+trans_id_size);
       	offset = 6 + trans_id_size + 2; /* past message_code */

	if (check_col(pinfo->cinfo, COL_INFO))
       		col_append_fstr(pinfo->cinfo, COL_INFO,
			"TID=%x %s ",
                        trans_id,
                        val_to_str(message_code, message_code_vals,
                        TEXT_UNDEFINED));

	if (!tree)
		return;

	ti = proto_tree_add_item(tree, proto_ipdc, tvb, 0, -1, FALSE);
	ipdc_tree = proto_item_add_subtree(ti, ett_ipdc);

	proto_tree_add_item(ipdc_tree, hf_ipdc_nr, tvb, 0, 1, nr);
	proto_tree_add_item(ipdc_tree, hf_ipdc_ns, tvb, 1, 1, ns);
	proto_tree_add_uint(ipdc_tree, hf_ipdc_payload_len, tvb,
		2, 2, payload_len);

	proto_tree_add_item(ipdc_tree, hf_ipdc_protocol_id, tvb,
		4, 1, protocol_id);
	proto_tree_add_item(ipdc_tree, hf_ipdc_trans_id_size, tvb,
		5, 1, trans_id_size);
	proto_tree_add_item(ipdc_tree, hf_ipdc_trans_id, tvb,
		6, trans_id_size, trans_id);
	proto_tree_add_item(ipdc_tree, hf_ipdc_message_code, tvb,
		6 + trans_id_size + 1, 1, message_code);

	ipdc_tag = proto_tree_add_text(ipdc_tree, tvb, offset,
	payload_len - offset, "IPDC tags");
	tag_tree = proto_item_add_subtree(ipdc_tag, ett_ipdc_tag);

	/* iterate through tags. first byte is tag, second is length,
           in bytes, following is tag data. tag of 0x0 should be
	   end of tags. */
	for (;;) {
		tag = tvb_get_guint8(tvb, offset);

		if (tag == 0x0) {
			if (offset == payload_len - 1) {
				proto_tree_add_text(tag_tree, tvb,
				offset, 1, "end of tags");
			} else {
				proto_tree_add_text(tag_tree, tvb,
				offset, 1, "data trailing end of tags");
			}

			break;
		}

		len = tvb_get_guint8(tvb,offset+1);
		des = val_to_str(tag, tag_description, TEXT_UNDEFINED);
		/* lookup tag type */
		for (i = 0; (ipdc_tag_types[i].tag != tag &&
			ipdc_tag_types[i].type != IPDC_UNKNOWN); i++)
		;
		type = ipdc_tag_types[i].type;

		tmp_tag = 0;

		switch (type) {
			/* simple IPDC_ASCII strings */
			case IPDC_ASCII:
				g_assert(len<=IPDC_STR_LEN);
				tvb_memcpy(tvb, tmp_tag_text, offset+2, len);
				tmp_tag_text[len] = 0;
				proto_tree_add_text(tag_tree, tvb, offset,
				len + 2, "0x%2.2x: %s: %s", tag, des,
				tmp_tag_text);
			break;

			/* unsigned integers, or bytes */
			case IPDC_UINT:
			case IPDC_BYTE:
				for (i = 0; i < len; i++) 
					tmp_tag += tvb_get_guint8(tvb,
						offset + 2 + i) * (guint32)
							pow(256, len - (i + 1));

				if (len == 1)
					enum_val =
						val_to_str(IPDC_TAG(tag) +
						tmp_tag,
						tag_enum_type, TEXT_UNDEFINED);

				if (len == 1 &&
					strcmp(enum_val, TEXT_UNDEFINED) != 0) {
					proto_tree_add_text(tag_tree, tvb,
						offset, len + 2,
						"0x%2.2x: %s: %s",
						tag, des, enum_val);
				} else {
					proto_tree_add_text(tag_tree, tvb,
						offset, len + 2,
						"0x%2.2x: %s: %u",
						tag, des, tmp_tag);
				}
			break;

			/* IP addresses */
			case IPDC_IPA:
				switch (len) {
					case 4:
						g_snprintf(tmp_tag_text,
						IPDC_STR_LEN,
						"%u.%u.%u.%u",
						tvb_get_guint8(tvb, offset + 2),
						tvb_get_guint8(tvb, offset + 3),
						tvb_get_guint8(tvb, offset + 4),
						tvb_get_guint8(tvb, offset + 5)
						);
					break;

					case 6:
						g_snprintf(tmp_tag_text,
						IPDC_STR_LEN,
						"%u.%u.%u.%u:%u",
						tvb_get_guint8(tvb, offset + 2),
						tvb_get_guint8(tvb, offset + 3),
						tvb_get_guint8(tvb, offset + 4),
						tvb_get_guint8(tvb, offset + 5),
						tvb_get_ntohs(tvb, offset + 6));
					break;

					default:
						g_snprintf(tmp_tag_text,
						IPDC_STR_LEN,
						"Invalid IP address length %u",
                                       		 len);
				}

				proto_tree_add_text(tag_tree, tvb,
					offset, len + 2,
					"0x%2.2x: %s: %s",
					tag, des, tmp_tag_text);
			break;

			/* Line status arrays */
			case IPDC_LINESTATUS:
			case IPDC_CHANNELSTATUS:
				proto_tree_add_text(tag_tree, tvb, offset,
				len + 2, "0x%2.2x: %s", tag, des);
				val_ptr = (type == IPDC_LINESTATUS) ?
					line_status_vals : channel_status_vals;

				for (i = 0; i < len; i++) {
					status = tvb_get_guint8(tvb,offset+2+i);

					proto_tree_add_text(tag_tree, tvb,
						offset + 2 + i, 1, 
						" %.2u: %.2x (%s)",
						i + 1, status,
						val_to_str(status,
						val_ptr,
						TEXT_UNDEFINED));
				}
			break;

			case IPDC_Q931:
				q931_tvb =
					tvb_new_subset(tvb, offset+2, len, len);
				call_dissector(q931_handle,q931_tvb,pinfo,tree);
			break;

			case IPDC_ENCTYPE:
				proto_tree_add_text(tag_tree, tvb,
					offset, len + 2,
					"0x%2.2x: %s: %s",
					tag, des, val_to_str(
					tvb_get_guint8(tvb,offset+2),
					encoding_type_vals,
					TEXT_UNDEFINED));

				if (len == 2) {
					proto_tree_add_text(tag_tree, tvb,
						offset, len + 2,
						"0x%2.2x: %s: %u",
						tag, des,
						tvb_get_guint8(tvb,offset+3));
				}
			break;
					
			/* default */
			default:
				proto_tree_add_text(tag_tree, tvb, offset,
				len + 2, "0x%2.2x: %s", tag, des);
		} /* switch */

		offset += len + 2;
	}
}

static void
dissect_ipdc_tcp_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree)
{
	dissect_ipdc_common(tvb, pinfo, tree);
}

static void
dissect_ipdc_tcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree)
{
	new_packet = TRUE;
	tcp_dissect_pdus(tvb, pinfo, tree, ipdc_desegment, 4,
		get_ipdc_pdu_len, dissect_ipdc_tcp_pdu);
}

void
proto_register_ipdc(void)
{                 

	static hf_register_info hf[] = {
		{ &hf_ipdc_nr,
			{ "N(r)",	"ipdc.nr",
			FT_UINT8, BASE_DEC, NULL, 0x0,
			"Receive sequence number", HFILL }
		},

		{ &hf_ipdc_ns,
			{ "N(s)",	"ipdc.ns",
			FT_UINT8, BASE_DEC, NULL, 0x0, 
			"Transmit sequence number", HFILL }
		},

		{ &hf_ipdc_payload_len,
			{ "Payload length",	"ipdc.length",
			FT_UINT16, BASE_DEC, NULL, 0x0,
			"Payload length", HFILL }
		},

		{ &hf_ipdc_protocol_id,
			{ "Protocol ID",	"ipdc.protocol_id",
			FT_UINT8, BASE_HEX, NULL, 0x0,
			"Protocol ID", HFILL }
		},

		{ &hf_ipdc_trans_id_size,
			{ "Transaction ID size",	"ipdc.trans_id_size",
			FT_UINT8, BASE_DEC, NULL, 0x0,
			"Transaction ID size", HFILL }
		},

		{ &hf_ipdc_trans_id,
			{ "Transaction ID",	"ipdc.trans_id",
			FT_BYTES, BASE_HEX, NULL, 0x0,
			"Transaction ID", HFILL }
		},

		{ &hf_ipdc_message_code,
			{ "Message code",	"ipdc.message_code",
			FT_UINT16, BASE_HEX, VALS(message_code_vals), 0x0,
			"Message Code", HFILL }
		},
	};

	static gint *ett[] = {
		&ett_ipdc,
		&ett_ipdc_tag,
	};

	module_t *ipdc_module;

	proto_ipdc = proto_register_protocol("IP Device Control (SS7 over IP)",
	    "IPDC", "ipdc");
	proto_register_field_array(proto_ipdc, hf, array_length(hf));
	proto_register_subtree_array(ett, array_length(ett));

	ipdc_module = prefs_register_protocol(proto_ipdc, proto_reg_handoff_ipdc);
	prefs_register_bool_preference(ipdc_module, "desegment_ipdc_messages",
		"Reassemble IPDC messages spanning multiple TCP segments",
		"Whether the IPDC dissector should reassemble messages spanning multiple TCP segments."
		" To use this option, you must also enable \"Allow subdissectors to reassemble TCP streams\" in the TCP protocol settings.",
		&ipdc_desegment);
	prefs_register_uint_preference(ipdc_module, "tcp.port",
		"IPDC monitoring port",
		"Set the IPDC monitoring port", 10,
		&ipdc_port_pref);
}

void
proto_reg_handoff_ipdc(void)
{
	static guint last_ipdc_port_pref = 0;
	static dissector_handle_t ipdc_tcp_handle = NULL;

	if (ipdc_tcp_handle) {
		dissector_delete("tcp.port", last_ipdc_port_pref,
			ipdc_tcp_handle);
	} else {
		ipdc_tcp_handle = 
			create_dissector_handle(dissect_ipdc_tcp, proto_ipdc);
		q931_handle = find_dissector("q931");
	}

	last_ipdc_port_pref = ipdc_port_pref;
	dissector_add("tcp.port", ipdc_port_pref, ipdc_tcp_handle);
}
