mirror of
https://github.com/warmcat/libwebsockets.git
synced 2025-03-16 00:00:07 +01:00

Thanks to Fabrice Gilot for reporting the problem that led to uncovering this. Due to a misunderstanding of the return value of snprintf (it is not truncated according to the max size passed in) in several places relying on snprintf to truncate the length overflows are possible. This patch wraps snprintf with a new lws_snprintf() which does truncate its length to allow the buffer limiting scheme to work properly. All users should update with these fixes.
692 lines
14 KiB
C
692 lines
14 KiB
C
#include "private-libwebsockets.h"
|
|
|
|
#include "ip_addr.h"
|
|
|
|
/* forced into this because new espconn accepted callbacks carry no context ptr */
|
|
static struct lws_context *hacky_context;
|
|
static unsigned int time_high, ot;
|
|
|
|
/*
|
|
* included from libwebsockets.c for esp8266 builds
|
|
*/
|
|
|
|
unsigned long long time_in_microseconds(void)
|
|
{
|
|
unsigned int t = system_get_time();
|
|
|
|
if (ot > t)
|
|
time_high++;
|
|
ot = t;
|
|
|
|
return (((long long)time_high) << 32) | t;
|
|
}
|
|
|
|
int gettimeofday(struct timeval *tv, void *tz)
|
|
{
|
|
unsigned long long t = time_in_microseconds();
|
|
|
|
tv->tv_sec = t / 1000000;
|
|
tv->tv_usec = t % 1000000;
|
|
|
|
return 0;
|
|
}
|
|
|
|
time_t time(time_t *tloc)
|
|
{
|
|
unsigned long long t = time_in_microseconds();
|
|
|
|
if (tloc)
|
|
*tloc = t / 1000000;
|
|
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_get_random(struct lws_context *context, void *buf, int len)
|
|
{
|
|
// return read(context->fd_random, (char *)buf, len);
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_send_pipe_choked(struct lws *wsi)
|
|
{
|
|
return wsi->pending_send_completion;
|
|
}
|
|
|
|
LWS_VISIBLE struct lws *
|
|
wsi_from_fd(const struct lws_context *context, lws_sockfd_type fd)
|
|
{
|
|
int n;
|
|
|
|
for (n = 0; n < context->max_fds; n++)
|
|
if (context->connpool[n] == fd)
|
|
return (struct lws *)context->connpool[n + context->max_fds];
|
|
|
|
return NULL;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_ssl_capable_write_no_ssl(struct lws *wsi, unsigned char *buf, int len)
|
|
{
|
|
//lwsl_notice("%s: wsi %p: len %d\n", __func__, wsi, len);
|
|
|
|
wsi->pending_send_completion++;
|
|
espconn_send(wsi->sock, buf, len);
|
|
|
|
return len;
|
|
}
|
|
|
|
void abort(void)
|
|
{
|
|
while(1) ;
|
|
}
|
|
|
|
void exit(int n)
|
|
{
|
|
abort();
|
|
}
|
|
|
|
void _sint(void *s)
|
|
{
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
insert_wsi(struct lws_context *context, struct lws *wsi)
|
|
{
|
|
(void)context;
|
|
(void)wsi;
|
|
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
delete_from_fd(struct lws_context *context, lws_sockfd_type fd)
|
|
{
|
|
(void)context;
|
|
(void)fd;
|
|
|
|
return 1;
|
|
}
|
|
|
|
struct tm *localtime(const time_t *timep)
|
|
{
|
|
return NULL;
|
|
}
|
|
struct tm *localtime_r(const time_t *timep, struct tm *t)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
int atoi(const char *s)
|
|
{
|
|
int n = 0;
|
|
|
|
while (*s && (*s >= '0' && *s <= '9'))
|
|
n = (n * 10) + ((*s++) - '0');
|
|
|
|
return n;
|
|
}
|
|
|
|
#undef isxdigit
|
|
int isxdigit(int c)
|
|
{
|
|
if (c >= 'A' && c <= 'F')
|
|
return 1;
|
|
|
|
if (c >= 'a' && c <= 'f')
|
|
return 1;
|
|
|
|
if (c >= '0' && c <= '9')
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int strcasecmp(const char *s1, const char *s2)
|
|
{
|
|
char a, b;
|
|
while (*s1 && *s2) {
|
|
a = *s1++;
|
|
b = *s2++;
|
|
|
|
if (a == b)
|
|
continue;
|
|
|
|
if (a >= 'a' && a <= 'z')
|
|
a -= 'a' - 'A';
|
|
if (b >= 'a' && b <= 'z')
|
|
b -= 'a' - 'A';
|
|
|
|
if (a != b)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_poll_listen_fd(struct lws_pollfd *fd)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_cancel_service_pt(struct lws *wsi)
|
|
{
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_cancel_service(struct lws_context *context)
|
|
{
|
|
}
|
|
|
|
LWS_VISIBLE void lwsl_emit_syslog(int level, const char *line)
|
|
{
|
|
extern void output_redirect(const char *str);
|
|
output_redirect(line);
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_plat_check_connection_error(struct lws *wsi)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_plat_service(struct lws_context *context, int timeout_ms)
|
|
{
|
|
// return lws_plat_service_tsi(context, timeout_ms, 0);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
esp8266_find_free_conn(struct lws_context *context)
|
|
{
|
|
int n;
|
|
|
|
for (n = 0; n < context->max_fds; n++)
|
|
if (!context->connpool[n]) {
|
|
lwsl_info(" using connpool %d\n", n);
|
|
return n;
|
|
}
|
|
|
|
lwsl_err("%s: no free conns\n", __func__);
|
|
|
|
return -1;
|
|
}
|
|
|
|
lws_sockfd_type
|
|
esp8266_create_tcp_listen_socket(struct lws_vhost *vh)
|
|
{
|
|
int n = esp8266_find_free_conn(vh->context);
|
|
struct espconn *conn;
|
|
|
|
if (n < 0)
|
|
return NULL;
|
|
|
|
conn = lws_zalloc(sizeof *conn);
|
|
if (!conn)
|
|
return NULL;
|
|
|
|
vh->context->connpool[n] = conn;
|
|
|
|
conn->type = ESPCONN_TCP;
|
|
conn->state = ESPCONN_NONE;
|
|
conn->proto.tcp = &vh->tcp;
|
|
|
|
return conn;
|
|
}
|
|
|
|
const char *
|
|
lws_plat_get_peer_simple(struct lws *wsi, char *name, int namelen)
|
|
{
|
|
unsigned char *p = wsi->sock->proto.tcp->remote_ip;
|
|
|
|
lws_snprintf(name, namelen, "%u.%u.%u.%u", p[0], p[1], p[2], p[3]);
|
|
|
|
return name;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, int len)
|
|
{
|
|
//lwsl_notice("%s\n", __func__);
|
|
|
|
if (!wsi->context->rxd)
|
|
return 0;
|
|
|
|
if (len < wsi->context->rxd_len)
|
|
lwsl_err("trunc read (%d vs %d)\n", len, wsi->context->rxd_len);
|
|
else
|
|
len = wsi->context->rxd_len;
|
|
|
|
ets_memcpy(buf, wsi->context->rxd, len);
|
|
|
|
wsi->context->rxd = NULL;
|
|
|
|
return len;
|
|
}
|
|
|
|
static void
|
|
cb_1Hz(void *arg)
|
|
{
|
|
struct lws_context *context = arg;
|
|
struct lws_context_per_thread *pt = &context->pt[0];
|
|
struct lws *wsi;
|
|
struct lws_pollfd *pollfd;
|
|
int n;
|
|
|
|
/* Service any ah that has pending rx */
|
|
for (n = 0; n < context->max_http_header_pool; n++)
|
|
if (pt->ah_pool[n].rxpos != pt->ah_pool[n].rxlen) {
|
|
wsi = pt->ah_pool[n].wsi;
|
|
pollfd = &pt->fds[wsi->position_in_fds_table];
|
|
if (pollfd->events & LWS_POLLIN) {
|
|
pollfd->revents |= LWS_POLLIN;
|
|
lws_service_fd(context, pollfd);
|
|
}
|
|
}
|
|
|
|
/* handle timeouts */
|
|
|
|
lws_service_fd(context, NULL);
|
|
}
|
|
|
|
static void
|
|
esp8266_cb_rx(void *arg, char *data, unsigned short len)
|
|
{
|
|
struct espconn *conn = arg;
|
|
struct lws *wsi = conn->reverse;
|
|
struct lws_context_per_thread *pt = &wsi->context->pt[0];
|
|
struct lws_pollfd pollfd;
|
|
int n = 0;
|
|
|
|
/*
|
|
* if we're doing HTTP headers, and we have no ah, check if there is
|
|
* a free ah, if not, have to buffer it
|
|
*/
|
|
if (!wsi->hdr_parsing_completed && !wsi->u.hdr.ah) {
|
|
for (n = 0; n < wsi->context->max_http_header_pool; n++)
|
|
if (!pt->ah_pool[n].in_use)
|
|
break;
|
|
|
|
n = n == wsi->context->max_http_header_pool;
|
|
}
|
|
|
|
if (!(pt->fds[wsi->position_in_fds_table].events & LWS_POLLIN) || n) {
|
|
wsi->premature_rx = realloc(wsi->premature_rx,
|
|
wsi->prem_rx_size + len);
|
|
if (!wsi->premature_rx)
|
|
return;
|
|
os_memcpy((char *)wsi->premature_rx + wsi->prem_rx_size, data, len);
|
|
wsi->prem_rx_size += len;
|
|
// lwsl_notice("%s: wsi %p: len %d BUFFERING\n", __func__, wsi, len);
|
|
|
|
if (n) /* we know it will fail, but we will get on the wait list */
|
|
n = lws_header_table_attach(wsi, 0);
|
|
|
|
(void)n;
|
|
return;
|
|
}
|
|
|
|
//lwsl_err("%s: wsi %p. len %d\n", __func__, wsi, len);
|
|
|
|
pollfd.fd = arg;
|
|
pollfd.events = LWS_POLLIN;
|
|
pollfd.revents = LWS_POLLIN;
|
|
|
|
wsi->context->rxd = data;
|
|
wsi->context->rxd_len = len;
|
|
|
|
lws_service_fd(lws_get_context(wsi), &pollfd);
|
|
|
|
}
|
|
|
|
static void
|
|
esp8266_cb_sent(void *arg)
|
|
{
|
|
struct espconn *conn = arg;
|
|
struct lws *wsi = conn->reverse;
|
|
struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
|
|
|
|
// lwsl_err("%s: wsi %p (psc %d) wsi->position_in_fds_table=%d\n", __func__, wsi, wsi->pending_send_completion, wsi->position_in_fds_table);
|
|
|
|
wsi->pending_send_completion--;
|
|
if (wsi->close_is_pending_send_completion &&
|
|
!wsi->pending_send_completion &&
|
|
!lws_partial_buffered(wsi)) {
|
|
lwsl_notice("doing delayed close\n");
|
|
lws_close_free_wsi(wsi, LWS_CLOSE_STATUS_NOSTATUS);
|
|
}
|
|
|
|
if (pt->fds[wsi->position_in_fds_table].events & LWS_POLLOUT) {
|
|
struct lws_pollfd pollfd;
|
|
|
|
pollfd.fd = arg;
|
|
pollfd.events = LWS_POLLOUT;
|
|
pollfd.revents = LWS_POLLOUT;
|
|
|
|
// lwsl_notice("informing POLLOUT\n");
|
|
|
|
lws_service_fd(lws_get_context(wsi), &pollfd);
|
|
}
|
|
}
|
|
|
|
static void
|
|
esp8266_cb_disconnected(void *arg)
|
|
{
|
|
struct espconn *conn = arg;
|
|
struct lws *wsi = conn->reverse;
|
|
int n;
|
|
|
|
lwsl_notice("%s: %p\n", __func__, wsi);
|
|
|
|
for (n = 0; n < hacky_context->max_fds; n++)
|
|
if (hacky_context->connpool[n] == arg) {
|
|
hacky_context->connpool[n] = NULL;
|
|
lwsl_info(" freed connpool %d\n", n);
|
|
}
|
|
|
|
if (wsi) {
|
|
conn->reverse = NULL;
|
|
lws_close_free_wsi(wsi, LWS_CLOSE_STATUS_NOSTATUS);
|
|
lwsl_notice("closed ok\n");
|
|
}
|
|
}
|
|
|
|
static void
|
|
esp8266_cb_recon(void *arg, signed char err)
|
|
{
|
|
struct espconn *conn = arg;
|
|
|
|
lwsl_err("%s: wsi %p. err %d\n", __func__, conn->reverse, err);
|
|
|
|
conn->state = ESPCONN_CLOSE;
|
|
|
|
esp8266_cb_disconnected(arg);
|
|
}
|
|
|
|
/*
|
|
* there is no reliable indication of which listen socket we were accepted on.
|
|
*/
|
|
|
|
static void
|
|
esp8266_cb_connect(void *arg)
|
|
{
|
|
struct espconn *cs = arg;
|
|
// struct ip_addr *ipa = (struct ip_addr *)cs->proto.tcp->remote_ip;
|
|
struct lws_vhost *vh = hacky_context->vhost_list;
|
|
// struct ip_info info;
|
|
struct lws *wsi;
|
|
int n;
|
|
|
|
lwsl_notice("%s: (wsi coming): %p\n", __func__, cs->reverse);
|
|
#if 0
|
|
wifi_get_ip_info(0, &info);
|
|
if (ip_addr_netcmp(ipa, &info.ip, &info.netmask)) {
|
|
/* we are on the same subnet as the AP, ie, connected to AP */
|
|
while (vh && strcmp(vh->name, "ap"))
|
|
vh = vh->vhost_next;
|
|
} else
|
|
while (vh && !strcmp(vh->name, "ap"))
|
|
vh = vh->vhost_next;
|
|
|
|
if (!vh)
|
|
goto bail;
|
|
#endif
|
|
n = esp8266_find_free_conn(hacky_context);
|
|
if (n < 0)
|
|
goto bail;
|
|
|
|
hacky_context->connpool[n] = cs;
|
|
|
|
espconn_recv_hold(cs);
|
|
|
|
wsi = lws_adopt_socket_vhost(vh, cs);
|
|
if (!wsi)
|
|
goto bail;
|
|
|
|
lwsl_err("%s: wsi %p (using free_conn %d): vh %s\n", __func__, wsi, n, vh->name);
|
|
|
|
espconn_regist_recvcb(cs, esp8266_cb_rx);
|
|
espconn_regist_reconcb(cs, esp8266_cb_recon);
|
|
espconn_regist_disconcb(cs, esp8266_cb_disconnected);
|
|
espconn_regist_sentcb(cs, esp8266_cb_sent);
|
|
|
|
espconn_set_opt(cs, ESPCONN_NODELAY | ESPCONN_REUSEADDR);
|
|
espconn_regist_time(cs, 7200, 1);
|
|
|
|
return;
|
|
|
|
bail:
|
|
lwsl_err("%s: bailed]n", __func__);
|
|
espconn_disconnect(cs);
|
|
}
|
|
|
|
void
|
|
esp8266_tcp_stream_bind(lws_sockfd_type fd, int port, struct lws *wsi)
|
|
{
|
|
fd->proto.tcp->local_port = port;
|
|
fd->reverse = wsi;
|
|
|
|
hacky_context = wsi->context;
|
|
|
|
espconn_regist_connectcb(fd, esp8266_cb_connect);
|
|
/* hmmm it means, listen() + accept() */
|
|
espconn_accept(fd);
|
|
|
|
espconn_tcp_set_max_con_allow(fd, 10);
|
|
}
|
|
|
|
void
|
|
esp8266_tcp_stream_accept(lws_sockfd_type fd, struct lws *wsi)
|
|
{
|
|
int n;
|
|
|
|
fd->reverse = wsi;
|
|
|
|
for (n = 0; n < wsi->context->max_fds ; n++)
|
|
if (wsi->context->connpool[n] == wsi->sock)
|
|
wsi->position_in_fds_table = n;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_plat_set_socket_options(struct lws_vhost *vhost, lws_sockfd_type fd)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_plat_drop_app_privileges(struct lws_context_creation_info *info)
|
|
{
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_plat_context_early_init(void)
|
|
{
|
|
espconn_tcp_set_max_con(12);
|
|
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_plat_context_early_destroy(struct lws_context *context)
|
|
{
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_plat_context_late_destroy(struct lws_context *context)
|
|
{
|
|
#if 0
|
|
struct lws_context_per_thread *pt = &context->pt[0];
|
|
int m = context->count_threads;
|
|
|
|
if (context->lws_lookup)
|
|
lws_free(context->lws_lookup);
|
|
|
|
while (m--) {
|
|
close(pt->dummy_pipe_fds[0]);
|
|
close(pt->dummy_pipe_fds[1]);
|
|
pt++;
|
|
}
|
|
#endif
|
|
// close(context->fd_random);
|
|
}
|
|
|
|
/* cast a struct sockaddr_in6 * into addr for ipv6 */
|
|
|
|
LWS_VISIBLE int
|
|
lws_interface_to_sa(int ipv6, const char *ifname, struct sockaddr_in *addr,
|
|
size_t addrlen)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_plat_insert_socket_into_fds(struct lws_context *context, struct lws *wsi)
|
|
{
|
|
struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
|
|
|
|
context->connpool[wsi->position_in_fds_table + context->max_fds] = (lws_sockfd_type)wsi;
|
|
wsi->sock->reverse = wsi;
|
|
pt->fds_count++;
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_plat_delete_socket_from_fds(struct lws_context *context,
|
|
struct lws *wsi, int m)
|
|
{
|
|
struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
|
|
int n;
|
|
|
|
for (n = 0; n < wsi->context->max_fds; n++)
|
|
if (wsi->context->connpool[n] == wsi->sock) {
|
|
wsi->context->connpool[n] = NULL;
|
|
wsi->context->connpool[n + wsi->context->max_fds] = NULL;
|
|
lwsl_notice(" freed connpool %d\n", n);
|
|
}
|
|
|
|
wsi->sock->reverse = NULL;
|
|
pt->fds_count--;
|
|
}
|
|
|
|
LWS_VISIBLE void
|
|
lws_plat_service_periodic(struct lws_context *context)
|
|
{
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_plat_change_pollfd(struct lws_context *context,
|
|
struct lws *wsi, struct lws_pollfd *pfd)
|
|
{
|
|
void *p;
|
|
|
|
//lwsl_notice("%s: %p: wsi->pift=%d, events %d\n",
|
|
// __func__, wsi, wsi->position_in_fds_table, pfd->events);
|
|
|
|
if (pfd->events & LWS_POLLIN) {
|
|
if (wsi->premature_rx) {
|
|
lwsl_notice("replaying buffered rx: wsi %p\n", wsi);
|
|
p = wsi->premature_rx;
|
|
wsi->premature_rx = NULL;
|
|
esp8266_cb_rx(wsi->sock,
|
|
(char *)p + wsi->prem_rx_pos,
|
|
wsi->prem_rx_size - wsi->prem_rx_pos);
|
|
wsi->prem_rx_size = 0;
|
|
wsi->prem_rx_pos = 0;
|
|
lws_free(p);
|
|
}
|
|
if (espconn_recv_unhold(wsi->sock) < 0)
|
|
return -1;
|
|
} else
|
|
if (espconn_recv_hold(wsi->sock) < 0)
|
|
return -1;
|
|
|
|
if (!(pfd->events & LWS_POLLOUT))
|
|
return 0;
|
|
|
|
if (!wsi->pending_send_completion) {
|
|
pfd->revents |= LWS_POLLOUT;
|
|
|
|
// lwsl_notice("doing POLLOUT\n");
|
|
lws_service_fd(lws_get_context(wsi), pfd);
|
|
} //else
|
|
//lwsl_notice("pending sc\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE const char *
|
|
lws_plat_inet_ntop(int af, const void *src, char *dst, int cnt)
|
|
{
|
|
// return inet_ntop(af, src, dst, cnt);
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
lws_plat_init(struct lws_context *context,
|
|
struct lws_context_creation_info *info)
|
|
{
|
|
// struct lws_context_per_thread *pt = &context->pt[0];
|
|
// int n = context->count_threads, fd;
|
|
|
|
/* master context has the global fd lookup array */
|
|
context->connpool = lws_zalloc(sizeof(struct espconn *) *
|
|
context->max_fds * 2);
|
|
if (context->connpool == NULL) {
|
|
lwsl_err("OOM on lws_lookup array for %d connections\n",
|
|
context->max_fds);
|
|
return 1;
|
|
}
|
|
|
|
lwsl_notice(" mem: platform fd map: %5u bytes\n",
|
|
sizeof(struct espconn *) * context->max_fds);
|
|
// fd = open(SYSTEM_RANDOM_FILEPATH, O_RDONLY);
|
|
|
|
// context->fd_random = fd;
|
|
// if (context->fd_random < 0) {
|
|
// lwsl_err("Unable to open random device %s %d\n",
|
|
// SYSTEM_RANDOM_FILEPATH, context->fd_random);
|
|
// return 1;
|
|
// }
|
|
|
|
os_memset(&context->to_timer, 0, sizeof(os_timer_t));
|
|
os_timer_disarm(&context->to_timer);
|
|
os_timer_setfn(&context->to_timer, (os_timer_func_t *)cb_1Hz, context);
|
|
os_timer_arm(&context->to_timer, 1000, 1);
|
|
|
|
if (!lws_libev_init_fd_table(context) &&
|
|
!lws_libuv_init_fd_table(context)) {
|
|
/* otherwise libev handled it instead */
|
|
#if 0
|
|
while (n--) {
|
|
if (pipe(pt->dummy_pipe_fds)) {
|
|
lwsl_err("Unable to create pipe\n");
|
|
return 1;
|
|
}
|
|
|
|
/* use the read end of pipe as first item */
|
|
pt->fds[0].fd = pt->dummy_pipe_fds[0];
|
|
pt->fds[0].events = LWS_POLLIN;
|
|
pt->fds[0].revents = 0;
|
|
pt->fds_count = 1;
|
|
pt++;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
#ifdef LWS_WITH_PLUGINS
|
|
if (info->plugin_dirs)
|
|
lws_plat_plugins_init(context, info->plugin_dirs);
|
|
#endif
|
|
|
|
return 0;
|
|
}
|