X-Git-Url: http://git.annexia.org/?a=blobdiff_plain;ds=sidebyside;f=hostinfod%2Fcommands.c;h=d2a1d2de5095d07888a2a7e20e004cf1560bce41;hb=b266768eda005d900a9f232de0ba3827b37627f4;hp=1bbddc0735edf5d8c97e79e6380ffe5eb983df99;hpb=dcbfcc566fa812fd3085c89a8cfed7fe34bb05e8;p=virt-hostinfo.git diff --git a/hostinfod/commands.c b/hostinfod/commands.c index 1bbddc0..d2a1d2d 100644 --- a/hostinfod/commands.c +++ b/hostinfod/commands.c @@ -27,19 +27,532 @@ #include #include +#include +#include +#include #include #include +#include + +#include +#include +#include +#include #include "hostinfod.h" +#define PROTOCOL_VERSION "1.0" +#define CRLF "\r\n" + +apr_hash_t *commands = NULL; + +static const char *string_of_arg_type (enum arg_type); +static int not_printable (const char *str); +static int contains_crlf (const char *str); +static char *parse_c_string (struct guest_description *hval, const char *str, size_t *posn); +static int parse_long (struct guest_description *hval, const char *str, size_t *posn, long *ret); + void -execute_command (time_t now, +execute_command (const struct timespec *now, struct guest_description *hval, const char *command) { + size_t len, i, j; + int neg; + char *cmd; + apr_array_header_t *args; + struct arg arg; + command_fn fn; + int enabled; + double interval; + struct timespec *last; + debug ("%s: %s", hval->name, command); + /* Create a new pool for allocation during the lifetime of the + * request/response. **NB** the hval->reply field is allocated + * from this pool, which is why it gets nulled below. + */ + if (hval->rpool) + apr_pool_destroy (hval->rpool); + hval->rpool = NULL; + hval->reply = NULL; + hval->reply_size = hval->reply_posn = 0; + apr_pool_create (&hval->rpool, hval->pool); + + /* Split up the command. Commands have a very narrowly-defined + * format, and we reject any malformed commands with a 400 error. + * + * NB. A lot of the code below assumes 7 bit, printable ASCII, + * and this is only safe because of the call to 'not_printable' + * here. + */ + len = strlen (command); + if (len < 1 || len > 4094 || not_printable (command)) { + warning ("%s: command too short, too long or contained unprintable chars (len = %zu)", + hval->name, len); + send_error (hval, 400); + return; + } + + /* Command is alphanumeric string, non-zero length. */ + i = strspn (command, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789"); + if (i < 1) { + warning ("%s: no command part in request", hval->name); + send_error (hval, 400); + return; + } + + cmd = apr_pstrmemdup (hval->rpool, command, i); + for (j = 0; j < i; ++j) + cmd[j] = tolower (cmd[j]); + + args = apr_array_make (hval->rpool, 0, sizeof (struct arg)); + + while (command[i]) { /* command[i] is the space before the next arg */ + if (command[i] != ' ') { + warning ("%s: there must be a single space between command and each argument", + hval->name); + send_error (hval, 400); + return; + } + + neg = 0; + + i++; + switch (command[i]) { + case '\0': + warning ("%s: trailing space after command", hval->name); + send_error (hval, 400); + return; + + case '"': /* string literal */ + arg.type = arg_type_string; + arg.u.str = parse_c_string (hval, command, &i); + if (!arg.u.str) { + send_error (hval, 400); + return; + } + break; + + case '-': /* integer literal */ + neg = 1; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + arg.type = arg_type_int; + if (parse_long (hval, command, &i, &arg.u.i) == -1) { + send_error (hval, 400); + return; + } + break; + + case 't': /* boolean */ + case 'f': + case 'T': + case 'F': + arg.type = arg_type_bool; + if (strncasecmp (&command[i], "true", 4) == 0) { + arg.u.i = 1; + i += 4; + } else if (strncasecmp (&command[i], "false", 5) == 0) { + arg.u.i = 0; + i += 5; + } else + goto unknown_arg; + break; + + default: + unknown_arg: + warning ("%s: unknown or malformed argument starting at position %zu ('%c')", + hval->name, i, command[i]); + send_error (hval, 400); + return; + } /* switch */ + + APR_ARRAY_PUSH (args, struct arg) = arg; + } + + if (verbose) { + /* Debug output what the guest sent / what we decoded. */ + debug ("%s: command '%s' with %d args", hval->name, cmd, args->nelts); + for (i = 0; i < args->nelts; ++i) { + struct arg arg = APR_ARRAY_IDX (args, i, struct arg); + + switch (arg.type) { + case arg_type_bool: + case arg_type_int: + debug ("%s: arg %zu : %s = %ld", + hval->name, i, string_of_arg_type (arg.type), arg.u.i); + break; + case arg_type_string: + debug ("%s: arg %zu : %s = %s", + hval->name, i, string_of_arg_type (arg.type), arg.u.str); + break; + } + } + } + + /* Know about this command? */ + fn = apr_hash_get (commands, cmd, APR_HASH_KEY_STRING); + if (!fn) { + send_error (hval, 404); + return; + } + + /* Before dispatching the command, check the command is enabled + * and guest is not calling it too frequently. + */ + check_guests_file (hval, cmd, &interval, &enabled); + + if (!enabled) { + warning ("%s: guest tried disabled command '%s'", hval->name, cmd); + send_error (hval, 401); + return; + } + + last = apr_hash_get (hval->lasttime, cmd, APR_HASH_KEY_STRING); + if (last) { + struct timespec timediff; + double interval_int, interval_frac; + struct timespec interval_ts; + + diff_timespec (&timediff, now, last); + + interval_frac = modf (interval, &interval_int); + interval_ts.tv_sec = interval_int; + interval_ts.tv_nsec = interval_frac * 1000000000; + + debug ("%s: %s: interval %ds %ldns, time since last %ds %ldns", + hval->name, cmd, + (int) interval_ts.tv_sec, interval_ts.tv_nsec, + (int) timediff.tv_sec, timediff.tv_nsec); + + if (interval_ts.tv_sec > timediff.tv_sec || + (interval_ts.tv_sec == timediff.tv_sec && + interval_ts.tv_nsec > timediff.tv_nsec)) { + warning ("%s: command '%s' exceeded interval allowed", hval->name, cmd); + send_error (hval, 406); + return; + } + } + + last = apr_pmemdup (hval->pool, now, sizeof *now); + apr_hash_set (hval->lasttime, cmd, APR_HASH_KEY_STRING, last); + + /* Dispatch the command. */ + fn (hval, cmd, args); +} + +/* All commands must consist only of printable 7 bit ASCII. + * NB. Don't use isprint(3). + */ +static int +not_printable (const char *str) +{ + int c; + + while ((c = *(unsigned char *)str)) { + if (c < 32 || c > 126) + return 1; + str++; + } + return 0; +} + +static int +contains_crlf (const char *str) +{ + int c; + + while ((c = *(unsigned char *)str)) { + if (c == '\r' || c == '\n') + return 1; + str++; + } + return 0; +} + +static int +hexval (int c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return c - '0'; + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + return c - 'a' + 10; + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + return c - 'A' + 10; + default: + abort (); + } +} + +static char * +parse_c_string (struct guest_description *hval, const char *str, size_t *posn) +{ + apr_array_header_t *r; + int c; + + r = apr_array_make (hval->rpool, 0, 1); +#define APPEND(c) APR_ARRAY_PUSH (r,char) = (c); + + assert (str[*posn] == '"'); + (*posn)++; + + while (str[*posn] != '"') { + if (str[*posn] == '\\') { /* Start of \escape sequence. */ + switch (str[*posn+1]) { + case '0': case '1': case '2': case '3': + if ((str[*posn+2] >= '0' && str[*posn+2] <= '7') && + (str[*posn+3] >= '0' && str[*posn+3] <= '7')) { + c = (str[*posn+1] - '0') * 0100; + c += (str[*posn+2] - '0') * 010; + c += str[*posn+3] - '0'; + if (c != 0) { + APPEND (c); + *posn += 4; + } else { + warning ("%s: \\0 cannot appear in string literal", hval->name); + return NULL; + } + break; + } else { + warning ("%s: invalid octal sequence in string literal", hval->name); + return NULL; + } + case 'x': + if (isxdigit (str[*posn+2]) && isxdigit (str[*posn+3])) { + c = hexval (str[*posn+2]) * 0x10; + c += hexval (str[*posn+3]); + if (c != 0) { + APPEND (c); + *posn += 4; + } else { + warning ("%s: \\0 cannot appear in string literal", hval->name); + return NULL; + } + break; + } else { + warning ("%s: invalid hex sequence in string literal", hval->name); + return NULL; + } + case 'a': + APPEND ('\a'); + *posn += 2; + break; + case 'b': + APPEND ('\b'); + *posn += 2; + break; + case 'f': + APPEND ('\f'); + *posn += 2; + break; + case 'n': + APPEND ('\n'); + *posn += 2; + break; + case 'r': + APPEND ('\r'); + *posn += 2; + break; + case 't': + APPEND ('\t'); + *posn += 2; + break; + case 'v': + APPEND ('\v'); + *posn += 2; + break; + case '"': + APPEND ('"'); + *posn += 2; + break; + case '\\': + APPEND ('\\'); + *posn += 2; + break; + + default: + warning ("%s: unterminated escape sequence in string literal", + hval->name); + return NULL; + } + } else if (str[*posn] == '\0') { /* Unterminated string literal. */ + warning ("%s: unterminated string literal in request", hval->name); + return NULL; + } else { /* Ordinary character. */ + APPEND (str[*posn]); + (*posn)++; + } + } + + /* Finish off the string and return it. */ + APPEND ('\0'); + (*posn)++; /* Skips over the final quote. */ + + return r->elts; +} + +static int +parse_long (struct guest_description *hval, + const char *str, size_t *posn, long *ret) +{ + error ("XXXXXXX parse_long not implemented XXXXXXX"); + return -1; +} + +/* For single line replies. */ +void +send_reply (struct guest_description *hval, int code, const char *fs, ...) +{ + va_list args; + char *msg; + + /* All success codes must be 2xx. */ + assert (code >= 200 && code < 300); + + va_start (args, fs); + msg = apr_pvsprintf (hval->rpool, fs, args); + va_end (args); + + /* The result string must not contain any CR or LF characters. If + * not it's an internal error in the command, or else the caller has + * (somehow) managed to pass a bad string through. + */ + if (contains_crlf (msg)) { + error ("%s: send_reply: refusing the send a reply message containing CR/LF characters. This is a serious internal error in the current command.", + hval->name); + send_error (hval, 500); + return; + } + + /* Send reply. */ + hval->reply = + apr_psprintf (hval->rpool, PROTOCOL_VERSION " %03d %s" CRLF, + code, msg); + hval->reply_size = strlen (hval->reply); + hval->reply_posn = 0; + hval->state = guest_state_reply; +} + +void +send_error (struct guest_description *hval, int code) +{ + const char *msg; + + /* All errors must be 4xx or 5xx. */ + assert (code >= 400 && code < 600); + + /* NB: If you add a code, update COMMON STATUS CODES section + * in hostinfo-protocol.pod. + */ + switch (code) { + case 400: msg = "Bad request"; break; + case 401: msg = "Command disabled"; break; + case 404: msg = "Command not found"; break; + case 406: msg = "Too frequent"; break; + case 500: msg = "Internal server error"; break; + default: msg = "Unknown error"; break; + } + + /* Construct the reply. */ + hval->reply = + apr_psprintf (hval->rpool, PROTOCOL_VERSION " %03d %s" CRLF, + code, msg); + hval->reply_size = strlen (hval->reply); + hval->reply_posn = 0; + hval->state = guest_state_reply; + + /* Penalty is always increased on errors, to ensure the guest + * cannot flood us with invalid commands. + */ + hval->penalty++; +} + +static const char * +string_of_arg_type (enum arg_type t) +{ + switch (t) { + case arg_type_string: return "string"; + case arg_type_int: return "int"; + case arg_type_bool: return "bool"; + } + abort (); +} + +int +get_args (apr_array_header_t *args, const char *argfs, ...) +{ + va_list vargs; + int i = 0, ret = 0; + + va_start (vargs, argfs); + + while (*argfs) { + struct arg arg; + + if (i >= args->nelts) { + ret = -1; + goto end; + } + + arg = APR_ARRAY_IDX (args, i, struct arg); + + switch (*argfs) { + case 's': + if (arg.type == arg_type_string) + * va_arg (vargs, const char **) = arg.u.str; + else { + ret = -1; + goto end; + } + break; + + case 'i': + if (arg.type == arg_type_int) + * va_arg (vargs, int *) = arg.u.i; + else { + ret = -1; + goto end; + } + break; + + case 'b': + if (arg.type == arg_type_bool) + * va_arg (vargs, int *) = arg.u.i; + else { + ret = -1; + goto end; + } + break; + + default: + error ("get_args: invalid character '%c'", *argfs); + ret = -1; + goto end; + } + argfs++; + i++; + } + if (i < args->nelts) + ret = -1; + end: + va_end (vargs); + return ret; }