/* engine-g13.c - G13 engine. Copyright (C) 2000 Werner Koch (dd9jn) Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007, 2009 g10 Code GmbH This file is part of GPGME. GPGME is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. GPGME 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #if HAVE_CONFIG_H #include #endif #include #include #ifdef HAVE_SYS_TYPES_H # include #endif #include #ifdef HAVE_UNISTD_H # include #endif #ifdef HAVE_LOCALE_H #include #endif #include /* FIXME */ #include #include "gpgme.h" #include "util.h" #include "ops.h" #include "wait.h" #include "priv-io.h" #include "sema.h" #include "assuan.h" #include "debug.h" #include "engine-backend.h" typedef struct { int fd; /* FD we talk about. */ int server_fd;/* Server FD for this connection. */ int dir; /* Inbound/Outbound, maybe given implicit? */ void *data; /* Handler-specific data. */ void *tag; /* ID from the user for gpgme_remove_io_callback. */ char server_fd_str[15]; /* Same as SERVER_FD but as a string. We need this because _gpgme_io_fd2str can't be used on a closed descriptor. */ } iocb_data_t; struct engine_g13 { assuan_context_t assuan_ctx; int lc_ctype_set; int lc_messages_set; iocb_data_t status_cb; struct gpgme_io_cbs io_cbs; /* User provided callbacks. */ struct { gpgme_assuan_data_cb_t data_cb; void *data_cb_value; gpgme_assuan_inquire_cb_t inq_cb; void *inq_cb_value; gpgme_assuan_status_cb_t status_cb; void *status_cb_value; } user; }; typedef struct engine_g13 *engine_g13_t; static void g13_io_event (void *engine, gpgme_event_io_t type, void *type_data); static char * g13_get_version (const char *file_name) { return _gpgme_get_program_version (file_name ? file_name : _gpgme_get_g13_path ()); } static const char * g13_get_req_version (void) { return NEED_G13_VERSION; } static void close_notify_handler (int fd, void *opaque) { engine_g13_t g13 = opaque; assert (fd != -1); if (g13->status_cb.fd == fd) { if (g13->status_cb.tag) (*g13->io_cbs.remove) (g13->status_cb.tag); g13->status_cb.fd = -1; g13->status_cb.tag = NULL; } } /* This is the default inquiry callback. We use it to handle the Pinentry notifications. */ static gpgme_error_t default_inq_cb (engine_g13_t g13, const char *keyword, const char *args) { gpg_error_t err; if (!strcmp (keyword, "PINENTRY_LAUNCHED")) { _gpgme_allow_set_foreground_window ((pid_t)strtoul (args, NULL, 10)); } if (g13->user.inq_cb) { gpgme_data_t data = NULL; err = g13->user.inq_cb (g13->user.inq_cb_value, keyword, args, &data); if (!err && data) { /* FIXME: Returning data is not yet implemented. However we need to allow the caller to cleanup his data object. Thus we run the callback in finish mode immediately. */ err = g13->user.inq_cb (g13->user.inq_cb_value, NULL, NULL, &data); } } else err = 0; return err; } static gpgme_error_t g13_cancel (void *engine) { engine_g13_t g13 = engine; if (!g13) return gpg_error (GPG_ERR_INV_VALUE); if (g13->status_cb.fd != -1) _gpgme_io_close (g13->status_cb.fd); if (g13->assuan_ctx) { assuan_release (g13->assuan_ctx); g13->assuan_ctx = NULL; } return 0; } static gpgme_error_t g13_cancel_op (void *engine) { engine_g13_t g13 = engine; if (!g13) return gpg_error (GPG_ERR_INV_VALUE); if (g13->status_cb.fd != -1) _gpgme_io_close (g13->status_cb.fd); return 0; } static void g13_release (void *engine) { engine_g13_t g13 = engine; if (!g13) return; g13_cancel (engine); free (g13); } static gpgme_error_t g13_new (void **engine, const char *file_name, const char *home_dir) { gpgme_error_t err = 0; engine_g13_t g13; int argc; const char *argv[5]; char *dft_display = NULL; char dft_ttyname[64]; char *dft_ttytype = NULL; char *optstr; g13 = calloc (1, sizeof *g13); if (!g13) return gpg_error_from_errno (errno); g13->status_cb.fd = -1; g13->status_cb.dir = 1; g13->status_cb.tag = 0; g13->status_cb.data = g13; argc = 0; argv[argc++] = "g13"; if (home_dir) { argv[argc++] = "--homedir"; argv[argc++] = home_dir; } argv[argc++] = "--server"; argv[argc++] = NULL; err = assuan_new_ext (&g13->assuan_ctx, GPG_ERR_SOURCE_GPGME, &_gpgme_assuan_malloc_hooks, _gpgme_assuan_log_cb, NULL); if (err) goto leave; assuan_ctx_set_system_hooks (g13->assuan_ctx, &_gpgme_assuan_system_hooks); #if USE_DESCRIPTOR_PASSING err = assuan_pipe_connect (g13->assuan_ctx, file_name ? file_name : _gpgme_get_g13_path (), argv, NULL, NULL, NULL, ASSUAN_PIPE_CONNECT_FDPASSING); #else err = assuan_pipe_connect (g13->assuan_ctx, file_name ? file_name : _gpgme_get_g13_path (), argv, NULL, NULL, NULL, 0); #endif if (err) goto leave; err = _gpgme_getenv ("DISPLAY", &dft_display); if (err) goto leave; if (dft_display) { if (asprintf (&optstr, "OPTION display=%s", dft_display) < 0) { free (dft_display); err = gpg_error_from_errno (errno); goto leave; } free (dft_display); err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL, NULL, NULL, NULL, NULL); free (optstr); if (err) goto leave; } if (isatty (1)) { int rc; rc = ttyname_r (1, dft_ttyname, sizeof (dft_ttyname)); if (rc) { err = gpg_error_from_errno (rc); goto leave; } else { if (asprintf (&optstr, "OPTION ttyname=%s", dft_ttyname) < 0) { err = gpg_error_from_errno (errno); goto leave; } err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL, NULL, NULL, NULL, NULL); free (optstr); if (err) goto leave; err = _gpgme_getenv ("TERM", &dft_ttytype); if (err) goto leave; if (dft_ttytype) { if (asprintf (&optstr, "OPTION ttytype=%s", dft_ttytype) < 0) { free (dft_ttytype); err = gpg_error_from_errno (errno); goto leave; } free (dft_ttytype); err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL, NULL, NULL, NULL, NULL); free (optstr); if (err) goto leave; } } } #ifdef HAVE_W32_SYSTEM /* Under Windows we need to use AllowSetForegroundWindow. Tell g13 to tell us when it needs it. */ if (!err) { err = assuan_transact (g13->assuan_ctx, "OPTION allow-pinentry-notify", NULL, NULL, NULL, NULL, NULL, NULL); if (gpg_err_code (err) == GPG_ERR_UNKNOWN_OPTION) err = 0; /* This is a new feature of g13. */ } #endif /*HAVE_W32_SYSTEM*/ leave: if (err) g13_release (g13); else *engine = g13; return err; } static gpgme_error_t g13_set_locale (void *engine, int category, const char *value) { engine_g13_t g13 = engine; gpgme_error_t err; char *optstr; char *catstr; /* FIXME: If value is NULL, we need to reset the option to default. But we can't do this. So we error out here. G13 needs support for this. */ if (0) ; #ifdef LC_CTYPE else if (category == LC_CTYPE) { catstr = "lc-ctype"; if (!value && g13->lc_ctype_set) return gpg_error (GPG_ERR_INV_VALUE); if (value) g13->lc_ctype_set = 1; } #endif #ifdef LC_MESSAGES else if (category == LC_MESSAGES) { catstr = "lc-messages"; if (!value && g13->lc_messages_set) return gpg_error (GPG_ERR_INV_VALUE); if (value) g13->lc_messages_set = 1; } #endif /* LC_MESSAGES */ else return gpg_error (GPG_ERR_INV_VALUE); /* FIXME: Reset value to default. */ if (!value) return 0; if (asprintf (&optstr, "OPTION %s=%s", catstr, value) < 0) err = gpg_error_from_errno (errno); else { err = assuan_transact (g13->assuan_ctx, optstr, NULL, NULL, NULL, NULL, NULL, NULL); free (optstr); } return err; } #if USE_DESCRIPTOR_PASSING static gpgme_error_t g13_assuan_simple_command (assuan_context_t ctx, char *cmd, engine_status_handler_t status_fnc, void *status_fnc_value) { gpg_error_t err; char *line; size_t linelen; err = assuan_write_line (ctx, cmd); if (err) return err; do { err = assuan_read_line (ctx, &line, &linelen); if (err) return err; if (*line == '#' || !linelen) continue; if (linelen >= 2 && line[0] == 'O' && line[1] == 'K' && (line[2] == '\0' || line[2] == ' ')) return 0; else if (linelen >= 4 && line[0] == 'E' && line[1] == 'R' && line[2] == 'R' && line[3] == ' ') err = atoi (&line[4]); else if (linelen >= 2 && line[0] == 'S' && line[1] == ' ') { char *rest; rest = strchr (line + 2, ' '); if (!rest) rest = line + linelen; /* set to an empty string */ else *(rest++) = 0; /* Nothing to do with status lines. */ } else err = gpg_error (GPG_ERR_GENERAL); } while (!err); return err; } #endif static gpgme_error_t status_handler (void *opaque, int fd) { struct io_cb_data *data = (struct io_cb_data *) opaque; engine_g13_t g13 = (engine_g13_t) data->handler_value; gpgme_error_t err = 0; char *line; size_t linelen; do { err = assuan_read_line (g13->assuan_ctx, &line, &linelen); if (err) { /* Try our best to terminate the connection friendly. */ /* assuan_write_line (g13->assuan_ctx, "BYE"); */ TRACE2 (DEBUG_CTX, "gpgme:status_handler", g13, "fd 0x%x: error reading assuan line: %s", fd, gpg_strerror (err)); } else if (linelen >= 3 && line[0] == 'E' && line[1] == 'R' && line[2] == 'R' && (line[3] == '\0' || line[3] == ' ')) { if (line[3] == ' ') err = atoi (&line[4]); if (! err) err = gpg_error (GPG_ERR_GENERAL); TRACE2 (DEBUG_CTX, "gpgme:status_handler", g13, "fd 0x%x: ERR line: %s", fd, err ? gpg_strerror (err) : "ok"); /* Command execution errors are not fatal, as we use a session based protocol. */ data->op_err = err; /* The caller will do the rest (namely, call cancel_op, which closes status_fd). */ return 0; } else if (linelen >= 2 && line[0] == 'O' && line[1] == 'K' && (line[2] == '\0' || line[2] == ' ')) { TRACE1 (DEBUG_CTX, "gpgme:status_handler", g13, "fd 0x%x: OK line", fd); _gpgme_io_close (g13->status_cb.fd); return 0; } else if (linelen > 2 && line[0] == 'D' && line[1] == ' ') { /* We are using the colon handler even for plain inline data - strange name for that function but for historic reasons we keep it. */ /* FIXME We can't use this for binary data because we assume this is a string. For the current usage of colon output it is correct. */ char *src = line + 2; char *end = line + linelen; char *dst = src; linelen = 0; while (src < end) { if (*src == '%' && src + 2 < end) { /* Handle escaped characters. */ ++src; *dst++ = _gpgme_hextobyte (src); src += 2; } else *dst++ = *src++; linelen++; } src = line + 2; if (linelen && g13->user.data_cb) err = g13->user.data_cb (g13->user.data_cb_value, src, linelen); else err = 0; TRACE2 (DEBUG_CTX, "gpgme:g13_status_handler", g13, "fd 0x%x: D inlinedata; status from cb: %s", fd, (g13->user.data_cb ? (err? gpg_strerror (err):"ok"):"no callback")); } else if (linelen > 2 && line[0] == 'S' && line[1] == ' ') { char *src; char *args; src = line + 2; while (*src == ' ') src++; args = strchr (line + 2, ' '); if (!args) args = line + linelen; /* set to an empty string */ else *(args++) = 0; while (*args == ' ') args++; if (g13->user.status_cb) err = g13->user.status_cb (g13->user.status_cb_value, src, args); else err = 0; TRACE3 (DEBUG_CTX, "gpgme:g13_status_handler", g13, "fd 0x%x: S line (%s) - status from cb: %s", fd, line+2, (g13->user.status_cb ? (err? gpg_strerror (err):"ok"):"no callback")); } else if (linelen >= 7 && line[0] == 'I' && line[1] == 'N' && line[2] == 'Q' && line[3] == 'U' && line[4] == 'I' && line[5] == 'R' && line[6] == 'E' && (line[7] == '\0' || line[7] == ' ')) { char *src; char *args; for (src=line+7; *src == ' '; src++) ; args = strchr (src, ' '); if (!args) args = line + linelen; /* Let it point to an empty string. */ else *(args++) = 0; while (*args == ' ') args++; err = default_inq_cb (g13, src, args); if (!err) { /* Flush and send END. */ err = assuan_send_data (g13->assuan_ctx, NULL, 0); } else if (gpg_err_code (err) == GPG_ERR_ASS_CANCELED) { /* Flush and send CANcel. */ err = assuan_send_data (g13->assuan_ctx, NULL, 1); } assuan_write_line (g13->assuan_ctx, "END"); } } while (!err && assuan_pending_line (g13->assuan_ctx)); return err; } static gpgme_error_t add_io_cb (engine_g13_t g13, iocb_data_t *iocbd, gpgme_io_cb_t handler) { gpgme_error_t err; TRACE_BEG2 (DEBUG_ENGINE, "engine-g13:add_io_cb", g13, "fd %d, dir %d", iocbd->fd, iocbd->dir); err = (*g13->io_cbs.add) (g13->io_cbs.add_priv, iocbd->fd, iocbd->dir, handler, iocbd->data, &iocbd->tag); if (err) return TRACE_ERR (err); if (!iocbd->dir) /* FIXME Kludge around poll() problem. */ err = _gpgme_io_set_nonblocking (iocbd->fd); return TRACE_ERR (err); } static gpgme_error_t start (engine_g13_t g13, const char *command) { gpgme_error_t err; assuan_fd_t afdlist[5]; int fdlist[5]; int nfds; int i; /* We need to know the fd used by assuan for reads. We do this by using the assumption that the first returned fd from assuan_get_active_fds() is always this one. */ nfds = assuan_get_active_fds (g13->assuan_ctx, 0 /* read fds */, afdlist, DIM (afdlist)); if (nfds < 1) return gpg_error (GPG_ERR_GENERAL); /* FIXME */ /* For now... */ for (i = 0; i < nfds; i++) fdlist[i] = (int) afdlist[i]; /* We "duplicate" the file descriptor, so we can close it here (we can't close fdlist[0], as that is closed by libassuan, and closing it here might cause libassuan to close some unrelated FD later). Alternatively, we could special case status_fd and register/unregister it manually as needed, but this increases code duplication and is more complicated as we can not use the close notifications etc. A third alternative would be to let Assuan know that we closed the FD, but that complicates the Assuan interface. */ g13->status_cb.fd = _gpgme_io_dup (fdlist[0]); if (g13->status_cb.fd < 0) return gpg_error_from_syserror (); if (_gpgme_io_set_close_notify (g13->status_cb.fd, close_notify_handler, g13)) { _gpgme_io_close (g13->status_cb.fd); g13->status_cb.fd = -1; return gpg_error (GPG_ERR_GENERAL); } err = add_io_cb (g13, &g13->status_cb, status_handler); if (!err) err = assuan_write_line (g13->assuan_ctx, command); if (!err) g13_io_event (g13, GPGME_EVENT_START, NULL); return err; } #if USE_DESCRIPTOR_PASSING static gpgme_error_t g13_reset (void *engine) { engine_g13_t g13 = engine; /* We must send a reset because we need to reset the list of signers. Note that RESET does not reset OPTION commands. */ return g13_assuan_simple_command (g13->assuan_ctx, "RESET", NULL, NULL); } #endif static gpgme_error_t g13_transact (void *engine, const char *command, gpgme_assuan_data_cb_t data_cb, void *data_cb_value, gpgme_assuan_inquire_cb_t inq_cb, void *inq_cb_value, gpgme_assuan_status_cb_t status_cb, void *status_cb_value) { engine_g13_t g13 = engine; gpgme_error_t err; if (!g13 || !command || !*command) return gpg_error (GPG_ERR_INV_VALUE); g13->user.data_cb = data_cb; g13->user.data_cb_value = data_cb_value; g13->user.inq_cb = inq_cb; g13->user.inq_cb_value = inq_cb_value; g13->user.status_cb = status_cb; g13->user.status_cb_value = status_cb_value; err = start (g13, command); return err; } static void g13_set_io_cbs (void *engine, gpgme_io_cbs_t io_cbs) { engine_g13_t g13 = engine; g13->io_cbs = *io_cbs; } static void g13_io_event (void *engine, gpgme_event_io_t type, void *type_data) { engine_g13_t g13 = engine; TRACE3 (DEBUG_ENGINE, "gpgme:g13_io_event", g13, "event %p, type %d, type_data %p", g13->io_cbs.event, type, type_data); if (g13->io_cbs.event) (*g13->io_cbs.event) (g13->io_cbs.event_priv, type, type_data); } struct engine_ops _gpgme_engine_ops_g13 = { /* Static functions. */ _gpgme_get_g13_path, NULL, g13_get_version, g13_get_req_version, g13_new, /* Member functions. */ g13_release, #if USE_DESCRIPTOR_PASSING g13_reset, #else NULL, /* reset */ #endif NULL, /* set_status_handler */ NULL, /* set_command_handler */ NULL, /* set_colon_line_handler */ g13_set_locale, NULL, /* set_protocol */ NULL, /* decrypt */ NULL, /* decrypt_verify */ NULL, /* delete */ NULL, /* edit */ NULL, /* encrypt */ NULL, /* encrypt_sign */ NULL, /* export */ NULL, /* export_ext */ NULL, /* genkey */ NULL, /* import */ NULL, /* keylist */ NULL, /* keylist_ext */ NULL, /* sign */ NULL, /* trustlist */ NULL, /* verify */ NULL, /* getauditlog */ g13_transact, NULL, /* conf_load */ NULL, /* conf_save */ g13_set_io_cbs, g13_io_event, g13_cancel, g13_cancel_op, };