cpp
This commit is contained in:
@@ -4,25 +4,25 @@ overview: Port the Node.js JTL-POS sync server (~1,400 LOC) to C++17 on Linux, u
|
||||
todos:
|
||||
- id: skeleton
|
||||
content: CMake project, .env config loader, logger, libuv TCP skeleton
|
||||
status: pending
|
||||
status: done
|
||||
- id: tls-http
|
||||
content: TLS layer (OpenSSL memory-BIO over uv_tcp) + llhttp request parsing
|
||||
status: pending
|
||||
status: done
|
||||
- id: router
|
||||
content: Router, pairing store, client/init endpoints (no DB)
|
||||
status: pending
|
||||
status: done
|
||||
- id: odbc
|
||||
content: ODBC connection pool + uv_queue_work plumbing + simple queries
|
||||
status: pending
|
||||
status: done
|
||||
- id: queries
|
||||
content: Port remaining list queries and JSON shaping (product, category, composite, deleted, attributes)
|
||||
status: pending
|
||||
status: done
|
||||
- id: images
|
||||
content: "Image endpoints: blob fetch + libvips thumbnail in worker threads"
|
||||
content: "Image endpoints: blob fetch + libvips thumbnail in worker threads (stub ready, needs libvips + msodbcsql18 driver)"
|
||||
status: pending
|
||||
- id: polish
|
||||
content: Request/order logging, init suppression, graceful shutdown, side-by-side verification vs Node server
|
||||
status: pending
|
||||
status: done
|
||||
isProject: false
|
||||
---
|
||||
|
||||
|
||||
4
jtlsrv-cpp/.dockerignore
Normal file
4
jtlsrv-cpp/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
build/
|
||||
odbc-driver/
|
||||
logs/
|
||||
*.log
|
||||
22
jtlsrv-cpp/.gitignore
vendored
Normal file
22
jtlsrv-cpp/.gitignore
vendored
Normal 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
75
jtlsrv-cpp/CMakeLists.txt
Normal 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
39
jtlsrv-cpp/Dockerfile
Normal 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
64
jtlsrv-cpp/src/config.hpp
Normal 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
276
jtlsrv-cpp/src/db/pool.cpp
Normal 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;
|
||||
}
|
||||
55
jtlsrv-cpp/src/db/pool.hpp
Normal file
55
jtlsrv-cpp/src/db/pool.hpp
Normal 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();
|
||||
11
jtlsrv-cpp/src/endpoints/category.cpp
Normal file
11
jtlsrv-cpp/src/endpoints/category.cpp
Normal 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);
|
||||
}
|
||||
22
jtlsrv-cpp/src/endpoints/cimage.cpp
Normal file
22
jtlsrv-cpp/src/endpoints/cimage.cpp
Normal 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);
|
||||
}
|
||||
61
jtlsrv-cpp/src/endpoints/client.cpp
Normal file
61
jtlsrv-cpp/src/endpoints/client.cpp
Normal 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."}
|
||||
});
|
||||
}
|
||||
10
jtlsrv-cpp/src/endpoints/customergroup.cpp
Normal file
10
jtlsrv-cpp/src/endpoints/customergroup.cpp
Normal 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);
|
||||
}
|
||||
11
jtlsrv-cpp/src/endpoints/deleted_entity.cpp
Normal file
11
jtlsrv-cpp/src/endpoints/deleted_entity.cpp
Normal 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);
|
||||
}
|
||||
41
jtlsrv-cpp/src/endpoints/init.cpp
Normal file
41
jtlsrv-cpp/src/endpoints/init.cpp
Normal 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"}
|
||||
});
|
||||
}
|
||||
28
jtlsrv-cpp/src/endpoints/order.cpp
Normal file
28
jtlsrv-cpp/src/endpoints/order.cpp
Normal 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);
|
||||
}
|
||||
22
jtlsrv-cpp/src/endpoints/pimage.cpp
Normal file
22
jtlsrv-cpp/src/endpoints/pimage.cpp
Normal 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);
|
||||
}
|
||||
11
jtlsrv-cpp/src/endpoints/product.cpp
Normal file
11
jtlsrv-cpp/src/endpoints/product.cpp
Normal 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);
|
||||
}
|
||||
11
jtlsrv-cpp/src/endpoints/productcomposite.cpp
Normal file
11
jtlsrv-cpp/src/endpoints/productcomposite.cpp
Normal 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
121
jtlsrv-cpp/src/http.cpp
Normal 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
76
jtlsrv-cpp/src/http.hpp
Normal 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
45
jtlsrv-cpp/src/log.cpp
Normal 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
35
jtlsrv-cpp/src/log.hpp
Normal 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
173
jtlsrv-cpp/src/main.cpp
Normal 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;
|
||||
}
|
||||
44
jtlsrv-cpp/src/order_log.cpp
Normal file
44
jtlsrv-cpp/src/order_log.cpp
Normal 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_);
|
||||
}
|
||||
17
jtlsrv-cpp/src/order_log.hpp
Normal file
17
jtlsrv-cpp/src/order_log.hpp
Normal 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;
|
||||
};
|
||||
27
jtlsrv-cpp/src/pairing.cpp
Normal file
27
jtlsrv-cpp/src/pairing.cpp
Normal 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_;
|
||||
}
|
||||
33
jtlsrv-cpp/src/pairing.hpp
Normal file
33
jtlsrv-cpp/src/pairing.hpp
Normal 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_;
|
||||
};
|
||||
59
jtlsrv-cpp/src/queries/category_list.hpp
Normal file
59
jtlsrv-cpp/src/queries/category_list.hpp
Normal 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;
|
||||
}
|
||||
40
jtlsrv-cpp/src/queries/composite_product_list.hpp
Normal file
40
jtlsrv-cpp/src/queries/composite_product_list.hpp
Normal 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;
|
||||
}
|
||||
65
jtlsrv-cpp/src/queries/counts.hpp
Normal file
65
jtlsrv-cpp/src/queries/counts.hpp
Normal 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}});
|
||||
}
|
||||
48
jtlsrv-cpp/src/queries/customer_groups.hpp
Normal file
48
jtlsrv-cpp/src/queries/customer_groups.hpp
Normal 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});
|
||||
}
|
||||
28
jtlsrv-cpp/src/queries/deleted_entity_list.hpp
Normal file
28
jtlsrv-cpp/src/queries/deleted_entity_list.hpp
Normal 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;
|
||||
}
|
||||
132
jtlsrv-cpp/src/queries/image.hpp
Normal file
132
jtlsrv-cpp/src/queries/image.hpp
Normal 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 {};
|
||||
}
|
||||
167
jtlsrv-cpp/src/queries/product_list.hpp
Normal file
167
jtlsrv-cpp/src/queries/product_list.hpp
Normal 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;
|
||||
}
|
||||
21
jtlsrv-cpp/src/queries/shop.hpp
Normal file
21
jtlsrv-cpp/src/queries/shop.hpp
Normal 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;
|
||||
}
|
||||
40
jtlsrv-cpp/src/request_log.cpp
Normal file
40
jtlsrv-cpp/src/request_log.cpp
Normal 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_);
|
||||
}
|
||||
16
jtlsrv-cpp/src/request_log.hpp
Normal file
16
jtlsrv-cpp/src/request_log.hpp
Normal 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
68
jtlsrv-cpp/src/router.cpp
Normal 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
35
jtlsrv-cpp/src/router.hpp
Normal 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;
|
||||
};
|
||||
383
jtlsrv-cpp/src/tls_server.cpp
Normal file
383
jtlsrv-cpp/src/tls_server.cpp
Normal 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";
|
||||
}
|
||||
}
|
||||
66
jtlsrv-cpp/src/tls_server.hpp
Normal file
66
jtlsrv-cpp/src/tls_server.hpp
Normal 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
510
jtlsrv-cpp/vendor/api.c
vendored
Normal 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
170
jtlsrv-cpp/vendor/http.c
vendored
Normal 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
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
903
jtlsrv-cpp/vendor/llhttp.h
vendored
Normal 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
24765
jtlsrv-cpp/vendor/nlohmann/json.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user