/*
 * Copyright 2013 University of Chicago
 *  Contact: Bryce Allen
 * Copyright 2013 Collabora Ltd.
 *  Contact: Youness Alaoui
 * Copyright 2025 Ying-Chun Liu (PaulLiu) <paulliu@debian.org>
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * the GNU Lesser General Public License Version 2.1 (the "LGPL"), in which
 * case the provisions of LGPL are applicable instead of those above. If you
 * wish to allow use of your version of this file only under the terms of the
 * LGPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replace
 * them with the notice and other provisions required by the LGPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the LGPL.
 */

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

#include <agent.h>

#include <glib.h>
#include <gio/gio.h>
#include <gio/gnetworking.h>

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "sshice"

/// GLIB Main loop object
static GMainLoop *gloop;
/// stdin IO channel. This is used to receive data from stdin for ssh proxy.
static GIOChannel* io_stdin;

/// GSocket secret
static char gsocket_secret[512] = {"953c2394-801d-4a2b-b1b7-671200a48743"};
/// log filename for debug.
static char log_filename[512] = {"/tmp/ssh-ice-cli.log.txt"};
/// Default stun server address
static gchar stun_server_addr[512] = {"stun.l.google.com"};
/// Default stun server port
static gint stun_server_port = 19302;

static gchar *gopt_gsocket_secret = NULL;
static gchar *gopt_log_filename = NULL;
static gchar *gopt_stun_server_addr = NULL;
static gchar *gopt_upnp = NULL;

static GOptionEntry gopt_entries[] = {
  { "gsocket-secret", 's', 0, G_OPTION_ARG_STRING, &gopt_gsocket_secret, "GSocket secret, have to be same between clients and server", "SECRET" },
  { "log-filename", 'l', 0, G_OPTION_ARG_FILENAME, &gopt_log_filename, "Filename for log. G_MESSAGES_DEBUG environment must be set.", "FILENAME" },
  { "stun-server-addr", 0, 0, G_OPTION_ARG_STRING, &gopt_stun_server_addr, "Stun server addr", stun_server_addr },
  { "stun-server-port", 0, 0, G_OPTION_ARG_INT, &stun_server_port, "Stun server port", "19302" },
  { "upnp", 0, 0, G_OPTION_ARG_STRING, &gopt_upnp, "Use UPNP", "yes|no" },
  G_OPTION_ENTRY_NULL
};

static const gchar *candidate_type_name[] = {"host", "srflx", "prflx", "relay"};

static const gchar *state_name[] = {"disconnected", "gathering", "connecting",
                                    "connected", "ready", "failed"};

static int print_local_data(NiceAgent *agent, guint stream_id,
			    guint component_id,
			    char *local_candidates,
			    size_t local_candidates_size);
static int parse_remote_data(NiceAgent *agent, guint stream_id,
    guint component_id, char *line);
static void cb_candidate_gathering_done(NiceAgent *agent, guint stream_id,
    gpointer data);
static void cb_new_selected_pair(NiceAgent *agent, guint stream_id,
    guint component_id, gchar *lfoundation,
    gchar *rfoundation, gpointer data);
static void cb_component_state_changed(NiceAgent *agent, guint stream_id,
    guint component_id, guint state,
    gpointer data);
static void cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_id,
    guint len, gchar *buf, gpointer data);
struct clientsocket_receive_cb_data {
  NiceAgent *agent;
  guint stream_id;
};  
static gboolean clientsocket_receive_cb (GIOChannel *source, GIOCondition cond,
    gpointer data);

/**
 * function to log to a file
 * @param log_domain log domain. A string.
 * @param log_level log level
 * @param message The message to log.
 * @param user_data A pointer to a FILE.
 */
void log_to_file(const char *log_domain,  GLogLevelFlags log_level,
		 const gchar *message,
		 gpointer user_data) {
  FILE *outputfile = (FILE *)user_data;
  fprintf(outputfile, "%s: %s\n", log_domain, message);
  fflush(outputfile);
}

int
main(int argc, char *argv[])
{
  NiceAgent *agent;
  gboolean controlling=0;
  guint stream_id;
  FILE *log_file = NULL;
  GError *error = NULL;
  GOptionContext *gopt_context;

  /* use gopt to get command line arguments */
  gopt_context = g_option_context_new ("- ssh over ice client");
  g_option_context_add_main_entries (gopt_context, gopt_entries, NULL);

  if (!g_option_context_parse(gopt_context, &argc, &argv, &error)) {
    g_print ("option parsing failed: %s\n", error->message);
    exit (1);
  }

  /* set gsocket secret get from command line arguments */
  if (gopt_gsocket_secret != NULL) {
    memcpy(gsocket_secret, gopt_gsocket_secret, sizeof(gsocket_secret));
    g_free(gopt_gsocket_secret);
    gopt_gsocket_secret = NULL;
  }
  /* set log filename from command line arguments */
  if (gopt_log_filename != NULL) {
    memcpy(log_filename, gopt_log_filename, sizeof(log_filename));
    g_free(gopt_log_filename);
    gopt_log_filename = NULL;
  }
  /* set stun server addr from command line arguments */
  if (gopt_stun_server_addr != NULL) {
    memcpy(stun_server_addr, gopt_stun_server_addr, sizeof(stun_server_addr));
    g_free(gopt_stun_server_addr);
    gopt_stun_server_addr = NULL;
  }
  /* open the log file only when G_MESSAGES_DEBUG is set */
  if (g_getenv("G_MESSAGES_DEBUG") != NULL) {
    log_file = fopen(log_filename, "w");
    g_log_set_default_handler(log_to_file, log_file);
  }

  g_debug("Using stun server '[%s]:%u'\n", stun_server_addr, stun_server_port);

  g_networking_init();

  gloop = g_main_loop_new(NULL, FALSE);
  /* create g_io_channel for stdin */
#ifdef G_OS_WIN32
  io_stdin = g_io_channel_win32_new_fd(_fileno(stdin));
#else
  io_stdin = g_io_channel_unix_new(fileno(stdin));
#endif
  /* set stdin to binary mode. */
  g_io_channel_set_encoding(io_stdin, NULL, NULL);
  g_io_channel_set_buffered(io_stdin, FALSE);
  
  // Create the nice agent
  agent = nice_agent_new_reliable(g_main_loop_get_context (gloop),
      NICE_COMPATIBILITY_RFC5245);
  if (agent == NULL)
    g_error("Failed to create agent");

  // Set the STUN settings and controlling mode
  g_object_set(agent, "stun-server", stun_server_addr, NULL);
  g_object_set(agent, "stun-server-port", stun_server_port, NULL);
  g_object_set(agent, "controlling-mode", controlling, NULL);
  /* Use UPNP or not based on command line arguments */
  if (gopt_upnp != NULL) {
    if (g_strcmp0(gopt_upnp, "yes") == 0 || g_strcmp0(gopt_upnp, "true") == 0 || g_strcmp0(gopt_upnp, "1") == 0) {
      gboolean upnpflag = TRUE;
      g_object_set(agent, "upnp", upnpflag, NULL);
    } else if (g_strcmp0(gopt_upnp, "no") == 0 || g_strcmp0(gopt_upnp, "false") == 0 || g_strcmp0(gopt_upnp, "0") == 0) {
      gboolean upnpflag = FALSE;
      g_object_set(agent, "upnp", upnpflag, NULL);
    }
    g_free(gopt_upnp);
    gopt_upnp = NULL;
  }

  // Connect to the signals
  g_signal_connect(agent, "candidate-gathering-done",
      G_CALLBACK(cb_candidate_gathering_done), NULL);
  g_signal_connect(agent, "new-selected-pair",
      G_CALLBACK(cb_new_selected_pair), NULL);
  g_signal_connect(agent, "component-state-changed",
      G_CALLBACK(cb_component_state_changed), NULL);

  // Create a new stream with one component
  stream_id = nice_agent_add_stream(agent, 1);
  if (stream_id == 0)
    g_error("Failed to add stream");

  // Attach to the component to receive the data
  // Without this call, candidates cannot be gathered
  nice_agent_attach_recv(agent, stream_id, 1,
      g_main_loop_get_context (gloop), cb_nice_recv, NULL);

  // Start gathering local candidates
  if (!nice_agent_gather_candidates(agent, stream_id))
    g_error("Failed to start candidate gathering");

  g_debug("waiting for candidate-gathering-done signal...");

  // Run the mainloop. Everything else will happen asynchronously
  // when the candidates are done gathering.
  g_main_loop_run (gloop);

  g_main_loop_unref(gloop);
  g_object_unref(agent);
  g_io_channel_unref (io_stdin);

  return EXIT_SUCCESS;
}

/**
 * Child watch callback. This function is called after we call gsocket and
 * it exits.
 */
static void
child_watch_cb (GPid     pid,
                gint     status,
                gpointer user_data)
{
  g_debug ("Child %" G_PID_FORMAT " exited %s", pid,
             g_spawn_check_wait_status (status, NULL) ? "normally" : "abnormally");

  // Free any resources associated with the child here, such as I/O channels
  // on its stdout and stderr FDs. If you have no code to put in the
  // child_watch_cb() callback, you can remove it and the g_child_watch_add()
  // call, but you must also remove the G_SPAWN_DO_NOT_REAP_CHILD flag,
  // otherwise the child process will stay around as a zombie until this
  // process exits.

  g_spawn_close_pid (pid);
}

/**
 * Spawn gsocket.
 */
static void call_gsocket(NiceAgent *agent, guint _stream_id, const char *_local_candidates) {
  gchar * argv[] = {
    "/usr/bin/gs-netcat", "-s", gsocket_secret, "-w", NULL};

  gint child_stdin, child_stdout, child_stderr;
  GPid child_pid;
  char line[4096];
  g_autoptr(GError) error = NULL;
  int rval;

  /* spawn gsocket */
  g_spawn_async_with_pipes (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &child_pid, &child_stdin, &child_stdout, &child_stderr, &error);

  if (error != NULL) {
    g_error ("Spawning child failed: %s", error->message);
    return;
  }

  /* add callback for child exits */
  g_child_watch_add(child_pid, child_watch_cb, NULL);

  /* send our local candidates */
  write(child_stdin, _local_candidates, strlen(_local_candidates));

  /* read local candidates */
  memset(line, 0, sizeof(line));
  read(child_stdout, line, sizeof(line));

  // Parse remote candidate list and set it on the agent
  rval = parse_remote_data(agent, _stream_id, 1, line);
  if (rval == EXIT_SUCCESS) {
    // Return FALSE so we stop listening to stdin since we parsed the
    // candidates correctly
    g_debug("waiting for state READY or FAILED signal...");
  } else {
    g_error("ERROR: failed to parse remote data");
  }

  close(child_stdin);
}

/**
 * Callback function. This function is called when nice is successful
 * gathering the local candidates
 */
static void
cb_candidate_gathering_done(NiceAgent *agent, guint _stream_id,
    gpointer data)
{
  char local_candidates[4096];

  g_debug("SIGNAL candidate gathering done");

  // Candidate gathering is done. Send our local candidates on stdout
  g_debug("Generate local candidate data...");
  print_local_data(agent, _stream_id, 1, local_candidates, sizeof(local_candidates));

  /* Call gsocket to send out local candidates and receive remote
     candidate list */
  g_debug ("Using gsocket to transfer local candidate data to remote.");
  call_gsocket(agent, _stream_id, local_candidates);
}

static void
cb_component_state_changed(NiceAgent *agent, guint _stream_id,
    guint component_id, guint state,
    gpointer data)
{
  g_debug("SIGNAL: state changed %d %d %s[%d]\n",
      _stream_id, component_id, state_name[state], state);

  if (state == NICE_COMPONENT_STATE_CONNECTED) {
    NiceCandidate *local, *remote;

    // Get current selected candidate pair and print IP address used
    if (nice_agent_get_selected_pair (agent, _stream_id, component_id,
                &local, &remote)) {
      gchar ipaddr[INET6_ADDRSTRLEN];

      nice_address_to_string(&local->addr, ipaddr);
      g_debug("Negotiation complete: ([%s]:%d,",
          ipaddr, nice_address_get_port(&local->addr));
      nice_address_to_string(&remote->addr, ipaddr);
      g_debug(" [%s]:%d)\n", ipaddr, nice_address_get_port(&remote->addr));
    }

    /* Since nice is ready. We add a callback function when there are data
       comes from the stdin*/
    g_debug("STDIN starts reciving data.");
    struct clientsocket_receive_cb_data *clientdata = malloc(sizeof(struct clientsocket_receive_cb_data));
    memset(clientdata, 0, sizeof(struct clientsocket_receive_cb_data));
    clientdata->agent = agent;
    clientdata->stream_id = _stream_id;
    g_io_add_watch(io_stdin, G_IO_IN, clientsocket_receive_cb, clientdata);
  } else if (state == NICE_COMPONENT_STATE_FAILED) {
    g_main_loop_quit (gloop);
  }
}

static GTree *nice_agent_send_largebuffer_buffers = NULL;
static gint nice_agent_send_largebuffer_buffers_key_compare(gconstpointer a, gconstpointer b, gpointer user_data) {
  const char *a1 = (const char *)a;
  const char *b1 = (const char *)b;
  return strcmp(a1, b1);
}
static void nice_agent_send_largebuffer_buffers_key_destroy(gpointer data) {
  char *a1 = (char *)data;
  free(a1);
}

struct nice_agent_send_largebuffer_buffer_node {
  char *buf;
  int len;
};
struct nice_agent_flush_buffer_data {
  NiceAgent *agent;
  guint stream_id;
  guint component_id;
};

/**
 * Flush the buffer.
*/
static gboolean nice_agent_flush_buffer(gpointer user_data) {
  struct nice_agent_flush_buffer_data *data1 = (struct nice_agent_flush_buffer_data *)user_data;
  NiceAgent *agent = data1->agent;
  guint stream_id = data1->stream_id;
  guint component_id = data1->component_id;
  GList *nice_agent_send_largebuffer_buffer = NULL;
  char tree_key[512];
  gboolean ret = G_SOURCE_CONTINUE;

  if (nice_agent_get_component_state (agent, stream_id, component_id) == NICE_COMPONENT_STATE_FAILED) {
    ret = G_SOURCE_REMOVE;
    goto nice_agent_flush_buffer_end;
  }
  if (nice_agent_get_component_state (agent, stream_id, component_id) != NICE_COMPONENT_STATE_READY) {
    goto nice_agent_flush_buffer_end;
  }

  snprintf(tree_key, sizeof(tree_key), "%d:%d", stream_id, component_id);

  if (g_tree_lookup_node(nice_agent_send_largebuffer_buffers, tree_key) == NULL) {
    /* buffer not initialized yet */
    goto nice_agent_flush_buffer_end;
  }

  nice_agent_send_largebuffer_buffer = (GList *)g_tree_lookup(nice_agent_send_largebuffer_buffers, tree_key);

  while (g_list_length(nice_agent_send_largebuffer_buffer) > 0) {
    struct nice_agent_send_largebuffer_buffer_node *first_node;
    int send_bytes;
    first_node = (struct nice_agent_send_largebuffer_buffer_node *)g_list_first(nice_agent_send_largebuffer_buffer)->data;
    nice_agent_send_largebuffer_buffer = g_list_remove(nice_agent_send_largebuffer_buffer, first_node);
    g_tree_replace(nice_agent_send_largebuffer_buffers, strdup(tree_key), nice_agent_send_largebuffer_buffer);

    send_bytes = nice_agent_send(agent, stream_id, component_id, first_node->len, first_node->buf);
    if (send_bytes <= 0) {
      /* buffer totally full. put the data back */
      nice_agent_send_largebuffer_buffer = g_list_prepend(nice_agent_send_largebuffer_buffer, first_node);
      g_tree_replace(nice_agent_send_largebuffer_buffers, strdup(tree_key), nice_agent_send_largebuffer_buffer);
      break;
    } else if (send_bytes < first_node->len) {
      /* data partially written. put the unwritten data back */
      struct nice_agent_send_largebuffer_buffer_node *new_node;
      new_node = malloc(sizeof(struct nice_agent_send_largebuffer_buffer_node));
      memset(new_node, 0, sizeof(struct nice_agent_send_largebuffer_buffer_node));
      new_node->buf = malloc(first_node->len - send_bytes);
      new_node->len = first_node->len - send_bytes;
      memcpy(new_node->buf, &(first_node->buf[send_bytes]), new_node->len);
      free(first_node->buf);
      free(first_node);
      nice_agent_send_largebuffer_buffer = g_list_prepend(nice_agent_send_largebuffer_buffer, new_node);
      g_tree_replace(nice_agent_send_largebuffer_buffers, strdup(tree_key), nice_agent_send_largebuffer_buffer);
      break;
    } else {
      /* data sent. free the memory */
      free(first_node->buf);
      first_node->buf = NULL;
      free(first_node);
      first_node = NULL;
    }
  }

 nice_agent_flush_buffer_end:

  return ret;
}

/**
 * Same function as nice_agent_send. But it has its own buffer so that
 * we will never have a buffer overflow. This function also checks
 * if the agent is ready or not. If it is not ready, it will store the data
 * in the buffer too. Different stream_id:component_id will have different
 * buffer.
 */
static void nice_agent_send_largebuffer(NiceAgent *agent,
				 guint stream_id,
				 guint component_id,
				 guint len,
				 const gchar *buf) {
  GList *nice_agent_send_largebuffer_buffer = NULL;
  struct nice_agent_send_largebuffer_buffer_node *new_node;
  int nice_agent_flush_buffer_init_flag = 0;
  char tree_key[512];

  /* No data, directly exit. */
  if (len <= 0) {
    return;
  }

  /* Init the Map of stream_id:component_id -> buffer */
  if (nice_agent_send_largebuffer_buffers == NULL) {
    nice_agent_send_largebuffer_buffers = g_tree_new_full(nice_agent_send_largebuffer_buffers_key_compare, NULL, nice_agent_send_largebuffer_buffers_key_destroy, NULL);
  }

  snprintf(tree_key, sizeof(tree_key), "%d:%d", stream_id, component_id);
  if (g_tree_lookup_node(nice_agent_send_largebuffer_buffers, tree_key) == NULL) {
    /* init the buffer */
    nice_agent_flush_buffer_init_flag = 0;
    g_tree_insert(nice_agent_send_largebuffer_buffers, strdup(tree_key), nice_agent_send_largebuffer_buffer);
  } else {
    nice_agent_flush_buffer_init_flag = 1;
  }

  nice_agent_send_largebuffer_buffer = (GList *)g_tree_lookup(nice_agent_send_largebuffer_buffers, tree_key);

  /* put data into the buffer */
  new_node = malloc(sizeof(struct nice_agent_send_largebuffer_buffer_node));
  memset(new_node, 0, sizeof(struct nice_agent_send_largebuffer_buffer_node));
  new_node->buf = malloc(len);
  new_node->len = len;
  memcpy(new_node->buf, buf, len);
  nice_agent_send_largebuffer_buffer = g_list_append(nice_agent_send_largebuffer_buffer, new_node);
  g_tree_replace(nice_agent_send_largebuffer_buffers, strdup(tree_key), nice_agent_send_largebuffer_buffer);

  /* flush the data out */
  struct nice_agent_flush_buffer_data flushdata1 = {
    .agent = agent,
    .stream_id = stream_id,
    .component_id = component_id
  };
  nice_agent_flush_buffer(&flushdata1);

  /* add a timeout function to periodically flush the data */
  if (nice_agent_flush_buffer_init_flag == 0) {
    struct nice_agent_flush_buffer_data *flushdata;
    nice_agent_flush_buffer_init_flag = 1;
    flushdata = malloc(sizeof(struct nice_agent_flush_buffer_data));
    memset(flushdata, 0, sizeof(struct nice_agent_flush_buffer_data));
    flushdata->agent = agent;
    flushdata->stream_id = stream_id;
    flushdata->component_id = component_id;
    g_timeout_add(500, nice_agent_flush_buffer, flushdata);
  }
}

/**
 * Callback function. This callback function is called when stdin has
 * data. And it will send the data from stdin to ICE.
 */
static gboolean
clientsocket_receive_cb (GIOChannel *source, GIOCondition cond,
    gpointer data)
{
  struct clientsocket_receive_cb_data *data1 = (struct clientsocket_receive_cb_data *) data;
  NiceAgent *agent = data1->agent;
  gchar buf[384];
  gsize len;
  gchar *p1;
 
  p1 = buf;
  if (g_io_channel_read_chars(source, p1, sizeof(buf), &len, NULL) == G_IO_STATUS_NORMAL) {
    nice_agent_send_largebuffer(agent, data1->stream_id, 1, len, p1);
    g_debug ("Send data %ld bytes to remote ice server", len);
  } else {
    g_debug ("Bye client");
    free(data1);
    return FALSE;
  } 
  return TRUE;
}

static void
cb_new_selected_pair(NiceAgent *agent, guint _stream_id,
    guint component_id, gchar *lfoundation,
    gchar *rfoundation, gpointer data)
{
  g_debug("SIGNAL: selected pair %s %s", lfoundation, rfoundation);
}

static void
cb_nice_recv(NiceAgent *agent, guint _stream_id, guint component_id,
    guint len, gchar *buf, gpointer data)
{
  if (component_id == 1) {
    fwrite(buf, 1, len, stdout);
    fflush(stdout);
    g_debug("Send %d bytes to stdout", len);
  }
}

static NiceCandidate *
parse_candidate(char *scand, guint _stream_id)
{
  NiceCandidate *cand = NULL;
  NiceCandidateType ntype = NICE_CANDIDATE_TYPE_HOST;
  gchar **tokens = NULL;
  guint i;

  tokens = g_strsplit (scand, ",", 5);
  for (i = 0; tokens[i]; i++);
  if (i != 5)
    goto end;

  for (i = 0; i < G_N_ELEMENTS (candidate_type_name); i++) {
    if (strcmp(tokens[4], candidate_type_name[i]) == 0) {
      ntype = i;
      break;
    }
  }
  if (i == G_N_ELEMENTS (candidate_type_name))
    goto end;

  cand = nice_candidate_new(ntype);
  cand->component_id = 1;
  cand->stream_id = _stream_id;
  cand->transport = NICE_CANDIDATE_TRANSPORT_UDP;
  strncpy(cand->foundation, tokens[0], NICE_CANDIDATE_MAX_FOUNDATION - 1);
  cand->foundation[NICE_CANDIDATE_MAX_FOUNDATION - 1] = 0;
  cand->priority = atoi (tokens[1]);

  if (!nice_address_set_from_string(&cand->addr, tokens[2])) {
    g_message("failed to parse addr: %s", tokens[2]);
    nice_candidate_free(cand);
    cand = NULL;
    goto end;
  }

  nice_address_set_port(&cand->addr, atoi (tokens[3]));

 end:
  g_strfreev(tokens);

  return cand;
}

/**
 * Store local candidates to a char* array.
 * @param agent the nice agent
 * @param _stream_id the stream_id
 * @param component_id the component_id
 * @param local_candidates the buffer to store local candidates.
 * @param local_candidates_size the size of local_candidates
 */
static int
print_local_data (NiceAgent *agent, guint _stream_id, guint component_id, char *local_candidates, size_t local_candidates_size)
{
  int result = EXIT_FAILURE;
  gchar *local_ufrag = NULL;
  gchar *local_password = NULL;
  gchar ipaddr[INET6_ADDRSTRLEN];
  GSList *cands = NULL, *item;
  char buf1[1024];

  memset(local_candidates, 0, local_candidates_size);
  
  if (!nice_agent_get_local_credentials(agent, _stream_id,
      &local_ufrag, &local_password))
    goto end;

  cands = nice_agent_get_local_candidates(agent, _stream_id, component_id);
  if (cands == NULL)
    goto end;

  snprintf(buf1, sizeof(buf1), "%s %s", local_ufrag, local_password);
  if (local_candidates_size-1-strlen(local_candidates) > 0) {
    strncat(local_candidates, buf1, local_candidates_size-1-strlen(local_candidates));
  }

  for (item = cands; item; item = item->next) {
    NiceCandidate *c = (NiceCandidate *)item->data;

    nice_address_to_string(&c->addr, ipaddr);

    // (foundation),(prio),(addr),(port),(type)
    snprintf(buf1, sizeof(buf1), " %s,%u,%s,%u,%s",
        c->foundation,
        c->priority,
        ipaddr,
        nice_address_get_port(&c->addr),
        candidate_type_name[c->type]);
    if (local_candidates_size-1-strlen(local_candidates) > 0) {
      strncat(local_candidates, buf1, local_candidates_size-1-strlen(local_candidates));
    }
  }
  result = EXIT_SUCCESS;

 end:
  if (local_ufrag)
    g_free(local_ufrag);
  if (local_password)
    g_free(local_password);
  if (cands)
    g_slist_free_full(cands, (GDestroyNotify)&nice_candidate_free);

  return result;
}


static int
parse_remote_data(NiceAgent *agent, guint _stream_id,
    guint component_id, char *line)
{
  GSList *remote_candidates = NULL;
  gchar **line_argv = NULL;
  const gchar *ufrag = NULL;
  const gchar *passwd = NULL;
  int result = EXIT_FAILURE;
  int i;

  line_argv = g_strsplit_set (line, " \t\n", 0);
  for (i = 0; line_argv && line_argv[i]; i++) {
    if (strlen (line_argv[i]) == 0)
      continue;

    // first two args are remote ufrag and password
    if (!ufrag) {
      ufrag = line_argv[i];
    } else if (!passwd) {
      passwd = line_argv[i];
    } else {
      // Remaining args are serialized canidates (at least one is required)
      NiceCandidate *c = parse_candidate(line_argv[i], _stream_id);

      if (c == NULL) {
        g_message("failed to parse candidate: %s", line_argv[i]);
        goto end;
      }
      remote_candidates = g_slist_prepend(remote_candidates, c);
    }
  }
  if (ufrag == NULL || passwd == NULL || remote_candidates == NULL) {
    g_message("line must have at least ufrag, password, and one candidate");
    goto end;
  }

  if (!nice_agent_set_remote_credentials(agent, _stream_id, ufrag, passwd)) {
    g_message("failed to set remote credentials");
    goto end;
  }

  // Note: this will trigger the start of negotiation.
  if (nice_agent_set_remote_candidates(agent, _stream_id, component_id,
      remote_candidates) < 1) {
    g_message("failed to set remote candidates");
    goto end;
  }

  result = EXIT_SUCCESS;

 end:
  if (line_argv != NULL)
    g_strfreev(line_argv);
  if (remote_candidates != NULL)
    g_slist_free_full(remote_candidates, (GDestroyNotify)&nice_candidate_free);

  return result;
}
