Resolved Issues

This commit is contained in:
Jitin George
2018-02-14 15:15:50 +05:30
parent 8cd3c47956
commit e29294b49a
9 changed files with 220 additions and 91 deletions

View File

@@ -1,3 +1,16 @@
// Copyright 2017-2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@@ -32,10 +45,10 @@ static struct addrinfo *resolve_host_name(const char *host, size_t hostlen)
return NULL; return NULL;
} }
ESP_LOGD(TAG, "host:%s: strlen %zu\n", use_host, hostlen); ESP_LOGD(TAG, "host:%s: strlen %lu", use_host, (unsigned long)hostlen);
struct addrinfo *res; struct addrinfo *res;
if (getaddrinfo(use_host, NULL, &hints, &res)) { if (getaddrinfo(use_host, NULL, &hints, &res)) {
ESP_LOGE(TAG, "couldn't get hostname for :%s:\n", use_host); ESP_LOGE(TAG, "couldn't get hostname for :%s:", use_host);
free(use_host); free(use_host);
return NULL; return NULL;
} }
@@ -43,18 +56,18 @@ static struct addrinfo *resolve_host_name(const char *host, size_t hostlen)
return res; return res;
} }
static ssize_t tcp_read(struct esp_tls *tls, char *data, size_t datalen) static ssize_t tcp_read(esp_tls_t *tls, char *data, size_t datalen)
{ {
return recv(tls->sockfd, data, datalen, 0); return recv(tls->sockfd, data, datalen, 0);
} }
static ssize_t tls_read(struct esp_tls *tls, char *data, size_t datalen) static ssize_t tls_read(esp_tls_t *tls, char *data, size_t datalen)
{ {
ssize_t ret = SSL_read(tls->ssl, data, datalen); ssize_t ret = SSL_read(tls->ssl, data, datalen);
if (ret < 0) { if (ret < 0) {
int err = SSL_get_error(tls->ssl, ret); int err = SSL_get_error(tls->ssl, ret);
if (err != SSL_ERROR_WANT_WRITE && err != SSL_ERROR_WANT_READ) { if (err != SSL_ERROR_WANT_WRITE && err != SSL_ERROR_WANT_READ) {
ESP_LOGE(TAG, "read error :%d:\n", ret); ESP_LOGE(TAG, "read error :%d:", ret);
} }
return -err; return -err;
} }
@@ -104,11 +117,12 @@ err_freeaddr:
return -1; return -1;
} }
static int create_ssl_handle(struct esp_tls *tls, const char *hostname, size_t hostlen, struct esp_tls_cfg *cfg) static int create_ssl_handle(esp_tls_t *tls, const char *hostname, size_t hostlen, const esp_tls_cfg_t *cfg)
{ {
int ret; int ret;
SSL_CTX *ssl_ctx = SSL_CTX_new(TLSv1_2_client_method()); const SSL_METHOD *method = cfg->ssl_method!= NULL ? cfg->ssl_method : TLSv1_2_client_method();
SSL_CTX *ssl_ctx = SSL_CTX_new(method);
if (!ssl_ctx) { if (!ssl_ctx) {
return -1; return -1;
} }
@@ -128,9 +142,13 @@ static int create_ssl_handle(struct esp_tls *tls, const char *hostname, size_t h
X509 *ca = PEM_read_bio_X509(bio, NULL, 0, NULL); X509 *ca = PEM_read_bio_X509(bio, NULL, 0, NULL);
if (!ca) { if (!ca) {
ESP_LOGE(TAG, "CA Error\n"); ESP_LOGE(TAG, "CA Error");
X509_free(ca);
BIO_free(bio);
SSL_CTX_free(ssl_ctx);
return -1;
} }
ESP_LOGD(TAG, "CA OK\n"); ESP_LOGD(TAG, "CA OK");
X509_STORE_add_cert(SSL_CTX_get_cert_store(ssl_ctx), ca); X509_STORE_add_cert(SSL_CTX_get_cert_store(ssl_ctx), ca);
@@ -149,8 +167,8 @@ static int create_ssl_handle(struct esp_tls *tls, const char *hostname, size_t h
char *use_host = strndup(hostname, hostlen); char *use_host = strndup(hostname, hostlen);
if (!use_host) { if (!use_host) {
SSL_CTX_free(ssl_ctx); SSL_CTX_free(ssl_ctx);
return -1; return -1;
} }
SSL_set_tlsext_host_name(ssl, use_host); SSL_set_tlsext_host_name(ssl, use_host);
free(use_host); free(use_host);
@@ -169,7 +187,10 @@ static int create_ssl_handle(struct esp_tls *tls, const char *hostname, size_t h
return 0; return 0;
} }
void esp_tls_conn_delete(struct esp_tls *tls) /**
* @brief Close the TLS connection and free any allocated resources.
*/
void esp_tls_conn_delete(esp_tls_t *tls)
{ {
if (!tls) { if (!tls) {
return; return;
@@ -186,32 +207,35 @@ void esp_tls_conn_delete(struct esp_tls *tls)
free(tls); free(tls);
}; };
static ssize_t tcp_write(struct esp_tls *tls, const char *data, size_t datalen) static ssize_t tcp_write(esp_tls_t *tls, const char *data, size_t datalen)
{ {
return send(tls->sockfd, data, datalen, 0); return send(tls->sockfd, data, datalen, 0);
} }
static ssize_t tls_write(struct esp_tls *tls, const char *data, size_t datalen) static ssize_t tls_write(esp_tls_t *tls, const char *data, size_t datalen)
{ {
ssize_t ret = SSL_write(tls->ssl, data, datalen); ssize_t ret = SSL_write(tls->ssl, data, datalen);
if (ret < 0) { if (ret < 0) {
int err = SSL_get_error(tls->ssl, ret); int err = SSL_get_error(tls->ssl, ret);
if (err != SSL_ERROR_WANT_WRITE && err != SSL_ERROR_WANT_READ) { if (err != SSL_ERROR_WANT_WRITE && err != SSL_ERROR_WANT_READ) {
ESP_LOGE(TAG, "write error :%d:\n", ret); ESP_LOGE(TAG, "write error :%d:", ret);
} }
return -err; return -err;
} }
return ret; return ret;
} }
struct esp_tls *esp_tls_conn_new(const char *hostname, int hostlen, int port, struct esp_tls_cfg *cfg) /**
* @brief Create a new TLS/SSL connection
*/
esp_tls_t *esp_tls_conn_new(const char *hostname, int hostlen, int port, const esp_tls_cfg_t *cfg)
{ {
int sockfd = esp_tcp_connect(hostname, hostlen, port); int sockfd = esp_tcp_connect(hostname, hostlen, port);
if (sockfd < 0) { if (sockfd < 0) {
return NULL; return NULL;
} }
struct esp_tls *tls = (struct esp_tls *)calloc(1, sizeof(struct esp_tls)); esp_tls_t *tls = (esp_tls_t *)calloc(1, sizeof(esp_tls_t));
if (!tls) { if (!tls) {
close(sockfd); close(sockfd);
return NULL; return NULL;
@@ -228,6 +252,12 @@ struct esp_tls *esp_tls_conn_new(const char *hostname, int hostlen, int port, st
tls->read = tls_read; tls->read = tls_read;
tls->write = tls_write; tls->write = tls_write;
} }
if(cfg->non_block == true) {
int flags = fcntl(tls->sockfd, F_GETFL, 0);
fcntl(tls->sockfd, F_SETFL, flags | O_NONBLOCK);
}
return tls; return tls;
} }
@@ -245,7 +275,10 @@ static int get_port(const char *url, struct http_parser_url *u)
return 0; return 0;
} }
struct esp_tls *esp_tls_conn_http_new(const char *url, struct esp_tls_cfg *cfg) /**
* @brief Create a new TLS/SSL connection with a given "HTTP" url
*/
esp_tls_t *esp_tls_conn_http_new(const char *url, const esp_tls_cfg_t *cfg)
{ {
/* Parse URI */ /* Parse URI */
struct http_parser_url u; struct http_parser_url u;

View File

@@ -1,58 +1,149 @@
// Copyright 2017-2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _ESP_TLS_H_ #ifndef _ESP_TLS_H_
#define _ESP_TLS_H_ #define _ESP_TLS_H_
#include <stdbool.h> #include <stdbool.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <fcntl.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
struct esp_tls_cfg { /**
/* If HTTP2/ALPN support is required, a list of protocols that * @brief ESP-TLS configuration parameters
* should be negotiated. The format is length followed by protocol
* name.
* For the most common cases the following is ok:
* "\x02h2"
* - where the first '2' is the length of the protocol and
* - the subsequent 'h2' is the protocol name
*/
const unsigned char *alpn_protos;
const unsigned char *cacert_pem_buf;
const unsigned int cacert_pem_bytes;
};
struct esp_tls {
SSL_CTX *ctx;
SSL *ssl;
int sockfd;
ssize_t (*read)(struct esp_tls *tls, char *data, size_t datalen);
ssize_t (*write)(struct esp_tls *tls, const char *data, size_t datalen);
};
/*
*
* cfg: If you wish to open non-TLS connection, keep this NULL. For TLS
* connection, a pass pointer to 'struct esp_tls_cfg'. At a minimum, this
* structure should be zero-initialized.
*/ */
struct esp_tls *esp_tls_conn_new(const char *hostname, int hostlen, int port, struct esp_tls_cfg *cfg); typedef struct esp_tls_cfg {
const unsigned char *alpn_protos; /*!< Application protocols required for HTTP2.
If HTTP2/ALPN support is required, a list
of protocols that should be negotiated.
The format is length followed by protocol
name.
For the most common cases the following is ok:
"\x02h2"
- where the first '2' is the length of the protocol and
- the subsequent 'h2' is the protocol name */
const unsigned char *cacert_pem_buf; /*!< Certificate Authority's certificate in a buffer */
const unsigned int cacert_pem_bytes; /*!< Size of Certificate Authority certificate
pointed to by cacert_pem_buf */
const SSL_METHOD *ssl_method; /*!< SSL method that describes internal ssl library
methods/functions which implements the various protocol
versions. If set to NULL, it defaults to
method returned by TLSv1_2_client_method() API. */
bool non_block; /*!< Configure non-blocking mode. If set to true the
underneath socket will be configured in non
blocking mode after tls session is established */
} esp_tls_cfg_t;
/* Convenience API for HTTP URIs */ /**
struct esp_tls *esp_tls_conn_http_new(const char *url, struct esp_tls_cfg *cfg); * @brief ESP-TLS Connection Handle
*/
typedef struct esp_tls {
SSL_CTX *ctx; /*!< SSL_CTX object is used to establish
TLS/SSL enabled connection */
SSL *ssl; /*!< SSL object which is needed to hold the data for a
TLS/SSL connection. The new structure inherits the settings of the
underlying context ctx: connection method (SSLv2/v3/TLSv1),
options, verification settings, timeout settings. */
int sockfd; /*!< Underlying socket file descriptor. */
ssize_t (*read)(struct esp_tls *tls, char *data, size_t datalen); /*!< Callback function for reading data from TLS/SSL
connection. */
ssize_t (*write)(struct esp_tls *tls, const char *data, size_t datalen); /*!< Callback function for writing data to TLS/SSL
connection. */
} esp_tls_t;
static inline ssize_t esp_tls_conn_write(struct esp_tls *tls, const char *data, size_t datalen) /**
* @brief Create a new TLS/SSL connection
*
* This function establishes a TLS/SSL connection with the specified host.
*
* @param[in] hostname Hostname of the host.
* @param[in] hostlen Length of hostname.
* @param[in] port Port number of the host.
* @param[in] cfg TLS configuration as esp_tls_cfg_t. If you wish to open
* non-TLS connection, keep this NULL. For TLS connection,
* a pass pointer to esp_tls_cfg_t. At a minimum, this
* structure should be zero-initialized.
* @return pointer to esp_tls_t, or NULL if connection couldn't be opened.
*/
esp_tls_t *esp_tls_conn_new(const char *hostname, int hostlen, int port, const esp_tls_cfg_t *cfg);
/**
* @brief Create a new TLS/SSL connection with a given "HTTP" url
*
* The behaviour is same as esp_tls_conn_new() API. However this API accepts host's url.
*
* @param[in] url url of host.
* @param[in] cfg TLS configuration as esp_tls_cfg_t. If you wish to open
* non-TLS connection, keep this NULL. For TLS connection,
* a pass pointer to 'esp_tls_cfg_t'. At a minimum, this
* structure should be zero-initialized.
* @return pointer to esp_tls_t, or NULL if connection couldn't be opened.
*/
esp_tls_t *esp_tls_conn_http_new(const char *url, const esp_tls_cfg_t *cfg);
/**
* @brief Write from buffer 'data' into specified tls connection.
*
* @param[in] tls pointer to esp-tls as esp-tls handle.
* @param[in] data Buffer from which data will be written.
* @param[in] datalen Length of data buffer.
*
* @return
* - >0 if write operation was successful, the return value is the number
* of bytes actually written to the TLS/SSL connection.
* - 0 if write operation was not successful. The underlying
* connection was closed.
* - <0 if write operation was not successful, because either an
* error occured or an action must be taken by the calling process.
*/
static inline ssize_t esp_tls_conn_write(esp_tls_t *tls, const void *data, size_t datalen)
{ {
return tls->write(tls, data, datalen); return tls->write(tls, (char *)data, datalen);
} }
static inline ssize_t esp_tls_conn_read(struct esp_tls *tls, char *data, size_t datalen) /**
* @brief Read from specified tls connection into the buffer 'data'.
*
* @param[in] tls pointer to esp-tls as esp-tls handle.
* @param[in] data Buffer to hold read data.
* @param[in] datalen Length of data buffer.
*
* @return
* - >0 if read operation was successful, the return value is the number
* of bytes actually read from the TLS/SSL connection.
* - 0 if read operation was not successful. The underlying
* connection was closed.
* - <0 if read operation was not successful, because either an
* error occured or an action must be taken by the calling process.
*/
static inline ssize_t esp_tls_conn_read(esp_tls_t *tls, void *data, size_t datalen)
{ {
return tls->read(tls, data, datalen); return tls->read(tls, (char *)data, datalen);
} }
void esp_tls_conn_delete(struct esp_tls *tls); /**
* @brief Close the TLS/SSL connection and free any allocated resources.
*
* This function should be called to close each tls connection opened with esp_tls_conn_new() or
* esp_tls_conn_http_new() APIs.
*
* @param[in] tls pointer to esp-tls as esp-tls handle.
*/
void esp_tls_conn_delete(esp_tls_t *tls);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -87,6 +87,9 @@ INPUT = \
## ##
## Protocols - API Reference ## Protocols - API Reference
## ##
## ESP-TLS
../../components/esp-tls/esp_tls.h \
## mDNS
../../components/mdns/include/mdns.h \ ../../components/mdns/include/mdns.h \
## ##
## Storage - API Reference ## Storage - API Reference

View File

@@ -0,0 +1,24 @@
ESP-TLS
=======
Overview
--------
The ESP-TLS component provides a simplified API interface for accessing the commonly used TLS functionality.
It supports common scenarios like CA certification validation, SNI, ALPN negotiation, non-blocking connection among others.
All the configuration can be specified in the esp_tls_cfg_t data structure. Once done, TLS communication can be conducted using the following APIs:
* esp_tls_conn_new(): for opening a new TLS connection
* esp_tls_conn_read/write(): for reading/writing from the connection
* esp_tls_conn_delete(): for freeing up the connection
Any application layer protocol like HTTP1, HTTP2 etc can be executed on top of this layer.
Application Example
-------------------
Simple HTTPS example that uses ESP-TLS to establish a secure socket connection: :example:`protocols/https_request`.
API Reference
-------------
.. include:: /_build/inc/esp_tls.inc

View File

@@ -5,6 +5,6 @@ Protocols API
:maxdepth: 1 :maxdepth: 1
mDNS <mdns> mDNS <mdns>
ESP-TLS <esp_tls>
Example code for this API section is provided in :example:`protocols` directory of ESP-IDF examples. Example code for this API section is provided in :example:`protocols` directory of ESP-IDF examples.

View File

@@ -17,7 +17,6 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <fcntl.h>
#include <ctype.h> #include <ctype.h>
#include <netdb.h> #include <netdb.h>
#include <esp_log.h> #include <esp_log.h>
@@ -75,7 +74,7 @@ static int do_ssl_connect(struct sh2lib_handle *hd, int sockfd, const char *host
static ssize_t callback_send_inner(struct sh2lib_handle *hd, const uint8_t *data, static ssize_t callback_send_inner(struct sh2lib_handle *hd, const uint8_t *data,
size_t length) size_t length)
{ {
int rv = esp_tls_conn_write(hd->http2_tls, (const char *)data, (int)length); int rv = esp_tls_conn_write(hd->http2_tls, data, length);
if (rv <= 0) { if (rv <= 0) {
if (rv == -SSL_ERROR_WANT_WRITE || rv == -SSL_ERROR_WANT_READ) { if (rv == -SSL_ERROR_WANT_WRITE || rv == -SSL_ERROR_WANT_READ) {
rv = NGHTTP2_ERR_WOULDBLOCK; rv = NGHTTP2_ERR_WOULDBLOCK;
@@ -279,8 +278,10 @@ static int do_http2_connect(struct sh2lib_handle *hd)
int sh2lib_connect(struct sh2lib_handle *hd, const char *uri) int sh2lib_connect(struct sh2lib_handle *hd, const char *uri)
{ {
memset(hd, 0, sizeof(*hd)); memset(hd, 0, sizeof(*hd));
struct esp_tls_cfg tls_cfg; esp_tls_cfg_t tls_cfg = {
tls_cfg.alpn_protos = (unsigned char *) "\x02h2"; .alpn_protos = (unsigned char *) "\x02h2",
.non_block = true,
};
if ((hd->http2_tls = esp_tls_conn_http_new(uri, &tls_cfg)) == NULL) { if ((hd->http2_tls = esp_tls_conn_http_new(uri, &tls_cfg)) == NULL) {
ESP_LOGE(TAG, "[sh2-connect] esp-tls connection failed"); ESP_LOGE(TAG, "[sh2-connect] esp-tls connection failed");
goto error; goto error;
@@ -292,9 +293,6 @@ int sh2lib_connect(struct sh2lib_handle *hd, const char *uri)
goto error; goto error;
} }
int flags = fcntl(hd->http2_tls->sockfd, F_GETFL, 0);
fcntl(hd->http2_tls->sockfd, F_SETFL, flags | O_NONBLOCK);
return 0; return 0;
error: error:
sh2lib_free(hd); sh2lib_free(hd);

View File

@@ -14,7 +14,7 @@
#ifndef __ESP_EXAMPLE_SH2_LIB_H_ #ifndef __ESP_EXAMPLE_SH2_LIB_H_
#define __ESP_EXAMPLE_SH2_LIB_H_ #define __ESP_EXAMPLE_SH2_LIB_H_
#include <openssl/ssl.h> #include "esp-tls.h"
#include <nghttp2/nghttp2.h> #include <nghttp2/nghttp2.h>
/* /*

View File

@@ -1,5 +1,5 @@
# HTTPS Request Example # HTTPS Request Example
Uses an mbedTLS socket to make a very simple HTTPS request over a secure connection, including verifying the server TLS certificate. Uses APIs from `esp-tls` component to make a very simple HTTPS request over a secure connection, including verifying the server TLS certificate.
See the README.md file in the upper level 'examples' directory for more information about examples. See the README.md file in the upper level 'examples' directory for more information about examples.

View File

@@ -38,15 +38,6 @@
#include "lwip/netdb.h" #include "lwip/netdb.h"
#include "lwip/dns.h" #include "lwip/dns.h"
#include "mbedtls/platform.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/esp_debug.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/error.h"
#include "mbedtls/certs.h"
#include "esp-tls.h" #include "esp-tls.h"
/* The examples use simple WiFi configuration that you can set via /* The examples use simple WiFi configuration that you can set via
@@ -144,7 +135,7 @@ static void https_get_task(void *pvParameters)
xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT, xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT,
false, true, portMAX_DELAY); false, true, portMAX_DELAY);
ESP_LOGI(TAG, "Connected to AP"); ESP_LOGI(TAG, "Connected to AP");
struct esp_tls_cfg cfg = { esp_tls_cfg_t cfg = {
.cacert_pem_buf = server_root_cert_pem_start, .cacert_pem_buf = server_root_cert_pem_start,
.cacert_pem_bytes = server_root_cert_pem_end - server_root_cert_pem_start, .cacert_pem_bytes = server_root_cert_pem_end - server_root_cert_pem_start,
}; };
@@ -161,13 +152,13 @@ static void https_get_task(void *pvParameters)
size_t written_bytes = 0; size_t written_bytes = 0;
do { do {
ret = esp_tls_conn_write(tls, ret = esp_tls_conn_write(tls,
(const char *)REQUEST + written_bytes, REQUEST + written_bytes,
strlen(REQUEST) - written_bytes); strlen(REQUEST) - written_bytes);
if (ret >= 0) { if (ret >= 0) {
ESP_LOGI(TAG, "%d bytes written", ret); ESP_LOGI(TAG, "%d bytes written", ret);
written_bytes += ret; written_bytes += ret;
} else if (-ret != SSL_ERROR_WANT_WRITE && -ret != SSL_ERROR_WANT_READ) { } else if (ret != -SSL_ERROR_WANT_WRITE && ret != -SSL_ERROR_WANT_READ) {
ESP_LOGE(TAG, "esp_tls_conn_write returned -0x%x", ret); ESP_LOGE(TAG, "esp_tls_conn_write returned 0x%x", ret);
goto exit; goto exit;
} }
} while(written_bytes < strlen(REQUEST)); } while(written_bytes < strlen(REQUEST));
@@ -180,17 +171,12 @@ static void https_get_task(void *pvParameters)
bzero(buf, sizeof(buf)); bzero(buf, sizeof(buf));
ret = esp_tls_conn_read(tls, (char *)buf, len); ret = esp_tls_conn_read(tls, (char *)buf, len);
if(-ret == SSL_ERROR_WANT_WRITE || -ret == SSL_ERROR_WANT_READ) if(ret == -SSL_ERROR_WANT_WRITE || ret == -SSL_ERROR_WANT_READ)
continue; continue;
if(ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
ret = 0;
break;
}
if(ret < 0) if(ret < 0)
{ {
ESP_LOGE(TAG, "esp_tls_conn_read returned -0x%x", ret); ESP_LOGE(TAG, "esp_tls_conn_read returned 0x%x", ret);
break; break;
} }
@@ -209,12 +195,6 @@ static void https_get_task(void *pvParameters)
} while(1); } while(1);
exit: exit:
if(ret != 0)
{
mbedtls_strerror(ret, buf, 100);
ESP_LOGE(TAG, "Last error was: -0x%x - %s", -ret, buf);
}
esp_tls_conn_delete(tls); esp_tls_conn_delete(tls);
putchar('\n'); // JSON output doesn't have a newline at end putchar('\n'); // JSON output doesn't have a newline at end