diff --git a/.github/workflows/appimage-build.yml b/.github/workflows/appimage-build.yml index e300a878..50840a5c 100644 --- a/.github/workflows/appimage-build.yml +++ b/.github/workflows/appimage-build.yml @@ -29,6 +29,7 @@ jobs: build-essential pkg-config meson ninja-build cmake \ gettext \ libcanberra-dev libglib2.0-dev \ + libminiupnpc-dev \ libarchive-dev \ libgtk-3-dev \ libwayland-client0 libwayland-cursor0 libwayland-egl1 \ diff --git a/meson.build b/meson.build index ffcfd62f..2f2f9f65 100644 --- a/meson.build +++ b/meson.build @@ -18,6 +18,8 @@ libgmodule_dep = dependency('gmodule-2.0') libcanberra_dep = dependency('libcanberra', version: '>= 0.22', required: get_option('libcanberra')) +miniupnpc_dep = dependency('miniupnpc', version: '>= 2.0.0', + required: get_option('miniupnpc')) dbus_dep = dependency('gio-2.0', required: get_option('dbus')) global_deps = [] @@ -40,6 +42,7 @@ config_h.set10('ENABLE_NLS', true) config_h.set('USE_OPENSSL', libssl_dep.found()) config_h.set('USE_LIBCANBERRA', libcanberra_dep.found()) config_h.set('USE_DBUS', dbus_dep.found()) +config_h.set('USE_MINIUPNPC', miniupnpc_dep.found()) config_h.set('USE_PLUGIN', get_option('plugin')) config_h.set('USE_GTK_FRONTEND', get_option('gtk-frontend')) @@ -183,6 +186,7 @@ if meson.version().version_compare('>= 0.55.0') 'Plugin Support': get_option('plugin'), 'DBus Support': dbus_dep.found(), 'libcanberra': libcanberra_dep.found(), + 'miniupnpc': miniupnpc_dep.found(), }, section: 'Features') summary({ diff --git a/meson_options.txt b/meson_options.txt index 04b0265e..a52178b6 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -19,6 +19,9 @@ option('dbus', type: 'feature', value: 'auto', option('libcanberra', type: 'feature', value: 'auto', description: 'Support for sound alerts, Unix only' ) +option('miniupnpc', type: 'feature', value: 'auto', + description: 'Support for DCC Universal Plug & Play, Unix only' +) option('appindicator', type: 'feature', value: 'auto', description: 'Use Ayatana/AppIndicator-based tray backend for GTK frontend (non-Windows only)' ) diff --git a/src/common/cfgfiles.c b/src/common/cfgfiles.c index c8fcf760..4542943d 100644 --- a/src/common/cfgfiles.c +++ b/src/common/cfgfiles.c @@ -562,6 +562,7 @@ const struct prefs vars[] = {"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR}, {"net_reconnect_delay", P_OFFINT (hex_net_reconnect_delay), TYPE_INT}, {"net_throttle", P_OFFINT (hex_net_throttle), TYPE_BOOL}, + {"net_upnp", P_OFFINT (hex_net_upnp), TYPE_BOOL}, {"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT}, {"notify_whois_online", P_OFFINT (hex_notify_whois_online), TYPE_BOOL}, @@ -811,6 +812,7 @@ load_default_config(void) prefs.hex_irc_whois_front = 1; prefs.hex_net_auto_reconnect = 1; prefs.hex_net_throttle = 1; + prefs.hex_net_upnp = 1; prefs.hex_stamp_log = 1; prefs.hex_stamp_text = 1; prefs.hex_text_autocopy_text = 1; diff --git a/src/common/dcc.c b/src/common/dcc.c index a8fb68fa..69e5880e 100644 --- a/src/common/dcc.c +++ b/src/common/dcc.c @@ -57,6 +57,7 @@ #include "text.h" #include "url.h" #include "zoitechatc.h" +#include "upnp.h" /* Setting _FILE_OFFSET_BITS to 64 doesn't change lseek to use off64_t on Windows, so override lseek to the version that does */ #if defined(WIN32) && (!defined(__MINGW32__) && !defined(__MINGW64__)) @@ -371,6 +372,12 @@ dcc_connect_sok (struct DCC *dcc) static void dcc_close (struct DCC *dcc, enum dcc_state dccstat, int destroy) { + if (dcc->port > 0) + { + if (prefs.hex_net_upnp) + upnp_rem_redir(dcc->port); + } + if (dcc->wiotag) { fe_input_remove (dcc->wiotag); @@ -1711,6 +1718,8 @@ dcc_listen_init (struct DCC *dcc, session *sess) set_blocking (dcc->sok); dcc->iotag = fe_input_add (dcc->sok, FIA_READ|FIA_EX, dcc_accept, dcc); + if (prefs.hex_net_upnp) + upnp_add_redir(inet_ntoa(SAddr.sin_addr), dcc->port); return TRUE; } diff --git a/src/common/meson.build b/src/common/meson.build index c3a9219f..893fc9d8 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -24,7 +24,8 @@ common_sources = [ 'tree.c', 'url.c', 'userlist.c', - 'util.c' + 'util.c', + 'upnp.c' ] common_sysinfo_deps = [] @@ -115,6 +116,10 @@ if libssl_dep.found() common_deps += libssl_dep endif +if miniupnpc_dep.found() + common_deps += miniupnpc_dep +endif + if dbus_dep.found() subdir('dbus') common_deps += zoitechat_dbus_dep diff --git a/src/common/upnp.c b/src/common/upnp.c new file mode 100644 index 00000000..c072b963 --- /dev/null +++ b/src/common/upnp.c @@ -0,0 +1,111 @@ +/* + * Copyright (C) Thomas Bernard + * Copyright (C) HexChat contributors + * Copyright (C) ZoiteChat contributors + */ + +#include +#include + +#include "upnp.h" + +#ifdef USE_MINIUPNPC +#include +#include +#include + +static struct UPNPUrls urls; +static struct IGDdatas data; +static int ready; +static char upnp_lanaddr[64]; + +void +upnp_init(void) +{ + int err = 0; + int igd = 0; + char lanaddr[64] = {0}; + struct UPNPDev *devlist; + + memset(&urls, 0, sizeof(urls)); + memset(&data, 0, sizeof(data)); + ready = 0; + upnp_lanaddr[0] = 0; + + devlist = upnpDiscover(2000, NULL, NULL, 0, 0, 2, &err); + if (!devlist) + devlist = upnpDiscover(2000, NULL, NULL, 0, 1, 2, &err); + if (!devlist) + return; + + igd = UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr)); + if (igd == 1 || igd == 2 || igd == 3) + { + ready = 1; + snprintf(upnp_lanaddr, sizeof(upnp_lanaddr), "%s", lanaddr); + } + + freeUPNPDevlist(devlist); +} + +void +upnp_add_redir(const char *addr, int port) +{ + char port_str[16]; + const char *map_addr; + int r; + + if (!ready) + upnp_init(); + if (!ready) + return; + + map_addr = upnp_lanaddr[0] ? upnp_lanaddr : addr; + if (!map_addr || !map_addr[0]) + return; + + snprintf(port_str, sizeof(port_str), "%d", port); + r = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, + port_str, port_str, NULL, "zoitechat", "TCP", NULL, NULL); + if (r != UPNPCOMMAND_SUCCESS) + r = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, + port_str, port_str, map_addr, "zoitechat", "TCP", NULL, NULL); + if (r != UPNPCOMMAND_SUCCESS) + return; +} + +void +upnp_rem_redir(int port) +{ + char port_str[16]; + + if (!ready) + upnp_init(); + if (!ready) + return; + + snprintf(port_str, sizeof(port_str), "%d", port); + UPNP_DeletePortMapping(urls.controlURL, data.first.servicetype, port_str, "TCP", NULL); +} + +#else + +void +upnp_init(void) +{ +} + +void +upnp_add_redir(const char *addr, int port) +{ + (void)addr; + (void)port; +} + +void +upnp_rem_redir(int port) +{ + (void)port; +} + +#endif diff --git a/src/common/upnp.h b/src/common/upnp.h new file mode 100644 index 00000000..9b64dd6f --- /dev/null +++ b/src/common/upnp.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) Thomas Bernard + * Copyright (C) HexChat contributors + * Copyright (C) ZoiteChat contributors + */ + +#ifndef ZOITECHAT_UPNP_H +#define ZOITECHAT_UPNP_H + +void upnp_init(void); +void upnp_add_redir(const char *addr, int port); +void upnp_rem_redir(int port); + +#endif diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c index aba1c32e..7207bca7 100644 --- a/src/common/zoitechat.c +++ b/src/common/zoitechat.c @@ -54,6 +54,7 @@ #include "text.h" #include "url.h" #include "zoitechatc.h" +#include "upnp.h" #if ! GLIB_CHECK_VERSION (2, 36, 0) #include /* for g_type_init() */ @@ -961,6 +962,8 @@ xchat_init (void) sound_load (); notify_load (); ignore_load (); + if (prefs.hex_net_upnp) + upnp_init (); sts_init (); g_snprintf (buf, sizeof (buf), diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h index 6f8591c3..04fdfb8c 100644 --- a/src/common/zoitechat.h +++ b/src/common/zoitechat.h @@ -202,6 +202,7 @@ struct zoitechatprefs unsigned int hex_net_auto_reconnectonfail; unsigned int hex_net_proxy_auth; unsigned int hex_net_throttle; + unsigned int hex_net_upnp; unsigned int hex_notify_whois_online; unsigned int hex_perl_warnings; unsigned int hex_stamp_log; diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index d2dc0882..4697785c 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -650,7 +650,7 @@ static const setting network_settings[] = {ST_NUMBER, N_("First DCC listen port:"), P_OFFINTNL(hex_dcc_port_first), 0, 0, 65535}, {ST_NUMBER, N_("Last DCC listen port:"), P_OFFINTNL(hex_dcc_port_last), 0, (const char **)N_("!Leave ports at zero for full range."), 65535}, - + {ST_TOGGLE, N_("Enable UPnP port mapping for DCC"), P_OFFINTNL(hex_net_upnp), 0, 0, 0}, {ST_HEADER, N_("Proxy Server"), 0, 0, 0, 0}, {ST_ENTRY, N_("Hostname:"), P_OFFSETNL(hex_net_proxy_host), 0, 0, sizeof prefs.hex_net_proxy_host}, {ST_NUMBER, N_("Port:"), P_OFFINTNL(hex_net_proxy_port), 0, 0, 65535},