jingle-filetransfer/filetransfer.c
author Nicolas Cornu <nicolas.cornu@ensi-bourges.fr>
Sat, 14 Aug 2010 01:39:05 +0200
changeset 113 cb5adb25ad87
parent 112 77d68793ce61
child 114 813867884159
permissions -rw-r--r--
The start of doc

/*
 * filetransfer.c
 *
 * Copyrigth (C) 2010 Nicolas Cornu <nicolas.cornu@ensi-bourges.fr>
 *
 * 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
 */

#include "config.h"

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

#include <mcabber/modules.h>
#include <mcabber/utils.h>
#include <mcabber/xmpp_helper.h>
#include <mcabber/settings.h>
#include <mcabber/logprint.h>
#include <mcabber/compl.h>
#include <mcabber/commands.h>
#include <mcabber/roster.h>
#include <mcabber/utils.h>

#include <jingle/jingle.h>
#include <jingle/check.h>
#include <jingle/register.h>
#include <jingle/sessions.h>
#include <jingle/send.h>

#include "filetransfer.h"


gconstpointer jingle_ft_check(JingleContent *cn, GError **err);
gboolean jingle_ft_handle(JingleAction action, gconstpointer data, LmMessageNode *node);
void jingle_ft_tomessage(gconstpointer data, LmMessageNode *node);
gboolean jingle_ft_handle_data(gconstpointer data, const gchar *data2, guint len);
void jingle_ft_start(session_content *sc, gsize size);
void jingle_ft_send(session_content *sc, gsize size);
void jingle_ft_stop(gconstpointer data);

static gboolean is_md5_hash(const gchar *hash);

static void jingle_ft_init(void);
static void jingle_ft_uninit(void);

const gchar *deps[] = { "jingle", NULL };

static JingleAppFuncs funcs = {
  .check        = jingle_ft_check,
  .handle       = jingle_ft_handle,
  .tomessage    = jingle_ft_tomessage,
  .handle_data  = jingle_ft_handle_data,
  .start        = jingle_ft_start,
  .send         = jingle_ft_send,
  .stop         = jingle_ft_stop
};

module_info_t info_jingle_filetransfer = {
  .branch          = MCABBER_BRANCH,
  .api             = MCABBER_API_VERSION,
  .version         = PROJECT_VERSION,
  .description     = "Jingle File Transfer (XEP-0234)\n",
  .requires        = deps,
  .init            = jingle_ft_init,
  .uninit          = jingle_ft_uninit,
  .next            = NULL,
};


gconstpointer jingle_ft_check(JingleContent *cn, GError **err)
{
  JingleFT *ft = NULL;
  LmMessageNode *node;
  gint64 tmpsize;
  const gchar *datestr, *sizestr;

  node = lm_message_node_get_child(cn->description, "offer");
 
  if (!node) {
    g_set_error(err, JINGLE_CHECK_ERROR, JINGLE_CHECK_ERROR_MISSING,
                "the offer element is missing");
    return NULL;
  }

  node = lm_message_node_get_child(node, "file");
  if (!node) {
    g_set_error(err, JINGLE_CHECK_ERROR, JINGLE_CHECK_ERROR_MISSING,
                "the file element is missing");
    return NULL;
  }

  if (g_strcmp0(lm_message_node_get_attribute(node, "xmlns"), NS_SI_FT)) {
    g_set_error(err, JINGLE_CHECK_ERROR, JINGLE_CHECK_ERROR_MISSING,
                "the file transfer offer has an invalid/unsupported namespace");
    return NULL;
  }

  ft = g_new0(JingleFT, 1);
  datestr  = lm_message_node_get_attribute(node, "date");
  ft->hash = (gchar *) lm_message_node_get_attribute(node, "hash");
  ft->name = (gchar *) lm_message_node_get_attribute(node, "name");
  sizestr  = lm_message_node_get_attribute(node, "size");

  if (!ft->name || !sizestr) {
    g_set_error(err, JINGLE_CHECK_ERROR, JINGLE_CHECK_ERROR_MISSING,
                "an attribute of the file element is missing");
    g_free(ft);
    return NULL;
  }

  ft->date = from_iso8601(datestr, 1);
  tmpsize = g_ascii_strtoll(sizestr, NULL, 10);

  // the size attribute is a xs:integer an therefore can be negative.
  if (tmpsize < 0) {
    g_set_error(err, JINGLE_CHECK_ERROR, JINGLE_CHECK_ERROR_BADVALUE,
                "the offered file has a negative size");
    g_free(ft);
    return NULL;
  }
  ft->size = tmpsize;

  ft->name = g_path_get_basename(ft->name);
  
  if (settings_opt_get("jingle_ft_dir") != NULL)
    ft->name = g_build_filename(settings_opt_get("jingle_ft_dir"), ft->name, NULL);
  else
    ft->name = g_build_filename("/tmp", ft->name, NULL);

  if (!g_strcmp0(ft->name, ".")) {
    g_set_error(err, JINGLE_CHECK_ERROR, JINGLE_CHECK_ERROR_BADVALUE,
                "the offered file has an invalid filename");
    g_free(ft->name);
    g_free(ft);
    return NULL;
  }

  // check if the md5 hash is valid ([a-fA-F0-9){32})
  if (ft->hash != NULL && (strlen(ft->hash) != 32 || !is_md5_hash(ft->hash))) {
    g_set_error(err, JINGLE_CHECK_ERROR, JINGLE_CHECK_ERROR_BADVALUE,
                "the offered file has an invalid filename");
    g_free(ft->name);
    g_free(ft);
    return NULL;
  }
  ft->hash = g_strndup(ft->hash, 32);

  return (gconstpointer) ft;
}

gboolean jingle_ft_handle(JingleAction action, gconstpointer data, LmMessageNode *node)
{
  if (action == JINGLE_SESSION_INFO) {
    if (!g_strcmp0(lm_message_node_get_attribute(node, "xmlns"), NS_JINGLE_APP_FT_INFO)
        && !g_strcmp0(node->name, "hash")) {
      ((JingleFT *)data)->hash = lm_message_node_get_value(node);
	}
  }
}

static gboolean is_md5_hash(const gchar *hash)
{
  int i = 0;
  for (i = 0; i < 32 && hash[i]; i++)
    if (!g_ascii_isxdigit(hash[i])) break;

  if (i == 32)
    return TRUE;
  else
    return FALSE;
}

gboolean jingle_ft_handle_data(gconstpointer jingleft, const gchar *data, guint len)
{
  JingleFT *jft = (JingleFT *) jingleft;
  GError *err = NULL;
  GIOStatus status;
  gsize bytes_written = 0;

  if (jft->md5 == NULL) {
    jft->md5 = g_checksum_new(G_CHECKSUM_MD5);
  }
  
  g_checksum_update(jft->md5, (guchar*)data, (gsize)len);
    
  // TODO: check if the file already exist or if it was created
  // during the call to jingle_ft_check and handle_data
  if (jft->outfile == NULL) {
    jft->outfile = g_io_channel_new_file(jft->name, "w", &err);
    if (jft->outfile == NULL || err != NULL) {
      // propagate the GError ?
      return FALSE;
	}
	g_io_channel_set_encoding(jft->outfile, NULL, NULL);
  }

  status = g_io_channel_write_chars(jft->outfile, data, (gssize) len,
                                    &bytes_written, &err);
  g_io_channel_flush(jft->outfile, NULL);
  if (status != G_IO_STATUS_NORMAL || err != NULL) {
    return FALSE;
  }
  if (bytes_written != len) {
    // not supposed to happen if status is normal, unless outfile is non-blocking
    return FALSE;
  }
  return TRUE;
}

static void do_sendfile(char *arg)
{
  char **args = split_arg(arg, 1, 0);
  gchar *filename;
  struct stat fileinfo;

  if (!args[0]) {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: give me a name!");
    return;
  }
  
  filename = expand_filename(args[0]); // expand ~ to HOME
  
  if (g_stat(filename, &fileinfo) != 0) {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: unable to stat %s", args[1]);
    return;
  }
  
  if (!S_ISREG(fileinfo.st_mode) && !S_ISLNK(fileinfo.st_mode)) {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: File doesn't exist!");
    return;
  }
  
  scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: Trying to send %s",
               args[0]);

  {
    JingleSession *sess;
    gchar *sid = jingle_generate_sid();
    gchar *ressource, *recipientjid;
    const gchar *namespaces[] = {NS_JINGLE, NS_JINGLE_APP_FT, NULL};
    const gchar *myjid = g_strdup(lm_connection_get_jid(lconnection));
    JingleFT *jft = g_new0(JingleFT, 1);

    if (CURRENT_JID == NULL) { // CURRENT_JID = the jid of the user which has focus
      scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: Please, choose a valid JID in the roster");
      return;
    }
    ressource = jingle_find_compatible_res(CURRENT_JID, namespaces);
    if (ressource == NULL) {
      scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: Cannot send file, because there is no ressource available");
      return;
    }
    
    recipientjid = g_strdup_printf("%s/%s", CURRENT_JID, ressource);

    sess = session_new(sid, myjid, recipientjid, JINGLE_SESSION_OUTGOING);
    session_add_content(sess, "file", JINGLE_SESSION_STATE_PENDING);

    jft->desc = g_strdup(args[0]);
    jft->type = JINGLE_FT_OFFER;
    jft->name = g_path_get_basename(filename);
    jft->date = fileinfo.st_mtime;
    jft->size = fileinfo.st_size;
    jft->outfile = g_io_channel_new_file (filename, "r", NULL);
    if (jft->outfile == NULL) {
      scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: Cannot open file %s", args[1]);
      return;
    }
    
    g_io_channel_set_encoding(jft->outfile, NULL, NULL);
    
    session_add_app(sess, "file", NS_JINGLE_APP_FT, jft);

    jingle_handle_app(sess, "file", NS_JINGLE_APP_FT, jft, recipientjid);

    g_free(ressource);
    g_free(sid);
    //g_io_channel_unref(jft->outfile);
    //g_io_channel_shutdown(jft->outfile, TRUE, NULL);
  }

  free_arg_lst(args);
}

void jingle_ft_tomessage(gconstpointer data, LmMessageNode *node)
{
  JingleFT *jft = (JingleFT*) data;
  gchar *size = NULL;
  
  if (lm_message_node_get_child(node, "description") != NULL)
    return;

  LmMessageNode *node2 = lm_message_node_add_child(node, "description", NULL);
  lm_message_node_set_attribute(node2, "xmlns", NS_JINGLE_APP_FT);
  if (jft->type == JINGLE_FT_OFFER)
    node2 = lm_message_node_add_child(node2, "offer", NULL);
  else
    node2 = lm_message_node_add_child(node2, "request", NULL);

  node2 = lm_message_node_add_child(node2, "file", NULL);

  size = g_strdup_printf("%" G_GUINT64_FORMAT, jft->size);
  
  lm_message_node_set_attributes(node2, "xmlns", NS_SI_FT, "name", jft->name,
                                 "size", size, NULL);
  g_free(size);
  
  if (jft->hash != NULL)
    lm_message_node_set_attribute(node2, "hash", jft->hash);

  if (jft->desc != NULL)
    lm_message_node_add_child(node2, "desc", jft->desc);

  //if (jft->data != 0)
}

void jingle_ft_send_hash(gchar *sid, gchar *to, gchar *hash)
{
  JingleAckHandle *ackhandle;
  
  LmMessage *r = lm_message_new_with_sub_type(to, LM_MESSAGE_TYPE_IQ, LM_MESSAGE_SUB_TYPE_SET);
  LmMessageNode *node = lm_message_get_node(r);
  lm_message_node_add_child(node, "jingle", NULL);
  node = lm_message_node_get_child(node, "jingle");
  lm_message_node_set_attributes(node, "xmlns", NS_JINGLE, "sid", sid, "action", "session-info", NULL);
  lm_message_node_add_child(node, "hash", hash);
  node = lm_message_node_get_child(node, "hash");
  lm_message_node_set_attribute(node, "xmlns", NS_JINGLE_APP_FT_INFO);
  
  ackhandle = g_new0(JingleAckHandle, 1);
  ackhandle->callback = NULL;
  ackhandle->user_data = NULL;
  
  lm_connection_send_with_reply(lconnection, r,
                                jingle_new_ack_handler(ackhandle), NULL);
  lm_message_unref(r);
}

void jingle_ft_send(session_content *sc, gsize size)
{
  JingleFT *jft;
  gchar *buf = g_new0(gchar, size);
  gsize read;
  GIOStatus status;
  int count = 0;
  JingleSession *sess = session_find_by_sid(sc->sid, sc->from);
  if (sess == NULL) {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: error before transfer");
    // We haven't LmMessage: jingle_send_iq_error(jn->message, "cancel", "item-not-found", "unknown-session");
    return;
  }
  
  SessionContent *sc2 = session_find_sessioncontent(sess, sc->name);
  
  jft = (JingleFT*)sc2->description;
  
  do {
    count++;
    status = g_io_channel_read_chars(jft->outfile, (gchar*)buf, size, &read, NULL);
  } while (status == G_IO_STATUS_AGAIN && count < 10);
  
  if (status == G_IO_STATUS_AGAIN) {
    // TODO: something better
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: file unavailable");
    return;
  }
  
  if (status == G_IO_STATUS_ERROR) {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: an error occured");
    return;
  }
  
  if (status == G_IO_STATUS_NORMAL) {
    g_checksum_update(jft->md5, (guchar*)buf, read);
    // Call a handle in jingle who will call the trans
    handle_app_data(sc->sid, sc->from, sc->name, buf, read);
    g_free(buf);
  }
  
  if (status == G_IO_STATUS_EOF) {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: transfer finish (%s)", jft->name);
    jft->hash = g_strdup(g_checksum_get_string(jft->md5));
    // Call a function to say state is ended
    session_changestate_sessioncontent(sess, sc2->name, JINGLE_SESSION_STATE_ENDED);
    // Send the hash
    jingle_ft_send_hash(sess->sid, sess->to, jft->hash);
    g_checksum_free(jft->md5);
    
    if (!session_remove_sessioncontent(sess, sc2->name)) {
      jingle_send_session_terminate(sess, "success");
      session_delete(sess);
    }
  }
}

void jingle_ft_start(session_content *sc, gsize size)
{
  JingleFT *jft;
  
  JingleSession *sess = session_find_by_sid(sc->sid, sc->from);
  if (sess == NULL) {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: error before transfer");
    return;
  }
  
  SessionContent *sc2 = session_find_sessioncontent(sess, sc->name);

  jft = (JingleFT*)sc2->description;
  
  jft->md5 = g_checksum_new(G_CHECKSUM_MD5);
  
  scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: Transfer start (%s)",
               jft->name);

  sc2->appfuncs->send(sc, size);
}

// When we got a session-terminate
void jingle_ft_stop(gconstpointer data)
{
  JingleFT *jft = (JingleFT*)data;

  if (jft->hash != NULL) {
    if (g_strcmp0(jft->hash, g_checksum_get_string(jft->md5))) {
      scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: File corrupt (%s)", jft->name);
    } else {
      scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: transfer finished (%s) and verified", jft->name);
    }
  } else {
    scr_LogPrint(LPRINT_LOGNORM, "Jingle File Transfer: transfer finished (%s) but not verified", jft->name);
  }

  g_checksum_free(jft->md5);

  g_io_channel_flush(jft->outfile, NULL);

  g_io_channel_unref(jft->outfile);
}

static void jingle_ft_init(void)
{
  jingle_register_app(NS_JINGLE_APP_FT, &funcs, JINGLE_TRANSPORT_STREAMING);
  xmpp_add_feature(NS_JINGLE_APP_FT);
  /*file_cid = compl_new_category();
  if (file_cid) {
    compl_add_category_word(sendfile_cid, "send");
    compl_add_category_word(send file_cid, "request");
  }*/
  /* Add command */
  cmd_add("sendfile", "Send a file", 0, 0, do_sendfile, NULL);
}

static void jingle_ft_uninit(void)
{
  xmpp_del_feature(NS_JINGLE_APP_FT);
  jingle_unregister_app(NS_JINGLE_APP_FT);
  cmd_del("file");
  /*if (file_cid)
    compl_del_category(file_cid);*/
}