Commit 2c6b39e2 authored by jan.koester's avatar jan.koester
Browse files

test

parent e3627280
Loading
Loading
Loading
Loading
+8 −11
Changes for CMakeLists.txt: 8 added lines, 11 removed lines.
Original line number Diff line number Diff line
@@ -12,10 +12,18 @@ endif()
# --- Mandated libraries (see docs/ARCHITECTURE.md) ---
# libhttppp's own package config does not chain find_dependency() for its transitive deps,
# so netplus/cmdplus must be resolved before it (see libhttppp/CMakeLists.txt for precedent).
# Same reason ZLIB/brotli are resolved here too: libhttppp's own gzip/brotli support
# (src/httpcompression.cpp, used by HttpProxyServer for response compression) links them
# PUBLICly, so httppp::httppp's exported target requires these imported targets to already
# exist in the consumer's scope even though proxyplus itself no longer calls zlib/brotli
# directly.
find_package(libnetplus REQUIRED)
find_package(libcmdplus REQUIRED)
find_package(libuuidp REQUIRED)
find_package(libconfplus REQUIRED)
find_package(ZLIB REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(BROTLI REQUIRED IMPORTED_TARGET libbrotlienc libbrotlidec)
find_package(libhttppp REQUIRED)
find_package(libsmtppp REQUIRED)
find_package(paritypp REQUIRED)
@@ -24,14 +32,6 @@ find_package(json-c REQUIRED)

find_package(Threads REQUIRED)

# --- Response compression (gzip/brotli, src/http/HttpCompression.cpp) ---
# No CMake config package for either upstream, and no bundled FindBrotli module -- zlib gets
# CMake's own FindZLIB, brotli goes through pkg-config (libbrotlienc/libbrotlidec, Debian
# package libbrotli-dev).
find_package(ZLIB REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(BROTLI REQUIRED IMPORTED_TARGET libbrotlienc libbrotlidec)

# --- DKIM / DMARC (see docs/ARCHITECTURE.md §8) ---
# Own RFC 6376 / RFC 7489 implementation on top of libnetplus's RSA/SHA256/
# base64 primitives and a small hand-rolled DNS TXT resolver -- no dependency
@@ -44,7 +44,6 @@ set(PROXYPLUS_CORE_SOURCES
    src/cluster/ClusterManager.cpp
    src/plugin/PluginManager.cpp
    src/http/HttpProxyServer.cpp
    src/http/HttpCompression.cpp
    src/smtp/SmtpProxyServer.cpp
    src/api/JsonApiServer.cpp
    src/security/DnsTxtResolver.cpp
@@ -70,8 +69,6 @@ target_link_libraries(proxyplus_core PUBLIC
    uuidp::uuidp
    json-c::json-c
    Threads::Threads
    ZLIB::ZLIB
    PkgConfig::BROTLI
    ${CMAKE_DL_LIBS}
)

+1 −1
Changes for admin/static/index.html: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@
    <input id="route-upstream" placeholder="Upstream (direkt), genau eine URL, z.B. http://10.0.0.5:8080">
    <input id="route-upstream-pool" placeholder="oder Pool-ID fuer mehrere Ziele (statt Upstream)">
    <input id="route-permission" placeholder="Berechtigung, z.B. http.proxy.use">
    <label><input type="checkbox" id="route-tls-verify"> Upstream-TLS-Zertifikat pruefen</label>
    <label><input type="checkbox" id="route-tls-verify" checked> Upstream-TLS-Zertifikat pruefen</label>
    <input id="route-tls-pins" placeholder="Gepinnte SHA-256-Fingerprints, kommagetrennt (fuer selbstsignierte Upstreams)">
    <button onclick="ProxyPlusAdmin.createRoute()">Anlegen</button>
    <div id="route-error" class="error"></div>
+1 −1
Changes for src/api/JsonApiServer.cpp: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -227,7 +227,7 @@ void JsonApiController::registerRoutes() {
             rec.upstream = getStringField(body, "upstream");
             rec.upstreamPool = getStringField(body, "upstream_pool");
             rec.requiredPermission = getStringField(body, "required_permission");
             rec.tlsVerifyUpstream = getBoolField(body, "tls_verify_upstream");
             rec.tlsVerifyUpstream = getBoolField(body, "tls_verify_upstream", /*def=*/true);
             json_object *fpArr = nullptr;
             if (json_object_object_get_ex(body, "tls_pinned_fingerprints_sha256", &fpArr) && fpArr) {
                 int n = json_object_array_length(fpArr);

src/http/HttpCompression.cpp

deleted100644 → 0
+0 −214
Changes for src/http/HttpCompression.cpp: 0 added lines, 214 removed lines.
Original line number Diff line number Diff line
#include "HttpCompression.h"

#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <set>

#include <zlib.h>

#include <brotli/decode.h>
#include <brotli/encode.h>

namespace proxyplus::httpproxy::compression {

namespace {

std::string toLower(const std::string &s) {
    std::string out(s);
    std::transform(out.begin(), out.end(), out.begin(),
                    [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
    return out;
}

std::string trim(const std::string &s) {
    size_t start = 0, end = s.size();
    while (start < end && std::isspace(static_cast<unsigned char>(s[start]))) ++start;
    while (end > start && std::isspace(static_cast<unsigned char>(s[end - 1]))) --end;
    return s.substr(start, end - start);
}

struct Preference {
    double q = 1.0;
    bool present = false;
};

} // namespace

Encoding fromToken(const std::string &contentEncoding) {
    std::string tok = trim(toLower(contentEncoding));
    if (tok.empty() || tok == "identity") return Encoding::None;
    if (tok == "gzip" || tok == "x-gzip") return Encoding::Gzip;
    if (tok == "br") return Encoding::Brotli;
    return Encoding::Other;
}

const char *toToken(Encoding enc) {
    switch (enc) {
        case Encoding::Gzip: return "gzip";
        case Encoding::Brotli: return "br";
        default: return "";
    }
}

Encoding negotiate(const std::string &acceptEncoding) {
    if (acceptEncoding.empty()) return Encoding::None;

    Preference br, gzip, star;
    size_t pos = 0;
    while (pos <= acceptEncoding.size()) {
        size_t comma = acceptEncoding.find(',', pos);
        std::string part = acceptEncoding.substr(pos, comma == std::string::npos
                                                            ? std::string::npos
                                                            : comma - pos);
        pos = (comma == std::string::npos) ? acceptEncoding.size() + 1 : comma + 1;
        if (part.empty()) continue;

        size_t semi = part.find(';');
        std::string name = toLower(trim(part.substr(0, semi)));
        double q = 1.0;
        if (semi != std::string::npos) {
            std::string qpart = trim(part.substr(semi + 1));
            size_t eq = qpart.find('=');
            if (eq != std::string::npos) {
                try {
                    q = std::stod(trim(qpart.substr(eq + 1)));
                } catch (const std::exception &) {
                    q = 1.0;
                }
            }
        }

        if (name == "br") {
            br = {q, true};
        } else if (name == "gzip" || name == "x-gzip") {
            gzip = {q, true};
        } else if (name == "*") {
            star = {q, true};
        }
        if (comma == std::string::npos) break;
    }

    // A token not explicitly listed falls back to "*"'s preference, if present, per RFC
    // 7231 §5.3.4.
    double brQ = br.present ? br.q : (star.present ? star.q : 0.0);
    double gzipQ = gzip.present ? gzip.q : (star.present ? star.q : 0.0);
    bool brOk = brQ > 0.0;
    bool gzipOk = gzipQ > 0.0;

    if (brOk && gzipOk) return brQ >= gzipQ ? Encoding::Brotli : Encoding::Gzip;
    if (brOk) return Encoding::Brotli;
    if (gzipOk) return Encoding::Gzip;
    return Encoding::None;
}

bool isCompressible(const std::string &contentType) {
    if (contentType.empty()) return false;
    if (contentType.rfind("text/", 0) == 0) return true;

    static const std::set<std::string> kAlso = {
        "application/json",
        "application/javascript",
        "application/x-javascript",
        "application/xml",
        "application/xhtml+xml",
        "application/rss+xml",
        "application/atom+xml",
        "image/svg+xml",
    };
    return kAlso.count(contentType) != 0;
}

bool gzipCompress(const std::vector<char> &in, std::vector<char> &out, int level) {
    z_stream strm{};
    // windowBits 15+16 asks zlib for a gzip (not raw-deflate/zlib) wrapper.
    if (deflateInit2(&strm, level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
        return false;
    }

    out.resize(deflateBound(&strm, static_cast<uLong>(in.size())));
    strm.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(in.data()));
    strm.avail_in = static_cast<uInt>(in.size());
    strm.next_out = reinterpret_cast<Bytef *>(out.data());
    strm.avail_out = static_cast<uInt>(out.size());

    bool ok = deflate(&strm, Z_FINISH) == Z_STREAM_END;
    out.resize(ok ? strm.total_out : 0);
    deflateEnd(&strm);
    return ok;
}

bool gzipDecompress(const std::vector<char> &in, std::vector<char> &out) {
    z_stream strm{};
    // windowBits 15+32 auto-detects a gzip or zlib header.
    if (inflateInit2(&strm, 15 + 32) != Z_OK) return false;

    strm.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(in.data()));
    strm.avail_in = static_cast<uInt>(in.size());

    out.clear();
    std::array<char, 64 * 1024> chunk;
    int ret = Z_OK;
    do {
        strm.next_out = reinterpret_cast<Bytef *>(chunk.data());
        strm.avail_out = static_cast<uInt>(chunk.size());
        ret = inflate(&strm, Z_NO_FLUSH);
        if (ret != Z_OK && ret != Z_STREAM_END && ret != Z_BUF_ERROR) {
            inflateEnd(&strm);
            return false;
        }
        out.insert(out.end(), chunk.data(), chunk.data() + (chunk.size() - strm.avail_out));
    } while (ret != Z_STREAM_END && strm.avail_in > 0);

    inflateEnd(&strm);
    return ret == Z_STREAM_END;
}

bool brotliCompress(const std::vector<char> &in, std::vector<char> &out, int quality) {
    size_t bound = BrotliEncoderMaxCompressedSize(in.size());
    if (bound == 0) bound = 64; // MaxCompressedSize documents 0 as "input too large to bound";
                                // never true here, but keep resize() from truncating to empty.
    out.resize(bound);
    size_t encodedSize = out.size();

    BROTLI_BOOL ok = BrotliEncoderCompress(
        quality, BROTLI_DEFAULT_WINDOW, BROTLI_MODE_GENERIC, in.size(),
        reinterpret_cast<const uint8_t *>(in.data()), &encodedSize,
        reinterpret_cast<uint8_t *>(out.data()));
    if (!ok) {
        out.clear();
        return false;
    }
    out.resize(encodedSize);
    return true;
}

bool brotliDecompress(const std::vector<char> &in, std::vector<char> &out) {
    BrotliDecoderState *state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr);
    if (!state) return false;

    out.clear();
    const uint8_t *nextIn = reinterpret_cast<const uint8_t *>(in.data());
    size_t availIn = in.size();
    std::array<uint8_t, 64 * 1024> chunk;
    BrotliDecoderResult result;
    do {
        uint8_t *nextOut = chunk.data();
        size_t availOut = chunk.size();
        result = BrotliDecoderDecompressStream(state, &availIn, &nextIn, &availOut, &nextOut,
                                                nullptr);
        out.insert(out.end(), reinterpret_cast<char *>(chunk.data()),
                   reinterpret_cast<char *>(chunk.data() + (chunk.size() - availOut)));
        if (result == BROTLI_DECODER_RESULT_ERROR) {
            BrotliDecoderDestroyInstance(state);
            return false;
        }
    } while (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT);

    bool ok = result == BROTLI_DECODER_RESULT_SUCCESS;
    BrotliDecoderDestroyInstance(state);
    return ok;
}

} // namespace proxyplus::httpproxy::compression

src/http/HttpCompression.h

deleted100644 → 0
+0 −38
Changes for src/http/HttpCompression.h: 0 added lines, 38 removed lines.
Original line number Diff line number Diff line
#pragma once

#include <string>
#include <vector>

namespace proxyplus::httpproxy::compression {

// None: no encoding (or "identity") -- send as-is.
// Gzip/Brotli: an encoding this proxy can itself produce and consume.
// Other: some encoding present that isn't gzip or brotli (e.g. "deflate", "zstd", or a
// comma-joined list) -- opaque to this module, callers must leave the body untouched.
enum class Encoding { None, Gzip, Brotli, Other };

// Maps a single Content-Encoding token (case-insensitive) to Encoding.
Encoding fromToken(const std::string &contentEncoding);

// Content-Encoding token this proxy would send for enc ("gzip"/"br"). Only valid for
// Gzip/Brotli.
const char *toToken(Encoding enc);

// Picks the best encoding this proxy can produce for a client's Accept-Encoding header,
// honoring q-values (q=0 excludes) and "*" per RFC 7231 §5.3.4. Prefers Brotli over gzip
// when both are acceptable with equal preference (better ratio at comparable CPU cost).
// Returns Encoding::None if the header is empty/absent or nothing acceptable remains.
Encoding negotiate(const std::string &acceptEncoding);

// True if contentType (as returned by HttpResponse::getContentType() -- already lowercased,
// parameters stripped) is worth spending CPU to compress. Binary/already-compressed formats
// (images, video, archives) are excluded.
bool isCompressible(const std::string &contentType);

bool gzipCompress(const std::vector<char> &in, std::vector<char> &out, int level = 6);
bool gzipDecompress(const std::vector<char> &in, std::vector<char> &out);

bool brotliCompress(const std::vector<char> &in, std::vector<char> &out, int quality = 5);
bool brotliDecompress(const std::vector<char> &in, std::vector<char> &out);

} // namespace proxyplus::httpproxy::compression
Loading