
/*
  Meanwhile - Unofficial Lotus Sametime Community Client Library
  Copyright (C) 2004  Christopher (siege) O'Brien
  
  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.
  
  This library 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
  Library General Public License for more details.
  
  You should have received a copy of the GNU Library General Public
  License along with this library; if not, write to the Free
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <glib.h>
#include <string.h>

#include "mw_channel.h"
#include "mw_cipher.h"
#include "mw_debug.h"
#include "mw_error.h"
#include "mw_message.h"
#include "mw_service.h"
#include "mw_session.h"
#include "mw_util.h"


/** @todo reorganize this file, stuff is just strewn about */


struct mwChannel {

  /** session this channel belongs to */
  struct mwSession *session;

  enum mwChannelState state;

  /** creator for incoming channel, target for outgoing channel */
  struct mwLoginInfo user;

  /* similar to data from the CreateCnl message in 8.4.1.7 */
  guint32 reserved;    /**< special, unknown meaning */
  guint32 id;          /**< channel ID */
  guint32 service;     /**< service ID */
  guint32 proto_type;  /**< service protocol type */
  guint32 proto_ver;   /**< service protocol version */
  guint32 options;     /**< channel options */

  struct mwOpaque addtl_create;
  struct mwOpaque addtl_accept;

  /** all those supported ciphers */
  GHashTable *supported;
  guint16 offered_policy;  /**< @see enum mwEncryptPolicy */
  guint16 policy;          /**< @see enum mwEncryptPolicy */

  /** cipher information determined at channel acceptance */
  struct mwCipherInstance *cipher;

  /** statistics table */
  GHashTable *stats;

  GSList *outgoing_queue;     /**< queued outgoing messages */
  GSList *incoming_queue;     /**< queued incoming messages */

  struct mw_datum srvc_data;  /**< service-specific data */
};


struct mwChannelSet {
  struct mwSession *session;  /**< owning session */
  GHashTable *map;            /**< map of all channels, by ID */
  guint32 counter;            /**< counter for outgoing ID */
};


static void flush_channel(struct mwChannel *);


static const char *state_str(enum mwChannelState state) {
  switch(state) {
  case mwChannel_NEW:      return "new";
  case mwChannel_INIT:     return "initializing";
  case mwChannel_WAIT:     return "waiting";
  case mwChannel_OPEN:     return "open";
  case mwChannel_DESTROY:  return "closing";
  case mwChannel_ERROR:    return "error";

  case mwChannel_UNKNOWN:  /* fall through */
  default:                 return "UNKNOWN";
  }
}


static void state(struct mwChannel *chan, enum mwChannelState state,
		  guint32 err_code) {

  g_return_if_fail(chan != NULL);

  if(chan->state == state) return;

  chan->state = state;

  if(err_code) {
    g_message("channel 0x%08x state: %s (0x%08x)",
	      chan->id, state_str(state), err_code);
  } else {
    g_message("channel 0x%08x state: %s", chan->id, state_str(state));
  }
}


static gpointer get_stat(struct mwChannel *chan,
			 enum mwChannelStatField field) {

  return g_hash_table_lookup(chan->stats, (gpointer) field);
}


static void set_stat(struct mwChannel *chan, enum mwChannelStatField field,
		     gpointer val) {

  g_hash_table_insert(chan->stats, (gpointer) field, val);
}


#define incr_stat(chan, field, incr) \
  set_stat(chan, field, get_stat(chan, field) + incr)


#define timestamp_stat(chan, field) \
  set_stat(chan, field, (gpointer) time(NULL))


static void sup_free(gpointer a) {
  mwCipherInstance_free(a);
}


static struct mwCipherInstance *
get_supported(struct mwChannel *chan, guint16 id) {

  guint32 cid = (guint32) id;
  return g_hash_table_lookup(chan->supported, GUINT_TO_POINTER(cid));
}


static void put_supported(struct mwChannel *chan,
			  struct mwCipherInstance *ci) {

  struct mwCipher *cipher = mwCipherInstance_getCipher(ci);
  guint32 cid = (guint32) mwCipher_getType(cipher);
  g_hash_table_insert(chan->supported, GUINT_TO_POINTER(cid), ci);
}


struct mwChannel *mwChannel_newIncoming(struct mwChannelSet *cs, guint32 id) {
  struct mwChannel *chan;

  g_return_val_if_fail(cs != NULL, NULL);
  g_return_val_if_fail(cs->session != NULL, NULL);

  chan = g_new0(struct mwChannel, 1);
  chan->state = mwChannel_NEW;
  chan->session = cs->session;
  chan->id = id;

  chan->stats = g_hash_table_new(g_direct_hash, g_direct_equal);

  chan->supported = g_hash_table_new_full(g_direct_hash, g_direct_equal,
					  NULL, sup_free);

  g_hash_table_insert(cs->map, GUINT_TO_POINTER(id), chan);

  state(chan, mwChannel_WAIT, 0);

  return chan;
}


struct mwChannel *mwChannel_newOutgoing(struct mwChannelSet *cs) {
  guint32 id;
  struct mwChannel *chan;

  g_return_val_if_fail(cs != NULL, NULL);
  g_return_val_if_fail(cs->map != NULL, NULL);

  /* grab the next id, and try to make sure there isn't already a
     channel using it */
  do {
    id = ++cs->counter;
  } while(g_hash_table_lookup(cs->map, GUINT_TO_POINTER(id)));
  
  chan = mwChannel_newIncoming(cs, id);
  state(chan, mwChannel_INIT, 0);

  return chan;
}


guint32 mwChannel_getId(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, 0);
  return chan->id;
}


struct mwSession *mwChannel_getSession(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, NULL);
  return chan->session;
}


guint32 mwChannel_getServiceId(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, 0);
  return chan->service;
}


struct mwService *mwChannel_getService(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, NULL);
  return mwSession_getService(chan->session, chan->service);
}


void mwChannel_setService(struct mwChannel *chan, struct mwService *srvc) {
  g_return_if_fail(chan != NULL);
  g_return_if_fail(srvc != NULL);
  g_return_if_fail(chan->state == mwChannel_INIT);
  chan->service = mwService_getType(srvc);
}


gpointer mwChannel_getServiceData(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, NULL);
  return mw_datum_get(&chan->srvc_data);
}


void mwChannel_setServiceData(struct mwChannel *chan,
			      gpointer data, GDestroyNotify clean) {

  g_return_if_fail(chan != NULL);
  mw_datum_set(&chan->srvc_data, data, clean);
}


void mwChannel_removeServiceData(struct mwChannel *chan) {
  g_return_if_fail(chan != NULL);
  mw_datum_clear(&chan->srvc_data);
}


guint32 mwChannel_getProtoType(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, 0x00);
  return chan->proto_type;
}


void mwChannel_setProtoType(struct mwChannel *chan, guint32 proto_type) {
  g_return_if_fail(chan != NULL);
  g_return_if_fail(chan->state == mwChannel_INIT);
  chan->proto_type = proto_type;
}


guint32 mwChannel_getProtoVer(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, 0x00);
  return chan->proto_ver;
}


void mwChannel_setProtoVer(struct mwChannel *chan, guint32 proto_ver) {
  g_return_if_fail(chan != NULL);
  g_return_if_fail(chan->state == mwChannel_INIT);
  chan->proto_ver = proto_ver;
}


guint16 mwChannel_getEncryptPolicy(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, 0x00);
  return chan->policy;
}


guint32 mwChannel_getOptions(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, 0x00);
  return chan->options;
}


void mwChannel_setOptions(struct mwChannel *chan, guint32 options) {
  g_return_if_fail(chan != NULL);
  g_return_if_fail(chan->state == mwChannel_INIT);
  chan->options = options;
}


struct mwLoginInfo *mwChannel_getUser(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, NULL);
  return &chan->user;
}


struct mwOpaque *mwChannel_getAddtlCreate(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, NULL);
  return &chan->addtl_create;
}


struct mwOpaque *mwChannel_getAddtlAccept(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, NULL);
  return &chan->addtl_accept;
}


struct mwCipherInstance *
mwChannel_getCipherInstance(struct mwChannel *chan) {

  g_return_val_if_fail(chan != NULL, NULL);
  return chan->cipher;
}


enum mwChannelState mwChannel_getState(struct mwChannel *chan) {
  g_return_val_if_fail(chan != NULL, mwChannel_UNKNOWN);
  return chan->state;
}


gpointer mwChannel_getStatistic(struct mwChannel *chan,
				enum mwChannelStatField stat) {
  
  g_return_val_if_fail(chan != NULL, 0);
  g_return_val_if_fail(chan->stats != NULL, 0);

  return get_stat(chan, stat);
}


/* send a channel create message */
int mwChannel_create(struct mwChannel *chan) {
  struct mwMsgChannelCreate *msg;
  GList *list, *l;
  int ret;

  g_return_val_if_fail(chan != NULL, -1);
  g_return_val_if_fail(chan->state == mwChannel_INIT, -1);
  g_return_val_if_fail(mwChannel_isOutgoing(chan), -1);

  msg = (struct mwMsgChannelCreate *)
    mwMessage_new(mwMessage_CHANNEL_CREATE);

  msg->channel = chan->id;
  msg->target.user = g_strdup(chan->user.user_id);
  msg->target.community = g_strdup(chan->user.community);
  msg->service = chan->service;
  msg->proto_type = chan->proto_type;
  msg->proto_ver = chan->proto_ver;
  msg->options = chan->options;
  mwOpaque_clone(&msg->addtl, &chan->addtl_create);

  list = mwChannel_getSupportedCipherInstances(chan);
  if(list) {
    /* offer what we have */
    for(l = list; l; l = l->next) {
      struct mwEncryptItem *ei = mwCipherInstance_offer(l->data);
      msg->encrypt.items = g_list_append(msg->encrypt.items, ei);
    }

    /* we're easy to get along with */
    chan->offered_policy = mwEncrypt_WHATEVER;
    g_list_free(list);

  } else {
    /* we apparently don't support anything */
    chan->offered_policy = mwEncrypt_NONE;
  }

  msg->encrypt.mode = chan->offered_policy;
  msg->encrypt.extra = chan->offered_policy;
  
  ret = mwSession_send(chan->session, MW_MESSAGE(msg));
  mwMessage_free(MW_MESSAGE(msg));

  state(chan, (ret)? mwChannel_ERROR: mwChannel_WAIT, ret);

  return ret;
}


static void channel_open(struct mwChannel *chan) {
  state(chan, mwChannel_OPEN, 0);
  timestamp_stat(chan, mwChannelStat_OPENED_AT);
  flush_channel(chan);
}


int mwChannel_accept(struct mwChannel *chan) {
  struct mwSession *session;
  struct mwMsgChannelAccept *msg;
  struct mwCipherInstance *ci;

  int ret;

  g_return_val_if_fail(chan != NULL, -1);
  g_return_val_if_fail(mwChannel_isIncoming(chan), -1);
  g_return_val_if_fail(chan->state == mwChannel_WAIT, -1);

  session = chan->session;
  g_return_val_if_fail(session != NULL, -1);

  msg = (struct mwMsgChannelAccept *)
    mwMessage_new(mwMessage_CHANNEL_ACCEPT);

  msg->head.channel = chan->id;
  msg->service = chan->service;
  msg->proto_type = chan->proto_type;
  msg->proto_ver = chan->proto_ver;
  mwOpaque_clone(&msg->addtl, &chan->addtl_accept);

  ci = chan->cipher;

  if(! ci) {
    /* automatically select a cipher if one hasn't been already */

    switch(chan->offered_policy) {
    case mwEncrypt_NONE:
      mwChannel_selectCipherInstance(chan, NULL);
      break;
      
    case mwEncrypt_RC2_40:
      ci = get_supported(chan, mwCipher_RC2_40);
      mwChannel_selectCipherInstance(chan, ci);
      break;

    case mwEncrypt_RC2_128:
      ci = get_supported(chan, mwCipher_RC2_128);
      mwChannel_selectCipherInstance(chan, ci);
      break;
      
    case mwEncrypt_WHATEVER:
    case mwEncrypt_ALL:
    default:
      {
	GList *l, *ll;

	l = mwChannel_getSupportedCipherInstances(chan);
	if(l) {
	  /* nobody selected a cipher, so we'll just pick the last in
	     the list of available ones */
	  for(ll = l; ll->next; ll = ll->next);
	  ci = ll->data;
	  g_list_free(l);
	  
	  mwChannel_selectCipherInstance(chan, ci);
	  
	} else {
	  /* this may cause breakage, but there's really nothing else
	     we can do. They want something we can't provide. If they
	     don't like it, then they'll error the channel out */
	  mwChannel_selectCipherInstance(chan, NULL);
	}
      }
    }
  }

  msg->encrypt.mode = chan->policy; /* set in selectCipherInstance */
  msg->encrypt.extra = chan->offered_policy;

  if(chan->cipher) {
    msg->encrypt.item = mwCipherInstance_accept(chan->cipher);
  }

  ret = mwSession_send(session, MW_MESSAGE(msg));
  mwMessage_free(MW_MESSAGE(msg));

  if(ret) {
    state(chan, mwChannel_ERROR, ret);
  } else {
    channel_open(chan);
  }

  return ret;
}


static void channel_free(struct mwChannel *chan) {
  struct mwSession *s;
  struct mwMessage *msg;
  GSList *l;

  /* maybe no warning in the future */
  g_return_if_fail(chan != NULL);

  s = chan->session;

  mwLoginInfo_clear(&chan->user);
  mwOpaque_clear(&chan->addtl_create);
  mwOpaque_clear(&chan->addtl_accept);

  if(chan->supported) {
    g_hash_table_destroy(chan->supported);
    chan->supported = NULL;
  }

  if(chan->stats) {
    g_hash_table_destroy(chan->stats);
    chan->stats = NULL;
  }
  
  mwCipherInstance_free(chan->cipher);

  /* clean up the outgoing queue */
  for(l = chan->outgoing_queue; l; l = l->next) {
    msg = (struct mwMessage *) l->data;
    l->data = NULL;
    mwMessage_free(msg);
  }
  g_slist_free(chan->outgoing_queue);

  /* clean up the incoming queue */
  for(l = chan->incoming_queue; l; l = l->next) {
    msg = (struct mwMessage *) l->data;
    l->data = NULL;
    mwMessage_free(msg);
  }
  g_slist_free(chan->incoming_queue);

  g_free(chan);
}


int mwChannel_destroy(struct mwChannel *chan,
		      guint32 reason, struct mwOpaque *info) {

  struct mwMsgChannelDestroy *msg;
  struct mwSession *session;
  struct mwChannelSet *cs;
  int ret;

  /* may make this not a warning in the future */
  g_return_val_if_fail(chan != NULL, 0);

  state(chan, reason? mwChannel_ERROR: mwChannel_DESTROY, reason);

  session = chan->session;
  g_return_val_if_fail(session != NULL, -1);

  cs = mwSession_getChannels(session);
  g_return_val_if_fail(cs != NULL, -1);

  /* compose the message */
  msg = (struct mwMsgChannelDestroy *)
    mwMessage_new(mwMessage_CHANNEL_DESTROY);
  msg->head.channel = chan->id;
  msg->reason = reason;
  if(info) mwOpaque_clone(&msg->data, info);

  /* remove the channel from the channel set */
  g_hash_table_remove(cs->map, GUINT_TO_POINTER(chan->id));
  
  /* send the message */
  ret = mwSession_send(session, (struct mwMessage *) msg);
  mwMessage_free(MW_MESSAGE(msg));

  return ret;
}


static void queue_outgoing(struct mwChannel *chan,
			   struct mwMsgChannelSend *msg) {

  g_info("queue_outgoing, channel 0x%08x", chan->id);
  chan->outgoing_queue = g_slist_append(chan->outgoing_queue, msg);
}


static int channel_send(struct mwChannel *chan,
			struct mwMsgChannelSend *msg) {

  int ret = 0;

  /* if the channel is open, send and free the message. Otherwise,
     queue the message to be sent once the channel is finally
     opened */

  if(chan->state == mwChannel_OPEN) {
    ret = mwSession_send(chan->session, (struct mwMessage *) msg);
    mwMessage_free(MW_MESSAGE(msg));

  } else {
    queue_outgoing(chan, msg);
  }

  return ret;
}


int mwChannel_sendEncrypted(struct mwChannel *chan,
			    guint32 type, struct mwOpaque *data,
			    gboolean encrypt) {

  struct mwMsgChannelSend *msg;

  g_return_val_if_fail(chan != NULL, -1);

  msg = (struct mwMsgChannelSend *) mwMessage_new(mwMessage_CHANNEL_SEND);
  msg->head.channel = chan->id;
  msg->type = type;

  mwOpaque_clone(&msg->data, data);

  if(encrypt && chan->cipher) {
    msg->head.options = mwMessageOption_ENCRYPT;
    mwCipherInstance_encrypt(chan->cipher, &msg->data);
  }

  return channel_send(chan, msg);  
}


int mwChannel_send(struct mwChannel *chan, guint32 type,
		   struct mwOpaque *data) {

  return mwChannel_sendEncrypted(chan, type, data, TRUE);
}


static void queue_incoming(struct mwChannel *chan,
			   struct mwMsgChannelSend *msg) {

  /* we clone the message, because session_process will clear it once
     we return */

  struct mwMsgChannelSend *m = g_new0(struct mwMsgChannelSend, 1);
  m->head.type = msg->head.type;
  m->head.options = msg->head.options;
  m->head.channel = msg->head.channel;
  mwOpaque_clone(&m->head.attribs, &msg->head.attribs);

  m->type = msg->type;
  mwOpaque_clone(&m->data, &msg->data);

  g_info("queue_incoming, channel 0x%08x", chan->id);
  chan->incoming_queue = g_slist_append(chan->incoming_queue, m);
}


static void channel_recv(struct mwChannel *chan,
			 struct mwMsgChannelSend *msg) {

  struct mwService *srvc;
  srvc = mwChannel_getService(chan);

  incr_stat(chan, mwChannelStat_MSG_RECV, 1);

  if(msg->head.options & mwMessageOption_ENCRYPT) {
    struct mwOpaque data = { 0, 0 };
    mwOpaque_clone(&data, &msg->data);

    mwCipherInstance_decrypt(chan->cipher, &data);
    mwService_recv(srvc, chan, msg->type, &data);
    mwOpaque_clear(&data);
    
  } else {
    mwService_recv(srvc, chan, msg->type, &msg->data);
  }
}


static void flush_channel(struct mwChannel *chan) {
  GSList *l;

  for(l = chan->incoming_queue; l; l = l->next) {
    struct mwMsgChannelSend *msg = (struct mwMsgChannelSend *) l->data;
    l->data = NULL;

    channel_recv(chan, msg);
    mwMessage_free(MW_MESSAGE(msg));
  }
  g_slist_free(chan->incoming_queue);
  chan->incoming_queue = NULL;

  for(l = chan->outgoing_queue; l; l = l->next) {
    struct mwMessage *msg = (struct mwMessage *) l->data;
    l->data = NULL;

    mwSession_send(chan->session, msg);
    mwMessage_free(msg);
  }
  g_slist_free(chan->outgoing_queue);
  chan->outgoing_queue = NULL;
}


void mwChannel_recv(struct mwChannel *chan, struct mwMsgChannelSend *msg) {
  if(chan->state == mwChannel_OPEN) {
    channel_recv(chan, msg);

  } else {
    queue_incoming(chan, msg);
  }
}


struct mwChannel *mwChannel_find(struct mwChannelSet *cs, guint32 chan) {
  g_return_val_if_fail(cs != NULL, NULL);
  g_return_val_if_fail(cs->map != NULL, NULL);
  return g_hash_table_lookup(cs->map, GUINT_TO_POINTER(chan));
}


void mwChannelSet_free(struct mwChannelSet *cs) {
  if(! cs) return;
  if(cs->map) g_hash_table_destroy(cs->map);
  g_free(cs);
}


struct mwChannelSet *mwChannelSet_new(struct mwSession *s) {
  struct mwChannelSet *cs = g_new0(struct mwChannelSet, 1);
  cs->session = s;

  /* for some reason, g_int_hash/g_int_equal cause a SIGSEGV */
  cs->map = g_hash_table_new_full(g_direct_hash, g_direct_equal,
				  NULL, (GDestroyNotify) channel_free);
  return cs;
}


void mwChannel_recvCreate(struct mwChannel *chan,
			  struct mwMsgChannelCreate *msg) {

  struct mwSession *session;
  GList *list;
  struct mwService *srvc;
  
  g_return_if_fail(chan != NULL);
  g_return_if_fail(msg != NULL);
  g_return_if_fail(chan->id == msg->channel);

  session = chan->session;
  g_return_if_fail(session != NULL);

  if(mwChannel_isOutgoing(chan)) {
    g_warning("channel 0x%08x not an incoming channel", chan->id);
    mwChannel_destroy(chan, ERR_REQUEST_INVALID, NULL);
    return;
  }

  chan->offered_policy = msg->encrypt.mode;
  g_message("channel offered with encrypt policy 0x%04x", chan->policy);

  for(list = msg->encrypt.items; list; list = list->next) {
    struct mwEncryptItem *ei = list->data;
    struct mwCipher *cipher;
    struct mwCipherInstance *ci;

    g_message("channel offered cipher id 0x%04x", ei->id);
    cipher = mwSession_getCipher(session, ei->id);
    if(! cipher) {
      g_message("no such cipher found in session");
      continue;
    }

    ci = mwCipher_newInstance(cipher, chan);
    mwCipherInstance_offered(ci, ei);
    mwChannel_addSupportedCipherInstance(chan, ci);
  }

  mwLoginInfo_clone(&chan->user, &msg->creator);
  chan->service = msg->service;
  chan->proto_type = msg->proto_type;
  chan->proto_ver = msg->proto_ver;
  
  srvc = mwSession_getService(session, msg->service);
  if(srvc) {
    mwService_recvCreate(srvc, chan, msg);

  } else {
    mwChannel_destroy(chan, ERR_SERVICE_NO_SUPPORT, NULL);
  }  
}


void mwChannel_recvAccept(struct mwChannel *chan,
			  struct mwMsgChannelAccept *msg) {

  struct mwService *srvc;

  g_return_if_fail(chan != NULL);
  g_return_if_fail(msg != NULL);
  g_return_if_fail(chan->id == msg->head.channel);

  if(mwChannel_isIncoming(chan)) {
    g_warning("channel 0x%08x not an outgoing channel", chan->id);
    mwChannel_destroy(chan, ERR_REQUEST_INVALID, NULL);
    return;
  }

  if(chan->state != mwChannel_WAIT) {
    g_warning("channel 0x%08x state not WAIT: %s",
	      chan->id, state_str(chan->state));
    mwChannel_destroy(chan, ERR_REQUEST_INVALID, NULL);
    return;
  }

  mwLoginInfo_clone(&chan->user, &msg->acceptor);

  srvc = mwSession_getService(chan->session, chan->service);
  if(! srvc) {
    g_warning("no service: 0x%08x", chan->service);
    mwChannel_destroy(chan, ERR_SERVICE_NO_SUPPORT, NULL);
    return;
  }

  chan->policy = msg->encrypt.mode;
  g_message("channel accepted with encrypt policy 0x%04x", chan->policy);

  if(! msg->encrypt.mode || ! msg->encrypt.item) {
    /* no mode or no item means no encryption */
    mwChannel_selectCipherInstance(chan, NULL);

  } else {
    guint16 cid = msg->encrypt.item->id;
    struct mwCipherInstance *ci = get_supported(chan, cid);

    if(! ci) {
      g_warning("not an offered cipher: 0x%04x", cid);
      mwChannel_destroy(chan, ERR_REQUEST_INVALID, NULL);
      return;
    }

    mwCipherInstance_accepted(ci, msg->encrypt.item);
    mwChannel_selectCipherInstance(chan, ci);
  }

  /* mark it as open for the service */
  state(chan, mwChannel_OPEN, 0);

  /* let the service know */
  mwService_recvAccept(srvc, chan, msg);

  /* flush it if the service didn't just immediately close it */
  if(mwChannel_isState(chan, mwChannel_OPEN)) {
    channel_open(chan);
  }
}


void mwChannel_recvDestroy(struct mwChannel *chan,
			   struct mwMsgChannelDestroy *msg) {

  struct mwChannelSet *cs;
  struct mwService *srvc;

  g_return_if_fail(chan != NULL);
  g_return_if_fail(msg != NULL);
  g_return_if_fail(chan->id == msg->head.channel);

  state(chan, msg->reason? mwChannel_ERROR: mwChannel_DESTROY, msg->reason);

  srvc = mwChannel_getService(chan);
  if(srvc) mwService_recvDestroy(srvc, chan, msg);

  cs = mwSession_getChannels(chan->session);
  g_return_if_fail(cs != NULL);
  g_return_if_fail(cs->map != NULL);

  g_hash_table_remove(cs->map, GUINT_TO_POINTER(chan->id));
}


void mwChannel_populateSupportedCipherInstances(struct mwChannel *chan) {
  struct mwSession *session;
  GList *list;

  g_return_if_fail(chan != NULL);

  session = chan->session;
  g_return_if_fail(session != NULL);

  for(list = mwSession_getCiphers(session); list; list = list->next) {
    struct mwCipherInstance *ci = mwCipher_newInstance(list->data, chan);
    if(! ci) continue;
    put_supported(chan, ci);
  }
}


void mwChannel_addSupportedCipherInstance(struct mwChannel *chan,
					  struct mwCipherInstance *ci) {
  g_return_if_fail(chan != NULL);
  g_message("channel 0x%08x added cipher %s", chan->id,
	    NSTR(mwCipher_getName(mwCipherInstance_getCipher(ci))));
  put_supported(chan, ci);
}


static void collect(gpointer a, gpointer b, gpointer c) {
  // `a` isn't used
  (void)a;

  GList **list = c;
  *list = g_list_append(*list, b);
}


GList *mwChannel_getSupportedCipherInstances(struct mwChannel *chan) {
  GList *list = NULL;

  g_return_val_if_fail(chan != NULL, NULL);
  g_hash_table_foreach(chan->supported, collect, &list);

  return list;
}


void mwChannel_selectCipherInstance(struct mwChannel *chan,
				    struct mwCipherInstance *ci) {
  struct mwCipher *c;

  g_return_if_fail(chan != NULL);
  g_return_if_fail(chan->supported != NULL);

  chan->cipher = ci;
  if(ci) {
    guint cid;

    c = mwCipherInstance_getCipher(ci);
    cid = mwCipher_getType(c);

    g_hash_table_steal(chan->supported, GUINT_TO_POINTER(cid));

    switch(mwCipher_getType(c)) {
    case mwCipher_RC2_40:
      chan->policy = mwEncrypt_RC2_40;
      break;

    case mwCipher_RC2_128:
      chan->policy = mwEncrypt_RC2_128;
      break;

    default:
      /* unsure if this is bad */
      chan->policy = mwEncrypt_WHATEVER;
    }

    g_message("channel 0x%08x selected cipher %s",
	      chan->id, NSTR(mwCipher_getName(c)));

  } else {

    chan->policy = mwEncrypt_NONE;
    g_message("channel 0x%08x selected no cipher", chan->id);
  }

  g_hash_table_destroy(chan->supported);
  chan->supported = NULL;
}


