mirror of
https://github.com/letscontrolit/ESPEasy.git
synced 2026-07-28 04:07:47 +00:00
2211 lines
65 KiB
C++
2211 lines
65 KiB
C++
#include "../Helpers/Networking.h"
|
|
|
|
#include "../Commands/ExecuteCommand.h"
|
|
#include "../CustomBuild/CompiletimeDefines.h"
|
|
#include "../DataStructs/NodeStruct.h"
|
|
#include "../DataStructs/TimingStats.h"
|
|
#include "../DataTypes/EventValueSource.h"
|
|
#include "../ESPEasyCore/ESPEasy_Log.h"
|
|
#include "../ESPEasyCore/ESPEasy_backgroundtasks.h"
|
|
#include "../../ESPEasy/net/eth/ESPEasyEth.h"
|
|
#include "../../ESPEasy/net/ESPEasyNetwork.h"
|
|
#include "../../ESPEasy/net/wifi/ESPEasyWifi.h"
|
|
#include "../ESPEasyCore/Serial.h"
|
|
#include "../../ESPEasy/net/Globals/ESPEasyWiFiEvent.h"
|
|
#include "../Globals/ESPEasy_Scheduler.h"
|
|
#include "../Globals/ESPEasy_time.h"
|
|
|
|
#ifdef USES_ESPEASY_NOW
|
|
# include "../Globals/ESPEasy_now_handler.h"
|
|
#endif
|
|
|
|
#include "../Globals/EventQueue.h"
|
|
#include "../../ESPEasy/net/Globals/NetworkState.h"
|
|
#include "../Globals/Nodes.h"
|
|
#include "../Globals/ResetFactoryDefaultPref.h"
|
|
#include "../Globals/Settings.h"
|
|
#include "../Helpers/ESPEasy_Storage.h"
|
|
#include "../Helpers/ESPEasy_time_calc.h"
|
|
#include "../Helpers/Hardware.h"
|
|
#include "../Helpers/Misc.h"
|
|
#include "../Helpers/NetworkStatusLED.h"
|
|
#include "../Helpers/Numerical.h"
|
|
#include "../Helpers/StringConverter.h"
|
|
#include "../Helpers/StringProvider.h"
|
|
|
|
#if RESPONSE_PARSER_SUPPORT
|
|
#include "../Helpers/HTTPResponseParser.h"
|
|
#endif
|
|
|
|
#include "../../ESPEasy-Globals.h"
|
|
|
|
#include <IPAddress.h>
|
|
#include <base64.h>
|
|
#include <MD5Builder.h> // for getDigestAuth
|
|
|
|
#include <WiFiUdp.h>
|
|
|
|
#include <lwip/dns.h>
|
|
|
|
|
|
#if FEATURE_DOWNLOAD && defined(ESP32)
|
|
#include <Update.h>
|
|
#endif
|
|
|
|
// Generic Networking routines
|
|
|
|
// Syslog
|
|
// UDP system messaging
|
|
// SSDP
|
|
// #if LWIP_VERSION_MAJOR == 2
|
|
#define IPADDR2STR(addr) (uint8_t)((uint32_t)addr & 0xFF), (uint8_t)(((uint32_t)addr >> 8) & 0xFF), \
|
|
(uint8_t)(((uint32_t)addr >> 16) & 0xFF), (uint8_t)(((uint32_t)addr >> 24) & 0xFF)
|
|
|
|
// #endif
|
|
|
|
#include <lwip/netif.h>
|
|
|
|
#ifdef ESP8266
|
|
# include <lwip/opt.h>
|
|
# include <lwip/udp.h>
|
|
# include <lwip/igmp.h>
|
|
# include <include/UdpContext.h>
|
|
#endif // ifdef ESP8266
|
|
|
|
#ifdef SUPPORT_ARP
|
|
# include <lwip/etharp.h>
|
|
|
|
# ifdef ESP32
|
|
# include <lwip/etharp.h>
|
|
# include <lwip/tcpip.h>
|
|
|
|
void _etharp_gratuitous_func(struct netif *netif) { etharp_gratuitous(netif); }
|
|
|
|
void etharp_gratuitous_r(struct netif *netif) { tcpip_callback_with_block((tcpip_callback_fn)_etharp_gratuitous_func, netif, 0); }
|
|
|
|
# endif // ifdef ESP32
|
|
|
|
#endif // ifdef SUPPORT_ARP
|
|
|
|
#include <vector>
|
|
|
|
|
|
#if FEATURE_ESPEASY_P2P
|
|
|
|
/*********************************************************************************************\
|
|
Send event using UDP message
|
|
\*********************************************************************************************/
|
|
void SendUDPCommand(uint8_t destUnit, const char *data, uint8_t dataLength)
|
|
{
|
|
if (!NetworkConnected(10)) {
|
|
return;
|
|
}
|
|
|
|
if (destUnit != 0)
|
|
{
|
|
sendUDP(destUnit, (const uint8_t *)data, dataLength);
|
|
delay(10);
|
|
} else {
|
|
for (auto it = Nodes.begin(); it != Nodes.end(); ++it) {
|
|
if (it->first != Settings.Unit) {
|
|
sendUDP(it->first, (const uint8_t *)data, dataLength);
|
|
delay(10);
|
|
}
|
|
}
|
|
}
|
|
delay(50);
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
Send UDP message to specific unit (unit 255=broadcast)
|
|
\*********************************************************************************************/
|
|
void sendUDP(uint8_t unit, const uint8_t *data, uint8_t size)
|
|
{
|
|
if (!NetworkConnected(10)) {
|
|
return;
|
|
}
|
|
|
|
IPAddress remoteNodeIP = getIPAddressForUnit(unit);
|
|
|
|
if (remoteNodeIP[0] == 0) {
|
|
return;
|
|
}
|
|
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
|
|
addLogMove(LOG_LEVEL_DEBUG_MORE, strformat(
|
|
F("UDP : Send UDP message to %d (%s)"),
|
|
unit,
|
|
remoteNodeIP.toString().c_str()
|
|
));
|
|
}
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
|
|
statusLED(true);
|
|
FeedSW_watchdog();
|
|
portUDP.beginPacket(remoteNodeIP, Settings.UDPPort);
|
|
portUDP.write(data, size);
|
|
portUDP.endPacket();
|
|
FeedSW_watchdog();
|
|
delay(0);
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
Update UDP port (ESPEasy propiertary protocol)
|
|
\*********************************************************************************************/
|
|
uint16_t lastUsedUDPPort = 0;
|
|
|
|
void stopUDPport()
|
|
{
|
|
if (lastUsedUDPPort == 0) return;
|
|
if (ESPEasy::net::NetworkConnected(true)) {
|
|
portUDP.stop();
|
|
}
|
|
lastUsedUDPPort = 0;
|
|
}
|
|
|
|
void updateUDPport(bool force)
|
|
{
|
|
|
|
if (!force && (Settings.UDPPort == lastUsedUDPPort)) {
|
|
return;
|
|
}
|
|
|
|
// FIXME TD-er: For now, all interfaces that can handle UDP are the same as those that need the webserver to run
|
|
// May need a separate NWPlugin function for check if default interface can handle UDP p2p packets
|
|
// Or we may need to look into AsyncUDP as that allows to send to specific interfaces.
|
|
const bool connected = ESPEasy::net::NWPluginCall(NWPlugin::Function::NWPLUGIN_WEBSERVER_SHOULD_RUN);
|
|
//const bool connected = ESPEasy::net::NetworkConnected();
|
|
if (!connected || (Settings.UDPPort != lastUsedUDPPort)) {
|
|
if (lastUsedUDPPort != 0) {
|
|
if (connected)
|
|
portUDP.stop();
|
|
lastUsedUDPPort = 0;
|
|
#ifndef BUILD_NO_DEBUG
|
|
addLogMove(LOG_LEVEL_INFO, concat(F("UDP : Stop listening on port "), Settings.UDPPort));
|
|
#endif
|
|
}
|
|
if (!connected) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((Settings.UDPPort != lastUsedUDPPort) && Settings.UDPPort != 0) {
|
|
if (portUDP.begin(Settings.UDPPort) == 0) {
|
|
#ifndef BUILD_NO_DEBUG
|
|
if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
|
|
addLogMove(LOG_LEVEL_ERROR, concat(F("UDP : Cannot bind to ESPEasy p2p UDP port "), Settings.UDPPort));
|
|
}
|
|
#endif
|
|
} else {
|
|
lastUsedUDPPort = Settings.UDPPort;
|
|
#ifndef BUILD_NO_DEBUG
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
addLogMove(LOG_LEVEL_INFO, concat(F("UDP : Start listening on port "), Settings.UDPPort));
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
Check UDP messages (ESPEasy propiertary protocol)
|
|
\*********************************************************************************************/
|
|
boolean runningUPDCheck = false;
|
|
|
|
void checkUDP()
|
|
{
|
|
if (!ESPEasy::net::NetworkConnected(true)) {
|
|
portUDP.PR_9453_FLUSH_TO_CLEAR();
|
|
portUDP.stop();
|
|
return;
|
|
}
|
|
|
|
if (Settings.UDPPort == 0) {
|
|
return;
|
|
}
|
|
|
|
if (runningUPDCheck) {
|
|
return;
|
|
}
|
|
START_TIMER
|
|
|
|
runningUPDCheck = true;
|
|
|
|
// UDP events
|
|
int packetSize = portUDP.parsePacket();
|
|
|
|
if (packetSize > 0 /*&& portUDP.remotePort() == Settings.UDPPort*/)
|
|
{
|
|
statusLED(true);
|
|
|
|
IPAddress remoteIP = portUDP.remoteIP();
|
|
|
|
if (portUDP.remotePort() == 123)
|
|
{
|
|
// unexpected NTP reply, drop for now...
|
|
while (portUDP.available()) {
|
|
// Do not call portUDP.flush() as that's meant to sending the packet (on ESP8266)
|
|
portUDP.read();
|
|
}
|
|
|
|
runningUPDCheck = false;
|
|
return;
|
|
}
|
|
|
|
// UDP_PACKETSIZE_MAX should be as small as possible but still enough to hold all
|
|
// data for PLUGIN_UDP_IN or CPLUGIN_UDP_IN calls
|
|
// This node may also receive other UDP packets which may be quite large
|
|
// and then crash due to memory allocation failures
|
|
if ((packetSize >= 2) && (packetSize < UDP_PACKETSIZE_MAX)) {
|
|
// Allocate buffer to process packet.
|
|
// Resize it to be 1 byte larger so we can 0-terminate it
|
|
// in case it is some plain text string
|
|
std::vector<char> packetBuffer;
|
|
packetBuffer.resize(packetSize + 1);
|
|
|
|
if (packetBuffer.size() >= static_cast<size_t>(packetSize)) {
|
|
memset(&packetBuffer[0], 0, packetSize + 1);
|
|
int len = portUDP.read(&packetBuffer[0], packetSize);
|
|
|
|
if (len >= 2) {
|
|
if (static_cast<uint8_t>(packetBuffer[0]) != 255)
|
|
{
|
|
packetBuffer[len] = 0;
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
|
|
addLogMove(LOG_LEVEL_DEBUG,
|
|
strformat(F("UDP : %s Command: %s"),
|
|
formatIP(remoteIP, true).c_str(),
|
|
wrapWithQuotesIfContainsParameterSeparatorChar(String(&packetBuffer[0])).c_str()
|
|
));
|
|
}
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
ExecuteCommand_all({ EventValueSource::Enum::VALUE_SOURCE_UDP, &packetBuffer[0] }, true);
|
|
}
|
|
else
|
|
{
|
|
// binary data!
|
|
switch (packetBuffer[1])
|
|
{
|
|
case 1: // sysinfo message
|
|
{
|
|
if (len < 13) {
|
|
break;
|
|
}
|
|
int copy_length = sizeof(NodeStruct);
|
|
|
|
// Older versions sent 80 bytes, regardless of the size of NodeStruct
|
|
// Make sure the extra data received is ignored as it was also not initialized
|
|
if (len == 80) {
|
|
copy_length = 56;
|
|
}
|
|
|
|
if (copy_length > (len - 2)) {
|
|
copy_length = (len - 2);
|
|
}
|
|
NodeStruct received;
|
|
memcpy(&received, &packetBuffer[2], copy_length);
|
|
|
|
if (received.validate(remoteIP)) {
|
|
Nodes.addNode(received); // Create a new element when not present
|
|
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
|
|
addLogMove(LOG_LEVEL_DEBUG_MORE,
|
|
strformat(F("UDP : %s (%d) %s,%s,%d"),
|
|
formatIP(remoteIP).c_str(),
|
|
received.unit,
|
|
received.STA_MAC().toString().c_str(),
|
|
formatIP(received.IP(), true).c_str(),
|
|
received.unit));
|
|
}
|
|
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
struct EventStruct TempEvent;
|
|
TempEvent.Data = reinterpret_cast<uint8_t *>(&packetBuffer[0]);
|
|
TempEvent.Par1 = remoteIP[3];
|
|
TempEvent.Par2 = len;
|
|
String dummy;
|
|
|
|
// TD-er: Disabled the PLUGIN_UDP_IN call as we don't have any plugin using this.
|
|
// PluginCall(PLUGIN_UDP_IN, &TempEvent, dummy);
|
|
CPluginCall(CPlugin::Function::CPLUGIN_UDP_IN, &TempEvent);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flush any remaining content of the packet.
|
|
while (portUDP.available()) {
|
|
// Do not call portUDP.flush() as that's meant to sending the packet (on ESP8266)
|
|
portUDP.read();
|
|
}
|
|
runningUPDCheck = false;
|
|
STOP_TIMER(CHECK_UDP);
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
Get formatted IP address for unit
|
|
formatcodes: 0 = default toString(), 1 = empty string when invalid, 2 = 0 when invalid
|
|
\*********************************************************************************************/
|
|
String formatUnitToIPAddress(uint8_t unit, uint8_t formatCode) {
|
|
IPAddress unitIPAddress = getIPAddressForUnit(unit);
|
|
|
|
if (unitIPAddress[0] == 0) { // Invalid?
|
|
switch (formatCode)
|
|
{
|
|
case 1: // Return empty string
|
|
{
|
|
return EMPTY_STRING;
|
|
}
|
|
case 2: // Return "0"
|
|
{
|
|
return String('0');
|
|
}
|
|
}
|
|
}
|
|
return formatIP(unitIPAddress);
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
Get IP address for unit
|
|
\*********************************************************************************************/
|
|
IPAddress getIPAddressForUnit(uint8_t unit) {
|
|
if (unit == 255) {
|
|
const IPAddress ip(255, 255, 255, 255);
|
|
return ip;
|
|
}
|
|
auto it = Nodes.find(unit);
|
|
|
|
if ((it == Nodes.end()) || (it->second.ip[0] == 0)) {
|
|
IPAddress ip;
|
|
return ip;
|
|
}
|
|
# if FEATURE_USE_IPV6
|
|
|
|
/*
|
|
// FIXME TD-er: for now do not try to send to IPv6
|
|
if (it->second.hasIPv6_mac_based_link_local) {
|
|
return it->second.IPv6_link_local();
|
|
}
|
|
if (it->second.hasIPv6_mac_based_link_global) {
|
|
return it->second.IPv6_global();
|
|
}
|
|
*/
|
|
# endif // if FEATURE_USE_IPV6
|
|
return it->second.IP();
|
|
}
|
|
|
|
String getNameForUnit(uint8_t unit) {
|
|
auto it = Nodes.find(unit);
|
|
|
|
if ((it == Nodes.end()) || it->second.getNodeName().isEmpty()) {
|
|
return EMPTY_STRING;
|
|
}
|
|
return it->second.getNodeName();
|
|
}
|
|
|
|
long getAgeForUnit(uint8_t unit) {
|
|
auto it = Nodes.find(unit);
|
|
|
|
if (it == Nodes.end()) {
|
|
return -1000; // milliseconds, negative == unknown
|
|
}
|
|
return static_cast<long>(it->second.getAge());
|
|
}
|
|
|
|
uint16_t getBuildnrForUnit(uint8_t unit) {
|
|
auto it = Nodes.find(unit);
|
|
|
|
if ((it == Nodes.end()) || (it->second.build == 0)) {
|
|
return 0;
|
|
}
|
|
return it->second.build;
|
|
}
|
|
|
|
float getLoadForUnit(uint8_t unit) {
|
|
auto it = Nodes.find(unit);
|
|
|
|
if (it == Nodes.end()) {
|
|
return 0.0f;
|
|
}
|
|
return it->second.getLoad();
|
|
}
|
|
|
|
uint8_t getTypeForUnit(uint8_t unit) {
|
|
auto it = Nodes.find(unit);
|
|
|
|
if (it == Nodes.end()) {
|
|
return 0;
|
|
}
|
|
return it->second.nodeType;
|
|
}
|
|
|
|
String getTypeStringForUnit(uint8_t unit) {
|
|
auto it = Nodes.find(unit);
|
|
|
|
if (it == Nodes.end()) {
|
|
return EMPTY_STRING;
|
|
}
|
|
return it->second.getNodeTypeDisplayString();
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
Refresh aging for remote units, drop if too old...
|
|
\*********************************************************************************************/
|
|
void refreshNodeList()
|
|
{
|
|
unsigned long max_age;
|
|
const unsigned long max_age_allowed = 10 * 60 * 1000; // 10 minutes
|
|
|
|
Nodes.refreshNodeList(max_age_allowed, max_age);
|
|
|
|
# ifdef USES_ESPEASY_NOW
|
|
# ifdef ESP8266
|
|
|
|
// FIXME TD-er: Do not perform regular scans on ESP32 as long as we cannot scan per channel
|
|
if (!Nodes.isEndpoint()) {
|
|
ESPEasy::net::wifi::WifiScan(true, Nodes.getESPEasyNOW_channel());
|
|
}
|
|
# endif // ifdef ESP8266
|
|
# endif // ifdef USES_ESPEASY_NOW
|
|
|
|
if (max_age > (0.75 * max_age_allowed)) {
|
|
Scheduler.sendGratuitousARP_now();
|
|
}
|
|
sendSysInfoUDP(1);
|
|
# ifdef USES_ESPEASY_NOW
|
|
|
|
if (Nodes.recentlyBecameDistanceZero()) {
|
|
// Send to all channels
|
|
ESPEasy_now_handler.sendDiscoveryAnnounce(-1);
|
|
} else {
|
|
ESPEasy_now_handler.sendDiscoveryAnnounce();
|
|
}
|
|
ESPEasy_now_handler.sendNTPquery();
|
|
ESPEasy_now_handler.sendTraceRoute();
|
|
# endif // ifdef USES_ESPEASY_NOW
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
Broadcast system info to other nodes. (to update node lists)
|
|
\*********************************************************************************************/
|
|
void sendSysInfoUDP(uint8_t repeats)
|
|
{
|
|
if ((Settings.UDPPort == 0) || !NetworkConnected(10)) {
|
|
return;
|
|
}
|
|
|
|
// 1 byte 'binary token 255'
|
|
// 1 byte id '1'
|
|
// NodeStruct object (packed data struct)
|
|
|
|
// send my info to the world...
|
|
# ifndef BUILD_NO_DEBUG
|
|
addLog(LOG_LEVEL_DEBUG_MORE, F("UDP : Send Sysinfo message"));
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
|
|
const NodeStruct *thisNode = Nodes.getThisNode();
|
|
|
|
if (thisNode == nullptr) {
|
|
// Should not happen
|
|
return;
|
|
}
|
|
|
|
// Prepare UDP packet to send
|
|
constexpr size_t data_size = sizeof(NodeStruct) + 2;
|
|
uint8_t data[data_size] = { 0 };
|
|
data[0] = 255;
|
|
data[1] = 1;
|
|
memcpy(&data[2], thisNode, sizeof(NodeStruct));
|
|
|
|
for (uint8_t counter = 0; counter < repeats; counter++)
|
|
{
|
|
statusLED(true);
|
|
|
|
IPAddress broadcastIP(255, 255, 255, 255);
|
|
FeedSW_watchdog();
|
|
portUDP.beginPacket(broadcastIP, Settings.UDPPort);
|
|
portUDP.write(data, data_size);
|
|
portUDP.endPacket();
|
|
|
|
if (counter < (repeats - 1)) {
|
|
// FIXME TD-er: Must use scheduler to send out messages, not using delay
|
|
delay(100);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // FEATURE_ESPEASY_P2P
|
|
|
|
#if defined(ESP8266)
|
|
|
|
# if FEATURE_SSDP
|
|
|
|
/********************************************************************************************\
|
|
Respond to HTTP XML requests for SSDP information
|
|
\*********************************************************************************************/
|
|
void SSDP_schema() {
|
|
if (!NetworkConnected(10)) {
|
|
return;
|
|
}
|
|
|
|
const IPAddress ip = ESPEasy::net::NetworkLocalIP();
|
|
const uint32_t chipId = ESP.getChipId();
|
|
char uuid[64];
|
|
|
|
sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%02x%02x%02x"),
|
|
(uint16_t)((chipId >> 16) & 0xff),
|
|
(uint16_t)((chipId >> 8) & 0xff),
|
|
(uint16_t)chipId & 0xff);
|
|
|
|
web_server.client().print(F(
|
|
"HTTP/1.1 200 OK\r\n"
|
|
"Content-Type: text/xml\r\n"
|
|
"Connection: close\r\n"
|
|
"Access-Control-Allow-Origin: *\r\n"
|
|
"\r\n"
|
|
"<?xml version=\"1.0\"?>"
|
|
"<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
|
|
"<specVersion>"
|
|
"<major>1</major>"
|
|
"<minor>0</minor>"
|
|
"</specVersion>"
|
|
"<URLBase>http://"));
|
|
|
|
web_server.client().print(formatIP(ip));
|
|
web_server.client().print(F(":80/</URLBase>"
|
|
"<device>"
|
|
"<deviceType>urn:schemas-upnp-org:device:BinaryLight:1</deviceType>"
|
|
"<friendlyName>"));
|
|
web_server.client().print(Settings.getName());
|
|
web_server.client().print(F("</friendlyName>"
|
|
"<presentationURL>/</presentationURL>"
|
|
"<serialNumber>"));
|
|
web_server.client().print(String(ESP.getChipId()));
|
|
web_server.client().print(F("</serialNumber>"
|
|
"<modelName>ESP Easy</modelName>"
|
|
"<modelNumber>"));
|
|
web_server.client().print(getValue(LabelType::GIT_BUILD));
|
|
web_server.client().print(F("</modelNumber>"
|
|
"<modelURL>http://www.letscontrolit.com</modelURL>"
|
|
"<manufacturer>http://www.letscontrolit.com</manufacturer>"
|
|
"<manufacturerURL>http://www.letscontrolit.com</manufacturerURL>"
|
|
"<UDN>uuid:"));
|
|
web_server.client().print(String(uuid));
|
|
web_server.client().print(F("</UDN></device>"
|
|
"</root>\r\n"
|
|
"\r\n"));
|
|
}
|
|
|
|
/********************************************************************************************\
|
|
Global SSDP stuff
|
|
\*********************************************************************************************/
|
|
|
|
UdpContext *_server;
|
|
|
|
IPAddress _respondToAddr;
|
|
uint16_t _respondToPort;
|
|
|
|
bool _pending;
|
|
unsigned short _delay;
|
|
unsigned long _process_time;
|
|
unsigned long _notify_time;
|
|
|
|
# define SSDP_INTERVAL 1200
|
|
# define SSDP_PORT 1900
|
|
# define SSDP_METHOD_SIZE 10
|
|
# define SSDP_URI_SIZE 2
|
|
# define SSDP_BUFFER_SIZE 64
|
|
# define SSDP_MULTICAST_TTL 2
|
|
|
|
static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250);
|
|
|
|
/********************************************************************************************\
|
|
Launch SSDP listener and send initial notify
|
|
\*********************************************************************************************/
|
|
bool SSDP_begin() {
|
|
_pending = false;
|
|
|
|
if (_server != nullptr) {
|
|
_server->unref();
|
|
|
|
// FIXME TD-er: Shouldn't this also call delete _server ?
|
|
|
|
_server = nullptr;
|
|
}
|
|
|
|
constexpr unsigned size = sizeof(UdpContext);
|
|
void *ptr = special_calloc(1, size);
|
|
|
|
if (ptr != nullptr) {
|
|
_server = new (ptr) UdpContext;
|
|
}
|
|
|
|
if (_server == nullptr) {
|
|
return false;
|
|
}
|
|
_server->ref();
|
|
|
|
ip_addr_t ifaddr;
|
|
|
|
ifaddr.addr = ESPEasy::net::NetworkLocalIP();
|
|
ip_addr_t multicast_addr;
|
|
|
|
multicast_addr.addr = (uint32_t)SSDP_MULTICAST_ADDR;
|
|
|
|
if (igmp_joingroup(&ifaddr, &multicast_addr) != ERR_OK) {
|
|
return false;
|
|
}
|
|
|
|
# ifdef CORE_POST_2_5_0
|
|
|
|
// Core 2.5.0 changed the signature of some UdpContext function.
|
|
if (!_server->listen(IP_ADDR_ANY, SSDP_PORT)) {
|
|
return false;
|
|
}
|
|
|
|
_server->setMulticastInterface(&ifaddr);
|
|
_server->setMulticastTTL(SSDP_MULTICAST_TTL);
|
|
_server->onRx(&SSDP_update);
|
|
|
|
if (!_server->connect(&multicast_addr, SSDP_PORT)) {
|
|
return false;
|
|
}
|
|
# else // ifdef CORE_POST_2_5_0
|
|
|
|
if (!_server->listen(*IP_ADDR_ANY, SSDP_PORT)) {
|
|
return false;
|
|
}
|
|
|
|
_server->setMulticastInterface(ifaddr);
|
|
_server->setMulticastTTL(SSDP_MULTICAST_TTL);
|
|
_server->onRx(&SSDP_update);
|
|
|
|
if (!_server->connect(multicast_addr, SSDP_PORT)) {
|
|
return false;
|
|
}
|
|
# endif // ifdef CORE_POST_2_5_0
|
|
|
|
SSDP_update();
|
|
|
|
return true;
|
|
}
|
|
|
|
/********************************************************************************************\
|
|
Send SSDP messages (notify & responses)
|
|
\*********************************************************************************************/
|
|
void SSDP_send(uint8_t method) {
|
|
uint32_t ip = ESPEasy::net::NetworkLocalIP();
|
|
|
|
// FIXME TD-er: Why create String objects of these flashstrings?
|
|
String _ssdp_response_template = F(
|
|
"HTTP/1.1 200 OK\r\n"
|
|
"EXT:\r\n"
|
|
"ST: upnp:rootdevice\r\n");
|
|
|
|
String _ssdp_notify_template = F(
|
|
"NOTIFY * HTTP/1.1\r\n"
|
|
"HOST: 239.255.255.250:1900\r\n"
|
|
"NT: upnp:rootdevice\r\n"
|
|
"NTS: ssdp:alive\r\n");
|
|
|
|
String _ssdp_packet_template = F(
|
|
"%s" // _ssdp_response_template / _ssdp_notify_template
|
|
"CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL
|
|
"SERVER: Arduino/1.0 UPNP/1.1 ESPEasy/%u\r\n" // _modelNumber
|
|
"USN: uuid:%s\r\n" // _uuid
|
|
"LOCATION: http://%u.%u.%u.%u:80/ssdp.xml\r\n" // NetworkLocalIP(),
|
|
"\r\n");
|
|
{
|
|
char uuid[64] = { 0 };
|
|
uint32_t chipId = ESP.getChipId();
|
|
sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%02x%02x%02x"),
|
|
(uint16_t)((chipId >> 16) & 0xff),
|
|
(uint16_t)((chipId >> 8) & 0xff),
|
|
(uint16_t)chipId & 0xff);
|
|
|
|
char *buffer = nullptr;
|
|
# ifdef USE_SECOND_HEAP
|
|
{
|
|
HeapSelectIram ephemeral;
|
|
buffer = new (std::nothrow) char[1460]();
|
|
}
|
|
# endif // ifdef USE_SECOND_HEAP
|
|
if (buffer == nullptr) {
|
|
buffer = new (std::nothrow) char[1460]();
|
|
}
|
|
|
|
if (buffer == nullptr) { return; }
|
|
int len = snprintf(buffer, 1460,
|
|
_ssdp_packet_template.c_str(),
|
|
(method == 0) ? _ssdp_response_template.c_str() : _ssdp_notify_template.c_str(),
|
|
SSDP_INTERVAL,
|
|
Settings.Build,
|
|
uuid,
|
|
IPADDR2STR(&ip)
|
|
);
|
|
|
|
_server->append(buffer, len);
|
|
delete[] buffer;
|
|
}
|
|
|
|
ip_addr_t remoteAddr;
|
|
uint16_t remotePort;
|
|
|
|
if (method == 0) {
|
|
remoteAddr.addr = _respondToAddr;
|
|
remotePort = _respondToPort;
|
|
} else {
|
|
remoteAddr.addr = SSDP_MULTICAST_ADDR;
|
|
remotePort = SSDP_PORT;
|
|
}
|
|
_server->send(&remoteAddr, remotePort);
|
|
statusLED(true);
|
|
}
|
|
|
|
/********************************************************************************************\
|
|
SSDP message processing
|
|
\*********************************************************************************************/
|
|
void SSDP_update() {
|
|
if (!_pending && _server->next()) {
|
|
ssdp_method_t method = NONE;
|
|
|
|
_respondToAddr = _server->getRemoteAddress();
|
|
_respondToPort = _server->getRemotePort();
|
|
|
|
typedef enum {
|
|
METHOD,
|
|
URI,
|
|
PROTO,
|
|
KEY,
|
|
VALUE,
|
|
ABORT
|
|
|
|
} states;
|
|
states state = METHOD;
|
|
|
|
typedef enum {
|
|
START,
|
|
MAN,
|
|
ST,
|
|
MX
|
|
|
|
} headers;
|
|
headers header = START;
|
|
|
|
uint8_t cursor = 0;
|
|
uint8_t cr = 0;
|
|
|
|
char buffer[SSDP_BUFFER_SIZE] = { 0 };
|
|
|
|
while (_server->getSize() > 0) {
|
|
char c = _server->read();
|
|
|
|
(c == '\r' || c == '\n') ? cr++ : cr = 0;
|
|
|
|
switch (state)
|
|
{
|
|
case METHOD:
|
|
|
|
if (c == ' ') {
|
|
if (strcmp_P(buffer, PSTR("M-SEARCH")) == 0) { method = SEARCH; }
|
|
else if (strcmp_P(buffer, PSTR("NOTIFY")) == 0) { method = NOTIFY; }
|
|
|
|
if (method == NONE) { state = ABORT; }
|
|
else { state = URI; }
|
|
cursor = 0;
|
|
} else if (cursor < SSDP_METHOD_SIZE - 1) {
|
|
buffer[cursor++] = c;
|
|
buffer[cursor] = '\0';
|
|
}
|
|
break;
|
|
case URI:
|
|
|
|
if (c == ' ') {
|
|
if (strcmp(buffer, "*")) { state = ABORT; }
|
|
else { state = PROTO; }
|
|
cursor = 0;
|
|
} else if (cursor < SSDP_URI_SIZE - 1) {
|
|
buffer[cursor++] = c;
|
|
buffer[cursor] = '\0';
|
|
}
|
|
break;
|
|
case PROTO:
|
|
|
|
if (cr == 2) {
|
|
state = KEY;
|
|
cursor = 0;
|
|
}
|
|
break;
|
|
case KEY:
|
|
|
|
if (cr == 4) {
|
|
_pending = true;
|
|
_process_time = millis();
|
|
}
|
|
else if (c == ' ') {
|
|
cursor = 0;
|
|
state = VALUE;
|
|
}
|
|
else if ((c != '\r') && (c != '\n') && (c != ':') && (cursor < SSDP_BUFFER_SIZE - 1)) {
|
|
buffer[cursor++] = c;
|
|
buffer[cursor] = '\0';
|
|
}
|
|
break;
|
|
case VALUE:
|
|
|
|
if (cr == 2) {
|
|
switch (header)
|
|
{
|
|
case START:
|
|
break;
|
|
case MAN:
|
|
break;
|
|
case ST:
|
|
|
|
if (strcmp_P(buffer, PSTR("ssdp:all"))) {
|
|
state = ABORT;
|
|
}
|
|
|
|
// if the search type matches our type, we should respond instead of ABORT
|
|
if (strcmp_P(buffer, PSTR("urn:schemas-upnp-org:device:BinaryLight:1")) == 0) {
|
|
_pending = true;
|
|
_process_time = millis();
|
|
state = KEY;
|
|
}
|
|
break;
|
|
case MX:
|
|
_delay = HwRandom(0, atoi(buffer)) * 1000L;
|
|
break;
|
|
}
|
|
|
|
if (state != ABORT) {
|
|
state = KEY;
|
|
header = START;
|
|
cursor = 0;
|
|
}
|
|
} else if ((c != '\r') && (c != '\n')) {
|
|
if (header == START) {
|
|
if (strncmp(buffer, "MA", 2) == 0) { header = MAN; }
|
|
else if (strcmp(buffer, "ST") == 0) { header = ST; }
|
|
else if (strcmp(buffer, "MX") == 0) { header = MX; }
|
|
}
|
|
|
|
if (cursor < SSDP_BUFFER_SIZE - 1) {
|
|
buffer[cursor++] = c;
|
|
buffer[cursor] = '\0';
|
|
}
|
|
}
|
|
break;
|
|
case ABORT:
|
|
_pending = false;
|
|
_delay = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_pending && timeOutReached(_process_time + _delay)) {
|
|
_pending = false;
|
|
_delay = 0;
|
|
SSDP_send(NONE);
|
|
} else if ((_notify_time == 0) || timeOutReached(_notify_time + (SSDP_INTERVAL * 1000L))) {
|
|
_notify_time = millis();
|
|
SSDP_send(NOTIFY);
|
|
}
|
|
|
|
if (_pending) {
|
|
while (_server->next()) {
|
|
_server->flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
# endif // if FEATURE_SSDP
|
|
#endif // if defined(ESP8266)
|
|
|
|
// ********************************************************************************
|
|
// Return subnet range of active network interface.
|
|
// ********************************************************************************
|
|
bool getSubnetRange(IPAddress& low, IPAddress& high)
|
|
{
|
|
low = ESPEasy::net::NetworkID();
|
|
high = ESPEasy::net::NetworkBroadcast();
|
|
return true;
|
|
}
|
|
|
|
// ********************************************************************************
|
|
// Functions to test and handle network/client connectivity.
|
|
// ********************************************************************************
|
|
|
|
#ifdef CORE_POST_2_5_0
|
|
# include <AddrList.h>
|
|
#endif // ifdef CORE_POST_2_5_0
|
|
|
|
bool hasIPaddr() {
|
|
if (useStaticIP()) { return true; }
|
|
|
|
#ifdef CORE_POST_2_5_0
|
|
bool configured = false;
|
|
|
|
for (auto addr : addrList) {
|
|
if ((configured = (!addr.isLocal() && (addr.ifnumber() == STATION_IF)))) {
|
|
/*
|
|
ESPEASY_SERIAL_CONSOLE_PORT.printf("STA: IF='%s' hostname='%s' addr= %s\n",
|
|
addr.ifname().c_str(),
|
|
addr.ifhostname(),
|
|
addr.toString().c_str());
|
|
*/
|
|
break;
|
|
}
|
|
}
|
|
return configured;
|
|
#else // ifdef CORE_POST_2_5_0
|
|
return WiFi.isConnected();
|
|
#endif // ifdef CORE_POST_2_5_0
|
|
}
|
|
|
|
bool useStaticIP() {
|
|
#if FEATURE_ETHERNET
|
|
|
|
if (active_network_medium == ESPEasy::net::NetworkMedium_t::Ethernet) {
|
|
return ESPEasy::net::eth::ethUseStaticIP();
|
|
}
|
|
#endif // if FEATURE_ETHERNET
|
|
return ESPEasy::net::wifi::WiFiUseStaticIP();
|
|
}
|
|
|
|
// Check connection. Maximum timeout 500 msec.
|
|
bool NetworkConnected(uint32_t timeout_ms) {
|
|
if (ESPEasy::net::NetworkConnected()) return true;
|
|
|
|
#ifdef USES_ESPEASY_NOW
|
|
|
|
if (isESPEasy_now_only()) {
|
|
return false;
|
|
}
|
|
#endif // ifdef USES_ESPEASY_NOW
|
|
|
|
if (timeout_ms > 500) {
|
|
timeout_ms = 500;
|
|
}
|
|
|
|
uint32_t timer = millis() + timeout_ms;
|
|
uint32_t min_delay = timeout_ms / 20;
|
|
|
|
if (min_delay < 10) {
|
|
delay(0); // Allow at least once time for backgroundtasks
|
|
min_delay = 10;
|
|
}
|
|
|
|
// Apparently something needs network, perform check to see if it is ready now.
|
|
while (!ESPEasy::net::NetworkConnected()) {
|
|
if (timeOutReached(timer)) {
|
|
return false;
|
|
}
|
|
delay(min_delay); // Allow the backgroundtasks to continue procesing.
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool hostReachable(const IPAddress& ip) {
|
|
if (!ESPEasy::net::NetworkConnected()) { return false; }
|
|
|
|
return true; // Disabled ping as requested here:
|
|
// https://github.com/letscontrolit/ESPEasy/issues/1494#issuecomment-397872538
|
|
|
|
/*
|
|
// Only do 1 ping at a time to return early
|
|
uint8_t retry = 3;
|
|
while (retry > 0) {
|
|
#if defined(ESP8266)
|
|
if (Ping.ping(ip, 1)) return true;
|
|
#endif
|
|
#if defined(ESP32)
|
|
if (ping_start(ip, 4, 0, 0, 5)) return true;
|
|
#endif
|
|
delay(50);
|
|
--retry;
|
|
}
|
|
if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
|
|
String log = F("Host unreachable: ");
|
|
log += formatIP(ip);
|
|
addLog(LOG_LEVEL_ERROR, log);
|
|
}
|
|
if (ip[1] == 0 && ip[2] == 0 && ip[3] == 0) {
|
|
// Work-around to fix connected but not able to communicate.
|
|
addLog(LOG_LEVEL_ERROR, F("WiFi : Detected strange behavior, reconnect wifi."));
|
|
WifiDisconnect();
|
|
}
|
|
logConnectionStatus();
|
|
return false;
|
|
*/
|
|
}
|
|
|
|
#if FEATURE_HTTP_CLIENT
|
|
|
|
bool connectClient(WiFiClient& client, const char *hostname, uint16_t port, uint32_t timeout_ms) {
|
|
IPAddress ip;
|
|
|
|
if (resolveHostByName(hostname, ip, timeout_ms)) {
|
|
return connectClient(client, ip, port, timeout_ms);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool connectClient(WiFiClient& client, IPAddress ip, uint16_t port, uint32_t timeout_ms)
|
|
{
|
|
START_TIMER;
|
|
|
|
if (!ESPEasy::net::NetworkConnected(true)) {
|
|
client.stop();
|
|
return false;
|
|
}
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
|
|
addLog(LOG_LEVEL_DEBUG, strformat(
|
|
F("connectClient: '%s' port: %u"),
|
|
ip.toString().c_str(),
|
|
port));
|
|
}
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
|
|
// In case of domain name resolution error result can be negative.
|
|
// https://github.com/esp8266/Arduino/blob/18f643c7e2d6a0da9d26ff2b14c94e6536ab78c1/libraries/Ethernet/src/Dns.cpp#L44
|
|
// Thus must match the result with 1.
|
|
bool connected = (client.connect(ip, port) == 1);
|
|
|
|
delay(0);
|
|
|
|
if (!connected) {
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
|
|
addLog(LOG_LEVEL_ERROR, strformat(
|
|
F("connectClient: connect failed to '%s' port: %u"),
|
|
ip.toString().c_str(),
|
|
port));
|
|
}
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
|
|
Scheduler.sendGratuitousARP_now();
|
|
client.stop(); // Make sure to start over without some stale connection
|
|
}
|
|
STOP_TIMER(CONNECT_CLIENT_STATS);
|
|
# if defined(ESP32) || defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0)
|
|
# else
|
|
|
|
if (connected) {
|
|
client.keepAlive(); // Use default keep alive values
|
|
}
|
|
# endif // if defined(ESP32) || defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0)
|
|
return connected;
|
|
}
|
|
|
|
#endif // FEATURE_HTTP_CLIENT
|
|
|
|
void scrubDNS() {
|
|
/*
|
|
#if FEATURE_ETHERNET
|
|
|
|
if (active_network_medium == ESPEasy::net::NetworkMedium_t::Ethernet) {
|
|
if (EthEventData.EthServicesInitialized()) {
|
|
setDNS(0, EthEventData.dns0_cache);
|
|
setDNS(1, EthEventData.dns1_cache);
|
|
}
|
|
return;
|
|
}
|
|
#endif // if FEATURE_ETHERNET
|
|
*/
|
|
}
|
|
|
|
#ifdef ESP32
|
|
bool IPAddressSet(const IPAddress& ip)
|
|
{
|
|
return !(ip == INADDR_NONE
|
|
# if CONFIG_LWIP_IPV6
|
|
|| ip == IN6ADDR_ANY
|
|
# endif
|
|
);
|
|
}
|
|
#endif
|
|
#ifdef ESP8266
|
|
bool IPAddressSet(const IPAddress& ip)
|
|
{
|
|
return !(ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0);
|
|
}
|
|
#endif
|
|
|
|
|
|
bool valid_DNS_address(const IPAddress& dns) {
|
|
return /*dns.v4() != (uint32_t)0x00000000 && */
|
|
dns != IPAddress((uint32_t)0xFD000000) &&
|
|
#ifdef ESP32
|
|
|
|
// Bug where IPv6 global prefix is set as DNS
|
|
// Global IPv6 prefixes currently start with 2xxx::
|
|
(dns[0] & 0xF0) != 0x20 &&
|
|
#endif // ifdef ESP32
|
|
IPAddressSet(dns);
|
|
}
|
|
|
|
bool setDNS(int index, const IPAddress& dns) {
|
|
if (index >= 2) { return false; }
|
|
#ifdef ESP8266
|
|
|
|
if (dns.isSet() && (dns != WiFi.dnsIP(index))) {
|
|
dns_setserver(index, dns);
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
addLogMove(LOG_LEVEL_INFO, strformat(F("IP : Set DNS(%d): %s"), index, formatIP(dns).c_str()));
|
|
}
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
return true;
|
|
}
|
|
#endif // ifdef ESP8266
|
|
#ifdef ESP32
|
|
|
|
if (valid_DNS_address(dns)) {
|
|
const ip_addr_t*cur_dns = dns_getserver(index);
|
|
|
|
if (cur_dns != nullptr) {
|
|
const IPAddress cur_dns_IP(cur_dns);
|
|
|
|
if (valid_DNS_address(cur_dns_IP) && (cur_dns_IP == dns)) {
|
|
// Still the same as before
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Set DNS-Server
|
|
ip_addr_t d;
|
|
dns.to_ip_addr_t(&d);
|
|
dns_setserver(index, &d);
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
addLogMove(LOG_LEVEL_INFO, strformat(F("IP : Set DNS(%d): %s"), index, formatIP(dns).c_str()));
|
|
}
|
|
return true;
|
|
}
|
|
#endif // ifdef ESP32
|
|
return false;
|
|
}
|
|
|
|
bool resolveHostByName(const char *aHostname, IPAddress& aResult, uint32_t timeout_ms) {
|
|
START_TIMER;
|
|
|
|
if (!ESPEasy::net::NetworkConnected(true)) {
|
|
return false;
|
|
}
|
|
|
|
FeedSW_watchdog();
|
|
|
|
// FIXME TD-er: Must try to restore DNS server entries.
|
|
scrubDNS();
|
|
|
|
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ESP32)
|
|
bool resolvedIP = WiFi.hostByName(aHostname, aResult) == 1;
|
|
#else // if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ESP32)
|
|
bool resolvedIP = WiFi.hostByName(aHostname, aResult, timeout_ms) == 1;
|
|
#endif // if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ESP32)
|
|
delay(0);
|
|
FeedSW_watchdog();
|
|
|
|
if (!resolvedIP) {
|
|
Scheduler.sendGratuitousARP_now();
|
|
}
|
|
STOP_TIMER(HOST_BY_NAME_STATS);
|
|
return resolvedIP;
|
|
}
|
|
|
|
bool hostReachable(const String& hostname) {
|
|
IPAddress remote_addr;
|
|
|
|
if (resolveHostByName(hostname.c_str(), remote_addr)) {
|
|
return hostReachable(remote_addr);
|
|
}
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
|
|
addLogMove(LOG_LEVEL_ERROR, concat(F("Hostname cannot be resolved: "), hostname));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Create a random port for the UDP connection.
|
|
// Return true when successful.
|
|
bool beginWiFiUDP_randomPort(WiFiUDP& udp) {
|
|
if (!ESPEasy::net::NetworkConnected(true)) {
|
|
return false;
|
|
}
|
|
unsigned int attempts = 3;
|
|
|
|
while (attempts > 0) {
|
|
--attempts;
|
|
long port = HwRandom(1025, 65535);
|
|
|
|
if (udp.begin(port) != 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void sendGratuitousARP() {
|
|
if (!ESPEasy::net::NetworkConnected()) {
|
|
return;
|
|
}
|
|
#ifdef SUPPORT_ARP
|
|
|
|
// See https://github.com/letscontrolit/ESPEasy/issues/2374
|
|
START_TIMER;
|
|
netif *n = netif_list;
|
|
|
|
while (n) {
|
|
if ((n->hwaddr_len == ETH_HWADDR_LEN) &&
|
|
(n->flags & NETIF_FLAG_ETHARP) &&
|
|
((n->flags & NETIF_FLAG_LINK_UP) && (n->flags & NETIF_FLAG_UP))) {
|
|
# ifdef ESP32
|
|
etharp_gratuitous_r(n);
|
|
# else // ifdef ESP32
|
|
etharp_gratuitous(n);
|
|
# endif // ifdef ESP32
|
|
}
|
|
n = n->next;
|
|
}
|
|
STOP_TIMER(GRAT_ARP_STATS);
|
|
#endif // ifdef SUPPORT_ARP
|
|
}
|
|
|
|
bool splitHostPortString(const String& hostPortString, String& host, uint16_t& port) {
|
|
port = 80; // Some default
|
|
int index_colon = hostPortString.indexOf(':');
|
|
|
|
if (index_colon >= 0) {
|
|
int32_t port_tmp;
|
|
|
|
if (!validIntFromString(hostPortString.substring(index_colon + 1), port_tmp)) {
|
|
return false;
|
|
}
|
|
|
|
if ((port_tmp < 0) || (port_tmp > 65535)) { return false; }
|
|
port = port_tmp;
|
|
host = hostPortString.substring(0, index_colon);
|
|
} else {
|
|
// No port nr defined.
|
|
host = hostPortString;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool splitUserPass_HostPortString(const String& hostPortString, String& user, String& pass, String& host, uint16_t& port)
|
|
{
|
|
const int pos_at = hostPortString.indexOf('@');
|
|
|
|
if (pos_at != -1) {
|
|
user = hostPortString.substring(0, pos_at);
|
|
const int pos_colon = user.indexOf(':');
|
|
|
|
if (pos_colon != -1) {
|
|
pass = user.substring(pos_colon + 1);
|
|
user = user.substring(0, pos_colon);
|
|
}
|
|
return splitHostPortString(hostPortString.substring(pos_at + 1), host, port);
|
|
}
|
|
return splitHostPortString(hostPortString, host, port);
|
|
}
|
|
|
|
// Split a full URL like "http://hostname:port/path/file.htm"
|
|
// Return value is everything after the hostname:port section (including /)
|
|
String splitURL(const String& fullURL, String& user, String& pass, String& host, uint16_t& port, String& file, String& protocol) {
|
|
int starthost = fullURL.indexOf(F("://"));
|
|
|
|
if (starthost == -1) {
|
|
starthost = 0;
|
|
} else {
|
|
starthost += 3;
|
|
}
|
|
const int endhost = fullURL.indexOf('/', starthost);
|
|
splitUserPass_HostPortString(fullURL.substring(starthost, endhost), user, pass, host, port);
|
|
|
|
if (endhost == -1) {
|
|
return EMPTY_STRING;
|
|
}
|
|
|
|
if (starthost > 3) {
|
|
protocol = fullURL.substring(0, starthost - 3);
|
|
protocol.toLowerCase(); // Shouldn't be problematic
|
|
|
|
#if FEATURE_HTTP_TLS
|
|
if ((port == 80) && equals(protocol, F("https"))) { // 'Upgrade' from port 80 to port 443 for HTTPS url
|
|
port = 443;
|
|
}
|
|
#endif // if FEATURE_HTTP_TLS
|
|
}
|
|
|
|
int startfile = fullURL.lastIndexOf('/');
|
|
|
|
if (startfile >= 0) {
|
|
file = fullURL.substring(startfile);
|
|
}
|
|
return fullURL.substring(endhost);
|
|
}
|
|
|
|
String joinURL(const String& user, const String& pass, const String& host, uint16_t& port, const String& uri, const String& protocol)
|
|
{
|
|
String fullURL;
|
|
if (protocol.isEmpty()) {
|
|
if (port == 443) { fullURL = F("https://"); }
|
|
else { fullURL = F("http://"); }
|
|
} else {
|
|
fullURL += protocol;
|
|
if (!fullURL.endsWith(F("://"))) { fullURL += F("://"); }
|
|
}
|
|
if (!user.isEmpty() && !pass.isEmpty()) {
|
|
fullURL += user;
|
|
fullURL += ':';
|
|
fullURL += pass;
|
|
fullURL += '@';
|
|
}
|
|
if (host.isEmpty()) { return EMPTY_STRING; }
|
|
fullURL += host;
|
|
if ((port != 80) && (port != 443)) {
|
|
fullURL += ':';
|
|
fullURL += port;
|
|
}
|
|
if (!uri.startsWith("/")) fullURL += '/';
|
|
fullURL += uri;
|
|
|
|
return fullURL;
|
|
}
|
|
|
|
String get_user_agent_string() {
|
|
static unsigned int agent_size = 20;
|
|
String userAgent;
|
|
|
|
userAgent.reserve(agent_size);
|
|
userAgent += F("ESP Easy/");
|
|
userAgent += get_build_nr();
|
|
userAgent += '/';
|
|
userAgent += get_build_date();
|
|
userAgent += ' ';
|
|
userAgent += get_build_time();
|
|
agent_size = userAgent.length();
|
|
return userAgent;
|
|
}
|
|
|
|
bool splitHeaders(int& strpos, const String& multiHeaders, String& name, String& value) {
|
|
if (strpos < 0) {
|
|
return false;
|
|
}
|
|
int colonPos = multiHeaders.indexOf(':', strpos);
|
|
|
|
if (colonPos < 0) {
|
|
return false;
|
|
}
|
|
name = multiHeaders.substring(strpos, colonPos);
|
|
int valueEndPos = multiHeaders.indexOf('\n', colonPos + 1);
|
|
|
|
if (valueEndPos < 0) {
|
|
value = multiHeaders.substring(colonPos + 1);
|
|
strpos = -1;
|
|
} else {
|
|
value = multiHeaders.substring(colonPos + 1, valueEndPos);
|
|
strpos = valueEndPos + 1;
|
|
}
|
|
value.replace('\r', ' ');
|
|
value.trim();
|
|
return true;
|
|
}
|
|
|
|
String extractParam(const String& authReq, const String& param, const char delimit) {
|
|
int _begin = authReq.indexOf(param);
|
|
|
|
if (_begin == -1) { return EMPTY_STRING; }
|
|
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
|
|
}
|
|
|
|
#if FEATURE_HTTP_CLIENT
|
|
|
|
String getCNonce(const int len) {
|
|
static const char alphanum[] = "0123456789"
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
"abcdefghijklmnopqrstuvwxyz";
|
|
String s;
|
|
|
|
for (int i = 0; i < len; ++i) {
|
|
// FIXME TD-er: Is this "-1" correct? The mod operator makes sure we never reach the sizeof index
|
|
s += alphanum[rand() % (sizeof(alphanum) - 1)];
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
String getDigestAuth(const String& authReq,
|
|
const String& username,
|
|
const String& password,
|
|
const String& method,
|
|
const String& uri,
|
|
unsigned int counter) {
|
|
// extracting required parameters for RFC 2069 simpler Digest
|
|
const String realm = extractParam(authReq, F("realm=\""), '"');
|
|
const String nonce = extractParam(authReq, F("nonce=\""), '"');
|
|
const String cNonce = getCNonce(8);
|
|
|
|
char nc[9];
|
|
|
|
snprintf(nc, sizeof(nc), "%08x", counter);
|
|
|
|
// parameters for the RFC 2617 newer Digest
|
|
MD5Builder md5;
|
|
|
|
md5.begin();
|
|
md5.add(username + ':' + realm + ':' + password); // md5 of the user:realm:user
|
|
md5.calculate();
|
|
const String h1 = md5.toString();
|
|
|
|
md5.begin();
|
|
md5.add(method + ':' + uri);
|
|
md5.calculate();
|
|
const String h2 = md5.toString();
|
|
|
|
md5.begin();
|
|
md5.add(h1 + ':' + nonce + ':' + String(nc) + ':' + cNonce + F(":auth:") + h2);
|
|
md5.calculate();
|
|
|
|
// return authorization
|
|
return strformat(
|
|
F("Digest username=\"%s\""
|
|
", realm=\"%s\""
|
|
", nonce=\"%s\""
|
|
", uri=\"%s\""
|
|
", algorithm=\"MD5\", qop=auth, nc=%s, cnonce=\"%s\""
|
|
", response=\"%s\""),
|
|
username.c_str(),
|
|
realm.c_str(),
|
|
nonce.c_str(),
|
|
uri.c_str(),
|
|
nc,
|
|
cNonce.c_str(),
|
|
md5.toString().c_str()); // response
|
|
}
|
|
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
void log_http_result(const ESPEasy_HTTPClient& http,
|
|
const String & logIdentifier,
|
|
const String & host,
|
|
const String & HttpMethod,
|
|
int httpCode,
|
|
const String & response)
|
|
{
|
|
uint8_t loglevel = LOG_LEVEL_ERROR;
|
|
bool success = false;
|
|
|
|
// HTTP codes:
|
|
// 1xx Informational response
|
|
// 2xx Success
|
|
if ((httpCode >= 100) && (httpCode < 300)) {
|
|
loglevel = LOG_LEVEL_INFO;
|
|
success = true;
|
|
}
|
|
|
|
if (loglevelActiveFor(loglevel)) {
|
|
String log = strformat(F("HTTP : %s %s %s"),
|
|
logIdentifier.c_str(), host.c_str(), HttpMethod.c_str());
|
|
|
|
if (!success) {
|
|
log += F("failed ");
|
|
}
|
|
log += concat(F("HTTP code: "), httpCode);
|
|
|
|
if (!success) {
|
|
log += ' ';
|
|
log += http.errorToString(httpCode);
|
|
}
|
|
|
|
if (response.length() > 0) {
|
|
log += concat(F(" Received reply: "), response.substring(0, 100)); // Returned string may be huge, so only log the first part.
|
|
}
|
|
addLogMove(loglevel, log);
|
|
}
|
|
}
|
|
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
|
|
int http_authenticate(const String& logIdentifier,
|
|
WiFiClient & client,
|
|
ESPEasy_HTTPClient & http,
|
|
uint16_t timeout,
|
|
const String& user,
|
|
const String& pass,
|
|
const String& host,
|
|
uint16_t port,
|
|
const String& protocol,
|
|
# if FEATURE_JSON_EVENT
|
|
String uri,
|
|
# else // if FEATURE_JSON_EVENT
|
|
const String& uri,
|
|
# endif // if FEATURE_JSON_EVENT
|
|
const String& HttpMethod,
|
|
const String& header,
|
|
const String& postStr,
|
|
bool must_check_reply)
|
|
{
|
|
if (!uri.startsWith(F("/"))) {
|
|
return http_authenticate(
|
|
logIdentifier,
|
|
client,
|
|
http,
|
|
timeout,
|
|
user,
|
|
pass,
|
|
host,
|
|
port,
|
|
protocol,
|
|
concat(F("/"), uri),
|
|
HttpMethod,
|
|
header,
|
|
postStr,
|
|
must_check_reply);
|
|
}
|
|
|
|
# if RESPONSE_PARSER_SUPPORT
|
|
int parseJson = -1;
|
|
# if FEATURE_JSON_EVENT
|
|
UriParseResult uriResult = parseUriPath(uri);
|
|
parseJson = uriResult.parseJson;
|
|
uri = uriResult.cleanedPath;
|
|
# endif // if FEATURE_JSON_EVENT
|
|
# endif // if RESPONSE_PARSER_SUPPORT
|
|
|
|
int httpCode = 0;
|
|
const bool hasCredentials = !user.isEmpty() && !pass.isEmpty();
|
|
|
|
if (hasCredentials) {
|
|
must_check_reply = true;
|
|
http.setAuthorization(user.c_str(), pass.c_str());
|
|
} else {
|
|
http.setAuthorization(""); // Clear Basic authorization
|
|
# ifdef ESP32
|
|
# if !FEATURE_TLS
|
|
http.setAuthorizationType(""); // Default type is "Basic"
|
|
#endif
|
|
# endif
|
|
}
|
|
http.setTimeout(timeout);
|
|
http.setUserAgent(get_user_agent_string());
|
|
|
|
if (Settings.SendToHTTP_follow_redirects()) {
|
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
|
http.setRedirectLimit(2);
|
|
}
|
|
# if FEATURE_TLS
|
|
http.setTimeout(timeout);
|
|
#else
|
|
# ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
|
|
|
|
// See: https://github.com/espressif/arduino-esp32/pull/6676
|
|
client.setTimeout((timeout + 500) / 1000); // in seconds!!!!
|
|
Client *pClient = &client;
|
|
pClient->setTimeout(timeout);
|
|
# else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
|
|
client.setTimeout(timeout); // in msec as it should be!
|
|
# endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
|
|
#endif
|
|
|
|
// Add request header as fall back.
|
|
// When adding another "accept" header, it may be interpreted as:
|
|
// "if you have XXX, send it; or failing that, just give me what you've got."
|
|
http.addHeader(F("Accept"), F("*/*;q=0.1"));
|
|
|
|
// Add client IP
|
|
http.addHeader(F("X-Forwarded-For"), formatIP(ESPEasy::net::NetworkLocalIP()));
|
|
|
|
delay(0);
|
|
scrubDNS();
|
|
# if defined(CORE_POST_2_6_0) || defined(ESP32)
|
|
# if FEATURE_TLS
|
|
const String fullURL = joinURL(user, pass, host, port, uri, protocol);
|
|
if (fullURL.startsWith(F("https")))
|
|
http.begin(fullURL, nullptr); // HTTPS
|
|
else
|
|
http.begin(fullURL); // HTTP
|
|
#else
|
|
http.begin(client, host, port, uri, false); // HTTP
|
|
#endif
|
|
# else // if defined(CORE_POST_2_6_0) || defined(ESP32)
|
|
http.begin(client, host, port, uri);
|
|
# endif // if defined(CORE_POST_2_6_0) || defined(ESP32)
|
|
|
|
const char *keys[] = { "WWW-Authenticate" };
|
|
http.collectHeaders(keys, 1);
|
|
|
|
{
|
|
int headerpos = 0;
|
|
String name, value;
|
|
|
|
while (splitHeaders(headerpos, header, name, value)) {
|
|
// Disabled the check to exclude "Authorization", due to:
|
|
// https://github.com/letscontrolit/ESPEasy/issues/4364
|
|
// Check was added for: https://github.com/letscontrolit/ESPEasy/issues/4355
|
|
// However, I doubt this was the actual bug. More likely the supplied credential strings were not entirely empty for whatever reason.
|
|
//
|
|
// Work-around to not add Authorization header since the HTTPClient code
|
|
// only ignores this when base64Authorication is set.
|
|
|
|
// if (!name.equalsIgnoreCase(F("Authorization"))) {
|
|
http.addHeader(name, value);
|
|
|
|
// }
|
|
}
|
|
}
|
|
|
|
// start connection and send HTTP header (and body)
|
|
if (equals(HttpMethod, F("HEAD")) || equals(HttpMethod, F("GET"))) {
|
|
httpCode = http.sendRequest(HttpMethod.c_str());
|
|
} else {
|
|
httpCode = http.sendRequest(HttpMethod.c_str(), postStr);
|
|
}
|
|
|
|
// Check to see if we need to try digest auth
|
|
if ((httpCode == 401) && must_check_reply) {
|
|
const String authReq = http.header(String(F("WWW-Authenticate")).c_str());
|
|
|
|
if (authReq.indexOf(F("Digest")) != -1) {
|
|
// Use Digest authorization
|
|
#ifndef BUILD_NO_DEBUG
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
addLogMove(LOG_LEVEL_INFO, concat(F("HTTP : Start Digest Authorization for "), host));
|
|
}
|
|
#endif
|
|
|
|
http.setAuthorization(""); // Clear Basic authorization
|
|
# ifdef ESP32
|
|
# if !FEATURE_TLS
|
|
http.setAuthorizationType(""); // Default type is "Basic" and "Digest" is already part of the string generated by getDigestAuth()
|
|
# endif
|
|
# endif
|
|
const String authorization = getDigestAuth(authReq, user, pass, F("GET"), uri, 1);
|
|
|
|
http.end();
|
|
# if defined(CORE_POST_2_6_0) || defined(ESP32)
|
|
# if FEATURE_TLS
|
|
const String fullURL = joinURL(user, pass, host, port, uri);
|
|
if (fullURL.startsWith(F("https")))
|
|
http.begin(fullURL, nullptr); // HTTPS
|
|
else
|
|
http.begin(fullURL); // HTTP
|
|
# else
|
|
http.begin(client, host, port, uri, false); // HTTP, not HTTPS
|
|
# endif
|
|
# else // if defined(CORE_POST_2_6_0) || defined(ESP32)
|
|
http.begin(client, host, port, uri);
|
|
# endif // if defined(CORE_POST_2_6_0) || defined(ESP32)
|
|
|
|
http.addHeader(F("Authorization"), authorization);
|
|
|
|
// start connection and send HTTP header (and body)
|
|
if (equals(HttpMethod, F("HEAD")) || equals(HttpMethod, F("GET"))) {
|
|
httpCode = http.sendRequest(HttpMethod.c_str());
|
|
} else {
|
|
httpCode = http.sendRequest(HttpMethod.c_str(), postStr);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!must_check_reply) {
|
|
// There are services which do not send an ack.
|
|
// So if the return code matches a read timeout, we change it into HTTP code 200
|
|
if (httpCode == HTTPC_ERROR_READ_TIMEOUT) {
|
|
httpCode = 200;
|
|
}
|
|
}
|
|
|
|
if (Settings.UseRules) {
|
|
// Generate event with the HTTP return code
|
|
// e.g. http#hostname=401
|
|
eventQueue.addMove(strformat(F("http#%s=%d"), host.c_str(), httpCode));
|
|
|
|
// ----This way to the custom response parser----------------
|
|
#if RESPONSE_PARSER_SUPPORT
|
|
eventFromResponse(host, httpCode, uri, http, parseJson);
|
|
#endif
|
|
}
|
|
// -----------------------------------------------------------
|
|
|
|
# ifndef BUILD_NO_DEBUG
|
|
log_http_result(http, logIdentifier, host + ':' + port, HttpMethod, httpCode, EMPTY_STRING);
|
|
# endif
|
|
return httpCode;
|
|
}
|
|
|
|
# if FEATURE_HTTP_TLS
|
|
# include <WiFiClientSecureLightBearSSL.h>
|
|
# include "../CustomBuild/Certificate_CA.h"
|
|
# endif // if FEATURE_HTTP_TLS
|
|
|
|
String send_via_http(const String& logIdentifier,
|
|
uint16_t timeout,
|
|
const String& user,
|
|
const String& pass,
|
|
const String& host,
|
|
uint16_t port,
|
|
const String& uri,
|
|
const String& HttpMethod,
|
|
const String& header,
|
|
const String& postStr,
|
|
int & httpCode,
|
|
bool must_check_reply,
|
|
const String& protocol
|
|
#if FEATURE_HTTP_TLS
|
|
, TLS_types tlsType
|
|
#endif // if FEATURE_HTTP_TLS
|
|
) {
|
|
WiFiClient client;
|
|
#if FEATURE_HTTP_TLS
|
|
/*
|
|
BearSSL::WiFiClientSecure_light *client_secure = nullptr;
|
|
if (tlsType != TLS_types::NoTLS) {
|
|
client_secure = new (std::nothrow) BearSSL::WiFiClientSecure_light(2048, 2048);
|
|
if (nullptr == client_secure) {
|
|
return F("HTTPS: Could not create TLS client, out of memory");
|
|
}
|
|
client_secure->setUtcTime_fcn(getUnixTime);
|
|
client_secure->setCfgTime_fcn(get_build_unixtime);
|
|
switch (tlsType) {
|
|
case TLS_types::NoTLS:
|
|
break;
|
|
// TODO: Implement?
|
|
// case TLS_types::TLS_PSK:
|
|
// case TLS_types::TLS_CA_CLI_CERT:
|
|
// break;
|
|
case TLS_types::TLS_FINGERPRINT:
|
|
// TODO: Implement
|
|
addLog(LOG_LEVEL_ERROR, F("HTTPS: TLS_FINGERPRINT Not implemented yet."));
|
|
client_secure->setInsecure();
|
|
break;
|
|
case TLS_types::TLS_CA_CERT:
|
|
// TODO: Read from filesystem?
|
|
client_secure->setTrustAnchor(Tasmota_TA, Tasmota_TA_size);
|
|
break;
|
|
case TLS_types::TLS_insecure:
|
|
client_secure->setInsecure();
|
|
break;
|
|
}
|
|
# ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
|
|
|
|
// See: https://github.com/espressif/arduino-esp32/pull/6676
|
|
client_secure->setTimeout((timeout + 500) / 1000); // in seconds!!!!
|
|
Client *pClient = client_secure;
|
|
pClient->setTimeout(timeout);
|
|
# else // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
|
|
client_secure->setTimeout(timeout); // in msec as it should be!
|
|
# endif // ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS
|
|
}
|
|
*/
|
|
#endif // if FEATURE_HTTP_TLS
|
|
ESPEasy_HTTPClient http;
|
|
|
|
http.setReuse(false);
|
|
|
|
httpCode = http_authenticate(
|
|
logIdentifier,
|
|
/*
|
|
#if FEATURE_HTTP_TLS
|
|
tlsType != TLS_types::NoTLS ? *client_secure :
|
|
#endif // if FEATURE_HTTP_TLS
|
|
*/
|
|
client,
|
|
http,
|
|
timeout,
|
|
user,
|
|
pass,
|
|
host,
|
|
port,
|
|
protocol,
|
|
uri,
|
|
HttpMethod,
|
|
header,
|
|
postStr,
|
|
must_check_reply);
|
|
|
|
String response;
|
|
|
|
if ((httpCode > 0) && must_check_reply) {
|
|
response = http.getString();
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (!response.isEmpty()) {
|
|
log_http_result(http, logIdentifier, host, HttpMethod, httpCode, response);
|
|
}
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
}
|
|
|
|
http.end();
|
|
|
|
// http.end() does not call client.stop() if it is no longer connected.
|
|
// However the client may still keep its internal state which may prevent
|
|
// future connections to the same host until there has been a connection to another host inbetween.
|
|
client.stop();
|
|
/*
|
|
#if FEATURE_HTTP_TLS
|
|
if (nullptr != client_secure) {
|
|
client_secure->stop();
|
|
}
|
|
#endif // if FEATURE_HTTP_TLS
|
|
*/
|
|
return response;
|
|
}
|
|
|
|
#endif // FEATURE_HTTP_CLIENT
|
|
|
|
#if FEATURE_DOWNLOAD
|
|
|
|
// FIXME TD-er: Must set the timeout somewhere
|
|
# ifndef DOWNLOAD_FILE_TIMEOUT
|
|
# define DOWNLOAD_FILE_TIMEOUT 2000
|
|
# endif // ifndef DOWNLOAD_FILE_TIMEOUT
|
|
|
|
// Download a file from a given URL and save to a local file named "file_save"
|
|
// If the URL ends with a /, the file part will be assumed the same as file_save.
|
|
// If file_save is empty, the file part from the URL will be used as local file name.
|
|
// Return true when successful.
|
|
bool downloadFile(const String& url, String file_save) {
|
|
String error;
|
|
|
|
return downloadFile(url, file_save, EMPTY_STRING, EMPTY_STRING, error);
|
|
}
|
|
|
|
// User and Pass may be updated if they occur in the hostname part.
|
|
// Thus have to be copied instead of const reference.
|
|
bool start_downloadFile(WiFiClient & client,
|
|
ESPEasy_HTTPClient & http,
|
|
const String& url,
|
|
String & file_save,
|
|
String user,
|
|
String pass,
|
|
String & error) {
|
|
String host, file, protocol;
|
|
uint16_t port;
|
|
String uri = splitURL(url, user, pass, host, port, file, protocol);
|
|
|
|
if (file_save.isEmpty()) {
|
|
file_save = file;
|
|
} else if ((file.isEmpty()) && uri.endsWith("/")) {
|
|
// file = file_save;
|
|
uri += file_save;
|
|
}
|
|
# ifndef BUILD_NO_DEBUG
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
|
|
addLogMove(LOG_LEVEL_DEBUG, strformat(F("downloadFile: URL: %s decoded: %s:%d%s"),
|
|
url.c_str(), host.c_str(), port, uri.c_str()));
|
|
}
|
|
# endif // ifndef BUILD_NO_DEBUG
|
|
|
|
if (file_save.isEmpty()) {
|
|
error = F("Empty filename");
|
|
addLog(LOG_LEVEL_ERROR, error);
|
|
return false;
|
|
}
|
|
|
|
const int httpCode = http_authenticate(
|
|
F("DownloadFile"),
|
|
client,
|
|
http,
|
|
DOWNLOAD_FILE_TIMEOUT,
|
|
user,
|
|
pass,
|
|
host,
|
|
port,
|
|
protocol,
|
|
uri,
|
|
F("GET"),
|
|
EMPTY_STRING, // header
|
|
EMPTY_STRING, // postStr
|
|
true // must_check_reply
|
|
);
|
|
|
|
if (httpCode != HTTP_CODE_OK) {
|
|
error = strformat(F("HTTP code: %d %s"), httpCode, url.c_str());
|
|
|
|
addLog(LOG_LEVEL_ERROR, error);
|
|
http.end();
|
|
client.stop();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool downloadFile(const String& url, String file_save, const String& user, const String& pass, String& error) {
|
|
WiFiClient client;
|
|
ESPEasy_HTTPClient http;
|
|
|
|
http.setReuse(false);
|
|
|
|
if (!start_downloadFile(client, http, url, file_save, user, pass, error)) {
|
|
return false;
|
|
}
|
|
|
|
if (fileExists(file_save)) {
|
|
error = concat(F("File exists: "), file_save);
|
|
addLog(LOG_LEVEL_ERROR, error);
|
|
http.end();
|
|
client.stop();
|
|
return false;
|
|
}
|
|
|
|
long len = http.getSize();
|
|
fs::File f = tryOpenFile(file_save, "w");
|
|
|
|
if (f) {
|
|
const size_t downloadBuffSize = 256;
|
|
uint8_t buff[downloadBuffSize];
|
|
size_t bytesWritten = 0;
|
|
unsigned long timeout = millis() + DOWNLOAD_FILE_TIMEOUT;
|
|
|
|
// get tcp stream
|
|
WiFiClient *stream = &client;
|
|
|
|
// read all data from server
|
|
while (http.connected() && (len > 0 || len == -1)) {
|
|
// read up to downloadBuffSize at a time.
|
|
size_t bytes_to_read = downloadBuffSize;
|
|
|
|
if ((len > 0) && (len < static_cast<int>(bytes_to_read))) {
|
|
bytes_to_read = len;
|
|
}
|
|
const size_t c = stream->readBytes(buff, bytes_to_read);
|
|
|
|
if (c > 0) {
|
|
timeout = millis() + DOWNLOAD_FILE_TIMEOUT;
|
|
|
|
if (f.write(buff, c) != c) {
|
|
error = strformat(F("Error saving file: %s %d Bytes written"), file_save.c_str(), bytesWritten);
|
|
addLog(LOG_LEVEL_ERROR, error);
|
|
http.end();
|
|
client.stop();
|
|
return false;
|
|
}
|
|
bytesWritten += c;
|
|
|
|
if (len > 0) { len -= c; }
|
|
}
|
|
|
|
if (timeOutReached(timeout)) {
|
|
error = concat(F("Timeout: "), file_save);
|
|
addLog(LOG_LEVEL_ERROR, error);
|
|
delay(0);
|
|
http.end();
|
|
client.stop();
|
|
return false;
|
|
}
|
|
delay(0);
|
|
}
|
|
f.close();
|
|
http.end();
|
|
client.stop();
|
|
#ifndef BUILD_NO_DEBUG
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
addLog(LOG_LEVEL_INFO, strformat(F("downloadFile: %s Success"), file_save.c_str()));
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
http.end();
|
|
client.stop();
|
|
error = concat(F("Failed to open file for writing: "), file_save);
|
|
addLog(LOG_LEVEL_ERROR, error);
|
|
return false;
|
|
}
|
|
|
|
bool downloadFirmware(String filename, String& error)
|
|
{
|
|
String baseurl, user, pass;
|
|
|
|
# if FEATURE_CUSTOM_PROVISIONING
|
|
MakeProvisioningSettings(ProvisioningSettings);
|
|
|
|
if (ProvisioningSettings.get()) {
|
|
loadProvisioningSettings(*ProvisioningSettings);
|
|
|
|
if (!ProvisioningSettings->allowedFlags.allowFetchFirmware) {
|
|
error = F("Not Allowed");
|
|
return false;
|
|
}
|
|
baseurl = ProvisioningSettings->url;
|
|
user = ProvisioningSettings->user;
|
|
pass = ProvisioningSettings->pass;
|
|
}
|
|
# endif // if FEATURE_CUSTOM_PROVISIONING
|
|
|
|
const String fullUrl = joinUrlFilename(baseurl, filename);
|
|
|
|
return downloadFirmware(fullUrl, filename, user, pass, error);
|
|
}
|
|
|
|
bool downloadFirmware(const String& url, String& file_save, String& user, String& pass, String& error)
|
|
{
|
|
WiFiClient client;
|
|
ESPEasy_HTTPClient http;
|
|
|
|
error.clear();
|
|
|
|
if (!start_downloadFile(client, http, url, file_save, user, pass, error)) {
|
|
return false;
|
|
}
|
|
|
|
int len = http.getSize();
|
|
|
|
if (Update.begin(len, U_FLASH, Settings.Pin_status_led, Settings.Pin_status_led_Inversed ? LOW : HIGH)) {
|
|
const size_t downloadBuffSize = 256;
|
|
uint8_t buff[downloadBuffSize];
|
|
size_t bytesWritten = 0;
|
|
unsigned long timeout = millis() + DOWNLOAD_FILE_TIMEOUT;
|
|
|
|
// get tcp stream
|
|
WiFiClient *stream = &client;
|
|
|
|
while (error.isEmpty() && http.connected() && (len > 0 || len == -1)) {
|
|
// read up to downloadBuffSize at a time.
|
|
size_t bytes_to_read = downloadBuffSize;
|
|
|
|
if ((len > 0) && (len < static_cast<int>(bytes_to_read))) {
|
|
bytes_to_read = len;
|
|
}
|
|
const size_t c = stream->readBytes(buff, bytes_to_read);
|
|
|
|
if (c > 0) {
|
|
timeout = millis() + DOWNLOAD_FILE_TIMEOUT;
|
|
|
|
if (Update.write(buff, c) != c) {
|
|
error = strformat(F("Error saving firmware update: %s %d Bytes written"),
|
|
file_save.c_str(), bytesWritten);
|
|
break;
|
|
}
|
|
bytesWritten += c;
|
|
|
|
if (len > 0) { len -= c; }
|
|
}
|
|
|
|
if (timeOutReached(timeout)) {
|
|
error = concat(F("Timeout: "), file_save);
|
|
break;
|
|
}
|
|
|
|
if (!UseRTOSMultitasking) {
|
|
// On ESP32 the schedule is executed on the 2nd core.
|
|
Scheduler.handle_schedule();
|
|
}
|
|
backgroundtasks();
|
|
}
|
|
}
|
|
http.end();
|
|
client.stop();
|
|
#ifndef BUILD_NO_DEBUG
|
|
if (error.isEmpty() && loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
addLog(LOG_LEVEL_INFO, strformat(F("downloadFile: %s Success"), file_save.c_str()));
|
|
}
|
|
#endif
|
|
|
|
uint8_t errorcode = 0;
|
|
|
|
if (!Update.end()) {
|
|
errorcode = Update.getError();
|
|
# ifdef ESP32
|
|
const __FlashStringHelper *err_fstr = F("Unknown");
|
|
|
|
switch (errorcode)
|
|
{
|
|
case UPDATE_ERROR_OK: err_fstr = F("OK");
|
|
break;
|
|
case UPDATE_ERROR_WRITE: err_fstr = F("WRITE");
|
|
break;
|
|
case UPDATE_ERROR_ERASE: err_fstr = F("ERASE");
|
|
break;
|
|
case UPDATE_ERROR_READ: err_fstr = F("READ");
|
|
break;
|
|
case UPDATE_ERROR_SPACE: err_fstr = F("SPACE");
|
|
break;
|
|
case UPDATE_ERROR_SIZE: err_fstr = F("SIZE");
|
|
break;
|
|
case UPDATE_ERROR_STREAM: err_fstr = F("STREAM");
|
|
break;
|
|
case UPDATE_ERROR_MD5: err_fstr = F("MD5");
|
|
break;
|
|
case UPDATE_ERROR_MAGIC_BYTE: err_fstr = F("MAGIC_BYTE");
|
|
break;
|
|
case UPDATE_ERROR_ACTIVATE: err_fstr = F("ACTIVATE");
|
|
break;
|
|
case UPDATE_ERROR_NO_PARTITION: err_fstr = F("NO_PARTITION");
|
|
break;
|
|
case UPDATE_ERROR_BAD_ARGUMENT: err_fstr = F("BAD_ARGUMENT");
|
|
break;
|
|
case UPDATE_ERROR_ABORT: err_fstr = F("ABORT");
|
|
break;
|
|
}
|
|
error += concat(F(" Error: "), err_fstr);
|
|
# else // ifdef ESP32
|
|
error += concat(F(" Error: "), errorcode);
|
|
# endif // ifdef ESP32
|
|
} else {
|
|
if (Settings.UseRules) {
|
|
eventQueue.addMove(concat(F("ProvisionFirmware#success="), file_save));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
backgroundtasks();
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
|
|
addLog(LOG_LEVEL_ERROR, concat(F("Failed update firmware: "), error));
|
|
}
|
|
|
|
if (Settings.UseRules) {
|
|
eventQueue.addMove(concat(F("ProvisionFirmware#failed="), file_save));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
String joinUrlFilename(const String& url, String& filename)
|
|
{
|
|
String fullUrl;
|
|
|
|
fullUrl.reserve(url.length() + filename.length() + 1); // May need to add an extra slash
|
|
fullUrl = url;
|
|
fullUrl = parseTemplate(fullUrl, true); // URL encode
|
|
|
|
// URLEncode may also encode the '/' into "%2f"
|
|
// FIXME TD-er: Can this really occur?
|
|
fullUrl.replace(F("%2f"), F("/"));
|
|
|
|
while (filename.startsWith(F("/"))) {
|
|
filename = filename.substring(1);
|
|
}
|
|
|
|
if (!fullUrl.endsWith(F("/"))) {
|
|
fullUrl += F("/");
|
|
}
|
|
fullUrl += filename;
|
|
return fullUrl;
|
|
}
|
|
|
|
#endif // if FEATURE_DOWNLOAD
|