2018-06-07 11:33:24 +00:00
|
|
|
/*
|
|
|
|
* libwebsockets - small server side websockets and web server implementation
|
|
|
|
*
|
|
|
|
* Copyright (C) 2010-2017 Andy Green <andy@warmcat.com>
|
|
|
|
*
|
|
|
|
* This library is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
|
|
* License as published by the Free Software Foundation:
|
|
|
|
* version 2.1 of the License.
|
|
|
|
*
|
|
|
|
* This library is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
* Lesser General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
|
|
* License along with this library; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
|
|
* MA 02110-1301 USA
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "core/private.h"
|
|
|
|
|
|
|
|
/*
|
|
|
|
* fakes POLLIN on all tls guys with buffered rx
|
|
|
|
*
|
|
|
|
* returns nonzero if any tls guys had POLLIN faked
|
|
|
|
*/
|
|
|
|
|
|
|
|
int
|
|
|
|
lws_tls_fake_POLLIN_for_buffered(struct lws_context_per_thread *pt)
|
|
|
|
{
|
2019-04-01 00:14:42 +00:00
|
|
|
struct lws *wsi, *wsi_next;
|
2018-06-07 11:33:24 +00:00
|
|
|
int ret = 0;
|
|
|
|
|
2019-04-01 00:14:42 +00:00
|
|
|
wsi = pt->tls.pending_read_list;
|
|
|
|
while (wsi && wsi->position_in_fds_table != LWS_NO_FDS_POS) {
|
|
|
|
wsi_next = wsi->tls.pending_read_list_next;
|
2018-06-07 11:33:24 +00:00
|
|
|
pt->fds[wsi->position_in_fds_table].revents |=
|
|
|
|
pt->fds[wsi->position_in_fds_table].events & LWS_POLLIN;
|
|
|
|
ret |= pt->fds[wsi->position_in_fds_table].revents & LWS_POLLIN;
|
|
|
|
|
2019-04-01 00:14:42 +00:00
|
|
|
wsi = wsi_next;
|
|
|
|
}
|
2018-06-07 11:33:24 +00:00
|
|
|
|
|
|
|
return !!ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
__lws_ssl_remove_wsi_from_buffered_list(struct lws *wsi)
|
|
|
|
{
|
2019-04-01 00:14:42 +00:00
|
|
|
struct lws_context *context = wsi->context;
|
|
|
|
struct lws_context_per_thread *pt = &context->pt[(int)wsi->tsi];
|
|
|
|
|
|
|
|
if (!wsi->tls.pending_read_list_prev &&
|
|
|
|
!wsi->tls.pending_read_list_next &&
|
|
|
|
pt->tls.pending_read_list != wsi)
|
|
|
|
/* we are not on the list */
|
2018-06-07 11:33:24 +00:00
|
|
|
return;
|
|
|
|
|
2019-04-01 00:14:42 +00:00
|
|
|
/* point previous guy's next to our next */
|
|
|
|
if (!wsi->tls.pending_read_list_prev)
|
|
|
|
pt->tls.pending_read_list = wsi->tls.pending_read_list_next;
|
|
|
|
else
|
|
|
|
wsi->tls.pending_read_list_prev->tls.pending_read_list_next =
|
|
|
|
wsi->tls.pending_read_list_next;
|
|
|
|
|
|
|
|
/* point next guy's previous to our previous */
|
|
|
|
if (wsi->tls.pending_read_list_next)
|
|
|
|
wsi->tls.pending_read_list_next->tls.pending_read_list_prev =
|
|
|
|
wsi->tls.pending_read_list_prev;
|
|
|
|
|
|
|
|
wsi->tls.pending_read_list_prev = NULL;
|
|
|
|
wsi->tls.pending_read_list_next = NULL;
|
2018-06-07 11:33:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
lws_ssl_remove_wsi_from_buffered_list(struct lws *wsi)
|
|
|
|
{
|
|
|
|
struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
|
|
|
|
|
|
|
|
lws_pt_lock(pt, __func__);
|
|
|
|
__lws_ssl_remove_wsi_from_buffered_list(wsi);
|
|
|
|
lws_pt_unlock(pt);
|
|
|
|
}
|
|
|
|
|
|
|
|
#if defined(LWS_WITH_ESP32)
|
|
|
|
int alloc_file(struct lws_context *context, const char *filename, uint8_t **buf,
|
|
|
|
lws_filepos_t *amount)
|
|
|
|
{
|
|
|
|
nvs_handle nvh;
|
|
|
|
size_t s;
|
|
|
|
int n = 0;
|
|
|
|
|
|
|
|
ESP_ERROR_CHECK(nvs_open("lws-station", NVS_READWRITE, &nvh));
|
|
|
|
if (nvs_get_blob(nvh, filename, NULL, &s) != ESP_OK) {
|
|
|
|
n = 1;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
*buf = lws_malloc(s + 1, "alloc_file");
|
|
|
|
if (!*buf) {
|
|
|
|
n = 2;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
if (nvs_get_blob(nvh, filename, (char *)*buf, &s) != ESP_OK) {
|
|
|
|
lws_free(*buf);
|
|
|
|
n = 1;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
|
|
|
|
*amount = s;
|
|
|
|
(*buf)[s] = '\0';
|
|
|
|
|
|
|
|
lwsl_notice("%s: nvs: read %s, %d bytes\n", __func__, filename, (int)s);
|
|
|
|
|
|
|
|
bail:
|
|
|
|
nvs_close(nvh);
|
|
|
|
|
|
|
|
return n;
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
int alloc_file(struct lws_context *context, const char *filename, uint8_t **buf,
|
|
|
|
lws_filepos_t *amount)
|
|
|
|
{
|
|
|
|
FILE *f;
|
|
|
|
size_t s;
|
|
|
|
int n = 0;
|
|
|
|
|
|
|
|
f = fopen(filename, "rb");
|
|
|
|
if (f == NULL) {
|
|
|
|
n = 1;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fseek(f, 0, SEEK_END) != 0) {
|
|
|
|
n = 1;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
|
|
|
|
s = ftell(f);
|
|
|
|
if (s == (size_t)-1) {
|
|
|
|
n = 1;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fseek(f, 0, SEEK_SET) != 0) {
|
|
|
|
n = 1;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
|
|
|
|
*buf = lws_malloc(s, "alloc_file");
|
|
|
|
if (!*buf) {
|
|
|
|
n = 2;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fread(*buf, s, 1, f) != 1) {
|
|
|
|
lws_free(*buf);
|
|
|
|
n = 1;
|
|
|
|
goto bail;
|
|
|
|
}
|
|
|
|
|
|
|
|
*amount = s;
|
|
|
|
|
|
|
|
bail:
|
|
|
|
if (f)
|
|
|
|
fclose(f);
|
|
|
|
|
|
|
|
return n;
|
|
|
|
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
int
|
|
|
|
lws_tls_alloc_pem_to_der_file(struct lws_context *context, const char *filename,
|
2019-04-01 00:14:42 +00:00
|
|
|
const char *inbuf, lws_filepos_t inlen,
|
|
|
|
uint8_t **buf, lws_filepos_t *amount)
|
2018-06-07 11:33:24 +00:00
|
|
|
{
|
|
|
|
const uint8_t *pem, *p, *end;
|
2019-03-06 00:07:13 +00:00
|
|
|
uint8_t *q;
|
2019-04-01 00:14:42 +00:00
|
|
|
lws_filepos_t len;
|
2018-06-07 11:33:24 +00:00
|
|
|
int n;
|
|
|
|
|
|
|
|
if (filename) {
|
|
|
|
n = alloc_file(context, filename, (uint8_t **)&pem, &len);
|
|
|
|
if (n)
|
|
|
|
return n;
|
|
|
|
} else {
|
|
|
|
pem = (const uint8_t *)inbuf;
|
|
|
|
len = inlen;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* trim the first line */
|
|
|
|
|
|
|
|
p = pem;
|
|
|
|
end = p + len;
|
|
|
|
if (strncmp((char *)p, "-----", 5))
|
|
|
|
goto bail;
|
|
|
|
p += 5;
|
|
|
|
while (p < end && *p != '\n' && *p != '-')
|
|
|
|
p++;
|
|
|
|
|
|
|
|
if (*p != '-')
|
|
|
|
goto bail;
|
|
|
|
|
|
|
|
while (p < end && *p != '\n')
|
|
|
|
p++;
|
|
|
|
|
|
|
|
if (p >= end)
|
|
|
|
goto bail;
|
|
|
|
|
|
|
|
p++;
|
|
|
|
|
|
|
|
/* trim the last line */
|
|
|
|
|
|
|
|
q = (uint8_t *)end - 2;
|
|
|
|
|
|
|
|
while (q > pem && *q != '\n')
|
|
|
|
q--;
|
|
|
|
|
|
|
|
if (*q != '\n')
|
|
|
|
goto bail;
|
|
|
|
|
|
|
|
*q = '\0';
|
|
|
|
|
|
|
|
*amount = lws_b64_decode_string((char *)p, (char *)pem,
|
|
|
|
(int)(long long)len);
|
|
|
|
*buf = (uint8_t *)pem;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
bail:
|
|
|
|
lws_free((uint8_t *)pem);
|
|
|
|
|
|
|
|
return 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
lws_tls_check_cert_lifetime(struct lws_vhost *v)
|
|
|
|
{
|
2019-04-01 00:14:42 +00:00
|
|
|
union lws_tls_cert_info_results ir;
|
2018-06-07 11:33:24 +00:00
|
|
|
time_t now = (time_t)lws_now_secs(), life = 0;
|
|
|
|
struct lws_acme_cert_aging_args caa;
|
|
|
|
int n;
|
|
|
|
|
|
|
|
if (v->tls.ssl_ctx && !v->tls.skipped_certs) {
|
|
|
|
|
2019-04-01 00:14:42 +00:00
|
|
|
if (now < 1464083026) /* May 2016 */
|
2018-06-07 11:33:24 +00:00
|
|
|
/* our clock is wrong and we can't judge the certs */
|
|
|
|
return -1;
|
|
|
|
|
2019-04-01 00:14:42 +00:00
|
|
|
n = lws_tls_vhost_cert_info(v, LWS_TLS_CERT_INFO_VALIDITY_TO, &ir, 0);
|
2018-06-07 11:33:24 +00:00
|
|
|
if (n)
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
life = (ir.time - now) / (24 * 3600);
|
2019-04-01 00:14:42 +00:00
|
|
|
lwsl_notice(" vhost %s: cert expiry: %dd\n", v->name, (int)life);
|
2018-06-07 11:33:24 +00:00
|
|
|
} else
|
|
|
|
lwsl_notice(" vhost %s: no cert\n", v->name);
|
|
|
|
|
|
|
|
memset(&caa, 0, sizeof(caa));
|
|
|
|
caa.vh = v;
|
|
|
|
lws_broadcast(v->context, LWS_CALLBACK_VHOST_CERT_AGING, (void *)&caa,
|
|
|
|
(size_t)(ssize_t)life);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
lws_tls_check_all_cert_lifetimes(struct lws_context *context)
|
|
|
|
{
|
|
|
|
struct lws_vhost *v = context->vhost_list;
|
|
|
|
|
|
|
|
while (v) {
|
|
|
|
if (lws_tls_check_cert_lifetime(v) < 0)
|
|
|
|
return -1;
|
|
|
|
v = v->vhost_next;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
#if !defined(LWS_WITH_ESP32) && !defined(LWS_PLAT_OPTEE)
|
|
|
|
static int
|
|
|
|
lws_tls_extant(const char *name)
|
|
|
|
{
|
|
|
|
/* it exists if we can open it... */
|
|
|
|
int fd = open(name, O_RDONLY), n;
|
|
|
|
char buf[1];
|
|
|
|
|
|
|
|
if (fd < 0)
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
/* and we can read at least one byte out of it */
|
|
|
|
n = read(fd, buf, 1);
|
|
|
|
close(fd);
|
|
|
|
|
|
|
|
return n != 1;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
/*
|
|
|
|
* Returns 0 if the filepath "name" exists and can be read from.
|
|
|
|
*
|
|
|
|
* In addition, if "name".upd exists, backup "name" to "name.old.1"
|
|
|
|
* and rename "name".upd to "name" before reporting its existence.
|
|
|
|
*
|
|
|
|
* There are four situations and three results possible:
|
|
|
|
*
|
|
|
|
* 1) LWS_TLS_EXTANT_NO: There are no certs at all (we are waiting for them to
|
|
|
|
* be provisioned). We also feel like this if we need privs we don't have
|
|
|
|
* any more to look in the directory.
|
|
|
|
*
|
|
|
|
* 2) There are provisioned certs written (xxx.upd) and we still have root
|
|
|
|
* privs... in this case we rename any existing cert to have a backup name
|
|
|
|
* and move the upd cert into place with the correct name. This then becomes
|
|
|
|
* situation 4 for the caller.
|
|
|
|
*
|
|
|
|
* 3) LWS_TLS_EXTANT_ALTERNATIVE: There are provisioned certs written (xxx.upd)
|
|
|
|
* but we no longer have the privs needed to read or rename them. In this
|
|
|
|
* case, indicate that the caller should use temp copies if any we do have
|
|
|
|
* rights to access. This is normal after we have updated the cert.
|
|
|
|
*
|
|
|
|
* But if we dropped privs, we can't detect the provisioned xxx.upd cert +
|
|
|
|
* key, because we can't see in the dir. So we have to upgrade NO to
|
|
|
|
* ALTERNATIVE when we actually have the in-memory alternative.
|
|
|
|
*
|
|
|
|
* 4) LWS_TLS_EXTANT_YES: The certs are present with the correct name and we
|
|
|
|
* have the rights to read them.
|
|
|
|
*/
|
|
|
|
enum lws_tls_extant
|
|
|
|
lws_tls_use_any_upgrade_check_extant(const char *name)
|
|
|
|
{
|
|
|
|
#if !defined(LWS_PLAT_OPTEE)
|
|
|
|
|
|
|
|
int n;
|
|
|
|
|
|
|
|
#if !defined(LWS_WITH_ESP32)
|
|
|
|
char buf[256];
|
|
|
|
|
|
|
|
lws_snprintf(buf, sizeof(buf) - 1, "%s.upd", name);
|
|
|
|
if (!lws_tls_extant(buf)) {
|
|
|
|
/* ah there is an updated file... how about the desired file? */
|
|
|
|
if (!lws_tls_extant(name)) {
|
|
|
|
/* rename the desired file */
|
|
|
|
for (n = 0; n < 50; n++) {
|
|
|
|
lws_snprintf(buf, sizeof(buf) - 1,
|
|
|
|
"%s.old.%d", name, n);
|
|
|
|
if (!rename(name, buf))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (n == 50) {
|
|
|
|
lwsl_notice("unable to rename %s\n", name);
|
|
|
|
|
|
|
|
return LWS_TLS_EXTANT_ALTERNATIVE;
|
|
|
|
}
|
|
|
|
lws_snprintf(buf, sizeof(buf) - 1, "%s.upd", name);
|
|
|
|
}
|
|
|
|
/* desired file is out of the way, rename the updated file */
|
|
|
|
if (rename(buf, name)) {
|
|
|
|
lwsl_notice("unable to rename %s to %s\n", buf, name);
|
|
|
|
|
|
|
|
return LWS_TLS_EXTANT_ALTERNATIVE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lws_tls_extant(name))
|
|
|
|
return LWS_TLS_EXTANT_NO;
|
|
|
|
#else
|
|
|
|
nvs_handle nvh;
|
|
|
|
size_t s = 8192;
|
|
|
|
|
|
|
|
if (nvs_open("lws-station", NVS_READWRITE, &nvh)) {
|
|
|
|
lwsl_notice("%s: can't open nvs\n", __func__);
|
|
|
|
return LWS_TLS_EXTANT_NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
n = nvs_get_blob(nvh, name, NULL, &s);
|
|
|
|
nvs_close(nvh);
|
|
|
|
|
|
|
|
if (n)
|
|
|
|
return LWS_TLS_EXTANT_NO;
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
return LWS_TLS_EXTANT_YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* LWS_TLS_EXTANT_NO : skip adding the cert
|
|
|
|
* LWS_TLS_EXTANT_YES : use the cert and private key paths normally
|
|
|
|
* LWS_TLS_EXTANT_ALTERNATIVE: normal paths not usable, try alternate if poss
|
|
|
|
*/
|
|
|
|
enum lws_tls_extant
|
|
|
|
lws_tls_generic_cert_checks(struct lws_vhost *vhost, const char *cert,
|
|
|
|
const char *private_key)
|
|
|
|
{
|
|
|
|
int n, m;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The user code can choose to either pass the cert and
|
|
|
|
* key filepaths using the info members like this, or it can
|
|
|
|
* leave them NULL; force the vhost SSL_CTX init using the info
|
|
|
|
* options flag LWS_SERVER_OPTION_CREATE_VHOST_SSL_CTX; and
|
|
|
|
* set up the cert himself using the user callback
|
|
|
|
* LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS, which
|
|
|
|
* happened just above and has the vhost SSL_CTX * in the user
|
|
|
|
* parameter.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (!cert || !private_key)
|
|
|
|
return LWS_TLS_EXTANT_NO;
|
|
|
|
|
|
|
|
n = lws_tls_use_any_upgrade_check_extant(cert);
|
|
|
|
if (n == LWS_TLS_EXTANT_ALTERNATIVE)
|
|
|
|
return LWS_TLS_EXTANT_ALTERNATIVE;
|
|
|
|
m = lws_tls_use_any_upgrade_check_extant(private_key);
|
|
|
|
if (m == LWS_TLS_EXTANT_ALTERNATIVE)
|
|
|
|
return LWS_TLS_EXTANT_ALTERNATIVE;
|
|
|
|
|
|
|
|
if ((n == LWS_TLS_EXTANT_NO || m == LWS_TLS_EXTANT_NO) &&
|
|
|
|
(vhost->options & LWS_SERVER_OPTION_IGNORE_MISSING_CERT)) {
|
|
|
|
lwsl_notice("Ignoring missing %s or %s\n", cert, private_key);
|
|
|
|
vhost->tls.skipped_certs = 1;
|
|
|
|
|
|
|
|
return LWS_TLS_EXTANT_NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* the cert + key exist
|
|
|
|
*/
|
|
|
|
|
|
|
|
return LWS_TLS_EXTANT_YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if !defined(LWS_NO_SERVER)
|
|
|
|
/*
|
|
|
|
* update the cert for every vhost using the given path
|
|
|
|
*/
|
|
|
|
|
|
|
|
LWS_VISIBLE int
|
|
|
|
lws_tls_cert_updated(struct lws_context *context, const char *certpath,
|
|
|
|
const char *keypath,
|
|
|
|
const char *mem_cert, size_t len_mem_cert,
|
|
|
|
const char *mem_privkey, size_t len_mem_privkey)
|
|
|
|
{
|
|
|
|
struct lws wsi;
|
|
|
|
|
|
|
|
wsi.context = context;
|
|
|
|
|
|
|
|
lws_start_foreach_ll(struct lws_vhost *, v, context->vhost_list) {
|
2019-04-01 00:14:42 +00:00
|
|
|
wsi.vhost = v;
|
2018-06-07 11:33:24 +00:00
|
|
|
if (v->tls.alloc_cert_path && v->tls.key_path &&
|
|
|
|
!strcmp(v->tls.alloc_cert_path, certpath) &&
|
|
|
|
!strcmp(v->tls.key_path, keypath)) {
|
|
|
|
lws_tls_server_certs_load(v, &wsi, certpath, keypath,
|
|
|
|
mem_cert, len_mem_cert,
|
|
|
|
mem_privkey, len_mem_privkey);
|
|
|
|
|
|
|
|
if (v->tls.skipped_certs)
|
|
|
|
lwsl_notice("%s: vhost %s: cert unset\n",
|
|
|
|
__func__, v->name);
|
|
|
|
}
|
|
|
|
} lws_end_foreach_ll(v, vhost_next);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
int
|
|
|
|
lws_gate_accepts(struct lws_context *context, int on)
|
|
|
|
{
|
|
|
|
struct lws_vhost *v = context->vhost_list;
|
|
|
|
|
|
|
|
lwsl_notice("%s: on = %d\n", __func__, on);
|
|
|
|
|
|
|
|
#if defined(LWS_WITH_STATS)
|
|
|
|
context->updated = 1;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
while (v) {
|
|
|
|
if (v->tls.use_ssl && v->lserv_wsi &&
|
|
|
|
lws_change_pollfd(v->lserv_wsi, (LWS_POLLIN) * !on,
|
|
|
|
(LWS_POLLIN) * on))
|
|
|
|
lwsl_notice("Unable to set accept POLLIN %d\n", on);
|
|
|
|
|
|
|
|
v = v->vhost_next;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* comma-separated alpn list, like "h2,http/1.1" to openssl alpn format */
|
|
|
|
|
|
|
|
int
|
|
|
|
lws_alpn_comma_to_openssl(const char *comma, uint8_t *os, int len)
|
|
|
|
{
|
|
|
|
uint8_t *oos = os, *plen = NULL;
|
|
|
|
|
|
|
|
while (*comma && len > 1) {
|
|
|
|
if (!plen && *comma == ' ') {
|
|
|
|
comma++;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!plen) {
|
|
|
|
plen = os++;
|
|
|
|
len--;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*comma == ',') {
|
|
|
|
*plen = lws_ptr_diff(os, plen + 1);
|
|
|
|
plen = NULL;
|
|
|
|
comma++;
|
|
|
|
} else {
|
|
|
|
*os++ = *comma++;
|
|
|
|
len--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (plen)
|
|
|
|
*plen = lws_ptr_diff(os, plen + 1);
|
|
|
|
|
|
|
|
return lws_ptr_diff(os, oos);
|
|
|
|
}
|
|
|
|
|