diff --git a/.gitignore b/.gitignore index 8c04291e..61e3525a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ Makefile.in /stamp-h1 src/modbus-version.h src/win32/modbus.dll.manifest +src/stamp-h3 tests/unit-test.h # mkdocs diff --git a/Makefile.am b/Makefile.am index 5e9fc7ce..63c16bde 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,6 +1,8 @@ CLEANFILES = ACLOCAL_AMFLAGS = -I m4 ${ACLOCAL_FLAGS} -AM_MAKEFLAGS = --no-print-directory + +# GNU make-ism, precludes builds with BSD and Sun Makes at least +#AM_MAKEFLAGS = --no-print-directory pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = libmodbus.pc diff --git a/README.md b/README.md index bdf13994..5a36e704 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,13 @@ You will only need to install automake, autoconf, libtool and a C compiler (gcc or clang) to compile the library and asciidoc and xmlto to generate the documentation (optional). -To install, just run the usual dance, `./configure && make install`. Run -`./autogen.sh` first to generate the `configure` script if required. +To install, just run the usual dance, `./configure && make install`. +Run `./autogen.sh` first to generate the `configure` script if required. +You may be required to use `gmake` on platforms where default `make` is +different (BSD make, Sun make, etc.) You may also require GNU `tar` +impementation (or some other archivation tool, possibly `cpio`, that +would be recognized by your installed version of `automake` as supporting +the "pax" archive format) to pass the optional `make distcheck` routine. You can change installation directory with prefix option, eg. `./configure --prefix=/usr/local/`. You have to check that the installation library path is diff --git a/configure.ac b/configure.ac index 920612f0..1b8d1c8f 100644 --- a/configure.ac +++ b/configure.ac @@ -30,11 +30,17 @@ AC_INIT([libmodbus], AC_CONFIG_SRCDIR([src/modbus.c]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([check-news foreign 1.11 silent-rules tar-pax subdir-objects]) +AC_PROG_CPP AC_PROG_CC +AM_PROG_CC_C_O AC_USE_SYSTEM_EXTENSIONS AC_SYS_LARGEFILE AC_CONFIG_MACRO_DIR([m4]) -AM_SILENT_RULES([yes]) +dnl This feature seems to require automake-1.13 or newer (1.11+ by other info) +dnl On very old systems can comment it away with little loss (then automake-1.10 +dnl is known to suffice): +m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])], + [AC_MSG_NOTICE([Silent Rules feature not defined in this automake version, skipped])]) LIBMODBUS_VERSION_MAJOR=libmodbus_version_major LIBMODBUS_VERSION_MINOR=libmodbus_version_minor @@ -89,7 +95,6 @@ AC_CHECK_HEADERS([ \ linux/serial.h \ netdb.h \ netinet/in.h \ - netinet/ip.h \ netinet/tcp.h \ sys/ioctl.h \ sys/params.h \ @@ -101,6 +106,17 @@ AC_CHECK_HEADERS([ \ unistd.h \ ]) +dnl On some platforms like FreeBSD and OpenIndiana (illumos) the +dnl netinet/ip.h requires netinet/in.h explicitly included first: +AC_CHECK_HEADERS([ \ + netinet/ip.h \ +], [], [], [ + AC_INCLUDES_DEFAULT +#if HAVE_NETINET_IN_H +# include +#endif +]) + # Cygwin defines IPTOS_LOWDELAY but can't handle that flag so it's necessary to # workaround that problem and Cygwin doesn't define MSG_DONTWAIT. AC_CHECK_DECLS([__CYGWIN__]) @@ -109,7 +125,7 @@ AC_CHECK_DECLS([__CYGWIN__]) AC_SEARCH_LIBS(accept, network socket) # Checks for library functions. -AC_CHECK_FUNCS([accept4 gai_strerror getaddrinfo gettimeofday inet_pton inet_ntop select socket strerror strlcpy]) +AC_CHECK_FUNCS([accept4 gai_strerror getaddrinfo gettimeofday select socket strerror strlcpy]) # Required for MinGW with GCC v4.8.1 on Win7 AC_DEFINE(WINVER, 0x0501, _) @@ -129,12 +145,19 @@ AC_TYPE_UINT32_T AC_TYPE_UINT8_T if test "$os_cygwin" = "false"; then + AC_CHECK_HEADERS([windows.h], HAVE_WINDOWS_H=yes) + # Required for getaddrinfo (TCP IP - IPv6) AC_CHECK_HEADERS([winsock2.h], HAVE_WINSOCK2_H=yes) if test "x$HAVE_WINSOCK2_H" = "xyes"; then LIBS="$LIBS -lws2_32" - AC_SUBST(LIBS) + AC_SUBST(LIBS) fi + + dnl Can bring inet_ntop()/inet_pton()... or not, depending on distro + dnl (e.g. mingw "native" with MSYS2 or cross-built from Linux); that + dnl is further checked below: + AC_CHECK_HEADERS([ws2tcpip.h], HAVE_WS2TCPIP_H=yes) fi if test "$os_sunos" = "true"; then @@ -153,7 +176,98 @@ WARNING_CFLAGS="-Wall \ -Wsign-compare -Wchar-subscripts \ -Wstrict-prototypes -Wshadow \ -Wformat-security" -AC_SUBST([WARNING_CFLAGS]) + +dnl FIXME: define more thoroughly if C++ code ever appears here +WARNING_CXXFLAGS="$WARNING_CFLAGS" + +dnl Adapted from NUT v2.8.2 configure.ac : +myCFLAGS="$CFLAGS" +AS_IF([test "${GCC}" = "yes"], + [CFLAGS="$myCFLAGS -Werror -Werror=implicit-function-declaration"], + [dnl # Don't know what to complain about for unknown compilers + dnl # FIXME: We presume here they have at least a "-Werror" option + CFLAGS="$myCFLAGS -Werror" + ]) + +AC_CACHE_CHECK([for inet_ntop() with IPv4 and IPv6 support], + [ac_cv_func_inet_ntop], + [AC_LANG_PUSH([C]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[ +#if HAVE_WINDOWS_H +# undef inline +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +# if HAVE_WINSOCK2_H +# include +# endif +# if HAVE_WS2TCPIP_H +# include +# endif +#else +# include +#endif +#include +]], + [[/* const char* inet_ntop(int af, const void* src, char* dst, size_t cnt); */ +char buf[128]; +printf("%s", inet_ntop(AF_INET, "1.2.3.4", buf, 10)); +printf("%s", inet_ntop(AF_INET6, "::1", buf, 10)) +/* autoconf adds ";return 0;" */ +]])], + [ac_cv_func_inet_ntop=yes], [ac_cv_func_inet_ntop=no] + ) + AC_LANG_POP([C]) +]) +AS_IF([test x"${ac_cv_func_inet_ntop}" = xyes], + [AC_DEFINE([HAVE_INET_NTOP], 1, [defined if system has the inet_ntop() method])], + [AC_MSG_WARN([Required C library routine inet_ntop() not found]) + AS_IF([test "${os_win32}" = "true"], [AC_MSG_WARN([Windows antivirus might block this test])]) + ] +) + +AC_CACHE_CHECK([for inet_pton() with IPv4 and IPv6 support], + [ac_cv_func_inet_pton], + [AC_LANG_PUSH([C]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[ +#if HAVE_WINDOWS_H +# undef inline +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +# if HAVE_WINSOCK2_H +# include +# endif +# if HAVE_WS2TCPIP_H +# include +# endif +#else +# include +#endif +#include +]], + [[/* int inet_pton(int af, const char *src, char *dst); */ +struct in_addr ipv4; +struct in6_addr ipv6; +printf("%i ", inet_pton(AF_INET, "1.2.3.4", &ipv4)); +printf("%i ", inet_pton(AF_INET6, "::1", &ipv6)) +/* autoconf adds ";return 0;" */ +]])], + [ac_cv_func_inet_pton=yes], [ac_cv_func_inet_pton=no] + ) + AC_LANG_POP([C]) +]) +AS_IF([test x"${ac_cv_func_inet_pton}" = xyes], + [AC_DEFINE([HAVE_INET_PTON], 1, [defined if system has the inet_pton() method])], + [AC_MSG_WARN([Required C library routine inet_pton() not found]) + AS_IF([test "${os_win32}" = "true"], [AC_MSG_WARN([Windows antivirus might block this test])]) + ] +) +CFLAGS="$myCFLAGS" # Build options AC_ARG_ENABLE(tests, @@ -186,10 +300,29 @@ AS_IF([test "x$enable_debug" = "xyes"], [ CXXFLAGS="-O2" ]) +dnl NOTE: Do not pass these among C(XX)FLAGS to the configure script itself, +dnl they can break common tests unexpectedly. Variants below should work for +dnl GCC and CLANG, and other compilers that emulate them in terms of CLI API. +AC_ARG_ENABLE([Werror], + [AS_HELP_STRING([--enable-Werror], + [Enable compilation failure on warnings (default is no)])], + [enable_Werror=$enableval], + [enable_Werror=no]) +AS_IF([test "x$enable_Werror" = "xyes"], [ + WARNING_CFLAGS="$WARNING_CFLAGS -Werror" + WARNING_CXXFLAGS="$WARNING_CXXFLAGS -Werror" +]) + +AC_SUBST([WARNING_CFLAGS]) +AC_SUBST([WARNING_CXXFLAGS]) + +_PKG_VER_TITLE="$PACKAGE $VERSION" +_PKG_VER_SEPARATOR="`echo "${_PKG_VER_TITLE}" | sed 's,.,=,g'`" \ +&& test x"${_PKG_VER_SEPARATOR}" != x || _PKG_VER_SEPARATOR="================" AC_OUTPUT AC_MSG_RESULT([ - $PACKAGE $VERSION - =============== + ${_PKG_VER_TITLE} + ${_PKG_VER_SEPARATOR} prefix: ${prefix} sysconfdir: ${sysconfdir} @@ -200,5 +333,8 @@ AC_MSG_RESULT([ cflags: ${CFLAGS} ${WARNING_CFLAGS} ldflags: ${LDFLAGS} + build shared lib: ${enable_shared} + build static lib: ${enable_static} + tests: ${enable_tests} ]) diff --git a/src/modbus-tcp.c b/src/modbus-tcp.c index 8e5e37cd..1084fc8b 100644 --- a/src/modbus-tcp.c +++ b/src/modbus-tcp.c @@ -68,6 +68,217 @@ #include "modbus-tcp.h" #ifdef OS_WIN32 + +// inet_ntop() and inet_pton() fallbacks picked up from NUT v2.8.2 common/wincompat.c: +#if !HAVE_INET_NTOP +static const char *inet_ntop(int af, const void *src, char *dst, size_t cnt) +{ + /* Instead of WSAAddressToString() consider getnameinfo() if this would in fact + * return decorated addresses (brackets, ports...) as discussed below: + * https://users.ipv6.narkive.com/RXpR5aML/windows-and-inet-ntop-vs-wsaaddresstostring + * https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getnameinfo + * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaaddresstostringa + */ + switch (af) { + case AF_INET: { + struct sockaddr_in srcaddr; + memset(&srcaddr, 0, sizeof(struct sockaddr_in)); + memcpy(&(srcaddr.sin_addr), src, sizeof(srcaddr.sin_addr)); + srcaddr.sin_family = af; + if (WSAAddressToString((struct sockaddr *) &srcaddr, + sizeof(struct sockaddr_in), + 0, + dst, + (LPDWORD) &cnt) != 0) { + WSAGetLastError(); + return NULL; + } + } break; + + case AF_INET6: + /* NOTE: Since WinXP SP1, with IPv6 installed on the system */ + { + struct sockaddr_in6 srcaddr; + memset(&srcaddr, 0, sizeof(struct sockaddr_in6)); + memcpy(&(srcaddr.sin6_addr), src, sizeof(srcaddr.sin6_addr)); + srcaddr.sin6_family = af; + if (WSAAddressToString((struct sockaddr *) &srcaddr, + sizeof(struct sockaddr_in6), + 0, + dst, + (LPDWORD) &cnt) != 0) { + WSAGetLastError(); + return NULL; + } + } + break; + + default: + errno = EAFNOSUPPORT; + return NULL; + } /* switch */ + + return dst; +} +#endif /* !HAVE_INET_NTOP */ + +#if !HAVE_INET_PTON +/* Fallback implementation of inet_pton() for systems that lack it, + * such as older versions of Windows (including MinGW builds that do + * not specifically target _WIN32_WINNT or newer). + * + * Based on code attributed to Paul Vixie, 1996, + * sourced from https://stackoverflow.com/a/15370175/4715872 + */ + +#define NS_INADDRSZ sizeof(struct in_addr) /* 4 */ +#define NS_IN6ADDRSZ sizeof(struct in6_addr) /* 16 */ +#define NS_INT16SZ sizeof(uint16_t) /* 2 */ + +static int inet_pton4(const char *src, void *dst) +{ + uint8_t tmp[NS_INADDRSZ], *tp; /* for struct in_addr *dst */ + + int saw_digit = 0; + int octets = 0; + int ch; + + *(tp = tmp) = 0; + + while ((ch = *src++) != '\0') { + if (ch >= '0' && ch <= '9') { + uint32_t n = *tp * 10 + (ch - '0'); + + if (saw_digit && *tp == 0) + return 0; + + if (n > 255) + return 0; + + *tp = n; + if (!saw_digit) { + if (++octets > 4) + return 0; + saw_digit = 1; + } + } else if (ch == '.' && saw_digit) { + if (octets == 4) + return 0; + *++tp = 0; + saw_digit = 0; + } else + return 0; + } + if (octets < 4) + return 0; + + memcpy(dst, tmp, NS_INADDRSZ); + + return 1; +} + +static int inet_pton6(const char *src, void *dst) +{ + static const char xdigits[] = "0123456789abcdef"; + uint8_t tmp[NS_IN6ADDRSZ]; /* for struct in6_addr *dst */ + + uint8_t *tp = (uint8_t *) memset(tmp, '\0', NS_IN6ADDRSZ); + uint8_t *endp = tp + NS_IN6ADDRSZ; + uint8_t *colonp = NULL; + + const char *curtok = NULL; + int saw_xdigit = 0; + uint32_t val = 0; + int ch; + + /* Leading :: requires some special handling. */ + if (*src == ':') { + if (*++src != ':') + return 0; + } + + curtok = src; + + while ((ch = tolower(*src++)) != '\0') { + const char *pch = strchr(xdigits, ch); + if (pch != NULL) { + val <<= 4; + val |= (pch - xdigits); + if (val > 0xffff) + return 0; + saw_xdigit = 1; + continue; + } + if (ch == ':') { + curtok = src; + if (!saw_xdigit) { + if (colonp) + return 0; + colonp = tp; + continue; + } else if (*src == '\0') { + return 0; + } + if (tp + NS_INT16SZ > endp) + return 0; + *tp++ = (uint8_t) (val >> 8) & 0xff; + *tp++ = (uint8_t) val & 0xff; + saw_xdigit = 0; + val = 0; + continue; + } + if (ch == '.' && ((tp + NS_INADDRSZ) <= endp) && + inet_pton4(curtok, (char *) tp) > 0) { + tp += NS_INADDRSZ; + saw_xdigit = 0; + break; /* '\0' was seen by inet_pton4(). */ + } + return 0; + } + if (saw_xdigit) { + if (tp + NS_INT16SZ > endp) + return 0; + *tp++ = (uint8_t) (val >> 8) & 0xff; + *tp++ = (uint8_t) val & 0xff; + } + if (colonp != NULL) { + /* + * Since some memmove()'s erroneously fail to handle + * overlapping regions, we'll do the shift by hand. + */ + const int n = tp - colonp; + int i; + + if (tp == endp) + return 0; + + for (i = 1; i <= n; i++) { + endp[-i] = colonp[n - i]; + colonp[n - i] = 0; + } + tp = endp; + } + if (tp != endp) + return 0; + + memcpy(dst, tmp, NS_IN6ADDRSZ); + + return 1; +} + +static int inet_pton(int af, const char *src, void *dst) +{ + switch (af) { + case AF_INET: + return inet_pton4(src, dst); + case AF_INET6: + return inet_pton6(src, dst); + default: + return -1; + } +} +#endif /* !HAVE_INET_PTON */ + static int _modbus_tcp_init_win32(void) { /* Initialise Windows Socket API */ @@ -236,7 +447,7 @@ static int _modbus_tcp_set_ipv4_options(int s) /* Set the TCP no delay flag */ /* SOL_TCP = IPPROTO_TCP */ option = 1; - rc = setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &option, sizeof(int)); + rc = setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (const void *)&option, sizeof(option)); if (rc == -1) { return -1; } @@ -264,7 +475,7 @@ static int _modbus_tcp_set_ipv4_options(int s) **/ /* Set the IP low delay option */ option = IPTOS_LOWDELAY; - rc = setsockopt(s, IPPROTO_IP, IP_TOS, &option, sizeof(int)); + rc = setsockopt(s, IPPROTO_IP, IP_TOS, (const void *)&option, sizeof(option)); if (rc == -1) { return -1; } @@ -547,6 +758,8 @@ int modbus_tcp_listen(modbus_t *ctx, int nb_connection) #ifdef OS_WIN32 if (_modbus_tcp_init_win32() == -1) { + if (ctx->debug) + perror("_modbus_tcp_init_win32"); return -1; } #endif @@ -559,11 +772,15 @@ int modbus_tcp_listen(modbus_t *ctx, int nb_connection) new_s = socket(PF_INET, flags, IPPROTO_TCP); if (new_s == -1) { + if (ctx->debug) + perror("socket"); return -1; } enable = 1; - if (setsockopt(new_s, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) == -1) { + if (setsockopt(new_s, SOL_SOCKET, SO_REUSEADDR, (const void *)&enable, sizeof(enable)) == -1) { + if (ctx->debug) + perror("setsockopt"); close(new_s); return -1; } @@ -588,11 +805,15 @@ int modbus_tcp_listen(modbus_t *ctx, int nb_connection) } if (bind(new_s, (struct sockaddr *) &addr, sizeof(addr)) == -1) { + if (ctx->debug) + perror("bind"); close(new_s); return -1; } if (listen(new_s, nb_connection) == -1) { + if (ctx->debug) + perror("listen"); close(new_s); return -1; } @@ -680,7 +901,7 @@ int modbus_tcp_pi_listen(modbus_t *ctx, int nb_connection) continue; } else { int enable = 1; - rc = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); + rc = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const void *)&enable, sizeof(enable)); if (rc != 0) { close(s); if (ctx->debug) { diff --git a/src/modbus.c b/src/modbus.c index e3737bb2..6ad0f1fa 100644 --- a/src/modbus.c +++ b/src/modbus.c @@ -414,7 +414,7 @@ int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type) while (length_to_read != 0) { rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read); if (rc == -1) { - _error_print(ctx, "select"); + _error_print(ctx, "ctx->backend->select"); if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) { #ifdef _WIN32 wsa_err = WSAGetLastError(); @@ -476,6 +476,7 @@ int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type) int i; for (i = 0; i < rc; i++) printf("<%.2X>", msg[msg_length + i]); + printf("\n"); } /* Sums bytes received */ @@ -755,7 +756,9 @@ static int response_exception(modbus_t *ctx, va_list ap; va_start(ap, template); + fflush(stderr); vfprintf(stderr, template, ap); + fflush(stderr); va_end(ap); }