/* This file is part of GNU Pies. Copyright (C) 2015 Sergey Poznyakoff GNU Pies 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 3, or (at your option) any later version. GNU Pies 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 GNU Pies. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "base64.h" #include "progname.h" #include "libpies.h" #include "grecsasrt.h" struct pies_url *default_url; /* Control socket URL */ struct pies_url *url; char const *instance = "pies"; char *config_file; char default_config_file[] = SYSCONFDIR "/piesctl.conf"; int preprocess_only; int verbose; static void config_help (void); #include "piesctl-cl.h" static struct grecs_keyword instance_keywords[] = { {"socket", N_("url: string"), N_("Socket URL for that instance."), grecs_type_string, GRECS_DFLT, NULL, 0, conf_callback_url}, { NULL } }; static int callback_instance (enum grecs_callback_command cmd, grecs_locus_t *locus, void *varptr, grecs_value_t *value, void *cb_data) { switch (cmd) { case grecs_callback_section_begin: if (GRECS_VALUE_EMPTY_P (value)) { grecs_error (locus, 0, _("missing tag")); return 1; } if (assert_grecs_value_type (&value->locus, value, GRECS_TYPE_STRING)) return 1; if (!url && strcmp (value->v.string, instance) == 0) *(struct pies_url ***) cb_data = &url; else *(struct pies_url ***) cb_data = NULL; break; case grecs_callback_section_end: break; case grecs_callback_set_value: grecs_error (locus, 0, _("invalid use of block statement")); } return 0; } struct grecs_keyword piesctl_keywords[] = { {"socket", N_("url: string"), N_("Default socket URL."), grecs_type_string, GRECS_DFLT, &default_url, 0, conf_callback_url}, {"instance", N_("name"), N_("Configure connection to a pies instance"), grecs_type_section, GRECS_DFLT, NULL, 0, callback_instance, NULL, instance_keywords }, { NULL } }; static void parse_config () { char *file_name; struct grecs_node *tree; grecs_include_path_setup (DEFAULT_VERSION_INCLUDE_DIR, DEFAULT_INCLUDE_DIR, NULL); grecs_log_to_stderr = 1; grecs_preprocessor = pp_command_line (); if (config_file) file_name = config_file; else if (access (default_config_file, F_OK) == 0) file_name = default_config_file; else file_name = NULL; if (file_name) { if (preprocess_only) exit (grecs_preproc_run (file_name, grecs_preprocessor) ? EX_CONFIG : 0); if (verbose) printf ("%s: reading configuration from %s\n", program_name, file_name); tree = grecs_parse (file_name); if (!tree) exit (EX_CONFIG); if (grecs_tree_process (tree, piesctl_keywords)) exit (EX_CONFIG); if (!url && verbose) printf ("%s: URL not found in %s\n", program_name, file_name); } if (!url) { /* Try local instance configuration */ file_name = mkfilename (SYSCONFDIR, instance, ".conf"); if (access (file_name, F_OK) == 0) { struct grecs_node *node; if (verbose) printf ("%s: reading configuration from %s\n", program_name, file_name); if (preprocess_only) exit (grecs_preproc_run (file_name, grecs_preprocessor) ? EX_CONFIG : 0); tree = grecs_parse (file_name); node = grecs_find_node (tree, "/control/socket"); if (node) { assert_grecs_value_type (&node->locus, node->v.value, GRECS_TYPE_STRING); if (pies_url_create (&url, node->v.value->v.string)) { grecs_error (&node->locus, 0, _("%s: cannot create URL: %s"), node->v.value->v.string, strerror (errno)); exit (EX_CONFIG); } } else if (verbose) printf ("%s: URL not found in %s\n", program_name, file_name); } free (file_name); } if (!url) { if (strcmp (instance, "pies") == 0) { if (verbose) printf ("%s: falling back to default URL\n", program_name); if (default_url) url = default_url; else { int rc; size_t len = 0; file_name = NULL; grecs_asprintf (&file_name, &len, DEFAULT_CONTROL_URL, instance); rc = pies_url_create (&url, file_name); free (file_name); if (rc) { grecs_error (NULL, 0, _("%s: cannot create URL: %s"), DEFAULT_CONTROL_URL, strerror (errno)); exit (EX_SOFTWARE); } } } else { grecs_error (NULL, 0, _("socket name for instance %s not configured"), instance); exit (EX_CONFIG); } } if (verbose) printf ("%s: using URL %s\n", program_name, url->string); } static void config_help (void) { static char docstring[] = /* TRANSLATORS: do not translate words between ` and ' */ N_("Configuration file structure for piesctl.\n" "For more information, use `info piesctl configuration'."); grecs_print_docstring (docstring, 0, stdout); grecs_print_statement_array (piesctl_keywords, 1, 0, stdout); } static void piesctl_diag(grecs_locus_t const *locus, int err, int errcode, const char *msg) { fflush (stdout); fprintf (stderr, "%s: ", program_name); if (locus) { YY_LOCATION_PRINT (stderr, *locus); fputc(':', stderr); fputc(' ', stderr); } if (!err) fprintf (stderr, "warning: "); fprintf (stderr, "%s", msg); if (errcode) fprintf (stderr, ": %s", strerror (errno)); fputc ('\n', stderr); } enum http_method { METH_GET, METH_POST, METH_DELETE, METH_PUT, METH_OPTIONS, METH_INVALID }; char *method_names[] = { [METH_GET] = "GET", [METH_POST] = "POST", [METH_DELETE] = "DELETE", [METH_PUT] = "PUT", [METH_OPTIONS] = "OPTIONS", NULL }; static char http_version[] = "HTTP/1.1"; struct shttp_io { int code; struct grecs_symtab *headers; unsigned long content_length; char *content; }; struct shttp_buf { char *base; size_t size; }; struct shttp_connection { FILE *fp; struct pies_url *url; struct shttp_io req; struct shttp_io resp; struct shttp_buf sendbuf; struct shttp_buf readbuf; char *b64auth; char *status_line[3]; struct json_value *result; }; struct http_header { char *name; char *value; }; static unsigned http_header_hash (void *data, unsigned long n_buckets) { const struct http_header *p = data; return grecs_hash_string_ci (p->name, n_buckets); } static int http_header_compare (void const *data1, void const *data2) { const struct http_header *p1 = data1; const struct http_header *p2 = data2; return strcasecmp (p1->name, p2->name); } static int http_header_copy (void *a, void *b) { struct http_header *dst = a; struct http_header *src = b; dst->name = grecs_strdup (src->name); dst->value = src->value ? grecs_strdup (src->value) : NULL; return 0; } static void http_header_free (void *p) { const struct http_header *h = p; grecs_free (h->name); grecs_free (h->value); free (p); } static void shttp_io_init (struct shttp_io *io) { io->code = 0; io->content_length = 0; grecs_free (io->content); io->content = NULL; grecs_symtab_clear (io->headers); } static int shttp_io_header_put (struct shttp_io *io, char const *name, char const *fmt, ...) { int install = 1; struct http_header key, *ret; va_list ap; size_t len = 0; if (!io->headers) io->headers = grecs_symtab_create (sizeof (struct http_header), http_header_hash, http_header_compare, http_header_copy, NULL, http_header_free); key.name = (char *) name; key.value = NULL; ret = grecs_symtab_lookup_or_install (io->headers, &key, &install); if (!ret) { grecs_error (NULL, 0, _("cannot install header %s: %s"), name, strerror (errno)); return 1; } if (!install) grecs_free (ret->value); va_start (ap, fmt); grecs_vasprintf (&ret->value, &len, fmt, ap); va_end (ap); return 0; } static char * shttp_io_header_get (struct shttp_io *io, char const *name) { struct http_header key, *ret; if (!io->headers) return NULL; key.name = (char*) name; ret = grecs_symtab_lookup_or_install (io->headers, &key, NULL); return ret ? ret->value : NULL; } static void shttp_io_free (struct shttp_io *io) { grecs_symtab_free (io->headers); grecs_free (io->content); } static struct shttp_connection * shttp_connect (struct pies_url *url) { int fd; union pies_sockaddr_storage addr; socklen_t socklen; int flags; struct shttp_connection *conn; FILE *fp; if (strcmp (url->scheme, "unix") == 0 || strcmp (url->scheme, "file") == 0 || strcmp (url->scheme, "socket") == 0) { struct stat st; if (url->port) { grecs_error (NULL, 0, _("%s: invalid connection type: " "port is meaningless for UNIX sockets"), url->string); return NULL; } if (strlen (url->path) > sizeof addr.s_un.sun_path) { errno = EINVAL; grecs_error (NULL, 0, _("%s: UNIX socket name too long"), url->path); return NULL; } addr.s.sa_family = PF_UNIX; socklen = sizeof (addr.s_un); strcpy (addr.s_un.sun_path, url->path); if (stat (url->path, &st)) { if (errno != ENOENT) { grecs_error (NULL, 0, _("%s: cannot stat socket: %s"), url->path, strerror (errno)); return NULL; } } else { if (!S_ISSOCK (st.st_mode)) { grecs_error (NULL, 0, _("%s: not a socket"), url->path); return NULL; } } } else if (strcmp (url->scheme, "inet") == 0) { short pnum; struct hostent *hp; addr.s_in.sin_family = PF_INET; socklen = sizeof (addr.s_in); pnum = url->port ? url->port : 8080; hp = gethostbyname (url->host); if (!hp) { grecs_error (NULL, 0, _("%s: unknown host name"), url->string); return NULL; } addr.s_in.sin_family = hp->h_addrtype; switch (hp->h_addrtype) { case AF_INET: memmove (&addr.s_in.sin_addr, hp->h_addr, 4); addr.s_in.sin_port = htons (pnum); break; default: grecs_error (NULL, 0, _("%s: invalid connection type: " "unsupported address family"), url->string); return NULL; } } else { grecs_error (NULL, 0, _("%s: unsupported protocol"), url->string); return NULL; } fd = socket (addr.s.sa_family, SOCK_STREAM, 0); if (fd == -1) { grecs_error (NULL, 0, _("%s: unable to create new socket: %s"), url->string, strerror(errno)); return NULL; } if ((flags = fcntl (fd, F_GETFD, 0)) == -1 || fcntl (fd, F_SETFD, flags | FD_CLOEXEC) == -1) grecs_error (NULL, 0, _("%s: cannot set close-on-exec: %s"), url->string, strerror (errno)); // FIXME: Bind to local address if (connect (fd, &addr.s, socklen)) { grecs_error (NULL, 0, _("%s: cannot connect: %s"), url->string, strerror (errno)); close (fd); return NULL; } fp = fdopen (fd, "w+"); if (!fp) { grecs_error (NULL, 0, "fdopen: %s", strerror (errno)); close (fd); return NULL; } conn = grecs_zalloc (sizeof (*conn)); conn->fp = fp; if (pies_url_copy (&conn->url, url)) grecs_alloc_die (); netrc_scan (conn->url); return conn; } static void shttp_auth_free (struct shttp_connection *conn) { if (conn->b64auth) { memset (conn->b64auth, 0, strlen (conn->b64auth)); free (conn->b64auth); conn->b64auth = NULL; } } static void shttp_status_line_free (struct shttp_connection *conn) { free (conn->status_line[0]); free (conn->status_line[1]); free (conn->status_line[2]); conn->status_line[0] = NULL; conn->status_line[1] = NULL; conn->status_line[2] = NULL; } static void shttp_close (struct shttp_connection *conn) { fclose (conn->fp); pies_url_destroy (&conn->url); shttp_io_free (&conn->req); shttp_io_free (&conn->resp); grecs_free (conn->sendbuf.base); grecs_free (conn->readbuf.base); shttp_status_line_free (conn); shttp_auth_free (conn); json_value_free (conn->result); grecs_free (conn); } static void shttp_send_line (struct shttp_connection *conn, char const *fmt, ...) { va_list ap; va_start (ap, fmt); grecs_vasprintf (&conn->sendbuf.base, &conn->sendbuf.size, fmt, ap); va_end (ap); if (verbose > 1) printf ("> %s\n", conn->sendbuf.base); fprintf (conn->fp, "%s\r\n", conn->sendbuf.base); } static int shttp_read_line (struct shttp_connection *conn) { ssize_t rc; rc = grecs_getline (&conn->readbuf.base, &conn->readbuf.size, conn->fp); if (rc > 0) { if (conn->readbuf.base[--rc] == '\n') { if (rc > 0 && conn->readbuf.base[rc-1] == '\r') --rc; conn->readbuf.base[rc] = 0; } if (verbose > 1) printf ("< %s\n", conn->readbuf.base); return 0; } else if (rc == 0) { if (verbose > 1) printf ("\n"); return 1; } grecs_error (NULL, errno, _("HTTP read error")); exit (EX_UNAVAILABLE); } static int send_header (void *sym, void *data) { struct http_header *h = sym; struct shttp_connection *conn = data; shttp_send_line (conn, "%s: %s", h->name, h->value); return 0; } static void shttp_request_send (struct shttp_connection *conn, int method, char const *uri) { shttp_io_init (&conn->req); shttp_send_line (conn, "%s %s %s", method_names[method], uri, http_version); shttp_send_line (conn, "Host: %s", conn->url->host ? conn->url->host : "localhost"); if (conn->url->user) { shttp_auth_free (conn); grecs_asprintf (&conn->readbuf.base, &conn->readbuf.size, "%s:%s", conn->url->user, conn->url->passwd ? conn->url->passwd : 0); base64_encode_alloc (conn->readbuf.base, strlen (conn->readbuf.base), &conn->b64auth); pies_url_free_user (conn->url); pies_url_free_passwd (conn->url); } if (conn->b64auth) shttp_send_line (conn, "Authorization: Basic %s", conn->b64auth); grecs_symtab_enumerate (conn->req.headers, send_header, conn); if (conn->req.content_length) { shttp_send_line (conn, "Content-Length: %lu", conn->req.content_length); shttp_send_line (conn, "Content-Type: application/json"); } shttp_send_line (conn, ""); if (conn->req.content_length) { size_t s = fwrite (conn->req.content, 1, conn->req.content_length, conn->fp); if (s != conn->req.content_length) { grecs_error (NULL, 0, _("wrote %lu of %lu bytes of content"), (unsigned long) s, conn->req.content_length); exit (EX_UNAVAILABLE); } } } #define ISWS(c) ((c) == ' ' || (c) == '\t') static void shttp_save_header (struct shttp_connection *conn) { char *p; for (p = conn->readbuf.base; *p; p++) if (*p == ':') { *p++ = 0; p += strspn (p, " \t"); shttp_io_header_put (&conn->resp, conn->readbuf.base, "%s", p); return; } grecs_error (NULL, 0, _("malformed header line: %s"), conn->readbuf.base); exit (EX_UNAVAILABLE); } static void shttp_get_reply (struct shttp_connection *conn) { enum input_state { is_initial, is_headers, is_content } state = is_initial; char const *val; shttp_io_init (&conn->resp); shttp_status_line_free (conn); json_value_free (conn->result); conn->result = NULL; while (state != is_content) { if (shttp_read_line (conn)) break; if (state == is_initial) { unsigned long n; char *p; int rc; rc = strsplit3 (conn->readbuf.base, conn->status_line, 0); if (rc == -1) { grecs_error (NULL, errno, "strsplit3"); exit (EX_OSERR); } else if (rc == 1) { grecs_error (NULL, 0, _("unexpected reply: %s"), conn->readbuf.base); exit (EX_UNAVAILABLE); } if (strcmp (conn->status_line[0], http_version)) { grecs_error (NULL, 0, _("unsupported HTTP version: %s"), conn->status_line[0]); exit (EX_UNAVAILABLE); } n = strtoul (conn->status_line[1], &p, 0); if (*p || n > 599) { grecs_error (NULL, 0, _("bad reply status: %s"), conn->readbuf.base); exit (EX_UNAVAILABLE); } conn->resp.code = n; state = is_headers; } else if (state == is_headers) { if (conn->readbuf.base[0]) shttp_save_header (conn); else state = is_content; } } val = shttp_io_header_get (&conn->resp, "content-length"); if (val) { ssize_t rc; char *p; unsigned long len = strtoul (val, &p, 10); if (*p) { grecs_error (NULL, 0, _("bad content length: %s"), val); exit (EX_UNAVAILABLE); } if (len) { conn->resp.content = grecs_malloc (len + 1); conn->resp.content_length = len; rc = fread (conn->resp.content, len, 1, conn->fp); if (rc != 1) { grecs_error (NULL, ferror (conn->fp) ? errno : 0, _("content read error")); exit (EX_UNAVAILABLE); } val = shttp_io_header_get (&conn->resp, "content-type"); if (!val) grecs_error (NULL, 0, _("no content type specified")); else if (strcmp (val, "application/json")) grecs_error (NULL, 0, _("unsupported content type: %s"), val); else { conn->result = json_parse_string (conn->resp.content, conn->resp.content_length); if (!conn->result) { grecs_error (&json_err_locus, 0, "%s", json_err_diag); grecs_error (NULL, 0, _("original json was: %s"), conn->resp.content); grecs_error (NULL, 0, _("original status line: %s %s %s"), conn->status_line[0], conn->status_line[1], conn->status_line[2]); exit (EX_UNAVAILABLE); } } } } } static void stderr_writer (void *closure, char const *text, size_t len) { fwrite (text, len, 1, stderr); } static void shttp_format_result (struct shttp_connection *conn) { struct json_format fmt = { .indent = 2, .precision = 0, .write = stderr_writer, }; fprintf (stderr, "%s: raw JSON reply follows:\n", program_name); json_format_value (conn->result, &fmt); } static void shttp_fatal (struct shttp_connection *conn) { struct json_value *jv; if (conn->result && (jv = json_value_lookup (conn->result, "error_message"))) { if (jv->type == json_string) grecs_error (NULL, 0, "%s", jv->v.s); else shttp_format_result (conn); } else grecs_error (NULL, 0, "%s", conn->status_line[2]); exit (1); } static void echo_off (struct termios *stored_settings) { struct termios new_settings; tcgetattr (0, stored_settings); new_settings = *stored_settings; new_settings.c_lflag &= (~ECHO); tcsetattr (0, TCSANOW, &new_settings); } static void echo_on (struct termios *stored_settings) { tcsetattr (0, TCSANOW, stored_settings); } static char * getans (int echo, const char *fmt, ...) { struct termios stored_settings; char *reply = NULL; size_t reply_len = 0; size_t len; va_list ap; va_start (ap, fmt); vprintf (fmt, ap); va_end (ap); printf (": "); fflush (stdout); if (!echo) echo_off (&stored_settings); grecs_getline (&reply, &reply_len, stdin); if (!echo) { echo_on (&stored_settings); putchar ('\n'); fflush (stdout); } len = strlen (reply); if (len) reply[len - 1] = 0; return reply; } static void shttp_get_credentials (struct shttp_connection *conn) { char const *val = shttp_io_header_get (&conn->resp, "WWW-Authenticate"); char *realm = NULL; if (val) { struct wordsplit ws; if (wordsplit (val, &ws, WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_QUOTE | WRDSF_SQUEEZE_DELIMS)) grecs_error (NULL, 0, "wordsplit: %s", wordsplit_strerror (&ws)); else { if (ws.ws_wordc > 0) { size_t i; if (strcmp (ws.ws_wordv[0], "Basic")) { grecs_error (NULL, 0, _("unsupported authentication type: %s"), ws.ws_wordv[0]); shttp_fatal (conn); } for (i = 1; i < ws.ws_wordc; i++) if (strncmp (ws.ws_wordv[i], "realm=", 6) == 0) { realm = grecs_strdup (ws.ws_wordv[i] + 6); break; } } wordsplit_free (&ws); } } if (!realm) realm = grecs_strdup ("Server"); conn->url->user = getans (1, "%s user", realm); conn->url->passwd = getans (0, "%s password", realm); grecs_free (realm); } static void shttp_process (struct shttp_connection *conn, int method, char const *uri) { for (;;) { shttp_request_send (conn, method, uri); shttp_get_reply (conn); if (conn->resp.code / 100 == 2) return; if (conn->resp.code == 401 && isatty (fileno (stdin))) shttp_get_credentials (conn); else shttp_fatal (conn); } } static int com_list (struct shttp_connection *conn, int argc, char **argv) { shttp_process (conn, METH_GET, "/programs"); return 0; } static int com_stop (struct shttp_connection *conn, int argc, char **argv) { abort (); return 0; } static int com_start (struct shttp_connection *conn, int argc, char **argv) { abort (); return 0; } static int com_restart (struct shttp_connection *conn, int argc, char **argv) { abort (); return 0; } static int com_shutdown (struct shttp_connection *conn, int argc, char **argv) { abort (); return 0; } static int com_reboot (struct shttp_connection *conn, int argc, char **argv) { abort (); return 0; } typedef int (*ctlcom_t) (struct shttp_connection *, int, char **); struct comtab { char const *name; ctlcom_t command; }; static struct comtab comtab[] = { { "list", com_list }, { "stop", com_stop }, { "start", com_start }, { "restart", com_restart }, { "shutdown", com_shutdown }, { "reboot", com_reboot }, { NULL } }; static struct comtab * find_command (char const *name) { struct comtab *p, *match = NULL; size_t match_count = 0; size_t len = strlen (name); for (p = comtab; p->name; p++) if (len <= strlen (p->name) && memcmp (p->name, name, len) == 0) { if (!match) { match = p; ++match_count; } else { if (match_count == 1) fprintf (stderr, _("%s: ambiguous command %s:\n"), program_name, name); ++match_count; fprintf (stderr, " %s\n", p->name); } } if (match_count == 0) { grecs_error (NULL, 0, _("unknown command: %s"), name); exit (EX_USAGE); } else if (match_count > 1) exit (EX_USAGE); return match; } int main (int argc, char **argv) { int i, rc; struct shttp_connection *conn; struct comtab *cmd; set_program_name (argv[0]); #ifdef ENABLE_NLS setlocale (LC_ALL, ""); bindtextdomain (PACKAGE, LOCALEDIR); textdomain (PACKAGE); #endif grecs_print_diag_fun = piesctl_diag; parse_options (argc, argv, &i); argc -= i; argv += i; parse_config (); conn = shttp_connect (url); if (!conn) return EX_UNAVAILABLE; cmd = find_command (argv[0]); argv[0] = (char*) cmd->name; rc = cmd->command (conn, argc, argv); shttp_close (conn); return rc; }