/* * This file is a part of Winix * and is not publicly distributed * * Copyright (c) 2010, Tomasz Sowa * All rights reserved. * */ #include #include #include #include #include #include #include #include #include "app.h" #include "plugin.h" #include "misc.h" #include "functions/functions.h" #include "utf8.h" App::App() { stdout_is_closed = false; last_sessions_save = std::time(0); fcgi_socket = -1; db.SetConn(db_conn); plugin.SetDb(&db); plugin.SetConfig(&config); plugin.SetRequest(&request); plugin.SetSystem(&system); plugin.SetFunctions(&functions); plugin.SetTemplates(&templates); plugin.SetSessionManager(&session_manager); request.SetConfig(&config); functions.SetConfig(&config); functions.SetRequest(&request); functions.SetDb(&db); functions.SetSystem(&system); functions.SetTemplates(&templates); system.SetConfig(&config); system.SetRequest(&request); system.SetDb(&db); system.SetSynchro(&synchro); templates.SetConfig(&config); templates.SetRequest(&request); templates.SetDb(&db); templates.SetSystem(&system); templates.SetFunctions(&functions); templates.SetSessionManager(&session_manager); session_manager.SetLastContainer(&system.users.last); session_manager.SetConfig(&config); session_manager.SetRequest(&request); session_manager.SetSystem(&system); post_multi_parser.SetConfig(&config); } bool App::InitFCGI() { const char * sock = config.fcgi_socket.c_str(); unlink(sock); fcgi_socket = FCGX_OpenSocket(sock, 100); // !! dodac 100 do konfiga 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; chmod(sock, config.fcgi_socket_chmod); passwd * pw = getpwnam(config.fcgi_socket_user.c_str()); if( !pw ) { log << log1 << "App: there is no user: " << config.fcgi_socket_user << logend; return false; } group * gr = getgrnam(config.fcgi_socket_group.c_str()); if( !gr ) { log << log1 << "App: there is no group: " << config.fcgi_socket_group << logend; return false; } chown(sock, pw->pw_uid, gr->gr_gid); 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::Init() { db_conn.SetConnParam(config.db_database, config.db_user, config.db_pass); db_conn.WaitForConnection(); db.LogQueries(config.log_db_query); request.Clear(); compress.Init(); system.Init(); functions.Init(); // init templates after functions are created templates.CreateFunctions(); // create functions first (functions will be cached by patterns) templates.ReadIndexFileNames(); templates.ReadTemplates(); // init notify after templates (it uses locales from templates) system.notify.ReadTemplates(); session_manager.LoadSessions(); plugin.Call(WINIX_PLUGIN_INIT); return true; } void App::Close() { session_manager.SaveSessions(); session_manager.DeleteSessions(); request.Clear(); } bool App::BaseUrlRedirect() { if( request.role == Request::responder ) { if( config.base_url_http_host.empty() ) return false; if( Equal(config.base_url_http_host.c_str(), request.env_http_host) ) return false; request.redirect_to = config.base_url; AssignString(request.env_request_uri, request.redirect_to, false); } else { // authorizer if( config.base_url_auth_http_host.empty() ) return false; if( Equal(config.base_url_auth_http_host.c_str(), request.env_http_host) ) return false; request.redirect_to = config.base_url_auth; AssignString(request.env_request_uri, request.redirect_to, false); } log << log3 << "RC: BaseUrlRedirect from: " << request.env_http_host << logend; return true; } void App::ProcessRequestThrow() { ReadRequest(); // when BaseUrlRedirect() return true we didn't have to set everything in request.Read() // in the future 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() ) { session_manager.SetSession(); // set request.session as well // !! dac zeby zwracalo wskaznik (zawsze prawidlowy) na sesje // i tutaj przypisanie do request.session // !! tutaj dodac to ustawianie request.session functions.Parse(); system.mounts.CalcCurMount(); Make(); } SendAnswer(); // it's better to remove sessions at the end of a request // as it can take a little time session_manager.DeleteOldSessions(); } void App::ProcessRequest() { try { system.load_avg.StartRequest(); log << log2 << config.log_delimiter << logend; ProcessRequestThrow(); SaveSessionsIfNeeded(); if( request.function ) request.function->Clear(); request.Clear(); system.load_avg.StopRequest(); log << logsave; } 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; } } void App::Start() { while( !synchro.was_stop_signal && FCGX_Accept_r(&fcgi_request) == 0 ) { Lock(); if( !synchro.was_stop_signal ) ProcessRequest(); Unlock(); } } 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(); } // !! zmienic na lepsza nazwe void App::MakePage() { bool sent = false; if( !request.redirect_to.empty() || !request.x_sendfile.empty() ) return; if( request.is_item && request.item.auth == Item::auth_none && request.item.content_type == Item::ct_raw && request.status == WINIX_ERR_OK && request.function ) { if( request.function == &functions.fun_cat ) { request.page << request.item.content; sent = true; } else if( request.function == &functions.fun_run ) { templates.GenerateRunRaw(); sent = true; } } if( !sent ) { templates.Generate(); } } // zmienic nazwe np na ProcessRequest // !! ta nazwa chyba juz zajeta... void App::Make() { if( request.dir_tab.empty() ) { log << log1 << "Content: there is no a root dir (dir_tab is empty)" << logend; return; } // request.status can be changed by function_parser if( request.status == WINIX_ERR_OK ) { plugin.Call(WINIX_PREPARE_REQUEST); if( system.DirsHaveReadExecPerm() ) { if( request.method == Request::post ) functions.MakePost(); if( request.redirect_to.empty() && request.status == WINIX_ERR_OK ) functions.MakeGet(); } else request.status = WINIX_ERR_PERMISSION_DENIED; } if( request.session->spam_score > 0 ) log << log1 << "App: spam score: " << request.session->spam_score << logend; if( request.IsParam(L"noredirect") ) request.redirect_to.clear(); if( request.status == WINIX_ERR_OK ) plugin.Call(WINIX_PROCESS_REQUEST); if( !request.redirect_to.empty() ) return; if( request.dir_tab.empty() ) { log << log1 << "App: there is no a root dir (dir_tab is empty -- after calling a function)" << logend; return; } plugin.Call(WINIX_CONTENT_MAKE); MakePage(); if( config.debug_info ) { // !! dodac inne informacje (get, post, itp) // jesli jest debug_info wlaczone to nie robic przekierowan request.PrintGetTab(); //request.PrintEnv(); // !! PrintEnv() mozna przeniesc tutaj (do klasy App) } } void App::ReadRequest() { ReadEnvVariables(); CheckRequestMethod(); CheckFCGIRole(); LogAccess(); ReadGetPostVars(); cookie_parser.Parse(request.env_http_cookie, request.cookie_tab); accept_encoding_parser.Parse(request.env_http_accept_encoding); CheckIE(); CheckKonqueror(); if( request.role == Request::authorizer ) log << log3 << "Request: fast cgi role: authorizer" << logend; } void App::SetEnv(const char * & env, const char * name) { const char * v = FCGX_GetParam(name, fcgi_request.envp); if( v ) env = v; // by default env is set to an empty string (in request.Clear() method) } void App::ReadEnvVariables() { // we store that values because FCGX_GetParam has O(n) complexity // with this variables (env_*) we have O(1) SetEnv(request.env_request_method, "REQUEST_METHOD"); // !! mozna nie uzywac tego, teraz mamy w strukturze fcgi_request SetEnv(request.env_request_uri, "REQUEST_URI"); SetEnv(request.env_http_cookie, "HTTP_COOKIE"); SetEnv(request.env_remote_addr, "REMOTE_ADDR"); SetEnv(request.env_http_host, "HTTP_HOST"); SetEnv(request.env_http_user_agent, "HTTP_USER_AGENT"); SetEnv(request.env_fcgi_role, "FCGI_ROLE"); SetEnv(request.env_content_type, "CONTENT_TYPE"); SetEnv(request.env_http_accept_encoding,"HTTP_ACCEPT_ENCODING"); } void App::CheckRequestMethod() { request.method = Request::none; if( ToSmall(request.env_request_method[0]) == 'g' ) request.method = Request::get; else if( ToSmall(request.env_request_method[0]) == 'p' ) request.method = Request::post; else if( ToSmall(request.env_request_method[0]) == 'h' ) request.method = Request::head; } void App::CheckFCGIRole() { // default we assume 'responder' request.role = Request::responder; if( ToSmall(request.env_fcgi_role[0]) == 'a' ) request.role = Request::authorizer; } void App::LogAccess() { log.PutDate(log1); log << request.env_remote_addr << ' ' << request.env_request_method << ' ' << request.env_http_host << request.env_request_uri << ' ' << request.env_http_user_agent << logend; } void App::ReadGetPostVars() { // get parameters we have always get_parser.UTF8(config.utf8); get_parser.Parse(request.env_request_uri, request.get_tab); if( request.method == Request::post ) { if( IsSubStringNoCase("multipart/form-data", request.env_content_type) ) { log << log3 << "Request: post content type: multipart/form-data" << logend; // !! dodac metode UTF8 do post_multi_parsera // (narazie bierze bezposrednio z konfigu) // w ogole wywalic zaleznosc od konfiga post_multi_parser.Parse(fcgi_request.in, request.post_tab, request.post_file_tab); } else { post_parser.UTF8(config.utf8); post_parser.Parse(fcgi_request.in, request.post_tab); } } } void App::CheckIE() { char * msie = strstr(request.env_http_user_agent, "MSIE"); if( msie ) request.browser_msie = true; else request.browser_msie = false; } void App::CheckKonqueror() { char * kon = strstr(request.env_http_user_agent, "Konqueror"); if( kon ) request.browser_konqueror = true; else request.browser_konqueror = false; } void App::PrepareSessionCookie() { if( !request.session || request.session->id==0 ) return; if( !request.session->puser || !request.session->remember_me ) { request.SetCookie(config.http_session_id_name.c_str(), request.session->id); } else { time_t t = std::time(0) + config.session_remember_max_idle; tm expires = Time(t); request.SetCookie(config.http_session_id_name.c_str(), request.session->id, &expires); } } void App::SendHeaders(bool compressing, Header header) { PrepareSessionCookie(); if( request.send_as_attachment ) FCGX_PutS("Content-Disposition: attachment\r\n", fcgi_request.out); if( !request.redirect_to.empty() ) { FCGX_PutS("Status: 301 Moved Permanently\r\n", fcgi_request.out); AssignString(request.redirect_to, request.aredirect_to); FCGX_FPrintF(fcgi_request.out, "Location: %s\r\n", request.aredirect_to.c_str()); log << log2 << "Redirect to: " << request.redirect_to << logend; } else if( !request.x_sendfile.empty() ) { static std::string temp, temp2; // !! wrzucic gdzies to AssignString(config.http_header_send_file, temp); AssignString(request.x_sendfile, temp2); FCGX_FPrintF(fcgi_request.out, "%s: %s\r\n", temp.c_str(), temp2.c_str()); FCGX_PutS("Status: 200 OK\r\n", fcgi_request.out); log << log2 << "Sending file: " << request.x_sendfile << logend; } else { switch( header ) { case h_404: FCGX_PutS("Status: 404 Not Found\r\n", fcgi_request.out); FCGX_PutS("Content-Type: text/html\r\n", fcgi_request.out); log << log2 << "Request: response: 404 Not Found" << logend; break; case h_403: FCGX_PutS("Status: 403 Forbidden\r\n", fcgi_request.out); FCGX_PutS("Content-Type: text/html\r\n", fcgi_request.out); log << log2 << "Request: response: 403 Forbidden" << logend; break; default: FCGX_PutS("Status: 200 OK\r\n", fcgi_request.out); if( request.role != Request::authorizer ) FCGX_PutS("Content-Type: text/html\r\n", fcgi_request.out); } } if( compressing ) FCGX_PutS("Content-Encoding: deflate\r\n", fcgi_request.out); FCGX_PutS(request.headers.CStr(), fcgi_request.out); FCGX_PutS("\r\n", fcgi_request.out); } void App::SetHtmlFilterConf() { html_filter.TrimWhite(config.html_filter_trim_white); html_filter.BreakLines(config.html_filter_break_lines); html_filter.InsertTabs(config.html_filter_tabs); if( config.html_filter_orphans ) html_filter.CheckOrphans(config.html_filter_orphans_lang, config.html_filter_orphans_mode); } // !! kopiowanie tych stringow bedzie zmienione // gdy bedziemy korzystac w przyszlosci z wlasnego stringstream void App::FilterCompressSend(bool compressing, const std::wstring & source_ref) { const std::wstring * source = &source_ref; bool raw = request.is_item && request.item.content_type == Item::ct_raw && request.status == WINIX_ERR_OK && request.function && (request.function == &functions.fun_cat || request.function == &functions.fun_run); if( config.html_filter && !raw ) { SetHtmlFilterConf(); html_filter.Filter(*source, clean_html); AddDebugInfo(clean_html); source = &clean_html; } else { html_with_debug = *source; AddDebugInfo(html_with_debug); source = &html_with_debug; } // !! zrobic z tym porzadek std::string temp; Ezc::WideToUTF8(*source, temp); if( compressing ) compress.CompressAndPut(temp.c_str(), temp.length(), fcgi_request.out); else FCGX_PutS(temp.c_str(), fcgi_request.out); } bool App::IsCompressionAllowed(const std::wstring & source) { return( config.compression && request.role == Request::responder && request.redirect_to.empty() && request.x_sendfile.empty() && !request.browser_msie && !request.browser_konqueror && accept_encoding_parser.AcceptDeflate() && source.size() >= (size_t)config.compression_page_min_size ); } bool App::CanSendContent(Header header) { if( !request.redirect_to.empty() || !request.x_sendfile.empty() ) // if there is a redirect or a file to send then we do not send a content return false; if( header == h_200 && request.role == Request::authorizer && request.is_item && request.item.auth != Item::auth_none ) // if there is an item and the item has 'file' storage we do not send a content return false; /* we don't have to check the HEAD method the server (lighttpd) doesn't send the body of its own */ if( request.method == Request::head ) return false; return true; } void App::AddDebugInfo(std::wstring & out) { if( config.debug_info ) { if( !request.debug.Empty() ) { out += L"\n\n"; } } } void App::SendAnswer() { const std::wstring & source = request.page.Str(); Header header = h_200; bool compressing = IsCompressionAllowed(source); Error status = request.status; if( status == WINIX_ERR_NO_ITEM || status == WINIX_ERR_NO_FUNCTION || status == WINIX_ERR_UNKNOWN_PARAM ) header = h_404; if( status == WINIX_ERR_PERMISSION_DENIED || status == WINIX_ERR_CANT_CHANGE_USER || status == WINIX_ERR_CANT_CHANGE_GROUP ) header = h_403; SendHeaders(compressing, header); if( CanSendContent(header) ) { // filtering (html), compressing (deflate) and sending back to the web browser FilterCompressSend(compressing, source); } } 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(); } bool App::DropPrivileges(const std::string & user, uid_t uid, gid_t gid, bool additional_groups) { if( additional_groups ) { if( initgroups(user.c_str(), gid) < 0 ) { log << log1 << "App: I can't init groups for user: " << user << logend << logsave; return false; } } else { if( setgroups(1, &gid) < 0 ) { log << log1 << "App: I can't init groups for user: " << user << logend << logsave; return false; } } // for setting real and saved gid too if( setgid(gid) ) { log << log1 << "App: I can't change real and saved gid" << logend << logsave; return false; } if( setuid(uid) < 0 ) { log << log1 << "App: I can't drop privileges to user: " << user << " (uid:" << (int)uid << ")" << logend << logsave; return false; } if( getuid()==0 || geteuid()==0 ) { log << log1 << "App: sorry, for security reasons you should not run me as the root" << logend << logsave; return false; } return true; } bool App::DropPrivileges() { if( getuid()!=0 && geteuid()!=0 ) return true; log << log2 << "App: dropping privileges" << logend; if( config.user.empty() ) { log << log1 << "App: you should specify user name in the config file " << "to which I have to drop privileges" << logend << logsave; return false; } if( config.group.empty() ) { log << log1 << "App: you should specify group name in the config file " << "to which I have to drop privileges" << logend << logsave; return false; } passwd * p = getpwnam(config.user.c_str()); group * g = getgrnam(config.group.c_str()); if( !p ) { log << log1 << "App: there is no such a user as: \"" << config.user << "\"" << logend << logsave; return false; } if( !g ) { log << log1 << "App: there is no such a group as: \"" << config.group << "\"" << logend << logsave; return false; } if( !DropPrivileges(config.user, 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 << logsave; return false; } if( pid != 0 ) { // parent exit(0); } // child if( setsid() == -1 ) { log << log1 << "App: I can't demonize myself (setsid problem)" << logend << logsave; 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() { // special thread hangs on fetchStatURL -- I don't know why // but it doesn't matter, don't use pthread_join on it //pthread_join(signal_thread, 0); system.notify.WaitForThread(); } void App::FetchPageOnExit() { // stupid trick to break FCGX_Accept_r() function // even with FCGX_InitRequest(..., ..., FCGI_FAIL_ACCEPT_ON_INTR) the FCGX_Accept_r // doesn't want to break on a signal // so we request one page from the server for exiting from FCGX_Accept_r url_stat url; fetchStatURL(url_to_fetch_on_exit.c_str(), &url, ""); } void * App::SpecialThreadForSignals(void * app_object) { sigset_t set; App * app = reinterpret_cast(app_object); sigemptyset(&set); sigaddset(&set, SIGTERM); sigaddset(&set, SIGINT); // waiting for SIGTERM or SIGINT sigwait(&set, 0); app->Lock(); app->synchro.was_stop_signal = true; FCGX_ShutdownPending(); Ezc::WideToUTF8(app->config.base_url, app->url_to_fetch_on_exit); app->system.notify.SendSignalToThread(); app->Unlock(); // this thread will hang on this method // but will be terminated when the main thread exits app->FetchPageOnExit(); pthread_exit(0); return 0; } void App::StartThreads() { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGTERM); sigaddset(&set, SIGINT); // blocking SIGTERM and SIGINT // new threads will have the signals blocked too pthread_sigmask(SIG_BLOCK, &set, 0); // special thread only for signals pthread_create(&signal_thread, 0, SpecialThreadForSignals, this); // thread for notifications system.notify.StartThread(); }