Initial version.
This commit is contained in:
621
src/nxt_cyassl.c
Normal file
621
src/nxt_cyassl.c
Normal file
@@ -0,0 +1,621 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) NGINX, Inc.
|
||||
* Copyright (C) Igor Sysoev
|
||||
*/
|
||||
|
||||
#include <nxt_main.h>
|
||||
#include <cyassl/ssl.h>
|
||||
#include <cyassl/error-ssl.h>
|
||||
|
||||
|
||||
typedef struct {
|
||||
CYASSL *session;
|
||||
|
||||
int ssl_error;
|
||||
uint8_t times; /* 2 bits */
|
||||
|
||||
nxt_buf_mem_t buffer;
|
||||
} nxt_cyassl_conn_t;
|
||||
|
||||
|
||||
static nxt_int_t nxt_cyassl_server_init(nxt_ssltls_conf_t *conf);
|
||||
static void nxt_cyassl_conn_init(nxt_thread_t *thr, nxt_ssltls_conf_t *conf,
|
||||
nxt_event_conn_t *c);
|
||||
static void nxt_cyassl_session_cleanup(void *data);
|
||||
static int nxt_cyassl_io_recv(CYASSL *ssl, char *buf, int size, void *data);
|
||||
static int nxt_cyassl_io_send(CYASSL *ssl, char *buf, int size, void *data);
|
||||
static void nxt_cyassl_conn_handshake(nxt_thread_t *thr, void *obj, void *data);
|
||||
static void nxt_cyassl_conn_io_read(nxt_thread_t *thr, void *obj, void *data);
|
||||
static void nxt_cyassl_conn_io_shutdown(nxt_thread_t *thr, void *obj,
|
||||
void *data);
|
||||
static ssize_t nxt_cyassl_conn_io_write_chunk(nxt_thread_t *thr,
|
||||
nxt_event_conn_t *c, nxt_buf_t *b, size_t limit);
|
||||
static ssize_t nxt_cyassl_conn_io_send(nxt_event_conn_t *c, void *buf,
|
||||
size_t size);
|
||||
static nxt_int_t nxt_cyassl_conn_test_error(nxt_thread_t *thr,
|
||||
nxt_event_conn_t *c, int err, nxt_work_handler_t handler);
|
||||
static void nxt_cdecl nxt_cyassl_conn_error(nxt_event_conn_t *c, nxt_err_t err,
|
||||
const char *fmt, ...);
|
||||
static nxt_uint_t nxt_cyassl_log_error_level(nxt_event_conn_t *c, nxt_err_t err,
|
||||
int ssl_error);
|
||||
static void nxt_cdecl nxt_cyassl_log_error(nxt_uint_t level, nxt_log_t *log,
|
||||
int ret, const char *fmt, ...);
|
||||
static u_char *nxt_cyassl_copy_error(int err, u_char *p, u_char *end);
|
||||
|
||||
|
||||
const nxt_ssltls_lib_t nxt_cyassl_lib = {
|
||||
nxt_cyassl_server_init,
|
||||
NULL,
|
||||
};
|
||||
|
||||
|
||||
static nxt_event_conn_io_t nxt_cyassl_event_conn_io = {
|
||||
NULL,
|
||||
NULL,
|
||||
|
||||
nxt_cyassl_conn_io_read,
|
||||
NULL,
|
||||
NULL,
|
||||
|
||||
nxt_event_conn_io_write,
|
||||
nxt_cyassl_conn_io_write_chunk,
|
||||
NULL,
|
||||
NULL,
|
||||
nxt_cyassl_conn_io_send,
|
||||
|
||||
nxt_cyassl_conn_io_shutdown,
|
||||
};
|
||||
|
||||
|
||||
static nxt_int_t
|
||||
nxt_cyassl_start(void)
|
||||
{
|
||||
int err;
|
||||
nxt_thread_t *thr;
|
||||
static nxt_bool_t started;
|
||||
|
||||
if (nxt_fast_path(started)) {
|
||||
return NXT_OK;
|
||||
}
|
||||
|
||||
started = 1;
|
||||
|
||||
thr = nxt_thread();
|
||||
|
||||
/* TODO: CyaSSL_Cleanup() */
|
||||
|
||||
err = CyaSSL_Init();
|
||||
if (err != SSL_SUCCESS) {
|
||||
nxt_cyassl_log_error(NXT_LOG_CRIT, thr->log, err,
|
||||
"CyaSSL_Init() failed");
|
||||
return NXT_ERROR;
|
||||
}
|
||||
|
||||
nxt_thread_log_error(NXT_LOG_INFO, "CyaSSL version: %s",
|
||||
LIBCYASSL_VERSION_STRING);
|
||||
|
||||
/* CyaSSL_SetLoggingCb */
|
||||
/* CyaSSL_SetAllocators */
|
||||
|
||||
return NXT_OK;
|
||||
}
|
||||
|
||||
|
||||
static nxt_int_t
|
||||
nxt_cyassl_server_init(nxt_ssltls_conf_t *conf)
|
||||
{
|
||||
int err;
|
||||
char *certificate, *key;
|
||||
CYASSL_CTX *ctx;
|
||||
nxt_thread_t *thr;
|
||||
|
||||
thr = nxt_thread();
|
||||
|
||||
if (nxt_slow_path(nxt_cyassl_start() != NXT_OK)) {
|
||||
return NXT_ERROR;
|
||||
}
|
||||
|
||||
ctx = CyaSSL_CTX_new(CyaSSLv23_server_method());
|
||||
if (ctx == NULL) {
|
||||
nxt_cyassl_log_error(NXT_LOG_CRIT, thr->log, 0,
|
||||
"CyaSSL_CTX_new() failed");
|
||||
return NXT_ERROR;
|
||||
}
|
||||
|
||||
conf->ctx = ctx;
|
||||
conf->conn_init = nxt_cyassl_conn_init;
|
||||
|
||||
certificate = conf->certificate;
|
||||
|
||||
err = CyaSSL_CTX_use_certificate_file(ctx, certificate, SSL_FILETYPE_PEM);
|
||||
if (err != SSL_SUCCESS) {
|
||||
nxt_cyassl_log_error(NXT_LOG_CRIT, thr->log, err,
|
||||
"CyaSSL_CTX_use_certificate_file(\"%s\") failed",
|
||||
certificate);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
key = conf->certificate_key;
|
||||
|
||||
err = CyaSSL_CTX_use_PrivateKey_file(ctx, key, SSL_FILETYPE_PEM);
|
||||
if (err != SSL_SUCCESS) {
|
||||
nxt_cyassl_log_error(NXT_LOG_CRIT, thr->log, err,
|
||||
"CyaSSL_CTX_use_PrivateKey_file(\"%s\") failed",
|
||||
key);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (conf->ciphers != NULL) {
|
||||
err = CyaSSL_CTX_set_cipher_list(ctx, conf->ciphers);
|
||||
if (err != SSL_SUCCESS) {
|
||||
nxt_cyassl_log_error(NXT_LOG_CRIT, thr->log, err,
|
||||
"CyaSSL_CTX_set_cipher_list(\"%s\") failed",
|
||||
conf->ciphers);
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: ca_certificate */
|
||||
|
||||
CyaSSL_SetIORecv(ctx, nxt_cyassl_io_recv);
|
||||
CyaSSL_SetIOSend(ctx, nxt_cyassl_io_send);
|
||||
|
||||
return NXT_OK;
|
||||
|
||||
fail:
|
||||
|
||||
CyaSSL_CTX_free(ctx);
|
||||
|
||||
return NXT_ERROR;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
nxt_cyassl_conn_init(nxt_thread_t *thr, nxt_ssltls_conf_t *conf,
|
||||
nxt_event_conn_t *c)
|
||||
{
|
||||
CYASSL *s;
|
||||
CYASSL_CTX *ctx;
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
nxt_mem_pool_cleanup_t *mpcl;
|
||||
|
||||
nxt_log_debug(c->socket.log, "cyassl conn init");
|
||||
|
||||
ssltls = nxt_mem_zalloc(c->mem_pool, sizeof(nxt_cyassl_conn_t));
|
||||
if (ssltls == NULL) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
c->u.ssltls = ssltls;
|
||||
nxt_buf_mem_set_size(&ssltls->buffer, conf->buffer_size);
|
||||
|
||||
mpcl = nxt_mem_pool_cleanup(c->mem_pool, 0);
|
||||
if (mpcl == NULL) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
ctx = conf->ctx;
|
||||
|
||||
s = CyaSSL_new(ctx);
|
||||
if (s == NULL) {
|
||||
nxt_cyassl_log_error(NXT_LOG_CRIT, c->socket.log, 0,
|
||||
"CyaSSL_new() failed");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
ssltls->session = s;
|
||||
mpcl->handler = nxt_cyassl_session_cleanup;
|
||||
mpcl->data = ssltls;
|
||||
|
||||
CyaSSL_SetIOReadCtx(s, c);
|
||||
CyaSSL_SetIOWriteCtx(s, c);
|
||||
|
||||
c->io = &nxt_cyassl_event_conn_io;
|
||||
c->sendfile = NXT_CONN_SENDFILE_OFF;
|
||||
|
||||
nxt_cyassl_conn_handshake(thr, c, c->socket.data);
|
||||
return;
|
||||
|
||||
fail:
|
||||
|
||||
nxt_event_conn_io_handle(thr, c->read_work_queue,
|
||||
c->read_state->error_handler, c, c->socket.data);
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
nxt_cyassl_session_cleanup(void *data)
|
||||
{
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
|
||||
ssltls = data;
|
||||
|
||||
nxt_thread_log_debug("cyassl session cleanup");
|
||||
|
||||
nxt_free(ssltls->buffer.start);
|
||||
|
||||
CyaSSL_free(ssltls->session);
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
nxt_cyassl_io_recv(CYASSL *ssl, char *buf, int size, void *data)
|
||||
{
|
||||
ssize_t n;
|
||||
nxt_thread_t *thr;
|
||||
nxt_event_conn_t *c;
|
||||
|
||||
c = data;
|
||||
thr = nxt_thread();
|
||||
|
||||
n = thr->engine->event->io->recv(c, (u_char *) buf, size, 0);
|
||||
|
||||
if (n > 0) {
|
||||
return n;
|
||||
}
|
||||
|
||||
if (n == 0) {
|
||||
return CYASSL_CBIO_ERR_CONN_CLOSE;
|
||||
}
|
||||
|
||||
if (n == NXT_AGAIN) {
|
||||
return CYASSL_CBIO_ERR_WANT_READ;
|
||||
}
|
||||
|
||||
return CYASSL_CBIO_ERR_GENERAL;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
nxt_cyassl_io_send(CYASSL *ssl, char *buf, int size, void *data)
|
||||
{
|
||||
ssize_t n;
|
||||
nxt_thread_t *thr;
|
||||
nxt_event_conn_t *c;
|
||||
|
||||
c = data;
|
||||
thr = nxt_thread();
|
||||
|
||||
n = thr->engine->event->io->send(c, (u_char *) buf, size);
|
||||
|
||||
if (n > 0) {
|
||||
return n;
|
||||
}
|
||||
|
||||
if (n == NXT_AGAIN) {
|
||||
return CYASSL_CBIO_ERR_WANT_WRITE;
|
||||
}
|
||||
|
||||
return CYASSL_CBIO_ERR_GENERAL;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
nxt_cyassl_conn_handshake(nxt_thread_t *thr, void *obj, void *data)
|
||||
{
|
||||
int ret;
|
||||
nxt_int_t n;
|
||||
nxt_err_t err;
|
||||
nxt_event_conn_t *c;
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
|
||||
c = obj;
|
||||
ssltls = c->u.ssltls;
|
||||
|
||||
nxt_log_debug(thr->log, "cyassl conn handshake: %d", ssltls->times);
|
||||
|
||||
/* "ssltls->times == 1" is suitable to run CyaSSL_negotiate() in job. */
|
||||
|
||||
ret = CyaSSL_negotiate(ssltls->session);
|
||||
|
||||
err = (ret != 0) ? nxt_socket_errno : 0;
|
||||
|
||||
nxt_thread_time_debug_update(thr);
|
||||
|
||||
nxt_log_debug(thr->log, "CyaSSL_negotiate(%d): %d", c->socket.fd, ret);
|
||||
|
||||
if (ret == 0) {
|
||||
nxt_cyassl_conn_io_read(thr, c, data);
|
||||
return;
|
||||
}
|
||||
|
||||
n = nxt_cyassl_conn_test_error(thr, c, ret, nxt_cyassl_conn_handshake);
|
||||
|
||||
if (n == NXT_ERROR) {
|
||||
nxt_cyassl_conn_error(c, err, "CyaSSL_negotiate(%d) failed",
|
||||
c->socket.fd);
|
||||
|
||||
nxt_event_conn_io_handle(thr, c->read_work_queue,
|
||||
c->read_state->error_handler, c, data);
|
||||
|
||||
} else if (ssltls->ssl_error == SSL_ERROR_WANT_READ && ssltls->times < 2) {
|
||||
ssltls->times++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
nxt_cyassl_conn_io_read(nxt_thread_t *thr, void *obj, void *data)
|
||||
{
|
||||
int ret;
|
||||
nxt_buf_t *b;
|
||||
nxt_err_t err;
|
||||
nxt_int_t n;
|
||||
nxt_event_conn_t *c;
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
nxt_work_handler_t handler;
|
||||
|
||||
c = obj;
|
||||
|
||||
nxt_log_debug(thr->log, "cyassl conn read");
|
||||
|
||||
handler = c->read_state->ready_handler;
|
||||
b = c->read;
|
||||
|
||||
/* b == NULL is used to test descriptor readiness. */
|
||||
|
||||
if (b != NULL) {
|
||||
ssltls = c->u.ssltls;
|
||||
|
||||
ret = CyaSSL_read(ssltls->session, b->mem.free,
|
||||
b->mem.end - b->mem.free);
|
||||
|
||||
err = (ret <= 0) ? nxt_socket_errno : 0;
|
||||
|
||||
nxt_log_debug(thr->log, "CyaSSL_read(%d, %p, %uz): %d",
|
||||
c->socket.fd, b->mem.free, b->mem.end - b->mem.free, ret);
|
||||
|
||||
if (ret > 0) {
|
||||
/* c->socket.read_ready is kept. */
|
||||
b->mem.free += ret;
|
||||
handler = c->read_state->ready_handler;
|
||||
|
||||
} else {
|
||||
n = nxt_cyassl_conn_test_error(thr, c, ret,
|
||||
nxt_cyassl_conn_io_read);
|
||||
|
||||
if (nxt_fast_path(n != NXT_ERROR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nxt_cyassl_conn_error(c, err, "CyaSSL_read(%d, %p, %uz) failed",
|
||||
c->socket.fd, b->mem.free,
|
||||
b->mem.end - b->mem.free);
|
||||
|
||||
handler = c->read_state->error_handler;
|
||||
}
|
||||
}
|
||||
|
||||
nxt_event_conn_io_handle(thr, c->read_work_queue, handler, c, data);
|
||||
}
|
||||
|
||||
|
||||
static ssize_t
|
||||
nxt_cyassl_conn_io_write_chunk(nxt_thread_t *thr, nxt_event_conn_t *c,
|
||||
nxt_buf_t *b, size_t limit)
|
||||
{
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
|
||||
nxt_log_debug(thr->log, "cyassl conn write chunk");
|
||||
|
||||
ssltls = c->u.ssltls;
|
||||
|
||||
return nxt_sendbuf_copy_coalesce(c, &ssltls->buffer, b, limit);
|
||||
}
|
||||
|
||||
|
||||
static ssize_t
|
||||
nxt_cyassl_conn_io_send(nxt_event_conn_t *c, void *buf, size_t size)
|
||||
{
|
||||
int ret;
|
||||
nxt_err_t err;
|
||||
nxt_int_t n;
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
|
||||
nxt_log_debug(c->socket.log, "cyassl send");
|
||||
|
||||
ssltls = c->u.ssltls;
|
||||
|
||||
ret = CyaSSL_write(ssltls->session, buf, size);
|
||||
|
||||
if (ret <= 0) {
|
||||
err = nxt_socket_errno;
|
||||
c->socket.error = err;
|
||||
|
||||
} else {
|
||||
err = 0;
|
||||
}
|
||||
|
||||
nxt_log_debug(c->socket.log, "CyaSSL_write(%d, %p, %uz): %d",
|
||||
c->socket.fd, buf, size, ret);
|
||||
|
||||
if (ret > 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
n = nxt_cyassl_conn_test_error(nxt_thread(), c, ret,
|
||||
nxt_event_conn_io_write);
|
||||
|
||||
if (nxt_slow_path(n == NXT_ERROR)) {
|
||||
nxt_cyassl_conn_error(c, err, "CyaSSL_write(%d, %p, %uz) failed",
|
||||
c->socket.fd, buf, size);
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
nxt_cyassl_conn_io_shutdown(nxt_thread_t *thr, void *obj, void *data)
|
||||
{
|
||||
int ret;
|
||||
nxt_event_conn_t *c;
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
|
||||
c = obj;
|
||||
|
||||
nxt_log_debug(thr->log, "cyassl conn shutdown");
|
||||
|
||||
ssltls = c->u.ssltls;
|
||||
|
||||
ret = CyaSSL_shutdown(ssltls->session);
|
||||
|
||||
nxt_log_debug(thr->log, "CyaSSL_shutdown(%d): %d", c->socket.fd, ret);
|
||||
|
||||
if (nxt_slow_path(ret != SSL_SUCCESS)) {
|
||||
nxt_cyassl_conn_error(c, 0, "CyaSSL_shutdown(%d) failed", c->socket.fd);
|
||||
}
|
||||
|
||||
nxt_event_conn_io_handle(thr, c->write_work_queue,
|
||||
c->write_state->close_handler, c, data);
|
||||
}
|
||||
|
||||
|
||||
static nxt_int_t
|
||||
nxt_cyassl_conn_test_error(nxt_thread_t *thr, nxt_event_conn_t *c, int ret,
|
||||
nxt_work_handler_t handler)
|
||||
{
|
||||
nxt_work_queue_t *wq;
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
|
||||
ssltls = c->u.ssltls;
|
||||
ssltls->ssl_error = CyaSSL_get_error(ssltls->session, ret);
|
||||
|
||||
nxt_log_debug(thr->log, "CyaSSL_get_error(): %d", ssltls->ssl_error);
|
||||
|
||||
switch (ssltls->ssl_error) {
|
||||
|
||||
case SSL_ERROR_WANT_READ:
|
||||
nxt_event_fd_block_write(thr->engine, &c->socket);
|
||||
|
||||
c->socket.read_ready = 0;
|
||||
c->socket.read_handler = handler;
|
||||
|
||||
if (nxt_event_fd_is_disabled(c->socket.read)) {
|
||||
nxt_event_fd_enable_read(thr->engine, &c->socket);
|
||||
}
|
||||
|
||||
return NXT_AGAIN;
|
||||
|
||||
case SSL_ERROR_WANT_WRITE:
|
||||
nxt_event_fd_block_read(thr->engine, &c->socket);
|
||||
|
||||
c->socket.write_ready = 0;
|
||||
c->socket.write_handler = handler;
|
||||
|
||||
if (nxt_event_fd_is_disabled(c->socket.write)) {
|
||||
nxt_event_fd_enable_write(thr->engine, &c->socket);
|
||||
}
|
||||
|
||||
return NXT_AGAIN;
|
||||
|
||||
case SSL_ERROR_ZERO_RETURN:
|
||||
/* A "close notify" alert */
|
||||
|
||||
if (c->read_state != NULL) {
|
||||
wq = c->read_work_queue;
|
||||
handler = c->read_state->close_handler;
|
||||
|
||||
} else {
|
||||
wq = c->write_work_queue;
|
||||
handler = c->write_state->close_handler;
|
||||
}
|
||||
|
||||
nxt_event_conn_io_handle(thr, wq, handler, c, c->socket.data);
|
||||
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return NXT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void nxt_cdecl
|
||||
nxt_cyassl_conn_error(nxt_event_conn_t *c, nxt_err_t err, const char *fmt, ...)
|
||||
{
|
||||
u_char *p, *end;
|
||||
va_list args;
|
||||
nxt_uint_t level;
|
||||
nxt_cyassl_conn_t *ssltls;
|
||||
u_char msg[NXT_MAX_ERROR_STR];
|
||||
|
||||
ssltls = c->u.ssltls;
|
||||
|
||||
level = nxt_cyassl_log_error_level(c, err, ssltls->ssl_error);
|
||||
|
||||
if (nxt_log_level_enough(c->socket.log, level)) {
|
||||
|
||||
end = msg + sizeof(msg);
|
||||
|
||||
va_start(args, fmt);
|
||||
p = nxt_vsprintf(msg, end, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (err != 0) {
|
||||
p = nxt_sprintf(p, end, " %E", err);
|
||||
}
|
||||
|
||||
p = nxt_cyassl_copy_error(ssltls->ssl_error, p, end);
|
||||
|
||||
nxt_log_error(level, c->socket.log, "%*s", p - msg, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static nxt_uint_t
|
||||
nxt_cyassl_log_error_level(nxt_event_conn_t *c, nxt_err_t err, int ssl_error)
|
||||
{
|
||||
switch (ssl_error) {
|
||||
|
||||
case SOCKET_ERROR_E: /* -208 */
|
||||
case MATCH_SUITE_ERROR: /* -261 */
|
||||
break;
|
||||
|
||||
default:
|
||||
return NXT_LOG_CRIT;
|
||||
}
|
||||
|
||||
return NXT_LOG_INFO;
|
||||
}
|
||||
|
||||
|
||||
static void nxt_cdecl
|
||||
nxt_cyassl_log_error(nxt_uint_t level, nxt_log_t *log, int err,
|
||||
const char *fmt, ...)
|
||||
{
|
||||
u_char *p, *end;
|
||||
va_list args;
|
||||
u_char msg[NXT_MAX_ERROR_STR];
|
||||
|
||||
if (nxt_log_level_enough(log, level)) {
|
||||
|
||||
end = msg + sizeof(msg);
|
||||
|
||||
va_start(args, fmt);
|
||||
p = nxt_vsprintf(msg, end, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
p = nxt_cyassl_copy_error(err, p, end);
|
||||
|
||||
nxt_log_error(level, log, "%*s", p - msg, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static u_char *
|
||||
nxt_cyassl_copy_error(int err, u_char *p, u_char *end)
|
||||
{
|
||||
p = nxt_sprintf(p, end, " (SSL:%d ", err);
|
||||
|
||||
CyaSSL_ERR_error_string_n(err, (char *) p, end - p);
|
||||
|
||||
p += nxt_strlen(p);
|
||||
|
||||
if (p < end) {
|
||||
*p++ = ')';
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
Reference in New Issue
Block a user