23 #ifndef JWT_GAME_SERVER_BASE_SERVER_HPP
24 #define JWT_GAME_SERVER_BASE_SERVER_HPP
26 #include <websocketpp/server.hpp>
27 #include <websocketpp/common/asio_ssl.hpp>
28 #include <websocketpp/common/asio.hpp>
30 #include <jwt-cpp/jwt.h>
32 #include <spdlog/spdlog.h>
38 #include <unordered_map>
46 #include <condition_variable>
54 using websocketpp::connection_hdl;
61 using std::unordered_map;
67 using std::placeholders::_1;
68 using std::placeholders::_2;
73 using std::lock_guard;
74 using std::unique_lock;
75 using std::condition_variable;
79 static inline std::string invalid_jwt() {
80 return "INVALID_TOKEN";
82 static inline std::string duplicate_connection() {
83 return "DUPLICATE_CONNECTION";
85 static inline std::string server_shutdown() {
86 return "SERVER_SHUTDOWN";
88 static inline std::string session_complete() {
89 return "SESSION_COMPLETE";
105 template<
typename player_traits,
typename jwt_clock,
typename json_traits,
106 typename server_config,
typename close_reasons>
113 using super = std::runtime_error;
114 explicit server_error(
const std::string& what_arg) noexcept :
116 explicit server_error(
const char* what_arg) noexcept : super(what_arg) {}
129 using json =
typename json_traits::json;
131 using clock = std::chrono::high_resolution_clock;
135 websocketpp::lib::shared_ptr<websocketpp::lib::asio::ssl::context>;
138 using time_point = std::chrono::time_point<clock>;
150 using ws_server = websocketpp::server<server_config>;
151 using message_ptr =
typename ws_server::message_ptr;
156 action(action_type t, connection_hdl h) : type(t), hdl(h) {}
157 action(action_type t, connection_hdl h, std::string&& m)
158 : type(t), hdl(h), msg(std::move(m)) {}
159 action(action_type t, connection_hdl h,
const std::string& m)
160 : type(t), hdl(h), msg(m) {}
168 struct session_data {
169 session_data(
const session_id& s,
const json& d) : session(s), data(d) {}
181 template<
typename map_type>
184 using key_type =
typename map_type::key_type;
185 using mapped_type =
typename map_type::mapped_type;
186 using value_type =
typename map_type::value_type;
188 void insert(value_type&& p) {
189 m_map1.insert(std::move(p));
192 const mapped_type& at(
const key_type& key)
const {
193 auto it = m_map1.find(key);
194 if(it != m_map1.end()) {
197 it = m_map2.find(key);
198 if(it != m_map2.end()) {
201 throw std::out_of_range{
"key not in buffered_map"};
206 bool contains(
const key_type& key)
const {
208 if(m_map1.count(key) > 0) {
210 }
else if(m_map2.count(key) > 0) {
218 std::swap(m_map1, m_map2);
237 const jwt::verifier<jwt_clock, json_traits>& v,
239 std::chrono::milliseconds t
240 ) : m_is_running(false), m_jwt_verifier(v), m_get_result_str(f),
241 m_session_release_time(t), m_player_count(0),
244 m_handle_message([](
const combined_id&, std::string&&){})
246 m_server.init_asio();
248 m_server.set_open_handler(bind(&base_server::on_open,
this,
249 simple_web_game_server::_1));
250 m_server.set_close_handler(bind(&base_server::on_close,
this,
251 simple_web_game_server::_1));
252 m_server.set_message_handler(
253 bind(&base_server::on_message,
this, simple_web_game_server::_1,
254 simple_web_game_server::_2)
261 m_server.set_tls_init_handler(f);
263 throw server_error{
"set_tls_init_handler called on running server"};
272 throw server_error{
"set_open_handler called on running server"};
281 throw server_error{
"set_close_handler called on running server"};
288 m_handle_message = f;
290 throw server_error{
"set_message_handler called on running server"};
299 void run(uint16_t port,
bool unlock_address) {
301 spdlog::info(
"server is listening on port {}", port);
304 m_server.set_reuse_addr(unlock_address);
305 m_server.listen(port);
306 m_server.start_accept();
328 m_is_running =
false;
329 m_server.stop_listening();
331 lock_guard<mutex> action_guard(m_action_lock);
332 lock_guard<mutex> session_guard(m_session_lock);
333 lock_guard<mutex> conn_guard(m_connection_lock);
334 lock_guard<mutex> pc_guard(m_player_count_lock);
337 while(!m_actions.empty()) {
338 action a = m_actions.front();
340 if(a.type == SUBSCRIBE || a.type == CLOSE_CONNECTION) {
341 m_new_connections.insert(a.hdl);
346 for(
auto& con_pair : m_connection_ids) {
347 m_new_connections.insert(con_pair.first);
351 for(connection_hdl hdl : m_new_connections) {
352 close_hdl(hdl, close_reasons::server_shutdown());
356 m_connection_ids.clear();
357 m_id_connections.clear();
358 m_new_connections.clear();
359 m_locked_sessions.clear();
360 m_locked_sessions.clear();
361 m_session_players.clear();
363 m_action_cond.notify_all();
375 while(m_is_running) {
376 unique_lock<mutex> action_lock(m_action_lock);
378 while(m_actions.empty()) {
379 m_action_cond.wait(action_lock);
385 action a = m_actions.front();
388 action_lock.unlock();
390 if (a.type == SUBSCRIBE) {
391 spdlog::trace(
"processing SUBSCRIBE action");
392 unique_lock<mutex> conn_lock(m_connection_lock);
393 m_new_connections.insert(a.hdl);
394 }
else if (a.type == UNSUBSCRIBE) {
395 spdlog::trace(
"processing UNSUBSCRIBE action");
396 unique_lock<mutex> conn_lock(m_connection_lock);
398 auto it = m_connection_ids.find(a.hdl);
399 if(it == m_connection_ids.end()) {
400 m_new_connections.erase(a.hdl);
402 "client hdl {} disconnected without opening session",
409 player_disconnect(a.hdl,
id);
411 }
else if (a.type == IN_MESSAGE) {
412 spdlog::trace(
"processing IN_MESSAGE action");
413 unique_lock<mutex> conn_lock(m_connection_lock);
415 auto it = m_connection_ids.find(a.hdl);
416 if(it == m_connection_ids.end()) {
418 "recieved message from client hdl {} w/no id: {}",
423 open_session(a.hdl, a.msg);
429 "player {} with session {} sent: {}",
435 m_handle_message(
id, std::move(a.msg));
437 }
else if(a.type == OUT_MESSAGE) {
438 spdlog::trace(
"processing OUT_MESSAGE action");
440 "sending message to client hdl {}: {}",
444 send_to_hdl(a.hdl, a.msg);
445 }
else if(a.type == CLOSE_CONNECTION) {
446 spdlog::trace(
"processing CLOSE_CONNECTION action");
448 "closing client hdl {} with final message: {}",
453 send_to_hdl(a.hdl, a.msg);
454 close_hdl(a.hdl, close_reasons::session_complete());
463 lock_guard<mutex> guard(m_player_count_lock);
464 return m_player_count;
474 if(get_connection_hdl_from_id(hdl,
id)) {
476 lock_guard<mutex> guard(m_action_lock);
477 spdlog::trace(
"out_message: {}", msg);
479 action{OUT_MESSAGE, hdl, std::move(msg)}
482 m_action_cond.notify_one();
485 "ignored message sent to player {} with session {}: connection closed",
486 id.player,
id.session
504 const json& result_data
507 unique_lock<mutex> lock(m_session_lock);
508 update_session_locks();
509 if(!m_locked_sessions.contains(sid)) {
510 spdlog::trace(
"completing session {}", sid);
511 session_data result{result_sid, result_data};
513 auto it = m_session_players.find(sid);
514 if(it != m_session_players.end()) {
515 if(it->second.size() > 0) {
520 if(get_connection_hdl_from_id(hdl,
id)) {
522 lock_guard<mutex> guard(m_action_lock);
523 spdlog::trace(
"closing session {} player {}", sid, pid);
524 m_actions.push(action(
528 {
id.player, result.session },
533 m_action_cond.notify_one();
536 "can't close player {} session {}: connection already closed",
545 m_locked_sessions.insert(
546 std::make_pair(sid, std::move(result))
552 void player_disconnect(connection_hdl hdl,
const combined_id&
id) {
554 lock_guard<mutex> connection_guard(m_connection_lock);
555 m_connection_ids.erase(hdl);
556 m_id_connections.erase(
id);
559 lock_guard<mutex> session_guard(m_session_lock);
560 auto it = m_session_players.find(
id.session);
561 if(it != m_session_players.end()) {
562 it->second.erase(
id.player);
563 if(it->second.empty()) {
564 m_session_players.erase(it);
569 lock_guard<mutex> pc_guard(m_player_count_lock);
573 spdlog::debug(
"player {} with session {} disconnected",
574 id.player,
id.session);
578 bool get_connection_hdl_from_id(
583 lock_guard<mutex> guard(m_connection_lock);
584 auto it = m_id_connections.find(
id);
585 if(it != m_id_connections.end()) {
593 void send_to_hdl(connection_hdl hdl,
const std::string& msg) {
595 m_server.send(hdl, msg, websocketpp::frame::opcode::text);
596 }
catch (std::exception& e) {
598 "error sending message \"{}\": {}",
605 void close_hdl(connection_hdl hdl,
const std::string& reason) {
609 websocketpp::close::status::normal,
612 }
catch (std::exception& e) {
613 spdlog::debug(
"error closing connection: {}",
618 void on_open(connection_hdl hdl) {
620 lock_guard<mutex> guard(m_action_lock);
622 m_actions.push(action(SUBSCRIBE,hdl));
624 close_hdl(hdl, close_reasons::server_shutdown());
627 m_action_cond.notify_one();
630 void on_close(connection_hdl hdl) {
632 lock_guard<mutex> guard(m_action_lock);
633 m_actions.push(action(UNSUBSCRIBE, hdl));
635 m_action_cond.notify_one();
638 void on_message(connection_hdl hdl, message_ptr msg) {
640 lock_guard<mutex> guard(m_action_lock);
641 m_actions.push(action(IN_MESSAGE, hdl, std::move(msg->get_raw_payload())));
643 m_action_cond.notify_one();
647 void update_session_locks() {
648 auto delta_time = std::chrono::duration_cast<std::chrono::milliseconds>(
649 clock::now() - m_last_session_update_time
653 if(delta_time > m_session_release_time) {
654 if(delta_time > 2 * m_session_release_time) {
655 m_locked_sessions.clear();
657 m_locked_sessions.clear();
658 m_last_session_update_time = clock::now();
662 void setup_connection_id(connection_hdl hdl,
const combined_id&
id) {
663 lock_guard<mutex> connection_guard(m_connection_lock);
666 auto id_connections_it = m_id_connections.find(
id);
667 if(id_connections_it != m_id_connections.end()) {
669 "closing duplicate connection for player {} session {}",
674 close_hdl(id_connections_it->second, close_reasons::duplicate_connection());
676 m_connection_ids.erase(id_connections_it->second);
677 m_id_connections.erase(id_connections_it);
679 lock_guard<mutex> pc_guard(m_player_count_lock);
683 m_connection_ids.emplace(hdl,
id);
684 m_id_connections.emplace(
id, hdl);
685 m_new_connections.erase(hdl);
688 void open_session(connection_hdl hdl,
const std::string& login_token) {
691 bool completed =
false;
693 jwt::decoded_jwt<json_traits> decoded_token =
694 jwt::decode<json_traits>(login_token);
695 m_jwt_verifier.verify(decoded_token);
696 auto claim_map = decoded_token.get_payload_claims();
697 player_id pid = player_traits::parse_player_id(
698 claim_map.at(
"pid").to_json()
700 session_id sid = player_traits::parse_session_id(
701 claim_map.at(
"sid").to_json()
703 login_json = claim_map.at(
"data").to_json();
706 }
catch(std::out_of_range& e) {
708 "connection provided jwt without id and/or data claims: {}",
711 }
catch(jwt::error::token_verification_exception& e) {
713 "connection provided jwt with invalid signature: {}",
716 }
catch(std::invalid_argument& e) {
718 "connection provided invalid jwt token string: {}",
721 }
catch(std::exception& e) {
722 spdlog::debug(
"connection provided jwt with invalid claims: {}", e.what());
726 lock_guard<mutex> session_guard(m_session_lock);
727 update_session_locks();
729 if(!m_locked_sessions.contains(
id.session)) {
730 setup_connection_id(hdl,
id);
731 m_session_players[
id.session].insert(
id.player);
733 "player {} connected with session {}: {}",
738 m_handle_open(
id, std::move(login_json));
743 {
id.player, m_locked_sessions.at(
id.session).session },
744 m_locked_sessions.at(
id.session).data
747 close_hdl(hdl, close_reasons::session_complete());
750 close_hdl(hdl, close_reasons::invalid_jwt());
756 atomic<bool> m_is_running;
758 jwt::verifier<jwt_clock, json_traits> m_jwt_verifier;
759 function<std::string(
const combined_id&,
const json&)> m_get_result_str;
761 set<connection_hdl, std::owner_less<connection_hdl> > m_new_connections;
765 std::owner_less<connection_hdl>
767 unordered_map<combined_id, connection_hdl, id_hash> m_id_connections;
771 mutex m_connection_lock;
773 time_point m_last_session_update_time;
774 std::chrono::milliseconds m_session_release_time;
777 unordered_map<session_id, session_data, id_hash>
779 unordered_map<session_id, set<player_id>,
id_hash> m_session_players;
783 mutex m_session_lock;
785 queue<action> m_actions;
787 condition_variable m_action_cond;
789 std::size_t m_player_count;
790 mutex m_player_count_lock;
795 function<void(
const combined_id&, std::string&&)> m_handle_message;
The class representing errors with the base_server.
Definition: base_server.hpp:111
A WebSocket server that performs authentication and manages sessions.
Definition: base_server.hpp:107
void run(uint16_t port, bool unlock_address)
Runs the underlying websocketpp server m_server.
Definition: base_server.hpp:299
std::chrono::high_resolution_clock clock
The type of clock for server time-step management.
Definition: base_server.hpp:131
typename combined_id::player_id player_id
The type of the player component of a client id.
Definition: base_server.hpp:122
void stop()
Stops the server, closes all connections, and clears all actions.
Definition: base_server.hpp:326
void set_message_handler(function< void(const combined_id &, std::string &&)> f)
Sets the given function to be called when a client sends a message.
Definition: base_server.hpp:286
bool is_running()
Returns whether the underlying WebSocket++ server is running.
Definition: base_server.hpp:313
base_server(const jwt::verifier< jwt_clock, json_traits > &v, function< std::string(const combined_id &, const json &)> f, std::chrono::milliseconds t)
The constructor for the base_server class.
Definition: base_server.hpp:236
typename player_traits::id combined_id
The type of a client id.
Definition: base_server.hpp:120
typename combined_id::session_id session_id
The type of the session component of a client id.
Definition: base_server.hpp:124
std::size_t get_player_count()
Returns the number of verified clients connected.
Definition: base_server.hpp:462
websocketpp::lib::shared_ptr< websocketpp::lib::asio::ssl::context > ssl_context_ptr
The type of a pointer to an asio ssl context.
Definition: base_server.hpp:135
void send_message(const combined_id &id, std::string &&msg)
Asynchronously sends a message to the given client.
Definition: base_server.hpp:472
typename json_traits::json json
The type of a json object.
Definition: base_server.hpp:129
void set_close_handler(function< void(const combined_id &)> f)
Sets the given function to be called when a client disconnects.
Definition: base_server.hpp:277
void process_messages()
Worker loop that processes server actions.
Definition: base_server.hpp:374
void set_open_handler(function< void(const combined_id &, json &&)> f)
Sets the given function to be called when a client sends a valid JWT.
Definition: base_server.hpp:268
void complete_session(const session_id &sid, const session_id &result_sid, const json &result_data)
Asynchronously closes the given session and sends out result tokens.
Definition: base_server.hpp:501
typename combined_id::hash id_hash
The type of the hash struct for all id types.
Definition: base_server.hpp:126
void set_tls_init_handler(function< ssl_context_ptr(connection_hdl)> f)
Sets a the given function f as the tls_init_handler for m_server.
Definition: base_server.hpp:259
void reset()
Resets the server so it may be started again.
Definition: base_server.hpp:318
Definition: base_server.hpp:52
A struct defining default close message strings.
Definition: base_server.hpp:78