Download | Plain Text | Line Numbers


/*
 * Copyright 2014-2020 Manuel Mausz
 *
 * Licensed 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_log.h"
#include "http_protocol.h"
#include "apr_strings.h"
 
#define VERSION "0.7"
#define PHP_MAGIC_TYPE "application/x-httpd-php"
 
module AP_MODULE_DECLARE_DATA phpfpm_handler_module;
 
typedef struct {
    const char *url;
    const char *body_temp_path;
    apr_table_t *php_values;
} phpfpm_dir_conf;
 
static apr_status_t write_body(request_rec *r, apr_file_t *file, apr_off_t *size)
{
    apr_status_t rc = OK;
 
    if ((rc = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK)))
        return rc;
 
    if (ap_should_client_block(r)) {
        char argsbuffer[HUGE_STRING_LEN];
        apr_off_t rsize, len_read, rpos = 0;
        apr_off_t length = r->remaining;
        apr_size_t written;
 
        while ((len_read = ap_get_client_block(r, argsbuffer,
                                               sizeof(argsbuffer))) > 0) {
            if (length > 0 && (rpos + len_read) > length)
                rsize = (apr_size_t)length - rpos;
            else
                rsize = len_read;
 
            rc = apr_file_write_full(file, argsbuffer, (apr_size_t) rsize,
                                     &written);
            if (written != rsize || rc != OK)
                return APR_ENOSPC;
            if (rc != APR_SUCCESS)
                return rc;
            rpos += rsize;
        }
 
        *size = rpos;
    }
 
    return rc;
}
 
static const char *pass_request_body(request_rec *r, phpfpm_dir_conf *conf,
                                     apr_off_t *size)
{
#if APR_HAS_THREADS
    apr_os_thread_t tid = apr_os_thread_current();
    char *filename = apr_psprintf(r->pool, "%s/php.%pT.XXXXXX",
                                        conf->body_temp_path, &tid);
#else
    char *filename = apr_psprintf(r->pool, "%s/php.%" APR_PID_T_FMT ".XXXXXX",
                                        conf->body_temp_path, getpid());
#endif
 
    apr_file_t *file;
    apr_status_t rc = apr_file_mktemp(&file, filename,
            APR_FOPEN_CREATE | APR_FOPEN_WRITE | APR_FOPEN_TRUNCATE |
            APR_FOPEN_DELONCLOSE, r->pool);
    if (rc == APR_SUCCESS)
        rc = apr_file_perms_set(filename, APR_FPROT_UREAD | APR_FPROT_UWRITE
                | APR_FPROT_GREAD | APR_FPROT_WREAD);
    if (rc == APR_SUCCESS)
        rc = write_body(r, file, size);
    return (rc == APR_SUCCESS) ? filename : NULL;
}
 
#define MAX_SAVED_LENGTHS 3
static char *serialize_table(request_rec *r, const apr_array_header_t *arr)
{
    struct {
      apr_size_t key_len;
      apr_size_t val_len;
    } saved_lengths[MAX_SAVED_LENGTHS];
    apr_size_t len = 0;
    int i;
 
    if (!arr)
        return NULL;
 
    const apr_table_entry_t *elts = (const apr_table_entry_t *)arr->elts;
    if (arr->nelts == 0)
        return NULL;
    for (i = 0; i < arr->nelts; ++i) {
        apr_size_t key_len = strlen(elts[i].key);
        apr_size_t val_len = strlen(elts[i].val);
        len += key_len + val_len + 2;
        if (i < MAX_SAVED_LENGTHS) {
            saved_lengths[i].key_len = key_len;
            saved_lengths[i].val_len = val_len;
        }
    }
 
    char *res = apr_palloc(r->pool, len + 1);
    char *cp = res;
    for (i = 0; i < arr->nelts; ++i) {
        apr_size_t key_len, val_len;
        if (i < MAX_SAVED_LENGTHS) {
            key_len = saved_lengths[i].key_len;
            val_len = saved_lengths[i].val_len;
        }
        else
        {
            key_len = strlen(elts[i].key);
            val_len = strlen(elts[i].val);
        }
 
        memcpy(cp, elts[i].key, key_len);
        cp += key_len;
        *cp = '=';
        cp++;
        memcpy(cp, elts[i].val, val_len);
        cp += val_len;
        *cp = '\n';
        cp++;
    }
    *cp = '\0';
 
    return res;
}
 
static int handler(request_rec *r)
{
    if (!r->filename || r->proxyreq
          || (r->handler && strcmp(r->handler, PHP_MAGIC_TYPE)))
      return DECLINED;
 
    if (r->finfo.filetype == 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "script '%s' not found or unable to stat", r->filename);
        return HTTP_NOT_FOUND;
    }
    if (r->finfo.filetype == APR_DIR) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "attempt to invoke directory '%s' as script", r->filename);
        return HTTP_FORBIDDEN;
    }
 
    phpfpm_dir_conf *conf =
        ap_get_module_config(r->per_dir_config, &phpfpm_handler_module);
    if (conf->url) {
        /* handle body temp path */
        if (conf->body_temp_path) {
            const char *filename = NULL;
            apr_off_t size;
 
            if (r->method_number == M_POST) {
                const char *ct = apr_table_get(r->headers_in, "Content-Type");
                if (ct && !strncasecmp(ct, "multipart/form-data", 19))
                    filename = pass_request_body(r, conf, &size);
            }
            else if (r->method_number == M_PUT) {
                /* HTTP PUT (e.g. webdav) */
                filename = pass_request_body(r, conf, &size);
            }
 
            if (filename) {
               apr_table_setn(r->subprocess_env, "REQUEST_BODY_FILE", filename);
               apr_table_set(r->headers_in, "Content-Length",
                             apr_off_t_toa(r->pool, size));
               /*
                * mod_proxy_fcgi tries to read the input in order to determine the content-length:
                * see https://svn.apache.org/viewvc?view=revision&revision=1884069
                * we prevent that by creating an empty input brigade beforehand
                */
               apr_bucket_brigade *input_brigade = apr_brigade_create(r->pool, r->connection->bucket_alloc);
               apr_pool_userdata_setn(input_brigade, "proxy-fcgi-input", NULL, r->pool);
            }
        }
 
        r->proxyreq = PROXYREQ_REVERSE;
        r->filename = apr_pstrcat(r->pool, conf->url, r->filename, NULL);
        apr_table_setn(r->notes, "rewrite-proxy", "1");
        r->handler = "proxy-server";
 
        const char *php_values =
            serialize_table(r, apr_table_elts(conf->php_values));
        if (php_values)
            apr_table_setn(r->subprocess_env, "PHP_VALUE", php_values);
    }
 
    return DECLINED;
}
 
static void *create_dir_config(apr_pool_t *p, char *dummy)
{
    phpfpm_dir_conf *conf = apr_palloc(p, sizeof(*conf));
    conf->url            = NULL;
    conf->body_temp_path = NULL;
    conf->php_values     = apr_table_make(p, 4);
    return conf;
}
 
static void *merge_dir_config(apr_pool_t *p, void *base_, void *add_)
{
    phpfpm_dir_conf *base = (phpfpm_dir_conf *)base_;
    phpfpm_dir_conf *add  = (phpfpm_dir_conf *)add_;
    phpfpm_dir_conf *conf = apr_pcalloc(p, sizeof(*conf));
 
    conf->url            = (add->url == NULL) ? base->url : add->url;
    conf->body_temp_path = (add->body_temp_path == NULL) ? base->body_temp_path
                                                         : add->body_temp_path;
    conf->php_values     = apr_table_overlay(p, base->php_values, add->php_values);
    apr_table_compress(conf->php_values, APR_OVERLAP_TABLES_SET);
    return conf;
}
 
static const char *add_phpfpm_url(cmd_parms *cmd, void *conf_,
                                  const char *arg)
{
    phpfpm_dir_conf *conf = (phpfpm_dir_conf *)conf_;
    conf->url = arg;
    return NULL;
}
 
static const char *add_php_value(cmd_parms *cmd, void *conf_,
                                const char *name, const char *value)
{
    phpfpm_dir_conf *conf = (phpfpm_dir_conf *)conf_;
    apr_table_setn(conf->php_values, name, value);
    return NULL;
}
 
static const char *add_phpfpm_body_temp_path(cmd_parms *cmd, void *conf_,
                                             const char *arg)
{
    phpfpm_dir_conf *conf = (phpfpm_dir_conf *)conf_;
    conf->body_temp_path = arg;
    return NULL;
}
 
static const char *add_php_flag(cmd_parms *cmd, void *conf,
                                const char *name, const char *value)
{
    char *bool_val = apr_palloc(cmd->pool, sizeof(char) * 2);
 
    if (!strcasecmp(value, "On") || (value[0] == '1' && value[1] == '\0'))
        bool_val[0] = '1';
    else
        bool_val[0] = '0';
    bool_val[1] = 0;
 
    return add_php_value(cmd, conf, name, bool_val);
}
 
static const command_rec config_cmds[] =
{
    AP_INIT_TAKE1("phpfpm_url", add_phpfpm_url,
                  NULL, RSRC_CONF|ACCESS_CONF,
                  "URL to PHP-FPM."),
    AP_INIT_TAKE1("phpfpm_body_temp_path", add_phpfpm_body_temp_path,
                  NULL, RSRC_CONF|ACCESS_CONF,
                  "Directory for storing temporary files holding client request"
                  " bodies."),
    AP_INIT_TAKE2("php_value", add_php_value,
                  NULL, OR_OPTIONS|OR_FILEINFO,
                  "PHP Value Modifier"),
    AP_INIT_TAKE2("php_flag", add_php_flag,
                  NULL, OR_OPTIONS|OR_FILEINFO,
                  "PHP Flag Modifier"),
    {NULL}
};
 
static void register_hooks(apr_pool_t *p)
{
    static const char * const aszSucc[] = { "mod_proxy.c", NULL };
    ap_hook_handler(handler, NULL, aszSucc, APR_HOOK_REALLY_FIRST);
}
 
AP_DECLARE_MODULE(phpfpm_handler) =
{
    STANDARD20_MODULE_STUFF,
    create_dir_config,      /* create per-dir config structure */
    merge_dir_config,       /* merge per-dir config structures */
    NULL,                   /* create per-server config structure */
    NULL,                   /* merge per-server config structures */
    config_cmds,            /* command-table */
    register_hooks          /* register hooks */
};