/src/wt/src/web/WebController.C
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2008 Emweb bv, Herent, Belgium. |
3 | | * |
4 | | * See the LICENSE file for terms of use. |
5 | | */ |
6 | | |
7 | | #include <fstream> |
8 | | |
9 | | #include <boost/algorithm/string.hpp> |
10 | | |
11 | | #ifdef WT_THREADED |
12 | | #include <chrono> |
13 | | #endif // WT_THREADED |
14 | | |
15 | | #include "Wt/Utils.h" |
16 | | #include "Wt/WApplication.h" |
17 | | #include "Wt/WEvent.h" |
18 | | #include "Wt/WRandom.h" |
19 | | #include "Wt/WResource.h" |
20 | | #include "Wt/WServer.h" |
21 | | #include "Wt/WSocketNotifier.h" |
22 | | #include "Wt/WWebSocketResource.h" |
23 | | |
24 | | #include "Configuration.h" |
25 | | #include "CgiParser.h" |
26 | | #include "WebController.h" |
27 | | #include "WebRequest.h" |
28 | | #include "WebSession.h" |
29 | | #include "TimeUtil.h" |
30 | | #include "WebUtils.h" |
31 | | |
32 | | #ifdef HAVE_GRAPHICSMAGICK |
33 | | #include <magick/api.h> |
34 | | #endif |
35 | | |
36 | | #include <boost/utility/string_view.hpp> |
37 | | |
38 | | #include <algorithm> |
39 | | #include <csignal> |
40 | | |
41 | 0 | #define WT_REDIRECT_SECRET_HEADER "X-Wt-Redirect-Secret" |
42 | | |
43 | | namespace { |
44 | | std::string str(const std::string *strPtr) |
45 | 0 | { |
46 | 0 | if (strPtr) { |
47 | 0 | return *strPtr; |
48 | 0 | } else { |
49 | 0 | return std::string(); |
50 | 0 | } |
51 | 0 | } |
52 | | } |
53 | | |
54 | | namespace Wt { |
55 | | |
56 | | LOGGER("WebController"); |
57 | | |
58 | | WebController::WebController(WServer& server, |
59 | | const std::string& singleSessionId, |
60 | | bool autoExpire) |
61 | 0 | : conf_(server.configuration()), |
62 | 0 | singleSessionId_(singleSessionId), |
63 | 0 | autoExpire_(autoExpire), |
64 | 0 | plainHtmlSessions_(0), |
65 | 0 | ajaxSessions_(0), |
66 | 0 | zombieSessions_(0), |
67 | 0 | running_(false), |
68 | | #ifdef WT_THREADED |
69 | 0 | socketNotifier_(this), |
70 | | #endif // WT_THREADED |
71 | 0 | server_(server) |
72 | 0 | { |
73 | 0 | #ifndef WT_DEBUG_JS |
74 | 0 | WObject::seedId(WRandom::get()); |
75 | | #else |
76 | | WObject::seedId(0); |
77 | | #endif |
78 | |
|
79 | 0 | redirectSecret_ = WRandom::generateId(32); |
80 | |
|
81 | | #ifdef HAVE_GRAPHICSMAGICK |
82 | | InitializeMagick(0); |
83 | | #endif |
84 | |
|
85 | 0 | start(); |
86 | 0 | } |
87 | | |
88 | | WebController::~WebController() |
89 | 0 | { |
90 | | #ifdef HAVE_GRAPHICSMAGICK |
91 | | DestroyMagick(); |
92 | | #endif |
93 | 0 | } |
94 | | |
95 | | void WebController::start() |
96 | 0 | { |
97 | 0 | running_ = true; |
98 | 0 | } |
99 | | |
100 | | void WebController::shutdown() |
101 | 0 | { |
102 | 0 | { |
103 | 0 | std::vector<std::shared_ptr<WebSession>> sessionList; |
104 | |
|
105 | 0 | { |
106 | 0 | #ifdef WT_THREADED |
107 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
108 | 0 | #endif // WT_THREADED |
109 | |
|
110 | 0 | running_ = false; |
111 | |
|
112 | 0 | LOG_INFO_S(&server_, "shutdown: stopping " << sessions_.size() |
113 | 0 | << " sessions."); |
114 | |
|
115 | 0 | for (SessionMap::iterator i = sessions_.begin(); i != sessions_.end(); |
116 | 0 | ++i) |
117 | 0 | sessionList.push_back(i->second); |
118 | |
|
119 | 0 | sessions_.clear(); |
120 | |
|
121 | 0 | ajaxSessions_ = 0; |
122 | 0 | plainHtmlSessions_ = 0; |
123 | 0 | } |
124 | |
|
125 | 0 | for (unsigned i = 0; i < sessionList.size(); ++i) { |
126 | 0 | std::shared_ptr<WebSession> session = sessionList[i]; |
127 | 0 | WebSession::Handler handler(session, |
128 | 0 | WebSession::Handler::LockOption::TakeLock); |
129 | 0 | session->expire(); |
130 | 0 | } |
131 | 0 | } |
132 | |
|
133 | 0 | #ifdef WT_THREADED |
134 | 0 | while (zombieSessions_ > 0) { |
135 | 0 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); |
136 | 0 | } |
137 | 0 | #endif |
138 | 0 | } |
139 | | |
140 | | void WebController::sessionDeleted() |
141 | 0 | { |
142 | 0 | #ifdef WT_THREADED |
143 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
144 | 0 | #endif // WT_THREADED |
145 | 0 | --zombieSessions_; |
146 | 0 | } |
147 | | |
148 | | Configuration& WebController::configuration() |
149 | 0 | { |
150 | 0 | return conf_; |
151 | 0 | } |
152 | | |
153 | | const Configuration& WebController::configuration() const |
154 | 0 | { |
155 | 0 | return conf_; |
156 | 0 | } |
157 | | |
158 | | int WebController::sessionCount() const |
159 | 0 | { |
160 | 0 | #ifdef WT_THREADED |
161 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
162 | 0 | #endif |
163 | 0 | return sessions_.size(); |
164 | 0 | } |
165 | | |
166 | | std::vector<std::string> WebController::sessions(bool onlyRendered) |
167 | 0 | { |
168 | 0 | #ifdef WT_THREADED |
169 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
170 | 0 | #endif |
171 | 0 | std::vector<std::string> sessionIds; |
172 | 0 | for (SessionMap::const_iterator i = sessions_.begin(); i != sessions_.end(); ++i) { |
173 | 0 | if (!onlyRendered || i->second->app() != nullptr) |
174 | 0 | sessionIds.push_back(i->first); |
175 | 0 | } |
176 | 0 | return sessionIds; |
177 | 0 | } |
178 | | |
179 | | bool WebController::expireSessions() |
180 | 0 | { |
181 | 0 | std::vector<std::shared_ptr<WebSession>> toExpire; |
182 | |
|
183 | 0 | bool result; |
184 | 0 | { |
185 | 0 | Time now; |
186 | |
|
187 | 0 | #ifdef WT_THREADED |
188 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
189 | 0 | #endif // WT_THREADED |
190 | |
|
191 | 0 | for (SessionMap::iterator i = sessions_.begin(); i != sessions_.end(); ++i) { |
192 | 0 | std::shared_ptr<WebSession> session = i->second; |
193 | |
|
194 | 0 | int diff = session->expireTime() - now; |
195 | |
|
196 | 0 | if (diff < 1000 && configuration().sessionTimeout() != -1) { |
197 | 0 | toExpire.push_back(session); |
198 | | // Note: the session is not yet removed from sessions_ map since |
199 | | // we want to grab the UpdateLock to do this and grabbing it here |
200 | | // might cause a deadlock. |
201 | 0 | } |
202 | 0 | } |
203 | |
|
204 | 0 | result = !sessions_.empty(); |
205 | 0 | } |
206 | |
|
207 | 0 | for (unsigned i = 0; i < toExpire.size(); ++i) { |
208 | 0 | std::shared_ptr<WebSession> session = toExpire[i]; |
209 | |
|
210 | 0 | LOG_INFO_S(session, "timeout: expiring"); |
211 | 0 | WebSession::Handler handler(session, |
212 | 0 | WebSession::Handler::LockOption::TakeLock); |
213 | |
|
214 | 0 | #ifdef WT_THREADED |
215 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
216 | 0 | #endif // WT_THREADED |
217 | | |
218 | | // Another thread might have already removed it |
219 | 0 | if (sessions_.find(session->sessionId()) == sessions_.end()) |
220 | 0 | continue; |
221 | | |
222 | 0 | if (session->env().ajax()) |
223 | 0 | --ajaxSessions_; |
224 | 0 | else |
225 | 0 | --plainHtmlSessions_; |
226 | |
|
227 | 0 | ++zombieSessions_; |
228 | |
|
229 | 0 | sessions_.erase(session->sessionId()); |
230 | |
|
231 | 0 | session->expire(); |
232 | 0 | } |
233 | |
|
234 | 0 | return result; |
235 | 0 | } |
236 | | |
237 | | void WebController::addSession(const std::shared_ptr<WebSession>& session) |
238 | 0 | { |
239 | 0 | #ifdef WT_THREADED |
240 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
241 | 0 | #endif // WT_THREADED |
242 | |
|
243 | 0 | sessions_[session->sessionId()] = session; |
244 | 0 | } |
245 | | |
246 | | void WebController::removeSession(const std::string& sessionId) |
247 | 0 | { |
248 | 0 | #ifdef WT_THREADED |
249 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
250 | 0 | #endif // WT_THREADED |
251 | |
|
252 | 0 | LOG_INFO("Removing session " << sessionId); |
253 | |
|
254 | 0 | SessionMap::iterator i = sessions_.find(sessionId); |
255 | 0 | if (i != sessions_.end()) { |
256 | 0 | ++zombieSessions_; |
257 | 0 | if (i->second->env().ajax()) |
258 | 0 | --ajaxSessions_; |
259 | 0 | else |
260 | 0 | --plainHtmlSessions_; |
261 | 0 | sessions_.erase(i); |
262 | 0 | } |
263 | |
|
264 | 0 | if (server_.dedicatedSessionProcess() && sessions_.size() == 0) { |
265 | 0 | server_.scheduleStop(); |
266 | 0 | } |
267 | 0 | } |
268 | | |
269 | | std::string WebController::appSessionCookie(const std::string& url) |
270 | 0 | { |
271 | 0 | return Utils::urlEncode(url); |
272 | 0 | } |
273 | | |
274 | | std::string WebController::sessionFromCookie(const char * const cookies, |
275 | | const std::string& scriptName, |
276 | | const int sessionIdLength) |
277 | 0 | { |
278 | 0 | if (!cookies) |
279 | 0 | return std::string(); |
280 | | |
281 | 0 | std::string cookieName = appSessionCookie(scriptName); |
282 | | |
283 | | // is_whitespace returns whether a character is whitespace according to RFC 5234 WSP |
284 | 0 | auto is_whitespace = [](char c) { return c == ' ' || c == '\t'; }; |
285 | 0 | auto is_alphanumeric = [](char c) { return (c >= 'A' && c <= 'Z') || |
286 | 0 | (c >= 'a' && c <= 'z') || |
287 | 0 | (c >= '0' && c <= '9'); }; |
288 | |
|
289 | 0 | const char *start = cookies; |
290 | 0 | const char * const end = cookies + strlen(cookies); |
291 | 0 | start = std::find_if_not(start, end, is_whitespace); // Skip leading whitespace |
292 | 0 | while (start < end) { |
293 | 0 | const char *const nextEquals = std::find(start, end, '='); |
294 | 0 | if (nextEquals == end) |
295 | 0 | return std::string{}; // Cookie header has no equals anymore |
296 | 0 | const char *const nextSemicolon = std::find(nextEquals+1, end, ';'); |
297 | 0 | if (nextSemicolon != end && |
298 | 0 | *(nextSemicolon + 1) != ' ') |
299 | 0 | return std::string{}; // Malformed cookie header, no space after semicolon |
300 | | |
301 | 0 | assert(nextEquals < nextSemicolon); // Should be guaranteed because nextSemicolon search starts at nextEquals+1 |
302 | 0 | assert(nextSemicolon <= end); // Should be guaranteed because nextSemicolon search ends at 'end' |
303 | 0 | assert(start <= nextEquals); // Should be guaranteed because nextEquals search starts at start |
304 | | |
305 | | // othercookie=value; cookiename=cookievalue; lastCookie = value |
306 | | // ^- cookies ^- start ^ ^- nextSemicolon ^- end |
307 | | // \- nextEquals |
308 | | // or (last cookie) |
309 | | // othercookie=value; cookiename=cookievalue |
310 | | // ^- cookies ^- start ^ ^- nextSemicolon = end |
311 | | // \- nextEquals |
312 | |
|
313 | 0 | if (std::distance(start, nextEquals) == (long)cookieName.size() && |
314 | 0 | std::equal(start, nextEquals, cookieName.c_str())) { |
315 | 0 | const char * cookieValueStart = nextEquals+1; |
316 | 0 | assert(cookieValueStart <= end); // Because of nextEquals == end check earlier |
317 | | // Leave out trailing whitespace |
318 | 0 | const char * cookieValueEnd = nextSemicolon == end ? std::find_if(cookieValueStart, end, is_whitespace) : nextSemicolon; |
319 | | |
320 | | // Handle cookie value in double quotes |
321 | 0 | if (*cookieValueStart == '"') { |
322 | 0 | ++cookieValueStart; |
323 | 0 | assert(cookieValueEnd - 1 >= cookies); // Should be guaranteed because cookieValueStart >= nextEquals + 1 |
324 | 0 | if (*(cookieValueEnd - 1) != '"') |
325 | 0 | return std::string{}; // Malformed cookie header, unbalanced double quote |
326 | 0 | --cookieValueEnd; |
327 | 0 | } |
328 | | |
329 | | // cookiename=cookievalue; |
330 | | // ^ ^- cookieValueEnd |
331 | | // \- cookieValueStart |
332 | | // or (double quotes) |
333 | | // cookiename="cookievalue"; |
334 | | // ^ ^- cookieValueEnd |
335 | | // \- cookieValueStart |
336 | | |
337 | 0 | if (sessionIdLength != std::distance(cookieValueStart, cookieValueEnd)) |
338 | 0 | return std::string{}; // Session ID cookie length incorrect! |
339 | 0 | if (!std::all_of(cookieValueStart, cookieValueEnd, is_alphanumeric)) |
340 | 0 | return std::string{}; // Session IDs should be alphanumeric! |
341 | 0 | return std::string(cookieValueStart, sessionIdLength); |
342 | 0 | } |
343 | | |
344 | 0 | start = nextSemicolon + 2; // Skip over '; ' |
345 | 0 | } |
346 | 0 | return std::string{}; |
347 | 0 | } |
348 | | |
349 | | #ifdef WT_THREADED |
350 | | WebController::SocketNotifierMap& |
351 | | WebController::socketNotifiers(WSocketNotifier::Type type) |
352 | 0 | { |
353 | 0 | switch (type) { |
354 | 0 | case WSocketNotifier::Type::Read: |
355 | 0 | return socketNotifiersRead_; |
356 | 0 | case WSocketNotifier::Type::Write: |
357 | 0 | return socketNotifiersWrite_; |
358 | 0 | case WSocketNotifier::Type::Exception: |
359 | 0 | default: // to avoid return warning |
360 | 0 | return socketNotifiersExcept_; |
361 | 0 | } |
362 | 0 | } |
363 | | #endif // WT_THREADED |
364 | | |
365 | | void WebController::socketSelected(int descriptor, WSocketNotifier::Type type) |
366 | 0 | { |
367 | 0 | #ifdef WT_THREADED |
368 | | /* |
369 | | * Find notifier, extract session Id |
370 | | */ |
371 | 0 | std::string sessionId; |
372 | 0 | { |
373 | 0 | std::unique_lock<std::recursive_mutex> lock(notifierMutex_); |
374 | |
|
375 | 0 | SocketNotifierMap ¬ifiers = socketNotifiers(type); |
376 | 0 | SocketNotifierMap::iterator k = notifiers.find(descriptor); |
377 | |
|
378 | 0 | if (k == notifiers.end()) { |
379 | 0 | LOG_ERROR_S(&server_, "socketSelected(): socket notifier should have been " |
380 | 0 | "cancelled?"); |
381 | |
|
382 | 0 | return; |
383 | 0 | } else { |
384 | 0 | sessionId = k->second->sessionId(); |
385 | 0 | } |
386 | 0 | } |
387 | | |
388 | 0 | server_.post(sessionId, std::bind(&WebController::socketNotify, |
389 | 0 | this, descriptor, type)); |
390 | 0 | #endif // WT_THREADED |
391 | 0 | } |
392 | | |
393 | | #ifdef WT_THREADED |
394 | | void WebController::socketNotify(int descriptor, WSocketNotifier::Type type) |
395 | 0 | { |
396 | 0 | WSocketNotifier *notifier = nullptr; |
397 | 0 | { |
398 | 0 | std::unique_lock<std::recursive_mutex> lock(notifierMutex_); |
399 | 0 | SocketNotifierMap ¬ifiers = socketNotifiers(type); |
400 | 0 | SocketNotifierMap::iterator k = notifiers.find(descriptor); |
401 | 0 | if (k != notifiers.end()) { |
402 | 0 | notifier = k->second; |
403 | 0 | notifiers.erase(k); |
404 | 0 | } |
405 | 0 | } |
406 | |
|
407 | 0 | if (notifier) |
408 | 0 | notifier->notify(); |
409 | 0 | } |
410 | | #endif // WT_THREADED |
411 | | |
412 | | void WebController::addSocketNotifier(WSocketNotifier *notifier) |
413 | 0 | { |
414 | 0 | #ifdef WT_THREADED |
415 | 0 | { |
416 | 0 | std::unique_lock<std::recursive_mutex> lock(notifierMutex_); |
417 | 0 | socketNotifiers(notifier->type())[notifier->socket()] = notifier; |
418 | 0 | } |
419 | |
|
420 | 0 | switch (notifier->type()) { |
421 | 0 | case WSocketNotifier::Type::Read: |
422 | 0 | socketNotifier_.addReadSocket(notifier->socket()); |
423 | 0 | break; |
424 | 0 | case WSocketNotifier::Type::Write: |
425 | 0 | socketNotifier_.addWriteSocket(notifier->socket()); |
426 | 0 | break; |
427 | 0 | case WSocketNotifier::Type::Exception: |
428 | 0 | socketNotifier_.addExceptSocket(notifier->socket()); |
429 | 0 | break; |
430 | 0 | } |
431 | 0 | #endif // WT_THREADED |
432 | 0 | } |
433 | | |
434 | | void WebController::removeSocketNotifier(WSocketNotifier *notifier) |
435 | 0 | { |
436 | 0 | #ifdef WT_THREADED |
437 | 0 | switch (notifier->type()) { |
438 | 0 | case WSocketNotifier::Type::Read: |
439 | 0 | socketNotifier_.removeReadSocket(notifier->socket()); |
440 | 0 | break; |
441 | 0 | case WSocketNotifier::Type::Write: |
442 | 0 | socketNotifier_.removeWriteSocket(notifier->socket()); |
443 | 0 | break; |
444 | 0 | case WSocketNotifier::Type::Exception: |
445 | 0 | socketNotifier_.removeExceptSocket(notifier->socket()); |
446 | 0 | break; |
447 | 0 | } |
448 | | |
449 | 0 | std::unique_lock<std::recursive_mutex> lock(notifierMutex_); |
450 | |
|
451 | 0 | SocketNotifierMap ¬ifiers = socketNotifiers(notifier->type()); |
452 | 0 | SocketNotifierMap::iterator i = notifiers.find(notifier->socket()); |
453 | 0 | if (i != notifiers.end()) |
454 | 0 | notifiers.erase(i); |
455 | 0 | #endif // WT_THREADED |
456 | 0 | } |
457 | | |
458 | | bool WebController::requestDataReceived(WebRequest *request, |
459 | | std::uintmax_t current, |
460 | | std::uintmax_t total) |
461 | 0 | { |
462 | 0 | #ifdef WT_THREADED |
463 | 0 | std::unique_lock<std::mutex> lock(uploadProgressUrlsMutex_); |
464 | 0 | #endif // WT_THREADED |
465 | |
|
466 | 0 | if (!running_) |
467 | 0 | return false; |
468 | | |
469 | 0 | if (uploadProgressUrls_.find(request->queryString()) |
470 | 0 | != uploadProgressUrls_.end()) { |
471 | 0 | #ifdef WT_THREADED |
472 | 0 | lock.unlock(); |
473 | 0 | #endif // WT_THREADED |
474 | |
|
475 | 0 | CgiParser cgi(conf_.maxRequestSize(), conf_.maxFormDataSize()); |
476 | |
|
477 | 0 | try { |
478 | 0 | cgi.parse(*request, CgiParser::ReadHeadersOnly); |
479 | 0 | } catch (std::exception& e) { |
480 | 0 | LOG_ERROR_S(&server_, "could not parse request: " << e.what()); |
481 | 0 | return false; |
482 | 0 | } |
483 | | |
484 | 0 | const std::string *wtdE = request->getParameter("wtd"); |
485 | 0 | if (!wtdE) |
486 | 0 | return false; |
487 | | |
488 | 0 | std::string sessionId = *wtdE; |
489 | |
|
490 | 0 | UpdateResourceProgressParams params { |
491 | 0 | str(request->getParameter("request")), |
492 | 0 | str(request->getParameter("resource")), |
493 | 0 | request->postDataExceeded(), |
494 | 0 | request->extraPathInfo().to_string(), |
495 | 0 | current, |
496 | 0 | total |
497 | 0 | }; |
498 | 0 | auto event = std::make_shared<ApplicationEvent>(sessionId, |
499 | 0 | std::bind(&WebController::updateResourceProgress, |
500 | 0 | this, params)); |
501 | |
|
502 | 0 | if (handleApplicationEvent(event)) |
503 | 0 | return !request->postDataExceeded(); |
504 | 0 | else |
505 | 0 | return false; |
506 | 0 | } |
507 | | |
508 | 0 | return true; |
509 | 0 | } |
510 | | |
511 | | void WebController::updateResourceProgress(const UpdateResourceProgressParams ¶ms) |
512 | 0 | { |
513 | 0 | WApplication *app = WApplication::instance(); |
514 | |
|
515 | 0 | WResource *resource = nullptr; |
516 | 0 | if (!params.requestParam.empty() && |
517 | 0 | !params.pathInfo.empty()) { |
518 | 0 | resource = app->decodeExposedResource("/path/" + params.pathInfo); |
519 | 0 | } |
520 | |
|
521 | 0 | if (!resource) { |
522 | 0 | resource = app->decodeExposedResource(params.resourceParam); |
523 | 0 | } |
524 | |
|
525 | 0 | if (resource) { |
526 | 0 | ::int64_t dataExceeded = params.postDataExceeded; |
527 | 0 | if (dataExceeded) |
528 | 0 | resource->dataExceeded().emit(dataExceeded); |
529 | 0 | else |
530 | 0 | resource->dataReceived().emit(params.current, params.total); |
531 | 0 | } |
532 | 0 | } |
533 | | |
534 | | bool WebController::handleApplicationEvent(const std::shared_ptr<ApplicationEvent>& event) |
535 | 0 | { |
536 | | /* |
537 | | * This should always be run from within a virgin thread of the |
538 | | * thread-pool |
539 | | */ |
540 | 0 | assert(!WebSession::Handler::instance()); |
541 | | |
542 | | /* |
543 | | * Find session (and guard it against deletion) |
544 | | */ |
545 | 0 | std::shared_ptr<WebSession> session; |
546 | 0 | { |
547 | 0 | #ifdef WT_THREADED |
548 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
549 | 0 | #endif // WT_THREADED |
550 | |
|
551 | 0 | SessionMap::iterator i = sessions_.find(event->sessionId); |
552 | |
|
553 | 0 | if (i != sessions_.end() && !i->second->dead()) |
554 | 0 | session = i->second; |
555 | 0 | } |
556 | |
|
557 | 0 | if (!session) { |
558 | 0 | if (event->fallbackFunction) |
559 | 0 | event->fallbackFunction(); |
560 | 0 | return false; |
561 | 0 | } else |
562 | 0 | session->queueEvent(event); |
563 | | |
564 | | /* |
565 | | * Try to take the session lock now to propagate the event to the |
566 | | * application. |
567 | | */ |
568 | 0 | { |
569 | 0 | WebSession::Handler handler(session, WebSession::Handler::LockOption::TryLock); |
570 | 0 | } |
571 | |
|
572 | 0 | return true; |
573 | 0 | } |
574 | | |
575 | | void WebController::addUploadProgressUrl(const std::string& url) |
576 | 0 | { |
577 | 0 | #ifdef WT_THREADED |
578 | 0 | std::unique_lock<std::mutex> lock(uploadProgressUrlsMutex_); |
579 | 0 | #endif // WT_THREADED |
580 | |
|
581 | 0 | uploadProgressUrls_.insert(url.substr(url.find("?") + 1)); |
582 | 0 | } |
583 | | |
584 | | void WebController::removeUploadProgressUrl(const std::string& url) |
585 | 0 | { |
586 | 0 | #ifdef WT_THREADED |
587 | 0 | std::unique_lock<std::mutex> lock(uploadProgressUrlsMutex_); |
588 | 0 | #endif // WT_THREADED |
589 | |
|
590 | 0 | uploadProgressUrls_.erase(url.substr(url.find("?") + 1)); |
591 | 0 | } |
592 | | |
593 | | std::string WebController::computeRedirectHash(const std::string& secret, |
594 | | const std::string& url) |
595 | 0 | { |
596 | 0 | return Utils::base64Encode(Utils::md5(secret + url)); |
597 | 0 | } |
598 | | |
599 | | std::string WebController::redirectSecret(const Wt::WebRequest &request) const |
600 | 0 | { |
601 | 0 | #ifndef WT_TARGET_JAVA |
602 | 0 | if (configuration().behindReverseProxy() || |
603 | 0 | configuration().isTrustedProxy(request.remoteAddr())) { |
604 | 0 | const auto secretHeader = request.headerValue(WT_REDIRECT_SECRET_HEADER); |
605 | 0 | if (secretHeader && secretHeader[0] != '\0') { |
606 | 0 | return secretHeader; |
607 | 0 | } |
608 | 0 | } |
609 | 0 | #endif // WT_TARGET_JAVA |
610 | | |
611 | 0 | return redirectSecret_; |
612 | 0 | } |
613 | | |
614 | | void WebController::handleRequest(WebRequest *request) |
615 | 0 | { |
616 | 0 | if (!running_) { |
617 | 0 | request->setStatus(500); |
618 | 0 | request->flush(); |
619 | 0 | return; |
620 | 0 | } |
621 | | |
622 | 0 | if (configuration().useScriptNonce()) { |
623 | 0 | request->addNonce(); |
624 | 0 | } |
625 | |
|
626 | 0 | if (!request->entryPoint_) { |
627 | 0 | EntryPointMatch match = getEntryPoint(request); |
628 | 0 | request->entryPoint_ = match.entryPoint; |
629 | 0 | request->extraStartIndex_ = match.extraStartIndex; |
630 | 0 | if (!request->entryPoint_) { |
631 | 0 | request->setStatus(404); |
632 | 0 | request->flush(); |
633 | 0 | return; |
634 | 0 | } |
635 | 0 | request->urlParams_ = std::move(match.urlParams); |
636 | 0 | } |
637 | | |
638 | 0 | CgiParser cgi(conf_.maxRequestSize(), conf_.maxFormDataSize()); |
639 | |
|
640 | 0 | try { |
641 | 0 | cgi.parse(*request, conf_.needReadBodyBeforeResponse() |
642 | 0 | ? CgiParser::ReadBodyAnyway |
643 | 0 | : CgiParser::ReadDefault); |
644 | 0 | } catch (std::exception& e) { |
645 | 0 | LOG_ERROR_S(&server_, "could not parse request: " << e.what()); |
646 | |
|
647 | 0 | request->setContentType("text/html"); |
648 | 0 | request->out() |
649 | 0 | << "<title>Error occurred.</title>" |
650 | 0 | << "<h2>Error occurred.</h2>" |
651 | 0 | "Error parsing CGI request: " << e.what() << std::endl; |
652 | |
|
653 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
654 | 0 | return; |
655 | 0 | } |
656 | | |
657 | 0 | if (request->entryPoint_->type() == EntryPointType::StaticResource) { |
658 | | // Requests to WWebSocketResources need some spacial handling: |
659 | | // after the handshake is done, the socket is transferred to the |
660 | | // WWebSocketConnection for futher communication. |
661 | 0 | if (request->isWebSocketRequest()) { |
662 | | // Retrieve the static resource |
663 | 0 | WebSocketHandlerResource* wsResource = nullptr; |
664 | 0 | if (request->entryPoint_->resource()) { |
665 | 0 | wsResource = dynamic_cast<WebSocketHandlerResource*>(request->entryPoint_->resource()); |
666 | 0 | } |
667 | 0 | if(!wsResource) { |
668 | | // No static resource found |
669 | 0 | LOG_ERROR("handleRequest: resource '" << request->pathInfo() << "' is not a WWebSocketResource"); |
670 | 0 | request->setStatus(400); |
671 | 0 | request->setContentType("text/html"); |
672 | 0 | request->out() |
673 | 0 | << "<title>Error occurred.</title>" |
674 | 0 | << "<html><body><h1>Not a websocket</h1></body></html>" |
675 | 0 | << std::endl; |
676 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
677 | 0 | return; |
678 | 0 | } else if (!request->supportsTransferWebSocketResourceSocket()) { |
679 | | // The HTTP frontend doesn't allow WWebSocketResources |
680 | 0 | LOG_ERROR("handleRequest: websocket resources not supported by HTTP frontend"); |
681 | 0 | request->setStatus(500); |
682 | 0 | request->setContentType("text/html"); |
683 | 0 | request->out() |
684 | 0 | << "<title>Error occurred.</title>" |
685 | 0 | << "<html><body><h1>WebSocket not supported</h1></body></html>" |
686 | 0 | << std::endl; |
687 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
688 | 0 | return; |
689 | 0 | } |
690 | | // Regular handling, perform the actual handshake. |
691 | 0 | request->entryPoint_->resource()->handle(request, (WebResponse *)request); |
692 | | |
693 | | // If the handshake is successful, transfer the socket. |
694 | 0 | if (request->status() == 101) { |
695 | 0 | request->setTransferWebSocketResourceSocketCallBack(std::bind(&WebSocketHandlerResource::moveSocket, wsResource, std::placeholders::_1, std::placeholders::_2)); |
696 | 0 | } |
697 | 0 | } else { |
698 | | // A regular static resource |
699 | 0 | request->entryPoint_->resource()->handle(request, (WebResponse *)request); |
700 | 0 | } |
701 | 0 | return; |
702 | 0 | } |
703 | | |
704 | 0 | const std::string *requestE = request->getParameter("request"); |
705 | 0 | if (requestE && *requestE == "redirect") { |
706 | 0 | handleRedirect(request); |
707 | 0 | return; |
708 | 0 | } |
709 | | |
710 | 0 | std::string sessionId; |
711 | |
|
712 | 0 | const std::string *wtdE = request->getParameter("wtd"); |
713 | |
|
714 | 0 | const char* userAgent = request->headerValue("User-Agent"); |
715 | | |
716 | | // Block bot agents that request "follow-up" requests |
717 | | // These are requests from the framework, with session tracking or session signals. |
718 | 0 | if (userAgent && conf_.agentIsBot(userAgent)) { |
719 | 0 | const std::string *signalE = request->getParameter("signal"); |
720 | 0 | if (wtdE || signalE) { |
721 | 0 | LOG_INFO_S(&server_, "A request from a bot agent with a wtd or signal attached, was blocked. " |
722 | 0 | "Bots are not allowed to do subsequent requests since their session has been killed."); |
723 | 0 | request->setStatus(403); |
724 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
725 | 0 | return; |
726 | 0 | } |
727 | 0 | } |
728 | | |
729 | 0 | if (conf_.sessionTracking() == Configuration::CookiesURL |
730 | 0 | && !conf_.reloadIsNewSession()) |
731 | 0 | sessionId = sessionFromCookie(request->headerValue("Cookie"), |
732 | 0 | request->scriptName(), |
733 | 0 | conf_.fullSessionIdLength()); |
734 | |
|
735 | 0 | std::string multiSessionCookie; |
736 | 0 | if (conf_.sessionTracking() == Configuration::Combined) |
737 | 0 | multiSessionCookie = sessionFromCookie(request->headerValue("Cookie"), |
738 | 0 | "ms" + request->scriptName(), |
739 | 0 | conf_.sessionIdLength()); |
740 | |
|
741 | 0 | if (sessionId.empty() && wtdE) { |
742 | 0 | sessionId = *wtdE; |
743 | 0 | } |
744 | |
|
745 | 0 | std::shared_ptr<WebSession> session; |
746 | 0 | { |
747 | 0 | #ifdef WT_THREADED |
748 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
749 | 0 | #endif // WT_THREADED |
750 | |
|
751 | 0 | if (!singleSessionId_.empty() && sessionId != singleSessionId_) { |
752 | 0 | if (conf_.persistentSessions()) { |
753 | | // This may be because of a race condition in the filesystem: |
754 | | // the session file is renamed in generateNewSessionId() but |
755 | | // still a request for an old session may have arrived here |
756 | | // while this was happening. |
757 | | // |
758 | | // If it is from the old app, We should be sent a reload signal, |
759 | | // this is what will be done by a new session (which does not create |
760 | | // an application). |
761 | | // |
762 | | // If it is another request to take over the persistent session, |
763 | | // it should be handled by the persistent session. We can distinguish |
764 | | // using the type of the request |
765 | 0 | LOG_INFO_S(&server_, |
766 | 0 | "persistent session requested Id: " << sessionId << ", " |
767 | 0 | << "persistent Id: " << singleSessionId_); |
768 | |
|
769 | 0 | if (sessions_.empty() || strcmp(request->requestMethod(), "GET") == 0) |
770 | 0 | sessionId = singleSessionId_; |
771 | 0 | } else |
772 | 0 | sessionId = singleSessionId_; |
773 | 0 | } |
774 | |
|
775 | 0 | SessionMap::iterator i = sessions_.find(sessionId); |
776 | |
|
777 | 0 | Configuration::SessionTracking sessionTracking = configuration().sessionTracking(); |
778 | |
|
779 | 0 | if (i == sessions_.end() || i->second->dead() || |
780 | 0 | (sessionTracking == Configuration::Combined && |
781 | 0 | (multiSessionCookie.empty() || multiSessionCookie != i->second->multiSessionId()))) { |
782 | 0 | try { |
783 | 0 | if (sessionTracking == Configuration::Combined && |
784 | 0 | i != sessions_.end() && !i->second->dead()) { |
785 | 0 | if (!request->headerValue("Cookie")) { |
786 | 0 | LOG_ERROR_S(&server_, "Valid session id: " << sessionId << ", but " |
787 | 0 | "no cookie received (expecting multi session cookie)"); |
788 | 0 | request->setStatus(403); |
789 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
790 | 0 | return; |
791 | 0 | } |
792 | 0 | } |
793 | | |
794 | 0 | if (request->isWebSocketRequest()) { |
795 | 0 | LOG_INFO_S(&server_, "WebSocket request for non-existing session rejected. " |
796 | 0 | "This is likely because of a browser with an old session " |
797 | 0 | "trying to reconnect (e.g. when the server was restarted)"); |
798 | 0 | request->setStatus(403); |
799 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
800 | 0 | return; |
801 | 0 | } |
802 | | |
803 | 0 | if (singleSessionId_.empty()) { |
804 | 0 | do { |
805 | 0 | sessionId = conf_.generateSessionId(); |
806 | 0 | if (!conf_.registerSessionId(std::string(), sessionId)) |
807 | 0 | sessionId.clear(); |
808 | 0 | } while (sessionId.empty()); |
809 | 0 | } |
810 | |
|
811 | 0 | std::string favicon = request->entryPoint_->favicon(); |
812 | 0 | if (favicon.empty()) |
813 | 0 | conf_.readConfigurationProperty("favicon", favicon); |
814 | |
|
815 | 0 | session.reset(new WebSession(this, sessionId, |
816 | 0 | request->entryPoint_->type(), |
817 | 0 | favicon, request)); |
818 | |
|
819 | 0 | if (sessionTracking == Configuration::Combined) { |
820 | 0 | if (multiSessionCookie.empty()) |
821 | 0 | multiSessionCookie = conf_.generateSessionId(); |
822 | 0 | session->setMultiSessionId(multiSessionCookie); |
823 | 0 | } |
824 | |
|
825 | 0 | if (sessionTracking == Configuration::CookiesURL) |
826 | 0 | request->addHeader("Set-Cookie", |
827 | 0 | appSessionCookie(request->scriptName()) |
828 | 0 | + "=" + sessionId + "; Version=1;" |
829 | 0 | + " Path=" + session->env().deploymentPath() |
830 | 0 | + "; httponly;" + (session->env().urlScheme() == "https" ? " secure;" : "") |
831 | 0 | + " SameSite=Strict;"); |
832 | |
|
833 | 0 | sessions_[sessionId] = session; |
834 | 0 | ++plainHtmlSessions_; |
835 | 0 | #ifdef WT_TEST_VISIBILITY |
836 | 0 | addedSessionId_.emit(sessionId); |
837 | 0 | #endif // WT_TEST_VISIBILITY |
838 | |
|
839 | 0 | if (server_.dedicatedSessionProcess()) { |
840 | 0 | server_.updateProcessSessionId(sessionId); |
841 | 0 | } |
842 | 0 | } catch (std::exception& e) { |
843 | 0 | LOG_ERROR_S(&server_, "could not create new session: " << e.what()); |
844 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
845 | 0 | return; |
846 | 0 | } |
847 | 0 | } else { |
848 | 0 | session = i->second; |
849 | 0 | } |
850 | 0 | } |
851 | | |
852 | 0 | bool handled = false; |
853 | 0 | { |
854 | 0 | WebSession::Handler handler(session, *request, *(WebResponse *)request); |
855 | |
|
856 | 0 | if (!session->dead()) { |
857 | 0 | handled = true; |
858 | 0 | session->handleRequest(handler); |
859 | 0 | } |
860 | 0 | } |
861 | |
|
862 | 0 | if (session->dead()) |
863 | 0 | removeSession(sessionId); |
864 | |
|
865 | 0 | session.reset(); |
866 | |
|
867 | 0 | if (autoExpire_) |
868 | 0 | expireSessions(); |
869 | |
|
870 | 0 | if (!handled) |
871 | 0 | handleRequest(request); |
872 | 0 | } |
873 | | |
874 | | void WebController::handleRedirect(Wt::WebRequest *request) |
875 | 0 | { |
876 | 0 | const std::string *urlE = request->getParameter("url"); |
877 | 0 | const std::string *hashE = request->getParameter("hash"); |
878 | |
|
879 | 0 | if (urlE && hashE) { |
880 | 0 | if (*hashE != computeRedirectHash(redirectSecret(*request), *urlE)) |
881 | 0 | hashE = nullptr; |
882 | 0 | } |
883 | |
|
884 | 0 | if (urlE && hashE) { |
885 | 0 | request->setRedirect(*urlE); |
886 | 0 | } else { |
887 | 0 | request->setContentType("text/html"); |
888 | 0 | request->out() |
889 | 0 | << "<title>Error occurred.</title>" |
890 | 0 | << "<h2>Error occurred.</h2><p>Invalid redirect.</p>" << std::endl; |
891 | 0 | } |
892 | |
|
893 | 0 | request->flush(WebResponse::ResponseState::ResponseDone); |
894 | 0 | } |
895 | | |
896 | | std::unique_ptr<WApplication> WebController |
897 | | ::doCreateApplication(WebSession *session) |
898 | 0 | { |
899 | 0 | std::shared_ptr<const EntryPoint> ep |
900 | 0 | = WebSession::Handler::instance()->request()->entryPoint_; |
901 | |
|
902 | 0 | return ep->appCallback()(session->env()); |
903 | 0 | } |
904 | | |
905 | | EntryPointMatch WebController::getEntryPoint(WebRequest *request) |
906 | 0 | { |
907 | 0 | const std::string& scriptName = request->scriptName(); |
908 | 0 | const std::string& pathInfo = request->pathInfo(); |
909 | |
|
910 | 0 | return conf_.matchEntryPoint(scriptName, pathInfo, true); |
911 | 0 | } |
912 | | |
913 | | std::string |
914 | | WebController::generateNewSessionId(const std::shared_ptr<WebSession>& session) |
915 | 0 | { |
916 | 0 | #ifdef WT_THREADED |
917 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
918 | 0 | #endif // WT_THREADED |
919 | |
|
920 | 0 | std::string newSessionId; |
921 | 0 | do { |
922 | 0 | newSessionId = conf_.generateSessionId(); |
923 | 0 | if (!conf_.registerSessionId(session->sessionId(), newSessionId)) |
924 | 0 | newSessionId.clear(); |
925 | 0 | } while (newSessionId.empty()); |
926 | |
|
927 | 0 | sessions_[newSessionId] = session; |
928 | |
|
929 | 0 | SessionMap::iterator i = sessions_.find(session->sessionId()); |
930 | 0 | sessions_.erase(i); |
931 | |
|
932 | 0 | if (!singleSessionId_.empty()) |
933 | 0 | singleSessionId_ = newSessionId; |
934 | |
|
935 | 0 | return newSessionId; |
936 | 0 | } |
937 | | |
938 | | void WebController::newAjaxSession() |
939 | 0 | { |
940 | 0 | #ifdef WT_THREADED |
941 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
942 | 0 | #endif // WT_THREADED |
943 | |
|
944 | 0 | --plainHtmlSessions_; |
945 | 0 | ++ajaxSessions_; |
946 | 0 | } |
947 | | |
948 | | bool WebController::limitPlainHtmlSessions() |
949 | 0 | { |
950 | 0 | if (conf_.maxPlainSessionsRatio() > 0) { |
951 | 0 | #ifdef WT_THREADED |
952 | 0 | std::unique_lock<std::recursive_mutex> lock(mutex_); |
953 | 0 | #endif // WT_THREADED |
954 | |
|
955 | 0 | if (plainHtmlSessions_ + ajaxSessions_ > conf_.minSessionsForDoS()) |
956 | 0 | return plainHtmlSessions_ > conf_.maxPlainSessionsRatio() |
957 | 0 | * (ajaxSessions_ + plainHtmlSessions_); |
958 | 0 | else |
959 | 0 | return false; |
960 | 0 | } else |
961 | 0 | return false; |
962 | 0 | } |
963 | | |
964 | | } |