add config options: db_startup_connection_max_attempts - default 0 (infinite) db_startup_connection_attempt_delay - delay in seconds between attempts (default 5) BREAKING CHANGE: WINIX_PLUGIN_INIT plugin message requires to set result status, you have to set the result status to true (env.res) if your plugin was initialized correctly, otherwise winix will not start
2866 lines
64 KiB
C++
2866 lines
64 KiB
C++
/*
|
|
* This file is a part of Winix
|
|
* and is distributed under the 2-Clause BSD licence.
|
|
* Author: Tomasz Sowa <t.sowa@ttmath.org>
|
|
*/
|
|
|
|
/*
|
|
* Copyright (c) 2010-2022, 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 <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <utility>
|
|
#include <fastcgi.h>
|
|
#include <stddef.h>
|
|
#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::LoadPlugins()
|
|
{
|
|
plugin.LoadPlugins(config.plugins_dir, config.plugin_file);
|
|
}
|
|
|
|
|
|
bool App::TranslateFCGInames(char * sock, char * sock_user, char * sock_group)
|
|
{
|
|
if( !wide_to_utf8(config.fcgi_socket, sock, WINIX_OS_PATH_SIZE) )
|
|
{
|
|
log << log1 << "App: I cannot correctly change FastCGI socket path to utf-8 string" << logend;
|
|
return false;
|
|
}
|
|
|
|
if( config.fcgi_set_socket_owner )
|
|
{
|
|
if( !wide_to_utf8(config.fcgi_socket_user, sock_user, WINIX_OS_USERNAME_SIZE) )
|
|
{
|
|
log << log1 << "App: I cannot correctly change FastCGI user name to utf-8 string" << logend;
|
|
return false;
|
|
}
|
|
|
|
if( !wide_to_utf8(config.fcgi_socket_group, sock_group, WINIX_OS_USERNAME_SIZE) )
|
|
{
|
|
log << log1 << "App: I cannot correctly change FastCGI group name to utf-8 string" << logend;
|
|
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( config.fcgi_set_socket_chmod )
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
if( config.fcgi_set_socket_owner )
|
|
{
|
|
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( !TranslateFCGInames(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;
|
|
|
|
migration.set_connector(model_connector);
|
|
migration.create_winix_schema();
|
|
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::InitializePlugins()
|
|
{
|
|
PluginRes plugin_res = plugin.Call((Session*)0, WINIX_PLUGIN_INIT);
|
|
|
|
if( plugin_res.res_false > 0 )
|
|
{
|
|
log << log1 << "App: " << plugin_res.res_false << " plugin(s) cannot initialize itself, exiting" << logend;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool App::Init()
|
|
{
|
|
// load plugins before loading sessions - session_manager.LoadSessions()
|
|
// because some of the plugins can init its own sessions dates
|
|
LoadPlugins();
|
|
|
|
if( !config.db_conn_string.empty() )
|
|
postgresql_connector.set_conn_param(config.db_conn_string);
|
|
else
|
|
postgresql_connector.set_conn_param(config.db_host, config.db_hostaddr, config.db_port, config.db_database, config.db_user, config.db_pass);
|
|
|
|
postgresql_connector.set_logger(log);
|
|
postgresql_connector.set_log_queries(config.log_db_query);
|
|
|
|
if( !postgresql_connector.wait_for_connection(config.db_startup_connection_max_attempts, config.db_startup_connection_attempt_delay) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
|
|
// CHECKME this will call WINIX_MAKE_DATABASE_MIGRATION, but WINIX_PLUGIN_INIT was not called yet, it is correct?
|
|
if( !TryToMakeDatabaseMigration() )
|
|
return false;
|
|
|
|
// will be removed
|
|
if( !config.db_conn_string.empty() )
|
|
db_conn.SetConnParam(config.db_conn_string);
|
|
else
|
|
db_conn.SetConnParam(config.db_host, config.db_hostaddr, config.db_port, config.db_database, config.db_user, config.db_pass);
|
|
|
|
if( !db_conn.WaitForConnection(config.db_startup_connection_max_attempts, config.db_startup_connection_attempt_delay) )
|
|
return false;
|
|
|
|
db.LogQueries(config.log_db_query);
|
|
|
|
cur.request->Clear();
|
|
compress.set_dependency(&winix_base);
|
|
compress.Init();
|
|
|
|
if( !system.Init() )
|
|
return false;
|
|
|
|
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);
|
|
|
|
if( !AddSystemThreads() )
|
|
return false;
|
|
|
|
return InitializePlugins();
|
|
}
|
|
|
|
|
|
|
|
void App::Close()
|
|
{
|
|
{
|
|
Winix::Lock lock(synchro);
|
|
|
|
plugin.Call((Winix::Session*)0, WINIX_PREPARE_TO_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
|
|
|
|
/*
|
|
* at the moment plugins are removed when winix is finishing
|
|
* but in the future we can add plug-in removal before the end
|
|
*/
|
|
plugin.Call(nullptr, WINIX_PLUGIN_QUIT);
|
|
|
|
/*
|
|
* this is the last message for plugins
|
|
*/
|
|
plugin.Call(nullptr, WINIX_QUIT);
|
|
}
|
|
|
|
|
|
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<HeaderValue> & 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<size_t>(-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 && cur.request->send_bin_stream )
|
|
{
|
|
Send8bitOutput(cur.request->out_bin_stream);
|
|
return;
|
|
}
|
|
else
|
|
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_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 << "</";
|
|
pt::esc_to_xml(config.xml_root, output_8bit);
|
|
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 << "</";
|
|
pt::esc_to_xml(field_name, output_8bit);
|
|
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<gid_t> & tab)
|
|
{
|
|
log << log3 << "App: effective groups:";
|
|
|
|
for(size_t i=0 ; i<tab.size() ; ++i)
|
|
{
|
|
bool was_printed = false;
|
|
|
|
for(size_t x=0 ; x<i ; ++x)
|
|
{
|
|
if( tab[i] == tab[x] )
|
|
{
|
|
was_printed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !was_printed )
|
|
LogGroup("", tab[i], false);
|
|
}
|
|
|
|
log << logend;
|
|
}
|
|
|
|
|
|
void App::LogGroups()
|
|
{
|
|
std::vector<gid_t> 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*>(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;
|
|
}
|
|
|
|
|
|
bool App::AddSystemThreads()
|
|
{
|
|
bool ok = true;
|
|
|
|
ok = ok && system.thread_manager.Add(&session_manager, L"session_manager");
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
// start all threads from thread 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
|
|
|