#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gm-net.h" #include "gm-marshal.h" #include "gm-debug.h" #include "gm-support.h" #define GM_NET_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE((object), GM_TYPE_NET, GmNetPrivate)) typedef struct _threadinfo { gchar *host; gchar *port; struct addrinfo *addr; struct addrinfo *current; struct addrinfo *tmp; pthread_mutex_t *mutex_idle; guint idle_id; gint ret; gchar *message; GmNet *net; } threadinfo; struct _GmNetPrivate { int socket; /**< the connection socket, is -1 if not connected */ GIOChannel *channel; /**< the channel which is used to 'listen' on the socket via the glib main loop */ guint source; /**< the id of the socket watch */ guint connect_timeout_id; /**< the connect timeout id */ guint connect_check_id; /**< the connect timeout id */ struct timeval last_connected; /**< contains when the last connect happened */ int tn_last; /**< used for telnet */ int tn_subneg; /**< used for telnet */ pthread_t thread; threadinfo *thread_info; pthread_mutex_t mutex_idle; struct addrinfo *addr; struct addrinfo *current; GmNetState state; /**< state of the connection */ gchar *current_host; gchar *current_port; }; /* Signals */ enum { STATE_CHANGING, STATE_CHANGED, NET_ERROR, BYTES_RECV, NUM_SIGNALS }; static guint net_signals[NUM_SIGNALS] = {0}; G_DEFINE_TYPE(GmNet, gm_net, G_TYPE_OBJECT) /* These values taken from RFC 854 and RFC 857. */ #define TN_WILL 251 #define TN_WONT 252 #define TN_DO 253 #define TN_DONT 254 #define TN_IAC 255 /* Interpret As Command */ #define TN_SB 250 /* start of subnegotiation */ #define TN_SE 240 /* end of subnegotiaton */ void gm_net_connect_next(GmNet *net); gboolean on_gm_net_input_recv(GIOChannel * source, GIOCondition condition, GmNet *net); gboolean on_gm_net_connect_check(GIOChannel * source, GIOCondition condition, GmNet *net); gboolean on_gm_net_connect_timeout(GmNet *net); static void gm_net_free_thread_info(GmNet *net) { g_free(net->priv->thread_info->message); g_free(net->priv->thread_info->host); g_free(net->priv->thread_info->port); g_free(net->priv->thread_info); net->priv->thread_info = NULL; } static void gm_net_close_thread(GmNet *net) { if (net->priv->thread != 0) { pthread_kill(net->priv->thread, SIGKILL); if (net->priv->thread_info->idle_id) { g_source_remove(net->priv->thread_info->idle_id); } gm_net_free_thread_info(net); } } static void gm_net_finalize(GObject *object) { GmNet *net = GM_NET(object); gm_net_close_thread(net); g_free(net->priv->current_host); g_free(net->priv->current_port); G_OBJECT_CLASS(gm_net_parent_class)->finalize(object); } static void gm_net_class_init(GmNetClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = gm_net_finalize; net_signals[STATE_CHANGING] = g_signal_new("state_changing", G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(GmNetClass, state_changing), NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); net_signals[STATE_CHANGED] = g_signal_new("state_changed", G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(GmNetClass, state_changed), NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); net_signals[NET_ERROR] = g_signal_new("net_error", G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(GmNetClass, net_error), NULL, NULL, gm_marshal_VOID__STRING_INT, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_INT); net_signals[BYTES_RECV] = g_signal_new("bytes_recv", G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(GmNetClass, bytes_recv), NULL, NULL, gm_marshal_VOID__STRING_UINT, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_UINT); g_type_class_add_private(object_class, sizeof(GmNetPrivate)); } static void gm_net_init(GmNet *net) { net->priv = GM_NET_GET_PRIVATE(net); net->priv->state = GM_NET_STATE_DISCONNECTED; net->priv->addr = NULL; net->priv->current = NULL; net->priv->channel = NULL; net->priv->source = 0; net->priv->connect_timeout_id = 0; net->priv->connect_check_id = 0; pthread_mutex_init(&(net->priv->mutex_idle), NULL); } static void gm_net_set_host(GmNet *net, gchar const *host) { g_free(net->priv->current_host); net->priv->current_host = g_strdup(host); } static void gm_net_set_port(GmNet *net, gchar const *port) { g_free(net->priv->current_port); net->priv->current_port = g_strdup(port); } void gm_net_set_state(GmNet *net, GmNetState state) { g_signal_emit(net, net_signals[STATE_CHANGING], 0, state); net->priv->state = state; g_signal_emit(net, net_signals[STATE_CHANGED], 0, state); } void gm_net_clean_disconnection(GmNet *net) { GError *err = NULL; if (!net->priv->channel) { gm_debug_msg(DEBUG_DEFAULT, "GmNet.CleanDisconnection: NOT clean for %d", net->priv->socket); return; } gm_debug_msg(DEBUG_DEFAULT, "GmNet.CleanDisconnection: clean disconnect for %d", net->priv->socket); // Shutdown the channel g_io_channel_shutdown(net->priv->channel, TRUE, &err); if (err) { gm_debug_msg(DEBUG_DEFAULT, "GmNet.CleanDisconnection: error on channel shutdown: " "%s", err->message); g_error_free(err); err = NULL; } g_io_channel_unref(net->priv->channel); if (net->priv->source) { g_source_remove(net->priv->source); net->priv->source = 0; } net->priv->channel = NULL; net->priv->socket = -1; net->priv->tn_last = 0; net->priv->tn_subneg = 0; gm_net_set_state(net, GM_NET_STATE_DISCONNECTED); } void gm_net_dirty_disconnection(GmNet *net, int err) { gchar *msg; // Pff, stupid, we print a message and pass it on to clean_disconnection gm_debug_msg(DEBUG_DEFAULT, "GmNet.DirtyDisconnection: dirty disconnect %d", net->priv->socket); msg = g_strdup_printf(_("Connection lost... (%s)"), strerror(err)); g_signal_emit(net, net_signals[NET_ERROR], 0, msg, GM_NET_ERROR_DISCONNECTED); g_free(msg); gm_net_clean_disconnection(net); } void gm_net_connect_succeed(GmNet *net) { freeaddrinfo(net->priv->addr); net->priv->addr = NULL; net->priv->current = NULL; net->priv->source = g_io_add_watch(net->priv->channel, G_IO_IN | G_IO_HUP, (GIOFunc)on_gm_net_input_recv, net); if (net->priv->connect_timeout_id != 0) { g_source_remove(net->priv->connect_timeout_id); net->priv->connect_timeout_id = 0; } if (net->priv->connect_check_id != 0) { g_source_remove(net->priv->connect_check_id); net->priv->connect_check_id = 0; } gettimeofday(&(net->priv->last_connected), NULL); gm_net_set_state(net, GM_NET_STATE_CONNECTED); } void gm_net_connect_failed(GmNet *net, gchar *err, gint code) { if (net->priv->channel) { g_io_channel_shutdown(net->priv->channel, TRUE, NULL); g_io_channel_unref(net->priv->channel); net->priv->channel = NULL; } g_signal_emit(net, net_signals[NET_ERROR], 0, err, GM_NET_ERROR_CONNECTING); if (net->priv->addr && net->priv->current->ai_next) { net->priv->current = net->priv->current->ai_next; gm_net_connect_next(net); } else { net->priv->socket = -1; if (net->priv->addr) { freeaddrinfo(net->priv->addr); net->priv->addr = NULL; net->priv->current = NULL; } g_signal_emit(net, net_signals[NET_ERROR], 0, _("Could not make connection..."), GM_NET_ERROR_CONNECTING); gm_net_set_state(net, GM_NET_STATE_DISCONNECTED); } } void gm_net_handle_telnet(GmNet *net, unsigned char *buf, int *len) { int i, j; unsigned char c; j = 0; for (i = 0; i < *len; ++i) { c = buf[i]; if (net->priv->tn_last) { switch (net->priv->tn_last) { case TN_WILL: case TN_WONT: case TN_DO: case TN_DONT: net->priv->tn_last = 0; break; case TN_IAC: switch (c) { case TN_WILL: case TN_WONT: case TN_DO: case TN_DONT: net->priv->tn_last = c; break; case TN_SB: net->priv->tn_subneg = 1; net->priv->tn_last = 0; break; case TN_SE: net->priv->tn_subneg = 0; net->priv->tn_last = 0; break; case TN_IAC: if (!net->priv->tn_subneg) { buf[j] = c; ++j; } net->priv->tn_last = 0; break; default: net->priv->tn_last = 0; break; } } } else if (c == TN_IAC) { net->priv->tn_last = TN_IAC; } else if (net->priv->tn_subneg) { continue; } else { buf[j] = c; ++j; } } *len = j; //Since j-- is the last written char } static gboolean idle_proceed_connect_next(gpointer user_data) { threadinfo *info = (threadinfo *)user_data; GmNet *net = info->net; struct addrinfo *tmp; gint ret = info->ret; gint result; info->idle_id = 0; pthread_join(net->priv->thread, NULL); net->priv->thread = 0; if (info->ret != 0) { gm_debug_msg(DEBUG_DEFAULT, "GmNet.ConnectNext: getnameinfo error: %s", gai_strerror(info->ret)); gm_net_free_thread_info(net); gm_net_connect_failed(net, (gchar *)gai_strerror(ret), ret); return FALSE; } gm_net_set_host(net, info->host); gm_net_set_port(net, info->port); gm_net_set_state(net, GM_NET_STATE_TRY_ADDRESS); tmp = info->tmp; gm_net_free_thread_info(net); net->priv->socket = socket(tmp->ai_family, tmp->ai_socktype, tmp->ai_protocol); if (net->priv->socket < 0) { gm_net_connect_failed(net, strerror(errno), errno); } else { fcntl(net->priv->socket, F_SETFL, fcntl(net->priv->socket, F_GETFL) | O_NONBLOCK); if ((result = connect(net->priv->socket, tmp->ai_addr, net->priv->addr->ai_addrlen)) == -1 && errno != EINPROGRESS) { gm_net_connect_failed(net, strerror(errno), errno); } else { net->priv->channel = g_io_channel_unix_new(net->priv->socket); g_io_channel_set_close_on_unref(net->priv->channel, TRUE); if (result == 0) { gm_net_connect_succeed(net); } else { net->priv->connect_check_id = g_io_add_watch(net->priv->channel, G_IO_OUT|G_IO_ERR, (GIOFunc)on_gm_net_connect_check, net); net->priv->connect_timeout_id = g_timeout_add(5000, (GSourceFunc)on_gm_net_connect_timeout, net); } } } return FALSE; } void mutex_unlock(void *ptr) { pthread_mutex_unlock((pthread_mutex_t *)ptr); } void * nameinfo_thread(void *ptr) { threadinfo *info = (threadinfo *)ptr; char host[NI_MAXHOST], port[NI_MAXSERV]; info->tmp = info->current; info->ret = getnameinfo(info->tmp->ai_addr, info->tmp->ai_addrlen, host, NI_MAXHOST, port, NI_MAXSERV, NI_NUMERICHOST | NI_NUMERICSERV); if (info->ret == 0) { info->host = g_strdup(host); info->port = g_strdup(port); } pthread_mutex_lock(info->mutex_idle); pthread_cleanup_push(mutex_unlock, info->mutex_idle); info->idle_id = g_idle_add(idle_proceed_connect_next, info); pthread_cleanup_pop(1); return NULL; } void gm_net_connect_next(GmNet *net) { threadinfo *info; if (net->priv->current == NULL) { return; } else { info = g_new0(threadinfo, 1); info->net = net; info->current = net->priv->current; info->mutex_idle = &(net->priv->mutex_idle); net->priv->thread_info = info; pthread_create(&(net->priv->thread), NULL, nameinfo_thread, (void *)info); } } static gboolean idle_proceed_addrinfo(gpointer user_data) { threadinfo *info = (threadinfo *)user_data; GmNet *net = info->net; info->idle_id = 0; pthread_join(net->priv->thread, NULL); net->priv->thread = 0; if (info->ret != 0) { gm_debug_msg(DEBUG_DEFAULT, "GmNet.Connect: getaddrinfo failed: %s", gai_strerror(info->ret)); gm_net_connect_failed(net, (gchar *)gai_strerror(info->ret), info->ret); gm_net_free_thread_info(net); return FALSE; } net->priv->addr = info->addr; if (info->addr != NULL) { net->priv->current = info->addr; gm_net_free_thread_info(net); gm_net_connect_next(net); } else { gm_net_free_thread_info(net); gm_net_connect_failed(net, _("No addresses available"), 0); } return FALSE; } static void * addrinfo_thread(void *ptr) { threadinfo *info = (threadinfo *)ptr; struct addrinfo hint; memset(&hint, 0, sizeof(hint)); hint.ai_flags = 0; hint.ai_family = AF_UNSPEC; hint.ai_socktype = SOCK_STREAM; hint.ai_protocol = IPPROTO_TCP; gm_debug_msg(DEBUG_DEFAULT, "GmNet.Connect: getaddrinfo: %s : %s", info->host, info->port); info->ret = getaddrinfo(info->host, info->port, &hint, &(info->addr)); pthread_mutex_lock(info->mutex_idle); pthread_cleanup_push(mutex_unlock, info->mutex_idle); info->idle_id = g_idle_add(idle_proceed_addrinfo, info); pthread_cleanup_pop(1); return NULL; } /* Public */ GmNet * gm_net_new() { GmNet *net = GM_NET(g_object_new(GM_TYPE_NET, NULL)); return net; } GmNetState gm_net_state(GmNet *net) { return net->priv->state; } void gm_net_connect(GmNet *net, const gchar *host, const gchar *port) { char shost[NI_MAXHOST], sport[NI_MAXSERV]; threadinfo *info; if (net->priv->state != GM_NET_STATE_DISCONNECTED) { return; } snprintf(shost, NI_MAXHOST - 1, "%s", host); snprintf(sport, NI_MAXSERV - 1, "%s", port); gm_net_set_host(net, shost); gm_net_set_port(net, sport); gm_net_set_state(net, GM_NET_STATE_CONNECTING); info = g_new0(threadinfo, 1); info->host = g_strdup(shost); info->port = g_strdup(sport); info->net = net; info->mutex_idle = &(net->priv->mutex_idle); net->priv->thread_info = info; pthread_create(&(net->priv->thread), NULL, addrinfo_thread, (void *)info); } void gm_net_disconnect(GmNet *net) { // thread running if (net->priv->state != GM_NET_STATE_DISCONNECTED) { gm_net_close_thread(net); gm_net_set_state(net, GM_NET_STATE_DISCONNECTING); if (net->priv->connect_timeout_id != 0) { g_source_remove(net->priv->connect_timeout_id); net->priv->connect_timeout_id = 0; } if (net->priv->connect_check_id != 0) { g_source_remove(net->priv->connect_check_id); net->priv->connect_check_id = 0; } gm_net_clean_disconnection(net); } } void gm_net_send_line(GmNet *net, gchar *line) { gchar *send_line; send_line = (gchar *)(g_strconcat(line, "\r\n", NULL)); gm_net_send(net, send_line); g_free(send_line); } void gm_net_send(GmNet *net, gchar *text) { int result; fd_set connect_set; if (net->priv->state == GM_NET_STATE_CONNECTED) { gm_debug_msg(DEBUG_DEFAULT, "GmNet.Send: %s", text); if ((result = send(net->priv->socket, text, strlen(text), 0)) == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { FD_ZERO(&connect_set); FD_SET(net->priv->socket, &connect_set); // Wait for sending to be done select(net->priv->socket + 1, NULL, &connect_set, NULL, NULL); } else if (result == -1) { gm_debug_msg(DEBUG_DEFAULT, "GmNet.Send: error on sending line: %s", strerror(errno)); gm_net_dirty_disconnection(net, errno); } } else { g_signal_emit(net, net_signals[NET_ERROR], 0, _("Not connected"), GM_NET_ERROR); gm_debug_msg(DEBUG_DEFAULT, "GmNet.Send: not connected!"); } } const gchar * gm_net_current_host(GmNet *net) { return net->priv->current_host; } const gchar * gm_net_current_port(GmNet *net) { return net->priv->current_port; } /* Callbacks */ #define MAX_RECV_BUF 1024 gboolean on_gm_net_input_recv(GIOChannel * source, GIOCondition condition, GmNet *net) { unsigned char lbuf[MAX_RECV_BUF]; int len; if (condition == G_IO_HUP) { gm_net_set_state(net, GM_NET_STATE_DISCONNECTING); gm_net_clean_disconnection(net); return FALSE; } if (net->priv->state != GM_NET_STATE_CONNECTED) { gm_debug_msg(DEBUG_DEFAULT, "GmNet.OnInputRecv: not connected!"); return FALSE; } // Break the received line by newline (skip \r) len = recv(net->priv->socket, lbuf, MAX_RECV_BUF - 2, 0); gm_debug_msg(DEBUG_DEFAULT, "GmNet.OnInputRecv: received %d bytes", len); if (len < 1) { // Disconnected, either clean or dirty // (shouldn't this be caught by G_IO_HUP?) if (len < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return TRUE; } gm_net_set_state(net, GM_NET_STATE_DISCONNECTING); gm_net_dirty_disconnection(net, errno); } else { gm_net_set_state(net, GM_NET_STATE_DISCONNECTING); gm_net_clean_disconnection(net); } return FALSE; } else { // It's fine, we have text! gm_net_handle_telnet(net, lbuf, &len); g_signal_emit(net, net_signals[BYTES_RECV], 0, lbuf, len); } return TRUE; } gboolean on_gm_net_connect_check(GIOChannel * source, GIOCondition condition, GmNet *net) { int option = 0; socklen_t optionsize = sizeof(option); if (net->priv->connect_timeout_id != 0) { g_source_remove(net->priv->connect_timeout_id); net->priv->connect_timeout_id = 0; } if (condition == G_IO_ERR) { getsockopt(net->priv->socket, SOL_SOCKET, SO_ERROR, &option, &optionsize); gm_net_connect_failed(net, strerror(option), option); } else { gm_net_connect_succeed(net); } return FALSE; } gboolean on_gm_net_connect_timeout(GmNet *net) { net->priv->connect_timeout_id = 0; if (net->priv->connect_check_id != 0) { g_source_remove(net->priv->connect_check_id); net->priv->connect_check_id = 0; } gm_net_connect_failed(net, _("Connect timeout (5)"), 0); return FALSE; }