Release 2.14.6
- [BUGFIX] Fix amplification mitigation in 0-RTT case. - [BUGFIX] IETF mini connection should not tickable if cannot send a packet due to amplification. - [BUGFIX] Fail if active_connection_id_limit TP is smaller than 2. - [BUGFIX] Qlog server certificates for IETF QUIC connections. - [BUGFIX] Uninitialized struct padding usage in tokgen (benign). - [BUGFIX] Incorrect argument to shi_lookup() (benign).
This commit is contained in:
parent
72585dc942
commit
b55a5117d9
10
CHANGELOG
10
CHANGELOG
|
@ -1,3 +1,13 @@
|
|||
2020-05-06
|
||||
- 2.14.6
|
||||
- [BUGFIX] Fix amplification mitigation in 0-RTT case.
|
||||
- [BUGFIX] IETF mini connection should not tickable if cannot send
|
||||
a packet due to amplification.
|
||||
- [BUGFIX] Fail if active_connection_id_limit TP is smaller than 2.
|
||||
- [BUGFIX] Qlog server certificates for IETF QUIC connections.
|
||||
- [BUGFIX] Uninitialized struct padding usage in tokgen (benign).
|
||||
- [BUGFIX] Incorrect argument to shi_lookup() (benign).
|
||||
|
||||
2020-04-29
|
||||
- 2.14.5
|
||||
- [BUGFIX] In coalesced datagram, ignore packets whose CID does not match.
|
||||
|
|
|
@ -26,7 +26,7 @@ author = u'LiteSpeed Technologies'
|
|||
# The short X.Y version
|
||||
version = u'2.14'
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = u'2.14.5'
|
||||
release = u'2.14.6'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
|
|
@ -25,7 +25,7 @@ extern "C" {
|
|||
|
||||
#define LSQUIC_MAJOR_VERSION 2
|
||||
#define LSQUIC_MINOR_VERSION 14
|
||||
#define LSQUIC_PATCH_VERSION 5
|
||||
#define LSQUIC_PATCH_VERSION 6
|
||||
|
||||
/**
|
||||
* Engine flags:
|
||||
|
|
|
@ -57,6 +57,11 @@ struct lsquic_conn_public {
|
|||
#if LSQUIC_EXTRA_CHECKS
|
||||
unsigned long stream_frame_bytes;
|
||||
#endif
|
||||
/* "unsigned" is wide enough: these values are only used for amplification
|
||||
* limit before initial path is validated.
|
||||
*/
|
||||
unsigned bytes_in; /* successfully processed */
|
||||
unsigned bytes_out;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1019,9 +1019,15 @@ verify_server_cert_callback (SSL *ssl, uint8_t *out_alert)
|
|||
return ssl_verify_invalid;
|
||||
}
|
||||
|
||||
s = enc_sess->esi_enpub->enp_verify_cert(
|
||||
enc_sess->esi_enpub->enp_verify_ctx, chain);
|
||||
return s == 0 ? ssl_verify_ok : ssl_verify_invalid;
|
||||
EV_LOG_CERT_CHAIN(LSQUIC_LOG_CONN_ID, chain);
|
||||
if (enc_sess->esi_enpub->enp_verify_cert)
|
||||
{
|
||||
s = enc_sess->esi_enpub->enp_verify_cert(
|
||||
enc_sess->esi_enpub->enp_verify_ctx, chain);
|
||||
return s == 0 ? ssl_verify_ok : ssl_verify_invalid;
|
||||
}
|
||||
else
|
||||
return ssl_verify_ok;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1302,7 +1308,9 @@ init_client (struct enc_sess_iquic *const enc_sess)
|
|||
SSL_CTX_sess_set_new_cb(ssl_ctx, iquic_new_session_cb);
|
||||
if (enc_sess->esi_enpub->enp_kli)
|
||||
SSL_CTX_set_keylog_callback(ssl_ctx, keylog_callback);
|
||||
if (enc_sess->esi_enpub->enp_verify_cert)
|
||||
if (enc_sess->esi_enpub->enp_verify_cert
|
||||
|| LSQ_LOG_ENABLED_EXT(LSQ_LOG_DEBUG, LSQLM_EVENT)
|
||||
|| LSQ_LOG_ENABLED_EXT(LSQ_LOG_DEBUG, LSQLM_QLOG))
|
||||
SSL_CTX_set_custom_verify(ssl_ctx, SSL_VERIFY_PEER,
|
||||
verify_server_cert_callback);
|
||||
SSL_CTX_set_early_data_enabled(ssl_ctx, 1);
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
#include <string.h>
|
||||
#include <sys/queue.h>
|
||||
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#include "lsquic.h"
|
||||
#include "lsquic_types.h"
|
||||
#include "lsquic_int_types.h"
|
||||
|
@ -566,6 +568,24 @@ lsquic_ev_log_check_certs (const lsquic_cid_t *cid, const lsquic_str_t **certs,
|
|||
}
|
||||
|
||||
|
||||
void
|
||||
lsquic_ev_log_cert_chain (const lsquic_cid_t *cid, struct stack_st_X509 *chain)
|
||||
{
|
||||
X509_NAME *name;
|
||||
X509 *cert;
|
||||
unsigned i;
|
||||
char buf[0x100];
|
||||
|
||||
for (i = 0; i < sk_X509_num(chain); ++i)
|
||||
{
|
||||
cert = sk_X509_value(chain, i);
|
||||
name = X509_get_subject_name(cert);
|
||||
LCID("cert #%u: name: %s", i,
|
||||
X509_NAME_oneline(name, buf, sizeof(buf)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
lsquic_ev_log_version_negotiation (const lsquic_cid_t *cid,
|
||||
const char *action, const char *ver)
|
||||
|
|
|
@ -17,6 +17,7 @@ struct lsquic_packet_out;
|
|||
struct parse_funcs;
|
||||
struct stream_frame;
|
||||
struct uncompressed_headers;
|
||||
struct stack_st_X509;
|
||||
|
||||
|
||||
/* Log a generic event not tied to any particular connection */
|
||||
|
@ -329,6 +330,16 @@ lsquic_ev_log_check_certs (const lsquic_cid_t *, const lsquic_str_t **, size_t);
|
|||
lsquic_qlog_check_certs(__VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
void
|
||||
lsquic_ev_log_cert_chain (const lsquic_cid_t *, struct stack_st_X509 *);
|
||||
|
||||
#define EV_LOG_CERT_CHAIN(...) do { \
|
||||
if (LSQ_LOG_ENABLED_EXT(LSQ_LOG_DEBUG, LSQLM_EVENT)) \
|
||||
lsquic_ev_log_cert_chain(__VA_ARGS__); \
|
||||
if (LSQ_LOG_ENABLED_EXT(LSQ_LOG_DEBUG, LSQLM_QLOG)) \
|
||||
lsquic_qlog_cert_chain(__VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
void
|
||||
lsquic_ev_log_version_negotiation (const lsquic_cid_t *, const char *, const char *);
|
||||
|
||||
|
|
|
@ -138,6 +138,12 @@ enum ifull_conn_flags
|
|||
};
|
||||
|
||||
|
||||
enum more_flags
|
||||
{
|
||||
MF_VALIDATE_PATH = 1 << 0,
|
||||
};
|
||||
|
||||
|
||||
#define N_PATHS 2
|
||||
|
||||
enum send
|
||||
|
@ -315,6 +321,7 @@ struct ietf_full_conn
|
|||
unsigned ifc_max_streams_in[N_SDS];
|
||||
uint64_t ifc_max_stream_data_uni;
|
||||
enum ifull_conn_flags ifc_flags;
|
||||
enum more_flags ifc_mflags;
|
||||
enum send_flags ifc_send_flags;
|
||||
enum send_flags ifc_delayed_send;
|
||||
struct {
|
||||
|
@ -1318,6 +1325,12 @@ lsquic_ietf_full_conn_server_new (struct lsquic_engine_public *enpub,
|
|||
conn->ifc_paths[0].cop_path = imc->imc_path;
|
||||
conn->ifc_paths[0].cop_flags = COP_VALIDATED;
|
||||
conn->ifc_used_paths = 1 << 0;
|
||||
if (imc->imc_flags & IMC_ADDR_VALIDATED)
|
||||
lsquic_send_ctl_path_validated(&conn->ifc_send_ctl);
|
||||
else
|
||||
conn->ifc_mflags |= MF_VALIDATE_PATH;
|
||||
conn->ifc_pub.bytes_out = imc->imc_bytes_out;
|
||||
conn->ifc_pub.bytes_in = imc->imc_bytes_in;
|
||||
if (imc->imc_flags & IMC_PATH_CHANGED)
|
||||
{
|
||||
LSQ_DEBUG("path changed during mini conn: schedule PATH_CHALLENGE");
|
||||
|
@ -6284,6 +6297,14 @@ process_regular_packet (struct ietf_full_conn *conn,
|
|||
else
|
||||
conn->ifc_spin_bit = !lsquic_packet_in_spin_bit(packet_in);
|
||||
}
|
||||
conn->ifc_pub.bytes_in += packet_in->pi_data_sz;
|
||||
if ((conn->ifc_mflags & MF_VALIDATE_PATH) &&
|
||||
(packet_in->pi_header_type == HETY_NOT_SET
|
||||
|| packet_in->pi_header_type == HETY_HANDSHAKE))
|
||||
{
|
||||
conn->ifc_mflags &= ~MF_VALIDATE_PATH;
|
||||
lsquic_send_ctl_path_validated(&conn->ifc_send_ctl);
|
||||
}
|
||||
return 0;
|
||||
case REC_ST_DUP:
|
||||
LSQ_INFO("packet %"PRIu64" is a duplicate", packet_in->pi_packno);
|
||||
|
@ -6482,6 +6503,8 @@ ietf_full_conn_ci_packet_sent (struct lsquic_conn *lconn,
|
|||
lsquic_alarmset_set(&conn->ifc_alset, AL_BLOCKED_KA,
|
||||
packet_out->po_sent + (1 + (7 & lsquic_crand_get_nybble(
|
||||
conn->ifc_enpub->enp_crand))) * 1000000);
|
||||
conn->ifc_pub.bytes_out += lsquic_packet_out_sent_sz(&conn->ifc_conn,
|
||||
packet_out);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -46,6 +46,9 @@ static const struct conn_iface mini_conn_ietf_iface;
|
|||
|
||||
static unsigned highest_bit_set (unsigned long long);
|
||||
|
||||
static int
|
||||
imico_can_send (const struct ietf_mini_conn *, size_t);
|
||||
|
||||
|
||||
static const enum header_type el2hety[] =
|
||||
{
|
||||
|
@ -625,11 +628,15 @@ ietf_mini_conn_ci_is_tickable (struct lsquic_conn *lconn)
|
|||
{
|
||||
struct ietf_mini_conn *const conn = (struct ietf_mini_conn *) lconn;
|
||||
const struct lsquic_packet_out *packet_out;
|
||||
size_t packet_size;
|
||||
|
||||
if (conn->imc_enpub->enp_flags & ENPUB_CAN_SEND)
|
||||
TAILQ_FOREACH(packet_out, &conn->imc_packets_out, po_next)
|
||||
if (!(packet_out->po_flags & PO_SENT))
|
||||
return 1;
|
||||
{
|
||||
packet_size = lsquic_packet_out_total_sz(lconn, packet_out);
|
||||
return imico_can_send(conn, packet_size + IQUIC_TAG_LEN);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ typedef struct lsquic_packet_out
|
|||
lconn->cn_pf->pf_packout_max_header_size(lconn, po_flags, dcid_len))
|
||||
|
||||
#define lsquic_packet_out_total_sz(lconn, p) (\
|
||||
lconn->cn_pf->pf_packout_size(lconn, p))
|
||||
(lconn)->cn_pf->pf_packout_size(lconn, p))
|
||||
|
||||
#if __GNUC__
|
||||
#if LSQUIC_EXTRA_CHECKS
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#include "lsquic.h"
|
||||
#include "lsquic_types.h"
|
||||
#include "lsquic_int_types.h"
|
||||
|
@ -203,6 +206,46 @@ lsquic_qlog_check_certs (const lsquic_cid_t* cid, const lsquic_str_t **certs,
|
|||
}
|
||||
|
||||
|
||||
void
|
||||
lsquic_qlog_cert_chain (const lsquic_cid_t* cid, struct stack_st_X509 *chain)
|
||||
{
|
||||
X509 *cert;
|
||||
unsigned i;
|
||||
unsigned char *buf;
|
||||
char *hexbuf, *newbuf;
|
||||
size_t hexbuf_sz;
|
||||
int len;
|
||||
lsquic_time_t now;
|
||||
|
||||
now = lsquic_time_now();
|
||||
hexbuf = NULL;
|
||||
hexbuf_sz = 0;
|
||||
for (i = 0; i < sk_X509_num(chain); ++i)
|
||||
{
|
||||
cert = sk_X509_value(chain, i);
|
||||
buf = NULL;
|
||||
len = i2d_X509(cert, &buf);
|
||||
if (len <= 0)
|
||||
break;
|
||||
if ((size_t) len * 2 + 1 > hexbuf_sz)
|
||||
{
|
||||
hexbuf_sz = len * 2 + 1;
|
||||
newbuf = realloc(hexbuf, hexbuf_sz);
|
||||
if (!newbuf)
|
||||
break;
|
||||
hexbuf = newbuf;
|
||||
}
|
||||
lsquic_hexstr(buf, (size_t) len, hexbuf, hexbuf_sz);
|
||||
LCID("[%" PRIu64 ",\"SECURITY\",\"CHECK_CERT\",\"CERTLOG\","
|
||||
"{\"certificate\":\"%s\"}]", now, hexbuf);
|
||||
OPENSSL_free(buf);
|
||||
}
|
||||
|
||||
if (hexbuf)
|
||||
free(hexbuf);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
lsquic_qlog_version_negotiation (const lsquic_cid_t* cid,
|
||||
const char *action, const char *ver)
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
#include "lsquic_packet_common.h"
|
||||
#include "lsquic_str.h"
|
||||
|
||||
struct stack_st_X509;
|
||||
|
||||
/*
|
||||
EventCategory
|
||||
CONNECTIVITY
|
||||
|
@ -83,6 +85,9 @@ lsquic_qlog_zero_rtt (const lsquic_cid_t *);
|
|||
void
|
||||
lsquic_qlog_check_certs (const lsquic_cid_t *, const lsquic_str_t **, size_t);
|
||||
|
||||
void
|
||||
lsquic_qlog_cert_chain (const lsquic_cid_t *, struct stack_st_X509 *);
|
||||
|
||||
void
|
||||
lsquic_qlog_version_negotiation (const lsquic_cid_t *, const char *, const char *);
|
||||
|
||||
|
|
|
@ -119,6 +119,12 @@ send_ctl_all_bytes_out (const struct lsquic_send_ctl *ctl);
|
|||
static void
|
||||
send_ctl_reschedule_poison (struct lsquic_send_ctl *ctl);
|
||||
|
||||
static int
|
||||
send_ctl_can_send_pre_hsk (struct lsquic_send_ctl *ctl);
|
||||
|
||||
static int
|
||||
send_ctl_can_send (struct lsquic_send_ctl *ctl);
|
||||
|
||||
#ifdef NDEBUG
|
||||
static
|
||||
#elif __GNUC__
|
||||
|
@ -363,6 +369,11 @@ lsquic_send_ctl_init (lsquic_send_ctl_t *ctl, struct lsquic_alarmset *alset,
|
|||
ctl->sc_flags |= SC_SANITY_CHECK;
|
||||
#endif
|
||||
ctl->sc_gap = UINT64_MAX - 1 /* Can't have +1 == 0 */;
|
||||
if ((ctl->sc_conn_pub->lconn->cn_flags & (LSCONN_IETF|LSCONN_SERVER))
|
||||
== (LSCONN_IETF|LSCONN_SERVER))
|
||||
ctl->sc_can_send = send_ctl_can_send_pre_hsk;
|
||||
else
|
||||
ctl->sc_can_send = send_ctl_can_send;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1101,8 +1112,6 @@ lsquic_send_ctl_got_ack (lsquic_send_ctl_t *ctl,
|
|||
packet_out->po_packno);
|
||||
#if LSQUIC_CONN_STATS
|
||||
++ctl->sc_conn_pub->conn_stats->out.acked_via_loss;
|
||||
LSQ_DEBUG("acking via loss record %"PRIu64,
|
||||
packet_out->po_packno);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
|
@ -1367,13 +1376,8 @@ lsquic_send_ctl_pacer_blocked (struct lsquic_send_ctl *ctl)
|
|||
}
|
||||
|
||||
|
||||
#ifndef NDEBUG
|
||||
#if __GNUC__
|
||||
__attribute__((weak))
|
||||
#endif
|
||||
#endif
|
||||
int
|
||||
lsquic_send_ctl_can_send (lsquic_send_ctl_t *ctl)
|
||||
static int
|
||||
send_ctl_can_send (struct lsquic_send_ctl *ctl)
|
||||
{
|
||||
const unsigned n_out = send_ctl_all_bytes_out(ctl);
|
||||
LSQ_DEBUG("%s: n_out: %u (unacked_all: %u); cwnd: %"PRIu64, __func__,
|
||||
|
@ -1400,6 +1404,37 @@ lsquic_send_ctl_can_send (lsquic_send_ctl_t *ctl)
|
|||
}
|
||||
|
||||
|
||||
static int
|
||||
send_ctl_can_send_pre_hsk (struct lsquic_send_ctl *ctl)
|
||||
{
|
||||
unsigned bytes_in, bytes_out;
|
||||
|
||||
bytes_in = ctl->sc_conn_pub->bytes_in;
|
||||
bytes_out = ctl->sc_conn_pub->bytes_out + ctl->sc_bytes_scheduled;
|
||||
if (bytes_out >= bytes_in * 2 + bytes_in / 2 /* This should work out
|
||||
to around 3 on average */)
|
||||
{
|
||||
LSQ_DEBUG("%s: amplification block: %u bytes in, %u bytes out",
|
||||
__func__, bytes_in, bytes_out);
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
return send_ctl_can_send(ctl);
|
||||
}
|
||||
|
||||
|
||||
#ifndef NDEBUG
|
||||
#if __GNUC__
|
||||
__attribute__((weak))
|
||||
#endif
|
||||
#endif
|
||||
int
|
||||
lsquic_send_ctl_can_send (struct lsquic_send_ctl *ctl)
|
||||
{
|
||||
return ctl->sc_can_send(ctl);
|
||||
}
|
||||
|
||||
|
||||
/* Like lsquic_send_ctl_can_send(), but no mods */
|
||||
static int
|
||||
send_ctl_could_send (const struct lsquic_send_ctl *ctl)
|
||||
|
@ -2986,3 +3021,11 @@ lsquic_send_ctl_begin_optack_detection (struct lsquic_send_ctl *ctl)
|
|||
rand = lsquic_crand_get_byte(ctl->sc_enpub->enp_crand);
|
||||
ctl->sc_gap = ctl->sc_cur_packno + 1 + rand;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
lsquic_send_ctl_path_validated (struct lsquic_send_ctl *ctl)
|
||||
{
|
||||
LSQ_DEBUG("path validated: switch to regular can_send");
|
||||
ctl->sc_can_send = send_ctl_can_send;
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ typedef struct lsquic_send_ctl {
|
|||
lsquic_time_t sc_largest_acked_sent_time;
|
||||
lsquic_time_t sc_last_sent_time;
|
||||
lsquic_time_t sc_last_rto_time;
|
||||
int (*sc_can_send)(struct lsquic_send_ctl *);
|
||||
unsigned sc_bytes_unacked_retx;
|
||||
unsigned sc_bytes_scheduled;
|
||||
union {
|
||||
|
@ -383,4 +384,7 @@ lsquic_send_ctl_begin_optack_detection (struct lsquic_send_ctl *);
|
|||
|
||||
#define lsquic_send_ctl_n_unacked(ctl_) ((ctl_)->sc_n_in_flight_retx)
|
||||
|
||||
void
|
||||
lsquic_send_ctl_path_validated (struct lsquic_send_ctl *);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -92,11 +92,18 @@ get_or_generate_state (struct lsquic_engine_public *enpub, time_t now,
|
|||
size_t bufsz;
|
||||
struct {
|
||||
time_t now;
|
||||
unsigned char buf[20];
|
||||
} srst_ikm;
|
||||
unsigned char buf[24];
|
||||
}
|
||||
#if __GNUC__
|
||||
/* This is more of a documentation note: this struct should already
|
||||
* have a multiple-of-eight size.
|
||||
*/
|
||||
__attribute__((packed))
|
||||
#endif
|
||||
srst_ikm;
|
||||
|
||||
data = shm_state;
|
||||
sz = sizeof(shm_state);
|
||||
sz = sizeof(*shm_state);
|
||||
s = shi->shi_lookup(ctx, TOKGEN_SHM_KEY, TOKGEN_SHM_KEY_SIZE, &data, &sz);
|
||||
|
||||
if (s == 1)
|
||||
|
|
|
@ -152,6 +152,7 @@ static const uint64_t min_vals[MAX_NUMERIC_TPI + 1] =
|
|||
/* On the other hand, we do enforce the lower bound. */
|
||||
[TPI_MAX_PACKET_SIZE] = 1200,
|
||||
[TPI_MIN_ACK_DELAY] = 1,
|
||||
[TPI_ACTIVE_CONNECTION_ID_LIMIT] = 2,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ SET(TESTS
|
|||
stop_waiting_gquic_be
|
||||
streamgen
|
||||
streamparse
|
||||
tokgen
|
||||
trapa
|
||||
varint
|
||||
ver_nego
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/* Copyright (c) 2017 - 2020 LiteSpeed Technologies Inc. See LICENSE. */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/queue.h>
|
||||
|
||||
#include "lsquic.h"
|
||||
#include "lsquic_types.h"
|
||||
#include "lsquic_int_types.h"
|
||||
#include "lsquic_mm.h"
|
||||
#include "lsquic_tokgen.h"
|
||||
#include "lsquic_stock_shi.h"
|
||||
#include "lsquic_engine_public.h"
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
struct lsquic_engine_public enpub = {
|
||||
.enp_shi_ctx = lsquic_stock_shared_hash_new(),
|
||||
.enp_shi = &stock_shi,
|
||||
};
|
||||
struct token_generator *tg;
|
||||
unsigned char token[16];
|
||||
unsigned i;
|
||||
lsquic_cid_t cid;
|
||||
|
||||
memset(&cid, 0, sizeof(cid));
|
||||
cid.len = 8;
|
||||
|
||||
tg = lsquic_tg_new(&enpub);
|
||||
|
||||
lsquic_tg_generate_sreset(tg, &cid, token);
|
||||
for (i = 0; i < sizeof(token); ++i)
|
||||
printf("%02X", token[i]);
|
||||
printf("\n");
|
||||
|
||||
lsquic_tg_destroy(tg);
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue