├── .gitignore ├── CMakeLists.txt ├── Changelog.md ├── Dockerfile ├── LICENSE ├── README.md ├── build-and-install.sh ├── proxy.conf └── src ├── CMakeLists.txt ├── main.cpp ├── proxy.cpp ├── proxy.hpp ├── sharding ├── hasher.cpp ├── hasher.hpp ├── node.cpp ├── node.hpp ├── sharder.cpp └── sharder.hpp ├── timer.cpp ├── timer.hpp ├── util.cpp └── util.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | oplb 34 | 35 | # Other 36 | .idea/ 37 | cmake-build-debug/ 38 | build/ 39 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(oplb VERSION 1.1.0) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_CXX_FLAGS "-pedantic -ansi -Wall -Wextra -Werror -O3") 7 | 8 | add_subdirectory(src) 9 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | **Project mistakenly started as v0.0.1, so the second release became v1.0.0 to become SemVer compliant.** 3 | **Dates are expressed in dd-MM-yyyy** 4 | 5 | ### Release: v1.0.0 6 | Who: rafaelsilverioit (https://github.com/rafaelsilverioit) 7 | Date: 18-06-2021 8 | Description: 9 | * Major refactoring of files to be compatible with C++ style and to be more maintainable; 10 | * Implemented consistent hashing to shard requests for suitable peers (ref: https://github.com/rafaelsilverioit/sharder); 11 | * If there's a log_file key in proxy.conf, then we log everything in there. 12 | 13 | ### Release: v1.1.0 14 | Who: rafaelsilverioit (https://github.com/rafaelsilverioit) 15 | Date: 07-07-2021 16 | Description: 17 | * Getting rid of forking code as io_service takes care of concurrency for us. 18 | 19 | ### Release: v1.2.0 20 | Who: rafaelsilverioit (https://github.com/rafaelsilverioit) 21 | Date: 29-12-2021 22 | Description: 23 | * Avoiding process termination in case of fatal errors. 24 | 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN apk update 4 | RUN apk add vim bash git g++ make cmake boost-dev curl curl-dev jsoncpp-dev libc6-compat 5 | 6 | COPY . /opt/proxy-load-balancer 7 | COPY proxy.conf /opt/proxy-load-balancer/proxy.conf 8 | 9 | WORKDIR /opt/proxy-load-balancer 10 | RUN ./build-and-install.sh 11 | 12 | WORKDIR / 13 | RUN rm -rf /opt/proxy-load-balancer 14 | EXPOSE 8080 15 | CMD oplb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Oystr - Robôs Inteligentes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Libraries licenses: 24 | 25 | Boost: 26 | 27 | // Copyright Joe Coder 2004 - 2006. 28 | // Distributed under the Boost Software License, Version 1.0. 29 | // (See accompanying file LICENSE_1_0.txt or copy at 30 | // https://www.boost.org/LICENSE_1_0.txt) 31 | 32 | 33 | Curl: 34 | 35 | // Copyright (c) 1996 - 2020, Daniel Stenberg, , and many 36 | // contributors, see the THANKS file. 37 | // 38 | // All rights reserved. 39 | // https://github.com/curl/curl/blob/master/COPYING 40 | 41 | JsonCpp: 42 | 43 | // Copyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors 44 | // 45 | // https://github.com/open-source-parsers/jsoncpp/blob/master/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oplb (Oystr Proxy - Load Balancer) 2 | 3 | [![badge](https://img.shields.io/badge/license-MIT-blue)](https://github.com/oystr-foss/proxy-load-balancer/blob/main/LICENSE) 4 | 5 | oplb is a dynamic proxy load balancer that enables us to handle as many ephemeral clients as possible, while being careless about what discovery backend is being used. All requests are sharded using a consistent hashing algorithm based in a Java implementation by [rafaelsilverioit](https://github.com/rafaelsilverioit/sharder). 6 | 7 | ### Requirements 8 | 9 | * build-essential 10 | * C++17 11 | * cmake 12 | * libboost-dev 13 | * libboost-system-dev 14 | * libboost-thread-dev 15 | * libcurl4-openssl-dev 16 | * libssl-dev 17 | * libcrypto++-dev 18 | * libjsoncpp-dev 19 | 20 | ### Building from source 21 | 22 | Just follow the steps below on a Docker image based on Alpine: 23 | ```bash 24 | $ git clone https://github.com/oystr-foss/proxy-load-balancer oplb 25 | $ cd oplb 26 | $ ./build-and-install.sh 27 | ``` 28 | 29 | ### Configuration 30 | A `proxy.conf` configuration file is expected to exist under `/etc/oplb/`. 31 | 32 | The default configuration looks like: 33 | 34 | ```markdown 35 | # general config 36 | host=0.0.0.0 37 | port=8080 38 | log_info= 39 | 40 | # interval in seconds between each query to the discovery backend. 41 | refresh_interval=60 42 | 43 | # discovery backend 44 | discovery_url=http://localhost:10000 45 | endpoint=/services 46 | ``` 47 | 48 | ### Discovery backend 49 | Basically, we expect an endpoint that serves a JSON payload with at least the following strutcture: 50 | ```json 51 | { 52 | "host": "", 53 | "port": 8888 54 | } 55 | ``` 56 | 57 | ### Running 58 | In order to run the load balancer, just type oplb: 59 | 60 | ```bash 61 | $ oplb 62 | Listening on: 0.0.0.0:8080 63 | 64 | [06/10/2020 12:39:24] 127.0.0.1:36588 -> 127.0.0.1:8888 65 | [06/10/2020 12:39:28] 127.0.0.1:36596 -> 127.0.0.1:8888 66 | ``` 67 | 68 | ### Debugging 69 | By default, all logs are sent to stdin/stderr but you can set `log_info=` to store it in a custom file. 70 | 71 | ### TODO 72 | 73 | * Create/use an http client; 74 | * Add tests. 75 | -------------------------------------------------------------------------------- /build-and-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf build && mkdir build && cd build || exit 1 4 | cmake .. && make && make install && cd .. || exit 1 5 | 6 | if [ ! -e /etc/oplb/ ]; 7 | then 8 | mkdir /etc/oplb/ 9 | fi 10 | 11 | cp proxy.conf /etc/oplb/ 12 | -------------------------------------------------------------------------------- /proxy.conf: -------------------------------------------------------------------------------- 1 | # general config 2 | host=0.0.0.0 3 | port=8080 4 | refresh_interval=15 5 | #log_file=oplb.log 6 | 7 | # discovery service 8 | discovery_url=http://localhost:10000 9 | endpoint=/services 10 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(oplb VERSION 1.1.0) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_CXX_FLAGS "-pedantic -ansi -Wall -Wextra -O3 -fpermissive") 7 | set(CMAKE_BUILD_TYPE RelWithDebInfo) 8 | 9 | add_executable(oplb main.cpp util.hpp util.cpp proxy.cpp proxy.hpp timer.cpp timer.hpp sharding/hasher.hpp sharding/node.hpp sharding/sharder.hpp sharding/hasher.cpp sharding/node.cpp sharding/sharder.cpp ../proxy.conf) 10 | 11 | include_directories(/usr/include) 12 | include_directories(/usr/lib) 13 | include_directories(/usr/local/lib) 14 | include_directories(/usr/lib/x86_64-linux-gnu) 15 | 16 | target_link_libraries(oplb stdc++) 17 | target_link_libraries(oplb pthread) 18 | target_link_libraries(oplb boost_thread) 19 | target_link_libraries(oplb boost_system) 20 | target_link_libraries(oplb curl) 21 | target_link_libraries(oplb jsoncpp) 22 | target_link_libraries(oplb rt) 23 | target_link_libraries(oplb ssl) 24 | target_link_libraries(oplb crypto) 25 | 26 | install(TARGETS oplb DESTINATION /usr/bin COMPONENT binaries) 27 | install(FILES ../proxy.conf DESTINATION /etc/oplb COMPONENT config) 28 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include "util.hpp" 12 | #include "timer.hpp" 13 | #include "proxy.hpp" 14 | 15 | // TODO: 16 | // * Limit forking and reuse childs instead of killing them. 17 | int main(int argc, char *argv[]) { 18 | std::map config; 19 | 20 | if (argc > 1) { 21 | read_config(config, argv[1]); 22 | } else { 23 | read_config(config); 24 | } 25 | 26 | if (config.count("log_file") >= 1) { 27 | // Logging to specified file. 28 | freopen(config["log_file"].c_str(), "w", stdout); 29 | freopen(config["log_file"].c_str(), "w", stderr); 30 | } 31 | 32 | if (config.count("port") != 1) { 33 | std::cerr << "a port must be specified in proxy.conf" << std::endl; 34 | exit(1); 35 | } 36 | 37 | std::string local_host; 38 | unsigned short local_port; 39 | 40 | try { 41 | local_host = "0.0.0.0"; 42 | if (config.count("host") == 1) { 43 | local_host = config["host"]; 44 | } 45 | 46 | local_port = static_cast(boost::lexical_cast(config["port"])); 47 | } catch (boost::bad_lexical_cast const &e) { 48 | std::cerr << "Invalid port" << std::endl; 49 | exit(1); 50 | } 51 | 52 | boost::asio::io_service ios; 53 | timer t(ios, config); 54 | 55 | // in case of fatal errors, just start all over again. 56 | while (true) { 57 | try { 58 | auto sharder = ConsistentHash(); 59 | tcp_proxy::bridge::acceptor acceptor(ios, local_host, local_port, t, config, sharder); 60 | std::cout << "Listening on: " << local_host << ":" << local_port << "\n" << std::endl; 61 | 62 | ios.run(); 63 | } catch (std::exception & e) 64 | { 65 | std::cerr << "Error: " << e.what() << std::endl; 66 | return 1; 67 | } 68 | } 69 | 70 | return 0; 71 | } 72 | -------------------------------------------------------------------------------- /src/proxy.cpp: -------------------------------------------------------------------------------- 1 | #pragma clang diagnostic push 2 | #pragma ide diagnostic ignored "modernize-avoid-bind" 3 | // 4 | // Created by realngnx on 11/06/2021. 5 | // 6 | 7 | #include "proxy.hpp" 8 | #include 9 | 10 | 11 | namespace { 12 | std::size_t callback( 13 | const char *in, 14 | std::size_t size, 15 | std::size_t num, 16 | std::string *out) { 17 | const std::size_t total_bytes(size * num); 18 | out->append(in, total_bytes); 19 | return total_bytes; 20 | } 21 | } 22 | 23 | namespace tcp_proxy { 24 | bridge::bridge(boost::asio::io_service &ios): 25 | downstream_socket_(ios), 26 | upstream_socket_(ios) {} 27 | 28 | bridge::socket_type &bridge::downstream_socket() { 29 | // Client socket 30 | return downstream_socket_; 31 | } 32 | 33 | bridge::socket_type &bridge::upstream_socket() { 34 | // Remote socket 35 | return upstream_socket_; 36 | } 37 | 38 | void bridge::start(const std::string &upstream_host, unsigned short upstream_port) { 39 | upstream_socket_.async_connect( 40 | ip::tcp::endpoint(ip::address::from_string(upstream_host), upstream_port), 41 | boost::bind(&bridge::handle_upstream_connect,shared_from_this(), boost::asio::placeholders::error)); 42 | } 43 | 44 | void bridge::handle_upstream_connect(const boost::system::error_code &error) { 45 | if (!error) { 46 | // Setup async read from upstream 47 | upstream_socket_.async_read_some( 48 | boost::asio::buffer(upstream_data_, max_data_length), 49 | boost::bind(&bridge::handle_upstream_read, 50 | shared_from_this(), 51 | boost::asio::placeholders::error, 52 | boost::asio::placeholders::bytes_transferred)); 53 | 54 | downstream_socket_.async_read_some( 55 | boost::asio::buffer(downstream_data_, max_data_length), 56 | boost::bind(&bridge::handle_downstream_read, 57 | shared_from_this(), 58 | boost::asio::placeholders::error, 59 | boost::asio::placeholders::bytes_transferred)); 60 | } else { 61 | std::cout << "[ERROR] upstream_connect: " << error.message() << std::endl; 62 | 63 | if (!upstream_socket_.is_open()) { 64 | std::cout << "[ERROR] upstream socket not connected " << std::endl; 65 | } 66 | 67 | if (!downstream_socket_.is_open()) { 68 | std::cout << "[ERROR] downstream socket not connected" << std::endl; 69 | } 70 | close(); 71 | } 72 | } 73 | 74 | void bridge::handle_upstream_read(const boost::system::error_code &error, const size_t &bytes_transferred) { 75 | if (!error) { 76 | async_write(downstream_socket_, boost::asio::buffer(upstream_data_, bytes_transferred), 77 | boost::bind(&bridge::handle_downstream_write, 78 | shared_from_this(), 79 | boost::asio::placeholders::error)); 80 | } else { 81 | close(); 82 | } 83 | } 84 | 85 | void bridge::handle_downstream_write(const boost::system::error_code &error) { 86 | if (!error) { 87 | upstream_socket_.async_read_some( 88 | boost::asio::buffer(upstream_data_, max_data_length), 89 | boost::bind(&bridge::handle_upstream_read, shared_from_this(), 90 | boost::asio::placeholders::error, 91 | boost::asio::placeholders::bytes_transferred)); 92 | } else { 93 | close(); 94 | } 95 | } 96 | 97 | void bridge::handle_downstream_read(const boost::system::error_code &error, const size_t &bytes_transferred) { 98 | if (!error) { 99 | async_write(upstream_socket_, boost::asio::buffer(downstream_data_, bytes_transferred), 100 | boost::bind(&bridge::handle_upstream_write, shared_from_this(), 101 | boost::asio::placeholders::error)); 102 | } else { 103 | close(); 104 | } 105 | } 106 | 107 | void bridge::handle_upstream_write(const boost::system::error_code &error) { 108 | if (!error) { 109 | downstream_socket_.async_read_some( 110 | boost::asio::buffer(downstream_data_, max_data_length), 111 | boost::bind(&bridge::handle_downstream_read, shared_from_this(), 112 | boost::asio::placeholders::error, 113 | boost::asio::placeholders::bytes_transferred)); 114 | } else { 115 | close(); 116 | } 117 | } 118 | 119 | void bridge::close() { 120 | boost::mutex::scoped_lock lock(mutex_); 121 | 122 | if (downstream_socket_.is_open()) { 123 | downstream_socket_.close(); 124 | } 125 | 126 | if (upstream_socket_.is_open()) { 127 | upstream_socket_.close(); 128 | } 129 | } 130 | 131 | bridge::acceptor::acceptor( 132 | boost::asio::io_service &io_service, 133 | const std::string &local_host, 134 | unsigned short local_port, 135 | timer &t, 136 | std::map &config, 137 | ConsistentHash& sharder 138 | ): 139 | io_service_(io_service), 140 | localhost_address(boost::asio::ip::address_v4::from_string(local_host)), 141 | acceptor_(io_service_, ip::tcp::endpoint(localhost_address, local_port)), 142 | sharder_(sharder) { 143 | 144 | const boost::system::error_code null; 145 | get_available_nodes(null, &t, config, sharder_); 146 | accept_connections(); 147 | } 148 | 149 | bool bridge::acceptor::accept_connections() { 150 | try { 151 | session_ = boost::shared_ptr(new bridge(io_service_)); 152 | acceptor_.async_accept(session_->downstream_socket(), 153 | boost::bind(&acceptor::handle_accept, 154 | this, 155 | boost::asio::placeholders::error)); 156 | return true; 157 | } catch (std::exception &e) { 158 | std::cerr << "acceptor exception: " << e.what() << std::endl; 159 | return false; 160 | } 161 | } 162 | 163 | void bridge::acceptor::handle_accept(const boost::system::error_code &error) { 164 | if (!error) { 165 | boost::system::error_code client_error; 166 | // using synchronous error message for convenience as we may call session_->close() safely. 167 | const ip::basic_endpoint &client_socket = session_->downstream_socket_.remote_endpoint(client_error); 168 | 169 | if(client_error) { 170 | session_->close(); 171 | accept_connections(); 172 | return; 173 | } 174 | 175 | auto remote = client_socket.address().to_string(); 176 | auto time = get_date("%Y%m%d-%H"); 177 | auto n = sharder_.route(remote + time); 178 | 179 | if(!n.has_value()) { 180 | service_unavailable(); 181 | return; 182 | } 183 | 184 | const std::string host(n.value()->get_host()); 185 | const unsigned short port(n.value()->get_port()); 186 | 187 | std::cout << "[" << get_date("%d/%m/%Y %H:%M:%S") << "] " << 188 | client_socket << " -> " << host << ":" << port << std::endl; 189 | 190 | session_->start(host, port); 191 | 192 | if(!accept_connections()) { 193 | std::cerr << "Failed to accept connections." << std::endl; 194 | } 195 | } else { 196 | std::cerr << "Error: " << error.message() << std::endl; 197 | accept_connections(); 198 | } 199 | } 200 | 201 | void bridge::acceptor::service_unavailable() { 202 | std::string message("No hosts available to forward requests to."); 203 | std::cout << message << std::endl; 204 | std::string contentLength = std::to_string(message.length()); 205 | std::string response = "HTTP/1.1 503 Service Unavailable\nX-Oystr-Proxy: true\nContent-Type:" 206 | " text/html; charset=utf-8\nContent-Length: " + contentLength + "\n\n" + message; 207 | this->session_->downstream_socket().write_some(boost::asio::buffer(response)); 208 | this->accept_connections(); 209 | } 210 | } 211 | 212 | // TODO: Implement a HTTP Client. 213 | void get_available_nodes(const boost::system::error_code & /*e*/, timer *scheduler, 214 | std::map &config, ConsistentHash& sharder) { 215 | 216 | const std::string url(config["discovery_url"]); 217 | const std::string endpoint(config["endpoint"]); 218 | const std::string final_url(url + endpoint); 219 | 220 | CURL *curl; 221 | curl = curl_easy_init(); 222 | 223 | if (curl) { 224 | curl_easy_setopt(curl, CURLOPT_URL, final_url.c_str()); 225 | curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 226 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60); 227 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 228 | 229 | long http_code(0); 230 | std::unique_ptr http_data(new std::string()); 231 | 232 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callback); 233 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, http_data.get()); 234 | 235 | curl_easy_perform(curl); 236 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); 237 | curl_easy_cleanup(curl); 238 | 239 | if (http_code != 200) { 240 | std::cerr << "Non-200 response (" << http_code << "): " << http_data->c_str() << std::endl; 241 | } else { 242 | Json::Value data; 243 | Json::Reader reader; 244 | 245 | if (reader.parse(*http_data, data)) { 246 | // Cleaning up to get rid of unhealthy nodes/peers. 247 | sharder.clear(); 248 | for (auto item : data) { 249 | const std::string service_id(item["service_id"].asString()); 250 | const std::string host(item["host"].asString()); 251 | const unsigned short port(item["port"].asInt()); 252 | 253 | ServiceNode node(service_id, host, port); 254 | 255 | if(sharder.get_existing_replicas(node) == 0) { 256 | sharder.add(node, 5); 257 | } 258 | } 259 | } else { 260 | std::cout << "Non JSON response: " << http_data->c_str() << std::endl; 261 | } 262 | } 263 | } 264 | 265 | scheduler->schedule(get_available_nodes, sharder); 266 | } 267 | 268 | 269 | #pragma clang diagnostic pop 270 | -------------------------------------------------------------------------------- /src/proxy.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | 5 | #ifndef OPLB_PROXY_HPP 6 | #define OPLB_PROXY_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | 27 | #if __has_include() 28 | #include 29 | #else 30 | #include 31 | #endif 32 | 33 | #include 34 | #include 35 | 36 | #include "timer.hpp" 37 | #include "util.hpp" 38 | #include "sharding/node.hpp" 39 | #include "sharding/sharder.hpp" 40 | 41 | 42 | void get_available_nodes(const boost::system::error_code & /*e*/, timer *scheduler, 43 | std::map &config, ConsistentHash& sharder); 44 | 45 | namespace { 46 | std::size_t callback( 47 | const char *in, 48 | std::size_t size, 49 | std::size_t num, 50 | std::string *out 51 | ); 52 | } 53 | 54 | namespace tcp_proxy { 55 | namespace ip = boost::asio::ip; 56 | 57 | class bridge : public boost::enable_shared_from_this { 58 | public: 59 | typedef ip::tcp::socket socket_type; 60 | typedef boost::shared_ptr ptr_type; 61 | 62 | explicit bridge(boost::asio::io_service &ios); 63 | 64 | socket_type downstream_socket_; 65 | socket_type upstream_socket_; 66 | 67 | socket_type &downstream_socket(); 68 | socket_type &upstream_socket(); 69 | void start(const std::string &upstream_host, unsigned short upstream_port); 70 | void handle_upstream_connect(const boost::system::error_code &error); 71 | 72 | private: 73 | 74 | // Read from remote server complete, send data to client 75 | void handle_upstream_read(const boost::system::error_code &error, const size_t &bytes_transferred); 76 | void handle_downstream_write(const boost::system::error_code &error); 77 | void handle_downstream_read(const boost::system::error_code &error, const size_t &bytes_transferred); 78 | void handle_upstream_write(const boost::system::error_code &error); 79 | void close(); 80 | 81 | enum { 82 | max_data_length = 8196 // 8KB 83 | }; 84 | unsigned char downstream_data_[max_data_length]; 85 | unsigned char upstream_data_[max_data_length]; 86 | 87 | boost::mutex mutex_; 88 | 89 | public: 90 | class acceptor { 91 | public: 92 | acceptor(boost::asio::io_service &io_service, 93 | const std::string &local_host, 94 | unsigned short local_port, 95 | timer &t, 96 | std::map &config, 97 | ConsistentHash& sharder); 98 | 99 | bool accept_connections(); 100 | 101 | private: 102 | void handle_accept(const boost::system::error_code &error); 103 | void service_unavailable(); 104 | 105 | boost::asio::io_service &io_service_; 106 | ip::address_v4 localhost_address; 107 | ip::tcp::acceptor acceptor_; 108 | ptr_type session_; 109 | ConsistentHash& sharder_; 110 | 111 | }; 112 | }; 113 | } 114 | 115 | #endif //OPLB_PROXY_HPP 116 | -------------------------------------------------------------------------------- /src/sharding/hasher.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | #include 5 | #include 6 | #include "./hasher.hpp" 7 | 8 | 9 | long Digest::to_md5_hash(std::string key) { 10 | const char * hashed = reinterpret_cast(md5(key)); 11 | return calculate_hash(hashed); 12 | } 13 | 14 | long Digest::to_sha256_hash(std::string key) { 15 | const char * hashed = reinterpret_cast(sha256(key)); 16 | return calculate_hash(hashed); 17 | } 18 | 19 | long Digest::calculate_hash(const char *hashed) { 20 | long hash = 0; 21 | for (int i = 0; i < 4; i++) { 22 | hash <<= 8; 23 | hash |= ((int) hashed[i]) & 0xFF; 24 | } 25 | return hash; 26 | } 27 | 28 | const unsigned char * Digest::sha256(const std::string &key) { 29 | char const *c = key.c_str(); 30 | return SHA256(reinterpret_cast(c), key.length(), nullptr); 31 | } 32 | 33 | const unsigned char * Digest::md5(const std::string &key) { 34 | char const *c = key.c_str(); 35 | return MD5(reinterpret_cast(c), key.length(), nullptr); 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/sharding/hasher.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | #ifndef HASHER_H 5 | #define HASHER_H 6 | 7 | #include 8 | 9 | 10 | class Hasher { 11 | public: 12 | virtual long to_md5_hash(std::string key) = 0; 13 | virtual long to_sha256_hash(std::string key) = 0; 14 | }; 15 | 16 | class Digest: public Hasher { 17 | public: 18 | Digest() = default; 19 | 20 | long to_md5_hash(std::string key) override; 21 | long to_sha256_hash(std::string key) override; 22 | 23 | private: 24 | static long calculate_hash(char const* hashed); 25 | static unsigned char const* sha256(const std::string& key); 26 | static unsigned char const* md5(const std::string& key); 27 | 28 | }; 29 | 30 | 31 | #endif //HASHER_H 32 | -------------------------------------------------------------------------------- /src/sharding/node.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | #include 5 | #include 6 | #include 7 | #include "./node.hpp" 8 | 9 | 10 | ServiceNode::ServiceNode(std::string service, std::string address, int port): 11 | descriptor(std::move(service)), 12 | address(std::move(address)), 13 | port(port) {} 14 | 15 | std::string ServiceNode::get_key() { 16 | auto format = boost::format("%1%:%2%:%3%") % this->descriptor % this->address % this->port; 17 | return boost::str(format); 18 | } 19 | 20 | std::string ServiceNode::get_host() { 21 | return this->address; 22 | } 23 | 24 | int ServiceNode::get_port() { 25 | return this->port; 26 | } 27 | 28 | VirtualNode::VirtualNode(ServiceNode node, long replica_index): 29 | physical_node(std::make_shared(node)), 30 | replica_index(replica_index) {} 31 | 32 | std::string VirtualNode::get_key() { 33 | auto format = boost::format("%1%:%2%") % get_physical_node()->get_key() % replica_index; 34 | return boost::str(format); 35 | } 36 | 37 | bool VirtualNode::is_virtual_node_of(ServiceNode& node) { 38 | std::string key = node.get_key(); 39 | return boost::iequals(key, get_physical_node()->get_key()); 40 | } 41 | 42 | std::shared_ptr VirtualNode::get_physical_node() { 43 | return physical_node; 44 | } 45 | -------------------------------------------------------------------------------- /src/sharding/node.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | #ifndef NODE_H 5 | #define NODE_H 6 | 7 | #include 8 | 9 | 10 | class Node { 11 | public: 12 | virtual std::string get_key() = 0; 13 | 14 | std::string to_string() { 15 | return get_key(); 16 | } 17 | 18 | }; 19 | 20 | class ServiceNode: public Node { 21 | public: 22 | ServiceNode() = default; 23 | ServiceNode(std::string service, std::string address, int port); 24 | 25 | std::string get_key() override; 26 | std::string get_host(); 27 | int get_port(); 28 | 29 | private: 30 | std::string descriptor; 31 | std::string address; 32 | int port; 33 | 34 | }; 35 | 36 | class VirtualNode: public Node { 37 | public: 38 | VirtualNode() = default; 39 | VirtualNode(ServiceNode node, long replica_index); 40 | 41 | bool is_virtual_node_of(ServiceNode& node); 42 | std::shared_ptr get_physical_node(); 43 | std::string get_key() override; 44 | 45 | private: 46 | std::shared_ptr physical_node; 47 | long replica_index; 48 | 49 | }; 50 | 51 | 52 | #endif //NODE_H 53 | -------------------------------------------------------------------------------- /src/sharding/sharder.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | #include 5 | #include "./sharder.hpp" 6 | 7 | 8 | ConsistentHash::~ConsistentHash() { 9 | ring.clear(); 10 | } 11 | 12 | ConsistentHash::ConsistentHash() { 13 | std::map hosts_map {}; 14 | ring = hosts_map; 15 | } 16 | 17 | boost::optional> ConsistentHash::route(std::string key) { 18 | if(ring.empty()) { 19 | boost::optional> opt; 20 | 21 | return opt; 22 | } 23 | 24 | long hash_val = digest.to_md5_hash(key); 25 | std::map tail_map; 26 | for(auto n : ring) { 27 | if(n.first >= hash_val) { 28 | tail_map[n.first] = n.second; 29 | } 30 | } 31 | 32 | try { 33 | long node_hash_val = !tail_map.empty() ? tail_map.begin()->first : ring.begin()->first; 34 | auto route_to = ring[node_hash_val]->get_physical_node(); 35 | 36 | return boost::make_optional(route_to); 37 | } catch (std::exception &e) { 38 | std::cerr << "Error: " << e.what() << std::endl; 39 | 40 | return boost::make_optional(ring.begin()->second->get_physical_node()); 41 | } 42 | } 43 | 44 | void ConsistentHash::add(ServiceNode& node, long virtual_node_count) { 45 | if(virtual_node_count < 0) { 46 | std::cout << "Virtual node count is < 0!" << std::endl; 47 | return; 48 | } 49 | 50 | long existing_replicas = get_existing_replicas(node); 51 | 52 | for(long i = 0; i < virtual_node_count; i++) { 53 | long index = i + existing_replicas; 54 | auto v_node = new VirtualNode(node, index); 55 | long hash = digest.to_md5_hash(v_node->get_key()); 56 | ring[hash] = v_node; 57 | } 58 | std::cout << "Added! " << node.get_key() << std::endl; 59 | } 60 | 61 | void ConsistentHash::remove(ServiceNode& node) { 62 | std::map ring2(ring); 63 | if(ring2.empty()) { 64 | return; 65 | } 66 | 67 | for(auto const & tup : ring2) { 68 | if((*tup.second).is_virtual_node_of(node)) { 69 | auto iterator = ring.find(tup.first); 70 | ring.erase(iterator); 71 | } 72 | } 73 | 74 | std::cout << "Removed " << node.get_key() << std::endl; 75 | } 76 | 77 | void ConsistentHash::clear() { 78 | ring.clear(); 79 | } 80 | 81 | long ConsistentHash::get_existing_replicas(ServiceNode& node) { 82 | long count = 0; 83 | auto keys = extract_keys(); 84 | 85 | for(long key : keys) { 86 | VirtualNode* n = ring[key]; 87 | if(n->is_virtual_node_of(node)) { 88 | count += 1; 89 | } 90 | } 91 | 92 | return count; 93 | } 94 | -------------------------------------------------------------------------------- /src/sharding/sharder.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | #ifndef SHARDER_H 5 | #define SHARDER_H 6 | 7 | #include 8 | #include 9 | #include 10 | #include "./hasher.hpp" 11 | #include "./node.hpp" 12 | 13 | 14 | class Sharder { 15 | public: 16 | virtual boost::optional> route(std::string key) = 0; 17 | 18 | }; 19 | 20 | class ConsistentHash: public Sharder { 21 | public: 22 | ConsistentHash(); 23 | ~ConsistentHash(); 24 | 25 | boost::optional> route(std::string key) override; 26 | void add(ServiceNode& node, long virtual_node_count); 27 | void remove(ServiceNode& node); 28 | void clear(); 29 | long get_existing_replicas(ServiceNode& node); 30 | 31 | private: 32 | Digest digest; 33 | std::map ring; 34 | 35 | std::vector extract_keys() { 36 | std::vector keys; 37 | for (auto const& element : ring) { 38 | keys.push_back(element.first); 39 | } 40 | return keys; 41 | } 42 | }; 43 | 44 | 45 | #endif //SHARDER_H 46 | -------------------------------------------------------------------------------- /src/timer.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | 5 | #include "timer.hpp" 6 | #include "util.hpp" 7 | #include 8 | 9 | 10 | timer::timer(boost::asio::io_service &io_service, std::map &configuration): 11 | scheduler(boost::asio::deadline_timer(io_service)), 12 | config(configuration) {} 13 | 14 | void timer::schedule(void (&fun)(const boost::system::error_code & /*e*/, timer *scheduler, 15 | std::map &config, ConsistentHash& sharder), ConsistentHash& param) { 16 | this->set_interval(); 17 | this->scheduler.async_wait(boost::bind(fun, boost::asio::placeholders::error, this, config, boost::ref(param))); 18 | } 19 | 20 | void timer::set_interval() { 21 | scheduler.expires_from_now(boost::posix_time::seconds(get_interval(config))); 22 | } 23 | -------------------------------------------------------------------------------- /src/timer.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by realngnx on 11/06/2021. 3 | // 4 | 5 | #ifndef OPLB_TIMER_HPP 6 | #define OPLB_TIMER_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | #include "sharding/node.hpp" 12 | #include "sharding/sharder.hpp" 13 | 14 | 15 | class timer { 16 | public: 17 | boost::asio::deadline_timer scheduler; 18 | 19 | timer(boost::asio::io_service &io_service, std::map &configuration); 20 | 21 | void schedule(void (&fun)(const boost::system::error_code & /*e*/, timer *scheduler, 22 | std::map &config, ConsistentHash& sharder), ConsistentHash& param); 23 | 24 | private: 25 | std::map &config; 26 | 27 | void set_interval(); 28 | }; 29 | 30 | 31 | #endif //OPLB_TIMER_HPP 32 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.hpp" 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | int get_interval(std::map &config) { 8 | if (config.count("refresh_interval") == 1) { 9 | std::string interval = config["refresh_interval"]; 10 | try { 11 | int i = std::stoi(interval); 12 | return i > 0 ? i : 60; 13 | } catch (std::invalid_argument &e) { 14 | std::cerr << "refresh_interval must be a positive integer and greater than 0" << std::endl; 15 | exit(1); 16 | } 17 | } 18 | return 60; 19 | } 20 | 21 | void read_config(std::map &config, const std::string& config_file) { 22 | std::ifstream file(config_file); 23 | if (file.is_open()) { 24 | std::string line; 25 | while (getline(file, line)) { 26 | line.erase(std::remove_if(line.begin(), line.end(), isspace), line.end()); 27 | 28 | if (line[0] == '#' || line.empty()) { 29 | continue; 30 | } 31 | 32 | auto delimiter_pos = line.find('='); 33 | auto name = line.substr(0, delimiter_pos); 34 | auto val = line.substr(delimiter_pos + 1); 35 | 36 | config[name] = val; 37 | } 38 | } else { 39 | std::cerr << "proxy.conf not found" << std::endl; 40 | exit(1); 41 | } 42 | } 43 | 44 | void read_config(std::map &config) { 45 | read_config(config, "/etc/oplb/proxy.conf"); 46 | } 47 | 48 | std::string get_date() { 49 | time_t now = time(nullptr); 50 | tm *ltm = localtime(&now); 51 | char date[12]; 52 | std::strftime(date, sizeof(date), "%d/%m/%Y", ltm); 53 | return std::string(date); 54 | } 55 | 56 | std::string get_date(const char *format) { 57 | time_t now = time(nullptr); 58 | tm *ltm = localtime(&now); 59 | char date[32]; // should we be concerned about this? 60 | std::strftime(date, sizeof(date), format, ltm); 61 | return std::string(date); 62 | } -------------------------------------------------------------------------------- /src/util.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifndef OLPB_UTIL_HPP 5 | #define OLPB_UTIL_HPP 6 | 7 | int get_interval(std::map &config); 8 | void read_config(std::map &config, const std::string& config_file); 9 | void read_config(std::map &config); 10 | std::string get_date(); 11 | std::string get_date(const char *format); 12 | 13 | #endif //OLPB_UTIL_HPP 14 | --------------------------------------------------------------------------------