/* packet-applemidi.c
 * Routines for dissection of Apple network-midi session establishment.
 * Copyright 2006-2012, Tobias Erichsen <t.erichsen@gmx.de>
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * Copied from packet-data.c, README.developer, and various other files.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 *
 * Apple network-midi session establishment is a lightweight protocol for
 * providing a simple session establishment for MIDI-data sent in the form
 * of RTP-MIDI (RFC 4695 / 6295).  Peers recognize each other using the
 * Apple Bonjour scheme with the service-name "_apple-midi._udp", establish
 * a connection using AppleMIDI (no official name, just an abbreviation)
 * and then send payload using RTP-MIDI.  The implementation of this
 * dissector is based on the Apple implementation summary from May 6th, 2005
 * and the extension from August 13th, 2010.
 *
 * 2010-11-29
 * - initial version of dissector
 * 2012-02-24
 * - implemented dynamic payloadtype support to automatically punt
 *   the decoding to the RTP-MIDI dissector via the RTP dissector
 * - added new bitrate receive limit feature
 *
 * Here are some links:
 *
 * http://www.cs.berkeley.edu/~lazzaro/rtpmidi/
 * https://tools.ietf.org/html/rfc4695
 * https://tools.ietf.org/html/rfc6925
 */

#include "config.h"

#include <epan/packet.h>
#include <epan/conversation.h>

#include "packet-rtp.h"

void proto_register_applemidi(void);
void proto_reg_handoff_applemidi(void);

/* Definitions for protocol name during dissector-register */
#define APPLEMIDI_DISSECTOR_NAME			"Apple Network-MIDI Session Protocol"
#define APPLEMIDI_DISSECTOR_SHORTNAME			"AppleMIDI"
#define APPLEMIDI_DISSECTOR_ABBREVIATION		"applemidi"

/* Signature "Magic Value" for Apple network MIDI session establishment */
#define APPLEMIDI_PROTOCOL_SIGNATURE			0xffff

/* Apple network MIDI valid commands */
#define APPLEMIDI_COMMAND_INVITATION			0x494e		/*   "IN"   */
#define APPLEMIDI_COMMAND_INVITATION_REJECTED		0x4e4f		/*   "NO"   */
#define APLLEMIDI_COMMAND_INVITATION_ACCEPTED		0x4f4b		/*   "OK"   */
#define APPLEMIDI_COMMAND_ENDSESSION			0x4259		/*   "BY"   */
#define APPLEMIDI_COMMAND_SYNCHRONIZATION		0x434b		/*   "CK"   */
#define APPLEMIDI_COMMAND_RECEIVER_FEEDBACK		0x5253		/*   "RS"   */
#define APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT		0x524c		/*   "RL"   */

static int	hf_applemidi_signature			= -1;
static int	hf_applemidi_command			= -1;
static int	hf_applemidi_protocol_version		= -1;
static int	hf_applemidi_token			= -1;
static int	hf_applemidi_ssrc			= -1;
static int	hf_applemidi_name			= -1;
static int	hf_applemidi_count			= -1;
static int	hf_applemidi_padding			= -1;
static int	hf_applemidi_timestamp1			= -1;
static int	hf_applemidi_timestamp2			= -1;
static int	hf_applemidi_timestamp3			= -1;
static int	hf_applemidi_sequence_num		= -1;
static int	hf_applemidi_rtp_sequence_num		= -1;
static int	hf_applemidi_rtp_bitrate_limit		= -1;
static int	hf_applemidi_unknown_data		= -1;


static gint	ett_applemidi				= -1;
static gint	ett_applemidi_seq_num			= -1;


static const value_string applemidi_commands[] = {
	{ APPLEMIDI_COMMAND_INVITATION,			"Invitation" },
	{ APPLEMIDI_COMMAND_INVITATION_REJECTED,	"Invitation Rejected" },
	{ APLLEMIDI_COMMAND_INVITATION_ACCEPTED,	"Invitation Accepted" },
	{ APPLEMIDI_COMMAND_ENDSESSION,			"End Session" },
	{ APPLEMIDI_COMMAND_SYNCHRONIZATION,		"Synchronization" },
	{ APPLEMIDI_COMMAND_RECEIVER_FEEDBACK,		"Receiver Feedback" },
	{ APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT,	"Bitrate Receive Limit" },
	{ 0,						NULL },
};


static int			proto_applemidi		= -1;

static dissector_handle_t	applemidi_handle;
static dissector_handle_t	rtp_handle;

static const char applemidi_unknown_command[]		= "unknown command: 0x%04x";

static void
dissect_applemidi_common( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, guint16 command ) {

	proto_item *ti;
	guint16		 seq_num;
	guint8		 count;
	guint8		*name;
	gint		 offset			= 0;
	gint		 len;
	gint		 string_size;
	proto_tree	*applemidi_tree;
	proto_tree	*applemidi_tree_seq_num;


	col_set_str( pinfo->cinfo, COL_PROTOCOL, APPLEMIDI_DISSECTOR_SHORTNAME );

	col_add_fstr( pinfo->cinfo, COL_INFO, "%s", val_to_str( command, applemidi_commands, applemidi_unknown_command ) );

	ti = proto_tree_add_item( tree, proto_applemidi, tvb, 0, -1, ENC_NA  );
	applemidi_tree = proto_item_add_subtree( ti, ett_applemidi );

	proto_tree_add_item( applemidi_tree, hf_applemidi_signature, tvb, offset, 2, ENC_BIG_ENDIAN  );
	offset += 2;

	proto_tree_add_item( applemidi_tree, hf_applemidi_command, tvb, offset, 2, ENC_BIG_ENDIAN  );
	offset += 2;

	/* the format of packets for "IN", "NO", "OK" and "BY" is identical and contains
	 * the protocol version, a random number generated by the initiator of the session,
	 * the SSRC that is used by the respective sides RTP-entity and optionally the
	 * name of the participant */
	if ( ( APPLEMIDI_COMMAND_INVITATION == command ) ||
	     ( APPLEMIDI_COMMAND_INVITATION_REJECTED == command ) ||
	     ( APLLEMIDI_COMMAND_INVITATION_ACCEPTED == command ) ||
	     ( APPLEMIDI_COMMAND_ENDSESSION == command ) ) {

		proto_tree_add_item( applemidi_tree, hf_applemidi_protocol_version, tvb, offset, 4, ENC_BIG_ENDIAN  );
		offset += 4;

		proto_tree_add_item( applemidi_tree, hf_applemidi_token, tvb, offset, 4, ENC_BIG_ENDIAN  );
		offset += 4;

		proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN  );
		offset += 4;

		len = tvb_reported_length(tvb) - offset;

		/* Name is optional */
		if ( len > 0 ) {
			name = tvb_get_string_enc( wmem_packet_scope(), tvb, offset, len, ENC_UTF_8|ENC_NA );
			string_size = (gint)( strlen( name ) + 1 );
			proto_tree_add_item( applemidi_tree, hf_applemidi_name, tvb, offset, string_size, ENC_UTF_8|ENC_NA );
			col_append_fstr( pinfo->cinfo, COL_INFO, ": peer = \"%s\"", name );
			offset += string_size;
		}

		/* the synchronization packet contains three 64bit timestamps,  and a value to define how
		 * many of the timestamps transmitted are valid */
	} else if ( APPLEMIDI_COMMAND_SYNCHRONIZATION == command ) {
		proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN );
		offset += 4;

		count = tvb_get_guint8( tvb, offset );
		proto_tree_add_item( applemidi_tree, hf_applemidi_count, tvb, offset, 1, ENC_BIG_ENDIAN );
		col_append_fstr( pinfo->cinfo, COL_INFO, ": count = %u", count );
		offset += 1;

		proto_tree_add_item( applemidi_tree, hf_applemidi_padding, tvb, offset, 3, ENC_BIG_ENDIAN );
		offset += 3;

		proto_tree_add_item( applemidi_tree, hf_applemidi_timestamp1, tvb, offset, 8, ENC_BIG_ENDIAN );
		offset += 8;

		proto_tree_add_item( applemidi_tree, hf_applemidi_timestamp2, tvb, offset, 8, ENC_BIG_ENDIAN );
		offset += 8;

		proto_tree_add_item( applemidi_tree, hf_applemidi_timestamp3, tvb, offset, 8, ENC_BIG_ENDIAN );
		offset += 8;
		/* With the receiver feedback packet, the recipient can tell the sender up to what sequence
		 * number in the RTP-stream the packets have been received; this can be used to shorten the
		 * recovery-journal-section in the RTP-session */
	} else if ( APPLEMIDI_COMMAND_RECEIVER_FEEDBACK == command ) {
		proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN );
		offset += 4;

		ti = proto_tree_add_item( applemidi_tree, hf_applemidi_sequence_num, tvb, offset, 4, ENC_BIG_ENDIAN );
		/* Apple includes a 32bit sequence-number, but the RTP-packet only specifies 16bit.
		 * this subtree and subitem are added to be able to associate the sequence-number
		 * here easier with the one specified in the corresponding RTP-packet */
		applemidi_tree_seq_num = proto_item_add_subtree( ti, ett_applemidi_seq_num );
		seq_num = tvb_get_ntohs( tvb, offset );
		proto_tree_add_uint( applemidi_tree_seq_num, hf_applemidi_rtp_sequence_num, tvb, offset, 2, seq_num );
		offset += 4;

		col_append_fstr( pinfo->cinfo, COL_INFO, ": seq = %u", seq_num );
		/* With the bitrate receive limit packet, the recipient can tell the sender to limit
		   the transmission to a certain bitrate.  This is important if the peer is a gateway
		   to a hardware-device that only supports a certain speed.  Like the MIDI 1.0 DIN-cable
		   MIDI-implementation which is limited to 31250.  */
	} else if ( APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT == command ) {
		proto_tree_add_item( applemidi_tree, hf_applemidi_ssrc, tvb, offset, 4, ENC_BIG_ENDIAN );
		offset += 4;

		proto_tree_add_item( applemidi_tree, hf_applemidi_rtp_bitrate_limit,
				     tvb, offset, 4, ENC_BIG_ENDIAN );
		offset += 4;
	}
	/* If there is any remaining data (possibly because an unknown command was encountered),
	 * we just dump it here */
	len = tvb_reported_length_remaining( tvb, offset );
	if ( len > 0 ) {
		proto_tree_add_item( applemidi_tree, hf_applemidi_unknown_data, tvb, offset, len, ENC_NA );
	}
}

static gboolean
test_applemidi(tvbuff_t *tvb, guint16 *command_p, gboolean conversation_established ) {

	*command_p = 0xffff;

	/* An applemidi session protocol UDP-packet must start with the "magic value" of 0xffff ... */
	if ( APPLEMIDI_PROTOCOL_SIGNATURE != tvb_get_ntohs( tvb, 0 ) )
		return FALSE;

	*command_p = tvb_get_ntohs( tvb, 2 );

	/* If the conversation is establised (one prior packet with a valid known command)
	 * we won't check the commands anymore - this way we still show new commands
	 * Apple might introduce as "unknown" instead of punting to RTP-dissector */
	if ( conversation_established ) {
		return TRUE;
	}


	/* ... followed by packet-command: "IN", "NO", "OK", "BY", "CK" and "RS" and "RL" */
	if ( ( APPLEMIDI_COMMAND_INVITATION            == *command_p ) ||
	     ( APPLEMIDI_COMMAND_INVITATION_REJECTED   == *command_p ) ||
	     ( APLLEMIDI_COMMAND_INVITATION_ACCEPTED   == *command_p ) ||
	     ( APPLEMIDI_COMMAND_ENDSESSION            == *command_p ) ||
	     ( APPLEMIDI_COMMAND_SYNCHRONIZATION       == *command_p ) ||
	     ( APPLEMIDI_COMMAND_RECEIVER_FEEDBACK     == *command_p ) ||
	     ( APPLEMIDI_COMMAND_BITRATE_RECEIVE_LIMIT == *command_p ) )
		return TRUE;

	return FALSE;
}



/* dissect_applemidi() is called when a packet is seen from a previously identified applemidi conversation */
/*  If the packet isn't a valid applemidi packet, assume it's an RTP-MIDI packet.                          */

static int
dissect_applemidi( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_ ) {
	guint16		command;

	if ( test_applemidi( tvb, &command, TRUE ) )
		dissect_applemidi_common( tvb, pinfo, tree, command );
	else
		call_dissector( rtp_handle, tvb, pinfo, tree );

	return tvb_captured_length(tvb);
}

static gboolean
dissect_applemidi_heur( tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_ ) {

	guint16		 command;
	conversation_t	*p_conv;
	rtp_dyn_payload_t *rtp_dyn_payload;

	if ( tvb_captured_length( tvb ) < 4)
		return FALSE;  /* not enough bytes to check */

	if ( !test_applemidi( tvb, &command, FALSE ) ) {
		return FALSE;
	}

	/* set dynamic payload-type 97 which is used by Apple for their RTP-MIDI implementation for this
	   address/port-tuple to cause RTP-dissector to call the RTP-MIDI-dissector for payload-decoding */

	rtp_dyn_payload = rtp_dyn_payload_new();
	rtp_dyn_payload_insert(rtp_dyn_payload, 97, "rtp-midi", 10000);
	rtp_add_address( pinfo, PT_UDP, &pinfo->src, pinfo->srcport, 0, APPLEMIDI_DISSECTOR_SHORTNAME,
			 pinfo->num, FALSE, rtp_dyn_payload);

	/* call dissect_applemidi() from now on for UDP packets on this "connection"
	   it is important to do this step after calling rtp_add_address, otherwise
	   all further packets will go directly to the RTP-dissector!                */

	p_conv = find_or_create_conversation(pinfo);
	conversation_set_dissector( p_conv, applemidi_handle );

	/* punt to actual decoding */

	dissect_applemidi_common( tvb, pinfo, tree, command );
	return TRUE;

}


void
proto_register_applemidi( void )
{
	static hf_register_info hf[] =	{
		{
			&hf_applemidi_signature,
			{
				"Signature",
				"applemidi.signature",
				FT_UINT16,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_command,
			{
				"Command",
				"applemidi.command",
				FT_UINT16,
				BASE_HEX,
				VALS( applemidi_commands ),
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_protocol_version,
			{
				"Protocol Version",
				"applemidi.protocol_version",
				FT_UINT32,
				BASE_DEC,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_token,
			{
				"Initiator Token",
				"applemidi.initiator_token",
				FT_UINT32,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_ssrc,
			{
				"Sender SSRC",
				"applemidi.sender_ssrc",
				FT_UINT32,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_name,
			{
				"Name",
				"applemidi.name",
				FT_STRING,
				BASE_NONE,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_count,
			{
				"Count",
				"applemidi.count",
				FT_UINT8,
				BASE_DEC,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_padding,
			{
				"Padding",
				"applemidi.padding",
				FT_UINT24,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_timestamp1,
			{
				"Timestamp 1",
				"applemidi.timestamp1",
				FT_UINT64,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_timestamp2,
			{
				"Timestamp 2",
				"applemidi.timestamp2",
				FT_UINT64,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_timestamp3,
			{
				"Timestamp 3",
				"applemidi.timestamp3",
				FT_UINT64,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_sequence_num,
			{
				"Sequence Number",
				"applemidi.sequence_number",
				FT_UINT32,
				BASE_HEX,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_rtp_sequence_num,
			{
				"RTP Sequence Number",
				"applemidi.rtp_sequence_number",
				FT_UINT16,
				BASE_DEC,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_rtp_bitrate_limit,
			{
				"Bitrate limit",
				"applemidi.bitrate_limit",
				FT_UINT32,
				BASE_DEC,
				NULL,
				0x0,
				NULL, HFILL
			}
		},
		{
			&hf_applemidi_unknown_data,
			{
				"Unknown Data",
				"applemidi.unknown_data",
				FT_BYTES,
				BASE_NONE,
				NULL,
				0x00,
				NULL, HFILL
			}
		},
	};


	static gint *ett[] = {
		&ett_applemidi,
		&ett_applemidi_seq_num
	};

	proto_applemidi = proto_register_protocol( APPLEMIDI_DISSECTOR_NAME,
						   APPLEMIDI_DISSECTOR_SHORTNAME,
						   APPLEMIDI_DISSECTOR_ABBREVIATION );
	proto_register_field_array( proto_applemidi, hf, array_length( hf ) );
	proto_register_subtree_array( ett, array_length( ett ) );

}

void
proto_reg_handoff_applemidi( void ) {


	applemidi_handle = create_dissector_handle( dissect_applemidi, proto_applemidi );

	/* If we cannot decode the data it will be RTP-MIDI since the Apple session protocol uses
	 * two ports: the control-port and the MIDI-port.  On both ports an invitation is being sent.
	 * The second port is then used for the RTP-MIDI-data. So if we can't find valid AppleMidi
	 * packets, it will be most likely RTP-MIDI...
	 */
	rtp_handle = find_dissector_add_dependency( "rtp", proto_applemidi );
	heur_dissector_add( "udp", dissect_applemidi_heur, "Apple MIDI over UDP", "applemidi_udp", proto_applemidi, HEURISTIC_ENABLE );
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 8
 * tab-width: 8
 * indent-tabs-mode: t
 * End:
 *
 * vi: set shiftwidth=8 tabstop=8 noexpandtab:
 * :indentSize=8:tabSize=8:noTabs=false:
 */
