diff options
Diffstat (limited to 'openvpn/src/openvpn/error.c')
-rw-r--r-- | openvpn/src/openvpn/error.c | 900 |
1 files changed, 900 insertions, 0 deletions
diff --git a/openvpn/src/openvpn/error.c b/openvpn/src/openvpn/error.c new file mode 100644 index 00000000..68497941 --- /dev/null +++ b/openvpn/src/openvpn/error.c @@ -0,0 +1,900 @@ +/* + * OpenVPN -- An application to securely tunnel IP networks + * over a single TCP/UDP port, with support for SSL/TLS-based + * session authentication and key exchange, + * packet encryption, packet authentication, and + * packet compression. + * + * Copyright (C) 2002-2010 OpenVPN Technologies, Inc. <sales@openvpn.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program 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 (see the file COPYING included with this + * distribution); if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#elif defined(_MSC_VER) +#include "config-msvc.h" +#endif + +#include "syshead.h" + +#include "error.h" +#include "buffer.h" +#include "misc.h" +#include "win32.h" +#include "socket.h" +#include "tun.h" +#include "otime.h" +#include "perf.h" +#include "status.h" +#include "integer.h" +#include "ps.h" +#include "mstats.h" + +#ifdef ENABLE_CRYPTO +#ifdef ENABLE_CRYPTO_OPENSSL +#include <openssl/err.h> +#endif +#endif + +#include "memdbg.h" + +#if SYSLOG_CAPABILITY +#ifndef LOG_OPENVPN +#define LOG_OPENVPN LOG_DAEMON +#endif +#endif + +/* Globals */ +unsigned int x_debug_level; /* GLOBAL */ + +/* Mute state */ +static int mute_cutoff; /* GLOBAL */ +static int mute_count; /* GLOBAL */ +static int mute_category; /* GLOBAL */ + +/* + * Output mode priorities are as follows: + * + * (1) --log-x overrides everything + * (2) syslog is used if --daemon or --inetd is defined and not --log-x + * (3) if OPENVPN_DEBUG_COMMAND_LINE is defined, output + * to constant logfile name. + * (4) Output to stdout. + */ + +/* If true, indicates that stdin/stdout/stderr + have been redirected due to --log */ +static bool std_redir; /* GLOBAL */ + +/* Should messages be written to the syslog? */ +static bool use_syslog; /* GLOBAL */ + +/* Should timestamps be included on messages to stdout/stderr? */ +static bool suppress_timestamps; /* GLOBAL */ + +/* The program name passed to syslog */ +#if SYSLOG_CAPABILITY +static char *pgmname_syslog; /* GLOBAL */ +#endif + +/* If non-null, messages should be written here (used for debugging only) */ +static FILE *msgfp; /* GLOBAL */ + +/* If true, we forked from main OpenVPN process */ +static bool forked; /* GLOBAL */ + +/* our default output targets */ +static FILE *default_out; /* GLOBAL */ +static FILE *default_err; /* GLOBAL */ + +void +msg_forked (void) +{ + forked = true; +} + +bool +set_debug_level (const int level, const unsigned int flags) +{ + const int ceiling = 15; + + if (level >= 0 && level <= ceiling) + { + x_debug_level = level; + return true; + } + else if (flags & SDL_CONSTRAIN) + { + x_debug_level = constrain_int (level, 0, ceiling); + return true; + } + return false; +} + +bool +set_mute_cutoff (const int cutoff) +{ + if (cutoff >= 0) + { + mute_cutoff = cutoff; + return true; + } + else + return false; +} + +int +get_debug_level (void) +{ + return x_debug_level; +} + +int +get_mute_cutoff (void) +{ + return mute_cutoff; +} + +void +set_suppress_timestamps (bool suppressed) +{ + suppress_timestamps = suppressed; +} + +void +error_reset () +{ + use_syslog = std_redir = false; + suppress_timestamps = false; + x_debug_level = 1; + mute_cutoff = 0; + mute_count = 0; + mute_category = 0; + default_out = OPENVPN_MSG_FP; + default_err = OPENVPN_MSG_FP; + +#ifdef OPENVPN_DEBUG_COMMAND_LINE + msgfp = fopen (OPENVPN_DEBUG_FILE, "w"); + if (!msgfp) + openvpn_exit (OPENVPN_EXIT_STATUS_CANNOT_OPEN_DEBUG_FILE); /* exit point */ +#else + msgfp = NULL; +#endif +} + +void +errors_to_stderr (void) +{ + default_err = OPENVPN_ERROR_FP; +} + +/* + * Return a file to print messages to before syslog is opened. + */ +FILE * +msg_fp(const unsigned int flags) +{ + FILE *fp = msgfp; + if (!fp) + fp = (flags & (M_FATAL|M_USAGE_SMALL)) ? default_err : default_out; + if (!fp) + openvpn_exit (OPENVPN_EXIT_STATUS_CANNOT_OPEN_DEBUG_FILE); /* exit point */ + return fp; +} + +#define SWAP { tmp = m1; m1 = m2; m2 = tmp; } + +int x_msg_line_num; /* GLOBAL */ + +#include "android/log.h" + + + + +void x_msg (const unsigned int flags, const char *format, ...) +{ + struct gc_arena gc; + va_list arglist; +#if SYSLOG_CAPABILITY + int level; +#endif + char *m1; + char *m2; + char *tmp; + int e; + const char *prefix; + const char *prefix_sep; + + void usage_small (void); + + +#ifndef HAVE_VARARG_MACROS + /* the macro has checked this otherwise */ + if (!MSG_TEST (flags)) + return; +#endif + + if (flags & M_ERRNO_SOCK) + e = openvpn_errno_socket (); + else + e = openvpn_errno (); + + /* + * Apply muting filter. + */ +#ifndef HAVE_VARARG_MACROS + /* the macro has checked this otherwise */ + if (!dont_mute (flags)) + return; +#endif + + gc_init (&gc); + + m1 = (char *) gc_malloc (ERR_BUF_SIZE, false, &gc); + m2 = (char *) gc_malloc (ERR_BUF_SIZE, false, &gc); + + va_start (arglist, format); + vsnprintf (m1, ERR_BUF_SIZE, format, arglist); + va_end (arglist); + m1[ERR_BUF_SIZE - 1] = 0; /* windows vsnprintf needs this */ + + if ((flags & (M_ERRNO|M_ERRNO_SOCK)) && e) + { + openvpn_snprintf (m2, ERR_BUF_SIZE, "%s: %s (errno=%d)", + m1, strerror_ts (e, &gc), e); + SWAP; + } + +#ifdef ENABLE_CRYPTO +#ifdef ENABLE_CRYPTO_OPENSSL + if (flags & M_SSL) + { + int nerrs = 0; + int err; + while ((err = ERR_get_error ())) + { + openvpn_snprintf (m2, ERR_BUF_SIZE, "%s: %s", + m1, ERR_error_string (err, NULL)); + SWAP; + ++nerrs; + } + if (!nerrs) + { + openvpn_snprintf (m2, ERR_BUF_SIZE, "%s (OpenSSL)", m1); + SWAP; + } + } +#endif +#endif + + if (flags & M_OPTERR) + { + openvpn_snprintf (m2, ERR_BUF_SIZE, "Options error: %s", m1); + SWAP; + } + +#if SYSLOG_CAPABILITY + if (flags & (M_FATAL|M_NONFATAL|M_USAGE_SMALL)) + level = LOG_ERR; + else if (flags & M_WARN) + level = LOG_WARNING; + else + level = LOG_NOTICE; +#endif + + /* set up client prefix */ + if (flags & M_NOIPREFIX) + prefix = NULL; + else + prefix = msg_get_prefix (); + prefix_sep = " "; + if (!prefix) + prefix_sep = prefix = ""; + + + /* virtual output capability used to copy output to management subsystem */ + if (!forked) + { + const struct virtual_output *vo = msg_get_virtual_output (); + + if (vo) + { + openvpn_snprintf (m2, ERR_BUF_SIZE, "%s%s%s", + prefix, + prefix_sep, + m1); + virtual_output_print (vo, flags, m2); + } + } + + if (!(flags & M_MSG_VIRT_OUT) ) + { + if (use_syslog && !std_redir && !forked) + { +#if SYSLOG_CAPABILITY + syslog (level, "%s%s%s", + prefix, + prefix_sep, + m1); +#endif + } + else // No Syslog + { + FILE *fp = msg_fp(flags); + const bool show_usec = check_debug_level (DEBUG_LEVEL_USEC_TIME); + + if ((flags & M_NOPREFIX) || suppress_timestamps) + { + fprintf (fp, "%s%s%s%s", + prefix, + prefix_sep, + m1, + (flags&M_NOLF) ? "" : "\n"); + } + else + { + fprintf (fp, "%s %s%s%s%s", + time_string (0, 0, show_usec, &gc), + prefix, + prefix_sep, + m1, + (flags&M_NOLF) ? "" : "\n"); + } + fflush(fp); + ++x_msg_line_num; + } +#ifdef TARGET_ANDROID + android_openvpn_log(prefix,prefix_sep,m1);; +#endif + } + + + + if (flags & M_FATAL) + msg (M_INFO, "Exiting due to fatal error"); + + if (flags & M_FATAL) + openvpn_exit (OPENVPN_EXIT_STATUS_ERROR); /* exit point */ + + if (flags & M_USAGE_SMALL) + usage_small (); + + gc_free (&gc); +} + +/* + * Apply muting filter. + */ +bool +dont_mute (unsigned int flags) +{ + bool ret = true; + if (mute_cutoff > 0 && !(flags & M_NOMUTE)) + { + const int mute_level = DECODE_MUTE_LEVEL (flags); + if (mute_level > 0 && mute_level == mute_category) + { + if (mute_count == mute_cutoff) + msg (M_INFO | M_NOMUTE, "NOTE: --mute triggered..."); + if (++mute_count > mute_cutoff) + ret = false; + } + else + { + const int suppressed = mute_count - mute_cutoff; + if (suppressed > 0) + msg (M_INFO | M_NOMUTE, + "%d variation(s) on previous %d message(s) suppressed by --mute", + suppressed, + mute_cutoff); + mute_count = 1; + mute_category = mute_level; + } + } + return ret; +} + +void +assert_failed (const char *filename, int line) +{ + msg (M_FATAL, "Assertion failed at %s:%d", filename, line); +} + +/* + * Fail memory allocation. Don't use msg() because it tries + * to allocate memory as part of its operation. + */ +void +out_of_memory (void) +{ + fprintf (stderr, PACKAGE_NAME ": Out of Memory\n"); + exit (1); +} + +void +open_syslog (const char *pgmname, bool stdio_to_null) +{ +#if SYSLOG_CAPABILITY + if (!msgfp && !std_redir) + { + if (!use_syslog) + { + pgmname_syslog = string_alloc (pgmname ? pgmname : PACKAGE, NULL); + openlog (pgmname_syslog, LOG_PID, LOG_OPENVPN); + use_syslog = true; + + /* Better idea: somehow pipe stdout/stderr output to msg() */ + if (stdio_to_null) + set_std_files_to_null (false); + } + } +#else + msg (M_WARN, "Warning on use of --daemon/--inetd: this operating system lacks daemon logging features, therefore when I become a daemon, I won't be able to log status or error messages"); +#endif +} + +void +close_syslog () +{ +#if SYSLOG_CAPABILITY + if (use_syslog) + { + closelog(); + use_syslog = false; + if (pgmname_syslog) + { + free (pgmname_syslog); + pgmname_syslog = NULL; + } + } +#endif +} + +#ifdef WIN32 + +static HANDLE orig_stderr; + +HANDLE +get_orig_stderr (void) +{ + if (orig_stderr) + return orig_stderr; + else + return GetStdHandle (STD_ERROR_HANDLE); +} + +#endif + +void +redirect_stdout_stderr (const char *file, bool append) +{ +#if defined(WIN32) + if (!std_redir) + { + struct gc_arena gc = gc_new (); + HANDLE log_handle; + int log_fd; + + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + log_handle = CreateFileW (wide_string (file, &gc), + GENERIC_WRITE, + FILE_SHARE_READ, + &saAttr, + append ? OPEN_ALWAYS : CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + + gc_free (&gc); + + if (log_handle == INVALID_HANDLE_VALUE) + { + msg (M_WARN|M_ERRNO, "Warning: cannot open --log file: %s", file); + return; + } + + /* append to logfile? */ + if (append) + { + if (SetFilePointer (log_handle, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER) + msg (M_ERR, "Error: cannot seek to end of --log file: %s", file); + } + + /* save original stderr for password prompts */ + orig_stderr = GetStdHandle (STD_ERROR_HANDLE); + +#if 0 /* seems not be necessary with stdout/stderr redirection below*/ + /* set up for redirection */ + if (!SetStdHandle (STD_OUTPUT_HANDLE, log_handle) + || !SetStdHandle (STD_ERROR_HANDLE, log_handle)) + msg (M_ERR, "Error: cannot redirect stdout/stderr to --log file: %s", file); +#endif + + /* direct stdout/stderr to point to log_handle */ + log_fd = _open_osfhandle ((intptr_t)log_handle, _O_TEXT); + if (log_fd == -1) + msg (M_ERR, "Error: --log redirect failed due to _open_osfhandle failure"); + + /* open log_handle as FILE stream */ + ASSERT (msgfp == NULL); + msgfp = _fdopen (log_fd, "wt"); + if (msgfp == NULL) + msg (M_ERR, "Error: --log redirect failed due to _fdopen"); + + /* redirect C-library stdout/stderr to log file */ + if (_dup2 (log_fd, 1) == -1 || _dup2 (log_fd, 2) == -1) + msg (M_WARN, "Error: --log redirect of stdout/stderr failed"); + + std_redir = true; + } +#elif defined(HAVE_DUP2) + if (!std_redir) + { + int out = open (file, + O_CREAT | O_WRONLY | (append ? O_APPEND : O_TRUNC), + S_IRUSR | S_IWUSR); + + if (out < 0) + { + msg (M_WARN|M_ERRNO, "Warning: Error redirecting stdout/stderr to --log file: %s", file); + return; + } + + if (dup2 (out, 1) == -1) + msg (M_ERR, "--log file redirection error on stdout"); + if (dup2 (out, 2) == -1) + msg (M_ERR, "--log file redirection error on stderr"); + + if (out > 2) + close (out); + + std_redir = true; + } + +#else + msg (M_WARN, "WARNING: The --log option is not supported on this OS because it lacks the dup2 function"); +#endif +} + +/* + * Functions used to check return status + * of I/O operations. + */ + +unsigned int x_cs_info_level; /* GLOBAL */ +unsigned int x_cs_verbose_level; /* GLOBAL */ +unsigned int x_cs_err_delay_ms; /* GLOBAL */ + +void +reset_check_status () +{ + x_cs_info_level = 0; + x_cs_verbose_level = 0; +} + +void +set_check_status (unsigned int info_level, unsigned int verbose_level) +{ + x_cs_info_level = info_level; + x_cs_verbose_level = verbose_level; +} + +/* + * Called after most socket or tun/tap operations, via the inline + * function check_status(). + * + * Decide if we should print an error message, and see if we can + * extract any useful info from the error, such as a Path MTU hint + * from the OS. + */ +void +x_check_status (int status, + const char *description, + struct link_socket *sock, + struct tuntap *tt) +{ + const int my_errno = (sock ? openvpn_errno_socket () : (int)openvpn_errno ()); + const char *extended_msg = NULL; + + msg (x_cs_verbose_level, "%s %s returned %d", + sock ? proto2ascii (sock->info.proto, true) : "", + description, + status); + + if (status < 0) + { + struct gc_arena gc = gc_new (); +#if EXTENDED_SOCKET_ERROR_CAPABILITY + /* get extended socket error message and possible PMTU hint from OS */ + if (sock) + { + int mtu; + extended_msg = format_extended_socket_error (sock->sd, &mtu, &gc); + if (mtu > 0 && sock->mtu != mtu) + { + sock->mtu = mtu; + sock->info.mtu_changed = true; + } + } +#elif defined(WIN32) + /* get possible driver error from TAP-Windows driver */ + extended_msg = tap_win_getinfo (tt, &gc); +#endif + if (!ignore_sys_error (my_errno)) + { + if (extended_msg) + msg (x_cs_info_level, "%s %s [%s]: %s (code=%d)", + description, + sock ? proto2ascii (sock->info.proto, true) : "", + extended_msg, + strerror_ts (my_errno, &gc), + my_errno); + else + msg (x_cs_info_level, "%s %s: %s (code=%d)", + description, + sock ? proto2ascii (sock->info.proto, true) : "", + strerror_ts (my_errno, &gc), + my_errno); + + if (x_cs_err_delay_ms) + platform_sleep_milliseconds (x_cs_err_delay_ms); + } + gc_free (&gc); + } +} + +/* + * In multiclient mode, put a client-specific prefix + * before each message. + */ +const char *x_msg_prefix; /* GLOBAL */ + +/* + * Allow MSG to be redirected through a virtual_output object + */ + +const struct virtual_output *x_msg_virtual_output; /* GLOBAL */ + +/* + * Exiting. + */ + +void +openvpn_exit (const int status) +{ + if (!forked) + { + void tun_abort(); +#ifdef ENABLE_PLUGIN + void plugin_abort (void); +#endif + + tun_abort(); + +#ifdef WIN32 + uninit_win32 (); +#endif + + close_syslog (); + +#ifdef ENABLE_PLUGIN + plugin_abort (); +#endif + +#if PORT_SHARE + if (port_share) + port_share_abort (port_share); +#endif + +#ifdef ENABLE_MEMSTATS + mstats_close(); +#endif + +#ifdef ABORT_ON_ERROR + if (status == OPENVPN_EXIT_STATUS_ERROR) + abort (); +#endif + + if (status == OPENVPN_EXIT_STATUS_GOOD) + perf_output_results (); + } +#ifdef TARGET_ANDROID + android_openvpn_exit(status); +#endif + + exit (status); +} + +/* + * Translate msg flags into a string + */ +const char * +msg_flags_string (const unsigned int flags, struct gc_arena *gc) +{ + struct buffer out = alloc_buf_gc (16, gc); + if (flags == M_INFO) + buf_printf (&out, "I"); + if (flags & M_FATAL) + buf_printf (&out, "F"); + if (flags & M_NONFATAL) + buf_printf (&out, "N"); + if (flags & M_WARN) + buf_printf (&out, "W"); + if (flags & M_DEBUG) + buf_printf (&out, "D"); + return BSTR (&out); +} + +#ifdef ENABLE_DEBUG +void +crash (void) +{ + char *null = NULL; + *null = 0; +} +#endif + +#ifdef WIN32 + +const char * +strerror_win32 (DWORD errnum, struct gc_arena *gc) +{ + /* + * This code can be omitted, though often the Windows + * WSA error messages are less informative than the + * Posix equivalents. + */ +#if 1 + switch (errnum) { + /* + * When the TAP-Windows driver returns STATUS_UNSUCCESSFUL, this code + * gets returned to user space. + */ + case ERROR_GEN_FAILURE: + return "General failure (ERROR_GEN_FAILURE)"; + case ERROR_IO_PENDING: + return "I/O Operation in progress (ERROR_IO_PENDING)"; + case WSA_IO_INCOMPLETE: + return "I/O Operation in progress (WSA_IO_INCOMPLETE)"; + case WSAEINTR: + return "Interrupted system call (WSAEINTR)"; + case WSAEBADF: + return "Bad file number (WSAEBADF)"; + case WSAEACCES: + return "Permission denied (WSAEACCES)"; + case WSAEFAULT: + return "Bad address (WSAEFAULT)"; + case WSAEINVAL: + return "Invalid argument (WSAEINVAL)"; + case WSAEMFILE: + return "Too many open files (WSAEMFILE)"; + case WSAEWOULDBLOCK: + return "Operation would block (WSAEWOULDBLOCK)"; + case WSAEINPROGRESS: + return "Operation now in progress (WSAEINPROGRESS)"; + case WSAEALREADY: + return "Operation already in progress (WSAEALREADY)"; + case WSAEDESTADDRREQ: + return "Destination address required (WSAEDESTADDRREQ)"; + case WSAEMSGSIZE: + return "Message too long (WSAEMSGSIZE)"; + case WSAEPROTOTYPE: + return "Protocol wrong type for socket (WSAEPROTOTYPE)"; + case WSAENOPROTOOPT: + return "Bad protocol option (WSAENOPROTOOPT)"; + case WSAEPROTONOSUPPORT: + return "Protocol not supported (WSAEPROTONOSUPPORT)"; + case WSAESOCKTNOSUPPORT: + return "Socket type not supported (WSAESOCKTNOSUPPORT)"; + case WSAEOPNOTSUPP: + return "Operation not supported on socket (WSAEOPNOTSUPP)"; + case WSAEPFNOSUPPORT: + return "Protocol family not supported (WSAEPFNOSUPPORT)"; + case WSAEAFNOSUPPORT: + return "Address family not supported by protocol family (WSAEAFNOSUPPORT)"; + case WSAEADDRINUSE: + return "Address already in use (WSAEADDRINUSE)"; + case WSAENETDOWN: + return "Network is down (WSAENETDOWN)"; + case WSAENETUNREACH: + return "Network is unreachable (WSAENETUNREACH)"; + case WSAENETRESET: + return "Net dropped connection or reset (WSAENETRESET)"; + case WSAECONNABORTED: + return "Software caused connection abort (WSAECONNABORTED)"; + case WSAECONNRESET: + return "Connection reset by peer (WSAECONNRESET)"; + case WSAENOBUFS: + return "No buffer space available (WSAENOBUFS)"; + case WSAEISCONN: + return "Socket is already connected (WSAEISCONN)"; + case WSAENOTCONN: + return "Socket is not connected (WSAENOTCONN)"; + case WSAETIMEDOUT: + return "Connection timed out (WSAETIMEDOUT)"; + case WSAECONNREFUSED: + return "Connection refused (WSAECONNREFUSED)"; + case WSAELOOP: + return "Too many levels of symbolic links (WSAELOOP)"; + case WSAENAMETOOLONG: + return "File name too long (WSAENAMETOOLONG)"; + case WSAEHOSTDOWN: + return "Host is down (WSAEHOSTDOWN)"; + case WSAEHOSTUNREACH: + return "No Route to Host (WSAEHOSTUNREACH)"; + case WSAENOTEMPTY: + return "Directory not empty (WSAENOTEMPTY)"; + case WSAEPROCLIM: + return "Too many processes (WSAEPROCLIM)"; + case WSAEUSERS: + return "Too many users (WSAEUSERS)"; + case WSAEDQUOT: + return "Disc Quota Exceeded (WSAEDQUOT)"; + case WSAESTALE: + return "Stale NFS file handle (WSAESTALE)"; + case WSASYSNOTREADY: + return "Network SubSystem is unavailable (WSASYSNOTREADY)"; + case WSAVERNOTSUPPORTED: + return "WINSOCK DLL Version out of range (WSAVERNOTSUPPORTED)"; + case WSANOTINITIALISED: + return "Successful WSASTARTUP not yet performed (WSANOTINITIALISED)"; + case WSAEREMOTE: + return "Too many levels of remote in path (WSAEREMOTE)"; + case WSAHOST_NOT_FOUND: + return "Host not found (WSAHOST_NOT_FOUND)"; + default: + break; + } +#endif + + /* format a windows error message */ + { + char message[256]; + struct buffer out = alloc_buf_gc (256, gc); + const int status = FormatMessage ( + FORMAT_MESSAGE_IGNORE_INSERTS + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + NULL, + errnum, + 0, + message, + sizeof (message), + NULL); + if (!status) + { + buf_printf (&out, "[Unknown Win32 Error]"); + } + else + { + char *cp; + for (cp = message; *cp != '\0'; ++cp) + { + if (*cp == '\n' || *cp == '\r') + *cp = ' '; + } + + buf_printf(&out, "%s", message); + } + + return BSTR (&out); + } +} + +#endif |