cURL / Mailing Lists / curl-library / Single Mail

curl-library

[PATCH 10/11] libcurl: add UNIX domain sockets support

From: Peter Wu <peter_at_lekensteyn.nl>
Date: Thu, 27 Nov 2014 23:59:25 +0100

The ability to do HTTP requests over a UNIX domain socket has been
requested before, in Apr 2008 [0][1] and Sep 2010 [2]. While a
discussion happened, no patch seems to get through. I decided to give it
a go since I need to test a nginx HTTP server which listens on a UNIX
domain socket.

One patch [3] seems to make it possible to use the
CURLOPT_OPENSOCKETFUNCTION function to gain a UNIX domain socket.
Another person wrote a Go program which can do HTTP over a UNIX socket
for Docker[4] which uses a special URL scheme (though the name contains
cURL, it has no relation to the cURL library).

This patch considers support for UNIX domain sockets at the same level
as HTTP proxies / IPv6, it acts as an intermediate socket provider and
not as a separate protocol. Since this feature affects network
operations, a new feature flag was added ("unix-sockets") with a
corresponding CURL_VERSION_UNIX_SOCKETS macro.

A new CURLOPT_UNIX_SOCKET_PATH option is added and documented. This
option enables UNIX domain sockets support for all requests on the
handle (replacing IP sockets and skipping proxies).

A new configure option (--enable-unix-sockets) and CMake option
(ENABLE_UNIX_SOCKETS) can disable this optional feature. Note that I
deliberately did not mark this feature as advanced, this is a
feature/component that should easily be available.

 [0]: http://curl.haxx.se/mail/lib-2008-04/0279.html
 [1]: http://daniel.haxx.se/blog/2008/04/14/http-over-unix-domain-sockets/
 [2]: http://sourceforge.net/p/curl/feature-requests/53/
 [3]: http://curl.haxx.se/mail/lib-2008-04/0361.html
 [4]: https://github.com/Soulou/curl-unix-socket

Signed-off-by: Peter Wu <peter_at_lekensteyn.nl>

---
 CMakeLists.txt                               |  8 +++
 configure.ac                                 | 38 ++++++++++++++
 docs/libcurl/curl_easy_setopt.3              |  2 +
 docs/libcurl/curl_version_info.3             |  3 ++
 docs/libcurl/opts/CURLOPT_UNIX_SOCKET_PATH.3 | 76 ++++++++++++++++++++++++++++
 docs/libcurl/symbols-in-versions             |  2 +
 include/curl/curl.h                          |  4 ++
 lib/curl_addrinfo.c                          | 39 ++++++++++++++
 lib/curl_addrinfo.h                          |  4 ++
 lib/curl_config.h.cmake                      |  3 ++
 lib/url.c                                    | 44 ++++++++++++++++
 lib/urldata.h                                |  4 +-
 lib/version.c                                |  3 ++
 packages/OS400/curl.inc.in                   |  4 ++
 src/tool_help.c                              |  3 +-
 15 files changed, 235 insertions(+), 2 deletions(-)
 create mode 100644 docs/libcurl/opts/CURLOPT_UNIX_SOCKET_PATH.3
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 17eacfa..d6abaab 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -527,6 +527,14 @@ if(CMAKE_USE_GSSAPI)
   endif()
 endif()
 
+option(ENABLE_UNIX_SOCKETS "Define if you want UNIX domain sockets support" ON)
+if(ENABLE_UNIX_SOCKETS)
+  include(CheckStructHasMember)
+  check_struct_has_member("struct sockaddr_un" sun_path "sys/un.h" USE_UNIX_SOCKETS)
+else()
+  unset(USE_UNIX_SOCKETS CACHE)
+endif()
+
 # Check for header files
 if(NOT UNIX)
   check_include_file_concat("ws2tcpip.h"     HAVE_WS2TCPIP_H)
diff --git a/configure.ac b/configure.ac
index 1adb2a5..840e8ca 100644
--- a/configure.ac
+++ b/configure.ac
@@ -154,6 +154,7 @@ dnl initialize all the info variables
 curl_tls_srp_msg="no      (--enable-tls-srp)"
     curl_res_msg="default (--enable-ares / --enable-threaded-resolver)"
    curl_ipv6_msg="no      (--enable-ipv6)"
+curl_unix_sockets_msg="no      (--enable-unix-sockets)"
     curl_idn_msg="no      (--with-{libidn,winidn})"
  curl_manual_msg="no      (--enable-manual)"
 curl_libcurl_msg="enabled (--disable-libcurl-option)"
@@ -3258,6 +3259,39 @@ if test "$want_tls_srp" = "yes" && ( test "x$HAVE_GNUTLS_SRP" = "x1" || test "x$
 fi
 
 dnl ************************************************************
+dnl disable UNIX domain sockets support
+dnl
+AC_MSG_CHECKING([whether to enable UNIX domain sockets])
+AC_ARG_ENABLE(unix-sockets,
+AC_HELP_STRING([--enable-unix-sockets],[Enable UNIX domain sockets])
+AC_HELP_STRING([--disable-unix-sockets],[Disable UNIX domain sockets]),
+[ case "$enableval" in
+  no)  AC_MSG_RESULT(no)
+       want_unix_sockets=no
+       ;;
+  *)   AC_MSG_RESULT(yes)
+       want_unix_sockets=yes
+       ;;
+  esac ], [
+       AC_MSG_RESULT(auto)
+       want_unix_sockets=auto
+       ]
+)
+if test "x$want_unix_sockets" != "xno"; then
+  AC_CHECK_MEMBER([struct sockaddr_un.sun_path], [
+    AC_DEFINE(USE_UNIX_SOCKETS, 1, [Use UNIX domain sockets])
+    AC_SUBST(USE_UNIX_SOCKETS, [1])
+    curl_unix_sockets_msg="enabled"
+  ], [
+    if test "x$want_unix_sockets" = "xyes"; then
+      AC_MSG_ERROR([--enable-unix-sockets is not available on this platform!])
+    fi
+  ], [
+    #include <sys/un.h>
+  ])
+fi
+
+dnl ************************************************************
 dnl disable cookies support
 dnl
 AC_MSG_CHECKING([whether to enable support for cookies])
@@ -3340,6 +3374,9 @@ fi
 if test "x$IPV6_ENABLED" = "x1"; then
   SUPPORT_FEATURES="$SUPPORT_FEATURES IPv6"
 fi
+if test "x$USE_UNIX_SOCKETS" = "x1"; then
+  SUPPORT_FEATURES="$SUPPORT_FEATURES unix-sockets"
+fi
 if test "x$HAVE_LIBZ" = "x1"; then
   SUPPORT_FEATURES="$SUPPORT_FEATURES libz"
 fi
@@ -3538,6 +3575,7 @@ AC_MSG_NOTICE([Configured to build curl/libcurl:
   TLS-SRP support:  ${curl_tls_srp_msg}
   resolver:         ${curl_res_msg}
   ipv6 support:     ${curl_ipv6_msg}
+  UNIX sockets support: ${curl_unix_sockets_msg}
   IDN support:      ${curl_idn_msg}
   Build libcurl:    Shared=${enable_shared}, Static=${enable_static}
   Built-in manual:  ${curl_manual_msg}
diff --git a/docs/libcurl/curl_easy_setopt.3 b/docs/libcurl/curl_easy_setopt.3
index d79a42b..71f681b 100644
--- a/docs/libcurl/curl_easy_setopt.3
+++ b/docs/libcurl/curl_easy_setopt.3
@@ -187,6 +187,8 @@ Enable TCP keep-alive. See \fICURLOPT_TCP_KEEPALIVE(3)\fP
 Idle time before sending keep-alive. See \fICURLOPT_TCP_KEEPIDLE(3)\fP
 .IP CURLOPT_TCP_KEEPINTVL
 Interval between keep-alive probes. See \fICURLOPT_TCP_KEEPINTVL(3)\fP
+.IP CURLOPT_UNIX_SOCKET_PATH
+Path to a UNIX domain socket. See \fICURLOPT_UNIX_SOCKET_PATH(3)\fP
 .SH NAMES and PASSWORDS OPTIONS (Authentication)
 .IP CURLOPT_NETRC
 Enable .netrc parsing. See \fICURLOPT_NETRC(3)\fP
diff --git a/docs/libcurl/curl_version_info.3 b/docs/libcurl/curl_version_info.3
index 3acf785..6e55346 100644
--- a/docs/libcurl/curl_version_info.3
+++ b/docs/libcurl/curl_version_info.3
@@ -146,6 +146,9 @@ libcurl was built with support for NTLM delegation to a winbind helper.
 .IP CURL_VERSION_HTTP2
 libcurl was built with support for HTTP2.
 (Added in 7.33.0)
+.IP CURL_VERSION_UNIX_SOCKETS
+libcurl was built with support for UNIX domain sockets.
+(Added in 7.40.0)
 .RE
 \fIssl_version\fP is an ASCII string for the OpenSSL version used. If libcurl
 has no SSL support, this is NULL.
diff --git a/docs/libcurl/opts/CURLOPT_UNIX_SOCKET_PATH.3 b/docs/libcurl/opts/CURLOPT_UNIX_SOCKET_PATH.3
new file mode 100644
index 0000000..880c2f7
--- /dev/null
+++ b/docs/libcurl/opts/CURLOPT_UNIX_SOCKET_PATH.3
@@ -0,0 +1,76 @@
+.\" **************************************************************************
+.\" *                                  _   _ ____  _
+.\" *  Project                     ___| | | |  _ \| |
+.\" *                             / __| | | | |_) | |
+.\" *                            | (__| |_| |  _ <| |___
+.\" *                             \___|\___/|_| \_\_____|
+.\" *
+.\" * Copyright (C) 1998 - 2014, Daniel Stenberg, <daniel_at_haxx.se>, et al.
+.\" *
+.\" * This software is licensed as described in the file COPYING, which
+.\" * you should have received as part of this distribution. The terms
+.\" * are also available at http://curl.haxx.se/docs/copyright.html.
+.\" *
+.\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+.\" * copies of the Software, and permit persons to whom the Software is
+.\" * furnished to do so, under the terms of the COPYING file.
+.\" *
+.\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+.\" * KIND, either express or implied.
+.\" *
+.\" **************************************************************************
+.\"
+.TH CURLOPT_UNIX_SOCKET_PATH 3 "09 Oct 2014" "libcurl 7.40.0" "curl_easy_setopt options"
+.SH NAME
+CURLOPT_UNIX_SOCKET_PATH \- set UNIX domain socket
+.SH SYNOPSIS
+#include <curl/curl.h>
+
+CURLcode curl_easy_setopt(CURL *handle, CURLOPT_UNIX_SOCKET_PATH, char *path);
+.SH DESCRIPTION
+Enables the use of UNIX domain sockets as connection end point and sets the path
+to \fIpath\fP. If \fIpath\fP is NULL, then UNIX domain sockets are disabled. An
+empty string will result in an error at some point.
+
+When enabled, cURL will connect to the UNIX domain socket instead of
+establishing a TCP connection to a host. Since no TCP connection is established,
+cURL does not need to resolve the DNS hostname in the URL.
+
+The maximum path length on Cygwin, Linux and Solaris is 107. On other platforms
+might be even less.
+
+Proxy and TCP options such as
+.BR CURLOPT_TCP_NODELAY "(3)
+are not supported. Proxy options such as
+.BR CURLOPT_PROXY "(3)
+have no effect either as these are TCP-oriented, and asking a proxy server to
+connect to a certain UNIX domain socket is not possible.
+.SH DEFAULT
+Default is NULL, meaning that no UNIX domain sockets are used.
+.SH PROTOCOLS
+All protocols except for file:// and FTP are supported in theory. HTTP, IMAP,
+POP3 and SMTP should in particular work (including their SSL/TLS variants).
+.SH EXAMPLE
+Given that you have an nginx server running, listening on /tmp/nginx.sock, you
+can request a HTTP resource with:
+
+    curl_easy_setopt(curl_handle, CURLOPT_UNIX_SOCKET_PATH, "/tmp/nginx.sock");
+    curl_easy_setopt(curl_handle, CURLOPT_URL, "http://localhost/");
+
+If you are on Linux and somehow have a need for paths larger than 107 bytes, you
+could use the proc filesystem to bypass the limitation:
+
+    int dirfd = open(long_directory_path_to_socket, O_DIRECTORY | O_RDONLY);
+    char path[108];
+    snprintf(path, sizeof(path), "/proc/self/fd/%d/nginx.sock", dirfd);
+    curl_easy_setopt(curl_handle, CURLOPT_UNIX_SOCKET_PATH, path);
+    /* Be sure to keep dirfd valid until you discard the handle */
+
+.SH AVAILABILITY
+Since 7.40.0.
+.SH RETURN VALUE
+Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not.
+.SH "SEE ALSO"
+.BR CURLOPT_OPENSOCKETFUNCTION "(3)
+,
+.BR unix "(7)
diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions
index 8e4ca9c..b50a9f5 100644
--- a/docs/libcurl/symbols-in-versions
+++ b/docs/libcurl/symbols-in-versions
@@ -529,6 +529,7 @@ CURLOPT_TLSAUTH_TYPE            7.21.4
 CURLOPT_TLSAUTH_USERNAME        7.21.4
 CURLOPT_TRANSFERTEXT            7.1.1
 CURLOPT_TRANSFER_ENCODING       7.21.6
+CURLOPT_UNIX_SOCKET_PATH        7.40.0
 CURLOPT_UNRESTRICTED_AUTH       7.10.4
 CURLOPT_UPLOAD                  7.1
 CURLOPT_URL                     7.1
@@ -747,6 +748,7 @@ CURL_VERSION_SPNEGO             7.10.8
 CURL_VERSION_SSL                7.10
 CURL_VERSION_SSPI               7.13.2
 CURL_VERSION_TLSAUTH_SRP        7.21.4
+CURL_VERSION_UNIX_SOCKETS       7.40.0
 CURL_WAIT_POLLIN                7.28.0
 CURL_WAIT_POLLOUT               7.28.0
 CURL_WAIT_POLLPRI               7.28.0
diff --git a/include/curl/curl.h b/include/curl/curl.h
index 90d0251..eda491a 100644
--- a/include/curl/curl.h
+++ b/include/curl/curl.h
@@ -1617,6 +1617,9 @@ typedef enum {
      this option is used only if SSL_VERIFYPEER is true */
   CINIT(PINNEDPUBLICKEY, OBJECTPOINT, 230),
 
+  /* Path to UNIX domain socket */
+  CINIT(UNIX_SOCKET_PATH, OBJECTPOINT, 231),
+
   CURLOPT_LASTENTRY /* the last unused */
 } CURLoption;
 
@@ -2264,6 +2267,7 @@ typedef struct {
 #define CURL_VERSION_HTTP2        (1<<16) /* HTTP2 support built-in */
 #define CURL_VERSION_GSSAPI       (1<<17) /* Built against a GSS-API library */
 #define CURL_VERSION_KERBEROS5    (1<<18) /* Kerberos V5 auth is supported */
+#define CURL_VERSION_UNIX_SOCKETS (1<<19) /* UNIX domain sockets support */
 
  /*
  * NAME curl_version_info()
diff --git a/lib/curl_addrinfo.c b/lib/curl_addrinfo.c
index 94ed63d..b8032f4 100644
--- a/lib/curl_addrinfo.c
+++ b/lib/curl_addrinfo.c
@@ -33,6 +33,9 @@
 #ifdef HAVE_ARPA_INET_H
 #  include <arpa/inet.h>
 #endif
+#ifdef HAVE_SYS_UN_H
+#  include <sys/un.h>
+#endif
 
 #ifdef __VMS
 #  include <in.h>
@@ -477,6 +480,42 @@ Curl_addrinfo *Curl_str2addr(char *address, int port)
   return NULL; /* bad input format */
 }
 
+#ifdef USE_UNIX_SOCKETS
+/**
+ * Given a path to a UNIX domain socket, return a newly allocated Curl_addrinfo
+ * struct initialized with this path.
+ */
+Curl_addrinfo *Curl_unix2addr(const char *path)
+{
+  Curl_addrinfo *ai;
+  struct sockaddr_un *sun;
+  size_t path_len;
+
+  ai = calloc(1, sizeof(Curl_addrinfo));
+  if(!ai)
+    return NULL;
+  if((ai->ai_addr = calloc(1, sizeof(struct sockaddr_un))) == NULL) {
+    free(ai);
+    return NULL;
+  }
+  /* sun_path must be able to store the NUL-terminated path */
+  path_len = strlen(path);
+  if(path_len >= sizeof(sun->sun_path)) {
+    free(ai->ai_addr);
+    free(ai);
+    return NULL;
+  }
+
+  ai->ai_family = AF_UNIX;
+  ai->ai_socktype = SOCK_STREAM; /* assume reliable transport for HTTP */
+  ai->ai_addrlen = (curl_socklen_t) sizeof(struct sockaddr_un);
+  sun = (void *) ai->ai_addr;
+  sun->sun_family = AF_UNIX;
+  memcpy(sun->sun_path, path, path_len + 1); /* copy NUL byte */
+  return ai;
+}
+#endif
+
 #if defined(CURLDEBUG) && defined(HAVE_FREEADDRINFO)
 /*
  * curl_dofreeaddrinfo()
diff --git a/lib/curl_addrinfo.h b/lib/curl_addrinfo.h
index 6d2b753..4ef8827 100644
--- a/lib/curl_addrinfo.h
+++ b/lib/curl_addrinfo.h
@@ -79,6 +79,10 @@ Curl_ip2addr(int af, const void *inaddr, const char *hostname, int port);
 
 Curl_addrinfo *Curl_str2addr(char *dotted, int port);
 
+#ifdef USE_UNIX_SOCKETS
+Curl_addrinfo *Curl_unix2addr(const char *path);
+#endif
+
 #if defined(CURLDEBUG) && defined(HAVE_FREEADDRINFO)
 void
 curl_dofreeaddrinfo(struct addrinfo *freethis,
diff --git a/lib/curl_config.h.cmake b/lib/curl_config.h.cmake
index 44014f5..bd367c2 100644
--- a/lib/curl_config.h.cmake
+++ b/lib/curl_config.h.cmake
@@ -912,6 +912,9 @@
 /* if SSL is enabled */
 #cmakedefine USE_SSLEAY 1
 
+/* if UNIX domain sockets are enabled  */
+#cmakedefine USE_UNIX_SOCKETS
+
 /* Define to 1 if you are building a Windows target without large file
    support. */
 #cmakedefine USE_WIN32_LARGE_FILES 1
diff --git a/lib/url.c b/lib/url.c
index 5b19f89..2776c78 100644
--- a/lib/url.c
+++ b/lib/url.c
@@ -47,6 +47,10 @@
 #include <inet.h>
 #endif
 
+#ifdef HAVE_SYS_UN_H
+#include <sys/un.h>
+#endif
+
 #ifndef HAVE_SOCKET
 #error "We can't compile without socket() support!"
 #endif
@@ -2565,6 +2569,13 @@ CURLcode Curl_setopt(struct SessionHandle *data, CURLoption option,
     data->set.ssl_enable_alpn = (0 != va_arg(param, long))?TRUE:FALSE;
     break;
 
+#ifdef USE_UNIX_SOCKETS
+  case CURLOPT_UNIX_SOCKET_PATH:
+    result = setstropt(&data->set.str[STRING_UNIX_SOCKET_PATH],
+                       va_arg(param, char *));
+    break;
+#endif
+
   default:
     /* unknown tag and its companion, just ignore: */
     result = CURLE_UNKNOWN_OPTION;
@@ -5055,6 +5066,32 @@ static CURLcode resolve_server(struct SessionHandle *data,
     /* set a pointer to the hostname we display */
     fix_hostname(data, conn, &conn->host);
 
+#ifdef USE_UNIX_SOCKETS
+    if(data->set.str[STRING_UNIX_SOCKET_PATH]) {
+      /* UNIX domain sockets are local. The host gets ignored, just use the
+       * specified domain socket address. Do not cache "DNS entries". There is
+       * no DNS involved and we already have the filesystem path available */
+      const char *path = data->set.str[STRING_UNIX_SOCKET_PATH];
+
+      hostaddr = calloc(1, sizeof(struct Curl_dns_entry));
+      if(!hostaddr)
+        result = CURLE_OUT_OF_MEMORY;
+      else if((hostaddr->addr = Curl_unix2addr(path)) != NULL)
+        hostaddr->inuse++;
+      else {
+        /* Long paths are not supported for now */
+        if(strlen(path) >= sizeof(((struct sockaddr_un *)0)->sun_path)) {
+          failf(data, "UNIX socket path too long: '%s'", path);
+          result = CURLE_COULDNT_RESOLVE_HOST;
+        }
+        else
+          result = CURLE_OUT_OF_MEMORY;
+        free(hostaddr);
+        hostaddr = NULL;
+      }
+    }
+    else
+#endif
     if(!conn->proxy.name || !*conn->proxy.name) {
       /* If not connecting via a proxy, extract the port from the URL, if it is
        * there, thus overriding any defaults that might have been set above. */
@@ -5370,6 +5407,13 @@ static CURLcode create_conn(struct SessionHandle *data,
   else if(!proxy)
     proxy = detect_proxy(conn);
 
+#ifdef USE_UNIX_SOCKETS
+  if(proxy && data->set.str[STRING_UNIX_SOCKET_PATH]) {
+    free(proxy);  /* UNIX domain sockets cannot be proxied, so disable it */
+    proxy = NULL;
+  }
+#endif
+
   if(proxy && (!*proxy || (conn->handler->flags & PROTOPT_NONETWORK))) {
     free(proxy);  /* Don't bother with an empty proxy string or if the
                      protocol doesn't work with network */
diff --git a/lib/urldata.h b/lib/urldata.h
index d0746b4..8d1c3d5 100644
--- a/lib/urldata.h
+++ b/lib/urldata.h
@@ -1416,8 +1416,10 @@ enum dupstring {
   STRING_TLSAUTH_USERNAME,     /* TLS auth <username> */
   STRING_TLSAUTH_PASSWORD,     /* TLS auth <password> */
 #endif
-
   STRING_BEARER,          /* <bearer>, if used */
+#ifdef USE_UNIX_SOCKETS
+  STRING_UNIX_SOCKET_PATH,  /* path to UNIX socket, if used */
+#endif
 
   /* -- end of zero-terminated strings -- */
 
diff --git a/lib/version.c b/lib/version.c
index 1ea8a21..58057af 100644
--- a/lib/version.c
+++ b/lib/version.c
@@ -289,6 +289,9 @@ static curl_version_info_data version_info = {
 #if defined(USE_NGHTTP2)
   | CURL_VERSION_HTTP2
 #endif
+#if defined(USE_UNIX_SOCKETS)
+  | CURL_VERSION_UNIX_SOCKETS
+#endif
   ,
   NULL, /* ssl_version */
   0,    /* ssl_version_num, this is kept at zero */
diff --git a/packages/OS400/curl.inc.in b/packages/OS400/curl.inc.in
index 2f6d86a..d0fe4a2 100644
--- a/packages/OS400/curl.inc.in
+++ b/packages/OS400/curl.inc.in
@@ -121,6 +121,8 @@
      d                 c                   X'00020000'
      d CURL_VERSION_KERBEROS5...
      d                 c                   X'00040000'
+     d CURL_VERSION_UNIX_SOCKETS...
+     d                 c                   X'00080000'
       *
      d HTTPPOST_FILENAME...
      d                 c                   X'00000001'
@@ -1195,6 +1197,8 @@
      d                 c                   00229
      d  CURLOPT_PINNEDPUBLICKEY...
      d                 c                   10230
+     d  CURLOPT_UNIX_SOCKET_PATH...
+     d                 c                   10231
       *
       /if not defined(CURL_NO_OLDIES)
      d  CURLOPT_FILE   c                   10001
diff --git a/src/tool_help.c b/src/tool_help.c
index 280d33a..6f8f292 100644
--- a/src/tool_help.c
+++ b/src/tool_help.c
@@ -273,7 +273,8 @@ static const struct feat feats[] = {
   {"libz",           CURL_VERSION_LIBZ},
   {"CharConv",       CURL_VERSION_CONV},
   {"TLS-SRP",        CURL_VERSION_TLSAUTH_SRP},
-  {"HTTP2",          CURL_VERSION_HTTP2}
+  {"HTTP2",          CURL_VERSION_HTTP2},
+  {"unix-sockets",   CURL_VERSION_UNIX_SOCKETS},
 };
 
 void tool_help(void)
-- 
2.1.3
-------------------------------------------------------------------
List admin: http://cool.haxx.se/list/listinfo/curl-library
Etiquette:  http://curl.haxx.se/mail/etiquette.html
Received on 2014-11-28