mirror of
git://git.gnupg.org/gnupg.git
synced 2024-12-22 10:19:57 +01:00
Add code for a threaded LDAP access to replace the wrapper process.
Currently used for W32 and W32CE.
This commit is contained in:
parent
5b664bed4f
commit
819f3be358
@ -1,3 +1,12 @@
|
|||||||
|
2010-08-02 Werner Koch <wk@g10code.com>
|
||||||
|
|
||||||
|
* configure.ac: Require libksba 1.1.0 due to the use of
|
||||||
|
ksba_reader_set_release_notify.
|
||||||
|
|
||||||
|
2010-07-30 Werner Koch <wk@g10code.com>
|
||||||
|
|
||||||
|
* configure.ac (GNUPG_PTH_PATH) [W32]: Require version 2.0.3.
|
||||||
|
|
||||||
2010-07-25 Werner Koch <wk@g10code.com>
|
2010-07-25 Werner Koch <wk@g10code.com>
|
||||||
|
|
||||||
* configure.ac (USE_LDAPWRAPPER): AC_DEFINE and AM_CONDITIONAL it.
|
* configure.ac (USE_LDAPWRAPPER): AC_DEFINE and AM_CONDITIONAL it.
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
2010-07-26 Werner Koch <wk@g10code.com>
|
||||||
|
|
||||||
|
* estream.c (es_func_fp_write) [W32]: Write smaller chunks.
|
||||||
|
|
||||||
2010-07-25 Werner Koch <wk@g10code.com>
|
2010-07-25 Werner Koch <wk@g10code.com>
|
||||||
|
|
||||||
* argparse.c (initialize): Use ARGPARSE_PRINT_WARNING constant.
|
* argparse.c (initialize): Use ARGPARSE_PRINT_WARNING constant.
|
||||||
|
@ -955,7 +955,28 @@ es_func_fp_write (void *cookie, const void *buffer, size_t size)
|
|||||||
|
|
||||||
|
|
||||||
if (file_cookie->fp)
|
if (file_cookie->fp)
|
||||||
bytes_written = fwrite (buffer, 1, size, file_cookie->fp);
|
{
|
||||||
|
#ifdef HAVE_W32_SYSTEM
|
||||||
|
/* Using an fwrite to stdout connected to the console fails with
|
||||||
|
the error "Not enough space" for an fwrite size of >= 52KB
|
||||||
|
(tested on Windows XP SP2). To solve this we always chunk
|
||||||
|
the writes up into smaller blocks. */
|
||||||
|
bytes_written = 0;
|
||||||
|
while (bytes_written < size)
|
||||||
|
{
|
||||||
|
size_t cnt = size - bytes_written;
|
||||||
|
|
||||||
|
if (cnt > 32*1024)
|
||||||
|
cnt = 32*1024;
|
||||||
|
if (fwrite ((const char*)buffer + bytes_written,
|
||||||
|
cnt, 1, file_cookie->fp) != 1)
|
||||||
|
break; /* Write error. */
|
||||||
|
bytes_written += cnt;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
bytes_written = fwrite (buffer, 1, size, file_cookie->fp);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
else
|
else
|
||||||
bytes_written = size; /* Successfully written to the bit bucket. */
|
bytes_written = size; /* Successfully written to the bit bucket. */
|
||||||
if (bytes_written != size)
|
if (bytes_written != size)
|
||||||
|
@ -49,7 +49,7 @@ NEED_LIBASSUAN_API=2
|
|||||||
NEED_LIBASSUAN_VERSION=2.0.0
|
NEED_LIBASSUAN_VERSION=2.0.0
|
||||||
|
|
||||||
NEED_KSBA_API=1
|
NEED_KSBA_API=1
|
||||||
NEED_KSBA_VERSION=1.0.2
|
NEED_KSBA_VERSION=1.1.0
|
||||||
|
|
||||||
|
|
||||||
PACKAGE=$PACKAGE_NAME
|
PACKAGE=$PACKAGE_NAME
|
||||||
@ -785,7 +785,11 @@ AC_DEFINE_UNQUOTED(SHRED,
|
|||||||
# Check whether the GNU Pth library is available
|
# Check whether the GNU Pth library is available
|
||||||
# Note, that we include a Pth emulation for W32.
|
# Note, that we include a Pth emulation for W32.
|
||||||
#
|
#
|
||||||
GNUPG_PATH_PTH
|
if test "$have_w32_system" = yes; then
|
||||||
|
GNUPG_PATH_PTH([2.0.4])
|
||||||
|
else
|
||||||
|
GNUPG_PATH_PTH
|
||||||
|
fi
|
||||||
if test "$have_pth" = "yes"; then
|
if test "$have_pth" = "yes"; then
|
||||||
AC_DEFINE(USE_GNU_PTH, 1,
|
AC_DEFINE(USE_GNU_PTH, 1,
|
||||||
[Defined if the GNU Portable Thread Library should be used])
|
[Defined if the GNU Portable Thread Library should be used])
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
2010-07-26 Werner Koch <wk@g10code.com>
|
||||||
|
|
||||||
|
* dirmngr_ldap.c (print_ldap_entries): Remove special fwrite case
|
||||||
|
for W32 because that is now handles by estream.
|
||||||
|
|
||||||
2010-07-25 Werner Koch <wk@g10code.com>
|
2010-07-25 Werner Koch <wk@g10code.com>
|
||||||
|
|
||||||
* Makefile.am (dirmngr_SOURCES) [!USE_LDAPWRAPPER]: Build
|
* Makefile.am (dirmngr_SOURCES) [!USE_LDAPWRAPPER]: Build
|
||||||
|
@ -36,12 +36,18 @@ AM_CFLAGS = $(LIBGCRYPT_CFLAGS) $(KSBA_CFLAGS) \
|
|||||||
|
|
||||||
BUILT_SOURCES = no-libgcrypt.c
|
BUILT_SOURCES = no-libgcrypt.c
|
||||||
|
|
||||||
|
if HAVE_W32_SYSTEM
|
||||||
|
ldap_url = ldap-url.h ldap-url.c
|
||||||
|
else
|
||||||
|
ldap_url =
|
||||||
|
endif
|
||||||
|
|
||||||
noinst_HEADERS = dirmngr.h crlcache.h crlfetch.h misc.h
|
noinst_HEADERS = dirmngr.h crlcache.h crlfetch.h misc.h
|
||||||
|
|
||||||
dirmngr_SOURCES = dirmngr.c dirmngr.h server.c crlcache.c crlfetch.c \
|
dirmngr_SOURCES = dirmngr.c dirmngr.h server.c crlcache.c crlfetch.c \
|
||||||
ldapserver.h ldapserver.c certcache.c certcache.h \
|
ldapserver.h ldapserver.c certcache.c certcache.h \
|
||||||
cdb.h cdblib.c ldap.c misc.c dirmngr-err.h \
|
cdb.h cdblib.c ldap.c misc.c dirmngr-err.h \
|
||||||
ocsp.c ocsp.h validate.c validate.h ldap-wrapper.h
|
ocsp.c ocsp.h validate.c validate.h ldap-wrapper.h $(ldap_url)
|
||||||
|
|
||||||
if USE_LDAPWRAPPER
|
if USE_LDAPWRAPPER
|
||||||
dirmngr_SOURCES += ldap-wrapper.c
|
dirmngr_SOURCES += ldap-wrapper.c
|
||||||
@ -52,13 +58,11 @@ endif
|
|||||||
|
|
||||||
dirmngr_LDADD = $(libcommonpth) ../gl/libgnu.a $(DNSLIBS) $(LIBASSUAN_LIBS) \
|
dirmngr_LDADD = $(libcommonpth) ../gl/libgnu.a $(DNSLIBS) $(LIBASSUAN_LIBS) \
|
||||||
$(LIBGCRYPT_LIBS) $(KSBA_LIBS) $(PTH_LIBS) $(LIBINTL) $(LIBICONV)
|
$(LIBGCRYPT_LIBS) $(KSBA_LIBS) $(PTH_LIBS) $(LIBINTL) $(LIBICONV)
|
||||||
|
if !USE_LDAPWRAPPER
|
||||||
if HAVE_W32_SYSTEM
|
dirmngr_LDADD += $(LDAPLIBS)
|
||||||
ldap_url = ldap-url.h ldap-url.c
|
|
||||||
else
|
|
||||||
ldap_url =
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
if USE_LDAPWRAPPER
|
if USE_LDAPWRAPPER
|
||||||
dirmngr_ldap_SOURCES = dirmngr_ldap.c $(ldap_url) no-libgcrypt.c
|
dirmngr_ldap_SOURCES = dirmngr_ldap.c $(ldap_url) no-libgcrypt.c
|
||||||
dirmngr_ldap_CFLAGS = $(GPG_ERROR_CFLAGS)
|
dirmngr_ldap_CFLAGS = $(GPG_ERROR_CFLAGS)
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#ifndef USE_LDAPWRAPPER
|
||||||
|
# include <pth.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_W32_SYSTEM
|
#ifdef HAVE_W32_SYSTEM
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
@ -55,10 +58,15 @@
|
|||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
/* If we are not using the ldap wrapper process we need to include the
|
/* With the ldap wrapper, there is no need for the pth_enter and leave
|
||||||
prototype for our module's main function. */
|
functions; thus we redefine them to nops. If we are not using the
|
||||||
#ifndef USE_LDAPWRAPPER
|
ldap wrapper process we need to include the prototype for our
|
||||||
#include "./ldap-wrapper.h"
|
module's main function. */
|
||||||
|
#ifdef USE_LDAPWRAPPER
|
||||||
|
# define pth_enter() do { } while (0)
|
||||||
|
# define pth_leave() do { } while (0)
|
||||||
|
#else
|
||||||
|
# include "./ldap-wrapper.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define DEFAULT_LDAP_TIMEOUT 100 /* Arbitrary long timeout. */
|
#define DEFAULT_LDAP_TIMEOUT 100 /* Arbitrary long timeout. */
|
||||||
@ -145,6 +153,7 @@ static int process_url (my_opt_t myopt, const char *url);
|
|||||||
|
|
||||||
|
|
||||||
/* Function called by argparse.c to display information. */
|
/* Function called by argparse.c to display information. */
|
||||||
|
#ifndef USE_LDAPWRAPPER
|
||||||
static const char *
|
static const char *
|
||||||
my_strusage (int level)
|
my_strusage (int level)
|
||||||
{
|
{
|
||||||
@ -172,6 +181,7 @@ my_strusage (int level)
|
|||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
#endif /*!USE_LDAPWRAPPER*/
|
||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
@ -330,8 +340,10 @@ catch_alarm (int dummy)
|
|||||||
static void
|
static void
|
||||||
set_timeout (my_opt_t myopt)
|
set_timeout (my_opt_t myopt)
|
||||||
{
|
{
|
||||||
#ifndef HAVE_W32_SYSTEM
|
#ifdef HAVE_W32_SYSTEM
|
||||||
/* FIXME for W32. */
|
/* FIXME for W32. */
|
||||||
|
(void)myopt;
|
||||||
|
#else
|
||||||
if (myopt->alarm_timeout)
|
if (myopt->alarm_timeout)
|
||||||
alarm (myopt->alarm_timeout);
|
alarm (myopt->alarm_timeout);
|
||||||
#endif
|
#endif
|
||||||
@ -345,8 +357,9 @@ print_ldap_entries (my_opt_t myopt, LDAP *ld, LDAPMessage *msg, char *want_attr)
|
|||||||
LDAPMessage *item;
|
LDAPMessage *item;
|
||||||
int any = 0;
|
int any = 0;
|
||||||
|
|
||||||
for (item = ldap_first_entry (ld, msg); item;
|
for (pth_enter (), item = ldap_first_entry (ld, msg), pth_leave ();
|
||||||
item = ldap_next_entry (ld, item))
|
item;
|
||||||
|
pth_enter (), item = ldap_next_entry (ld, item), pth_leave ())
|
||||||
{
|
{
|
||||||
BerElement *berctx;
|
BerElement *berctx;
|
||||||
char *attr;
|
char *attr;
|
||||||
@ -366,8 +379,11 @@ print_ldap_entries (my_opt_t myopt, LDAP *ld, LDAPMessage *msg, char *want_attr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (attr = ldap_first_attribute (ld, item, &berctx); attr;
|
for (pth_enter (), attr = ldap_first_attribute (ld, item, &berctx),
|
||||||
attr = ldap_next_attribute (ld, item, berctx))
|
pth_leave ();
|
||||||
|
attr;
|
||||||
|
pth_enter (), attr = ldap_next_attribute (ld, item, berctx),
|
||||||
|
pth_leave ())
|
||||||
{
|
{
|
||||||
struct berval **values;
|
struct berval **values;
|
||||||
int idx;
|
int idx;
|
||||||
@ -404,8 +420,10 @@ print_ldap_entries (my_opt_t myopt, LDAP *ld, LDAPMessage *msg, char *want_attr)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pth_enter ();
|
||||||
values = ldap_get_values_len (ld, item, attr);
|
values = ldap_get_values_len (ld, item, attr);
|
||||||
|
pth_leave ();
|
||||||
|
|
||||||
if (!values)
|
if (!values)
|
||||||
{
|
{
|
||||||
if (myopt->verbose)
|
if (myopt->verbose)
|
||||||
@ -469,11 +487,7 @@ print_ldap_entries (my_opt_t myopt, LDAP *ld, LDAPMessage *msg, char *want_attr)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if 1
|
|
||||||
/* Note: this does not work for STDOUT on a Windows
|
|
||||||
console, where it fails with "Not enough space" for
|
|
||||||
CRLs which are 52 KB or larger. */
|
|
||||||
#warning still true - implement in estream
|
|
||||||
if (es_fwrite (values[0]->bv_val, values[0]->bv_len,
|
if (es_fwrite (values[0]->bv_val, values[0]->bv_len,
|
||||||
1, myopt->outstream) != 1)
|
1, myopt->outstream) != 1)
|
||||||
{
|
{
|
||||||
@ -484,33 +498,7 @@ print_ldap_entries (my_opt_t myopt, LDAP *ld, LDAPMessage *msg, char *want_attr)
|
|||||||
ber_free (berctx, 0);
|
ber_free (berctx, 0);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
/* On Windows console STDOUT, we have to break up the
|
|
||||||
writes into small parts. */
|
|
||||||
{
|
|
||||||
int n = 0;
|
|
||||||
while (n < values[0]->bv_len)
|
|
||||||
{
|
|
||||||
int cnt = values[0]->bv_len - n;
|
|
||||||
/* The actual limit is (52 * 1024 - 1) on Windows XP SP2. */
|
|
||||||
#define MAX_CNT (32*1024)
|
|
||||||
if (cnt > MAX_CNT)
|
|
||||||
cnt = MAX_CNT;
|
|
||||||
|
|
||||||
if (es_fwrite (((char *) values[0]->bv_val) + n, cnt, 1,
|
|
||||||
myopt->outstream) != 1)
|
|
||||||
{
|
|
||||||
log_error (_("error writing to stdout: %s\n"),
|
|
||||||
strerror (errno));
|
|
||||||
ldap_value_free_len (values);
|
|
||||||
ldap_memfree (attr);
|
|
||||||
ber_free (berctx, 0);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
n += cnt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
any = 1;
|
any = 1;
|
||||||
if (!myopt->multi)
|
if (!myopt->multi)
|
||||||
break; /* Print only the first value. */
|
break; /* Print only the first value. */
|
||||||
@ -540,6 +528,7 @@ fetch_ldap (my_opt_t myopt, const char *url, const LDAPURLDesc *ludp)
|
|||||||
int rc = 0;
|
int rc = 0;
|
||||||
char *host, *dn, *filter, *attrs[2], *attr;
|
char *host, *dn, *filter, *attrs[2], *attr;
|
||||||
int port;
|
int port;
|
||||||
|
int ret;
|
||||||
|
|
||||||
host = myopt->host? myopt->host : ludp->lud_host;
|
host = myopt->host? myopt->host : ludp->lud_host;
|
||||||
port = myopt->port? myopt->port : ludp->lud_port;
|
port = myopt->port? myopt->port : ludp->lud_port;
|
||||||
@ -594,14 +583,19 @@ fetch_ldap (my_opt_t myopt, const char *url, const LDAPURLDesc *ludp)
|
|||||||
|
|
||||||
|
|
||||||
set_timeout (myopt);
|
set_timeout (myopt);
|
||||||
|
pth_enter ();
|
||||||
ld = ldap_init (host, port);
|
ld = ldap_init (host, port);
|
||||||
|
pth_leave ();
|
||||||
if (!ld)
|
if (!ld)
|
||||||
{
|
{
|
||||||
log_error (_("LDAP init to `%s:%d' failed: %s\n"),
|
log_error (_("LDAP init to `%s:%d' failed: %s\n"),
|
||||||
host, port, strerror (errno));
|
host, port, strerror (errno));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (ldap_simple_bind_s (ld, myopt->user, myopt->pass))
|
pth_enter ();
|
||||||
|
ret = ldap_simple_bind_s (ld, myopt->user, myopt->pass);
|
||||||
|
pth_leave ();
|
||||||
|
if (ret)
|
||||||
{
|
{
|
||||||
log_error (_("binding to `%s:%d' failed: %s\n"),
|
log_error (_("binding to `%s:%d' failed: %s\n"),
|
||||||
host, port, strerror (errno));
|
host, port, strerror (errno));
|
||||||
@ -610,11 +604,13 @@ fetch_ldap (my_opt_t myopt, const char *url, const LDAPURLDesc *ludp)
|
|||||||
}
|
}
|
||||||
|
|
||||||
set_timeout (myopt);
|
set_timeout (myopt);
|
||||||
|
pth_enter ();
|
||||||
rc = ldap_search_st (ld, dn, ludp->lud_scope, filter,
|
rc = ldap_search_st (ld, dn, ludp->lud_scope, filter,
|
||||||
myopt->multi && !myopt->attr && ludp->lud_attrs?
|
myopt->multi && !myopt->attr && ludp->lud_attrs?
|
||||||
ludp->lud_attrs:attrs,
|
ludp->lud_attrs:attrs,
|
||||||
0,
|
0,
|
||||||
&myopt->timeout, &msg);
|
&myopt->timeout, &msg);
|
||||||
|
pth_leave ();
|
||||||
if (rc == LDAP_SIZELIMIT_EXCEEDED && myopt->multi)
|
if (rc == LDAP_SIZELIMIT_EXCEEDED && myopt->multi)
|
||||||
{
|
{
|
||||||
if (es_fwrite ("E\0\0\0\x09truncated", 14, 1, myopt->outstream) != 1)
|
if (es_fwrite ("E\0\0\0\x09truncated", 14, 1, myopt->outstream) != 1)
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <pth.h>
|
#include <pth.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
#include "dirmngr.h"
|
#include "dirmngr.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
@ -46,37 +47,25 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
/* To keep track of the LDAP wrapper state we use this structure. */
|
|
||||||
struct wrapper_context_s
|
/* Read a fixed amount of data from READER into BUFFER. */
|
||||||
|
static gpg_error_t
|
||||||
|
read_buffer (ksba_reader_t reader, unsigned char *buffer, size_t count)
|
||||||
{
|
{
|
||||||
struct wrapper_context_s *next;
|
gpg_error_t err;
|
||||||
|
size_t nread;
|
||||||
|
|
||||||
|
while (count)
|
||||||
|
{
|
||||||
|
err = ksba_reader_read (reader, buffer, count, &nread);
|
||||||
|
if (err)
|
||||||
|
return err;
|
||||||
|
buffer += nread;
|
||||||
|
count -= nread;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
pid_t pid; /* The pid of the wrapper process. */
|
|
||||||
int printable_pid; /* Helper to print diagnostics after the process has
|
|
||||||
been cleaned up. */
|
|
||||||
int fd; /* Connected with stdout of the ldap wrapper. */
|
|
||||||
gpg_error_t fd_error; /* Set to the gpg_error of the last read error
|
|
||||||
if any. */
|
|
||||||
int log_fd; /* Connected with stderr of the ldap wrapper. */
|
|
||||||
pth_event_t log_ev;
|
|
||||||
ctrl_t ctrl; /* Connection data. */
|
|
||||||
int ready; /* Internally used to mark to be removed contexts. */
|
|
||||||
ksba_reader_t reader; /* The ksba reader object or NULL. */
|
|
||||||
char *line; /* Used to print the log lines (malloced). */
|
|
||||||
size_t linesize;/* Allocated size of LINE. */
|
|
||||||
size_t linelen; /* Use size of LINE. */
|
|
||||||
time_t stamp; /* The last time we noticed ativity. */
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* We keep a global list of spawed wrapper process. A separate thread
|
|
||||||
makes use of this list to log error messages and to watch out for
|
|
||||||
finished processes. */
|
|
||||||
static struct wrapper_context_s *wrapper_list;
|
|
||||||
|
|
||||||
/* We need to know whether we are shutting down the process. */
|
|
||||||
static int shutting_down;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -84,25 +73,7 @@ static int shutting_down;
|
|||||||
void
|
void
|
||||||
ldap_wrapper_launch_thread (void)
|
ldap_wrapper_launch_thread (void)
|
||||||
{
|
{
|
||||||
static int done;
|
/* Not required. */
|
||||||
pth_attr_t tattr;
|
|
||||||
|
|
||||||
if (done)
|
|
||||||
return;
|
|
||||||
done = 1;
|
|
||||||
|
|
||||||
tattr = pth_attr_new();
|
|
||||||
pth_attr_set (tattr, PTH_ATTR_JOINABLE, 0);
|
|
||||||
pth_attr_set (tattr, PTH_ATTR_STACK_SIZE, 256*1024);
|
|
||||||
pth_attr_set (tattr, PTH_ATTR_NAME, "ldap-reaper");
|
|
||||||
|
|
||||||
if (!pth_spawn (tattr, ldap_wrapper_thread, NULL))
|
|
||||||
{
|
|
||||||
log_error (_("error spawning ldap wrapper reaper thread: %s\n"),
|
|
||||||
strerror (errno) );
|
|
||||||
dirmngr_exit (1);
|
|
||||||
}
|
|
||||||
pth_attr_destroy (tattr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -114,191 +85,360 @@ ldap_wrapper_launch_thread (void)
|
|||||||
void
|
void
|
||||||
ldap_wrapper_wait_connections ()
|
ldap_wrapper_wait_connections ()
|
||||||
{
|
{
|
||||||
shutting_down = 1;
|
/* Not required. */
|
||||||
while (wrapper_list)
|
|
||||||
pth_yield (NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* This function is to be used to release a context associated with the
|
|
||||||
given reader object. */
|
|
||||||
void
|
|
||||||
ldap_wrapper_release_context (ksba_reader_t reader)
|
|
||||||
{
|
|
||||||
if (!reader )
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (ctx=wrapper_list; ctx; ctx=ctx->next)
|
|
||||||
if (ctx->reader == reader)
|
|
||||||
{
|
|
||||||
if (DBG_LOOKUP)
|
|
||||||
log_info ("releasing ldap worker c=%p pid=%d/%d rdr=%p ctrl=%p/%d\n",
|
|
||||||
ctx,
|
|
||||||
(int)ctx->pid, (int)ctx->printable_pid,
|
|
||||||
ctx->reader,
|
|
||||||
ctx->ctrl, ctx->ctrl? ctx->ctrl->refcount:0);
|
|
||||||
|
|
||||||
ctx->reader = NULL;
|
|
||||||
SAFE_PTH_CLOSE (ctx->fd);
|
|
||||||
if (ctx->ctrl)
|
|
||||||
{
|
|
||||||
ctx->ctrl->refcount--;
|
|
||||||
ctx->ctrl = NULL;
|
|
||||||
}
|
|
||||||
if (ctx->fd_error)
|
|
||||||
log_info (_("reading from ldap wrapper %d failed: %s\n"),
|
|
||||||
ctx->printable_pid, gpg_strerror (ctx->fd_error));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cleanup all resources held by the connection associated with
|
/* Cleanup all resources held by the connection associated with
|
||||||
CTRL. This is used after a cancel to kill running wrappers. */
|
CTRL. This is used after a cancel to kill running wrappers. */
|
||||||
void
|
void
|
||||||
ldap_wrapper_connection_cleanup (ctrl_t ctrl)
|
ldap_wrapper_connection_cleanup (ctrl_t ctrl)
|
||||||
{
|
{
|
||||||
struct wrapper_context_s *ctx;
|
(void)ctrl;
|
||||||
|
|
||||||
for (ctx=wrapper_list; ctx; ctx=ctx->next)
|
/* Not required. */
|
||||||
if (ctx->ctrl && ctx->ctrl == ctrl)
|
|
||||||
{
|
|
||||||
ctx->ctrl->refcount--;
|
|
||||||
ctx->ctrl = NULL;
|
|
||||||
if (ctx->pid != (pid_t)(-1))
|
|
||||||
gnupg_kill_process (ctx->pid);
|
|
||||||
if (ctx->fd_error)
|
|
||||||
log_info (_("reading from ldap wrapper %d failed: %s\n"),
|
|
||||||
ctx->printable_pid, gpg_strerror (ctx->fd_error));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* The cookie we use to implement the outstream of the wrapper thread. */
|
||||||
|
struct outstream_cookie_s
|
||||||
|
{
|
||||||
|
int refcount; /* Reference counter - possible values are 1 and 2. */
|
||||||
|
|
||||||
|
int eof_seen; /* EOF indicator. */
|
||||||
|
size_t buffer_len; /* The valid length of the BUFFER. */
|
||||||
|
size_t buffer_pos; /* The next read position of the BUFFER. */
|
||||||
|
char buffer[4000]; /* Data buffer. */
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* The writer function for the outstream. This is used to transfer
|
||||||
|
the output of the ldap wrapper thread to the ksba reader object. */
|
||||||
|
static ssize_t
|
||||||
|
outstream_cookie_writer (void *cookie_arg, const void *buffer, size_t size)
|
||||||
|
{
|
||||||
|
struct outstream_cookie_s *cookie = cookie_arg;
|
||||||
|
const char *src;
|
||||||
|
char *dst;
|
||||||
|
ssize_t nwritten = 0;
|
||||||
|
|
||||||
|
src = buffer;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
/* Wait for free space. */
|
||||||
|
while (cookie->buffer_len == DIM (cookie->buffer))
|
||||||
|
{
|
||||||
|
/* Buffer is full: Wait for space. */
|
||||||
|
pth_yield (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy data. */
|
||||||
|
dst = cookie->buffer + cookie->buffer_len;
|
||||||
|
while (size && cookie->buffer_len < DIM (cookie->buffer))
|
||||||
|
{
|
||||||
|
*dst++ = *src++;
|
||||||
|
size--;
|
||||||
|
cookie->buffer_len++;
|
||||||
|
nwritten++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (size); /* Until done. */
|
||||||
|
|
||||||
|
if (nwritten)
|
||||||
|
{
|
||||||
|
/* Signal data is available - a pth_yield is sufficient because
|
||||||
|
the test is explicit. To increase performance we could do a
|
||||||
|
pth_yield to the other thread and only fall back to yielding
|
||||||
|
to any thread if that returns an error (i.e. the other thread
|
||||||
|
is not runnable). However our w32pth does not yet support
|
||||||
|
yielding to a specific thread, thus this won't help. */
|
||||||
|
pth_yield (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nwritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
outstream_release_cookie (struct outstream_cookie_s *cookie)
|
||||||
|
{
|
||||||
|
cookie->refcount--;
|
||||||
|
if (!cookie->refcount)
|
||||||
|
xfree (cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Closer function for the outstream. This deallocates the cookie if
|
||||||
|
it won't be used anymore. */
|
||||||
|
static int
|
||||||
|
outstream_cookie_closer (void *cookie_arg)
|
||||||
|
{
|
||||||
|
struct outstream_cookie_s *cookie = cookie_arg;
|
||||||
|
|
||||||
|
if (!cookie)
|
||||||
|
return 0; /* Nothing to do. */
|
||||||
|
|
||||||
|
cookie->eof_seen = 1; /* (only useful if refcount > 1) */
|
||||||
|
|
||||||
|
assert (cookie->refcount > 0);
|
||||||
|
outstream_release_cookie (cookie);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* The KSBA reader callback which takes the output of the ldap thread
|
||||||
|
form the outstream_cookie_writer and make it available to the ksba
|
||||||
|
reader. */
|
||||||
|
static int
|
||||||
|
outstream_reader_cb (void *cb_value, char *buffer, size_t count,
|
||||||
|
size_t *r_nread)
|
||||||
|
{
|
||||||
|
struct outstream_cookie_s *cookie = cb_value;
|
||||||
|
char *dst;
|
||||||
|
const char *src;
|
||||||
|
size_t nread = 0;
|
||||||
|
|
||||||
|
if (!buffer && !count && !nread)
|
||||||
|
return gpg_error (GPG_ERR_NOT_SUPPORTED); /* Rewind is not supported. */
|
||||||
|
|
||||||
|
*r_nread = 0;
|
||||||
|
dst = buffer;
|
||||||
|
|
||||||
|
while (cookie->buffer_pos == cookie->buffer_len)
|
||||||
|
{
|
||||||
|
if (cookie->eof_seen)
|
||||||
|
return gpg_error (GPG_ERR_EOF);
|
||||||
|
|
||||||
|
/* Wait for data to become available. */
|
||||||
|
pth_yield (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
src = cookie->buffer + cookie->buffer_pos;
|
||||||
|
while (count && cookie->buffer_pos < cookie->buffer_len)
|
||||||
|
{
|
||||||
|
*dst++ = *src++;
|
||||||
|
count--;
|
||||||
|
cookie->buffer_pos++;
|
||||||
|
nread++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookie->buffer_pos == cookie->buffer_len)
|
||||||
|
cookie->buffer_pos = cookie->buffer_len = 0;
|
||||||
|
|
||||||
|
/* Now there should be some space available. We do this even if
|
||||||
|
COUNT was zero so to give the writer end a chance to continue. */
|
||||||
|
pth_yield (NULL);
|
||||||
|
|
||||||
|
*r_nread = nread;
|
||||||
|
return 0; /* Success. */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* This function is called by ksba_reader_release. */
|
||||||
|
static void
|
||||||
|
outstream_reader_released (void *cb_value, ksba_reader_t r)
|
||||||
|
{
|
||||||
|
struct outstream_cookie_s *cookie = cb_value;
|
||||||
|
|
||||||
|
(void)r;
|
||||||
|
|
||||||
|
assert (cookie->refcount > 0);
|
||||||
|
outstream_release_cookie (cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* This function is to be used to release a context associated with the
|
||||||
|
given reader object. This does not release the reader object, though. */
|
||||||
|
void
|
||||||
|
ldap_wrapper_release_context (ksba_reader_t reader)
|
||||||
|
{
|
||||||
|
(void)reader;
|
||||||
|
/* Nothing to do. */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Free a NULL terminated array of malloced strings and the array
|
||||||
|
itself. */
|
||||||
|
static void
|
||||||
|
free_arg_list (char **arg_list)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (arg_list)
|
||||||
|
{
|
||||||
|
for (i=0; arg_list[i]; i++)
|
||||||
|
xfree (arg_list[i]);
|
||||||
|
xfree (arg_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Copy ARGV into a new array and prepend one element as name of the
|
||||||
|
program (which is more or less a stub). We need to allocate all
|
||||||
|
the strings to get ownership of them. */
|
||||||
|
static gpg_error_t
|
||||||
|
create_arg_list (const char *argv[], char ***r_arg_list)
|
||||||
|
{
|
||||||
|
gpg_error_t err;
|
||||||
|
char **arg_list;
|
||||||
|
int i, j;
|
||||||
|
|
||||||
|
for (i = 0; argv[i]; i++)
|
||||||
|
;
|
||||||
|
arg_list = xtrycalloc (i + 2, sizeof *arg_list);
|
||||||
|
if (!arg_list)
|
||||||
|
goto outofcore;
|
||||||
|
|
||||||
|
i = 0;
|
||||||
|
arg_list[i] = xtrystrdup ("<ldap-wrapper-thread>");
|
||||||
|
if (!arg_list[i])
|
||||||
|
goto outofcore;
|
||||||
|
i++;
|
||||||
|
for (j=0; argv[j]; j++)
|
||||||
|
{
|
||||||
|
arg_list[i] = xtrystrdup (argv[j]);
|
||||||
|
if (!arg_list[i])
|
||||||
|
goto outofcore;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
arg_list[i] = NULL;
|
||||||
|
*r_arg_list = arg_list;
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
outofcore:
|
||||||
|
err = gpg_error_from_syserror ();
|
||||||
|
log_error (_("error allocating memory: %s\n"), strerror (errno));
|
||||||
|
free_arg_list (arg_list);
|
||||||
|
*r_arg_list = NULL;
|
||||||
|
return err;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Parameters passed to the wrapper thread. */
|
||||||
|
struct ldap_wrapper_thread_parms
|
||||||
|
{
|
||||||
|
char **arg_list;
|
||||||
|
estream_t outstream;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* The thread which runs the LDAP wrapper. */
|
||||||
|
static void *
|
||||||
|
ldap_wrapper_thread (void *opaque)
|
||||||
|
{
|
||||||
|
struct ldap_wrapper_thread_parms *parms = opaque;
|
||||||
|
|
||||||
|
/*err =*/ ldap_wrapper_main (parms->arg_list, parms->outstream);
|
||||||
|
|
||||||
|
/* FIXME: Do we need to return ERR? */
|
||||||
|
|
||||||
|
free_arg_list (parms->arg_list);
|
||||||
|
es_fclose (parms->outstream);
|
||||||
|
xfree (parms);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Start a new LDAP thread and returns a new libksba reader
|
/* Start a new LDAP thread and returns a new libksba reader
|
||||||
object at READER. ARGV is a NULL terminated list of arguments for
|
object at READER. ARGV is a NULL terminated list of arguments for
|
||||||
the wrapper. The function returns 0 on success or an error code. */
|
the wrapper. The function returns 0 on success or an error code. */
|
||||||
gpg_error_t
|
gpg_error_t
|
||||||
ldap_wrapper (ctrl_t ctrl, ksba_reader_t *reader, const char *argv[])
|
ldap_wrapper (ctrl_t ctrl, ksba_reader_t *r_reader, const char *argv[])
|
||||||
{
|
{
|
||||||
gpg_error_t err;
|
gpg_error_t err;
|
||||||
pid_t pid;
|
struct ldap_wrapper_thread_parms *parms;
|
||||||
struct wrapper_context_s *ctx;
|
pth_attr_t tattr;
|
||||||
int i;
|
es_cookie_io_functions_t outstream_func = { NULL };
|
||||||
int j;
|
struct outstream_cookie_s *outstream_cookie;
|
||||||
const char **arg_list;
|
ksba_reader_t reader;
|
||||||
const char *pgmname;
|
|
||||||
int outpipe[2], errpipe[2];
|
|
||||||
|
|
||||||
/* It would be too simple to connect stderr just to our logging
|
(void)ctrl;
|
||||||
stream. The problem is that if we are running multi-threaded
|
|
||||||
everything gets intermixed. Clearly we don't want this. So the
|
|
||||||
only viable solutions are either to have another thread
|
|
||||||
responsible for logging the messages or to add an option to the
|
|
||||||
wrapper module to do the logging on its own. Given that we anyway
|
|
||||||
need a way to rip the child process and this is best done using a
|
|
||||||
general ripping thread, that thread can do the logging too. */
|
|
||||||
|
|
||||||
*reader = NULL;
|
*r_reader = NULL;
|
||||||
|
|
||||||
/* Files: We need to prepare stdin and stdout. We get stderr from
|
parms = xtrycalloc (1, sizeof *parms);
|
||||||
the function. */
|
if (!parms)
|
||||||
if (!opt.ldap_wrapper_program || !*opt.ldap_wrapper_program)
|
return gpg_error_from_syserror ();
|
||||||
pgmname = gnupg_module_name (GNUPG_MODULE_NAME_DIRMNGR_LDAP);
|
|
||||||
else
|
|
||||||
pgmname = opt.ldap_wrapper_program;
|
|
||||||
|
|
||||||
/* Create command line argument array. */
|
err = create_arg_list (argv, &parms->arg_list);
|
||||||
for (i = 0; argv[i]; i++)
|
|
||||||
;
|
|
||||||
arg_list = xtrycalloc (i + 2, sizeof *arg_list);
|
|
||||||
if (!arg_list)
|
|
||||||
{
|
|
||||||
err = gpg_error_from_syserror ();
|
|
||||||
log_error (_("error allocating memory: %s\n"), strerror (errno));
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
for (i = j = 0; argv[i]; i++, j++)
|
|
||||||
if (!i && argv[i + 1] && !strcmp (*argv, "--pass"))
|
|
||||||
{
|
|
||||||
arg_list[j] = "--env-pass";
|
|
||||||
setenv ("DIRMNGR_LDAP_PASS", argv[1], 1);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
arg_list[j] = (char*) argv[i];
|
|
||||||
|
|
||||||
ctx = xtrycalloc (1, sizeof *ctx);
|
|
||||||
if (!ctx)
|
|
||||||
{
|
|
||||||
err = gpg_error_from_syserror ();
|
|
||||||
log_error (_("error allocating memory: %s\n"), strerror (errno));
|
|
||||||
xfree (arg_list);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = gnupg_create_inbound_pipe (outpipe);
|
|
||||||
if (!err)
|
|
||||||
{
|
|
||||||
err = gnupg_create_inbound_pipe (errpipe);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
close (outpipe[0]);
|
|
||||||
close (outpipe[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (err)
|
if (err)
|
||||||
{
|
{
|
||||||
log_error (_("error creating pipe: %s\n"), gpg_strerror (err));
|
xfree (parms);
|
||||||
xfree (arg_list);
|
|
||||||
xfree (ctx);
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = gnupg_spawn_process_fd (pgmname, arg_list,
|
outstream_cookie = xtrycalloc (1, sizeof *outstream_cookie);
|
||||||
-1, outpipe[1], errpipe[1], &pid);
|
if (!outstream_cookie)
|
||||||
xfree (arg_list);
|
|
||||||
close (outpipe[1]);
|
|
||||||
close (errpipe[1]);
|
|
||||||
if (err)
|
|
||||||
{
|
{
|
||||||
close (outpipe[0]);
|
err = gpg_error_from_syserror ();
|
||||||
close (errpipe[0]);
|
free_arg_list (parms->arg_list);
|
||||||
xfree (ctx);
|
xfree (parms);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
outstream_cookie->refcount++;
|
||||||
|
|
||||||
ctx->pid = pid;
|
err = ksba_reader_new (&reader);
|
||||||
ctx->printable_pid = (int) pid;
|
|
||||||
ctx->fd = outpipe[0];
|
|
||||||
ctx->log_fd = errpipe[0];
|
|
||||||
ctx->log_ev = pth_event (PTH_EVENT_FD | PTH_UNTIL_FD_READABLE, ctx->log_fd);
|
|
||||||
if (! ctx->log_ev)
|
|
||||||
{
|
|
||||||
xfree (ctx);
|
|
||||||
return gpg_error_from_syserror ();
|
|
||||||
}
|
|
||||||
ctx->ctrl = ctrl;
|
|
||||||
ctrl->refcount++;
|
|
||||||
ctx->stamp = time (NULL);
|
|
||||||
|
|
||||||
err = ksba_reader_new (reader);
|
|
||||||
if (!err)
|
if (!err)
|
||||||
err = ksba_reader_set_cb (*reader, reader_callback, ctx);
|
err = ksba_reader_set_release_notify (reader,
|
||||||
|
outstream_reader_released,
|
||||||
|
outstream_cookie);
|
||||||
|
if (!err)
|
||||||
|
err = ksba_reader_set_cb (reader,
|
||||||
|
outstream_reader_cb, outstream_cookie);
|
||||||
if (err)
|
if (err)
|
||||||
{
|
{
|
||||||
log_error (_("error initializing reader object: %s\n"),
|
log_error (_("error initializing reader object: %s\n"),
|
||||||
gpg_strerror (err));
|
gpg_strerror (err));
|
||||||
destroy_wrapper (ctx);
|
ksba_reader_release (reader);
|
||||||
ksba_reader_release (*reader);
|
outstream_release_cookie (outstream_cookie);
|
||||||
*reader = NULL;
|
free_arg_list (parms->arg_list);
|
||||||
|
xfree (parms);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hook the context into our list of running wrappers. */
|
|
||||||
ctx->reader = *reader;
|
outstream_func.func_write = outstream_cookie_writer;
|
||||||
ctx->next = wrapper_list;
|
outstream_func.func_close = outstream_cookie_closer;
|
||||||
wrapper_list = ctx;
|
parms->outstream = es_fopencookie (outstream_cookie, "wb", outstream_func);
|
||||||
if (opt.verbose)
|
if (!parms->outstream)
|
||||||
log_info ("ldap wrapper %d started (reader %p)\n",
|
{
|
||||||
(int)ctx->pid, ctx->reader);
|
err = gpg_error_from_syserror ();
|
||||||
|
free_arg_list (parms->arg_list);
|
||||||
|
outstream_release_cookie (outstream_cookie);
|
||||||
|
xfree (parms);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
outstream_cookie->refcount++;
|
||||||
|
|
||||||
|
tattr = pth_attr_new();
|
||||||
|
pth_attr_set (tattr, PTH_ATTR_JOINABLE, 0);
|
||||||
|
pth_attr_set (tattr, PTH_ATTR_STACK_SIZE, 128*1024);
|
||||||
|
pth_attr_set (tattr, PTH_ATTR_NAME, "ldap-wrapper");
|
||||||
|
|
||||||
|
if (pth_spawn (tattr, ldap_wrapper_thread, parms))
|
||||||
|
parms = NULL; /* Now owned by the thread. */
|
||||||
|
else
|
||||||
|
{
|
||||||
|
err = gpg_error_from_syserror ();
|
||||||
|
log_error ("error spawning ldap wrapper thread: %s\n",
|
||||||
|
strerror (errno) );
|
||||||
|
}
|
||||||
|
pth_attr_destroy (tattr);
|
||||||
|
if (parms)
|
||||||
|
{
|
||||||
|
free_arg_list (parms->arg_list);
|
||||||
|
es_fclose (parms->outstream);
|
||||||
|
xfree (parms);
|
||||||
|
}
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
ksba_reader_release (reader);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
/* Need to wait for the first byte so we are able to detect an empty
|
/* Need to wait for the first byte so we are able to detect an empty
|
||||||
output and not let the consumer see an EOF without further error
|
output and not let the consumer see an EOF without further error
|
||||||
@ -308,19 +448,20 @@ ldap_wrapper (ctrl_t ctrl, ksba_reader_t *reader, const char *argv[])
|
|||||||
{
|
{
|
||||||
unsigned char c;
|
unsigned char c;
|
||||||
|
|
||||||
err = read_buffer (*reader, &c, 1);
|
err = read_buffer (reader, &c, 1);
|
||||||
if (err)
|
if (err)
|
||||||
{
|
{
|
||||||
ldap_wrapper_release_context (*reader);
|
ksba_reader_release (reader);
|
||||||
ksba_reader_release (*reader);
|
reader = NULL;
|
||||||
*reader = NULL;
|
|
||||||
if (gpg_err_code (err) == GPG_ERR_EOF)
|
if (gpg_err_code (err) == GPG_ERR_EOF)
|
||||||
return gpg_error (GPG_ERR_NO_DATA);
|
return gpg_error (GPG_ERR_NO_DATA);
|
||||||
else
|
else
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
ksba_reader_unread (*reader, &c, 1);
|
ksba_reader_unread (reader, &c, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*r_reader = reader;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user