본문 바로가기
-----ETC-----/C++ 네트워크 프로그래밍 시리즈

[C++ 네트워크 프로그래밍] Day 28: 프로젝트: 보안 기능 추가

by cogito21_cpp 2024. 8. 1.
반응형

실시간 채팅 애플리케이션 보안 기능 추가

이전 단계에서는 실시간 채팅 애플리케이션의 기본 기능을 완성했습니다. 이번 단계에서는 보안 기능을 추가하여 애플리케이션을 더 안전하게 만들겠습니다. SSL/TLS를 사용하여 서버와 클라이언트 간의 통신을 암호화합니다.

SSL/TLS 개요

SSL(Secure Sockets Layer)과 TLS(Transport Layer Security)는 네트워크 통신을 보호하기 위한 프로토콜입니다. 이를 통해 데이터가 전송 중에 도청되거나 변조되지 않도록 보호합니다.

설정 파일 준비

  1. 서버 인증서와 키: server.crt, server.key 파일이 필요합니다.
  2. 클라이언트 인증서와 키 (선택 사항): 클라이언트 인증서를 사용하여 추가적인 보안을 제공할 수 있습니다.

서버 코드 업데이트

ChatServer.h

#ifndef CHATSERVER_H
#define CHATSERVER_H

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast.hpp>
#include <memory>
#include <unordered_set>
#include <unordered_map>
#include <string>
#include <nlohmann/json.hpp>

namespace net = boost::asio;
namespace ssl = net::ssl;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;

class ClientSession;

class ChatServer : public std::enable_shared_from_this<ChatServer> {
public:
    ChatServer(net::io_context& ioc, ssl::context& ssl_ctx, tcp::endpoint endpoint)
        : acceptor_(ioc), ssl_ctx_(ssl_ctx), socket_(ioc) {
        acceptor_.open(endpoint.protocol());
        acceptor_.set_option(net::socket_base::reuse_address(true));
        acceptor_.bind(endpoint);
        acceptor_.listen();
        do_accept();
    }

    void broadcast(const nlohmann::json& message);
    void join(std::shared_ptr<ClientSession> session, const std::string& username);
    void leave(std::shared_ptr<ClientSession> session);
    void updateUserList();

private:
    void do_accept();

    tcp::acceptor acceptor_;
    ssl::context& ssl_ctx_;
    tcp::socket socket_;
    std::unordered_set<std::shared_ptr<ClientSession>> sessions_;
    std::unordered_map<std::string, std::shared_ptr<ClientSession>> user_sessions_;
};

#endif // CHATSERVER_H

 

ClientSession.h

#ifndef CLIENTSESSION_H
#define CLIENTSESSION_H

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast.hpp>
#include <memory>
#include "ChatServer.h"

namespace net = boost::asio;
namespace ssl = net::ssl;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;

class ClientSession : public std::enable_shared_from_this<ClientSession> {
public:
    ClientSession(tcp::socket socket, ssl::context& ssl_ctx, ChatServer& server)
        : ws_(std::move(socket), ssl_ctx), server_(server) {}

    void start();
    void send(const nlohmann::json& message);

private:
    void do_handshake();
    void do_read();
    void do_write();
    void handle_message(const std::string& message);

    websocket::stream<ssl::stream<tcp::socket>> ws_;
    ChatServer& server_;
    boost::beast::flat_buffer buffer_;
    std::vector<std::string> write_msgs_;
    std::string username_;
};

#endif // CLIENTSESSION_H

 

ChatServer.cpp

#include "ChatServer.h"
#include "ClientSession.h"

void ChatServer::broadcast(const nlohmann::json& message) {
    for (auto& session : sessions_) {
        session->send(message);
    }
}

void ChatServer::join(std::shared_ptr<ClientSession> session, const std::string& username) {
    sessions_.insert(session);
    user_sessions_[username] = session;

    // 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
    nlohmann::json notification;
    notification["type"] = "join";
    notification["username"] = username;
    broadcast(notification);

    updateUserList();
}

void ChatServer::leave(std::shared_ptr<ClientSession> session) {
    sessions_.erase(session);
    for (auto it = user_sessions_.begin(); it != user_sessions_.end(); ++it) {
        if (it->second == session) {
            // 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
            nlohmann::json notification;
            notification["type"] = "leave";
            notification["username"] = it->first;
            broadcast(notification);

            user_sessions_.erase(it);
            break;
        }
    }

    updateUserList();
}

void ChatServer::updateUserList() {
    nlohmann::json user_list_message;
    user_list_message["type"] = "user_list";
    for (const auto& user_session : user_sessions_) {
        user_list_message["users"].push_back(user_session.first);
    }
    broadcast(user_list_message);
}

void ChatServer::do_accept() {
    acceptor_.async_accept(socket_, [this](boost::system::error_code ec) {
        if (!ec) {
            auto session = std::make_shared<ClientSession>(std::move(socket_), ssl_ctx_, *this);
            session->start();
        }
        do_accept();
    });
}

 

ClientSession.cpp

#include "ClientSession.h"
#include <nlohmann/json.hpp>

void ClientSession::start() {
    do_handshake();
}

void ClientSession::do_handshake() {
    auto self(shared_from_this());
    ws_.next_layer().async_handshake(ssl::stream_base::server, [this, self](boost::system::error_code ec) {
        if (!ec) {
            ws_.async_accept([this, self](boost::system::error_code ec) {
                if (!ec) {
                    do_read();
                } else {
                    server_.leave(shared_from_this());
                }
            });
        } else {
            server_.leave(shared_from_this());
        }
    });
}

void ClientSession::send(const nlohmann::json& message) {
    auto self(shared_from_this());
    net::post(ws_.get_executor(), [this, self, message]() {
        bool write_in_progress = !write_msgs_.empty();
        write_msgs_.push_back(message.dump());
        if (!write_in_progress) {
            do_write();
        }
    });
}

void ClientSession::do_read() {
    auto self(shared_from_this());
    ws_.async_read(buffer_, [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
            auto message = boost::beast::buffers_to_string(buffer_.data());
            handle_message(message);
            buffer_.consume(length);
            do_read();
        } else {
            server_.leave(shared_from_this());
        }
    });
}

void ClientSession::do_write() {
    auto self(shared_from_this());
    ws_.async_write(net::buffer(write_msgs_.front()), [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
            write_msgs_.erase(write_msgs_.begin());
            if (!write_msgs_.empty()) {
                do_write();
            }
        } else {
            server_.leave(shared_from_this());
        }
    });
}

void ClientSession::handle_message(const std::string& message) {
    auto json_message = nlohmann::json::parse(message);
    if (json_message.contains("username")) {
        username_ = json_message["username"].get<std::string>();
        server_.join(shared_from_this(), username_);
    }
    if (json_message.contains("message")) {
        nlohmann::json broadcast_message;
        broadcast_message["type"] = "message";
        broadcast_message["username"] = username_;
        broadcast_message["message"] = json_message["message"];
        server_.broadcast(broadcast_message);
    }
}

 

main.cpp

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include "ChatServer.h"

namespace net = boost::asio;
namespace ssl = net::ssl;
using tcp = net::ip::tcp;

int main() {
    try {
        net::io_context ioc{1};

        ssl::context ctx{ssl::context::tlsv12};
        ctx.set_options(ssl::context::default_workarounds
                      | ssl::context::no_sslv2
                      | ssl::context::single_dh_use);
        ctx.use_certificate_chain_file("server.crt");
        ctx.use_private_key_file("server.key", ssl::context::pem);

        tcp::endpoint endpoint{net::ip::make_address("0.0.0.0"), 12345};
        std::make_shared<ChatServer>(ioc, ctx, endpoint)->run();

        ioc.run();
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

 

클라이언트 코드 업데이트

ChatClient.h

#ifndef CHATCLIENT_H
#define CHATCLIENT_H

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <

boost/beast.hpp>
#include <memory>
#include <string>
#include <queue>
#include <iostream>

namespace net = boost::asio;
namespace ssl = net::ssl;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;

class ChatClient : public std::enable_shared_from_this<ChatClient> {
public:
    ChatClient(net::io_context& ioc, ssl::context& ssl_ctx, const tcp::resolver::results_type& endpoints)
        : resolver_(ioc), ws_(ioc, ssl_ctx), endpoints_(endpoints) {}

    void connect();
    void disconnect();
    void sendMessage(const std::string& message);
    void setUsername(const std::string& username);

private:
    void doRead();
    void doWrite();
    void doHandshake();
    std::string formatMessage(const std::string& message);

    tcp::resolver resolver_;
    websocket::stream<ssl::stream<tcp::socket>> ws_;
    tcp::resolver::results_type endpoints_;
    boost::beast::flat_buffer buffer_;
    std::queue<std::string> writeMessages_;
    std::string username_;
};

#endif // CHATCLIENT_H

 

ChatClient.cpp

#include "ChatClient.h"
#include <nlohmann/json.hpp>

void ChatClient::connect() {
    auto self(shared_from_this());
    net::async_connect(ws_.next_layer().next_layer(), endpoints_,
        [this, self](boost::system::error_code ec, tcp::endpoint) {
            if (!ec) {
                doHandshake();
            } else {
                std::cerr << "Connect failed: " << ec.message() << std::endl;
            }
        });
}

void ChatClient::doHandshake() {
    auto self(shared_from_this());
    ws_.next_layer().async_handshake(ssl::stream_base::client, [this, self](boost::system::error_code ec) {
        if (!ec) {
            ws_.async_handshake("localhost", "/",
                [this, self](boost::system::error_code ec) {
                    if (!ec) {
                        doRead();
                    } else {
                        std::cerr << "Handshake failed: " << ec.message() << std::endl;
                    }
                });
        } else {
            std::cerr << "SSL Handshake failed: " << ec.message() << std::endl;
        }
    });
}

void ChatClient::disconnect() {
    auto self(shared_from_this());
    ws_.async_close(websocket::close_code::normal,
        [this, self](boost::system::error_code ec) {
            if (ec) {
                std::cerr << "Close failed: " << ec.message() << std::endl;
            }
        });
}

void ChatClient::sendMessage(const std::string& message) {
    auto self(shared_from_this());
    net::post(ws_.get_executor(), [this, self, message]() {
        bool writeInProgress = !writeMessages_.empty();
        writeMessages_.push(formatMessage(message));
        if (!writeInProgress) {
            doWrite();
        }
    });
}

void ChatClient::setUsername(const std::string& username) {
    username_ = username;
}

std::string ChatClient::formatMessage(const std::string& message) {
    nlohmann::json jsonMessage;
    jsonMessage["username"] = username_;
    jsonMessage["message"] = message;
    return jsonMessage.dump();
}

void ChatClient::doRead() {
    auto self(shared_from_this());
    ws_.async_read(buffer_,
        [this, self](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                std::cout << "Received: " << boost::beast::buffers_to_string(buffer_.data()) << std::endl;
                buffer_.consume(length);
                doRead();
            } else {
                std::cerr << "Read failed: " << ec.message() << std::endl;
                disconnect();
            }
        });
}

void ChatClient::doWrite() {
    auto self(shared_from_this());
    ws_.async_write(net::buffer(writeMessages_.front()),
        [this, self](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                writeMessages_.pop();
                if (!writeMessages_.empty()) {
                    doWrite();
                }
            } else {
                std::cerr << "Write failed: " << ec.message() << std::endl;
                disconnect();
            }
        });
}

 

main.cpp

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include "ChatClient.h"

namespace net = boost::asio;
namespace ssl = net::ssl;
using tcp = net::ip::tcp;

int main() {
    try {
        net::io_context ioc;
        ssl::context ctx{ssl::context::tlsv12};
        ctx.set_verify_mode(ssl::verify_none); // 또는 서버의 인증서를 검증하도록 설정할 수 있습니다.

        tcp::resolver resolver(ioc);
        auto endpoints = resolver.resolve("localhost", "12345");

        auto client = std::make_shared<ChatClient>(ioc, ctx, endpoints);

        std::cout << "Enter your username: ";
        std::string username;
        std::getline(std::cin, username);
        client->setUsername(username);

        client->connect();

        std::thread t([&ioc]() { ioc.run(); });

        std::string message;
        while (std::getline(std::cin, message)) {
            client->sendMessage(message);
        }

        client->disconnect();
        t.join();
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

 

설명

위의 코드는 SSL/TLS를 사용하여 서버와 클라이언트 간의 통신을 암호화한 예제입니다.

  • ChatServer 클래스:
    • SSL 컨텍스트를 설정하여 서버가 SSL/TLS 통신을 지원하도록 합니다.
    • do_accept() 메서드에서 SSL/TLS 스트림을 초기화합니다.
  • ClientSession 클래스:
    • SSL/TLS 핸드셰이크를 수행하여 안전한 연결을 설정합니다.
  • ChatClient 클래스:
    • SSL 컨텍스트를 설정하여 클라이언트가 SSL/TLS 통신을 지원하도록 합니다.
    • doHandshake() 메서드를 추가하여 SSL/TLS 핸드셰이크를 수행합니다.

이제 스물여덟 번째 날의 학습을 마쳤습니다. SSL/TLS를 사용하여 실시간 채팅 애플리케이션의 통신을 암호화하는 방법을 학습했습니다.

질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "프로젝트: 최적화 및 테스트"에 대해 학습하겠습니다.

반응형