4

I'm implementing http proxy server with some business logic on boost asio.

In point (1) boost::asio::streambuf response_ contains http headers and part of http body.

After parsing with http_response::parse buffer boost::asio::streambuf response_ is empty.

At (2) i check all business logic and read body if there was Content-Length header in headers.

Then if response_ data fits specific condtions i want to send the original response_ buffer to another socket (3).

The problem is that buffer is empty after parsing. Is there a way to copy boost::asio::streambuf to reuse data?

void http_response::parse(boost::asio::streambuf& buffer)
{
    std::istream response_stream(&buffer);
    response_stream >> version_;
    response_stream >> status_code_;
    response_stream >> status_message_;
    std::string key;
    std::string value;
    std::string header;
    std::getline(response_stream, header);
    while (std::getline(response_stream, header) && header != "\r") {
        header.resize(header.size() - 1);
        std::size_t found = header.find(':');
        if (found != std::string::npos) {
            key = header.substr(0, found);
            value = header.substr(found + 2);
            headers_[key] = value;
        }
    }
}
bool go(const std::string& hostname, const std::string& path,
    const std::string& server, int port,
    boost::asio::io_service::strand& strand,
    boost::asio::yield_context& yield)
{
    ...
    http_response response;
    boost::asio::streambuf response_;
    // async read http header from socket
    std::clog << "<- " << sequence_ << " schedule async_read_until head" << std::endl;
    boost::asio::async_read_until(socket_, response_, "\r\n\r\n", yield[err]);
    check_error_and_timeout(err, timeout_);
    // 1. response_.size() == 512 here
    response.parse(response_); 
    // 2. response_.size() == 0 empty here
    // using headers for business logic check
    ...
    // read http body if Content-Length > 0
    const std::string str_content_length = response.get_header("Content-Length", "");
    const size_t content_length = std::stoi(str_content_length);
    if(!str_content_length.empty() && content_length > response_.size())
    {
        std::clog << "<- " << sequence_ << " schedule async_read body" << std::endl;
        boost::asio::async_read(socket_, response_,
        boost::asio::transfer_at_least(content_length - response_.size()),
            yield[err]);
        check_error_and_timeout(err, timeout_);
    }
    // 3. after read all header and body write all data to server sock
    boost::asio::async_write(server_socket_, response_, yield[err]);
}
e.proydakov
  • 544
  • 6
  • 18

2 Answers2

13

One can use boost::asio::buffer_copy() to copy the contents of Asio buffers. This can be convenient if, for example, one wishes to copy the contents of one streambuf to another streambuf.

boost::asio::streambuf source, target;
...
std::size_t bytes_copied = buffer_copy(
  target.prepare(source.size()), // target's output sequence
  source.data());                // source's input sequence
// Explicitly move target's output sequence to its input sequence.
target.commit(bytes_copied);

A similar approach can be used to copy from a streambuf into any type for which Asio supports mutable buffers. For example, copying content into a std::vector<char>:

boost::asio::streambuf source;
...
std::vector<char> target(source.size());
buffer_copy(boost::asio::buffer(target), source.data());

One notable exception is that Asio does not support returning a mutable buffer for std::string. However, one can still accomplish copying into std::string via iterators:

boost::asio::streambuf source;
...
std::string target{
  buffers_begin(source.data()),
  buffers_end(source.data())
};

Here is an example demonstrating copying contents from boost::asio::streambuf into various other types:

#include <iostream>
#include <string>
#include <vector>
#include <boost/asio.hpp>

int main()
{
  const std::string expected = "This is a demo";

  // Populate source's input sequence.
  boost::asio::streambuf source;
  std::ostream ostream(&source);
  ostream << expected;

  // streambuf example
  {
    boost::asio::streambuf target;
    target.commit(buffer_copy(
      target.prepare(source.size()), // target's output sequence
      source.data()));               // source's input sequence

    // Verify the contents are equal.
    assert(std::equal(
      buffers_begin(target.data()),
      buffers_end(target.data()),
      begin(expected)
    ));
  }

  // std::vector example
  {
    std::vector<char> target(source.size());
    buffer_copy(boost::asio::buffer(target), source.data());

    // Verify the contents are equal.
    assert(std::equal(begin(target), end(target), begin(expected)));
  }

  // std::string example
  {
    // Copy directly into std::string.  Asio does not support
    // returning a mutable buffer for std::string.
    std::string target{
      buffers_begin(source.data()),
      buffers_end(source.data())
    };

    // Verify the contents are equal.
    assert(std::equal(begin(target), end(target), begin(expected)));
  }
}
sehe
  • 374,641
  • 47
  • 450
  • 633
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • Nice answer. It would be also nice to mention source.consume(bytes_copied); in your buffer_copy example. I needed this since in some cases I need to transfer the contents instead of just copying. – Mert Jan 23 '19 at 11:33
1

First, you cannot copy streambuf - it has deleted copy constructor.

I should suggest using your own buffer using asio::buffer and parse it. But with this you will not be able to use async_read_until. Also, you can do consuming-copy into your own buffer, and then send it if needed.

Lifehack: you can get the data without consuming it using buffer_cast, but note its not safe way since its not documented and may broke (but it worked for me many times and many boost upgrades):

// req_buf is a streambuf
const char *req_data = boost::asio::buffer_cast<const char *>( req_buf.data() );
auto sz = req_buf.size();

Note again: this cast is working because streambuf implementing some concepts, but its not documented directly. Also, remember req_data can be invalidated after any req_buf modify.

Galimov Albert
  • 7,269
  • 1
  • 24
  • 50
  • boost::asio::streambuf is not a contiguous array. Thus, I assume using raw pointers will lead to unexpected behaviour, e.g. trying to traverse the raw pointer array, or copy data using the raw pointer. Won't it? – Oleh Zaiats Nov 09 '22 at 18:34
  • UPD. the boost.asio docs say that `streambuf` _currently_ uses a contiguous array. From the docs: "... single contiguous character array, which is reallocated as necessary to accommodate changes in the size of the character sequence. This is the implementation approach currently used in Asio." Not sure if it will still be the case for latter versions. – Oleh Zaiats Nov 15 '22 at 14:55