/* * This file is a part of Winix * and is distributed under the 2-Clause BSD licence. * Author: Tomasz Sowa */ /* * Copyright (c) 2010-2021, Tomasz Sowa * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include "app.h" #include "plugin.h" #include "misc.h" #include "functions/functions.h" #include "utf8/utf8.h" #include "convert/convert.h" #include "models/migration.h" namespace Winix { App::App() { stdout_is_closed = false; last_sessions_save = std::time(0); fcgi_socket = -1; file_log.set_synchro(&synchro); file_log.set_time_zones(&system.time_zones); log.set_log_buffer(&log_buffer); log.set_file_log(&file_log); // objects dependency for main thread winix_base.set_config(&config); winix_base.set_log_buffer(&log_buffer); winix_base.set_file_log(&file_log); winix_base.set_synchro(&synchro); winix_model.set_dependency(&winix_base); winix_model.set_plugin(&plugin); winix_model.set_model_connector(&model_connector); winix_system.set_dependency(&winix_model); winix_system.set_system(&system); winix_request.set_dependency(&winix_system); winix_request.set_cur(&cur); winix_request.set_session_manager(&session_manager); winix_request.set_locale(&TemplatesFunctions::locale); winix_request.set_model_connector(&model_connector); // ////////////////////////////////// config.SetFileLog(&file_log); config.SetLogBuffer(&log_buffer); // temporary there is only one request cur.request = &req; cur.session = session_manager.GetTmpSession(); cur.mount = system.mounts.GetEmptyMount(); db_conn.set_dependency(&winix_base); db.set_dependency(&winix_base); db.SetConn(db_conn); plugin.set_dependency(&winix_base); plugin.SetDb(&db); //plugin.SetConfig(&config); plugin.SetCur(&cur); plugin.SetSystem(&system); plugin.SetFunctions(&functions); plugin.SetTemplates(&templates); //plugin.SetSynchro(&synchro); plugin.SetSessionManager(&session_manager); plugin.SetWinixRequest(&winix_request); req.SetConfig(&config); req.set_connector(&model_connector); functions.set_dependency(&winix_request); // functions.set_config(&config); // functions.set_file_log(&file_log); // functions.set_model_connector(&model_connector); // functions.set_synchro(&synchro); //functions.SetConfig(&config); functions.SetCur(&cur); functions.SetDb(&db); functions.SetSystem(&system); functions.SetTemplates(&templates); //functions.SetSynchro(&synchro); functions.SetSessionManager(&session_manager); system.set_dependency(&winix_model); //system.SetConfig(&config); system.SetCur(&cur); system.SetDb(&db); //system.SetSynchro(&synchro); system.SetFunctions(&functions); system.SetSessionManager(&session_manager); templates.set_dependency(&winix_request); templates.SetConfig(&config); templates.SetCur(&cur); templates.SetDb(&db); templates.SetSystem(&system); templates.SetFunctions(&functions); templates.SetSessionManager(&session_manager); session_manager.set_dependency(&winix_model); // zobaczyc czy wskoczy do przeciazonej metody w session manager session_manager.SetLastContainer(&system.users.last); session_manager.SetCur(&cur); session_manager.SetSystem(&system); post_multi_parser.set_dependency(&winix_base); post_multi_parser.SetConfig(&config); } void App::InitLoggers() { file_log.init(config.log_file, config.log_stdout, config.log_level, config.log_save_each_line, config.log_time_zone_id); log.SetMaxRequests(config.log_request); } Log & App::GetMainLog() { return log; } void App::InitPlugins() { plugin.LoadPlugins(config.plugins_dir, config.plugin_file); } bool App::InitFCGI(char * sock, char * sock_user, char * sock_group) { if( !wide_to_utf8(config.fcgi_socket, sock, WINIX_OS_PATH_SIZE) ) return false; if( !wide_to_utf8(config.fcgi_socket_user, sock_user, WINIX_OS_USERNAME_SIZE) ) return false; if( !wide_to_utf8(config.fcgi_socket_group, sock_group, WINIX_OS_USERNAME_SIZE) ) return false; return true; } /* * chmod and chown of the socket are set before winix drops privileges */ bool App::InitFCGIChmodChownSocket(char * sock, char * sock_user, char * sock_group) { if( chmod(sock, config.fcgi_socket_chmod) < 0 ) { log << log1 << "App: I cannot chmod a FastCGI socket, check fcgi_socket_chmod in the config" << logend; return false; } passwd * pw = getpwnam(sock_user); if( !pw ) { log << log1 << "App: there is no a user: " << config.fcgi_socket_user << logend; return false; } group * gr = getgrnam(sock_group); if( !gr ) { log << log1 << "App: there is no a group: " << config.fcgi_socket_group << logend; return false; } if( chown(sock, pw->pw_uid, gr->gr_gid) < 0 ) { log << log1 << "App: I cannot chown a FastCGI socket, check fcgi_socket_user " << "and fcgi_socket_group in the config" << logend; return false; } return true; } bool App::InitFCGI() { char sock[WINIX_OS_PATH_SIZE]; char sock_user[WINIX_OS_USERNAME_SIZE]; char sock_group[WINIX_OS_USERNAME_SIZE]; if( !InitFCGI(sock, sock_user, sock_group) ) return false; unlink(sock); fcgi_socket = FCGX_OpenSocket(sock, config.fcgi_socket_listen); if( fcgi_socket < 0 ) { log << log1 << "App: An error during creating a fcgi socket" << logend; return false; } log << log3 << "App: FastCGI socket number: " << fcgi_socket << logend; if( !InitFCGIChmodChownSocket(sock, sock_user, sock_group) ) return false; if( FCGX_Init() != 0 ) { log << log1 << "App: FCGX_Init fails" << logend; return false; } if( FCGX_InitRequest(&fcgi_request, fcgi_socket, FCGI_FAIL_ACCEPT_ON_INTR) != 0 ) { log << log1 << "App: FCGX_InitRequest fails" << logend; return false; } return true; } bool App::DoDatabaseMigration() { bool ok = true; Migration migration; User user; ItemContent item_content; Item item; Group group; ok = ok && Migration::do_migration(&model_connector, migration); ok = ok && Migration::do_migration(&model_connector, user); /* * do migration of ItemContent before Item * Item::do_migration_to_3() requires that ItemContent should have new columns * */ ok = ok && Migration::do_migration(&model_connector, item_content); ok = ok && Migration::do_migration(&model_connector, item); ok = ok && Migration::do_migration(&model_connector, group); return ok; } bool App::TryToMakeDatabaseMigration() { if( config.db_make_migration_if_needed ) { if( !DoDatabaseMigration() ) { if( config.db_stop_if_migration_fails ) { log << log1 << "App: database migration failed, stopping winix" << logend; return false; } } PluginRes res = plugin.Call(nullptr, WINIX_MAKE_DATABASE_MIGRATION); if( config.db_stop_if_migration_fails && res.res_false > 0 ) { log << log1 << "App: database migration of some plugins failed, stopping winix" << logend; return false; } } return true; } bool App::Init() { postgresql_connector.set_conn_param(config.db_database, config.db_user, config.db_pass); postgresql_connector.set_logger(log); postgresql_connector.set_log_queries(config.log_db_query); postgresql_connector.wait_for_connection(); model_connector.set_flat_connector(json_connector); model_connector.set_db_connector(postgresql_connector); model_connector.set_logger(log); model_connector.set_winix_config(&config); model_connector.set_winix_request(&req); model_connector.set_winix_logger(&log); model_connector.set_winix_dirs(&system.dirs); model_connector.set_winix_mounts(&system.mounts); model_connector.set_winix_users(&system.users); model_connector.set_winix_groups(&system.groups); model_connector.set_winix_session_logger(nullptr); // will be set for each request model_connector.set_winix_session(nullptr); // will be set for each request model_connector.set_winix_locale(&TemplatesFunctions::locale); model_connector.set_winix_session_manager(&session_manager); model_connector.set_winix_time_zones(&system.time_zones); model_connector.set_winix_pattern_cacher(&TemplatesFunctions::pattern_cacher); if( !TryToMakeDatabaseMigration() ) return false; db_conn.SetConnParam(config.db_database, config.db_user, config.db_pass); db_conn.WaitForConnection(); db.LogQueries(config.log_db_query); cur.request->Clear(); compress.set_dependency(&winix_base); compress.Init(); system.Init(); functions.Init(); templates.Init(); // init templates after functions are created // init notify after templates (it uses locales from templates) system.notify.ReadTemplates(); session_manager.InitTmpSession(); session_manager.InitBanList(); session_manager.InitCookieEncoding(); session_manager.LoadSessions(); CreateStaticTree(); post_parser.set_dependency(&winix_model); post_parser.LogValueSize(config.log_post_value_size); // post_multi_parser has a pointer to the config cookie_parser.set_dependency(&winix_model); plugin.Call((Session*)0, WINIX_PLUGIN_INIT); return true; } void App::Close() { { Winix::Lock lock(synchro); plugin.Call((Winix::Session*)0, WINIX_CLOSE); session_manager.SaveSessions(); session_manager.DeleteSessions(); cur.request->Clear(); session_manager.UninitTmpSession(); functions.Finish(); // now all sessions are cleared } WaitForThreads(); // now all others threads are terminated } void App::BaseUrlRedirect(int code, bool add_subdomain) { system.PutUrlProto(cur.request->redirect_to); if( add_subdomain && !cur.request->subdomain.empty() ) { cur.request->redirect_to += cur.request->subdomain; cur.request->redirect_to += '.'; } cur.request->redirect_to += config.base_url; cur.request->redirect_to += cur.request->env_request_uri; // cur.request->env_request_uri should not be UrlEncoded because it contains slashes cur.request->redirect_type = code; } bool App::BaseUrlRedirect() { plugin.Call((Session*)0, WINIX_BASE_URL_REDIRECT); if( !cur.request->redirect_to.empty() ) return true; if( !config.base_url_redirect ) return false; if( config.base_url.empty() ) return false; if( cur.request->method == Request::post ) return false; if( config.base_url == cur.request->env_http_host ) return false; BaseUrlRedirect(config.base_url_redirect_code, false); log << log3 << "App: BaseUrlRedirect from: " << cur.request->env_http_host << logend; return true; } void App::CheckIfNeedSSLredirect() { if( cur.request->method == Request::post ) { // something comes via POST, don't do the redirect because you lose the date return; } if( config.use_ssl ) { if( config.use_ssl_only_for_logged_users ) { bool function_need_ssl = (cur.request->function && cur.request->function->need_ssl); if( cur.request->using_ssl ) { if( !cur.session->puser && !function_need_ssl ) { log << log3 << "App: this operation should NOT be used through SSL" << logend; BaseUrlRedirect(config.use_ssl_redirect_code, true); } } else { if( cur.session->puser || function_need_ssl ) { log << log3 << "App: this operation should be used through SSL" << logend; BaseUrlRedirect(config.use_ssl_redirect_code, true); } } } else { /* * use ssl for everyone */ if( !cur.request->using_ssl ) { log << log3 << "App: this operation should be used through SSL" << logend; BaseUrlRedirect(config.use_ssl_redirect_code, true); } } } else { if( cur.request->using_ssl ) { log << log3 << "App: this operation should NOT be used through SSL" << logend; BaseUrlRedirect(config.use_ssl_redirect_code, true); } } } void App::SetLocale() { size_t locale_id; if( cur.session->puser ) { locale_id = cur.session->puser->locale_id; if( !TemplatesFunctions::locale.HasLanguage(locale_id) ) locale_id = config.locale_default_id; } else { locale_id = config.locale_default_id; } TemplatesFunctions::locale.SetCurLang(locale_id); } bool App::CheckAccessFromPlugins() { PluginRes res = plugin.Call(WINIX_CHECK_PLUGIN_ACCESS); if( res.res_false > 0 ) { cur.request->status = WINIX_ERR_PERMISSION_DENIED; log << log2 << "App: access prevented by a plugin" << logend; return false; } return true; } void App::ProcessRequestThrow() { ReadRequest(); // when BaseUrlRedirect() return true we didn't have to set everything in cur.request->Read() // in the future cur.request->Read() can be split and at the beginning only environment variables will be read // and then BaseUrlRedirect() will be called (for performance) if( !BaseUrlRedirect() ) { if( cur.request->env_request_uri.size() <= WINIX_URL_MAX_SIZE ) { functions.Parse(); // parsing directories, files, functions and parameters if( cur.request->function ) { cur.request->function->fun.set_connector(model_connector); // IMPROVEME may would be better to add set_connector() method to functions? cur.request->function->fun.propagate_connector(); } /* * set global connector for now * in the future each thread will have its own model_connector * * don't set connector for item_tab - it will be moved out from request */ cur.request->item.set_connector(model_connector); if( !cur.request->dir_tab.empty() ) { cur.mount = system.mounts.CalcCurMount(); cur.session = session_manager.PrepareSession(); model_connector.set_winix_session(cur.session); functions.CheckFunctionAndSymlink(); // here a function can be changed if( cur.request->function ) { cur.request->function->fun.set_connector(model_connector); cur.request->function->fun.propagate_connector(); } cur.session = session_manager.CheckIfFunctionRequireSession(); model_connector.set_winix_session(cur.session); SetLocale(); if( cur.session->new_session ) { cur.session->plugin_data.Resize(plugin.Size()); plugin.Call(WINIX_SESSION_CREATED); } plugin.Call(WINIX_SESSION_CHANGED); } } else { /* * IMPROVE ME * it will not have the root directory set * so as a response only a blank page is shown * (root directory is set in funcions.Parse()) * * IMPROVE ME * we can add a better return code (http status): * http://www.ietf.org/rfc/rfc2616.txt * "A server SHOULD return 414 (Request-URI Too Long) status if a URI is longer than the server can handle" * */ cur.request->status = WINIX_ERR_PERMISSION_DENIED; log << log1 << "App: the URL is too long: " << cur.request->env_request_uri.size() << logend; } if( cur.request->dir_tab.empty() ) { log << log1 << "App: there is no a root dir (dir_tab is empty), adding a root dir" << logend; Item * root_dir = system.dirs.GetRootDir(); if( root_dir ) { cur.request->dir_tab.push_back(root_dir); cur.request->last_item = cur.request->dir_tab.back(); cur.mount = system.mounts.CalcCurMount(); } else { log << log1 << "App: oops, we do not have a root dir" << logend; } } if( cur.mount->type != system.mounts.MountTypeStatic() ) Make(); } SendAnswer(); } void App::ProcessRequest() { try { cur.request->set_connector(model_connector); model_connector.set_winix_request(cur.request); cur.request->RequestStarts(); system.load_avg.StartRequest(cur.request); log << log2 << config.log_delimiter << logend; ProcessRequestThrow(); SaveSessionsIfNeeded(); cur.request->RequestEnds(); system.load_avg.StopRequest(cur.request); LogRequestTime(); } catch(const std::exception & e) { log << log1 << "App: there was an exception: std::exception: " << e.what() << logend; } catch(const Error & e) { log << log1 << "App: there was an exception: Error: " << e << logend; } catch(...) { log << log1 << "App: there was an unknown exception" << logend; } ClearAfterRequest(); } void App::ClearAfterRequest() { try { plugin.Call(WINIX_END_REQUEST); } catch(...) { log << log1 << "App: an exception from a plugin when clearing after a request" << logend; } try { // simple operations which should not throw an exception json_out_stream.clear(); templates.ClearAfterRequest(); cur.request->Clear(); cur.session->ClearAfterRequest(); cur.session = session_manager.GetTmpSession(); output_8bit.clear(); output_tmp_filtered_stream.clear(); compressed_output.clear(); //html_filtered.clear(); aheader_name.clear(); aheader_value.clear(); cur.mount = system.mounts.GetEmptyMount(); system.mounts.pmount = cur.mount; // IMPROVE ME system.mounts.pmount will be removed // send_data_buf doesn't have to be cleared and it is better to not clear it (optimizing) model_connector.set_winix_request(nullptr); model_connector.set_winix_session(nullptr); model_connector.set_winix_session_logger(nullptr); cur.request->item.set_connector(nullptr); // it is needed? log << logendrequest; } catch(...) { log << log1 << "App: an exception when clearing after a request" << logend; } } void App::Start() { bool was_stop_signal = false; { Winix::Lock lock(synchro); was_stop_signal = synchro.was_stop_signal; } while( !was_stop_signal && FCGX_Accept_r(&fcgi_request) == 0 ) { Winix::Lock lock(synchro); if( synchro.was_stop_signal ) { was_stop_signal = true; FCGX_Finish_r(&fcgi_request); } else { ProcessRequest(); } } } void App::SaveSessionsIfNeeded() { time_t t = std::time(0); if( last_sessions_save + 86400 > t ) return; // saving once a day for safety last_sessions_save = t; session_manager.SaveSessions(); } // !! IMPROVE ME change to a better name void App::UseEzcGenerator() { // if( cur.request->page_generated || !cur.request->redirect_to.empty() || !cur.request->x_sendfile.empty() ) // return; clock_gettime(CLOCK_REALTIME, &cur.request->timespec_ezc_engine_start); templates.SetEzcParameters( cur.request->gen_trim_white, cur.request->gen_skip_new_line, cur.request->gen_use_special_chars); templates.Generate(); clock_gettime(CLOCK_REALTIME, &cur.request->timespec_ezc_engine_stop); timespec diff; calculate_timespec_diff(cur.request->timespec_ezc_engine_start, cur.request->timespec_ezc_engine_stop, diff); pt::TextStream str; timespec_to_stream_with_unit(diff, str); // IMPROVEME in the future Log can be used directly log << log3 << "App: ezc engine took: " << str << logend; } void App::CheckPostRedirect() { if( cur.request->method == Request::post ) { if( cur.request->IsParam(L"postredirect") ) { cur.request->redirect_to = cur.request->ParamValue(L"postredirect"); cur.request->redirect_type = 303; } else if( cur.request->IsPostVar(L"postredirect") ) { cur.request->redirect_to = cur.request->PostVar(L"postredirect"); cur.request->redirect_type = 303; } } } void App::AddDefaultModels() { if( cur.request->function && cur.request->function->register_default_models ) { // may it would be better do not return cur.request by default? cur.request->models.Add(L"request", cur.request); if( cur.session && cur.session->puser ) { cur.request->models.Add(L"user", *cur.session->puser); } if( cur.request->is_item ) { cur.request->models.Add(L"item", cur.request->item); } } } // !! IMPROVE ME change to a better name // may ProcessRequest()? but probably it is already defined... // this method needs some refactoring void App::Make() { if( cur.request->dir_tab.empty() ) { log << log1 << "App: there is no a root dir (dir_tab is empty)" << logend; return; } cur.request->PrepareAnswerType(); if( cur.session->ip_ban && cur.session->ip_ban->IsIPBanned() ) { pt::Date date(cur.session->ip_ban->expires); // IMPROVE ME there is no slog now //slog << logerror << T("this_ip_is_banned_until") << ' ' << date << " UTC" << logend; cur.request->status = WINIX_ERR_PERMISSION_DENIED; } // cur.request->status can be changed by function_parser if( cur.request->status == WINIX_ERR_OK ) plugin.Call(WINIX_PREPARE_REQUEST); // if( cur.request->status == WINIX_ERR_OK ) // functions.CheckFunctionAndSymlink(); CheckAccessFromPlugins(); // !! CHECK ME CheckFunctionAndSymlink can set redirect_to // may it should be tested before calling CheckIfNeedSSLredirect? CheckIfNeedSSLredirect(); if( !cur.request->redirect_to.empty() ) return; AddDefaultModels(); if( cur.request->status == WINIX_ERR_OK ) functions.MakeFunction(); if( cur.session->spam_score > 0 ) log << log1 << "App: spam score: " << cur.session->spam_score << logend; if( cur.request->IsParam(L"noredirect") ) cur.request->redirect_to.clear(); if( cur.request->status == WINIX_ERR_OK ) plugin.Call(WINIX_PROCESS_REQUEST); CheckPostRedirect(); if( !cur.request->redirect_to.empty() ) return; if( cur.request->dir_tab.empty() ) { log << log1 << "App: there is no a root dir (dir_tab is empty -- after calling a function)" << logend; return; } } void App::LogEnvironmentVariables() { for(char ** e = fcgi_request.envp ; *e ; ++e ) log << log1 << "Env: " << *e << logend; } void App::LogEnvironmentHTTPVariables() { if( cur.request->headers_in.is_object() ) { pt::Space::ObjectType::iterator i = cur.request->headers_in.value.value_object.begin(); for( ; i != cur.request->headers_in.value.value_object.end() ; ++i) { log << log1 << "HTTP Env: " << i->first << "="; if( i->second->is_wstr() ) log << *i->second->get_wstr() << logend; else log << "(incorrect value type, expected wstr)" << logend; } } } void App::ParseAcceptHeader(const wchar_t * header_name, const std::wstring & env, std::vector & container, size_t max_len) { accept_base_parser.parse(env, container, max_len); if( !container.empty() ) { log << log3 << "App: " << header_name << " header consists of: "; HeaderValue::log_values(container, log); log << logend; } else { log << log3 << "App: there is no " << header_name << " header" << logend; } } void App::ParseAcceptHeader() { ParseAcceptHeader( Winix::Header::accept, cur.request->env_http_accept, cur.request->accept_mime_types, config.request_max_accept_fields); } void App::ParseAcceptLanguageHeader() { ParseAcceptHeader( Winix::Header::accept_language, cur.request->env_http_accept_language, cur.request->accept_languages, config.request_max_accept_language_fields); } /* * reading the request (without GET parameters in the URL) */ void App::ReadRequest() { ReadEnvVariables(); ReadEnvRemoteIP(); CheckRequestMethod(); CheckSSL(); SetSubdomain(); LogAccess(); ReadEnvHTTPVariables(); ReadPostVars(); if( config.log_env_variables ) LogEnvironmentVariables(); if( config.log_env_http_variables ) LogEnvironmentHTTPVariables(); ParseAcceptHeader(); ParseAcceptLanguageHeader(); accept_encoding_parser.ParseAndLog(cur.request->env_http_accept_encoding, log); cookie_parser.Parse(cur.request->env_http_cookie, cur.request->cookie_tab); CheckIE(); CheckKonqueror(); CheckHtmx(); if( cur.request->using_ssl ) log << log3 << "App: connection secure through SSL" << logend; } void App::SetEnv(const char * name, std::wstring & env) { const char * v = FCGX_GetParam(name, fcgi_request.envp); if( v ) { pt::utf8_to_wide(v, env); } } /* * IMPROVE ME take it from cur.request.headers_in? */ void App::ReadEnvVariables() { SetEnv("REQUEST_METHOD", cur.request->env_request_method); SetEnv("REQUEST_URI", cur.request->env_request_uri); SetEnv("FCGI_ROLE", cur.request->env_fcgi_role); SetEnv("CONTENT_TYPE", cur.request->env_content_type); SetEnv("HTTPS", cur.request->env_https); SetEnv("HTTP_HOST", cur.request->env_http_host); SetEnv("HTTP_USER_AGENT", cur.request->env_http_user_agent); SetEnv("HTTP_COOKIE", cur.request->env_http_cookie); SetEnv("HTTP_ACCEPT_ENCODING", cur.request->env_http_accept_encoding); SetEnv("HTTP_ACCEPT", cur.request->env_http_accept); SetEnv("HTTP_ACCEPT_LANGUAGE", cur.request->env_http_accept_language); } // reading from fastcgi env void App::ReadEnvHTTPVariables() { const char http_prefix[] = "HTTP_"; size_t http_prefix_len = sizeof(http_prefix) / sizeof(char) - 1; // 1 means terminating null character size_t http_headers_saved = 0; for(char ** e = fcgi_request.envp ; *e && http_headers_saved < Request::MAX_INPUT_HEADERS ; ++e) { char * env = *e; if( pt::is_substr_nc("HTTP_", env) ) { env += http_prefix_len; // cookies we have in a different table if( !pt::is_substr_nc("COOKIE=", env) ) { if( SaveEnvHTTPVariable(env) ) { http_headers_saved += 1; } } } } if( http_headers_saved == Request::MAX_INPUT_HEADERS ) { log << log4 << "App: the maximum number of HTTP headers has been reached, skipping the rest" << logend; } } /* * headers in fcgi are in the form of name=value */ bool App::SaveEnvHTTPVariable(const char * env) { // CHECK ME may move to a better place? Request::INPUT_HEADER_VALUE_MAX_LENGTH is a high value char header_name[Request::INPUT_HEADER_NAME_MAX_LENGTH + 1]; char header_value[Request::INPUT_HEADER_VALUE_MAX_LENGTH + 1]; size_t i = 0; for( ; env[i] != 0 && env[i] != '=' && i < Request::INPUT_HEADER_NAME_MAX_LENGTH ; ++i) { header_name[i] = pt::to_lower(env[i]); } header_name[i] = 0; if( env[i] != '=' ) { // too long header name, skipping log << log4 << "App: skipped HTTP header \"" << env << "\" because the header name is too long" << logend; return false; } i += 1; // skipping '=' character size_t h = 0; for( ; env[i] != 0 && h < Request::INPUT_HEADER_VALUE_MAX_LENGTH ; ++i, ++h) { header_value[h] = env[i]; } header_value[h] = 0; if( env[i] != 0 ) { // too long header value, skipping log << log4 << "App: skipped HTTP header \"" << env << "\" because the header value is too long" << logend; return false; } pt::utf8_to_wide(header_name, http_header_name); pt::utf8_to_wide(header_value, http_header_value); cur.request->headers_in.add(http_header_name, http_header_value); http_header_name.clear(); http_header_value.clear(); return true; } void App::ReadEnvRemoteIP() { const char * v = nullptr; if( config.check_proxy_ip_header ) { http_header_name = L"HTTP_"; http_header_name += config.proxy_ip_header; pt::to_upper_emplace(http_header_name); pt::wide_to_utf8(http_header_name, http_header_8bit); v = FCGX_GetParam(http_header_8bit.c_str(), fcgi_request.envp); } else { v = FCGX_GetParam("REMOTE_ADDR", fcgi_request.envp); } if( v ) { cur.request->ip = (int)inet_addr(v); pt::utf8_to_wide(v, cur.request->ip_str); } } void App::CheckRequestMethod() { cur.request->method = Request::unknown_method; if( !cur.request->env_request_method.empty() ) { if( pt::to_lower(cur.request->env_request_method[0]) == 'g' ) cur.request->method = Request::get; else if( pt::to_lower(cur.request->env_request_method[0]) == 'p' ) cur.request->method = Request::post; else if( pt::to_lower(cur.request->env_request_method[0]) == 'h' ) cur.request->method = Request::head; else if( pt::to_lower(cur.request->env_request_method[0]) == 'd' ) cur.request->method = Request::delete_; } } void App::CheckSSL() { // !! CHECK ME // value "on" exists in lighttpd server // make sure that for other servers is "on" too if( config.assume_connection_is_through_ssl ) cur.request->using_ssl = true; else if( pt::is_equal_nc(cur.request->env_https.c_str(), L"on") ) cur.request->using_ssl = true; } void App::CheckHtmx() { // fastcgi will change the header to hx_request cur.request->is_htmx_request = (cur.request->headers_in.has_key(L"HX-Request") || cur.request->headers_in.has_key(L"hx_request")); if( cur.request->is_htmx_request && config.add_header_cache_no_store_in_htmx_request ) { cur.request->out_headers.add(L"Cache-Control", L"no-store, max-age=0"); } } void App::SetSubdomain() { CreateSubdomain(config.base_url.c_str(), cur.request->env_http_host.c_str(), cur.request->subdomain); } void App::LogAccess() { log << log1; log.PrintDate(cur.request->start_date); log << ' ' << cur.request->ip_str << ' ' << cur.request->env_request_method << ' ' << cur.request->env_http_host << cur.request->env_request_uri << ' ' << cur.request->env_http_user_agent << logend; if( !cur.request->subdomain.empty() ) log << log3 << "Subdomain: " << cur.request->subdomain << logend; } void App::ReadPostJson() { char buffer[1024]; const int buffer_len = sizeof(buffer) / sizeof(char) - 1; int read_len; post_buffer.clear(); post_buffer.reserve(1024 * 1024 * 5); // IMPROVEME add to config? cur.request->is_postin_used = true; do { // IMPROVE ME // we can read to pt::TextBuffer and make a pt::JSONToSpaceParser::Parse(pt::TextBuffer &) method read_len = FCGX_GetStr(buffer, buffer_len, fcgi_request.in); if( read_len > 0 ) post_buffer.append(buffer, read_len); } while( read_len == buffer_len ); if( !post_buffer.empty() ) { pt::SpaceParser::Status status = space_parser.parse_json(post_buffer.c_str(), cur.request->post_in); post_buffer.clear(); if( status != pt::SpaceParser::ok ) { log << log1 << "App: cannot parse the input stream as an JSON object"; if( status == pt::SpaceParser::syntax_error ) log << ", syntax error in line: " << space_parser.get_last_parsed_line() << logend; cur.request->post_in.clear(); // return an error (http error of some kind?) } } else { log << log3 << "App: input body empty (skipping parsing)" << logend; } } void App::ReadPostVars() { // CHECKME // what about errors during parsing input? if( cur.request->method == Request::post || cur.request->method == Request::delete_ ) { if( pt::is_substr_nc(L"multipart/form-data", cur.request->env_content_type.c_str()) ) { log << log3 << "App: post content type: multipart/form-data" << logend; post_multi_parser.Parse(fcgi_request.in, cur.request->post_tab, cur.request->post_file_tab); } else if( pt::is_substr_nc(Winix::Header::application_json, cur.request->env_content_type.c_str()) ) { log << log3 << "App: post content type: " << Winix::Header::application_json << logend; ReadPostJson(); } else { // IMPROVE ME may to check a correct content-type header? post_parser.Parse(fcgi_request.in, cur.request->post_tab); } } } void App::CheckIE() { size_t msie = cur.request->env_http_user_agent.find(L"MSIE"); cur.request->browser_msie = (msie != std::wstring::npos); } void App::CheckKonqueror() { size_t kon = cur.request->env_http_user_agent.find(L"Konqueror"); cur.request->browser_konqueror = (kon != std::wstring::npos); } void App::PrepareSessionCookie() { if( !cur.session || cur.session->id==0 ) return; if( config.session_cookie_encode ) { if( !session_manager.EncodeSessionId(cur.session->id, cur.session->id_index, cookie_id_string) ) Toa(cur.session->id, cookie_id_string); } else { Toa(cur.session->id, cookie_id_string); } if( !cur.session->puser || !cur.session->remember_me ) { cur.request->AddCookie(config.http_session_id_name, cookie_id_string); } else { pt::Date expires = cur.request->start_time + config.session_remember_max_idle; cur.request->AddCookie(config.http_session_id_name, cookie_id_string, expires); } } bool App::AddHeader(const wchar_t * name, const wchar_t * value) { if( !cur.request->out_headers.has_key(name) ) { cur.request->out_headers.add(name, value); return true; } return false; } bool App::AddHeader(const std::wstring & name, const std::wstring & value) { if( !cur.request->out_headers.has_key(name) ) { cur.request->out_headers.add(name, value); return true; } return false; } bool App::AddHeader(const wchar_t * name, const pt::WTextStream & value) { if( !cur.request->out_headers.has_key(name) ) { cur.request->out_headers.add_stream(name, value); return true; } return false; } bool App::AddHeader(const std::wstring & name, const pt::WTextStream & value) { if( !cur.request->out_headers.has_key(name) ) { cur.request->out_headers.add_stream(name, value); return true; } return false; } bool App::PrepareHeadersStaticCreateResource(pt::WTextStream & out_path) { size_t i = 0; Item * dir = system.dirs.GetDir(system.mounts.pmount->dir_id); if( !dir ) { log << log1 << "App: cannot find the mount directory" << logend; return false; } size_t how_many_dirs = system.dirs.DirLevel(dir->id); const wchar_t * path = SkipDirs(cur.request->env_request_uri.c_str(), how_many_dirs); // the path begins with a slash only if how_many_dirs is zero while( *path == '/' ) path += 1; while( path[i]!=0 && path[i]!='?' && path[i]!='#' ) ++i; if( i > 0 ) out_path.write(path, i); return true; } void App::PrepareHeadersStatic() { if( PathHasUpDir(cur.request->env_request_uri) ) { log << log1 << "App: incorrect path for a static file" << logend; PrepareHeadersForbidden(); return; } const std::wstring & index_str = system.mounts.pmount->FirstArg(system.mounts.MountParStatic()); size_t index = Toi(index_str); if( index >= config.static_dirs.size() ) { log << log1 << "App: static dir with index " << index << " is not defined in the config" << logend; PrepareHeadersForbidden(); return; } pt::WTextStream path; path << config.static_dirs[index] << L"/"; if( !PrepareHeadersStaticCreateResource(path) ) { PrepareHeadersForbidden(); return; } AddHeader(L"Status", L"200 OK"); /* * FIX ME now we can send full path (apache, lighttpd) and relative path (nginx) * but this feature for mounting static content probably will be removed */ if( AddHeader(config.send_file_header, path) ) log << log2 << "App: sending a file from a static mountpoint: " << path << logend; } void App::PrepareHeaderContentType() { if( !cur.request->out_headers.has_key(Winix::Header::content_type) ) { if( cur.request->container_type == Request::ContainerType::container_json ) { cur.request->out_headers.add(Winix::Header::content_type, Winix::Header::application_json_utf8); } else if( cur.request->container_type == Request::ContainerType::container_xml ) { cur.request->out_headers.add(Winix::Header::content_type, Winix::Header::application_xml_utf8); } else if( cur.request->container_type == Request::ContainerType::container_csv ) { cur.request->out_headers.add(Winix::Header::content_type, Winix::Header::text_csv_utf8); } else if( cur.request->container_type == Request::ContainerType::container_raw ) { if( cur.request->send_bin_stream ) { cur.request->out_headers.add(Winix::Header::content_type, Winix::Header::application_octet_stream); } else { switch( config.content_type_header ) { case 1: cur.request->out_headers.add(Winix::Header::content_type, Winix::Header::application_xhtml_xml_utf8); break; case 2: cur.request->out_headers.add(Winix::Header::content_type, Winix::Header::application_xml_utf8); break; case 0: default: cur.request->out_headers.add(Winix::Header::content_type, Winix::Header::text_html_utf8); } } } } } void App::PrepareHeadersForbidden() { AddHeader(L"Status", L"403 Forbidden"); PrepareHeaderContentType(); } void App::PrepareHeadersRedirect() { switch(cur.request->redirect_type) { case 300: AddHeader(L"Status", L"300 Multiple Choices"); break; case 301: AddHeader(L"Status", L"301 Moved Permanently"); break; case 302: AddHeader(L"Status", L"302 Found"); break; case 307: AddHeader(L"Status", L"307 Temporary Redirect"); break; case 303: default: AddHeader(L"Status", L"303 See Other"); break; } AddHeader(L"Location", cur.request->redirect_to); log << log2 << "App: redirect to: " << cur.request->redirect_to << logend; } void App::PrepareHeadersSendFile() { AddHeader(L"Status", L"200 OK"); if( AddHeader(config.send_file_header, cur.request->x_sendfile) ) log << log2 << "App: sending file: " << cur.request->x_sendfile << logend; } void App::PrepareHeadersCompression(int compress_encoding) { if( compress_encoding == 0 || compress_encoding == 1 ) AddHeader(L"Content-Encoding", L"deflate"); else AddHeader(L"Content-Encoding", L"gzip"); } void App::PrepareHeadersNormal(Header header, size_t output_size) { switch( header ) { case h_404: AddHeader(L"Status", L"404 Not Found"); PrepareHeaderContentType(); break; case h_403: PrepareHeadersForbidden(); break; default: AddHeader(L"Status", L"200 OK"); PrepareHeaderContentType(); } if( output_size != static_cast(-1) ) { pt::WTextStream buf; buf << output_size; AddHeader(L"Content-Length", buf); } } // we can improve SendHeaders and SendCookies methods by checking // whether there is a new line character in either a name or a value // and if such character exists and is being sent to the client it breaks the http headers and content // and if compression is enabled the client's browser will not be able to decompress the stream void App::SendHeaders() { pt::Space::ObjectType::iterator i; pt::Space & headers = cur.request->out_headers; if( headers.is_object() ) { plugin.Call(WINIX_PREPARE_TO_SEND_HTTP_HEADERS, &headers); for(i=headers.value.value_object.begin() ; i != headers.value.value_object.end() ; ++i) { if( i->second->is_wstr() ) { pt::wide_to_utf8(i->first, aheader_name); pt::wide_to_utf8(*i->second->get_wstr(), aheader_value); FCGX_PutS(aheader_name.c_str(), fcgi_request.out); FCGX_PutS(": ", fcgi_request.out); FCGX_PutS(aheader_value.c_str(), fcgi_request.out); FCGX_PutS("\r\n", fcgi_request.out); if( config.log_http_answer_headers ) log << log1 << "HTTP Header: " << aheader_name << ": " << aheader_value << logend; } else { log << log2 << "Skipping HTTP Header: " << i->first << " - it's not a wstr" << logend; } } } } void App::SendCookies() { pt::Space::ObjectType::iterator i; pt::Space & cookies = cur.request->out_cookies; if( cookies.is_object() ) { plugin.Call(WINIX_PREPARE_TO_SEND_HTTP_COOKIES, &cookies); for(i=cookies.value.value_object.begin() ; i != cookies.value.value_object.end() ; ++i) { if( i->second->is_wstr() ) { pt::wide_to_utf8(i->first, aheader_name); pt::wide_to_utf8(*i->second->get_wstr(), aheader_value); FCGX_PutS("Set-Cookie: ", fcgi_request.out); FCGX_PutS(aheader_name.c_str(), fcgi_request.out); FCGX_PutS("=", fcgi_request.out); FCGX_PutS(aheader_value.c_str(), fcgi_request.out); FCGX_PutS("\r\n", fcgi_request.out); if( config.log_http_answer_headers ) log << log1 << "HTTP Header: Set-Cookie: " << aheader_name << "=" << aheader_value << logend; } else { log << log2 << "Skipping Cookie: " << i->first << " - it's not a wstr" << logend; } } } } void App::PrepareHeaders(bool compressing, int compress_encoding, Header header, size_t output_size) { PrepareSessionCookie(); if( cur.request->send_as_attachment ) AddHeader(L"Content-Disposition", L"attachment"); //if( !cur.request->redirect_to.empty() && !cur.request->return_json ) if( !cur.request->redirect_to.empty() ) { PrepareHeadersRedirect(); } else if( system.mounts.pmount->type == system.mounts.MountTypeStatic() ) { PrepareHeadersStatic(); } else if( !cur.request->x_sendfile.empty() ) { PrepareHeadersSendFile(); } else { PrepareHeadersNormal(header, output_size); } if( compressing ) PrepareHeadersCompression(compress_encoding); } int App::SelectDeflateVersion() { if( cur.request->browser_msie ) return 0; // raw deflate else return 1; // deflate } void App::SelectCompression(size_t source_len, bool & compression_allowed, int & compression_encoding) { compression_allowed = false; compression_encoding = 0; if( config.compression && cur.request->redirect_to.empty() && cur.request->x_sendfile.empty() && !cur.request->browser_konqueror && /* !! sprawdzic czy Konqueror bedzie obslugiwal raw deflate */ source_len >= config.compression_page_min_size ) { if( config.compression_encoding == 1 || config.compression_encoding == 10 ) { if( accept_encoding_parser.AcceptDeflate() ) { compression_allowed = true; compression_encoding = SelectDeflateVersion(); } else if( config.compression_encoding == 10 && accept_encoding_parser.AcceptGzip() ) { compression_allowed = true; compression_encoding = 2; // gzip } } if( config.compression_encoding == 2 || config.compression_encoding == 20 ) { if( accept_encoding_parser.AcceptGzip() ) { compression_allowed = true; compression_encoding = 2; // gzip } else if( config.compression_encoding == 20 && accept_encoding_parser.AcceptDeflate() ) { compression_allowed = true; compression_encoding = SelectDeflateVersion(); } } } } bool App::CanSendContent() { if( !cur.request->x_sendfile.empty() ) { // if there is a file to send then we do not send a content return false; } // if( cur.request->return_json ) // { // // if there is a redirect flag then it will be put to info struct // return true; // } if( !cur.request->redirect_to.empty() ) { // if there is a redirect and no json is requred then we do not send the content return false; } /* we don't have to check the HEAD method the server (lighttpd) doesn't send the body of its own */ if( cur.request->method == Request::head ) return false; return true; } App::Header App::GetHTTPStatusCode() { Error status = cur.request->status; Header header = h_200; if( status == WINIX_ERR_NO_ITEM || status == WINIX_ERR_NO_FUNCTION || status == WINIX_ERR_UNKNOWN_PARAM ) { header = h_404; log << log2 << "App: http response: 404 Not Found" << logend; } if( status == WINIX_ERR_PERMISSION_DENIED || status == WINIX_ERR_CANT_CHANGE_USER || status == WINIX_ERR_CANT_CHANGE_GROUP ) { header = h_403; log << log2 << "App: http response: 403 Forbidden" << logend; } if( cur.request->use_200_status_for_not_found_and_permission_denied && (header == h_404 || header == h_403) ) { log << log3 << "App: changing the http response to: 200 OK" << logend; header = h_200; } return header; } bool App::IsRequestedFrame() { if( !config.request_frame_parameter.empty() ) { return cur.request->ParamValuep(config.request_frame_parameter); } return false; } // IMPROVEME // we can send directly from BinaryPage without copying to a temporary buffer // (but there is no an interface in BinaryPage yet) void App::SendData(const BinaryPage & page, FCGX_Stream * out) { const size_t buf_size = 4096; if( send_data_buf.size() != buf_size ) send_data_buf.resize(buf_size); BinaryPage::const_iterator i = page.begin(); BinaryPage::const_iterator end = page.end(); while( i != end ) { size_t s = 0; for( ; i != end && s < buf_size ; ++i, ++s) send_data_buf[s] = *i; if( s > 0 ) FCGX_PutStr(send_data_buf.c_str(), s, out); } } void App::SendAnswer() { output_8bit.clear(); compressed_output.clear(); // may use CanSendContent() method? // what about method HEAD? if( !cur.request->redirect_to.empty() || !cur.request->x_sendfile.empty() ) { Send8bitOutput(output_8bit); // send empty content return; } plugin.Call(WINIX_CONTENT_MAKE); if( cur.request->use_ezc_engine ) { UseEzcGenerator(); } if( cur.request->container_type == Request::ContainerType::container_raw ) { PrepareRawAnswer(); } else if( cur.request->container_type == Request::ContainerType::container_json ) { PrepareJsonAnswer(); } else if( cur.request->container_type == Request::ContainerType::container_xml ) { PrepareXmlAnswer(); } else if( cur.request->container_type == Request::ContainerType::container_csv ) { PrepareCsvAnswer(); } Send8bitOutput(output_8bit); } void App::PrepareRawAnswer() { if( cur.request->send_bin_stream ) { Send8bitOutput(cur.request->out_bin_stream); } else if( cur.request->send_main_stream ) { FilterHtmlIfNeeded(cur.request->out_main_stream.get_buffer(), output_8bit, false); } else if( cur.request->send_all_frames ) { SerializeAllFrames(); } else if( !cur.request->send_frames.empty() ) { SerializeSpecificFrames(); } } void App::PrepareJsonAnswer() { output_8bit << '{'; PrepareContenerizedAnswer(); output_8bit << '}'; } void App::PrepareXmlAnswer() { output_8bit << '<'; pt::esc_to_xml(config.xml_root, output_8bit); output_8bit << '>'; PrepareContenerizedAnswer(); output_8bit << "'; } void App::PrepareCsvAnswer() { PrepareContenerizedAnswer(); } void App::PrepareContenerizedAnswer() { bool put_separator = false; if( cur.request->serialize_models ) { SerializeModels(); put_separator = true; } if( cur.request->send_bin_stream ) { PutSeparatorIfNeeded(put_separator); // IMPLEMENT ME serialize binary stream as base64 and put in 'bin_stream' field pt::WTextStream str; str << "NOT IMPLEMENTED YET"; SerializeStream(str, config.bin_stream_field.c_str()); put_separator = true; } if( cur.request->send_main_stream ) { PutSeparatorIfNeeded(put_separator); SerializeStream(cur.request->out_main_stream.get_buffer(), config.main_stream_field.c_str()); put_separator = true; } if( cur.request->send_all_frames || !cur.request->send_frames.empty() ) { PutSeparatorIfNeeded(put_separator); SerializeFieldJson(config.ezc_frames_field.c_str()); output_8bit << "{"; if( cur.request->send_all_frames ) { SerializeAllFrames(); } else if( !cur.request->send_frames.empty() ) { SerializeSpecificFrames(); } output_8bit << "}"; put_separator = true; } } void App::PutSeparatorIfNeeded(bool put_separator) { if( put_separator ) { switch( cur.request->container_type ) { case Request::ContainerType::container_json: output_8bit << ","; break; case Request::ContainerType::container_xml: break; case Request::ContainerType::container_csv: output_8bit << ";"; break; case Request::ContainerType::container_raw: default: break; } } } void App::SerializeFieldJson(const wchar_t * field_name) { if( field_name ) { output_8bit << '"'; pt::esc_to_json(field_name, output_8bit); output_8bit << "\":"; } } void App::SerializeStream(const pt::WTextStream & input_stream, const wchar_t * field_name) { switch( cur.request->container_type ) { case Request::ContainerType::container_json: SerializeStreamJson(input_stream, field_name); break; case Request::ContainerType::container_xml: SerializeStreamXml(input_stream, field_name); break; case Request::ContainerType::container_csv: SerializeStreamCsv(input_stream, field_name); break; case Request::ContainerType::container_raw: default: FilterHtmlIfNeeded(input_stream, output_8bit, false); break; } } void App::SerializeStreamJson(const pt::WTextStream & input_stream, const wchar_t * field_name) { SerializeFieldJson(field_name); output_8bit << '"'; if( config.html_filter && cur.request->use_html_filter ) { TemplatesFunctions::html_filter.filter(input_stream, output_tmp_filtered_stream, true); pt::esc_to_json(output_tmp_filtered_stream, output_8bit); } else { pt::esc_to_json(input_stream, output_8bit); } output_8bit << '"'; } void App::SerializeStreamXml(const pt::WTextStream & input_stream, const wchar_t * field_name) { if( field_name ) { output_8bit << '<'; pt::esc_to_xml(field_name, output_8bit); output_8bit << '>'; } if( config.html_filter && cur.request->use_html_filter ) { TemplatesFunctions::html_filter.filter(input_stream, output_tmp_filtered_stream, true); pt::esc_to_xml(output_tmp_filtered_stream, output_8bit); } else { pt::esc_to_xml(input_stream, output_8bit); } if( field_name ) { output_8bit << "'; } } void App::SerializeStreamCsv(const pt::WTextStream & input_stream, const wchar_t * field_name) { if( field_name ) { output_8bit << '"'; pt::esc_to_csv(field_name, output_8bit); output_8bit << "\";"; } output_8bit << '"'; if( config.html_filter && cur.request->use_html_filter ) { TemplatesFunctions::html_filter.filter(input_stream, output_tmp_filtered_stream, true); pt::esc_to_csv(output_tmp_filtered_stream, output_8bit); } else { pt::esc_to_csv(input_stream, output_8bit); } output_8bit << "\";\n"; } void App::SerializeAllFrames() { auto i = cur.request->out_streams.streams_map.begin(); bool is_first = true; for( ; i != cur.request->out_streams.streams_map.end() ; ++i) { if( cur.request->container_type == Request::ContainerType::container_json && !is_first ) { output_8bit << ','; } if( cur.request->container_type == Request::ContainerType::container_xml && i->first.empty() ) { log << log2 << "App: I cannot serialize a frame with an empty name to xml (frame skipped)" << logend; } else { SerializeStream(i->second->get_buffer(), i->first.c_str()); } is_first = false; } } void App::SerializeSpecificFrames() { bool is_first = true; for(std::wstring & frame: cur.request->send_frames) { auto i = cur.request->out_streams.streams_map.find(frame); if( i != cur.request->out_streams.streams_map.end() ) { if( cur.request->container_type == Request::ContainerType::container_json && !is_first ) { output_8bit << ','; } SerializeStream(i->second->get_buffer(), frame.c_str()); is_first = false; } else { log << log2 << "App: there is no such a frame: " << frame << logend; } } } void App::SerializeModels() { Ezc::Models::ModelsMap models_map = cur.request->models.GetMap(); auto i = models_map.begin(); bool is_first = true; for( ; i != models_map.end() ; ++i) { if( cur.request->container_type == Request::ContainerType::container_json && !is_first ) { output_8bit << ','; } if( cur.request->container_type == Request::ContainerType::container_xml && i->first.empty() ) { log << log2 << "App: I cannot serialize a model with an empty name to xml (model skipped)" << logend; } else { SerializeModel(i->second, i->first.c_str()); } is_first = false; } } void App::SerializeModel(morm::Wrapper & wrapper, const wchar_t * field_name) { switch( cur.request->container_type ) { case Request::ContainerType::container_json: SerializeModelJson(wrapper, field_name); break; case Request::ContainerType::container_xml: SerializeModelXml(wrapper, field_name); break; case Request::ContainerType::container_csv: SerializeModelCsv(wrapper, field_name); break; case Request::ContainerType::container_raw: default: break; } } void App::SerializeModelJson(morm::Wrapper & wrapper, const wchar_t * field_name) { SerializeFieldJson(field_name); if( wrapper.model ) { serialized_model.clear(); wrapper.model->set_connector(model_connector); wrapper.model->to_text(serialized_model); output_8bit << serialized_model; } if( wrapper.date ) { output_8bit << '"'; wrapper.date->SerializeISO(output_8bit); output_8bit << '"'; } if( wrapper.space_wrapper ) { wrapper.space_wrapper->get_space()->serialize_to_json_stream(output_8bit, false); } if( wrapper.model_container_wrapper ) { wrapper.model_container_wrapper->set_iterator_at_first_model(); bool is_first = true; output_8bit << '['; while( wrapper.model_container_wrapper->is_iterator_correct() ) { if( !is_first ) output_8bit << ','; morm::Model * model = wrapper.model_container_wrapper->get_model(); serialized_model.clear(); model->set_connector(model_connector); model->to_text(serialized_model); output_8bit << serialized_model; wrapper.model_container_wrapper->increment_iterator(); is_first = false; } output_8bit << ']'; } } void App::SerializeModelXml(morm::Wrapper & wrapper, const wchar_t * field_name) { // IMPROVEME log << log2 << "App: serializing models to xml not implemented yet" << logend; } void App::SerializeModelCsv(morm::Wrapper & wrapper, const wchar_t * field_name) { // IMPROVEME log << log2 << "App: serializing models to csv not implemented yet" << logend; } // IMPROVEME // gime me a better name void App::FilterHtmlIfNeeded(const pt::WTextStream & input_stream, BinaryPage & output, bool clear_stream) { if( config.html_filter && cur.request->use_html_filter ) { TemplatesFunctions::html_filter.filter(input_stream, output, clear_stream); } else { pt::wide_stream_to_utf8(input_stream, output, clear_stream); } } void App::Send8bitOutput(BinaryPage & output) { bool compressing = false; int compress_encoding = 0; Header header = GetHTTPStatusCode(); size_t output_size = 0; SelectCompression(output.size(), compressing, compress_encoding); if( config.log_server_answer ) { log << log1 << "App: the server's answer is:\n" << output << "\nApp: end of the server's answer" << logend; } if( compressing ) { compress.Compressing(output, compressed_output, compress_encoding); output_size = compressed_output.size(); } else { output_size = output.size(); } PrepareHeaders(compressing, compress_encoding, header, output_size); SendHeaders(); SendCookies(); FCGX_PutS("\r\n", fcgi_request.out); if( CanSendContent() ) { if( compressing ) SendData(compressed_output, fcgi_request.out); else SendData(output, fcgi_request.out); } } void App::LogUser(const char * msg, uid_t id) { log << log3 << msg << " "; passwd * p = getpwuid(id); if( p ) log << p->pw_name; else log << (int)id; log << logend; } void App::LogGroup(const char * msg, gid_t id, bool put_logend) { log << log3 << msg << " "; group * g = getgrgid(id); if( g ) log << g->gr_name; else log << (int)id; if( put_logend ) log << logend; } void App::LogUsers() { uid_t eid, rid; eid = geteuid(); rid = getuid(); if( eid == rid ) { LogUser("App: effective/real user:", eid); } else { LogUser("App: effective user:", eid); LogUser("App: real user:", rid); } } void App::LogEffectiveGroups(std::vector & tab) { log << log3 << "App: effective groups:"; for(size_t i=0 ; i tab; gid_t rgid; int len; rgid = getgid(); len = getgroups(0, 0); if( len <= 0 ) { log << log3 << "App: I can't read how many groups there are" << logend; return; } tab.resize(len); len = getgroups(len, &(tab[0])); if( len == -1 ) { log << log3 << "App: I can't read groups" << logend; return; } if( len == 1 && rgid == tab[0] ) { LogGroup("App: effective/real group:", rgid); } else { tab.resize(len); LogEffectiveGroups(tab); LogGroup("App: real group:", rgid); } } void App::LogUserGroups() { LogUsers(); LogGroups(); log << log3 << "base_url: " << config.base_url << logend; } void App::LogRequestTime() { pt::TextStream str; timespec_to_stream_with_unit(cur.request->timespec_req_diff, str); log << log2 << "App: request took: " << str << logend; } bool App::DropPrivileges(char * user, char * group) { if( !wide_to_utf8(config.user, user, WINIX_OS_USERNAME_SIZE) ) return false; if( !wide_to_utf8(config.group, group, WINIX_OS_USERNAME_SIZE) ) return false; return true; } bool App::DropPrivileges(const char * user, uid_t uid, gid_t gid, bool additional_groups) { if( additional_groups ) { if( initgroups(user, gid) < 0 ) { log << log1 << "App: I can't init groups for a user: " << user << logend; return false; } } else { if( setgroups(1, &gid) < 0 ) { log << log1 << "App: I can't init groups for user: " << user << logend; return false; } } // for setting real and saved gid too if( setgid(gid) < 0 ) { log << log1 << "App: I can't change real and saved gid" << logend; return false; } if( setuid(uid) < 0 ) { log << log1 << "App: I can't drop privileges to user: " << user << " (uid:" << (int)uid << ")" << logend; return false; } if( getuid()==0 || geteuid()==0 ) { log << log1 << "App: sorry, for security reasons you should not run me as the root" << logend; return false; } return true; } bool App::DropPrivileges() { char user_name[WINIX_OS_USERNAME_SIZE]; char group_name[WINIX_OS_USERNAME_SIZE]; if( getuid()!=0 && geteuid()!=0 ) return true; log << log2 << "App: dropping privileges" << logend; if( config.user.empty() ) { log << log1 << "App: in the config file you should specify a user name and a group " << "to which I have to drop privileges" << logend; return false; } if( config.group.empty() ) { log << log1 << "App: you should specify a group name in the config file " << "to which I have to drop privileges" << logend; return false; } if( !DropPrivileges(user_name, group_name) ) return false; passwd * p = getpwnam(user_name); group * g = getgrnam(group_name); if( !p ) { log << log1 << "App: there is no such a user as: \"" << config.user << "\"" << logend; return false; } if( !g ) { log << log1 << "App: there is no such a group as: \"" << config.group << "\"" << logend; return false; } if( !DropPrivileges(user_name, p->pw_uid, g->gr_gid, config.additional_groups) ) return false; return true; } bool App::Demonize() { // in linux fork() should be used twice int pid = fork(); if( pid == -1 ) { log << log1 << "App: I can't demonize myself (fork problem)" << logend; return false; } if( pid != 0 ) { // parent exit(0); } // child if( setsid() == -1 ) { log << log1 << "App: I can't demonize myself (setsid problem)" << logend; return false; } return true; } // this method is called from a signal routine void App::SetStopSignal() { synchro.was_stop_signal = true; } bool App::WasStopSignal() { return synchro.was_stop_signal; } bool App::Lock() { return synchro.Lock(); } void App::Unlock() { synchro.Unlock(); } void App::WaitForThreads() { pthread_join(signal_thread, 0); system.thread_manager.StopAll(); } /* we send a one FastCGI record at the end when winix closes (to wake up the main thread) this method is called from the special thread typedef struct { unsigned char version; unsigned char type; unsigned char requestIdB1; unsigned char requestIdB0; unsigned char contentLengthB1; unsigned char contentLengthB0; unsigned char paddingLength; unsigned char reserved; unsigned char contentData[contentLength]; unsigned char paddingData[paddingLength]; } FCGI_Record; */ void App::SendEmptyFastCGIPacket() { std::string msg; sockaddr_un to; int res; int s = socket(PF_LOCAL, SOCK_STREAM, 0); if( s < 0 ) return; memset(&to, 0, sizeof(to)); #ifdef __FreeBSD__ to.sun_len = offsetof(sockaddr_un, sun_path) + socket_to_send_on_exit.size() + 1; // terminating zero #endif to.sun_family = AF_UNIX; snprintf(to.sun_path, sizeof(to.sun_path)/sizeof(char), "%s", socket_to_send_on_exit.c_str()); // actually we can send one byte only msg += FCGI_VERSION_1; msg += FCGI_GET_VALUES; msg += (char)0; // requestid msg += (char)0; msg += (char)0; // contentlength msg += (char)0; msg += (char)0; // padding length msg += (char)0; msg += (char)0; // reserved res = connect(s, (sockaddr*)&to, sizeof(to)); if( res == 0 ) { send(s, msg.c_str(), msg.size(), 0); } close(s); } void * App::SpecialThreadForSignals(void * app_object) { sigset_t set; int sig; App * app = reinterpret_cast(app_object); sigemptyset(&set); sigaddset(&set, SIGTERM); sigaddset(&set, SIGINT); // waiting for SIGTERM or SIGINT sigwait(&set, &sig); app->Lock(); app->synchro.was_stop_signal = true; FCGX_ShutdownPending(); pt::wide_to_utf8(app->config.fcgi_socket, app->socket_to_send_on_exit); app->Unlock(); app->SendEmptyFastCGIPacket(); pthread_exit(0); return 0; } void App::StartThreads() { // make sure system.thread_manager.Init() was called beforehand // it is called in Init() -> system.Init() // special thread only for signals pthread_create(&signal_thread, 0, SpecialThreadForSignals, this); system.thread_manager.Add(&session_manager, L"session_manager"); system.thread_manager.StartAll(); } void App::CreateStaticTree() { if( config.upload_dir.empty() ) { log << log1 << "App: config: upload_dir not set, you are not allowed to upload static content" << logend; return; } CreateDirs(L"/", config.upload_dir.c_str(), config.upload_dirs_chmod); CreateDirs(config.upload_dir.c_str(), L"simplefs/normal", config.upload_dirs_chmod); CreateDirs(config.upload_dir.c_str(), L"simplefs/thumb", config.upload_dirs_chmod); CreateDirs(config.upload_dir.c_str(), L"hashfs/normal", config.upload_dirs_chmod); CreateDirs(config.upload_dir.c_str(), L"hashfs/thumb", config.upload_dirs_chmod); CreateDirs(config.upload_dir.c_str(), L"tmp", config.upload_dirs_chmod); } } // namespace Winix