This commit is contained in:
seb
2026-07-13 07:34:17 +02:00
parent d471f2411d
commit b7b76c9d39
46 changed files with 39025 additions and 7 deletions

4
jtlsrv-cpp/.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
build/
odbc-driver/
logs/
*.log

22
jtlsrv-cpp/.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# Build
build/
*.o
*.a
jtlsrv
jtlsrv-debug
# Compiled commands
compile_commands.json
# ODBC driver (downloaded, not source)
odbc-driver/
# Secrets / config
.env
.env.docker
# Certs (generated)
certs/
# Docker
docker-compose.override.yml

75
jtlsrv-cpp/CMakeLists.txt Normal file
View File

@@ -0,0 +1,75 @@
cmake_minimum_required(VERSION 3.16)
project(jtlsrv-cpp LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- Dependencies -----------------------------------------------------------
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBUV REQUIRED libuv)
pkg_check_modules(OPENSSL REQUIRED openssl)
pkg_check_modules(VIPS REQUIRED vips)
find_library(ODBC_LIBRARY odbc)
find_path(ODBC_INCLUDE_DIR sql.h)
# llhttp (vendored)
add_library(llhttp STATIC
vendor/llhttp.c
vendor/api.c
vendor/http.c
)
target_include_directories(llhttp PUBLIC vendor)
# nlohmann/json (vendored, header-only)
add_library(json INTERFACE)
target_include_directories(json INTERFACE vendor)
# --- jtlsrv -----------------------------------------------------------------
add_executable(jtlsrv
src/main.cpp
src/log.cpp
src/http.cpp
src/tls_server.cpp
src/pairing.cpp
src/router.cpp
src/endpoints/client.cpp
src/endpoints/init.cpp
src/endpoints/category.cpp
src/endpoints/product.cpp
src/endpoints/productcomposite.cpp
src/endpoints/deleted_entity.cpp
src/endpoints/customergroup.cpp
src/endpoints/order.cpp
src/endpoints/pimage.cpp
src/endpoints/cimage.cpp
src/db/pool.cpp
src/request_log.cpp
src/order_log.cpp
)
target_include_directories(jtlsrv PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${LIBUV_INCLUDE_DIRS}
${OPENSSL_INCLUDE_DIRS}
${ODBC_INCLUDE_DIR}
${VIPS_INCLUDE_DIRS}
)
target_link_libraries(jtlsrv PRIVATE
llhttp
json
${LIBUV_LIBRARIES}
${OPENSSL_LIBRARIES}
${ODBC_LIBRARY}
${VIPS_LIBRARIES}
pthread
)
target_link_directories(jtlsrv PRIVATE
${LIBUV_LIBRARY_DIRS}
${OPENSSL_LIBRARY_DIRS}
${VIPS_LIBRARY_DIRS}
)

39
jtlsrv-cpp/Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
FROM ubuntu:24.04
# Install build deps
RUN apt-get update && apt-get install -y \
build-essential cmake pkg-config \
libuv1-dev libssl-dev \
libodbc2 unixodbc-dev \
libvips-dev \
curl gnupg2 gdbserver \
&& rm -rf /var/lib/apt/lists/*
# Install Microsoft ODBC Driver 18
RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/24.04/prod noble main" > /etc/apt/sources.list.d/mssql-release.list \
&& apt-get update \
&& ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 \
&& rm -rf /var/lib/apt/lists/*
# Copy source
WORKDIR /build
COPY CMakeLists.txt .
COPY vendor/ vendor/
COPY src/ src/
# Build (default Debug for gdb, override with --build-arg CMAKE_BUILD_TYPE=Release)
ARG CMAKE_BUILD_TYPE=Debug
RUN mkdir -p build && cd build \
&& cmake .. -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \
&& cmake --build . -j$(nproc) \
&& cp jtlsrv /usr/local/bin/
# Copy certs and env
WORKDIR /app
COPY certs/ certs/
COPY .env.docker .env
EXPOSE 4443
CMD ["jtlsrv"]

64
jtlsrv-cpp/src/config.hpp Normal file
View File

@@ -0,0 +1,64 @@
#pragma once
#include <cstdlib>
#include <fstream>
#include <string>
#include <unordered_map>
namespace config {
// Loads a .env file into the process environment (and returns the map).
// Lines starting with '#' and blank lines are skipped.
// No shell expansion, no quoting -- matches Node's dotenv behavior.
inline std::unordered_map<std::string, std::string> load(const std::string& path = ".env") {
std::unordered_map<std::string, std::string> vars;
std::ifstream file(path);
if (!file.is_open()) return vars;
std::string line;
while (std::getline(file, line)) {
// Trim leading whitespace
size_t start = line.find_first_not_of(" \t\r\n");
if (start == std::string::npos) continue;
line = line.substr(start);
if (line.empty() || line[0] == '#') continue;
size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
// Trim trailing whitespace from value
size_t end = val.find_last_not_of(" \t\r\n");
if (end != std::string::npos) val = val.substr(0, end + 1);
// Strip surrounding quotes
if (val.size() >= 2 &&
((val.front() == '"' && val.back() == '"') ||
(val.front() == '\'' && val.back() == '\''))) {
val = val.substr(1, val.size() - 2);
}
setenv(key.c_str(), val.c_str(), 0); // don't overwrite existing
vars[key] = val;
}
return vars;
}
// Read a string env var with a fallback default.
inline std::string get(const char* key, const char* def = "") {
const char* val = std::getenv(key);
return val ? val : def;
}
// Read an integer env var with a fallback default.
inline int get_int(const char* key, int def = 0) {
const char* val = std::getenv(key);
if (!val) return def;
try { return std::stoi(val); }
catch (...) { return def; }
}
} // namespace config

276
jtlsrv-cpp/src/db/pool.cpp Normal file
View File

@@ -0,0 +1,276 @@
#include "pool.hpp"
#include "../config.hpp"
#include "../log.hpp"
#include <cstring>
#include <cstdlib>
static OdbcPool g_pool;
OdbcPool& get_pool() { return g_pool; }
OdbcPool::~OdbcPool() { disconnect(); }
int OdbcPool::connect() {
SQLRETURN rc;
rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv_);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) return -1;
SQLSetEnvAttr(henv_, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
std::string server = config::get("MSSQL_SERVER", "localhost");
int port = config::get_int("MSSQL_PORT", 1433);
std::string database = config::get("MSSQL_DATABASE", "eazybusiness");
std::string user = config::get("MSSQL_USER");
std::string password = config::get("MSSQL_PASSWORD");
bool encrypt = (config::get("MSSQL_ENCRYPT", "true") != "false");
bool trust_cert = (config::get("MSSQL_TRUST_SERVER_CERTIFICATE", "true") != "false");
if (user.empty()) {
logc::warn("MSSQL_USER not set, skipping DB connection");
return -1;
}
// Connection string
std::string conn_str =
"DRIVER={ODBC Driver 18 for SQL Server};"
"SERVER=" + server + "," + std::to_string(port) + ";"
"DATABASE=" + database + ";"
"UID=" + user + ";"
"PWD=" + password + ";"
"Encrypt=" + (encrypt ? std::string("yes") : std::string("Optional")) + ";"
"TrustServerCertificate=" + (trust_cert ? std::string("yes") : std::string("no")) + ";";
logc::info("ODBC connecting to %s:%d/%s as %s", server.c_str(), port, database.c_str(), user.c_str());
// Create pool of 4 connections
const int POOL_SIZE = 4;
conns_.resize(POOL_SIZE);
int connected = 0;
for (int i = 0; i < POOL_SIZE; i++) {
rc = SQLAllocHandle(SQL_HANDLE_DBC, henv_, &conns_[i].hdbc);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) continue;
rc = SQLDriverConnect(conns_[i].hdbc, nullptr,
(SQLCHAR*)conn_str.c_str(), SQL_NTS,
nullptr, 0, nullptr, SQL_DRIVER_COMPLETE);
if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
SQLAllocHandle(SQL_HANDLE_STMT, conns_[i].hdbc, &conns_[i].hstmt);
connected++;
} else {
SQLCHAR state[6], msg[SQL_MAX_MESSAGE_LENGTH];
SQLINTEGER native;
SQLSMALLINT msg_len;
SQLGetDiagRec(SQL_HANDLE_DBC, conns_[i].hdbc, 1,
state, &native, msg, sizeof(msg), &msg_len);
logc::warn("ODBC connect[%d] failed: %s - %s", i, state, msg);
}
}
if (connected == 0) {
logc::error("no ODBC connections established");
return -1;
}
logc::success("ODBC pool: %d connections to %s/%s", connected, server.c_str(), database.c_str());
return 0;
}
void OdbcPool::disconnect() {
for (auto& c : conns_) {
if (c.hstmt != SQL_NULL_HSTMT) SQLFreeHandle(SQL_HANDLE_STMT, c.hstmt);
if (c.hdbc != SQL_NULL_HDBC) { SQLDisconnect(c.hdbc); SQLFreeHandle(SQL_HANDLE_DBC, c.hdbc); }
}
conns_.clear();
if (henv_ != SQL_NULL_HENV) { SQLFreeHandle(SQL_HANDLE_ENV, henv_); henv_ = SQL_NULL_HENV; }
}
OdbcPool::Connection* OdbcPool::checkout() {
std::lock_guard<std::mutex> lock(mutex_);
for (auto& c : conns_) {
if (!c.in_use) { c.in_use = true; return &c; }
}
return nullptr;
}
void OdbcPool::release(Connection* c) {
std::lock_guard<std::mutex> lock(mutex_);
c->in_use = false;
}
bool OdbcPool::execute(const std::string& sql, const std::vector<Param>& params, ResultSet& out) {
Connection* c = checkout();
if (!c) return false;
SQLRETURN rc;
// Prepare statement
rc = SQLPrepare(c->hstmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
release(c);
return false;
}
// Wide buffer storage for NVarChar parameters (must outlive SQLExecute)
std::vector<std::vector<SQLWCHAR>> wbufs;
// Bind parameters
for (size_t i = 0; i < params.size(); i++) {
const auto& p = params[i];
SQLUSMALLINT param_num = static_cast<SQLUSMALLINT>(i + 1);
SQLPOINTER val_ptr = nullptr;
SQLLEN buf_len = 0;
SQLLEN indicator = 0;
SQLSMALLINT c_type = SQL_C_CHAR;
SQLSMALLINT sql_type = SQL_VARCHAR;
switch (p.type) {
case ParamType::Int:
c_type = SQL_C_SLONG;
sql_type = SQL_INTEGER;
val_ptr = (SQLPOINTER)&p.int_val;
buf_len = sizeof(SQLINTEGER);
break;
case ParamType::BigInt:
c_type = SQL_C_SBIGINT;
sql_type = SQL_BIGINT;
val_ptr = (SQLPOINTER)&p.int_val;
buf_len = sizeof(SQLBIGINT);
break;
case ParamType::Float:
case ParamType::Double:
c_type = SQL_C_DOUBLE;
sql_type = SQL_DOUBLE;
val_ptr = (SQLPOINTER)&p.dbl_val;
buf_len = sizeof(SQLDOUBLE);
break;
case ParamType::NVarChar: {
// Convert narrow string to SQLWCHAR (unsigned short = UTF-16)
// SQLWCHAR is unsigned short (2 bytes) on this platform
std::vector<SQLWCHAR> wbuf(p.str_val.size());
for (size_t k = 0; k < p.str_val.size(); k++)
wbuf[k] = static_cast<SQLWCHAR>((unsigned char)p.str_val[k]);
wbufs.push_back(std::move(wbuf));
auto& wb = wbufs.back();
c_type = SQL_C_WCHAR;
sql_type = SQL_WVARCHAR;
val_ptr = (SQLPOINTER)wb.data();
buf_len = (SQLLEN)(wb.size() * sizeof(SQLWCHAR));
indicator = wb.empty() ? SQL_NULL_DATA : (SQLLEN)(wb.size() * sizeof(SQLWCHAR));
break;
}
case ParamType::Bit:
c_type = SQL_C_BIT;
sql_type = SQL_BIT;
val_ptr = (SQLPOINTER)&p.int_val;
buf_len = 1;
break;
}
rc = SQLBindParameter(c->hstmt, param_num, SQL_PARAM_INPUT,
c_type, sql_type, p.str_val.size() + 1, 0, val_ptr, buf_len, &indicator);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
release(c);
return false;
}
}
// Execute
rc = SQLExecute(c->hstmt);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA) {
release(c);
return false;
}
// Fetch results if it's a SELECT
out.clear();
SQLSMALLINT col_count = 0;
SQLNumResultCols(c->hstmt, &col_count);
if (col_count > 0) {
// Discover column types
std::vector<SQLSMALLINT> col_types(col_count);
bool has_blobs = false;
for (SQLSMALLINT col = 0; col < col_count; col++) {
SQLSMALLINT data_type;
SQLDescribeCol(c->hstmt, col + 1, nullptr, 0, nullptr, &data_type, nullptr, nullptr, nullptr);
col_types[col] = data_type;
if (data_type == SQL_BINARY || data_type == SQL_VARBINARY || data_type == SQL_LONGVARBINARY)
has_blobs = true;
}
while (true) {
rc = SQLFetch(c->hstmt);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break;
Row row;
for (SQLSMALLINT col = 0; col < col_count; col++) {
Cell cell;
SQLLEN ind;
SQLSMALLINT sql_type = col_types[col];
// Binary columns: read as binary
if (sql_type == SQL_BINARY || sql_type == SQL_VARBINARY ||
sql_type == SQL_LONGVARBINARY) {
std::vector<uint8_t> blob_data;
unsigned char chunk[8192];
int chunk_count = 0;
while (true) {
rc = SQLGetData(c->hstmt, col + 1, SQL_C_BINARY, chunk, sizeof(chunk), &ind);
if (ind == SQL_NULL_DATA) { break; }
if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
SQLLEN copy_len = std::min(ind, (SQLLEN)sizeof(chunk));
if (has_blobs && chunk_count == 0)
logc::info(" blob col=%d rc=%d ind=%d copy=%d", col, rc, (int)ind, (int)copy_len);
blob_data.insert(blob_data.end(), chunk, chunk + copy_len);
chunk_count++;
}
if (rc == SQL_SUCCESS) break;
if (rc != SQL_SUCCESS_WITH_INFO) break;
}
if (chunk_count > 0) {
cell.type = CellType::Blob;
cell.blob = std::move(blob_data);
if (has_blobs)
logc::info(" blob col=%d total=%zu chunks=%d", col, cell.blob.size(), chunk_count);
}
} else {
// String/numeric columns: read as char
char buf[4096];
rc = SQLGetData(c->hstmt, col + 1, SQL_C_CHAR, buf, sizeof(buf) - 1, &ind);
if (ind == SQL_NULL_DATA) {
cell.type = CellType::Null;
} else if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
cell.type = CellType::String;
cell.str = std::string(buf, std::min((SQLLEN)(sizeof(buf)-1), ind));
}
}
row.push_back(std::move(cell));
}
out.push_back(std::move(row));
}
}
// Reset statement for reuse
SQLFreeStmt(c->hstmt, SQL_UNBIND);
SQLFreeStmt(c->hstmt, SQL_CLOSE);
release(c);
return true;
}
bool OdbcPool::execute(const std::string& sql, ResultSet& out) {
return execute(sql, {}, out);
}
int64_t OdbcPool::execute_scalar(const std::string& sql, const std::vector<Param>& params, int64_t fallback) {
ResultSet rs;
if (!execute(sql, params, rs) || rs.empty() || rs[0].empty()) return fallback;
const auto& cell = rs[0][0];
if (cell.type == CellType::Int64) return cell.i64;
if (cell.type == CellType::String) {
try { return std::stoll(cell.str); } catch (...) {}
}
return fallback;
}

View File

@@ -0,0 +1,55 @@
#pragma once
#include <string>
#include <vector>
#include <mutex>
#include <cstdint>
#include <sql.h>
#include <sqlext.h>
#include <uv.h>
enum class CellType { Null, Int64, Double, String, Blob };
struct Cell {
CellType type = CellType::Null;
int64_t i64 = 0;
double dbl = 0.0;
std::string str;
std::vector<uint8_t> blob;
};
using Row = std::vector<Cell>;
using ResultSet = std::vector<Row>;
enum class ParamType { Int, BigInt, Float, Double, NVarChar, Bit };
struct Param {
ParamType type;
std::string str_val;
int64_t int_val = 0;
double dbl_val = 0.0;
};
class OdbcPool {
public:
~OdbcPool();
int connect();
void disconnect();
bool execute(const std::string& sql, const std::vector<Param>& params, ResultSet& out);
bool execute(const std::string& sql, ResultSet& out);
int64_t execute_scalar(const std::string& sql, const std::vector<Param>& params = {}, int64_t fallback = 0);
private:
struct Connection {
SQLHDBC hdbc = SQL_NULL_HDBC;
SQLHSTMT hstmt = SQL_NULL_HSTMT;
bool in_use = false;
};
SQLHENV henv_ = SQL_NULL_HENV;
std::vector<Connection> conns_;
std::mutex mutex_;
Connection* checkout();
void release(Connection* c);
};
OdbcPool& get_pool();

View File

@@ -0,0 +1,11 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../queries/category_list.hpp"
void handle_category(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedCategory", "0"));
int limit = std::stoi(req.get_query_param("limit", "20"));
auto categories = get_category_list(cursor, limit);
resp.send_json(200, categories);
}

View File

@@ -0,0 +1,22 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../log.hpp"
#include "../queries/image.hpp"
void handle_cimage(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
std::string path = req.get_query_param("path");
if (path.empty()) {
resp.send_json(400, {{"Message", "Missing required query parameter 'path'."}});
return;
}
std::string size = req.get_query_param("size", "200");
ImageResult image = get_image_by_hash(path, size);
if (image.buffer.empty()) {
resp.send_json(404, {{"Message", "No image was found for path '" + path + "'."}});
return;
}
resp.send_binary(200, image.buffer, image.content_type);
}

View File

@@ -0,0 +1,61 @@
// GET /v1/client — pairing handshake (no DB)
// Port of src/endpoints/client.js
#include "../http.hpp"
#include "../pairing.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
static json build_client_step1(const json& config) {
return {
{"authCode", nullptr},
{"authToken", config["authToken"]},
{"certificateFingerprint", config["certificateFingerprint"]},
{"certificateSerialNumber", config["certificateSerialNumber"]},
{"mandantId", config["mandantId"]},
{"mandantName", nullptr},
{"mandantDatabase", nullptr},
{"serverFingerprint", config["serverFingerprint"]},
{"name", nullptr},
{"serverTimestamp", server_timestamp()},
};
}
static json build_client_step2(const std::string& auth_code, const json& config) {
return {
{"authCode", auth_code},
{"authToken", config["authToken"]},
{"certificateFingerprint", config["certificateFingerprint"]},
{"certificateSerialNumber", config["certificateSerialNumber"]},
{"mandantId", config["mandantId"]},
{"mandantName", config["mandantName"]},
{"mandantDatabase", config["mandantDatabase"]},
{"serverFingerprint", nullptr},
{"name", nullptr},
{"serverTimestamp", server_timestamp()},
};
}
void handle_client(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
std::string auth_code = req.get_query_param("authCode");
std::string name = req.get_query_param("name", "JTL-POS");
if (auth_code.size() <= 4 && !auth_code.empty()) {
return resp.send_json(200, build_client_step1(ctx.config));
}
if (auth_code.size() == 6) {
if (ctx.pairing_store->has_pairing_code(auth_code)) {
ctx.pairing_store->revoke_pairing_code(auth_code);
ctx.pairing_store->register_device(ctx.config["authToken"].get<std::string>(), name);
return resp.send_json(200, build_client_step2(auth_code, ctx.config));
}
return resp.send_json(400, {
{"Message", "Der Authentifizierungscode ist falsch."}
});
}
return resp.send_json(400, {
{"Message", "Keinen passenden Authentifizierungscode gefunden."}
});
}

View File

@@ -0,0 +1,10 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../queries/customer_groups.hpp"
void handle_customergroup(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedCustomerGroup", "0"));
auto groups = get_customer_group_list(cursor);
resp.send_json(200, groups);
}

View File

@@ -0,0 +1,11 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../queries/deleted_entity_list.hpp"
void handle_deleted_entity(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedDeletedEntity", "0"));
int limit = std::stoi(req.get_query_param("limit", "600"));
auto deleted = get_deleted_entity_list(cursor, limit);
resp.send_json(200, deleted);
}

View File

@@ -0,0 +1,41 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../queries/counts.hpp"
#include "../queries/customer_groups.hpp"
#include "../queries/shop.hpp"
#include "../config.hpp"
void handle_init(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
int64_t product_cursor = std::stoll(req.get_query_param("lastChangedProduct", "0"));
int64_t category_cursor = std::stoll(req.get_query_param("lastChangedCategory", "0"));
int64_t cg_cursor = std::stoll(req.get_query_param("lastChangedCustomerGroup", "0"));
int64_t composite_cursor = std::stoll(req.get_query_param("lastChangedCompositeProduct", "0"));
int64_t deleted_cursor = std::stoll(req.get_query_param("lastChangedDeletedEntity", "0"));
int root = config::get_int("ROOT_CATEGORY_ID", 1);
int shop = get_active_shop_id();
int64_t product_count = 0, category_count = 0, cg_count = 0, composite_count = 0, deleted_count = 0;
if (get_pool().execute_scalar("SELECT 1") != 0) {
product_count = get_product_count(root, shop, product_cursor);
category_count = get_category_count(root, shop, category_cursor);
cg_count = get_customer_group_count(cg_cursor);
composite_count = get_composite_count(shop, composite_cursor);
deleted_count = get_deleted_count(deleted_cursor);
}
resp.send_json(200, {
{"version", "1.10.12.0"},
{"product_count", std::to_string(product_count)},
{"category_count", std::to_string(category_count)},
{"customer_count", "0"},
{"customerGroup_count", std::to_string(cg_count)},
{"compositeProduct_count", std::to_string(composite_count)},
{"configurationGroup_count", "0"},
{"configurationItem_count", "0"},
{"deletedEntity_count", std::to_string(deleted_count)},
{"max_orderId_count", "0"}
});
}

View File

@@ -0,0 +1,28 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../log.hpp"
void handle_order(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
// TODO: full order creation (Milestone 5/6)
// For now, parse the JSON body and acknowledge
nlohmann::json body;
try {
body = nlohmann::json::parse(req.body);
} catch (...) {
return resp.send_json(500, nlohmann::json::array());
}
nlohmann::json results = nlohmann::json::array();
if (body.contains("orders") && body["orders"].is_array()) {
for (auto& order : body["orders"]) {
std::string ext_id = order.value("externalId", "");
results.push_back({
{"status", "OK"},
{"externalOrderId", ext_id},
{"message", ""}
});
}
}
resp.send_json(200, results);
}

View File

@@ -0,0 +1,22 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../log.hpp"
#include "../queries/image.hpp"
void handle_pimage(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
std::string path = req.get_query_param("path");
if (path.empty()) {
resp.send_json(400, {{"Message", "Missing required query parameter 'path'."}});
return;
}
std::string size = req.get_query_param("size", "200");
ImageResult image = get_image_by_hash(path, size);
if (image.buffer.empty()) {
resp.send_json(404, {{"Message", "No image was found for path '" + path + "'."}});
return;
}
resp.send_binary(200, image.buffer, image.content_type);
}

View File

@@ -0,0 +1,11 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../queries/product_list.hpp"
void handle_product(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedProduct", "0"));
int limit = std::stoi(req.get_query_param("limit", "20"));
auto products = get_product_list(cursor, limit);
resp.send_json(200, products);
}

View File

@@ -0,0 +1,11 @@
#include "../http.hpp"
#include "../tls_server.hpp"
#include "../router.hpp"
#include "../queries/composite_product_list.hpp"
void handle_productcomposite(HttpRequest& req, HttpResponse& resp, RouteContext& ctx) {
int64_t cursor = std::stoll(req.get_query_param("lastChangedCompositeProduct", "0"));
int limit = std::stoi(req.get_query_param("limit", "100"));
auto composites = get_composite_product_list(cursor, limit);
resp.send_json(200, composites);
}

121
jtlsrv-cpp/src/http.cpp Normal file
View File

@@ -0,0 +1,121 @@
#include "http.hpp"
#include "tls_server.hpp"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <ctime>
// ---------------------------------------------------------------------------
// HttpRequest
// ---------------------------------------------------------------------------
std::string HttpRequest::get_query_param(const std::string& key, const std::string& def) const {
std::string needle = key + "=";
size_t pos = query_string.find(needle);
if (pos == std::string::npos) return def;
size_t val_start = pos + needle.size();
size_t val_end = query_string.find('&', val_start);
if (val_end == std::string::npos) val_end = query_string.size();
std::string raw = query_string.substr(val_start, val_end - val_start);
// Simple URL decode
std::string decoded;
decoded.reserve(raw.size());
for (size_t i = 0; i < raw.size(); ++i) {
if (raw[i] == '%' && i + 2 < raw.size()) {
char hex[3] = { raw[i+1], raw[i+2], 0 };
decoded += static_cast<char>(std::strtol(hex, nullptr, 16));
i += 2;
} else if (raw[i] == '+') {
decoded += ' ';
} else {
decoded += raw[i];
}
}
return decoded;
}
// ---------------------------------------------------------------------------
// HttpResponse
// ---------------------------------------------------------------------------
void HttpResponse::send_json(int code, const json& body) {
if (headers_sent) return;
status_code = code;
std::string body_str = body.dump();
body_for_log = body_str;
std::string resp = "HTTP/1.1 " + std::to_string(code) + " " + reason_phrase(code) + "\r\n"
"Content-Type: application/json; charset=utf-8\r\n"
"Content-Length: " + std::to_string(body_str.size()) + "\r\n"
"Connection: keep-alive\r\n"
"\r\n"
+ body_str;
session_write(session, resp);
headers_sent = true;
}
void HttpResponse::send_binary(int code, const std::vector<uint8_t>& data, const std::string& content_type) {
if (headers_sent) return;
status_code = code;
std::string header = "HTTP/1.1 " + std::to_string(code) + " " + reason_phrase(code) + "\r\n"
"Content-Type: " + content_type + "\r\n"
"Content-Length: " + std::to_string(data.size()) + "\r\n"
"Connection: keep-alive\r\n"
"\r\n";
session_write_binary(session, header, data);
headers_sent = true;
}
void HttpResponse::send_empty(int code) {
if (headers_sent) return;
status_code = code;
std::string resp = "HTTP/1.1 " + std::to_string(code) + " " + reason_phrase(code) + "\r\n"
"Content-Length: 0\r\n"
"Connection: keep-alive\r\n"
"\r\n";
session_write(session, resp);
headers_sent = true;
}
void HttpResponse::finish() {
// Connection keep-alive: don't close after each request.
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
std::string normalize_path(const std::string& pathname) {
// /api/v1/... -> /v1/...
const std::string prefix = "/api";
if (pathname.size() >= prefix.size() &&
pathname.compare(0, prefix.size(), prefix) == 0) {
if (pathname.size() > prefix.size() && pathname[prefix.size()] == '/' &&
pathname.size() > prefix.size() + 1 &&
pathname.compare(prefix.size() + 1, 2, "v1") == 0) {
return pathname.substr(prefix.size());
}
}
return pathname;
}
std::string server_timestamp() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf{};
localtime_r(&t, &tm_buf);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
return buf;
}

76
jtlsrv-cpp/src/http.hpp Normal file
View File

@@ -0,0 +1,76 @@
#pragma once
// HTTP request / response types + llhttp glue, and send_json/send_binary.
// Modeled after the Node.js src/http.js.
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#include <uv.h>
#include <nlohmann/json.hpp>
// Forward-declared; defined in tls_server.
struct tls_session;
using json = nlohmann::json;
// ---------------------------------------------------------------------------
// Parsed HTTP request
// ---------------------------------------------------------------------------
struct HttpRequest {
std::string method; // "GET", "POST", ...
std::string path; // "/v1/client?authCode=..."
std::string version; // "1.1"
std::unordered_map<std::string, std::string> headers;
std::vector<uint8_t> body;
// Parsed query string
std::string query_string; // "authCode=xxx&name=yyy"
std::string get_query_param(const std::string& key, const std::string& def = "") const;
};
// ---------------------------------------------------------------------------
// Response writer (wraps a tls_session*)
// ---------------------------------------------------------------------------
struct HttpResponse {
tls_session* session = nullptr;
int status_code = 200;
bool headers_sent = false;
std::string body_for_log; // captured for request logging
void send_json(int code, const json& body);
void send_binary(int code, const std::vector<uint8_t>& data, const std::string& content_type);
void send_empty(int code);
// After response is fully written, close the connection.
void finish();
};
// ---------------------------------------------------------------------------
// Route handler signature
// ---------------------------------------------------------------------------
struct RouteContext {
std::string query_path; // path without query string
std::string full_url; // raw path + query
class PairingStore* pairing_store = nullptr;
json config;
};
using RouteHandler = std::function<void(HttpRequest&, HttpResponse&, RouteContext&)>;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// Normalize /api/v1/... -> /v1/... (matches src/http.js normalizePath)
std::string normalize_path(const std::string& pathname);
// Format "YYYY-MM-DD HH:MM:SS" from current time (matches serverTimestamp)
std::string server_timestamp();

45
jtlsrv-cpp/src/log.cpp Normal file
View File

@@ -0,0 +1,45 @@
#include "log.hpp"
#include <chrono>
#include <cstdio>
#include <cstdarg>
#include <ctime>
namespace logc {
static const char* level_label(Level l) {
switch (l) {
case Level::Info: return "\033[36mINFO\033[0m";
case Level::Success: return "\033[32mOK \033[0m";
case Level::Warn: return "\033[33mWARN\033[0m";
case Level::Error: return "\033[31mERROR\033[0m";
}
return "????";
}
static FILE* log_stream(Level l) {
return (l == Level::Warn || l == Level::Error) ? stderr : stdout;
}
void write(Level level, const char* fmt, ...) {
// ISO-8601 timestamp
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf{};
localtime_r(&t, &tm_buf);
char ts[32];
std::strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", &tm_buf);
FILE* out = log_stream(level);
fprintf(out, "\033[90m%s\033[0m %s ", ts, level_label(level));
va_list args;
va_start(args, fmt);
vfprintf(out, fmt, args);
va_end(args);
fprintf(out, "\n");
fflush(out);
}
} // namespace logc

35
jtlsrv-cpp/src/log.hpp Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <utility>
namespace logc {
enum class Level { Info, Success, Warn, Error };
void write(Level level, const char* fmt, ...);
// Convenience wrappers
template <typename... Args>
void info(const char* fmt, Args&&... args) {
write(Level::Info, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void success(const char* fmt, Args&&... args) {
write(Level::Success, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void warn(const char* fmt, Args&&... args) {
write(Level::Warn, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void error(const char* fmt, Args&&... args) {
write(Level::Error, fmt, std::forward<Args>(args)...);
}
} // namespace logc

173
jtlsrv-cpp/src/main.cpp Normal file
View File

@@ -0,0 +1,173 @@
// jtlsrv-cpp — main.cpp (Milestone 3: Router + pairing + endpoints)
#include <cstdio>
#include <string>
#include <chrono>
#include <uv.h>
#include "config.hpp"
#include "log.hpp"
#include "tls_server.hpp"
#include "http.hpp"
#include "router.hpp"
#include "pairing.hpp"
#include "db/pool.hpp"
#include <vips/vips.h>
#include "queries/shop.hpp"
#include "request_log.hpp"
#include "order_log.hpp"
// ---------------------------------------------------------------------------
// Globals
// ---------------------------------------------------------------------------
static uv_loop_t* loop = nullptr;
static Router router;
static PairingStore pairing_store;
static RequestLog request_log;
static OrderLog order_log;
static json build_config() {
return {
{"authToken", config::get("AUTH_TOKEN", "df40ad2067954646abb0499548a52241")},
{"certificateFingerprint", config::get("CERTIFICATE_FINGERPRINT", "BC2114CF407A42724BEEF417960F76DCBF9DE879")},
{"certificateSerialNumber", config::get("CERTIFICATE_SERIAL_NUMBER", "00BFC8BEACDB981B165210EF111CB9D3")},
{"serverFingerprint", config::get("SERVER_FINGERPRINT", "39-6D-BD-DE-F3-5C-5A-EA-C2-19-CF-EB-A7-A9-58-2F-20-3F-20-F7-3D-E6-CA-8E-AE-FD-28-30-37-A6-45-AE")},
{"mandantId", config::get("MANDANT_ID", "1")},
{"mandantName", config::get("MANDANT_NAME", "eB-Standard")},
{"mandantDatabase", config::get("MANDANT_DATABASE", "eazybusiness")},
};
}
static json server_config;
// ---------------------------------------------------------------------------
// Endpoint declarations (defined in src/endpoints/*.cpp)
// ---------------------------------------------------------------------------
extern void handle_client(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_init(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_category(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_product(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_productcomposite(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_deleted_entity(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_customergroup(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_order(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_pimage(HttpRequest&, HttpResponse&, RouteContext&);
extern void handle_cimage(HttpRequest&, HttpResponse&, RouteContext&);
// ---------------------------------------------------------------------------
// Request handler — dispatches via Router
// ---------------------------------------------------------------------------
static void handle_request(tls_session* sess) {
auto start = std::chrono::steady_clock::now();
router.dispatch(sess, pairing_store, server_config);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
auto& req = sess->current_request;
auto& resp = sess->current_response;
std::string url = req.path;
if (!req.query_string.empty()) url += "?" + req.query_string;
// Build response body for logging
std::string resp_body;
if (resp.status_code == 200) {
// Re-serialize to get size (body already sent, but we can dump from status)
// We need to capture the body before sending - patch: store it in resp
resp_body = resp.body_for_log;
}
size_t resp_size = resp_body.size();
// Console log with response size and truncated body
if (resp_size > 0) {
std::string preview = resp_body.substr(0, std::min(resp_size, (size_t)200));
logc::info("127.0.0.1 %s %s %d %dms [%zu bytes] %s",
req.method.c_str(), url.c_str(), resp.status_code,
(int)elapsed, resp_size, preview.c_str());
} else {
logc::info("127.0.0.1 %s %s %d %dms",
req.method.c_str(), url.c_str(), resp.status_code, (int)elapsed);
}
// Request log
request_log.log("127.0.0.1", req.method, url, resp.status_code, (int)elapsed, resp_body);
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
int main(int argc, char* argv[]) {
config::load(".env");
if (VIPS_INIT(argv[0])) {
vips_error_exit("unable to init libvips");
}
int port = config::get_int("PORT", 4443);
std::string cert_path = "certs/cert.pem";
std::string key_path = "certs/key.pem";
loop = uv_default_loop();
server_config = build_config();
// Register routes
router.add_route("GET", "/v1/client", handle_client);
router.add_route("GET", "/v1/init", handle_init);
router.add_route("GET", "/v1/category", handle_category);
router.add_route("GET", "/v1/product", handle_product);
router.add_route("GET", "/v1/productcomposite", handle_productcomposite);
router.add_route("GET", "/v1/deletedentity", handle_deleted_entity);
router.add_route("GET", "/v1/customergroup", handle_customergroup);
router.add_route("POST", "/v1/order", handle_order);
router.add_route("GET", "/v1/pimage", handle_pimage);
router.add_route("GET", "/v1/cimage", handle_cimage);
// Initialize pairing store
pairing_store.set_pairing_code(config::get("PAIRING_CODE", "307018"), "JTL-POS");
pairing_store.register_device(server_config["authToken"].get<std::string>(), "JTL-POS");
// Connect to MSSQL
if (get_pool().connect() == 0) {
logc::success("MSSQL connected: %s/%s",
config::get("MSSQL_SERVER").c_str(),
config::get("MSSQL_DATABASE").c_str());
if (fetch_active_shop()) {
logc::info("Active shop ID: %d", get_active_shop_id());
}
} else {
logc::warn("MSSQL connection skipped");
logc::warn("POS handshake will still work; sync from database is not available yet.");
}
tls_server_set_handler(handle_request);
// Open log files
request_log.open(config::get("LOG_FILE", "logs/requests.log"));
order_log.open(config::get("ORDER_LOG_FILE", "logs/orders.log"));
int r = tls_server_init(loop, "0.0.0.0", port,
cert_path.c_str(), key_path.c_str());
if (r != 0) {
logc::error("failed to start TLS server");
return 1;
}
tls_server_install_signals(loop);
logc::info("pairing code: %s", config::get("PAIRING_CODE", "307018").c_str());
uv_run(loop, UV_RUN_DEFAULT);
request_log.close();
order_log.close();
get_pool().disconnect();
vips_shutdown();
logc::info("shutdown complete.");
return 0;
}

View File

@@ -0,0 +1,44 @@
#include "order_log.hpp"
#include <chrono>
#include <ctime>
#include <sys/stat.h>
static void ensure_parent_dir(const std::string& path) {
size_t pos = path.rfind('/');
if (pos != std::string::npos) {
mkdir(path.substr(0, pos).c_str(), 0755);
}
}
void OrderLog::open(const std::string& path) {
ensure_parent_dir(path);
fp_ = std::fopen(path.c_str(), "a");
}
void OrderLog::close() {
if (fp_) { std::fclose(fp_); fp_ = nullptr; }
}
int OrderLog::log_order(const std::string& order_json) {
std::lock_guard<std::mutex> lock(mutex_);
sequence_++;
max_external_id_++;
if (fp_) {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf{};
localtime_r(&t, &tm_buf);
char ts[32];
std::strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", &tm_buf);
std::fprintf(fp_, "%s #%d externalId=%d %s\n",
ts, sequence_, max_external_id_, order_json.c_str());
std::fflush(fp_);
}
return max_external_id_;
}
std::string OrderLog::get_max_external_id() const {
return std::to_string(max_external_id_);
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <cstdio>
#include <string>
#include <mutex>
class OrderLog {
public:
void open(const std::string& path);
void close();
int log_order(const std::string& order_json);
std::string get_max_external_id() const;
private:
FILE* fp_ = nullptr;
std::mutex mutex_;
int sequence_ = 0;
int max_external_id_ = 0;
};

View File

@@ -0,0 +1,27 @@
#include "pairing.hpp"
#include <chrono>
static uint64_t now_ms() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
void PairingStore::set_pairing_code(const std::string& code, const std::string& name) {
auth_codes_[code] = {code, name, now_ms()};
}
void PairingStore::revoke_pairing_code(const std::string& code) {
auth_codes_.erase(code);
}
bool PairingStore::has_pairing_code(const std::string& code) const {
return auth_codes_.count(code) > 0;
}
void PairingStore::register_device(const std::string& token, const std::string& name) {
paired_devices_[token] = {name, token, now_ms()};
}
const std::unordered_map<std::string, DeviceEntry>& PairingStore::get_paired_devices() const {
return paired_devices_;
}

View File

@@ -0,0 +1,33 @@
#pragma once
// In-memory pairing store - trivial port of src/pairing.js
#include <string>
#include <unordered_map>
#include <cstdint>
struct PairingEntry {
std::string code;
std::string name;
uint64_t created_at;
};
struct DeviceEntry {
std::string name;
std::string token;
uint64_t created_at;
};
class PairingStore {
public:
void set_pairing_code(const std::string& code, const std::string& name = "JTL-POS");
void revoke_pairing_code(const std::string& code);
bool has_pairing_code(const std::string& code) const;
void register_device(const std::string& token, const std::string& name = "JTL-POS");
const std::unordered_map<std::string, DeviceEntry>& get_paired_devices() const;
private:
std::unordered_map<std::string, PairingEntry> auth_codes_;
std::unordered_map<std::string, DeviceEntry> paired_devices_;
};

View File

@@ -0,0 +1,59 @@
#pragma once
#include "../db/pool.hpp"
#include "nlohmann/json.hpp"
#include "shop.hpp"
#include "../http.hpp"
#include "../config.hpp"
inline nlohmann::json get_category_list(int64_t cursor, int limit) {
int root = config::get_int("ROOT_CATEGORY_ID", 1);
int lang = config::get_int("LANGUAGE_ID", 1);
int shop = get_active_shop_id();
std::string sql =
"WITH CategoryTree AS ("
" SELECT kKategorie FROM dbo.tKategorie WHERE kKategorie = ?"
" UNION ALL"
" SELECT t.kKategorie FROM dbo.tKategorie t"
" INNER JOIN CategoryTree ct ON t.kOberKategorie = ct.kKategorie"
") SELECT TOP (?) k.kKategorie AS id, k.kOberKategorie AS pid, "
"k.nSort AS sort, ks.cName AS name, b.cHash AS imgHash, "
"CONVERT(BIGINT, k.bRowversion) AS lastChanged "
"FROM dbo.tKategorie k "
"INNER JOIN dbo.tKategorieSprache ks ON ks.kKategorie = k.kKategorie AND ks.kSprache = ? "
"LEFT JOIN dbo.tKategoriebildPlattform kbp ON kbp.kKategorie = k.kKategorie "
"LEFT JOIN dbo.tBild b ON b.kBild = kbp.kBild "
"WHERE k.kKategorie IN (SELECT kKategorie FROM CategoryTree WHERE kKategorie <> ?) "
"AND k.cAktiv = 'Y' "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tKategorieShop ks2 "
"WHERE ks2.kKategorie = k.kKategorie AND ks2.kShop = ?)) "
"AND CONVERT(BIGINT, k.bRowversion) > ? "
"ORDER BY lastChanged ASC";
std::vector<Param> ps = {
{ParamType::BigInt,"",root},{ParamType::Int,"",limit},
{ParamType::Int,"",lang},{ParamType::BigInt,"",root},
{ParamType::Int,"",shop},{ParamType::Int,"",shop},
{ParamType::BigInt,"",cursor}
};
ResultSet rs;
get_pool().execute(sql, ps, rs);
auto ts = server_timestamp();
nlohmann::json result = nlohmann::json::array();
for (auto& row : rs) {
std::string pid = (std::stoll(row[1].str) == root) ? "0" : row[1].str;
result.push_back({
{"_id", row[0].str},
{"imghash", row[4].str.empty() ? nullptr : nlohmann::json(row[4].str)},
{"imgsrc", row[4].str.empty() ? nullptr : nlohmann::json(row[4].str)},
{"name", row[3].str},
{"pid", pid},
{"discounts", nlohmann::json::array()},
{"sort", row[2].str},
{"lastChanged", row[5].str},
{"updated_at", ts},
{"created_at", ts}
});
}
return result;
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include "../db/pool.hpp"
#include "nlohmann/json.hpp"
#include "shop.hpp"
inline nlohmann::json get_composite_product_list(int64_t cursor, int limit) {
int shop = get_active_shop_id();
std::string sql =
"SELECT TOP (?) s.kVaterArtikel AS productId, "
"s.kArtikel AS productIdComponent, "
"CONVERT(VARCHAR(20), s.fAnzahl, 2) AS quantity, "
"CONVERT(BIGINT, a.bRowversion) AS lastChanged "
"FROM dbo.tStueckliste s "
"INNER JOIN dbo.tArtikel a ON a.kArtikel = s.kVaterArtikel "
"WHERE a.kStueckliste <> 0 "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tKategorieArtikel ka "
"INNER JOIN dbo.tKategorieShop ks ON ks.kKategorie = ka.kKategorie "
"AND ks.kShop = ? WHERE ka.kArtikel = a.kArtikel)) "
"AND CONVERT(BIGINT, a.bRowversion) > ? "
"ORDER BY lastChanged ASC";
std::vector<Param> ps = {
{ParamType::Int,"",limit},
{ParamType::Int,"",shop},
{ParamType::Int,"",shop},
{ParamType::BigInt,"",cursor}
};
ResultSet rs;
get_pool().execute(sql, ps, rs);
nlohmann::json result = nlohmann::json::array();
for (auto& row : rs) {
result.push_back({
{"productId", row[0].str},
{"productIdComponent", row[1].str},
{"quantity", row[2].str},
{"lastChanged", row[3].str}
});
}
return result;
}

View File

@@ -0,0 +1,65 @@
#pragma once
#include "../db/pool.hpp"
static const char* CATEGORY_COUNT_SQL =
"WITH CategoryTree AS ("
" SELECT kKategorie FROM dbo.tKategorie WHERE kKategorie = ?"
" UNION ALL"
" SELECT t.kKategorie FROM dbo.tKategorie t"
" INNER JOIN CategoryTree ct ON t.kOberKategorie = ct.kKategorie"
") SELECT COUNT(*) AS cnt FROM dbo.tKategorie k "
"WHERE k.kKategorie IN (SELECT kKategorie FROM CategoryTree) "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tKategorieShop ks "
"WHERE ks.kKategorie = k.kKategorie AND ks.kShop = ?)) "
"AND CONVERT(BIGINT, k.bRowversion) > ?";
static const char* PRODUCT_COUNT_SQL =
"WITH CategoryTree AS ("
" SELECT kKategorie FROM dbo.tKategorie WHERE kKategorie = ?"
" UNION ALL"
" SELECT t.kKategorie FROM dbo.tKategorie t"
" INNER JOIN CategoryTree ct ON t.kOberKategorie = ct.kKategorie"
") SELECT COUNT(DISTINCT a.kArtikel) AS cnt FROM dbo.tArtikel a "
"INNER JOIN dbo.tKategorieArtikel ka ON ka.kArtikel = a.kArtikel "
"WHERE a.cAktiv = 'Y' "
"AND ka.kKategorie IN (SELECT kKategorie FROM CategoryTree) "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tKategorieShop ks "
"WHERE ks.kKategorie = ka.kKategorie AND ks.kShop = ?)) "
"AND (CONVERT(BIGINT, a.bRowversion) > ? "
"OR EXISTS (SELECT 1 FROM dbo.tArtikelbildPlattform abp "
"WHERE abp.kArtikel = a.kArtikel AND abp.kShop = ? "
"AND CONVERT(BIGINT, abp.bRowversion) > ?))";
static const char* COMPOSITE_PRODUCT_COUNT_SQL =
"SELECT COUNT(DISTINCT a.kArtikel) AS cnt FROM dbo.tArtikel a "
"INNER JOIN dbo.tStueckliste s ON s.kStueckliste = a.kStueckliste "
"WHERE a.kStueckliste <> 0 "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tKategorieArtikel ka "
"INNER JOIN dbo.tKategorieShop ks ON ks.kKategorie = ka.kKategorie "
"AND ks.kShop = ? WHERE ka.kArtikel = a.kArtikel)) "
"AND CONVERT(BIGINT, a.bRowversion) > ?";
static const char* DELETED_ENTITY_COUNT_SQL =
"SELECT COUNT(*) AS cnt FROM Pos.vDeletedEntity "
"WHERE CONVERT(BIGINT, vDeletedEntity.bLastChanged) > ?";
inline int64_t get_category_count(int64_t root_cat, int k_shop, int64_t cursor) {
return get_pool().execute_scalar(CATEGORY_COUNT_SQL,
{{ParamType::BigInt,"",root_cat},{ParamType::BigInt,"",k_shop},
{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",cursor}});
}
inline int64_t get_product_count(int64_t root_cat, int k_shop, int64_t cursor) {
return get_pool().execute_scalar(PRODUCT_COUNT_SQL,
{{ParamType::BigInt,"",root_cat},{ParamType::BigInt,"",k_shop},
{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",cursor},
{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",cursor}});
}
inline int64_t get_composite_count(int k_shop, int64_t cursor) {
return get_pool().execute_scalar(COMPOSITE_PRODUCT_COUNT_SQL,
{{ParamType::BigInt,"",k_shop},{ParamType::BigInt,"",k_shop},
{ParamType::BigInt,"",cursor}});
}
inline int64_t get_deleted_count(int64_t cursor) {
return get_pool().execute_scalar(DELETED_ENTITY_COUNT_SQL,
{{ParamType::BigInt,"",cursor}});
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include "../db/pool.hpp"
#include "nlohmann/json.hpp"
static const char* CUSTOMER_GROUP_IDS_SQL =
"SELECT kKundenGruppe FROM dbo.tKundenGruppe ORDER BY kKundenGruppe";
static const char* CUSTOMER_GROUP_LIST_SQL =
"SELECT kKundenGruppe AS id, cName AS name, nStandard AS standard, "
"fRabatt AS discountPercent, CONVERT(BIGINT, bRowversion) AS lastChanged "
"FROM dbo.tKundenGruppe WHERE CONVERT(BIGINT, bRowversion) > ? "
"ORDER BY lastChanged ASC";
static const char* CUSTOMER_GROUP_COUNT_SQL =
"SELECT COUNT(*) AS cnt FROM dbo.tKundenGruppe "
"WHERE CONVERT(BIGINT, bRowversion) > ?";
inline std::vector<int64_t> get_customer_group_ids() {
ResultSet rs;
get_pool().execute(CUSTOMER_GROUP_IDS_SQL, rs);
std::vector<int64_t> ids;
for (auto& row : rs) {
ids.push_back(std::stoll(row[0].str));
}
return ids;
}
inline nlohmann::json get_customer_group_list(int64_t cursor = 0) {
Param p; p.type = ParamType::BigInt; p.int_val = cursor;
ResultSet rs;
get_pool().execute(CUSTOMER_GROUP_LIST_SQL, {p}, rs);
nlohmann::json result = nlohmann::json::array();
for (auto& row : rs) {
result.push_back({
{"customerGroupId", row[0].str},
{"name", row[1].str},
{"standard", row[2].str},
{"discountPercent", std::to_string(std::stod(row[3].str))},
{"lastChanged", row[4].str}
});
}
return result;
}
inline int64_t get_customer_group_count(int64_t cursor = 0) {
Param p; p.type = ParamType::BigInt; p.int_val = cursor;
return get_pool().execute_scalar(CUSTOMER_GROUP_COUNT_SQL, {p});
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "../db/pool.hpp"
#include "nlohmann/json.hpp"
inline nlohmann::json get_deleted_entity_list(int64_t cursor, int limit) {
std::string sql =
"SELECT TOP (?) vDeletedEntity.kEntityId, "
"vDeletedEntity.nEntityType, "
"CONVERT(BIGINT, vDeletedEntity.bLastChanged) AS lastChanged "
"FROM Pos.vDeletedEntity "
"WHERE CONVERT(BIGINT, vDeletedEntity.bLastChanged) > ? "
"ORDER BY lastChanged ASC";
std::vector<Param> ps = {
{ParamType::Int,"",limit},
{ParamType::BigInt,"",cursor}
};
ResultSet rs;
get_pool().execute(sql, ps, rs);
nlohmann::json result = nlohmann::json::array();
for (auto& row : rs) {
result.push_back({
{"entityId", row[0].str},
{"entityType", row[1].str},
{"lastChanged", row[2].str}
});
}
return result;
}

View File

@@ -0,0 +1,132 @@
#pragma once
#include "../db/pool.hpp"
#include "../log.hpp"
#include <string>
#include <vector>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <vips/vips.h>
struct ImageResult {
std::vector<uint8_t> buffer;
std::string content_type;
};
static std::string content_type_for(const std::string& quelle) {
size_t dot = quelle.rfind('.');
std::string ext = (dot != std::string::npos) ? quelle.substr(dot + 1) : "";
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == "png") return "image/png";
if (ext == "gif") return "image/gif";
if (ext == "webp") return "image/webp";
return "image/jpeg";
}
static std::string ext_for_content_type(const std::string& ct) {
if (ct == "image/png") return ".png";
if (ct == "image/gif") return ".gif";
if (ct == "image/webp") return ".webp";
return ".jpg";
}
static ImageResult resize_image(const uint8_t* data, size_t len,
int target_size, const std::string& content_type) {
std::string fmt = ext_for_content_type(content_type);
// Use vips_thumbnail_buffer for fast shrink-on-load
VipsImage* out = nullptr;
if (vips_thumbnail_buffer((void*)data, len, &out, target_size,
"height", target_size,
"no_rotate", TRUE,
nullptr)) {
logc::warn("vips_thumbnail_buffer failed (%d): %s",
vips_error_buffer(), vips_error_buffer());
vips_error_clear();
return {};
}
// Write to memory buffer
void* buf = nullptr;
size_t buf_len = 0;
if (vips_image_write_to_buffer(out, fmt.c_str(), &buf, &buf_len, nullptr)) {
logc::warn("vips: failed to write resized image");
g_object_unref(out);
return {};
}
ImageResult result;
result.buffer.assign((uint8_t*)buf, (uint8_t*)buf + buf_len);
result.content_type = content_type;
g_free(buf);
g_object_unref(out);
return result;
}
inline ImageResult get_image_by_hash(const std::string& hash, const std::string& size) {
std::string sql =
"SELECT bBild, bVorschauBild, nBreite, nHoehe, "
"nVorschauBreite, nVorschauHoehe, cQuelle "
"FROM dbo.tBild WHERE cHash = ?";
Param p; p.type = ParamType::NVarChar; p.str_val = hash;
ResultSet rs;
bool ok = get_pool().execute(sql, {p}, rs);
if (!ok) {
logc::warn("image query failed for hash=%s", hash.c_str());
return {};
}
if (rs.empty()) {
logc::warn("image not found for hash=%s (query ok, 0 rows)", hash.c_str());
return {};
}
auto& row = rs[0];
int target = size.empty() ? 200 : std::stoi(size);
std::string ct = content_type_for(row[6].str);
int preview_w = row[4].str.empty() ? 0 : std::stoi(row[4].str);
int preview_h = row[5].str.empty() ? 0 : std::stoi(row[5].str);
int preview_max = std::max(preview_w, preview_h);
bool has_full = !row[0].blob.empty();
bool has_preview = !row[1].blob.empty();
if (target <= 0) {
if (!has_full) return {};
ImageResult r;
r.buffer = std::move(row[0].blob);
r.content_type = ct;
return r;
}
// If preview exists and target fits within preview, resize from preview
if (has_preview && preview_max > 0 && target <= preview_max) {
auto r = resize_image(row[1].blob.data(), row[1].blob.size(), target, ct);
if (!r.buffer.empty()) return r;
// Fallback: raw preview
r.buffer = std::move(row[1].blob);
r.content_type = ct;
return r;
}
// Otherwise resize from full image
if (has_full) {
auto r = resize_image(row[0].blob.data(), row[0].blob.size(), target, ct);
if (!r.buffer.empty()) return r;
// Fallback: raw full
r.buffer = std::move(row[0].blob);
r.content_type = ct;
return r;
}
// Last resort: raw preview
if (has_preview) {
ImageResult r;
r.buffer = std::move(row[1].blob);
r.content_type = ct;
return r;
}
return {};
}

View File

@@ -0,0 +1,167 @@
#pragma once
#include "../db/pool.hpp"
#include "nlohmann/json.hpp"
#include "shop.hpp"
#include "customer_groups.hpp"
#include "../config.hpp"
#include "../http.hpp"
#include <cmath>
static const char* PRODUCT_LIST_SQL =
"WITH TaxRates AS ("
" SELECT kSteuerklasse, fSteuersatz FROM dbo.tSteuersatz"
" WHERE kSteuerzone IN (SELECT kSteuerzone FROM dbo.tSteuerzone WHERE cName = ?)"
"), ImageRV AS ("
" SELECT kArtikel, MAX(CONVERT(BIGINT, bRowversion)) AS maxImageRV"
" FROM dbo.tArtikelbildPlattform WHERE kShop = ? GROUP BY kArtikel"
") SELECT TOP (?) "
"a.kArtikel AS id, a.cArtNr AS sku, ab.cName AS name, "
"a.fVKNetto AS netPrice, tr.fSteuersatz AS taxRate, "
"a.dErstelldatum AS createdAt, "
"CASE WHEN ir.maxImageRV IS NOT NULL AND ir.maxImageRV > CONVERT(BIGINT, a.bRowversion) "
"THEN ir.maxImageRV ELSE CONVERT(BIGINT, a.bRowversion) END AS lastChanged, "
"(SELECT TOP 1 img.cHash FROM dbo.tArtikelbildPlattform abp "
"INNER JOIN dbo.tBild img ON img.kBild = abp.kBild "
"WHERE abp.kArtikel = a.kArtikel ORDER BY abp.nNr) AS imgHash, "
"(SELECT STRING_AGG(CAST(ka.kKategorie AS varchar(20)), ',') "
"FROM dbo.tkategorieartikel ka WHERE ka.kArtikel = a.kArtikel) AS categoryIds, "
"a.nIstVater AS isParent, a.kVaterArtikel AS parentArticleId, "
"CASE WHEN a.kStueckliste <> 0 THEN '1' ELSE '0' END AS isCompositeProduct, "
"(SELECT TOP 1 pv.cVariantName FROM Pos.vProductVariant pv "
"WHERE pv.kProduct = a.kArtikel) AS variantName "
"FROM dbo.tArtikel a "
"INNER JOIN dbo.tArtikelBeschreibung ab ON ab.kArtikel = a.kArtikel AND ab.kSprache = ? "
"LEFT JOIN TaxRates tr ON tr.kSteuerklasse = a.kSteuerklasse "
"LEFT JOIN ImageRV ir ON ir.kArtikel = a.kArtikel "
"WHERE a.cAktiv = 'Y' "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tKategorieArtikel ka "
"INNER JOIN dbo.tKategorieShop ks ON ks.kKategorie = ka.kKategorie AND ks.kShop = ? "
"WHERE ka.kArtikel = a.kArtikel)) "
"AND (CONVERT(BIGINT, a.bRowversion) > ? "
"OR (ir.maxImageRV IS NOT NULL AND ir.maxImageRV > ?)) "
"ORDER BY lastChanged ASC";
static const char* PRICE_OVERRIDES_SQL =
"SELECT p.kArtikel AS articleId, p.kKundenGruppe AS customerGroupId, "
"MIN(pd.fNettoPreis) AS netPrice FROM dbo.tPreis p "
"INNER JOIN dbo.tPreisDetail pd ON pd.kPreis = p.kPreis "
"WHERE p.kArtikel IN (%s) AND p.kShop = 0 AND pd.nAnzahlAb = 0 "
"GROUP BY p.kArtikel, p.kKundenGruppe";
static std::string gross_price(const std::string& net, const std::string& tax) {
double n = std::stod(net.empty() ? "0" : net);
double t = std::stod(tax.empty() ? "0" : tax);
char buf[32];
std::snprintf(buf, sizeof(buf), "%.2f", n * (1.0 + t / 100.0));
return buf;
}
inline nlohmann::json get_product_list(int64_t cursor, int limit) {
std::string tax_zone = config::get("TAX_ZONE_NAME", "Zone-EU");
int lang = config::get_int("LANGUAGE_ID", 1);
int shop = get_active_shop_id();
std::vector<Param> ps = {
{ParamType::NVarChar, tax_zone, 0},
{ParamType::Int,"",shop},
{ParamType::Int,"",limit},
{ParamType::Int,"",lang},
{ParamType::Int,"",shop},
{ParamType::Int,"",shop},
{ParamType::BigInt,"",cursor},
{ParamType::BigInt,"",cursor}
};
ResultSet rs;
get_pool().execute(PRODUCT_LIST_SQL, ps, rs);
auto cg_ids = get_customer_group_ids();
nlohmann::json result = nlohmann::json::array();
for (auto& row : rs) {
std::string base_price = gross_price(row[3].str, row[4].str);
std::string cats_raw = row[8].str;
std::vector<std::string> cat_ids;
if (!cats_raw.empty()) {
size_t pos = 0;
while ((pos = cats_raw.find(',')) != std::string::npos) {
cat_ids.push_back(cats_raw.substr(0, pos));
cats_raw.erase(0, pos + 1);
}
cat_ids.push_back(cats_raw);
}
nlohmann::json cats = nlohmann::json::array();
for (auto& c : cat_ids) cats.push_back({{"categoryId", c}});
nlohmann::json prices = nlohmann::json::array();
for (auto& cgid : cg_ids) {
prices.push_back({
{"customerGroupId", std::to_string(cgid)},
{"customerId", "0"},
{"price", base_price},
{"quantity", "0"}
});
}
nlohmann::json product = {
{"_id", row[0].str},
{"imghash", row[7].str.empty() ? nullptr : nlohmann::json(row[7].str)},
{"imgsrc", row[7].str.empty() ? nullptr : nlohmann::json(row[7].str)},
{"sku", row[1].str},
{"name", row[2].str},
{"tax_rate", std::to_string((int)std::round(std::stod(row[4].str.empty()?"0":row[4].str)))},
{"price", base_price},
{"created_at", row[5].str},
{"lastChanged", row[6].str},
{"categories_id", cat_ids.empty() ? "0" : cat_ids[0]},
{"categories", cats},
{"prices", prices},
{"is_parent", row[9].str == "1" ? "1" : "0"},
{"parent", std::stoll(row[10].str) > 0 ? row[10].str : "0"},
{"variants", row[12].str},
{"isCompositeProduct", row[11].str},
{"attributes", nlohmann::json::array()},
{"sort", "0"},
{"p_price", "0.00"},
{"discountable", "0"},
{"deposit", "0"},
{"discount", ""},
{"d_price", "0.0"},
{"tax_rate2", ""},
{"use_in_out_tax", "0"},
{"barcode", nullptr},
{"use_stock", "0"},
{"q_div", "0"},
{"quantity", "0"},
{"unit", nullptr},
{"single_bookable", "0"},
{"annotation", ""},
{"status", "0"},
{"tags", ""},
{"variants", row[12].str},
{"print_kitchen_receipt", "0"},
{"deposit_name", ""},
{"updated_at", "0001-01-01 00:00:00"},
{"configurationGroups", ""},
{"options", nullptr},
{"hasBestBeforeDate", "0"},
{"hasLotNumber", "0"},
{"hasSerialNumber", "0"},
{"PLU", ""},
{"short_description", ""},
{"minStock", "0"},
{"container", nlohmann::json::array()},
{"reservedQuantity", "0.00"},
{"deliveryDetails", nlohmann::json::array()},
{"isbn", ""},
{"manufacturerName", nullptr},
{"han", nullptr},
{"productType", "0"},
{"voucherData", nullptr},
{"inputPrice", "0"},
{"inputQuantity", "0"}
};
result.push_back(std::move(product));
}
return result;
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "../db/pool.hpp"
#include "../config.hpp"
static int g_active_shop_id = 0;
static int g_active_shop_subshop_id = 0;
inline int get_active_shop_id() { return g_active_shop_id; }
inline int get_active_shop_subshop_id() { return g_active_shop_subshop_id; }
inline bool fetch_active_shop() {
ResultSet rs;
if (!get_pool().execute(
"SELECT TOP 1 kShop, kShopSubshop FROM dbo.tShopSubshop "
"WHERE nGesperrt = 0 ORDER BY kShop", rs) || rs.empty()) {
return false;
}
g_active_shop_id = std::stoi(rs[0][0].str);
g_active_shop_subshop_id = rs[0].size() > 1 ? std::stoi(rs[0][1].str) : 0;
return true;
}

View File

@@ -0,0 +1,40 @@
#include "request_log.hpp"
#include "log.hpp"
#include <chrono>
#include <ctime>
#include <sys/stat.h>
static void ensure_parent_dir(const std::string& path) {
size_t pos = path.rfind('/');
if (pos != std::string::npos) {
mkdir(path.substr(0, pos).c_str(), 0755);
}
}
void RequestLog::open(const std::string& path) {
ensure_parent_dir(path);
fp_ = std::fopen(path.c_str(), "a");
if (!fp_) logc::warn("cannot open request log: %s", path.c_str());
}
void RequestLog::close() {
if (fp_) { std::fclose(fp_); fp_ = nullptr; }
}
void RequestLog::log(const std::string& remote, const std::string& method,
const std::string& url, int status, int duration_ms,
const std::string& response) {
if (!fp_) return;
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf{};
localtime_r(&t, &tm_buf);
char ts[32];
std::strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", &tm_buf);
std::lock_guard<std::mutex> lock(mutex_);
std::fprintf(fp_, "%s %s %s %s %d %dms %s\n",
ts, remote.c_str(), method.c_str(), url.c_str(),
status, duration_ms, response.c_str());
std::fflush(fp_);
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include <cstdio>
#include <string>
#include <mutex>
class RequestLog {
public:
void open(const std::string& path);
void close();
void log(const std::string& remote, const std::string& method,
const std::string& url, int status, int duration_ms,
const std::string& response);
private:
FILE* fp_ = nullptr;
std::mutex mutex_;
};

68
jtlsrv-cpp/src/router.cpp Normal file
View File

@@ -0,0 +1,68 @@
#include "router.hpp"
#include "log.hpp"
#include <chrono>
#include <algorithm>
void Router::add_route(const std::string& method, const std::string& path, Handler handler) {
std::string key = method + " " + path;
routes_[key] = std::move(handler);
}
void Router::dispatch(tls_session* sess, PairingStore& pairing, const json& config) {
auto& req = sess->current_request;
auto& resp = sess->current_response;
// Build the full URL path (without host) for query param access
std::string full_url = req.path;
if (!req.query_string.empty()) {
full_url += "?" + req.query_string;
}
std::string route_key = req.method + " " + req.path;
auto it = routes_.find(route_key);
if (it != routes_.end()) {
RouteContext ctx;
ctx.query_path = req.path;
ctx.full_url = full_url;
ctx.pairing_store = &pairing;
ctx.config = config;
it->second(req, resp, ctx);
return;
}
// 404
resp.send_json(404, {
{"Message", "No HTTP resource was found that matches the request URI '" + full_url + "'."}
});
}
// Init suppression: log only if non-200, slow, or first/different request
// within 30s window. Port of server.js shouldLogInit().
bool Router::should_log_init(const std::string& url, int status, int duration_ms,
const std::string& response) {
// Report suppressed count hourly
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
if (suppressed_count_ > 0 && (now - suppressed_report_ts_) > 3'600'000) {
logc::info("Suppressed %d init log(s) in the last hour", suppressed_count_);
suppressed_count_ = 0;
suppressed_report_ts_ = now;
}
if (status != 200) return true;
if (duration_ms > 400) return true;
int64_t time_since_last = now - last_init_timestamp_;
if (time_since_last < 30'000 && url == last_init_url_ && response == last_init_result_) {
last_init_timestamp_ = now;
suppressed_count_++;
return false;
}
last_init_url_ = url;
last_init_result_ = response;
last_init_timestamp_ = now;
return true;
}

35
jtlsrv-cpp/src/router.hpp Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
// Router: maps "METHOD /path" -> handler. Port of src/jtl-server.js.
#include "http.hpp"
#include "pairing.hpp"
#include "tls_server.hpp"
#include <functional>
#include <string>
#include <unordered_map>
class Router {
public:
using Handler = RouteHandler;
void add_route(const std::string& method, const std::string& path, Handler handler);
// Dispatch a request. Called from the TLS on_request callback.
void dispatch(tls_session* sess, PairingStore& pairing, const json& config);
// Suppress init logs for repeated identical requests within 30s
bool should_log_init(const std::string& url, int status, int duration_ms,
const std::string& response);
private:
std::unordered_map<std::string, Handler> routes_;
// Init suppression state
std::string last_init_url_;
std::string last_init_result_;
int64_t last_init_timestamp_ = 0;
int suppressed_count_ = 0;
int64_t suppressed_report_ts_ = 0;
};

View File

@@ -0,0 +1,383 @@
// tls_server.cpp — OpenSSL memory-BIOs pumped over uv_tcp_t + llhttp.
//
// This is the genuinely new piece compared to the Node.js original (~300 lines).
// Pattern: accept TCP -> SSL_new with mem BIOs -> uv_read_start feeds encrypted
// bytes into rbio -> SSL_read drains plaintext into llhttp -> SSL_write puts
// response plaintext into wbio -> flush wbio to socket.
#include "tls_server.hpp"
#include "log.hpp"
#include <cstring>
// ---------------------------------------------------------------------------
// Globals
// ---------------------------------------------------------------------------
static uv_loop_t* g_loop = nullptr;
static SSL_CTX* g_ssl_ctx = nullptr;
static uv_tcp_t g_server{};
static uv_signal_t g_sigint{};
static uv_signal_t g_sigterm{};
static void (*g_on_request)(tls_session*) = nullptr;
// ---------------------------------------------------------------------------
// Forward declarations for signal shutdown
// ---------------------------------------------------------------------------
static void tls_server_shutdown();
// ---------------------------------------------------------------------------
// SSL error logging helper
// ---------------------------------------------------------------------------
static void ssl_log_errors(const char* ctx) {
unsigned long e;
while ((e = ERR_get_error()) != 0) {
char buf[256];
ERR_error_string_n(e, buf, sizeof(buf));
logc::warn("[%s] SSL: %s", ctx, buf);
}
}
// ---------------------------------------------------------------------------
// Encrypted data flush: wbio -> socket
// ---------------------------------------------------------------------------
static void flush_encrypted(tls_session* sess) {
char buf[16384];
for (;;) {
int n = BIO_read(sess->wbio, buf, sizeof(buf));
if (n <= 0) break;
auto* req = new uv_write_t{};
char* data = new char[n];
std::memcpy(data, buf, n);
uv_buf_t wbuf = uv_buf_init(data, n);
req->data = data;
uv_write(req, reinterpret_cast<uv_stream_t*>(&sess->tcp_handle),
&wbuf, 1, [](uv_write_t* r, int) {
delete[] static_cast<char*>(r->data);
delete r;
});
}
}
// ---------------------------------------------------------------------------
// Drain SSL plaintext -> llhttp
// ---------------------------------------------------------------------------
static void drain_ssl_to_llhttp(tls_session* sess) {
char buf[16384];
for (;;) {
int n = SSL_read(sess->ssl, buf, sizeof(buf));
if (n <= 0) break;
llhttp_errno_t err = llhttp_execute(&sess->parser, buf, n);
if (err != HPE_OK && err != HPE_PAUSED) {
logc::warn("llhttp: %s", llhttp_errno_name(err));
session_close(sess);
return;
}
}
}
// ---------------------------------------------------------------------------
// llhttp callbacks (store parsed data into the tls_session's HttpRequest)
// ---------------------------------------------------------------------------
static int cb_begin(llhttp_t* p) {
auto* s = static_cast<tls_session*>(p->data);
s->current_request = HttpRequest{};
return 0;
}
static int cb_method(llhttp_t* p, const char* at, size_t len) {
auto* s = static_cast<tls_session*>(p->data);
s->current_request.method.assign(at, len);
return 0;
}
static int cb_url(llhttp_t* p, const char* at, size_t len) {
auto* s = static_cast<tls_session*>(p->data);
std::string raw(at, len);
size_t q = raw.find('?');
if (q != std::string::npos) {
s->current_request.query_string = raw.substr(q + 1);
s->current_request.path = raw.substr(0, q);
} else {
s->current_request.path = raw;
}
return 0;
}
static int cb_header_field(llhttp_t* p, const char* at, size_t len) {
auto* s = static_cast<tls_session*>(p->data);
s->current_header_field.assign(at, len);
return 0;
}
static int cb_header_value(llhttp_t* p, const char* at, size_t len) {
auto* s = static_cast<tls_session*>(p->data);
s->current_request.headers[s->current_header_field].assign(at, len);
return 0;
}
static int cb_headers_complete(llhttp_t* p) {
auto* s = static_cast<tls_session*>(p->data);
auto it = s->current_request.headers.find("Content-Length");
if (it != s->current_request.headers.end()) {
s->body_length = static_cast<uint32_t>(std::stoul(it->second));
}
return 0;
}
static int cb_body(llhttp_t* p, const char* at, size_t len) {
auto* s = static_cast<tls_session*>(p->data);
auto& body = s->current_request.body;
body.insert(body.end(),
reinterpret_cast<const uint8_t*>(at),
reinterpret_cast<const uint8_t*>(at + len));
return 0;
}
static int cb_message_complete(llhttp_t* p) {
auto* s = static_cast<tls_session*>(p->data);
s->current_response = HttpResponse{};
s->current_response.session = s;
// Normalize /api/v1 -> /v1
s->current_request.path = normalize_path(s->current_request.path);
// Reset idle timer
uv_timer_stop(&s->timer_handle);
uv_timer_start(&s->timer_handle,
[](uv_timer_t* h) {
session_close(static_cast<tls_session*>(h->data));
}, 300'000, 0);
if (g_on_request) g_on_request(s);
return 0;
}
static void init_parser(tls_session* s) {
llhttp_settings_init(&s->parser_settings);
s->parser_settings.on_message_begin = cb_begin;
s->parser_settings.on_method = cb_method;
s->parser_settings.on_url = cb_url;
s->parser_settings.on_header_field = cb_header_field;
s->parser_settings.on_header_value = cb_header_value;
s->parser_settings.on_headers_complete = cb_headers_complete;
s->parser_settings.on_body = cb_body;
s->parser_settings.on_message_complete = cb_message_complete;
llhttp_init(&s->parser, HTTP_REQUEST, &s->parser_settings);
s->parser.data = s;
}
// ---------------------------------------------------------------------------
// Session lifecycle
// ---------------------------------------------------------------------------
static void on_alloc(uv_handle_t*, size_t suggested, uv_buf_t* buf) {
buf->base = new char[suggested];
buf->len = suggested;
}
static void on_read(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) {
auto* sess = static_cast<tls_session*>(stream->data);
if (nread > 0 && sess->ssl) {
// Feed encrypted bytes into the read BIO
BIO_write(sess->rbio, buf->base, nread);
// TLS handshake (may need multiple rounds)
if (!sess->handshake_done) {
int ret = SSL_do_handshake(sess->ssl);
if (ret == 1) {
sess->handshake_done = true;
logc::info("TLS handshake complete");
flush_encrypted(sess);
} else {
int err = SSL_get_error(sess->ssl, ret);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
flush_encrypted(sess);
delete[] buf->base;
return;
}
ssl_log_errors("handshake");
delete[] buf->base;
session_close(sess);
return;
}
}
// Read plaintext and pump through llhttp
drain_ssl_to_llhttp(sess);
}
if (nread < 0 && nread != UV_EOF) {
logc::info("client disconnected: %s", uv_strerror(static_cast<int>(nread)));
}
if (nread < 0) {
delete[] buf->base;
session_close(sess);
return;
}
delete[] buf->base;
}
static void on_close(uv_handle_t* handle) {
auto* sess = static_cast<tls_session*>(handle->data);
if (sess->ssl) { SSL_free(sess->ssl); sess->ssl = nullptr; }
delete sess;
}
// ---------------------------------------------------------------------------
// New connection
// ---------------------------------------------------------------------------
static void on_connection(uv_stream_t* server, int status) {
if (status < 0) {
logc::error("accept: %s", uv_strerror(status));
return;
}
auto* sess = new tls_session{};
sess->tcp_handle.data = sess;
uv_tcp_init(g_loop, &sess->tcp_handle);
if (uv_accept(server, reinterpret_cast<uv_stream_t*>(&sess->tcp_handle)) != 0) {
uv_close(reinterpret_cast<uv_handle_t*>(&sess->tcp_handle), on_close);
return;
}
uv_tcp_nodelay(&sess->tcp_handle, 1);
// OpenSSL
sess->ssl = SSL_new(g_ssl_ctx);
sess->rbio = BIO_new(BIO_s_mem());
sess->wbio = BIO_new(BIO_s_mem());
SSL_set_bio(sess->ssl, sess->rbio, sess->wbio);
SSL_set_accept_state(sess->ssl);
// llhttp
init_parser(sess);
// Peer address
struct sockaddr_storage saddr;
int slen = sizeof(saddr);
uv_tcp_getpeername(&sess->tcp_handle, reinterpret_cast<struct sockaddr*>(&saddr), &slen);
char addr_buf[INET6_ADDRSTRLEN] = {};
if (saddr.ss_family == AF_INET)
uv_ip4_name(reinterpret_cast<struct sockaddr_in*>(&saddr), addr_buf, sizeof(addr_buf));
else
uv_ip6_name(reinterpret_cast<struct sockaddr_in6*>(&saddr), addr_buf, sizeof(addr_buf));
logc::info("TLS connection from %s", addr_buf);
// Read encrypted data
uv_read_start(reinterpret_cast<uv_stream_t*>(&sess->tcp_handle), on_alloc, on_read);
// Idle timer
uv_timer_init(g_loop, &sess->timer_handle);
sess->timer_handle.data = sess;
uv_timer_start(&sess->timer_handle,
[](uv_timer_t* h) {
logc::info("idle timeout");
session_close(static_cast<tls_session*>(h->data));
}, 300'000, 0);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void tls_server_set_handler(void (*handler)(tls_session*)) {
g_on_request = handler;
}
int tls_server_init(uv_loop_t* loop, const char* host, int port,
const char* cert_path, const char* key_path) {
g_loop = loop;
g_ssl_ctx = SSL_CTX_new(TLS_server_method());
if (!g_ssl_ctx) { ssl_log_errors("SSL_CTX_new"); return -1; }
SSL_CTX_set_min_proto_version(g_ssl_ctx, TLS1_2_VERSION);
if (SSL_CTX_use_certificate_chain_file(g_ssl_ctx, cert_path) != 1) {
ssl_log_errors("cert"); return -1;
}
if (SSL_CTX_use_PrivateKey_file(g_ssl_ctx, key_path, SSL_FILETYPE_PEM) != 1) {
ssl_log_errors("key"); return -1;
}
if (SSL_CTX_check_private_key(g_ssl_ctx) != 1) {
ssl_log_errors("check_key"); return -1;
}
uv_tcp_init(loop, &g_server);
g_server.data = nullptr;
struct sockaddr_in addr;
uv_ip4_addr(host, port, &addr);
int r = uv_tcp_bind(&g_server, reinterpret_cast<struct sockaddr*>(&addr), 0);
if (r) { logc::error("bind: %s", uv_strerror(r)); return r; }
r = uv_listen(reinterpret_cast<uv_stream_t*>(&g_server), 128, on_connection);
if (r) { logc::error("listen: %s", uv_strerror(r)); return r; }
logc::success("HTTPS server listening on %s:%d", host, port);
return 0;
}
static void tls_server_shutdown() {
logc::info("shutting down...");
uv_signal_stop(&g_sigint);
uv_signal_stop(&g_sigterm);
uv_close(reinterpret_cast<uv_handle_t*>(&g_sigint), nullptr);
uv_close(reinterpret_cast<uv_handle_t*>(&g_sigterm), nullptr);
uv_close(reinterpret_cast<uv_handle_t*>(&g_server), nullptr);
}
void tls_server_install_signals(uv_loop_t* loop) {
uv_signal_init(loop, &g_sigint);
uv_signal_start(&g_sigint, [](uv_signal_t*, int) { tls_server_shutdown(); }, SIGINT);
uv_signal_init(loop, &g_sigterm);
uv_signal_start(&g_sigterm, [](uv_signal_t*, int) { tls_server_shutdown(); }, SIGTERM);
}
void session_write(tls_session* sess, const std::string& data) {
if (!sess->ssl || sess->tcp_handle.type != UV_TCP) return;
SSL_write(sess->ssl, data.data(), data.size());
flush_encrypted(sess);
}
void session_write_binary(tls_session* sess, const std::string& header,
const std::vector<uint8_t>& data) {
if (!sess->ssl || sess->tcp_handle.type != UV_TCP) return;
SSL_write(sess->ssl, header.data(), header.size());
if (!data.empty())
SSL_write(sess->ssl, data.data(), data.size());
flush_encrypted(sess);
}
void session_close(tls_session* sess) {
if (sess->tcp_handle.type != UV_TCP) return;
uv_timer_stop(&sess->timer_handle);
uv_close(reinterpret_cast<uv_handle_t*>(&sess->timer_handle), nullptr);
uv_close(reinterpret_cast<uv_handle_t*>(&sess->tcp_handle), on_close);
}
const char* reason_phrase(int code) {
switch (code) {
case 200: return "OK";
case 400: return "Bad Request";
case 404: return "Not Found";
case 500: return "Internal Server Error";
default: return "Unknown";
}
}

View File

@@ -0,0 +1,66 @@
#pragma once
// TLS server: OpenSSL memory-BIOs pumped over uv_tcp_t + llhttp for HTTP parsing.
// This is the genuinely new piece compared to the Node.js original.
#include <cstdint>
#include <string>
#include <vector>
#include <uv.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <llhttp.h>
#include "http.hpp"
// ---------------------------------------------------------------------------
// Per-connection TLS session
// ---------------------------------------------------------------------------
struct tls_session {
uv_tcp_t tcp_handle{};
uv_timer_t timer_handle{};
SSL* ssl = nullptr;
BIO* rbio = nullptr; // we feed encrypted data here
BIO* wbio = nullptr; // we read encrypted data from here
llhttp_t parser{};
llhttp_settings_t parser_settings{};
HttpRequest current_request{};
HttpResponse current_response{};
std::string current_header_field{}; // tracks header name during parsing
bool handshake_done = false;
uint32_t body_length = 0;
};
// ---------------------------------------------------------------------------
// Server API
// ---------------------------------------------------------------------------
// Initialize the OpenSSL context and start listening.
// Returns 0 on success.
int tls_server_init(uv_loop_t* loop, const char* host, int port,
const char* cert_path, const char* key_path);
// Set the callback invoked for each fully-parsed HTTP request.
void tls_server_set_handler(void (*handler)(tls_session*));
// Write an encrypted response buffer to the client.
void session_write(tls_session* sess, const std::string& data);
// Write an encrypted response with binary payload to the client.
void session_write_binary(tls_session* sess, const std::string& header,
const std::vector<uint8_t>& data);
// Get the reason phrase for an HTTP status code.
const char* reason_phrase(int code);
// Close a TLS session cleanly.
void session_close(tls_session* sess);
// Install SIGINT/SIGTERM handlers for graceful shutdown.
void tls_server_install_signals(uv_loop_t* loop);

510
jtlsrv-cpp/vendor/api.c vendored Normal file
View File

@@ -0,0 +1,510 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "llhttp.h"
#define CALLBACK_MAYBE(PARSER, NAME) \
do { \
const llhttp_settings_t* settings; \
settings = (const llhttp_settings_t*) (PARSER)->settings; \
if (settings == NULL || settings->NAME == NULL) { \
err = 0; \
break; \
} \
err = settings->NAME((PARSER)); \
} while (0)
#define SPAN_CALLBACK_MAYBE(PARSER, NAME, START, LEN) \
do { \
const llhttp_settings_t* settings; \
settings = (const llhttp_settings_t*) (PARSER)->settings; \
if (settings == NULL || settings->NAME == NULL) { \
err = 0; \
break; \
} \
err = settings->NAME((PARSER), (START), (LEN)); \
if (err == -1) { \
err = HPE_USER; \
llhttp_set_error_reason((PARSER), "Span callback error in " #NAME); \
} \
} while (0)
void llhttp_init(llhttp_t* parser, llhttp_type_t type,
const llhttp_settings_t* settings) {
llhttp__internal_init(parser);
parser->type = type;
parser->settings = (void*) settings;
}
#if defined(__wasm__)
extern int wasm_on_message_begin(llhttp_t * p);
extern int wasm_on_url(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_status(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_header_field(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_header_value(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_headers_complete(llhttp_t * p, int status_code,
uint8_t upgrade, int should_keep_alive);
extern int wasm_on_body(llhttp_t* p, const char* at, size_t length);
extern int wasm_on_message_complete(llhttp_t * p);
static int wasm_on_headers_complete_wrap(llhttp_t* p) {
return wasm_on_headers_complete(p, p->status_code, p->upgrade,
llhttp_should_keep_alive(p));
}
const llhttp_settings_t wasm_settings = {
wasm_on_message_begin,
wasm_on_url,
wasm_on_status,
NULL,
NULL,
wasm_on_header_field,
wasm_on_header_value,
NULL,
NULL,
wasm_on_headers_complete_wrap,
wasm_on_body,
wasm_on_message_complete,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
};
llhttp_t* llhttp_alloc(llhttp_type_t type) {
llhttp_t* parser = malloc(sizeof(llhttp_t));
llhttp_init(parser, type, &wasm_settings);
return parser;
}
void llhttp_free(llhttp_t* parser) {
free(parser);
}
#endif // defined(__wasm__)
/* Some getters required to get stuff from the parser */
uint8_t llhttp_get_type(llhttp_t* parser) {
return parser->type;
}
uint8_t llhttp_get_http_major(llhttp_t* parser) {
return parser->http_major;
}
uint8_t llhttp_get_http_minor(llhttp_t* parser) {
return parser->http_minor;
}
uint8_t llhttp_get_method(llhttp_t* parser) {
return parser->method;
}
int llhttp_get_status_code(llhttp_t* parser) {
return parser->status_code;
}
uint8_t llhttp_get_upgrade(llhttp_t* parser) {
return parser->upgrade;
}
void llhttp_reset(llhttp_t* parser) {
llhttp_type_t type = parser->type;
const llhttp_settings_t* settings = parser->settings;
void* data = parser->data;
uint16_t lenient_flags = parser->lenient_flags;
llhttp__internal_init(parser);
parser->type = type;
parser->settings = (void*) settings;
parser->data = data;
parser->lenient_flags = lenient_flags;
}
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len) {
return llhttp__internal_execute(parser, data, data + len);
}
void llhttp_settings_init(llhttp_settings_t* settings) {
memset(settings, 0, sizeof(*settings));
}
llhttp_errno_t llhttp_finish(llhttp_t* parser) {
int err;
/* We're in an error state. Don't bother doing anything. */
if (parser->error != 0) {
return 0;
}
switch (parser->finish) {
case HTTP_FINISH_SAFE_WITH_CB:
CALLBACK_MAYBE(parser, on_message_complete);
if (err != HPE_OK) return err;
/* FALLTHROUGH */
case HTTP_FINISH_SAFE:
return HPE_OK;
case HTTP_FINISH_UNSAFE:
parser->reason = "Invalid EOF state";
return HPE_INVALID_EOF_STATE;
default:
abort();
}
}
void llhttp_pause(llhttp_t* parser) {
if (parser->error != HPE_OK) {
return;
}
parser->error = HPE_PAUSED;
parser->reason = "Paused";
}
void llhttp_resume(llhttp_t* parser) {
if (parser->error != HPE_PAUSED) {
return;
}
parser->error = 0;
}
void llhttp_resume_after_upgrade(llhttp_t* parser) {
if (parser->error != HPE_PAUSED_UPGRADE) {
return;
}
parser->error = 0;
}
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) {
return parser->error;
}
const char* llhttp_get_error_reason(const llhttp_t* parser) {
return parser->reason;
}
void llhttp_set_error_reason(llhttp_t* parser, const char* reason) {
parser->reason = reason;
}
const char* llhttp_get_error_pos(const llhttp_t* parser) {
return parser->error_pos;
}
const char* llhttp_errno_name(llhttp_errno_t err) {
#define HTTP_ERRNO_GEN(CODE, NAME, _) case HPE_##NAME: return "HPE_" #NAME;
switch (err) {
HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
default: abort();
}
#undef HTTP_ERRNO_GEN
}
const char* llhttp_method_name(llhttp_method_t method) {
#define HTTP_METHOD_GEN(NUM, NAME, STRING) case HTTP_##NAME: return #STRING;
switch (method) {
HTTP_ALL_METHOD_MAP(HTTP_METHOD_GEN)
default: abort();
}
#undef HTTP_METHOD_GEN
}
const char* llhttp_status_name(llhttp_status_t status) {
#define HTTP_STATUS_GEN(NUM, NAME, STRING) case HTTP_STATUS_##NAME: return #STRING;
switch (status) {
HTTP_STATUS_MAP(HTTP_STATUS_GEN)
default: abort();
}
#undef HTTP_STATUS_GEN
}
void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_HEADERS;
} else {
parser->lenient_flags &= ~LENIENT_HEADERS;
}
}
void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_CHUNKED_LENGTH;
} else {
parser->lenient_flags &= ~LENIENT_CHUNKED_LENGTH;
}
}
void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_KEEP_ALIVE;
} else {
parser->lenient_flags &= ~LENIENT_KEEP_ALIVE;
}
}
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_TRANSFER_ENCODING;
} else {
parser->lenient_flags &= ~LENIENT_TRANSFER_ENCODING;
}
}
void llhttp_set_lenient_version(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_VERSION;
} else {
parser->lenient_flags &= ~LENIENT_VERSION;
}
}
void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_DATA_AFTER_CLOSE;
} else {
parser->lenient_flags &= ~LENIENT_DATA_AFTER_CLOSE;
}
}
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR;
} else {
parser->lenient_flags &= ~LENIENT_OPTIONAL_LF_AFTER_CR;
}
}
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
} else {
parser->lenient_flags &= ~LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
}
}
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF;
} else {
parser->lenient_flags &= ~LENIENT_OPTIONAL_CR_BEFORE_LF;
}
}
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
} else {
parser->lenient_flags &= ~LENIENT_SPACES_AFTER_CHUNK_SIZE;
}
}
/* Callbacks */
int llhttp__on_message_begin(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_message_begin);
return err;
}
int llhttp__on_url(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_url, p, endp - p);
return err;
}
int llhttp__on_url_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_url_complete);
return err;
}
int llhttp__on_status(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_status, p, endp - p);
return err;
}
int llhttp__on_status_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_status_complete);
return err;
}
int llhttp__on_method(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_method, p, endp - p);
return err;
}
int llhttp__on_method_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_method_complete);
return err;
}
int llhttp__on_version(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_version, p, endp - p);
return err;
}
int llhttp__on_version_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_version_complete);
return err;
}
int llhttp__on_header_field(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_header_field, p, endp - p);
return err;
}
int llhttp__on_header_field_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_header_field_complete);
return err;
}
int llhttp__on_header_value(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_header_value, p, endp - p);
return err;
}
int llhttp__on_header_value_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_header_value_complete);
return err;
}
int llhttp__on_headers_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_headers_complete);
return err;
}
int llhttp__on_message_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_message_complete);
return err;
}
int llhttp__on_body(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_body, p, endp - p);
return err;
}
int llhttp__on_chunk_header(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_header);
return err;
}
int llhttp__on_chunk_extension_name(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_chunk_extension_name, p, endp - p);
return err;
}
int llhttp__on_chunk_extension_name_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_extension_name_complete);
return err;
}
int llhttp__on_chunk_extension_value(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_chunk_extension_value, p, endp - p);
return err;
}
int llhttp__on_chunk_extension_value_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_extension_value_complete);
return err;
}
int llhttp__on_chunk_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_chunk_complete);
return err;
}
int llhttp__on_reset(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_reset);
return err;
}
/* Private */
void llhttp__debug(llhttp_t* s, const char* p, const char* endp,
const char* msg) {
if (p == endp) {
fprintf(stderr, "p=%p type=%d flags=%02x next=null debug=%s\n", s, s->type,
s->flags, msg);
} else {
fprintf(stderr, "p=%p type=%d flags=%02x next=%02x debug=%s\n", s,
s->type, s->flags, *p, msg);
}
}

170
jtlsrv-cpp/vendor/http.c vendored Normal file
View File

@@ -0,0 +1,170 @@
#include <stdio.h>
#ifndef LLHTTP__TEST
# include "llhttp.h"
#else
# define llhttp_t llparse_t
#endif /* */
int llhttp_message_needs_eof(const llhttp_t* parser);
int llhttp_should_keep_alive(const llhttp_t* parser);
int llhttp__before_headers_complete(llhttp_t* parser, const char* p,
const char* endp) {
/* Set this here so that on_headers_complete() callbacks can see it */
if ((parser->flags & F_UPGRADE) &&
(parser->flags & F_CONNECTION_UPGRADE)) {
/* For responses, "Upgrade: foo" and "Connection: upgrade" are
* mandatory only when it is a 101 Switching Protocols response,
* otherwise it is purely informational, to announce support.
*/
parser->upgrade =
(parser->type == HTTP_REQUEST || parser->status_code == 101);
} else {
parser->upgrade = (parser->method == HTTP_CONNECT);
}
return 0;
}
/* Return values:
* 0 - No body, `restart`, message_complete
* 1 - CONNECT request, `restart`, message_complete, and pause
* 2 - chunk_size_start
* 3 - body_identity
* 4 - body_identity_eof
* 5 - invalid transfer-encoding for request
*/
int llhttp__after_headers_complete(llhttp_t* parser, const char* p,
const char* endp) {
int hasBody;
hasBody = parser->flags & F_CHUNKED || parser->content_length > 0;
if (
(parser->upgrade && (parser->method == HTTP_CONNECT ||
(parser->flags & F_SKIPBODY) || !hasBody)) ||
/* See RFC 2616 section 4.4 - 1xx e.g. Continue */
(parser->type == HTTP_RESPONSE && parser->status_code == 101)
) {
/* Exit, the rest of the message is in a different protocol. */
return 1;
}
if (parser->type == HTTP_RESPONSE && parser->status_code == 100) {
/* No body, restart as the message is complete */
return 0;
}
/* See RFC 2616 section 4.4 */
if (
parser->flags & F_SKIPBODY || /* response to a HEAD request */
(
parser->type == HTTP_RESPONSE && (
parser->status_code == 102 || /* Processing */
parser->status_code == 103 || /* Early Hints */
parser->status_code == 204 || /* No Content */
parser->status_code == 304 /* Not Modified */
)
)
) {
return 0;
} else if (parser->flags & F_CHUNKED) {
/* chunked encoding - ignore Content-Length header, prepare for a chunk */
return 2;
} else if (parser->flags & F_TRANSFER_ENCODING) {
if (parser->type == HTTP_REQUEST &&
(parser->lenient_flags & LENIENT_CHUNKED_LENGTH) == 0 &&
(parser->lenient_flags & LENIENT_TRANSFER_ENCODING) == 0) {
/* RFC 7230 3.3.3 */
/* If a Transfer-Encoding header field
* is present in a request and the chunked transfer coding is not
* the final encoding, the message body length cannot be determined
* reliably; the server MUST respond with the 400 (Bad Request)
* status code and then close the connection.
*/
return 5;
} else {
/* RFC 7230 3.3.3 */
/* If a Transfer-Encoding header field is present in a response and
* the chunked transfer coding is not the final encoding, the
* message body length is determined by reading the connection until
* it is closed by the server.
*/
return 4;
}
} else {
if (!(parser->flags & F_CONTENT_LENGTH)) {
if (!llhttp_message_needs_eof(parser)) {
/* Assume content-length 0 - read the next */
return 0;
} else {
/* Read body until EOF */
return 4;
}
} else if (parser->content_length == 0) {
/* Content-Length header given but zero: Content-Length: 0\r\n */
return 0;
} else {
/* Content-Length header given and non-zero */
return 3;
}
}
}
int llhttp__after_message_complete(llhttp_t* parser, const char* p,
const char* endp) {
int should_keep_alive;
should_keep_alive = llhttp_should_keep_alive(parser);
parser->finish = HTTP_FINISH_SAFE;
parser->flags = 0;
/* NOTE: this is ignored in loose parsing mode */
return should_keep_alive;
}
int llhttp_message_needs_eof(const llhttp_t* parser) {
if (parser->type == HTTP_REQUEST) {
return 0;
}
/* See RFC 2616 section 4.4 */
if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */
parser->status_code == 204 || /* No Content */
parser->status_code == 304 || /* Not Modified */
(parser->flags & F_SKIPBODY)) { /* response to a HEAD request */
return 0;
}
/* RFC 7230 3.3.3, see `llhttp__after_headers_complete` */
if ((parser->flags & F_TRANSFER_ENCODING) &&
(parser->flags & F_CHUNKED) == 0) {
return 1;
}
if (parser->flags & (F_CHUNKED | F_CONTENT_LENGTH)) {
return 0;
}
return 1;
}
int llhttp_should_keep_alive(const llhttp_t* parser) {
if (parser->http_major > 0 && parser->http_minor > 0) {
/* HTTP/1.1 */
if (parser->flags & F_CONNECTION_CLOSE) {
return 0;
}
} else {
/* HTTP/1.0 or earlier */
if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) {
return 0;
}
}
return !llhttp_message_needs_eof(parser);
}

10168
jtlsrv-cpp/vendor/llhttp.c vendored Normal file

File diff suppressed because it is too large Load Diff

903
jtlsrv-cpp/vendor/llhttp.h vendored Normal file
View File

@@ -0,0 +1,903 @@
#ifndef INCLUDE_LLHTTP_H_
#define INCLUDE_LLHTTP_H_
#define LLHTTP_VERSION_MAJOR 9
#define LLHTTP_VERSION_MINOR 2
#define LLHTTP_VERSION_PATCH 1
#ifndef INCLUDE_LLHTTP_ITSELF_H_
#define INCLUDE_LLHTTP_ITSELF_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
typedef struct llhttp__internal_s llhttp__internal_t;
struct llhttp__internal_s {
int32_t _index;
void* _span_pos0;
void* _span_cb0;
int32_t error;
const char* reason;
const char* error_pos;
void* data;
void* _current;
uint64_t content_length;
uint8_t type;
uint8_t method;
uint8_t http_major;
uint8_t http_minor;
uint8_t header_state;
uint16_t lenient_flags;
uint8_t upgrade;
uint8_t finish;
uint16_t flags;
uint16_t status_code;
uint8_t initial_message_completed;
void* settings;
};
int llhttp__internal_init(llhttp__internal_t* s);
int llhttp__internal_execute(llhttp__internal_t* s, const char* p, const char* endp);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* INCLUDE_LLHTTP_ITSELF_H_ */
#ifndef LLLLHTTP_C_HEADERS_
#define LLLLHTTP_C_HEADERS_
#ifdef __cplusplus
extern "C" {
#endif
enum llhttp_errno {
HPE_OK = 0,
HPE_INTERNAL = 1,
HPE_STRICT = 2,
HPE_CR_EXPECTED = 25,
HPE_LF_EXPECTED = 3,
HPE_UNEXPECTED_CONTENT_LENGTH = 4,
HPE_UNEXPECTED_SPACE = 30,
HPE_CLOSED_CONNECTION = 5,
HPE_INVALID_METHOD = 6,
HPE_INVALID_URL = 7,
HPE_INVALID_CONSTANT = 8,
HPE_INVALID_VERSION = 9,
HPE_INVALID_HEADER_TOKEN = 10,
HPE_INVALID_CONTENT_LENGTH = 11,
HPE_INVALID_CHUNK_SIZE = 12,
HPE_INVALID_STATUS = 13,
HPE_INVALID_EOF_STATE = 14,
HPE_INVALID_TRANSFER_ENCODING = 15,
HPE_CB_MESSAGE_BEGIN = 16,
HPE_CB_HEADERS_COMPLETE = 17,
HPE_CB_MESSAGE_COMPLETE = 18,
HPE_CB_CHUNK_HEADER = 19,
HPE_CB_CHUNK_COMPLETE = 20,
HPE_PAUSED = 21,
HPE_PAUSED_UPGRADE = 22,
HPE_PAUSED_H2_UPGRADE = 23,
HPE_USER = 24,
HPE_CB_URL_COMPLETE = 26,
HPE_CB_STATUS_COMPLETE = 27,
HPE_CB_METHOD_COMPLETE = 32,
HPE_CB_VERSION_COMPLETE = 33,
HPE_CB_HEADER_FIELD_COMPLETE = 28,
HPE_CB_HEADER_VALUE_COMPLETE = 29,
HPE_CB_CHUNK_EXTENSION_NAME_COMPLETE = 34,
HPE_CB_CHUNK_EXTENSION_VALUE_COMPLETE = 35,
HPE_CB_RESET = 31
};
typedef enum llhttp_errno llhttp_errno_t;
enum llhttp_flags {
F_CONNECTION_KEEP_ALIVE = 0x1,
F_CONNECTION_CLOSE = 0x2,
F_CONNECTION_UPGRADE = 0x4,
F_CHUNKED = 0x8,
F_UPGRADE = 0x10,
F_CONTENT_LENGTH = 0x20,
F_SKIPBODY = 0x40,
F_TRAILING = 0x80,
F_TRANSFER_ENCODING = 0x200
};
typedef enum llhttp_flags llhttp_flags_t;
enum llhttp_lenient_flags {
LENIENT_HEADERS = 0x1,
LENIENT_CHUNKED_LENGTH = 0x2,
LENIENT_KEEP_ALIVE = 0x4,
LENIENT_TRANSFER_ENCODING = 0x8,
LENIENT_VERSION = 0x10,
LENIENT_DATA_AFTER_CLOSE = 0x20,
LENIENT_OPTIONAL_LF_AFTER_CR = 0x40,
LENIENT_OPTIONAL_CRLF_AFTER_CHUNK = 0x80,
LENIENT_OPTIONAL_CR_BEFORE_LF = 0x100,
LENIENT_SPACES_AFTER_CHUNK_SIZE = 0x200
};
typedef enum llhttp_lenient_flags llhttp_lenient_flags_t;
enum llhttp_type {
HTTP_BOTH = 0,
HTTP_REQUEST = 1,
HTTP_RESPONSE = 2
};
typedef enum llhttp_type llhttp_type_t;
enum llhttp_finish {
HTTP_FINISH_SAFE = 0,
HTTP_FINISH_SAFE_WITH_CB = 1,
HTTP_FINISH_UNSAFE = 2
};
typedef enum llhttp_finish llhttp_finish_t;
enum llhttp_method {
HTTP_DELETE = 0,
HTTP_GET = 1,
HTTP_HEAD = 2,
HTTP_POST = 3,
HTTP_PUT = 4,
HTTP_CONNECT = 5,
HTTP_OPTIONS = 6,
HTTP_TRACE = 7,
HTTP_COPY = 8,
HTTP_LOCK = 9,
HTTP_MKCOL = 10,
HTTP_MOVE = 11,
HTTP_PROPFIND = 12,
HTTP_PROPPATCH = 13,
HTTP_SEARCH = 14,
HTTP_UNLOCK = 15,
HTTP_BIND = 16,
HTTP_REBIND = 17,
HTTP_UNBIND = 18,
HTTP_ACL = 19,
HTTP_REPORT = 20,
HTTP_MKACTIVITY = 21,
HTTP_CHECKOUT = 22,
HTTP_MERGE = 23,
HTTP_MSEARCH = 24,
HTTP_NOTIFY = 25,
HTTP_SUBSCRIBE = 26,
HTTP_UNSUBSCRIBE = 27,
HTTP_PATCH = 28,
HTTP_PURGE = 29,
HTTP_MKCALENDAR = 30,
HTTP_LINK = 31,
HTTP_UNLINK = 32,
HTTP_SOURCE = 33,
HTTP_PRI = 34,
HTTP_DESCRIBE = 35,
HTTP_ANNOUNCE = 36,
HTTP_SETUP = 37,
HTTP_PLAY = 38,
HTTP_PAUSE = 39,
HTTP_TEARDOWN = 40,
HTTP_GET_PARAMETER = 41,
HTTP_SET_PARAMETER = 42,
HTTP_REDIRECT = 43,
HTTP_RECORD = 44,
HTTP_FLUSH = 45,
HTTP_QUERY = 46
};
typedef enum llhttp_method llhttp_method_t;
enum llhttp_status {
HTTP_STATUS_CONTINUE = 100,
HTTP_STATUS_SWITCHING_PROTOCOLS = 101,
HTTP_STATUS_PROCESSING = 102,
HTTP_STATUS_EARLY_HINTS = 103,
HTTP_STATUS_RESPONSE_IS_STALE = 110,
HTTP_STATUS_REVALIDATION_FAILED = 111,
HTTP_STATUS_DISCONNECTED_OPERATION = 112,
HTTP_STATUS_HEURISTIC_EXPIRATION = 113,
HTTP_STATUS_MISCELLANEOUS_WARNING = 199,
HTTP_STATUS_OK = 200,
HTTP_STATUS_CREATED = 201,
HTTP_STATUS_ACCEPTED = 202,
HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203,
HTTP_STATUS_NO_CONTENT = 204,
HTTP_STATUS_RESET_CONTENT = 205,
HTTP_STATUS_PARTIAL_CONTENT = 206,
HTTP_STATUS_MULTI_STATUS = 207,
HTTP_STATUS_ALREADY_REPORTED = 208,
HTTP_STATUS_TRANSFORMATION_APPLIED = 214,
HTTP_STATUS_IM_USED = 226,
HTTP_STATUS_MISCELLANEOUS_PERSISTENT_WARNING = 299,
HTTP_STATUS_MULTIPLE_CHOICES = 300,
HTTP_STATUS_MOVED_PERMANENTLY = 301,
HTTP_STATUS_FOUND = 302,
HTTP_STATUS_SEE_OTHER = 303,
HTTP_STATUS_NOT_MODIFIED = 304,
HTTP_STATUS_USE_PROXY = 305,
HTTP_STATUS_SWITCH_PROXY = 306,
HTTP_STATUS_TEMPORARY_REDIRECT = 307,
HTTP_STATUS_PERMANENT_REDIRECT = 308,
HTTP_STATUS_BAD_REQUEST = 400,
HTTP_STATUS_UNAUTHORIZED = 401,
HTTP_STATUS_PAYMENT_REQUIRED = 402,
HTTP_STATUS_FORBIDDEN = 403,
HTTP_STATUS_NOT_FOUND = 404,
HTTP_STATUS_METHOD_NOT_ALLOWED = 405,
HTTP_STATUS_NOT_ACCEPTABLE = 406,
HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED = 407,
HTTP_STATUS_REQUEST_TIMEOUT = 408,
HTTP_STATUS_CONFLICT = 409,
HTTP_STATUS_GONE = 410,
HTTP_STATUS_LENGTH_REQUIRED = 411,
HTTP_STATUS_PRECONDITION_FAILED = 412,
HTTP_STATUS_PAYLOAD_TOO_LARGE = 413,
HTTP_STATUS_URI_TOO_LONG = 414,
HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415,
HTTP_STATUS_RANGE_NOT_SATISFIABLE = 416,
HTTP_STATUS_EXPECTATION_FAILED = 417,
HTTP_STATUS_IM_A_TEAPOT = 418,
HTTP_STATUS_PAGE_EXPIRED = 419,
HTTP_STATUS_ENHANCE_YOUR_CALM = 420,
HTTP_STATUS_MISDIRECTED_REQUEST = 421,
HTTP_STATUS_UNPROCESSABLE_ENTITY = 422,
HTTP_STATUS_LOCKED = 423,
HTTP_STATUS_FAILED_DEPENDENCY = 424,
HTTP_STATUS_TOO_EARLY = 425,
HTTP_STATUS_UPGRADE_REQUIRED = 426,
HTTP_STATUS_PRECONDITION_REQUIRED = 428,
HTTP_STATUS_TOO_MANY_REQUESTS = 429,
HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL = 430,
HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
HTTP_STATUS_LOGIN_TIMEOUT = 440,
HTTP_STATUS_NO_RESPONSE = 444,
HTTP_STATUS_RETRY_WITH = 449,
HTTP_STATUS_BLOCKED_BY_PARENTAL_CONTROL = 450,
HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451,
HTTP_STATUS_CLIENT_CLOSED_LOAD_BALANCED_REQUEST = 460,
HTTP_STATUS_INVALID_X_FORWARDED_FOR = 463,
HTTP_STATUS_REQUEST_HEADER_TOO_LARGE = 494,
HTTP_STATUS_SSL_CERTIFICATE_ERROR = 495,
HTTP_STATUS_SSL_CERTIFICATE_REQUIRED = 496,
HTTP_STATUS_HTTP_REQUEST_SENT_TO_HTTPS_PORT = 497,
HTTP_STATUS_INVALID_TOKEN = 498,
HTTP_STATUS_CLIENT_CLOSED_REQUEST = 499,
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500,
HTTP_STATUS_NOT_IMPLEMENTED = 501,
HTTP_STATUS_BAD_GATEWAY = 502,
HTTP_STATUS_SERVICE_UNAVAILABLE = 503,
HTTP_STATUS_GATEWAY_TIMEOUT = 504,
HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED = 505,
HTTP_STATUS_VARIANT_ALSO_NEGOTIATES = 506,
HTTP_STATUS_INSUFFICIENT_STORAGE = 507,
HTTP_STATUS_LOOP_DETECTED = 508,
HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED = 509,
HTTP_STATUS_NOT_EXTENDED = 510,
HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511,
HTTP_STATUS_WEB_SERVER_UNKNOWN_ERROR = 520,
HTTP_STATUS_WEB_SERVER_IS_DOWN = 521,
HTTP_STATUS_CONNECTION_TIMEOUT = 522,
HTTP_STATUS_ORIGIN_IS_UNREACHABLE = 523,
HTTP_STATUS_TIMEOUT_OCCURED = 524,
HTTP_STATUS_SSL_HANDSHAKE_FAILED = 525,
HTTP_STATUS_INVALID_SSL_CERTIFICATE = 526,
HTTP_STATUS_RAILGUN_ERROR = 527,
HTTP_STATUS_SITE_IS_OVERLOADED = 529,
HTTP_STATUS_SITE_IS_FROZEN = 530,
HTTP_STATUS_IDENTITY_PROVIDER_AUTHENTICATION_ERROR = 561,
HTTP_STATUS_NETWORK_READ_TIMEOUT = 598,
HTTP_STATUS_NETWORK_CONNECT_TIMEOUT = 599
};
typedef enum llhttp_status llhttp_status_t;
#define HTTP_ERRNO_MAP(XX) \
XX(0, OK, OK) \
XX(1, INTERNAL, INTERNAL) \
XX(2, STRICT, STRICT) \
XX(25, CR_EXPECTED, CR_EXPECTED) \
XX(3, LF_EXPECTED, LF_EXPECTED) \
XX(4, UNEXPECTED_CONTENT_LENGTH, UNEXPECTED_CONTENT_LENGTH) \
XX(30, UNEXPECTED_SPACE, UNEXPECTED_SPACE) \
XX(5, CLOSED_CONNECTION, CLOSED_CONNECTION) \
XX(6, INVALID_METHOD, INVALID_METHOD) \
XX(7, INVALID_URL, INVALID_URL) \
XX(8, INVALID_CONSTANT, INVALID_CONSTANT) \
XX(9, INVALID_VERSION, INVALID_VERSION) \
XX(10, INVALID_HEADER_TOKEN, INVALID_HEADER_TOKEN) \
XX(11, INVALID_CONTENT_LENGTH, INVALID_CONTENT_LENGTH) \
XX(12, INVALID_CHUNK_SIZE, INVALID_CHUNK_SIZE) \
XX(13, INVALID_STATUS, INVALID_STATUS) \
XX(14, INVALID_EOF_STATE, INVALID_EOF_STATE) \
XX(15, INVALID_TRANSFER_ENCODING, INVALID_TRANSFER_ENCODING) \
XX(16, CB_MESSAGE_BEGIN, CB_MESSAGE_BEGIN) \
XX(17, CB_HEADERS_COMPLETE, CB_HEADERS_COMPLETE) \
XX(18, CB_MESSAGE_COMPLETE, CB_MESSAGE_COMPLETE) \
XX(19, CB_CHUNK_HEADER, CB_CHUNK_HEADER) \
XX(20, CB_CHUNK_COMPLETE, CB_CHUNK_COMPLETE) \
XX(21, PAUSED, PAUSED) \
XX(22, PAUSED_UPGRADE, PAUSED_UPGRADE) \
XX(23, PAUSED_H2_UPGRADE, PAUSED_H2_UPGRADE) \
XX(24, USER, USER) \
XX(26, CB_URL_COMPLETE, CB_URL_COMPLETE) \
XX(27, CB_STATUS_COMPLETE, CB_STATUS_COMPLETE) \
XX(32, CB_METHOD_COMPLETE, CB_METHOD_COMPLETE) \
XX(33, CB_VERSION_COMPLETE, CB_VERSION_COMPLETE) \
XX(28, CB_HEADER_FIELD_COMPLETE, CB_HEADER_FIELD_COMPLETE) \
XX(29, CB_HEADER_VALUE_COMPLETE, CB_HEADER_VALUE_COMPLETE) \
XX(34, CB_CHUNK_EXTENSION_NAME_COMPLETE, CB_CHUNK_EXTENSION_NAME_COMPLETE) \
XX(35, CB_CHUNK_EXTENSION_VALUE_COMPLETE, CB_CHUNK_EXTENSION_VALUE_COMPLETE) \
XX(31, CB_RESET, CB_RESET) \
#define HTTP_METHOD_MAP(XX) \
XX(0, DELETE, DELETE) \
XX(1, GET, GET) \
XX(2, HEAD, HEAD) \
XX(3, POST, POST) \
XX(4, PUT, PUT) \
XX(5, CONNECT, CONNECT) \
XX(6, OPTIONS, OPTIONS) \
XX(7, TRACE, TRACE) \
XX(8, COPY, COPY) \
XX(9, LOCK, LOCK) \
XX(10, MKCOL, MKCOL) \
XX(11, MOVE, MOVE) \
XX(12, PROPFIND, PROPFIND) \
XX(13, PROPPATCH, PROPPATCH) \
XX(14, SEARCH, SEARCH) \
XX(15, UNLOCK, UNLOCK) \
XX(16, BIND, BIND) \
XX(17, REBIND, REBIND) \
XX(18, UNBIND, UNBIND) \
XX(19, ACL, ACL) \
XX(20, REPORT, REPORT) \
XX(21, MKACTIVITY, MKACTIVITY) \
XX(22, CHECKOUT, CHECKOUT) \
XX(23, MERGE, MERGE) \
XX(24, MSEARCH, M-SEARCH) \
XX(25, NOTIFY, NOTIFY) \
XX(26, SUBSCRIBE, SUBSCRIBE) \
XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
XX(28, PATCH, PATCH) \
XX(29, PURGE, PURGE) \
XX(30, MKCALENDAR, MKCALENDAR) \
XX(31, LINK, LINK) \
XX(32, UNLINK, UNLINK) \
XX(33, SOURCE, SOURCE) \
XX(46, QUERY, QUERY) \
#define RTSP_METHOD_MAP(XX) \
XX(1, GET, GET) \
XX(3, POST, POST) \
XX(6, OPTIONS, OPTIONS) \
XX(35, DESCRIBE, DESCRIBE) \
XX(36, ANNOUNCE, ANNOUNCE) \
XX(37, SETUP, SETUP) \
XX(38, PLAY, PLAY) \
XX(39, PAUSE, PAUSE) \
XX(40, TEARDOWN, TEARDOWN) \
XX(41, GET_PARAMETER, GET_PARAMETER) \
XX(42, SET_PARAMETER, SET_PARAMETER) \
XX(43, REDIRECT, REDIRECT) \
XX(44, RECORD, RECORD) \
XX(45, FLUSH, FLUSH) \
#define HTTP_ALL_METHOD_MAP(XX) \
XX(0, DELETE, DELETE) \
XX(1, GET, GET) \
XX(2, HEAD, HEAD) \
XX(3, POST, POST) \
XX(4, PUT, PUT) \
XX(5, CONNECT, CONNECT) \
XX(6, OPTIONS, OPTIONS) \
XX(7, TRACE, TRACE) \
XX(8, COPY, COPY) \
XX(9, LOCK, LOCK) \
XX(10, MKCOL, MKCOL) \
XX(11, MOVE, MOVE) \
XX(12, PROPFIND, PROPFIND) \
XX(13, PROPPATCH, PROPPATCH) \
XX(14, SEARCH, SEARCH) \
XX(15, UNLOCK, UNLOCK) \
XX(16, BIND, BIND) \
XX(17, REBIND, REBIND) \
XX(18, UNBIND, UNBIND) \
XX(19, ACL, ACL) \
XX(20, REPORT, REPORT) \
XX(21, MKACTIVITY, MKACTIVITY) \
XX(22, CHECKOUT, CHECKOUT) \
XX(23, MERGE, MERGE) \
XX(24, MSEARCH, M-SEARCH) \
XX(25, NOTIFY, NOTIFY) \
XX(26, SUBSCRIBE, SUBSCRIBE) \
XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
XX(28, PATCH, PATCH) \
XX(29, PURGE, PURGE) \
XX(30, MKCALENDAR, MKCALENDAR) \
XX(31, LINK, LINK) \
XX(32, UNLINK, UNLINK) \
XX(33, SOURCE, SOURCE) \
XX(34, PRI, PRI) \
XX(35, DESCRIBE, DESCRIBE) \
XX(36, ANNOUNCE, ANNOUNCE) \
XX(37, SETUP, SETUP) \
XX(38, PLAY, PLAY) \
XX(39, PAUSE, PAUSE) \
XX(40, TEARDOWN, TEARDOWN) \
XX(41, GET_PARAMETER, GET_PARAMETER) \
XX(42, SET_PARAMETER, SET_PARAMETER) \
XX(43, REDIRECT, REDIRECT) \
XX(44, RECORD, RECORD) \
XX(45, FLUSH, FLUSH) \
XX(46, QUERY, QUERY) \
#define HTTP_STATUS_MAP(XX) \
XX(100, CONTINUE, CONTINUE) \
XX(101, SWITCHING_PROTOCOLS, SWITCHING_PROTOCOLS) \
XX(102, PROCESSING, PROCESSING) \
XX(103, EARLY_HINTS, EARLY_HINTS) \
XX(110, RESPONSE_IS_STALE, RESPONSE_IS_STALE) \
XX(111, REVALIDATION_FAILED, REVALIDATION_FAILED) \
XX(112, DISCONNECTED_OPERATION, DISCONNECTED_OPERATION) \
XX(113, HEURISTIC_EXPIRATION, HEURISTIC_EXPIRATION) \
XX(199, MISCELLANEOUS_WARNING, MISCELLANEOUS_WARNING) \
XX(200, OK, OK) \
XX(201, CREATED, CREATED) \
XX(202, ACCEPTED, ACCEPTED) \
XX(203, NON_AUTHORITATIVE_INFORMATION, NON_AUTHORITATIVE_INFORMATION) \
XX(204, NO_CONTENT, NO_CONTENT) \
XX(205, RESET_CONTENT, RESET_CONTENT) \
XX(206, PARTIAL_CONTENT, PARTIAL_CONTENT) \
XX(207, MULTI_STATUS, MULTI_STATUS) \
XX(208, ALREADY_REPORTED, ALREADY_REPORTED) \
XX(214, TRANSFORMATION_APPLIED, TRANSFORMATION_APPLIED) \
XX(226, IM_USED, IM_USED) \
XX(299, MISCELLANEOUS_PERSISTENT_WARNING, MISCELLANEOUS_PERSISTENT_WARNING) \
XX(300, MULTIPLE_CHOICES, MULTIPLE_CHOICES) \
XX(301, MOVED_PERMANENTLY, MOVED_PERMANENTLY) \
XX(302, FOUND, FOUND) \
XX(303, SEE_OTHER, SEE_OTHER) \
XX(304, NOT_MODIFIED, NOT_MODIFIED) \
XX(305, USE_PROXY, USE_PROXY) \
XX(306, SWITCH_PROXY, SWITCH_PROXY) \
XX(307, TEMPORARY_REDIRECT, TEMPORARY_REDIRECT) \
XX(308, PERMANENT_REDIRECT, PERMANENT_REDIRECT) \
XX(400, BAD_REQUEST, BAD_REQUEST) \
XX(401, UNAUTHORIZED, UNAUTHORIZED) \
XX(402, PAYMENT_REQUIRED, PAYMENT_REQUIRED) \
XX(403, FORBIDDEN, FORBIDDEN) \
XX(404, NOT_FOUND, NOT_FOUND) \
XX(405, METHOD_NOT_ALLOWED, METHOD_NOT_ALLOWED) \
XX(406, NOT_ACCEPTABLE, NOT_ACCEPTABLE) \
XX(407, PROXY_AUTHENTICATION_REQUIRED, PROXY_AUTHENTICATION_REQUIRED) \
XX(408, REQUEST_TIMEOUT, REQUEST_TIMEOUT) \
XX(409, CONFLICT, CONFLICT) \
XX(410, GONE, GONE) \
XX(411, LENGTH_REQUIRED, LENGTH_REQUIRED) \
XX(412, PRECONDITION_FAILED, PRECONDITION_FAILED) \
XX(413, PAYLOAD_TOO_LARGE, PAYLOAD_TOO_LARGE) \
XX(414, URI_TOO_LONG, URI_TOO_LONG) \
XX(415, UNSUPPORTED_MEDIA_TYPE, UNSUPPORTED_MEDIA_TYPE) \
XX(416, RANGE_NOT_SATISFIABLE, RANGE_NOT_SATISFIABLE) \
XX(417, EXPECTATION_FAILED, EXPECTATION_FAILED) \
XX(418, IM_A_TEAPOT, IM_A_TEAPOT) \
XX(419, PAGE_EXPIRED, PAGE_EXPIRED) \
XX(420, ENHANCE_YOUR_CALM, ENHANCE_YOUR_CALM) \
XX(421, MISDIRECTED_REQUEST, MISDIRECTED_REQUEST) \
XX(422, UNPROCESSABLE_ENTITY, UNPROCESSABLE_ENTITY) \
XX(423, LOCKED, LOCKED) \
XX(424, FAILED_DEPENDENCY, FAILED_DEPENDENCY) \
XX(425, TOO_EARLY, TOO_EARLY) \
XX(426, UPGRADE_REQUIRED, UPGRADE_REQUIRED) \
XX(428, PRECONDITION_REQUIRED, PRECONDITION_REQUIRED) \
XX(429, TOO_MANY_REQUESTS, TOO_MANY_REQUESTS) \
XX(430, REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL, REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL) \
XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, REQUEST_HEADER_FIELDS_TOO_LARGE) \
XX(440, LOGIN_TIMEOUT, LOGIN_TIMEOUT) \
XX(444, NO_RESPONSE, NO_RESPONSE) \
XX(449, RETRY_WITH, RETRY_WITH) \
XX(450, BLOCKED_BY_PARENTAL_CONTROL, BLOCKED_BY_PARENTAL_CONTROL) \
XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, UNAVAILABLE_FOR_LEGAL_REASONS) \
XX(460, CLIENT_CLOSED_LOAD_BALANCED_REQUEST, CLIENT_CLOSED_LOAD_BALANCED_REQUEST) \
XX(463, INVALID_X_FORWARDED_FOR, INVALID_X_FORWARDED_FOR) \
XX(494, REQUEST_HEADER_TOO_LARGE, REQUEST_HEADER_TOO_LARGE) \
XX(495, SSL_CERTIFICATE_ERROR, SSL_CERTIFICATE_ERROR) \
XX(496, SSL_CERTIFICATE_REQUIRED, SSL_CERTIFICATE_REQUIRED) \
XX(497, HTTP_REQUEST_SENT_TO_HTTPS_PORT, HTTP_REQUEST_SENT_TO_HTTPS_PORT) \
XX(498, INVALID_TOKEN, INVALID_TOKEN) \
XX(499, CLIENT_CLOSED_REQUEST, CLIENT_CLOSED_REQUEST) \
XX(500, INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR) \
XX(501, NOT_IMPLEMENTED, NOT_IMPLEMENTED) \
XX(502, BAD_GATEWAY, BAD_GATEWAY) \
XX(503, SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE) \
XX(504, GATEWAY_TIMEOUT, GATEWAY_TIMEOUT) \
XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP_VERSION_NOT_SUPPORTED) \
XX(506, VARIANT_ALSO_NEGOTIATES, VARIANT_ALSO_NEGOTIATES) \
XX(507, INSUFFICIENT_STORAGE, INSUFFICIENT_STORAGE) \
XX(508, LOOP_DETECTED, LOOP_DETECTED) \
XX(509, BANDWIDTH_LIMIT_EXCEEDED, BANDWIDTH_LIMIT_EXCEEDED) \
XX(510, NOT_EXTENDED, NOT_EXTENDED) \
XX(511, NETWORK_AUTHENTICATION_REQUIRED, NETWORK_AUTHENTICATION_REQUIRED) \
XX(520, WEB_SERVER_UNKNOWN_ERROR, WEB_SERVER_UNKNOWN_ERROR) \
XX(521, WEB_SERVER_IS_DOWN, WEB_SERVER_IS_DOWN) \
XX(522, CONNECTION_TIMEOUT, CONNECTION_TIMEOUT) \
XX(523, ORIGIN_IS_UNREACHABLE, ORIGIN_IS_UNREACHABLE) \
XX(524, TIMEOUT_OCCURED, TIMEOUT_OCCURED) \
XX(525, SSL_HANDSHAKE_FAILED, SSL_HANDSHAKE_FAILED) \
XX(526, INVALID_SSL_CERTIFICATE, INVALID_SSL_CERTIFICATE) \
XX(527, RAILGUN_ERROR, RAILGUN_ERROR) \
XX(529, SITE_IS_OVERLOADED, SITE_IS_OVERLOADED) \
XX(530, SITE_IS_FROZEN, SITE_IS_FROZEN) \
XX(561, IDENTITY_PROVIDER_AUTHENTICATION_ERROR, IDENTITY_PROVIDER_AUTHENTICATION_ERROR) \
XX(598, NETWORK_READ_TIMEOUT, NETWORK_READ_TIMEOUT) \
XX(599, NETWORK_CONNECT_TIMEOUT, NETWORK_CONNECT_TIMEOUT) \
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* LLLLHTTP_C_HEADERS_ */
#ifndef INCLUDE_LLHTTP_API_H_
#define INCLUDE_LLHTTP_API_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#if defined(__wasm__)
#define LLHTTP_EXPORT __attribute__((visibility("default")))
#elif defined(_WIN32)
#define LLHTTP_EXPORT __declspec(dllexport)
#else
#define LLHTTP_EXPORT
#endif
typedef llhttp__internal_t llhttp_t;
typedef struct llhttp_settings_s llhttp_settings_t;
typedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length);
typedef int (*llhttp_cb)(llhttp_t*);
struct llhttp_settings_s {
/* Possible return values 0, -1, `HPE_PAUSED` */
llhttp_cb on_message_begin;
/* Possible return values 0, -1, HPE_USER */
llhttp_data_cb on_url;
llhttp_data_cb on_status;
llhttp_data_cb on_method;
llhttp_data_cb on_version;
llhttp_data_cb on_header_field;
llhttp_data_cb on_header_value;
llhttp_data_cb on_chunk_extension_name;
llhttp_data_cb on_chunk_extension_value;
/* Possible return values:
* 0 - Proceed normally
* 1 - Assume that request/response has no body, and proceed to parsing the
* next message
* 2 - Assume absence of body (as above) and make `llhttp_execute()` return
* `HPE_PAUSED_UPGRADE`
* -1 - Error
* `HPE_PAUSED`
*/
llhttp_cb on_headers_complete;
/* Possible return values 0, -1, HPE_USER */
llhttp_data_cb on_body;
/* Possible return values 0, -1, `HPE_PAUSED` */
llhttp_cb on_message_complete;
llhttp_cb on_url_complete;
llhttp_cb on_status_complete;
llhttp_cb on_method_complete;
llhttp_cb on_version_complete;
llhttp_cb on_header_field_complete;
llhttp_cb on_header_value_complete;
llhttp_cb on_chunk_extension_name_complete;
llhttp_cb on_chunk_extension_value_complete;
/* When on_chunk_header is called, the current chunk length is stored
* in parser->content_length.
* Possible return values 0, -1, `HPE_PAUSED`
*/
llhttp_cb on_chunk_header;
llhttp_cb on_chunk_complete;
llhttp_cb on_reset;
};
/* Initialize the parser with specific type and user settings.
*
* NOTE: lifetime of `settings` has to be at least the same as the lifetime of
* the `parser` here. In practice, `settings` has to be either a static
* variable or be allocated with `malloc`, `new`, etc.
*/
LLHTTP_EXPORT
void llhttp_init(llhttp_t* parser, llhttp_type_t type,
const llhttp_settings_t* settings);
LLHTTP_EXPORT
llhttp_t* llhttp_alloc(llhttp_type_t type);
LLHTTP_EXPORT
void llhttp_free(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_type(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_http_major(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_http_minor(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_method(llhttp_t* parser);
LLHTTP_EXPORT
int llhttp_get_status_code(llhttp_t* parser);
LLHTTP_EXPORT
uint8_t llhttp_get_upgrade(llhttp_t* parser);
/* Reset an already initialized parser back to the start state, preserving the
* existing parser type, callback settings, user data, and lenient flags.
*/
LLHTTP_EXPORT
void llhttp_reset(llhttp_t* parser);
/* Initialize the settings object */
LLHTTP_EXPORT
void llhttp_settings_init(llhttp_settings_t* settings);
/* Parse full or partial request/response, invoking user callbacks along the
* way.
*
* If any of `llhttp_data_cb` returns errno not equal to `HPE_OK` - the parsing
* interrupts, and such errno is returned from `llhttp_execute()`. If
* `HPE_PAUSED` was used as a errno, the execution can be resumed with
* `llhttp_resume()` call.
*
* In a special case of CONNECT/Upgrade request/response `HPE_PAUSED_UPGRADE`
* is returned after fully parsing the request/response. If the user wishes to
* continue parsing, they need to invoke `llhttp_resume_after_upgrade()`.
*
* NOTE: if this function ever returns a non-pause type error, it will continue
* to return the same error upon each successive call up until `llhttp_init()`
* is called.
*/
LLHTTP_EXPORT
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len);
/* This method should be called when the other side has no further bytes to
* send (e.g. shutdown of readable side of the TCP connection.)
*
* Requests without `Content-Length` and other messages might require treating
* all incoming bytes as the part of the body, up to the last byte of the
* connection. This method will invoke `on_message_complete()` callback if the
* request was terminated safely. Otherwise a error code would be returned.
*/
LLHTTP_EXPORT
llhttp_errno_t llhttp_finish(llhttp_t* parser);
/* Returns `1` if the incoming message is parsed until the last byte, and has
* to be completed by calling `llhttp_finish()` on EOF
*/
LLHTTP_EXPORT
int llhttp_message_needs_eof(const llhttp_t* parser);
/* Returns `1` if there might be any other messages following the last that was
* successfully parsed.
*/
LLHTTP_EXPORT
int llhttp_should_keep_alive(const llhttp_t* parser);
/* Make further calls of `llhttp_execute()` return `HPE_PAUSED` and set
* appropriate error reason.
*
* Important: do not call this from user callbacks! User callbacks must return
* `HPE_PAUSED` if pausing is required.
*/
LLHTTP_EXPORT
void llhttp_pause(llhttp_t* parser);
/* Might be called to resume the execution after the pause in user's callback.
* See `llhttp_execute()` above for details.
*
* Call this only if `llhttp_execute()` returns `HPE_PAUSED`.
*/
LLHTTP_EXPORT
void llhttp_resume(llhttp_t* parser);
/* Might be called to resume the execution after the pause in user's callback.
* See `llhttp_execute()` above for details.
*
* Call this only if `llhttp_execute()` returns `HPE_PAUSED_UPGRADE`
*/
LLHTTP_EXPORT
void llhttp_resume_after_upgrade(llhttp_t* parser);
/* Returns the latest return error */
LLHTTP_EXPORT
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser);
/* Returns the verbal explanation of the latest returned error.
*
* Note: User callback should set error reason when returning the error. See
* `llhttp_set_error_reason()` for details.
*/
LLHTTP_EXPORT
const char* llhttp_get_error_reason(const llhttp_t* parser);
/* Assign verbal description to the returned error. Must be called in user
* callbacks right before returning the errno.
*
* Note: `HPE_USER` error code might be useful in user callbacks.
*/
LLHTTP_EXPORT
void llhttp_set_error_reason(llhttp_t* parser, const char* reason);
/* Returns the pointer to the last parsed byte before the returned error. The
* pointer is relative to the `data` argument of `llhttp_execute()`.
*
* Note: this method might be useful for counting the number of parsed bytes.
*/
LLHTTP_EXPORT
const char* llhttp_get_error_pos(const llhttp_t* parser);
/* Returns textual name of error code */
LLHTTP_EXPORT
const char* llhttp_errno_name(llhttp_errno_t err);
/* Returns textual name of HTTP method */
LLHTTP_EXPORT
const char* llhttp_method_name(llhttp_method_t method);
/* Returns textual name of HTTP status */
LLHTTP_EXPORT
const char* llhttp_status_name(llhttp_status_t status);
/* Enables/disables lenient header value parsing (disabled by default).
*
* Lenient parsing disables header value token checks, extending llhttp's
* protocol support to highly non-compliant clients/server. No
* `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when
* lenient parsing is "on".
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_headers(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of conflicting `Transfer-Encoding` and
* `Content-Length` headers (disabled by default).
*
* Normally `llhttp` would error when `Transfer-Encoding` is present in
* conjunction with `Content-Length`. This error is important to prevent HTTP
* request smuggling, but may be less desirable for small number of cases
* involving legacy servers.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of `Connection: close` and HTTP/1.0
* requests responses.
*
* Normally `llhttp` would error on (in strict mode) or discard (in loose mode)
* the HTTP request/response after the request/response with `Connection: close`
* and `Content-Length`. This is important to prevent cache poisoning attacks,
* but might interact badly with outdated and insecure clients. With this flag
* the extra request/response will be parsed normally.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* poisoning attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of `Transfer-Encoding` header.
*
* Normally `llhttp` would error when a `Transfer-Encoding` has `chunked` value
* and another value after it (either in a single header or in multiple
* headers whose value are internally joined using `, `).
* This is mandated by the spec to reliably determine request body size and thus
* avoid request smuggling.
* With this flag the extra value will be parsed normally.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of HTTP version.
*
* Normally `llhttp` would error when the HTTP version in the request or status line
* is not `0.9`, `1.0`, `1.1` or `2.0`.
* With this flag the invalid value will be parsed normally.
*
* **Enabling this flag can pose a security issue since you will allow unsupported
* HTTP versions. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_version(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of additional data received after a message ends
* and keep-alive is disabled.
*
* Normally `llhttp` would error when additional unexpected data is received if the message
* contains the `Connection` header with `close` value.
* With this flag the extra data will discarded without throwing an error.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* poisoning attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of incomplete CRLF sequences.
*
* Normally `llhttp` would error when a CR is not followed by LF when terminating the
* request line, the status line, the headers or a chunk header.
* With this flag only a CR is required to terminate such sections.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled);
/*
* Enables/disables lenient handling of line separators.
*
* Normally `llhttp` would error when a LF is not preceded by CR when terminating the
* request line, the status line, the headers, a chunk header or a chunk data.
* With this flag only a LF is required to terminate such sections.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of chunks not separated via CRLF.
*
* Normally `llhttp` would error when after a chunk data a CRLF is missing before
* starting a new chunk.
* With this flag the new chunk can start immediately after the previous one.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled);
/* Enables/disables lenient handling of spaces after chunk size.
*
* Normally `llhttp` would error when after a chunk size is followed by one or more
* spaces are present instead of a CRLF or `;`.
* With this flag this check is disabled.
*
* **Enabling this flag can pose a security issue since you will be exposed to
* request smuggling attacks. USE WITH CAUTION!**
*/
LLHTTP_EXPORT
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* INCLUDE_LLHTTP_API_H_ */
#endif /* INCLUDE_LLHTTP_H_ */

24765
jtlsrv-cpp/vendor/nlohmann/json.hpp vendored Normal file

File diff suppressed because it is too large Load Diff