diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index beba2bc8..bb0851ee 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -1411,29 +1411,47 @@ // Not clearing it will result in latest values for each field being sent every time #endif +#ifndef THINGSPEAK_USE_ASYNC #define THINGSPEAK_USE_ASYNC 1 // Use AsyncClient instead of WiFiClientSecure +#endif // THINGSPEAK OVER SSL // Using THINGSPEAK over SSL works well but generates problems with the web interface, // so you should compile it with WEB_SUPPORT to 0. // When THINGSPEAK_USE_ASYNC is 1, requires EspAsyncTCP to be built with ASYNC_TCP_SSL_ENABLED=1 and ESP8266 Arduino Core >= 2.4.0. +// When THINGSPEAK_USE_ASYNC is 0, requires Arduino Core >= 2.6.0 and SECURE_CLIENT_BEARSSL + +#ifndef THINGSPEAK_USE_SSL #define THINGSPEAK_USE_SSL 0 // Use secure connection +#endif + +#ifndef THINGSPEAK_SECURE_CLIENT_CHECK +#define THINGSPEAK_SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK +#endif + +#ifndef THINGSPEAK_SECURE_CLIENT_MFLN +#define THINGSPEAK_SECURE_CLIENT_MFLN SECURE_CLIENT_MFLN +#endif +#ifndef THINGSPEAK_FINGERPRINT #define THINGSPEAK_FINGERPRINT "78 60 18 44 81 35 BF DF 77 84 D4 0A 22 0D 9B 4E 6C DC 57 2C" +#endif +#ifndef THINGSPEAK_ADDRESS #if THINGSPEAK_USE_SSL #define THINGSPEAK_ADDRESS "https://api.thingspeak.com/update" #else #define THINGSPEAK_ADDRESS "http://api.thingspeak.com/update" #endif - -#define THINGSPEAK_MIN_INTERVAL 15000 // Minimum interval between POSTs (in millis) -#define THINGSPEAK_FIELDS 8 // Number of fields +#endif // ifndef THINGSPEAK_ADDRESS #ifndef THINGSPEAK_TRIES #define THINGSPEAK_TRIES 3 // Number of tries when sending data (minimum 1) #endif +#define THINGSPEAK_MIN_INTERVAL 15000 // Minimum interval between POSTs (in millis) +#define THINGSPEAK_FIELDS 8 // Number of fields + // ----------------------------------------------------------------------------- // SCHEDULER // ----------------------------------------------------------------------------- diff --git a/code/espurna/espurna.ino b/code/espurna/espurna.ino index f50a9d12..e89e68ef 100644 --- a/code/espurna/espurna.ino +++ b/code/espurna/espurna.ino @@ -59,6 +59,7 @@ along with this program. If not, see . #include "web.h" #include "ws.h" +#include "libs/URL.h" #include "libs/HeapStats.h" using void_callback_f = void (*)(); diff --git a/code/espurna/libs/AsyncClientHelpers.h b/code/espurna/libs/AsyncClientHelpers.h new file mode 100644 index 00000000..446d59a8 --- /dev/null +++ b/code/espurna/libs/AsyncClientHelpers.h @@ -0,0 +1,13 @@ +// ----------------------------------------------------------------------------- +// AsyncClient helpers +// ----------------------------------------------------------------------------- + +#pragma once + +enum class AsyncClientState { + Disconnected, + Connecting, + Connected +}; + + diff --git a/code/espurna/libs/URL.h b/code/espurna/libs/URL.h index 43cfe124..f8b786c6 100644 --- a/code/espurna/libs/URL.h +++ b/code/espurna/libs/URL.h @@ -10,6 +10,7 @@ class URL { public: + URL(); URL(const String&); String protocol; @@ -18,11 +19,21 @@ class URL { uint16_t port; private: - String buffer; + void _parse(String); }; +URL::URL() : + protocol(), + host(), + path(), + port(0) +{} -URL::URL(const String& url) : buffer(url) { +URL::URL(const String& string) { + _parse(string); +} + +void URL::_parse(String buffer) { // cut the protocol part int index = buffer.indexOf("://"); @@ -65,3 +76,4 @@ URL::URL(const String& url) : buffer(url) { } } + diff --git a/code/espurna/static/digicert_high_assurance_pem.h b/code/espurna/static/digicert_high_assurance_pem.h new file mode 100644 index 00000000..dca31ebc --- /dev/null +++ b/code/espurna/static/digicert_high_assurance_pem.h @@ -0,0 +1,42 @@ +// https://api.thingspeak.com root issuer + +// Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance EV Root CA +// Validity +// Not Before: Oct 22 12:00:00 2013 GMT +// Not After : Oct 22 12:00:00 2028 GMT +// Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 High Assurance Server CA + +#pragma once + +#include + +const char PROGMEM _ssl_digicert_high_assurance_ev_root_ca[] = R"EOF( +-----BEGIN CERTIFICATE----- +MIIEsTCCA5mgAwIBAgIQBOHnpNxc8vNtwCtCuF0VnzANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTEvMC0GA1UEAxMmRGlnaUNlcnQgU0hBMiBIaWdoIEFzc3Vy +YW5jZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2 +4C/CJAbIbQRf1+8KZAayfSImZRauQkCbztyfn3YHPsMwVYcZuU+UDlqUH1VWtMIC +Kq/QmO4LQNfE0DtyyBSe75CxEamu0si4QzrZCwvV1ZX1QK/IHe1NnF9Xt4ZQaJn1 +itrSxwUfqJfJ3KSxgoQtxq2lnMcZgqaFD15EWCo3j/018QsIJzJa9buLnqS9UdAn +4t07QjOjBSjEuyjMmqwrIw14xnvmXnG3Sj4I+4G3FhahnSMSTeXXkgisdaScus0X +sh5ENWV/UyU50RwKmmMbGZJ0aAo3wsJSSMs5WqK24V3B3aAguCGikyZvFEohQcft +bZvySC/zA/WiaJJTL17jAgMBAAGjggFJMIIBRTASBgNVHRMBAf8ECDAGAQH/AgEA +MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw +NAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy +dC5jb20wSwYDVR0fBEQwQjBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29t +L0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDA9BgNVHSAENjA0MDIG +BFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ +UzAdBgNVHQ4EFgQUUWj/kK8CB3U8zNllZGKiErhZcjswHwYDVR0jBBgwFoAUsT7D +aQP4v0cB1JgmGggC72NkK8MwDQYJKoZIhvcNAQELBQADggEBABiKlYkD5m3fXPwd +aOpKj4PWUS+Na0QWnqxj9dJubISZi6qBcYRb7TROsLd5kinMLYBq8I4g4Xmk/gNH +E+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly +/D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2LSaCYJkJA69aSEaRkCldUxPUd1gJea6zu +xICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF +0wGjIChBWUMo0oHjqvbsezt3tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0Ae +cPUeybQ= +-----END CERTIFICATE----- +)EOF"; diff --git a/code/espurna/thingspeak.h b/code/espurna/thingspeak.h new file mode 100644 index 00000000..14aed294 --- /dev/null +++ b/code/espurna/thingspeak.h @@ -0,0 +1,26 @@ +/* + +THINGSPEAK MODULE + +Copyright (C) 2019 by Xose Pérez + +*/ + +#pragma once + +#if THINGSPEAK_SUPPORT + +#if THINGSPEAK_USE_ASYNC +#include +#endif + +constexpr const size_t tspkDataBufferSize = 256; + +bool tspkEnqueueRelay(unsigned char index, bool status); +bool tspkEnqueueMeasurement(unsigned char index, const char * payload); +void tspkFlush(); + +bool tspkEnabled(); +void tspkSetup(); + +#endif // THINGSPEAK_SUPPORT == 1 diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino index 16263cb9..08900f4b 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -8,16 +8,24 @@ Copyright (C) 2019 by Xose Pérez #if THINGSPEAK_SUPPORT +#include + #include "broker.h" +#include "thingspeak.h" #include "libs/URL.h" +#include "libs/SecureClientHelpers.h" +#include "libs/AsyncClientHelpers.h" -#if THINGSPEAK_USE_ASYNC -#include +#if SECURE_CLIENT != SECURE_CLIENT_NONE + +#if THINGSPEAK_SECURE_CLIENT_INCLUDE_CA +#include "static/thingspeak_client_trusted_root_ca.h" #else -#include +#include "static/digicert_high_assurance_pem.h" +#define _tspk_client_trusted_root_ca _ssl_digicert_high_assurance_ev_root_ca #endif -#define THINGSPEAK_DATA_BUFFER_SIZE 256 +#endif // SECURE_CLIENT != SECURE_CLIENT_NONE const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM = "POST %s HTTP/1.1\r\n" @@ -37,19 +45,32 @@ bool _tspk_flush = false; unsigned long _tspk_last_flush = 0; unsigned char _tspk_tries = THINGSPEAK_TRIES; -class AsyncThingspeak : public AsyncClient -{ - public: +#if THINGSPEAK_USE_ASYNC + +class AsyncThingspeak : public AsyncClient { + public: + URL address; AsyncThingspeak(const String& _url) : address(_url) { }; + + bool connect() { + #if ASYNC_TCP_SSL_ENABLED && THINGSPEAK_USE_SSL + return AsyncClient::connect(address.host.c_str(), address.port, true); + #else + return AsyncClient::connect(address.host.c_str(), address.port); + #endif + } + + bool connect(const String& url) { + address = url; + return connect(); + } }; -AsyncThingspeak * _tspk_client; +AsyncThingspeak* _tspk_client = nullptr; +AsyncClientState _tspk_state = AsyncClientState::Disconnected; -#if THINGSPEAK_USE_ASYNC -bool _tspk_connecting = false; -bool _tspk_connected = false; -#endif +#endif // THINGSPEAK_USE_ASYNC == 1 // ----------------------------------------------------------------------------- @@ -105,7 +126,10 @@ void _tspkConfigure() { _tspk_enabled = false; setSetting("tspkEnabled", 0); } - if (_tspk_enabled && !_tspk_client) _tspkInitClient(getSetting("tspkAddress", THINGSPEAK_ADDRESS)); + + #if THINGSPEAK_USE_ASYNC + if (_tspk_enabled && !_tspk_client) _tspkInitClient(getSetting("tspkAddress", THINGSPEAK_ADDRESS)); + #endif } #if THINGSPEAK_USE_ASYNC @@ -129,8 +153,7 @@ void _tspkInitClient(const String& _url) { _tspk_data = ""; _tspk_client_ts = 0; _tspk_last_flush = millis(); - _tspk_connected = false; - _tspk_connecting = false; + _tspk_state = AsyncClientState::Disconnected; _tspk_client_state = tspk_state_t::NONE; }, nullptr); @@ -201,27 +224,26 @@ void _tspkInitClient(const String& _url) { _tspk_client->onConnect([](void * arg, AsyncClient * client) { - _tspk_connected = true; - _tspk_connecting = false; - AsyncThingspeak* _tspk_client = reinterpret_cast(client); + _tspk_state = AsyncClientState::Disconnected; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), _tspk_client->address.host.c_str(), _tspk_client->address.port); + AsyncThingspeak* tspk_client = reinterpret_cast(client); + DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), tspk_client->address.host.c_str(), tspk_client->address.port); #if THINGSPEAK_USE_SSL uint8_t fp[20] = {0}; sslFingerPrintArray(THINGSPEAK_FINGERPRINT, fp); - SSL * ssl = _tspk_client->getSSL(); + SSL * ssl = tspk_client->getSSL(); if (ssl_match_fingerprint(ssl, fp) != SSL_OK) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n")); } #endif - DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), _tspk_client->address.path.c_str(), _tspk_data.c_str()); - char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + _tspk_client->address.path.length() + _tspk_client->address.host.length() + 1]; + DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), tspk_client->address.path.c_str(), _tspk_data.c_str()); + char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + tspk_client->address.path.length() + tspk_client->address.host.length() + 1]; snprintf_P(headers, sizeof(headers), THINGSPEAK_REQUEST_TEMPLATE, - _tspk_client->address.path.c_str(), - _tspk_client->address.host.c_str(), + tspk_client->address.path.c_str(), + tspk_client->address.host.c_str(), _tspk_data.length() ); @@ -232,22 +254,16 @@ void _tspkInitClient(const String& _url) { } -void _tspkPost() { +void _tspkPost(const String& address) { - if (_tspk_connected || _tspk_connecting) return; + if (_tspk_state != AsyncClientState::Disconnected) return; _tspk_client_ts = millis(); - - #if THINGSPEAK_USE_SSL - bool connected = _tspk_client->connect(_tspk_host.c_str(), _tspk_port, THINGSPEAK_USE_SSL); - #else - _tspk_client->address = URL(getSetting("tspkAddress", THINGSPEAK_ADDRESS)); - bool connected = _tspk_client->connect(_tspk_client->address.host.c_str(), _tspk_client->address.port); - #endif - - _tspk_connecting = connected; + _tspk_state = _tspk_client->connect(address) + ? AsyncClientState::Connecting + : AsyncClientState::Disconnected; - if (!connected) { + if (_tspk_state == AsyncClientState::Disconnected) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n")); _tspk_client->close(true); } @@ -256,55 +272,95 @@ void _tspkPost() { #else // THINGSPEAK_USE_ASYNC -void _tspkPost() { +#if THINGSPEAK_USE_SSL && (SECURE_CLIENT == SECURE_CLIENT_BEARSSL) + +SecureClientConfig _tspk_sc_config { + "THINGSPEAK", + []() -> int { + return getSetting("tspkScCheck", THINGSPEAK_SECURE_CLIENT_CHECK); + }, + []() -> PGM_P { + return _tspk_client_trusted_root_ca; + }, + []() -> String { + return getSetting("tspkFP", THINGSPEAK_FINGERPRINT); + }, + []() -> uint16_t { + return getSetting("tspkScMFLN", THINGSPEAK_SECURE_CLIENT_MFLN); + }, + true +}; - #if THINGSPEAK_USE_SSL - WiFiClientSecure _tspk_client; - #else - WiFiClient _tspk_client; - #endif +#endif // THINGSPEAK_USE_SSL && SECURE_CLIENT_BEARSSL - if (_tspk_client.connect(_tspk_host.c_str(), _tspk_port)) { +void _tspkPost(WiFiClient* client, const URL& url) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), _tspk_host.c_str(), _tspk_port); + if (!client->connect(url.host.c_str(), url.port)) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n")); + return; + } - if (!_tspk_client.verify(THINGSPEAK_FINGERPRINT, _tspk_host.c_str())) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n")); - } + DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), url.host.c_str(), url.port); + DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), url.path.c_str(), _tspk_data.c_str()); - DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), _tspk_client.path.c_str(), _tspk_data.c_str()); - char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + _tspk_client.path.length() + _tspk_client.host.lengh() + 1]; - snprintf_P(headers, sizeof(headers), - THINGSPEAK_REQUEST_TEMPLATE, - _tspk_client.path.c_str(), - _tspk_client.host.c_str(), - _tspk_data.length() - ); + char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + url.path.length() + url.host.length() + 1]; + snprintf_P(headers, sizeof(headers), + THINGSPEAK_REQUEST_TEMPLATE, + url.path.c_str(), + url.host.c_str(), + _tspk_data.length() + ); - _tspk_client.print(headers); - _tspk_client.print(_tspk_data); + client->print(headers); + client->print(_tspk_data); - nice_delay(100); + nice_delay(100); - String response = _tspk_client.readString(); - int pos = response.indexOf("\r\n\r\n"); - unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); - _tspk_client.stop(); + const auto response = client->readString(); + int pos = response.indexOf("\r\n\r\n"); - _tspk_last_flush = millis(); - if ((0 == code) && _tspk_tries) { - _tspk_flush = true; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries); - } else { - _tspkClearQueue(); - } + unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0; + DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); - return; + client->stop(); + _tspk_last_flush = millis(); + if ((0 == code) && _tspk_tries) { + _tspk_flush = true; + DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries); + } else { + _tspkClearQueue(); } - DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n")); +} + +void _tspkPost(const String& address) { + + const URL url(address); + + #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL + if (url.protocol == "https") { + const int check = _ota_sc_config.on_check(); + if (!ntpSynced() && (check == SECURE_CLIENT_CHECK_CA)) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Time not synced! Cannot use CA validation\n")); + return; + } + + auto client = std::make_unique(_tspk_sc_config); + if (!client->beforeConnected()) { + return; + } + + _tspkPost(&client->get(), url); + return; + } + #endif + + if (url.protocol == "http") { + auto client = std::make_unique(); + _tspkPost(client.get(), url); + return; + } } @@ -333,11 +389,14 @@ void _tspkFlush() { if (!_tspk_flush) return; if (millis() - _tspk_last_flush < THINGSPEAK_MIN_INTERVAL) return; - if (_tspk_connected || _tspk_connecting) return; + + #if THINGSPEAK_USE_ASYNC + if (_tspk_state != AsyncClientState::Disconnected) return; + #endif _tspk_last_flush = millis(); _tspk_flush = false; - _tspk_data.reserve(THINGSPEAK_DATA_BUFFER_SIZE); + _tspk_data.reserve(tspkDataBufferSize); // Walk the fields, numbered 1...THINGSPEAK_FIELDS for (unsigned char id=0; id