Search code examples
c++boostboost-asio

Boost Beast sending a file using HTTP with SSL connection


I am trying to send a file to a server via BOOST BEAST library through the HTTP header.

My code is as follows (an extraction of my code):

        using boost::system::error_code;
        using boost::system::system_error;
        using net::ip::TCP;
        using stream = ssl::stream<tcp::socket>;

        typedef ssl::stream<tcp::socket> ssl_socket;




        boost::asio::io_context io_service;

        // Create a context that uses the default paths for
        // finding CA certificates.
        ssl::context ctx(ssl::context::sslv23);
        ctx.set_default_verify_paths();

        // Get a list of endpoints corresponding to the server name.
        tcp::resolver resolver(io_service);
        tcp::resolver::query query(host "https");
        tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);

        // Try each endpoint until we successfully establish a connection.
        ssl_socket socket(io_service, ctx);
        boost::asio::connect(socket.lowest_layer(), endpoint_iterator);
        socket.lowest_layer().set_option(tcp::no_delay(true));

        // Perform SSL handshake and verify the remote host's
        // certificate.
        socket.set_verify_mode(ssl::verify_peer);
        // socket.set_verify_callback(ssl::rfc2818_verification(host));
        socket.handshake(ssl_socket::client);

        std::court<<"sending the request\n";

        boost::asio::streambuf request;
        std::ostream request_stream(&request);

        std::ifstream source_file( sendFilePath, std::ios::binary | std::ios::ate );
        size_t file_size = source_file.tellg();


        std::cout<<"THE PATH:   "<<path<<"\n\n";

        std::stringstream buffer;
        buffer << source_file.rdbuf();

        request_stream << "PUT "<< path <<" HTTP/1.1\r\n";
        request_stream << "Host: " << host << "\r\n";
        request_stream << "User-Agent: "<<BOOST_BEAST_VERSION_STRING<< "\r\n";
        request_stream << "Content-Type: application/zip \r\n";
        request_stream << "Accept: */*\r\n";
        request_stream << "Content-Length: " <<  std::to_string((int)file_size) << "\r\n";
        request_stream << "path: "<< path <<"\r\n";
        request_stream << "Expect: 100-continue"<< "\r\n\r\n";
        // request_stream << "Connection: close\r\n\r\n";  //NOTE THE Double line feed
        request_stream <<  buffer.str();


        boost::asio::write(socket, request);


        boost::asio::streambuf response;
        boost::asio::read_until(socket, response, "\r\n");




        std::string s( (std::istreambuf_iterator<char>(&response)), std::istreambuf_iterator<char>() );

        std::cout<<"THE RESPONSE:   "<<s<<"\n";

I get the following response from the server:

HTTP/1.1 401 Unauthorized
Date: Fri, 12 Jan 2024 16:51:49 GMT
Server: Apache
WWW-Authenticate: Basic realm="Authentication Required"
Content-Length: 381
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Unauthorized</title>
</head><body>
<h1>Unauthorized</h1>
<p>This server could not verify that you
are authorized to access the document
requested.  Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.</p>
</body></html>

while when I do the curl function as follows: curl -v -T ./Settings.csv https:/<hostname>/filetransfer/upload/104/18cfe8ef810-683/setting.csv

It is successfully uploaded and can be viewed on the server:

Is there some parameters that I should add in my C++ code? The server doesn't need any credentials to upload the file as evident from the curl command.

curl output:

*   Trying <host_ip>:5999...
* Connected to <host> port 5999 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=<host>
*  start date: Nov 24 18:34:50 2023 GMT
*  expire date: Feb 22 18:34:49 2024 GMT
*  subjectAltName: host "<host>" matched cert's <host>
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> PUT /filetransfer/upload/104/18cfe8ef810-683/settignsss.zip HTTP/1.1
> Host: <host>:5999
> User-Agent: curl/7.85.0
> Accept: */*
> Content-Length: 1397
> Expect: 100-continue
> 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 100 Continue
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* We are completely uploaded and fine
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Fri, 12 Jan 2024 17:23:00 GMT
< Server: Apache
< X-Powered-By: Express
< Set-Cookie: connect.sid=s%3A6aLxOXdCiLoK2ZEj9KpqS3ONuG-avYBF.BCqcr9WcsfJkn%2BkWiNSge4gSOnDSFOEX38ACvLWg0cA; Path=/; HttpOnly
< Content-Length: 0
< Content-Type: application/zip
< 
* Connection #0 to host <host> left intact

*hidden the host actual name


Solution

  • Like the other commenters said, make sure the request matches what curl does. For example, Expect: 100-Continue does NOT apply to your code.

    Also, you're not using Beast at all. You're using only Asio, with the sole exception of BOOST_BEAST_VERSION_STRING which made no sense to use.

    Instead this is what it would look like in Beast:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/asio/ssl.hpp>
    #include <boost/beast.hpp>
    #include <boost/beast/http/file_body.hpp>
    #include <fstream>
    #include <iostream>
    namespace net   = boost::asio;
    namespace ssl   = net::ssl;
    namespace beast = boost::beast;
    namespace http  = beast::http;
    using boost::system::error_code;
    using net::ip::tcp;
    
    struct Demo {
        Demo() {
            // Create a context that uses the default paths for
            // finding CA certificates.
            ctx_.set_default_verify_paths();
        }
    
        auto pure_asio(std::string sendFilePath, std::string host, std::string path) {
            do_connect(host);
    
            net::streambuf request;
            {
                std::ifstream source_file(sendFilePath, std::ios::binary | std::ios::ate);
                size_t        file_size = source_file.tellg();
                source_file.seekg(0);
    
                std::ostream request_stream(&request);
                request_stream << "PUT " << path << " HTTP/1.1\r\n";
                request_stream << "Host: " << host << "\r\n";
                request_stream << "Content-Type: application/zip \r\n";
                request_stream << "Accept: */*\r\n";
                request_stream << "Content-Length: " << std::to_string((int)file_size) << "\r\n";
                request_stream << "\r\n" << source_file.rdbuf();
            }
    
            write(socket_, request);
    
            std::string response;
            read_until(socket_, net::dynamic_buffer(response), "\r\n");
            return response;
        }
    
        auto actually_beast(std::string sendFilePath, std::string host, std::string path) {
            do_connect(host);
    
            {
                http::request<http::file_body> req(http::verb::put, path, 11);
                error_code                     ec;
                req.body().open(sendFilePath.c_str(), beast::file_mode::read, ec);
                req.set(http::field::host, host);
                req.set(http::field::content_type, "application/zip");
                req.set(http::field::accept, "*/*");
                req.prepare_payload();
                // req.keep_alive(false); // to close the connection
    
                write(socket_, req);
            }
    
            {
                http::response<http::string_body> res;
                beast::flat_buffer                buf;
                read(socket_, buf, res);
    
                return res;
            }
        }
    
      private:
        using ssl_socket = ssl::stream<tcp::socket>;
        boost::asio::io_context ioc_;
        ssl::context            ctx_{ssl::context::sslv23};
    
        ssl_socket socket_{ioc_, ctx_};
    
        void do_connect(std::string const& host) {
            if (socket_.lowest_layer().is_open()) {
                std::cerr << "Warning: new connection\n";
                socket_ = ssl_socket{ioc_, ctx_};
            }
    
            {
                auto& ll = socket_.lowest_layer();
                connect(ll, tcp::resolver(ioc_).resolve(host, "https"));
                ll.set_option(tcp::no_delay(true));
            }
    
            // Perform SSL handshake and verify the remote host's certificate.
            socket_.set_verify_mode(ssl::verify_peer);
            // socket_.set_verify_callback(ssl::rfc2818_verification(host));
            socket_.handshake(ssl_socket::client);
        }
    };
    
    int main() { //
        Demo demo;
    
        std::cout << "----- pure_asio:" << demo.pure_asio("main.cpp", "httpbin.org", "/put") << std::endl;
        std::cout << "----- actually_beast:" << demo.actually_beast("main.cpp", "httpbin.org", "/put") << std::endl;
    }
    

    Printing

    ----- pure_asio:HTTP/1.1 200 OK
    Date: Fri, 12 Jan 2024 23:01:05 GMT
    Content-Type: application/json
    Content-Length: 2802
    Connection: keep-alive
    Server: gunicorn/19.9.0
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Credentials: true
    
    
    ----- actually_beast:Warning: new connection
    HTTP/1.1 200 OK
    Date: Fri, 12 Jan 2024 23:01:06 GMT
    Content-Type: application/json
    Content-Length: 2802
    Connection: keep-alive
    Server: gunicorn/19.9.0
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Credentials: true
    
    {
      "args": {}, 
      "data": "#include <boost/asio.hpp>\n#include <boost/endian/arithmetic.hpp>\n#include <filesystem>\n#include <fstream>\n#include <iostream>\nusing std::filesystem::path;\nnamespace asio = boost::asio;\nusing asio::ip::tcp;\nusing boost::system::error_code;\nusing NetworkSize = boost::endian::big_uint64_t;\n\nstatic std::vector<char> read_file(path const& fspec) {\n    std::ifstream file(fspec, std::ios::binary);\n    return std::vector<char>(std::istreambuf_iterator<char>(file), {});\n}\n\nvoid send_file_content_over_tcp(path const& fspec, tcp::socket& socket) {\n    auto const contents = read_file(fspec);\n    std::cout << \"File read successfully. File size: \" << contents.size() << \" bytes.\" << std::endl;\n\n    // Send the file content\n    NetworkSize fileSize = contents.size();\n\n    std::vector<asio::const_buffer> payload{\n        asio::buffer(&fileSize, sizeof(fileSize)),\n        asio::buffer(contents),\n    };\n    error_code ec;\n    auto       n = write(socket, payload, ec);\n\n    std::cout << \"Content bytes sent: \" << n << \" (\" << ec.message() << \")\\n\";\n}\n\nvoid receive_file_content_over_tcp(path fspec, tcp::socket& socket) {\n    error_code  ec;\n    NetworkSize expected = 0;\n    read(socket, asio::buffer(&expected, sizeof(expected)), ec);\n    std::cout << \"Expected file size: \" << expected << \" bytes (\" << ec.message() << \")\" << std::endl;\n\n    std::ofstream ofs(fspec, std::ios::trunc);\n    // ofs.exceptions(std::ios::badbit | std::ios::failbit);\n\n    for (std::array<char, 1024> buf; expected && !ec;) {\n        size_t actual = read(socket, asio::buffer(buf), ec);\n        assert(actual <= expected);\n\n        std::cout << \"Received \" << actual << \"/\" << expected << \": \" << ec.message() << \"\\n\";\n        ofs.write(buf.data(), actual);\n\n        expected -= actual;\n    }\n    if (ofs.good() && expected == 0 && (!ec || ec == asio::error::eof))\n        std::cout << \"File content received successfully.\" << std::endl;\n}\n\nint main() {\n    asio::io_context ioc;\n\n    std::thread server([ex = ioc.get_executor()] {\n        auto s = tcp::acceptor(ex, {{}, 7878}).accept();\n        send_file_content_over_tcp(\"main.cpp\", s);\n    });\n\n    ::sleep(1); // make sure server is accepting\n    {\n        tcp::socket client(ioc);\n        client.connect({{}, 7878});\n        receive_file_content_over_tcp(\"main-clone.cpp\", client);\n    }\n\n    server.join();\n}\n", 
      "files": {}, 
      "form": {}, 
      "headers": {
        "Accept": "*/*", 
        "Content-Length": "2342", 
        "Content-Type": "application/zip", 
        "Host": "httpbin.org", 
        "X-Amzn-Trace-Id": "Root=1-65a1c4b1-38d1f0da28b6e3a07392f3ff"
      }, 
      "json": null, 
      "origin": "149.143.62.235", 
      "url": "https://httpbin.org/put"
    }