/** * @file tcpSockets.hpp * @author Gustice * @brief Function wrapper for ESP-IDF socket implementation * * @copyright Copyright (c) 2024 */ #pragma once #include "esp_log.h" #include "etherCommon.hpp" #include "utils.hpp" #include #include /** * @brief TCP-Socket wrapper for ESP-IDF socket implementation */ class TcpSocket { public: /// @brief Constructor /// @note Socket not active until call of attach method /// @param addr Server/Client address TcpSocket(const IpAdr &addr) : Local(addr) { _listenSockId = socket(addr.ProtocolFamily, SOCK_STREAM, addr.IpProtocol); if (_listenSockId < 0) { ESP_LOGE("TCP", "Unable to create socket: errno %d", errno); throw CoreException("Unable to create socket"); } if (addr.ProtocolFamily == IPPROTO_IP) { int opt = 1; setsockopt(_listenSockId, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); } if constexpr (USE_IPV4 && USE_IPV6) { int opt = 1; // Note that by default IPV6 binds to both protocols, it is must be disabled // if both protocols used at the same time (used in CI) setsockopt(_listenSockId, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); } ESP_LOGI("TCP", "Socket created"); } /// @brief Destructor ~TcpSocket() { close(_listenSockId); } /// @brief Attach to configured socket /// @note Blocks until successful or failed /// @return true if successful bool bindTo() { int err = bind(_listenSockId, Local.asSockAddrPtr(), sizeof(sockaddr_storage)); if (err != 0) { ESP_LOGE("TCP", "Socket unable to bind: errno %d", errno); ESP_LOGE("TCP", "IPPROTO: %d", Local.ProtocolFamily); throw CoreException("Socket unable to bind"); } ESP_LOGI("TCP", "Socket bound, port %d", Local.getPort()); err = listen(_listenSockId, 1); if (err != 0) { ESP_LOGE("TCP", "Error occurred during listen: errno %d", errno); throw CoreException("Error occurred during listen"); } return true; } /// @brief Get socket id for stream-operations /// @return Id of socket int getSocketId() { return _listenSockId; } /// @brief Address of this station (server perspective) const IpAdr &Local; private: int _listenSockId; }; /** * @brief TCP session */ class TcpSession : public SocketBase, public CharStream { public: /** * @brief Configuration structure for Session */ struct Config { int keepAlive; int keepIdle; int keepInterval; int keepCount; }; /// @brief Constructor /// @note Constructor blocks /// @param config Configuration for session /// @param listener reference to socket TcpSession(Config &config, TcpSocket &listener) : _other(IPADDR_ANY, 0) { socklen_t addr_len = _other.Size; _socket = accept(listener.getSocketId(), _other.asSockAddrPtr(), &addr_len); if (_socket < 0) { ESP_LOGE("TCP", "Unable to accept connection: errno %d", errno); active = false; throw CoreException("Unable to accept connection"); } // Set tcp keepalive option setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &config.keepAlive, sizeof(int)); setsockopt(_socket, IPPROTO_TCP, TCP_KEEPIDLE, &config.keepIdle, sizeof(int)); setsockopt(_socket, IPPROTO_TCP, TCP_KEEPINTVL, &config.keepInterval, sizeof(int)); setsockopt(_socket, IPPROTO_TCP, TCP_KEEPCNT, &config.keepCount, sizeof(int)); std::string addrStr; if (_other.ProtocolFamily == listener.Local.ProtocolFamily) { if (_other.getProtocolFamily() == PF_INET) { addrStr = _other.getIp(); // } else if (_other.addrStore.ss_family == PF_INET6) { // inet6_ntoa_r(((struct sockaddr_in6 *)&_other.addrStore)->sin6_addr, addrStr, sizeof(addrStr) - 1); } else { throw CoreException("Unknown case"); } } else { ESP_LOGW("TCP", "Wrong IP-version request"); active = false; } ESP_LOGI("TCP", "Socket accepted ip address: %s", addrStr.c_str()); active = true; } /// @brief Destructor ~TcpSession() { if (_socket < 0) return; ESP_LOGD("TCP", "Tearing down socket"); shutdown(_socket, 0); } /// @brief Get IP of remote /// @return IpAdr getSource() { return _other; } /// @brief Read string /// @note This call blocks until data is received /// @return read string std::string read() override { auto data = readRaw(); if (data.size() == 0) { return ""; } std::string ret((const char *)&data.data()[0]); return ret; } /// @brief Write character to socket /// @param b character to write void write(char c) override { if (!isActive()) { return; } int written = send(_socket, &c, 1, 0); if (written < 0) { ESP_LOGE("TCP", "Error occurred during sending: errno %d", errno); } } /// @brief Write string to specific socket /// @param message payload /// @param dest Destination for message void write(std::string message) override { if (!isActive()) { return; } int written = send(_socket, message.c_str(), message.size(), 0); if (written < 0) { ESP_LOGE("TCP", "Error occurred during sending: errno %d", errno); } } IpAdr getOther() { return _other; } protected: int _socket = 0; IpAdr _other; std::vector readRaw() { uint8_t data[128]; int len = recv(_socket, data, sizeof(data) - 1, 0); if (len < 0) { ESP_LOGE("TCP", "Error occurred during receiving: errno %d", errno); active = false; return {}; } data[len] = 0; if (len == 0) { ESP_LOGW("TCP", "Connection closed"); active = false; return {}; } ESP_LOGD("TCP", "Received %d bytes: %s", len, data); std::vector vec(data, data + len + 1); return vec; } }; constexpr TcpSession::Config DefaultTcpConfig{ .keepAlive = 1, .keepIdle = 5, .keepInterval = 5, .keepCount = 3, }; constexpr uint16_t TelnetPort = 23; /** * @brief TCP-Socket for Telnet-Applications */ class TelnetServer { /** * @brief Specialization of TCP-Session for Telnet */ class TelnetSession : public TcpSession { public: // Constructor TelnetSession(Config &config, TcpSocket &listener) : TcpSession(config, listener) { } /// @brief Read string /// @note This call blocks until data is received /// @return read string std::string read() override { auto data = readRaw(); if (data.size() == 0) { return ""; } data.pop_back(); // 0-termination is not needed here int i = 0; while (data[i] == 0xFF) { // while telnet command i += 3; // skip this command } if (i == data.size()) { // only configuration send(_socket, data.data(), data.size(), 0); return ""; } std::string ret((const char *)&data[i]); return ret; } }; public: /// @brief Constructor /// @param addrFamily IPV4 or IPV6 TelnetServer(int addrFamily) : Local(addrFamily == AF_INET ? IpAdr(INADDR_ANY, TelnetPort) : IpAdr(TelnetPort)), Config(DefaultTcpConfig) { } /** * @brief Start server * * @param initCall Callback for shell init * @param cyclicCall Callback for cyclic polling */ void run(std::function initCall, std::function cyclicCall) { TcpSocket socket(Local); socket.bindTo(); while (true) { // Run telnet-session as long socket is active else restart socket ESP_LOGI("Telnet", "Socket waiting for connect"); TelnetSession session(Config, socket); initCall(session); while (session.isActive()) { cyclicCall(session); } } } private: IpAdr Local; TcpSession::Config Config; };