]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: haterm: new "haterm" utility
authorFrederic Lecaille <flecaille@haproxy.com>
Wed, 11 Feb 2026 14:11:27 +0000 (15:11 +0100)
committerWilly Tarreau <w@1wt.eu>
Thu, 19 Feb 2026 14:45:01 +0000 (15:45 +0100)
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.

Makefile
doc/haterm.txt [new file with mode: 0644]
src/haterm.c
src/haterm_init.c [new file with mode: 0644]

index da50e262563258279ca952178b2b311fc2248816..d4d9a57f40e1516a34eb0bc731b3adee049aa927 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -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
 
diff --git a/doc/haterm.txt b/doc/haterm.txt
new file mode 100644 (file)
index 0000000..fbc58b9
--- /dev/null
@@ -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
+
index 672a699126cf4d0898101b1f6da856f6a7e06ac0..9299a9f5cda29f1fffd0da1bdfd7548c1aa69780 100644 (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)
 {
diff --git a/src/haterm_init.c b/src/haterm_init.c
new file mode 100644 (file)
index 0000000..acb6caf
--- /dev/null
@@ -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;
+}