Protocol-HTTP

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

           - update docs
           - add MANIFEST.SKIP
1.1.0    08.04.2021
           - C++ codes moved to its own library with cmake
           - do not automatically add "Content-Length: 0" header to response for 304 code unless provided by user
           - treat protocol-relative urls as path
1.0.11   25.11.2020
           - improve makefile
           - change constant names
1.0.10   30.10.2020
           - bugfix: chunked data streaming correctly supports compression
           - bugfix: gzip uncompression
           - multipart/form-data: allow to define filename and content/type
1.0.9    25.06.2020
           - $request->method_str now returns stringified effective method
           - fix compilation issues on *BSD
           - fix tests on *BSD
1.0.8    22.06.2020
           - version bump
1.0.7    22.06.2020
           - fix tests: make them timezone- and randomizer- neutral

clib/README.md  view on Meta::CPAN

# Protocol-HTTP

Protocol-HTTP - very fast HTTP protocol incremental parser and serializer

Features: cookies, transparent (un)compression with `Gzip` or `Brotli`, transparent chunked transfer encoding for body streaming, respecting request's preferences when making response.

The module is a protocol implementation, by itself it does not perform any I/O activity. For HTTP library see [UniEvent-HTTP](https://github.com/CrazyPandaLimited/UniEvent-HTTP).

Currenly supported HTTP versions are 1.0 and 1.1

# Synopsis

```cpp
using namespace panda::protocol::http;
RequestSP request = Request::Builder()

clib/src/panda/protocol/http/Request.cc  view on Meta::CPAN

    for(size_t i = r.size() - 17; i < r.size(); ++i) {
        r[i] = alphabet[*random_bytes++ % alphabet_sz];
    }

    return r;
}

Request::Method Request::method() const noexcept {
    if (_method == Method::Unspecified) {
        bool use_post = (form && form.enc_type() == EncType::Multipart && (!form.empty() || (uri && !uri->query().empty()))) // complete form
                     || _form_streaming != FormStreaming::None;
        return use_post ? Method::Post : Method::Get;
    }
    return _method;
}

static inline bool _method_has_meaning_for_body (Request::Method method) {
    return method == Request::Method::Post || method == Request::Method::Put;
}

string Request::_http_header (SerializationContext& ctx) const {

clib/src/panda/protocol/http/Request.cc  view on Meta::CPAN

    s += "\r\n";

    //assert((sz_cookies + headers.size()) ==  0);
    //assert(reserved >= s.length());

    return s;
}

std::vector<string> Request::to_vector () const {
    SerializationContext ctx;
    if (_form_streaming != FormStreaming::None && _form_streaming != FormStreaming::Started)
        throw "form streaming wasn't finished";

    bool form_streaming =  _form_streaming == FormStreaming::Started;
    /* it seems nobody supports muliptart + gzip + chunk */
    ctx.compression = !form_streaming ? compression.type : Compression::Type::IDENTITY;
    ctx.body        = &body;
    ctx.uri         = uri.get();
    ctx.chunked     = this->chunked || form_streaming;

    auto add_form_header = [&](auto& boundary) {
        string ct = "multipart/form-data; boundary=";
        ct += boundary;
        ctx.handled_headers.add("Content-Type", ct);
    };

    Body form_body;
    URI form_uri;
    if (form) {
        if (form.enc_type() == EncType::Multipart) {
            if (!form.empty() || (uri && !uri->query().empty())) {
                auto boundary = form_streaming ? _form_boundary : _generate_boundary();
                ctx.uri  = form.to_body(form_body, form_uri, uri, boundary);
                ctx.body = &form_body;
                add_form_header(boundary);
            } else if (form_streaming) {
                add_form_header(_form_boundary);
            }
        }
        else if((form.enc_type() == EncType::UrlEncoded) && !form.empty()) {
            form.to_uri(form_uri, uri);
            ctx.uri = &form_uri;
        }
    }
    else if (form_streaming) {
        add_form_header(_form_boundary);
    }
    return _to_vector(ctx, [&]() { return _compile_prepare(ctx); }, [&]() { return _http_header(ctx); });
}

bool Request::expects_continue () const {
    for (auto& val : headers.get_multi("Expect")) if (val == "100-continue") return true;
    return false;
}

clib/src/panda/protocol/http/Request.cc  view on Meta::CPAN

        r += it.first;
        r += "\"";
        r += "\r\n";
        r += "\r\n";
        r += it.second;
        r += "\r\n";
    }
};

void Request::form_file_finalize(string& out) noexcept {
    if (_form_streaming == FormStreaming::File) {
        if (compressor) {
            out += compressor->flush();
            compressor.reset();
        }
        out += "\r\n"; /* finalize file */
    }
}


Request::wrapped_chunk Request::form_finish() {
    if (_form_streaming == FormStreaming::None) throw "form streaming was not started";
    if (_form_streaming == FormStreaming::Done) throw "form streaming already complete";

    string data;
    form_file_finalize(data);
    data += form_trailer(_form_boundary);
    _form_streaming = FormStreaming::Done;
    return final_chunk(data);
}

Request::wrapped_chunk Request::form_field(const string& name, const string& content, const string& filename, const string& mime_type) {
    using H = Helper<tag::form>;
    if (_form_streaming == FormStreaming::None) throw "form streaming was not started";
    if (_form_streaming == FormStreaming::Done) throw "form streaming already complete";

    string data;
    form_file_finalize(data);
    H::append(data, H::FullField{name, filename, mime_type, content}, _form_boundary);
    return make_chunk(data, compression::CompressorPtr{});  // we don't compress
}

Request::wrapped_chunk Request::form_file(const string& name, const string filename, const string& mime_type) {
    using H = Helper<tag::form>;
    if (_form_streaming == FormStreaming::None) throw "form streaming was not started";
    if (_form_streaming == FormStreaming::Done) throw "form streaming already complete";

    string data;
    form_file_finalize(data);
    _form_streaming = FormStreaming::File;

    H::append(data, H::PatrialField{name, mime_type, filename, false}, _form_boundary);
    data += "\r\n";

    return make_chunk(data, compression::CompressorPtr{});  // we don't compress
}

Request::wrapped_chunk Request::form_data(const string& content) {
    if (_form_streaming != FormStreaming::File) throw "form file streaming was not started";
    return make_chunk(content);
}

template<typename Tag, typename Container>
void _serialize(Body& body, const string &boundary, const Container& container) {
    using H = Helper<Tag>;
    string r(H::buffer_size(boundary, container));
    for(auto it : container) { H::append(r, it, boundary); }
    r += form_trailer(boundary);
    body.parts.emplace_back(r);

clib/src/panda/protocol/http/Request.h  view on Meta::CPAN

    }

    Method method     () const noexcept;
    Method method_raw () const noexcept { return _method; }

    void   method_raw (Method value) noexcept { _method = value; }

    std::uint8_t allowed_compression (bool inverse = false) const noexcept;

    void form_stream () {
        if (_form_streaming == FormStreaming::None) {
            _form_streaming = FormStreaming::Started;
            form._enc_type = EncType::Multipart;
            _form_boundary = _generate_boundary();
        }
        else if (_form_streaming != FormStreaming::Started) {
            throw "invalid state for form streaming";
        }
    }

    bool form_streaming () noexcept { return  _form_streaming == FormStreaming::Started; }

    wrapped_chunk form_finish ();
    wrapped_chunk form_field  (const string& name, const string& content, const string& filename = "", const string& mime_type = "");
    wrapped_chunk form_file   (const string& name, const string filename = "", const string& mime_type = "application/octet-stream");
    wrapped_chunk form_data   (const string& data);

protected:
    struct SerializationContext: Message::SerializationContext {
        const URI* uri;
    };

clib/src/panda/protocol/http/Request.h  view on Meta::CPAN

    string form_trailer (const string& boundary) const noexcept {
        auto sz = boundary.size() + 6;
        string r(sz);
        r += "--";
        r += boundary;
        r += "--\r\n";
        return r;
    }

    Method  _method = Method::Unspecified;
    FormStreaming _form_streaming = FormStreaming::None;
    string _form_boundary;

    template<typename... PrefN>
    void _allow_compression (Compression::Type p, PrefN... prefn) {
        compression::pack(this->compression_prefs, p);
        return _allow_compression(prefn...);
    }
    void _allow_compression () {}
    void form_file_finalize (string& out) noexcept;

clib/tests/compile/form.cc  view on Meta::CPAN

template<typename String, typename Container>
string merge(String s, Container c) {
    for(auto& it:c) {
        s += string(it);
    }
    return s;
}

// content can be tested with http://ptsv2.com + netcat

TEST("multipart/form-data (streaming)") {
    auto req = Request::Builder().form_stream().build();
    SECTION("emtpy form") {
        auto data = req->to_string();
        data = merge(data, req->form_finish());
        CHECK(canonize(data).first ==
            "POST / HTTP/1.1\r\n"
            "Content-Type: multipart/form-data; boundary=-----------------------XXXXXXXXXXXXXXXXX\r\n"
            "Transfer-Encoding: chunked\r\n"
            "\r\n"
            "2e\r\n"

clib/tests/compile/form.cc  view on Meta::CPAN

            "\r\n"
            "[pdf]"
            "\r\n\r\n"
            "2e\r\n"
            "-------------------------XXXXXXXXXXXXXXXXX--\r\n"
            "\r\n"
            "0\r\n\r\n"
        );
    }

    SECTION("start streaming file") {
        auto data = req->to_string();
        data = merge(data, req->form_file("key", "cv.pdf", "application/pdf"));
        data = merge(data, req->form_data("[0123456789]"));
        data = merge(data, req->form_finish());
        //std::cout << "zzz:\n" << data << "zzz\n";
        CHECK(canonize(data).first ==
            "POST / HTTP/1.1\r\n"
            "Content-Type: multipart/form-data; boundary=-----------------------XXXXXXXXXXXXXXXXX\r\n"
            "Transfer-Encoding: chunked\r\n"
            "\r\n"

clib/tests/compile/form.cc  view on Meta::CPAN

            "[0123456789]"
            "\r\n"
            "30\r\n"
            "\r\n"
            "-------------------------XXXXXXXXXXXXXXXXX--\r\n"
            "\r\n"
            "0\r\n\r\n"
        );
    }

    SECTION("start streaming file, then embed field") {
        //auto req = Request::Builder().uri("http://ptsv2.com/t/27ibp-1600433748/post").form_stream().build();
        auto data = req->to_string();
        data = merge(data, req->form_file("key", "cv.pdf", "application/pdf"));
        data = merge(data, req->form_data("[0123456789]"));
        data = merge(data, req->form_field("key2", "[pdf]"));
        data = merge(data, req->form_finish());
        //std::cout << "zzz:\n" << data << "zzz\n";
        CHECK(canonize(data).first ==
            "POST / HTTP/1.1\r\n"
            "Content-Type: multipart/form-data; boundary=-----------------------XXXXXXXXXXXXXXXXX\r\n"

clib/tests/compile/form.cc  view on Meta::CPAN

            "Content-Disposition: form-data; name=\"key2\"\r\n"
            "\r\n"
            "[pdf]\r\n\r\n"
            "30\r\n\r\n"
            "-------------------------XXXXXXXXXXXXXXXXX--\r\n"
            "\r\n"
            "0\r\n\r\n"
        );
    }

    SECTION("start streaming file, then embed field, gzip compression is ignored") {
        //auto req = Request::Builder().uri("/").form_stream()/* .compress(Compression::Type::GZIP) */ .build();
        auto req = Request::Builder().uri("/").form_stream().compress(Compression::Type::GZIP).build();
        auto data = req->to_string();
        data = merge(data, req->form_file("key", "cv.pdf", "application/pdf"));
        data = merge(data, req->form_data("[0123456789]"));
        data = merge(data, req->form_field("key2", "[pdf]"));
        data = merge(data, req->form_finish());
        CHECK(canonize(data).first ==
            "POST / HTTP/1.1\r\n"
            "Content-Type: multipart/form-data; boundary=-----------------------XXXXXXXXXXXXXXXXX\r\n"

lib/Protocol/HTTP.pod  view on Meta::CPAN

    $jar->collect($response);   # after response is received


=head1 DESCRIPTION

The C<Protocol::HTTP> is a port of C<panda::protocol::http> library, an RFC-compiant very fast HTTP protocol implementation, written in C++
with XS-adapters in perl. The module has dual Perl/C++ interface (see L<XS::Manifesto>) so futher XS-bindings can be written on the top of
C<Protocol::HTTP>.

The following features are supported in the C<Protocol::HTTP>: cookies, transparent (un)compression with C<Gzip> or C<Brotli>,
transparent chunked transfer encoding for body streaming, respecting request's preferences when making response.

The module is a protocol implementation, by itself it does not perform any I/O activity.

Currenly supported HTTP versions are 1.0 and 1.1



=head1 REFERENCE

L<Protocol::HTTP::Request>



( run in 0.276 second using v1.01-cache-2.11-cpan-4d50c553e7e )