Line data Source code
1 : #ifndef THINGER_HTTP_SERVER_RESPONSE_HPP
2 : #define THINGER_HTTP_SERVER_RESPONSE_HPP
3 :
4 : #include "../common/http_response.hpp"
5 : #include "../../util/logger.hpp"
6 : #include "server_connection.hpp"
7 : #include "http_stream.hpp"
8 : #include "websocket_connection.hpp"
9 : #include "sse_connection.hpp"
10 : #include "../../util/compression.hpp"
11 : #include <nlohmann/json.hpp>
12 : #include <memory>
13 : #include <functional>
14 : #include <filesystem>
15 : #include <set>
16 :
17 : namespace thinger::http {
18 :
19 : // Forward declarations
20 : class websocket_connection;
21 : class sse_connection;
22 :
23 : class response {
24 : private:
25 : std::weak_ptr<server_connection> connection_;
26 : std::weak_ptr<http_stream> stream_;
27 : std::shared_ptr<http::http_request> http_request_;
28 : std::shared_ptr<http_response> response_;
29 : bool responded_ = false;
30 : bool cors_enabled_ = false;
31 :
32 943 : bool ensure_not_responded() const {
33 943 : if (responded_) {
34 0 : LOG_ERROR("Response already sent");
35 0 : return false;
36 : }
37 943 : return true;
38 : }
39 :
40 1755 : void prepare_response() {
41 1755 : if (!response_) {
42 872 : response_ = std::make_shared<http_response>();
43 872 : response_->set_keep_alive(http_request_->keep_alive());
44 :
45 : // Add CORS headers if enabled
46 872 : if (cors_enabled_) {
47 8 : response_->add_header("Access-Control-Allow-Origin", "*");
48 8 : response_->add_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH");
49 8 : response_->add_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
50 10 : response_->add_header("Access-Control-Allow-Credentials", "true");
51 : }
52 : }
53 1755 : }
54 :
55 51 : static bool is_compressible_content_type(const std::string& content_type) {
56 : // Only compress text-based content types
57 51 : return content_type.starts_with("text/")
58 48 : || content_type.starts_with("application/json")
59 0 : || content_type.starts_with("application/xml")
60 0 : || content_type.starts_with("application/javascript")
61 0 : || content_type.starts_with("application/x-javascript")
62 99 : || content_type.starts_with("image/svg+xml");
63 : }
64 :
65 828 : void compress_response_if_needed() {
66 : // Only compress if there's a body worth compressing
67 828 : const auto& content = response_->get_content();
68 837 : if (content.size() < 200) return;
69 :
70 : // Don't compress if already compressed
71 51 : if (response_->has_header("Content-Encoding")) return;
72 :
73 : // Only compress text-based content types
74 51 : const auto& ct = response_->get_content_type();
75 51 : if (ct.empty() || !is_compressible_content_type(ct)) return;
76 :
77 : // Check what the client accepts
78 51 : std::string accept_encoding = http_request_->get_header("Accept-Encoding");
79 51 : if (accept_encoding.empty()) return;
80 :
81 42 : if (accept_encoding.find("gzip") != std::string::npos) {
82 42 : auto compressed = ::thinger::util::gzip::compress(content);
83 42 : if (compressed) {
84 42 : response_->set_content(std::move(*compressed));
85 210 : response_->add_header("Content-Encoding", "gzip");
86 : }
87 42 : } else if (accept_encoding.find("deflate") != std::string::npos) {
88 0 : auto compressed = ::thinger::util::deflate::compress(content);
89 0 : if (compressed) {
90 0 : response_->set_content(std::move(*compressed));
91 0 : response_->add_header("Content-Encoding", "deflate");
92 : }
93 0 : }
94 51 : }
95 :
96 826 : void send_prepared_response() {
97 826 : if (!ensure_not_responded()) return;
98 826 : prepare_response();
99 826 : compress_response_if_needed();
100 :
101 826 : if (auto conn = connection_.lock()) {
102 820 : if (auto str = stream_.lock()) {
103 820 : conn->handle_stream(str, response_);
104 820 : }
105 826 : }
106 826 : responded_ = true;
107 : }
108 :
109 : public:
110 957 : response(const std::shared_ptr<server_connection>& connection,
111 : const std::shared_ptr<http_stream>& stream,
112 : const std::shared_ptr<http::http_request>& http_request,
113 : bool cors_enabled = false)
114 957 : : connection_(connection), stream_(stream), http_request_(http_request), cors_enabled_(cors_enabled) {}
115 :
116 : // JSON response
117 681 : void json(const nlohmann::json& data, http::http_response::status status = http::http_response::status::ok) {
118 681 : prepare_response();
119 681 : response_->set_status(status);
120 1362 : response_->set_content(data.dump(), "application/json");
121 681 : send_prepared_response();
122 681 : }
123 :
124 : // Text response
125 45 : void send(const std::string& text, const std::string& content_type = "text/plain") {
126 45 : prepare_response();
127 45 : response_->set_content(text, content_type);
128 45 : send_prepared_response();
129 45 : }
130 :
131 : // HTML response
132 2 : void html(const std::string& html) {
133 2 : send(html, "text/html");
134 2 : }
135 :
136 : // Error response
137 25 : void error(http::http_response::status status, const std::string& message = "") {
138 25 : prepare_response();
139 25 : response_->set_status(status);
140 25 : if (!message.empty()) {
141 69 : response_->set_content(message, "text/plain");
142 : }
143 25 : send_prepared_response();
144 25 : }
145 :
146 : // Set status code (for building custom responses)
147 34 : void status(http::http_response::status s) {
148 34 : if (!ensure_not_responded()) return;
149 34 : prepare_response();
150 34 : response_->set_status(s);
151 : }
152 :
153 : // Set header (for building custom responses)
154 23 : void header(const std::string& key, const std::string& value) {
155 23 : if (!ensure_not_responded()) return;
156 23 : prepare_response();
157 23 : response_->add_header(key, value);
158 : }
159 :
160 : // Send raw http_response object (for advanced use cases)
161 2 : void send_response(const std::shared_ptr<http_response>& response) {
162 2 : if (!ensure_not_responded()) return;
163 2 : response_ = response;
164 :
165 : // Ensure keep-alive is set properly
166 2 : response_->set_keep_alive(http_request_->keep_alive());
167 :
168 : // Add CORS headers if enabled
169 2 : if (cors_enabled_) {
170 0 : response_->add_header("Access-Control-Allow-Origin", "*");
171 0 : response_->add_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH");
172 0 : response_->add_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
173 0 : response_->add_header("Access-Control-Allow-Credentials", "true");
174 : }
175 :
176 2 : compress_response_if_needed();
177 :
178 2 : if (auto conn = connection_.lock()) {
179 2 : if (auto str = stream_.lock()) {
180 2 : conn->handle_stream(str, response_);
181 2 : }
182 2 : }
183 2 : responded_ = true;
184 : }
185 :
186 : // WebSocket upgrade
187 : void upgrade_websocket(std::function<void(std::shared_ptr<websocket_connection>)> handler,
188 : const std::set<std::string>& supported_protocols = {});
189 :
190 : // Server-Sent Events
191 : void start_sse(std::function<void(std::shared_ptr<sse_connection>)> handler);
192 :
193 : // File sending
194 : void send_file(const std::filesystem::path& path, bool force_download = false);
195 :
196 : // Redirect response
197 : void redirect(const std::string& url, http::http_response::status redirect_type = http::http_response::status::moved_temporarily);
198 :
199 : // Chunked response support
200 : bool start_chunked(const std::string& content_type, http::http_response::status status = http::http_response::status::ok);
201 : bool write_chunk(const std::string& data);
202 : bool end_chunked();
203 :
204 : // Check if response has been sent
205 0 : bool has_responded() const {
206 0 : return responded_;
207 : }
208 :
209 : // Get the underlying connection (for advanced use cases)
210 2 : std::shared_ptr<server_connection> get_connection() const {
211 2 : return connection_.lock();
212 : }
213 : };
214 :
215 : } // namespace thinger::http
216 :
217 : #endif // THINGER_HTTP_SERVER_RESPONSE_HPP
|