Hostinfo day 4: Implement command processing code.
[virt-hostinfo.git] / hostinfod / commands.c
index 1bbddc0..708b4a8 100644 (file)
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <assert.h>
+#include <ctype.h>
 #include <unistd.h>
 #include <time.h>
 
 #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 void init_commands_hash (void);
+static int not_printable (const char *str);
+
 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;
+
   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",
+            hval->name);
+    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;
+      XXX;
+      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;
+      XXX ();
+      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 += 4;
+      } else
+       goto unknown_arg;
+      break;
+
+    default:
+    unknown_arg:
+      warning ("%s: unknown or malformed argument starting at position %d ('%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 %d : %s = %ld",
+              hval->name, i, string_of_arg_type (arg.type), arg.u.i);
+       break;
+      case arg_type_string:
+       debug ("%s: arg %d : %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.
+   */
+  error ("XXXXXXX enabled check not implemented XXXXXXX");
+  error ("XXXXXXX frequency check not implemented XXXXXXX");
+
+  /* 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 0;
+    str++;
+  }
+  return 1;
+}
+
+/* For single line replies. */
+void
+send_reply (struct guest_description *hval, int code, const char *fs, ...)
+{
+  const char *msg;
+
+  /* All success codes must be 2xx. */
+  assert (code >= 200 && code < 300);
+
+  XXX;
+}
+
+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++;
 }