Skip to content

Implement NTLM proxy authentication #337

Open
@DavidKinder

Description

@DavidKinder

The following patch, against the 0.3.0-alpha4 release, adds support for NTLM proxy authentication on Windows. I guess this probably isn't quite right for inclusion as-is, but I can change it round if you provide guidance on how you would like it structured.

Patch for connection.hpp:

--- WebSocketPP/websocketpp-drop/websocketpp/transport/asio/connection.hpp  2013-12-06 08:18:29.415002400 +0000
+++ ThirdParty/WebSocketPP/include/websocketpp/transport/asio/connection.hpp    2014-03-07 13:55:22.297122400 +0000
@@ -38,6 +38,7 @@

 #include <websocketpp/base64/base64.hpp>
 #include <websocketpp/error.hpp>
+#include <websocketpp/impl/windows_authenticate.hpp>

 #include <boost/asio.hpp>
 #include <boost/system/error_code.hpp>
@@ -361,6 +369,8 @@

         m_proxy_data->req.set_uri(authority);
         m_proxy_data->req.replace_header("Host",authority);
+        m_proxy_data->req.replace_header("Connection","keep-alive");
+        m_proxy_data->req.replace_header("Proxy-Connection","keep-alive");

         return lib::error_code();
     }
@@ -502,12 +512,12 @@
         // Set a timer so we don't wait forever for the proxy to respond
         m_proxy_data->timer = this->set_timer(
             m_proxy_data->timeout_proxy,
-            lib::bind(
+            m_strand->wrap(lib::bind(
                 &type::handle_proxy_timeout,
                 get_shared(),
                 callback,
                 lib::placeholders::_1
-            )
+            ))
         );

         // Send proxy request
@@ -586,7 +596,7 @@
             m_proxy_data->read_buf,
             "\r\n\r\n",
             m_strand->wrap(lib::bind(
-                &type::handle_proxy_read,
+                &type::handle_proxy_read_headers,
                 get_shared(),
                 callback,
                 lib::placeholders::_1,
@@ -595,80 +605,200 @@
         );
     }

-    void handle_proxy_read(init_handler callback, const
+    void handle_proxy_read_headers(init_handler callback, const
         boost::system::error_code& ec, size_t bytes_transferred)
     {
         if (m_alog.static_test(log::alevel::devel)) {
-            m_alog.write(log::alevel::devel,"asio connection handle_proxy_read");
+            m_alog.write(log::alevel::devel,"asio connection handle_proxy_read_headers");
         }

         // Timer expired or the operation was aborted for some reason.
         // Whatever aborted it will be issuing the callback so we are safe to
         // return
-        if (ec == boost::asio::error::operation_aborted ||
+        if (ec == boost::asio::error::operation_aborted || 
             m_proxy_data->timer->expires_from_now().is_negative())
         {
             m_elog.write(log::elevel::devel,"read operation aborted");
             return;
         }
+        
+        if (ec) {
+            m_elog.write(log::elevel::info,
+                "asio handle_proxy_read_headers error: "+ec.message());
+            m_proxy_data->timer->cancel();
+            callback(make_error_code(error::pass_through)); 
+            return;
+        }

-        // At this point there is no need to wait for the timer anymore
-        m_proxy_data->timer->cancel();
+        if (!m_proxy_data) {
+            m_elog.write(log::elevel::library,
+                "assertion failed: !m_proxy_data in asio::connection::handle_proxy_read_headers");
+            m_proxy_data->timer->cancel();
+            callback(make_error_code(error::general));
+            return;
+        }
+            
+        std::istream input(&m_proxy_data->read_buf);
+        m_proxy_data->res.consume(input);
+
+        if (!m_proxy_data->res.headers_ready()) {
+            // we read until the headers were done in theory but apparently
+            // they aren't. Internal endpoint error.
+            m_proxy_data->timer->cancel();
+            callback(make_error_code(error::general));
+            return;
+        }
+
+        // Read the body of the proxy response, if any
+        int content_length = atoi(m_proxy_data->res.get_header("Content-Length").c_str());
+        if (content_length > 0) {
+            boost::asio::async_read(
+                socket_con_type::get_next_layer(),
+                m_proxy_data->read_buf,
+                boost::asio::transfer_at_least(1),
+                m_strand->wrap(lib::bind(
+                    &type::handle_proxy_read_body,
+                    get_shared(),
+                    callback,
+                    lib::placeholders::_1,
+                    lib::placeholders::_2
+                ))
+            );
+        } else {
+            handle_proxy_read_body(callback, ec, 0);
+        }
+    }
+
+    void handle_proxy_read_body(init_handler callback, const 
+        boost::system::error_code& ec, size_t bytes_transferred)
+    {
+        if (m_alog.static_test(log::alevel::devel)) {
+            m_alog.write(log::alevel::devel,"asio connection handle_proxy_read_body");
+        }

+        // Timer expired or the operation was aborted for some reason.
+        // Whatever aborted it will be issuing the callback so we are safe to
+        // return
+        if (ec == boost::asio::error::operation_aborted || 
+            m_proxy_data->timer->expires_from_now().is_negative())
+        {
+            m_elog.write(log::elevel::devel,"read operation aborted");
+            return;
+        }
+        
         if (ec) {
             m_elog.write(log::elevel::info,
-                "asio handle_proxy_read error: "+ec.message());
-            callback(make_error_code(error::pass_through));
-        } else {
-            if (!m_proxy_data) {
-                m_elog.write(log::elevel::library,
-                    "assertion failed: !m_proxy_data in asio::connection::handle_proxy_read");
-                callback(make_error_code(error::general));
-                return;
-            }
+                "asio handle_proxy_read_body error: "+ec.message());
+            m_proxy_data->timer->cancel();
+            callback(make_error_code(error::pass_through)); 
+            return;
+        }

+        if (bytes_transferred > 0) {
             std::istream input(&m_proxy_data->read_buf);
-
             m_proxy_data->res.consume(input);
-
-            if (!m_proxy_data->res.headers_ready()) {
-                // we read until the headers were done in theory but apparently
-                // they aren't. Internal endpoint error.
-                callback(make_error_code(error::general));
+            if (!m_proxy_data->res.ready()) {
+                // Read more of the proxy response
+                boost::asio::async_read(
+                    socket_con_type::get_next_layer(),
+                    m_proxy_data->read_buf,
+                    boost::asio::transfer_at_least(1),
+                    m_strand->wrap(lib::bind(
+                        &type::handle_proxy_read_body,
+                        get_shared(),
+                        callback,
+                        lib::placeholders::_1,
+                        lib::placeholders::_2
+                    ))
+                );
                 return;
             }
+        }
+
+        // At this point there is no need to wait for the timer anymore
+        m_proxy_data->timer->cancel();

-            m_alog.write(log::alevel::devel,m_proxy_data->res.raw());
+        m_alog.write(log::alevel::devel,m_proxy_data->res.raw());

-            if (m_proxy_data->res.get_status_code() != http::status_code::ok) {
-                // got an error response back
-                // TODO: expose this error in a programmatically accessible way?
-                // if so, see below for an option on how to do this.
-                std::stringstream s;
-                s << "Proxy connection error: "
-                  << m_proxy_data->res.get_status_code()
-                  << " ("
-                  << m_proxy_data->res.get_status_msg()
-                  << ")";
-                m_elog.write(log::elevel::info,s.str());
-                callback(make_error_code(error::proxy_failed));
+        http::status_code::value status_code = m_proxy_data->res.get_status_code();
+        if (status_code == http::status_code::proxy_authentication_required) {
+            if (proxy_authenticate(callback)) {
                 return;
             }
+        }
+        if (status_code != http::status_code::ok) {
+            // got an error response back
+            // TODO: expose this error in a programmatically accessible way?
+            // if so, see below for an option on how to do this.
+            std::stringstream s;
+            s << "Proxy connection error: " 
+              << m_proxy_data->res.get_status_code()
+              << " ("
+              << m_proxy_data->res.get_status_msg()
+              << ")";
+            m_elog.write(log::elevel::info,s.str());
+            callback(make_error_code(error::proxy_failed));
+            return;
+        }
+            
+        // we have successfully established a connection to the proxy, now
+        // we can continue and the proxy will transparently forward the 
+        // WebSocket connection.
+
+        // TODO: decide if we want an on_proxy callback that would allow
+        // access to the proxy response.
+            
+        // free the proxy buffers and req/res objects as they aren't needed
+        // anymore
+        m_proxy_data.reset();

-            // we have successfully established a connection to the proxy, now
-            // we can continue and the proxy will transparently forward the
-            // WebSocket connection.
-
-            // TODO: decide if we want an on_proxy callback that would allow
-            // access to the proxy response.
-
-            // free the proxy buffers and req/res objects as they aren't needed
-            // anymore
-            m_proxy_data.reset();
+        // Continue with post proxy initialization
+        post_init(callback);
+    }

-            // Continue with post proxy initialization
-            post_init(callback);
+    void proxy_authenticate_ntlm(init_handler callback, const std::string& challenge) {
+        lib::error_code ec;
+        if (challenge.empty()) {
+          m_proxy_data->authenticate.reset(new security::authenticate());
+          ec = m_proxy_data->authenticate->init("NTLM");
+          if (ec) {
+              callback(ec);
+              return;
+          }
         }
+
+        std::string response = m_proxy_data->authenticate->challenge_response(base64_decode(challenge),ec);
+        if (ec) {
+            callback(ec);
+            return;
+        }
+        m_proxy_data->res = response_type();
+        m_proxy_data->req.replace_header("Proxy-Authorization","NTLM "+base64_encode(response));
+        proxy_write(callback);
+    }
+
+    bool proxy_authenticate(init_handler callback) {
+        std::string auth = m_proxy_data->res.get_header("Proxy-Authenticate");
+
+        m_alog.write(log::alevel::connect,"Proxy-Authenticate: "+auth);
+
+        // Look for a supported authentication protocol
+        std::string::iterator current = auth.begin(), end = auth.end();
+        while (current != end)
+        {
+            std::string::iterator current_end = std::find(current,end,' ');
+            std::string protocol(current,current_end);
+            current = current_end;
+            if (current != end) {
+                current++;
+            }
+
+            if (protocol == "NTLM") {
+                proxy_authenticate_ntlm(callback,std::string(current,end));
+                return true;
+            }
+        }
+        return false;
     }

     /// read at least num_bytes bytes into buf and then call handler.
@@ -913,6 +1043,7 @@
         boost::asio::streambuf read_buf;
         long timeout_proxy;
         timer_ptr timer;
+        lib::shared_ptr<security::authenticate> authenticate;
     };

     std::string m_proxy;

New file to implement the NTLM challenge-response, which I put in impl/windows_authenticate.hpp:

#ifndef WEBSOCKETPP_IMPL_WIN_AUTHENTICATE_HPP
#define WEBSOCKETPP_IMPL_WIN_AUTHENTICATE_HPP

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef SECURITY_WIN32
#define SECURITY_WIN32
#endif
#include <security.h>

#pragma comment(lib,"secur32.lib")

#include <boost/system/error_code.hpp>

namespace websocketpp {
namespace security {

class authenticate {
    CredHandle m_credentials;
    CtxtHandle m_context;

public:
    authenticate() {
      ::ZeroMemory(&m_credentials,sizeof m_credentials);
      ::ZeroMemory(&m_context,sizeof m_context);
    }

    ~authenticate() {
        ::FreeCredentialsHandle(&m_credentials);
        ::DeleteSecurityContext(&m_context);
    }

    lib::error_code init(const std::string& package) {
        TimeStamp lifetime;
        SECURITY_STATUS status = ::AcquireCredentialsHandleA(0,const_cast<LPSTR>(package.c_str()),
          SECPKG_CRED_OUTBOUND,0,0,0,0,&m_credentials,&lifetime);
        return lib::error_code(status,boost::system::get_system_category());
    }

    std::string challenge_response(const std::string& challenge, lib::error_code& ec) {
        SecBuffer inBuffer[1] = {{ challenge.size(),SECBUFFER_TOKEN,const_cast<char*>(challenge.c_str()) }};
        SecBufferDesc inBuffers = { SECBUFFER_VERSION,1,inBuffer };
        SecBuffer outBuffer[1] = {{ 0,SECBUFFER_TOKEN,0 }};
        SecBufferDesc outBuffers = { SECBUFFER_VERSION,1,outBuffer };
        bool first = challenge.empty();
        ULONG attrs = 0;
        SECURITY_STATUS status = ::InitializeSecurityContext(
          &m_credentials,
          first ? 0 : &m_context,
          0,ISC_REQ_ALLOCATE_MEMORY,0,SECURITY_NETWORK_DREP,
          first ? 0 : &inBuffers,
          0,&m_context,&outBuffers,&attrs,NULL);
        if ((status == SEC_E_OK) || (status == SEC_I_CONTINUE_NEEDED)) {
            std::string response(static_cast<char*>(outBuffer[0].pvBuffer),outBuffer[0].cbBuffer);
            ::FreeContextBuffer(outBuffer[0].pvBuffer);
            return response;
        } else {
            ec = lib::error_code(status,boost::system::get_system_category());
            return std::string();
        }
    }
};

} // namespace security
} // namespace websocketpp

#endif // WEBSOCKETPP_IMPL_WIN_AUTHENTICATE_HPP

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions