mirror of
git://git.gnupg.org/gnupg.git
synced 2024-11-09 21:28:51 +01:00
096e7457ec
The asymmetric quotes used by GNU in the past (`...') don't render nicely on modern systems. We now use two \x27 characters ('...'). The proper solution would be to use the correct Unicode symmetric quotes here. However this has the disadvantage that the system requires Unicode support. We don't want that today. If Unicode is available a generated po file can be used to output proper quotes. A simple sed script like the one used for en@quote is sufficient to change them. The changes have been done by applying sed -i "s/\`\([^'\`]*\)'/'\1'/g" to most files and fixing obvious problems by hand. The msgid strings in the po files were fixed with a similar command.
1324 lines
35 KiB
C
1324 lines
35 KiB
C
/* audit.c - GnuPG's audit subsystem
|
||
* Copyright (C) 2007, 2009 Free Software Foundation, Inc.
|
||
*
|
||
* This file is part of GnuPG.
|
||
*
|
||
* GnuPG 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 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* GnuPG 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, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include <config.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdarg.h>
|
||
#include <assert.h>
|
||
|
||
#include "util.h"
|
||
#include "i18n.h"
|
||
#include "audit.h"
|
||
#include "audit-events.h"
|
||
|
||
/* A list to maintain a list of helptags. */
|
||
struct helptag_s
|
||
{
|
||
struct helptag_s *next;
|
||
const char *name;
|
||
};
|
||
typedef struct helptag_s *helptag_t;
|
||
|
||
|
||
/* One log entry. */
|
||
struct log_item_s
|
||
{
|
||
audit_event_t event; /* The event. */
|
||
gpg_error_t err; /* The logged error code. */
|
||
int intvalue; /* A logged integer value. */
|
||
char *string; /* A malloced string or NULL. */
|
||
ksba_cert_t cert; /* A certifciate or NULL. */
|
||
int have_err:1;
|
||
int have_intvalue:1;
|
||
};
|
||
typedef struct log_item_s *log_item_t;
|
||
|
||
|
||
|
||
/* The main audit object. */
|
||
struct audit_ctx_s
|
||
{
|
||
const char *failure; /* If set a description of the internal failure. */
|
||
audit_type_t type;
|
||
|
||
log_item_t log; /* The table with the log entries. */
|
||
size_t logsize; /* The allocated size for LOG. */
|
||
size_t logused; /* The used size of LOG. */
|
||
|
||
estream_t outstream; /* The current output stream. */
|
||
int use_html; /* The output shall be HTML formatted. */
|
||
int indentlevel; /* Current level of indentation. */
|
||
helptag_t helptags; /* List of help keys. */
|
||
};
|
||
|
||
|
||
|
||
|
||
static void writeout_para (audit_ctx_t ctx,
|
||
const char *format, ...) JNLIB_GCC_A_PRINTF(2,3);
|
||
static void writeout_li (audit_ctx_t ctx, const char *oktext,
|
||
const char *format, ...) JNLIB_GCC_A_PRINTF(3,4);
|
||
static void writeout_rem (audit_ctx_t ctx,
|
||
const char *format, ...) JNLIB_GCC_A_PRINTF(2,3);
|
||
|
||
|
||
/* Add NAME to the list of help tags. NAME needs to be a const string
|
||
an this function merly stores this pointer. */
|
||
static void
|
||
add_helptag (audit_ctx_t ctx, const char *name)
|
||
{
|
||
helptag_t item;
|
||
|
||
for (item=ctx->helptags; item; item = item->next)
|
||
if (!strcmp (item->name, name))
|
||
return; /* Already in the list. */
|
||
item = xtrycalloc (1, sizeof *item);
|
||
if (!item)
|
||
return; /* Don't care about memory problems. */
|
||
item->name = name;
|
||
item->next = ctx->helptags;
|
||
ctx->helptags = item;
|
||
}
|
||
|
||
|
||
/* Remove all help tags from the context. */
|
||
static void
|
||
clear_helptags (audit_ctx_t ctx)
|
||
{
|
||
while (ctx->helptags)
|
||
{
|
||
helptag_t tmp = ctx->helptags->next;
|
||
xfree (ctx->helptags);
|
||
ctx->helptags = tmp;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
static const char *
|
||
event2str (audit_event_t event)
|
||
{
|
||
/* We need the cast so that compiler does not complain about an
|
||
always true comparison (>= 0) for an unsigned value. */
|
||
int idx = eventstr_msgidxof ((int)event);
|
||
if (idx == -1)
|
||
return "Unknown event";
|
||
else
|
||
return eventstr_msgstr + eventstr_msgidx[idx];
|
||
}
|
||
|
||
|
||
|
||
/* Create a new audit context. In case of an error NULL is returned
|
||
and errno set appropriately. */
|
||
audit_ctx_t
|
||
audit_new (void)
|
||
{
|
||
audit_ctx_t ctx;
|
||
|
||
ctx = xtrycalloc (1, sizeof *ctx);
|
||
|
||
return ctx;
|
||
}
|
||
|
||
|
||
/* Release an audit context. Passing NULL for CTX is allowed and does
|
||
nothing. */
|
||
void
|
||
audit_release (audit_ctx_t ctx)
|
||
{
|
||
int idx;
|
||
if (!ctx)
|
||
return;
|
||
if (ctx->log)
|
||
{
|
||
for (idx=0; idx < ctx->logused; idx++)
|
||
{
|
||
if (ctx->log[idx].string)
|
||
xfree (ctx->log[idx].string);
|
||
if (ctx->log[idx].cert)
|
||
ksba_cert_release (ctx->log[idx].cert);
|
||
}
|
||
xfree (ctx->log);
|
||
}
|
||
clear_helptags (ctx);
|
||
xfree (ctx);
|
||
}
|
||
|
||
|
||
/* Set the type for the audit operation. If CTX is NULL, this is a
|
||
dummy fucntion. */
|
||
void
|
||
audit_set_type (audit_ctx_t ctx, audit_type_t type)
|
||
{
|
||
if (!ctx || ctx->failure)
|
||
return; /* Audit not enabled or an internal error has occurred. */
|
||
|
||
if (ctx->type && ctx->type != type)
|
||
{
|
||
ctx->failure = "conflict in type initialization";
|
||
return;
|
||
}
|
||
ctx->type = type;
|
||
}
|
||
|
||
|
||
/* Create a new log item and put it into the table. Return that log
|
||
item on success; return NULL on memory failure and mark that in
|
||
CTX. */
|
||
static log_item_t
|
||
create_log_item (audit_ctx_t ctx)
|
||
{
|
||
log_item_t item, table;
|
||
size_t size;
|
||
|
||
if (!ctx->log)
|
||
{
|
||
size = 10;
|
||
table = xtrymalloc (size * sizeof *table);
|
||
if (!table)
|
||
{
|
||
ctx->failure = "Out of memory in create_log_item";
|
||
return NULL;
|
||
}
|
||
ctx->log = table;
|
||
ctx->logsize = size;
|
||
item = ctx->log + 0;
|
||
ctx->logused = 1;
|
||
}
|
||
else if (ctx->logused >= ctx->logsize)
|
||
{
|
||
size = ctx->logsize + 10;
|
||
table = xtryrealloc (ctx->log, size * sizeof *table);
|
||
if (!table)
|
||
{
|
||
ctx->failure = "Out of memory while reallocating in create_log_item";
|
||
return NULL;
|
||
}
|
||
ctx->log = table;
|
||
ctx->logsize = size;
|
||
item = ctx->log + ctx->logused++;
|
||
}
|
||
else
|
||
item = ctx->log + ctx->logused++;
|
||
|
||
item->event = AUDIT_NULL_EVENT;
|
||
item->err = 0;
|
||
item->have_err = 0;
|
||
item->intvalue = 0;
|
||
item->have_intvalue = 0;
|
||
item->string = NULL;
|
||
item->cert = NULL;
|
||
|
||
return item;
|
||
|
||
}
|
||
|
||
/* Add a new event to the audit log. If CTX is NULL, this function
|
||
does nothing. */
|
||
void
|
||
audit_log (audit_ctx_t ctx, audit_event_t event)
|
||
{
|
||
log_item_t item;
|
||
|
||
if (!ctx || ctx->failure)
|
||
return; /* Audit not enabled or an internal error has occurred. */
|
||
if (!event)
|
||
{
|
||
ctx->failure = "Invalid event passed to audit_log";
|
||
return;
|
||
}
|
||
if (!(item = create_log_item (ctx)))
|
||
return;
|
||
item->event = event;
|
||
}
|
||
|
||
/* Add a new event to the audit log. If CTX is NULL, this function
|
||
does nothing. This version also adds the result of the operation
|
||
to the log. */
|
||
void
|
||
audit_log_ok (audit_ctx_t ctx, audit_event_t event, gpg_error_t err)
|
||
{
|
||
log_item_t item;
|
||
|
||
if (!ctx || ctx->failure)
|
||
return; /* Audit not enabled or an internal error has occurred. */
|
||
if (!event)
|
||
{
|
||
ctx->failure = "Invalid event passed to audit_log_ok";
|
||
return;
|
||
}
|
||
if (!(item = create_log_item (ctx)))
|
||
return;
|
||
item->event = event;
|
||
item->err = err;
|
||
item->have_err = 1;
|
||
}
|
||
|
||
|
||
/* Add a new event to the audit log. If CTX is NULL, this function
|
||
does nothing. This version also add the integer VALUE to the log. */
|
||
void
|
||
audit_log_i (audit_ctx_t ctx, audit_event_t event, int value)
|
||
{
|
||
log_item_t item;
|
||
|
||
if (!ctx || ctx->failure)
|
||
return; /* Audit not enabled or an internal error has occurred. */
|
||
if (!event)
|
||
{
|
||
ctx->failure = "Invalid event passed to audit_log_i";
|
||
return;
|
||
}
|
||
if (!(item = create_log_item (ctx)))
|
||
return;
|
||
item->event = event;
|
||
item->intvalue = value;
|
||
item->have_intvalue = 1;
|
||
}
|
||
|
||
|
||
/* Add a new event to the audit log. If CTX is NULL, this function
|
||
does nothing. This version also add the integer VALUE to the log. */
|
||
void
|
||
audit_log_s (audit_ctx_t ctx, audit_event_t event, const char *value)
|
||
{
|
||
log_item_t item;
|
||
char *tmp;
|
||
|
||
if (!ctx || ctx->failure)
|
||
return; /* Audit not enabled or an internal error has occurred. */
|
||
if (!event)
|
||
{
|
||
ctx->failure = "Invalid event passed to audit_log_s";
|
||
return;
|
||
}
|
||
tmp = xtrystrdup (value? value : "");
|
||
if (!tmp)
|
||
{
|
||
ctx->failure = "Out of memory in audit_event";
|
||
return;
|
||
}
|
||
if (!(item = create_log_item (ctx)))
|
||
{
|
||
xfree (tmp);
|
||
return;
|
||
}
|
||
item->event = event;
|
||
item->string = tmp;
|
||
}
|
||
|
||
/* Add a new event to the audit log. If CTX is NULL, this function
|
||
does nothing. This version also adds the certificate CERT and the
|
||
result of an operation to the log. */
|
||
void
|
||
audit_log_cert (audit_ctx_t ctx, audit_event_t event,
|
||
ksba_cert_t cert, gpg_error_t err)
|
||
{
|
||
log_item_t item;
|
||
|
||
if (!ctx || ctx->failure)
|
||
return; /* Audit not enabled or an internal error has occurred. */
|
||
if (!event)
|
||
{
|
||
ctx->failure = "Invalid event passed to audit_log_cert";
|
||
return;
|
||
}
|
||
if (!(item = create_log_item (ctx)))
|
||
return;
|
||
item->event = event;
|
||
item->err = err;
|
||
item->have_err = 1;
|
||
if (cert)
|
||
{
|
||
ksba_cert_ref (cert);
|
||
item->cert = cert;
|
||
}
|
||
}
|
||
|
||
|
||
/* Write TEXT to the outstream. */
|
||
static void
|
||
writeout (audit_ctx_t ctx, const char *text)
|
||
{
|
||
if (ctx->use_html)
|
||
{
|
||
for (; *text; text++)
|
||
{
|
||
if (*text == '<')
|
||
es_fputs ("<", ctx->outstream);
|
||
else if (*text == '&')
|
||
es_fputs ("&", ctx->outstream);
|
||
else
|
||
es_putc (*text, ctx->outstream);
|
||
}
|
||
}
|
||
else
|
||
es_fputs (text, ctx->outstream);
|
||
}
|
||
|
||
|
||
/* Write TEXT to the outstream using a variable argument list. */
|
||
static void
|
||
writeout_v (audit_ctx_t ctx, const char *format, va_list arg_ptr)
|
||
{
|
||
char *buf;
|
||
|
||
estream_vasprintf (&buf, format, arg_ptr);
|
||
if (buf)
|
||
{
|
||
writeout (ctx, buf);
|
||
xfree (buf);
|
||
}
|
||
else
|
||
writeout (ctx, "[!!Out of core!!]");
|
||
}
|
||
|
||
|
||
/* Write TEXT as a paragraph. */
|
||
static void
|
||
writeout_para (audit_ctx_t ctx, const char *format, ...)
|
||
{
|
||
va_list arg_ptr;
|
||
|
||
if (ctx->use_html)
|
||
es_fputs ("<p>", ctx->outstream);
|
||
va_start (arg_ptr, format) ;
|
||
writeout_v (ctx, format, arg_ptr);
|
||
va_end (arg_ptr);
|
||
if (ctx->use_html)
|
||
es_fputs ("</p>\n", ctx->outstream);
|
||
else
|
||
es_fputc ('\n', ctx->outstream);
|
||
}
|
||
|
||
|
||
static void
|
||
enter_li (audit_ctx_t ctx)
|
||
{
|
||
if (ctx->use_html)
|
||
{
|
||
if (!ctx->indentlevel)
|
||
{
|
||
es_fputs ("<table border=\"0\">\n"
|
||
" <colgroup>\n"
|
||
" <col width=\"80%\" />\n"
|
||
" <col width=\"20%\" />\n"
|
||
" </colgroup>\n",
|
||
ctx->outstream);
|
||
}
|
||
}
|
||
ctx->indentlevel++;
|
||
}
|
||
|
||
|
||
static void
|
||
leave_li (audit_ctx_t ctx)
|
||
{
|
||
ctx->indentlevel--;
|
||
if (ctx->use_html)
|
||
{
|
||
if (!ctx->indentlevel)
|
||
es_fputs ("</table>\n", ctx->outstream);
|
||
}
|
||
}
|
||
|
||
|
||
/* Write TEXT as a list element. If OKTEXT is not NULL, append it to
|
||
the last line. */
|
||
static void
|
||
writeout_li (audit_ctx_t ctx, const char *oktext, const char *format, ...)
|
||
{
|
||
va_list arg_ptr;
|
||
const char *color = NULL;
|
||
|
||
if (ctx->use_html && format && oktext)
|
||
{
|
||
if (!strcmp (oktext, "Yes")
|
||
|| !strcmp (oktext, "good") )
|
||
color = "green";
|
||
else if (!strcmp (oktext, "No")
|
||
|| !strcmp (oktext, "bad") )
|
||
color = "red";
|
||
}
|
||
|
||
if (format && oktext)
|
||
{
|
||
const char *s = NULL;
|
||
|
||
if (!strcmp (oktext, "Yes"))
|
||
oktext = _("Yes");
|
||
else if (!strcmp (oktext, "No"))
|
||
oktext = _("No");
|
||
else if (!strcmp (oktext, "good"))
|
||
{
|
||
/* TRANSLATORS: Copy the prefix between the vertical bars
|
||
verbatim. It will not be printed. */
|
||
oktext = _("|audit-log-result|Good");
|
||
}
|
||
else if (!strcmp (oktext, "bad"))
|
||
oktext = _("|audit-log-result|Bad");
|
||
else if (!strcmp (oktext, "unsupported"))
|
||
oktext = _("|audit-log-result|Not supported");
|
||
else if (!strcmp (oktext, "no-cert"))
|
||
oktext = _("|audit-log-result|No certificate");
|
||
else if (!strcmp (oktext, "disabled"))
|
||
oktext = _("|audit-log-result|Not enabled");
|
||
else if (!strcmp (oktext, "error"))
|
||
oktext = _("|audit-log-result|Error");
|
||
else if (!strcmp (oktext, "not-used"))
|
||
oktext = _("|audit-log-result|Not used");
|
||
else if (!strcmp (oktext, "okay"))
|
||
oktext = _("|audit-log-result|Okay");
|
||
else if (!strcmp (oktext, "skipped"))
|
||
oktext = _("|audit-log-result|Skipped");
|
||
else if (!strcmp (oktext, "some"))
|
||
oktext = _("|audit-log-result|Some");
|
||
else
|
||
s = "";
|
||
|
||
/* If we have set a prefix, skip it. */
|
||
if (!s && *oktext == '|' && (s=strchr (oktext+1,'|')))
|
||
oktext = s+1;
|
||
}
|
||
|
||
if (ctx->use_html)
|
||
{
|
||
int i;
|
||
|
||
es_fputs (" <tr><td><table><tr><td>", ctx->outstream);
|
||
if (color)
|
||
es_fprintf (ctx->outstream, "<font color=\"%s\">*</font>", color);
|
||
else
|
||
es_fputs ("*", ctx->outstream);
|
||
for (i=1; i < ctx->indentlevel; i++)
|
||
es_fputs (" ", ctx->outstream);
|
||
es_fputs ("</td><td>", ctx->outstream);
|
||
}
|
||
else
|
||
es_fprintf (ctx->outstream, "* %*s", (ctx->indentlevel-1)*2, "");
|
||
if (format)
|
||
{
|
||
va_start (arg_ptr, format) ;
|
||
writeout_v (ctx, format, arg_ptr);
|
||
va_end (arg_ptr);
|
||
}
|
||
if (ctx->use_html)
|
||
es_fputs ("</td></tr></table>", ctx->outstream);
|
||
if (format && oktext)
|
||
{
|
||
if (ctx->use_html)
|
||
{
|
||
es_fputs ("</td><td>", ctx->outstream);
|
||
if (color)
|
||
es_fprintf (ctx->outstream, "<font color=\"%s\">", color);
|
||
}
|
||
else
|
||
writeout (ctx, ": ");
|
||
writeout (ctx, oktext);
|
||
if (color)
|
||
es_fputs ("</font>", ctx->outstream);
|
||
}
|
||
|
||
if (ctx->use_html)
|
||
es_fputs ("</td></tr>\n", ctx->outstream);
|
||
else
|
||
es_fputc ('\n', ctx->outstream);
|
||
}
|
||
|
||
|
||
/* Write a remark line. */
|
||
static void
|
||
writeout_rem (audit_ctx_t ctx, const char *format, ...)
|
||
{
|
||
va_list arg_ptr;
|
||
|
||
if (ctx->use_html)
|
||
{
|
||
int i;
|
||
|
||
es_fputs (" <tr><td><table><tr><td>*", ctx->outstream);
|
||
for (i=1; i < ctx->indentlevel; i++)
|
||
es_fputs (" ", ctx->outstream);
|
||
es_fputs (" </td><td> (", ctx->outstream);
|
||
|
||
}
|
||
else
|
||
es_fprintf (ctx->outstream, "* %*s (", (ctx->indentlevel-1)*2, "");
|
||
if (format)
|
||
{
|
||
va_start (arg_ptr, format) ;
|
||
writeout_v (ctx, format, arg_ptr);
|
||
va_end (arg_ptr);
|
||
}
|
||
if (ctx->use_html)
|
||
es_fputs (")</td></tr></table></td></tr>\n", ctx->outstream);
|
||
else
|
||
es_fputs (")\n", ctx->outstream);
|
||
}
|
||
|
||
|
||
/* Return the first log item for EVENT. If STOPEVENT is not 0 never
|
||
look behind that event in the log. If STARTITEM is not NULL start
|
||
search _after_that item. */
|
||
static log_item_t
|
||
find_next_log_item (audit_ctx_t ctx, log_item_t startitem,
|
||
audit_event_t event, audit_event_t stopevent)
|
||
{
|
||
int idx;
|
||
|
||
for (idx=0; idx < ctx->logused; idx++)
|
||
{
|
||
if (startitem)
|
||
{
|
||
if (ctx->log + idx == startitem)
|
||
startitem = NULL;
|
||
}
|
||
else if (stopevent && ctx->log[idx].event == stopevent)
|
||
break;
|
||
else if (ctx->log[idx].event == event)
|
||
return ctx->log + idx;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
|
||
static log_item_t
|
||
find_log_item (audit_ctx_t ctx, audit_event_t event, audit_event_t stopevent)
|
||
{
|
||
return find_next_log_item (ctx, NULL, event, stopevent);
|
||
}
|
||
|
||
|
||
/* Helper to a format a serial number. */
|
||
static char *
|
||
format_serial (ksba_const_sexp_t sn)
|
||
{
|
||
const char *p = (const char *)sn;
|
||
unsigned long n;
|
||
char *endp;
|
||
|
||
if (!p)
|
||
return NULL;
|
||
if (*p != '(')
|
||
BUG (); /* Not a valid S-expression. */
|
||
n = strtoul (p+1, &endp, 10);
|
||
p = endp;
|
||
if (*p != ':')
|
||
BUG (); /* Not a valid S-expression. */
|
||
return bin2hex (p+1, n, NULL);
|
||
}
|
||
|
||
|
||
/* Return a malloced string with the serial number and the issuer DN
|
||
of the certificate. */
|
||
static char *
|
||
get_cert_name (ksba_cert_t cert)
|
||
{
|
||
char *result;
|
||
ksba_sexp_t sn;
|
||
char *issuer, *p;
|
||
|
||
if (!cert)
|
||
return xtrystrdup ("[no certificate]");
|
||
|
||
issuer = ksba_cert_get_issuer (cert, 0);
|
||
sn = ksba_cert_get_serial (cert);
|
||
if (issuer && sn)
|
||
{
|
||
p = format_serial (sn);
|
||
if (!p)
|
||
result = xtrystrdup ("[invalid S/N]");
|
||
else
|
||
{
|
||
result = xtrymalloc (strlen (p) + strlen (issuer) + 2 + 1);
|
||
if (result)
|
||
{
|
||
*result = '#';
|
||
strcpy (stpcpy (stpcpy (result+1, p),"/"), issuer);
|
||
}
|
||
xfree (p);
|
||
}
|
||
}
|
||
else
|
||
result = xtrystrdup ("[missing S/N or issuer]");
|
||
ksba_free (sn);
|
||
xfree (issuer);
|
||
return result;
|
||
}
|
||
|
||
/* Return a malloced string with the serial number and the issuer DN
|
||
of the certificate. */
|
||
static char *
|
||
get_cert_subject (ksba_cert_t cert, int idx)
|
||
{
|
||
char *result;
|
||
char *subject;
|
||
|
||
if (!cert)
|
||
return xtrystrdup ("[no certificate]");
|
||
|
||
subject = ksba_cert_get_subject (cert, idx);
|
||
if (subject)
|
||
{
|
||
result = xtrymalloc (strlen (subject) + 1 + 1);
|
||
if (result)
|
||
{
|
||
*result = '/';
|
||
strcpy (result+1, subject);
|
||
}
|
||
}
|
||
else
|
||
result = NULL;
|
||
xfree (subject);
|
||
return result;
|
||
}
|
||
|
||
|
||
/* List the given certificiate. If CERT is NULL, this is a NOP. */
|
||
static void
|
||
list_cert (audit_ctx_t ctx, ksba_cert_t cert, int with_subj)
|
||
{
|
||
char *name;
|
||
int idx;
|
||
|
||
name = get_cert_name (cert);
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
if (with_subj)
|
||
{
|
||
enter_li (ctx);
|
||
for (idx=0; (name = get_cert_subject (cert, idx)); idx++)
|
||
{
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
}
|
||
leave_li (ctx);
|
||
}
|
||
}
|
||
|
||
|
||
/* List the chain of certificates from STARTITEM up to STOPEVENT. The
|
||
certifcates are written out as comments. */
|
||
static void
|
||
list_certchain (audit_ctx_t ctx, log_item_t startitem, audit_event_t stopevent)
|
||
{
|
||
log_item_t item;
|
||
|
||
startitem = find_next_log_item (ctx, startitem, AUDIT_CHAIN_BEGIN,stopevent);
|
||
writeout_li (ctx, startitem? "Yes":"No", _("Certificate chain available"));
|
||
if (!startitem)
|
||
return;
|
||
|
||
item = find_next_log_item (ctx, startitem,
|
||
AUDIT_CHAIN_ROOTCERT, AUDIT_CHAIN_END);
|
||
if (!item)
|
||
writeout_rem (ctx, "%s", _("root certificate missing"));
|
||
else
|
||
{
|
||
list_cert (ctx, item->cert, 0);
|
||
}
|
||
item = startitem;
|
||
while ( ((item = find_next_log_item (ctx, item,
|
||
AUDIT_CHAIN_CERT, AUDIT_CHAIN_END))))
|
||
{
|
||
list_cert (ctx, item->cert, 1);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/* Process an encrypt operation's log. */
|
||
static void
|
||
proc_type_encrypt (audit_ctx_t ctx)
|
||
{
|
||
log_item_t loopitem, item;
|
||
int recp_no, idx;
|
||
char numbuf[35];
|
||
int algo;
|
||
char *name;
|
||
|
||
item = find_log_item (ctx, AUDIT_ENCRYPTION_DONE, 0);
|
||
writeout_li (ctx, item?"Yes":"No", "%s", _("Data encryption succeeded"));
|
||
|
||
enter_li (ctx);
|
||
|
||
item = find_log_item (ctx, AUDIT_GOT_DATA, 0);
|
||
writeout_li (ctx, item? "Yes":"No", "%s", _("Data available"));
|
||
|
||
item = find_log_item (ctx, AUDIT_SESSION_KEY, 0);
|
||
writeout_li (ctx, item? "Yes":"No", "%s", _("Session key created"));
|
||
if (item)
|
||
{
|
||
algo = gcry_cipher_map_name (item->string);
|
||
if (algo)
|
||
writeout_rem (ctx, _("algorithm: %s"), gnupg_cipher_algo_name (algo));
|
||
else if (item->string && !strcmp (item->string, "1.2.840.113549.3.2"))
|
||
writeout_rem (ctx, _("unsupported algorithm: %s"), "RC2");
|
||
else if (item->string)
|
||
writeout_rem (ctx, _("unsupported algorithm: %s"), item->string);
|
||
else
|
||
writeout_rem (ctx, _("seems to be not encrypted"));
|
||
}
|
||
|
||
item = find_log_item (ctx, AUDIT_GOT_RECIPIENTS, 0);
|
||
snprintf (numbuf, sizeof numbuf, "%d",
|
||
item && item->have_intvalue? item->intvalue : 0);
|
||
writeout_li (ctx, numbuf, "%s", _("Number of recipients"));
|
||
|
||
/* Loop over all recipients. */
|
||
loopitem = NULL;
|
||
recp_no = 0;
|
||
while ((loopitem=find_next_log_item (ctx, loopitem, AUDIT_ENCRYPTED_TO, 0)))
|
||
{
|
||
recp_no++;
|
||
writeout_li (ctx, NULL, _("Recipient %d"), recp_no);
|
||
if (loopitem->cert)
|
||
{
|
||
name = get_cert_name (loopitem->cert);
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
enter_li (ctx);
|
||
for (idx=0; (name = get_cert_subject (loopitem->cert, idx)); idx++)
|
||
{
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
}
|
||
leave_li (ctx);
|
||
}
|
||
}
|
||
|
||
leave_li (ctx);
|
||
}
|
||
|
||
|
||
|
||
/* Process a sign operation's log. */
|
||
static void
|
||
proc_type_sign (audit_ctx_t ctx)
|
||
{
|
||
log_item_t item, loopitem;
|
||
int signer, idx;
|
||
const char *result;
|
||
ksba_cert_t cert;
|
||
char *name;
|
||
int lastalgo;
|
||
|
||
item = find_log_item (ctx, AUDIT_SIGNING_DONE, 0);
|
||
writeout_li (ctx, item?"Yes":"No", "%s", _("Data signing succeeded"));
|
||
|
||
enter_li (ctx);
|
||
|
||
item = find_log_item (ctx, AUDIT_GOT_DATA, 0);
|
||
writeout_li (ctx, item? "Yes":"No", "%s", _("Data available"));
|
||
/* Write remarks with the data hash algorithms. We use a very
|
||
simple scheme to avoid some duplicates. */
|
||
loopitem = NULL;
|
||
lastalgo = 0;
|
||
while ((loopitem = find_next_log_item
|
||
(ctx, loopitem, AUDIT_DATA_HASH_ALGO, AUDIT_NEW_SIG)))
|
||
{
|
||
if (loopitem->intvalue && loopitem->intvalue != lastalgo)
|
||
writeout_rem (ctx, _("data hash algorithm: %s"),
|
||
gcry_md_algo_name (loopitem->intvalue));
|
||
lastalgo = loopitem->intvalue;
|
||
}
|
||
|
||
/* Loop over all signer. */
|
||
loopitem = NULL;
|
||
signer = 0;
|
||
while ((loopitem=find_next_log_item (ctx, loopitem, AUDIT_NEW_SIG, 0)))
|
||
{
|
||
signer++;
|
||
|
||
item = find_next_log_item (ctx, loopitem, AUDIT_SIGNED_BY, AUDIT_NEW_SIG);
|
||
if (!item)
|
||
result = "error";
|
||
else if (!item->err)
|
||
result = "okay";
|
||
else if (gpg_err_code (item->err) == GPG_ERR_CANCELED)
|
||
result = "skipped";
|
||
else
|
||
result = gpg_strerror (item->err);
|
||
cert = item? item->cert : NULL;
|
||
|
||
writeout_li (ctx, result, _("Signer %d"), signer);
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_ATTR_HASH_ALGO, AUDIT_NEW_SIG);
|
||
if (item)
|
||
writeout_rem (ctx, _("attr hash algorithm: %s"),
|
||
gcry_md_algo_name (item->intvalue));
|
||
|
||
if (cert)
|
||
{
|
||
name = get_cert_name (cert);
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
enter_li (ctx);
|
||
for (idx=0; (name = get_cert_subject (cert, idx)); idx++)
|
||
{
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
}
|
||
leave_li (ctx);
|
||
}
|
||
}
|
||
|
||
leave_li (ctx);
|
||
}
|
||
|
||
|
||
|
||
/* Process a decrypt operation's log. */
|
||
static void
|
||
proc_type_decrypt (audit_ctx_t ctx)
|
||
{
|
||
log_item_t loopitem, item;
|
||
int algo, recpno;
|
||
char *name;
|
||
char numbuf[35];
|
||
int idx;
|
||
|
||
item = find_log_item (ctx, AUDIT_DECRYPTION_RESULT, 0);
|
||
writeout_li (ctx, item && !item->err?"Yes":"No",
|
||
"%s", _("Data decryption succeeded"));
|
||
|
||
enter_li (ctx);
|
||
|
||
item = find_log_item (ctx, AUDIT_GOT_DATA, 0);
|
||
writeout_li (ctx, item? "Yes":"No", "%s", _("Data available"));
|
||
|
||
item = find_log_item (ctx, AUDIT_DATA_CIPHER_ALGO, 0);
|
||
algo = item? item->intvalue : 0;
|
||
writeout_li (ctx, algo?"Yes":"No", "%s", _("Encryption algorithm supported"));
|
||
if (algo)
|
||
writeout_rem (ctx, _("algorithm: %s"), gnupg_cipher_algo_name (algo));
|
||
|
||
item = find_log_item (ctx, AUDIT_BAD_DATA_CIPHER_ALGO, 0);
|
||
if (item && item->string)
|
||
{
|
||
algo = gcry_cipher_map_name (item->string);
|
||
if (algo)
|
||
writeout_rem (ctx, _("algorithm: %s"), gnupg_cipher_algo_name (algo));
|
||
else if (item->string && !strcmp (item->string, "1.2.840.113549.3.2"))
|
||
writeout_rem (ctx, _("unsupported algorithm: %s"), "RC2");
|
||
else if (item->string)
|
||
writeout_rem (ctx, _("unsupported algorithm: %s"), item->string);
|
||
else
|
||
writeout_rem (ctx, _("seems to be not encrypted"));
|
||
}
|
||
|
||
|
||
for (recpno = 0, item = NULL;
|
||
(item = find_next_log_item (ctx, item, AUDIT_NEW_RECP, 0)); recpno++)
|
||
;
|
||
snprintf (numbuf, sizeof numbuf, "%d", recpno);
|
||
writeout_li (ctx, numbuf, "%s", _("Number of recipients"));
|
||
|
||
/* Loop over all recipients. */
|
||
loopitem = NULL;
|
||
while ((loopitem = find_next_log_item (ctx, loopitem, AUDIT_NEW_RECP, 0)))
|
||
{
|
||
const char *result;
|
||
|
||
recpno = loopitem->have_intvalue? loopitem->intvalue : -1;
|
||
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_RECP_RESULT, AUDIT_NEW_RECP);
|
||
if (!item)
|
||
result = "not-used";
|
||
else if (!item->err)
|
||
result = "okay";
|
||
else if (gpg_err_code (item->err) == GPG_ERR_CANCELED)
|
||
result = "skipped";
|
||
else
|
||
result = gpg_strerror (item->err);
|
||
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_RECP_NAME, AUDIT_NEW_RECP);
|
||
writeout_li (ctx, result, _("Recipient %d"), recpno);
|
||
if (item && item->string)
|
||
writeout_rem (ctx, "%s", item->string);
|
||
|
||
/* If we have a certificate write out more infos. */
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_SAVE_CERT, AUDIT_NEW_RECP);
|
||
if (item && item->cert)
|
||
{
|
||
enter_li (ctx);
|
||
for (idx=0; (name = get_cert_subject (item->cert, idx)); idx++)
|
||
{
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
}
|
||
leave_li (ctx);
|
||
}
|
||
}
|
||
|
||
leave_li (ctx);
|
||
}
|
||
|
||
|
||
|
||
/* Process a verification operation's log. */
|
||
static void
|
||
proc_type_verify (audit_ctx_t ctx)
|
||
{
|
||
log_item_t loopitem, item;
|
||
int signo, count, idx, n_good, n_bad;
|
||
char numbuf[35];
|
||
const char *result;
|
||
|
||
/* If there is at least one signature status we claim that the
|
||
verification succeeded. This does not mean that the data has
|
||
verified okay. */
|
||
item = find_log_item (ctx, AUDIT_SIG_STATUS, 0);
|
||
writeout_li (ctx, item?"Yes":"No", "%s", _("Data verification succeeded"));
|
||
enter_li (ctx);
|
||
|
||
item = find_log_item (ctx, AUDIT_GOT_DATA, AUDIT_NEW_SIG);
|
||
writeout_li (ctx, item? "Yes":"No", "%s", _("Data available"));
|
||
if (!item)
|
||
goto leave;
|
||
|
||
item = find_log_item (ctx, AUDIT_NEW_SIG, 0);
|
||
writeout_li (ctx, item? "Yes":"No", "%s", _("Signature available"));
|
||
if (!item)
|
||
goto leave;
|
||
|
||
/* Print info about the used data hashing algorithms. */
|
||
for (idx=0, n_good=n_bad=0; idx < ctx->logused; idx++)
|
||
{
|
||
item = ctx->log + idx;
|
||
if (item->event == AUDIT_NEW_SIG)
|
||
break;
|
||
else if (item->event == AUDIT_DATA_HASH_ALGO)
|
||
n_good++;
|
||
else if (item->event == AUDIT_BAD_DATA_HASH_ALGO)
|
||
n_bad++;
|
||
}
|
||
item = find_log_item (ctx, AUDIT_DATA_HASHING, AUDIT_NEW_SIG);
|
||
if (!item || item->err || !n_good)
|
||
result = "No";
|
||
else if (n_good && !n_bad)
|
||
result = "Yes";
|
||
else
|
||
result = "Some";
|
||
writeout_li (ctx, result, "%s", _("Parsing data succeeded"));
|
||
if (n_good || n_bad)
|
||
{
|
||
for (idx=0; idx < ctx->logused; idx++)
|
||
{
|
||
item = ctx->log + idx;
|
||
if (item->event == AUDIT_NEW_SIG)
|
||
break;
|
||
else if (item->event == AUDIT_DATA_HASH_ALGO)
|
||
writeout_rem (ctx, _("data hash algorithm: %s"),
|
||
gcry_md_algo_name (item->intvalue));
|
||
else if (item->event == AUDIT_BAD_DATA_HASH_ALGO)
|
||
writeout_rem (ctx, _("bad data hash algorithm: %s"),
|
||
item->string? item->string:"?");
|
||
}
|
||
}
|
||
|
||
|
||
/* Loop over all signatures. */
|
||
loopitem = find_log_item (ctx, AUDIT_NEW_SIG, 0);
|
||
assert (loopitem);
|
||
do
|
||
{
|
||
signo = loopitem->have_intvalue? loopitem->intvalue : -1;
|
||
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_SIG_STATUS, AUDIT_NEW_SIG);
|
||
writeout_li (ctx, item? item->string:"?", _("Signature %d"), signo);
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_SIG_NAME, AUDIT_NEW_SIG);
|
||
if (item)
|
||
writeout_rem (ctx, "%s", item->string);
|
||
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_DATA_HASH_ALGO, AUDIT_NEW_SIG);
|
||
if (item)
|
||
writeout_rem (ctx, _("data hash algorithm: %s"),
|
||
gcry_md_algo_name (item->intvalue));
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_ATTR_HASH_ALGO, AUDIT_NEW_SIG);
|
||
if (item)
|
||
writeout_rem (ctx, _("attr hash algorithm: %s"),
|
||
gcry_md_algo_name (item->intvalue));
|
||
|
||
enter_li (ctx);
|
||
|
||
/* List the certificate chain. */
|
||
list_certchain (ctx, loopitem, AUDIT_NEW_SIG);
|
||
|
||
/* Show the result of the chain validation. */
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_CHAIN_STATUS, AUDIT_NEW_SIG);
|
||
if (item && item->have_err)
|
||
{
|
||
writeout_li (ctx, item->err? "No":"Yes",
|
||
_("Certificate chain valid"));
|
||
if (item->err)
|
||
writeout_rem (ctx, "%s", gpg_strerror (item->err));
|
||
}
|
||
|
||
/* Show whether the root certificate is fine. */
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_ROOT_TRUSTED, AUDIT_CHAIN_STATUS);
|
||
if (item)
|
||
{
|
||
writeout_li (ctx, item->err?"No":"Yes", "%s",
|
||
_("Root certificate trustworthy"));
|
||
if (item->err)
|
||
{
|
||
add_helptag (ctx, "gpgsm.root-cert-not-trusted");
|
||
writeout_rem (ctx, "%s", gpg_strerror (item->err));
|
||
list_cert (ctx, item->cert, 0);
|
||
}
|
||
}
|
||
|
||
/* Show result of the CRL/OCSP check. */
|
||
item = find_next_log_item (ctx, loopitem,
|
||
AUDIT_CRL_CHECK, AUDIT_NEW_SIG);
|
||
if (item)
|
||
{
|
||
const char *ok;
|
||
switch (gpg_err_code (item->err))
|
||
{
|
||
case 0: ok = "good"; break;
|
||
case GPG_ERR_CERT_REVOKED: ok = "bad"; break;
|
||
case GPG_ERR_NOT_ENABLED: ok = "disabled"; break;
|
||
case GPG_ERR_NO_CRL_KNOWN:
|
||
ok = _("no CRL found for certificate");
|
||
break;
|
||
case GPG_ERR_CRL_TOO_OLD:
|
||
ok = _("the available CRL is too old");
|
||
break;
|
||
default: ok = gpg_strerror (item->err); break;
|
||
}
|
||
|
||
writeout_li (ctx, ok, "%s", _("CRL/OCSP check of certificates"));
|
||
if (item->err
|
||
&& gpg_err_code (item->err) != GPG_ERR_CERT_REVOKED
|
||
&& gpg_err_code (item->err) != GPG_ERR_NOT_ENABLED)
|
||
add_helptag (ctx, "gpgsm.crl-problem");
|
||
}
|
||
|
||
leave_li (ctx);
|
||
}
|
||
while ((loopitem = find_next_log_item (ctx, loopitem, AUDIT_NEW_SIG, 0)));
|
||
|
||
|
||
leave:
|
||
/* Always list the certificates stored in the signature. */
|
||
item = NULL;
|
||
count = 0;
|
||
while ( ((item = find_next_log_item (ctx, item,
|
||
AUDIT_SAVE_CERT, AUDIT_NEW_SIG))))
|
||
count++;
|
||
snprintf (numbuf, sizeof numbuf, "%d", count);
|
||
writeout_li (ctx, numbuf, _("Included certificates"));
|
||
item = NULL;
|
||
while ( ((item = find_next_log_item (ctx, item,
|
||
AUDIT_SAVE_CERT, AUDIT_NEW_SIG))))
|
||
{
|
||
char *name = get_cert_name (item->cert);
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
enter_li (ctx);
|
||
for (idx=0; (name = get_cert_subject (item->cert, idx)); idx++)
|
||
{
|
||
writeout_rem (ctx, "%s", name);
|
||
xfree (name);
|
||
}
|
||
leave_li (ctx);
|
||
}
|
||
leave_li (ctx);
|
||
}
|
||
|
||
|
||
|
||
|
||
/* Print the formatted audit result. THIS IS WORK IN PROGRESS. */
|
||
void
|
||
audit_print_result (audit_ctx_t ctx, estream_t out, int use_html)
|
||
{
|
||
int idx;
|
||
size_t n;
|
||
log_item_t item;
|
||
helptag_t helptag;
|
||
const char *s;
|
||
int show_raw = 0;
|
||
char *orig_codeset;
|
||
|
||
if (!ctx)
|
||
return;
|
||
|
||
orig_codeset = i18n_switchto_utf8 ();
|
||
|
||
/* We use an environment variable to include some debug info in the
|
||
log. */
|
||
if ((s = getenv ("gnupg_debug_audit")))
|
||
show_raw = 1;
|
||
|
||
assert (!ctx->outstream);
|
||
ctx->outstream = out;
|
||
ctx->use_html = use_html;
|
||
ctx->indentlevel = 0;
|
||
clear_helptags (ctx);
|
||
|
||
if (use_html)
|
||
es_fputs ("<div class=\"GnuPGAuditLog\">\n", ctx->outstream);
|
||
|
||
if (!ctx->log || !ctx->logused)
|
||
{
|
||
writeout_para (ctx, _("No audit log entries."));
|
||
goto leave;
|
||
}
|
||
|
||
if (show_raw)
|
||
{
|
||
int maxlen;
|
||
|
||
for (idx=0,maxlen=0; idx < DIM (eventstr_msgidx); idx++)
|
||
{
|
||
n = strlen (eventstr_msgstr + eventstr_msgidx[idx]);
|
||
if (n > maxlen)
|
||
maxlen = n;
|
||
}
|
||
|
||
if (use_html)
|
||
es_fputs ("<pre>\n", out);
|
||
for (idx=0; idx < ctx->logused; idx++)
|
||
{
|
||
es_fprintf (out, "log: %-*s",
|
||
maxlen, event2str (ctx->log[idx].event));
|
||
if (ctx->log[idx].have_intvalue)
|
||
es_fprintf (out, " i=%d", ctx->log[idx].intvalue);
|
||
if (ctx->log[idx].string)
|
||
{
|
||
es_fputs (" s='", out);
|
||
writeout (ctx, ctx->log[idx].string);
|
||
es_fputs ("'", out);
|
||
}
|
||
if (ctx->log[idx].cert)
|
||
es_fprintf (out, " has_cert");
|
||
if (ctx->log[idx].have_err)
|
||
{
|
||
es_fputs (" err='", out);
|
||
writeout (ctx, gpg_strerror (ctx->log[idx].err));
|
||
es_fputs ("'", out);
|
||
}
|
||
es_fputs ("\n", out);
|
||
}
|
||
if (use_html)
|
||
es_fputs ("</pre>\n", out);
|
||
else
|
||
es_fputs ("\n", out);
|
||
}
|
||
|
||
enter_li (ctx);
|
||
switch (ctx->type)
|
||
{
|
||
case AUDIT_TYPE_NONE:
|
||
writeout_li (ctx, NULL, _("Unknown operation"));
|
||
break;
|
||
case AUDIT_TYPE_ENCRYPT:
|
||
proc_type_encrypt (ctx);
|
||
break;
|
||
case AUDIT_TYPE_SIGN:
|
||
proc_type_sign (ctx);
|
||
break;
|
||
case AUDIT_TYPE_DECRYPT:
|
||
proc_type_decrypt (ctx);
|
||
break;
|
||
case AUDIT_TYPE_VERIFY:
|
||
proc_type_verify (ctx);
|
||
break;
|
||
}
|
||
item = find_log_item (ctx, AUDIT_AGENT_READY, 0);
|
||
if (item && item->have_err)
|
||
{
|
||
writeout_li (ctx, item->err? "No":"Yes", "%s", _("Gpg-Agent usable"));
|
||
if (item->err)
|
||
{
|
||
writeout_rem (ctx, "%s", gpg_strerror (item->err));
|
||
add_helptag (ctx, "gnupg.agent-problem");
|
||
}
|
||
}
|
||
item = find_log_item (ctx, AUDIT_DIRMNGR_READY, 0);
|
||
if (item && item->have_err)
|
||
{
|
||
writeout_li (ctx, item->err? "No":"Yes", "%s", _("Dirmngr usable"));
|
||
if (item->err)
|
||
{
|
||
writeout_rem (ctx, "%s", gpg_strerror (item->err));
|
||
add_helptag (ctx, "gnupg.dirmngr-problem");
|
||
}
|
||
}
|
||
leave_li (ctx);
|
||
|
||
|
||
/* Show the help from the collected help tags. */
|
||
if (ctx->helptags)
|
||
{
|
||
if (use_html)
|
||
{
|
||
es_fputs ("<hr/>\n", ctx->outstream);
|
||
if (ctx->helptags->next)
|
||
es_fputs ("<ul>\n", ctx->outstream);
|
||
}
|
||
else
|
||
es_fputs ("\n\n", ctx->outstream);
|
||
}
|
||
for (helptag = ctx->helptags; helptag; helptag = helptag->next)
|
||
{
|
||
char *text;
|
||
|
||
if (use_html && ctx->helptags->next)
|
||
es_fputs ("<li>\n", ctx->outstream);
|
||
|
||
text = gnupg_get_help_string (helptag->name, 0);
|
||
if (text)
|
||
{
|
||
writeout_para (ctx, "%s", text);
|
||
xfree (text);
|
||
}
|
||
else
|
||
writeout_para (ctx, _("No help available for '%s'."), helptag->name);
|
||
if (use_html && ctx->helptags->next)
|
||
es_fputs ("</li>\n", ctx->outstream);
|
||
if (helptag->next)
|
||
es_fputs ("\n", ctx->outstream);
|
||
}
|
||
if (use_html && ctx->helptags && ctx->helptags->next)
|
||
es_fputs ("</ul>\n", ctx->outstream);
|
||
|
||
leave:
|
||
if (use_html)
|
||
es_fputs ("</div>\n", ctx->outstream);
|
||
ctx->outstream = NULL;
|
||
ctx->use_html = 0;
|
||
clear_helptags (ctx);
|
||
i18n_switchback (orig_codeset);
|
||
}
|