/* gpgme-tool.c - GnuPG Made Easy. Copyright (C) 2000 Werner Koch (dd9jn) Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007 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 #include #include #include #include #include #include #ifdef HAVE_ARGP_H #include #endif #include "gpgme.h" #ifndef HAVE_ARGP_H /* Minimal argp implementation. */ /* Differences to ARGP: argp_program_version: Required. argp_program_bug_address: Required. argp_program_version_hook: Not supported. argp_err_exit_status: Required. struct argp: Children and help_filter not supported. argp_domain: Not supported. struct argp_option: Group not supported. Options are printed in order given. Flags OPTION_ALIAS, OPTION_DOC and OPTION_NO_USAGE are not supported. argp_parse: No flags are supported (ARGP_PARSE_ARGV0, ARGP_NO_ERRS, ARGP_NO_ARGS, ARGP_IN_ORDER, ARGP_NO_HELP, ARGP_NO_EXIT, ARGP_LONG_ONLY, ARGP_SILENT). ARGP must not be NULL. argp_help: Flag ARGP_HELP_LONG_ONLY not supported. argp_state: argc, argv, next may not be modified and should not be used. */ extern const char *argp_program_version; extern const char *argp_program_bug_address; extern error_t argp_err_exit_status; struct argp_option { const char *name; int key; const char *arg; #define OPTION_ARG_OPTIONAL 0x1 #define OPTION_HIDDEN 0x2 int flags; const char *doc; int group; }; struct argp; struct argp_state { const struct argp *const root_argp; int argc; char **argv; int next; unsigned flags; unsigned arg_num; int quoted; void *input; void **child_inputs; void *hook; char *name; FILE *err_stream; FILE *out_stream; void *pstate; }; #define ARGP_ERR_UNKNOWN E2BIG #define ARGP_KEY_ARG 0 #define ARGP_KEY_ARGS 0x1000006 #define ARGP_KEY_END 0x1000001 #define ARGP_KEY_NO_ARGS 0x1000002 #define ARGP_KEY_INIT 0x1000003 #define ARGP_KEY_FINI 0x1000007 #define ARGP_KEY_SUCCESS 0x1000004 #define ARGP_KEY_ERROR 0x1000005 typedef error_t (*argp_parser_t) (int key, char *arg, struct argp_state *state); struct argp { const struct argp_option *options; argp_parser_t parser; const char *args_doc; const char *doc; const struct argp_child *children; char *(*help_filter) (int key, const char *text, void *input); const char *argp_domain; }; #define ARGP_HELP_USAGE ARGP_HELP_SHORT_USAGE #define ARGP_HELP_SHORT_USAGE 0x02 #define ARGP_HELP_SEE 0x04 #define ARGP_HELP_LONG 0x08 #define ARGP_HELP_PRE_DOC 0x10 #define ARGP_HELP_POST_DOC 0x20 #define ARGP_HELP_DOC (ARGP_HELP_PRE_DOC | ARGP_HELP_POST_DOC) #define ARGP_HELP_BUG_ADDR 0x40 #define ARGP_HELP_EXIT_ERR 0x100 #define ARGP_HELP_EXIT_OK 0x200 #define ARGP_HELP_STD_ERR (ARGP_HELP_SEE | ARGP_HELP_EXIT_ERR) #define ARGP_HELP_STD_USAGE \ (ARGP_HELP_SHORT_USAGE | ARGP_HELP_SEE | ARGP_HELP_EXIT_ERR) #define ARGP_HELP_STD_HELP \ (ARGP_HELP_SHORT_USAGE | ARGP_HELP_LONG | ARGP_HELP_EXIT_OK \ | ARGP_HELP_DOC | ARGP_HELP_BUG_ADDR) char * _argp_pname (char *name) { char *pname = name; char *bname = strrchr (pname, '/'); if (! bname) bname = strrchr (pname, '\\'); if (bname) pname = bname + 1; return pname; } void _argp_state_help (const struct argp *argp, const struct argp_state *state, FILE *stream, unsigned flags, char *name) { if (state) name = state->name; if (flags & ARGP_HELP_SHORT_USAGE) fprintf (stream, "Usage: %s [OPTIONS...] %s\n", name, argp->args_doc); if (flags & ARGP_HELP_SEE) fprintf (stream, "Try `%s --help' or `%s --usage' for more information.\n", name, name); if (flags & ARGP_HELP_PRE_DOC) { char buf[1024]; char *end; strncpy (buf, argp->doc, sizeof (buf)); buf[sizeof (buf) - 1] = '\0'; end = strchr (buf, '\v'); if (end) *end = '\0'; fprintf (stream, "%s\n%s", buf, buf[0] ? "\n" : ""); } if (flags & ARGP_HELP_LONG) { const struct argp_option *opt = argp->options; while (opt->key) { #define NSPACES 29 char spaces[NSPACES + 1] = " "; int len = 0; fprintf (stream, " "); len += 2; if (isascii (opt->key)) { fprintf (stream, "-%c", opt->key); len += 2; if (opt->name) { fprintf (stream, ", "); len += 2; } } if (opt->name) { fprintf (stream, "--%s", opt->name); len += 2 + strlen (opt->name); } if (opt->arg && (opt->flags & OPTION_ARG_OPTIONAL)) { fprintf (stream, "[=%s]", opt->arg); len += 3 + strlen (opt->arg); } else if (opt->arg) { fprintf (stream, "=%s", opt->arg); len += 1 + strlen (opt->arg); } if (len >= NSPACES) len = NSPACES - 1; spaces[NSPACES - len] = '\0'; fprintf (stream, "%s%s\n", spaces, opt->doc); opt++; } fprintf (stream, " -?, --help Give this help list\n"); fprintf (stream, " --usage Give a short usage " "message\n"); } if (flags & ARGP_HELP_POST_DOC) { char buf[1024]; char *end; strncpy (buf, argp->doc, sizeof (buf)); buf[sizeof (buf) - 1] = '\0'; end = strchr (buf, '\v'); if (end) { end++; if (*end) fprintf (stream, "\n%s\n", end); } fprintf (stream, "\nMandatory or optional arguments to long options are also mandatory or optional\n"); fprintf (stream, "for any corresponding short options.\n"); } if (flags & ARGP_HELP_BUG_ADDR) fprintf (stream, "\nReport bugs to %s.\n", argp_program_bug_address); if (flags & ARGP_HELP_EXIT_ERR) exit (argp_err_exit_status); if (flags & ARGP_HELP_EXIT_OK) exit (0); } void argp_usage (const struct argp_state *state) { _argp_state_help (state->root_argp, state, state->err_stream, ARGP_HELP_STD_USAGE, state->name); } void argp_state_help (const struct argp_state *state, FILE *stream, unsigned flags) { _argp_state_help (state->root_argp, state, stream, flags, state->name); } void argp_error (const struct argp_state *state, const char *fmt, ...) { va_list ap; fprintf (state->err_stream, "%s: ", state->name); va_start (ap, fmt); vfprintf (state->err_stream, fmt, ap); va_end (ap); fprintf (state->err_stream, "\n"); argp_state_help (state, state->err_stream, ARGP_HELP_STD_ERR); exit (argp_err_exit_status); } void argp_help (const struct argp *argp, FILE *stream, unsigned flags, char *name) { _argp_state_help (argp, NULL, stream, flags, name); } error_t argp_parse (const struct argp *argp, int argc, char **argv, unsigned flags, int *arg_index, void *input) { int rc = 0; struct argp_state state = { argp, argc, argv, 1, flags, 0, 0, input, NULL, NULL, _argp_pname (argv[0]), stderr, stdout, NULL }; /* All non-option arguments are collected at the beginning of &argv[1] during processing. This is a counter for their number. */ int non_opt_args = 0; rc = argp->parser (ARGP_KEY_INIT, NULL, &state); if (rc && rc != ARGP_ERR_UNKNOWN) goto argperror; while (state.next < state.argc - non_opt_args) { int idx = state.next; state.next++; if (! strcasecmp (state.argv[idx], "--")) { state.quoted = idx; continue; } if (state.quoted || state.argv[idx][0] != '-') { char *arg_saved = state.argv[idx]; non_opt_args++; memmove (&state.argv[idx], &state.argv[idx + 1], (state.argc - 1 - idx) * sizeof (char *)); state.argv[argc - 1] = arg_saved; state.next--; } else if (! strcasecmp (state.argv[idx], "--help") || !strcmp (state.argv[idx], "-?")) { argp_state_help (&state, state.out_stream, ARGP_HELP_STD_HELP); } else if (! strcasecmp (state.argv[idx], "--usage")) { argp_state_help (&state, state.out_stream, ARGP_HELP_USAGE | ARGP_HELP_EXIT_OK); } else if (! strcasecmp (state.argv[idx], "--version") || !strcmp (state.argv[idx], "-V")) { fprintf (state.out_stream, "%s\n", argp_program_version); exit (0); } else { /* Search for option and call parser with its KEY. */ int key = ARGP_KEY_ARG; /* Just some dummy value. */ const struct argp_option *opt = argp->options; char *arg = NULL; int found = 0; /* Check for --opt=value syntax. */ arg = strchr (state.argv[idx], '='); if (arg) { *arg = '\0'; arg++; } if (state.argv[idx][1] != '-') key = state.argv[idx][1]; while (! found && opt->key) { if (key == opt->key || (key == ARGP_KEY_ARG && ! strcasecmp (&state.argv[idx][2], opt->name))) { if (arg && !opt->arg) argp_error (&state, "Option %s does not take an argument", state.argv[idx]); if (opt->arg && state.next < state.argc && state.argv[idx + 1][0] != '-') { arg = state.argv[idx + 1]; state.next++; } if (opt->arg && !(opt->flags & OPTION_ARG_OPTIONAL)) argp_error (&state, "Option %s requires an argument", state.argv[idx]); rc = argp->parser (opt->key, arg, &state); if (rc == ARGP_ERR_UNKNOWN) break; else if (rc) goto argperror; found = 1; } opt++; } if (! found) argp_error (&state, "Unknown option %s", state.argv[idx]); } } while (state.next < state.argc) { /* Call parser for all non-option args. */ int idx = state.next; state.next++; rc = argp->parser (ARGP_KEY_ARG, state.argv[idx], &state); if (rc && rc != ARGP_ERR_UNKNOWN) goto argperror; if (rc == ARGP_ERR_UNKNOWN) { int old_next = state.next; rc = argp->parser (ARGP_KEY_ARGS, NULL, &state); if (rc == ARGP_ERR_UNKNOWN) { argp_error (&state, "Too many arguments", state.argv[idx]); goto argperror; } if (! rc && state.next == old_next) { state.arg_num += state.argc - state.next; state.next = state.argc; } } else state.arg_num++; } if (state.arg_num == 0) { rc = argp->parser (ARGP_KEY_NO_ARGS, NULL, &state); if (rc && rc != ARGP_ERR_UNKNOWN) goto argperror; } if (state.next == state.argc) { rc = argp->parser (ARGP_KEY_END, NULL, &state); if (rc && rc != ARGP_ERR_UNKNOWN) goto argperror; } rc = argp->parser (ARGP_KEY_FINI, NULL, &state); if (rc && rc != ARGP_ERR_UNKNOWN) goto argperror; rc = 0; argp->parser (ARGP_KEY_SUCCESS, NULL, &state); argperror: if (rc) { argp_error (&state, "unexpected error: %s", strerror (rc)); argp->parser (ARGP_KEY_ERROR, NULL, &state); } argp->parser (ARGP_KEY_FINI, NULL, &state); if (arg_index) *arg_index = state.next - 1; return 0; } #endif /* SUPPORT. */ FILE *log_stream; char *program_name = "gpgme-tool"; void log_init (void) { log_stream = stderr; } void log_error (int status, gpg_error_t errnum, const char *fmt, ...) { va_list ap; fprintf (log_stream, "%s: ", program_name); va_start (ap, fmt); vfprintf (log_stream, fmt, ap); va_end (ap); if (errnum) fprintf (log_stream, ": %s <%s>", gpg_strerror (errnum), gpg_strsource (errnum)); fprintf (log_stream, "\n"); if (status) exit (status); } typedef enum status { STATUS_PROTOCOL, STATUS_PROGRESS, STATUS_ENGINE, STATUS_ARMOR, STATUS_TEXTMODE, STATUS_INCLUDE_CERTS, STATUS_KEYLIST_MODE, STATUS_ENCRYPT_RESULT } status_t; const char *status_string[] = { "PROTOCOL", "PROGRESS", "ENGINE", "ARMOR", "TEXTMODE", "INCLUDE_CERTS", "KEYLIST_MODE", "ENCRYPT_RESULT" }; struct gpgme_tool { gpgme_ctx_t ctx; #define MAX_RECIPIENTS 10 gpgme_key_t recipients[MAX_RECIPIENTS + 1]; int recipients_nr; gpg_error_t (*write_status) (void *hook, const char *status, const char *msg); void *write_status_hook; }; typedef struct gpgme_tool *gpgme_tool_t; /* Forward declaration. */ void gt_write_status (gpgme_tool_t gt, status_t status, ...); void _gt_progress_cb (void *opaque, const char *what, int type, int current, int total) { gpgme_tool_t gt = opaque; char buf[100]; snprintf (buf, sizeof (buf), "0x%02x %i %i", type, current, total); gt_write_status (gt, STATUS_PROGRESS, what, buf); } gpg_error_t _gt_gpgme_new (gpgme_tool_t gt, gpgme_ctx_t *ctx) { gpg_error_t err; err = gpgme_new (ctx); if (err) return err; gpgme_set_progress_cb (*ctx, _gt_progress_cb, gt); return 0; } void gt_init (gpgme_tool_t gt) { memset (gt, '\0', sizeof (*gt)); gpg_error_t err; err = _gt_gpgme_new (gt, >->ctx); if (err) log_error (1, err, "can't create gpgme context"); } gpg_error_t gt_signers_add (gpgme_tool_t gt, const char *fpr) { gpg_error_t err; gpgme_key_t key; err = gpgme_get_key (gt->ctx, fpr, &key, 0); if (err) return err; return gpgme_signers_add (gt->ctx, key); } gpg_error_t gt_signers_clear (gpgme_tool_t gt) { gpgme_signers_clear (gt->ctx); return 0; } gpg_error_t gt_recipients_add (gpgme_tool_t gt, const char *fpr) { gpg_error_t err; gpgme_key_t key; if (gt->recipients_nr >= MAX_RECIPIENTS) return gpg_error_from_errno (ENOMEM); err = gpgme_get_key (gt->ctx, fpr, &key, 0); if (err) return err; gt->recipients[gt->recipients_nr++] = key; return 0; } void gt_recipients_clear (gpgme_tool_t gt) { int idx; for (idx = 0; idx < gt->recipients_nr; idx++) gpgme_key_unref (gt->recipients[idx]); memset (gt->recipients, '\0', gt->recipients_nr * sizeof (gpgme_key_t)); gt->recipients_nr = 0; } gpg_error_t gt_reset (gpgme_tool_t gt) { gpg_error_t err; gpgme_ctx_t ctx; err = _gt_gpgme_new (gt, &ctx); if (err) return err; gpgme_release (gt->ctx); gt->ctx = ctx; gt_recipients_clear (gt); return 0; } void gt_write_status (gpgme_tool_t gt, status_t status, ...) { va_list ap; const char *text; char buf[950]; char *p; size_t n; gpg_error_t err; va_start (ap, status); p = buf; n = 0; while ((text = va_arg (ap, const char *))) { if (n) { *p++ = ' '; n++; } while (*text && n < sizeof (buf) - 2) { *p++ = *text++; n++; } } *p = 0; va_end (ap); err = gt->write_status (gt->write_status_hook, status_string[status], buf); if (err) log_error (1, err, "can't write status line"); } gpg_error_t gt_get_engine_info (gpgme_tool_t gt, gpgme_protocol_t proto) { gpgme_engine_info_t info; info = gpgme_ctx_get_engine_info (gt->ctx); while (info) { if (proto == GPGME_PROTOCOL_UNKNOWN || proto == info->protocol) gt_write_status (gt, STATUS_ENGINE, gpgme_get_protocol_name (info->protocol), info->file_name, info->version, info->req_version, info->home_dir); info = info->next; } return 0; } gpgme_protocol_t gt_protocol_from_name (const char *name) { if (! strcasecmp (name, gpgme_get_protocol_name (GPGME_PROTOCOL_OpenPGP))) return GPGME_PROTOCOL_OpenPGP; if (! strcasecmp (name, gpgme_get_protocol_name (GPGME_PROTOCOL_CMS))) return GPGME_PROTOCOL_CMS; if (! strcasecmp (name,gpgme_get_protocol_name (GPGME_PROTOCOL_GPGCONF))) return GPGME_PROTOCOL_GPGCONF; if (! strcasecmp (name, gpgme_get_protocol_name (GPGME_PROTOCOL_ASSUAN))) return GPGME_PROTOCOL_ASSUAN; if (! strcasecmp (name, gpgme_get_protocol_name (GPGME_PROTOCOL_G13))) return GPGME_PROTOCOL_G13; return GPGME_PROTOCOL_UNKNOWN; } gpg_error_t gt_set_protocol (gpgme_tool_t gt, gpgme_protocol_t proto) { return gpgme_set_protocol (gt->ctx, proto); } gpg_error_t gt_get_protocol (gpgme_tool_t gt) { gpgme_protocol_t proto = gpgme_get_protocol (gt->ctx); gt_write_status (gt, STATUS_PROTOCOL, gpgme_get_protocol_name (proto), NULL); return 0; } gpg_error_t gt_set_armor (gpgme_tool_t gt, int armor) { gpgme_set_armor (gt->ctx, armor); return 0; } gpg_error_t gt_get_armor (gpgme_tool_t gt) { gt_write_status (gt, STATUS_ARMOR, gpgme_get_armor (gt->ctx) ? "true" : "false", NULL); return 0; } gpg_error_t gt_set_textmode (gpgme_tool_t gt, int textmode) { gpgme_set_textmode (gt->ctx, textmode); return 0; } gpg_error_t gt_get_textmode (gpgme_tool_t gt) { gt_write_status (gt, STATUS_TEXTMODE, gpgme_get_textmode (gt->ctx) ? "true" : "false", NULL); return 0; } gpg_error_t gt_set_keylist_mode (gpgme_tool_t gt, gpgme_keylist_mode_t keylist_mode) { gpgme_set_keylist_mode (gt->ctx, keylist_mode); return 0; } gpg_error_t gt_get_keylist_mode (gpgme_tool_t gt) { #define NR_KEYLIST_MODES 6 const char *modes[NR_KEYLIST_MODES + 1]; int idx = 0; gpgme_keylist_mode_t mode = gpgme_get_keylist_mode (gt->ctx); if (mode & GPGME_KEYLIST_MODE_LOCAL) modes[idx++] = "local"; if (mode & GPGME_KEYLIST_MODE_EXTERN) modes[idx++] = "extern"; if (mode & GPGME_KEYLIST_MODE_SIGS) modes[idx++] = "sigs"; if (mode & GPGME_KEYLIST_MODE_SIG_NOTATIONS) modes[idx++] = "sig_notations"; if (mode & GPGME_KEYLIST_MODE_EPHEMERAL) modes[idx++] = "ephemeral"; if (mode & GPGME_KEYLIST_MODE_VALIDATE) modes[idx++] = "validate"; modes[idx++] = NULL; gt_write_status (gt, STATUS_KEYLIST_MODE, modes[0], modes[1], modes[2], modes[3], modes[4], modes[5], modes[6]); return 0; } gpg_error_t gt_set_include_certs (gpgme_tool_t gt, int include_certs) { gpgme_set_include_certs (gt->ctx, include_certs); return 0; } gpg_error_t gt_get_include_certs (gpgme_tool_t gt) { int include_certs = gpgme_get_include_certs (gt->ctx); char buf[100]; if (include_certs == GPGME_INCLUDE_CERTS_DEFAULT) strcpy (buf, "default"); else snprintf (buf, sizeof (buf), "%i", include_certs); gt_write_status (gt, STATUS_INCLUDE_CERTS, buf, NULL); return 0; } gpg_error_t gt_decrypt_verify (gpgme_tool_t gt, gpgme_data_t cipher, gpgme_data_t plain, int verify) { if (verify) return gpgme_op_decrypt_verify (gt->ctx, cipher, plain); else return gpgme_op_decrypt (gt->ctx, cipher, plain); } gpg_error_t gt_sign_encrypt (gpgme_tool_t gt, gpgme_encrypt_flags_t flags, gpgme_data_t plain, gpgme_data_t cipher, int sign) { gpg_error_t err; if (sign) err = gpgme_op_encrypt (gt->ctx, gt->recipients, flags, plain, cipher); else err = gpgme_op_encrypt_sign (gt->ctx, gt->recipients, flags, plain, cipher); gt_recipients_clear (gt); return err; } gpg_error_t gt_sign (gpgme_tool_t gt, gpgme_data_t plain, gpgme_data_t sig, gpgme_sig_mode_t mode) { return gpgme_op_sign (gt->ctx, plain, sig, mode); } gpg_error_t gt_verify (gpgme_tool_t gt, gpgme_data_t sig, gpgme_data_t sig_text, gpgme_data_t plain) { return gpgme_op_verify (gt->ctx, sig, sig_text, plain); } gpg_error_t gt_import (gpgme_tool_t gt, gpgme_data_t data) { return gpgme_op_import (gt->ctx, data); } gpg_error_t gt_export (gpgme_tool_t gt, const char *pattern[], gpgme_export_mode_t mode, gpgme_data_t data) { return gpgme_op_export_ext (gt->ctx, pattern, mode, data); } gpg_error_t gt_genkey (gpgme_tool_t gt, const char *parms, gpgme_data_t public, gpgme_data_t secret) { return gpgme_op_genkey (gt->ctx, parms, public, secret); } gpg_error_t gt_import_keys (gpgme_tool_t gt, char *fpr[]) { gpg_error_t err; int cnt; int idx; gpgme_key_t *keys; cnt = 0; while (fpr[cnt]) cnt++; if (! cnt) return gpg_error (GPG_ERR_INV_VALUE); keys = malloc ((cnt + 1) * sizeof (gpgme_key_t)); if (! keys) return gpg_error_from_syserror (); for (idx = 0; idx < cnt; idx++) { err = gpgme_get_key (gt->ctx, fpr[idx], &keys[idx], 0); if (err) break; } if (! err) { keys[cnt] = NULL; err = gpgme_op_import_keys (gt->ctx, keys); } /* Rollback. */ while (--idx >= 0) gpgme_key_unref (keys[idx]); free (keys); return err; } gpg_error_t gt_delete (gpgme_tool_t gt, char *fpr, int allow_secret) { gpg_error_t err; gpgme_key_t key; err = gpgme_get_key (gt->ctx, fpr, &key, 0); if (err) return err; err = gpgme_op_delete (gt->ctx, key, allow_secret); gpgme_key_unref (key); return err; } gpg_error_t gt_keylist_start (gpgme_tool_t gt, const char *pattern[], int secret_only) { return gpgme_op_keylist_ext_start (gt->ctx, pattern, secret_only, 0); } gpg_error_t gt_keylist_next (gpgme_tool_t gt, gpgme_key_t *key) { return gpgme_op_keylist_next (gt->ctx, key); } gpg_error_t gt_getauditlog (gpgme_tool_t gt, gpgme_data_t output, unsigned int flags) { return gpgme_op_getauditlog (gt->ctx, output, flags); } gpg_error_t gt_vfs_mount (gpgme_tool_t gt, const char *container_file, const char *mount_dir, int flags) { gpg_error_t err; gpg_error_t op_err; err = gpgme_op_vfs_mount (gt->ctx, container_file, mount_dir, flags, &op_err); return err || op_err; } // TODO #define GT_RESULT_ENCRYPT 0x1 #define GT_RESULT_DECRYPT 0x2 #define GT_RESULT_SIGN 0x4 #define GT_RESULT_VERIFY 0x8 #define GT_RESULT_IMPORT 0x10 #define GT_RESULT_GENKEY 0x20 #define GT_RESULT_KEYLIST 0x40 #define GT_RESULT_VFS_MOUNT 0x80 #define GT_RESULT_ALL (~0U) gpg_error_t gt_result (gpgme_tool_t gt, unsigned int flags) { if (flags & GT_RESULT_ENCRYPT) { gpgme_encrypt_result_t res = gpgme_op_encrypt_result (gt->ctx); if (res) { gpgme_invalid_key_t invrec = res->invalid_recipients; while (invrec) { gt_write_status (gt, STATUS_ENCRYPT_RESULT, "invalid_recipient", invrec->fpr, invrec->reason); invrec = invrec->next; } } } return 0; } /* GPGME SERVER. */ #include struct server { gpgme_tool_t gt; assuan_context_t assuan_ctx; gpgme_data_encoding_t input_enc; gpgme_data_encoding_t output_enc; assuan_fd_t message_fd; gpgme_data_encoding_t message_enc; }; gpg_error_t server_write_status (void *hook, const char *status, const char *msg) { struct server *server = hook; return assuan_write_status (server->assuan_ctx, status, msg); } static gpgme_data_encoding_t server_data_encoding (const char *line) { if (strstr (line, "--binary")) return GPGME_DATA_ENCODING_BINARY; if (strstr (line, "--base64")) return GPGME_DATA_ENCODING_BASE64; if (strstr (line, "--armor")) return GPGME_DATA_ENCODING_ARMOR; if (strstr (line, "--url")) return GPGME_DATA_ENCODING_URL; if (strstr (line, "--urlesc")) return GPGME_DATA_ENCODING_URLESC; if (strstr (line, "--url0")) return GPGME_DATA_ENCODING_URL0; return GPGME_DATA_ENCODING_NONE; } static gpgme_error_t server_data_obj (assuan_fd_t fd, gpgme_data_encoding_t encoding, gpgme_data_t *data) { gpgme_error_t err; err = gpgme_data_new_from_fd (data, fd); if (err) return err; return gpgme_data_set_encoding (*data, encoding); } void server_reset_fds (struct server *server) { /* assuan closes the input and output FDs for us when doing a RESET, but we use this same function after commands, so repeat it here. */ assuan_close_input_fd (server->assuan_ctx); assuan_close_output_fd (server->assuan_ctx); if (server->message_fd != -1) { /* FIXME: Assuan should provide a close function. */ close (server->message_fd); server->message_fd = -1; } server->input_enc = GPGME_DATA_ENCODING_NONE; server->output_enc = GPGME_DATA_ENCODING_NONE; server->message_enc = GPGME_DATA_ENCODING_NONE; } static gpg_error_t reset_notify (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); server_reset_fds (server); gt_reset (server->gt); return 0; } static gpg_error_t cmd_version (assuan_context_t ctx, char *line) { if (line && *line) { const char *version = gpgme_check_version (line); return version ? 0 : gpg_error (GPG_ERR_SELFTEST_FAILED); } else { const char *version = gpgme_check_version (NULL); return assuan_send_data (ctx, version, strlen (version)); } } static gpg_error_t cmd_engine (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); return gt_get_engine_info (server->gt, gt_protocol_from_name (line)); } static gpg_error_t cmd_protocol (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); if (line && *line) return gt_set_protocol (server->gt, gt_protocol_from_name (line)); else return gt_get_protocol (server->gt); } static gpg_error_t cmd_armor (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); if (line && *line) { int flag = 0; if (! strcasecmp (line, "true") || ! strcasecmp (line, "yes") || line[0] == '1') flag = 1; return gt_set_armor (server->gt, flag); } else return gt_get_armor (server->gt); } static gpg_error_t cmd_textmode (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); if (line && *line) { int flag = 0; if (! strcasecmp (line, "true") || ! strcasecmp (line, "yes") || line[0] == '1') flag = 1; return gt_set_textmode (server->gt, flag); } else return gt_get_textmode (server->gt); } static gpg_error_t cmd_include_certs (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); if (line && *line) { int include_certs = 0; if (! strcasecmp (line, "default")) include_certs = GPGME_INCLUDE_CERTS_DEFAULT; else include_certs = atoi (line); return gt_set_include_certs (server->gt, include_certs); } else return gt_get_include_certs (server->gt); } static gpg_error_t cmd_keylist_mode (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); if (line && *line) { gpgme_keylist_mode_t mode = 0; if (strstr (line, "local")) mode |= GPGME_KEYLIST_MODE_LOCAL; if (strstr (line, "extern")) mode |= GPGME_KEYLIST_MODE_EXTERN; if (strstr (line, "sigs")) mode |= GPGME_KEYLIST_MODE_SIGS; if (strstr (line, "sig_notations")) mode |= GPGME_KEYLIST_MODE_SIG_NOTATIONS; if (strstr (line, "ephemeral")) mode |= GPGME_KEYLIST_MODE_EPHEMERAL; if (strstr (line, "validate")) mode |= GPGME_KEYLIST_MODE_VALIDATE; return gt_set_keylist_mode (server->gt, mode); } else return gt_get_keylist_mode (server->gt); } static gpg_error_t input_notify (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); server->input_enc = server_data_encoding (line); return 0; } static gpg_error_t output_notify (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); server->output_enc = server_data_encoding (line); return 0; } static gpg_error_t cmd_message (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t sysfd; err = assuan_command_parse_fd (ctx, line, &sysfd); if (err) return err; server->message_fd = sysfd; server->message_enc = server_data_encoding (line); return 0; } static gpg_error_t cmd_recipient (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); return gt_recipients_add (server->gt, line); } static gpg_error_t cmd_signer (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); return gt_signers_add (server->gt, line); } static gpg_error_t cmd_signers_clear (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); return gt_signers_clear (server->gt); } static gpg_error_t _cmd_decrypt_verify (assuan_context_t ctx, char *line, int verify) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t inp_fd; assuan_fd_t out_fd; gpgme_data_t inp_data; gpgme_data_t out_data; inp_fd = assuan_get_input_fd (ctx); if (inp_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_INPUT; out_fd = assuan_get_output_fd (ctx); if (out_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_OUTPUT; err = server_data_obj (inp_fd, server->input_enc, &inp_data); if (err) return err; err = server_data_obj (out_fd, server->output_enc, &out_data); if (err) { gpgme_data_release (inp_data); return err; } err = gt_decrypt_verify (server->gt, inp_data, out_data, verify); gpgme_data_release (inp_data); gpgme_data_release (out_data); server_reset_fds (server); return err; } static gpg_error_t cmd_decrypt (assuan_context_t ctx, char *line) { return _cmd_decrypt_verify (ctx, line, 0); } static gpg_error_t cmd_decrypt_verify (assuan_context_t ctx, char *line) { return _cmd_decrypt_verify (ctx, line, 1); } static gpg_error_t _cmd_sign_encrypt (assuan_context_t ctx, char *line, int sign) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t inp_fd; assuan_fd_t out_fd; gpgme_data_t inp_data; gpgme_data_t out_data; gpgme_encrypt_flags_t flags = 0; if (strstr (line, "--always-trust")) flags |= GPGME_ENCRYPT_ALWAYS_TRUST; if (strstr (line, "--no-encrypt-to")) flags |= GPGME_ENCRYPT_NO_ENCRYPT_TO; inp_fd = assuan_get_input_fd (ctx); if (inp_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_INPUT; out_fd = assuan_get_output_fd (ctx); if (out_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_OUTPUT; err = server_data_obj (inp_fd, server->input_enc, &inp_data); if (err) return err; err = server_data_obj (out_fd, server->output_enc, &out_data); if (err) { gpgme_data_release (inp_data); return err; } err = gt_sign_encrypt (server->gt, flags, inp_data, out_data, sign); gpgme_data_release (inp_data); gpgme_data_release (out_data); server_reset_fds (server); return err; } static gpg_error_t cmd_encrypt (assuan_context_t ctx, char *line) { return _cmd_sign_encrypt (ctx, line, 0); } static gpg_error_t cmd_sign_encrypt (assuan_context_t ctx, char *line) { return _cmd_sign_encrypt (ctx, line, 1); } static gpg_error_t cmd_sign (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t inp_fd; assuan_fd_t out_fd; gpgme_data_t inp_data; gpgme_data_t out_data; gpgme_sig_mode_t mode = GPGME_SIG_MODE_NORMAL; if (strstr (line, "--clear")) mode = GPGME_SIG_MODE_CLEAR; if (strstr (line, "--detach")) mode = GPGME_SIG_MODE_DETACH; inp_fd = assuan_get_input_fd (ctx); if (inp_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_INPUT; out_fd = assuan_get_output_fd (ctx); if (out_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_OUTPUT; err = server_data_obj (inp_fd, server->input_enc, &inp_data); if (err) return err; err = server_data_obj (out_fd, server->output_enc, &out_data); if (err) { gpgme_data_release (inp_data); return err; } err = gt_sign (server->gt, inp_data, out_data, mode); gpgme_data_release (inp_data); gpgme_data_release (out_data); server_reset_fds (server); return err; } static gpg_error_t cmd_verify (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t inp_fd; assuan_fd_t msg_fd; assuan_fd_t out_fd; gpgme_data_t inp_data; gpgme_data_t msg_data = NULL; gpgme_data_t out_data = NULL; inp_fd = assuan_get_input_fd (ctx); if (inp_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_INPUT; msg_fd = server->message_fd; out_fd = assuan_get_output_fd (ctx); err = server_data_obj (inp_fd, server->input_enc, &inp_data); if (err) return err; if (msg_fd != ASSUAN_INVALID_FD) { err = server_data_obj (msg_fd, server->message_enc, &msg_data); if (err) { gpgme_data_release (inp_data); return err; } } if (out_fd != ASSUAN_INVALID_FD) { err = server_data_obj (out_fd, server->output_enc, &out_data); if (err) { gpgme_data_release (inp_data); gpgme_data_release (msg_data); return err; } } err = gt_verify (server->gt, inp_data, msg_data, out_data); gpgme_data_release (inp_data); if (msg_data) gpgme_data_release (msg_data); if (out_data) gpgme_data_release (out_data); server_reset_fds (server); return err; } static gpg_error_t cmd_import (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); if (line && *line) { char *fprs[2] = { line, NULL }; return gt_import_keys (server->gt, fprs); } else { gpg_error_t err; assuan_fd_t inp_fd; gpgme_data_t inp_data; inp_fd = assuan_get_input_fd (ctx); if (inp_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_INPUT; err = server_data_obj (inp_fd, server->input_enc, &inp_data); if (err) return err; err = gt_import (server->gt, inp_data); gpgme_data_release (inp_data); server_reset_fds (server); return err; } } static gpg_error_t cmd_export (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t out_fd; gpgme_data_t out_data; gpgme_export_mode_t mode = 0; const char *pattern[2]; const char optstr[] = "--extern "; out_fd = assuan_get_output_fd (ctx); if (out_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_OUTPUT; err = server_data_obj (out_fd, server->output_enc, &out_data); if (err) return err; if (strncasecmp (line, optstr, strlen (optstr))) { mode |= GPGME_EXPORT_MODE_EXTERN; line += strlen (optstr); } pattern[0] = line; pattern[1] = NULL; err = gt_export (server->gt, pattern, mode, out_data); gpgme_data_release (out_data); server_reset_fds (server); return err; } static gpg_error_t _cmd_genkey_write (gpgme_data_t data, const void *buf, size_t size) { while (size > 0) { ssize_t writen = gpgme_data_write (data, buf, size); if (writen < 0 && errno != EAGAIN) return gpg_error_from_syserror (); else if (writen > 0) { buf = (void *) (((char *) buf) + writen); size -= writen; } } return 0; } static gpg_error_t cmd_genkey (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t inp_fd; assuan_fd_t out_fd; gpgme_data_t inp_data; gpgme_data_t out_data = NULL; gpgme_data_t parms_data = NULL; const char *parms; inp_fd = assuan_get_input_fd (ctx); if (inp_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_INPUT; out_fd = assuan_get_output_fd (ctx); err = server_data_obj (inp_fd, server->input_enc, &inp_data); if (err) return err; if (out_fd != ASSUAN_INVALID_FD) { err = server_data_obj (out_fd, server->output_enc, &out_data); if (err) { gpgme_data_release (inp_data); return err; } } /* Convert input data. */ err = gpgme_data_new (&parms_data); if (err) goto out; do { char buf[512]; ssize_t readn = gpgme_data_read (inp_data, buf, sizeof (buf)); if (readn < 0) { err = gpg_error_from_syserror (); goto out; } else if (readn == 0) break; err = _cmd_genkey_write (parms_data, buf, readn); if (err) goto out; } while (1); err = _cmd_genkey_write (parms_data, "", 1); if (err) goto out; parms = gpgme_data_release_and_get_mem (parms_data, NULL); parms_data = NULL; if (! parms) { err = gpg_error (GPG_ERR_GENERAL); goto out; } err = gt_genkey (server->gt, parms, out_data, NULL); server_reset_fds (server); out: gpgme_data_release (inp_data); if (out_data) gpgme_data_release (out_data); if (parms_data) gpgme_data_release (parms_data); return err; } static gpg_error_t cmd_delete (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); int allow_secret = 0; const char optstr[] = "--allow-secret "; if (strncasecmp (line, optstr, strlen (optstr))) { allow_secret = 1; line += strlen (optstr); } return gt_delete (server->gt, line, allow_secret); } static gpg_error_t cmd_keylist (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; int secret_only = 0; const char *pattern[2]; const char optstr[] = "--secret-only "; if (strncasecmp (line, optstr, strlen (optstr))) { secret_only = 1; line += strlen (optstr); } pattern[0] = line; pattern[1] = NULL; err = gt_keylist_start (server->gt, pattern, secret_only); while (! err) { gpgme_key_t key; err = gt_keylist_next (server->gt, &key); if (gpg_err_code (err) == GPG_ERR_EOF) { err = 0; break; } else if (! err) { char buf[100]; /* FIXME: More data. */ snprintf (buf, sizeof (buf), "key:%s\n", key->subkeys->fpr); assuan_send_data (ctx, buf, strlen (buf)); gpgme_key_unref (key); } } server_reset_fds (server); return err; } static gpg_error_t cmd_getauditlog (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); gpg_error_t err; assuan_fd_t out_fd; gpgme_data_t out_data; out_fd = assuan_get_output_fd (ctx); if (out_fd == ASSUAN_INVALID_FD) return GPG_ERR_ASS_NO_OUTPUT; err = server_data_obj (out_fd, server->output_enc, &out_data); if (err) return err; err = gt_getauditlog (server->gt, out_data, 0); gpgme_data_release (out_data); server_reset_fds (server); return err; } static gpg_error_t cmd_vfs_mount (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); char *mount_dir; gpg_error_t err; mount_dir = strchr (line, ' '); if (mount_dir) { *(mount_dir++) = '\0'; while (*mount_dir == ' ') mount_dir++; } err = gt_vfs_mount (server->gt, line, mount_dir, 0); return err; } static gpg_error_t cmd_result (assuan_context_t ctx, char *line) { struct server *server = assuan_get_pointer (ctx); return gt_result (server->gt, GT_RESULT_ALL); } /* STRERROR */ static gpg_error_t cmd_strerror (assuan_context_t ctx, char *line) { gpg_error_t err; char buf[100]; err = atoi (line); snprintf (buf, sizeof (buf), "%s <%s>", gpgme_strerror (err), gpgme_strsource (err)); return assuan_send_data (ctx, buf, strlen (buf)); } static gpg_error_t cmd_pubkey_algo_name (assuan_context_t ctx, char *line) { gpgme_pubkey_algo_t algo; char buf[100]; algo = atoi (line); snprintf (buf, sizeof (buf), "%s", gpgme_pubkey_algo_name (algo)); return assuan_send_data (ctx, buf, strlen (buf)); } static gpg_error_t cmd_hash_algo_name (assuan_context_t ctx, char *line) { gpgme_hash_algo_t algo; char buf[100]; algo = atoi (line); snprintf (buf, sizeof (buf), "%s", gpgme_hash_algo_name (algo)); return assuan_send_data (ctx, buf, strlen (buf)); } /* Tell the assuan library about our commands. */ static gpg_error_t register_commands (assuan_context_t ctx) { gpg_error_t err; static struct { const char *name; gpg_error_t (*handler)(assuan_context_t, char *line); } table[] = { // RESET, BYE are implicit. { "VERSION", cmd_version }, // TODO: Set engine info. { "ENGINE", cmd_engine }, { "PROTOCOL", cmd_protocol }, { "ARMOR", cmd_armor }, { "TEXTMODE", cmd_textmode }, { "INCLUDE_CERTS", cmd_include_certs }, { "KEYLIST_MODE", cmd_keylist_mode }, { "INPUT", NULL }, { "OUTPUT", NULL }, { "MESSAGE", cmd_message }, { "RECIPIENT", cmd_recipient }, { "SIGNER", cmd_signer }, { "SIGNERS_CLEAR", cmd_signers_clear }, // TODO: SIGNOTATION missing. // TODO: Could add wait interface if we allow more than one context // and add _START variants. // TODO: Could add data interfaces if we allow multiple data objects. { "DECRYPT", cmd_decrypt }, { "DECRYPT_VERIFY", cmd_decrypt_verify }, { "ENCRYPT", cmd_encrypt }, { "ENCRYPT_SIGN", cmd_sign_encrypt }, { "SIGN_ENCRYPT", cmd_sign_encrypt }, { "SIGN", cmd_sign }, { "VERIFY", cmd_verify }, { "IMPORT", cmd_import }, { "EXPORT", cmd_export }, { "GENKEY", cmd_genkey }, { "DELETE", cmd_delete }, // TODO: EDIT, CARD_EDIT (with INQUIRE) { "KEYLIST", cmd_keylist }, { "LISTKEYS", cmd_keylist }, // TODO: TRUSTLIST, TRUSTLIST_EXT { "GETAUDITLOG", cmd_getauditlog }, // TODO: ASSUAN { "VFS_MOUNT", cmd_vfs_mount }, { "MOUNT", cmd_vfs_mount }, // TODO: GPGCONF { "RESULT", cmd_result }, { "STRERROR", cmd_strerror }, { "PUBKEY_ALGO_NAME", cmd_pubkey_algo_name }, { "HASH_ALGO_NAME", cmd_hash_algo_name }, { NULL } }; int idx; for (idx = 0; table[idx].name; idx++) { err = assuan_register_command (ctx, table[idx].name, table[idx].handler); if (err) return err; } return 0; } /* TODO: password callback can do INQUIRE. */ void gpgme_server (gpgme_tool_t gt) { gpg_error_t err; int filedes[2]; struct server server; static const char hello[] = ("GPGME-Tool " VERSION " ready"); memset (&server, 0, sizeof (server)); server.message_fd = -1; server.input_enc = GPGME_DATA_ENCODING_NONE; server.output_enc = GPGME_DATA_ENCODING_NONE; server.message_enc = GPGME_DATA_ENCODING_NONE; server.gt = gt; gt->write_status = server_write_status; gt->write_status_hook = &server; /* We use a pipe based server so that we can work from scripts. assuan_init_pipe_server will automagically detect when we are called with a socketpair and ignore FIELDES in this case. */ filedes[0] = 0; filedes[1] = 1; err = assuan_new (&server.assuan_ctx); if (err) log_error (1, err, "can't create assuan context"); assuan_set_pointer (server.assuan_ctx, &server); err = assuan_init_pipe_server (server.assuan_ctx, filedes); if (err) log_error (1, err, "can't initialize assuan server"); err = register_commands (server.assuan_ctx); if (err) log_error (1, err, "can't register assuan commands"); assuan_set_hello_line (server.assuan_ctx, hello); assuan_register_reset_notify (server.assuan_ctx, reset_notify); assuan_register_input_notify (server.assuan_ctx, input_notify); assuan_register_output_notify (server.assuan_ctx, output_notify); #define DBG_ASSUAN 0 if (DBG_ASSUAN) assuan_set_log_stream (server.assuan_ctx, log_stream); for (;;) { err = assuan_accept (server.assuan_ctx); if (err == -1) break; else if (err) { log_error (0, err, "assuan accept problem"); break; } err = assuan_process (server.assuan_ctx); if (err) log_error (0, err, "assuan processing failed"); } assuan_release (server.assuan_ctx); } /* MAIN PROGRAM STARTS HERE. */ const char *argp_program_version = VERSION; const char *argp_program_bug_address = "bug-gpgme@gnupg.org"; error_t argp_err_exit_status = 1; static char doc[] = "GPGME Tool -- invoke GPGME operations"; static char args_doc[] = "COMMAND [OPTIONS...]"; static struct argp_option options[] = { { "server", 's', 0, 0, "Server mode" }, { 0 } }; static error_t parse_options (int key, char *arg, struct argp_state *state); static struct argp argp = { options, parse_options, args_doc, doc }; struct args { enum { CMD_DEFAULT, CMD_SERVER } cmd; }; void args_init (struct args *args) { memset (args, '\0', sizeof (*args)); args->cmd = CMD_DEFAULT; } static error_t parse_options (int key, char *arg, struct argp_state *state) { struct args *args = state->input; switch (key) { case 's': args->cmd = CMD_SERVER; break; #if 0 case ARGP_KEY_ARG: if (state->arg_num >= 2) argp_usage (state); printf ("Arg[%i] = %s\n", state->arg_num, arg); break; case ARGP_KEY_END: if (state->arg_num < 2) argp_usage (state); break; #endif default: return ARGP_ERR_UNKNOWN; } return 0; } int main (int argc, char *argv[]) { struct args args; struct gpgme_tool gt; setlocale (LC_ALL, ""); gpgme_check_version (NULL); gpgme_set_locale (NULL, LC_CTYPE, setlocale (LC_CTYPE, NULL)); #ifdef LC_MESSAGES gpgme_set_locale (NULL, LC_MESSAGES, setlocale (LC_MESSAGES, NULL)); #endif args_init (&args); argp_parse (&argp, argc, argv, 0, 0, &args); log_init (); gt_init (>); switch (args.cmd) { case CMD_DEFAULT: case CMD_SERVER: gpgme_server (>); break; } gpgme_release (gt.ctx); return 0; }