remove Request::post_tab and add Request::http_status

Instead of Reqest::post_tab we use now Request::post_in (pt::Space).

Request::http_status will be used instead Request::status,
but at the moment is not changed in all places.
Request::status has been marked as depracated.

While here:
- Check for root dir in App and not in FunctionParser,
let FunctionParser only log the root dir.
- Read post variables after parsing url parameters,
this allows winix functions to set limits
for post input.
- Set limits when parsing input json format, new
options added to config: post_max_object_items, post_max_table_items,
post_max_all_items, post_max_nested_objects.
There are similar options in each winix function (they are in
FunctionBase).
- Some refactoring in App.
- Add config option: log_whole_http_post if true
then the whole parsed post input is logged.
- Add config option: post_json_max - max length of input stream
for parsing json.
- Add config option: templates_request_status, default request_status.html
this is an ezc template used as [content] when the request status
is not 200_ok.
- Fix: Sort winix function didn't show items to sort (fix and do some
refactoring as well)
- Properly sort items in: ImgCrop, Ls, Sort, Upload
- Remove ezc templates: err_404.html, err_per_denied.html - now
request_status.html is used.
This commit is contained in:
2022-05-30 01:29:18 +02:00
parent 9e222f5b80
commit 7d1fb3c04e
46 changed files with 1224 additions and 835 deletions

View File

@@ -612,6 +612,7 @@ bool App::CheckAccessFromPlugins()
if( res.res_false > 0 )
{
cur.request->status = WINIX_ERR_PERMISSION_DENIED;
cur.request->http_status = Header::status_403_forbidden;
log << log2 << "App: access prevented by a plugin" << logend;
return false;
}
@@ -621,102 +622,155 @@ return true;
void App::ProcessRequestThrow()
/*
* REFACTOR ME
*/
void App::MakeRenameMeToABetterName()
{
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->function )
{
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();
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();
}
SendAnswer();
/*
* 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);
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);
////////////////////////
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->http_status = Header::status_403_forbidden;
}
// cur.request->status can be changed by function_parser
if( cur.request->status == WINIX_ERR_OK && cur.request->http_status == Header::status_200_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 && cur.request->http_status == Header::status_200_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 && cur.request->http_status == Header::status_200_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;
}
}
bool App::AddRootDir()
{
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();
}
else
{
log << log3 << "App: there is no a root directory" << logend;
cur.request->http_status = Header::status_404_not_found;
}
return root_dir != nullptr;
}
void App::ProcessRequestThrow()
{
if( AddRootDir() )
{
ReadRequest();
if( !BaseUrlRedirect() )
{
if( functions.Parse() )
{
ReadPostVars();
}
if( cur.mount->type != system.mounts.MountTypeStatic() )
{
MakeRenameMeToABetterName();
}
}
}
}
@@ -732,6 +786,8 @@ void App::ProcessRequest()
log << log2 << config.log_delimiter << logend;
ProcessRequestThrow();
ModifyStatusCodeIfNeeded();
SendAnswer();
SaveSessionsIfNeeded();
cur.request->RequestEnds();
@@ -784,6 +840,7 @@ void App::ClearAfterRequest()
aheader_value.clear();
cur.mount = system.mounts.GetEmptyMount();
system.mounts.pmount = cur.mount; // IMPROVE ME system.mounts.pmount will be removed
post_log_tmp_buffer.clear();
// send_data_buf doesn't have to be cleared and it is better to not clear it (optimizing)
model_connector.set_winix_request(nullptr);
@@ -908,70 +965,6 @@ void App::AddDefaultModels()
// !! 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()
@@ -1052,7 +1045,7 @@ void App::ReadRequest()
LogAccess();
ReadEnvHTTPVariables();
ReadPostVars();
//ReadPostVars();
if( config.log_env_variables )
LogEnvironmentVariables();
@@ -1071,6 +1064,7 @@ void App::ReadRequest()
if( cur.request->using_ssl )
log << log3 << "App: connection secure through SSL" << logend;
}
@@ -1293,8 +1287,7 @@ void App::LogAccess()
}
void App::ReadPostJson()
void App::ReadInputPostToBuffer()
{
char buffer[1024];
const int buffer_len = sizeof(buffer) / sizeof(char) - 1;
@@ -1303,34 +1296,89 @@ void App::ReadPostJson()
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);
post_buffer.write(buffer, read_len);
}
while( read_len == buffer_len );
}
void App::ParsePostJson()
{
FunctionBase * fun = cur.request->function;
space_parser.set_object_items_limit( (fun && fun->post_max_object_items != 0) ? fun->post_max_object_items : config.post_max_object_items);
space_parser.set_table_items_limit( (fun && fun->post_max_table_items != 0) ? fun->post_max_table_items : config.post_max_table_items);
space_parser.set_all_items_limit( (fun && fun->post_max_all_items != 0) ? fun->post_max_all_items : config.post_max_all_items);
space_parser.set_nested_level_limit( (fun && fun->post_max_nested_objects != 0) ? fun->post_max_nested_objects : config.post_max_nested_objects);
pt::SpaceParser::Status parse_status = space_parser.parse_json(post_buffer, cur.request->post_in);
if( parse_status == pt::SpaceParser::ok )
{
}
else
if( parse_status == pt::SpaceParser::syntax_error )
{
log << log1 << "App: cannot parse the input stream as an JSON object"
<< ", syntax error in line: " << space_parser.get_last_parsed_line() << ":" << space_parser.get_last_parsed_column() << logend;
cur.request->post_in.clear();
cur.request->http_status = Header::status_400_bad_request;
}
else
if( parse_status == pt::SpaceParser::limit_object_items_exceeded )
{
log << log1 << "App: object items limit exceeded when parsing input JSON object" << logend;
cur.request->post_in.clear();
cur.request->http_status = Header::status_400_bad_request;
}
else
if( parse_status == pt::SpaceParser::limit_table_items_exceeded )
{
log << log1 << "App: table items limit exceeded when parsing input JSON object" << logend;
cur.request->post_in.clear();
cur.request->http_status = Header::status_400_bad_request;
}
else
if( parse_status == pt::SpaceParser::limit_all_items_exceeded )
{
log << log1 << "App: all items limit exceeded when parsing input JSON object" << logend;
cur.request->post_in.clear();
cur.request->http_status = Header::status_400_bad_request;
}
else
if( parse_status == pt::SpaceParser::limit_nested_level_exceeded )
{
log << log1 << "App: nested objects/tables limit exceeded when parsing input JSON object" << logend;
cur.request->post_in.clear();
cur.request->http_status = Header::status_400_bad_request;
}
}
void App::ReadPostJson()
{
ReadInputPostToBuffer();
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 )
if( config.post_json_max == 0 || post_buffer.size() <= config.post_json_max )
{
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?)
ParsePostJson();
}
else
{
log << log1 << "App: the input stream exceeded the limit of " << config.post_json_max << " bytes (skipping parsing)" << logend;
cur.request->http_status = Header::status_400_bad_request;
}
post_buffer.clear();
}
else
{
@@ -1342,26 +1390,29 @@ void App::ReadPostJson()
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);
post_multi_parser.Parse(fcgi_request.in, *cur.request); // IMPROVEME add checking for return status
}
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;
log << log3 << "App: post content type: " << Winix::Header::application_json << ", using json parser" << logend;
ReadPostJson();
}
else
{
// IMPROVE ME may to check a correct content-type header?
post_parser.Parse(fcgi_request.in, cur.request->post_tab);
post_parser.Parse(fcgi_request.in, *cur.request); // IMPROVEME add checking for return status
}
if( config.log_whole_http_post )
{
cur.request->post_in.serialize_to_json_stream(post_log_tmp_buffer, true);
log << log3 << "App: the whole http post after parsing:" << logend << post_log_tmp_buffer << logend;
}
}
}
@@ -1464,7 +1515,7 @@ return false;
}
bool App::PrepareHeadersStaticCreateResource(pt::WTextStream & out_path)
bool App::CreateStaticResourcePath(pt::WTextStream & out_path)
{
size_t i = 0;
Item * dir = system.dirs.GetDir(system.mounts.pmount->dir_id);
@@ -1476,7 +1527,7 @@ bool App::PrepareHeadersStaticCreateResource(pt::WTextStream & out_path)
}
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);
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 == '/' )
@@ -1492,12 +1543,12 @@ return true;
}
void App::PrepareHeadersStatic()
void App::PrepareSendFileHeaderForStaticMountpoint()
{
if( PathHasUpDir(cur.request->env_request_uri) )
{
log << log1 << "App: incorrect path for a static file" << logend;
PrepareHeadersForbidden();
cur.request->http_status = Header::status_403_forbidden;
return;
}
@@ -1507,27 +1558,27 @@ void App::PrepareHeadersStatic()
if( index >= config.static_dirs.size() )
{
log << log1 << "App: static dir with index " << index << " is not defined in the config" << logend;
PrepareHeadersForbidden();
cur.request->http_status = Header::status_403_forbidden;
return;
}
pt::WTextStream path;
path << config.static_dirs[index] << L"/";
if( !PrepareHeadersStaticCreateResource(path) )
if( !CreateStaticResourcePath(path) )
{
PrepareHeadersForbidden();
cur.request->http_status = Header::status_403_forbidden;
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
* 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;
}
}
@@ -1579,80 +1630,58 @@ void App::PrepareHeaderContentType()
void App::PrepareHeadersForbidden()
{
AddHeader(L"Status", L"403 Forbidden");
PrepareHeaderContentType();
}
void App::PrepareHeadersRedirect()
void App::ModifyStatusForRedirect()
{
switch(cur.request->redirect_type)
{
case 300:
AddHeader(L"Status", L"300 Multiple Choices");
cur.request->http_status = Header::status_300_multiple_choices;
break;
case 301:
AddHeader(L"Status", L"301 Moved Permanently");
cur.request->http_status = Header::status_301_moved_permanently;
break;
case 302:
AddHeader(L"Status", L"302 Found");
cur.request->http_status = Header::status_302_found;
break;
case 307:
AddHeader(L"Status", L"307 Temporary Redirect");
cur.request->http_status = Header::status_307_temporary_redirect;
break;
case 303:
default:
AddHeader(L"Status", L"303 See Other");
cur.request->http_status = Header::status_303_see_other;
break;
}
AddHeader(L"Location", cur.request->redirect_to);
log << log2 << "App: redirect to: " << cur.request->redirect_to << logend;
}
void App::PrepareHeadersSendFile()
void App::PrepareSendFileHeader()
{
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)
void App::PrepareContentEncodingHeader(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)
void App::PrepareContentLengthHeader(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;
@@ -1734,35 +1763,53 @@ void App::SendCookies()
}
void App::PrepareHeaders(bool compressing, int compress_encoding, Header header, size_t output_size)
void App::PrepareHeaderStatus(int http_status)
{
pt::WTextStream value;
Header::prepare_status_value(http_status, value, false);
AddHeader(L"Status", value);
log << log2 << "App: http status: " << value << logend;
}
void App::PrepareHeaders(bool compressing, int compress_encoding, 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();
ModifyStatusForRedirect();
AddHeader(L"Location", cur.request->redirect_to);
log << log2 << "App: redirect to: " << cur.request->redirect_to << logend;
}
else
if( system.mounts.pmount->type == system.mounts.MountTypeStatic() )
{
PrepareHeadersStatic();
PrepareSendFileHeaderForStaticMountpoint();
}
else
if( !cur.request->x_sendfile.empty() )
{
PrepareHeadersSendFile();
PrepareSendFileHeader();
}
else
{
PrepareHeadersNormal(header, output_size);
PrepareContentLengthHeader(output_size);
}
if( compressing )
PrepareHeadersCompression(compress_encoding);
{
PrepareContentEncodingHeader(compress_encoding);
}
PrepareHeaderStatus(cur.request->http_status);
PrepareHeaderContentType();
}
@@ -1804,7 +1851,6 @@ void App::SelectCompression(size_t source_len, bool & compression_allowed, int &
}
}
if( config.compression_encoding == 2 || config.compression_encoding == 20 )
{
if( accept_encoding_parser.AcceptGzip() )
@@ -1833,54 +1879,53 @@ bool App::CanSendContent()
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;
return true;
}
App::Header App::GetHTTPStatusCode()
void App::ModifyStatusCodeIfNeeded()
{
Error status = cur.request->status;
Header header = h_200;
Error status = cur.request->status;
if( status == WINIX_ERR_NO_ITEM || status == WINIX_ERR_NO_FUNCTION || status == WINIX_ERR_UNKNOWN_PARAM )
// moved from Templates when a pattern was selected
switch( status )
{
header = h_404;
log << log2 << "App: http response: 404 Not Found" << logend;
case WINIX_ERR_INCORRECT_URI: // !!temporarily
case WINIX_ERR_INTERNAL_ERROR: // !! temprarily
case WINIX_ERR_PERMISSION_DENIED:
case WINIX_ERR_CANT_CHANGE_USER:
case WINIX_ERR_CANT_CHANGE_GROUP:
case WINIX_ERR_CANT_CHANGE_PRIVILEGES:
cur.request->http_status = Header::status_403_forbidden;
break;
case WINIX_ERR_NO_ITEM:
case WINIX_ERR_NO_FUNCTION:
case WINIX_ERR_UNKNOWN_PARAM:
cur.request->http_status = Header::status_404_not_found;
break;
}
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) )
if( cur.request->use_200_status_for_not_found_and_permission_denied && (
cur.request->http_status == Header::status_404_not_found ||
cur.request->http_status == Header::status_403_forbidden
))
{
cur.request->http_status = Header::status_200_ok;
log << log3 << "App: changing the http response to: 200 OK" << logend;
header = h_200;
}
return header;
}
@@ -2389,7 +2434,6 @@ 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);
@@ -2409,7 +2453,7 @@ void App::Send8bitOutput(BinaryPage & output)
output_size = output.size();
}
PrepareHeaders(compressing, compress_encoding, header, output_size);
PrepareHeaders(compressing, compress_encoding, output_size);
SendHeaders();
SendCookies();
FCGX_PutS("\r\n", fcgi_request.out);