From f64cdc6ad0bdd6567522692ee6408d4fa4a3933d Mon Sep 17 00:00:00 2001 From: Ravin Perera <33562092+ravinsp@users.noreply.github.com> Date: Fri, 11 Oct 2019 17:29:45 +0530 Subject: [PATCH] Implemented user connection challenge handshake (#20) Implemented user connection challenge handshake. Optimized user challenge message processing. --- CMakeLists.txt | 2 + README.md | 56 ++++----- examples/hpclient/.gitignore | 2 + examples/hpclient/client.js | 104 ++++++++++++++++ examples/hpclient/package-lock.json | 60 ++++++++++ examples/hpclient/package.json | 7 ++ src/conf.cpp | 9 +- src/crypto.cpp | 8 +- src/main.cpp | 7 +- src/proc.cpp | 10 +- src/sock/socket_session.cpp | 25 +++- src/sock/socket_session.hpp | 17 ++- src/usr/user_session_handler.cpp | 127 ++++++++++++++++++++ src/usr/user_session_handler.hpp | 18 +++ src/usr/usr.cpp | 177 ++++++++++++++++++---------- src/usr/usr.hpp | 25 ++-- src/util.cpp | 4 +- src/util.hpp | 17 ++- 18 files changed, 547 insertions(+), 128 deletions(-) create mode 100644 examples/hpclient/.gitignore create mode 100644 examples/hpclient/client.js create mode 100644 examples/hpclient/package-lock.json create mode 100644 examples/hpclient/package.json create mode 100644 src/usr/user_session_handler.cpp create mode 100644 src/usr/user_session_handler.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c90de44..33fec5ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,5 @@ cmake_minimum_required(VERSION 3.2) +project(HPCore) add_definitions("-std=c++17") @@ -10,6 +11,7 @@ add_executable(hpcore src/crypto.cpp src/proc.cpp src/usr/usr.cpp + src/usr/user_session_handler.cpp src/util.cpp src/p2p/message.pb.cc src/sock/socket_client.cpp diff --git a/README.md b/README.md index e30e48a9..2b306054 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,13 @@ A C++ version of hotpocket designed for production envrionments, original prototype here: https://github.com/codetsunami/hotpocket +[Hot Pocket Wiki](https://github.com/HotPocketDev/core/wiki/Hot-Pocket-Wiki) + ## Libraries * Crypto - Libsodium https://github.com/jedisct1/libsodium * Websockets - Boost|Beast https://github.com/boostorg/beast * RapidJSON - http://rapidjson.org -* Protocol - https://github.com/protocolbuffers/protobuf +* P2P Protocol - https://github.com/protocolbuffers/protobuf ## Steps to setup Hot Pocket @@ -44,52 +46,42 @@ Instructions are based on [this](https://github.com/protocolbuffers/protobuf/tre 4. Run `make && make check` 5. Run `sudo make install` -#### Compile Protocol buffers -1. Run `protoc -I=./src/p2p --cpp_out=./src/p2p ./src/p2p/message.proto` - Ex - For message protobuf - `protoc -I=./src/p2p --cpp_out=./src/p2p ./src/p2p/message.proto` - +##### Compiling Protocol buffers message definitions +When you make a change to `message.proto` defnition file, you need to run this: + +`protoc -I=./src/p2p --cpp_out=./src/p2p ./src/p2p/message.proto` + #### Run ldconfig -1. Run `sudo ldconfig` +`sudo ldconfig` This will update your library cache and avoid potential issues when running your compiled C++ program which links to newly installed libraries. #### Install CMAKE -If you use apt, run `sudo apt install cmake` -Or follow [this](https://cmake.org/install/) +If you use apt, run `sudo apt install cmake` or follow [this](https://cmake.org/install/). #### Build and run Hot Pocket -1. navigate to hotpocket repo root. +1. Navigate to hotpocket repo root. 1. Run `cmake .` (You only have to do this once) -1. Run `make` -1. Run `./build/hpcore new ~/mycontract`. This will initialize a new contract directory `mycontract` in your home directory. -1. Take a look at `~/mycontract/cfg/hp.cfg`. This is your new contract config file. You can modify it according to your contract hosting requirements. -1. Optional: Run `./build/hpcore rekey ~/mycontract` to generate new public/private key pair. -1. Run `./build/hpcore run ~/mycontract` to run your smart contract (to do). +1. Run `make` (Hot Pocket binary will be created as `./build/hpcore`) +1. Refer to [Running Hot Pocket](https://github.com/HotPocketDev/core/wiki/Running-Hot-Pocket) in the Wiki. + +Refer to [Hot Pocket Wiki](https://github.com/HotPocketDev/core/wiki/Hot-Pocket-Wiki) for more info. ## Code structure -Code is divided into subsystems via namespaces. Some subsystems mentioned here are yet to be introduced. +Code is divided into subsystems via namespaces. -#### conf -Handles contract configuration. Loads and holds the central configuration object. Used by most of the subsystems. +**conf::** Handles contract configuration. Loads and holds the central configuration object. Used by most of the subsystems. -#### crypto -Handles cryptographic activities. Wraps libsodium and offers convenience functions. +**crypto::** Handles cryptographic activities. Wraps libsodium and offers convenience functions. -#### proc -Handles contract process execution. +**proc::** Handles contract process execution. -#### usr -Handles user connections and processing of user I/O with the smart contract. Makes use of **crypto** and **sock**. +**usr::** Handles user connections and processing of user I/O with the smart contract. Makes use of **crypto** and **sock**. -#### p2p -Handles peer-to-peer connections and message exchange between nodes. Also handles smart contract node-party-line (npl) I/O. Makes use of **crypto** and **sock**. +**p2p::** Handles peer-to-peer connections and message exchange between nodes. Also handles smart contract node-party-line (npl) I/O. Makes use of **crypto** and **sock**. -#### cons -Handles consensus and proposal rounds. Makes use of **usr**, **ntn** and **proc** +**cons::** Handles consensus and proposal rounds. Makes use of **usr**, **p2p** and **proc** -#### sock -Handles generic web sockets functionality. Mainly acts as a wrapper for boost/beast. +**sock::** Handles generic web sockets functionality. Mainly acts as a wrapper for boost/beast. -#### shared -Contains shared data structures/helper functions used by multiple subsystems. Used by most of the subsystems. \ No newline at end of file +**util::** Contains shared data structures/helper functions used by multiple subsystems. \ No newline at end of file diff --git a/examples/hpclient/.gitignore b/examples/hpclient/.gitignore new file mode 100644 index 00000000..0a3fc7dd --- /dev/null +++ b/examples/hpclient/.gitignore @@ -0,0 +1,2 @@ +node_modules/** +.hp_client_keys \ No newline at end of file diff --git a/examples/hpclient/client.js b/examples/hpclient/client.js new file mode 100644 index 00000000..945c98a0 --- /dev/null +++ b/examples/hpclient/client.js @@ -0,0 +1,104 @@ +// +// HotPocket client example code adopted from: +// https://github.com/codetsunami/hotpocket/blob/master/hp_client.js +// + +const fs = require('fs') +const ws_api = require('ws'); +const sodium = require('libsodium-wrappers') +const readline = require('readline') + +// sodium has a trigger when it's ready, we will wait and execute from there +sodium.ready.then(main).catch((e) => { console.log(e) }) + + +function main() { + + var keys = sodium.crypto_sign_keypair() + + + // check for client keys + if (!fs.existsSync('.hp_client_keys')) { + keys.privateKey = sodium.to_hex(keys.privateKey) + keys.publicKey = sodium.to_hex(keys.publicKey) + fs.writeFileSync('.hp_client_keys', JSON.stringify(keys)) + } else { + keys = JSON.parse(fs.readFileSync('.hp_client_keys')) + keys.privateKey = Uint8Array.from(Buffer.from(keys.privateKey, 'hex')) + keys.publicKey = Uint8Array.from(Buffer.from(keys.publicKey, 'hex')) + } + + + var server = 'ws://localhost:8080' + + if (process.argv.length == 3) server = 'ws://localhost:' + process.argv[2] + + if (process.argv.length == 4) server = 'ws://' + process.argv[2] + ':' + process.argv[3] + + var ws = new ws_api(server) + + /* anatomy of a public challenge + { + hotpocket: 0.1, + type: 'public_challenge', + challenge: '' + } + */ + + + // if the console ctrl + c's us we should close ws gracefully + process.once('SIGINT', function (code) { + console.log('SIGINT received...'); + ws.close() + }); + + ws.on('message', (m) => { + console.log("-----Received raw message-----") + console.log(m) + console.log("------------------------------") + + try { + m = JSON.parse(m) + } catch (e) { + return + } + + if (m.type != 'public_challenge') return + + console.log("Received challenge message") + console.log(m) + + console.log('My public key is: ' + Buffer.from(keys.publicKey).toString('base64')); + + // sign the challenge and send back the response + var sigbytes = sodium.crypto_sign_detached(m.challenge, keys.privateKey); + var response = { + type: 'challenge_response', + challenge: m.challenge, + sig: Buffer.from(sigbytes).toString('base64'), + pubkey: Buffer.from(keys.publicKey).toString('base64') + } + + console.log('Sending challenge response...'); + ws.send(JSON.stringify(response)) + + // start listening for stdin + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + var input_pump = () => { + rl.question('', (answer) => { + ws.send(answer + "\n") + input_pump() + }) + } + input_pump() + + }); + + ws.on('close', () => { + console.log('Server disconnected.'); + }); +} diff --git a/examples/hpclient/package-lock.json b/examples/hpclient/package-lock.json new file mode 100644 index 00000000..4b99dfcd --- /dev/null +++ b/examples/hpclient/package-lock.json @@ -0,0 +1,60 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "graceful-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "libsodium": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.5.tgz", + "integrity": "sha512-0YVU2QJc5sDR5HHkGCaliYImS7pGeXi11fiOfm4DirBd96PJVZIn3LJa06ZOFjLNsWkL3UbNjYhLRUOABPL9vw==" + }, + "libsodium-wrappers": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.5.tgz", + "integrity": "sha512-QE9Q+FxLLGdJRiJTuC2GB3LEHZeHX/VcbMQeNPdAixEKo86JPy6bOWND1XmMLu0tjWUu0xIY0YpJYQApxIZwbQ==", + "requires": { + "libsodium": "0.7.5" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "ws": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", + "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", + "requires": { + "async-limiter": "^1.0.0" + } + } + } +} diff --git a/examples/hpclient/package.json b/examples/hpclient/package.json new file mode 100644 index 00000000..d4efdc45 --- /dev/null +++ b/examples/hpclient/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "fs-extra": "^8.1.0", + "libsodium-wrappers": "^0.7.5", + "ws": "^7.1.2" + } +} diff --git a/src/conf.cpp b/src/conf.cpp index 07432339..db6cc068 100644 --- a/src/conf.cpp +++ b/src/conf.cpp @@ -154,11 +154,11 @@ int load_config() } // Check whether this contract complies with the min version requirement. - int verresult = util::version_compare(cfgversion, string(util::min_contract_version)); + int verresult = util::version_compare(cfgversion, string(util::MIN_CONTRACT_VERSION)); if (verresult == -1) { cerr << "Contract version too old. Minimum " - << util::min_contract_version << " required. " + << util::MIN_CONTRACT_VERSION << " required. " << cfgversion << " found.\n"; return -1; } @@ -210,7 +210,7 @@ int save_config() Document d; d.SetObject(); Document::AllocatorType &allocator = d.GetAllocator(); - d.AddMember("version", StringRef(util::hp_version), allocator); + d.AddMember("version", StringRef(util::HP_VERSION), allocator); d.AddMember("pubkeyb64", StringRef(cfg.pubkeyb64.data()), allocator); d.AddMember("seckeyb64", StringRef(cfg.seckeyb64.data()), allocator); d.AddMember("keytype", StringRef(cfg.keytype.data()), allocator); @@ -382,12 +382,13 @@ int is_schema_valid(Document &d) const char *cfg_schema = "{" "\"type\": \"object\"," - "\"required\": [ \"version\", \"pubkeyb64\", \"seckeyb64\", \"binary\", \"binargs\", \"listenip\"" + "\"required\": [ \"version\", \"pubkeyb64\", \"seckeyb64\", \"keytype\", \"binary\", \"binargs\", \"listenip\"" ", \"peers\", \"unl\", \"peerport\", \"roundtime\", \"pubport\", \"pubmaxsize\", \"pubmaxcpm\" ]," "\"properties\": {" "\"version\": { \"type\": \"string\" }," "\"pubkeyb64\": { \"type\": \"string\" }," "\"seckeyb64\": { \"type\": \"string\" }," + "\"keytype\": { \"type\": \"string\" }," "\"binary\": { \"type\": \"string\" }," "\"binargs\": { \"type\": \"string\" }," "\"listenip\": { \"type\": \"string\" }," diff --git a/src/crypto.cpp b/src/crypto.cpp index 41fdc17d..7cf499a2 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -60,8 +60,8 @@ string sign(const string &msg, const string &seckey) /** * Returns the base64 signature string for a message. * - * @param msg Base64 message string to sign. - * @param seckey Base64 secret key string. + * @param msg Message bytes to sign. + * @param seckeyb64 Base64 secret key string. * @return Base64 signature string. */ string sign_b64(const string &msg, const string &seckeyb64) @@ -97,8 +97,8 @@ int verify(const string &msg, const string &sig, const string &pubkey) * Verifies the given base64 signature for the message. * * @param msg Base64 message string. - * @param sig Base64 signature string. - * @param pubkey Base64 secret key. + * @param sigb64 Base64 signature string. + * @param pubkeyb64 Base64 secret key. * @return 0 for successful verification. -1 for failure. */ int verify_b64(const string &msg, const string &sigb64, const string &pubkeyb64) diff --git a/src/main.cpp b/src/main.cpp index 4a185211..6bec6e12 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "util.hpp" #include "conf.hpp" #include "crypto.hpp" @@ -67,7 +68,7 @@ int main(int argc, char **argv) if (conf::ctx.command == "version") { // Print the version - cout << util::hp_version << endl; + cout << util::HP_VERSION << endl; } else { @@ -101,6 +102,10 @@ int main(int argc, char **argv) // This will start hosting the contract and start consensus rounds. // TODO + + // Temp code to avoid exiting. + string s; + cin >> s; } } } diff --git a/src/proc.cpp b/src/proc.cpp index e47c9b59..da2f26c0 100644 --- a/src/proc.cpp +++ b/src/proc.cpp @@ -19,7 +19,7 @@ namespace proc /** * Keeps the currently executing contract process id (if any) */ -int contract_pid = 0; +__pid_t contract_pid = 0; /** * Executes the contract process and passes the specified arguments. @@ -34,7 +34,7 @@ int exec_contract(const ContractExecArgs &args) return -1; } - int pid = fork(); + __pid_t pid = fork(); if (pid > 0) { // HotPocket process. @@ -84,12 +84,12 @@ int write_to_stdin(const ContractExecArgs &args) d.SetObject(); Document::AllocatorType &allocator = d.GetAllocator(); - d.AddMember("version", StringRef(util::hp_version), allocator); + d.AddMember("version", StringRef(util::HP_VERSION), allocator); d.AddMember("pubkey", StringRef(conf::cfg.pubkeyb64.data()), allocator); d.AddMember("ts", args.timestamp, allocator); Value users(kObjectType); - for (auto &[pk, user] : args.users) + for (auto &[sid, user] : args.users) { Value fdlist(kArrayType); fdlist.PushBack(user.inpipe[0], allocator); @@ -99,7 +99,7 @@ int write_to_stdin(const ContractExecArgs &args) d.AddMember("usrfd", users, allocator); Value peers(kObjectType); - for (auto &[pk, peer] : args.peers) + for (auto &[sid, peer] : args.peers) { Value fdlist(kArrayType); fdlist.PushBack(peer.inpipe[0], allocator); diff --git a/src/sock/socket_session.cpp b/src/sock/socket_session.cpp index d073e78c..8fcb9524 100644 --- a/src/sock/socket_session.cpp +++ b/src/sock/socket_session.cpp @@ -36,6 +36,10 @@ void socket_session::client_run(const std::uint16_t port, const std::string &add port_ = port; address_ = address; + // Create a unique id for the session combining ip and port. + uniqueid_ = address + ":"; + uniqueid_.append(std::to_string(port)); + if (ec) return fail(ec, "handshake"); @@ -51,12 +55,12 @@ void socket_session::client_run(const std::uint16_t port, const std::string &add void socket_session::fail(error_code ec, char const *what) { + // std::cerr << what << ": " << ec.message() << std::endl; + // Don't report these if (ec == net::error::operation_aborted || ec == websocket::error::closed) return; - - // std::cerr << what << ": " << ec.message() << "\n"; } void socket_session::on_accept(error_code ec) @@ -78,10 +82,15 @@ void socket_session::on_accept(error_code ec) void socket_session::on_read(error_code ec, std::size_t) { + // read may get called when operation_aborted as well. + // We don't need to process read operation in that case. + if (ec == net::error::operation_aborted) + return; + // Handle the error, if any if (ec) { - //if something goes wrong when trying to read, socket connection will be closed and calling this to inform it to the handler + // if something goes wrong when trying to read, socket connection will be closed and calling this to inform it to the handler on_close(ec, 1); return fail(ec, "read"); } @@ -159,4 +168,14 @@ void socket_session::on_close(error_code ec, std::int8_t type) if (ec) return fail(ec, "close"); } + +// When called, initializes the unique id string for this session. +void socket_session::init_uniqueid() +{ + // Create a unique id for the session combining ip and port. + // We prepare this appended string here because we need to use it for finding elemends from the maps + // for validation purposes whenever a message is received. + uniqueid_.append(address_).append(":").append(std::to_string(port_)); +} + } // namespace sock \ No newline at end of file diff --git a/src/sock/socket_session.hpp b/src/sock/socket_session.hpp index 807c9a7c..b63e5f03 100644 --- a/src/sock/socket_session.hpp +++ b/src/sock/socket_session.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include "socket_session_handler.hpp" @@ -44,15 +45,29 @@ class socket_session : public std::enable_shared_from_this public: socket_session(websocket::stream &websocket, socket_session_handler &sess_handler); + // The port of the remote party. std::uint16_t port_; + + // The IP address of the remote party. std::string address_; + // The unique identifier of the remote party (format :). + std::string uniqueid_; + + // The set of util::SESSION_FLAG enum flags that will be set by user-code of this calss. + // We mainly use this to store contexual information about this session based on the use case. + // Setting and reading flags to this is completely managed by user-code. + std::bitset<8> flags_; + void server_run(const std::uint16_t port, const std::string &address); void client_run(const std::uint16_t port, const std::string &address, error ec); - //Used to send message through an active websocket connection + // Used to send message through an active websocket connection. void send(std::shared_ptr const &ss); + // When called, initializes the unique id string for this session. + void init_uniqueid(); + void close(); }; } // namespace sock diff --git a/src/usr/user_session_handler.cpp b/src/usr/user_session_handler.cpp new file mode 100644 index 00000000..3a8813c9 --- /dev/null +++ b/src/usr/user_session_handler.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include +#include +#include "../util.hpp" +#include "../sock/socket_session.hpp" +#include "usr.hpp" +#include "user_session_handler.hpp" + +namespace net = boost::asio; +namespace beast = boost::beast; + +using tcp = net::ip::tcp; +using error = boost::system::error_code; + +using namespace std; + +namespace usr +{ + +/** + * This gets hit every time a client connects to HP via the public port (configured in contract config). + */ +void user_session_handler::on_connect(sock::socket_session *session) +{ + cout << "User client connected " << session->address_ << ":" << session->port_ << endl; + + // As a soon as a user conntects, we issue them a challenge message. We remember the + // challenge we issued and later verifies the user's response with it. + + string msg; + string challengeb64; + usr::create_user_challenge(msg, challengeb64); + + // We init the session unique id to associate with the challenge. + session->init_uniqueid(); + + // Create an entry in pending_challenges for later tracking upon challenge response. + usr::pending_challenges[session->uniqueid_] = challengeb64; + + // TODO: This needs to be reviewed to optimise passing the message. + session->send(make_shared(msg)); + + // Set the challenge-issued flag to help later checks in on_message. + session->flags_.set(util::SESSION_FLAG::USER_CHALLENGE_ISSUED); +} + +/** + * This gets hit every time we receive some data from a client connected to the HP public port. + */ +void user_session_handler::on_message(sock::socket_session *session, const std::string &message) +{ + // First check whether this session is pending challenge. + // Meaning we have previously issued a challenge to the client, + if (session->flags_[util::SESSION_FLAG::USER_CHALLENGE_ISSUED]) + { + // The received message must be the challenge response. We need to verify it. + auto itr = usr::pending_challenges.find(session->uniqueid_); + if (itr != usr::pending_challenges.end()) + { + string userpubkey; + const string &original_challenge = itr->second; + if (usr::verify_user_challenge_response(message, original_challenge, userpubkey) == 0) + { + // Challenge verification successful. + // Promote the connection from pending-challenges to authenticated users. + + session->flags_.reset(util::SESSION_FLAG::USER_CHALLENGE_ISSUED); // Clear challenge-issued flag + session->flags_.set(util::SESSION_FLAG::USER_AUTHED); // Set the user-authed flag + usr::pending_challenges.erase(session->uniqueid_); // Remove the stored challenge + usr::add_user(session->uniqueid_, userpubkey); // Add the user to the global authed user list + + cout << "User connection " << session->uniqueid_ << " authenticated.\n"; + return; + } + else + { + cout << "Challenge verification failed " << session->uniqueid_ << endl; + } + } + } + // Check whether this session belongs to an authenticated (challenge-verified) user. + else if (session->flags_[util::SESSION_FLAG::USER_AUTHED]) + { + // Check whether this user is among authenticated users + // and perform authenticated msg processing. + + auto itr = usr::users.find(session->uniqueid_); + if (itr != usr::users.end()) + { + // This is an authed user. + // Write the message to the user input pipe. SC will read from this pipe when it executes. + const contract_user &user = itr->second; + write(user.inpipe[1], message.data(), message.length()); + cout << "User " << user.pubkeyb64 << " wrote " << message.length() << " bytes to contract input.\n"; + return; + } + } + + // If for any reason we reach this point, we should drop the connection. + session->close(); + cout << "Dropped the user connection " << session->address_ << ":" << session->port_ << endl; +} + +/** + * This gets hit every time a client disconnects from the HP public port. + */ +void user_session_handler::on_close(sock::socket_session *session) +{ + // Cleanup any resources related to this session. + + // Session is awaiting challenge response. + if (session->flags_[util::SESSION_FLAG::USER_CHALLENGE_ISSUED]) + { + usr::pending_challenges.erase(session->uniqueid_); + } + // Session belongs to an authed user. + else if (session->flags_[util::SESSION_FLAG::USER_AUTHED]) + { + usr::remove_user(session->uniqueid_); + } + + cout << "User disconnected " << session->uniqueid_ << endl; +} + +} // namespace usr \ No newline at end of file diff --git a/src/usr/user_session_handler.hpp b/src/usr/user_session_handler.hpp new file mode 100644 index 00000000..bf165198 --- /dev/null +++ b/src/usr/user_session_handler.hpp @@ -0,0 +1,18 @@ +#include +#include "../sock/socket_session_handler.hpp" +#include "../sock/socket_session.hpp" + +using error = boost::system::error_code; + +namespace usr +{ + +class user_session_handler : public sock::socket_session_handler +{ +public: + void on_connect(sock::socket_session *session); + void on_message(sock::socket_session *session, const std::string &message); + void on_close(sock::socket_session *session); +}; + +} \ No newline at end of file diff --git a/src/usr/usr.cpp b/src/usr/usr.cpp index 5675dd34..48e3706c 100644 --- a/src/usr/usr.cpp +++ b/src/usr/usr.cpp @@ -4,14 +4,17 @@ #include #include #include -#include #include #include #include +#include +#include "../sock/socket_server.hpp" +#include "../sock/socket_session_handler.hpp" #include "../util.hpp" #include "../conf.hpp" #include "../crypto.hpp" #include "usr.hpp" +#include "user_session_handler.hpp" using namespace std; using namespace util; @@ -22,13 +25,44 @@ namespace usr /** * Global user list. (Exposed to other sub systems) + * Map key: User socket session id () */ map users; /** - * Json schema doc used for user challenge-response json validation. + * Keep track of verification-pending challenges issued to newly connected users. + * Map key: User socket session id () */ -Document challenge_response_schemadoc; +map pending_challenges; + +/** + * User session handler instance. This instance's methods will be fired for any user socket activity. + */ +user_session_handler global_usr_session_handler; + +/** + * The IO context used by the websocket listener. (not exposed out of this namespace) + */ +net::io_context ioc; + +/** + * The thread the websocket lsitener is running on. (not exposed out of this namespace) + */ +thread listener_thread; + +// Challenge response fields. +// These fields are used on challenge response validation. +static const char* CHALLENGE_RESP_TYPE = "type"; +static const char* CHALLENGE_RESP_CHALLENGE = "challenge"; +static const char* CHALLENGE_RESP_SIG = "sig"; +static const char* CHALLENGE_RESP_PUBKEY = "pubkey"; + +// Message type for the user challenge. +static const char *CHALLENGE_MSGTYPE = "public_challenge"; +// Message type for the user challenge response. +static const char *CHALLENGE_RESP_MSGTYPE = "challenge_response"; +// Length of user random challenge bytes. +static const int CHALLENGE_LEN = 16; /** * Initializes the usr subsystem. Must be called once during application startup. @@ -36,21 +70,8 @@ Document challenge_response_schemadoc; */ int init() { - //We initialize the response schema doc from this json string so we can - //use the schema repeatedly for all challenge-response validations. - - const char *challenge_response_schema = - "{" - "\"type\": \"object\"," - "\"required\": [ \"type\", \"challenge\", \"sig\", \"pubkey\" ]," - "\"properties\": {" - "\"type\": { \"type\": \"string\" }," - "\"challenge\": { \"type\": \"string\" }," - "\"sig\": { \"type\": \"string\" }," - "\"pubkey\": { \"type\": \"string\" }" - "}" - "}"; - challenge_response_schemadoc.Parse(challenge_response_schema); + // Start listening for incoming user connections. + start_listening(); return 0; } @@ -72,31 +93,31 @@ int init() void create_user_challenge(string &msg, string &challengeb64) { //Use libsodium to generate the random challenge bytes. - unsigned char challenge_bytes[user_challenge_len]; - randombytes_buf(challenge_bytes, user_challenge_len); + unsigned char challenge_bytes[CHALLENGE_LEN]; + randombytes_buf(challenge_bytes, CHALLENGE_LEN); //We pass the b64 challenge string separately to the caller even though //we also include it in the challenge msg as well. - base64_encode(challenge_bytes, user_challenge_len, challengeb64); + base64_encode(challenge_bytes, CHALLENGE_LEN, challengeb64); //Construct the challenge msg json. - Document d; - d.SetObject(); - Document::AllocatorType &allocator = d.GetAllocator(); - d.AddMember("version", StringRef(util::hp_version), allocator); - d.AddMember("type", StringRef(msg_public_challenge), allocator); - d.AddMember("challenge", StringRef(challengeb64.data()), allocator); + // We do not use RapidJson here in favour of performance because this is a simple json message. - StringBuffer buffer; - Writer writer(buffer); - d.Accept(writer); - msg = buffer.GetString(); + // Since we know the rough size of the challenge massage we reserve adequate amount for the holder. + // Only Hot Pocket version number is variable length. Therefore message size is roughly 95 bytes + // so allocating 128bits for heap padding. + msg.reserve(128); + msg.append("{\"version\":\"") + .append(util::HP_VERSION) + .append("\",\"type\":\"public_challenge\",\"challenge\":\"") + .append(challengeb64) + .append("\"}"); } /** * Verifies the user challenge response with the original challenge issued to the user - * and the user public contained in the response. + * and the user public key contained in the response. * * @param response The response bytes to verify. This will be parsed as json. * Accepted response format: @@ -107,42 +128,52 @@ void create_user_challenge(string &msg, string &challengeb64) * "pubkey": "" * } * @param original_challenge The original base64 challenge string issued to the user. + * @param extracted_pubkeyb64 The public key extracted from the response. * @return 0 if challenge response is verified. -1 if challenge not met or an error occurs. */ int verify_user_challenge_response(const string &response, const string &original_challenge, string &extracted_pubkeyb64) { - //We load response raw bytes into json document and validate the schema. + // We load response raw bytes into json document. Document d; d.Parse(response.data()); - - //Validate json scheme. - //This has a cost. But we have to do this first. Otherwise field value - //extraction will fail in subsequent steps if the message is malformed. - SchemaDocument schema(challenge_response_schemadoc); - SchemaValidator validator(schema); - if (!d.Accept(validator)) + if (d.HasParseError()) { - cerr << "User challenge resposne schema invalid.\n"; + cerr << "Challenge response json parser error.\n"; return -1; } - //Validate msg type. - if (d["type"] != msg_challenge_resp) + // Validate msg type. + if (!d.HasMember(CHALLENGE_RESP_TYPE) || d[CHALLENGE_RESP_TYPE] != CHALLENGE_RESP_MSGTYPE) { - cerr << "User challenge response type invalid. 'challenge_response' expeced.\n"; + cerr << "User challenge response type invalid. 'challenge_response' expected.\n"; return -1; } - //Compare the response challenge string with the original issued challenge. - if (d["challenge"] != original_challenge.data()) + // Compare the response challenge string with the original issued challenge. + if (!d.HasMember(CHALLENGE_RESP_CHALLENGE) || d[CHALLENGE_RESP_CHALLENGE] != original_challenge.data()) { - cerr << "User challenge resposne: challenge mismatch.\n"; + cerr << "User challenge response challenge invalid.\n"; return -1; } - //Verify the challenge signature. We do this last due to signature verification cost. - string sigb64 = d["sig"].GetString(); - extracted_pubkeyb64 = d["pubkey"].GetString(); + // Check for the 'sig' field existence. + if (!d.HasMember(CHALLENGE_RESP_SIG) || !d[CHALLENGE_RESP_SIG].IsString()) + { + cerr << "User challenge response signature invalid.\n"; + return -1; + } + + // Check for the 'pubkey' field existence. + if (!d.HasMember(CHALLENGE_RESP_PUBKEY) || !d[CHALLENGE_RESP_PUBKEY].IsString()) + { + cerr << "User challenge response public key invalid.\n"; + return -1; + } + + // Verify the challenge signature. We do this last due to signature verification cost. + string sigb64 = d[CHALLENGE_RESP_SIG].GetString(); + extracted_pubkeyb64 = d[CHALLENGE_RESP_PUBKEY].GetString(); + if (crypto::verify_b64(original_challenge, sigb64, extracted_pubkeyb64) != 0) { cerr << "User challenge response signature verification failed.\n"; @@ -153,16 +184,18 @@ int verify_user_challenge_response(const string &response, const string &origina } /** - * Adds the specified public key into the global user list. + * Adds the user denoted by specified session id and public key to the global authed user list. * This should get called after the challenge handshake is verified. * + * @param sessionid User socket session id. + * @param pubkeyb64 User's base64 public key. * @return 0 on successful additions. -1 on failure. */ -int add_user(const string &pubkeyb64) +int add_user(const string &sessionid, const string &pubkeyb64) { - if (users.count(pubkeyb64) == 1) + if (users.count(sessionid) == 1) { - cerr << pubkeyb64 << " already exist. Cannot add user.\n"; + cerr << sessionid << " already exist. Cannot add user.\n"; return -1; } @@ -172,7 +205,7 @@ int add_user(const string &pubkeyb64) int inpipe[2]; if (pipe(inpipe) != 0) { - cerr << "User in pipe creation failed. pubkey:" << pubkeyb64 << endl; + cerr << "User in pipe creation failed. sessionid:" << sessionid << endl; return -1; } @@ -180,7 +213,7 @@ int add_user(const string &pubkeyb64) int outpipe[2]; if (pipe(outpipe) != 0) { - cerr << "User out pipe creation failed. pubkey:" << pubkeyb64 << endl; + cerr << "User out pipe creation failed. sessionid:" << sessionid << endl; //We need to close 'inpipe' in case outpipe failed. close(inpipe[0]); @@ -189,7 +222,7 @@ int add_user(const string &pubkeyb64) return -1; } - users.insert(pair(pubkeyb64, contract_user(pubkeyb64, inpipe, outpipe))); + users.emplace(sessionid, contract_user(pubkeyb64, inpipe, outpipe)); return 0; } @@ -199,16 +232,17 @@ int add_user(const string &pubkeyb64) * * @return 0 on successful removals. -1 on failure. */ -int remove_user(const string &pubkeyb64) +int remove_user(const string &sessionid) { - if (users.count(pubkeyb64) == 0) + auto itr = users.find(sessionid); + + if (itr == users.end()) { - cerr << pubkeyb64 << " does not exist. Cannot remove user.\n"; + cerr << sessionid << " does not exist. Cannot remove user.\n"; return -1; } - auto itr = users.find(pubkeyb64); - contract_user user = itr->second; + const contract_user &user = itr->second; //Close the User <--> SC I/O pipes. close(user.inpipe[0]); @@ -236,7 +270,7 @@ int read_contract_user_outputs() //Currently this is sequential for simplicity which will not scale well //when there are large number of users connected to the same HP node. - for (auto &[pk, user] : users) + for (auto &[sid, user] : users) { int fdout = user.outpipe[0]; int bytes_available = 0; @@ -257,4 +291,21 @@ int read_contract_user_outputs() return 0; } +/** + * Starts listening for incoming user websocket connections. + */ +void start_listening() +{ + auto address = net::ip::make_address(conf::cfg.listenip); + make_shared( + ioc, + tcp::endpoint{address, conf::cfg.pubport}, + global_usr_session_handler) + ->run(); + + listener_thread = thread([&] { ioc.run(); }); + + cout << "Started listening for incoming user connections...\n"; +} + } // namespace usr \ No newline at end of file diff --git a/src/usr/usr.hpp b/src/usr/usr.hpp index 51743d7b..f09c294f 100644 --- a/src/usr/usr.hpp +++ b/src/usr/usr.hpp @@ -15,32 +15,35 @@ using namespace util; namespace usr { -// Length of user random challenge bytes. -static const int user_challenge_len = 16; - -// Message type for the user challenge. -static const char *msg_public_challenge = "public_challenge"; - -// Message type for the user challenge response. -static const char *msg_challenge_resp = "challenge_response"; - /** * Global authenticated (challenge-verified) user list. */ extern map users; +/** + * Keep track of verification-pending challenges issued to newly connected users. + */ +extern map pending_challenges; + +/** + * Keep track of verification-pending challenges issued to newly connected users. + */ +extern map pending_challenges; + int init(); void create_user_challenge(string &msg, string &challengeb64); int verify_user_challenge_response(const string &response, const string &original_challenge, string &extracted_pubkey); -int add_user(const string &pubkeyb64); +int add_user(const string &sessionid, const string &pubkeyb64); -int remove_user(const string &pubkeyb64); +int remove_user(const string &sessionid); int read_contract_user_outputs(); +void start_listening(); + } // namespace usr #endif \ No newline at end of file diff --git a/src/util.cpp b/src/util.cpp index 5d48babb..b35da857 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -30,7 +30,9 @@ int base64_encode(const unsigned char *bin, size_t bin_len, string &encoded_stri return -1; // Assign the encoded char* onto the provided string reference. - encoded_string = string(base64chars, base64_len); + // "base64_len - 1" because sodium include '\0' in the calculated base64 length. + // Therefore we need to omit it when initializing the std::string. + encoded_string = string(base64chars, base64_len - 1); return 0; } diff --git a/src/util.hpp b/src/util.hpp index 0937732d..b067b1bb 100644 --- a/src/util.hpp +++ b/src/util.hpp @@ -13,14 +13,25 @@ namespace util { // Hot Pocket version. Displayed on 'hotpocket version' and written to new contract configs. -static const char *hp_version = "0.1"; +static const char *HP_VERSION = "0.1"; // Minimum compatible contract config version (this will be used to validate contract configs) -static const char *min_contract_version = "0.1"; +static const char *MIN_CONTRACT_VERSION = "0.1"; // Minimum compatible peer message version (this will be used to accept/reject incoming peer connections) // (Keeping this as int for effcient msg payload and comparison) -static const int min_peermsg_version = 1; +static const int MIN_PEERMSG_VERSION = 1; + +/** + * Set of flags used to mark status information on the session. + * usr and p2p subsystems makes use of this to mark status information of user and peer sessions. + * Set flags are stored in 'flags_' bitset. + */ +enum SESSION_FLAG +{ + USER_CHALLENGE_ISSUED = 0, + USER_AUTHED = 1 +}; /** * Holds information about an authenticated (challenge-verified) user