]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
squidclient: Support TLS for testing https:// URLs and HTTPS servers
authorAmos Jeffries <squid3@treenet.co.nz>
Fri, 18 Apr 2014 15:47:24 +0000 (08:47 -0700)
committerAmos Jeffries <squid3@treenet.co.nz>
Fri, 18 Apr 2014 15:47:24 +0000 (08:47 -0700)
Also adds detection of the GnuTLS library which is used to provide this
squidclient feature.

configure.ac
tools/squidclient/Makefile.am
tools/squidclient/Parameters.h
tools/squidclient/Transport.cc [new file with mode: 0644]
tools/squidclient/Transport.h [new file with mode: 0644]
tools/squidclient/squidclient.1
tools/squidclient/squidclient.cc

index 3b74f995311dd0763052a49dc5c5fdaad350a962..d4b89c46c0b72d25f8a612b6ff15d6b2eb4e83ce 100644 (file)
@@ -1213,6 +1213,53 @@ dnl Solaris10 provides MD5 natively through libmd5
 AC_CHECK_LIB(md5, MD5Init, [CRYPTLIB="$CRYPTLIB -lmd5"])
 AC_SUBST(CRYPTLIB)
 
+SSLLIB=""
+
+dnl User may want to disable GnuTLS
+AC_ARG_WITH(gnutls,
+  AS_HELP_STRING([--without-gnutls],
+                 [Do not use GnuTLS for SSL. Default: auto-detect]), [ 
+case "$with_gnutls" in
+  yes|no)
+    : # Nothing special to do here
+    ;;
+  *)
+    if test ! -d "$withval" ; then
+      AC_MSG_ERROR([--with-gnutls path does not point to a directory])
+    fi
+    LIBGNUTLS_PATH="-L$with_gnutls/lib"
+    CPPFLAGS="-I$with_gnutls/include $CPPFLAGS"
+  esac
+])
+AH_TEMPLATE(USE_GNUTLS,[GnuTLS support is available])
+if test "x$with_gnutls" != "xno"; then
+  AC_CHECK_HEADERS(gnutls/gnutls.h gnutls/x509.h)
+
+  # User may have provided a custom location for GnuTLS. Otherwise...
+  SQUID_STATE_SAVE(squid_gnutls_state)
+  LIBS="$LIBS $LIBGNUTLS_PATH"
+
+  # auto-detect using pkg-config
+  PKG_CHECK_MODULES([LIBGNUTLS],[gnutls],,[
+    ## find the package without pkg-config
+    AC_CHECK_LIB(gnutls,gnutls_init,[LIBGNUTLS_LIBS="-lgnutls"])
+  ])
+
+  SQUID_STATE_ROLLBACK(squid_gnutls_state) #de-pollute LIBS
+
+  if test "x$with_gnutls" = "xyes" -a "x$LIBGNUTLS_LIBS" = "x"; then
+    AC_MSG_ERROR([Required GnuTLS library not found])
+  fi
+  if test "x$LIBGNUTLS_LIBS" != "x" ; then
+    CXXFLAGS="$LIBGNUTLS_CFLAGS $CXXFLAGS"
+    SSLLIB="$LIBGNUTLS_PATH $LIBGNUTLS_LIBS $SSLLIB"
+    AC_DEFINE(USE_GNUTLS,1,[GnuTLS support is available])
+  else
+    with_gnutls=no
+  fi
+fi
+AC_MSG_NOTICE([GnuTLS library support: ${with_gnutls:=auto} ${LIBGNUTLS_PATH} ${LIBGNUTLS_LIBS}])
+
 dnl User may specify OpenSSL is needed from a non-standard location
 AC_ARG_WITH(openssl,
   AS_HELP_STRING([--with-openssl=PATH],
index bd90808b2b52f9b169a56cd266de5f2041dcc2fe..499acf3db9a76a7e1b6d2ecb6622175783e54f24 100644 (file)
@@ -12,6 +12,7 @@ LDADD = \
        $(top_builddir)/lib/libmiscencoding.la \
        $(top_builddir)/lib/libmiscutil.la \
        $(COMPAT_LIB) \
+       $(LIBGNUTLS_LIBS) \
        $(NETTLELIB) \
        $(KRB5LIBS) \
        $(XTRA_LIBS)
@@ -49,4 +50,6 @@ squidclient_SOURCES = \
        squidclient.cc \
        stub_debug.cc \
        test_tools.cc \
-       time.cc
+       time.cc \
+       Transport.cc \
+       Transport.h
index e6ecb0e587b90ccf50c4b8c6ef62aefce181679e..27253cfefda833cdb045f96a8651b6fdcc70456a 100644 (file)
@@ -19,6 +19,10 @@ public:
     int verbosityLevel;
 };
 
+/// display debug messages at varying verbosity levels
+#define debugVerbose(LEVEL, MESSAGE) \
+    while ((LEVEL) <= scParams.verbosityLevel) {std::cerr << MESSAGE << std::endl; break;}
+
 /// global squidcleint parameters
 extern Parameters scParams;
 
diff --git a/tools/squidclient/Transport.cc b/tools/squidclient/Transport.cc
new file mode 100644 (file)
index 0000000..8f43cb2
--- /dev/null
@@ -0,0 +1,499 @@
+#include "squid.h"
+#include "ip/Address.h"
+#include "tools/squidclient/Ping.h"
+#include "tools/squidclient/Transport.h"
+
+#if HAVE_GETOPT_H
+#include <getopt.h>
+#endif
+#if HAVE_GNUTLS_X509_H
+#include <gnutls/x509.h>
+#endif
+
+Transport::TheConfig Transport::Config;
+
+/// the current server connection FD
+int conn = -1;
+
+void
+Transport::TheConfig::usage()
+{
+    std::cerr << "Connection Settings" << std::endl
+              << "  -h | --host host     Send message to server on 'host'.  Default is localhost." << std::endl
+              << "  -l | --local host    Specify a local IP address to bind to.  Default is none." << std::endl
+              << "  -p | --port port     Port number on server to contact. Default is " << CACHE_HTTP_PORT << "." << std::endl
+              << "  -T timeout           Timeout in seconds for read/write operations" << std::endl
+#if USE_GNUTLS
+              << "  --tls [TLS options]  Use TLS on the connection" << std::endl
+              << std::endl
+              << "  TLS options:" << std::endl
+              << "    --anonymous        Use Anonymous TLS. Sets default parameters:" << std::endl
+              << "                         \"PERFORMANCE:+ANON-ECDH:+ANON-DH\"" << std::endl
+              << "    --params=\"...\"   Use the given parameters." << std::endl
+              << "    --cert=FILE        Path to a PEM file holding the client X.509 certificate chain." << std::endl
+              << "                       May be repeated if there are multiple certificates to use for the server." << std::endl
+              << "    --trusted-ca=PATH  Path to a PEM file holding trusted CA certificate(s)." << std::endl
+              << "                       May be repeated." << std::endl
+              << "                       Example path: \"/etc/ssl/certs/ca-certificates.crt\"" << std::endl
+              << std::endl;
+#endif
+}
+
+bool
+Transport::TheConfig::parseCommandOpts(int argc, char *argv[], int c, int &optIndex)
+{
+    bool tls = false;
+    const char *shortOpStr = "A:C:h:l:p:P:T:?";
+
+    // options for controlling squidclient transport connection
+    static struct option longOptions[] = {
+        {"anonymous",    no_argument, 0, '\1'},
+        {"tls",          no_argument, 0, '\3'},
+        {"trusted-ca",   required_argument, 0, 'A'},
+        {"cert",         required_argument, 0, 'C'},
+        {"host",         required_argument, 0, 'h'},
+        {"local",        required_argument, 0, 'l'},
+        {"port",         required_argument, 0, 'p'},
+        {"params",       required_argument, 0, 'P'},
+        {0, 0, 0, 0}
+    };
+
+    int saved_opterr = opterr;
+    opterr = 0; // suppress errors from getopt
+    do {
+        switch (c) {
+        case '\1':
+            tls = true;
+            tlsAnonymous = true;
+            params = "PERFORMANCE:+ANON-ECDH:+ANON-DH";
+            break;
+
+        case '\3':
+            tls = true;
+            break;
+
+        case 'A':
+            tls = true;
+            caFiles.push_back(std::string(optarg));
+            break;
+
+        case 'C':
+            tls = true;
+            certFiles.push_back(std::string(optarg));
+            break;
+
+        case 'h':
+            hostname = optarg;
+            break;
+
+        case 'l':
+            localHost = optarg;
+            break;
+
+        case 'p':           /* port number */
+            sscanf(optarg, "%hd", &port);
+            if (port < 1)
+                port = CACHE_HTTP_PORT;     /* default */
+            break;
+
+        case 'P':
+            tls = true;
+            params = optarg;
+            break;
+
+        case 'T':
+            ioTimeout = atoi(optarg);
+            break;
+
+        default:
+            if (tls)
+                Transport::InitTls();
+
+            // rewind and let the caller handle unknown options
+            --optind;
+            opterr = saved_opterr;
+            return true;
+        }
+    } while ((c = getopt_long(argc, argv, shortOpStr, longOptions, &optIndex)) != -1);
+
+    if (tls)
+        Transport::InitTls();
+
+    opterr = saved_opterr;
+    return false;
+}
+
+/// Set up the source socket address from which to send.
+static int
+client_comm_bind(int sock, const Ip::Address &addr)
+{
+    static struct addrinfo *AI = NULL;
+    addr.getAddrInfo(AI);
+    int res = bind(sock, AI->ai_addr, AI->ai_addrlen);
+    Ip::Address::FreeAddrInfo(AI);
+    return res;
+}
+
+static void
+resolveDestination(Ip::Address &iaddr)
+{
+    struct addrinfo *AI = NULL;
+
+    if (Transport::Config.localHost) {
+        debugVerbose(2, "Resolving " << Transport::Config.localHost << " ...");
+
+        if ( !iaddr.GetHostByName(Transport::Config.localHost) ) {
+            std::cerr << "ERROR: Cannot resolve " << Transport::Config.localHost << ": Host unknown." << std::endl;
+            exit(1);
+        }
+    } else {
+        debugVerbose(2, "Resolving " << Transport::Config.hostname << " ...");
+        /* Process the remote host name to locate the Protocol required
+           in case we are being asked to link to another version of squid */
+        if ( !iaddr.GetHostByName(Transport::Config.hostname) ) {
+            std::cerr << "ERROR: Cannot resolve " << Transport::Config.hostname << ": Host unknown." << std::endl;
+            exit(1);
+        }
+    }
+
+    iaddr.getAddrInfo(AI);
+    if ((conn = socket(AI->ai_family, AI->ai_socktype, 0)) < 0) {
+        std::cerr << "ERROR: could not open socket to " << iaddr << std::endl;
+        Ip::Address::FreeAddrInfo(AI);
+        exit(1);
+    }
+    Ip::Address::FreeAddrInfo(AI);
+
+    if (Transport::Config.localHost) {
+        if (client_comm_bind(conn, iaddr) < 0) {
+            std::cerr << "ERROR: could not bind socket to " << iaddr << std::endl;
+            exit(1);
+        }
+
+        iaddr.setEmpty();
+
+        debugVerbose(2, "Resolving... " << Transport::Config.hostname);
+
+        if ( !iaddr.GetHostByName(Transport::Config.hostname) ) {
+            std::cerr << "ERROR: Cannot resolve " << Transport::Config.hostname << ": Host unknown." << std::endl;
+            exit(1);
+        }
+    }
+
+    iaddr.port(Transport::Config.port);
+}
+
+/// Set up the destination socket address for message to send to.
+static int
+client_comm_connect(int sock, const Ip::Address &addr)
+{
+    static struct addrinfo *AI = NULL;
+    addr.getAddrInfo(AI);
+    int res = connect(sock, AI->ai_addr, AI->ai_addrlen);
+    Ip::Address::FreeAddrInfo(AI);
+    Ping::TimerStart();
+    return res;
+}
+
+bool
+Transport::Connect()
+{
+    Ip::Address iaddr;
+    resolveDestination(iaddr);
+
+    debugVerbose(2, "Connecting... " << Config.hostname << " (" << iaddr << ")");
+
+    if (client_comm_connect(conn, iaddr) < 0) {
+        char hostnameBuf[MAX_IPSTRLEN];
+        iaddr.toUrl(hostnameBuf, MAX_IPSTRLEN);
+        std::cerr << "ERROR: Cannot connect to " << hostnameBuf
+                  << (!errno ?": Host unknown." : "") << std::endl;
+        exit(1);
+    }
+    debugVerbose(2, "Connected to: " << Config.hostname << " (" << iaddr << ")");
+
+    // do any TLS setup that might be needed
+    if (!Transport::MaybeStartTls(Config.hostname))
+        return false;
+
+    return true;
+}
+
+ssize_t
+Transport::Write(void *buf, size_t len)
+{
+    if (conn < 0)
+        return -1;
+
+    if (Config.tlsEnabled) {
+#if USE_GNUTLS
+        gnutls_record_send(Config.session, buf, len);
+        return len;
+#else
+        return 0;
+#endif
+    } else {
+
+#if _SQUID_WINDOWS_
+        return send(conn, buf, len, 0);
+#else
+        alarm(Config.ioTimeout);
+        return write(conn, buf, len);
+#endif
+    }
+}
+
+ssize_t
+Transport::Read(void *buf, size_t len)
+{
+    if (conn < 0)
+        return -1;
+
+    if (Config.tlsEnabled) {
+#if USE_GNUTLS
+        return gnutls_record_recv(Config.session, buf, len);
+#else
+        return 0;
+#endif
+    } else {
+
+#if _SQUID_WINDOWS_
+        return recv(conn, buf, len, 0);
+#else
+        alarm(Config.ioTimeout);
+        return read(conn, buf, len);
+#endif
+    }
+}
+
+void
+Transport::CloseConnection()
+{
+    (void) close(conn);
+    conn = -1;
+}
+
+#if USE_GNUTLS
+/* This function will verify the peer's certificate, and check
+ * if the hostname matches, as well as the activation, expiration dates.
+ */
+static int
+verifyByCA(gnutls_session_t session)
+{
+    /* read hostname */
+    const char *hostname = static_cast<const char*>(gnutls_session_get_ptr(session));
+
+    /* This verification function uses the trusted CAs in the credentials
+     * structure. So you must have installed one or more CA certificates.
+     */
+    unsigned int status;
+    if (gnutls_certificate_verify_peers3(session, hostname, &status) < 0) {
+        std::cerr << "VERIFY peers failure";
+        return GNUTLS_E_CERTIFICATE_ERROR;
+    }
+
+    gnutls_certificate_type_t type = gnutls_certificate_type_get(session);
+    gnutls_datum_t out;
+    if (gnutls_certificate_verification_status_print(status, type, &out, 0) < 0) {
+        std::cerr << "VERIFY status failure";
+        return GNUTLS_E_CERTIFICATE_ERROR;
+    }
+
+    std::cerr << "VERIFY DATUM: " << out.data << std::endl;
+    gnutls_free(out.data);
+
+    if (status != 0)        /* Certificate is not trusted */
+        return GNUTLS_E_CERTIFICATE_ERROR;
+
+    /* notify gnutls to continue handshake normally */
+    return GNUTLS_E_SUCCESS;
+}
+
+static int
+verifyTlsCertificate(gnutls_session_t session)
+{
+    // XXX: 1) try to verify using DANE -> Secure Authenticated Connection
+
+    // 2) try to verify using CA
+    if (verifyByCA(session) == GNUTLS_E_SUCCESS) {
+        std::cerr << "SUCCESS: CA verified Encrypted Connection" << std::endl;
+        return GNUTLS_E_SUCCESS;
+    }
+
+    // 3) fails both is insecure, but show the results anyway.
+    std::cerr << "WARNING: Insecure Connection" << std::endl;
+    return GNUTLS_E_SUCCESS;
+}
+#endif
+
+void
+Transport::InitTls()
+{
+#if USE_GNUTLS
+    debugVerbose(3, "Initializing TLS library...");
+    // NP: gnutls init is re-entrant and lock-counted with deinit but not thread safe.
+    if (gnutls_global_init() != GNUTLS_E_SUCCESS) {
+        std::cerr << "FATAL ERROR: TLS Initialize failed: " << xstrerror() << std::endl;
+        exit(1);
+    }
+
+    Config.tlsEnabled = true;
+
+    // Initialize for anonymous TLS
+    gnutls_anon_allocate_client_credentials(&Config.anonCredentials);
+
+    // Initialize for X.509 certificate exchange
+    gnutls_certificate_allocate_credentials(&Config.certCredentials);
+    for (std::list<std::string>::const_iterator i = Config.caFiles.begin(); i != Config.caFiles.end(); ++i) {
+        int x = gnutls_certificate_set_x509_trust_file(Config.certCredentials, (*i).c_str(), GNUTLS_X509_FMT_PEM);
+        if (x < 0) {
+            debugVerbose(3, "WARNING: Failed to load Certificate Authorities from " << *i);
+        } else {
+            debugVerbose(3, "Loaded " << x << " Certificate Authorities from " << *i);
+        }
+    }
+    gnutls_certificate_set_verify_function(Config.certCredentials, verifyTlsCertificate);
+
+    for (std::list<std::string>::const_iterator i = Config.certFiles.begin(); i != Config.certFiles.end(); ++i) {
+        if (gnutls_certificate_set_x509_key_file(Transport::Config.certCredentials, (*i).c_str(), (*i).c_str(), GNUTLS_X509_FMT_PEM) != GNUTLS_E_SUCCESS) {
+            debugVerbose(3, "WARNING: Failed to load Certificate from " << *i);
+        } else {
+            debugVerbose(3, "Loaded Certificate from " << *i);
+        }
+    }
+
+#else
+    std::cerr << "ERROR: TLS support not available." << std::endl;
+#endif
+}
+
+#if USE_GNUTLS
+
+// perform the actual handshake exchange with remote server
+static bool
+doTlsHandshake(const char *type)
+{
+    // setup the connection for TLS
+    gnutls_transport_set_int(Transport::Config.session, conn);
+    gnutls_handshake_set_timeout(Transport::Config.session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
+
+    debugVerbose(2, type << " TLS handshake ... ");
+
+    int ret = 0;
+    do {
+        ret = gnutls_handshake(Transport::Config.session);
+    } while (ret < 0 && gnutls_error_is_fatal(ret) == 0);
+
+    if (ret < 0) {
+        std::cerr << "ERROR: " << type << " TLS Handshake failed (" << ret << ") "
+                  << gnutls_alert_get_name(gnutls_alert_get(Transport::Config.session))
+                  << std::endl;
+        gnutls_perror(ret);
+        gnutls_deinit(Transport::Config.session);
+        return false;
+    }
+
+    char *desc = gnutls_session_get_desc(Transport::Config.session);
+    debugVerbose(3, "TLS Session info: " << std::endl << desc << std::endl);
+    gnutls_free(desc);
+    return true;
+}
+
+static bool
+loadTlsParameters()
+{
+    const char *err = NULL;
+    int x;
+    if ((x = gnutls_priority_set_direct(Transport::Config.session, Transport::Config.params, &err)) != GNUTLS_E_SUCCESS) {
+        if (x == GNUTLS_E_INVALID_REQUEST)
+            std::cerr << "ERROR: Syntax error at: " << err << std::endl;
+        gnutls_perror(x);
+        return false;
+    }
+    return true;
+}
+
+// attempt an anonymous TLS handshake
+// this encrypts the connection but does not secure it
+// so many public servers do not support this handshake type.
+static bool
+tryTlsAnonymous()
+{
+    if (!loadTlsParameters())
+        return false;
+
+    // put the anonymous credentials to the current session
+    int x;
+    if ((x = gnutls_credentials_set(Transport::Config.session, GNUTLS_CRD_ANON, Transport::Config.anonCredentials)) != GNUTLS_E_SUCCESS) {
+        std::cerr << "ERROR: Anonymous TLS credentials setup failed (" << x << ") " << std::endl;
+        gnutls_perror(x);
+        return false;
+    }
+
+    return doTlsHandshake("Anonymous");
+}
+
+// attempt a X.509 certificate exchange
+// this both encrypts and authenticates the connection
+static bool
+tryTlsCertificate(const char *hostname)
+{
+    gnutls_session_set_ptr(Transport::Config.session, (void *) hostname);
+    gnutls_server_name_set(Transport::Config.session, GNUTLS_NAME_DNS, hostname, strlen(hostname));
+
+    if (!loadTlsParameters())
+        return false;
+
+    // put the X.509 credentials to the current session
+    gnutls_credentials_set(Transport::Config.session, GNUTLS_CRD_CERTIFICATE, Transport::Config.certCredentials);
+
+    // setup the connection for TLS
+    gnutls_transport_set_int(Transport::Config.session, conn);
+    gnutls_handshake_set_timeout(Transport::Config.session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
+
+    return doTlsHandshake("X.509");
+}
+#endif
+
+bool
+Transport::MaybeStartTls(const char *hostname)
+{
+#if USE_GNUTLS
+    if (Config.tlsEnabled) {
+
+        // Initialize TLS session
+        gnutls_init(&Transport::Config.session, GNUTLS_CLIENT);
+
+        if (Transport::Config.tlsAnonymous && !tryTlsAnonymous()) {
+            gnutls_deinit(Config.session);
+            return false;
+        }
+
+        if (!tryTlsCertificate(hostname)) {
+            gnutls_deinit(Config.session);
+            return false;
+        }
+    }
+#endif
+    return true;
+}
+
+void
+Transport::ShutdownTls()
+{
+#if USE_GNUTLS
+    if (!Config.tlsEnabled)
+        return;
+
+    debugVerbose(3, "Shutting down TLS library...");
+
+    // release any existing session and credentials
+    gnutls_deinit(Config.session);
+    gnutls_anon_free_client_credentials(Config.anonCredentials);
+    gnutls_certificate_free_credentials(Config.certCredentials);
+
+    // NP: gnutls init is re-entrant and lock-counted with deinit but not thread safe.
+    gnutls_global_deinit();
+    Config.tlsEnabled = false;
+#endif
+}
diff --git a/tools/squidclient/Transport.h b/tools/squidclient/Transport.h
new file mode 100644 (file)
index 0000000..f6ece63
--- /dev/null
@@ -0,0 +1,113 @@
+#ifndef SQUID_TOOLS_SQUIDCLIENT_TRANSPORT_H
+#define SQUID_TOOLS_SQUIDCLIENT_TRANSPORT_H
+
+#include "tools/squidclient/Parameters.h"
+
+#if HAVE_GNUTLS_GNUTLS_H
+#include <gnutls/gnutls.h>
+#endif
+#include <list>
+#include <string>
+
+namespace Transport
+{
+
+/// parameters controlling outgoing connection
+class TheConfig
+{
+public:
+    TheConfig() :
+        ioTimeout(120),
+        localHost(NULL),
+        port(CACHE_HTTP_PORT),
+        tlsEnabled(false),
+        tlsAnonymous(false)
+    {
+        params = "NORMAL";
+        hostname = "localhost";
+    }
+
+// TODO: implicit transport options depending on the protocol-specific options
+//     ie --https enables TLS connection settings
+
+    /// display Transport Options command line help to stderr
+    void usage();
+
+    /**
+     * parse transport related command line options
+     * \return true if there are other options still to parse
+     */
+    bool parseCommandOpts(int argc, char *argv[], int c, int &optIndex);
+
+    /// I/O operation timeout
+    int ioTimeout;
+
+    /// the local hostname to bind as for outgoing IP
+    const char *localHost;
+
+    /// the destination server host name to contact
+    const char *hostname;
+
+    /// port on the server to contact
+    uint16_t port;
+
+    /// whether to enable TLS on the server connnection
+    bool tlsEnabled;
+
+    /// whether to do anonymous TLS (non-authenticated)
+    bool tlsAnonymous;
+
+    /// The TLS parameters (list of ciphers, versions, flags)
+    /// Default is "NORMAL" unless tlsAnonymous is used,
+    /// in which case it becomes "PERFORMANCE:+ANON-ECDH:+ANON-DH".
+    /// see http://gnutls.org/manual/html_node/Priority-Strings.html
+    const char *params;
+
+    // client certificate PEM file(s)
+    std::list<std::string> certFiles;
+
+    // client trusted x509 certificate authorities file
+    std::list<std::string> caFiles;
+
+#if USE_GNUTLS
+    /// anonymous client credentials
+    gnutls_anon_client_credentials_t anonCredentials;
+
+    // client x509 certificate credentials
+    gnutls_certificate_credentials_t certCredentials;
+
+    /// TLS session state
+    gnutls_session_t session;
+#endif
+};
+
+extern TheConfig Config;
+
+/// locate and connect to the configured server
+bool Connect();
+
+/// close the current connection
+void CloseConnection();
+
+/// Initialize TLS library environment when necessary.
+void InitTls();
+
+/// perform TLS handshake on the currently open connection if
+/// TLS library has been initialized.
+/// return false on errors, true otherwise even if TLS not performed.
+bool MaybeStartTls(const char *hostname);
+
+/// De-initialize TLS library environment when necessary.
+void ShutdownTls();
+
+/// write len bytes to the currently open connection.
+/// \return the number of bytes written, or -1 on errors
+ssize_t Write(void *buf, size_t len);
+
+/// read up to len bytes from the currently open connection.
+/// \return the number of bytes read, or -1 on errors
+ssize_t Read(void *buf, size_t len);
+
+} // namespace Transport
+
+#endif /* SQUID_TOOLS_SQUIDCLIENT_TRANSPORT_H */
index b158298db16181dc312f431b035c6835a4b48614..662374b01aae6829cf90a4bae4173c04a0bed220 100644 (file)
@@ -7,10 +7,11 @@ A simple HTTP web client tool
 .
 .SH SYNOPSIS
 .if !'po4a'hide' .B squidclient
-.if !'po4a'hide' .B "[ \-\-ping [ping-options] ] "
-.if !'po4a'hide' .B "[ \-aknNrsv ] [ \-A"
+.if !'po4a'hide' .B "[ \-aknNrsv ] "
+.if !'po4a'hide' .B "[ \-\-ping [ping\-options] ] "
+.if !'po4a'hide' .B "[ \-\-tls [tls\-options] ] [ \-A"
 string
-.if !'po4a'hide' .B "] [ \-h"
+.if !'po4a'hide' .B "] [ \-h | \-\-host"
 remote host
 .if !'po4a'hide' .B "] [ \-H '"
 string
@@ -18,11 +19,11 @@ string
 IMS
 .if !'po4a'hide' .B "] [ \-j '"
 Host header
-.if !'po4a'hide' .B "' ] [ \-l"
-local host
+.if !'po4a'hide' .B "' ] [ \-l | \-\-local"
+host
 .if !'po4a'hide' .B "] [ \-m"
 method
-.if !'po4a'hide' .B "] [ \-p"
+.if !'po4a'hide' .B "] [ \-p | \-\-port"
 port
 .if !'po4a'hide' .B "] [ \-P"
 file
@@ -49,6 +50,14 @@ count
 interval
 .if !'po4a'hide' .B "] "
 .
+.if !'po4a'hide' .B "TLS options: [ \-\-anonymous ] [ \-\-trusted\-ca"
+CA certificates file
+.if !'po4a'hide' .B "...] [ \-\-cert"
+client X.509 certificate file
+.if !'po4a'hide' .B "] [ \-\-params"
+TLS session parameters
+.if !'po4a'hide' .B "] "
+.
 .SH DESCRIPTION
 .B squidclient
 is a tool providing a command line interface for retrieving URLs.
@@ -70,8 +79,8 @@ Send
 as User-Agent: header. To omit the header completely set string to empty ('').
 .
 .if !'po4a'hide' .TP
-.if !'po4a'hide' .B "\-h host"
-Retrieve URL from cache on hostname.  Default is
+.if !'po4a'hide' .B "\-h | \-\-host host"
+Retrieve URL from server host.  Default is
 .B localhost
 .
 .if !'po4a'hide' .TP
@@ -93,7 +102,7 @@ Host header content
 Keep the connection active. Default is to do only one request then close.
 .
 .if !'po4a'hide' .TP
-.if !'po4a'hide' .B "\-l host"
+.if !'po4a'hide' .B "\-l | \-\-local host"
 Specify a local IP address to bind to.  Default is none.
 .
 .if !'po4a'hide' .TP
@@ -192,6 +201,31 @@ iterations (default is to loop until interrupted).
 .if !'po4a'hide' .B "\-I interval"
 Ping interval in seconds (default 1 second).
 .
+.if !'po4a'hide' .TP 10
+.if !'po4a'hide' .B "\-\-tls [options]"
+Use Transport Layer Security on the connection.
+.
+.if !'po4a'hide' .TP 12
+.if !'po4a'hide' .B "\-\-anonymous"
+Use TLS with unauthenticated (anonymous) certificate.
+.
+.if !'po4a'hide' .TP
+.if !'po4a'hide' .B "\-\-cert file"
+File containing client X.509 certificate in PEM format.
+May be repeated to load several client certificates.
+.
+.if !'po4a'hide' .TP 12
+.if !'po4a'hide' .B "\-\-trusted-ca file"
+File containing trusted Certificate Authority (CA) certificates in PEM format.
+May be repeated to load any number of files.
+.
+.if !'po4a'hide' .TP 12
+.if !'po4a'hide' .B "\-\-params values"
+TLS library specific parameters for the communication session.
+See the library documentation for details on valid parameters.
+.if !'po4a'hide' .I "GnuTLS: http://gnutls.org/manual/html_node/Priority\-Strings.html"
+If repeated only the last value will have effect.
+.
 .SH AUTHOR
 Derived from Harvest. Further developed by by numerous individuals from
 the internet community. Development is led by Duane Wessels of the
index 06019cb15c0a2f7c37cf1eaba476bcdf99e8f783..2cb3c8859226fd050b1abdfd1ac9917fee245430 100644 (file)
@@ -38,6 +38,7 @@
 #include "tools/squidclient/gssapi_support.h"
 #include "tools/squidclient/Parameters.h"
 #include "tools/squidclient/Ping.h"
+#include "tools/squidclient/Transport.h"
 
 #if _SQUID_WINDOWS_
 /** \cond AUTODOCS-IGNORE */
@@ -84,20 +85,11 @@ using namespace Squid;
 #define HEADERLEN      65536
 #endif
 
-/// display debug messages at varying verbosity levels
-#define debugVerbose(LEVEL, MESSAGE) \
-    while ((LEVEL) <= scParams.verbosityLevel) {std::cerr << MESSAGE << std::endl; break;}
-
 /* Local functions */
-static int client_comm_bind(int, const Ip::Address &);
-
-static int client_comm_connect(int, const Ip::Address &);
 static void usage(const char *progname);
 
 void pipe_handler(int sig);
 static void set_our_signal(void);
-static ssize_t myread(int fd, void *buf, size_t len);
-static ssize_t mywrite(int fd, void *buf, size_t len);
 
 Parameters scParams;
 
@@ -106,7 +98,6 @@ static char *put_file = NULL;
 
 static struct stat sb;
 int total_bytes = 0;
-int io_timeout = 120;
 
 #if _SQUID_AIX_
 /* Bug 3854: AIX 6.1 tries to link in this fde.h global symbol
@@ -129,19 +120,16 @@ usage(const char *progname)
 {
     std::cerr << "Version: " << VERSION << std::endl
               << "Usage: " << progname << " [Basic Options] [HTTP Options]" << std::endl
-              << std::endl
-              << "Basic Options:" << std::endl
-              << "    -h host         Send message to server on 'host'.  Default is localhost." << std::endl
-              << "    -l host         Specify a local IP address to bind to.  Default is none." << std::endl
-              << "    -p port         Port number on server to contact. Default is " << CACHE_HTTP_PORT << "." << std::endl
+              << std::endl;
+    std::cerr
               << "    -s | --quiet    Silent.  Do not print response message to stdout." << std::endl
-              << "    -T timeout      Timeout value (seconds) for read/write operations" << std::endl
               << "    -v | --verbose  Verbose debugging. Repeat (-vv) to increase output level." << std::endl
               << "                    Levels:" << std::endl
               << "                      1 - Print outgoing request message to stderr." << std::endl
               << "                      2 - Print action trace to stderr." << std::endl
               << "    --help          Display this help text." << std::endl
               << std::endl;
+    Transport::Config.usage();
     Ping::Config.usage();
     std::cerr
         << "HTTP Options:" << std::endl
@@ -171,16 +159,13 @@ usage(const char *progname)
 int
 main(int argc, char *argv[])
 {
-    int conn, len, bytesWritten;
-    uint16_t port;
+    int len, bytesWritten;
     bool to_stdout, reload;
     int keep_alive = 0;
     int opt_noaccept = 0;
 #if HAVE_GSSAPI
     int www_neg = 0, proxy_neg = 0;
 #endif
-    const char *hostname, *localhost;
-    Ip::Address iaddr;
     char url[BUFSIZ], msg[MESSAGELEN], buf[BUFSIZ];
     char extra_hdrs[HEADERLEN];
     const char *method = "GET";
@@ -197,10 +182,7 @@ main(int argc, char *argv[])
     const char *useragent = NULL;
 
     /* set the defaults */
-    hostname = "localhost";
-    localhost = NULL;
     extra_hdrs[0] = '\0';
-    port = CACHE_HTTP_PORT;
     to_stdout = true;
     reload = false;
 
@@ -212,7 +194,7 @@ main(int argc, char *argv[])
         url[BUFSIZ - 1] = '\0';
 
         int optIndex = 0;
-        const char *shortOpStr = "aA:h:j:V:l:P:i:kmnN:p:rsvt:p:H:T:u:U:w:W:?";
+        const char *shortOpStr = "aA:h:j:V:l:P:i:kmnN:p:rsvt:H:T:u:U:w:W:?";
 
         // options for controlling squidclient
         static struct option basicOptions[] = {
@@ -220,7 +202,11 @@ main(int argc, char *argv[])
             {"help",    no_argument, 0, '?'},
             {"verbose", no_argument, 0, 'v'},
             {"quiet",   no_argument, 0, 's'},
+            {"host",    required_argument, 0, 'h'},
+            {"local",   required_argument, 0, 'l'},
+            {"port",    required_argument, 0, 'p'},
             {"ping",    no_argument, 0, '\1'},
+            {"tls",     no_argument, 0, '\3'},
             {0, 0, 0, 0}
         };
 
@@ -231,9 +217,18 @@ main(int argc, char *argv[])
             switch (c) {
             case '\1':
                 to_stdout = 0;
-                if (Ping::Config.parseCommandOpts(argc, argv, c, optIndex))
-                    continue;
-                break;
+                Ping::Config.parseCommandOpts(argc, argv, c, optIndex);
+                continue;
+
+            case 'h':          /* remote host */
+            case 'l':          /* local host */
+            case 'p':          /* port number */
+                // rewind and let the Transport::Config parser handle
+                optind -= 2;
+
+            case '\3': // request over a TLS connection
+                Transport::Config.parseCommandOpts(argc, argv, c, optIndex);
+                continue;
 
             default: // fall through to next switch
                 break;
@@ -252,10 +247,6 @@ main(int argc, char *argv[])
                 useragent = optarg;
                 break;
 
-            case 'h':          /* remote host */
-                hostname = optarg;
-                break;
-
             case 'j':
                 host = optarg;
                 break;
@@ -264,10 +255,6 @@ main(int argc, char *argv[])
                 version = optarg;
                 break;
 
-            case 'l':          /* local host */
-                localhost = optarg;
-                break;
-
             case 's':          /* silent */
                 to_stdout = false;
                 break;
@@ -280,12 +267,6 @@ main(int argc, char *argv[])
                 reload = true;
                 break;
 
-            case 'p':          /* port number */
-                sscanf(optarg, "%hd", &port);
-                if (port < 1)
-                    port = CACHE_HTTP_PORT;    /* default */
-                break;
-
             case 'P':
                 put_file = xstrdup(optarg);
                 break;
@@ -313,7 +294,7 @@ main(int argc, char *argv[])
                 break;
 
             case 'T':
-                io_timeout = atoi(optarg);
+                Transport::Config.ioTimeout = atoi(optarg);
                 break;
 
             case 'u':
@@ -380,9 +361,9 @@ main(int argc, char *argv[])
         }
         // embed the -w proxy password into old-style cachemgr URLs
         if (at)
-            snprintf(url, BUFSIZ, "cache_object://%s/%s@%s", hostname, t, at);
+            snprintf(url, BUFSIZ, "cache_object://%s/%s@%s", Transport::Config.hostname, t, at);
         else
-            snprintf(url, BUFSIZ, "cache_object://%s/%s", hostname, t);
+            snprintf(url, BUFSIZ, "cache_object://%s/%s", Transport::Config.hostname, t);
         xfree(t);
     }
     if (put_file) {
@@ -499,8 +480,8 @@ main(int argc, char *argv[])
                 std::cerr << "ERROR: server host missing" << std::endl;
         }
         if (proxy_neg) {
-            if (hostname) {
-                snprintf(buf, BUFSIZ, "Proxy-Authorization: Negotiate %s\r\n", GSSAPI_token(hostname));
+            if (Transport::Config.hostname) {
+                snprintf(buf, BUFSIZ, "Proxy-Authorization: Negotiate %s\r\n", GSSAPI_token(Transport::Config.hostname));
                 strcat(msg, buf);
             } else
                 std::cerr << "ERROR: proxy server host missing" << std::endl;
@@ -525,61 +506,13 @@ main(int argc, char *argv[])
 
     for (uint32_t i = 0; loops == 0 || i < loops; ++i) {
         size_t fsize = 0;
-        struct addrinfo *AI = NULL;
-
-        debugVerbose(2, "Resolving... " << hostname);
-
-        /* Connect to the server */
-
-        if (localhost) {
-            if ( !iaddr.GetHostByName(localhost) ) {
-                std::cerr << "ERROR: Cannot resolve " << localhost << ": Host unknown." << std::endl;
-                exit(1);
-            }
-        } else {
-            /* Process the remote host name to locate the Protocol required
-               in case we are being asked to link to another version of squid */
-            if ( !iaddr.GetHostByName(hostname) ) {
-                std::cerr << "ERROR: Cannot resolve " << hostname << ": Host unknown." << std::endl;
-                exit(1);
-            }
-        }
-
-        iaddr.getAddrInfo(AI);
-        if ((conn = socket(AI->ai_family, AI->ai_socktype, 0)) < 0) {
-            std::cerr << "ERROR: could not open socket to " << iaddr << std::endl;
-            Ip::Address::FreeAddrInfo(AI);
-            exit(1);
-        }
-        Ip::Address::FreeAddrInfo(AI);
-
-        if (localhost && client_comm_bind(conn, iaddr) < 0) {
-            std::cerr << "ERROR: could not bind socket to " << iaddr << std::endl;
-            exit(1);
-        }
-
-        iaddr.setEmpty();
-        if ( !iaddr.GetHostByName(hostname) ) {
-            std::cerr << "ERROR: Cannot resolve " << hostname << ": Host unknown." << std::endl;
-            exit(1);
-        }
-
-        iaddr.port(port);
 
-        debugVerbose(2, "Connecting... " << hostname << " (" << iaddr << ")");
-
-        if (client_comm_connect(conn, iaddr) < 0) {
-            char hostnameBuf[MAX_IPSTRLEN];
-            iaddr.toUrl(hostnameBuf, MAX_IPSTRLEN);
-            std::cerr << "ERROR: Cannot connect to " << hostnameBuf
-                      << (!errno ?": Host unknown." : "") << std::endl;
-            exit(1);
-        }
-        debugVerbose(2, "Connected to: " << hostname << " (" << iaddr << ")");
+        if (!Transport::Connect())
+            continue;
 
         /* Send the HTTP request */
         debugVerbose(2, "Sending HTTP request ... ");
-        bytesWritten = mywrite(conn, msg, strlen(msg));
+        bytesWritten = Transport::Write(msg, strlen(msg));
 
         if (bytesWritten < 0) {
             std::cerr << "ERROR: write" << std::endl;
@@ -596,7 +529,7 @@ main(int argc, char *argv[])
             lseek(put_fd, 0, SEEK_SET);
             while ((x = read(put_fd, buf, sizeof(buf))) > 0) {
 
-                x = mywrite(conn, buf, x);
+                x = Transport::Write(buf, x);
 
                 total_bytes += x;
 
@@ -615,18 +548,30 @@ main(int argc, char *argv[])
         setmode(1, O_BINARY);
 #endif
 
-        while ((len = myread(conn, buf, sizeof(buf))) > 0) {
+        while ((len = Transport::Read(buf, sizeof(buf))) > 0) {
             fsize += len;
 
             if (to_stdout && fwrite(buf, len, 1, stdout) != 1)
                 std::cerr << "ERROR: writing to stdout: " << xstrerror() << std::endl;
         }
 
+#if USE_GNUTLS
+        if (Transport::Config.tlsEnabled) {
+            if (len == 0) {
+                std::cerr << "- Peer has closed the TLS connection" << std::endl;
+            } else if (!gnutls_error_is_fatal(len)) {
+                std::cerr << "WARNING: " << gnutls_strerror(len) << std::endl;
+            } else {
+                std::cerr << "ERROR: " << gnutls_strerror(len) << std::endl;
+            }
+        }
+#endif
+
 #if _SQUID_WINDOWS_
         setmode(1, O_TEXT);
 #endif
 
-        (void) close(conn);    /* done with socket */
+        Transport::CloseConnection();
 
         if (Ping::LoopDone(i))
             break;
@@ -635,32 +580,10 @@ main(int argc, char *argv[])
     }
 
     Ping::DisplayStats();
+    Transport::ShutdownTls();
     return 0;
 }
 
-/// Set up the source socket address from which to send.
-static int
-client_comm_bind(int sock, const Ip::Address &addr)
-{
-    static struct addrinfo *AI = NULL;
-    addr.getAddrInfo(AI);
-    int res = bind(sock, AI->ai_addr, AI->ai_addrlen);
-    Ip::Address::FreeAddrInfo(AI);
-    return res;
-}
-
-/// Set up the destination socket address for message to send to.
-static int
-client_comm_connect(int sock, const Ip::Address &addr)
-{
-    static struct addrinfo *AI = NULL;
-    addr.getAddrInfo(AI);
-    int res = connect(sock, AI->ai_addr, AI->ai_addrlen);
-    Ip::Address::FreeAddrInfo(AI);
-    Ping::TimerStart();
-    return res;
-}
-
 void
 pipe_handler(int sig)
 {
@@ -684,25 +607,3 @@ set_our_signal(void)
     signal(SIGPIPE, pipe_handler);
 #endif
 }
-
-static ssize_t
-myread(int fd, void *buf, size_t len)
-{
-#if _SQUID_WINDOWS_
-    return recv(fd, buf, len, 0);
-#else
-    alarm(io_timeout);
-    return read(fd, buf, len);
-#endif
-}
-
-static ssize_t
-mywrite(int fd, void *buf, size_t len)
-{
-#if _SQUID_WINDOWS_
-    return send(fd, buf, len, 0);
-#else
-    alarm(io_timeout);
-    return write(fd, buf, len);
-#endif
-}