mirror of
https://gitlab.isc.org/isc-projects/dhcp
synced 2025-08-31 14:25:41 +00:00
Add pool/permit support.
This commit is contained in:
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
#ifndef lint
|
#ifndef lint
|
||||||
static char copyright[] =
|
static char copyright[] =
|
||||||
"$Id: confpars.c,v 1.54 1998/11/06 02:58:17 mellon Exp $ Copyright (c) 1995, 1996 The Internet Software Consortium. All rights reserved.\n";
|
"$Id: confpars.c,v 1.55 1998/11/09 02:46:36 mellon Exp $ Copyright (c) 1995, 1996 The Internet Software Consortium. All rights reserved.\n";
|
||||||
#endif /* not lint */
|
#endif /* not lint */
|
||||||
|
|
||||||
#include "dhcpd.h"
|
#include "dhcpd.h"
|
||||||
@@ -320,6 +320,14 @@ int parse_statement (cfile, group, type, host_decl, declaration)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case POOL:
|
||||||
|
next_token (&val, cfile);
|
||||||
|
if (type != SUBNET_DECL && type != SHARED_NET_DECL) {
|
||||||
|
parse_warn ("pool declared outside of network");
|
||||||
|
}
|
||||||
|
parse_pool_statement (cfile, group, type);
|
||||||
|
return declaration;
|
||||||
|
|
||||||
case RANGE:
|
case RANGE:
|
||||||
next_token (&val, cfile);
|
next_token (&val, cfile);
|
||||||
if (type != SUBNET_DECL || !group -> subnet) {
|
if (type != SUBNET_DECL || !group -> subnet) {
|
||||||
@@ -327,7 +335,7 @@ int parse_statement (cfile, group, type, host_decl, declaration)
|
|||||||
skip_to_semi (cfile);
|
skip_to_semi (cfile);
|
||||||
return declaration;
|
return declaration;
|
||||||
}
|
}
|
||||||
parse_address_range (cfile, group -> subnet);
|
parse_address_range (cfile, group, type, (struct pool *)0);
|
||||||
return declaration;
|
return declaration;
|
||||||
|
|
||||||
case ALLOW:
|
case ALLOW:
|
||||||
@@ -420,6 +428,136 @@ int parse_statement (cfile, group, type, host_decl, declaration)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void parse_pool_statement (cfile, group, type)
|
||||||
|
FILE *cfile;
|
||||||
|
struct group *group;
|
||||||
|
int type;
|
||||||
|
{
|
||||||
|
enum dhcp_token token;
|
||||||
|
char *val;
|
||||||
|
int done = 0;
|
||||||
|
struct pool *pool, **p;
|
||||||
|
struct permit *permit;
|
||||||
|
struct permit **permit_head;
|
||||||
|
|
||||||
|
pool = new_pool ("parse_pool_statement");
|
||||||
|
if (!pool)
|
||||||
|
error ("no memory for pool.");
|
||||||
|
|
||||||
|
if (!parse_lbrace (cfile))
|
||||||
|
return;
|
||||||
|
do {
|
||||||
|
switch (peek_token (&val, cfile)) {
|
||||||
|
case RANGE:
|
||||||
|
next_token (&val, cfile);
|
||||||
|
parse_address_range (cfile, group, type, pool);
|
||||||
|
break;
|
||||||
|
case ALLOW:
|
||||||
|
permit_head = &pool -> permit_list;
|
||||||
|
get_permit:
|
||||||
|
permit = new_permit ("parse_pool_statement");
|
||||||
|
if (!permit)
|
||||||
|
error ("no memory for permit");
|
||||||
|
next_token (&val, cfile);
|
||||||
|
token = next_token (&val, cfile);
|
||||||
|
switch (token) {
|
||||||
|
case UNKNOWN:
|
||||||
|
permit -> type = permit_unknown_clients;
|
||||||
|
get_clients:
|
||||||
|
if (next_token (&val, cfile) != CLIENTS) {
|
||||||
|
parse_warn ("expecting \"hosts\"");
|
||||||
|
skip_to_semi (cfile);
|
||||||
|
free_permit (permit,
|
||||||
|
"parse_pool_statement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KNOWN:
|
||||||
|
permit -> type = permit_known_clients;
|
||||||
|
goto get_clients;
|
||||||
|
|
||||||
|
case AUTHENTICATED:
|
||||||
|
permit -> type = permit_authenticated_clients;
|
||||||
|
goto get_clients;
|
||||||
|
|
||||||
|
case UNAUTHENTICATED:
|
||||||
|
permit -> type =
|
||||||
|
permit_unauthenticated_clients;
|
||||||
|
goto get_clients;
|
||||||
|
|
||||||
|
case ALL:
|
||||||
|
permit -> type = permit_all_clients;
|
||||||
|
goto get_clients;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DYNAMIC:
|
||||||
|
permit -> type = permit_dynamic_bootp_clients;
|
||||||
|
if (next_token (&val, cfile) != BOOTP) {
|
||||||
|
parse_warn ("expecting \"bootp\"");
|
||||||
|
skip_to_semi (cfile);
|
||||||
|
free_permit (permit,
|
||||||
|
"parse_pool_statement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
goto get_clients;
|
||||||
|
|
||||||
|
case MEMBERS:
|
||||||
|
if (next_token (&val, cfile) != OF) {
|
||||||
|
parse_warn ("expecting \"of\"");
|
||||||
|
skip_to_semi (cfile);
|
||||||
|
free_permit (permit,
|
||||||
|
"parse_pool_statement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (next_token (&val, cfile) != STRING) {
|
||||||
|
parse_warn ("expecting class name.");
|
||||||
|
skip_to_semi (cfile);
|
||||||
|
free_permit (permit,
|
||||||
|
"parse_pool_statement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
permit -> type = permit_class;
|
||||||
|
permit -> class = find_class (val);
|
||||||
|
if (!permit -> class)
|
||||||
|
parse_warn ("no such class: %s", val);
|
||||||
|
default:
|
||||||
|
parse_warn ("expecting permit type.");
|
||||||
|
skip_to_semi (cfile);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
while (*permit_head)
|
||||||
|
permit_head = &((*permit_head) -> next);
|
||||||
|
*permit_head = permit;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DENY:
|
||||||
|
permit_head = &pool -> prohibit_list;
|
||||||
|
goto get_permit;
|
||||||
|
|
||||||
|
case RBRACE:
|
||||||
|
next_token (&val, cfile);
|
||||||
|
done = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
parse_warn ("expecting address range or permit list.");
|
||||||
|
skip_to_semi (cfile);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (!done);
|
||||||
|
|
||||||
|
if (type == SUBNET_DECL)
|
||||||
|
pool -> shared_network = group -> subnet -> shared_network;
|
||||||
|
else
|
||||||
|
pool -> shared_network = group -> shared_network;
|
||||||
|
|
||||||
|
p = &pool -> shared_network -> pools;
|
||||||
|
for (; *p; p = &((*p) -> next))
|
||||||
|
;
|
||||||
|
*p = pool;
|
||||||
|
}
|
||||||
|
|
||||||
/* allow-deny-keyword :== BOOTP
|
/* allow-deny-keyword :== BOOTP
|
||||||
| BOOTING
|
| BOOTING
|
||||||
| DYNAMIC_BOOTP
|
| DYNAMIC_BOOTP
|
||||||
@@ -807,9 +945,7 @@ void parse_shared_net_declaration (cfile, group)
|
|||||||
share = new_shared_network ("parse_shared_net_declaration");
|
share = new_shared_network ("parse_shared_net_declaration");
|
||||||
if (!share)
|
if (!share)
|
||||||
error ("No memory for shared subnet");
|
error ("No memory for shared subnet");
|
||||||
share -> leases = (struct lease *)0;
|
share -> pools = (struct pool *)0;
|
||||||
share -> last_lease = (struct lease *)0;
|
|
||||||
share -> insertion_point = (struct lease *)0;
|
|
||||||
share -> next = (struct shared_network *)0;
|
share -> next = (struct shared_network *)0;
|
||||||
share -> interface = (struct interface_info *)0;
|
share -> interface = (struct interface_info *)0;
|
||||||
share -> group = clone_group (group, "parse_shared_net_declaration");
|
share -> group = clone_group (group, "parse_shared_net_declaration");
|
||||||
@@ -1229,16 +1365,21 @@ struct lease *parse_lease_declaration (cfile)
|
|||||||
/* address-range-declaration :== ip-address ip-address SEMI
|
/* address-range-declaration :== ip-address ip-address SEMI
|
||||||
| DYNAMIC_BOOTP ip-address ip-address SEMI */
|
| DYNAMIC_BOOTP ip-address ip-address SEMI */
|
||||||
|
|
||||||
void parse_address_range (cfile, subnet)
|
void parse_address_range (cfile, group, type, pool)
|
||||||
FILE *cfile;
|
FILE *cfile;
|
||||||
struct subnet *subnet;
|
struct group *group;
|
||||||
|
int type;
|
||||||
|
struct pool *pool;
|
||||||
{
|
{
|
||||||
struct iaddr low, high;
|
struct iaddr low, high, net;
|
||||||
unsigned char addr [4];
|
unsigned char addr [4];
|
||||||
int len = sizeof addr;
|
int len = sizeof addr;
|
||||||
enum dhcp_token token;
|
enum dhcp_token token;
|
||||||
char *val;
|
char *val;
|
||||||
int dynamic = 0;
|
int dynamic = 0;
|
||||||
|
struct subnet *subnet;
|
||||||
|
struct shared_network *share;
|
||||||
|
struct pool *p;
|
||||||
|
|
||||||
if ((token = peek_token (&val, cfile)) == DYNAMIC_BOOTP) {
|
if ((token = peek_token (&val, cfile)) == DYNAMIC_BOOTP) {
|
||||||
token = next_token (&val, cfile);
|
token = next_token (&val, cfile);
|
||||||
@@ -1270,7 +1411,65 @@ void parse_address_range (cfile, subnet)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type == SUBNET_DECL) {
|
||||||
|
subnet = group -> subnet;
|
||||||
|
share = subnet -> shared_network;
|
||||||
|
} else {
|
||||||
|
share = group -> shared_network;
|
||||||
|
for (subnet = share -> subnets;
|
||||||
|
subnet; subnet = subnet -> next_sibling) {
|
||||||
|
net = subnet_number (low, subnet -> netmask);
|
||||||
|
if (addr_eq (low, subnet -> net))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!subnet) {
|
||||||
|
parse_warn ("address range not on network %s",
|
||||||
|
group -> shared_network -> name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pool) {
|
||||||
|
struct pool *last;
|
||||||
|
/* If we're permitting dynamic bootp for this range,
|
||||||
|
then look for a pool with an empty prohibit list and
|
||||||
|
a permit list with one entry which permits dynamic
|
||||||
|
bootp. */
|
||||||
|
for (pool = share -> pools; pool; pool = pool -> next) {
|
||||||
|
if ((!dynamic &&
|
||||||
|
!pool -> permit_list && !pool -> prohibit_list) ||
|
||||||
|
(dynamic &&
|
||||||
|
!pool -> prohibit_list &&
|
||||||
|
pool -> permit_list &&
|
||||||
|
!pool -> permit_list -> next &&
|
||||||
|
(pool -> permit_list -> type ==
|
||||||
|
permit_dynamic_bootp_clients))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
last = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we didn't get a pool, make one. */
|
||||||
|
if (!pool) {
|
||||||
|
pool = new_pool ("parse_address_range");
|
||||||
|
if (!pool)
|
||||||
|
error ("no memory for ad-hoc pool.");
|
||||||
|
if (dynamic) {
|
||||||
|
pool -> permit_list =
|
||||||
|
new_permit ("parse_address_range");
|
||||||
|
if (!pool -> permit_list)
|
||||||
|
error ("no memory for ad-hoc permit.");
|
||||||
|
pool -> permit_list -> type =
|
||||||
|
permit_dynamic_bootp_clients;
|
||||||
|
}
|
||||||
|
if (share -> pools)
|
||||||
|
last -> next = pool;
|
||||||
|
else
|
||||||
|
share -> pools = pool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Create the new address range... */
|
/* Create the new address range... */
|
||||||
new_address_range (low, high, subnet, dynamic);
|
new_address_range (low, high, subnet, pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
162
server/dhcp.c
162
server/dhcp.c
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
#ifndef lint
|
#ifndef lint
|
||||||
static char copyright[] =
|
static char copyright[] =
|
||||||
"$Id: dhcp.c,v 1.70 1998/11/06 02:59:11 mellon Exp $ Copyright (c) 1995, 1996, 1997, 1998 The Internet Software Consortium. All rights reserved.\n";
|
"$Id: dhcp.c,v 1.71 1998/11/09 02:46:58 mellon Exp $ Copyright (c) 1995, 1996, 1997, 1998 The Internet Software Consortium. All rights reserved.\n";
|
||||||
#endif /* not lint */
|
#endif /* not lint */
|
||||||
|
|
||||||
#include "dhcpd.h"
|
#include "dhcpd.h"
|
||||||
@@ -110,24 +110,16 @@ void dhcpdiscover (packet)
|
|||||||
|
|
||||||
/* If we didn't find a lease, try to allocate one... */
|
/* If we didn't find a lease, try to allocate one... */
|
||||||
if (!lease) {
|
if (!lease) {
|
||||||
lease = packet -> shared_network -> last_lease;
|
lease = allocate_lease (packet,
|
||||||
|
packet -> shared_network -> pools, 0);
|
||||||
/* If there are no leases in that subnet that have
|
if (!lease) {
|
||||||
expired, we have nothing to offer this client. */
|
note ("no free leases on network %s match %s",
|
||||||
if (!lease || lease -> ends > cur_time) {
|
packet -> shared_network -> name,
|
||||||
note ("no free leases on subnet %s",
|
print_hw_addr (packet -> raw -> htype,
|
||||||
packet -> shared_network -> name);
|
packet -> raw -> hlen,
|
||||||
|
packet -> raw -> chaddr));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If we find an abandoned lease, take it, but print a
|
|
||||||
warning message, so that if it continues to lose,
|
|
||||||
the administrator will eventually investigate. */
|
|
||||||
if (lease -> flags & ABANDONED_LEASE) {
|
|
||||||
warn ("Reclaiming abandoned IP address %s.",
|
|
||||||
piaddr (lease -> ip_addr));
|
|
||||||
lease -> flags &= ~ABANDONED_LEASE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ack_lease (packet, lease, DHCPOFFER, cur_time + 120);
|
ack_lease (packet, lease, DHCPOFFER, cur_time + 120);
|
||||||
@@ -596,7 +588,7 @@ void ack_lease (packet, lease, offer, when)
|
|||||||
(struct group *)0);
|
(struct group *)0);
|
||||||
|
|
||||||
/* Vendor and user classes are only supported for DHCP clients. */
|
/* Vendor and user classes are only supported for DHCP clients. */
|
||||||
if (state -> offer) {
|
if (offer) {
|
||||||
for (i = packet -> class_count; i > 0; i--) {
|
for (i = packet -> class_count; i > 0; i--) {
|
||||||
execute_statements_in_scope
|
execute_statements_in_scope
|
||||||
(packet, &state -> options,
|
(packet, &state -> options,
|
||||||
@@ -1376,6 +1368,8 @@ struct lease *find_lease (packet, share, ours)
|
|||||||
hp = find_hosts_by_uid (client_identifier.data,
|
hp = find_hosts_by_uid (client_identifier.data,
|
||||||
client_identifier.len);
|
client_identifier.len);
|
||||||
if (hp) {
|
if (hp) {
|
||||||
|
/* Remember if we know of this client. */
|
||||||
|
packet -> known = 1;
|
||||||
fixed_lease = mockup_lease (packet, share, hp);
|
fixed_lease = mockup_lease (packet, share, hp);
|
||||||
} else
|
} else
|
||||||
fixed_lease = (struct lease *)0;
|
fixed_lease = (struct lease *)0;
|
||||||
@@ -1422,6 +1416,8 @@ struct lease *find_lease (packet, share, ours)
|
|||||||
packet -> raw -> chaddr,
|
packet -> raw -> chaddr,
|
||||||
packet -> raw -> hlen);
|
packet -> raw -> hlen);
|
||||||
if (hp) {
|
if (hp) {
|
||||||
|
/* Remember if we know of this client. */
|
||||||
|
packet -> known = 1;
|
||||||
host = hp; /* Save it for later. */
|
host = hp; /* Save it for later. */
|
||||||
fixed_lease = mockup_lease (packet, share, hp);
|
fixed_lease = mockup_lease (packet, share, hp);
|
||||||
#if defined (DEBUG_FIND_LEASE)
|
#if defined (DEBUG_FIND_LEASE)
|
||||||
@@ -1443,10 +1439,13 @@ struct lease *find_lease (packet, share, ours)
|
|||||||
if (hw_lease -> shared_network == share) {
|
if (hw_lease -> shared_network == share) {
|
||||||
if (hw_lease -> flags & ABANDONED_LEASE)
|
if (hw_lease -> flags & ABANDONED_LEASE)
|
||||||
continue;
|
continue;
|
||||||
if (packet -> packet_type)
|
/* If we're allowed to use this lease, do so. */
|
||||||
break;
|
if (!((lease -> pool -> prohibit_list &&
|
||||||
if (hw_lease -> flags &
|
permitted (packet,
|
||||||
(BOOTP_LEASE | DYNAMIC_BOOTP_OK))
|
lease -> pool -> prohibit_list)) ||
|
||||||
|
(lease -> pool -> permit_list &&
|
||||||
|
!permitted (packet,
|
||||||
|
lease -> pool -> permit_list))))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1743,3 +1742,122 @@ struct lease *mockup_lease (packet, share, hp)
|
|||||||
mock.flags = STATIC_LEASE;
|
mock.flags = STATIC_LEASE;
|
||||||
return &mock;
|
return &mock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Look through all the pools in a list starting with the specified pool
|
||||||
|
for a free lease. We try to find a virgin lease if we can. If we
|
||||||
|
don't find a virgin lease, we try to find a non-virgin lease that's
|
||||||
|
free. If we can't find one of those, we try to reclaim an abandoned
|
||||||
|
lease. If all of these possibilities fail to pan out, we don't return
|
||||||
|
a lease at all. */
|
||||||
|
|
||||||
|
struct lease *allocate_lease (packet, pool, ok)
|
||||||
|
struct packet *packet;
|
||||||
|
struct pool *pool;
|
||||||
|
int ok;
|
||||||
|
{
|
||||||
|
struct lease *lease, *lp;
|
||||||
|
struct permit *permit;
|
||||||
|
|
||||||
|
if (!pool)
|
||||||
|
return (struct lease *)0;
|
||||||
|
|
||||||
|
/* If we aren't elegible to try this pool, try a subsequent one. */
|
||||||
|
if ((pool -> prohibit_list &&
|
||||||
|
permitted (packet, pool -> prohibit_list)) ||
|
||||||
|
(pool -> permit_list && !permitted (packet, pool -> permit_list)))
|
||||||
|
return allocate_lease (packet, pool -> next, ok);
|
||||||
|
|
||||||
|
lease = pool -> last_lease;
|
||||||
|
|
||||||
|
/* If there are no leases in the pool that have
|
||||||
|
expired, try the next one. */
|
||||||
|
if (!lease || lease -> ends > cur_time)
|
||||||
|
return allocate_lease (packet, pool -> next, ok);
|
||||||
|
|
||||||
|
/* If we find an abandoned lease, and no other lease qualifies
|
||||||
|
better, take it. */
|
||||||
|
if (lease -> flags & ABANDONED_LEASE) {
|
||||||
|
/* If we already have a non-abandoned lease that we didn't
|
||||||
|
love, but that's okay, don't reclaim the abandoned lease. */
|
||||||
|
if (ok)
|
||||||
|
return allocate_lease (packet, pool -> next, ok);
|
||||||
|
lp = allocate_lease (packet, pool -> next, 0);
|
||||||
|
if (!lp) {
|
||||||
|
warn ("Reclaiming abandoned IP address %s.",
|
||||||
|
piaddr (lease -> ip_addr));
|
||||||
|
lease -> flags &= ~ABANDONED_LEASE;
|
||||||
|
return lease;
|
||||||
|
}
|
||||||
|
return lp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If there's a lease we could take, but it had previously been
|
||||||
|
allocated to a different client, try for a virgin lease before
|
||||||
|
stealing it. */
|
||||||
|
if (lease -> uid_len || lease -> hardware_addr.hlen) {
|
||||||
|
/* If we're already in that boat, no need to consider
|
||||||
|
allocating this particular lease. */
|
||||||
|
if (ok)
|
||||||
|
return allocate_lease (packet, pool -> next, ok);
|
||||||
|
|
||||||
|
lp = allocate_lease (packet, pool -> next, 1);
|
||||||
|
if (lp)
|
||||||
|
return lp;
|
||||||
|
return lease;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Determine whether or not a permit exists on a particular permit list
|
||||||
|
that matches the specified packet, returning nonzero if so, zero if
|
||||||
|
not. */
|
||||||
|
|
||||||
|
int permitted (packet, permit_list)
|
||||||
|
struct packet *packet;
|
||||||
|
struct permit *permit_list;
|
||||||
|
{
|
||||||
|
struct permit *p;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (p = permit_list; p; p = p -> next) {
|
||||||
|
switch (p -> type) {
|
||||||
|
case permit_unknown_clients:
|
||||||
|
if (!packet -> known)
|
||||||
|
return 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case permit_known_clients:
|
||||||
|
if (packet -> known)
|
||||||
|
return 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case permit_authenticated_clients:
|
||||||
|
if (packet -> authenticated)
|
||||||
|
return 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case permit_unauthenticated_clients:
|
||||||
|
if (!packet -> authenticated)
|
||||||
|
return 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case permit_all_clients:
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case permit_dynamic_bootp_clients:
|
||||||
|
if (!packet -> options_valid ||
|
||||||
|
!packet -> packet_type)
|
||||||
|
return 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case permit_class:
|
||||||
|
for (i = 0; i < packet -> class_count; i++)
|
||||||
|
if (p -> class == packet -> classes [i])
|
||||||
|
return 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user