neutrinotrap
neutrinotrap

Reputation: 13

HTTP proxy example in C++

So I've been trying to write a proxy in C++ using the boost.asio. My initial project includes the client that writes a string message into a socket, a server that receives this message and writes a string message into a socket, and a proxy that works with the two mentioned sockets.

The proxy code looks like this (The future intention is handle multiple connections and to use the transfered data somehow, and the callbacks would perform some actual work other than logging):

#include "commondata.h"
#include <boost/bind.hpp>
#include <boost/enable_shared_from_this.hpp>

using namespace boost::asio;
using ip::tcp;
using std::cout;
using std::endl;


class con_handler : public boost::enable_shared_from_this<con_handler> {
private:
    tcp::socket client_socket;
    tcp::socket server_socket;
    enum { max_length = 1024 };
    char client_data[max_length];
    char server_data[max_length];

public:
    typedef boost::shared_ptr<con_handler> pointer;
    con_handler(boost::asio::io_service& io_service):
            server_socket(io_service),
            client_socket(io_service) {
        memset(client_data, 0, max_length);
        memset(server_data, 0, max_length);
        server_socket.connect( tcp::endpoint( boost::asio::ip::address::from_string(SERVERIP), SERVERPORT ));
    }
// creating the pointer
    static pointer create(boost::asio::io_service& io_service) {
        return pointer(new con_handler(io_service));
    }
//socket creation
    tcp::socket& socket() {
        return client_socket;
    }

    void start() {
        //read the data into the input buffer
        client_socket.async_read_some(
                boost::asio::buffer(client_data, max_length),
                boost::bind(&con_handler::handle_read,
                            shared_from_this(),
                            client_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        server_socket.async_write_some(
                boost::asio::buffer(client_data, max_length),
                boost::bind(&con_handler::handle_write,
                            shared_from_this(),
                            client_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        server_socket.async_read_some(
                boost::asio::buffer(server_data, max_length),
                boost::bind(&con_handler::handle_read,
                            shared_from_this(),
                            server_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        client_socket.async_write_some(
                boost::asio::buffer(server_data, max_length),
                boost::bind(&con_handler::handle_write,
                            shared_from_this(),
                            server_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
    }

    void handle_read(const char* data, const boost::system::error_code& err, size_t bytes_transferred) {
        if (!err) {
            cout << "proxy handle_read" << endl;
            cout << data << endl;
        } else {
            std::cerr << "error: " << err.message() << std::endl;
            client_socket.close();
        }
    }

    void handle_write(const char* data, const boost::system::error_code& err, size_t bytes_transferred) {
        if (!err) {
            cout << "proxy handle_write" << endl;
            cout << data << endl;
        } else {
            std::cerr << "error: " << err.message() << endl;
            client_socket.close();
        }
    }
};


class Server {
private:
    boost::asio::io_service io_service;
    tcp::acceptor acceptor_;
    void start_accept() {
        // socket
        con_handler::pointer connection = con_handler::create(io_service);

        // asynchronous accept operation and wait for a new connection.
        acceptor_.async_accept(connection->socket(),
                               boost::bind(&Server::handle_accept, this, connection,
                                           boost::asio::placeholders::error));
    }

public:
//constructor for accepting connection from client
    Server()
            : acceptor_(io_service, tcp::endpoint(tcp::v4(), PROXYPORT)) {
        start_accept();
    }

    void handle_accept(const con_handler::pointer& connection, const boost::system::error_code& err) {
        if (!err) {
            connection->start();
        }
        start_accept();
    }
    boost::asio::io_service& get_io_service() {
        return io_service;
    }
};


int main(int argc, char *argv[]) {
    try {
        Server server;
        server.get_io_service().run();
    } catch(std::exception& e) {
        std::cerr << e.what() << endl;
    }
    return 0;
}

If the messages sent are strings (which I've used initially to test if my code works at all), then all of the callbacks are called the way I wanted them to be called, and the thing seems to be working.

Here's the stdout of the proxy for that case:

user@laptop:$ ./proxy 
proxy handle_read
message from the client
proxy handle_write
message from the client
proxy handle_read
message from server
proxy handle_write
message from server

So the client sends the "message from the client" string, which is received and saved by the proxy, the same string is sent to the server, then the server sends back the "message from server" string, which is also received and saved by the proxy and then is sent to the client.

The problem appears when I try to use the actual web server (Apache) and an application like JMeter to talk to each other. This is the stdout for this case:

user@laptop:$ ./proxy 
proxy handle_write

proxy handle_write

proxy handle_read
GET / HTTP/1.1
Connection: keep-alive
Host: 127.0.0.1:1337
User-Agent: Apache-HttpClient/4.5.5 (Java/11.0.8)


error: End of file

The JMeter test then fails with a timeout (that is when the proxy gets the EOF error), and no data seems to be sent to the apache webserver. The questions that I have for now are then why the callbacks are called in another order comparing to the case when the string messages are sent and why the data is not being transferred to the server socket, I guess. Thanks in advance for any help!

Upvotes: 1

Views: 3093

Answers (1)

sehe
sehe

Reputation: 392833

Abbreviating from start():

    client_socket.async_read_some  (buffer(client_data), ...);
    server_socket.async_write_some (buffer(client_data), ...);
    server_socket.async_read_some  (buffer(server_data), ...);
    client_socket.async_write_some (buffer(server_data), ...);
    //read the data into the input 
    client_socket.async_read_some  (buffer(client_data), ...);
    server_socket.async_write_some (buffer(client_data), ...);
    server_socket.async_read_some  (buffer(server_data), ...);
    client_socket.async_write_some (buffer(server_data), ...);

That's... not how async operations work. They run asynchronously, meaning that they will all immediately return.

You're simultaneously reading and writing from some buffers, without waiting for valid data. Also, you're writing the full buffer always, regardless of how much was received.

All of this spells Undefined Behaviour.

Start simple

Conceptually you just want to read:

void start() {
    //read the data into the input buffer
    client_socket.async_read_some(
            boost::asio::buffer(client_data, max_length),
            boost::bind(&con_handler::handle_read,
                        shared_from_this(),
                        client_data,
                        boost::asio::placeholders::error,
                        boost::asio::placeholders::bytes_transferred));
}

Now, once you received data, you might want to relay that:

void handle_read(const char* data, const boost::system::error_code& err, size_t bytes_transferred) {
    if (!err) {
        std::cout << "proxy handle_read" << std::endl;
        server_socket.async_write_some(
                boost::asio::buffer(client_data, bytes_transferred),
                boost::bind(&con_handler::handle_write,
                    shared_from_this(),
                    client_data,
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::bytes_transferred));
    } else {
        std::cerr << "error: " << err.message() << std::endl;
        client_socket.close();
    }
}

Note that it seems a bit arbitrary to only close one side of the connection on errors. You probably at least want to cancel() any async operations on both, optionally shutdown() and then just let the shared_ptr destruct your con_handler.

Full Duplex

Now, for full-duplex operation you can indeed start the reverse relay at the same time. It gets a little unweildy to maintain the call chains in separate methods (after all you don't just switch the buffers, but also the socket pairs).

It might be instructive to realize that you're doing the same thing twice:

client -> [...buffer...] -> server

server -> [...buffer...] -> client

You can encapsulate each side in a class, and avoid duplicating all the code:

struct relay {
    tcp::socket &from, &to;
    std::array<char, max_length> buf{};

    void run_relay(pointer self) {
      from.async_read_some(asio::buffer(buf),
          [this, self](error_code ec, size_t n) {
              if (ec) return handle(from, ec);

              /*
               *std::cout 
               *  << "From " << from.remote_endpoint()
               *  << ": " << std::quoted(std::string_view(buf.data(), n))
               *  << std::endl;
               */
              async_write(to, asio::buffer(buf, n), [this, self](error_code ec, size_t) {
                  if (ec) return handle(to, ec);
                  run_relay(self);
              });
          });
    }

    void handle(tcp::socket& which, error_code ec = {}) {
        if (ec == asio::error::eof) {
            // soft "error" - allow write to complete
            std::cout << "EOF on " << which.remote_endpoint() << std::endl;
            which.shutdown(tcp::socket::shutdown_receive, ec);
        }

        if (ec) {
            from.cancel();
            to.cancel();

            std::string reason = ec.message();
            auto fep = from.remote_endpoint(ec),
                 tep = to.remote_endpoint(ec);
            std::cout << "Stopped relay " << fep << " -> " << tep << " due to " << reason << std::endl;
        }
    }
} c_to_s {client_socket, server_socket, {0}}, 
  s_to_c {server_socket, client_socket, {0}};

Note

  • we sidestepped the bind mess by using lambdas
  • we cancel both ends of the relay on error
  • we use a std::array buffer - more safe and easier to use
  • we only write as many bytes as were received, regardless of the size of the buffer
  • we don't schedule another read until the write has completed to avoid clobbering the data in buf

Let's implement con_handler start again

Using the relay from just above:

void start() {
    c_to_s.run_relay(shared_from_this());
    s_to_c.run_relay(shared_from_this());
}

That's all. We pass ourselves so the con_handler stays alive until all operations complete.

DEMO Live On Coliru

#define PROXYPORT 8899
#define SERVERIP "173.203.57.63" // coliru IP at the time
#define SERVERPORT 80
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <iomanip>

namespace asio  = boost::asio;
using boost::asio::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;

class con_handler : public boost::enable_shared_from_this<con_handler> {
  public:
    con_handler(asio::io_service& io_service):
        server_socket(io_service),
        client_socket(io_service)
    {
        server_socket.connect({ asio::ip::address::from_string(SERVERIP), SERVERPORT });
    }
    // creating the pointer
    using pointer = boost::shared_ptr<con_handler>;
    static pointer create(asio::io_service& io_service) {
        return pointer(new con_handler(io_service));
    }

    //socket creation
    tcp::socket& socket() {
        return client_socket;
    }

    void start() {
        c_to_s.run_relay(shared_from_this());
        s_to_c.run_relay(shared_from_this());
    }

  private:
    tcp::socket server_socket;
    tcp::socket client_socket;
    enum { max_length = 1024 };

    struct relay {
        tcp::socket &from, &to;
        std::array<char, max_length> buf{};

        void run_relay(pointer self) {
          from.async_read_some(asio::buffer(buf),
              [this, self](error_code ec, size_t n) {
                  if (ec) return handle(from, ec);
    
                  /*
                   *std::cout 
                   *  << "From " << from.remote_endpoint()
                   *  << ": " << std::quoted(std::string_view(buf.data(), n))
                   *  << std::endl;
                   */
                  async_write(to, asio::buffer(buf, n), [this, self](error_code ec, size_t) {
                      if (ec) return handle(to, ec);
                      run_relay(self);
                  });
              });
        }

        void handle(tcp::socket& which, error_code ec = {}) {
            if (ec == asio::error::eof) {
                // soft "error" - allow write to complete
                std::cout << "EOF on " << which.remote_endpoint() << std::endl;
                which.shutdown(tcp::socket::shutdown_receive, ec);
            }

            if (ec) {
                from.cancel();
                to.cancel();

                std::string reason = ec.message();
                auto fep = from.remote_endpoint(ec),
                     tep = to.remote_endpoint(ec);
                std::cout << "Stopped relay " << fep << " -> " << tep << " due to " << reason << std::endl;
            }
        }
    } c_to_s {client_socket, server_socket, {0}}, 
      s_to_c {server_socket, client_socket, {0}};
};

class Server {
    asio::io_service io_service;
    tcp::acceptor acceptor_;

    void start_accept() {
        // socket
        auto connection = con_handler::create(io_service);

        // asynchronous accept operation and wait for a new connection.
        acceptor_.async_accept(
            connection->socket(),
            [connection, this](error_code ec) {
                if (!ec) connection->start();
                start_accept();
            });
    }

  public:
    Server() : acceptor_(io_service, {{}, PROXYPORT}) {
        start_accept();
    }

    void run() {
        io_service.run_for(5s); // .run();
    }
};

int main() {
    Server().run();
}

When run with

printf "GET / HTTP/1.1\r\nHost: coliru.stacked-crooked.com\r\n\r\n" | nc 127.0.0.1 8899

The server prints:

EOF on 127.0.0.1:36452

And the netcat receives reply:

HTTP/1.1 200 OK 
Content-Type: text/html;charset=utf-8
Content-Length: 8616
Server: WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29) OpenSSL/1.0.2g
Date: Sat, 01 Aug 2020 00:25:10 GMT
Connection: Keep-Alive

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<html>
    ....
</html>

Summary

Thinking clearly about what you are trying to achieve, avoids accidentally complexity. It allowed us to come up with a good building block (relay), evaporating complexity.

Upvotes: 3

Related Questions