diff --git a/wish/cpp/.gitignore b/wish/cpp/.gitignore new file mode 100644 index 0000000..c5e9848 --- /dev/null +++ b/wish/cpp/.gitignore @@ -0,0 +1,30 @@ +_deps +build +*.log +*.a + +# CMake artifacts (in-source builds) +CMakeFiles +CMakeCache.txt +cmake_install.cmake +Makefile +CTestTestfile.cmake + +# Compiled Object files +*.o +*.obj + +# Dynamic Libraries +*.so +*.dylib +*.dll + +# Executables +wish-server +wish-client + +# Editor/IDE files +.vscode/ +.idea/ +*.swp +.DS_Store \ No newline at end of file diff --git a/wish/cpp/CMakeLists.txt b/wish/cpp/CMakeLists.txt new file mode 100644 index 0000000..7cf6102 --- /dev/null +++ b/wish/cpp/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.14) +project(wish_cpp) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) + +FetchContent_Declare( + wslay + GIT_REPOSITORY https://github.com/tatsuhiro-t/wslay.git + GIT_TAG master +) + +FetchContent_Declare( + libevent + GIT_REPOSITORY https://github.com/libevent/libevent.git + GIT_TAG master +) +# Disable OpenSSL and other heavy features for libevent to keep build fast/simple if possible +set(EVENT__DISABLE_OPENSSL ON CACHE BOOL "" FORCE) +set(EVENT__DISABLE_MBEDTLS ON CACHE BOOL "" FORCE) +set(EVENT__DISABLE_TESTS ON CACHE BOOL "" FORCE) +set(EVENT__DISABLE_REGRESS ON CACHE BOOL "" FORCE) +set(EVENT__DISABLE_SAMPLES ON CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(wslay libevent) + +include_directories(${wslay_SOURCE_DIR}/lib/includes) +include_directories(${libevent_SOURCE_DIR}/include) +include_directories(${libevent_BINARY_DIR}/include) +include_directories(src) + +add_library(wish_handler + src/wish_handler.cc + src/wish_handler.h +) +target_link_libraries(wish_handler wslay event) + +add_executable(wish-server examples/server.cc) +target_link_libraries(wish-server wish_handler) + +add_executable(wish-client examples/client.cc) +target_link_libraries(wish-client wish_handler) diff --git a/wish/cpp/examples/client.cc b/wish/cpp/examples/client.cc new file mode 100644 index 0000000..7502190 --- /dev/null +++ b/wish/cpp/examples/client.cc @@ -0,0 +1,59 @@ +#include +#include +#include + +#include +#include +#include + +#include "../src/wish_handler.h" + +int main() { + struct event_base *base = event_base_new(); + if (!base) { + std::cerr << "Could not initialize libevent!" << std::endl; + return 1; + } + + struct evdns_base *dns_base = evdns_base_new(base, 1); + if (!dns_base) { + std::cerr << "Could not initialize dns!" << std::endl; + return 1; + } + + struct bufferevent *bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE); + if (!bev) { + std::cerr << "Could not create bufferevent!" << std::endl; + return 1; + } + + if (bufferevent_socket_connect_hostname(bev, dns_base, AF_INET, "127.0.0.1", 8080) < 0) { + std::cerr << "Could not connect!" << std::endl; + return 1; + } + + // Handler manages its own lifecycle (deletes itself on close) + WishHandler* handler = new WishHandler(bev, false); // is_server = false + + handler->SetOnOpen([handler]() { + std::cout << "Connected and Handshake Complete!" << std::endl; + + handler->SendText("Hello WiSH Text!"); + handler->SendBinary("Hello WiSH Binary!"); + handler->SendMetadata(true, "Hello WiSH Metadata!"); + }); + + handler->SetOnMessage([](uint8_t opcode, const std::string& msg) { + std::cout << "Server says [opcode " << (int)opcode << "]: " << msg << std::endl; + }); + + handler->Start(); + + std::cout << "Client running..." << std::endl; + event_base_dispatch(base); + + evdns_base_free(dns_base, 0); + event_base_free(base); + + return 0; +} diff --git a/wish/cpp/examples/server.cc b/wish/cpp/examples/server.cc new file mode 100644 index 0000000..1f47ce8 --- /dev/null +++ b/wish/cpp/examples/server.cc @@ -0,0 +1,88 @@ +#include +#include +#include // Added for std::to_string + +#include +#include +#include +#include + +#include "../src/wish_handler.h" + +void accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd, + struct sockaddr *address, int socklen, void *ctx) { + + struct event_base *base = evconnlistener_get_base(listener); + struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); + + std::cout << "Client connected." << std::endl; + + // Handler manages its own lifecycle (deletes itself on close) + WishHandler* handler = new WishHandler(bev, true); + + handler->SetOnMessage([handler](uint8_t opcode, const std::string& msg) { + std::string type; + switch(opcode) { + case 1: type = "TEXT"; break; + case 2: type = "BINARY"; break; + case 3: type = "TEXT_METADATA"; break; + case 4: type = "BINARY_METADATA"; break; + default: type = "UNKNOWN(" + std::to_string(opcode) + ")"; break; + } + std::cout << "Received [" << type << "]: " << msg << std::endl; + + // Echo back + int res = 0; + if (opcode == 1) res = handler->SendText("Echo: " + msg); + else if (opcode == 2) res = handler->SendBinary("Echo: " + msg); + else if (opcode == 3 || opcode == 4) res = handler->SendMetadata(opcode == 3, "Echo: " + msg); + + if (res != 0) { + std::cerr << "Failed to send echo." << std::endl; + } + }); + + handler->Start(); +} + +void accept_error_cb(struct evconnlistener *listener, void *ctx) { + struct event_base *base = evconnlistener_get_base(listener); + int err = EVUTIL_SOCKET_ERROR(); + std::cerr << "Got an error " << err << " (" << evutil_socket_error_to_string(err) << ") on the listener. Shutting down." << std::endl; + event_base_loopexit(base, NULL); +} + +int main(int argc, char **argv) { + struct event_base *base; + struct evconnlistener *listener; + struct sockaddr_in sin; + int port = 8080; + + base = event_base_new(); + if (!base) { + std::cerr << "Could not initialize libevent!" << std::endl; + return 1; + } + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(0); + sin.sin_port = htons(port); + + listener = evconnlistener_new_bind(base, accept_conn_cb, NULL, + LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE, -1, + (struct sockaddr*)&sin, sizeof(sin)); + + if (!listener) { + std::cerr << "Could not create a listener!" << std::endl; + return 1; + } + + std::cout << "Server listening on port " << port << "..." << std::endl; + event_base_dispatch(base); + + evconnlistener_free(listener); + event_base_free(base); + + return 0; +} diff --git a/wish/cpp/src/wish_handler.cc b/wish/cpp/src/wish_handler.cc new file mode 100644 index 0000000..17d6e83 --- /dev/null +++ b/wish/cpp/src/wish_handler.cc @@ -0,0 +1,236 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "wish_handler.h" + +WishHandler::WishHandler(struct bufferevent* bev, bool is_server) + : bev_(bev), is_server_(is_server), ctx_(nullptr), state_(HANDSHAKE) { + + struct wslay_event_callbacks callbacks = { + RecvCallback, + SendCallback, + GenMaskCallback, + NULL, + NULL, + NULL, + OnMsgRecvCallback + }; + + if (is_server_) { + wslay_event_context_server_init(&ctx_, &callbacks, this); + } else { + wslay_event_context_client_init(&ctx_, &callbacks, this); + } +} + +WishHandler::~WishHandler() { + wslay_event_context_free(ctx_); + if (bev_) { + bufferevent_free(bev_); + } +} + +void WishHandler::Start() { + bufferevent_setcb(bev_, ReadCallback, NULL, EventCallback, this); + bufferevent_enable(bev_, EV_READ | EV_WRITE); + + if (!is_server_) { + SendHttpRequest(); + } +} + +void WishHandler::SetOnMessage(MessageCallback cb) { + on_message_ = cb; +} + +void WishHandler::SetOnOpen(OpenCallback cb) { + on_open_ = cb; +} + +ssize_t WishHandler::RecvCallback(wslay_event_context *ctx, uint8_t *buf, size_t len, int flags, void *user_data) { + WishHandler* handler = static_cast(user_data); + struct evbuffer *input = bufferevent_get_input(handler->bev_); + + size_t data_len = evbuffer_get_length(input); + if (data_len == 0) { + wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK); + return -1; + } + + size_t copy_len = std::min(len, data_len); + evbuffer_remove(input, buf, copy_len); + return copy_len; +} + +ssize_t WishHandler::SendCallback(wslay_event_context *ctx, const uint8_t *data, size_t len, int flags, void *user_data) { + WishHandler* handler = static_cast(user_data); + bufferevent_write(handler->bev_, data, len); + return len; +} + +int WishHandler::GenMaskCallback(wslay_event_context *ctx, uint8_t *buf, size_t len, void *user_data) { + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, 255); + for (size_t i = 0; i < len; ++i) { + buf[i] = dist(rng); + } + return 0; +} + +void WishHandler::OnMsgRecvCallback(wslay_event_context *ctx, const wslay_event_on_msg_recv_arg *arg, void *user_data) { + WishHandler* handler = static_cast(user_data); + if (handler->on_message_) { + std::string msg(reinterpret_cast(arg->msg), arg->msg_length); + handler->on_message_(arg->opcode, msg); + } +} + +void WishHandler::ReadCallback(struct bufferevent *bev, void *ctx) { + WishHandler* handler = static_cast(ctx); + + if (handler->state_ == HANDSHAKE) { + handler->HandleHandshake(); + } + + if (handler->state_ == OPEN) { + int err = wslay_event_recv(handler->ctx_); + if (err != 0) { + std::cerr << "wslay_event_recv failed: " << err << std::endl; + // Should we close? + } + } +} + +void WishHandler::EventCallback(struct bufferevent *bev, short events, void *ctx) { + if (events & BEV_EVENT_ERROR) { + std::cerr << "Error on socket: " << evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()) << std::endl; + } + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + // Connection closed + std::cout << "Connection closed." << std::endl; + WishHandler* handler = static_cast(ctx); + delete handler; + } +} + +int WishHandler::SendMessage(uint8_t opcode, const std::string& msg) { + if (state_ != OPEN) return -1; + + struct wslay_event_msg msg_frame = { + opcode, + reinterpret_cast(msg.c_str()), + msg.length() + }; + // Queue msg + int res = wslay_event_queue_msg(ctx_, &msg_frame); + if (res != 0) return res; + + // Force send + return wslay_event_send(ctx_); +} + +int WishHandler::SendText(const std::string& msg) { + return SendMessage(1, msg); +} + +int WishHandler::SendBinary(const std::string& msg) { + return SendMessage(2, msg); +} + +int WishHandler::SendMetadata(bool is_text, const std::string& msg) { + return SendMessage(is_text ? 3 : 4, msg); +} + +// ---------------- Handshake Logic ---------------- + +void WishHandler::HandleHandshake() { + if (is_server_) { + if (ReadHttpRequest()) { + SendHttpResponse("200 OK", "application/web-stream"); + state_ = OPEN; + if (on_open_) on_open_(); + } + } else { + // Client waits for response + if (ReadHttpResponse()) { + state_ = OPEN; + // Maybe trigger some on_open callback? + std::cout << "Handshake complete!" << std::endl; + if (on_open_) on_open_(); + } + } +} + +bool WishHandler::ReadHttpRequest() { + struct evbuffer *input = bufferevent_get_input(bev_); + size_t len = evbuffer_get_length(input); + if (len == 0) return false; + + // Search for \r\n\r\n + struct evbuffer_ptr ptr = evbuffer_search(input, "\r\n\r\n", 4, NULL); + if (ptr.pos == -1) return false; // Not full headers yet + + // Read up to the end of headers + size_t header_len = ptr.pos + 4; + char* headers = new char[header_len + 1]; + evbuffer_remove(input, headers, header_len); + headers[header_len] = '\0'; + std::string data(headers); + delete[] headers; + + // Check for WiSH specific header + if (data.find("Content-Type: application/web-stream") == std::string::npos && + data.find("content-type: application/web-stream") == std::string::npos) { + std::cerr << "Missing WiSH Content-Type!" << std::endl; + return false; + } + return true; +} + +void WishHandler::SendHttpResponse(const std::string& status, const std::string& content_type) { + std::stringstream ss; + ss << "HTTP/1.1 " << status << "\r\n"; + ss << "Content-Type: " << content_type << "\r\n"; + ss << "\r\n"; // End of headers + std::string data = ss.str(); + bufferevent_write(bev_, data.c_str(), data.length()); +} + +void WishHandler::SendHttpRequest() { + std::stringstream ss; + ss << "POST / HTTP/1.1\r\n"; + ss << "Host: localhost\r\n"; + ss << "Content-Type: application/web-stream\r\n"; + ss << "\r\n"; + std::string data = ss.str(); + bufferevent_write(bev_, data.c_str(), data.length()); +} + +bool WishHandler::ReadHttpResponse() { + struct evbuffer *input = bufferevent_get_input(bev_); + + // Search for \r\n\r\n + struct evbuffer_ptr ptr = evbuffer_search(input, "\r\n\r\n", 4, NULL); + if (ptr.pos == -1) return false; + + size_t header_len = ptr.pos + 4; + char* headers = new char[header_len + 1]; + evbuffer_remove(input, headers, header_len); + headers[header_len] = '\0'; + std::string data(headers); + delete[] headers; + + if (data.find("200 OK") == std::string::npos) { + std::cerr << "Bad Handy handshake response: " << data << std::endl; + return false; + } + return true; +} diff --git a/wish/cpp/src/wish_handler.h b/wish/cpp/src/wish_handler.h new file mode 100644 index 0000000..bde6077 --- /dev/null +++ b/wish/cpp/src/wish_handler.h @@ -0,0 +1,73 @@ +#ifndef WISH_CPP_SRC_WISH_HANDLER_H_ +#define WISH_CPP_SRC_WISH_HANDLER_H_ + +#include +#include +#include +#include + +#include +#include + +// wslay forward decl +extern "C" { +struct wslay_event_context; +struct wslay_event_on_msg_recv_arg; +} + +class WishHandler { + public: + using MessageCallback = std::function; + using OpenCallback = std::function; + + // Constructor takes an already created bufferevent + WishHandler(struct bufferevent* bev, bool is_server); + ~WishHandler(); + + // Start the handler (sets up callbacks and enables events) + void Start(); + + // Send methods + int SendText(const std::string& msg); + int SendBinary(const std::string& msg); + int SendMetadata(bool is_text, const std::string& msg); + + void SetOnMessage(MessageCallback cb); + void SetOnOpen(OpenCallback cb); + + private: + struct bufferevent* bev_; + bool is_server_; + struct wslay_event_context* ctx_; + MessageCallback on_message_; + OpenCallback on_open_; + + enum State { + HANDSHAKE, + OPEN, + CLOSED + }; + State state_; + + // wslay callbacks + static ssize_t RecvCallback(struct wslay_event_context *ctx, uint8_t *buf, size_t len, int flags, void *user_data); + static ssize_t SendCallback(struct wslay_event_context *ctx, const uint8_t *data, size_t len, int flags, void *user_data); + static int GenMaskCallback(struct wslay_event_context *ctx, uint8_t *buf, size_t len, void *user_data); + static void OnMsgRecvCallback(struct wslay_event_context *ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data); + + // libevent callbacks + static void ReadCallback(struct bufferevent *bev, void *ctx); + // We might not need a write callback unless we want flow control + // static void WriteCallback(struct bufferevent *bev, void *ctx); + static void EventCallback(struct bufferevent *bev, short events, void *ctx); + + void HandleHandshake(); + bool ReadHttpRequest(); + bool ReadHttpResponse(); + void SendHttpResponse(const std::string& status, const std::string& content_type); + void SendHttpRequest(); + + int SendMessage(uint8_t opcode, const std::string& msg); +}; + +#endif // WISH_CPP_SRC_WISH_HANDLER_H_