/* virt-hostinfo * Copyright (C) 2009 Red Hat Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* This code parses and executes the commands sent by guests. It * is therefore particularly security sensitive. The protocol is * documented in hostinfo-protocol(5). */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "hostinfod.h" #include "apr12-compat.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 (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; }