summaryrefslogtreecommitdiffstats
path: root/src/util/attr_scan_plain.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/attr_scan_plain.c')
-rw-r--r--src/util/attr_scan_plain.c643
1 files changed, 643 insertions, 0 deletions
diff --git a/src/util/attr_scan_plain.c b/src/util/attr_scan_plain.c
new file mode 100644
index 0000000..d7e2f66
--- /dev/null
+++ b/src/util/attr_scan_plain.c
@@ -0,0 +1,643 @@
+/*++
+/* NAME
+/* attr_scan_plain 3
+/* SUMMARY
+/* recover attributes from byte stream
+/* SYNOPSIS
+/* #include <attr.h>
+/*
+/* int attr_scan_plain(fp, flags, type, name, ..., ATTR_TYPE_END)
+/* VSTREAM *fp;
+/* int flags;
+/* int type;
+/* char *name;
+/*
+/* int attr_vscan_plain(fp, flags, ap)
+/* VSTREAM *fp;
+/* int flags;
+/* va_list ap;
+/*
+/* int attr_scan_more_plain(fp)
+/* VSTREAM *fp;
+/* DESCRIPTION
+/* attr_scan_plain() takes zero or more (name, value) request attributes
+/* and recovers the attribute values from the byte stream that was
+/* possibly generated by attr_print_plain().
+/*
+/* attr_vscan_plain() provides an alternative interface that is convenient
+/* for calling from within a variadic function.
+/*
+/* attr_scan_more_plain() returns 0 when a terminator is found
+/* (and consumes that terminator), returns 1 when more input
+/* is expected (without consuming input), and returns -1
+/* otherwise (error).
+/*
+/* The input stream is formatted as follows, where (item)* stands
+/* for zero or more instances of the specified item, and where
+/* (item1 | item2) stands for choice:
+/*
+/* .in +5
+/* attr-list :== (simple-attr | multi-attr)* newline
+/* .br
+/* multi-attr :== "{" newline simple-attr* "}" newline
+/* .br
+/* simple-attr :== attr-name "=" attr-value newline
+/* .br
+/* attr-name :== any string without null or "=" or newline.
+/* .br
+/* attr-value :== any string without null or newline.
+/* .br
+/* newline :== the ASCII newline character
+/* .in
+/*
+/* All attribute names and attribute values are sent as plain
+/* strings. Each string must be no longer than 4*var_line_limit
+/* characters. The formatting rules aim to make implementations in PERL
+/* and other languages easy.
+/*
+/* Normally, attributes must be received in the sequence as specified
+/* with the attr_scan_plain() argument list. The input stream may
+/* contain additional attributes at any point in the input stream,
+/* including additional instances of requested attributes.
+/*
+/* Additional input attributes or input attribute instances are silently
+/* skipped over, unless the ATTR_FLAG_EXTRA processing flag is specified
+/* (see below). This allows for some flexibility in the evolution of
+/* protocols while still providing the option of being strict where
+/* this is desirable.
+/*
+/* Arguments:
+/* .IP fp
+/* Stream to recover the input attributes from.
+/* .IP flags
+/* The bit-wise OR of zero or more of the following.
+/* .RS
+/* .IP ATTR_FLAG_MISSING
+/* Log a warning when the input attribute list terminates before all
+/* requested attributes are recovered. It is always an error when the
+/* input stream ends without the newline attribute list terminator.
+/* .IP ATTR_FLAG_EXTRA
+/* Log a warning and stop attribute recovery when the input stream
+/* contains an attribute that was not requested. This includes the
+/* case of additional instances of a requested attribute.
+/* .IP ATTR_FLAG_MORE
+/* After recovering the requested attributes, leave the input stream
+/* in a state that is usable for more attr_scan_plain() operations
+/* from the same input attribute list.
+/* By default, attr_scan_plain() skips forward past the input attribute
+/* list terminator.
+/* .IP ATTR_FLAG_PRINTABLE
+/* Santize received string values with printable(_, '?').
+/* .IP ATTR_FLAG_STRICT
+/* For convenience, this value combines both ATTR_FLAG_MISSING and
+/* ATTR_FLAG_EXTRA.
+/* .IP ATTR_FLAG_NONE
+/* For convenience, this value requests none of the above.
+/* .RE
+/* .IP List of attributes followed by terminator:
+/* .RS
+/* .IP "RECV_ATTR_INT(const char *name, int *ptr)"
+/* This argument is followed by an attribute name and an integer pointer.
+/* .IP "RECV_ATTR_LONG(const char *name, long *ptr)"
+/* This argument is followed by an attribute name and a long pointer.
+/* .IP "RECV_ATTR_STR(const char *name, VSTRING *vp)"
+/* This argument is followed by an attribute name and a VSTRING pointer.
+/* .IP "RECV_ATTR_STREQ(const char *name, const char *value)"
+/* The name and value must match what the client sends.
+/* This attribute does not increment the result value.
+/* .IP "RECV_ATTR_DATA(const char *name, VSTRING *vp)"
+/* This argument is followed by an attribute name and a VSTRING pointer.
+/* .IP "RECV_ATTR_FUNC(ATTR_SCAN_CUSTOM_FN, void *data)"
+/* This argument is followed by a function pointer and a generic data
+/* pointer. The caller-specified function returns < 0 in case of
+/* error.
+/* .IP "RECV_ATTR_HASH(HTABLE *table)"
+/* .IP "RECV_ATTR_NAMEVAL(NVTABLE *table)"
+/* Receive a sequence of attribute names and string values.
+/* There can be no more than 1024 attributes in a hash table.
+/* .sp
+/* The attribute string values are stored in the hash table under
+/* keys equal to the attribute name (obtained from the input stream).
+/* Values from the input stream are added to the hash table. Existing
+/* hash table entries are not replaced.
+/* .sp
+/* Note: the SEND_ATTR_HASH or SEND_ATTR_NAMEVAL requests
+/* format their payload as a multi-attr sequence (see syntax
+/* above). When the receiver's input does not start with a
+/* multi-attr delimiter (i.e. the sender did not request
+/* SEND_ATTR_HASH or SEND_ATTR_NAMEVAL), the receiver will
+/* store all attribute names and values up to the attribute
+/* list terminator. In terms of code, this means that the
+/* RECV_ATTR_HASH or RECV_ATTR_NAMEVAL request must be followed
+/* by ATTR_TYPE_END.
+/* .IP ATTR_TYPE_END
+/* This argument terminates the requested attribute list.
+/* .RE
+/* BUGS
+/* RECV_ATTR_HASH (RECV_ATTR_NAMEVAL) accepts attributes with arbitrary
+/* names from possibly untrusted sources.
+/* This is unsafe, unless the resulting table is queried only with
+/* known to be good attribute names.
+/* DIAGNOSTICS
+/* attr_scan_plain() and attr_vscan_plain() return -1 when malformed input
+/* is detected (string too long, incomplete line, missing end marker).
+/* Otherwise, the result value is the number of attributes that were
+/* successfully recovered from the input stream (a hash table counts
+/* as the number of entries stored into the table).
+/*
+/* Panic: interface violation. All system call errors are fatal.
+/* SEE ALSO
+/* attr_print_plain(3) send attributes over byte stream.
+/* LICENSE
+/* .ad
+/* .fi
+/* The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/* Wietse Venema
+/* IBM T.J. Watson Research
+/* P.O. Box 704
+/* Yorktown Heights, NY 10598, USA
+/*
+/* Wietse Venema
+/* Google, Inc.
+/* 111 8th Avenue
+/* New York, NY 10011, USA
+/*--*/
+
+/* System library. */
+
+#include <sys_defs.h>
+#include <stdarg.h>
+#include <string.h>
+#include <stdio.h>
+
+/* Utility library. */
+
+#include <msg.h>
+#include <mymalloc.h>
+#include <vstream.h>
+#include <vstring.h>
+#include <htable.h>
+#include <base64_code.h>
+#include <stringops.h>
+#include <attr.h>
+
+/* Application specific. */
+
+#define STR(x) vstring_str(x)
+#define LEN(x) VSTRING_LEN(x)
+
+/* attr_scan_plain_string - pull a string from the input stream */
+
+static int attr_scan_plain_string(VSTREAM *fp, VSTRING *plain_buf,
+ int terminator, const char *context)
+{
+#if 0
+ extern int var_line_limit; /* XXX */
+ int limit = var_line_limit * 4;
+
+#endif
+ int ch;
+
+ VSTRING_RESET(plain_buf);
+ while ((ch = VSTREAM_GETC(fp)) != '\n'
+ && (terminator == 0 || ch != terminator)) {
+ if (ch == VSTREAM_EOF) {
+ msg_warn("%s on %s while reading %s",
+ vstream_ftimeout(fp) ? "timeout" : "premature end-of-input",
+ VSTREAM_PATH(fp), context);
+ return (-1);
+ }
+ VSTRING_ADDCH(plain_buf, ch);
+#if 0
+ if (LEN(plain_buf) > limit) {
+ msg_warn("string length > %d characters from %s while reading %s",
+ limit, VSTREAM_PATH(fp), context);
+ return (-1);
+ }
+#endif
+ }
+ VSTRING_TERMINATE(plain_buf);
+
+ if (msg_verbose)
+ msg_info("%s: %s", context, *STR(plain_buf) ? STR(plain_buf) : "(end)");
+ return (ch);
+}
+
+/* attr_scan_plain_data - pull a data blob from the input stream */
+
+static int attr_scan_plain_data(VSTREAM *fp, VSTRING *str_buf,
+ int terminator,
+ const char *context)
+{
+ static VSTRING *base64_buf = 0;
+ int ch;
+
+ if (base64_buf == 0)
+ base64_buf = vstring_alloc(10);
+ if ((ch = attr_scan_plain_string(fp, base64_buf, terminator, context)) < 0)
+ return (-1);
+ if (base64_decode(str_buf, STR(base64_buf), LEN(base64_buf)) == 0) {
+ msg_warn("malformed base64 data from %s while reading %s: %.100s",
+ VSTREAM_PATH(fp), context, STR(base64_buf));
+ return (-1);
+ }
+ return (ch);
+}
+
+/* attr_scan_plain_number - pull a number from the input stream */
+
+static int attr_scan_plain_number(VSTREAM *fp, unsigned *ptr, VSTRING *str_buf,
+ int terminator, const char *context)
+{
+ char junk = 0;
+ int ch;
+
+ if ((ch = attr_scan_plain_string(fp, str_buf, terminator, context)) < 0)
+ return (-1);
+ if (sscanf(STR(str_buf), "%u%c", ptr, &junk) != 1 || junk != 0) {
+ msg_warn("malformed numerical data from %s while reading %s: %.100s",
+ VSTREAM_PATH(fp), context, STR(str_buf));
+ return (-1);
+ }
+ return (ch);
+}
+
+/* attr_scan_plain_long_number - pull a number from the input stream */
+
+static int attr_scan_plain_long_number(VSTREAM *fp, unsigned long *ptr,
+ VSTRING *str_buf,
+ int terminator,
+ const char *context)
+{
+ char junk = 0;
+ int ch;
+
+ if ((ch = attr_scan_plain_string(fp, str_buf, terminator, context)) < 0)
+ return (-1);
+ if (sscanf(STR(str_buf), "%lu%c", ptr, &junk) != 1 || junk != 0) {
+ msg_warn("malformed numerical data from %s while reading %s: %.100s",
+ VSTREAM_PATH(fp), context, STR(str_buf));
+ return (-1);
+ }
+ return (ch);
+}
+
+/* attr_vscan_plain - receive attribute list from stream */
+
+int attr_vscan_plain(VSTREAM *fp, int flags, va_list ap)
+{
+ const char *myname = "attr_scan_plain";
+ static VSTRING *str_buf = 0;
+ static VSTRING *name_buf = 0;
+ int wanted_type = -1;
+ char *wanted_name;
+ unsigned int *number;
+ unsigned long *long_number;
+ VSTRING *string;
+ HTABLE *hash_table;
+ int ch;
+ int conversions;
+ ATTR_SCAN_CUSTOM_FN scan_fn;
+ void *scan_arg;
+ const char *expect_val;
+
+ /*
+ * Sanity check.
+ */
+ if (flags & ~ATTR_FLAG_ALL)
+ msg_panic("%s: bad flags: 0x%x", myname, flags);
+
+ /*
+ * EOF check.
+ */
+ if ((ch = VSTREAM_GETC(fp)) == VSTREAM_EOF)
+ return (0);
+ vstream_ungetc(fp, ch);
+
+ /*
+ * Initialize.
+ */
+ if (str_buf == 0) {
+ str_buf = vstring_alloc(10);
+ name_buf = vstring_alloc(10);
+ }
+
+ /*
+ * Iterate over all (type, name, value) triples.
+ */
+ for (conversions = 0; /* void */ ; conversions++) {
+
+ /*
+ * Determine the next attribute type and attribute name on the
+ * caller's wish list.
+ *
+ * If we're reading into a hash table, we already know that the
+ * attribute value is string-valued, and we get the attribute name
+ * from the input stream instead. This is secure only when the
+ * resulting table is queried with known to be good attribute names.
+ */
+ if (wanted_type != ATTR_TYPE_HASH
+ && wanted_type != ATTR_TYPE_CLOSE) {
+ wanted_type = va_arg(ap, int);
+ if (wanted_type == ATTR_TYPE_END) {
+ if ((flags & ATTR_FLAG_MORE) != 0)
+ return (conversions);
+ wanted_name = "(list terminator)";
+ } else if (wanted_type == ATTR_TYPE_HASH) {
+ wanted_name = "(any attribute name or list terminator)";
+ hash_table = va_arg(ap, HTABLE *);
+ } else if (wanted_type != ATTR_TYPE_FUNC) {
+ wanted_name = va_arg(ap, char *);
+ }
+ }
+
+ /*
+ * Locate the next attribute of interest in the input stream.
+ */
+ while (wanted_type != ATTR_TYPE_FUNC) {
+
+ /*
+ * Get the name of the next attribute. Hitting EOF is always bad.
+ * Hitting the end-of-input early is OK if the caller is prepared
+ * to deal with missing inputs.
+ */
+ if (msg_verbose)
+ msg_info("%s: wanted attribute: %s",
+ VSTREAM_PATH(fp), wanted_name);
+ if ((ch = attr_scan_plain_string(fp, name_buf, '=',
+ "input attribute name")) == VSTREAM_EOF)
+ return (-1);
+ if (ch == '\n' && LEN(name_buf) == 0) {
+ if (wanted_type == ATTR_TYPE_END
+ || wanted_type == ATTR_TYPE_HASH)
+ return (conversions);
+ if ((flags & ATTR_FLAG_MISSING) != 0)
+ msg_warn("missing attribute %s in input from %s",
+ wanted_name, VSTREAM_PATH(fp));
+ return (conversions);
+ }
+
+ /*
+ * See if the caller asks for this attribute.
+ */
+ if (wanted_type == ATTR_TYPE_HASH
+ && ch == '\n' && strcmp(ATTR_NAME_OPEN, STR(name_buf)) == 0) {
+ wanted_type = ATTR_TYPE_CLOSE;
+ wanted_name = "(any attribute name or '}')";
+ /* Advance in the input stream. */
+ continue;
+ } else if (wanted_type == ATTR_TYPE_CLOSE
+ && ch == '\n' && strcmp(ATTR_NAME_CLOSE, STR(name_buf)) == 0) {
+ /* Advance in the argument list. */
+ wanted_type = -1;
+ break;
+ }
+ if (wanted_type == ATTR_TYPE_HASH
+ || wanted_type == ATTR_TYPE_CLOSE
+ || (wanted_type != ATTR_TYPE_END
+ && strcmp(wanted_name, STR(name_buf)) == 0))
+ break;
+ if ((flags & ATTR_FLAG_EXTRA) != 0) {
+ msg_warn("unexpected attribute %s from %s (expecting: %s)",
+ STR(name_buf), VSTREAM_PATH(fp), wanted_name);
+ return (conversions);
+ }
+
+ /*
+ * Skip over this attribute. The caller does not ask for it.
+ */
+ while (ch != '\n' && (ch = VSTREAM_GETC(fp)) != VSTREAM_EOF)
+ /* void */ ;
+ }
+
+ /*
+ * Do the requested conversion.
+ */
+ switch (wanted_type) {
+ case ATTR_TYPE_INT:
+ if (ch != '=') {
+ msg_warn("missing value for number attribute %s from %s",
+ STR(name_buf), VSTREAM_PATH(fp));
+ return (-1);
+ }
+ number = va_arg(ap, unsigned int *);
+ if ((ch = attr_scan_plain_number(fp, number, str_buf, 0,
+ "input attribute value")) < 0)
+ return (-1);
+ break;
+ case ATTR_TYPE_LONG:
+ if (ch != '=') {
+ msg_warn("missing value for number attribute %s from %s",
+ STR(name_buf), VSTREAM_PATH(fp));
+ return (-1);
+ }
+ long_number = va_arg(ap, unsigned long *);
+ if ((ch = attr_scan_plain_long_number(fp, long_number, str_buf,
+ 0, "input attribute value")) < 0)
+ return (-1);
+ break;
+ case ATTR_TYPE_STR:
+ if (ch != '=') {
+ msg_warn("missing value for string attribute %s from %s",
+ STR(name_buf), VSTREAM_PATH(fp));
+ return (-1);
+ }
+ string = va_arg(ap, VSTRING *);
+ if ((ch = attr_scan_plain_string(fp, string, 0,
+ "input attribute value")) < 0)
+ return (-1);
+ if (flags & ATTR_FLAG_PRINTABLE)
+ (void) printable(STR(string), '?');
+ break;
+ case ATTR_TYPE_DATA:
+ if (ch != '=') {
+ msg_warn("missing value for data attribute %s from %s",
+ STR(name_buf), VSTREAM_PATH(fp));
+ return (-1);
+ }
+ string = va_arg(ap, VSTRING *);
+ if ((ch = attr_scan_plain_data(fp, string, 0,
+ "input attribute value")) < 0)
+ return (-1);
+ break;
+ case ATTR_TYPE_FUNC:
+ scan_fn = va_arg(ap, ATTR_SCAN_CUSTOM_FN);
+ scan_arg = va_arg(ap, void *);
+ if (scan_fn(attr_scan_plain, fp, flags | ATTR_FLAG_MORE, scan_arg) < 0)
+ return (-1);
+ break;
+ case ATTR_TYPE_STREQ:
+ if (ch != '=') {
+ msg_warn("missing value for string attribute %s from %s",
+ STR(name_buf), VSTREAM_PATH(fp));
+ return (-1);
+ }
+ expect_val = va_arg(ap, const char *);
+ if ((ch = attr_scan_plain_string(fp, str_buf, 0,
+ "input attribute value")) < 0)
+ return (-1);
+ if (strcmp(expect_val, STR(str_buf)) != 0) {
+ msg_warn("unexpected %s %s from %s (expected: %s)",
+ STR(name_buf), STR(str_buf), VSTREAM_PATH(fp),
+ expect_val);
+ return (-1);
+ }
+ conversions -= 1;
+ break;
+ case ATTR_TYPE_HASH:
+ case ATTR_TYPE_CLOSE:
+ if (ch != '=') {
+ msg_warn("missing value for string attribute %s from %s",
+ STR(name_buf), VSTREAM_PATH(fp));
+ return (-1);
+ }
+ if ((ch = attr_scan_plain_string(fp, str_buf, 0,
+ "input attribute value")) < 0)
+ return (-1);
+ if (flags & ATTR_FLAG_PRINTABLE) {
+ (void) printable(STR(name_buf), '?');
+ (void) printable(STR(str_buf), '?');
+ }
+ if (htable_locate(hash_table, STR(name_buf)) != 0) {
+ if ((flags & ATTR_FLAG_EXTRA) != 0) {
+ msg_warn("duplicate attribute %s in input from %s",
+ STR(name_buf), VSTREAM_PATH(fp));
+ return (conversions);
+ }
+ } else if (hash_table->used >= ATTR_HASH_LIMIT) {
+ msg_warn("attribute count exceeds limit %d in input from %s",
+ ATTR_HASH_LIMIT, VSTREAM_PATH(fp));
+ return (conversions);
+ } else {
+ htable_enter(hash_table, STR(name_buf),
+ mystrdup(STR(str_buf)));
+ }
+ break;
+ case -1:
+ conversions -= 1;
+ break;
+ default:
+ msg_panic("%s: unknown type code: %d", myname, wanted_type);
+ }
+ }
+}
+
+/* attr_scan_plain - read attribute list from stream */
+
+int attr_scan_plain(VSTREAM *fp, int flags,...)
+{
+ va_list ap;
+ int ret;
+
+ va_start(ap, flags);
+ ret = attr_vscan_plain(fp, flags, ap);
+ va_end(ap);
+ return (ret);
+}
+
+/* attr_scan_more_plain - look ahead for more */
+
+int attr_scan_more_plain(VSTREAM *fp)
+{
+ int ch;
+
+ switch (ch = VSTREAM_GETC(fp)) {
+ case '\n':
+ if (msg_verbose)
+ msg_info("%s: terminator (consumed)", VSTREAM_PATH(fp));
+ return (0);
+ case VSTREAM_EOF:
+ if (msg_verbose)
+ msg_info("%s: EOF", VSTREAM_PATH(fp));
+ return (-1);
+ default:
+ if (msg_verbose)
+ msg_info("%s: non-terminator '%c' (lookahead)",
+ VSTREAM_PATH(fp), ch);
+ (void) vstream_ungetc(fp, ch);
+ return (1);
+ }
+}
+
+#ifdef TEST
+
+ /*
+ * Proof of concept test program. Mirror image of the attr_scan_plain test
+ * program.
+ */
+#include <msg_vstream.h>
+
+int var_line_limit = 2048;
+
+int main(int unused_argc, char **used_argv)
+{
+ VSTRING *data_val = vstring_alloc(1);
+ VSTRING *str_val = vstring_alloc(1);
+ HTABLE *table = htable_create(1);
+ HTABLE_INFO **ht_info_list;
+ HTABLE_INFO **ht;
+ int int_val;
+ long long_val;
+ long long_val2;
+ int ret;
+
+ msg_verbose = 1;
+ msg_vstream_init(used_argv[0], VSTREAM_ERR);
+ if ((ret = attr_scan_plain(VSTREAM_IN,
+ ATTR_FLAG_STRICT,
+ RECV_ATTR_STREQ("protocol", "test"),
+ RECV_ATTR_INT(ATTR_NAME_INT, &int_val),
+ RECV_ATTR_LONG(ATTR_NAME_LONG, &long_val),
+ RECV_ATTR_STR(ATTR_NAME_STR, str_val),
+ RECV_ATTR_DATA(ATTR_NAME_DATA, data_val),
+ RECV_ATTR_HASH(table),
+ RECV_ATTR_LONG(ATTR_NAME_LONG, &long_val2),
+ ATTR_TYPE_END)) > 4) {
+ vstream_printf("%s %d\n", ATTR_NAME_INT, int_val);
+ vstream_printf("%s %ld\n", ATTR_NAME_LONG, long_val);
+ vstream_printf("%s %s\n", ATTR_NAME_STR, STR(str_val));
+ vstream_printf("%s %s\n", ATTR_NAME_DATA, STR(data_val));
+ ht_info_list = htable_list(table);
+ for (ht = ht_info_list; *ht; ht++)
+ vstream_printf("(hash) %s %s\n", ht[0]->key, (char *) ht[0]->value);
+ myfree((void *) ht_info_list);
+ vstream_printf("%s %ld\n", ATTR_NAME_LONG, long_val2);
+ } else {
+ vstream_printf("return: %d\n", ret);
+ }
+ if ((ret = attr_scan_plain(VSTREAM_IN,
+ ATTR_FLAG_STRICT,
+ RECV_ATTR_STREQ("protocol", "test"),
+ RECV_ATTR_INT(ATTR_NAME_INT, &int_val),
+ RECV_ATTR_LONG(ATTR_NAME_LONG, &long_val),
+ RECV_ATTR_STR(ATTR_NAME_STR, str_val),
+ RECV_ATTR_DATA(ATTR_NAME_DATA, data_val),
+ ATTR_TYPE_END)) == 4) {
+ vstream_printf("%s %d\n", ATTR_NAME_INT, int_val);
+ vstream_printf("%s %ld\n", ATTR_NAME_LONG, long_val);
+ vstream_printf("%s %s\n", ATTR_NAME_STR, STR(str_val));
+ vstream_printf("%s %s\n", ATTR_NAME_DATA, STR(data_val));
+ ht_info_list = htable_list(table);
+ for (ht = ht_info_list; *ht; ht++)
+ vstream_printf("(hash) %s %s\n", ht[0]->key, (char *) ht[0]->value);
+ myfree((void *) ht_info_list);
+ } else {
+ vstream_printf("return: %d\n", ret);
+ }
+ if ((ret = attr_scan_plain(VSTREAM_IN,
+ ATTR_FLAG_STRICT,
+ RECV_ATTR_STREQ("protocol", "test"),
+ ATTR_TYPE_END)) != 0)
+ vstream_printf("return: %d\n", ret);
+ if (vstream_fflush(VSTREAM_OUT) != 0)
+ msg_fatal("write error: %m");
+
+ vstring_free(data_val);
+ vstring_free(str_val);
+ htable_free(table, myfree);
+
+ return (0);
+}
+
+#endif