blob: 537b0e801b6e7b4f9532730d043015aca50a51ab [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "httpd.h"
#include "http_request.h"
#include "apr_strings.h"
#include "apr_portable.h"
#include "apr_pools.h"
#include "apr_file_io.h"
#include "util_script.h"
#include "fcgid_bridge.h"
#include "fcgid_pm.h"
#include "fcgid_proctbl.h"
#include "fcgid_proc.h"
#include "fcgid_conf.h"
#include "fcgid_spawn_ctl.h"
#include "fcgid_protocol.h"
#include "fcgid_bucket.h"
#define FCGID_APPLY_TRY_COUNT 2
#define FCGID_REQUEST_COUNT 32
static fcgid_procnode *apply_free_procnode(request_rec *r,
fcgid_command * command)
{
/* Scan idle list, find a node match inode, deviceid and groupid
If there is no one there, return NULL */
fcgid_procnode *previous_node, *current_node, *next_node;
fcgid_procnode *busy_list_header, *proc_table;
apr_ino_t inode = command->inode;
apr_dev_t deviceid = command->deviceid;
uid_t uid = command->uid;
gid_t gid = command->gid;
apr_size_t share_grp_id = command->share_grp_id;
const char *virtualhost = command->virtualhost;
proc_table = proctable_get_table_array();
previous_node = proctable_get_idle_list();
busy_list_header = proctable_get_busy_list();
proctable_lock(r);
current_node = &proc_table[previous_node->next_index];
while (current_node != proc_table) {
next_node = &proc_table[current_node->next_index];
if (current_node->inode == inode
&& current_node->deviceid == deviceid
&& current_node->share_grp_id == share_grp_id
&& current_node->virtualhost == virtualhost
&& current_node->uid == uid && current_node->gid == gid) {
/* Unlink from idle list */
previous_node->next_index = current_node->next_index;
/* Link to busy list */
current_node->next_index = busy_list_header->next_index;
busy_list_header->next_index = current_node - proc_table;
proctable_unlock(r);
return current_node;
}
else
previous_node = current_node;
current_node = next_node;
}
proctable_unlock(r);
/* Found nothing */
return NULL;
}
static void
return_procnode(request_rec *r,
fcgid_procnode *procnode, int communicate_error)
{
fcgid_procnode *previous_node, *current_node, *next_node;
fcgid_procnode *proc_table = proctable_get_table_array();
fcgid_procnode *error_list_header = proctable_get_error_list();
fcgid_procnode *idle_list_header = proctable_get_idle_list();
fcgid_procnode *busy_list_header = proctable_get_busy_list();
proctable_lock(r);
/* Unlink the node from busy list first */
previous_node = busy_list_header;
current_node = &proc_table[previous_node->next_index];
while (current_node != proc_table) {
next_node = &proc_table[current_node->next_index];
if (current_node == procnode) {
/* Unlink from busy list */
previous_node->next_index = current_node->next_index;
break;
}
else
previous_node = current_node;
current_node = next_node;
}
/* Return to error list or idle list */
if (communicate_error) {
/* Link to error list */
procnode->next_index = error_list_header->next_index;
error_list_header->next_index = procnode - proc_table;
}
else {
/* Link to idle list */
procnode->next_index = idle_list_header->next_index;
idle_list_header->next_index = procnode - proc_table;
}
proctable_unlock(r);
}
static int count_busy_processes(request_rec *r, fcgid_command *command)
{
int result = 0;
fcgid_procnode *previous_node, *current_node, *next_node;
fcgid_procnode *proc_table = proctable_get_table_array();
fcgid_procnode *busy_list_header = proctable_get_busy_list();
proctable_lock(r);
previous_node = busy_list_header;
current_node = &proc_table[previous_node->next_index];
while (current_node != proc_table) {
if (current_node->inode == command->inode
&& current_node->deviceid == command->deviceid
&& current_node->share_grp_id == command->share_grp_id
&& current_node->virtualhost == command->virtualhost
&& current_node->uid == command->uid
&& current_node->gid == command->gid) {
result++;
}
next_node = &proc_table[current_node->next_index];
current_node = next_node;
}
proctable_unlock(r);
return result;
}
apr_status_t bucket_ctx_cleanup(void *thectx)
{
/* Cleanup jobs:
1. Free bucket buffer
2. Return procnode
NOTE: ipc will be clean when request pool cleanup, so I don't need to close it here
*/
fcgid_bucket_ctx *ctx = (fcgid_bucket_ctx *) thectx;
request_rec *r = ctx->ipc.request;
/* Free bucket buffer */
if (ctx->buffer) {
apr_bucket_destroy(ctx->buffer);
ctx->buffer = NULL;
}
proc_close_ipc(&ctx->ipc);
if (ctx->procnode) {
++ctx->procnode->requests_handled;
/* Return procnode
I will return this slot to idle(or error) list
*/
if (ctx->procnode->diewhy == FCGID_DIE_BUSY_TIMEOUT) {
ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
"mod_fcgid: %s took longer than busy timeout "
"(%d secs)",
r->uri,
ctx->procnode->cmdopts.busy_timeout);
return_procnode(r, ctx->procnode, 1 /* busy timeout */ );
}
else if (ctx->has_error) {
ctx->procnode->diewhy = FCGID_DIE_COMM_ERROR;
return_procnode(r, ctx->procnode, 1 /* communication error */ );
}
else if (ctx->procnode->cmdopts.max_requests_per_process
&& ctx->procnode->requests_handled >=
ctx->procnode->cmdopts.max_requests_per_process) {
ctx->procnode->diewhy = FCGID_DIE_LIFETIME_EXPIRED;
return_procnode(r, ctx->procnode, 1 /* handled all requests */ );
}
else
return_procnode(r, ctx->procnode, 0 /* communication ok */ );
ctx->procnode = NULL;
}
return APR_SUCCESS;
}
static int getsfunc_fcgid_BRIGADE(char *buf, int len, void *arg)
{
apr_bucket_brigade *bb = (apr_bucket_brigade *) arg;
const char *dst_end = buf + len - 1; /* leave room for terminating null */
char *dst = buf;
apr_bucket *e = APR_BRIGADE_FIRST(bb);
apr_status_t rv;
int done = 0;
int getLF = 0;
int getColon = 0;
while ((dst < dst_end) && !done && !APR_BUCKET_IS_EOS(e)) {
const char *bucket_data;
apr_size_t bucket_data_len;
const char *src;
const char *src_end;
apr_bucket *next;
rv = apr_bucket_read(e, &bucket_data, &bucket_data_len,
APR_BLOCK_READ);
if (rv != APR_SUCCESS) {
return 0;
}
/* Move on to next bucket if it's fastcgi header bucket */
if (e->type == &ap_bucket_type_fcgid_header
|| e->type == &apr_bucket_type_immortal) {
next = APR_BUCKET_NEXT(e);
apr_bucket_delete(e);
e = next;
if (getLF) {
done = 1;
}
continue;
}
if (bucket_data_len == 0)
return 0;
/* Base on RFC2616 section 4.2 */
src = bucket_data;
src_end = bucket_data + bucket_data_len;
while ((src < src_end) && (dst < dst_end) && !done) {
if (*src == ':')
getColon = 1;
if (getLF && ((*src != ' ' && *src != '\t') || !getColon)) {
done = 1;
getColon = 0;
break;
}
else if (getLF && (*src == ' ' || *src == '\t')) {
*dst++ = '\r';
*dst++ = '\n';
getLF = 0;
}
if (*src == '\n') {
getLF = 1;
}
else if (*src != '\r') {
*dst++ = *src;
}
src++;
}
if (src < src_end) {
apr_bucket_split(e, src - bucket_data);
}
next = APR_BUCKET_NEXT(e);
apr_bucket_delete(e);
e = next;
}
*dst = 0;
return 1;
}
static int
handle_request(request_rec * r, int role, const char *argv0,
fcgid_wrapper_conf * wrapper_conf,
apr_bucket_brigade * output_brigade)
{
apr_pool_t *request_pool = r->main ? r->main->pool : r->pool;
fcgid_command fcgi_request;
fcgid_bucket_ctx *bucket_ctx;
int i, j, cond_status;
apr_status_t rv;
apr_bucket_brigade *brigade_stdout;
char sbuf[MAX_STRING_LEN];
const char *location;
bucket_ctx = apr_pcalloc(request_pool, sizeof(*bucket_ctx));
bucket_ctx->ipc.request = r;
apr_pool_cleanup_register(request_pool, bucket_ctx,
bucket_ctx_cleanup, apr_pool_cleanup_null);
/* Try to get a connected ipc handle */
for (i = 0; i < FCGID_REQUEST_COUNT; i++) {
/* Apply a free process slot, send a spawn request if I can't get one */
for (j = 0; j < FCGID_APPLY_TRY_COUNT; j++) {
apr_ino_t inode =
wrapper_conf ? wrapper_conf->inode : r->finfo.inode;
apr_dev_t deviceid =
wrapper_conf ? wrapper_conf->deviceid : r->finfo.device;
apr_size_t shareid =
wrapper_conf ? wrapper_conf->share_group_id : 0;
/* Init spawn request */
procmgr_init_spawn_cmd(&fcgi_request, r, argv0, deviceid,
inode, shareid);
bucket_ctx->ipc.connect_timeout =
fcgi_request.cmdopts.ipc_connect_timeout;
bucket_ctx->ipc.communation_timeout =
fcgi_request.cmdopts.ipc_comm_timeout;
/* Apply a process slot */
bucket_ctx->procnode = apply_free_procnode(r, &fcgi_request);
if (bucket_ctx->procnode)
break;
/* Avoid sleeping the very first time through if there are no
busy processes; the problem is just that we haven't spawned
anything yet, so waiting is pointless */
if (i > 0 || j > 0 || count_busy_processes(r, &fcgi_request)) {
apr_sleep(apr_time_from_sec(1));
bucket_ctx->procnode = apply_free_procnode(r, &fcgi_request);
if (bucket_ctx->procnode)
break;
}
/* Send a spawn request if I can't get a process slot */
procmgr_post_spawn_cmd(&fcgi_request, r);
}
/* Connect to the fastcgi server */
if (bucket_ctx->procnode) {
if (proc_connect_ipc(bucket_ctx->procnode,
&bucket_ctx->ipc) != APR_SUCCESS) {
proc_close_ipc(&bucket_ctx->ipc);
bucket_ctx->procnode->diewhy = FCGID_DIE_CONNECT_ERROR;
return_procnode(r, bucket_ctx->procnode, 1 /* has error */ );
bucket_ctx->procnode = NULL;
}
else
break;
}
}
/* Now I get a connected ipc handle */
if (!bucket_ctx->procnode) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
"mod_fcgid: can't apply process slot for %s", argv0);
return HTTP_SERVICE_UNAVAILABLE;
}
bucket_ctx->active_time = bucket_ctx->procnode->last_active_time =
apr_time_now();
bucket_ctx->procnode->diewhy = FCGID_DIE_KILLSELF;
/* Write output_brigade to fastcgi server */
if ((rv =
proc_write_ipc(&bucket_ctx->ipc, output_brigade)) != APR_SUCCESS) {
bucket_ctx->has_error = 1;
return HTTP_INTERNAL_SERVER_ERROR;
}
/* Create brigade */
brigade_stdout =
apr_brigade_create(request_pool, r->connection->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(brigade_stdout,
ap_bucket_fcgid_header_create(r->connection->
bucket_alloc,
bucket_ctx));
/*APR_BRIGADE_INSERT_TAIL(brigade_stdout, apr_bucket_flush_create(r->connection->bucket_alloc)); */
/* Check the script header first. If got error, return immediately */
if ((cond_status = ap_scan_script_header_err_core
(r, sbuf, getsfunc_fcgid_BRIGADE, brigade_stdout)) >= 400)
return cond_status;
/* Check redirect */
location = apr_table_get(r->headers_out, "Location");
if (location && location[0] == '/' && r->status == 200) {
/* This redirect needs to be a GET no matter what the original
* method was.
*/
r->method = apr_pstrdup(r->pool, "GET");
r->method_number = M_GET;
/* We already read the message body (if any), so don't allow
* the redirected request to think it has one. We can ignore
* Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR.
*/
apr_table_unset(r->headers_in, "Content-Length");
ap_internal_redirect_handler(location, r);
return HTTP_OK;
}
else if (location && r->status == 200) {
/* XX Note that if a script wants to produce its own Redirect
* body, it now has to explicitly *say* "Status: 302"
*/
return HTTP_MOVED_TEMPORARILY;
}
/* Now pass to output filter */
if (role == FCGI_RESPONDER && (rv =
ap_pass_brigade(r->output_filters,
brigade_stdout)) !=
APR_SUCCESS) {
if (!APR_STATUS_IS_ECONNABORTED(rv)) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r,
"mod_fcgid: ap_pass_brigade failed in handle_request function");
}
return HTTP_INTERNAL_SERVER_ERROR;
}
/* Retrun condition status */
return cond_status;
}
static int add_request_body(request_rec *r, apr_pool_t *request_pool,
apr_bucket_brigade *output_brigade)
{
apr_bucket *bucket_input, *bucket_header;
apr_file_t *fd = NULL;
apr_off_t cur_pos = 0, request_size = 0;
apr_status_t rv;
FCGI_Header *stdin_request_header;
fcgid_server_conf *sconf = ap_get_module_config(r->server->module_config,
&fcgid_module);
int seen_eos;
/* Stdin header and body */
/* I have to read all the request into memory before sending it
to fastcgi application server, this prevents slow clients from
keeping the server in processing too long.
But sometimes it's not acceptable (think about uploading a large attachment)
Request will be stored in tmp file if the size larger than max_mem_request_len
*/
seen_eos = 0;
do {
apr_bucket_brigade *input_brigade = apr_brigade_create(request_pool,
r->connection->
bucket_alloc);
if ((rv = ap_get_brigade(r->input_filters, input_brigade,
AP_MODE_READBYTES,
APR_BLOCK_READ,
HUGE_STRING_LEN)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r,
"mod_fcgid: can't get data from http client");
apr_brigade_destroy(output_brigade);
apr_brigade_destroy(input_brigade);
return HTTP_INTERNAL_SERVER_ERROR;
}
for (bucket_input = APR_BRIGADE_FIRST(input_brigade);
bucket_input != APR_BRIGADE_SENTINEL(input_brigade);
bucket_input = APR_BUCKET_NEXT(bucket_input)) {
const char *data;
apr_size_t len;
apr_bucket *bucket_stdin;
if (APR_BUCKET_IS_EOS(bucket_input)) {
seen_eos = 1;
break;
}
if (APR_BUCKET_IS_METADATA(bucket_input))
continue;
if ((rv = apr_bucket_read(bucket_input, &data, &len,
APR_BLOCK_READ)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r,
"mod_fcgid: can't read request from HTTP client");
apr_brigade_destroy(input_brigade);
apr_brigade_destroy(output_brigade);
return HTTP_INTERNAL_SERVER_ERROR;
}
/* Append a header, and the the bucket */
stdin_request_header = apr_bucket_alloc(sizeof(FCGI_Header),
r->connection->
bucket_alloc);
bucket_header =
apr_bucket_heap_create((const char *) stdin_request_header,
sizeof(*stdin_request_header),
apr_bucket_free,
r->connection->bucket_alloc);
request_size += len;
if (request_size > sconf->max_request_len) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
"mod_fcgid: HTTP request length %" APR_OFF_T_FMT
" (so far) exceeds MaxRequestLen (%"
APR_OFF_T_FMT ")", request_size,
sconf->max_request_len);
return HTTP_INTERNAL_SERVER_ERROR;
}
if (request_size > sconf->max_mem_request_len) {
apr_size_t wrote_len;
static const char *fd_key = "fcgid_fd";
if (fd == NULL) {
void *tmp;
apr_pool_userdata_get(&tmp, fd_key, r->connection->pool);
fd = tmp;
if (fd != NULL) {
if ((rv = apr_file_trunc(fd, 0)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r,
"mod_fcgid: can't truncate existing "
"temporary file");
return HTTP_INTERNAL_SERVER_ERROR;
}
}
}
if (fd == NULL) {
const char *tempdir = NULL;
char *template;
rv = apr_temp_dir_get(&tempdir, r->pool);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r,
"mod_fcgid: can't get tmp dir");
return HTTP_INTERNAL_SERVER_ERROR;
}
apr_filepath_merge(&template, tempdir,
"fcgid.tmp.XXXXXX",
APR_FILEPATH_NATIVE, r->pool);
rv = apr_file_mktemp(&fd, template, 0,
r->connection->pool);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, r,
"mod_fcgid: can't open tmp file fot stdin request");
return HTTP_INTERNAL_SERVER_ERROR;
}
apr_pool_userdata_set((const void *) fd, fd_key,
apr_pool_cleanup_null,
r->connection->pool);
}
/* Write request to tmp file */
if ((rv =
apr_file_write_full(fd, (const void *) data, len,
&wrote_len)) != APR_SUCCESS
|| len != wrote_len) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING,
rv, r,
"mod_fcgid: can't write tmp file for stdin request");
return HTTP_INTERNAL_SERVER_ERROR;
}
/* Create file bucket */
bucket_stdin =
apr_bucket_file_create(fd, cur_pos, len, r->pool,
r->connection->bucket_alloc);
cur_pos += len;
}
else {
if (APR_BUCKET_IS_HEAP(bucket_input))
apr_bucket_copy(bucket_input, &bucket_stdin);
else {
/* mod_ssl have a bug? */
char *pcopydata =
apr_bucket_alloc(len, r->connection->bucket_alloc);
memcpy(pcopydata, data, len);
bucket_stdin =
apr_bucket_heap_create(pcopydata, len,
apr_bucket_free,
r->connection->bucket_alloc);
}
}
if (!init_header(FCGI_STDIN, 1, len, 0, stdin_request_header)) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
"mod_fcgid: header overflow");
apr_brigade_destroy(input_brigade);
apr_brigade_destroy(output_brigade);
return HTTP_INTERNAL_SERVER_ERROR;
}
APR_BRIGADE_INSERT_TAIL(output_brigade, bucket_header);
APR_BRIGADE_INSERT_TAIL(output_brigade, bucket_stdin);
}
apr_brigade_destroy(input_brigade);
}
while (!seen_eos);
/* Append an empty body stdin header */
stdin_request_header = apr_bucket_alloc(sizeof(FCGI_Header),
r->connection->bucket_alloc);
bucket_header =
apr_bucket_heap_create((const char *) stdin_request_header,
sizeof(*stdin_request_header),
apr_bucket_free, r->connection->bucket_alloc);
if (!init_header(FCGI_STDIN, 1, 0, 0, stdin_request_header)) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
"mod_fcgid: header overflow");
return HTTP_INTERNAL_SERVER_ERROR;
}
APR_BRIGADE_INSERT_TAIL(output_brigade, bucket_header);
return 0;
}
int bridge_request(request_rec * r, int role, const char *argv0,
fcgid_wrapper_conf * wrapper_conf)
{
apr_pool_t *request_pool = r->main ? r->main->pool : r->pool;
apr_bucket_brigade *output_brigade;
apr_bucket *bucket_eos;
char **envp = ap_create_environment(request_pool,
r->subprocess_env);
int rc;
/* Create brigade for the request to fastcgi server */
output_brigade =
apr_brigade_create(request_pool, r->connection->bucket_alloc);
/* Build the begin request and environ request, append them to output_brigade */
if (!build_begin_block
(role, r, r->connection->bucket_alloc, output_brigade)
|| !build_env_block(r, envp, r->connection->bucket_alloc,
output_brigade)) {
ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
"mod_fcgid: can't build begin or env request");
return HTTP_INTERNAL_SERVER_ERROR;
}
if (role == FCGI_RESPONDER) {
rc = add_request_body(r, request_pool, output_brigade);
if (rc) {
return rc;
}
}
/* The eos bucket now */
bucket_eos = apr_bucket_eos_create(r->connection->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(output_brigade, bucket_eos);
/* Bridge the request */
return handle_request(r, role, argv0, wrapper_conf, output_brigade);
}