From 0b89d84626ae78b755ad734a85f217d522801aa2 Mon Sep 17 00:00:00 2001 From: vlados31 Date: Sat, 22 Apr 2023 21:55:43 +0300 Subject: [PATCH] first commit --- README.md | 0 favicon.png | Bin 0 -> 684 bytes index.html | 160 ++++ platformio.ini | 17 + .../Adafruit_BusIO_Register.cpp | 365 +++++++++ .../Adafruit_BusIO_Register.h | 105 +++ .../Adafruit_I2CDevice.cpp | 313 ++++++++ .../Adafruit_I2CDevice.h | 36 + .../Adafruit_I2CRegister.h | 10 + .../Adafruit_SPIDevice.cpp | 493 +++++++++++++ .../Adafruit_SPIDevice.h | 123 ++++ src/Adafruit_Si4713.cpp | 400 ++++++++++ src/Adafruit_Si4713.h | 190 +++++ src/config.h | 26 + src/http/ConnectionContext.cpp | 17 + src/http/ConnectionContext.hpp | 41 ++ src/http/HTTPBodyParser.hpp | 68 ++ src/http/HTTPConnection.cpp | 691 ++++++++++++++++++ src/http/HTTPConnection.hpp | 173 +++++ src/http/HTTPHeader.cpp | 43 ++ src/http/HTTPHeader.hpp | 32 + src/http/HTTPHeaders.cpp | 61 ++ src/http/HTTPHeaders.hpp | 37 + src/http/HTTPMiddlewareFunction.hpp | 26 + src/http/HTTPMultipartBodyParser.cpp | 288 ++++++++ src/http/HTTPMultipartBodyParser.hpp | 39 + src/http/HTTPNode.cpp | 54 ++ src/http/HTTPNode.hpp | 70 ++ src/http/HTTPRequest.cpp | 185 +++++ src/http/HTTPRequest.hpp | 68 ++ src/http/HTTPResponse.cpp | 188 +++++ src/http/HTTPResponse.hpp | 71 ++ src/http/HTTPSCallbackFunction.hpp | 14 + src/http/HTTPSConnection.cpp | 123 ++++ src/http/HTTPSConnection.hpp | 58 ++ src/http/HTTPSServer.cpp | 102 +++ src/http/HTTPSServer.hpp | 56 ++ src/http/HTTPSServerConstants.hpp | 89 +++ src/http/HTTPServer.cpp | 211 ++++++ src/http/HTTPServer.hpp | 76 ++ src/http/HTTPURLEncodedBodyParser.cpp | 128 ++++ src/http/HTTPURLEncodedBodyParser.hpp | 32 + src/http/HTTPValidator.cpp | 15 + src/http/HTTPValidator.hpp | 23 + src/http/ResolvedResource.cpp | 40 + src/http/ResolvedResource.hpp | 30 + src/http/ResourceNode.cpp | 16 + src/http/ResourceNode.hpp | 28 + src/http/ResourceParameters.cpp | 167 +++++ src/http/ResourceParameters.hpp | 59 ++ src/http/ResourceResolver.cpp | 179 +++++ src/http/ResourceResolver.hpp | 51 ++ src/http/SSLCert.cpp | 310 ++++++++ src/http/SSLCert.hpp | 182 +++++ src/http/ValidatorFunctions.cpp | 14 + src/http/ValidatorFunctions.hpp | 37 + src/http/WebsocketHandler.cpp | 254 +++++++ src/http/WebsocketHandler.hpp | 87 +++ src/http/WebsocketInputStreambuf.cpp | 110 +++ src/http/WebsocketInputStreambuf.hpp | 47 ++ src/http/WebsocketNode.cpp | 20 + src/http/WebsocketNode.hpp | 25 + src/http/util.cpp | 98 +++ src/http/util.hpp | 35 + src/main.cpp | 89 +++ src/web/web.cpp | 349 +++++++++ src/web/web.h | 8 + 67 files changed, 7552 insertions(+) create mode 100644 README.md create mode 100644 favicon.png create mode 100644 index.html create mode 100644 platformio.ini create mode 100644 src/Adafruit_BusIO-master/Adafruit_BusIO_Register.cpp create mode 100644 src/Adafruit_BusIO-master/Adafruit_BusIO_Register.h create mode 100644 src/Adafruit_BusIO-master/Adafruit_I2CDevice.cpp create mode 100644 src/Adafruit_BusIO-master/Adafruit_I2CDevice.h create mode 100644 src/Adafruit_BusIO-master/Adafruit_I2CRegister.h create mode 100644 src/Adafruit_BusIO-master/Adafruit_SPIDevice.cpp create mode 100644 src/Adafruit_BusIO-master/Adafruit_SPIDevice.h create mode 100644 src/Adafruit_Si4713.cpp create mode 100644 src/Adafruit_Si4713.h create mode 100644 src/config.h create mode 100644 src/http/ConnectionContext.cpp create mode 100644 src/http/ConnectionContext.hpp create mode 100644 src/http/HTTPBodyParser.hpp create mode 100644 src/http/HTTPConnection.cpp create mode 100644 src/http/HTTPConnection.hpp create mode 100644 src/http/HTTPHeader.cpp create mode 100644 src/http/HTTPHeader.hpp create mode 100644 src/http/HTTPHeaders.cpp create mode 100644 src/http/HTTPHeaders.hpp create mode 100644 src/http/HTTPMiddlewareFunction.hpp create mode 100644 src/http/HTTPMultipartBodyParser.cpp create mode 100644 src/http/HTTPMultipartBodyParser.hpp create mode 100644 src/http/HTTPNode.cpp create mode 100644 src/http/HTTPNode.hpp create mode 100644 src/http/HTTPRequest.cpp create mode 100644 src/http/HTTPRequest.hpp create mode 100644 src/http/HTTPResponse.cpp create mode 100644 src/http/HTTPResponse.hpp create mode 100644 src/http/HTTPSCallbackFunction.hpp create mode 100644 src/http/HTTPSConnection.cpp create mode 100644 src/http/HTTPSConnection.hpp create mode 100644 src/http/HTTPSServer.cpp create mode 100644 src/http/HTTPSServer.hpp create mode 100644 src/http/HTTPSServerConstants.hpp create mode 100644 src/http/HTTPServer.cpp create mode 100644 src/http/HTTPServer.hpp create mode 100644 src/http/HTTPURLEncodedBodyParser.cpp create mode 100644 src/http/HTTPURLEncodedBodyParser.hpp create mode 100644 src/http/HTTPValidator.cpp create mode 100644 src/http/HTTPValidator.hpp create mode 100644 src/http/ResolvedResource.cpp create mode 100644 src/http/ResolvedResource.hpp create mode 100644 src/http/ResourceNode.cpp create mode 100644 src/http/ResourceNode.hpp create mode 100644 src/http/ResourceParameters.cpp create mode 100644 src/http/ResourceParameters.hpp create mode 100644 src/http/ResourceResolver.cpp create mode 100644 src/http/ResourceResolver.hpp create mode 100644 src/http/SSLCert.cpp create mode 100644 src/http/SSLCert.hpp create mode 100644 src/http/ValidatorFunctions.cpp create mode 100644 src/http/ValidatorFunctions.hpp create mode 100644 src/http/WebsocketHandler.cpp create mode 100644 src/http/WebsocketHandler.hpp create mode 100644 src/http/WebsocketInputStreambuf.cpp create mode 100644 src/http/WebsocketInputStreambuf.hpp create mode 100644 src/http/WebsocketNode.cpp create mode 100644 src/http/WebsocketNode.hpp create mode 100644 src/http/util.cpp create mode 100644 src/http/util.hpp create mode 100644 src/main.cpp create mode 100644 src/web/web.cpp create mode 100644 src/web/web.h diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..626362abec1fe3de1b7c53f27ea871e449488f35 GIT binary patch literal 684 zcmV;d0#p5oP)EX>4Tx04R}tkv&MmKp2MKrb>%c9PA(>lA$_T5EXIMDionYs1;guFuC*(nlvOS zE{=k0!NH%!s)LKOt`4q(Aov5~=;Wm6A|-y86k5c1$8itueecWNcYx5SFwN>32Q=L_ z)5(OG&8>=|R|FA4nC7s|EMrcRQt%yL_XzO)F3z+3>;4?QYTjZ%KqQ`JhG`RT5KnK~ z2Iqa^C@aY-@j3CBNf#u3)BVfh)c3uQq_0Ptxmc zEp`O-Z37qAZB5w&E_Z-|Cqp)6R|?V+3I*W(jJ_!c^xpzKYhK@4=Qw=;GSsWo4RCM> zj20<--RIrm&c6Ly)9T+3`si|Z;3Mx~00009a7bBm001r{001r{0eGc9b^rhX2XskI zMF-{s2M7!w`49sa0000PbVXQnLvL+uWo~o;Lvm$dbY)~9cWHEJAV*0}P*;Ht7XSbN zmPtfGR7l6|RZ$LrAP9xl3C!!*mAgY-r#gc4=|Ig~Ytsf_Y%p;=-g`*!Z4A)+fH9;y zE1;FF#5<7K=jbr3+x})n{SQD2*~u#=0@<-TY&Y+cV$dR!2|X`4j@laC2Ubc&Wr(8! zSRkbp3UUPo2=)mxU-&^mQ|5wB2zx+fa*I+SOn(yg;FCnnn!aMtqUr03GI0UCJ04oe SnMfi40000 + + + + + Esp32 FM pirate + + + +
+

Lamps4 by Vlados31 & ESP32

+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..cf36a8e --- /dev/null +++ b/platformio.ini @@ -0,0 +1,17 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32doit-devkit-v1] +platform = espressif32 +board = esp32doit-devkit-v1 +framework = arduino +lib_deps = + WiFi + esp_tls diff --git a/src/Adafruit_BusIO-master/Adafruit_BusIO_Register.cpp b/src/Adafruit_BusIO-master/Adafruit_BusIO_Register.cpp new file mode 100644 index 0000000..673f774 --- /dev/null +++ b/src/Adafruit_BusIO-master/Adafruit_BusIO_Register.cpp @@ -0,0 +1,365 @@ +#include "Adafruit_BusIO_Register.h" + +#if !defined(SPI_INTERFACES_COUNT) || \ + (defined(SPI_INTERFACES_COUNT) && (SPI_INTERFACES_COUNT > 0)) + +/*! + * @brief Create a register we access over an I2C Device (which defines the + * bus and address) + * @param i2cdevice The I2CDevice to use for underlying I2C access + * @param reg_addr The address pointer value for the I2C/SMBus register, can + * be 8 or 16 bits + * @param width The width of the register data itself, defaults to 1 byte + * @param byteorder The byte order of the register (used when width is > 1), + * defaults to LSBFIRST + * @param address_width The width of the register address itself, defaults + * to 1 byte + */ +Adafruit_BusIO_Register::Adafruit_BusIO_Register(Adafruit_I2CDevice *i2cdevice, + uint16_t reg_addr, + uint8_t width, + uint8_t byteorder, + uint8_t address_width) { + _i2cdevice = i2cdevice; + _spidevice = nullptr; + _addrwidth = address_width; + _address = reg_addr; + _byteorder = byteorder; + _width = width; +} + +/*! + * @brief Create a register we access over an SPI Device (which defines the + * bus and CS pin) + * @param spidevice The SPIDevice to use for underlying SPI access + * @param reg_addr The address pointer value for the SPI register, can + * be 8 or 16 bits + * @param type The method we use to read/write data to SPI (which is not + * as well defined as I2C) + * @param width The width of the register data itself, defaults to 1 byte + * @param byteorder The byte order of the register (used when width is > 1), + * defaults to LSBFIRST + * @param address_width The width of the register address itself, defaults + * to 1 byte + */ +Adafruit_BusIO_Register::Adafruit_BusIO_Register(Adafruit_SPIDevice *spidevice, + uint16_t reg_addr, + Adafruit_BusIO_SPIRegType type, + uint8_t width, + uint8_t byteorder, + uint8_t address_width) { + _spidevice = spidevice; + _spiregtype = type; + _i2cdevice = nullptr; + _addrwidth = address_width; + _address = reg_addr; + _byteorder = byteorder; + _width = width; +} + +/*! + * @brief Create a register we access over an I2C or SPI Device. This is a + * handy function because we can pass in nullptr for the unused interface, + * allowing libraries to mass-define all the registers + * @param i2cdevice The I2CDevice to use for underlying I2C access, if + * nullptr we use SPI + * @param spidevice The SPIDevice to use for underlying SPI access, if + * nullptr we use I2C + * @param reg_addr The address pointer value for the I2C/SMBus/SPI register, + * can be 8 or 16 bits + * @param type The method we use to read/write data to SPI (which is not + * as well defined as I2C) + * @param width The width of the register data itself, defaults to 1 byte + * @param byteorder The byte order of the register (used when width is > 1), + * defaults to LSBFIRST + * @param address_width The width of the register address itself, defaults + * to 1 byte + */ +Adafruit_BusIO_Register::Adafruit_BusIO_Register( + Adafruit_I2CDevice *i2cdevice, Adafruit_SPIDevice *spidevice, + Adafruit_BusIO_SPIRegType type, uint16_t reg_addr, uint8_t width, + uint8_t byteorder, uint8_t address_width) { + _spidevice = spidevice; + _i2cdevice = i2cdevice; + _spiregtype = type; + _addrwidth = address_width; + _address = reg_addr; + _byteorder = byteorder; + _width = width; +} + +/*! + * @brief Write a buffer of data to the register location + * @param buffer Pointer to data to write + * @param len Number of bytes to write + * @return True on successful write (only really useful for I2C as SPI is + * uncheckable) + */ +bool Adafruit_BusIO_Register::write(uint8_t *buffer, uint8_t len) { + + uint8_t addrbuffer[2] = {(uint8_t)(_address & 0xFF), + (uint8_t)(_address >> 8)}; + + if (_i2cdevice) { + return _i2cdevice->write(buffer, len, true, addrbuffer, _addrwidth); + } + if (_spidevice) { + if (_spiregtype == ADDRESSED_OPCODE_BIT0_LOW_TO_WRITE) { + // very special case! + + // pass the special opcode address which we set as the high byte of the + // regaddr + addrbuffer[0] = + (uint8_t)(_address >> 8) & ~0x01; // set bottom bit low to write + // the 'actual' reg addr is the second byte then + addrbuffer[1] = (uint8_t)(_address & 0xFF); + // the address appears to be a byte longer + return _spidevice->write(buffer, len, addrbuffer, _addrwidth + 1); + } + + if (_spiregtype == ADDRBIT8_HIGH_TOREAD) { + addrbuffer[0] &= ~0x80; + } + if (_spiregtype == ADDRBIT8_HIGH_TOWRITE) { + addrbuffer[0] |= 0x80; + } + if (_spiregtype == AD8_HIGH_TOREAD_AD7_HIGH_TOINC) { + addrbuffer[0] &= ~0x80; + addrbuffer[0] |= 0x40; + } + return _spidevice->write(buffer, len, addrbuffer, _addrwidth); + } + return false; +} + +/*! + * @brief Write up to 4 bytes of data to the register location + * @param value Data to write + * @param numbytes How many bytes from 'value' to write + * @return True on successful write (only really useful for I2C as SPI is + * uncheckable) + */ +bool Adafruit_BusIO_Register::write(uint32_t value, uint8_t numbytes) { + if (numbytes == 0) { + numbytes = _width; + } + if (numbytes > 4) { + return false; + } + + // store a copy + _cached = value; + + for (int i = 0; i < numbytes; i++) { + if (_byteorder == LSBFIRST) { + _buffer[i] = value & 0xFF; + } else { + _buffer[numbytes - i - 1] = value & 0xFF; + } + value >>= 8; + } + return write(_buffer, numbytes); +} + +/*! + * @brief Read data from the register location. This does not do any error + * checking! + * @return Returns 0xFFFFFFFF on failure, value otherwise + */ +uint32_t Adafruit_BusIO_Register::read(void) { + if (!read(_buffer, _width)) { + return -1; + } + + uint32_t value = 0; + + for (int i = 0; i < _width; i++) { + value <<= 8; + if (_byteorder == LSBFIRST) { + value |= _buffer[_width - i - 1]; + } else { + value |= _buffer[i]; + } + } + + return value; +} + +/*! + * @brief Read cached data from last time we wrote to this register + * @return Returns 0xFFFFFFFF on failure, value otherwise + */ +uint32_t Adafruit_BusIO_Register::readCached(void) { return _cached; } + +/*! + * @brief Read a buffer of data from the register location + * @param buffer Pointer to data to read into + * @param len Number of bytes to read + * @return True on successful write (only really useful for I2C as SPI is + * uncheckable) + */ +bool Adafruit_BusIO_Register::read(uint8_t *buffer, uint8_t len) { + uint8_t addrbuffer[2] = {(uint8_t)(_address & 0xFF), + (uint8_t)(_address >> 8)}; + + if (_i2cdevice) { + return _i2cdevice->write_then_read(addrbuffer, _addrwidth, buffer, len); + } + if (_spidevice) { + if (_spiregtype == ADDRESSED_OPCODE_BIT0_LOW_TO_WRITE) { + // very special case! + + // pass the special opcode address which we set as the high byte of the + // regaddr + addrbuffer[0] = + (uint8_t)(_address >> 8) | 0x01; // set bottom bit high to read + // the 'actual' reg addr is the second byte then + addrbuffer[1] = (uint8_t)(_address & 0xFF); + // the address appears to be a byte longer + return _spidevice->write_then_read(addrbuffer, _addrwidth + 1, buffer, + len); + } + if (_spiregtype == ADDRBIT8_HIGH_TOREAD) { + addrbuffer[0] |= 0x80; + } + if (_spiregtype == ADDRBIT8_HIGH_TOWRITE) { + addrbuffer[0] &= ~0x80; + } + if (_spiregtype == AD8_HIGH_TOREAD_AD7_HIGH_TOINC) { + addrbuffer[0] |= 0x80 | 0x40; + } + return _spidevice->write_then_read(addrbuffer, _addrwidth, buffer, len); + } + return false; +} + +/*! + * @brief Read 2 bytes of data from the register location + * @param value Pointer to uint16_t variable to read into + * @return True on successful write (only really useful for I2C as SPI is + * uncheckable) + */ +bool Adafruit_BusIO_Register::read(uint16_t *value) { + if (!read(_buffer, 2)) { + return false; + } + + if (_byteorder == LSBFIRST) { + *value = _buffer[1]; + *value <<= 8; + *value |= _buffer[0]; + } else { + *value = _buffer[0]; + *value <<= 8; + *value |= _buffer[1]; + } + return true; +} + +/*! + * @brief Read 1 byte of data from the register location + * @param value Pointer to uint8_t variable to read into + * @return True on successful write (only really useful for I2C as SPI is + * uncheckable) + */ +bool Adafruit_BusIO_Register::read(uint8_t *value) { + if (!read(_buffer, 1)) { + return false; + } + + *value = _buffer[0]; + return true; +} + +/*! + * @brief Pretty printer for this register + * @param s The Stream to print to, defaults to &Serial + */ +void Adafruit_BusIO_Register::print(Stream *s) { + uint32_t val = read(); + s->print("0x"); + s->print(val, HEX); +} + +/*! + * @brief Pretty printer for this register + * @param s The Stream to print to, defaults to &Serial + */ +void Adafruit_BusIO_Register::println(Stream *s) { + print(s); + s->println(); +} + +/*! + * @brief Create a slice of the register that we can address without + * touching other bits + * @param reg The Adafruit_BusIO_Register which defines the bus/register + * @param bits The number of bits wide we are slicing + * @param shift The number of bits that our bit-slice is shifted from LSB + */ +Adafruit_BusIO_RegisterBits::Adafruit_BusIO_RegisterBits( + Adafruit_BusIO_Register *reg, uint8_t bits, uint8_t shift) { + _register = reg; + _bits = bits; + _shift = shift; +} + +/*! + * @brief Read 4 bytes of data from the register + * @return data The 4 bytes to read + */ +uint32_t Adafruit_BusIO_RegisterBits::read(void) { + uint32_t val = _register->read(); + val >>= _shift; + return val & ((1 << (_bits)) - 1); +} + +/*! + * @brief Write 4 bytes of data to the register + * @param data The 4 bytes to write + * @return True on successful write (only really useful for I2C as SPI is + * uncheckable) + */ +bool Adafruit_BusIO_RegisterBits::write(uint32_t data) { + uint32_t val = _register->read(); + + // mask off the data before writing + uint32_t mask = (1 << (_bits)) - 1; + data &= mask; + + mask <<= _shift; + val &= ~mask; // remove the current data at that spot + val |= data << _shift; // and add in the new data + + return _register->write(val, _register->width()); +} + +/*! + * @brief The width of the register data, helpful for doing calculations + * @returns The data width used when initializing the register + */ +uint8_t Adafruit_BusIO_Register::width(void) { return _width; } + +/*! + * @brief Set the default width of data + * @param width the default width of data read from register + */ +void Adafruit_BusIO_Register::setWidth(uint8_t width) { _width = width; } + +/*! + * @brief Set register address + * @param address the address from register + */ +void Adafruit_BusIO_Register::setAddress(uint16_t address) { + _address = address; +} + +/*! + * @brief Set the width of register address + * @param address_width the width for register address + */ +void Adafruit_BusIO_Register::setAddressWidth(uint16_t address_width) { + _addrwidth = address_width; +} + +#endif // SPI exists diff --git a/src/Adafruit_BusIO-master/Adafruit_BusIO_Register.h b/src/Adafruit_BusIO-master/Adafruit_BusIO_Register.h new file mode 100644 index 0000000..871a95d --- /dev/null +++ b/src/Adafruit_BusIO-master/Adafruit_BusIO_Register.h @@ -0,0 +1,105 @@ +#ifndef Adafruit_BusIO_Register_h +#define Adafruit_BusIO_Register_h + +#include + +#if !defined(SPI_INTERFACES_COUNT) || \ + (defined(SPI_INTERFACES_COUNT) && (SPI_INTERFACES_COUNT > 0)) + +#include "Adafruit_I2CDevice.h" +#include "Adafruit_SPIDevice.h" + +typedef enum _Adafruit_BusIO_SPIRegType { + ADDRBIT8_HIGH_TOREAD = 0, + /*!< + * ADDRBIT8_HIGH_TOREAD + * When reading a register you must actually send the value 0x80 + register + * address to the device. e.g. To read the register 0x0B the register value + * 0x8B is sent and to write 0x0B is sent. + */ + AD8_HIGH_TOREAD_AD7_HIGH_TOINC = 1, + + /*!< + * ADDRBIT8_HIGH_TOWRITE + * When writing to a register you must actually send the value 0x80 + + * the register address to the device. e.g. To write to the register 0x19 the + * register value 0x99 is sent and to read 0x19 is sent. + */ + ADDRBIT8_HIGH_TOWRITE = 2, + + /*!< + * ADDRESSED_OPCODE_LOWBIT_TO_WRITE + * Used by the MCP23S series, we send 0x40 |'rd with the opcode + * Then set the lowest bit to write + */ + ADDRESSED_OPCODE_BIT0_LOW_TO_WRITE = 3, + +} Adafruit_BusIO_SPIRegType; + +/*! + * @brief The class which defines a device register (a location to read/write + * data from) + */ +class Adafruit_BusIO_Register { +public: + Adafruit_BusIO_Register(Adafruit_I2CDevice *i2cdevice, uint16_t reg_addr, + uint8_t width = 1, uint8_t byteorder = LSBFIRST, + uint8_t address_width = 1); + + Adafruit_BusIO_Register(Adafruit_SPIDevice *spidevice, uint16_t reg_addr, + Adafruit_BusIO_SPIRegType type, uint8_t width = 1, + uint8_t byteorder = LSBFIRST, + uint8_t address_width = 1); + + Adafruit_BusIO_Register(Adafruit_I2CDevice *i2cdevice, + Adafruit_SPIDevice *spidevice, + Adafruit_BusIO_SPIRegType type, uint16_t reg_addr, + uint8_t width = 1, uint8_t byteorder = LSBFIRST, + uint8_t address_width = 1); + + bool read(uint8_t *buffer, uint8_t len); + bool read(uint8_t *value); + bool read(uint16_t *value); + uint32_t read(void); + uint32_t readCached(void); + bool write(uint8_t *buffer, uint8_t len); + bool write(uint32_t value, uint8_t numbytes = 0); + + uint8_t width(void); + + void setWidth(uint8_t width); + void setAddress(uint16_t address); + void setAddressWidth(uint16_t address_width); + + void print(Stream *s = &Serial); + void println(Stream *s = &Serial); + +private: + Adafruit_I2CDevice *_i2cdevice; + Adafruit_SPIDevice *_spidevice; + Adafruit_BusIO_SPIRegType _spiregtype; + uint16_t _address; + uint8_t _width, _addrwidth, _byteorder; + uint8_t _buffer[4]; // we won't support anything larger than uint32 for + // non-buffered read + uint32_t _cached = 0; +}; + +/*! + * @brief The class which defines a slice of bits from within a device register + * (a location to read/write data from) + */ +class Adafruit_BusIO_RegisterBits { +public: + Adafruit_BusIO_RegisterBits(Adafruit_BusIO_Register *reg, uint8_t bits, + uint8_t shift); + bool write(uint32_t value); + uint32_t read(void); + +private: + Adafruit_BusIO_Register *_register; + uint8_t _bits, _shift; +}; + +#endif // SPI exists +#endif // BusIO_Register_h diff --git a/src/Adafruit_BusIO-master/Adafruit_I2CDevice.cpp b/src/Adafruit_BusIO-master/Adafruit_I2CDevice.cpp new file mode 100644 index 0000000..9b518b4 --- /dev/null +++ b/src/Adafruit_BusIO-master/Adafruit_I2CDevice.cpp @@ -0,0 +1,313 @@ +#include "Adafruit_I2CDevice.h" + +//#define DEBUG_SERIAL Serial + +/*! + * @brief Create an I2C device at a given address + * @param addr The 7-bit I2C address for the device + * @param theWire The I2C bus to use, defaults to &Wire + */ +Adafruit_I2CDevice::Adafruit_I2CDevice(uint8_t addr, TwoWire *theWire) { + _addr = addr; + _wire = theWire; + _begun = false; +#ifdef ARDUINO_ARCH_SAMD + _maxBufferSize = 250; // as defined in Wire.h's RingBuffer +#elif defined(ESP32) + _maxBufferSize = I2C_BUFFER_LENGTH; +#else + _maxBufferSize = 32; +#endif +} + +/*! + * @brief Initializes and does basic address detection + * @param addr_detect Whether we should attempt to detect the I2C address + * with a scan. 99% of sensors/devices don't mind but once in a while, they spaz + * on a scan! + * @return True if I2C initialized and a device with the addr found + */ +bool Adafruit_I2CDevice::begin(bool addr_detect) { + _wire->begin(); + _begun = true; + + if (addr_detect) { + return detected(); + } + return true; +} + +/*! + * @brief De-initialize device, turn off the Wire interface + */ +void Adafruit_I2CDevice::end(void) { + // Not all port implement Wire::end(), such as + // - ESP8266 + // - AVR core without WIRE_HAS_END + // - ESP32: end() is implemented since 2.0.1 which is latest at the moment. + // Temporarily disable for now to give time for user to update. +#if !(defined(ESP8266) || \ + (defined(ARDUINO_ARCH_AVR) && !defined(WIRE_HAS_END)) || \ + defined(ARDUINO_ARCH_ESP32)) + _wire->end(); + _begun = false; +#endif +} + +/*! + * @brief Scans I2C for the address - note will give a false-positive + * if there's no pullups on I2C + * @return True if I2C initialized and a device with the addr found + */ +bool Adafruit_I2CDevice::detected(void) { + // Init I2C if not done yet + if (!_begun && !begin()) { + return false; + } + + // A basic scanner, see if it ACK's + _wire->beginTransmission(_addr); + if (_wire->endTransmission() == 0) { +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.println(F("Detected")); +#endif + return true; + } +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.println(F("Not detected")); +#endif + return false; +} + +/*! + * @brief Write a buffer or two to the I2C device. Cannot be more than + * maxBufferSize() bytes. + * @param buffer Pointer to buffer of data to write. This is const to + * ensure the content of this buffer doesn't change. + * @param len Number of bytes from buffer to write + * @param prefix_buffer Pointer to optional array of data to write before + * buffer. Cannot be more than maxBufferSize() bytes. This is const to + * ensure the content of this buffer doesn't change. + * @param prefix_len Number of bytes from prefix buffer to write + * @param stop Whether to send an I2C STOP signal on write + * @return True if write was successful, otherwise false. + */ +bool Adafruit_I2CDevice::write(const uint8_t *buffer, size_t len, bool stop, + const uint8_t *prefix_buffer, + size_t prefix_len) { + if ((len + prefix_len) > maxBufferSize()) { + // currently not guaranteed to work if more than 32 bytes! + // we will need to find out if some platforms have larger + // I2C buffer sizes :/ +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.println(F("\tI2CDevice could not write such a large buffer")); +#endif + return false; + } + + _wire->beginTransmission(_addr); + + // Write the prefix data (usually an address) + if ((prefix_len != 0) && (prefix_buffer != nullptr)) { + if (_wire->write(prefix_buffer, prefix_len) != prefix_len) { +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.println(F("\tI2CDevice failed to write")); +#endif + return false; + } + } + + // Write the data itself + if (_wire->write(buffer, len) != len) { +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.println(F("\tI2CDevice failed to write")); +#endif + return false; + } + +#ifdef DEBUG_SERIAL + + DEBUG_SERIAL.print(F("\tI2CWRITE @ 0x")); + DEBUG_SERIAL.print(_addr, HEX); + DEBUG_SERIAL.print(F(" :: ")); + if ((prefix_len != 0) && (prefix_buffer != nullptr)) { + for (uint16_t i = 0; i < prefix_len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(prefix_buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + } + } + for (uint16_t i = 0; i < len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + if (i % 32 == 31) { + DEBUG_SERIAL.println(); + } + } + + if (stop) { + DEBUG_SERIAL.print("\tSTOP"); + } +#endif + + if (_wire->endTransmission(stop) == 0) { +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.println(); + // DEBUG_SERIAL.println("Sent!"); +#endif + return true; + } else { +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.println("\tFailed to send!"); +#endif + return false; + } +} + +/*! + * @brief Read from I2C into a buffer from the I2C device. + * Cannot be more than maxBufferSize() bytes. + * @param buffer Pointer to buffer of data to read into + * @param len Number of bytes from buffer to read. + * @param stop Whether to send an I2C STOP signal on read + * @return True if read was successful, otherwise false. + */ +bool Adafruit_I2CDevice::read(uint8_t *buffer, size_t len, bool stop) { + size_t pos = 0; + while (pos < len) { + size_t read_len = + ((len - pos) > maxBufferSize()) ? maxBufferSize() : (len - pos); + bool read_stop = (pos < (len - read_len)) ? false : stop; + if (!_read(buffer + pos, read_len, read_stop)) + return false; + pos += read_len; + } + return true; +} + +bool Adafruit_I2CDevice::_read(uint8_t *buffer, size_t len, bool stop) { +#if defined(TinyWireM_h) + size_t recv = _wire->requestFrom((uint8_t)_addr, (uint8_t)len); +#elif defined(ARDUINO_ARCH_MEGAAVR) + size_t recv = _wire->requestFrom(_addr, len, stop); +#else + size_t recv = _wire->requestFrom((uint8_t)_addr, (uint8_t)len, (uint8_t)stop); +#endif + + if (recv != len) { + // Not enough data available to fulfill our obligation! +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.print(F("\tI2CDevice did not receive enough data: ")); + DEBUG_SERIAL.println(recv); +#endif + return false; + } + + for (uint16_t i = 0; i < len; i++) { + buffer[i] = _wire->read(); + } + +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.print(F("\tI2CREAD @ 0x")); + DEBUG_SERIAL.print(_addr, HEX); + DEBUG_SERIAL.print(F(" :: ")); + for (uint16_t i = 0; i < len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + if (len % 32 == 31) { + DEBUG_SERIAL.println(); + } + } + DEBUG_SERIAL.println(); +#endif + + return true; +} + +/*! + * @brief Write some data, then read some data from I2C into another buffer. + * Cannot be more than maxBufferSize() bytes. The buffers can point to + * same/overlapping locations. + * @param write_buffer Pointer to buffer of data to write from + * @param write_len Number of bytes from buffer to write. + * @param read_buffer Pointer to buffer of data to read into. + * @param read_len Number of bytes from buffer to read. + * @param stop Whether to send an I2C STOP signal between the write and read + * @return True if write & read was successful, otherwise false. + */ +bool Adafruit_I2CDevice::write_then_read(const uint8_t *write_buffer, + size_t write_len, uint8_t *read_buffer, + size_t read_len, bool stop) { + if (!write(write_buffer, write_len, stop)) { + return false; + } + + return read(read_buffer, read_len); +} + +/*! + * @brief Returns the 7-bit address of this device + * @return The 7-bit address of this device + */ +uint8_t Adafruit_I2CDevice::address(void) { return _addr; } + +/*! + * @brief Change the I2C clock speed to desired (relies on + * underlying Wire support! + * @param desiredclk The desired I2C SCL frequency + * @return True if this platform supports changing I2C speed. + * Not necessarily that the speed was achieved! + */ +bool Adafruit_I2CDevice::setSpeed(uint32_t desiredclk) { +#if defined(__AVR_ATmega328__) || \ + defined(__AVR_ATmega328P__) // fix arduino core set clock + // calculate TWBR correctly + + if ((F_CPU / 18) < desiredclk) { +#ifdef DEBUG_SERIAL + Serial.println(F("I2C.setSpeed too high.")); +#endif + return false; + } + uint32_t atwbr = ((F_CPU / desiredclk) - 16) / 2; + if (atwbr > 16320) { +#ifdef DEBUG_SERIAL + Serial.println(F("I2C.setSpeed too low.")); +#endif + return false; + } + + if (atwbr <= 255) { + atwbr /= 1; + TWSR = 0x0; + } else if (atwbr <= 1020) { + atwbr /= 4; + TWSR = 0x1; + } else if (atwbr <= 4080) { + atwbr /= 16; + TWSR = 0x2; + } else { // if (atwbr <= 16320) + atwbr /= 64; + TWSR = 0x3; + } + TWBR = atwbr; + +#ifdef DEBUG_SERIAL + Serial.print(F("TWSR prescaler = ")); + Serial.println(pow(4, TWSR)); + Serial.print(F("TWBR = ")); + Serial.println(atwbr); +#endif + return true; +#elif (ARDUINO >= 157) && !defined(ARDUINO_STM32_FEATHER) && \ + !defined(TinyWireM_h) + _wire->setClock(desiredclk); + return true; + +#else + (void)desiredclk; + return false; +#endif +} diff --git a/src/Adafruit_BusIO-master/Adafruit_I2CDevice.h b/src/Adafruit_BusIO-master/Adafruit_I2CDevice.h new file mode 100644 index 0000000..6bda7ba --- /dev/null +++ b/src/Adafruit_BusIO-master/Adafruit_I2CDevice.h @@ -0,0 +1,36 @@ +#ifndef Adafruit_I2CDevice_h +#define Adafruit_I2CDevice_h + +#include +#include + +///< The class which defines how we will talk to this device over I2C +class Adafruit_I2CDevice { +public: + Adafruit_I2CDevice(uint8_t addr, TwoWire *theWire = &Wire); + uint8_t address(void); + bool begin(bool addr_detect = true); + void end(void); + bool detected(void); + + bool read(uint8_t *buffer, size_t len, bool stop = true); + bool write(const uint8_t *buffer, size_t len, bool stop = true, + const uint8_t *prefix_buffer = nullptr, size_t prefix_len = 0); + bool write_then_read(const uint8_t *write_buffer, size_t write_len, + uint8_t *read_buffer, size_t read_len, + bool stop = false); + bool setSpeed(uint32_t desiredclk); + + /*! @brief How many bytes we can read in a transaction + * @return The size of the Wire receive/transmit buffer */ + size_t maxBufferSize() { return _maxBufferSize; } + +private: + uint8_t _addr; + TwoWire *_wire; + bool _begun; + size_t _maxBufferSize; + bool _read(uint8_t *buffer, size_t len, bool stop); +}; + +#endif // Adafruit_I2CDevice_h diff --git a/src/Adafruit_BusIO-master/Adafruit_I2CRegister.h b/src/Adafruit_BusIO-master/Adafruit_I2CRegister.h new file mode 100644 index 0000000..da3c439 --- /dev/null +++ b/src/Adafruit_BusIO-master/Adafruit_I2CRegister.h @@ -0,0 +1,10 @@ +#ifndef _ADAFRUIT_I2C_REGISTER_H_ +#define _ADAFRUIT_I2C_REGISTER_H_ + +#include "Adafruit_BusIO_Register.h" +#include + +typedef Adafruit_BusIO_Register Adafruit_I2CRegister; +typedef Adafruit_BusIO_RegisterBits Adafruit_I2CRegisterBits; + +#endif diff --git a/src/Adafruit_BusIO-master/Adafruit_SPIDevice.cpp b/src/Adafruit_BusIO-master/Adafruit_SPIDevice.cpp new file mode 100644 index 0000000..44a8f55 --- /dev/null +++ b/src/Adafruit_BusIO-master/Adafruit_SPIDevice.cpp @@ -0,0 +1,493 @@ +#include "Adafruit_SPIDevice.h" + +#if !defined(SPI_INTERFACES_COUNT) || \ + (defined(SPI_INTERFACES_COUNT) && (SPI_INTERFACES_COUNT > 0)) + +//#define DEBUG_SERIAL Serial + +/*! + * @brief Create an SPI device with the given CS pin and settings + * @param cspin The arduino pin number to use for chip select + * @param freq The SPI clock frequency to use, defaults to 1MHz + * @param dataOrder The SPI data order to use for bits within each byte, + * defaults to SPI_BITORDER_MSBFIRST + * @param dataMode The SPI mode to use, defaults to SPI_MODE0 + * @param theSPI The SPI bus to use, defaults to &theSPI + */ +Adafruit_SPIDevice::Adafruit_SPIDevice(int8_t cspin, uint32_t freq, + BusIOBitOrder dataOrder, + uint8_t dataMode, SPIClass *theSPI) { + _cs = cspin; + _sck = _mosi = _miso = -1; + _spi = theSPI; + _begun = false; + _spiSetting = new SPISettings(freq, dataOrder, dataMode); + _freq = freq; + _dataOrder = dataOrder; + _dataMode = dataMode; +} + +/*! + * @brief Create an SPI device with the given CS pin and settings + * @param cspin The arduino pin number to use for chip select + * @param sckpin The arduino pin number to use for SCK + * @param misopin The arduino pin number to use for MISO, set to -1 if not + * used + * @param mosipin The arduino pin number to use for MOSI, set to -1 if not + * used + * @param freq The SPI clock frequency to use, defaults to 1MHz + * @param dataOrder The SPI data order to use for bits within each byte, + * defaults to SPI_BITORDER_MSBFIRST + * @param dataMode The SPI mode to use, defaults to SPI_MODE0 + */ +Adafruit_SPIDevice::Adafruit_SPIDevice(int8_t cspin, int8_t sckpin, + int8_t misopin, int8_t mosipin, + uint32_t freq, BusIOBitOrder dataOrder, + uint8_t dataMode) { + _cs = cspin; + _sck = sckpin; + _miso = misopin; + _mosi = mosipin; + +#ifdef BUSIO_USE_FAST_PINIO + csPort = (BusIO_PortReg *)portOutputRegister(digitalPinToPort(cspin)); + csPinMask = digitalPinToBitMask(cspin); + if (mosipin != -1) { + mosiPort = (BusIO_PortReg *)portOutputRegister(digitalPinToPort(mosipin)); + mosiPinMask = digitalPinToBitMask(mosipin); + } + if (misopin != -1) { + misoPort = (BusIO_PortReg *)portInputRegister(digitalPinToPort(misopin)); + misoPinMask = digitalPinToBitMask(misopin); + } + clkPort = (BusIO_PortReg *)portOutputRegister(digitalPinToPort(sckpin)); + clkPinMask = digitalPinToBitMask(sckpin); +#endif + + _freq = freq; + _dataOrder = dataOrder; + _dataMode = dataMode; + _begun = false; + _spiSetting = new SPISettings(freq, dataOrder, dataMode); + _spi = nullptr; +} + +/*! + * @brief Release memory allocated in constructors + */ +Adafruit_SPIDevice::~Adafruit_SPIDevice() { delete _spiSetting; } + +/*! + * @brief Initializes SPI bus and sets CS pin high + * @return Always returns true because there's no way to test success of SPI + * init + */ +bool Adafruit_SPIDevice::begin(void) { + if (_cs != -1) { + pinMode(_cs, OUTPUT); + digitalWrite(_cs, HIGH); + } + + if (_spi) { // hardware SPI + _spi->begin(); + } else { + pinMode(_sck, OUTPUT); + + if ((_dataMode == SPI_MODE0) || (_dataMode == SPI_MODE1)) { + // idle low on mode 0 and 1 + digitalWrite(_sck, LOW); + } else { + // idle high on mode 2 or 3 + digitalWrite(_sck, HIGH); + } + if (_mosi != -1) { + pinMode(_mosi, OUTPUT); + digitalWrite(_mosi, HIGH); + } + if (_miso != -1) { + pinMode(_miso, INPUT); + } + } + + _begun = true; + return true; +} + +/*! + * @brief Transfer (send/receive) a buffer over hard/soft SPI, without + * transaction management + * @param buffer The buffer to send and receive at the same time + * @param len The number of bytes to transfer + */ +void Adafruit_SPIDevice::transfer(uint8_t *buffer, size_t len) { + if (_spi) { + // hardware SPI is easy + +#if defined(SPARK) + _spi->transfer(buffer, buffer, len, nullptr); +#elif defined(STM32) + for (size_t i = 0; i < len; i++) { + _spi->transfer(buffer[i]); + } +#else + _spi->transfer(buffer, len); +#endif + return; + } + + uint8_t startbit; + if (_dataOrder == SPI_BITORDER_LSBFIRST) { + startbit = 0x1; + } else { + startbit = 0x80; + } + + bool towrite, lastmosi = !(buffer[0] & startbit); + uint8_t bitdelay_us = (1000000 / _freq) / 2; + + // for softSPI we'll do it by hand + for (size_t i = 0; i < len; i++) { + // software SPI + uint8_t reply = 0; + uint8_t send = buffer[i]; + + /* + Serial.print("\tSending software SPI byte 0x"); + Serial.print(send, HEX); + Serial.print(" -> 0x"); + */ + + // Serial.print(send, HEX); + for (uint8_t b = startbit; b != 0; + b = (_dataOrder == SPI_BITORDER_LSBFIRST) ? b << 1 : b >> 1) { + + if (bitdelay_us) { + delayMicroseconds(bitdelay_us); + } + + if (_dataMode == SPI_MODE0 || _dataMode == SPI_MODE2) { + towrite = send & b; + if ((_mosi != -1) && (lastmosi != towrite)) { +#ifdef BUSIO_USE_FAST_PINIO + if (towrite) + *mosiPort |= mosiPinMask; + else + *mosiPort &= ~mosiPinMask; +#else + digitalWrite(_mosi, towrite); +#endif + lastmosi = towrite; + } + +#ifdef BUSIO_USE_FAST_PINIO + *clkPort |= clkPinMask; // Clock high +#else + digitalWrite(_sck, HIGH); +#endif + + if (bitdelay_us) { + delayMicroseconds(bitdelay_us); + } + + if (_miso != -1) { +#ifdef BUSIO_USE_FAST_PINIO + if (*misoPort & misoPinMask) { +#else + if (digitalRead(_miso)) { +#endif + reply |= b; + } + } + +#ifdef BUSIO_USE_FAST_PINIO + *clkPort &= ~clkPinMask; // Clock low +#else + digitalWrite(_sck, LOW); +#endif + } else { // if (_dataMode == SPI_MODE1 || _dataMode == SPI_MODE3) + +#ifdef BUSIO_USE_FAST_PINIO + *clkPort |= clkPinMask; // Clock high +#else + digitalWrite(_sck, HIGH); +#endif + + if (bitdelay_us) { + delayMicroseconds(bitdelay_us); + } + + if (_mosi != -1) { +#ifdef BUSIO_USE_FAST_PINIO + if (send & b) + *mosiPort |= mosiPinMask; + else + *mosiPort &= ~mosiPinMask; +#else + digitalWrite(_mosi, send & b); +#endif + } + +#ifdef BUSIO_USE_FAST_PINIO + *clkPort &= ~clkPinMask; // Clock low +#else + digitalWrite(_sck, LOW); +#endif + + if (_miso != -1) { +#ifdef BUSIO_USE_FAST_PINIO + if (*misoPort & misoPinMask) { +#else + if (digitalRead(_miso)) { +#endif + reply |= b; + } + } + } + if (_miso != -1) { + buffer[i] = reply; + } + } + } + return; +} + +/*! + * @brief Transfer (send/receive) one byte over hard/soft SPI, without + * transaction management + * @param send The byte to send + * @return The byte received while transmitting + */ +uint8_t Adafruit_SPIDevice::transfer(uint8_t send) { + uint8_t data = send; + transfer(&data, 1); + return data; +} + +/*! + * @brief Manually begin a transaction (calls beginTransaction if hardware + * SPI) + */ +void Adafruit_SPIDevice::beginTransaction(void) { + if (_spi) { + _spi->beginTransaction(*_spiSetting); + } +} + +/*! + * @brief Manually end a transaction (calls endTransaction if hardware SPI) + */ +void Adafruit_SPIDevice::endTransaction(void) { + if (_spi) { + _spi->endTransaction(); + } +} + +/*! + * @brief Assert/Deassert the CS pin if it is defined + * @param value The state the CS is set to + */ +void Adafruit_SPIDevice::setChipSelect(int value) { + if (_cs != -1) { + digitalWrite(_cs, value); + } +} + +/*! + * @brief Write a buffer or two to the SPI device, with transaction + * management. + * @brief Manually begin a transaction (calls beginTransaction if hardware + * SPI) with asserting the CS pin + */ +void Adafruit_SPIDevice::beginTransactionWithAssertingCS() { + beginTransaction(); + setChipSelect(LOW); +} + +/*! + * @brief Manually end a transaction (calls endTransaction if hardware SPI) + * with deasserting the CS pin + */ +void Adafruit_SPIDevice::endTransactionWithDeassertingCS() { + setChipSelect(HIGH); + endTransaction(); +} + +/*! + * @brief Write a buffer or two to the SPI device, with transaction + * management. + * @param buffer Pointer to buffer of data to write + * @param len Number of bytes from buffer to write + * @param prefix_buffer Pointer to optional array of data to write before + * buffer. + * @param prefix_len Number of bytes from prefix buffer to write + * @return Always returns true because there's no way to test success of SPI + * writes + */ +bool Adafruit_SPIDevice::write(const uint8_t *buffer, size_t len, + const uint8_t *prefix_buffer, + size_t prefix_len) { + beginTransactionWithAssertingCS(); + + // do the writing +#if defined(ARDUINO_ARCH_ESP32) + if (_spi) { + if (prefix_len > 0) { + _spi->transferBytes(prefix_buffer, nullptr, prefix_len); + } + if (len > 0) { + _spi->transferBytes(buffer, nullptr, len); + } + } else +#endif + { + for (size_t i = 0; i < prefix_len; i++) { + transfer(prefix_buffer[i]); + } + for (size_t i = 0; i < len; i++) { + transfer(buffer[i]); + } + } + endTransactionWithDeassertingCS(); + +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.print(F("\tSPIDevice Wrote: ")); + if ((prefix_len != 0) && (prefix_buffer != nullptr)) { + for (uint16_t i = 0; i < prefix_len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(prefix_buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + } + } + for (uint16_t i = 0; i < len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + if (i % 32 == 31) { + DEBUG_SERIAL.println(); + } + } + DEBUG_SERIAL.println(); +#endif + + return true; +} + +/*! + * @brief Read from SPI into a buffer from the SPI device, with transaction + * management. + * @param buffer Pointer to buffer of data to read into + * @param len Number of bytes from buffer to read. + * @param sendvalue The 8-bits of data to write when doing the data read, + * defaults to 0xFF + * @return Always returns true because there's no way to test success of SPI + * writes + */ +bool Adafruit_SPIDevice::read(uint8_t *buffer, size_t len, uint8_t sendvalue) { + memset(buffer, sendvalue, len); // clear out existing buffer + + beginTransactionWithAssertingCS(); + transfer(buffer, len); + endTransactionWithDeassertingCS(); + +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.print(F("\tSPIDevice Read: ")); + for (uint16_t i = 0; i < len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + if (len % 32 == 31) { + DEBUG_SERIAL.println(); + } + } + DEBUG_SERIAL.println(); +#endif + + return true; +} + +/*! + * @brief Write some data, then read some data from SPI into another buffer, + * with transaction management. The buffers can point to same/overlapping + * locations. This does not transmit-receive at the same time! + * @param write_buffer Pointer to buffer of data to write from + * @param write_len Number of bytes from buffer to write. + * @param read_buffer Pointer to buffer of data to read into. + * @param read_len Number of bytes from buffer to read. + * @param sendvalue The 8-bits of data to write when doing the data read, + * defaults to 0xFF + * @return Always returns true because there's no way to test success of SPI + * writes + */ +bool Adafruit_SPIDevice::write_then_read(const uint8_t *write_buffer, + size_t write_len, uint8_t *read_buffer, + size_t read_len, uint8_t sendvalue) { + beginTransactionWithAssertingCS(); + // do the writing +#if defined(ARDUINO_ARCH_ESP32) + if (_spi) { + if (write_len > 0) { + _spi->transferBytes(write_buffer, nullptr, write_len); + } + } else +#endif + { + for (size_t i = 0; i < write_len; i++) { + transfer(write_buffer[i]); + } + } + +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.print(F("\tSPIDevice Wrote: ")); + for (uint16_t i = 0; i < write_len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(write_buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + if (write_len % 32 == 31) { + DEBUG_SERIAL.println(); + } + } + DEBUG_SERIAL.println(); +#endif + + // do the reading + for (size_t i = 0; i < read_len; i++) { + read_buffer[i] = transfer(sendvalue); + } + +#ifdef DEBUG_SERIAL + DEBUG_SERIAL.print(F("\tSPIDevice Read: ")); + for (uint16_t i = 0; i < read_len; i++) { + DEBUG_SERIAL.print(F("0x")); + DEBUG_SERIAL.print(read_buffer[i], HEX); + DEBUG_SERIAL.print(F(", ")); + if (read_len % 32 == 31) { + DEBUG_SERIAL.println(); + } + } + DEBUG_SERIAL.println(); +#endif + + endTransactionWithDeassertingCS(); + + return true; +} + +/*! + * @brief Write some data and read some data at the same time from SPI + * into the same buffer, with transaction management. This is basicaly a wrapper + * for transfer() with CS-pin and transaction management. This /does/ + * transmit-receive at the same time! + * @param buffer Pointer to buffer of data to write/read to/from + * @param len Number of bytes from buffer to write/read. + * @return Always returns true because there's no way to test success of SPI + * writes + */ +bool Adafruit_SPIDevice::write_and_read(uint8_t *buffer, size_t len) { + beginTransactionWithAssertingCS(); + transfer(buffer, len); + endTransactionWithDeassertingCS(); + + return true; +} + +#endif // SPI exists diff --git a/src/Adafruit_BusIO-master/Adafruit_SPIDevice.h b/src/Adafruit_BusIO-master/Adafruit_SPIDevice.h new file mode 100644 index 0000000..3a792c7 --- /dev/null +++ b/src/Adafruit_BusIO-master/Adafruit_SPIDevice.h @@ -0,0 +1,123 @@ +#ifndef Adafruit_SPIDevice_h +#define Adafruit_SPIDevice_h + +#include + +#if !defined(SPI_INTERFACES_COUNT) || \ + (defined(SPI_INTERFACES_COUNT) && (SPI_INTERFACES_COUNT > 0)) + +#include + +// some modern SPI definitions don't have BitOrder enum +#if (defined(__AVR__) && !defined(ARDUINO_ARCH_MEGAAVR)) || \ + defined(ESP8266) || defined(TEENSYDUINO) || defined(SPARK) || \ + defined(ARDUINO_ARCH_SPRESENSE) || defined(MEGATINYCORE) || \ + defined(DXCORE) || defined(ARDUINO_AVR_ATmega4809) || \ + defined(ARDUINO_AVR_ATmega4808) || defined(ARDUINO_AVR_ATmega3209) || \ + defined(ARDUINO_AVR_ATmega3208) || defined(ARDUINO_AVR_ATmega1609) || \ + defined(ARDUINO_AVR_ATmega1608) || defined(ARDUINO_AVR_ATmega809) || \ + defined(ARDUINO_AVR_ATmega808) || defined(ARDUINO_ARCH_ARC32) + +typedef enum _BitOrder { + SPI_BITORDER_MSBFIRST = MSBFIRST, + SPI_BITORDER_LSBFIRST = LSBFIRST, +} BusIOBitOrder; + +#elif defined(ESP32) || defined(__ASR6501__) || defined(__ASR6502__) + +// some modern SPI definitions don't have BitOrder enum and have different SPI +// mode defines +typedef enum _BitOrder { + SPI_BITORDER_MSBFIRST = SPI_MSBFIRST, + SPI_BITORDER_LSBFIRST = SPI_LSBFIRST, +} BusIOBitOrder; + +#else +// Some platforms have a BitOrder enum but its named MSBFIRST/LSBFIRST +#define SPI_BITORDER_MSBFIRST MSBFIRST +#define SPI_BITORDER_LSBFIRST LSBFIRST +typedef BitOrder BusIOBitOrder; +#endif + +#if defined(__IMXRT1062__) // Teensy 4.x +// *Warning* I disabled the usage of FAST_PINIO as the set/clear operations +// used in the cpp file are not atomic and can effect multiple IO pins +// and if an interrupt happens in between the time the code reads the register +// and writes out the updated value, that changes one or more other IO pins +// on that same IO port, those change will be clobbered when the updated +// values are written back. A fast version can be implemented that uses the +// ports set and clear registers which are atomic. +// typedef volatile uint32_t BusIO_PortReg; +// typedef uint32_t BusIO_PortMask; +//#define BUSIO_USE_FAST_PINIO + +#elif defined(__AVR__) || defined(TEENSYDUINO) +typedef volatile uint8_t BusIO_PortReg; +typedef uint8_t BusIO_PortMask; +#define BUSIO_USE_FAST_PINIO + +#elif defined(ESP8266) || defined(ESP32) || defined(__SAM3X8E__) || \ + defined(ARDUINO_ARCH_SAMD) +typedef volatile uint32_t BusIO_PortReg; +typedef uint32_t BusIO_PortMask; +#define BUSIO_USE_FAST_PINIO + +#elif (defined(__arm__) || defined(ARDUINO_FEATHER52)) && \ + !defined(ARDUINO_ARCH_MBED) && !defined(ARDUINO_ARCH_RP2040) +typedef volatile uint32_t BusIO_PortReg; +typedef uint32_t BusIO_PortMask; +#if !defined(__ASR6501__) && !defined(__ASR6502__) +#define BUSIO_USE_FAST_PINIO +#endif + +#else +#undef BUSIO_USE_FAST_PINIO +#endif + +/**! The class which defines how we will talk to this device over SPI **/ +class Adafruit_SPIDevice { +public: + Adafruit_SPIDevice(int8_t cspin, uint32_t freq = 1000000, + BusIOBitOrder dataOrder = SPI_BITORDER_MSBFIRST, + uint8_t dataMode = SPI_MODE0, SPIClass *theSPI = &SPI); + + Adafruit_SPIDevice(int8_t cspin, int8_t sck, int8_t miso, int8_t mosi, + uint32_t freq = 1000000, + BusIOBitOrder dataOrder = SPI_BITORDER_MSBFIRST, + uint8_t dataMode = SPI_MODE0); + ~Adafruit_SPIDevice(); + + bool begin(void); + bool read(uint8_t *buffer, size_t len, uint8_t sendvalue = 0xFF); + bool write(const uint8_t *buffer, size_t len, + const uint8_t *prefix_buffer = nullptr, size_t prefix_len = 0); + bool write_then_read(const uint8_t *write_buffer, size_t write_len, + uint8_t *read_buffer, size_t read_len, + uint8_t sendvalue = 0xFF); + bool write_and_read(uint8_t *buffer, size_t len); + + uint8_t transfer(uint8_t send); + void transfer(uint8_t *buffer, size_t len); + void beginTransaction(void); + void endTransaction(void); + void beginTransactionWithAssertingCS(); + void endTransactionWithDeassertingCS(); + +private: + SPIClass *_spi; + SPISettings *_spiSetting; + uint32_t _freq; + BusIOBitOrder _dataOrder; + uint8_t _dataMode; + void setChipSelect(int value); + + int8_t _cs, _sck, _mosi, _miso; +#ifdef BUSIO_USE_FAST_PINIO + BusIO_PortReg *mosiPort, *clkPort, *misoPort, *csPort; + BusIO_PortMask mosiPinMask, misoPinMask, clkPinMask, csPinMask; +#endif + bool _begun; +}; + +#endif // has SPI defined +#endif // Adafruit_SPIDevice_h diff --git a/src/Adafruit_Si4713.cpp b/src/Adafruit_Si4713.cpp new file mode 100644 index 0000000..eb3ea2d --- /dev/null +++ b/src/Adafruit_Si4713.cpp @@ -0,0 +1,400 @@ +/*! + * @file Adafruit_Si4713.cpp + * + * @mainpage Adafruit Si4713 breakout + * + * @section intro_sec Introduction + * + * I2C Driver for Si4713 breakout + * + * This is a library for the Adafruit Si4713 breakout: + * http://www.adafruit.com/products/1958 + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing products from + * Adafruit! + * + * @section author Author + * + * Limor Fried/Ladyada (Adafruit Industries). + * + * @section license License + * + * BSD (see license.txt) + * + * @section HISTORY + * + * v1.0 - First release + */ + +#include "Adafruit_Si4713.h" + +/*! + * @brief Instantiates a new Si4713 class + * @param resetpin + * number of pin where reset is connected + * + */ +Adafruit_Si4713::Adafruit_Si4713(int8_t resetpin) { _rst = resetpin; } + +/*! + * @brief Setups the i2c and calls powerUp function. + * @param addr + * i2c address + * @param theWire + * wire object + * @return True if initialization was successful, otherwise false. + * + */ +bool Adafruit_Si4713::begin(uint8_t addr, TwoWire *theWire) { + if (i2c_dev) + delete i2c_dev; + i2c_dev = new Adafruit_I2CDevice(addr, theWire); + if (!i2c_dev->begin()) + return false; + + reset(); + + powerUp(); + + // check for Si4713 + if (getRev() != 13) + return false; + + return true; +} + +/*! + * @brief Resets the registers to default settings and puts chip in + * powerdown mode + */ +void Adafruit_Si4713::reset() { + if (_rst > 0) { + pinMode(_rst, OUTPUT); + digitalWrite(_rst, HIGH); + delay(10); + digitalWrite(_rst, LOW); + delay(10); + digitalWrite(_rst, HIGH); + } +} + +/*! + * @brief Set chip property over I2C + * @param property + * prooperty that will be set + * @param value + * value of property + */ +void Adafruit_Si4713::setProperty(uint16_t property, uint16_t value) { + _i2ccommand[0] = SI4710_CMD_SET_PROPERTY; + _i2ccommand[1] = 0; + _i2ccommand[2] = property >> 8; + _i2ccommand[3] = property & 0xFF; + _i2ccommand[4] = value >> 8; + _i2ccommand[5] = value & 0xFF; + sendCommand(6); + +#ifdef SI4713_CMD_DEBUG + Serial.print("Set Prop "); + Serial.print(property); + Serial.print(" = "); + Serial.println(value); +#endif +} + +/*! + * @brief Send command stored in _i2ccommand to chip. + * @param len + * length of command that will be send + */ +void Adafruit_Si4713::sendCommand(uint8_t len) { + // Send command + i2c_dev->write(_i2ccommand, len); + // Wait for status CTS bit + uint8_t status = 0; + while (!(status & SI4710_STATUS_CTS)) + i2c_dev->read(&status, 1); +} + +/*! + * @brief Tunes to given transmit frequency. + * @param freqKHz + * frequency in KHz + */ +void Adafruit_Si4713::tuneFM(uint16_t freqKHz) { + _i2ccommand[0] = SI4710_CMD_TX_TUNE_FREQ; + _i2ccommand[1] = 0; + _i2ccommand[2] = freqKHz >> 8; + _i2ccommand[3] = freqKHz; + sendCommand(4); + while ((getStatus() & 0x81) != 0x81) { + delay(10); + } +} + +/*! + * @brief Sets the output power level and tunes the antenna capacitor + * @param pwr + * power value + * @param antcap + * antenna capacitor (default to 0) + */ +void Adafruit_Si4713::setTXpower(uint8_t pwr, uint8_t antcap) { + _i2ccommand[0] = SI4710_CMD_TX_TUNE_POWER; + _i2ccommand[1] = 0; + _i2ccommand[2] = 0; + _i2ccommand[3] = pwr; + _i2ccommand[4] = antcap; + sendCommand(5); +} +/*! + * @brief Queries the TX status and input audio signal metrics. + */ +void Adafruit_Si4713::readASQ() { + _i2ccommand[0] = SI4710_CMD_TX_ASQ_STATUS; + _i2ccommand[1] = 0x1; + sendCommand(2); + + uint8_t resp[5]; + i2c_dev->read(resp, 5); + currASQ = resp[1]; + currInLevel = resp[4]; +} + +/*! + * @brief Queries the status of a previously sent TX Tune Freq, TX Tune + * Power, or TX Tune Measure using SI4710_CMD_TX_TUNE_STATUS command. + */ +void Adafruit_Si4713::readTuneStatus() { + _i2ccommand[0] = SI4710_CMD_TX_TUNE_STATUS; + _i2ccommand[1] = 0x1; // INTACK + sendCommand(2); + + uint8_t resp[8]; + i2c_dev->read(resp, 8); + currFreq = (uint16_t(resp[2]) << 8) | resp[3]; + currdBuV = resp[5]; + currAntCap = resp[6]; + currNoiseLevel = resp[7]; +} + +/*! + * @brief Measure the received noise level at the specified frequency using + * SI4710_CMD_TX_TUNE_MEASURE command. + * @param freq + * frequency + */ +void Adafruit_Si4713::readTuneMeasure(uint16_t freq) { + // check freq is multiple of 50khz + if (freq % 5 != 0) { + freq -= (freq % 5); + } + // Serial.print("Measuring "); Serial.println(freq); + _i2ccommand[0] = SI4710_CMD_TX_TUNE_MEASURE; + _i2ccommand[1] = 0; + _i2ccommand[2] = freq >> 8; + _i2ccommand[3] = freq; + _i2ccommand[4] = 0; + + sendCommand(5); + while (getStatus() != 0x81) + delay(10); +} + +/*! + * @brief Begin RDS + * Sets properties as follows: + * SI4713_PROP_TX_AUDIO_DEVIATION: 66.25KHz, + * SI4713_PROP_TX_RDS_DEVIATION: 2KHz, + * SI4713_PROP_TX_RDS_INTERRUPT_SOURCE: 1, + * SI4713_PROP_TX_RDS_PS_MIX: 50% mix (default value), + * SI4713_PROP_TX_RDS_PS_MISC: 0x1008, + * SI4713_PROP_TX_RDS_PS_REPEAT_COUNT: 3, + * SI4713_PROP_TX_RDS_MESSAGE_COUNT: 1, + * SI4713_PROP_TX_RDS_PS_AF: 0xE0E0, + * SI4713_PROP_TX_RDS_FIFO_SIZE: 0, + * SI4713_PROP_TX_COMPONENT_ENABLE: 7 + * @param programID + * sets SI4713_PROP_TX_RDS_PI to parameter value + */ +void Adafruit_Si4713::beginRDS(uint16_t programID) { + setProperty(SI4713_PROP_TX_AUDIO_DEVIATION, + 6625); // 66.25KHz (default is 68.25) + setProperty(SI4713_PROP_TX_RDS_DEVIATION, 200); // 2KHz (default) + setProperty(SI4713_PROP_TX_RDS_INTERRUPT_SOURCE, 0x0001); // RDS IRQ + setProperty(SI4713_PROP_TX_RDS_PI, programID); // program identifier + setProperty(SI4713_PROP_TX_RDS_PS_MIX, 0x03); // 50% mix (default) + setProperty(SI4713_PROP_TX_RDS_PS_MISC, 0x1008); // RDSD0 & RDSMS (default) + setProperty(SI4713_PROP_TX_RDS_PS_REPEAT_COUNT, 3); // 3 repeats (default) + setProperty(SI4713_PROP_TX_RDS_MESSAGE_COUNT, 1); // 1 message (default) + setProperty(SI4713_PROP_TX_RDS_PS_AF, 0xE0E0); // no AF (default) + setProperty(SI4713_PROP_TX_RDS_FIFO_SIZE, 0); // no FIFO (default) + setProperty(SI4713_PROP_TX_COMPONENT_ENABLE, + 0x0007); // enable RDS, stereo, tone +} + +/*! + * @brief Set up the RDS station string + * @param *s + * string to load + */ +void Adafruit_Si4713::setRDSstation(char *s) { + uint8_t len = strlen(s); + uint8_t slots = (len + 3) / 4; + + for (uint8_t i = 0; i < slots; i++) { + memset(_i2ccommand, ' ', 6); // clear it with ' ' + memcpy(_i2ccommand + 2, s, min(4, (int)strlen(s))); + s += 4; + _i2ccommand[6] = 0; +#ifdef SI4713_CMD_DEBUG + Serial.print("Set slot #"); + Serial.print(i); + char *slot = (char *)(_i2ccommand + 2); + Serial.print(" to '"); + Serial.print(slot); + Serial.println("'"); +#endif + _i2ccommand[0] = SI4710_CMD_TX_RDS_PS; + _i2ccommand[1] = i; // slot # + sendCommand(6); + } +} + +/*! + * @brief Queries the status of the RDS Group Buffer and loads new data into + * buffer. + * @param *s + * string to load + */ +void Adafruit_Si4713::setRDSbuffer(char *s) { + uint8_t len = strlen(s); + uint8_t slots = (len + 3) / 4; + char slot[5]; + + for (uint8_t i = 0; i < slots; i++) { + memset(_i2ccommand, ' ', 8); // clear it with ' ' + memcpy(_i2ccommand + 4, s, min(4, (int)strlen(s))); + s += 4; + _i2ccommand[8] = 0; +#ifdef SI4713_CMD_DEBUG + Serial.print("Set buff #"); + Serial.print(i); + char *slot = (char *)(_i2ccommand + 4); + Serial.print(" to '"); + Serial.print(slot); + Serial.println("'"); +#endif + _i2ccommand[0] = SI4710_CMD_TX_RDS_BUFF; + if (i == 0) + _i2ccommand[1] = 0x06; + else + _i2ccommand[1] = 0x04; + + _i2ccommand[2] = 0x20; + _i2ccommand[3] = i; + sendCommand(8); + } +} + +/*! + * @brief Read interrupt status bits. + * @return status bits + */ +uint8_t Adafruit_Si4713::getStatus() { + uint8_t resp[1] = {SI4710_CMD_GET_INT_STATUS}; + i2c_dev->write_then_read(resp, 1, resp, 1); + return resp[0]; +} + +/*! + * @brief Sends power up command to the breakout, than CTS and GPO2 output + * is disabled and than enable xtal oscilator. Also It sets properties: + * SI4713_PROP_REFCLK_FREQ: 32.768 + * SI4713_PROP_TX_PREEMPHASIS: 74uS pre-emph (USA standard) + * SI4713_PROP_TX_ACOMP_GAIN: max gain + * SI4713_PROP_TX_ACOMP_ENABLE: turned on limiter and AGC + */ +void Adafruit_Si4713::powerUp() { + _i2ccommand[0] = SI4710_CMD_POWER_UP; + _i2ccommand[1] = 0x12; + // CTS interrupt disabled + // GPO2 output disabled + // Boot normally + // xtal oscillator ENabled + // FM transmit + _i2ccommand[2] = 0x50; // analog input mode + sendCommand(3); + + // configuration! see page 254 + setProperty(SI4713_PROP_REFCLK_FREQ, 32768); // crystal is 32.768 + setProperty(SI4713_PROP_TX_PREEMPHASIS, 0); // 74uS pre-emph (USA std) + setProperty(SI4713_PROP_TX_ACOMP_GAIN, 10); // max gain? + // setProperty(SI4713_PROP_TX_ACOMP_ENABLE, 0x02); // turn on limiter, but no + // dynamic ranging + setProperty(SI4713_PROP_TX_ACOMP_ENABLE, 0x0); // turn on limiter and AGC +} + +/*! + * @brief Get the hardware revision code from the device using + * SI4710_CMD_GET_REV + * @return revision number + */ +uint8_t Adafruit_Si4713::getRev() { + _i2ccommand[0] = SI4710_CMD_GET_REV; + _i2ccommand[1] = 0; + sendCommand(2); + + uint8_t pn, fw, patch, cmp, chiprev, resp[9]; + i2c_dev->read(resp, 9); + pn = resp[1]; + fw = (uint16_t(resp[2]) << 8) | resp[3]; + patch = (uint16_t(resp[4]) << 8) | resp[5]; + cmp = (uint16_t(resp[6]) << 8) | resp[7]; + chiprev = resp[8]; + +#ifdef SI4713_CMD_DEBUG + Serial.print("Part # Si47"); + Serial.print(pn); + Serial.print("-"); + Serial.println(fw, HEX); + Serial.print("Firmware 0x"); + Serial.println(fw, HEX); + Serial.print("Patch 0x"); + Serial.println(patch, HEX); + Serial.print("Chip rev "); + Serial.write(chiprev); + Serial.println(); +#endif + + return pn; +} + +/*! + * @brief Configures GP1 / GP2 as output or Hi-Z. + * @param x + * bit value + */ +void Adafruit_Si4713::setGPIOctrl(uint8_t x) { +#ifdef SI4713_CMD_DEBUG + Serial.println("GPIO direction"); +#endif + _i2ccommand[0] = SI4710_CMD_GPO_CTL; + _i2ccommand[1] = x; + sendCommand(2); +} + +/*! + * @brief Sets GP1 / GP2 output level (low or high). + * @param x + * bit value + */ +void Adafruit_Si4713::setGPIO(uint8_t x) { +#ifdef SI4713_CMD_DEBUG + Serial.println("GPIO set"); +#endif + _i2ccommand[0] = SI4710_CMD_GPO_SET; + _i2ccommand[1] = x; + sendCommand(2); +} diff --git a/src/Adafruit_Si4713.h b/src/Adafruit_Si4713.h new file mode 100644 index 0000000..cab5898 --- /dev/null +++ b/src/Adafruit_Si4713.h @@ -0,0 +1,190 @@ +/*! + * @file Adafruit_Si4713.h + * + * This is a library for the Si4713 breakout + * + * Designed specifically to work with the Adafruit Si4713 breakout + * + * Pick one up today in the adafruit shop! + * ------> https://www.adafruit.com/product/1958 + * + * These transmitters use I2C to communicate, plus reset pin, + * 3 pins are required to interface + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit andopen-source hardware by purchasing products + * from Adafruit! + * + * Limor Fried/Ladyada (Adafruit Industries). + * + * BSD license, all text above must be included in any redistribution + */ + +#include "Arduino.h" +#include + +//#define SI471X_CMD_DEBUG +#define SI4710_ADDR0 0x11 ///< if SEN is low +#define SI4710_ADDR1 0x63 ///< if SEN is high, default! +#define SI4710_STATUS_CTS 0x80 ///< read status + +/* COMMANDS */ +#define SI4710_CMD_POWER_UP \ + 0x01 ///< Power up device and mode selection. Modes include FM transmit and + ///< analog/digital audio interface configuration. +#define SI4710_CMD_GET_REV 0x10 ///< Returns revision information on the device. +#define SI4710_CMD_POWER_DOWN 0x11 ///< Power down device +#define SI4710_CMD_SET_PROPERTY 0x12 ///< Sets the value of a property. +#define SI4710_CMD_GET_PROPERTY 0x13 ///< Retrieves a property’s value. +#define SI4710_CMD_GET_INT_STATUS 0x14 ///< Read interrupt status bits. +#define SI4710_CMD_PATCH_ARGS \ + 0x15 ///< Reserved command used for patch file downloads. +#define SI4710_CMD_PATCH_DATA \ + 0x16 ///< Reserved command used for patch file downloads. +#define SI4710_CMD_TX_TUNE_FREQ 0x30 ///< Tunes to given transmit frequency. +#define SI4710_CMD_TX_TUNE_POWER \ + 0x31 ///< Sets the output power level and tunes the antenna capacitor +#define SI4710_CMD_TX_TUNE_MEASURE \ + 0x32 ///< Measure the received noise level at the specified frequency. +#define SI4710_CMD_TX_TUNE_STATUS \ + 0x33 ///< Queries the status of a previously sent TX Tune Freq, TX Tune Power, + ///< or TX Tune Measure command. +#define SI4710_CMD_TX_ASQ_STATUS \ + 0x34 ///< Queries the TX status and input audio signal metrics. +#define SI4710_CMD_TX_RDS_BUFF \ + 0x35 ///< Queries the status of the RDS Group Buffer and loads new data into + ///< buffer. +#define SI4710_CMD_TX_RDS_PS 0x36 ///< Set up default PS strings +#define SI4710_CMD_GPO_CTL 0x80 ///< Configures GPO3 as output or Hi-Z. +#define SI4710_CMD_GPO_SET 0x81 ///< Sets GPO3 output level (low or high). + +/* Parameters */ +#define SI4713_PROP_GPO_IEN 0x0001 ///< Enables interrupt sources. +#define SI4713_PROP_DIGITAL_INPUT_FORMAT \ + 0x0101 ///< Configures the digital input format. +#define SI4713_PROP_DIGITAL_INPUT_SAMPLE_RATE \ + 0x0103 ///< Configures the digital input sample rate in 10 Hz steps. Default + ///< is 0. +#define SI4713_PROP_REFCLK_FREQ \ + 0x0201 ///< Sets frequency of the reference clock in Hz. The range is 31130 to + ///< 34406 Hz, or 0 to disable the AFC. Default is 32768 Hz. +#define SI4713_PROP_REFCLK_PRESCALE \ + 0x0202 ///< Sets the prescaler value for the reference clock. +#define SI4713_PROP_TX_COMPONENT_ENABLE \ + 0x2100 ///< Enable transmit multiplex signal components. Default has pilot and + ///< L-R enabled. +#define SI4713_PROP_TX_AUDIO_DEVIATION \ + 0x2101 ///< Configures audio frequency deviation level. Units are in 10 Hz + ///< increments. Default is 6285 (68.25 kHz). +#define SI4713_PROP_TX_PILOT_DEVIATION \ + 0x2102 ///< Configures pilot tone frequency deviation level. Units are in 10 + ///< Hz increments. Default is 675 (6.75 kHz) +#define SI4713_PROP_TX_RDS_DEVIATION \ + 0x2103 ///< Configures the RDS/RBDS frequency deviation level. Units are in 10 + ///< Hz increments. Default is 2 kHz. +#define SI4713_PROP_TX_LINE_LEVEL_INPUT_LEVEL \ + 0x2104 ///< Configures maximum analog line input level to the LIN/RIN pins to + ///< reach the maximum deviation level pro- grammed into the audio + ///< deviation property TX Audio Deviation. Default is 636 mVPK. +#define SI4713_PROP_TX_LINE_INPUT_MUTE \ + 0x2105 ///< Sets line input mute. L and R inputs may be indepen- dently muted. + ///< Default is not muted. +#define SI4713_PROP_TX_PREEMPHASIS \ + 0x2106 ///< Configures pre-emphasis time constant. Default is 0 (75 μS). +#define SI4713_PROP_TX_PILOT_FREQUENCY \ + 0x2107 ///< Configures the frequency of the stereo pilot. Default is 19000 Hz. +#define SI4713_PROP_TX_ACOMP_ENABLE \ + 0x2200 ///< Enables audio dynamic range control. Default is 0 (disabled). +#define SI4713_PROP_TX_ACOMP_THRESHOLD \ + 0x2201 ///< Sets the threshold level for audio dynamic range control. Default + ///< is –40 dB. +#define SI4713_PROP_TX_ATTACK_TIME \ + 0x2202 ///< Sets the attack time for audio dynamic range control. Default is 0 + ///< (0.5 ms). +#define SI4713_PROP_TX_RELEASE_TIME \ + 0x2203 ///< Sets the release time for audio dynamic range control. Default is + ///< 4 (1000 ms). +#define SI4713_PROP_TX_ACOMP_GAIN \ + 0x2204 ///< Sets the gain for audio dynamic range control. Default is 15 dB. +#define SI4713_PROP_TX_LIMITER_RELEASE_TIME \ + 0x2205 ///< Sets the limiter release time. Default is 102 (5.01 ms) +#define SI4713_PROP_TX_ASQ_INTERRUPT_SOURCE \ + 0x2300 ///< Configures measurements related to signal quality met- rics. + ///< Default is none selected. +#define SI4713_PROP_TX_ASQ_LEVEL_LOW \ + 0x2301 ///< Configures low audio input level detection threshold. This + ///< threshold can be used to detect silence on the incoming audio. +#define SI4713_PROP_TX_ASQ_DURATION_LOW \ + 0x2302 ///< Configures the duration which the input audio level must be below + ///< the low threshold in order to detect a low audio condition. +#define SI4713_PROP_TX_AQS_LEVEL_HIGH \ + 0x2303 ///< Configures high audio input level detection threshold. This + ///< threshold can be used to detect activity on the incoming audio. +#define SI4713_PROP_TX_AQS_DURATION_HIGH \ + 0x2304 ///< Configures the duration which the input audio level must be above + ///< the high threshold in order to detect a high audio condition. +#define SI4713_PROP_TX_RDS_INTERRUPT_SOURCE \ + 0x2C00 ///< Configure RDS interrupt sources. Default is none selected. +#define SI4713_PROP_TX_RDS_PI 0x2C01 ///< Sets transmit RDS program identifier. +#define SI4713_PROP_TX_RDS_PS_MIX \ + 0x2C02 ///< Configures mix of RDS PS Group with RDS Group Buffer. +#define SI4713_PROP_TX_RDS_PS_MISC \ + 0x2C03 ///< Miscellaneous bits to transmit along with RDS_PS Groups. +#define SI4713_PROP_TX_RDS_PS_REPEAT_COUNT \ + 0x2C04 ///< Number of times to repeat transmission of a PS message before + ///< transmitting the next PS mes- sage. +#define SI4713_PROP_TX_RDS_MESSAGE_COUNT \ + 0x2C05 ///< Number of PS messages in use. +#define SI4713_PROP_TX_RDS_PS_AF \ + 0x2C06 ///< RDS Program Service Alternate Fre- quency. This provides the + ///< ability to inform the receiver of a single alternate frequency + ///< using AF Method A coding and is transmitted along with the RDS_PS + ///< Groups. +#define SI4713_PROP_TX_RDS_FIFO_SIZE \ + 0x2C07 ///< Number of blocks reserved for the FIFO. Note that the value + ///< written must be one larger than the desired FIFO size. + +/*! + * @brief Class that stores state and functions for interacting with + * Si4713 breakout + */ +class Adafruit_Si4713 { +public: + Adafruit_Si4713(int8_t rstpin = -1); + bool begin(uint8_t addr = SI4710_ADDR1, TwoWire *theWire = &Wire); + void reset(); + + void powerUp(); + // void configure(); + uint8_t getRev(); + + void tuneFM(uint16_t freqKHz); + uint8_t getStatus(void); + void readTuneStatus(void); + void readTuneMeasure(uint16_t freq); + void setTXpower(uint8_t pwr, uint8_t antcap = 0); + void readASQ(void); + void setProperty(uint16_t p, uint16_t v); + + // RDS stuff + void beginRDS(uint16_t programID = 0xADAF); + void setRDSstation(char *s); + void setRDSbuffer(char *s); + + uint16_t currFreq; ///< current frequency + uint8_t currdBuV, ///< current BuV + currAntCap, ///< current antenna capacitor + currNoiseLevel, ///< current noise level + currASQ; ///< current ASQ + int8_t currInLevel; ///< current IN level + + void setGPIO(uint8_t x); + void setGPIOctrl(uint8_t x); + +private: + void sendCommand(uint8_t len); + + int8_t _rst; + uint8_t _i2ccommand[10]; // holds the command buffer + Adafruit_I2CDevice *i2c_dev = NULL; +}; diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..0030ea3 --- /dev/null +++ b/src/config.h @@ -0,0 +1,26 @@ +// +// Created by vlad on 18.02.2021. +// + +#ifndef GBO_WORK0001_ESP32_CONFIG_H +#define GBO_WORK0001_ESP32_CONFIG_H + +#include "Adafruit_Si4713.h" + +#define SERIAL_DEBUG 1 +#define IF_DEBUG if (SERIAL_DEBUG) + +#define WIFI_SSID "XOXJLbl nugapacbl" +#define WIFI_PSK "minetZaMinet" + + +#define FM_RST_PIN 13 + +//#define OUT_CHANNEL_0 13 +//#define OUT_CHANNEL_1 12 +//#define OUT_CHANNEL_2 14 +//#define OUT_CHANNEL_3 27 + +extern Adafruit_Si4713* radio; + +#endif //GBO_WORK0001_ESP32_CONFIG_H diff --git a/src/http/ConnectionContext.cpp b/src/http/ConnectionContext.cpp new file mode 100644 index 0000000..84aba8d --- /dev/null +++ b/src/http/ConnectionContext.cpp @@ -0,0 +1,17 @@ +#include "ConnectionContext.hpp" + +namespace httpsserver { + +ConnectionContext::ConnectionContext() { + +} + +ConnectionContext::~ConnectionContext() { + +} + +void ConnectionContext::setWebsocketHandler(WebsocketHandler *wsHandler) { + _wsHandler = wsHandler; +} + +} /* namespace httpsserver */ diff --git a/src/http/ConnectionContext.hpp b/src/http/ConnectionContext.hpp new file mode 100644 index 0000000..da88964 --- /dev/null +++ b/src/http/ConnectionContext.hpp @@ -0,0 +1,41 @@ +#ifndef SRC_CONNECTIONCONTEXT_HPP_ +#define SRC_CONNECTIONCONTEXT_HPP_ + +#include +#include + +// Required for SSL +#include "openssl/ssl.h" +#undef read + +namespace httpsserver { + +class WebsocketHandler; + +/** + * \brief Internal class to handle the state of a connection + */ +class ConnectionContext { +public: + ConnectionContext(); + virtual ~ConnectionContext(); + + virtual void signalRequestError() = 0; + virtual void signalClientClose() = 0; + virtual size_t getCacheSize() = 0; + + virtual size_t readBuffer(byte* buffer, size_t length) = 0; + virtual size_t pendingBufferSize() = 0; + + virtual size_t writeBuffer(byte* buffer, size_t length) = 0; + + virtual bool isSecure() = 0; + virtual void setWebsocketHandler(WebsocketHandler *wsHandler); + virtual IPAddress getClientIP() = 0; + + WebsocketHandler * _wsHandler; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_CONNECTIONCONTEXT_HPP_ */ diff --git a/src/http/HTTPBodyParser.hpp b/src/http/HTTPBodyParser.hpp new file mode 100644 index 0000000..6dad5b3 --- /dev/null +++ b/src/http/HTTPBodyParser.hpp @@ -0,0 +1,68 @@ +#ifndef SRC_HTTPBODYPARSER_HPP_ +#define SRC_HTTPBODYPARSER_HPP_ + +#include +#include +#include "HTTPRequest.hpp" + +namespace httpsserver { + +/** + * Superclass for various body parser implementations that can be used to + * interpret http-specific bodies (like x-www-form-urlencoded or multipart/form-data) + * + * To allow for arbitrary body length, the interface of the body parser provides access + * to one underlying "field" at a time. A field may be a value of the urlencoded string + * or a part of a multipart message. + * + * Using next() proceeds to the next field. + */ +class HTTPBodyParser { +public: + const size_t unknownLength = 0x7ffffffe; + + HTTPBodyParser(HTTPRequest * req): _request(req) {}; + virtual ~HTTPBodyParser() {} + + /** + * Proceeds to the next field of the body + * + * If a field has not been read completely, the remaining content is discarded. + * + * Returns true iff proceeding to the next field succeeded (ie there was a next field) + */ + virtual bool nextField() = 0; + + /** Returns the name of the current field */ + virtual std::string getFieldName() = 0; + + /** Returns the filename of the current field or an empty string */ + virtual std::string getFieldFilename() = 0; + + /** + * Returns the mime type of the current field. + * + * Note: This value is set by the client. It can be altered maliciously. Do NOT rely on it + * for anything that affects the security of your device or other clients connected to it! + * + * Not every BodyParser might provide this value, usually it's set to something like text/plain then + */ + virtual std::string getFieldMimeType() = 0; + + /** + * Reads a maximum of bufferSize bytes into buffer and returns the actual amount of bytes that have been read + */ + virtual size_t read(byte* buffer, size_t bufferSize) = 0; + + /** Returns true when all field data has been read */ + virtual bool endOfField() = 0; + + +protected: + /** The underlying request */ + HTTPRequest * _request; +}; + +} // namespace httpserver + +#endif \ No newline at end of file diff --git a/src/http/HTTPConnection.cpp b/src/http/HTTPConnection.cpp new file mode 100644 index 0000000..0ab739c --- /dev/null +++ b/src/http/HTTPConnection.cpp @@ -0,0 +1,691 @@ +#include "HTTPConnection.hpp" + +namespace httpsserver { + +HTTPConnection::HTTPConnection(ResourceResolver * resResolver): + _resResolver(resResolver) { + _socket = -1; + _addrLen = 0; + + _bufferProcessed = 0; + _bufferUnusedIdx = 0; + + _connectionState = STATE_UNDEFINED; + _clientState = CSTATE_UNDEFINED; + _httpHeaders = NULL; + _defaultHeaders = NULL; + _isKeepAlive = false; + _lastTransmissionTS = millis(); + _shutdownTS = 0; + _wsHandler = nullptr; +} + +HTTPConnection::~HTTPConnection() { + // Close the socket + closeConnection(); +} + +/** + * Initializes the connection from a server socket. + * + * The call WILL BLOCK if accept(serverSocketID) blocks. So use select() to check for that in advance. + */ +int HTTPConnection::initialize(int serverSocketID, HTTPHeaders *defaultHeaders) { + if (_connectionState == STATE_UNDEFINED) { + _defaultHeaders = defaultHeaders; + _addrLen = sizeof(_sockAddr); + _socket = accept(serverSocketID, (struct sockaddr * )&_sockAddr, &_addrLen); + + // Build up SSL Connection context if the socket has been created successfully + if (_socket >= 0) { + HTTPS_LOGI("New connection. Socket FID=%d", _socket); + _connectionState = STATE_INITIAL; + _httpHeaders = new HTTPHeaders(); + refreshTimeout(); + return _socket; + } + + HTTPS_LOGE("Could not accept() new connection"); + + _addrLen = 0; + _connectionState = STATE_ERROR; + _clientState = CSTATE_ACTIVE; + + // This will only be called if the connection could not be established and cleanup + // variables etc. + closeConnection(); + } + // Error: The connection has already been established or could not be established + return -1; +} + +/** + * Returns the client's IPv4 + */ +IPAddress HTTPConnection::getClientIP() { + if (_addrLen > 0 && _sockAddr.sa_family == AF_INET) { + struct sockaddr_in *sockAddrIn = (struct sockaddr_in *)(&_sockAddr); + return IPAddress(sockAddrIn->sin_addr.s_addr); + } + return IPAddress(0, 0, 0, 0); +} + +/** + * True if the connection is timed out. + * + * (Should be checkd in the loop and transition should go to CONNECTION_CLOSE if exceeded) + */ +bool HTTPConnection::isTimeoutExceeded() { + return _lastTransmissionTS + HTTPS_CONNECTION_TIMEOUT < millis(); +} + +/** + * Resets the timeout to allow again the full HTTPS_CONNECTION_TIMEOUT milliseconds + */ +void HTTPConnection::refreshTimeout() { + _lastTransmissionTS = millis(); +} + +/** + * Returns true, if the connection has been closed. + */ +bool HTTPConnection::isClosed() { + return (_connectionState == STATE_ERROR || _connectionState == STATE_CLOSED); +} + +/** + * Returns true, if the connection has been closed due to error + */ +bool HTTPConnection::isError() { + return (_connectionState == STATE_ERROR); +} + +bool HTTPConnection::isSecure() { + return false; +} + +void HTTPConnection::closeConnection() { + // TODO: Call an event handler here, maybe? + + if (_connectionState != STATE_ERROR && _connectionState != STATE_CLOSED) { + + // First call to closeConnection - set the timestamp to calculate the timeout later on + if (_connectionState != STATE_CLOSING) { + _shutdownTS = millis(); + } + + // Set the connection state to closing. We stay in closing as long as SSL has not been shutdown + // correctly + _connectionState = STATE_CLOSING; + } + + // Tear down the socket + if (_socket >= 0) { + HTTPS_LOGI("Connection closed. Socket FID=%d", _socket); + close(_socket); + _socket = -1; + _addrLen = 0; + } + + if (_connectionState != STATE_ERROR) { + _connectionState = STATE_CLOSED; + } + + if (_httpHeaders != NULL) { + HTTPS_LOGD("Free headers"); + delete _httpHeaders; + _httpHeaders = NULL; + } + + if (_wsHandler != nullptr) { + HTTPS_LOGD("Free WS Handler"); + delete _wsHandler; + _wsHandler = NULL; + } +} + +/** + * This method will try to fill up the buffer with data from + */ +int HTTPConnection::updateBuffer() { + if (!isClosed()) { + + // If there is buffer data that has been marked as processed. + // Some example is shown here: + // + // Previous configuration: + // GET / HTTP/1.1\\Host: test\\Foo: bar\\\\[some uninitialized memory] + // ^ processed ^ unusedIdx + // + // New configuration after shifting: + // Host: test\\Foo: bar\\\\[some uninitialized memory] + // ^ processed ^ unusedIdx + if (_bufferProcessed > 0) { + for(int i = 0; i < HTTPS_CONNECTION_DATA_CHUNK_SIZE; i++) { + int copyFrom = i + _bufferProcessed; + if (copyFrom < _bufferUnusedIdx) { + _receiveBuffer[i] = _receiveBuffer[copyFrom]; + } else { + break; + } + } + _bufferUnusedIdx -= _bufferProcessed; + _bufferProcessed = 0; + + } + + if (_bufferUnusedIdx < HTTPS_CONNECTION_DATA_CHUNK_SIZE) { + if (canReadData()) { + + HTTPS_LOGD("Data on Socket FID=%d", _socket); + + int readReturnCode; + + // The return code of SSL_read means: + // > 0 : Length of the data that has been read + // < 0 : Error + // = 0 : Connection closed + readReturnCode = readBytesToBuffer( + // Only after the part of the buffer that has not been processed yet + (byte*)(_receiveBuffer + sizeof(char) * _bufferUnusedIdx), + // Only append up to the end of the buffer + HTTPS_CONNECTION_DATA_CHUNK_SIZE - _bufferUnusedIdx + ); + + if (readReturnCode > 0) { + _bufferUnusedIdx += readReturnCode; + refreshTimeout(); + return readReturnCode; + + } else if (readReturnCode == 0) { + // The connection has been closed by the client + _clientState = CSTATE_CLOSED; + HTTPS_LOGI("Client closed connection, FID=%d", _socket); + // TODO: If we are in state websocket, we might need to do something here + return 0; + } else { + // An error occured + _connectionState = STATE_ERROR; + HTTPS_LOGE("An receive error occured, FID=%d", _socket); + closeConnection(); + return -1; + } + + } // data pending + + } // buffer can read more + } + return 0; +} + +bool HTTPConnection::canReadData() { + fd_set sockfds; + FD_ZERO( &sockfds ); + FD_SET(_socket, &sockfds); + + // We define an immediate timeout (return immediately, if there's no data) + timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 0; + + // Check for input + // As by 2017-12-14, it seems that FD_SETSIZE is defined as 0x40, but socket IDs now + // start at 0x1000, so we need to use _socket+1 here + select(_socket + 1, &sockfds, NULL, NULL, &timeout); + + return FD_ISSET(_socket, &sockfds); +} + +size_t HTTPConnection::readBuffer(byte* buffer, size_t length) { + updateBuffer(); + size_t bufferSize = _bufferUnusedIdx - _bufferProcessed; + + if (length > bufferSize) { + length = bufferSize; + } + + // Write until length is reached (either by param of by empty buffer + for(int i = 0; i < length; i++) { + buffer[i] = _receiveBuffer[_bufferProcessed++]; + } + + return length; +} + +size_t HTTPConnection::pendingBufferSize() { + updateBuffer(); + + return _bufferUnusedIdx - _bufferProcessed + pendingByteCount(); +} + +size_t HTTPConnection::pendingByteCount() { + return 0; // FIXME: Add the value of the equivalent function of SSL_pending() here +} + +size_t HTTPConnection::writeBuffer(byte* buffer, size_t length) { + return send(_socket, buffer, length, 0); +} + +size_t HTTPConnection::readBytesToBuffer(byte* buffer, size_t length) { + return recv(_socket, buffer, length, MSG_WAITALL | MSG_DONTWAIT); +} + +void HTTPConnection::raiseError(uint16_t code, std::string reason) { + _connectionState = STATE_ERROR; + std::string sCode = intToString(code); + + char headers[] = "\r\nConnection: close\r\nContent-Type: text/plain;charset=utf8\r\n\r\n"; + writeBuffer((byte*)"HTTP/1.1 ", 9); + writeBuffer((byte*)sCode.c_str(), sCode.length()); + writeBuffer((byte*)" ", 1); + writeBuffer((byte*)(reason.c_str()), reason.length()); + writeBuffer((byte*)headers, strlen(headers)); + writeBuffer((byte*)sCode.c_str(), sCode.length()); + writeBuffer((byte*)" ", 1); + writeBuffer((byte*)(reason.c_str()), reason.length()); + closeConnection(); +} + +void HTTPConnection::readLine(int lengthLimit) { + while(_bufferProcessed < _bufferUnusedIdx) { + char newChar = _receiveBuffer[_bufferProcessed]; + + if ( newChar == '\r') { + // Look ahead for \n (if not possible, wait for next round + if (_bufferProcessed+1 < _bufferUnusedIdx) { + if (_receiveBuffer[_bufferProcessed+1] == '\n') { + _bufferProcessed += 2; + _parserLine.parsingFinished = true; + return; + } else { + // Line has not been terminated by \r\n + HTTPS_LOGW("Line without \\r\\n (got only \\r). FID=%d", _socket); + raiseError(400, "Bad Request"); + return; + } + } + } else { + _parserLine.text += newChar; + _bufferProcessed += 1; + } + + // Check that the max request string size is not exceeded + if (_parserLine.text.length() > lengthLimit) { + HTTPS_LOGW("Header length exceeded. FID=%d", _socket); + raiseError(431, "Request Header Fields Too Large"); + return; + } + } +} + +/** + * Called by the request to signal that the client has closed the connection + */ +void HTTPConnection::signalClientClose() { + _clientState = CSTATE_CLOSED; +} + +/** + * Called by the request to signal that an error has occured + */ +void HTTPConnection::signalRequestError() { + // TODO: Check that no response has been transmitted yet + raiseError(400, "Bad Request"); +} + +/** + * Returns the cache size that should be cached (in the response) to enable keep-alive requests. + * + * 0 = no keep alive. + */ +size_t HTTPConnection::getCacheSize() { + return (_isKeepAlive ? HTTPS_KEEPALIVE_CACHESIZE : 0); +} + +void HTTPConnection::loop() { + // First, update the buffer + // newByteCount will contain the number of new bytes that have to be processed + updateBuffer(); + + if (_clientState == CSTATE_CLOSED) { + HTTPS_LOGI("Client closed (FID=%d, cstate=%d)", _socket, _clientState); + } + + if (_clientState == CSTATE_CLOSED && _bufferProcessed == _bufferUnusedIdx && _connectionState < STATE_HEADERS_FINISHED) { + closeConnection(); + } + + if (!isClosed() && isTimeoutExceeded()) { + HTTPS_LOGI("Connection timeout. FID=%d", _socket); + closeConnection(); + } + + if (!isError()) { + // State machine (Reading request, reading headers, ...) + switch(_connectionState) { + case STATE_INITIAL: // Read request line + readLine(HTTPS_REQUEST_MAX_REQUEST_LENGTH); + if (_parserLine.parsingFinished && !isClosed()) { + // Find the method + size_t spaceAfterMethodIdx = _parserLine.text.find(' '); + if (spaceAfterMethodIdx == std::string::npos) { + HTTPS_LOGW("Missing space after method"); + raiseError(400, "Bad Request"); + break; + } + _httpMethod = _parserLine.text.substr(0, spaceAfterMethodIdx); + + // Find the resource string: + size_t spaceAfterResourceIdx = _parserLine.text.find(' ', spaceAfterMethodIdx + 1); + if (spaceAfterResourceIdx == std::string::npos) { + HTTPS_LOGW("Missing space after resource"); + raiseError(400, "Bad Request"); + break; + } + _httpResource = _parserLine.text.substr(spaceAfterMethodIdx + 1, spaceAfterResourceIdx - _httpMethod.length() - 1); + + _parserLine.parsingFinished = false; + _parserLine.text = ""; + HTTPS_LOGI("Request: %s %s (FID=%d)", _httpMethod.c_str(), _httpResource.c_str(), _socket); + _connectionState = STATE_REQUEST_FINISHED; + } + + break; + case STATE_REQUEST_FINISHED: // Read headers + + while (_bufferProcessed < _bufferUnusedIdx && !isClosed()) { + readLine(HTTPS_REQUEST_MAX_HEADER_LENGTH); + if (_parserLine.parsingFinished && _connectionState != STATE_ERROR) { + + if (_parserLine.text.empty()) { + HTTPS_LOGD("Headers finished, FID=%d", _socket); + _connectionState = STATE_HEADERS_FINISHED; + + // Break, so that the rest of the body does not get flushed through + _parserLine.parsingFinished = false; + _parserLine.text = ""; + break; + } else { + int idxColon = _parserLine.text.find(':'); + if ( (idxColon != std::string::npos) && (_parserLine.text[idxColon+1]==' ') ) { + _httpHeaders->set(new HTTPHeader( + _parserLine.text.substr(0, idxColon), + _parserLine.text.substr(idxColon+2) + )); + HTTPS_LOGD("Header: %s = %s (FID=%d)", _parserLine.text.substr(0, idxColon).c_str(), _parserLine.text.substr(idxColon+2).c_str(), _socket); + } else { + HTTPS_LOGW("Malformed request header: %s", _parserLine.text.c_str()); + raiseError(400, "Bad Request"); + break; + } + } + + _parserLine.parsingFinished = false; + _parserLine.text = ""; + } + } + + break; + case STATE_HEADERS_FINISHED: // Handle body + { + HTTPS_LOGD("Resolving resource..."); + ResolvedResource resolvedResource; + + // Check which kind of node we need (Websocket or regular) + bool websocketRequested = checkWebsocket(); + + _resResolver->resolveNode(_httpMethod, _httpResource, resolvedResource, websocketRequested ? WEBSOCKET : HANDLER_CALLBACK); + + // Is there any match (may be the defaultNode, if it is configured) + if (resolvedResource.didMatch()) { + // Check for client's request to keep-alive if we have a handler function. + if (resolvedResource.getMatchingNode()->_nodeType == HANDLER_CALLBACK) { + // Did the client set connection:keep-alive? + HTTPHeader * connectionHeader = _httpHeaders->get("Connection"); + std::string connectionHeaderValue = ""; + if (connectionHeader != NULL) { + connectionHeaderValue += connectionHeader->_value; + std::transform( + connectionHeaderValue.begin(), + connectionHeaderValue.end(), + connectionHeaderValue.begin(), + [](unsigned char c){ return ::tolower(c); } + ); + } + if (std::string("keep-alive").compare(connectionHeaderValue)==0) { + HTTPS_LOGD("Keep-Alive activated. FID=%d", _socket); + _isKeepAlive = true; + } else { + HTTPS_LOGD("Keep-Alive disabled. FID=%d", _socket); + _isKeepAlive = false; + } + } else { + _isKeepAlive = false; + } + + // Create request context + HTTPRequest req = HTTPRequest( + this, + _httpHeaders, + resolvedResource.getMatchingNode(), + _httpMethod, + resolvedResource.getParams(), + _httpResource + ); + HTTPResponse res = HTTPResponse(this); + + // Add default headers to the response + auto allDefaultHeaders = _defaultHeaders->getAll(); + for(std::vector::iterator header = allDefaultHeaders->begin(); header != allDefaultHeaders->end(); ++header) { + res.setHeader((*header)->_name, (*header)->_value); + } + + // Find the request handler callback + HTTPSCallbackFunction * resourceCallback; + if (websocketRequested) { + // For the websocket, we use the handshake callback defined below + resourceCallback = &handleWebsocketHandshake; + } else { + // For resource nodes, we use the callback defined by the node itself + resourceCallback = ((ResourceNode*)resolvedResource.getMatchingNode())->_callback; + } + + // Get the current middleware chain + auto vecMw = _resResolver->getMiddleware(); + + // Anchor of the chain is the actual resource. The call to the handler is bound here + std::function next = std::function(std::bind(resourceCallback, &req, &res)); + + // Go back in the middleware chain and glue everything together + auto itMw = vecMw.rbegin(); + while(itMw != vecMw.rend()) { + next = std::function(std::bind((*itMw), &req, &res, next)); + itMw++; + } + + // We insert the internal validation middleware at the start of the chain: + next = std::function(std::bind(&validationMiddleware, &req, &res, next)); + + // Call the whole chain + next(); + + // The callback-function should have read all of the request body. + // However, if it does not, we need to clear the request body now, + // because otherwise it would be parsed in the next request. + if (!req.requestComplete()) { + HTTPS_LOGW("Callback function did not parse full request body"); + req.discardRequestBody(); + } + + // Finally, after the handshake is done, we create the WebsocketHandler and change the internal state. + if(websocketRequested) { + _wsHandler = ((WebsocketNode*)resolvedResource.getMatchingNode())->newHandler(); + _wsHandler->initialize(this); // make websocket with this connection + _connectionState = STATE_WEBSOCKET; + } else { + // Handling the request is done + HTTPS_LOGD("Handler function done, request complete"); + + // Now we need to check if we can use keep-alive to reuse the SSL connection + // However, if the client did not set content-size or defined connection: close, + // we have no chance to do so. + // Also, the programmer may have explicitly set Connection: close for the response. + std::string hConnection = res.getHeader("Connection"); + if (hConnection == "close") { + _isKeepAlive = false; + } + if (!_isKeepAlive) { + // No KeepAlive -> We are done. Transition to next state. + if (!isClosed()) { + res.finalize(); + _connectionState = STATE_BODY_FINISHED; + } + } else { + if (res.isResponseBuffered()) { + // If the response could be buffered: + res.setHeader("Connection", "keep-alive"); + res.finalize(); + if (_clientState != CSTATE_CLOSED) { + // Refresh the timeout for the new request + refreshTimeout(); + // Reset headers for the new connection + _httpHeaders->clearAll(); + // Go back to initial state + _connectionState = STATE_INITIAL; + } + } + // The response could not be buffered or the client has closed: + if (!isClosed() && _connectionState!=STATE_INITIAL) { + _connectionState = STATE_BODY_FINISHED; + } + } + } + } else { + // No match (no default route configured, nothing does match) + HTTPS_LOGW("Could not find a matching resource"); + raiseError(404, "Not Found"); + } + + } + break; + case STATE_BODY_FINISHED: // Request is complete + closeConnection(); + break; + case STATE_CLOSING: // As long as we are in closing state, we call closeConnection() again and wait for it to finish or timeout + closeConnection(); + break; + case STATE_WEBSOCKET: // Do handling of the websocket + refreshTimeout(); // don't timeout websocket connection + if(pendingBufferSize() > 0) { + HTTPS_LOGD("Calling WS handler, FID=%d", _socket); + _wsHandler->loop(); + } + + // If the client closed the connection unexpectedly + if (_clientState == CSTATE_CLOSED) { + HTTPS_LOGI("WS lost client, calling onClose, FID=%d", _socket); + _wsHandler->onClose(); + } + + // If the handler has terminated the connection, clean up and close the socket too + if (_wsHandler->closed() || _clientState == CSTATE_CLOSED) { + HTTPS_LOGI("WS closed, freeing Handler, FID=%d", _socket); + delete _wsHandler; + _wsHandler = nullptr; + _connectionState = STATE_CLOSING; + } + break; + default:; + } + } + +} + + +bool HTTPConnection::checkWebsocket() { + if(_httpMethod == "GET" && + !_httpHeaders->getValue("Host").empty() && + _httpHeaders->getValue("Upgrade") == "websocket" && + _httpHeaders->getValue("Connection").find("Upgrade") != std::string::npos && + !_httpHeaders->getValue("Sec-WebSocket-Key").empty() && + _httpHeaders->getValue("Sec-WebSocket-Version") == "13") { + + HTTPS_LOGI("Upgrading to WS, FID=%d", _socket); + return true; + } else + return false; +} + +/** + * Middleware function that handles the validation of parameters + */ +void validationMiddleware(HTTPRequest * req, HTTPResponse * res, std::function next) { + bool valid = true; + // Get the matched node + HTTPNode * node = req->getResolvedNode(); + // Get the parameters + ResourceParameters * params = req->getParams(); + + // Iterate over the validators and run them + std::vector * validators = node->getValidators(); + for(std::vector::iterator validator = validators->begin(); valid && validator != validators->end(); ++validator) { + std::string param; + if (params->getPathParameter((*validator)->_idx, param)) { + valid = ((*validator)->_validatorFunction)(param); + } else { + valid = false; + } + } + + if (valid) { + next(); + } else { + res->setStatusCode(400); + res->setStatusText("Bad Request"); + res->print("400 Bad Request"); + } +} + +/** + * Handler function for the websocket handshake. Will be used by HTTPConnection if a websocket is detected + */ +void handleWebsocketHandshake(HTTPRequest * req, HTTPResponse * res) { + res->setStatusCode(101); + res->setStatusText("Switching Protocols"); + res->setHeader("Upgrade", "websocket"); + res->setHeader("Connection", "Upgrade"); + res->setHeader("Sec-WebSocket-Accept", websocketKeyResponseHash(req->getHeader("Sec-WebSocket-Key"))); + res->print(""); +} + +/** + * Function used to compute the value of the Sec-WebSocket-Accept during Websocket handshake + */ +std::string websocketKeyResponseHash(std::string const &key) { + std::string newKey = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + uint8_t shaData[HTTPS_SHA1_LENGTH]; + esp_sha(SHA1, (uint8_t*)newKey.data(), newKey.length(), shaData); + + // Get output size required for base64 representation + size_t b64BufferSize = 0; + mbedtls_base64_encode(nullptr, 0, &b64BufferSize, (const unsigned char*)shaData, HTTPS_SHA1_LENGTH); + + // Do the real encoding + unsigned char bufferOut[b64BufferSize]; + size_t bytesEncoded = 0; + int res = mbedtls_base64_encode( + bufferOut, + b64BufferSize, + &bytesEncoded, + (const unsigned char*)shaData, + HTTPS_SHA1_LENGTH + ); + + // Check result and return the encoded string + if (res != 0) { + return std::string(); + } + return std::string((char*)bufferOut, bytesEncoded); +} // WebsocketKeyResponseHash + +} /* namespace httpsserver */ diff --git a/src/http/HTTPConnection.hpp b/src/http/HTTPConnection.hpp new file mode 100644 index 0000000..bd23057 --- /dev/null +++ b/src/http/HTTPConnection.hpp @@ -0,0 +1,173 @@ +#ifndef SRC_HTTPCONNECTION_HPP_ +#define SRC_HTTPCONNECTION_HPP_ + +#include +#include + +#include +#include +#include +#include + +// Required for sockets +#include "lwip/netdb.h" +#undef read +#include "lwip/sockets.h" + +#include "HTTPSServerConstants.hpp" +#include "ConnectionContext.hpp" + +#include "HTTPHeaders.hpp" +#include "HTTPHeader.hpp" + +#include "ResourceResolver.hpp" +#include "ResolvedResource.hpp" + +#include "ResourceNode.hpp" +#include "HTTPRequest.hpp" +#include "HTTPResponse.hpp" + +#include "WebsocketHandler.hpp" +#include "WebsocketNode.hpp" + +namespace httpsserver { + +/** + * \brief Represents a single open connection for the plain HTTPServer, without TLS + */ +class HTTPConnection : private ConnectionContext { +public: + HTTPConnection(ResourceResolver * resResolver); + virtual ~HTTPConnection(); + + virtual int initialize(int serverSocketID, HTTPHeaders *defaultHeaders); + virtual void closeConnection(); + virtual bool isSecure(); + virtual IPAddress getClientIP(); + + void loop(); + bool isClosed(); + bool isError(); + +protected: + friend class HTTPRequest; + friend class HTTPResponse; + friend class WebsocketInputStreambuf; + + virtual size_t writeBuffer(byte* buffer, size_t length); + virtual size_t readBytesToBuffer(byte* buffer, size_t length); + virtual bool canReadData(); + virtual size_t pendingByteCount(); + + // Timestamp of the last transmission action + unsigned long _lastTransmissionTS; + + // Timestamp of when the shutdown was started + unsigned long _shutdownTS; + + // Internal state machine of the connection: + // + // O --- > STATE_UNDEFINED -- initialize() --> STATE_INITIAL -- get / http/1.1 --> STATE_REQUEST_FINISHED --. + // | | | | + // | | | | Host: ...\r\n + // STATE_ERROR <- on error-----------------------<---------------------------------------< | Foo: bar\r\n + // ^ | | | \r\n + // | shutdown .--> STATE_CLOSED | | | \r\n + // | fails | | | | + // | | close() | | | + // STATE_CLOSING <---- STATE_WEBSOCKET <-. | | | + // ^ | | | | + // `---------- close() ---------- STATE_BODY_FINISHED <-- Body received or GET -- STATE_HEADERS_FINISHED <-´ + // + enum { + // The order is important, to be able to use state <= STATE_HEADERS_FINISHED etc. + + // The connection has not been established yet + STATE_UNDEFINED, + // The connection has just been created + STATE_INITIAL, + // The request line has been parsed + STATE_REQUEST_FINISHED, + // The headers have been parsed + STATE_HEADERS_FINISHED, + // The body has been parsed/the complete request has been processed (GET has body of length 0) + STATE_BODY_FINISHED, + // The connection is in websocket mode + STATE_WEBSOCKET, + // The connection is about to close (and waiting for the client to send close notify) + STATE_CLOSING, + // The connection has been closed + STATE_CLOSED, + // An error has occured + STATE_ERROR + } _connectionState; + + enum { + CSTATE_UNDEFINED, + CSTATE_ACTIVE, + CSTATE_CLOSED + } _clientState; + +private: + void raiseError(uint16_t code, std::string reason); + void readLine(int lengthLimit); + + bool isTimeoutExceeded(); + void refreshTimeout(); + + int updateBuffer(); + size_t pendingBufferSize(); + + void signalClientClose(); + void signalRequestError(); + size_t readBuffer(byte* buffer, size_t length); + size_t getCacheSize(); + bool checkWebsocket(); + + // The receive buffer + char _receiveBuffer[HTTPS_CONNECTION_DATA_CHUNK_SIZE]; + + // First index on _receive_buffer that has not been processed yet (anything before may be discarded) + int _bufferProcessed; + // The index on the receive_buffer that is the first one which is empty at the end. + int _bufferUnusedIdx; + + // Socket address, length etc for the connection + struct sockaddr _sockAddr; + socklen_t _addrLen; + int _socket; + + // Resource resolver used to resolve resources + ResourceResolver * _resResolver; + + // The parser line. The struct is used to read the next line up to the \r\n in readLine() + struct { + std::string text = ""; + bool parsingFinished = false; + } _parserLine; + + // HTTP properties: Method, Request, Headers + std::string _httpMethod; + std::string _httpResource; + HTTPHeaders * _httpHeaders; + + // Default headers that are applied to every response + HTTPHeaders * _defaultHeaders; + + // Should we use keep alive + bool _isKeepAlive; + + //Websocket connection + WebsocketHandler * _wsHandler; + +}; + +void handleWebsocketHandshake(HTTPRequest * req, HTTPResponse * res); + +std::string websocketKeyResponseHash(std::string const &key); + +void validationMiddleware(HTTPRequest * req, HTTPResponse * res, std::function next); + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPCONNECTION_HPP_ */ diff --git a/src/http/HTTPHeader.cpp b/src/http/HTTPHeader.cpp new file mode 100644 index 0000000..af75844 --- /dev/null +++ b/src/http/HTTPHeader.cpp @@ -0,0 +1,43 @@ +#include "HTTPHeader.hpp" + +#include +#include +#include + +namespace httpsserver { + +HTTPHeader::HTTPHeader(const std::string &name, const std::string &value): + _name(normalizeHeaderName(name)), + _value(value) { + +} + +HTTPHeader::~HTTPHeader() { + +} + +std::string HTTPHeader::print() { + return _name + ": " + _value; +} + +std::string normalizeHeaderName(std::string const &name) { + std::locale loc; + std::stringbuf buf; + std::ostream oBuf(&buf); + bool upper = true; + std::string::size_type len = name.length(); + for (std::string::size_type i = 0; i < len; ++i) { + if (upper) { + oBuf << std::toupper(name[i], loc); + upper = false; + } else { + oBuf << std::tolower(name[i], loc); + if (!std::isalnum(name[i], loc)) { + upper=true; + } + } + } + return buf.str(); +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPHeader.hpp b/src/http/HTTPHeader.hpp new file mode 100644 index 0000000..0e0432c --- /dev/null +++ b/src/http/HTTPHeader.hpp @@ -0,0 +1,32 @@ +#ifndef SRC_HTTPHEADER_HPP_ +#define SRC_HTTPHEADER_HPP_ +#include + +#include + +namespace httpsserver { + +/** + * \brief Represents a single name/value pair of an HTTP header + */ +class HTTPHeader { +public: + HTTPHeader(const std::string &name, const std::string &value); + virtual ~HTTPHeader(); + const std::string _name; + const std::string _value; + std::string print(); +}; + +/** + * \brief Normalizes case in header names + * + * It converts the first letter and every letter after a non-alnum character + * to uppercase. For example, "content-length" becomes "Content-Length" and + * "HOST" becomes "Host". + */ +std::string normalizeHeaderName(std::string const &name); + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPHEADER_HPP_ */ diff --git a/src/http/HTTPHeaders.cpp b/src/http/HTTPHeaders.cpp new file mode 100644 index 0000000..c06f6c6 --- /dev/null +++ b/src/http/HTTPHeaders.cpp @@ -0,0 +1,61 @@ +#include "HTTPHeaders.hpp" + +namespace httpsserver { + +HTTPHeaders::HTTPHeaders() { + _headers = new std::vector(); + +} + +HTTPHeaders::~HTTPHeaders() { + clearAll(); + delete _headers; +} + +HTTPHeader * HTTPHeaders::get(std::string const &name) { + std::string normalizedName = normalizeHeaderName(name); + for(std::vector::iterator header = _headers->begin(); header != _headers->end(); ++header) { + if ((*header)->_name.compare(normalizedName)==0) { + return (*header); + } + } + return NULL; +} + +std::string HTTPHeaders::getValue(std::string const &name) { + std::string normalizedName = normalizeHeaderName(name); + for(std::vector::iterator header = _headers->begin(); header != _headers->end(); ++header) { + if ((*header)->_name.compare(normalizedName)==0) { + return ((*header)->_value); + } + } + return ""; +} + + +void HTTPHeaders::set(HTTPHeader * header) { + for(int i = 0; i < _headers->size(); i++) { + if ((*_headers)[i]->_name.compare(header->_name)==0) { + delete (*_headers)[i]; + (*_headers)[i] = header; + return; + } + } + _headers->push_back(header); +} + +std::vector * HTTPHeaders::getAll() { + return _headers; +} + +/** + * Deletes all headers + */ +void HTTPHeaders::clearAll() { + for(std::vector::iterator header = _headers->begin(); header != _headers->end(); ++header) { + delete (*header); + } + _headers->clear(); +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPHeaders.hpp b/src/http/HTTPHeaders.hpp new file mode 100644 index 0000000..e3a6b2e --- /dev/null +++ b/src/http/HTTPHeaders.hpp @@ -0,0 +1,37 @@ +#ifndef SRC_HTTPHEADERS_HPP_ +#define SRC_HTTPHEADERS_HPP_ + +#include +// Arduino declares it's own min max, incompatible with the stl... +#undef min +#undef max +#include + +#include "HTTPSServerConstants.hpp" +#include "HTTPHeader.hpp" + +namespace httpsserver { + +/** + * \brief Groups and manages a set of HTTPHeader instances + */ +class HTTPHeaders { +public: + HTTPHeaders(); + virtual ~HTTPHeaders(); + + HTTPHeader * get(std::string const &name); + std::string getValue(std::string const &name); + void set(HTTPHeader * header); + + std::vector * getAll(); + + void clearAll(); + +private: + std::vector * _headers; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPHEADERS_HPP_ */ diff --git a/src/http/HTTPMiddlewareFunction.hpp b/src/http/HTTPMiddlewareFunction.hpp new file mode 100644 index 0000000..e8b7cec --- /dev/null +++ b/src/http/HTTPMiddlewareFunction.hpp @@ -0,0 +1,26 @@ +#ifndef SRC_HTTPMIDDLEWAREFUNCTION_HPP_ +#define SRC_HTTPMIDDLEWAREFUNCTION_HPP_ + +#include + +#include "HTTPRequest.hpp" +#include "HTTPResponse.hpp" +#include "HTTPSCallbackFunction.hpp" + +namespace httpsserver { + class HTTPRequest; + /** + * \brief A middleware function that can be registered at the server. + * + * It will be called before an incoming request is passed to any HTTPSCallbackFunction and may perform + * operations like redirects or authentication. + * + * It receives the request and response object as well as a function pointer ("next") to pass on processing. + * This allows chaining those functions. If next() is not called, the HTTPSCallbackFunction that + * would match the request url will not be invoked. This might become handy if you want to intercept request + * handling in case of missing authentication. Don't forget to call next in case you want to access your + * resources, though. + */ + typedef void (HTTPSMiddlewareFunction)(HTTPRequest * req, HTTPResponse * res, std::function next); +} + #endif /* SRC_HTTPMIDDLEWAREFUNCTION_HPP_ */ diff --git a/src/http/HTTPMultipartBodyParser.cpp b/src/http/HTTPMultipartBodyParser.cpp new file mode 100644 index 0000000..2211419 --- /dev/null +++ b/src/http/HTTPMultipartBodyParser.cpp @@ -0,0 +1,288 @@ +#include "HTTPMultipartBodyParser.hpp" +#include + +const size_t MAXLINESIZE = 256; + +namespace httpsserver { + +HTTPMultipartBodyParser::HTTPMultipartBodyParser(HTTPRequest * req): + HTTPBodyParser(req), + peekBuffer(NULL), + peekBufferSize(0), + boundary(""), + lastBoundary(""), + fieldName(""), + fieldMimeType(""), + fieldFilename("") +{ + auto contentType = _request->getHeader("Content-Type"); +#ifdef DEBUG_MULTIPART_PARSER + Serial.print("Content type: "); + Serial.println(contentType.c_str()); +#endif + auto boundaryIndex = contentType.find("boundary="); + if(boundaryIndex == std::string::npos) { + HTTPS_LOGE("Multipart: missing boundary="); + discardBody(); + return; + } + boundary = contentType.substr(boundaryIndex + 9); // "boundary=" + auto commaIndex = boundary.find(';'); + boundary = "--" + boundary.substr(0, commaIndex); + if(boundary.size() > 72) { + HTTPS_LOGE("Multipart: boundary string too long"); + discardBody(); + } + lastBoundary = boundary + "--"; +} + +HTTPMultipartBodyParser::~HTTPMultipartBodyParser() { + if (peekBuffer) { + free(peekBuffer); + peekBuffer = NULL; + } +} + +void HTTPMultipartBodyParser::discardBody() { + if (peekBuffer) { + free(peekBuffer); + } + peekBuffer = NULL; + peekBufferSize = 0; + _request->discardRequestBody(); +} + +bool HTTPMultipartBodyParser::endOfBody() { + return peekBufferSize == 0 && _request->requestComplete(); +} + +void HTTPMultipartBodyParser::fillBuffer(size_t maxLen) { + // Fill the buffer with up to maxLen bytes (total length, including + // what was already in the buffer), but stop reading ahead once + // we have a CR in the buffer (because the upper layers will + // stop consuming there anyway, to forestall overrunning + // a boundary) + char *bufPtr; + if (peekBuffer == NULL) { + // Nothing in the buffer. Allocate one of the wanted size + peekBuffer = (char *)malloc(maxLen); + if (peekBuffer == NULL) { + HTTPS_LOGE("Multipart: out of memory"); + discardBody(); + return; + } + bufPtr = peekBuffer; + peekBufferSize = 0; + } else if (peekBufferSize < maxLen) { + // Something in the buffer, but not enough + char *newPeekBuffer = (char *)realloc(peekBuffer, maxLen); + if (newPeekBuffer == NULL) { + HTTPS_LOGE("Multipart: out of memory"); + discardBody(); + return; + } + peekBuffer = newPeekBuffer; + bufPtr = peekBuffer + peekBufferSize; + } else { + // We already have enough data in the buffer. + return; + } + while(bufPtr < peekBuffer+maxLen) { + size_t didRead = _request->readChars(bufPtr, peekBuffer+maxLen-bufPtr); + if (didRead == 0) { + break; + } + bufPtr += didRead; + // We stop buffering once we have a CR in the buffer + if (memchr(peekBuffer, '\r', bufPtr-peekBuffer) != NULL) { + break; + } + } + peekBufferSize = bufPtr - peekBuffer; + if (peekBufferSize == 0) { + HTTPS_LOGE("Multipart incomplete"); + } +} + +void HTTPMultipartBodyParser::consumedBuffer(size_t consumed) { + if (consumed == 0) { + return; + } + if (consumed == peekBufferSize) { + free(peekBuffer); + peekBuffer = NULL; + peekBufferSize = 0; + } else { + memmove(peekBuffer, peekBuffer+consumed, peekBufferSize-consumed); + peekBufferSize -= consumed; + } +} + +bool HTTPMultipartBodyParser::skipCRLF() { + if (peekBufferSize < 2) { + fillBuffer(2); + } + if (peekBufferSize < 2) { + return false; + } + if (peekBuffer[0] != '\r') { + return false; + } + if (peekBuffer[1] != '\n') { + HTTPS_LOGE("Multipart incorrect line terminator"); + discardBody(); + return false; + } + consumedBuffer(2); + return true; +} + +std::string HTTPMultipartBodyParser::readLine() { + fillBuffer(MAXLINESIZE); + if (peekBufferSize == 0) { + return ""; + } + char *crPtr = (char *)memchr(peekBuffer, '\r', peekBufferSize); + if (crPtr == NULL) { + HTTPS_LOGE("Multipart line too long"); + discardBody(); + return ""; + } + size_t lineLength = crPtr-peekBuffer; + std::string rv(peekBuffer, lineLength); + consumedBuffer(lineLength); + skipCRLF(); + return rv; +} + +// Returns true if the buffer contains a boundary (or possibly lastBoundary) +bool HTTPMultipartBodyParser::peekBoundary() { + if (peekBuffer == NULL || peekBufferSize < boundary.size()) { + return false; + } + char *ptr = peekBuffer; + if (*ptr == '\r') { + ptr++; + } + if (*ptr == '\n') { + ptr++; + } + return memcmp(ptr, boundary.c_str(), boundary.size()) == 0; +} + +bool HTTPMultipartBodyParser::nextField() { + fillBuffer(MAXLINESIZE); + while(!peekBoundary()) { + std::string dummy = readLine(); + if (endOfBody()) { + HTTPS_LOGE("Multipart missing last boundary"); + return false; + } + fillBuffer(MAXLINESIZE); + } + skipCRLF(); + std::string line = readLine(); + if (line == lastBoundary) { + discardBody(); + return false; + } + if (line != boundary) { + HTTPS_LOGE("Multipart incorrect boundary"); + return false; + } + // Read header lines up to and including blank line + fieldName = ""; + fieldMimeType = "text/plain"; + fieldFilename = ""; + while (true) { + line = readLine(); + if (line == "") { + break; + } + if (line.substr(0, 14) == "Content-Type: ") { + fieldMimeType = line.substr(14); + } + if (line.substr(0, 31) == "Content-Disposition: form-data;") { + // Parse name=value; or name="value"; fields. + std::string field; + line = line.substr(31); + while(true) { + size_t pos = line.find_first_not_of(' '); + if (pos != std::string::npos) { + line = line.substr(pos); + } + if (line == "") break; + pos = line.find(';'); + if (pos == std::string::npos) { + field = line; + line = ""; + } else { + field = line.substr(0, pos); + line = line.substr(pos+1); + } + pos = field.find('='); + if (pos == std::string::npos) { + HTTPS_LOGE("Multipart ill-formed form-data header"); + return false; + } + std::string headerName = field.substr(0, pos); + std::string headerValue = field.substr(pos+1); + if (headerValue.substr(0,1) == "\"") { + headerValue = headerValue.substr(1, headerValue.size()-2); + } + if (headerName == "name") { + fieldName = headerValue; + } + if (headerName == "filename") { + fieldFilename = headerValue; + } + } + } + } + if (fieldName == "") { + HTTPS_LOGE("Multipart missing name"); + return false; + } + return true; +} + +std::string HTTPMultipartBodyParser::getFieldName() { + return fieldName; +} + +std::string HTTPMultipartBodyParser::getFieldFilename() { + return fieldFilename; +} + +std::string HTTPMultipartBodyParser::getFieldMimeType() { + return fieldMimeType; +} + +bool HTTPMultipartBodyParser::endOfField() { + return peekBoundary(); +} + +size_t HTTPMultipartBodyParser::read(byte* buffer, size_t bufferSize) { + if (peekBoundary()) { + return 0; + } + size_t readSize = std::min(bufferSize, MAXLINESIZE); + fillBuffer(readSize); + if (peekBoundary()) { + return 0; + } + // We read at most up to a CR (so we don't miss a boundary that has been partially buffered) + // but we always read at least one byte so if the first byte in the buffer is a CR we do read it. + if (peekBufferSize > 1) { + char *crPtr = (char *)memchr(peekBuffer+1, '\r', peekBufferSize-1); + if (crPtr != NULL && crPtr - peekBuffer < bufferSize) { + bufferSize = crPtr - peekBuffer; + } + } + size_t copySize = std::min(bufferSize, peekBufferSize); + memcpy(buffer, peekBuffer, copySize); + consumedBuffer(copySize); + return copySize; +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPMultipartBodyParser.hpp b/src/http/HTTPMultipartBodyParser.hpp new file mode 100644 index 0000000..c106fc2 --- /dev/null +++ b/src/http/HTTPMultipartBodyParser.hpp @@ -0,0 +1,39 @@ +#ifndef SRC_HTTPMULTIPARTBODYPARSER_HPP_ +#define SRC_HTTPMULTIPARTBODYPARSER_HPP_ + +#include +#include "HTTPBodyParser.hpp" + +namespace httpsserver { + +class HTTPMultipartBodyParser : public HTTPBodyParser { +public: + HTTPMultipartBodyParser(HTTPRequest * req); + ~HTTPMultipartBodyParser(); + virtual bool nextField(); + virtual std::string getFieldName(); + virtual std::string getFieldFilename(); + virtual std::string getFieldMimeType(); + virtual bool endOfField(); + virtual size_t read(byte* buffer, size_t bufferSize); +private: + std::string readLine(); + void fillBuffer(size_t maxLen); + void consumedBuffer(size_t consumed); + bool skipCRLF(); + bool peekBoundary(); + void discardBody(); + bool endOfBody(); + char *peekBuffer; + size_t peekBufferSize; + + std::string boundary; + std::string lastBoundary; + std::string fieldName; + std::string fieldMimeType; + std::string fieldFilename; +}; + +} // namespace httpserver + +#endif \ No newline at end of file diff --git a/src/http/HTTPNode.cpp b/src/http/HTTPNode.cpp new file mode 100644 index 0000000..a504af7 --- /dev/null +++ b/src/http/HTTPNode.cpp @@ -0,0 +1,54 @@ +#include "HTTPNode.hpp" +#include "HTTPSServerConstants.hpp" + +namespace httpsserver { + + HTTPNode::HTTPNode(std::string const &path, const HTTPNodeType nodeType, std::string const &tag): + _path(path), + _tag(tag), + _nodeType(nodeType) { + + // Count the parameters and store the indices + size_t idx = 0; + while((idx = path.find("/*", idx)) != std::string::npos) { + // Skip the "/*" + idx+=2; + // Assure that we have either /* at the URL's end or /*/ somewhere in between + if (idx == path.size() || path[idx] == '/') { + _pathParamIdx.push_back(idx - 1); + } + }; + } + + HTTPNode::~HTTPNode() { + // Delete validator references + for(std::vector::iterator validator = _validators.begin(); validator != _validators.end(); ++validator) { + delete *validator; + } + } + + bool HTTPNode::hasPathParameter() { + return !_pathParamIdx.empty(); + } + + ssize_t HTTPNode::getParamIdx(size_t idx) { + if (idx<_pathParamIdx.size()) { + return _pathParamIdx[idx]; + } else { + return -1; + } + } + + size_t HTTPNode::getPathParamCount() { + return _pathParamIdx.size(); + } + + void HTTPNode::addPathParamValidator(size_t paramIdx, const HTTPValidationFunction * validator) { + _validators.push_back(new HTTPValidator(paramIdx, validator)); + + } + + std::vector * HTTPNode::getValidators() { + return &_validators; + } +} diff --git a/src/http/HTTPNode.hpp b/src/http/HTTPNode.hpp new file mode 100644 index 0000000..85db1f2 --- /dev/null +++ b/src/http/HTTPNode.hpp @@ -0,0 +1,70 @@ +#ifndef SRC_HTTPNODE_HPP_ +#define SRC_HTTPNODE_HPP_ + +#include +#include +#undef min +#undef max +#include +#include "HTTPValidator.hpp" + +namespace httpsserver { + +enum HTTPNodeType { + /** Node with a handler callback function (class ResourceNode) */ + HANDLER_CALLBACK, + /** Node with a websocket handler (class WebsocketNode) */ + WEBSOCKET +}; + +/** + * \brief Base class for a URL/route-handler in the server. + * + * Use ResourceNode for requests that access dynamic or static resources or HttpNode for routes that + * create Websockets. + */ +class HTTPNode { +public: + HTTPNode(const std::string &path, const HTTPNodeType nodeType, const std::string &tag = ""); + virtual ~HTTPNode(); + + /** + * The path under which this node will be available. Should start with a slash. Example: + * "/myResource" + */ + const std::string _path; + + /** + * Stores a tag that can be used in middleware and handler functions to identify this + * specific node, tag the node with a required permission, ... + */ + const std::string _tag; + + /** Stores the type of the node (as we have not runtime type information by default) */ + const HTTPNodeType _nodeType; + + bool hasPathParameter(); + size_t getPathParamCount(); + ssize_t getParamIdx(size_t); + + std::vector * getValidators(); + + virtual std::string getMethod() = 0; + + /** + * Adds a validation function that checks if the actual value of a parameter matches the expectation + * @param paramIdx defines the ID of the parameter that should be checked (starts by 0) + * @param validator the function (string -> bool) that checks if the parameter matches the expecatation + * + * @see ValidatorFunctions.hpp if you need some predefined templates for functions + */ + void addPathParamValidator(size_t paramIdx, const HTTPValidationFunction * validator); + +private: + std::vector _pathParamIdx; + std::vector _validators; +}; + +} // namespace httpserver + +#endif diff --git a/src/http/HTTPRequest.cpp b/src/http/HTTPRequest.cpp new file mode 100644 index 0000000..2a83a09 --- /dev/null +++ b/src/http/HTTPRequest.cpp @@ -0,0 +1,185 @@ +#include "HTTPRequest.hpp" + +namespace httpsserver { + +HTTPRequest::HTTPRequest( + ConnectionContext * con, + HTTPHeaders * headers, + HTTPNode * resolvedNode, + std::string method, + ResourceParameters * params, + std::string requestString): + _con(con), + _headers(headers), + _resolvedNode(resolvedNode), + _method(method), + _params(params), + _requestString(requestString) { + + HTTPHeader * contentLength = headers->get("Content-Length"); + if (contentLength == NULL) { + _remainingContent = 0; + _contentLengthSet = false; + } else { + _remainingContent = parseInt(contentLength->_value); + _contentLengthSet = true; + } + +} + +HTTPRequest::~HTTPRequest() { + _headers->clearAll(); +} + + +ResourceParameters * HTTPRequest::getParams() { + return _params; +} + +HTTPHeaders * HTTPRequest::getHTTPHeaders() { + return _headers; +} + +std::string HTTPRequest::getHeader(std::string const &name) { + HTTPHeader * h = _headers->get(name); + if (h != NULL) { + return h->_value; + } else { + return std::string(); + } +} + +void HTTPRequest::setHeader(std::string const &name, std::string const &value) { + _headers->set(new HTTPHeader(name, value)); +} + +HTTPNode * HTTPRequest::getResolvedNode() { + return _resolvedNode; +} + +IPAddress HTTPRequest::getClientIP() { + return _con->getClientIP(); +} + +size_t HTTPRequest::readBytes(byte * buffer, size_t length) { + + // Limit reading to content length + if (_contentLengthSet && length > _remainingContent) { + length = _remainingContent; + } + + size_t bytesRead = 0; + if (length > 0) { + bytesRead = _con->readBuffer(buffer, length); + } + + if (_contentLengthSet) { + _remainingContent -= bytesRead; + } + + return bytesRead; +} + +size_t HTTPRequest::readChars(char * buffer, size_t length) { + return readBytes((byte*)buffer, length); +} + +size_t HTTPRequest::getContentLength() { + return _remainingContent; +} + +std::string HTTPRequest::getRequestString() { + return _requestString; +} + +std::string HTTPRequest::getMethod() { + return _method; +} + +std::string HTTPRequest::getTag() { + return _resolvedNode->_tag; +} + +bool HTTPRequest::requestComplete() { + if (_contentLengthSet) { + // If we have a content size, rely on it. + return (_remainingContent == 0); + } else { + // If there is no more input... + return (_con->pendingBufferSize() == 0); + } +} + +/** + * This function will drop whatever is remaining of the request body + */ +void HTTPRequest::discardRequestBody() { + byte buf[16]; + while(!requestComplete()) { + readBytes(buf, 16); + } +} + +std::string HTTPRequest::getBasicAuthUser() { + std::string token = decodeBasicAuthToken(); + size_t splitpoint = token.find(":"); + if (splitpoint != std::string::npos && splitpoint > 0) { + return token.substr(0, splitpoint); + } else { + return std::string(); + } +} + +std::string HTTPRequest::getBasicAuthPassword() { + std::string token = decodeBasicAuthToken(); + size_t splitpoint = token.find(":"); + if (splitpoint != std::string::npos && splitpoint > 0) { + return token.substr(splitpoint+1); + } else { + return std::string(); + } +} + +std::string HTTPRequest::decodeBasicAuthToken() { + std::string basicAuthString = getHeader("Authorization"); + // Get the length of the token + size_t sourceLength = basicAuthString.length(); + // Only handle basic auth tokens + if (basicAuthString.substr(0, 6) != "Basic ") { + return std::string(); + } + // If the token is too long, skip + if (sourceLength > 100) { + return std::string(); + } else { + // Try to decode. As we are using mbedtls anyway, we can use that function + unsigned char * bufOut = new unsigned char[basicAuthString.length()]; + size_t outputLength = 0; + int res = mbedtls_base64_decode( + bufOut, + sourceLength, + &outputLength, + ((const unsigned char *)basicAuthString.substr(6).c_str()), // Strip "Basic " + sourceLength - 6 // Strip "Basic " + ); + // Failure of decoding + if (res != 0) { + delete[] bufOut; + return std::string(); + } + std::string tokenRes = std::string((char*)bufOut, outputLength); + delete[] bufOut; + return tokenRes; + } +} + +bool HTTPRequest::isSecure() { + return _con->isSecure(); +} + + +void HTTPRequest::setWebsocketHandler(WebsocketHandler *wsHandler) { + _con->setWebsocketHandler(wsHandler); +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPRequest.hpp b/src/http/HTTPRequest.hpp new file mode 100644 index 0000000..7184180 --- /dev/null +++ b/src/http/HTTPRequest.hpp @@ -0,0 +1,68 @@ +#ifndef SRC_HTTPREQUEST_HPP_ +#define SRC_HTTPREQUEST_HPP_ + +#include +#include +#include + +#include + +#include "ConnectionContext.hpp" +#include "HTTPNode.hpp" +#include "HTTPHeader.hpp" +#include "HTTPHeaders.hpp" +#include "ResourceParameters.hpp" +#include "util.hpp" + +namespace httpsserver { + +/** + * \brief Represents the request stream for an HTTP request + */ +class HTTPRequest { +public: + HTTPRequest(ConnectionContext * con, HTTPHeaders * headers, HTTPNode * resolvedNode, std::string method, ResourceParameters * params, std::string requestString); + virtual ~HTTPRequest(); + + std::string getHeader(std::string const &name); + void setHeader(std::string const &name, std::string const &value); + HTTPNode * getResolvedNode(); + std::string getRequestString(); + std::string getMethod(); + std::string getTag(); + IPAddress getClientIP(); + + size_t readChars(char * buffer, size_t length); + size_t readBytes(byte * buffer, size_t length); + size_t getContentLength(); + bool requestComplete(); + void discardRequestBody(); + ResourceParameters * getParams(); + HTTPHeaders *getHTTPHeaders(); + std::string getBasicAuthUser(); + std::string getBasicAuthPassword(); + bool isSecure(); + void setWebsocketHandler(WebsocketHandler *wsHandler); + +private: + std::string decodeBasicAuthToken(); + + ConnectionContext * _con; + + HTTPHeaders * _headers; + + HTTPNode * _resolvedNode; + + std::string _method; + + ResourceParameters * _params; + + std::string _requestString; + + bool _contentLengthSet; + size_t _remainingContent; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPREQUEST_HPP_ */ diff --git a/src/http/HTTPResponse.cpp b/src/http/HTTPResponse.cpp new file mode 100644 index 0000000..baabc39 --- /dev/null +++ b/src/http/HTTPResponse.cpp @@ -0,0 +1,188 @@ +#include "HTTPResponse.hpp" + +#include +#include "lwip/sockets.h" + +namespace httpsserver { + +HTTPResponse::HTTPResponse(ConnectionContext * con): + _con(con) { + + // Default status code is 200 OK + _statusCode = 200; + _statusText = "OK"; + _headerWritten = false; + _isError = false; + + _responseCacheSize = con->getCacheSize(); + _responseCachePointer = 0; + if (_responseCacheSize > 0) { + HTTPS_LOGD("Creating buffered response, size: %d", _responseCacheSize); + _responseCache = new byte[_responseCacheSize]; + } else { + HTTPS_LOGD("Creating non-buffered response"); + _responseCache = NULL; + } +} + +HTTPResponse::~HTTPResponse() { + if (_responseCache != NULL) { + delete[] _responseCache; + } + _headers.clearAll(); +} + +void HTTPResponse::setStatusCode(uint16_t statusCode) { + _statusCode = statusCode; +} + +void HTTPResponse::setStatusText(std::string const &statusText) { + _statusText = statusText; +} + +uint16_t HTTPResponse::getStatusCode() { + return _statusCode; +} + +std::string HTTPResponse::getStatusText() { + return _statusText; +} + +void HTTPResponse::setHeader(std::string const &name, std::string const &value) { + _headers.set(new HTTPHeader(name, value)); +} + +std::string HTTPResponse::getHeader(std::string const &name) { + HTTPHeader * h = _headers.get(name); + if (h != NULL) { + return h->_value; + } else { + return std::string(); + } +} + +bool HTTPResponse::isHeaderWritten() { + return _headerWritten; +} + +bool HTTPResponse::isResponseBuffered() { + return _responseCache != NULL; +} + +void HTTPResponse::finalize() { + if (isResponseBuffered()) { + drainBuffer(); + } +} + +/** + * Writes a string to the response. May be called several times. + */ +void HTTPResponse::printStd(const std::string &str) { + write((uint8_t*)str.c_str(), str.length()); +} + +/** + * Writes bytes to the response. May be called several times. + */ +size_t HTTPResponse::write(const uint8_t *buffer, size_t size) { + if(!isResponseBuffered()) { + printHeader(); + } + return writeBytesInternal(buffer, size); +} + +/** + * Writes a single byte to the response. + */ +size_t HTTPResponse::write(uint8_t b) { + if(!isResponseBuffered()) { + printHeader(); + } + byte ba[] = {b}; + return writeBytesInternal(ba, 1); +} + +/** + * If not already done, writes the header. + */ +void HTTPResponse::printHeader() { + if (!_headerWritten) { + HTTPS_LOGD("Printing headers"); + + // Status line, like: "HTTP/1.1 200 OK\r\n" + std::string statusLine = "HTTP/1.1 " + intToString(_statusCode) + " " + _statusText + "\r\n"; + printInternal(statusLine, true); + + // Each header, like: "Host: myEsp32\r\n" + std::vector * headers = _headers.getAll(); + for(std::vector::iterator header = headers->begin(); header != headers->end(); ++header) { + printInternal((*header)->print()+"\r\n", true); + } + printInternal("\r\n", true); + + _headerWritten=true; + } +} + +/** + * This method can be called to cancel the ongoing transmission and send the error page (if possible) + */ +void HTTPResponse::error() { + _con->signalRequestError(); +} + +void HTTPResponse::printInternal(const std::string &str, bool skipBuffer) { + writeBytesInternal((uint8_t*)str.c_str(), str.length(), skipBuffer); +} + +size_t HTTPResponse::writeBytesInternal(const void * data, int length, bool skipBuffer) { + if (!_isError) { + if (isResponseBuffered() && !skipBuffer) { + // We are buffering ... + if(length <= _responseCacheSize - _responseCachePointer) { + // ... and there is space left in the buffer -> Write to buffer + size_t end = _responseCachePointer + length; + size_t i = 0; + while(_responseCachePointer < end) { + _responseCache[_responseCachePointer++] = ((byte*)data)[i++]; + } + // Returning skips the SSL_write below + return length; + } else { + // .., and the buffer is too small. This is the point where we switch from + // caching to streaming + if (!_headerWritten) { + setHeader("Connection", "close"); + } + drainBuffer(true); + } + } + + return _con->writeBuffer((byte*)data, length); + } else { + return 0; + } +} + +void HTTPResponse::drainBuffer(bool onOverflow) { + if (!_headerWritten) { + if (_responseCache != NULL && !onOverflow) { + _headers.set(new HTTPHeader("Content-Length", intToString(_responseCachePointer))); + } + printHeader(); + } + + if (_responseCache != NULL) { + HTTPS_LOGD("Draining response buffer"); + // Check for 0 as it may be an overflow reaction without any data that has been written earlier + if(_responseCachePointer > 0) { + // FIXME: Return value? + _con->writeBuffer((byte*)_responseCache, _responseCachePointer); + } + delete[] _responseCache; + _responseCache = NULL; + } +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPResponse.hpp b/src/http/HTTPResponse.hpp new file mode 100644 index 0000000..7bd1758 --- /dev/null +++ b/src/http/HTTPResponse.hpp @@ -0,0 +1,71 @@ +#ifndef SRC_HTTPRESPONSE_HPP_ +#define SRC_HTTPRESPONSE_HPP_ + +#include +#include +// Arduino declares it's own min max, incompatible with the stl... +#undef min +#undef max +#undef write +#include + +#include + +#include "util.hpp" + +#include "ConnectionContext.hpp" +#include "HTTPHeaders.hpp" +#include "HTTPHeader.hpp" + +namespace httpsserver { + +/** + * \brief Represents the response stream of an HTTP request + */ +class HTTPResponse : public Print { +public: + HTTPResponse(ConnectionContext * con); + virtual ~HTTPResponse(); + + void setStatusCode(uint16_t statusCode); + void setStatusText(std::string const &statusText); + uint16_t getStatusCode(); + std::string getStatusText(); + void setHeader(std::string const &name, std::string const &value); + std::string getHeader(std::string const &name); + bool isHeaderWritten(); + + void printStd(std::string const &str); + + // From Print: + size_t write(const uint8_t *buffer, size_t size); + size_t write(uint8_t); + + void error(); + + bool isResponseBuffered(); + void finalize(); + + ConnectionContext * _con; + +private: + void printHeader(); + void printInternal(const std::string &str, bool skipBuffer = false); + size_t writeBytesInternal(const void * data, int length, bool skipBuffer = false); + void drainBuffer(bool onOverflow = false); + + uint16_t _statusCode; + std::string _statusText; + HTTPHeaders _headers; + bool _headerWritten; + bool _isError; + + // Response cache + byte * _responseCache; + size_t _responseCacheSize; + size_t _responseCachePointer; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPRESPONSE_HPP_ */ diff --git a/src/http/HTTPSCallbackFunction.hpp b/src/http/HTTPSCallbackFunction.hpp new file mode 100644 index 0000000..1329e38 --- /dev/null +++ b/src/http/HTTPSCallbackFunction.hpp @@ -0,0 +1,14 @@ +#ifndef SRC_HTTPSCALLBACKFUNCTION_HPP_ +#define SRC_HTTPSCALLBACKFUNCTION_HPP_ + +#include "HTTPRequest.hpp" +#include "HTTPResponse.hpp" + +namespace httpsserver { + /** + * \brief A callback function that will be called by the server to handle a request + */ + typedef void (HTTPSCallbackFunction)(HTTPRequest * req, HTTPResponse * res); +} + +#endif /* SRC_HTTPSCALLBACKFUNCTION_HPP_ */ diff --git a/src/http/HTTPSConnection.cpp b/src/http/HTTPSConnection.cpp new file mode 100644 index 0000000..e0e3dd0 --- /dev/null +++ b/src/http/HTTPSConnection.cpp @@ -0,0 +1,123 @@ +#include "HTTPSConnection.hpp" + +namespace httpsserver { + + +HTTPSConnection::HTTPSConnection(ResourceResolver * resResolver): + HTTPConnection(resResolver) { + _ssl = NULL; +} + +HTTPSConnection::~HTTPSConnection() { + // Close the socket + closeConnection(); +} + +bool HTTPSConnection::isSecure() { + return true; +} + +/** + * Initializes the connection from a server socket. + * + * The call WILL BLOCK if accept(serverSocketID) blocks. So use select() to check for that in advance. + */ +int HTTPSConnection::initialize(int serverSocketID, SSL_CTX * sslCtx, HTTPHeaders *defaultHeaders) { + if (_connectionState == STATE_UNDEFINED) { + // Let the base class connect the plain tcp socket + int resSocket = HTTPConnection::initialize(serverSocketID, defaultHeaders); + + // Build up SSL Connection context if the socket has been created successfully + if (resSocket >= 0) { + + _ssl = SSL_new(sslCtx); + + if (_ssl) { + // Bind SSL to the socket + int success = SSL_set_fd(_ssl, resSocket); + if (success) { + + // Perform the handshake + success = SSL_accept(_ssl); + if (success) { + return resSocket; + } else { + HTTPS_LOGE("SSL_accept failed. Aborting handshake. FID=%d", resSocket); + } + } else { + HTTPS_LOGE("SSL_set_fd failed. Aborting handshake. FID=%d", resSocket); + } + } else { + HTTPS_LOGE("SSL_new failed. Aborting handshake. FID=%d", resSocket); + } + + } else { + HTTPS_LOGE("Could not accept() new connection. FID=%d", resSocket); + } + + _connectionState = STATE_ERROR; + _clientState = CSTATE_ACTIVE; + + // This will only be called if the connection could not be established and cleanup + // variables like _ssl etc. + closeConnection(); + } + // Error: The connection has already been established or could not be established + return -1; +} + + +void HTTPSConnection::closeConnection() { + + // FIXME: Copy from HTTPConnection, could be done better probably + if (_connectionState != STATE_ERROR && _connectionState != STATE_CLOSED) { + + // First call to closeConnection - set the timestamp to calculate the timeout later on + if (_connectionState != STATE_CLOSING) { + _shutdownTS = millis(); + } + + // Set the connection state to closing. We stay in closing as long as SSL has not been shutdown + // correctly + _connectionState = STATE_CLOSING; + } + + // Try to tear down SSL while we are in the _shutdownTS timeout period or if an error occurred + if (_ssl) { + if(_connectionState == STATE_ERROR || SSL_shutdown(_ssl) == 0) { + // SSL_shutdown will return 1 as soon as the client answered with close notify + // This means we are safe to close the socket + SSL_free(_ssl); + _ssl = NULL; + } else if (_shutdownTS + HTTPS_SHUTDOWN_TIMEOUT < millis()) { + // The timeout has been hit, we force SSL shutdown now by freeing the context + SSL_free(_ssl); + _ssl = NULL; + HTTPS_LOGW("SSL_shutdown did not receive close notification from the client"); + _connectionState = STATE_ERROR; + } + } + + // If SSL has been brought down, close the socket + if (!_ssl) { + HTTPConnection::closeConnection(); + } +} + +size_t HTTPSConnection::writeBuffer(byte* buffer, size_t length) { + return SSL_write(_ssl, buffer, length); +} + +size_t HTTPSConnection::readBytesToBuffer(byte* buffer, size_t length) { + return SSL_read(_ssl, buffer, length); +} + +size_t HTTPSConnection::pendingByteCount() { + return SSL_pending(_ssl); +} + +bool HTTPSConnection::canReadData() { + return HTTPConnection::canReadData() || (SSL_pending(_ssl) > 0); +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPSConnection.hpp b/src/http/HTTPSConnection.hpp new file mode 100644 index 0000000..8adbce5 --- /dev/null +++ b/src/http/HTTPSConnection.hpp @@ -0,0 +1,58 @@ +#ifndef SRC_HTTPSCONNECTION_HPP_ +#define SRC_HTTPSCONNECTION_HPP_ + +#include + +#include + +// Required for SSL +#include "openssl/ssl.h" +#undef read + +// Required for sockets +#include "lwip/netdb.h" +#undef read +#include "lwip/sockets.h" + +#include "HTTPSServerConstants.hpp" +#include "HTTPConnection.hpp" +#include "HTTPHeaders.hpp" +#include "HTTPHeader.hpp" +#include "ResourceResolver.hpp" +#include "ResolvedResource.hpp" +#include "ResourceNode.hpp" +#include "HTTPRequest.hpp" +#include "HTTPResponse.hpp" + +namespace httpsserver { + +/** + * \brief Connection class for an open TLS-enabled connection to an HTTPSServer + */ +class HTTPSConnection : public HTTPConnection { +public: + HTTPSConnection(ResourceResolver * resResolver); + virtual ~HTTPSConnection(); + + virtual int initialize(int serverSocketID, SSL_CTX * sslCtx, HTTPHeaders *defaultHeaders); + virtual void closeConnection(); + virtual bool isSecure(); + +protected: + friend class HTTPRequest; + friend class HTTPResponse; + + virtual size_t readBytesToBuffer(byte* buffer, size_t length); + virtual size_t pendingByteCount(); + virtual bool canReadData(); + virtual size_t writeBuffer(byte* buffer, size_t length); + +private: + // SSL context for this connection + SSL * _ssl; + +}; + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPSCONNECTION_HPP_ */ diff --git a/src/http/HTTPSServer.cpp b/src/http/HTTPSServer.cpp new file mode 100644 index 0000000..4d8352d --- /dev/null +++ b/src/http/HTTPSServer.cpp @@ -0,0 +1,102 @@ +#include "HTTPSServer.hpp" + +namespace httpsserver { + + +HTTPSServer::HTTPSServer(SSLCert * cert, const uint16_t port, const uint8_t maxConnections, const in_addr_t bindAddress): + HTTPServer(port, maxConnections, bindAddress), + _cert(cert) { + + // Configure runtime data + _sslctx = NULL; +} + +HTTPSServer::~HTTPSServer() { + +} + +/** + * This method starts the server and begins to listen on the port + */ +uint8_t HTTPSServer::setupSocket() { + if (!isRunning()) { + if (!setupSSLCTX()) { + Serial.println("setupSSLCTX failed"); + return 0; + } + + if (!setupCert()) { + Serial.println("setupCert failed"); + SSL_CTX_free(_sslctx); + _sslctx = NULL; + return 0; + } + + if (HTTPServer::setupSocket()) { + return 1; + } else { + Serial.println("setupSockets failed"); + SSL_CTX_free(_sslctx); + _sslctx = NULL; + return 0; + } + } else { + return 1; + } +} + +void HTTPSServer::teardownSocket() { + + HTTPServer::teardownSocket(); + + // Tear down the SSL context + SSL_CTX_free(_sslctx); + _sslctx = NULL; +} + +int HTTPSServer::createConnection(int idx) { + HTTPSConnection * newConnection = new HTTPSConnection(this); + _connections[idx] = newConnection; + return newConnection->initialize(_socket, _sslctx, &_defaultHeaders); +} + +/** + * This method configures the ssl context that is used for the server + */ +uint8_t HTTPSServer::setupSSLCTX() { + _sslctx = SSL_CTX_new(TLSv1_2_server_method()); + if (_sslctx) { + // Set SSL Timeout to 5 minutes + SSL_CTX_set_timeout(_sslctx, 300); + return 1; + } else { + _sslctx = NULL; + return 0; + } +} + +/** + * This method configures the certificate and private key for the given + * ssl context + */ +uint8_t HTTPSServer::setupCert() { + // Configure the certificate first + uint8_t ret = SSL_CTX_use_certificate_ASN1( + _sslctx, + _cert->getCertLength(), + _cert->getCertData() + ); + + // Then set the private key accordingly + if (ret) { + ret = SSL_CTX_use_RSAPrivateKey_ASN1( + _sslctx, + _cert->getPKData(), + _cert->getPKLength() + ); + } + + return ret; +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPSServer.hpp b/src/http/HTTPSServer.hpp new file mode 100644 index 0000000..68596bf --- /dev/null +++ b/src/http/HTTPSServer.hpp @@ -0,0 +1,56 @@ +#ifndef SRC_HTTPSSERVER_HPP_ +#define SRC_HTTPSSERVER_HPP_ + +// Standard library +#include + +// Arduino stuff +#include + +// Required for SSL +#include "openssl/ssl.h" +#undef read + +// Internal includes +#include "HTTPServer.hpp" +#include "HTTPSServerConstants.hpp" +#include "HTTPHeaders.hpp" +#include "HTTPHeader.hpp" +#include "ResourceNode.hpp" +#include "ResourceResolver.hpp" +#include "ResolvedResource.hpp" +#include "HTTPSConnection.hpp" +#include "SSLCert.hpp" + +namespace httpsserver { + +/** + * \brief Main implementation of the HTTP Server with TLS support. Use HTTPServer for plain HTTP + */ +class HTTPSServer : public HTTPServer { +public: + HTTPSServer(SSLCert * cert, const uint16_t portHTTPS = 443, const uint8_t maxConnections = 4, const in_addr_t bindAddress = 0); + virtual ~HTTPSServer(); + +private: + // Static configuration. Port, keys, etc. ==================== + // Certificate that should be used (includes private key) + SSLCert * _cert; + + //// Runtime data ============================================ + SSL_CTX * _sslctx; + // Status of the server: Are we running, or not? + + // Setup functions + virtual uint8_t setupSocket(); + virtual void teardownSocket(); + uint8_t setupSSLCTX(); + uint8_t setupCert(); + + // Helper functions + virtual int createConnection(int idx); +}; + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPSSERVER_HPP_ */ diff --git a/src/http/HTTPSServerConstants.hpp b/src/http/HTTPSServerConstants.hpp new file mode 100644 index 0000000..d71aa3d --- /dev/null +++ b/src/http/HTTPSServerConstants.hpp @@ -0,0 +1,89 @@ +#ifndef SRC_HTTPSSERVERCONSTANTS_HPP_ +#define SRC_HTTPSSERVERCONSTANTS_HPP_ + +#include "Arduino.h" + +// 1: Error +// 2: Error + Warn +// 3: Error + Warn + Info +// 4: Error + Warn + Info + Debug + +#ifndef HTTPS_LOGLEVEL + #define HTTPS_LOGLEVEL 3 +#endif + +#ifdef HTTPS_LOGTIMESTAMP + #define HTTPS_LOGTAG(LVL) Serial.printf("[HTTPS:" LVL ":%10lu] ", millis()) +#else + #define HTTPS_LOGTAG(LVL) Serial.print("[HTTPS:" LVL "] ") +#endif + +#if HTTPS_LOGLEVEL > 0 + #define HTTPS_LOGE(...) HTTPS_LOGTAG("E");Serial.printf(__VA_ARGS__);Serial.println() +#else + #define HTTPS_LOGE(...) do {} while (0) +#endif + +#if HTTPS_LOGLEVEL > 1 + #define HTTPS_LOGW(...) HTTPS_LOGTAG("W");Serial.printf(__VA_ARGS__);Serial.println() +#else + #define HTTPS_LOGW(...) do {} while (0) +#endif + +#if HTTPS_LOGLEVEL > 2 + #define HTTPS_LOGI(...) HTTPS_LOGTAG("I");Serial.printf(__VA_ARGS__);Serial.println() +#else + #define HTTPS_LOGI(...) do {} while (0) +#endif + +#if HTTPS_LOGLEVEL > 3 + #define HTTPS_LOGD(...) HTTPS_LOGTAG("D");Serial.printf(__VA_ARGS__);Serial.println() +#else + #define HTTPS_LOGD(...) do {} while (0) +#endif + +// The following lines define limits of the protocol. Exceeding these limits will lead to a 500 error + +// Maximum of header lines that are parsed +#ifndef HTTPS_REQUEST_MAX_HEADERS +#define HTTPS_REQUEST_MAX_HEADERS 20 +#endif + +// Maximum length of the request line (GET /... HTTP/1.1) +#ifndef HTTPS_REQUEST_MAX_REQUEST_LENGTH +#define HTTPS_REQUEST_MAX_REQUEST_LENGTH 128 +#endif + +// Maximum length of a header line (including name and value) +#ifndef HTTPS_REQUEST_MAX_HEADER_LENGTH +#define HTTPS_REQUEST_MAX_HEADER_LENGTH 384 +#endif + +// Chunk size used for reading data from the ssl-enabled socket +#ifndef HTTPS_CONNECTION_DATA_CHUNK_SIZE +#define HTTPS_CONNECTION_DATA_CHUNK_SIZE 512 +#endif + +// Size (in bytes) of the Connection:keep-alive Cache (we need to be able to +// store-and-forward the response to calculate the content-size) +#ifndef HTTPS_KEEPALIVE_CACHESIZE +#define HTTPS_KEEPALIVE_CACHESIZE 1400 +#endif + +// Timeout for an HTTPS connection without any transmission +#ifndef HTTPS_CONNECTION_TIMEOUT +#define HTTPS_CONNECTION_TIMEOUT 20000 +#endif + +// Timeout used to wait for shutdown of SSL connection (ms) +// (time for the client to return notify close flag) - without it, truncation attacks might be possible +#ifndef HTTPS_SHUTDOWN_TIMEOUT +#define HTTPS_SHUTDOWN_TIMEOUT 5000 +#endif + +// Length of a SHA1 hash +#ifndef HTTPS_SHA1_LENGTH +#define HTTPS_SHA1_LENGTH 20 +#endif + +#endif /* SRC_HTTPSSERVERCONSTANTS_HPP_ */ diff --git a/src/http/HTTPServer.cpp b/src/http/HTTPServer.cpp new file mode 100644 index 0000000..9705e0c --- /dev/null +++ b/src/http/HTTPServer.cpp @@ -0,0 +1,211 @@ +#include "HTTPServer.hpp" + +namespace httpsserver { + + +HTTPServer::HTTPServer(const uint16_t port, const uint8_t maxConnections, const in_addr_t bindAddress): + _port(port), + _maxConnections(maxConnections), + _bindAddress(bindAddress) { + + // Create space for the connections + _connections = new HTTPConnection*[maxConnections]; + for(uint8_t i = 0; i < maxConnections; i++) _connections[i] = NULL; + + // Configure runtime data + _socket = -1; + _running = false; +} + +HTTPServer::~HTTPServer() { + + // Stop the server. + // This will also remove all existing connections + if(_running) { + stop(); + } + + // Delete connection pointers + delete[] _connections; +} + +/** + * This method starts the server and begins to listen on the port + */ +uint8_t HTTPServer::start() { + if (!_running) { + if (setupSocket()) { + _running = true; + return 1; + } + return 0; + } else { + return 1; + } +} + +bool HTTPServer::isRunning() { + return _running; +} + +/** + * This method stops the server + */ +void HTTPServer::stop() { + + if (_running) { + // Set the flag that the server is stopped + _running = false; + + // Clean up the connections + bool hasOpenConnections = true; + while(hasOpenConnections) { + hasOpenConnections = false; + for(int i = 0; i < _maxConnections; i++) { + if (_connections[i] != NULL) { + _connections[i]->closeConnection(); + + // Check if closing succeeded. If not, we need to call the close function multiple times + // and wait for the client + if (_connections[i]->isClosed()) { + delete _connections[i]; + _connections[i] = NULL; + } else { + hasOpenConnections = true; + } + } + } + delay(1); + } + + teardownSocket(); + + } +} + +/** + * Adds a default header that is included in every response. + * + * This could be used for example to add a Server: header or for CORS options + */ +void HTTPServer::setDefaultHeader(std::string name, std::string value) { + _defaultHeaders.set(new HTTPHeader(name, value)); +} + +/** + * The loop method can either be called by periodical interrupt or in the main loop and handles processing + * of data + */ +void HTTPServer::loop() { + + // Only handle requests if the server is still running + if(!_running) return; + + // Step 1: Process existing connections + // Process open connections and store the index of a free connection + // (we might use that later on) + int freeConnectionIdx = -1; + for (int i = 0; i < _maxConnections; i++) { + // Fetch a free index in the pointer array + if (_connections[i] == NULL) { + freeConnectionIdx = i; + + } else { + // if there is a connection (_connections[i]!=NULL), check if its open or closed: + if (_connections[i]->isClosed()) { + // if it's closed, clean up: + delete _connections[i]; + _connections[i] = NULL; + freeConnectionIdx = i; + } else { + // if not, process it: + _connections[i]->loop(); + } + } + } + + // Step 2: Check for new connections + // This makes only sense if there is space to store the connection + if (freeConnectionIdx > -1) { + + // We create a file descriptor set to be able to use the select function + fd_set sockfds; + // Out socket is the only socket in this set + FD_ZERO(&sockfds); + FD_SET(_socket, &sockfds); + + // We define a "immediate" timeout + timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 0; // Return immediately, if possible + + // Wait for input + // As by 2017-12-14, it seems that FD_SETSIZE is defined as 0x40, but socket IDs now + // start at 0x1000, so we need to use _socket+1 here + select(_socket + 1, &sockfds, NULL, NULL, &timeout); + + // There is input + if (FD_ISSET(_socket, &sockfds)) { + int socketIdentifier = createConnection(freeConnectionIdx); + + // If initializing did not work, discard the new socket immediately + if (socketIdentifier < 0) { + delete _connections[freeConnectionIdx]; + _connections[freeConnectionIdx] = NULL; + } + } + + } +} + +int HTTPServer::createConnection(int idx) { + HTTPConnection * newConnection = new HTTPConnection(this); + _connections[idx] = newConnection; + return newConnection->initialize(_socket, &_defaultHeaders); +} + +/** + * This method prepares the tcp server socket + */ +uint8_t HTTPServer::setupSocket() { + // (AF_INET = IPv4, SOCK_STREAM = TCP) + _socket = socket(AF_INET, SOCK_STREAM, 0); + + if (_socket>=0) { + _sock_addr.sin_family = AF_INET; + // Listen on all interfaces + _sock_addr.sin_addr.s_addr = _bindAddress; + // Set the server port + _sock_addr.sin_port = htons(_port); + + // Now bind the TCP socket we did create above to the socket address we specified + // (The TCP-socket now listens on 0.0.0.0:port) + int err = bind(_socket, (struct sockaddr* )&_sock_addr, sizeof(_sock_addr)); + if(!err) { + err = listen(_socket, _maxConnections); + if (!err) { + return 1; + } else { + close(_socket); + _socket = -1; + return 0; + } + } else { + close(_socket); + _socket = -1; + return 0; + } + } else { + _socket = -1; + return 0; + } + +} + +void HTTPServer::teardownSocket() { + // Close the actual server sockets + close(_socket); + _socket = -1; +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPServer.hpp b/src/http/HTTPServer.hpp new file mode 100644 index 0000000..47746c1 --- /dev/null +++ b/src/http/HTTPServer.hpp @@ -0,0 +1,76 @@ +#ifndef SRC_HTTPSERVER_HPP_ +#define SRC_HTTPSERVER_HPP_ + +// Standard library +#include + +// Arduino stuff +#include + +// Required for sockets +#include "lwip/netdb.h" +#undef read +#include "lwip/sockets.h" +#include "lwip/inet.h" + +// Internal includes +#include "HTTPSServerConstants.hpp" +#include "HTTPHeaders.hpp" +#include "HTTPHeader.hpp" +#include "ResourceNode.hpp" +#include "ResourceResolver.hpp" +#include "ResolvedResource.hpp" +#include "HTTPConnection.hpp" + +namespace httpsserver { + +/** + * \brief Main implementation for the plain HTTP server. Use HTTPSServer for TLS support + */ +class HTTPServer : public ResourceResolver { +public: + HTTPServer(const uint16_t portHTTPS = 80, const uint8_t maxConnections = 8, const in_addr_t bindAddress = 0); + virtual ~HTTPServer(); + + uint8_t start(); + void stop(); + bool isRunning(); + + void loop(); + + void setDefaultHeader(std::string name, std::string value); + +protected: + // Static configuration. Port, keys, etc. ==================== + // Certificate that should be used (includes private key) + const uint16_t _port; + + // Max parallel connections that the server will accept + const uint8_t _maxConnections; + // Address to bind to (0 = all interfaces) + const in_addr_t _bindAddress; + + //// Runtime data ============================================ + // The array of connections that are currently active + HTTPConnection ** _connections; + // Status of the server: Are we running, or not? + boolean _running; + // The server socket + int _socket; + + // The server socket address, that our service is bound to + sockaddr_in _sock_addr; + // Headers that are included in every response + HTTPHeaders _defaultHeaders; + + // Setup functions + virtual uint8_t setupSocket(); + virtual void teardownSocket(); + + // Helper functions + virtual int createConnection(int idx); +}; + +} + +#endif /* SRC_HTTPSERVER_HPP_ */ diff --git a/src/http/HTTPURLEncodedBodyParser.cpp b/src/http/HTTPURLEncodedBodyParser.cpp new file mode 100644 index 0000000..d1ab73a --- /dev/null +++ b/src/http/HTTPURLEncodedBodyParser.cpp @@ -0,0 +1,128 @@ +#include "HTTPURLEncodedBodyParser.hpp" + +#define CHUNKSIZE 512 +#define MINCHUNKSIZE 64 + +namespace httpsserver { + +HTTPURLEncodedBodyParser::HTTPURLEncodedBodyParser(HTTPRequest * req): + HTTPBodyParser(req), + bodyBuffer(NULL), + bodyPtr(NULL), + bodyLength(0), + fieldBuffer(""), + fieldPtr(NULL), + fieldRemainingLength(0) +{ + bodyLength = _request->getContentLength(); + if (bodyLength) { + // We know the body length. We try to read that much and give an error if it fails. + bodyBuffer = (char *)malloc(bodyLength+1); + if (bodyBuffer == NULL) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: out of memory"); + return; + } + bodyPtr = bodyBuffer; + size_t toRead = bodyLength; + while(toRead > 0) { + size_t didRead = _request->readChars(bodyPtr, toRead); + if (didRead == 0) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: short read"); + bodyLength = bodyPtr - bodyBuffer; + break; + } + bodyPtr += didRead; + toRead -= didRead; + } + } else { + // We don't know the length. Read as much as possible. + bodyBuffer = (char *)malloc(CHUNKSIZE+1); + if (bodyBuffer == NULL) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: out of memory"); + return; + } + bodyPtr = bodyBuffer; + size_t bufferUsed = 0; + size_t bufferAvailable = CHUNKSIZE; + while(!_request->requestComplete()) { + if (bufferAvailable < MINCHUNKSIZE) { + char *pBuf = (char *)realloc(bodyBuffer, bufferUsed + CHUNKSIZE+1); + if (pBuf == NULL) { + HTTPS_LOGE("HTTPURLEncodedBodyParser: out of memory"); + free(bodyBuffer); + bodyBuffer = NULL; + return; + } + bodyBuffer = pBuf; + bufferAvailable = CHUNKSIZE; + } + size_t didRead = _request->readChars(bodyBuffer+bufferUsed, bufferAvailable); + bufferUsed += didRead; + bufferAvailable -= didRead; + } + bodyLength = bufferUsed; + } + bodyPtr = bodyBuffer; + bodyBuffer[bodyLength] = '\0'; +} + +HTTPURLEncodedBodyParser::~HTTPURLEncodedBodyParser() { + if (bodyBuffer) { + free(bodyBuffer); + } + bodyBuffer = NULL; +} + +bool HTTPURLEncodedBodyParser::nextField() { + fieldBuffer = ""; + fieldPtr = NULL; + fieldRemainingLength = 0; + + char *equalPtr = index(bodyPtr, '='); + if (equalPtr == NULL) { + return false; + } + fieldName = std::string(bodyPtr, equalPtr-bodyPtr); + + char *valuePtr = equalPtr + 1; + char *endPtr = index(valuePtr, '&'); + if (endPtr == NULL) { + endPtr = equalPtr + strlen(equalPtr); + bodyPtr = endPtr; + } else { + bodyPtr = endPtr+1; + } + fieldBuffer = std::string(valuePtr, endPtr - valuePtr); + fieldBuffer = urlDecode(fieldBuffer); + fieldRemainingLength = fieldBuffer.size(); + fieldPtr = fieldBuffer.c_str(); + return true; +} + +std::string HTTPURLEncodedBodyParser::getFieldName() { + return fieldName; +} + +std::string HTTPURLEncodedBodyParser::getFieldFilename() { + return ""; +} + +std::string HTTPURLEncodedBodyParser::getFieldMimeType() { + return std::string("text/plain"); +} + +bool HTTPURLEncodedBodyParser::endOfField() { + return fieldRemainingLength <= 0; +} + +size_t HTTPURLEncodedBodyParser::read(byte* buffer, size_t bufferSize) { + if (bufferSize > fieldRemainingLength) { + bufferSize = fieldRemainingLength; + } + memcpy(buffer, fieldPtr, bufferSize); + fieldRemainingLength -= bufferSize; + fieldPtr += bufferSize; + return bufferSize; +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPURLEncodedBodyParser.hpp b/src/http/HTTPURLEncodedBodyParser.hpp new file mode 100644 index 0000000..f8529a7 --- /dev/null +++ b/src/http/HTTPURLEncodedBodyParser.hpp @@ -0,0 +1,32 @@ +#ifndef SRC_HTTPURLENCODEDBODYPARSER_HPP_ +#define SRC_HTTPURLENCODEDBODYPARSER_HPP_ + +#include +#include "HTTPBodyParser.hpp" + +namespace httpsserver { + +class HTTPURLEncodedBodyParser : public HTTPBodyParser { +public: + // From HTTPBodyParser + HTTPURLEncodedBodyParser(HTTPRequest * req); + ~HTTPURLEncodedBodyParser(); + virtual bool nextField(); + virtual std::string getFieldName(); + virtual std::string getFieldFilename(); + virtual std::string getFieldMimeType(); + virtual bool endOfField(); + virtual size_t read(byte* buffer, size_t bufferSize); +protected: + char *bodyBuffer; + char *bodyPtr; + size_t bodyLength; + std::string fieldName; + std::string fieldBuffer; + const char *fieldPtr; + size_t fieldRemainingLength; +}; + +} // namespace httpserver + +#endif \ No newline at end of file diff --git a/src/http/HTTPValidator.cpp b/src/http/HTTPValidator.cpp new file mode 100644 index 0000000..bb3506b --- /dev/null +++ b/src/http/HTTPValidator.cpp @@ -0,0 +1,15 @@ +#include "HTTPValidator.hpp" + +namespace httpsserver { + +HTTPValidator::HTTPValidator(const uint8_t idx, const HTTPValidationFunction * validatorFunction): + _idx(idx), + _validatorFunction(validatorFunction) { + +} + +HTTPValidator::~HTTPValidator() { + +} + +} /* namespace httpsserver */ diff --git a/src/http/HTTPValidator.hpp b/src/http/HTTPValidator.hpp new file mode 100644 index 0000000..c07c009 --- /dev/null +++ b/src/http/HTTPValidator.hpp @@ -0,0 +1,23 @@ +#ifndef SRC_HTTPVALIDATOR_HPP_ +#define SRC_HTTPVALIDATOR_HPP_ + +#include + +namespace httpsserver { + +typedef bool (HTTPValidationFunction)(std::string); + +/** + * \brief Internal representation of a validator function + */ +class HTTPValidator { +public: + HTTPValidator(const uint8_t idx, const HTTPValidationFunction * validatorFunction); + virtual ~HTTPValidator(); + const uint8_t _idx; + const HTTPValidationFunction * _validatorFunction; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_HTTPVALIDATOR_HPP_ */ diff --git a/src/http/ResolvedResource.cpp b/src/http/ResolvedResource.cpp new file mode 100644 index 0000000..4b96927 --- /dev/null +++ b/src/http/ResolvedResource.cpp @@ -0,0 +1,40 @@ +#include "ResolvedResource.hpp" + +namespace httpsserver { + +ResolvedResource::ResolvedResource() { + _matchingNode = NULL; + _params = NULL; +} + +ResolvedResource::~ResolvedResource() { + // Delete only params, nodes are reused/server-internal + if (_params != NULL) { + delete _params; + } +} + +bool ResolvedResource::didMatch() { + return _matchingNode != NULL; +} + +HTTPNode * ResolvedResource::getMatchingNode() { + return _matchingNode; +} + +void ResolvedResource::setMatchingNode(HTTPNode * node) { + _matchingNode = node; +} + +ResourceParameters * ResolvedResource::getParams() { + return _params; +} + +void ResolvedResource::setParams(ResourceParameters * params) { + if (_params != NULL && _params!=params) { + delete _params; + } + _params = params; +} + +} /* namespace httpsserver */ diff --git a/src/http/ResolvedResource.hpp b/src/http/ResolvedResource.hpp new file mode 100644 index 0000000..5b901cc --- /dev/null +++ b/src/http/ResolvedResource.hpp @@ -0,0 +1,30 @@ +#ifndef SRC_RESOLVEDRESOURCE_HPP_ +#define SRC_RESOLVEDRESOURCE_HPP_ + +#include "ResourceNode.hpp" +#include "ResourceParameters.hpp" + +namespace httpsserver { + +/** + * \brief This class represents a resolved resource, meaning the result of mapping a string URL to an HTTPNode + */ +class ResolvedResource { +public: + ResolvedResource(); + ~ResolvedResource(); + + void setMatchingNode(HTTPNode * node); + HTTPNode * getMatchingNode(); + bool didMatch(); + ResourceParameters * getParams(); + void setParams(ResourceParameters * params); + +private: + HTTPNode * _matchingNode; + ResourceParameters * _params; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_RESOLVEDRESOURCE_HPP_ */ diff --git a/src/http/ResourceNode.cpp b/src/http/ResourceNode.cpp new file mode 100644 index 0000000..9b61e48 --- /dev/null +++ b/src/http/ResourceNode.cpp @@ -0,0 +1,16 @@ +#include "ResourceNode.hpp" + +namespace httpsserver { + +ResourceNode::ResourceNode(const std::string &path, const std::string &method, const HTTPSCallbackFunction * callback, const std::string &tag): + HTTPNode(path, HANDLER_CALLBACK, tag), + _method(method), + _callback(callback) { + +} + +ResourceNode::~ResourceNode() { + +} + +} /* namespace httpsserver */ diff --git a/src/http/ResourceNode.hpp b/src/http/ResourceNode.hpp new file mode 100644 index 0000000..c7c4519 --- /dev/null +++ b/src/http/ResourceNode.hpp @@ -0,0 +1,28 @@ +#ifndef SRC_RESOURCENODE_HPP_ +#define SRC_RESOURCENODE_HPP_ + +#include + +#include "HTTPNode.hpp" +#include "HTTPSCallbackFunction.hpp" + +namespace httpsserver { + +/** + * \brief This HTTPNode represents a route that maps to a regular HTTP request for a resource (static or dynamic) + * + * It therefore contrasts to the WebsocketNode, which handles requests for Websockets. + */ +class ResourceNode : public HTTPNode { +public: + ResourceNode(const std::string &path, const std::string &method, const HTTPSCallbackFunction * callback, const std::string &tag = ""); + virtual ~ResourceNode(); + + const std::string _method; + const HTTPSCallbackFunction * _callback; + std::string getMethod() { return _method; } +}; + +} /* namespace httpsserver */ + +#endif /* SRC_RESOURCENODE_HPP_ */ diff --git a/src/http/ResourceParameters.cpp b/src/http/ResourceParameters.cpp new file mode 100644 index 0000000..8a491b9 --- /dev/null +++ b/src/http/ResourceParameters.cpp @@ -0,0 +1,167 @@ +#include "ResourceParameters.hpp" +#include "HTTPSServerConstants.hpp" + +namespace httpsserver { + +ResourceParameters::ResourceParameters() { + +} + +ResourceParameters::~ResourceParameters() { + +} + +/** + * @brief Checks whether a specific HTTPS query parameter is set. + * + * Query parameters are key-value pairs that are appended to the URI after a question mark. + * + * If the key exists (either as a value-less parameter or with a value), the function returns true. + * + * @param name The parameter to check + * @return true iff the parameter exists + */ +bool ResourceParameters::isQueryParameterSet(std::string const &name) { + for(auto queryParam = _queryParams.begin(); queryParam != _queryParams.end(); ++queryParam) { + if ((*queryParam).first.compare(name)==0) { + return true; + } + } + return false; +} + +/** + * @brief Returns an HTTP query parameter. + * + * Query parameters are key-value pairs that are appended to the URI after a question mark. + * + * The name parameter specifies the name of the query parameter to retrieve. If it is set, + * the value is written to the value parameter and true is returned. If the parameter does + * not exist, value is left unchanged and false is returned. If the parameter is used + * without a value, an empty string is written to value and true is returned. + * + * @param name The name of the parameter to retrieve. If the parameter exists multiple times, + * the first occurence is used for the value. Use beginQueryParameters() to retrieve all values. + * @param value The target to write the value to, if the parameter exists. + * @return true iff the parameter exists and the corresponding value has been written. + */ +bool ResourceParameters::getQueryParameter(std::string const &name, std::string &value) { + for(auto queryParam = _queryParams.begin(); queryParam != _queryParams.end(); ++queryParam) { + if ((*queryParam).first.compare(name)==0) { + value=(*queryParam).second; + return true; + } + } + return false; +} + +/** + * @brief Returns the number of query parameters. + * + * Query parameters are key-value pairs that are appended to the URI after a question mark. + * + * @param unique If true, return the number of unique keys (using the same key multiple times + * is counted only once). False by default, as checking for uniqueness is not efficient. + * @return Number of query parameters + */ +size_t ResourceParameters::getQueryParameterCount(bool unique) { + if (!unique) { + return _queryParams.size(); + } + size_t count = 0; + for(auto a = _queryParams.begin(); a != _queryParams.end(); ++a) { + bool exists = false; + for(auto b = _queryParams.begin(); !exists && b != a; ++b) { + exists = (*a).first.compare((*b).first)==0; + } + count += exists ? 0 : 1; + } + return count; +} + +/** + * @brief Provides iterator access to the query parameters + * + * Query parameters are key-value pairs that are appended to the URI after a question mark. + * + * If you want just a specific parameter, have a look at getQueryParameter() + * + * The iterator will yield pairs of std::string, of which the first value specifies the + * query parameter key and the second value corresponds to the query parameters value. + * If the entry is value-less, the second value will be the empty string. + * + * If the same key is used multiple times in the query, the iterator will yield it multiple + * times, once for each occurence with the specific value. + * + * @return Iterator over std::pairs of std::strings that represent (key, value) pairs + */ +std::vector>::iterator ResourceParameters::beginQueryParameters() { + return _queryParams.begin(); +} + +/** + * @brief Counterpart to beginQueryParameters() for iterating over query parameters + */ +std::vector>::iterator ResourceParameters::endQueryParameters() { + return _queryParams.end(); +} + +void ResourceParameters::setQueryParameter(std::string const &name, std::string const &value) { + std::pair param; + param.first = name; + param.second = value; + _queryParams.push_back(param); +} + +/** + * @brief Checks for the existence of a path parameter and returns it as string. + * + * Path parameters are defined by an asterisk as placeholder when specifying the path of + * the ResourceNode and addressed by an index starting at 0 for the first parameter. + * + * For values of idx that have no matching placeholder, value is left unchanged and the + * method will return false. + * + * @param idx Defines the index of the parameter to return, starting with 0. + * @param value The value is written into this parameter. + * @return true iff the value could be written. + */ +bool ResourceParameters::getPathParameter(size_t const idx, std::string &value) { + if (idx < _pathParams.size()) { + value = _pathParams.at(idx); + return true; + } + return false; +} + +/** + * @brief Directly returns a path parameter + * + * Path parameters are defined by an asterisk as placeholder when specifying the path of + * the ResourceNode and addressed by an index starting at 0 for the first parameter. + * + * This method will return the parameter specified by the index. The caller is responsible + * to assure that the index exists. Otherwise, an empty string will be returned. + * + * @param idx Defines the index of the parameter to return, starting with 0. + * @return the value of the placeholder + */ +std::string ResourceParameters::getPathParameter(size_t const idx) { + if (idx < _pathParams.size()) { + return _pathParams.at(idx); + } + return ""; +} + +void ResourceParameters::resetPathParameters() { + _pathParams.clear(); +} + +void ResourceParameters::setPathParameter(size_t idx, std::string const &val) { + if(idx>=_pathParams.size()) { + _pathParams.resize(idx + 1); + } + _pathParams.at(idx) = val; +} + +} /* namespace httpsserver */ diff --git a/src/http/ResourceParameters.hpp b/src/http/ResourceParameters.hpp new file mode 100644 index 0000000..efa788a --- /dev/null +++ b/src/http/ResourceParameters.hpp @@ -0,0 +1,59 @@ +#ifndef SRC_RESOURCEPARAMETERS_HPP_ +#define SRC_RESOURCEPARAMETERS_HPP_ + +#include + +#include +// Arduino declares it's own min max, incompatible with the stl... +#undef min +#undef max +#include +#include + +#include "util.hpp" + +namespace httpsserver { + +class ResourceResolver; + +/** + * @brief The ResourceParameters provide access to the parameters passed in the URI. + * + * There are two types of parameters: Path parameters and query parameters. + * + * Path parameters are the values that fill the asterisk placeholders in the route + * definition of a ResourceNode. + * + * Query parameters are the key-value pairs after a question mark which can be added + * to each request, either by specifying them manually or as result of submitting an + * HTML form with a GET as method property. + */ +class ResourceParameters { +public: + ResourceParameters(); + virtual ~ResourceParameters(); + + bool isQueryParameterSet(std::string const &name); + bool getQueryParameter(std::string const &name, std::string &value); + std::vector>::iterator beginQueryParameters(); + std::vector>::iterator endQueryParameters(); + size_t getQueryParameterCount(bool unique=false); + bool getPathParameter(size_t const idx, std::string &value); + std::string getPathParameter(size_t const idx); + +protected: + friend class ResourceResolver; + void setQueryParameter(std::string const &name, std::string const &value); + void resetPathParameters(); + void setPathParameter(size_t idx, std::string const &val); + +private: + /** Parameters in the path of the URL, the actual values for asterisk placeholders */ + std::vector _pathParams; + /** HTTP Query parameters, as key-value pairs */ + std::vector> _queryParams; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_RESOURCEPARAMETERS_HPP_ */ diff --git a/src/http/ResourceResolver.cpp b/src/http/ResourceResolver.cpp new file mode 100644 index 0000000..6eab51d --- /dev/null +++ b/src/http/ResourceResolver.cpp @@ -0,0 +1,179 @@ +#include "ResourceResolver.hpp" + +namespace httpsserver { + +ResourceResolver::ResourceResolver() { + _nodes = new std::vector(); + _defaultNode = NULL; +} + +ResourceResolver::~ResourceResolver() { + delete _nodes; +} + +/** + * This method will register the HTTPSNode so it is reachable and its callback gets called for a request + */ +void ResourceResolver::registerNode(HTTPNode *node) { + _nodes->push_back(node); +} + +/** + * This method can be used to deactivate a HTTPSNode that has been registered previously + */ +void ResourceResolver::unregisterNode(HTTPNode *node) { + +} + +void ResourceResolver::resolveNode(const std::string &method, const std::string &url, ResolvedResource &resolvedResource, HTTPNodeType nodeType) { + // Reset the resource + resolvedResource.setMatchingNode(NULL); + resolvedResource.setParams(NULL); + + // Memory management of this object will be performed by the ResolvedResource instance + ResourceParameters * params = new ResourceParameters(); + + // Split URL in resource name and request params. Request params start after an optional '?' + size_t reqparamIdx = url.find('?'); + // Store this index to stop path parsing there + size_t pathEnd = reqparamIdx != std::string::npos ? reqparamIdx : url.size(); + + // If no '?' is contained in url, 0:npos will return the string as it is + std::string resourceName = url.substr(0, reqparamIdx); + + // Set request params in params object if a '?' exists + if (reqparamIdx != std::string::npos) { + do { + // Drop the '?' or '&' + reqparamIdx += 1; + + // Parameters are separated by '&' + size_t nextparamIdx = url.find('&', reqparamIdx); + + // Get the "name=value" string + std::string param = nextparamIdx == std::string::npos ? + url.substr(reqparamIdx) : + url.substr(reqparamIdx, nextparamIdx - reqparamIdx); + + if (param.length() > 0) { + // Find the position where the string has to be split + size_t nvSplitIdx = param.find('='); + + // Use empty string if only name is set. /foo?bar&baz=1 will return "" for bar + std::string name = urlDecode(param.substr(0, nvSplitIdx)); + std::string value = ""; + if (nvSplitIdx != std::string::npos) { + value = urlDecode(param.substr(nvSplitIdx+1)); + } + + // Now we finally have name and value. + params->setQueryParameter(name, value); + } + + // Update reqparamIdx + reqparamIdx = nextparamIdx; + + } while(reqparamIdx != std::string::npos); + } + + + // Check whether a resource matches + + for(std::vector::iterator itNode = _nodes->begin(); itNode != _nodes->end(); ++itNode) { + params->resetPathParameters(); + HTTPNode *node = *itNode; + if (node->_nodeType==nodeType) { + if ( + // For handler functions, check the method declared with the node + (node->_nodeType==HANDLER_CALLBACK && ((ResourceNode*)node)->_method == method) || + // For websockets, the specification says that GET is the only choice + (node->_nodeType==WEBSOCKET && method=="GET") + ) { + HTTPS_LOGD("Testing route %s", node->_path.c_str()); + bool match = true; + size_t paramCount = node->getPathParamCount(); + // indices in input and pattern + size_t inputIdx = 0, pathIdx = 0; + HTTPS_LOGD("(INIT) inputIdx: %d, pathIdx: %d, pathEnd: %d, path: %s, url: %s", + inputIdx, pathIdx, pathEnd, node->_path.c_str(), url.c_str()); + + for (size_t paramIdx = 0; match && paramIdx < paramCount; paramIdx += 1) { + HTTPS_LOGD("(LOOP) inputIdx: %d, pathIdx: %d, pathEnd: %d, path: %s, url: %s", + inputIdx, pathIdx, pathEnd, node->_path.c_str(), url.c_str()); + // Test static path before the parameter + size_t paramPos = node->getParamIdx(paramIdx); + size_t staticLength = paramPos - pathIdx; + match &= url.substr(inputIdx, staticLength) == node->_path.substr(pathIdx, staticLength); + inputIdx += staticLength; + pathIdx += staticLength; + + // Extract parameter value + if (match) { + size_t paramEnd = url.find('/', inputIdx); + if (paramEnd == std::string::npos && inputIdx <= pathEnd) { + // Consume the remaining input (might be "" for the last param) + paramEnd = pathEnd; + } + if (paramEnd != std::string::npos) { + size_t paramLength = paramEnd - inputIdx; + params->setPathParameter(paramIdx, urlDecode(url.substr(inputIdx, paramLength))); + pathIdx += 1; + inputIdx += paramLength; + } else { + match = false; + HTTPS_LOGD("(LOOP) No match on param part"); + } + } else { + HTTPS_LOGD("(LOOP) No match on static part"); + } + } + HTTPS_LOGD("(STTC) inputIdx: %d, pathIdx: %d, pathEnd: %d, path: %s, url: %s", + inputIdx, pathIdx, pathEnd, node->_path.c_str(), url.c_str()); + // Test static path after the parameter (up to pathEnd) + if (match) { + match = url.substr(inputIdx, pathEnd - inputIdx)==node->_path.substr(pathIdx); + } + HTTPS_LOGD("(END ) inputIdx: %d, pathIdx: %d, pathEnd: %d, path: %s, url: %s", + inputIdx, pathIdx, pathEnd, node->_path.c_str(), url.c_str()); + + if (match) { + resolvedResource.setMatchingNode(node); + HTTPS_LOGD("It's a match!"); + break; + } + } // method check + } // node type check + } // resource node for loop + + // If the resource did not match, configure the default resource + if (!resolvedResource.didMatch() && _defaultNode != NULL) { + params->resetPathParameters(); + resolvedResource.setMatchingNode(_defaultNode); + } + + // If resolving did work, set the params, otherwise delete them + if (resolvedResource.didMatch()) { + // The resolvedResource now takes care of memory management for the params + resolvedResource.setParams(params); + } else { + delete params; + } +} + +void ResourceResolver::addMiddleware(const HTTPSMiddlewareFunction * mwFunction) { + _middleware.push_back(mwFunction); +} + +void ResourceResolver::removeMiddleware(const HTTPSMiddlewareFunction * mwFunction) { + _middleware.erase(std::remove(_middleware.begin(), _middleware.end(), mwFunction), _middleware.end()); +} + +const std::vector ResourceResolver::getMiddleware() { + return _middleware; +} + +void ResourceResolver::setDefaultNode(HTTPNode * defaultNode) { + _defaultNode = defaultNode; +} + +} diff --git a/src/http/ResourceResolver.hpp b/src/http/ResourceResolver.hpp new file mode 100644 index 0000000..cb67e7f --- /dev/null +++ b/src/http/ResourceResolver.hpp @@ -0,0 +1,51 @@ +#ifndef SRC_RESOURCERESOLVER_HPP_ +#define SRC_RESOURCERESOLVER_HPP_ + +#include +// Arduino declares it's own min max, incompatible with the stl... +#undef min +#undef max +#include +#include + +#include "HTTPNode.hpp" +#include "WebsocketNode.hpp" +#include "ResourceNode.hpp" +#include "ResolvedResource.hpp" +#include "HTTPMiddlewareFunction.hpp" + +namespace httpsserver { + +/** + * \brief This class is used internally to resolve a string URL to the corresponding HTTPNode + */ +class ResourceResolver { +public: + ResourceResolver(); + ~ResourceResolver(); + + void registerNode(HTTPNode *node); + void unregisterNode(HTTPNode *node); + void setDefaultNode(HTTPNode *node); + void resolveNode(const std::string &method, const std::string &url, ResolvedResource &resolvedResource, HTTPNodeType nodeType); + + /** Add a middleware function to the end of the middleware function chain. See HTTPSMiddlewareFunction.hpp for details. */ + void addMiddleware(const HTTPSMiddlewareFunction * mwFunction); + /** Remove a specific function from the middleware function chain. */ + void removeMiddleware(const HTTPSMiddlewareFunction * mwFunction); + /** Get the current middleware chain with a resource function at the end */ + const std::vector getMiddleware(); + +private: + + // This vector holds all nodes (with callbacks) that are registered + std::vector * _nodes; + HTTPNode * _defaultNode; + + // Middleware functions, if any are registered. Will be called in order of the vector. + std::vector _middleware; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_RESOURCERESOLVER_HPP_ */ diff --git a/src/http/SSLCert.cpp b/src/http/SSLCert.cpp new file mode 100644 index 0000000..3df7073 --- /dev/null +++ b/src/http/SSLCert.cpp @@ -0,0 +1,310 @@ +#include "SSLCert.hpp" + +namespace httpsserver { + +SSLCert::SSLCert(unsigned char * certData, uint16_t certLength, unsigned char * pkData, uint16_t pkLength): + _certLength(certLength), + _certData(certData), + _pkLength(pkLength), + _pkData(pkData) { + +} + +SSLCert::~SSLCert() { + // TODO Auto-generated destructor stub +} + + +uint16_t SSLCert::getCertLength() { + return _certLength; +} + +uint16_t SSLCert::getPKLength() { + return _pkLength; +} + +unsigned char * SSLCert::getCertData() { + return _certData; +} + +unsigned char * SSLCert::getPKData() { + return _pkData; +} + +void SSLCert::setPK(unsigned char * pkData, uint16_t length) { + _pkData = pkData; + _pkLength = length; +} + +void SSLCert::setCert(unsigned char * certData, uint16_t length) { + _certData = certData; + _certLength = length; +} + +void SSLCert::clear() { + for(uint16_t i = 0; i < _certLength; i++) _certData[i]=0; + delete _certData; + _certLength = 0; + + for(uint16_t i = 0; i < _pkLength; i++) _pkData[i] = 0; + delete _pkData; + _pkLength = 0; +} + +#ifndef HTTPS_DISABLE_SELFSIGNING + +/** + * Function to create the key for a self-signed certificate. + * + * Writes private key as DER in certCtx + * + * Based on programs/pkey/gen_key.c + */ +static int gen_key(SSLCert &certCtx, SSLKeySize keySize) { + + // Initialize the entropy source + mbedtls_entropy_context entropy; + mbedtls_entropy_init( &entropy ); + + // Initialize the RNG + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ctr_drbg_init( &ctr_drbg ); + int rngRes = mbedtls_ctr_drbg_seed( + &ctr_drbg, mbedtls_entropy_func, &entropy, + NULL, 0 + ); + if (rngRes != 0) { + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_RNG; + } + + // Initialize the private key + mbedtls_pk_context key; + mbedtls_pk_init( &key ); + int resPkSetup = mbedtls_pk_setup( &key, mbedtls_pk_info_from_type( MBEDTLS_PK_RSA ) ); + if ( resPkSetup != 0) { + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK; + } + + // Actual key generation + int resPkGen = mbedtls_rsa_gen_key( + mbedtls_pk_rsa( key ), + mbedtls_ctr_drbg_random, + &ctr_drbg, + keySize, + 65537 + ); + if ( resPkGen != 0) { + mbedtls_pk_free( &key ); + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_GEN_PK; + } + + // Free the entropy source and the RNG as they are no longer needed + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + + // Allocate the space on the heap, as stack size is quite limited + unsigned char * output_buf = new unsigned char[4096]; + if (output_buf == NULL) { + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM; + } + memset(output_buf, 0, 4096); + + // Write the key to the temporary buffer and determine its length + int resPkWrite = mbedtls_pk_write_key_der( &key, output_buf, 4096 ); + if (resPkWrite < 0) { + delete[] output_buf; + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_WRITE_PK; + } + size_t pkLength = resPkWrite; + unsigned char *pkOffset = output_buf + sizeof(unsigned char) * 4096 - pkLength; + + // Copy the key into a new, fitting space on the heap + unsigned char * output_pk = new unsigned char[pkLength]; + if (output_pk == NULL) { + delete[] output_buf; + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_WRITE_PK; + } + memcpy(output_pk, pkOffset, pkLength); + + // Clean up the temporary buffer and clear the key context + delete[] output_buf; + mbedtls_pk_free( &key ); + + // Set the private key in the context + certCtx.setPK(output_pk, pkLength); + + return 0; +} + +/** + * Function to generate the X509 certificate data for a private key + * + * Writes certificate in certCtx + * + * Based on programs/x509/cert_write.c + */ +static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom, std::string validityTo) { + int funcRes = 0; + int stepRes = 0; + + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_pk_context key; + mbedtls_x509write_cert crt; + mbedtls_mpi serial; + unsigned char * primary_buffer; + unsigned char *certOffset; + unsigned char * output_buffer; + size_t certLength; + + // Make a C-friendly version of the distinguished name + char dn_cstr[dn.length()+1]; + strcpy(dn_cstr, dn.c_str()); + + // Initialize the entropy source + mbedtls_entropy_init( &entropy ); + + // Initialize the RNG + mbedtls_ctr_drbg_init( &ctr_drbg ); + stepRes = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0 ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_RNG; + goto error_after_entropy; + } + + mbedtls_pk_init( &key ); + stepRes = mbedtls_pk_parse_key( &key, certCtx.getPKData(), certCtx.getPKLength(), NULL, 0 ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_READKEY; + goto error_after_key; + } + + // Start configuring the certificate + mbedtls_x509write_crt_init( &crt ); + // Set version and hash algorithm + mbedtls_x509write_crt_set_version( &crt, MBEDTLS_X509_CRT_VERSION_3 ); + mbedtls_x509write_crt_set_md_alg( &crt, MBEDTLS_MD_SHA256 ); + + // Set the keys (same key as we self-sign) + mbedtls_x509write_crt_set_subject_key( &crt, &key ); + mbedtls_x509write_crt_set_issuer_key( &crt, &key ); + + // Set issuer and subject (same, as we self-sign) + stepRes = mbedtls_x509write_crt_set_subject_name( &crt, dn_cstr ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME; + goto error_after_cert; + } + stepRes = mbedtls_x509write_crt_set_issuer_name( &crt, dn_cstr ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME; + goto error_after_cert; + } + + // Set the validity of the certificate. At the moment, it's fixed from 2019 to end of 2029. + stepRes = mbedtls_x509write_crt_set_validity( &crt, validityFrom.c_str(), validityTo.c_str()); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY; + goto error_after_cert; + } + + // Make this a CA certificate + stepRes = mbedtls_x509write_crt_set_basic_constraints( &crt, 1, 0 ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY; + goto error_after_cert; + } + + // Initialize the serial number + mbedtls_mpi_init( &serial ); + stepRes = mbedtls_mpi_read_string( &serial, 10, "1" ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; + goto error_after_cert_serial; + } + stepRes = mbedtls_x509write_crt_set_serial( &crt, &serial ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; + goto error_after_cert_serial; + } + + // Create buffer to write the certificate + primary_buffer = new unsigned char[4096]; + if (primary_buffer == NULL) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM; + goto error_after_cert_serial; + } + + // Write the actual certificate + stepRes = mbedtls_x509write_crt_der(&crt, primary_buffer, 4096, mbedtls_ctr_drbg_random, &ctr_drbg ); + if (stepRes < 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_WRITE; + goto error_after_primary_buffer; + } + + // Create a matching buffer + certLength = stepRes; + certOffset = primary_buffer + sizeof(unsigned char) * 4096 - certLength; + + // Copy the cert into a new, fitting space on the heap + output_buffer = new unsigned char[certLength]; + if (output_buffer == NULL) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM; + goto error_after_primary_buffer; + } + memcpy(output_buffer, certOffset, certLength); + + // Configure the cert in the context + certCtx.setCert(output_buffer, certLength); + + // Run through the cleanup process +error_after_primary_buffer: + delete[] primary_buffer; + +error_after_cert_serial: + mbedtls_mpi_free( &serial ); + +error_after_cert: + mbedtls_x509write_crt_free( &crt ); + +error_after_key: + mbedtls_pk_free(&key); + +error_after_entropy: + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return funcRes; +} + +int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom, std::string validUntil) { + + // Add the private key + int keyRes = gen_key(certCtx, keySize); + if (keyRes != 0) { + // Key-generation failed, return the failure code + return keyRes; + } + + // Add the self-signed certificate + int certRes = cert_write(certCtx, dn, validFrom, validUntil); + if (certRes != 0) { + // Cert writing failed, reset the pk and return failure code + certCtx.setPK(NULL, 0); + return certRes; + } + + // If all went well, return 0 + return 0; +} + +#endif // !HTTPS_DISABLE_SELFSIGNING + +} /* namespace httpsserver */ diff --git a/src/http/SSLCert.hpp b/src/http/SSLCert.hpp new file mode 100644 index 0000000..6296c3d --- /dev/null +++ b/src/http/SSLCert.hpp @@ -0,0 +1,182 @@ +#ifndef SRC_SSLCERT_HPP_ +#define SRC_SSLCERT_HPP_ + +#include + +#ifndef HTTPS_DISABLE_SELFSIGNING +#include +#include +#include +#include +#include +#include +#include +#include + +#define HTTPS_SERVER_ERROR_KEYGEN 0x0F +#define HTTPS_SERVER_ERROR_KEYGEN_RNG 0x02 +#define HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK 0x03 +#define HTTPS_SERVER_ERROR_KEYGEN_GEN_PK 0x04 +#define HTTPS_SERVER_ERROR_KEY_WRITE_PK 0x05 +#define HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM 0x06 +#define HTTPS_SERVER_ERROR_CERTGEN 0x1F +#define HTTPS_SERVER_ERROR_CERTGEN_RNG 0x12 +#define HTTPS_SERVER_ERROR_CERTGEN_READKEY 0x13 +#define HTTPS_SERVER_ERROR_CERTGEN_WRITE 0x15 +#define HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM 0x16 +#define HTTPS_SERVER_ERROR_CERTGEN_NAME 0x17 +#define HTTPS_SERVER_ERROR_CERTGEN_SERIAL 0x18 +#define HTTPS_SERVER_ERROR_CERTGEN_VALIDITY 0x19 + +#endif // !HTTPS_DISABLE_SELFSIGNING + +namespace httpsserver { + +/** + * \brief Certificate and private key that can be passed to the HTTPSServer. + * + * **Converting PEM to DER Files** + * + * Certificate: + * ```bash + * openssl x509 -inform PEM -outform DER -in myCert.crt -out cert.der + * ``` + * + * Private Key: + * ```bash + * openssl rsa -inform PEM -outform DER -in myCert.key -out key.der + * ``` + * + * **Converting DER File to C Header** + * + * ```bash + * echo "#ifndef KEY_H_" > ./key.h + * echo "#define KEY_H_" >> ./key.h + * xxd -i key.der >> ./key.h + * echo "#endif" >> ./key.h + * ``` + */ +class SSLCert { +public: + /** + * \brief Creates a new SSLCert. + * + * The certificate and key data may be NULL (default values) if the certificate is meant + * to be passed to createSelfSignedCert(). + * + * Otherwise, the data must reside in a memory location that is not deleted until the server + * using the certificate is stopped. + * + * \param[in] certData The certificate data to use (DER format) + * \param[in] certLength The length of the certificate data + * \param[in] pkData The private key data to use (DER format) + * \param[in] pkLength The length of the private key + */ + SSLCert( + unsigned char * certData = NULL, + uint16_t certLength = 0, + unsigned char * pkData = NULL, + uint16_t pkLength = 0 + ); + virtual ~SSLCert(); + + /** + * \brief Returns the length of the certificate in byte + */ + uint16_t getCertLength(); + + /** + * \brief Returns the length of the private key in byte + */ + uint16_t getPKLength(); + + /** + * \brief Returns the certificate data + */ + unsigned char * getCertData(); + + /** + * \brief Returns the private key data + */ + unsigned char * getPKData(); + + /** + * \brief Sets the private key in DER format + * + * The data has to reside in a place in memory that is not deleted as long as the + * server is running. + * + * See SSLCert() for some information on how to generate DER data. + * + * \param[in] _pkData The data of the private key + * \param[in] length The length of the private key + */ + void setPK(unsigned char * _pkData, uint16_t length); + + /** + * \brief Sets the certificate data in DER format + * + * The data has to reside in a place in memory that is not deleted as long as the + * server is running. + * + * See SSLCert for some information on how to generate DER data. + * + * \param[in] _certData The data of the certificate + * \param[in] length The length of the certificate + */ + void setCert(unsigned char * _certData, uint16_t length); + + /** + * \brief Clears the key buffers and deletes them. + */ + void clear(); + +private: + uint16_t _certLength; + unsigned char * _certData; + uint16_t _pkLength; + unsigned char * _pkData; + +}; + +#ifndef HTTPS_DISABLE_SELFSIGNING + +/** + * \brief Defines the key size for key generation + * + * Not available if the `HTTPS_DISABLE_SELFSIGNING` compiler flag is set + */ +enum SSLKeySize { + /** \brief RSA key with 1024 bit */ + KEYSIZE_1024 = 1024, + /** \brief RSA key with 2048 bit */ + KEYSIZE_2048 = 2048, + /** \brief RSA key with 4096 bit */ + KEYSIZE_4096 = 4096 +}; + +/** + * \brief Creates a self-signed certificate on the ESP32 + * + * This function creates a new self-signed certificate for the given hostname on the heap. + * Make sure to clear() it before you delete it. + * + * The distinguished name (dn) parameter has to follow the x509 specifications. An example + * would be: + * CN=myesp.local,O=acme,C=US + * + * The strings validFrom and validUntil have to be formatted like this: + * "20190101000000", "20300101000000" + * + * This will take some time, so you should probably write the certificate data to non-volatile + * storage when you are done. + * + * Setting the `HTTPS_DISABLE_SELFSIGNING` compiler flag will remove this function from the library + */ +int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom = "20190101000000", std::string validUntil = "20300101000000"); + +#endif // !HTTPS_DISABLE_SELFSIGNING + +} /* namespace httpsserver */ + +#endif /* SRC_SSLCERT_HPP_ */ diff --git a/src/http/ValidatorFunctions.cpp b/src/http/ValidatorFunctions.cpp new file mode 100644 index 0000000..932b0a7 --- /dev/null +++ b/src/http/ValidatorFunctions.cpp @@ -0,0 +1,14 @@ +#include "ValidatorFunctions.hpp" + +namespace httpsserver { + bool validateNotEmpty(std::string s) { + return s!=""; + } + + bool validateUnsignedInteger(std::string s) { + for(size_t x = 0; x < s.size(); x++) { + if (s[x]<'0' || s[x]>'9') return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/http/ValidatorFunctions.hpp b/src/http/ValidatorFunctions.hpp new file mode 100644 index 0000000..f9328ae --- /dev/null +++ b/src/http/ValidatorFunctions.hpp @@ -0,0 +1,37 @@ +#ifndef SRC_VALIDATORFUNCTIONS_HPP_ +#define SRC_VALIDATORFUNCTIONS_HPP_ + +#include +#include +#undef max +#undef min +#include +#include +#include "HTTPValidator.hpp" +#include "util.hpp" + +/** + * This file contains some validator functions that can be used to validate URL parameters. + * + * They covor common cases like checking for integer, non-empty, ..., so the user of this library + * does not need to write them on his own. + */ + +namespace httpsserver { + + /** + * \brief **Built-in validator function**: Checks that a string is not empty. + */ + bool validateNotEmpty(std::string s); + + /** + * \brief **Built-in validator function**: Checks that a value is a positive int + * + * Checks that the value is a positive integer (combine it with newValidateUnsignedIntegerMax if + * you have constraints regarding the size of that number) + */ + bool validateUnsignedInteger(std::string s); + +} + +#endif diff --git a/src/http/WebsocketHandler.cpp b/src/http/WebsocketHandler.cpp new file mode 100644 index 0000000..5e4c1d1 --- /dev/null +++ b/src/http/WebsocketHandler.cpp @@ -0,0 +1,254 @@ +#include "WebsocketHandler.hpp" + +namespace httpsserver { + +/** + * @brief Dump the content of the WebSocket frame for debugging. + * @param [in] frame The frame to dump. + */ +static void dumpFrame(WebsocketFrame frame) { + std::string opcode = std::string("Unknown"); + switch(frame.opCode) { + case WebsocketHandler::OPCODE_BINARY: opcode = std::string("BINARY"); break; + case WebsocketHandler::OPCODE_CONTINUE: opcode = std::string("CONTINUE"); break; + case WebsocketHandler::OPCODE_CLOSE: opcode = std::string("CLOSE"); break; + case WebsocketHandler::OPCODE_PING: opcode = std::string("PING"); break; + case WebsocketHandler::OPCODE_PONG: opcode = std::string("PONG"); break; + case WebsocketHandler::OPCODE_TEXT: opcode = std::string("TEXT"); break; + } + ESP_LOGI( + TAG, + "Fin: %d, OpCode: %d (%s), Mask: %d, Len: %d", + (int)frame.fin, + (int)frame.opCode, + opcode.c_str(), + (int)frame.mask, + (int)frame.len + ); +} + +WebsocketHandler::WebsocketHandler() { + _con = nullptr; + _receivedClose = false; + _sentClose = false; +} + +WebsocketHandler::~WebsocketHandler() { + +} // ~WebSocketHandler() + + +/** +* @brief The default onClose handler. +* If no over-riding handler is provided for the "close" event, this method is called. +*/ +void WebsocketHandler::onClose() { + HTTPS_LOGD("WebsocketHandler close()"); +} + +/** +* @brief The default onData handler. +* If no over-riding handler is provided for the "message" event, this method is called. +* A particularly useful pattern for using onMessage is: +* ``` +* std::stringstream buffer; +* buffer << pWebSocketInputRecordStreambuf; +* ``` +* This will read the whole message into the string stream. +*/ +void WebsocketHandler::onMessage(WebsocketInputStreambuf* pWebsocketInputStreambuf) { //, Websocket *pWebSocket) { + HTTPS_LOGD("WebsocketHandler onMessage()"); +} + + +/** +* @brief The default onError handler. +* If no over-riding handler is provided for the "error" event, this method is called. +*/ +void WebsocketHandler::onError(std::string error) { + HTTPS_LOGD("WebsocketHandler onError()"); +} + +void WebsocketHandler::initialize(ConnectionContext * con) { + _con = con; +} + +void WebsocketHandler::loop() { + if(read() < 0) { + close(); + } +} + +int WebsocketHandler::read() { + WebsocketFrame frame; + int length = _con->readBuffer((uint8_t*)&frame, sizeof(frame)); + HTTPS_LOGD("Websocket: Read %d bytes", length); + if(length == 0) + return 0; + else if (length != sizeof(frame)) { + HTTPS_LOGE("Websocket read error"); + //_con->closeConnection(); + return -1; + } + dumpFrame(frame); + + // The following section parses the WebSocket frame. + uint32_t payloadLen = 0; + uint8_t mask[4]; + if (frame.len < 126) { + payloadLen = frame.len; + } else if (frame.len == 126) { + uint16_t tempLen; + _con->readBuffer((uint8_t*)&tempLen, sizeof(tempLen)); + payloadLen = ntohs(tempLen); + } else if (frame.len == 127) { + uint64_t tempLen; + _con->readBuffer((uint8_t*)&tempLen, sizeof(tempLen)); + payloadLen = ntohl((uint32_t)tempLen); + } + if (frame.mask == 1) { + _con->readBuffer(mask, sizeof(mask)); + } + + if (payloadLen == 0) { + HTTPS_LOGW("WS payload not present"); + } else { + HTTPS_LOGI("WS payload: length=%d", payloadLen); + } + + switch(frame.opCode) { + case OPCODE_TEXT: + case OPCODE_BINARY: { + HTTPS_LOGD("Creating Streambuf"); + WebsocketInputStreambuf streambuf(_con, payloadLen, frame.mask==1?mask:nullptr); + HTTPS_LOGD("Calling onMessage"); + onMessage(&streambuf); + HTTPS_LOGD("Discarding Streambuf"); + streambuf.discard(); + break; + } + + case OPCODE_CLOSE: { // If the WebSocket operation code is close then we are closing the connection. + _receivedClose = true; + onClose(); + //close(); // Close the websocket. + return -1; + break; + } + + case OPCODE_CONTINUE: { + break; + } + + case OPCODE_PING: { + break; + } + + case OPCODE_PONG: { + break; + } + + default: { + HTTPS_LOGW("WebSocketReader: Unknown opcode: %d", frame.opCode); + break; + } + } // Switch opCode + return 0; +} // Websocket::read + +/** + * @brief Close the Web socket + * @param [in] status The code passed in the close request. + * @param [in] message A clarification message on the close request. + */ +void WebsocketHandler::close(uint16_t status, std::string message) { + HTTPS_LOGD("Websocket close()"); + + _sentClose = true; // Flag that we have sent a close request. + + WebsocketFrame frame; // Build the web socket frame indicating a close request. + frame.fin = 1; + frame.rsv1 = 0; + frame.rsv2 = 0; + frame.rsv3 = 0; + frame.opCode = OPCODE_CLOSE; + frame.mask = 0; + frame.len = message.length() + 2; + int rc = _con->writeBuffer((uint8_t *)&frame, sizeof(frame)); + + if (rc > 0) { + rc = _con->writeBuffer((byte *) &status, 2); + } + + if (rc > 0) { + _con->writeBuffer((byte *) message.data(), message.length()); + } +} // Websocket::close + +/** + * @brief Send data down the web socket + * See the WebSocket spec (RFC6455) section "6.1 Sending Data". + * We build a WebSocket frame, send the frame followed by the data. + * @param [in] data The data to send down the WebSocket. + * @param [in] sendType The type of payload. Either SEND_TYPE_TEXT or SEND_TYPE_BINARY. + */ +void WebsocketHandler::send(std::string data, uint8_t sendType) { + HTTPS_LOGD(">> Websocket.send(): length=%d", data.length()); + WebsocketFrame frame; + frame.fin = 1; + frame.rsv1 = 0; + frame.rsv2 = 0; + frame.rsv3 = 0; + frame.opCode = sendType==SEND_TYPE_TEXT?OPCODE_TEXT:OPCODE_BINARY; + frame.mask = 0; + if (data.length() < 126) { + frame.len = data.length(); + _con->writeBuffer((uint8_t *)&frame, sizeof(frame)); + } else { + frame.len = 126; + _con->writeBuffer((uint8_t *)&frame, sizeof(frame)); + uint16_t net_len = htons((uint16_t)data.length()); + _con->writeBuffer((uint8_t *)&net_len, sizeof(uint16_t)); // Convert to network byte order from host byte order + } + _con->writeBuffer((uint8_t*)data.data(), data.length()); + HTTPS_LOGD("<< Websocket.send()"); +} // Websocket::send + + +/** + * @brief Send data down the web socket + * See the WebSocket spec (RFC6455) section "6.1 Sending Data". + * We build a WebSocket frame, send the frame followed by the data. + * @param [in] data The data to send down the WebSocket. + * @param [in] sendType The type of payload. Either SEND_TYPE_TEXT or SEND_TYPE_BINARY. + */ +void WebsocketHandler::send(uint8_t* data, uint16_t length, uint8_t sendType) { + HTTPS_LOGD(">> Websocket.send(): length=%d", length); + WebsocketFrame frame; + frame.fin = 1; + frame.rsv1 = 0; + frame.rsv2 = 0; + frame.rsv3 = 0; + frame.opCode = sendType==SEND_TYPE_TEXT?OPCODE_TEXT:OPCODE_BINARY; + frame.mask = 0; + if (length < 126) { + frame.len = length; + _con->writeBuffer((uint8_t *)&frame, sizeof(frame)); + } else { + frame.len = 126; + _con->writeBuffer((uint8_t *)&frame, sizeof(frame)); + uint16_t net_len = htons(length); + _con->writeBuffer((uint8_t *)&net_len, sizeof(uint16_t)); // Convert to network byte order from host byte order + } + _con->writeBuffer(data, length); + HTTPS_LOGD("<< Websocket.send()"); +} // Websocket::send + +/** + * Returns true if the connection has been closed, either by client or server + */ +bool WebsocketHandler::closed() { + return _receivedClose || _sentClose; +} + +} diff --git a/src/http/WebsocketHandler.hpp b/src/http/WebsocketHandler.hpp new file mode 100644 index 0000000..c8c710d --- /dev/null +++ b/src/http/WebsocketHandler.hpp @@ -0,0 +1,87 @@ +#ifndef SRC_WEBSOCKETHANDLER_HPP_ +#define SRC_WEBSOCKETHANDLER_HPP_ + +#include +#include + +#include +#undef min +#undef max + +#include + +#include "HTTPSServerConstants.hpp" +#include "ConnectionContext.hpp" +#include "WebsocketInputStreambuf.hpp" + +namespace httpsserver { + +// Structure definition for the WebSocket frame. +struct WebsocketFrame +{ + // Byte 0 + uint8_t opCode : 4; // [7:4] + uint8_t rsv3 : 1; // [3] + uint8_t rsv2 : 1; // [2] + uint8_t rsv1 : 1; // [1] + uint8_t fin : 1; // [0] + + // Byte 1 + uint8_t len : 7; // [7:1] + uint8_t mask : 1; // [0] +}; + +class WebsocketHandler +{ +public: + // WebSocket op codes as found in a WebSocket frame. + static const int OPCODE_CONTINUE = 0x00; + static const int OPCODE_TEXT = 0x01; + static const int OPCODE_BINARY = 0x02; + static const int OPCODE_CLOSE = 0x08; + static const int OPCODE_PING = 0x09; + static const int OPCODE_PONG = 0x0a; + + static const uint16_t CLOSE_NORMAL_CLOSURE = 1000; + static const uint16_t CLOSE_GOING_AWAY = 1001; + static const uint16_t CLOSE_PROTOCOL_ERROR = 1002; + static const uint16_t CLOSE_CANNOT_ACCEPT = 1003; + static const uint16_t CLOSE_NO_STATUS_CODE = 1005; + static const uint16_t CLOSE_CLOSED_ABNORMALLY = 1006; + static const uint16_t CLOSE_NOT_CONSISTENT = 1007; + static const uint16_t CLOSE_VIOLATED_POLICY = 1008; + static const uint16_t CLOSE_TOO_BIG = 1009; + static const uint16_t CLOSE_NO_EXTENSION = 1010; + static const uint16_t CLOSE_UNEXPECTED_CONDITION = 1011; + static const uint16_t CLOSE_SERVICE_RESTART = 1012; + static const uint16_t CLOSE_TRY_AGAIN_LATER = 1013; + static const uint16_t CLOSE_TLS_HANDSHAKE_FAILURE = 1015; + + static const uint8_t SEND_TYPE_BINARY = 0x01; + static const uint8_t SEND_TYPE_TEXT = 0x02; + + WebsocketHandler(); + virtual ~WebsocketHandler(); + virtual void onClose(); + virtual void onMessage(WebsocketInputStreambuf *pWebsocketInputStreambuf); + virtual void onError(std::string error); + + void close(uint16_t status = CLOSE_NORMAL_CLOSURE, std::string message = ""); + void send(std::string data, uint8_t sendType = SEND_TYPE_BINARY); + void send(uint8_t *data, uint16_t length, uint8_t sendType = SEND_TYPE_BINARY); + bool closed(); + + void loop(); + void initialize(ConnectionContext * con); + +private: + int read(); + + ConnectionContext * _con; + bool _receivedClose; // True when we have received a close request. + bool _sentClose; // True when we have sent a close request. +}; + +} + +#endif diff --git a/src/http/WebsocketInputStreambuf.cpp b/src/http/WebsocketInputStreambuf.cpp new file mode 100644 index 0000000..0e28b91 --- /dev/null +++ b/src/http/WebsocketInputStreambuf.cpp @@ -0,0 +1,110 @@ +#include "WebsocketInputStreambuf.hpp" + +namespace httpsserver { +/** + * @brief Create a Web Socket input record streambuf + * @param [in] socket The socket we will be reading from. + * @param [in] dataLength The size of a record. + * @param [in] bufferSize The size of the buffer we wish to allocate to hold data. + */ +WebsocketInputStreambuf::WebsocketInputStreambuf( + ConnectionContext *con, + size_t dataLength, + uint8_t *pMask, + size_t bufferSize +) { + _con = con; // The socket we will be reading from + _dataLength = dataLength; // The size of the record we wish to read. + _pMask = pMask; + _bufferSize = bufferSize; // The size of the buffer used to hold data + _sizeRead = 0; // The size of data read from the socket + _buffer = new char[bufferSize]; // Create the buffer used to hold the data read from the socket. + + setg(_buffer, _buffer, _buffer); // Set the initial get buffer pointers to no data. +} + +WebsocketInputStreambuf::~WebsocketInputStreambuf() { + //FIXME: Call order incorrect? discard() uses _buffer + delete[] _buffer; + discard(); +} + + +/** + * @brief Discard data for the record that has not yet been read. + * + * We are working on a logical fixed length record in a socket stream. This means that we know in advance + * how big the record should be. If we have read some data from the stream and no longer wish to consume + * any further, we have to discard the remaining bytes in the stream before we can get to process the + * next record. This function discards the remainder of the data. + * + * For example, if our record size is 1000 bytes and we have read 700 bytes and determine that we no + * longer need to continue, we can't just stop. There are still 300 bytes in the socket stream that + * need to be consumed/discarded before we can move on to the next record. + */ +void WebsocketInputStreambuf::discard() { + uint8_t byte; + HTTPS_LOGD(">> WebsocketContext.discard(): %d bytes", _dataLength - _sizeRead); + while(_sizeRead < _dataLength) { + _con->readBuffer(&byte, 1); + _sizeRead++; + } + HTTPS_LOGD("<< WebsocketContext.discard()"); +} // WebsocketInputStreambuf::discard + + +/** + * @brief Get the size of the expected record. + * @return The size of the expected record. + */ +size_t WebsocketInputStreambuf::getRecordSize() { + return _dataLength; +} // WebsocketInputStreambuf::getRecordSize + +/** + * @brief Handle the request to read data from the stream but we need more data from the source. + * + */ +WebsocketInputStreambuf::int_type WebsocketInputStreambuf::underflow() { + HTTPS_LOGD(">> WebSocketInputStreambuf.underflow()"); + + // If we have already read as many bytes as our record definition says we should read + // then don't attempt to ready any further. + if (_sizeRead >= getRecordSize()) { + HTTPS_LOGD("<< WebSocketInputStreambuf.underflow(): Already read maximum"); + return EOF; + } + + // We wish to refill the buffer. We want to read data from the socket. We want to read either + // the size of the buffer to fill it or the maximum number of bytes remaining to be read. + // We will choose which ever is smaller as the number of bytes to read into the buffer. + int remainingBytes = getRecordSize()-_sizeRead; + size_t sizeToRead; + if (remainingBytes < _bufferSize) { + sizeToRead = remainingBytes; + } else { + sizeToRead = _bufferSize; + } + + HTTPS_LOGD("WebSocketInputRecordStreambuf - getting next buffer of data; size request: %d", sizeToRead); + int bytesRead = _con->readBuffer((uint8_t*)_buffer, sizeToRead); + if (bytesRead == 0) { + HTTPS_LOGD("<< WebSocketInputRecordStreambuf.underflow(): Read 0 bytes"); + return EOF; + } + + // If the WebSocket frame shows that we have a mask bit set then we have to unmask the data. + if (_pMask != nullptr) { + for (int i=0; i +#include + +#include +#undef min +#undef max +#include +#include +#include + +#include + +#include "HTTPSServerConstants.hpp" +#include "ConnectionContext.hpp" + +namespace httpsserver { + +class WebsocketInputStreambuf : public std::streambuf { +public: + WebsocketInputStreambuf( + ConnectionContext *con, + size_t dataLength, + uint8_t *_ = nullptr, + size_t bufferSize = 2048 + ); + virtual ~WebsocketInputStreambuf(); + + int_type underflow(); + void discard(); + size_t getRecordSize(); + +private: + char *_buffer; + ConnectionContext *_con; + size_t _dataLength; + size_t _bufferSize; + size_t _sizeRead; + uint8_t *_pMask; + +}; + +} // namespace + +#endif diff --git a/src/http/WebsocketNode.cpp b/src/http/WebsocketNode.cpp new file mode 100644 index 0000000..b9d3e71 --- /dev/null +++ b/src/http/WebsocketNode.cpp @@ -0,0 +1,20 @@ +#include "WebsocketNode.hpp" + +namespace httpsserver { + +WebsocketNode::WebsocketNode(const std::string &path, const WebsocketHandlerCreator * creatorFunction, const std::string &tag): + HTTPNode(path, WEBSOCKET, tag), + _creatorFunction(creatorFunction) { + +} + +WebsocketNode::~WebsocketNode() { + +} + +WebsocketHandler* WebsocketNode::newHandler() { + WebsocketHandler * handler = _creatorFunction(); + return handler; +} + +} /* namespace httpsserver */ diff --git a/src/http/WebsocketNode.hpp b/src/http/WebsocketNode.hpp new file mode 100644 index 0000000..4e6e26c --- /dev/null +++ b/src/http/WebsocketNode.hpp @@ -0,0 +1,25 @@ +#ifndef SRC_WEBSOCKETNODE_HPP_ +#define SRC_WEBSOCKETNODE_HPP_ + +#include + +#include "HTTPNode.hpp" +#include "WebsocketHandler.hpp" + +namespace httpsserver { + +typedef WebsocketHandler* (WebsocketHandlerCreator)(); + +class WebsocketNode : public HTTPNode { +public: + WebsocketNode(const std::string &path, const WebsocketHandlerCreator creatorFunction, const std::string &tag = ""); + virtual ~WebsocketNode(); + WebsocketHandler* newHandler(); + std::string getMethod() { return std::string("GET"); } +private: + const WebsocketHandlerCreator * _creatorFunction; +}; + +} /* namespace httpsserver */ + +#endif /* SRC_WEBSOCKET_HPP_ */ diff --git a/src/http/util.cpp b/src/http/util.cpp new file mode 100644 index 0000000..bce2dce --- /dev/null +++ b/src/http/util.cpp @@ -0,0 +1,98 @@ +#include "util.hpp" + +namespace httpsserver { + +uint32_t parseUInt(std::string const &s, uint32_t max) { + uint32_t i = 0; // value + + // Check sign + size_t x = 0; + if (s[0]=='+') { + x = 1; + } + + // We device max by 10, so we can check if we would exceed it by the next *10 multiplication + max/=10; + + // Convert by base 10 + for(; x < s.size(); x++) { + char c = s[x]; + if (i < max) { + if (c >= '0' && c<='9') { + i = i*10 + (c-'0'); + } else { + break; + } + } else { + return max; + } + } + + return i; +} + +int32_t parseInt(std::string const &s) { + uint32_t max = 0x7fffffff; + if (s[0]=='-') { + return -1 * parseUInt(s.substr(1,max)); + } + return parseUInt(s,max); +} + +std::string intToString(int i) { + if (i==0) { + return "0"; + } + // We need this much digits + int digits = ceil(log10(i+1)); + char c[digits+1]; + c[digits] = '\0'; + + for(int x = digits-1; x >= 0; x--) { + char v = (i%10); + c[x] = '0' + v; + i = (i-v)/10; + } + + return std::string(c); +} + +} + +std::string urlDecode(std::string input) { + std::size_t idxReplaced = 0; + // First replace + by space + std::size_t idxFound = input.find('+'); + while (idxFound != std::string::npos) { + input.replace(idxFound, 1, 1, ' '); + idxFound = input.find('+', idxFound + 1); + } + // Now replace percent-escapes + idxFound = input.find('%'); + while (idxFound != std::string::npos) { + if (idxFound <= input.length() + 3) { + char hex[2] = { input[idxFound+1], input[idxFound+2] }; + byte val = 0; + for(int n = 0; n < sizeof(hex); n++) { + val <<= 4; + if ('0' <= hex[n] && hex[n] <= '9') { + val += hex[n]-'0'; + } + else if ('A' <= hex[n] && hex[n] <= 'F') { + val += hex[n]-'A'+10; + } + else if ('a' <= hex[n] && hex[n] <= 'f') { + val += hex[n]-'a'+10; + } + else { + goto skipChar; + } + } + input.replace(idxFound, 3, 1, (char)val); + } + skipChar: + idxReplaced = idxFound + 1; + idxFound = input.find('%', idxReplaced); + } + return input; +} diff --git a/src/http/util.hpp b/src/http/util.hpp new file mode 100644 index 0000000..07b859d --- /dev/null +++ b/src/http/util.hpp @@ -0,0 +1,35 @@ +#ifndef SRC_UTIL_HPP_ +#define SRC_UTIL_HPP_ + +#include + +#include +#include + +namespace httpsserver { + +/** + * \brief **Utility function**: Parse an unsigned integer from a string + * + * The second parameter can be used to define the maximum value that is acceptable + */ +uint32_t parseUInt(std::string const &s, uint32_t max = 0xffffffff); + +/** + * \brief **Utility function**: Parse a signed integer from a string + */ +int32_t parseInt(std::string const &s); + +/** + * \brief **Utility function**: Transform an int to a std::string + */ +std::string intToString(int i); + +} + +/** + * \brief **Utility function**: Removes URL encoding from the string (e.g. %20 -> space) + */ +std::string urlDecode(std::string input); + +#endif /* SRC_UTIL_HPP_ */ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a80da65 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,89 @@ +#include +#include "web/web.h" +#include "config.h" + +#include + +Adafruit_Si4713* radio; + +void do_i2c_scan() { + Serial.println(); + Serial.println("I2C scanner. Scanning ..."); + byte count = 0; + + Wire.begin(); + for (byte i = 8; i < 120; i++) { + Wire.beginTransmission(i); // Begin I2C transmission Address (i) + if (Wire.endTransmission() == 0) { + // Receive 0 = success (ACK response) + Serial.print("Found address: "); + Serial.print(i, DEC); + Serial.print(" (0x"); + Serial.print(i, HEX); // PCF8574 7 bit address + Serial.println(")"); + count++; + } + } + Serial.print("Found "); + Serial.print(count, DEC); // numbers of devices + Serial.println(" device(s)."); +} + + +[[noreturn]] void application() { + radio = new Adafruit_Si4713(FM_RST_PIN); + + IF_DEBUG { + Serial.begin(115200); + } + + Serial.println("Adafruit Radio - Si4713 Test"); + + if (!radio->begin()) { // begin with address 0x63 (CS high default) + Serial.println("Couldn't find radio?"); + while (1); + } + + // Uncomment to scan power of entire range from 87.5 to 108.0 MHz + /* + for (uint16_t f = 8750; f<10800; f+=10) { + radio.readTuneMeasure(f); + Serial.print("Measuring "); Serial.print(f); Serial.print("..."); + radio.readTuneStatus(); + Serial.println(radio.currNoiseLevel); + } + */ + + Serial.print("\nSet TX power"); + radio->setTXpower(114); // dBuV, 88-115 max + + Serial.print("\nTuning into freq"); + radio->tuneFM(10740); // 100.0 mhz + + // This will tell you the status in case you want to read it from the chip + radio->readTuneStatus(); + Serial.print("\tCurr freq: "); + Serial.println(radio->currFreq); + Serial.print("\tCurr freqdBuV:"); + Serial.println(radio->currdBuV); + Serial.print("\tCurr ANTcap:"); + Serial.println(radio->currAntCap); + + // begin the RDS/RDBS transmission + radio->beginRDS(); + radio->setRDSstation((char*)"FUCK YOU!1!"); + radio->setRDSbuffer( (char*)"Fisting is 300 bucks"); + + Serial.println("RDS on!"); + + webThread(); +} + + +// Остатки ардуиновского мусора + +void setup() { + application(); +// fuck(); +} +void loop() { /* делаем ровно них*я */ } \ No newline at end of file diff --git a/src/web/web.cpp b/src/web/web.cpp new file mode 100644 index 0000000..460a1a3 --- /dev/null +++ b/src/web/web.cpp @@ -0,0 +1,349 @@ +// +// Created by vlad on 18.02.2021. +// + +#include +#include "web.h" + +#include "config.h" + +#include +#include + +using namespace httpsserver; + + +static void getHTML(HTTPRequest * req, HTTPResponse * res) { + static char html[] = "\n" + "\n" + "\n" + " \n" + " \n" + " Esp32 FM pirate\n" + " \n" + "\n" + "\n" + "
\n" + "

Lamps4 by Vlados31 & ESP32

\n" + "
\n" + "
\n" + "\n" + "
\n" + " \n" + " \n" + "
\n" + "\n" + "
\n" + " \n" + " \n" + "
\n" + "\n" + "
\n" + " \n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + "
\n" + "\n" + ""; + // ответим + req->discardRequestBody(); + res->setStatusCode(200); + res->setHeader("Content-Type", "text/html; charset=utf-8"); + res->setHeader("Content-Length", intToString(sizeof(html) - 1)); + // отправим страничку + +// res->printf(html, pwm_values[0], pwm_values[1], pwm_values[2], pwm_values[3]); + res->write((const uint8_t*)html, sizeof(html) - 1); +} + +static void getFavicon(HTTPRequest * req, HTTPResponse * res) { + static const uint8_t favicon_data[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x08, 0x02, 0x00, 0x00, 0x00, 0xFC, 0x18, 0xED, + 0xA3, 0x00, 0x00, 0x01, 0x84, 0x69, 0x43, 0x43, 0x50, 0x49, 0x43, 0x43, 0x20, 0x70, 0x72, 0x6F, + 0x66, 0x69, 0x6C, 0x65, 0x00, 0x00, 0x28, 0x91, 0x7D, 0x91, 0x3D, 0x48, 0xC3, 0x40, 0x18, 0x86, + 0xDF, 0xA6, 0x4A, 0x8B, 0x54, 0x1C, 0xEC, 0x20, 0x22, 0x92, 0xA1, 0x3A, 0x59, 0x10, 0x15, 0x71, + 0xD4, 0x2A, 0x14, 0xA1, 0x42, 0xA8, 0x15, 0x5A, 0x75, 0x30, 0xB9, 0xF4, 0x0F, 0x9A, 0x34, 0x24, + 0x29, 0x2E, 0x8E, 0x82, 0x6B, 0xC1, 0xC1, 0x9F, 0xC5, 0xAA, 0x83, 0x8B, 0xB3, 0xAE, 0x0E, 0xAE, + 0x82, 0x20, 0xF8, 0x03, 0xE2, 0xE8, 0xE4, 0xA4, 0xE8, 0x22, 0x25, 0x7E, 0x97, 0x14, 0x5A, 0xC4, + 0x78, 0xC7, 0x71, 0x0F, 0xEF, 0x7D, 0xEF, 0xCB, 0xDD, 0x77, 0x80, 0xD0, 0xA8, 0x30, 0xCD, 0xEA, + 0x1A, 0x07, 0x34, 0xDD, 0x36, 0xD3, 0xC9, 0x84, 0x98, 0xCD, 0xAD, 0x8A, 0xA1, 0x57, 0x04, 0x11, + 0x42, 0x98, 0xE6, 0xB0, 0xCC, 0x2C, 0x63, 0x4E, 0x92, 0x52, 0xF0, 0x1D, 0x5F, 0xF7, 0x08, 0xF0, + 0xFD, 0x2E, 0xCE, 0xB3, 0xFC, 0xEB, 0xFE, 0x1C, 0xBD, 0x6A, 0xDE, 0x62, 0x40, 0x40, 0x24, 0x9E, + 0x65, 0x86, 0x69, 0x13, 0x6F, 0x10, 0x4F, 0x6F, 0xDA, 0x06, 0xE7, 0x7D, 0xE2, 0x28, 0x2B, 0xC9, + 0x2A, 0xF1, 0x39, 0xF1, 0x98, 0x49, 0x17, 0x24, 0x7E, 0xE4, 0xBA, 0xE2, 0xF1, 0x1B, 0xE7, 0xA2, + 0xCB, 0x02, 0xCF, 0x8C, 0x9A, 0x99, 0xF4, 0x3C, 0x71, 0x94, 0x58, 0x2C, 0x76, 0xB0, 0xD2, 0xC1, + 0xAC, 0x64, 0x6A, 0xC4, 0x53, 0xC4, 0x31, 0x55, 0xD3, 0x29, 0x5F, 0xC8, 0x7A, 0xAC, 0x72, 0xDE, + 0xE2, 0xAC, 0x55, 0x6A, 0xAC, 0x75, 0x4F, 0xFE, 0xC2, 0x48, 0x5E, 0x5F, 0x59, 0xE6, 0x3A, 0xAD, + 0x21, 0x24, 0xB1, 0x88, 0x25, 0x48, 0x10, 0xA1, 0xA0, 0x86, 0x32, 0x2A, 0xB0, 0x11, 0xA7, 0x5D, + 0x27, 0xC5, 0x42, 0x9A, 0xCE, 0x13, 0x3E, 0xFE, 0x41, 0xD7, 0x2F, 0x91, 0x4B, 0x21, 0x57, 0x19, + 0x8C, 0x1C, 0x0B, 0xA8, 0x42, 0x83, 0xEC, 0xFA, 0xC1, 0xFF, 0xE0, 0x77, 0x6F, 0xAD, 0xC2, 0xE4, + 0x84, 0x97, 0x14, 0x49, 0x00, 0xDD, 0x2F, 0x8E, 0xF3, 0x31, 0x02, 0x84, 0x76, 0x81, 0x66, 0xDD, + 0x71, 0xBE, 0x8F, 0x1D, 0xA7, 0x79, 0x02, 0x04, 0x9F, 0x81, 0x2B, 0xBD, 0xED, 0xAF, 0x36, 0x80, + 0x99, 0x4F, 0xD2, 0xEB, 0x6D, 0x2D, 0x76, 0x04, 0xF4, 0x6D, 0x03, 0x17, 0xD7, 0x6D, 0x4D, 0xD9, + 0x03, 0x2E, 0x77, 0x80, 0x81, 0x27, 0x43, 0x36, 0x65, 0x57, 0x0A, 0xD2, 0x12, 0x0A, 0x05, 0xE0, + 0xFD, 0x8C, 0xBE, 0x29, 0x07, 0xF4, 0xDF, 0x02, 0x3D, 0x6B, 0x5E, 0xDF, 0x5A, 0xE7, 0x38, 0x7D, + 0x00, 0x32, 0xD4, 0xAB, 0xD4, 0x0D, 0x70, 0x70, 0x08, 0x8C, 0x16, 0x29, 0x7B, 0xDD, 0xE7, 0xDD, + 0xE1, 0xCE, 0xBE, 0xFD, 0x5B, 0xD3, 0xEA, 0xDF, 0x0F, 0xFA, 0xE8, 0x72, 0x77, 0xE0, 0x23, 0xEF, + 0x60, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x2E, 0x23, 0x00, 0x00, 0x2E, + 0x23, 0x01, 0x78, 0xA5, 0x3F, 0x76, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4D, 0x45, 0x07, 0xE6, + 0x03, 0x07, 0x08, 0x0C, 0x1F, 0xF9, 0x10, 0x03, 0x17, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, + 0x74, 0x43, 0x6F, 0x6D, 0x6D, 0x65, 0x6E, 0x74, 0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x57, 0x81, 0x0E, 0x17, 0x00, 0x00, + 0x00, 0x96, 0x49, 0x44, 0x41, 0x54, 0x48, 0xC7, 0xED, 0x55, 0x51, 0x0E, 0x80, 0x20, 0x08, 0x85, + 0xD6, 0x09, 0xCC, 0xEB, 0xD8, 0x95, 0xBB, 0x43, 0x5D, 0xA7, 0x3A, 0x82, 0xF4, 0xE9, 0x40, 0xCD, + 0x5C, 0x6B, 0xD3, 0x06, 0x5F, 0x6C, 0x30, 0x71, 0x3C, 0xDE, 0x7B, 0x48, 0xF0, 0x6D, 0x0C, 0xD0, + 0xFB, 0x80, 0x31, 0xA4, 0x3B, 0x2B, 0xA0, 0x95, 0xAD, 0xC4, 0x3B, 0x90, 0xD8, 0xE7, 0xE8, 0x30, + 0xAC, 0xDB, 0xFE, 0x66, 0x45, 0xFD, 0x0F, 0x40, 0x0A, 0xD9, 0xC9, 0x2B, 0x26, 0x02, 0xD9, 0xB1, + 0x3A, 0x6C, 0x37, 0xEF, 0x92, 0x62, 0xD0, 0x22, 0x93, 0x09, 0x3D, 0x2F, 0x39, 0x8E, 0xDA, 0x1A, + 0xDD, 0x07, 0x56, 0x4A, 0x45, 0x65, 0x88, 0xA3, 0x02, 0x58, 0x20, 0xA5, 0x15, 0x0A, 0x72, 0x05, + 0x06, 0x08, 0xF6, 0x09, 0x33, 0x5F, 0xF8, 0x41, 0x41, 0x53, 0xE6, 0x82, 0x4E, 0x08, 0x7B, 0x40, + 0x65, 0x72, 0x8B, 0x52, 0x21, 0x4C, 0x7F, 0x12, 0xF6, 0xE0, 0x93, 0x44, 0xCD, 0x9A, 0xBE, 0x62, + 0xD0, 0xA2, 0xE9, 0xEB, 0x8A, 0x32, 0x71, 0x01, 0xBC, 0x3B, 0x1E, 0x5A, 0xC9, 0x99, 0x48, 0x22, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, + }; + // ответим + req->discardRequestBody(); + res->setStatusCode(200); + res->setHeader("Content-Type", "image/png"); + res->setHeader("Content-Length", intToString(sizeof(favicon_data))); + // отправим иконку + res->write(favicon_data, sizeof(favicon_data)); +} + +uint32_t output_frq = 8750; +std::string output_rds = "FUCK YOU!1!"; +bool output_enable = false; + +static void setOutputData(HTTPRequest * req, HTTPResponse * res) { + auto p = req->getParams(); + if (p != nullptr) { + // сначала частота + std::string param; + p->getQueryParameter("frq", param); + if (!param.empty()) { + auto val = parseInt(param); + val = min(10800, max(8750, val)); + output_frq = val; + radio->tuneFM(val); + // TODO прикрутить запись частоты в передатчик + IF_DEBUG Serial.printf("[web] _set_frq: set frq=%d\n", val); + } + + // теперь RDS + param.clear(); + p->getQueryParameter("rds", param); + if (!param.empty()) { + output_rds = param; + // TODO прикрутить запись rds в передатчик + IF_DEBUG Serial.printf("[web] _set_rds: set rds=\"%s\"\n", param.c_str()); + } + + // теперь включен/выключен + param.clear(); + p->getQueryParameter("fmen", param); + if (!param.empty()) { + bool val = parseInt(param) != 0; + output_enable = val; + // TODO прикрутить запись флага в передатчик + IF_DEBUG Serial.printf("[web] _set_fmen: set fmen=%d\n", val); + } + } else { + IF_DEBUG Serial.printf("[web] setOutputData: empty params!\n"); + } + + // сделаем json ответ со всеми интерфейсами + static char response_buffer[1024]; + sprintf(response_buffer, R"({"frq": %d, "rds": "%s", "fmen": %d})", output_frq, output_rds.c_str(), output_enable); + + // отправим json ответ + res->setStatusCode(200); + res->setHeader("Content-Type", "application/json; charset=utf-8"); + auto len = (uint32_t)strlen(response_buffer); + res->setHeader("Content-Length", intToString((int32_t)len)); + + res->write((const uint8_t*)response_buffer, len); + + IF_DEBUG Serial.printf("[web] send json: %s\n", response_buffer); +} + + +static void handle404(HTTPRequest * req, HTTPResponse * res) { + // Discard request body, if we received any + // We do this, as this is the default node and may also server POST/PUT requests + req->discardRequestBody(); + IF_DEBUG Serial.printf("[handle404] Resource '%s' not found\n", req->getRequestString().c_str()); + + // Set the response status + res->setStatusCode(404); + res->setStatusText("Not Found"); + + // Set content type of the response + res->setHeader("Content-Type", "text/html"); + + // Write a tiny HTML page + res->printf("\n" + "" + "" + "Not Found

404 Not Found

" + "

Вы запросили несуществующую страницу...

"); +} + +[[noreturn]] void webThread() { + IF_DEBUG { + Serial.printf("******** START SERVER ********\n"); + } + +// WiFiClass::mode(WIFI_MODE_STA); + WiFi.softAP(WIFI_SSID, WIFI_PSK); +// WiFi.begin(WIFI_SSID, WIFI_PSK); + Serial.printf("[webThread] IP address: %s\n", WiFi.softAPIP().toString().c_str()); +// Serial.printf("[webThread] IP address: %s\n", WiFi.localIP().toString().c_str()); + + HTTPServer server = HTTPServer(); + // Add all nodes to the server so they become accessible: + server.registerNode(new ResourceNode("/", "GET", &getHTML)); + server.registerNode(new ResourceNode("/favicon.ico", "GET", &getFavicon)); + server.registerNode(new ResourceNode("/", "POST", &setOutputData)); + server.setDefaultNode(new ResourceNode("", "GET", &handle404)); + + server.start(); + Serial.printf("[webThread] Server is running\n"); + + for (;;) { + // This call will let the server do its work + server.loop(); + // Other code would go here... + vTaskDelay(1); + } +} + diff --git a/src/web/web.h b/src/web/web.h new file mode 100644 index 0000000..2cf3c1f --- /dev/null +++ b/src/web/web.h @@ -0,0 +1,8 @@ +#ifndef GBO_WORK0001_ESP32_WEB_H +#define GBO_WORK0001_ESP32_WEB_H + +#include + +[[noreturn]] void webThread(); + +#endif //GBO_WORK0001_ESP32_WEB_H