310 lines
8.6 KiB
C++
310 lines
8.6 KiB
C++
/**
|
|
* @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 "coreExcept.hpp"
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <vector>
|
|
#include "simpleStreams.hpp"
|
|
|
|
/**
|
|
* @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<uint8_t> 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<uint8_t> 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<void(TcpSession &)> initCall, std::function<void(TcpSession &)> 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;
|
|
};
|