MINOR: haterm: new "haterm" utility

haterm_init.c is added to implement haproxy_init_args() which overloads
the one defined by haproxy.c. This way, haterm program uses its own argv[]
parsing function. It generates its own configuration in memory that is
parsed during boot and executed by the common code.
This commit is contained in:
Frederic Lecaille 2026-02-11 15:11:27 +01:00 committed by Willy Tarreau
parent c9d47804d1
commit b007b7aa04
4 changed files with 527 additions and 2 deletions

View File

@ -956,6 +956,7 @@ endif # obsolete targets
endif # TARGET
OBJS =
HATERM_OBJS =
ifneq ($(EXTRA_OBJS),)
OBJS += $(EXTRA_OBJS)
@ -1009,6 +1010,8 @@ ifneq ($(TRACE),)
OBJS += src/calltrace.o
endif
HATERM_OBJS += $(OBJS) src/haterm_init.o
# Used only for forced dependency checking. May be cleared during development.
INCLUDES = $(wildcard include/*/*.h)
DEP = $(INCLUDES) .build_opts
@ -1056,6 +1059,9 @@ endif # non-empty target
haproxy: $(OPTIONS_OBJS) $(OBJS)
$(cmd_LD) $(ARCH_FLAGS) $(LDFLAGS) -o $@ $^ $(LDOPTS)
haterm: $(OPTIONS_OBJS) $(HATERM_OBJS)
$(cmd_LD) $(ARCH_FLAGS) $(LDFLAGS) -o $@ $^ $(LDOPTS)
objsize: haproxy
$(Q)objdump -t $^|grep ' g '|grep -F '.text'|awk '{print $$5 FS $$6}'|sort

135
doc/haterm.txt Normal file
View File

@ -0,0 +1,135 @@
------
HATerm
------
HAProxy's dummy HTTP
server for benchmarks
1. Background
-------------
HATerm is a dummy HTTP server that leverages the flexible and scalable
architecture of HAProxy to ease benchmarking of HTTP agents in all versions of
HTTP currently supported by HAProxy (HTTP/1, HTTP/2, HTTP/3), and both in clear
and TLS / QUIC. It follows the same principle as its ancestor HTTPTerm [1],
consisting in producing HTTP responses entirely configured by the request
parameters (size, response time, status etc). It also preserves the spirit
HTTPTerm which does not require any configuration beyond an optional listening
address and a port number, though it also supports advanced configurations with
the full spectrum of HAProxy features for specific testing. The goal remains
to make it almost as fast as the original HTTPTerm so that it can become a
de-facto replacement, with a compatible command line and request parameters
that will not change users' habits.
[1] https://github.com/wtarreau/httpterm
2. Compilation
--------------
HATerm may be compiled in the same way as HAProxy but with "haterm" as Makefile
target to provide on the "make" command line as follows:
$ make -j $(nproc) TARGET=linux-glibc haterm
HATerm supports HTTPS/SSL/TCP:
$ make TARGET=linux-glibc USE_OPENSSL=1
It also supports QUIC:
$ make -j $(nproc) TARGET=linux-glibc USE_OPENSSL=1 USE_QUIC=1 haterm
Technically speaking, it uses the regular HAProxy source and object code with a
different command line parser. As such, all build options supported by HAProxy
also apply to HATerm. See INSTALL for more details about how to compile them.
3. Execution
------------
HATerm is a very easy to use HTTP server with supports for all the HTTP
versions. It displays its usage when run without argument or wrong arguments:
$ ./haterm
Usage : haterm -L [<ip>]:<clear port>[:<TCP&QUIC SSL port>] [-L...]* [opts]
where <opts> may be any combination of:
-G <line> : multiple option; append <line> to the "global" section
-F <line> : multiple option; append <line> to the "frontend" section
-T <line> : multiple option; append <line> to the "traces" section
-C : dump the configuration and exit
-D : goes daemon
-v : shows version
-d : enable the traces for all http protocols
Arguments -G, -F, -T permit to append one or multiple lines at the end of their
respective sections. A tab character ('\t') is prepended at the beginning of
the argument, and a line feed ('\n') is appended at the end. It is also
possible to insert multiple lines at once using escape sequences '\n' and '\t'
inside the string argument.
As HAProxy, HATerm may listen on several TCP/UDP addresses which can be
provided by multiple "-L" options. To be functional, it needs at least one
correct "-L" option to be set.
Examples:
$ ./haterm -L 127.0.0.1:8888 # listen on 127.0.0.1:8888 TCP address
$ ./haterm -L 127.0.0.1:8888:8889 # listen on 127.0.0.1:8888 TCP address,
# 127.0.01:8889 SSL/TCP address,
# and 127.0.01:8889 QUIC/UDP address
$ ./haterm -L 127.0.0.1:8888:8889 -L [::1]:8888:8889
With USE_QUIC_OPENSSL_COMPAT support, the user must configure a global
section as for HAProxy. HATerm sets internally its configuration in.
memory as this is done by HAProxy from configuration files:
$ ./haterm -L 127.0.0.1:8888:8889
[NOTICE] (1371578) : haproxy version is 3.4-dev4-ba5eab-28
[NOTICE] (1371578) : path to executable is ./haterm
[ALERT] (1371578) : Binding [haterm cfgfile:12] for frontend
___haterm_frontend___: this SSL library does not
support the QUIC protocol. A limited compatibility
layer may be enabled using the "limited-quic" global
option if desired.
Such an alert may be fixed with "-G' option:
$ ./haterm -L 127.0.0.1:8888:8889 -G "limited-quic"
When the SSL support is not compiled in, the second port is ignored. This is
also the case for the QUIC support.
HATerm adjusts its responses depending on the requests it receives. An empty
query string provides the information about how the URIs are understood by
HATerm:
$ curl http://127.0.0.1:8888/?
HAProxy's dummy HTTP server for benchmarks - version 3.4-dev4.
All integer argument values are in the form [digits]*[kmgr] (r=random(0..1))
The following arguments are supported to override the default objects :
- /?s=<size> return <size> bytes.
E.g. /?s=20k
- /?r=<retcode> present <retcode> as the HTTP return code.
E.g. /?r=404
- /?c=<cache> set the return as not cacheable if <1.
E.g. /?c=0
- /?A=<req-after> drain the request body after sending the response.
E.g. /?A=1
- /?C=<close> force the response to use close if >0.
E.g. /?C=1
- /?K=<keep-alive> force the response to use keep-alive if >0.
E.g. /?K=1
- /?t=<time> wait <time> milliseconds before responding.
E.g. /?t=500
- /?k=<enable> Enable transfer encoding chunked with only one chunk
if >0.
- /?R=<enable> Enable sending random data if >0.
Note that those arguments may be cumulated on one line separated by a set of
delimitors among [&?,;/] :
- GET /?s=20k&c=1&t=700&K=30r HTTP/1.0
- GET /?r=500?s=0?c=0?t=1000 HTTP/1.0

View File

@ -906,9 +906,9 @@ static struct task *process_hstream(struct task *t, void *context, unsigned int
goto leave;
}
/* Allocate a httpter stream as this is done for classical haproxy streams.
/* Allocate an haterm stream as this is done for classical haproxy streams.
* This function is called as proxy callback from muxes.
* Return the haterm stream object if succeede, NUL if not.
* Return the haterm stream object on success, NULL if not.
*/
void *hstream_new(struct session *sess, struct stconn *sc, struct buffer *input)
{

384
src/haterm_init.c Normal file
View File

@ -0,0 +1,384 @@
#include <haproxy/api.h>
#include <haproxy/buf.h>
#include <haproxy/chunk.h>
#include <haproxy/errors.h>
#include <haproxy/global.h>
#include <haproxy/version.h>
static int haterm_debug;
/*
* This function prints the command line usage for haterm and exits
*/
static void haterm_usage(char *name)
{
fprintf(stderr,
"Usage : %s -L [<ip>]:<clear port>[:<TCP&QUIC SSL port>] [-L...]* [opts]\n"
"where <opts> may be any combination of:\n"
" -G <line> : multiple option; append <line> to the \"global\" section\n"
" -F <line> : multiple option; append <line> to the \"frontend\" section\n"
" -T <line> : multiple option; append <line> to the \"traces\" section\n"
" -C : dump the configuration and exit\n"
" -D : goes daemon\n"
" -v : shows version\n"
" -d : enable the traces for all http protocols\n", name);
exit(1);
}
#define HATERM_FRONTEND_NAME "___haterm_frontend___"
#define HATERM_RSA_CERT_NAME "haterm.pem.rsa"
#define HATERM_ECDSA_CERT_NAME "haterm.pem.ecdsa"
static const char *haterm_cfg_dflt_str =
"defaults\n"
"\tmode haterm\n"
"\ttimeout client 25s\n";
static const char *haterm_cfg_crt_store_str =
"crt-store\n"
"\tload generate-dummy on keytype RSA crt " HATERM_RSA_CERT_NAME "\n"
"\tload generate-dummy on keytype ECDSA crt " HATERM_ECDSA_CERT_NAME "\n";
static const char *haterm_cfg_traces_str =
"traces\n"
"\ttrace h1 sink stderr level user start now verbosity minimal\n"
"\ttrace h2 sink stderr level user start now verbosity minimal\n"
"\ttrace h3 sink stderr level user start now verbosity minimal\n"
"\ttrace qmux sink stderr level user start now verbosity minimal\n";
/* Very small API similar to buffer API to carefully build some strings */
#define HBUF_NULL ((struct hbuf) { })
#define HBUF_SIZE (16 << 10) /* bytes */
struct hbuf {
char *area;
size_t data;
size_t size;
};
static struct hbuf *hbuf_alloc(struct hbuf *h)
{
h->area = malloc(HBUF_SIZE);
if (!h->area)
return NULL;
h->size = HBUF_SIZE;
h->data = 0;
return h;
}
static inline void free_hbuf(struct hbuf *h)
{
free(h->area);
h->area = NULL;
}
__attribute__ ((format(printf, 2, 3)))
static void hbuf_appendf(struct hbuf *h, char *fmt, ...)
{
va_list argp;
size_t room;
int ret;
room = h->size - h->data;
if (!room)
return;
va_start(argp, fmt);
ret = vsnprintf(h->area + h->data, room, fmt, argp);
if (ret >= room)
h->area[h->data] = '\0';
else
h->data += ret;
va_end(argp);
}
static inline size_t hbuf_is_null(const struct hbuf *h)
{
return h->size == 0;
}
/* Simple function, to append <line> to <b> without without
* trailing '\0' character.
* Take into an account the '\t' and '\n' escaped sequeces.
*/
static void hstream_str_buf_append(struct hbuf *h, const char *line)
{
const char *p, *end;
char *to = h->area + h->data;
char *wrap = h->area + h->size;
int nl = 0; /* terminal '\n' */
p = line;
end = line + strlen(line);
/* prepend '\t' if missing */
if (strncmp(line, "\\t", 2) != 0 && to < wrap) {
*to++ = '\t';
h->data++;
}
while (p < end && to < wrap) {
if (*p == '\\') {
if (!*++p || p >= end)
break;
if (*p == 'n') {
*to++ = '\n';
if (p + 1 >= end)
nl = 1;
}
else if (*p == 't')
*to++ = '\t';
p++;
h->data++;
}
else {
*to++ = *p++;
h->data++;
}
}
/* add a terminal '\n' if not already present */
if (to < wrap && !nl) {
*to++ = '\n';
h->data++;
}
}
/* This function initialises the haterm HTTP benchmark server from
* <argv>. This consists in building a configuration file in memory
* using the haproxy configuration language.
* Make exit(1) the process in case of any failure.
*/
void haproxy_init_args(int argc, char **argv)
{
/* Initialize haterm fileless cfgfile from <argv> arguments array.
* Never fails.
*/
int has_bind = 0, err = 1, dump = 0, has_ssl = 0;
struct hbuf gbuf = HBUF_NULL; // "global" section
struct hbuf mbuf = HBUF_NULL; // to build the main of the cfgfile
struct hbuf fbuf = HBUF_NULL; // "frontend" section
struct hbuf tbuf = HBUF_NULL; // "traces" section
fileless_mode = 1;
if (argc <= 1)
haterm_usage(progname);
if (hbuf_alloc(&mbuf) == NULL) {
ha_alert("failed to alloce a buffer.\n");
exit(1);
}
/* skip program name and start */
argc--; argv++;
while (argc > 0) {
char *opt;
if (**argv == '-') {
opt = *argv + 1;
if (*opt == 'd') {
/* empty option */
if (*(opt + 1))
haterm_usage(progname);
/* debug mode */
haterm_debug = 1;
}
else if (*opt == 'C') {
/* empty option */
if (*(opt + 1))
haterm_usage(progname);
dump = 1;
}
else if (*opt == 'D') {
/* empty option */
if (*(opt + 1))
haterm_usage(progname);
global.mode |= MODE_DAEMON;
}
else if (*opt == 'v') {
/* empty option */
if (*(opt + 1))
haterm_usage(progname);
printf("HATerm version " HAPROXY_VERSION " released " HAPROXY_DATE "\n");
exit(0);
}
else if (*opt == 'F') {
argv++; argc--;
if (argc <= 0 || **argv == '-')
haterm_usage(progname);
if (hbuf_is_null(&fbuf)) {
if (hbuf_alloc(&fbuf) == NULL) {
ha_alert("failed to allocate a buffer.\n");
goto leave;
}
hbuf_appendf(&fbuf, "frontend " HATERM_FRONTEND_NAME "\n");
hbuf_appendf(&fbuf, "\toption accept-unsafe-violations-in-http-request\n");
}
hstream_str_buf_append(&fbuf, *argv);
}
else if (*opt == 'G') {
argv++; argc--;
if (argc <= 0 || **argv == '-')
haterm_usage(progname);
if (hbuf_is_null(&gbuf)) {
if (hbuf_alloc(&gbuf) == NULL) {
ha_alert("failed to allocate a buffer.\n");
goto leave;
}
hbuf_appendf(&gbuf, "global\n");
}
hstream_str_buf_append(&gbuf, *argv);
}
else if (*opt == 'T') {
argv++; argc--;
if (argc <= 0 || **argv == '-')
haterm_usage(progname);
if (hbuf_is_null(&tbuf) && hbuf_alloc(&tbuf) == NULL) {
ha_alert("failed to allocate a buffer.\n");
goto leave;
}
haterm_debug = 1;
hstream_str_buf_append(&tbuf, *argv);
}
else if (*opt == 'L') {
/* binding */
int __maybe_unused ipv6 = 0;
char *ip, *port, *port1 = NULL, *port2 = NULL;
argv++; argc--;
if (argc <= 0 || **argv == '-')
haterm_usage(progname);
port = ip = *argv;
if (*ip == '[') {
/* IPv6 address */
ip++;
port = strchr(port, ']');
if (!port)
haterm_usage(progname);
*port++ = '\0';
ipv6 = 1;
}
while ((port = strchr(port, ':'))) {
*port++ = '\0';
if (!port1)
port1 = port;
else {
if (port2)
haterm_usage(progname);
port2 = port;
}
}
if (!port1)
haterm_usage(progname);
if (hbuf_is_null(&fbuf)) {
if (hbuf_alloc(&fbuf) == NULL) {
ha_alert("failed to allocate a buffer.\n");
goto leave;
}
hbuf_appendf(&fbuf, "frontend " HATERM_FRONTEND_NAME "\n");
hbuf_appendf(&fbuf, "\toption accept-unsafe-violations-in-http-request\n");
}
/* clear HTTP */
hbuf_appendf(&fbuf, "\tbind %s:%s shards by-thread\n", ip, port1);
has_bind = 1;
if (port2) {
has_ssl = 1;
/* SSL/TCP binding */
hbuf_appendf(&fbuf, "\tbind %s:%s shards by-thread ssl "
"alpn h2,http1.1,http1.0"
" crt " HATERM_RSA_CERT_NAME
" crt " HATERM_ECDSA_CERT_NAME "\n",
ip, port2);
/* QUIC binding */
hbuf_appendf(&fbuf, "\tbind %s@%s:%s shards by-thread ssl"
" crt " HATERM_RSA_CERT_NAME
" crt " HATERM_ECDSA_CERT_NAME "\n",
ipv6 ? "quic6" : "quic4", ip, port2);
}
}
else
haterm_usage(progname);
}
else
haterm_usage(progname);
argv++; argc--;
}
if (!has_bind) {
ha_alert("No binding! Exiting...\n");
haterm_usage(progname);
}
/* "global" section */
if (!hbuf_is_null(&gbuf))
hbuf_appendf(&mbuf, "%.*s\n", (int)gbuf.data, gbuf.area);
/* "traces" section */
if (haterm_debug) {
hbuf_appendf(&mbuf, "%s", haterm_cfg_traces_str);
if (!hbuf_is_null(&tbuf))
hbuf_appendf(&mbuf, "%.*s\n", (int)tbuf.data, tbuf.area);
}
/* "defaults" section */
hbuf_appendf(&mbuf, "%s\n", haterm_cfg_dflt_str);
/* "crt-store" section */
if (has_ssl)
hbuf_appendf(&mbuf, "%s\n", haterm_cfg_crt_store_str);
/* "frontend" section */
hbuf_appendf(&mbuf, "%.*s\n", (int)fbuf.data, fbuf.area);
fileless_cfg.filename = strdup("haterm cfgfile");
fileless_cfg.content = strdup(mbuf.area);
if (!fileless_cfg.filename || !fileless_cfg.content) {
ha_alert("cfgfile strdup() failed.\n");
goto leave;
}
fileless_cfg.size = mbuf.data;
if (dump) {
fprintf(stdout, "%.*s", (int)fileless_cfg.size, fileless_cfg.content);
exit(0);
}
/* no pool debugging */
pool_debugging = 0;
err = 0;
leave:
free_hbuf(&mbuf);
free_hbuf(&gbuf);
free_hbuf(&fbuf);
free_hbuf(&tbuf);
if (err)
exit(1);
}
/* Dummy arg copier function */
char **copy_argv(int argc, char **argv)
{
char **ret = calloc(1, sizeof(*ret));
*ret = strdup("");
return ret;
}