mirror of
https://github.com/opnsense/src.git
synced 2026-04-04 17:05:14 -04:00
several new kerberos related libraries and applications to FreeBSD:
o kgetcred(1) allows one to manually get a ticket for a particular service.
o kf(1) securily forwards ticket to another host through an authenticated
and encrypted stream.
o kcc(1) is an umbrella program around klist(1), kswitch(1), kgetcred(1)
and other user kerberos operations. klist and kswitch are just symlinks
to kcc(1) now.
o kswitch(1) allows you to easily switch between kerberos credentials if
you're running KCM.
o hxtool(1) is a certificate management tool to use with PKINIT.
o string2key(1) maps a password into key.
o kdigest(8) is a userland tool to access the KDC's digest interface.
o kimpersonate(8) creates a "fake" ticket for a service.
We also now install manpages for some lirbaries that were not installed
before, libheimntlm and libhx509.
- The new HEIMDAL version no longer supports Kerberos 4. All users are
recommended to switch to Kerberos 5.
- Weak ciphers are now disabled by default. To enable DES support (used
by telnet(8)), use "allow_weak_crypto" option in krb5.conf.
- libtelnet, pam_ksu and pam_krb5 are now compiled with error on warnings
disabled due to the function they use (krb5_get_err_text(3)) being
deprecated. I plan to work on this next.
- Heimdal's KDC now require sqlite to operate. We use the bundled version
and install it as libheimsqlite. If some other FreeBSD components will
require it in the future we can rename it to libbsdsqlite and use for these
components as well.
- This is not a latest Heimdal version, the new one was released while I was
working on the update. I will update it to 1.5.2 soon, as it fixes some
important bugs and security issues.
628 lines
15 KiB
C
628 lines
15 KiB
C
/*-
|
|
* Copyright (c) 1991, 1993
|
|
* The Regents of the University of California. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. All advertising materials mentioning features or use of this software
|
|
* must display the following acknowledgement:
|
|
* This product includes software developed by the University of
|
|
* California, Berkeley and its contributors.
|
|
* 4. Neither the name of the University nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
/*
|
|
* Copyright (C) 1990 by the Massachusetts Institute of Technology
|
|
*
|
|
* Export of this software from the United States of America is assumed
|
|
* to require a specific license from the United States Government.
|
|
* It is the responsibility of any person or organization contemplating
|
|
* export to obtain such a license before exporting.
|
|
*
|
|
* WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
|
|
* distribute this software and its documentation for any purpose and
|
|
* without fee is hereby granted, provided that the above copyright
|
|
* notice appear in all copies and that both that copyright notice and
|
|
* this permission notice appear in supporting documentation, and that
|
|
* the name of M.I.T. not be used in advertising or publicity pertaining
|
|
* to distribution of the software without specific, written prior
|
|
* permission. M.I.T. makes no representations about the suitability of
|
|
* this software for any purpose. It is provided "as is" without express
|
|
* or implied warranty.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
RCSID("$Id$");
|
|
|
|
#if defined(AUTHENTICATION)
|
|
#include <stdio.h>
|
|
#ifdef HAVE_SYS_TYPES_H
|
|
#include <sys/types.h>
|
|
#endif
|
|
#include <signal.h>
|
|
#define AUTH_NAMES
|
|
#ifdef HAVE_ARPA_TELNET_H
|
|
#include <arpa/telnet.h>
|
|
#endif
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <roken.h>
|
|
|
|
#ifdef SOCKS
|
|
#include <socks.h>
|
|
#endif
|
|
|
|
#include "encrypt.h"
|
|
#include "auth.h"
|
|
#include "misc-proto.h"
|
|
#include "auth-proto.h"
|
|
|
|
#define typemask(x) (1<<((x)-1))
|
|
|
|
#ifdef RSA_ENCPWD
|
|
extern rsaencpwd_init();
|
|
extern rsaencpwd_send();
|
|
extern rsaencpwd_is();
|
|
extern rsaencpwd_reply();
|
|
extern rsaencpwd_status();
|
|
extern rsaencpwd_printsub();
|
|
#endif
|
|
|
|
int auth_debug_mode = 0;
|
|
int auth_has_failed = 0;
|
|
int auth_enable_encrypt = 0;
|
|
static const char *Name = "Noname";
|
|
static int Server = 0;
|
|
static Authenticator *authenticated = 0;
|
|
static int authenticating = 0;
|
|
static int validuser = 0;
|
|
static unsigned char _auth_send_data[256];
|
|
static unsigned char *auth_send_data;
|
|
static int auth_send_cnt = 0;
|
|
|
|
/*
|
|
* Authentication types supported. Plese note that these are stored
|
|
* in priority order, i.e. try the first one first.
|
|
*/
|
|
Authenticator authenticators[] = {
|
|
#ifdef UNSAFE
|
|
{ AUTHTYPE_UNSAFE, AUTH_WHO_CLIENT|AUTH_HOW_ONE_WAY,
|
|
unsafe_init,
|
|
unsafe_send,
|
|
unsafe_is,
|
|
unsafe_reply,
|
|
unsafe_status,
|
|
unsafe_printsub },
|
|
#endif
|
|
#ifdef SRA
|
|
{ AUTHTYPE_SRA, AUTH_WHO_CLIENT|AUTH_HOW_ONE_WAY,
|
|
sra_init,
|
|
sra_send,
|
|
sra_is,
|
|
sra_reply,
|
|
sra_status,
|
|
sra_printsub },
|
|
#endif
|
|
#ifdef SPX
|
|
{ AUTHTYPE_SPX, AUTH_WHO_CLIENT|AUTH_HOW_MUTUAL,
|
|
spx_init,
|
|
spx_send,
|
|
spx_is,
|
|
spx_reply,
|
|
spx_status,
|
|
spx_printsub },
|
|
{ AUTHTYPE_SPX, AUTH_WHO_CLIENT|AUTH_HOW_ONE_WAY,
|
|
spx_init,
|
|
spx_send,
|
|
spx_is,
|
|
spx_reply,
|
|
spx_status,
|
|
spx_printsub },
|
|
#endif
|
|
#ifdef KRB5
|
|
{ AUTHTYPE_KERBEROS_V5, AUTH_WHO_CLIENT|AUTH_HOW_MUTUAL,
|
|
kerberos5_init,
|
|
kerberos5_send_mutual,
|
|
kerberos5_is,
|
|
kerberos5_reply,
|
|
kerberos5_status,
|
|
kerberos5_printsub },
|
|
{ AUTHTYPE_KERBEROS_V5, AUTH_WHO_CLIENT|AUTH_HOW_ONE_WAY,
|
|
kerberos5_init,
|
|
kerberos5_send_oneway,
|
|
kerberos5_is,
|
|
kerberos5_reply,
|
|
kerberos5_status,
|
|
kerberos5_printsub },
|
|
#endif
|
|
#ifdef RSA_ENCPWD
|
|
{ AUTHTYPE_RSA_ENCPWD, AUTH_WHO_CLIENT|AUTH_HOW_ONE_WAY,
|
|
rsaencpwd_init,
|
|
rsaencpwd_send,
|
|
rsaencpwd_is,
|
|
rsaencpwd_reply,
|
|
rsaencpwd_status,
|
|
rsaencpwd_printsub },
|
|
#endif
|
|
{ 0, },
|
|
};
|
|
|
|
static Authenticator NoAuth = { 0 };
|
|
|
|
static int i_support = 0;
|
|
static int i_wont_support = 0;
|
|
|
|
Authenticator *
|
|
findauthenticator(int type, int way)
|
|
{
|
|
Authenticator *ap = authenticators;
|
|
|
|
while (ap->type && (ap->type != type || ap->way != way))
|
|
++ap;
|
|
return(ap->type ? ap : 0);
|
|
}
|
|
|
|
void
|
|
auth_init(const char *name, int server)
|
|
{
|
|
Authenticator *ap = authenticators;
|
|
|
|
Server = server;
|
|
Name = name;
|
|
|
|
i_support = 0;
|
|
authenticated = 0;
|
|
authenticating = 0;
|
|
while (ap->type) {
|
|
if (!ap->init || (*ap->init)(ap, server)) {
|
|
i_support |= typemask(ap->type);
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: I support auth type %d %d\r\n",
|
|
Name,
|
|
ap->type, ap->way);
|
|
}
|
|
else if (auth_debug_mode)
|
|
printf(">>>%s: Init failed: auth type %d %d\r\n",
|
|
Name, ap->type, ap->way);
|
|
++ap;
|
|
}
|
|
}
|
|
|
|
void
|
|
auth_disable_name(char *name)
|
|
{
|
|
int x;
|
|
for (x = 0; x < AUTHTYPE_CNT; ++x) {
|
|
if (!strcasecmp(name, AUTHTYPE_NAME(x))) {
|
|
i_wont_support |= typemask(x);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
getauthmask(char *type, int *maskp)
|
|
{
|
|
int x;
|
|
|
|
if (!strcasecmp(type, AUTHTYPE_NAME(0))) {
|
|
*maskp = -1;
|
|
return(1);
|
|
}
|
|
|
|
for (x = 1; x < AUTHTYPE_CNT; ++x) {
|
|
if (!strcasecmp(type, AUTHTYPE_NAME(x))) {
|
|
*maskp = typemask(x);
|
|
return(1);
|
|
}
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
int
|
|
auth_enable(char *type)
|
|
{
|
|
return(auth_onoff(type, 1));
|
|
}
|
|
|
|
int
|
|
auth_disable(char *type)
|
|
{
|
|
return(auth_onoff(type, 0));
|
|
}
|
|
|
|
int
|
|
auth_onoff(char *type, int on)
|
|
{
|
|
int i, mask = -1;
|
|
Authenticator *ap;
|
|
|
|
if (!strcasecmp(type, "?") || !strcasecmp(type, "help")) {
|
|
printf("auth %s 'type'\n", on ? "enable" : "disable");
|
|
printf("Where 'type' is one of:\n");
|
|
printf("\t%s\n", AUTHTYPE_NAME(0));
|
|
mask = 0;
|
|
for (ap = authenticators; ap->type; ap++) {
|
|
if ((mask & (i = typemask(ap->type))) != 0)
|
|
continue;
|
|
mask |= i;
|
|
printf("\t%s\n", AUTHTYPE_NAME(ap->type));
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
if (!getauthmask(type, &mask)) {
|
|
printf("%s: invalid authentication type\n", type);
|
|
return(0);
|
|
}
|
|
if (on)
|
|
i_wont_support &= ~mask;
|
|
else
|
|
i_wont_support |= mask;
|
|
return(1);
|
|
}
|
|
|
|
int
|
|
auth_togdebug(int on)
|
|
{
|
|
if (on < 0)
|
|
auth_debug_mode ^= 1;
|
|
else
|
|
auth_debug_mode = on;
|
|
printf("auth debugging %s\n", auth_debug_mode ? "enabled" : "disabled");
|
|
return(1);
|
|
}
|
|
|
|
int
|
|
auth_status(void)
|
|
{
|
|
Authenticator *ap;
|
|
int i, mask;
|
|
|
|
if (i_wont_support == -1)
|
|
printf("Authentication disabled\n");
|
|
else
|
|
printf("Authentication enabled\n");
|
|
|
|
mask = 0;
|
|
for (ap = authenticators; ap->type; ap++) {
|
|
if ((mask & (i = typemask(ap->type))) != 0)
|
|
continue;
|
|
mask |= i;
|
|
printf("%s: %s\n", AUTHTYPE_NAME(ap->type),
|
|
(i_wont_support & typemask(ap->type)) ?
|
|
"disabled" : "enabled");
|
|
}
|
|
return(1);
|
|
}
|
|
|
|
/*
|
|
* This routine is called by the server to start authentication
|
|
* negotiation.
|
|
*/
|
|
void
|
|
auth_request(void)
|
|
{
|
|
static unsigned char str_request[64] = { IAC, SB,
|
|
TELOPT_AUTHENTICATION,
|
|
TELQUAL_SEND, };
|
|
Authenticator *ap = authenticators;
|
|
unsigned char *e = str_request + 4;
|
|
|
|
if (!authenticating) {
|
|
authenticating = 1;
|
|
while (ap->type) {
|
|
if (i_support & ~i_wont_support & typemask(ap->type)) {
|
|
if (auth_debug_mode) {
|
|
printf(">>>%s: Sending type %d %d\r\n",
|
|
Name, ap->type, ap->way);
|
|
}
|
|
*e++ = ap->type;
|
|
*e++ = ap->way;
|
|
}
|
|
++ap;
|
|
}
|
|
*e++ = IAC;
|
|
*e++ = SE;
|
|
telnet_net_write(str_request, e - str_request);
|
|
printsub('>', &str_request[2], e - str_request - 2);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This is called when an AUTH SEND is received.
|
|
* It should never arrive on the server side (as only the server can
|
|
* send an AUTH SEND).
|
|
* You should probably respond to it if you can...
|
|
*
|
|
* If you want to respond to the types out of order (i.e. even
|
|
* if he sends LOGIN KERBEROS and you support both, you respond
|
|
* with KERBEROS instead of LOGIN (which is against what the
|
|
* protocol says)) you will have to hack this code...
|
|
*/
|
|
void
|
|
auth_send(unsigned char *data, int cnt)
|
|
{
|
|
Authenticator *ap;
|
|
static unsigned char str_none[] = { IAC, SB, TELOPT_AUTHENTICATION,
|
|
TELQUAL_IS, AUTHTYPE_NULL, 0,
|
|
IAC, SE };
|
|
if (Server) {
|
|
if (auth_debug_mode) {
|
|
printf(">>>%s: auth_send called!\r\n", Name);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (auth_debug_mode) {
|
|
printf(">>>%s: auth_send got:", Name);
|
|
printd(data, cnt); printf("\r\n");
|
|
}
|
|
|
|
/*
|
|
* Save the data, if it is new, so that we can continue looking
|
|
* at it if the authorization we try doesn't work
|
|
*/
|
|
if (data < _auth_send_data ||
|
|
data > _auth_send_data + sizeof(_auth_send_data)) {
|
|
auth_send_cnt = cnt > sizeof(_auth_send_data)
|
|
? sizeof(_auth_send_data)
|
|
: cnt;
|
|
memmove(_auth_send_data, data, auth_send_cnt);
|
|
auth_send_data = _auth_send_data;
|
|
} else {
|
|
/*
|
|
* This is probably a no-op, but we just make sure
|
|
*/
|
|
auth_send_data = data;
|
|
auth_send_cnt = cnt;
|
|
}
|
|
while ((auth_send_cnt -= 2) >= 0) {
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: He supports %d\r\n",
|
|
Name, *auth_send_data);
|
|
if ((i_support & ~i_wont_support) & typemask(*auth_send_data)) {
|
|
ap = findauthenticator(auth_send_data[0],
|
|
auth_send_data[1]);
|
|
if (ap && ap->send) {
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: Trying %d %d\r\n",
|
|
Name, auth_send_data[0],
|
|
auth_send_data[1]);
|
|
if ((*ap->send)(ap)) {
|
|
/*
|
|
* Okay, we found one we like
|
|
* and did it.
|
|
* we can go home now.
|
|
*/
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: Using type %d\r\n",
|
|
Name, *auth_send_data);
|
|
auth_send_data += 2;
|
|
return;
|
|
}
|
|
}
|
|
/* else
|
|
* just continue on and look for the
|
|
* next one if we didn't do anything.
|
|
*/
|
|
}
|
|
auth_send_data += 2;
|
|
}
|
|
telnet_net_write(str_none, sizeof(str_none));
|
|
printsub('>', &str_none[2], sizeof(str_none) - 2);
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: Sent failure message\r\n", Name);
|
|
auth_finished(0, AUTH_REJECT);
|
|
auth_has_failed = 1;
|
|
#ifdef KANNAN
|
|
/*
|
|
* We requested strong authentication, however no mechanisms worked.
|
|
* Therefore, exit on client end.
|
|
*/
|
|
printf("Unable to securely authenticate user ... exit\n");
|
|
exit(0);
|
|
#endif /* KANNAN */
|
|
}
|
|
|
|
void
|
|
auth_send_retry(void)
|
|
{
|
|
/*
|
|
* if auth_send_cnt <= 0 then auth_send will end up rejecting
|
|
* the authentication and informing the other side of this.
|
|
*/
|
|
auth_send(auth_send_data, auth_send_cnt);
|
|
}
|
|
|
|
void
|
|
auth_is(unsigned char *data, int cnt)
|
|
{
|
|
Authenticator *ap;
|
|
|
|
if (cnt < 2)
|
|
return;
|
|
|
|
if (data[0] == AUTHTYPE_NULL) {
|
|
auth_finished(0, AUTH_REJECT);
|
|
return;
|
|
}
|
|
|
|
if ((ap = findauthenticator(data[0], data[1]))) {
|
|
if (ap->is)
|
|
(*ap->is)(ap, data+2, cnt-2);
|
|
} else if (auth_debug_mode)
|
|
printf(">>>%s: Invalid authentication in IS: %d\r\n",
|
|
Name, *data);
|
|
}
|
|
|
|
void
|
|
auth_reply(unsigned char *data, int cnt)
|
|
{
|
|
Authenticator *ap;
|
|
|
|
if (cnt < 2)
|
|
return;
|
|
|
|
if ((ap = findauthenticator(data[0], data[1]))) {
|
|
if (ap->reply)
|
|
(*ap->reply)(ap, data+2, cnt-2);
|
|
} else if (auth_debug_mode)
|
|
printf(">>>%s: Invalid authentication in SEND: %d\r\n",
|
|
Name, *data);
|
|
}
|
|
|
|
void
|
|
auth_name(unsigned char *data, int cnt)
|
|
{
|
|
char savename[256];
|
|
|
|
if (cnt < 1) {
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: Empty name in NAME\r\n", Name);
|
|
return;
|
|
}
|
|
if (cnt > sizeof(savename) - 1) {
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: Name in NAME (%d) exceeds %lu length\r\n",
|
|
Name, cnt, (unsigned long)(sizeof(savename)-1));
|
|
return;
|
|
}
|
|
memmove(savename, data, cnt);
|
|
savename[cnt] = '\0'; /* Null terminate */
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: Got NAME [%s]\r\n", Name, savename);
|
|
auth_encrypt_user(savename);
|
|
}
|
|
|
|
int
|
|
auth_sendname(unsigned char *cp, int len)
|
|
{
|
|
static unsigned char str_request[256+6]
|
|
= { IAC, SB, TELOPT_AUTHENTICATION, TELQUAL_NAME, };
|
|
unsigned char *e = str_request + 4;
|
|
unsigned char *ee = &str_request[sizeof(str_request)-2];
|
|
|
|
while (--len >= 0) {
|
|
if ((*e++ = *cp++) == IAC)
|
|
*e++ = IAC;
|
|
if (e >= ee)
|
|
return(0);
|
|
}
|
|
*e++ = IAC;
|
|
*e++ = SE;
|
|
telnet_net_write(str_request, e - str_request);
|
|
printsub('>', &str_request[2], e - &str_request[2]);
|
|
return(1);
|
|
}
|
|
|
|
void
|
|
auth_finished(Authenticator *ap, int result)
|
|
{
|
|
if (!(authenticated = ap))
|
|
authenticated = &NoAuth;
|
|
validuser = result;
|
|
}
|
|
|
|
/* ARGSUSED */
|
|
static void
|
|
auth_intr(int sig)
|
|
{
|
|
auth_finished(0, AUTH_REJECT);
|
|
}
|
|
|
|
int
|
|
auth_wait(char *name, size_t name_sz)
|
|
{
|
|
if (auth_debug_mode)
|
|
printf(">>>%s: in auth_wait.\r\n", Name);
|
|
|
|
if (Server && !authenticating)
|
|
return(0);
|
|
|
|
signal(SIGALRM, auth_intr);
|
|
alarm(30);
|
|
while (!authenticated)
|
|
if (telnet_spin())
|
|
break;
|
|
alarm(0);
|
|
signal(SIGALRM, SIG_DFL);
|
|
|
|
/*
|
|
* Now check to see if the user is valid or not
|
|
*/
|
|
if (!authenticated || authenticated == &NoAuth)
|
|
return(AUTH_REJECT);
|
|
|
|
if (validuser == AUTH_VALID)
|
|
validuser = AUTH_USER;
|
|
|
|
if (authenticated->status)
|
|
validuser = (*authenticated->status)(authenticated,
|
|
name, name_sz,
|
|
validuser);
|
|
return(validuser);
|
|
}
|
|
|
|
void
|
|
auth_debug(int mode)
|
|
{
|
|
auth_debug_mode = mode;
|
|
}
|
|
|
|
void
|
|
auth_printsub(unsigned char *data, size_t cnt,
|
|
unsigned char *buf, size_t buflen)
|
|
{
|
|
Authenticator *ap;
|
|
|
|
if ((ap = findauthenticator(data[1], data[2])) && ap->printsub)
|
|
(*ap->printsub)(data, cnt, buf, buflen);
|
|
else
|
|
auth_gen_printsub(data, cnt, buf, buflen);
|
|
}
|
|
|
|
void
|
|
auth_gen_printsub(unsigned char *data, size_t cnt,
|
|
unsigned char *buf, size_t buflen)
|
|
{
|
|
unsigned char *cp;
|
|
unsigned char tbuf[16];
|
|
|
|
cnt -= 3;
|
|
data += 3;
|
|
buf[buflen-1] = '\0';
|
|
buf[buflen-2] = '*';
|
|
buflen -= 2;
|
|
for (; cnt > 0; cnt--, data++) {
|
|
snprintf((char*)tbuf, sizeof(tbuf), " %d", *data);
|
|
for (cp = tbuf; *cp && buflen > 0; --buflen)
|
|
*buf++ = *cp++;
|
|
if (buflen <= 0)
|
|
return;
|
|
}
|
|
*buf = '\0';
|
|
}
|
|
#endif
|