// Copyright (c) 2021 hiimgoodpack // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #ifndef BRAINLET_H #define BRAINLET_H #include #include // std::unique_ptr #include #include #include "websocket.h" namespace Brainlet { enum Opcode { HELLO = 0, YOO = 1, YOO_ACK = 2, ACTION_CREATE_MESSAGE = 3, EVENT_NEW_MESSAGE = 4, ACTION_UPDATE_STATUS = 5, EVENT_CHANNEL_MEMBERS = 6, EVENT_CHANNEL_MEMBER_UPDATE = 7, }; const std::size_t minChannelNameLength = 3; const std::size_t maxChannelNameLength = 32; const std::size_t idLength = 24; typedef struct { std::string id; std::string name; } Channel; typedef struct { std::string authorId; std::string message; } Message; typedef struct { std::string id; std::string name; std::string color; ///< Color represented in #rrggbb } User; class Client : public User { private: enum State { NOT_CONNECTED, WAIT_HELLO, WAIT_ACK, READY }; std::string domain; std::string token; std::unique_ptr socket = nullptr; State state = NOT_CONNECTED; static void onReceive(Client* client, std::string buffer); void escapeJSON(std::string& string); ///< Escapes a string to a JSON value void escapeOutput(std::string& string); ///< Escapes a string to output safely public: std::function onNewChannel = nullptr; std::function onNewMessage = nullptr; Client(const std::string& domain); void processNextEvent(); void login(std::string name, std::string password); void sendMessage(std::string message, std::string channelId); }; } #endif #if defined(BRAINLET_IMPLEMENTATION) && !defined(BRAINLET_IMPLEMENTED) #include "http.h" #include // std::strncmp #include #include #include // std::istringstream, std::ostringstream #include // std::snprintf #include #include #include #include #include // std;:strtol #include // std::for_each #include // std::move #include // strerror #include namespace Brainlet { Client::Client(const std::string& domain) : domain(domain) {} void Client::escapeJSON(std::string& string) { std::string result; result.reserve(string.size()); std::for_each(string.begin(), string.end(), [&](const char character) { switch (character) { case '"': result.append("\\\""); break; case '\\': result.append("\\\\"); break; case '\b': result.append("\\b"); break; case '\f': result.append("\\f"); break; case '\n': result.append("\\n"); break; case '\r': result.append("\\r"); break; case '\t': result.append("\\t"); break; default: result.push_back(character); break; } }); string = std::move(result); } void Client::escapeOutput(std::string& string) { std::string result; result.reserve(string.size()); std::for_each(string.begin(), string.end(), [&](const char character) { switch (character) { case '\b': case '\x1f': result.push_back('?'); break; default: result.push_back(character); break; } }); string = std::move(result); } void Client::processNextEvent() { socket->run_until( // Stop in 250 microseconds std::chrono::system_clock::now() + std::chrono::microseconds(250) ); } void Client::login(std::string name, std::string password) { { // Escape the name and password auto escape = [](std::string& string) { std::string result; result.reserve(string.size()); std::for_each(string.begin(), string.end(), [&](const char character) { switch (character) { case '%': result.append("%25"); break; case '&': result.append("%26"); break; default: result.push_back(character); break; } }); string = std::move(result); }; escape(name); escape(password); } Http::Request request("http://" + domain + "/api/v1/users/token/create"); const std::string postFields = "username="+name+"&password="+password+"&alsoSetCookie=true"; request.setPostFields(postFields); request.perform(); Json::Value value; std::istringstream(request.buffer) >> value; bool didError = value.get("error", true).asBool(); if (didError) { // TODO: Use exception std::string error = value.get("message", "Server did not specify error").asString(); escapeOutput(error); std::cerr << "Error while logging in: " << error << "\n"; return; } token = value.get("token", "Unknown token").asString(); id = value["user"].get("id", "0").asString(); color = value["user"].get("color", "#ffffff").asString(); socket.reset(new WebSocket("ws://" + domain + "/gateway")); state = WAIT_HELLO; socket->onReceive = std::bind(onReceive, this, std::placeholders::_1); } void Client::onReceive(Client* client, std::string buffer) { Json::Value data; Opcode opcode; { char* characterAfterType = nullptr; errno = 0; opcode = (Opcode)std::strtol(buffer.c_str(), &characterAfterType, 10); if (errno != 0) { // TODO: Use exception std::cerr << "Failed to get opcode: " << strerror(errno) << "\n"; return; } // Skip opcode number and ending @ unsigned int charactersToSkip = characterAfterType - buffer.c_str() + 1; std::istringstream stream(buffer); for (unsigned int i = 0; i < charactersToSkip; i++) { char _; stream >> _; } stream >> data; } switch (client->state) { case NOT_CONNECTED: throw std::logic_error("Buffer was received while client is not connected"); case WAIT_HELLO: { if (opcode != HELLO) { escapeOutput(buffer); std::cerr << "Failed authentication: Expected HELLO packet, received: " << buffer << "\n"; client->socket->close(); return; } client->state = WAIT_ACK; char bufferToSend[256]; std::snprintf(bufferToSend, sizeof(bufferToSend), R"(%i@{"token":"%s"})", YOO, client->token.c_str()); client->socket->send(bufferToSend); break; } case WAIT_ACK: { if (opcode != YOO_ACK) { escapeOutput(buffer); std::cerr << "Failed authentication: Expected YOO_ACK packet, received: " << buffer << "\n"; client->socket->close(); return; } client->state = READY; const Json::Value channels = data["channels"]; for (Json::ArrayIndex i = 0; i < channels.size(); ++i) { const Json::Value channelValue = channels[i]; const Channel channelObject = { .id = channelValue["_id"].asString(), .name = channelValue["title"].asString() }; if (client->onNewChannel) client->onNewChannel(channelObject); } break; } case READY: { switch (opcode) { case EVENT_NEW_MESSAGE: { Channel channel = { .id = data["channel"]["_id"].asString(), .name = data["channel"]["title"].asString() }; Message message = { .authorId = data["author"]["_id"].asString(), .message = data["content"].asString() }; if (client->onNewMessage) client->onNewMessage(message, channel); break; } default: { escapeOutput(buffer); std::cerr << "Unknown opcode " << opcode << ". Buffer " << buffer << "\n"; break; } } break; } } } void Client::sendMessage(std::string message, std::string channelId) { escapeJSON(message); std::ostringstream bufferToSend; bufferToSend << ACTION_CREATE_MESSAGE << "@" << R"({"content":")" << message << R"(","channel":{"_id":")" << channelId << R"("}})"; socket->send(bufferToSend.str()); } } #endif