2016-12-31 13:14:03 +01:00
|
|
|
// Module for TLS
|
|
|
|
|
|
|
|
#include "module.h"
|
|
|
|
|
|
|
|
#if defined(CLIENT_SSL_ENABLE) && defined(LUA_USE_MODULES_NET)
|
|
|
|
|
|
|
|
#include "lauxlib.h"
|
|
|
|
#include "platform.h"
|
|
|
|
#include "lmem.h"
|
|
|
|
|
2019-07-21 23:58:21 +02:00
|
|
|
#include <string.h>
|
2019-07-23 06:22:38 +02:00
|
|
|
#include <stddef.h>
|
|
|
|
#include <stdint.h>
|
2020-06-16 09:19:55 +02:00
|
|
|
#include <ctype.h>
|
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
#include "mem.h"
|
|
|
|
#include "lwip/ip_addr.h"
|
|
|
|
#include "espconn.h"
|
Update to sdk 2.2
Initial commit for
https://github.com/nodemcu/nodemcu-firmware/issues/2225 .
Replay patches from Espressif's repository at
https://github.com/espressif/ESP8266_NONOS_SDK between tags v2.1.0 and
v2.2.0:
0001-sync-from-ccca00f2.patch
Superseded by existing changes, but lines reordered in app/driver/key.c
to minimize divergences.
0002-sync-from-3f38ad5a.patch
Upstream files only
0003-Update-links.patch
Not meaningful to NodeMCU
0004-sync-from-01990ad0.patch
0005-sync-from-cdf6877d.patch
Upstream files only
0006-sync-from-f29e744c.patch
Upstream files only, user_interface.h override non-conflicting
0009-feat-lwip-Move-lwip-source-code-to-third_party-folde.patch
Merged change to lwip/app/espconn_udp.c; rest is just moves or
appears to not apply.
0010-feat-mbedtls-Add-mbedtls-source-code-in-third_party-.patch
Does not apply; we use our own mbedtls
0011-added-C-support.patch
Merged to Makefile
0012-feat-mbedtls-Rebuild-libmbedtls.patch
Already applied
0013-fix-at-Fix-some-bugs-of-AT.patch
Upstream files only
0014-feat-err_t-Redefine-err_t-to-s32_t.patch
Merged to app/include/arch/cc.h and ./app/include/lwip/app/espconn.h;
the rest is upstream files.
0015-fix-wpa-Fix-wpa-wpa2-ptk-gtk-reinstallation-vulnerab.patch
0016-fix-wifi-Remove-group-key-entry-before-connecting-to.patch
0017-feat-lib-Remove-time-function-in-libmain.patch
Upstream files only
0018-feat-espconn-Modification-for-espconn.patch
Merged to app/include/lwip/app/espconn.h,
app/include/lwip/app/espconn_tcp.h, app/lwip/app/espconn.c,
app/lwip/app/espconn_tcp.c
0019-feat-at-Use-new-espconn_recv-to-fix-tcp-server-issue.patch
0020-feat-examples-Update-mqtt-demo-and-auto-bin-generate.patch
Upstream files only
0021-wifi-Add-scan-threshold-and-dwell-time.patch
0022-feat-wifi-Add-country-code-API.patch
0023-feat-wifi-Record-more-information-of-scanned-ap.patch
Upstream files only, user_interface.h override non-conflicting
0024-fix-example-Fix-IoT_Demo-user-sector-error.patch
Upstream files only
0025-fix-lwip-Fix-sequence-number-error-of-RST-ACK.patch
Merged app/lwip/core/tcp_in.c
0026-fix-mbedtls-Fix-memory-leak.patch
Merged app/mbedtls/app/lwIPSocket.c
0027-fix-mbedtls-Fix-call-send-callback-function-failed.patch
Merged app/mbedtls/app/espconn_mbedtls.c
0028-feat-Add-USE_OPTIMIZE_PRINTF-in-third_party-Makefile.patch
Merged app/Makefile
0029-fix-api-Fix-ets_delay_us-declaration.patch
Upstream files only, osapi.h override non-conflicting
0030-fix-wifi-Remove-max_tx_power-in-wifi_country_t-in-li.patch
0031-fix-wifi-Fix-softAP-wrong-behavior-after-call-system.patch
0032-fix-wifi-bugfix-of-scan-fail-after-connected-if-max-.patch
0033-feat-at-Enable-scan-time-scan-type-and-add-scan-resu.patch
0034-feat-at-Add-command-AT-CWCOUNTRY.patch
0035-fix-at-Fix-that-AT-CIPSTART-causes-busy-if-the-serve.patch
Upstream files only
0036-feat-mbedtls-Speed-up-mbedtls-handshake-process.patch
Merged app/mbedtls/app/espconn_mbedtls.c
0037-fix-api-Fix-os_calloc-declaration.patch
Merged app/include/lwip/mem.h; sdk-overrides/include/mem.h
non-conflicting.
0038-fix-mbedtls-Fix-disconnect-callback-function-never-b.patch
Merged app/mbedtls/app/espconn_mbedtls.c; minor revision to logic in
6576af959b1e704003ae5b93f6d6b89fcf86d429. Whitespace fixes.
0039-feat-at-Add-country-code-start-channel-in-AT-CWCOUNT.patch
0040-fix-net80211-Fix-Null-pointer-in-ieee80211_rfid_locp.patch
Upstream files only
0041-feat-wifi-Add-new-esp_init_data_default-v08-bin.patch
Upstream files only, but impacts Makefile
0042-fix-mbedtls-Fix-load-cert-fail-when-the-private-key-.patch
Merged app/mbedtls/app/espconn_mbedtls.c
0043-fix-wifi-The-start-channel-can-be-any-valid-channel.patch
0044-fix-wifi-Fix-scan-do-not-start-after-connect.patch
0045-feat-wifi-Add-keep-connection-for-station-to-keep-co.patch
0046-feat-at-Update-AT-version-to-1.6.0.0.patch
0047-fix-at-Fix-GSLP-too-long-time.patch
0048-fix-at-Fix-the-message-is-incorrect-when-creating-UD.patch
0049-feat-at-Add-AT-CIPSERVERMAXCONN.patch
Upstream files only
0050-feat-system-Add-softap-distributes-station-ip-event.patch
Upstream files only, user_interface.h override non-conflicting
0051-feat-example-Use-libmbedtls.a-instead-of-libssl.a-in.patch
Upstream files only
0052-feat-mesh-Remove-mesh-support.patch
Upstream files only, but go ahead and remove comment from
ld/nodemcu.ld.
0053-fix-example-Fix-forget-to-add-integer-parameter-when.patch
Upstream files only
0054-fix-mbedtls-Fix-reconnect_callback-is-not-triggered-.patch
Merged app/mbedtls/app/espconn_mbedtls.c
0055-feat-at-Add-AT-SYSMSG-to-enable-some-report-informat.patch
0056-fix-at-Fix-the-incorrect-link-id-when-client-connect.patch
0057-fix-at-Fix-the-bug-that-it-should-be-error-when-the-.patch
0058-fix-smartconfig-Fix-the-smartconfig-scan-time-issue.patch
0059-fix-lwip-Fix-the-bug-of-lwip-output.patch
Upstream files only
0060-fix-lwip-Fix-the-length-of-TCP-data-in-one-packet-is.patch
0061-fix-lwip-Fix-send-TCP-data-with-two-or-more-pbuf.patch
Merged app/lwip/core/tcp_out.c
0062-fix-wifi-Fix-assert-happen-when-smartconfig-start-th.patch
Upstream files only
0063-fix-mbedtls-Fix-memory-leak-when-ESP8266-as-SSL-TLS-.patch
Merged app/mbedtls/app/espconn_mbedtls.c
0064-fix-mbedtls-Fix-already-freed-and-exception-bug-when.patch
Merged app/mbedtls/app/lwIPSocket.c
0065-fix-at-Fix-bug-that-there-is-no-result-when-sending-.patch
0066-feat-example-Add-AT-bin-version.patch
0067-feat-version-Update-version-to-2.2.0-and-add-version.patch
0068-feat-bin-Update-AT-bin-for-SDK-2.2.0.patch
Upstream files only
Apply local changes to build:
app/include/lwip/app/espconn.h pulls changes (and license decl) from
upstream SDK. Makefile is altered to use this file ahead of the
SDK's.
Remove lwip's sntp support, since it was never really wired in anyway.
See https://github.com/nodemcu/nodemcu-firmware/issues/2042 for more
information. Patch Makefile to strip time.o, the consumer of lwip's
sntp functionality, from libmain.a, resulting in much
easier-to-understand error messages.
This has consequences for mbedtls. The simplest thing to do, which is,
impressively, not a change in behavior, is to completely disable TLS
certificate time validation; a later patch can optionally couple this to
RTCTIME support.
Similarly, it happens that the sqlite3 import was calling time(), but
this was not going to work out well for it. Just stub it out to always
return unix timestamp 0, as would have happened anyway.
Changes unprocessed:
0007-sync-from-080c37e1.patch
0008-feat-lib-Compile-some-libraries-with-ffunction-secti.patch
These two make changes to the linker script; perhaps
they are worth porting over, but I have not done so
here.
This is build-tested (ADC, BIT, COLOR_UTILS, CRON, CRYPTO, DHT, ENCODER,
FILE, GPIO, HTTP, I2C, MQTT, NET, NODE, OW, PCM, PERF, PWM, RTCFIFO,
RTCMEM, RTCTIME, SNTP, SPI, SQLITE3, STRUCT, TLS, TMR, UART, WIFI,
WS2812, WS2812_EFFECTS) and boots, but only limited run-time testing has
been performed. Testing done does, however, include having made a few
TLS connections through the HTTP module, so things are not hopelessly
broken, at the very least.
2018-02-20 03:03:09 +01:00
|
|
|
#include "sys/espconn_mbedtls.h"
|
2016-12-31 13:14:03 +01:00
|
|
|
#include "lwip/err.h"
|
|
|
|
#include "lwip/dns.h"
|
|
|
|
|
2018-03-03 23:28:26 +01:00
|
|
|
#include "mbedtls/debug.h"
|
|
|
|
#include "user_mbedtls.h"
|
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
#ifdef HAVE_SSL_SERVER_CRT
|
|
|
|
#include HAVE_SSL_SERVER_CRT
|
|
|
|
#else
|
|
|
|
__attribute__((section(".servercert.flash"))) unsigned char tls_server_cert_area[INTERNAL_FLASH_SECTOR_SIZE];
|
|
|
|
#endif
|
|
|
|
|
|
|
|
__attribute__((section(".clientcert.flash"))) unsigned char tls_client_cert_area[INTERNAL_FLASH_SECTOR_SIZE];
|
|
|
|
|
|
|
|
typedef struct {
|
2020-04-07 14:06:27 +02:00
|
|
|
struct espconn pesp_conn;
|
2016-12-31 13:14:03 +01:00
|
|
|
int self_ref;
|
|
|
|
int cb_connect_ref;
|
|
|
|
int cb_reconnect_ref;
|
|
|
|
int cb_disconnect_ref;
|
|
|
|
int cb_sent_ref;
|
|
|
|
int cb_receive_ref;
|
|
|
|
int cb_dns_ref;
|
|
|
|
} tls_socket_ud;
|
|
|
|
|
2020-04-27 02:13:38 +02:00
|
|
|
static int tls_socket_create( lua_State *L ) {
|
2016-12-31 13:14:03 +01:00
|
|
|
tls_socket_ud *ud = (tls_socket_ud*) lua_newuserdata(L, sizeof(tls_socket_ud));
|
|
|
|
|
2020-04-07 14:06:27 +02:00
|
|
|
bzero(&ud->pesp_conn, sizeof(ud->pesp_conn));
|
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
ud->self_ref =
|
|
|
|
ud->cb_connect_ref =
|
|
|
|
ud->cb_reconnect_ref =
|
|
|
|
ud->cb_disconnect_ref =
|
|
|
|
ud->cb_sent_ref =
|
|
|
|
ud->cb_receive_ref =
|
|
|
|
ud->cb_dns_ref = LUA_NOREF;
|
|
|
|
|
|
|
|
luaL_getmetatable(L, "tls.socket");
|
|
|
|
lua_setmetatable(L, -2);
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tls_socket_onconnect( struct espconn *pesp_conn ) {
|
2020-04-07 14:06:27 +02:00
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)pesp_conn;
|
2016-12-31 13:14:03 +01:00
|
|
|
if (!ud || ud->self_ref == LUA_NOREF) return;
|
|
|
|
if (ud->cb_connect_ref != LUA_NOREF) {
|
|
|
|
lua_State *L = lua_getstate();
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->cb_connect_ref);
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
lua_call(L, 1, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tls_socket_cleanup(tls_socket_ud *ud) {
|
2020-04-07 14:06:27 +02:00
|
|
|
if (ud->pesp_conn.proto.tcp) {
|
|
|
|
espconn_secure_disconnect(&ud->pesp_conn);
|
|
|
|
free(ud->pesp_conn.proto.tcp);
|
|
|
|
ud->pesp_conn.proto.tcp = NULL;
|
2016-12-31 13:14:03 +01:00
|
|
|
}
|
|
|
|
lua_State *L = lua_getstate();
|
|
|
|
lua_gc(L, LUA_GCSTOP, 0);
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
ud->self_ref = LUA_NOREF;
|
|
|
|
lua_gc(L, LUA_GCRESTART, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tls_socket_ondisconnect( struct espconn *pesp_conn ) {
|
2020-04-07 14:06:27 +02:00
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)pesp_conn;
|
2016-12-31 13:14:03 +01:00
|
|
|
if (!ud || ud->self_ref == LUA_NOREF) return;
|
|
|
|
tls_socket_cleanup(ud);
|
|
|
|
if (ud->cb_disconnect_ref != LUA_NOREF) {
|
|
|
|
lua_State *L = lua_getstate();
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->cb_disconnect_ref);
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
tls_socket_cleanup(ud);
|
|
|
|
lua_call(L, 1, 0);
|
|
|
|
} else tls_socket_cleanup(ud);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tls_socket_onreconnect( struct espconn *pesp_conn, s8 err ) {
|
2020-04-07 14:06:27 +02:00
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)pesp_conn;
|
2016-12-31 13:14:03 +01:00
|
|
|
if (!ud || ud->self_ref == LUA_NOREF) return;
|
|
|
|
if (ud->cb_reconnect_ref != LUA_NOREF) {
|
|
|
|
const char* reason = NULL;
|
|
|
|
switch (err) {
|
|
|
|
case(ESPCONN_MEM): reason = "Out of memory"; break;
|
|
|
|
case(ESPCONN_TIMEOUT): reason = "Timeout"; break;
|
|
|
|
case(ESPCONN_RTE): reason = "Routing problem"; break;
|
|
|
|
case(ESPCONN_ABRT): reason = "Connection aborted"; break;
|
|
|
|
case(ESPCONN_RST): reason = "Connection reset"; break;
|
|
|
|
case(ESPCONN_CLSD): reason = "Connection closed"; break;
|
|
|
|
case(ESPCONN_HANDSHAKE): reason = "SSL handshake failed"; break;
|
|
|
|
case(ESPCONN_SSL_INVALID_DATA): reason = "SSL application invalid"; break;
|
|
|
|
}
|
|
|
|
lua_State *L = lua_getstate();
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->cb_reconnect_ref);
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
if (reason != NULL) {
|
|
|
|
lua_pushstring(L, reason);
|
|
|
|
} else {
|
|
|
|
lua_pushnil(L);
|
|
|
|
}
|
|
|
|
tls_socket_cleanup(ud);
|
|
|
|
lua_call(L, 2, 0);
|
|
|
|
} else tls_socket_cleanup(ud);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tls_socket_onrecv( struct espconn *pesp_conn, char *buf, u16 length ) {
|
2020-04-07 14:06:27 +02:00
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)pesp_conn;
|
2016-12-31 13:14:03 +01:00
|
|
|
if (!ud || ud->self_ref == LUA_NOREF) return;
|
|
|
|
if (ud->cb_receive_ref != LUA_NOREF) {
|
|
|
|
lua_State *L = lua_getstate();
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->cb_receive_ref);
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
lua_pushlstring(L, buf, length);
|
|
|
|
lua_call(L, 2, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tls_socket_onsent( struct espconn *pesp_conn ) {
|
2020-04-07 14:06:27 +02:00
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)pesp_conn;
|
2016-12-31 13:14:03 +01:00
|
|
|
if (!ud || ud->self_ref == LUA_NOREF) return;
|
|
|
|
if (ud->cb_sent_ref != LUA_NOREF) {
|
|
|
|
lua_State *L = lua_getstate();
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->cb_sent_ref);
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
lua_call(L, 1, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tls_socket_dns_cb( const char* domain, const ip_addr_t *ip_addr, tls_socket_ud *ud ) {
|
|
|
|
if (ud->self_ref == LUA_NOREF) return;
|
|
|
|
ip_addr_t addr;
|
|
|
|
if (ip_addr) addr = *ip_addr;
|
|
|
|
else addr.addr = 0xFFFFFFFF;
|
|
|
|
lua_State *L = lua_getstate();
|
|
|
|
if (ud->cb_dns_ref != LUA_NOREF) {
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->cb_dns_ref);
|
|
|
|
lua_rawgeti(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
if (addr.addr == 0xFFFFFFFF) {
|
|
|
|
lua_pushnil(L);
|
|
|
|
} else {
|
|
|
|
char tmp[20];
|
2019-07-21 23:58:21 +02:00
|
|
|
sprintf(tmp, IPSTR, IP2STR(&addr.addr));
|
2016-12-31 13:14:03 +01:00
|
|
|
lua_pushstring(L, tmp);
|
|
|
|
}
|
|
|
|
lua_call(L, 2, 0);
|
|
|
|
}
|
|
|
|
if (addr.addr == 0xFFFFFFFF) {
|
|
|
|
lua_gc(L, LUA_GCSTOP, 0);
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
ud->self_ref = LUA_NOREF;
|
|
|
|
lua_gc(L, LUA_GCRESTART, 0);
|
|
|
|
} else {
|
2020-04-07 14:06:27 +02:00
|
|
|
os_memcpy(ud->pesp_conn.proto.tcp->remote_ip, &addr.addr, 4);
|
|
|
|
espconn_secure_connect(&ud->pesp_conn);
|
2016-12-31 13:14:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tls_socket_connect( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
2020-04-07 14:06:27 +02:00
|
|
|
if (ud->pesp_conn.proto.tcp) {
|
2016-12-31 13:14:03 +01:00
|
|
|
return luaL_error(L, "already connected");
|
|
|
|
}
|
|
|
|
|
|
|
|
u16 port = luaL_checkinteger( L, 2 );
|
|
|
|
size_t il;
|
|
|
|
const char *domain = "127.0.0.1";
|
|
|
|
if( lua_isstring(L, 3) )
|
|
|
|
domain = luaL_checklstring( L, 3, &il );
|
|
|
|
if (port == 0)
|
|
|
|
return luaL_error(L, "invalid port");
|
|
|
|
if (domain == NULL)
|
|
|
|
return luaL_error(L, "invalid domain");
|
|
|
|
|
2020-04-07 14:06:27 +02:00
|
|
|
ud->pesp_conn.proto.udp = NULL;
|
|
|
|
ud->pesp_conn.proto.tcp = (esp_tcp *)calloc(1,sizeof(esp_tcp));
|
|
|
|
if(!ud->pesp_conn.proto.tcp){
|
2016-12-31 13:14:03 +01:00
|
|
|
return luaL_error(L, "not enough memory");
|
|
|
|
}
|
2020-04-07 14:06:27 +02:00
|
|
|
ud->pesp_conn.type = ESPCONN_TCP;
|
|
|
|
ud->pesp_conn.state = ESPCONN_NONE;
|
|
|
|
ud->pesp_conn.proto.tcp->remote_port = port;
|
|
|
|
espconn_regist_connectcb(&ud->pesp_conn, (espconn_connect_callback)tls_socket_onconnect);
|
|
|
|
espconn_regist_disconcb(&ud->pesp_conn, (espconn_connect_callback)tls_socket_ondisconnect);
|
|
|
|
espconn_regist_reconcb(&ud->pesp_conn, (espconn_reconnect_callback)tls_socket_onreconnect);
|
|
|
|
espconn_regist_recvcb(&ud->pesp_conn, (espconn_recv_callback)tls_socket_onrecv);
|
|
|
|
espconn_regist_sentcb(&ud->pesp_conn, (espconn_sent_callback)tls_socket_onsent);
|
2016-12-31 13:14:03 +01:00
|
|
|
|
|
|
|
if (ud->self_ref == LUA_NOREF) {
|
|
|
|
lua_pushvalue(L, 1); // copy to the top of stack
|
|
|
|
ud->self_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
|
|
|
}
|
|
|
|
|
|
|
|
ip_addr_t addr;
|
|
|
|
err_t err = dns_gethostbyname(domain, &addr, (dns_found_callback)tls_socket_dns_cb, ud);
|
|
|
|
if (err == ERR_OK) {
|
|
|
|
tls_socket_dns_cb(domain, &addr, ud);
|
|
|
|
} else if (err != ERR_INPROGRESS) {
|
|
|
|
tls_socket_dns_cb(domain, NULL, ud);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tls_socket_on( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
|
|
|
size_t sl;
|
|
|
|
const char *method = luaL_checklstring( L, 2, &sl );
|
2020-04-07 14:06:27 +02:00
|
|
|
int *cbp;
|
|
|
|
|
|
|
|
if (strcmp(method, "connection" ) == 0) { cbp = &ud->cb_connect_ref ; }
|
|
|
|
else if (strcmp(method, "disconnection") == 0) { cbp = &ud->cb_disconnect_ref; }
|
|
|
|
else if (strcmp(method, "reconnection" ) == 0) { cbp = &ud->cb_reconnect_ref ; }
|
|
|
|
else if (strcmp(method, "receive" ) == 0) { cbp = &ud->cb_receive_ref ; }
|
|
|
|
else if (strcmp(method, "sent" ) == 0) { cbp = &ud->cb_sent_ref ; }
|
|
|
|
else if (strcmp(method, "dns" ) == 0) { cbp = &ud->cb_dns_ref ; }
|
|
|
|
else {
|
2016-12-31 13:14:03 +01:00
|
|
|
return luaL_error(L, "invalid method");
|
|
|
|
}
|
2020-04-07 14:06:27 +02:00
|
|
|
|
2020-04-27 02:13:38 +02:00
|
|
|
if (lua_isfunction(L, 3)) {
|
2020-04-07 14:06:27 +02:00
|
|
|
lua_pushvalue(L, 3); // copy argument (func) to the top of stack
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, *cbp);
|
|
|
|
*cbp = luaL_ref(L, LUA_REGISTRYINDEX);
|
|
|
|
} else if (lua_isnil(L, 3)) {
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, *cbp);
|
|
|
|
*cbp = LUA_NOREF;
|
|
|
|
} else {
|
|
|
|
return luaL_error(L, "invalid callback function");
|
|
|
|
}
|
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tls_socket_send( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
2020-04-27 02:13:38 +02:00
|
|
|
size_t sl;
|
|
|
|
const char* buf = luaL_checklstring(L, 2, &sl);
|
2020-04-07 14:06:27 +02:00
|
|
|
if(ud->pesp_conn.proto.tcp == NULL) {
|
2016-12-31 13:14:03 +01:00
|
|
|
NODE_DBG("not connected");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-04-07 14:06:27 +02:00
|
|
|
espconn_secure_send(&ud->pesp_conn, (void*)buf, sl);
|
2016-12-31 13:14:03 +01:00
|
|
|
return 0;
|
|
|
|
}
|
2020-04-27 02:13:38 +02:00
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
static int tls_socket_hold( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
|
|
|
luaL_argcheck(L, ud, 1, "TLS socket expected");
|
2020-04-07 14:06:27 +02:00
|
|
|
if(ud->pesp_conn.proto.tcp == NULL) {
|
2016-12-31 13:14:03 +01:00
|
|
|
NODE_DBG("not connected");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-04-07 14:06:27 +02:00
|
|
|
espconn_recv_hold(&ud->pesp_conn);
|
2016-12-31 13:14:03 +01:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
static int tls_socket_unhold( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
2020-04-07 14:06:27 +02:00
|
|
|
if(ud->pesp_conn.proto.tcp == NULL) {
|
2016-12-31 13:14:03 +01:00
|
|
|
NODE_DBG("not connected");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-04-07 14:06:27 +02:00
|
|
|
espconn_recv_unhold(&ud->pesp_conn);
|
2016-12-31 13:14:03 +01:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tls_socket_getpeer( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
|
|
|
|
2020-04-07 14:06:27 +02:00
|
|
|
if(ud->pesp_conn.proto.tcp && ud->pesp_conn.proto.tcp->remote_port != 0){
|
2016-12-31 13:14:03 +01:00
|
|
|
char temp[20] = {0};
|
2020-04-07 14:06:27 +02:00
|
|
|
sprintf(temp, IPSTR, IP2STR( &(ud->pesp_conn.proto.tcp->remote_ip) ) );
|
2016-12-31 13:14:03 +01:00
|
|
|
lua_pushstring( L, temp );
|
2020-04-07 14:06:27 +02:00
|
|
|
lua_pushinteger( L, ud->pesp_conn.proto.tcp->remote_port );
|
2016-12-31 13:14:03 +01:00
|
|
|
} else {
|
|
|
|
lua_pushnil( L );
|
|
|
|
lua_pushnil( L );
|
|
|
|
}
|
|
|
|
return 2;
|
|
|
|
}
|
2020-04-27 02:13:38 +02:00
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
static int tls_socket_close( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
2020-04-07 14:06:27 +02:00
|
|
|
if (ud->pesp_conn.proto.tcp) {
|
|
|
|
espconn_secure_disconnect(&ud->pesp_conn);
|
2016-12-31 13:14:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2020-04-27 02:13:38 +02:00
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
static int tls_socket_delete( lua_State *L ) {
|
|
|
|
tls_socket_ud *ud = (tls_socket_ud *)luaL_checkudata(L, 1, "tls.socket");
|
2020-04-07 14:06:27 +02:00
|
|
|
if (ud->pesp_conn.proto.tcp) {
|
|
|
|
espconn_secure_disconnect(&ud->pesp_conn);
|
|
|
|
free(ud->pesp_conn.proto.tcp);
|
|
|
|
ud->pesp_conn.proto.tcp = NULL;
|
2016-12-31 13:14:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_connect_ref);
|
|
|
|
ud->cb_connect_ref = LUA_NOREF;
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_disconnect_ref);
|
|
|
|
ud->cb_disconnect_ref = LUA_NOREF;
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_reconnect_ref);
|
|
|
|
ud->cb_reconnect_ref = LUA_NOREF;
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_dns_ref);
|
|
|
|
ud->cb_dns_ref = LUA_NOREF;
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_receive_ref);
|
|
|
|
ud->cb_receive_ref = LUA_NOREF;
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_sent_ref);
|
|
|
|
ud->cb_sent_ref = LUA_NOREF;
|
|
|
|
|
|
|
|
lua_gc(L, LUA_GCSTOP, 0);
|
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ud->self_ref);
|
|
|
|
ud->self_ref = LUA_NOREF;
|
|
|
|
lua_gc(L, LUA_GCRESTART, 0);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns NULL on success, error message otherwise
|
|
|
|
static const char *append_pem_blob(const char *pem, const char *type, uint8_t **buffer_p, uint8_t *buffer_limit, const char *name) {
|
|
|
|
char unb64[256];
|
|
|
|
memset(unb64, 0xff, sizeof(unb64));
|
|
|
|
int i;
|
|
|
|
for (i = 0; i < 64; i++) {
|
|
|
|
unb64["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!pem) {
|
|
|
|
return "No PEM blob";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scan for -----BEGIN CERT
|
|
|
|
pem = strstr(pem, "-----BEGIN ");
|
|
|
|
if (!pem) {
|
|
|
|
return "No PEM header";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (strncmp(pem + 11, type, strlen(type))) {
|
|
|
|
return "Wrong PEM type";
|
|
|
|
}
|
|
|
|
|
|
|
|
pem = strchr(pem, '\n');
|
|
|
|
if (!pem) {
|
|
|
|
return "Incorrect PEM format";
|
|
|
|
}
|
|
|
|
//
|
|
|
|
// Base64 encoded data starts here
|
|
|
|
// Get all the base64 data into a single buffer....
|
|
|
|
// We will use the back end of the buffer....
|
|
|
|
//
|
|
|
|
|
|
|
|
uint8_t *buffer = *buffer_p;
|
|
|
|
|
|
|
|
uint8_t *dest = buffer + 32 + 2; // Leave space for name and length
|
|
|
|
int bitcount = 0;
|
|
|
|
int accumulator = 0;
|
|
|
|
for (; *pem && dest < buffer_limit; pem++) {
|
|
|
|
int val = unb64[*(uint8_t*) pem];
|
|
|
|
if (val & 0xC0) {
|
|
|
|
// not a base64 character
|
|
|
|
if (isspace(*(uint8_t*) pem)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (*pem == '=') {
|
|
|
|
// just ignore -- at the end
|
|
|
|
bitcount = 0;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (*pem == '-') {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return "Invalid character in PEM";
|
|
|
|
} else {
|
|
|
|
bitcount += 6;
|
|
|
|
accumulator = (accumulator << 6) + val;
|
|
|
|
if (bitcount >= 8) {
|
|
|
|
bitcount -= 8;
|
|
|
|
*dest++ = accumulator >> bitcount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (dest >= buffer_limit || strncmp(pem, "-----END ", 9) || strncmp(pem + 9, type, strlen(type)) || bitcount) {
|
|
|
|
return "Invalid PEM format data";
|
|
|
|
}
|
|
|
|
size_t len = dest - (buffer + 32 + 2);
|
|
|
|
|
|
|
|
memset(buffer, 0, 32);
|
|
|
|
strcpy(buffer, name);
|
|
|
|
buffer[32] = len & 0xff;
|
|
|
|
buffer[33] = (len >> 8) & 0xff;
|
|
|
|
*buffer_p = dest;
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2019-02-17 19:26:29 +01:00
|
|
|
static const char *fill_page_with_pem(lua_State *L, const unsigned char *flash_memory, int flash_offset, const char **types, const char **names)
|
2016-12-31 13:14:03 +01:00
|
|
|
{
|
|
|
|
uint8_t *buffer = luaM_malloc(L, INTERNAL_FLASH_SECTOR_SIZE);
|
|
|
|
uint8_t *buffer_base = buffer;
|
|
|
|
uint8_t *buffer_limit = buffer + INTERNAL_FLASH_SECTOR_SIZE;
|
|
|
|
|
|
|
|
int argno;
|
|
|
|
|
|
|
|
for (argno = 1; argno <= lua_gettop(L) && types[argno - 1]; argno++) {
|
|
|
|
const char *pem = lua_tostring(L, argno);
|
|
|
|
|
|
|
|
const char *error = append_pem_blob(pem, types[argno - 1], &buffer, buffer_limit, names[argno - 1]);
|
|
|
|
if (error) {
|
|
|
|
luaM_free(L, buffer_base);
|
|
|
|
return error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
memset(buffer, 0xff, buffer_limit - buffer);
|
|
|
|
|
|
|
|
// Lets see if it matches what is already there....
|
2019-07-21 23:58:21 +02:00
|
|
|
if (memcmp(buffer_base, flash_memory, INTERNAL_FLASH_SECTOR_SIZE) != 0) {
|
2016-12-31 13:14:03 +01:00
|
|
|
// Starts being dangerous
|
|
|
|
if (platform_flash_erase_sector(flash_offset / INTERNAL_FLASH_SECTOR_SIZE) != PLATFORM_OK) {
|
|
|
|
luaM_free(L, buffer_base);
|
|
|
|
return "Failed to erase sector";
|
|
|
|
}
|
|
|
|
if (platform_s_flash_write(buffer_base, flash_offset, INTERNAL_FLASH_SECTOR_SIZE) != INTERNAL_FLASH_SECTOR_SIZE) {
|
|
|
|
luaM_free(L, buffer_base);
|
|
|
|
return "Failed to write sector";
|
|
|
|
}
|
|
|
|
// ends being dangerous
|
|
|
|
}
|
|
|
|
|
|
|
|
luaM_free(L, buffer_base);
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2020-02-01 22:24:12 +01:00
|
|
|
// Lua: tls.cert.auth(PEM data [, PEM data] )
|
|
|
|
// Lua: tls.cert.auth(true / false)
|
2016-12-31 13:14:03 +01:00
|
|
|
static int tls_cert_auth(lua_State *L)
|
|
|
|
{
|
2020-04-07 14:06:27 +02:00
|
|
|
if (ssl_client_options.cert_auth_callback != LUA_NOREF) {
|
2020-06-16 09:19:55 +02:00
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ssl_client_options.cert_auth_callback);
|
2020-04-07 14:06:27 +02:00
|
|
|
ssl_client_options.cert_auth_callback = LUA_NOREF;
|
|
|
|
}
|
2020-06-16 09:19:55 +02:00
|
|
|
if (lua_type(L, 1) == LUA_TFUNCTION) {
|
|
|
|
ssl_client_options.cert_auth_callback = luaL_ref(L, LUA_REGISTRYINDEX);
|
2020-04-07 14:06:27 +02:00
|
|
|
lua_pushboolean(L, true);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (lua_type(L, 1) != LUA_TNIL) {
|
|
|
|
platform_print_deprecation_note("tls.cert.auth's old interface", "soon");
|
|
|
|
}
|
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
int enable;
|
|
|
|
|
|
|
|
uint32_t flash_offset = platform_flash_mapped2phys((uint32_t) &tls_client_cert_area[0]);
|
|
|
|
if ((flash_offset & 0xfff) || flash_offset > 0xff000 || INTERNAL_FLASH_SECTOR_SIZE != 0x1000) {
|
|
|
|
// THis should never happen
|
|
|
|
return luaL_error( L, "bad offset" );
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lua_type(L, 1) == LUA_TSTRING) {
|
|
|
|
const char *types[3] = { "CERTIFICATE", "RSA PRIVATE KEY", NULL };
|
|
|
|
const char *names[2] = { "certificate", "private_key" };
|
|
|
|
const char *error = fill_page_with_pem(L, &tls_client_cert_area[0], flash_offset, types, names);
|
|
|
|
if (error) {
|
|
|
|
return luaL_error(L, error);
|
|
|
|
}
|
|
|
|
|
|
|
|
enable = 1;
|
|
|
|
} else {
|
|
|
|
enable = lua_toboolean(L, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool rc;
|
|
|
|
|
|
|
|
if (enable) {
|
|
|
|
// See if there is a cert there
|
|
|
|
if (tls_client_cert_area[0] == 0x00 || tls_client_cert_area[0] == 0xff) {
|
|
|
|
return luaL_error( L, "no certificates found" );
|
|
|
|
}
|
2020-04-07 14:06:27 +02:00
|
|
|
rc = espconn_secure_cert_req_enable(ESPCONN_CLIENT, flash_offset / INTERNAL_FLASH_SECTOR_SIZE);
|
2016-12-31 13:14:03 +01:00
|
|
|
} else {
|
2020-04-07 14:06:27 +02:00
|
|
|
rc = espconn_secure_cert_req_disable(ESPCONN_CLIENT);
|
2016-12-31 13:14:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
lua_pushboolean(L, rc);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-02-01 22:24:12 +01:00
|
|
|
// Lua: tls.cert.verify(PEM data [, PEM data] )
|
|
|
|
// Lua: tls.cert.verify(true / false)
|
2016-12-31 13:14:03 +01:00
|
|
|
static int tls_cert_verify(lua_State *L)
|
|
|
|
{
|
2020-04-07 14:06:27 +02:00
|
|
|
if (ssl_client_options.cert_verify_callback != LUA_NOREF) {
|
2020-06-16 09:19:55 +02:00
|
|
|
luaL_unref(L, LUA_REGISTRYINDEX, ssl_client_options.cert_verify_callback);
|
2020-04-07 14:06:27 +02:00
|
|
|
ssl_client_options.cert_verify_callback = LUA_NOREF;
|
|
|
|
}
|
2020-06-16 09:19:55 +02:00
|
|
|
if (lua_type(L, 1) == LUA_TFUNCTION) {
|
|
|
|
ssl_client_options.cert_verify_callback = luaL_ref(L, LUA_REGISTRYINDEX);
|
2020-04-07 14:06:27 +02:00
|
|
|
lua_pushboolean(L, true);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (lua_type(L, 1) != LUA_TNIL) {
|
|
|
|
platform_print_deprecation_note("tls.cert.verify's old interface", "soon");
|
|
|
|
}
|
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
int enable;
|
|
|
|
|
|
|
|
uint32_t flash_offset = platform_flash_mapped2phys((uint32_t) &tls_server_cert_area[0]);
|
|
|
|
if ((flash_offset & 0xfff) || flash_offset > 0xff000 || INTERNAL_FLASH_SECTOR_SIZE != 0x1000) {
|
|
|
|
// THis should never happen
|
|
|
|
return luaL_error( L, "bad offset" );
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lua_type(L, 1) == LUA_TSTRING) {
|
|
|
|
const char *types[2] = { "CERTIFICATE", NULL };
|
|
|
|
const char *names[1] = { "certificate" };
|
|
|
|
const char *error = fill_page_with_pem(L, &tls_server_cert_area[0], flash_offset, types, names);
|
|
|
|
if (error) {
|
|
|
|
return luaL_error(L, error);
|
|
|
|
}
|
|
|
|
|
|
|
|
enable = 1;
|
|
|
|
} else {
|
|
|
|
enable = lua_toboolean(L, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool rc;
|
|
|
|
|
|
|
|
if (enable) {
|
|
|
|
// See if there is a cert there
|
|
|
|
if (tls_server_cert_area[0] == 0x00 || tls_server_cert_area[0] == 0xff) {
|
|
|
|
return luaL_error( L, "no certificates found" );
|
|
|
|
}
|
2020-04-07 14:06:27 +02:00
|
|
|
rc = espconn_secure_ca_enable(ESPCONN_CLIENT, flash_offset / INTERNAL_FLASH_SECTOR_SIZE);
|
2016-12-31 13:14:03 +01:00
|
|
|
} else {
|
2020-04-07 14:06:27 +02:00
|
|
|
rc = espconn_secure_ca_disable(ESPCONN_CLIENT);
|
2016-12-31 13:14:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
lua_pushboolean(L, rc);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2018-03-03 23:28:26 +01:00
|
|
|
#if defined(MBEDTLS_DEBUG_C)
|
|
|
|
static int tls_set_debug_threshold(lua_State *L) {
|
|
|
|
mbedtls_debug_set_threshold(luaL_checkint( L, 1 ));
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2020-04-27 02:13:38 +02:00
|
|
|
|
|
|
|
LROT_BEGIN(tls_socket, NULL, LROT_MASK_GC_INDEX)
|
|
|
|
LROT_FUNCENTRY( __gc, tls_socket_delete )
|
|
|
|
LROT_TABENTRY( __index, tls_socket )
|
2019-05-08 13:08:20 +02:00
|
|
|
LROT_FUNCENTRY( connect, tls_socket_connect )
|
|
|
|
LROT_FUNCENTRY( close, tls_socket_close )
|
|
|
|
LROT_FUNCENTRY( on, tls_socket_on )
|
|
|
|
LROT_FUNCENTRY( send, tls_socket_send )
|
|
|
|
LROT_FUNCENTRY( hold, tls_socket_hold )
|
|
|
|
LROT_FUNCENTRY( unhold, tls_socket_unhold )
|
|
|
|
LROT_FUNCENTRY( getpeer, tls_socket_getpeer )
|
2020-04-27 02:13:38 +02:00
|
|
|
LROT_END(tls_socket, NULL, LROT_MASK_GC_INDEX)
|
2019-05-08 13:08:20 +02:00
|
|
|
|
|
|
|
|
2020-04-27 02:13:38 +02:00
|
|
|
LROT_BEGIN(tls_cert, NULL, LROT_MASK_INDEX)
|
|
|
|
LROT_TABENTRY( __index, tls_cert )
|
2019-05-08 13:08:20 +02:00
|
|
|
LROT_FUNCENTRY( verify, tls_cert_verify )
|
|
|
|
LROT_FUNCENTRY( auth, tls_cert_auth )
|
2020-04-27 02:13:38 +02:00
|
|
|
LROT_END(tls_cert, NULL, LROT_MASK_INDEX)
|
2019-05-08 13:08:20 +02:00
|
|
|
|
|
|
|
|
2020-04-27 02:13:38 +02:00
|
|
|
LROT_BEGIN(tls, NULL, 0)
|
2019-05-08 13:08:20 +02:00
|
|
|
LROT_FUNCENTRY( createConnection, tls_socket_create )
|
2018-03-03 23:28:26 +01:00
|
|
|
#if defined(MBEDTLS_DEBUG_C)
|
2019-05-08 13:08:20 +02:00
|
|
|
LROT_FUNCENTRY( setDebug, tls_set_debug_threshold )
|
2018-03-03 23:28:26 +01:00
|
|
|
#endif
|
2019-05-08 13:08:20 +02:00
|
|
|
LROT_TABENTRY( cert, tls_cert )
|
2020-04-27 02:13:38 +02:00
|
|
|
LROT_END(tls, NULL, 0)
|
2019-05-08 13:08:20 +02:00
|
|
|
|
2016-12-31 13:14:03 +01:00
|
|
|
|
|
|
|
int luaopen_tls( lua_State *L ) {
|
2019-05-08 13:08:20 +02:00
|
|
|
luaL_rometatable(L, "tls.socket", LROT_TABLEREF(tls_socket));
|
2016-12-31 13:14:03 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-05-08 13:08:20 +02:00
|
|
|
NODEMCU_MODULE(TLS, "tls", tls, luaopen_tls);
|
2016-12-31 13:14:03 +01:00
|
|
|
#endif
|