Author: jim
Date: Tue Jun 2 20:29:00 2026
New Revision: 1934905
Log:
support: add Python ports of the Perl support scripts
Added:
httpd/httpd/trunk/support/apxs-ng.in
httpd/httpd/trunk/support/dbmmanage-ng.in
httpd/httpd/trunk/support/log_server_status-ng.in
httpd/httpd/trunk/support/logresolve.py.in
httpd/httpd/trunk/support/phf_abuse_log-ng.cgi.in
httpd/httpd/trunk/support/split-logfile-ng.in
Modified:
httpd/httpd/trunk/configure.in
httpd/httpd/trunk/support/Makefile.in
Modified: httpd/httpd/trunk/configure.in
==============================================================================
--- httpd/httpd/trunk/configure.in Tue Jun 2 20:10:04 2026 (r1934904)
+++ httpd/httpd/trunk/configure.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -1071,6 +1071,15 @@ if test "x$perlbin" = "x"; then
fi
AC_SUBST(perlbin)
+pythonbin=`$ac_aux_dir/PrintPath python3`
+if test "x$pythonbin" = "x"; then
+ pythonbin=`$ac_aux_dir/PrintPath python`
+fi
+if test "x$pythonbin" = "x"; then
+ pythonbin="/replace/with/path/to/python/interpreter"
+fi
+AC_SUBST(pythonbin)
+
dnl If we are running on a BSD variant, see if we need to use the BSD .include syntax.
ap_make_include=include
@@ -1117,7 +1126,7 @@ AC_SUBST(ap_make_delimiter)
dnl Ensure that docs/conf is created.
test -d docs/conf||$mkdir_p docs/conf
-AC_CONFIG_FILES(docs/conf/httpd.conf docs/conf/extra/httpd-autoindex.conf docs/conf/extra/httpd-dav.conf docs/conf/extra/httpd-default.conf docs/conf/extra/httpd-info.conf docs/conf/extra/httpd-languages.conf docs/conf/extra/httpd-manual.conf docs/conf/extra/httpd-mpm.conf docs/conf/extra/httpd-multilang-errordoc.conf docs/conf/extra/httpd-policy.conf docs/conf/extra/httpd-ssl.conf docs/conf/extra/httpd-userdir.conf docs/conf/extra/httpd-vhosts.conf docs/conf/extra/proxy-html.conf docs/conf/extra/httpd-macro.conf include/ap_config_layout.h support/apxs support/apachectl support/dbmmanage support/envvars-std support/log_server_status support/logresolve.pl support/phf_abuse_log.cgi support/split-logfile build/rules.mk build/pkg/pkginfo build/config_vars.sh)
+AC_CONFIG_FILES(docs/conf/httpd.conf docs/conf/extra/httpd-autoindex.conf docs/conf/extra/httpd-dav.conf docs/conf/extra/httpd-default.conf docs/conf/extra/httpd-info.conf docs/conf/extra/httpd-languages.conf docs/conf/extra/httpd-manual.conf docs/conf/extra/httpd-mpm.conf docs/conf/extra/httpd-multilang-errordoc.conf docs/conf/extra/httpd-policy.conf docs/conf/extra/httpd-ssl.conf docs/conf/extra/httpd-userdir.conf docs/conf/extra/httpd-vhosts.conf docs/conf/extra/proxy-html.conf docs/conf/extra/httpd-macro.conf include/ap_config_layout.h support/apxs support/apxs-ng support/apachectl support/dbmmanage support/dbmmanage-ng support/envvars-std support/log_server_status support/log_server_status-ng support/logresolve.pl support/logresolve.py support/phf_abuse_log.cgi support/phf_abuse_log-
ng.cgi support/split-logfile support/split-logfile-ng build/rules.mk build/pkg/pkginfo build/config_vars.sh)
AC_CONFIG_COMMANDS([default],
[$SHELL $srcdir/build/fastgen.sh $srcdir $ac_cv_mkdir_p $BSD_MAKEFILE $APACHE_FAST_OUTPUT_FILES >&AS_MESSAGE_FD],
[ac_cv_mkdir_p=$ac_cv_mkdir_p
Modified: httpd/httpd/trunk/support/Makefile.in
==============================================================================
--- httpd/httpd/trunk/support/Makefile.in Tue Jun 2 20:10:04 2026 (r1934904)
+++ httpd/httpd/trunk/support/Makefile.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -1,5 +1,7 @@
-DISTCLEAN_TARGETS = apxs apachectl dbmmanage log_server_status \
- logresolve.pl phf_abuse_log.cgi split-logfile envvars-std
+DISTCLEAN_TARGETS = apxs apxs-ng apachectl dbmmanage dbmmanage-ng \
+ log_server_status log_server_status-ng logresolve.pl logresolve.py \
+ phf_abuse_log.cgi phf_abuse_log-ng.cgi split-logfile split-logfile-ng \
+ envvars-std
CLEAN_TARGETS = suexec
@@ -17,7 +19,7 @@ install:
@test -d $(DESTDIR)$(sbindir) || $(MKINSTALLDIRS) $(DESTDIR)$(sbindir)
@test -d $(DESTDIR)$(libexecdir) || $(MKINSTALLDIRS) $(DESTDIR)$(libexecdir)
@cp -p $(top_builddir)/server/httpd.exp $(DESTDIR)$(libexecdir)
- @for i in apxs dbmmanage; do \
+ @for i in apxs apxs-ng dbmmanage dbmmanage-ng; do \
if test -f "$(builddir)/$$i"; then \
cp -p $$i $(DESTDIR)$(bindir); \
chmod 755 $(DESTDIR)$(bindir)/$$i; \
Added: httpd/httpd/trunk/support/apxs-ng.in
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ httpd/httpd/trunk/support/apxs-ng.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -0,0 +1,775 @@
+#!@pythonbin@
+#
+# 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.
+#
+# apxs-ng -- APache eXtenSion tool (Python port of the historical Perl apxs)
+#
+# This is a behavior-compatible reimplementation of support/apxs.in in
+# Python 3. It is intended to produce byte-identical output and side
+# effects to the Perl version for all documented options (-g, -q, -c,
+# -i, -e, -a, -A).
+
+import os
+import re
+import sys
+import subprocess
+
+#
+# Configuration
+#
+
+# These tokens are substituted by ./configure (see AC_CONFIG_FILES),
+# exactly as in the Perl apxs.in.
+EXP_BINDIR = "@exp_bindir@"
+EXP_INSTALLBUILD = "@exp_installbuilddir@"
+MOD_SO_ENABLED = "@MOD_SO_ENABLED@"
+
+PROG = "apxs" # keep the historical name in all messages/errors
+
+
+def error(msg):
+ sys.stderr.write("%s:Error: %s.\n" % (PROG, msg))
+
+
+def notice(msg):
+ sys.stderr.write("%s\n" % msg)
+
+
+# are we building in a cross compile environment? If so, destdir contains
+# the base directory of the cross compiled environment, otherwise destdir
+# is the empty string.
+def compute_destdir():
+ argv0 = sys.argv[0]
+ idx = argv0.rfind(EXP_BINDIR)
+ if idx >= 0:
+ return argv0[0:idx]
+ return ""
+
+
+config_vars = {}
+
+
+def get_config_vars(path, dest):
+ try:
+ fh = open(path, "r")
+ except IOError as e:
+ sys.stderr.write("cannot open %s: %s\n" % (path, e.strerror))
+ sys.exit(2)
+ with fh:
+ for line in fh:
+ line = line.rstrip("\n")
+ m = re.match(r"^\s*(.*?)\s*=\s*(.*)$", line)
+ if m:
+ dest[m.group(1)] = m.group(2)
+
+
+# internal (CFG_*) variables -- uppercase aliases overridable via -S.
+# Stored in a dict keyed by the uppercase var name (without the CFG_
+# prefix), mirroring the Perl $CFG_<var> lexicals.
+internal_var_names = set("""TARGET CC CFLAGS CFLAGS_SHLIB LD_SHLIB
+ LDFLAGS_SHLIB LIBS_SHLIB PREFIX SBINDIR INCLUDEDIR LIBEXECDIR
+ SYSCONFDIR""".split())
+
+cfg = {}
+
+
+def _expand(value, seen=None):
+ """Reproduce the Perl `eval qq(...)` interpolation used by get_vars.
+
+ The raw make-style value first has all parentheses stripped
+ (s/[()]//g in the original), turning $(foo) into $foo, then $foo and
+ ${foo} references are interpolated against config_vars. We resolve
+ recursively so chained values (prefix -> exec_prefix -> sbindir)
+ collapse to their final string, matching the dependency-ordered
+ lexical interpolation the Perl version relied on.
+ """
+ if seen is None:
+ seen = set()
+ # strip parens: $(includedir) -> $includedir (matches s/[()]//g)
+ value = value.replace("(", "").replace(")", "")
+
+ def repl(m):
+ name = m.group(1) or m.group(2)
+ if name in seen:
+ return "" # guard against pathological self-reference
+ if name in config_vars:
+ seen.add(name)
+ out = _expand(config_vars[name], seen)
+ seen.discard(name)
+ return out
+ return "" # undefined lexical interpolates to empty, as in Perl qq
+
+ return re.sub(r"\$\{(\w+)\}|\$(\w+)", repl, value)
+
+
+def get_vars(*args):
+ result = ""
+ # NOTE: `ok` is intentionally NOT reset per-argument. The historical
+ # Perl get_vars() declared `my $ok = 0;` once outside the loop, so once
+ # any argument resolves (via config_vars or the internal CFG_* vars)
+ # every *later* argument that would need the internal branch -- e.g.
+ # TARGET, which has no lowercase key in config_vars -- is silently
+ # skipped, and a later invalid name raises no error. We preserve that
+ # quirk verbatim for byte-identical -q output.
+ ok = False
+ for arg in args:
+ if arg in config_vars or arg.lower() in config_vars:
+ val = config_vars[arg] if arg in config_vars else config_vars[arg.lower()]
+ result += _expand(val)
+ result += ";;"
+ ok = True
+ if not ok:
+ if arg in internal_var_names or arg.lower() in internal_var_names:
+ key = arg if arg in internal_var_names else arg.lower()
+ key = key.upper()
+ val = cfg.get(key)
+ if val is not None:
+ result += val
+ result += ";;"
+ ok = True
+ if not ok:
+ error("Invalid query string `%s'" % arg)
+ sys.exit(1)
+ if result.endswith(";;"):
+ result = result[:-2]
+ return result
+
+
+#
+# argument parsing -- faithful port of the Perl Getopts() that supports
+# ':' (single value) and '+' (appended list) option suffixes, clustering,
+# and "-Xvalue" / "-X value" forms.
+#
+
+class Opts:
+ def __init__(self):
+ self.n = ""
+ self.g = False
+ self.c = False
+ self.o = ""
+ self.D = []
+ self.I = []
+ self.L = []
+ self.l = []
+ self.W = []
+ self.S = []
+ self.e = False
+ self.i = False
+ self.a = False
+ self.A = False
+ self.q = False
+ self.h = False
+ self.p = False
+ self.v = False
+
+
+def getopts(spec, argv):
+ """Returns (ok, opts, remaining_argv)."""
+ opts = Opts()
+ errs = 0
+ argv = list(argv)
+ list_opts = set("DILlWS") # options with '+' suffix
+ value_opts = set("no") # options with ':' suffix
+
+ while argv:
+ cur = argv[0]
+ m = re.match(r"^-(.)(.*)", cur)
+ if not m:
+ break
+ if cur == "--":
+ argv.pop(0)
+ break
+ first, rest = m.group(1), m.group(2)
+ if first in spec:
+ if first in value_opts:
+ argv.pop(0)
+ if rest == "":
+ if not argv:
+ error("Incomplete option: %s (needs an argument)" % first)
+ errs += 1
+ rest = ""
+ else:
+ rest = argv.pop(0)
+ setattr(opts, first, rest)
+ elif first in list_opts:
+ argv.pop(0)
+ if rest == "":
+ if not argv:
+ error("Incomplete option: %s (needs an argument)" % first)
+ errs += 1
+ rest = ""
+ else:
+ rest = argv.pop(0)
+ getattr(opts, first).append(rest)
+ else:
+ setattr(opts, first, True)
+ if rest == "":
+ argv.pop(0)
+ else:
+ argv[0] = "-" + rest
+ else:
+ error("Unknown option: %s" % first)
+ errs += 1
+ if rest != "":
+ argv[0] = "-" + rest
+ else:
+ argv.pop(0)
+ return (errs == 0, opts, argv)
+
+
+def usage():
+ sys.stderr.write("Usage: apxs -g [-S <var>=<val>] -n <modname>\n")
+ sys.stderr.write(" apxs -q [-v] [-S <var>=<val>] [<query> ...]\n")
+ sys.stderr.write(" apxs -c [-S <var>=<val>] [-o <dsofile>] [-D <name>[=<value>]]\n")
+ sys.stderr.write(" [-I <incdir>] [-L <libdir>] [-l <libname>] [-Wc,<flags>]\n")
+ sys.stderr.write(" [-Wl,<flags>] [-p] <files> ...\n")
+ sys.stderr.write(" apxs -i [-S <var>=<val>] [-a] [-A] [-n <modname>] <dsofile> ...\n")
+ sys.stderr.write(" apxs -e [-S <var>=<val>] [-a] [-A] [-n <modname>] <dsofile> ...\n")
+ sys.exit(1)
+
+
+# helper: execute a list of system commands with return code checks
+def execute_cmds(cmds):
+ for cmd in cmds:
+ notice(cmd)
+ rc = subprocess.call(cmd, shell=True)
+ if rc:
+ # Perl's `system` returns the raw wait status ($?), i.e. the
+ # exit code shifted left by 8. The historical apxs then prints
+ # that value shifted left by 8 *again*, so a child exiting 1 is
+ # reported as rc=65536. Reconstruct $? from the exit code to
+ # reproduce that number exactly.
+ status = (rc << 8) if rc > 0 else rc
+ error("Command failed with rc=%d\n" % (status << 8))
+ sys.exit(1)
+
+
+def backtick(cmd):
+ """Run a command via the shell and return its stdout (like Perl ``)."""
+ return subprocess.check_output(cmd, shell=True).decode("utf-8", "replace")
+
+
+def main():
+ destdir = compute_destdir()
+
+ installbuilddir = EXP_INSTALLBUILD
+ get_config_vars(destdir + installbuilddir + "/config_vars.mk", config_vars)
+
+ # read the configuration variables once (module-level lexicals)
+ prefix = get_vars("prefix")
+ CFG_PREFIX = prefix
+ # exec_prefix, datadir, localstatedir are read for parity though the
+ # original only uses a subset directly.
+ get_vars("exec_prefix")
+ get_vars("datadir")
+ get_vars("localstatedir")
+ CFG_TARGET = get_vars("progname")
+ CFG_SYSCONFDIR = get_vars("sysconfdir")
+ CFG_CFLAGS = " ".join(get_vars(x) for x in
+ ("SHLTCFLAGS", "CFLAGS", "NOTEST_CPPFLAGS", "EXTRA_CPPFLAGS", "EXTRA_CFLAGS"))
+ CFG_LDFLAGS = " ".join(get_vars(x) for x in
+ ("LDFLAGS", "NOTEST_LDFLAGS", "SH_LDFLAGS"))
+ includedir = destdir + get_vars("includedir")
+ CFG_INCLUDEDIR = includedir
+ CFG_CC = get_vars("CC")
+ libexecdir = destdir + get_vars("libexecdir")
+ CFG_LIBEXECDIR = libexecdir
+ sbindir = get_vars("sbindir")
+ CFG_SBINDIR = sbindir
+
+ ltflags = os.environ.get("LTFLAGS") or "--silent"
+
+ # seed the CFG_* (internal var) namespace used by get_vars and -S
+ cfg["PREFIX"] = CFG_PREFIX
+ cfg["TARGET"] = CFG_TARGET
+ cfg["SYSCONFDIR"] = CFG_SYSCONFDIR
+ cfg["CFLAGS"] = CFG_CFLAGS
+ cfg["LDFLAGS"] = CFG_LDFLAGS
+ cfg["INCLUDEDIR"] = CFG_INCLUDEDIR
+ cfg["CC"] = CFG_CC
+ cfg["LIBEXECDIR"] = CFG_LIBEXECDIR
+ cfg["SBINDIR"] = CFG_SBINDIR
+
+ # option handling. The spec just enumerates valid option letters;
+ # which take a value (':') or append to a list ('+') is encoded by
+ # the value_opts/list_opts sets inside getopts().
+ spec = set("qngcoIDLlWSeiaApv")
+ ok, opt, args = getopts(spec, sys.argv[1:])
+
+ if not ok:
+ usage()
+ if len(args) == 0 and not opt.g and not opt.q:
+ usage()
+ if not opt.q and not (opt.g and opt.n) and not opt.i and not opt.c and not opt.e:
+ usage()
+
+ name = "unknown"
+ if opt.n != "":
+ name = opt.n
+
+ # -S var=val overrides
+ for s in opt.S:
+ m = re.match(r"^([^=]+)=(.*)$", s)
+ if m:
+ var = m.group(1)
+ val = m.group(2)
+ oldval = cfg.get(var)
+ if not (var and oldval):
+ error("no config variable %s" % var)
+ usage()
+ cfg[var] = val
+ else:
+ error("malformatted -S option")
+ usage()
+
+ # refresh local copies that may have been overridden via -S
+ CFG_PREFIX = cfg["PREFIX"]
+ CFG_TARGET = cfg["TARGET"]
+ CFG_SYSCONFDIR = cfg["SYSCONFDIR"]
+ CFG_CFLAGS = cfg["CFLAGS"]
+ CFG_LDFLAGS = cfg["LDFLAGS"]
+ CFG_INCLUDEDIR = cfg["INCLUDEDIR"]
+ CFG_CC = cfg["CC"]
+ CFG_LIBEXECDIR = cfg["LIBEXECDIR"]
+ CFG_SBINDIR = cfg["SBINDIR"]
+
+ #
+ # Initial shared object support check
+ #
+ if MOD_SO_ENABLED != "yes":
+ error("Sorry, no shared object support for Apache")
+ error("available under your platform. Make sure")
+ error("the Apache module mod_so is compiled into")
+ error("the server binary")
+ sys.exit(1)
+
+ #
+ # -g : SAMPLE MODULE SOURCE GENERATION
+ #
+ if opt.g:
+ if os.path.isdir(name):
+ error("Directory `%s' already exists. Remove first" % name)
+ sys.exit(1)
+
+ data = DATA
+ data = data.replace("%NAME%", name)
+ data = data.replace("%TARGET%", CFG_TARGET)
+ data = data.replace("%PREFIX%", prefix)
+ data = data.replace("%INSTALLBUILDDIR%", installbuilddir)
+
+ m = re.match(r"(.+)-=#=-\n(.+)-=#=-\n(.+)", data, re.S)
+ mkf, mods, src = m.group(1), m.group(2), m.group(3)
+
+ notice("Creating [DIR] %s" % name)
+ os.system("mkdir %s" % name)
+ notice("Creating [FILE] %s/Makefile" % name)
+ with open("%s/Makefile" % name, "w") as fp:
+ fp.write(mkf)
+ notice("Creating [FILE] %s/modules.mk" % name)
+ with open("%s/modules.mk" % name, "w") as fp:
+ fp.write(mods)
+ notice("Creating [FILE] %s/mod_%s.c" % (name, name))
+ with open("%s/mod_%s.c" % (name, name), "w") as fp:
+ fp.write(src)
+ notice("Creating [FILE] %s/.deps" % name)
+ os.system("touch %s/.deps" % name)
+ sys.exit(0)
+
+ #
+ # -q : QUERY INFORMATION
+ #
+ if opt.q:
+ if len(args) >= 1:
+ result = get_vars(*args)
+ sys.stdout.write("%s\n" % result)
+ else:
+ # -q without var name prints all variables and their values
+ if opt.v:
+ vars_sorted = sorted(config_vars.keys(), key=lambda s: s.upper())
+ width = 0
+ for k in vars_sorted:
+ if len(k) > width:
+ width = len(k)
+ for k in vars_sorted:
+ sys.stdout.write("%-*s = %s\n" % (width, k, config_vars[k]))
+ else:
+ for k in config_vars:
+ sys.stdout.write("%s=%s\n" % (k, config_vars[k]))
+
+ apr_config = destdir + get_vars("APR_CONFIG")
+ if not (os.path.isfile(apr_config) and os.access(apr_config, os.X_OK)):
+ error("%s not found!" % apr_config)
+ sys.exit(1)
+
+ apr_major_version = int(backtick("%s --version" % apr_config).split(".")[0])
+
+ apu_config = ""
+ if apr_major_version < 2:
+ apu_config = destdir + get_vars("APU_CONFIG")
+ if not (os.path.isfile(apu_config) and os.access(apu_config, os.X_OK)):
+ error("%s not found!" % apu_config)
+ sys.exit(1)
+
+ libtool = backtick("%s --apr-libtool" % apr_config).rstrip("\n")
+ apr_includedir = backtick("%s --includes" % apr_config).rstrip("\n")
+ apu_includedir = ""
+ if apr_major_version < 2:
+ apu_includedir = backtick("%s --includes" % apu_config).rstrip("\n")
+
+ #
+ # -c : SHARED OBJECT COMPILATION
+ #
+ if opt.c:
+ srcs = []
+ objs = []
+ for f in args:
+ if f.endswith(".c"):
+ srcs.append(f)
+ else:
+ objs.append(f)
+
+ # determine output file
+ if opt.o == "":
+ if srcs:
+ dso_file = re.sub(r"\.[^.]+$", ".la", srcs[0])
+ elif objs:
+ dso_file = re.sub(r"\.[^.]+$", ".la", objs[0])
+ else:
+ dso_file = "mod_unknown.la"
+ else:
+ dso_file = re.sub(r"\.[^.]+$", ".la", opt.o)
+
+ cmds = []
+ compile_opt = ""
+ for w in opt.W:
+ m = re.match(r"^\s*c,(.*)$", w)
+ if m:
+ compile_opt += m.group(1) + " "
+ for inc in opt.I:
+ compile_opt += "-I%s " % inc
+ for d in opt.D:
+ compile_opt += "-D%s " % d
+
+ cflags = CFG_CFLAGS
+ for s in srcs:
+ slo = re.sub(r"\.c$", ".slo", s)
+ lo = re.sub(r"\.c$", ".lo", s)
+ cmds.append("%s %s --mode=compile %s %s -I%s %s %s %s -c -o %s %s && touch %s" %
+ (libtool, ltflags, CFG_CC, cflags, CFG_INCLUDEDIR,
+ apr_includedir, apu_includedir, compile_opt, lo, s, slo))
+ objs.insert(0, lo)
+
+ # create link command
+ lo_all = ""
+ for o in objs:
+ lo_all += " " + o
+
+ link_opt = ""
+ for w in opt.W:
+ m = re.match(r"^\s*l,(.*)$", w)
+ if m:
+ link_opt += m.group(1) + " "
+ for L in opt.L:
+ link_opt += " -L%s" % L
+ for l in opt.l:
+ link_opt += " -l%s" % l
+
+ ldflags = CFG_LDFLAGS
+ if opt.p:
+ apr_libs = backtick("%s --cflags --ldflags --link-libtool --libs" % apr_config).rstrip("\n")
+ apu_libs = ""
+ if apr_major_version < 2:
+ apu_libs = backtick("%s --ldflags --link-libtool --libs" % apu_config).rstrip("\n")
+ link_opt += " " + apu_libs + " " + apr_libs
+ else:
+ apr_ldflags = backtick("%s --ldflags" % apr_config).rstrip("\n")
+ link_opt += " -rpath %s -module -avoid-version %s" % (CFG_LIBEXECDIR, apr_ldflags)
+
+ cmds.append("%s %s --mode=link %s %s -o %s %s %s" %
+ (libtool, ltflags, CFG_CC, ldflags, dso_file, link_opt, lo_all))
+
+ execute_cmds(cmds)
+
+ # allow one-step compilation and installation
+ if opt.i or opt.e:
+ args = [dso_file]
+
+ #
+ # -i / -e : SHARED OBJECT INSTALLATION
+ #
+ if opt.i or opt.e:
+ lmd = []
+ cmds = []
+ for f in args:
+ # ack all potential gcc, hp/ux, win32+os2+aix and os/x extensions
+ if not re.search(r"(\.so$|\.la$|\.sl$|\.dll$|\.dylib$|)", f):
+ error("file %s is not a shared object" % f)
+ sys.exit(1)
+ t = re.sub(r"^.+/([^/]+)$", r"\1", f)
+ # use .so unambigiously for installed shared library modules
+ t = re.sub(r"\.[^./\\]+$", ".so", t)
+ if opt.i:
+ cmds.append(destdir + "%s/instdso.sh SH_LIBTOOL='%s' %s %s" %
+ (installbuilddir, libtool, f, CFG_LIBEXECDIR))
+ cmds.append("chmod 755 %s/%s" % (CFG_LIBEXECDIR, t))
+
+ # determine module symbolname and filename
+ filename = ""
+ if name == "unknown":
+ name = ""
+ base = re.sub(r"\.[^.]+$", "", f)
+ if os.path.isfile("%s.c" % base):
+ with open("%s.c" % base, "r") as cfp:
+ content = cfp.read()
+ m = re.search(r".*AP_DECLARE_MODULE\s*\(\s*([a-zA-Z0-9_]+)\s*\)\s*=.*", content, re.S)
+ if not m:
+ m = re.search(r".*module\s+(?:AP_MODULE_DECLARE_DATA\s+)?([a-zA-Z0-9_]+)_module\s*=\s*.*", content, re.S)
+ if m:
+ name = m.group(1)
+ filename = "%s.c" % base
+ filename = re.sub(r"^[^/]+/", "", filename)
+ if name == "":
+ m = re.match(r".*mod_([a-zA-Z0-9_]+)(\..+|$)", base)
+ if m:
+ name = m.group(1)
+ filename = base
+ filename = re.sub(r"^[^/]+/", "", filename)
+ if name == "":
+ error("Sorry, cannot determine bootstrap symbol name")
+ error("Please specify one with option `-n'")
+ sys.exit(1)
+ if filename == "":
+ filename = "mod_%s.c" % name
+ dir_ = CFG_LIBEXECDIR
+ dir_ = re.sub(r"^%s/?" % re.escape(CFG_PREFIX), "", dir_)
+ dir_ = re.sub(r"(.)$", r"\1/", dir_)
+ t = re.sub(r"\.la$", ".so", t)
+ lmd.append("LoadModule %-18s %s" % ("%s_module" % name, "%s%s" % (dir_, t)))
+
+ execute_cmds(cmds)
+
+ # activate module via LoadModule/AddModule directive
+ if opt.a or opt.A:
+ conf = "%s/%s.conf" % (CFG_SYSCONFDIR, CFG_TARGET)
+ if not os.path.isfile(conf):
+ error("Config file %s not found" % conf)
+ sys.exit(1)
+
+ with open(conf, "r") as fp:
+ content = fp.read()
+
+ if not re.search(r"\n#?\s*LoadModule\s+", content):
+ error("Activation failed for custom %s file." % conf)
+ error("At least one `LoadModule' directive already has to exist")
+ sys.exit(1)
+
+ c = "#" if opt.A else ""
+ for entry in lmd:
+ what = "preparing" if opt.A else "activating"
+ lmd_re = re.sub(r"\s+", r"\\s+", entry)
+
+ if not re.search(r"\n#?\s*" + lmd_re, content):
+ # find everything up to and including the LAST LoadModule
+ # (greedy), to count open/closed <containers> before it.
+ bm = re.search(r"^(.*\n)#?\s*LoadModule\s+[^\n]+\n", content, re.S)
+ before = bm.group(1) if bm else ""
+
+ cntopen = len(re.findall(r"^\s*<[^/].*$", before, re.M))
+ cntclose = len(re.findall(r"^\s*</.*$", before, re.M))
+
+ if cntopen == cntclose:
+ # fine. Last LoadModule is contextless.
+ content = re.sub(r"^(.*\n#?\s*LoadModule\s+[^\n]+\n)",
+ lambda m: m.group(1) + c + entry + "\n",
+ content, count=1, flags=re.S)
+ elif cntopen < cntclose:
+ error("Configuration file is not valid. There are sections"
+ " closed before opened")
+ sys.exit(1)
+ else:
+ # put our cmd after the section containing the last
+ # LoadModule.
+ pat = (r"\A("
+ r"(?:(?:"
+ r"^\s*"
+ r"(?:[^<]|<[^/])"
+ r".*(?:$)\n"
+ r")*"
+ r"^\s*</.*(?:$)\n?"
+ r"){%d})" % cntopen)
+ new_content, found = re.subn(
+ pat,
+ lambda m: m.group(1) + c + entry + "\n",
+ content, count=1, flags=re.M)
+ if not found:
+ error("Configuration file is not valid. There are "
+ "sections opened and not closed")
+ sys.exit(1)
+ content = new_content
+ else:
+ # replace already existing LoadModule line
+ content = re.sub(r"^(.*\n)#?\s*" + lmd_re + r"[^\n]*\n",
+ lambda m: m.group(1) + c + entry + "\n",
+ content, count=1, flags=re.S)
+ m = re.match(r"LoadModule\s+(.+?)_module.*", entry)
+ modname = m.group(1) if m else ""
+ notice("[%s module `%s' in %s]" % (what, modname, conf))
+
+ if lmd:
+ try:
+ nfp = open("%s.new" % conf, "w")
+ except IOError:
+ notice("unable to open configuration file")
+ else:
+ with nfp:
+ nfp.write(content)
+ os.system("cp %s %s.bak && cp %s.new %s && rm %s.new" %
+ (conf, conf, conf, conf, conf))
+
+
+#
+# DATA -- templates for `apxs -g' (Makefile, modules.mk, mod_NAME.c),
+# split on the -=#=- markers, identical to the Perl __DATA__ section.
+#
+DATA = r"""##
+## Makefile -- Build procedure for sample %NAME% Apache module
+## Autogenerated via ``apxs -n %NAME% -g''.
+##
+
+builddir=.
+top_srcdir=%PREFIX%
+top_builddir=%PREFIX%
+include %INSTALLBUILDDIR%/special.mk
+
+# the used tools
+APACHECTL=apachectl
+
+# additional defines, includes and libraries
+#DEFS=-Dmy_define=my_value
+#INCLUDES=-Imy/include/dir
+#LIBS=-Lmy/lib/dir -lmylib
+
+# the default target
+all: local-shared-build
+
+# install the shared object file into Apache
+install: install-modules-yes
+
+# cleanup
+clean:
+ -rm -f mod_%NAME%.o mod_%NAME%.lo mod_%NAME%.slo mod_%NAME%.la
+
+# simple test
+test: reload
+ lynx -mime_header http://localhost/%NAME%
+
+# install and activate shared object by reloading Apache to
+# force a reload of the shared object file
+reload: install restart
+
+# the general Apache start/restart/stop
+# procedures
+start:
+ $(APACHECTL) start
+restart:
+ $(APACHECTL) restart
+stop:
+ $(APACHECTL) stop
+
+-=#=-
+mod_%NAME%.la: mod_%NAME%.slo
+ $(SH_LINK) -rpath $(libexecdir) -module -avoid-version mod_%NAME%.lo
+DISTCLEAN_TARGETS = modules.mk
+shared = mod_%NAME%.la
+-=#=-
+/*
+** mod_%NAME%.c -- Apache sample %NAME% module
+** [Autogenerated via ``apxs -n %NAME% -g'']
+**
+** To play with this sample module first compile it into a
+** DSO file and install it into Apache's modules directory
+** by running:
+**
+** $ apxs -c -i mod_%NAME%.c
+**
+** Then activate it in Apache's %TARGET%.conf file for instance
+** for the URL /%NAME% in as follows:
+**
+** # %TARGET%.conf
+** LoadModule %NAME%_module modules/mod_%NAME%.so
+** <Location /%NAME%>
+** SetHandler %NAME%
+** </Location>
+**
+** Then after restarting Apache via
+**
+** $ apachectl restart
+**
+** you immediately can request the URL /%NAME% and watch for the
+** output of this module. This can be achieved for instance via:
+**
+** $ lynx -mime_header http://localhost/%NAME%
+**
+** The output should be similar to the following one:
+**
+** HTTP/1.1 200 OK
+** Date: Tue, 31 Mar 1998 14:42:22 GMT
+** Server: Apache/1.3.4 (Unix)
+** Connection: close
+** Content-Type: text/html
+**
+** The sample page from mod_%NAME%.c
+*/
+
+#include "httpd.h"
+#include "http_config.h"
+#include "http_protocol.h"
+#include "ap_config.h"
+
+/* The sample content handler */
+static int %NAME%_handler(request_rec *r)
+{
+ if (strcmp(r->handler, "%NAME%")) {
+ return DECLINED;
+ }
+ r->content_type = "text/html";
+
+ if (!r->header_only)
+ ap_rputs("The sample page from mod_%NAME%.c\n", r);
+ return OK;
+}
+
+static void %NAME%_register_hooks(apr_pool_t *p)
+{
+ ap_hook_handler(%NAME%_handler, NULL, NULL, APR_HOOK_MIDDLE);
+}
+
+/* Dispatch list for API hooks */
+module AP_MODULE_DECLARE_DATA %NAME%_module = {
+ STANDARD20_MODULE_STUFF,
+ NULL, /* create per-dir config structures */
+ NULL, /* merge per-dir config structures */
+ NULL, /* create per-server config structures */
+ NULL, /* merge per-server config structures */
+ NULL, /* table of config file commands */
+ %NAME%_register_hooks /* register hooks */
+};
+
+"""
+
+
+if __name__ == "__main__":
+ main()
Added: httpd/httpd/trunk/support/dbmmanage-ng.in
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ httpd/httpd/trunk/support/dbmmanage-ng.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -0,0 +1,417 @@
+#!@pythonbin@
+#
+# 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.
+#
+# dbmmanage-ng -- Python port of the historical Perl dbmmanage.
+#
+# This is a behavior-compatible reimplementation of support/dbmmanage.in
+# in Python 3. It manages user/password DBM files for Apache auth.
+#
+# usage: dbmmanage <DBMfile> <command> <user> <password> <groups> <comment>
+
+import os
+import re
+import sys
+import base64
+import hashlib
+import getpass
+import random
+import dbm
+
+# Python's crypt module is deprecated in 3.11+ and removed in 3.13. The
+# Perl original uses crypt(), so we use it too -- but degrade gracefully
+# if it is unavailable on this interpreter.
+try:
+ import crypt as _crypt
+except ImportError:
+ _crypt = None
+
+
+# The set of characters used to build salts: [./0-9A-Za-z], matching the
+# Perl genseed()/randchar() range.
+_SALT_CHARS = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+# Default hashing method, like the Perl original (crypt on Unix).
+hash_method = "crypt"
+
+
+def usage():
+ # Commands are listed sorted, like "join '|', sort keys %dbmc::".
+ cmds = "|".join(sorted(COMMANDS.keys()))
+ sys.stderr.write(
+ "Usage: dbmmanage [enc] dbname command "
+ "[username [pw [group[,group] [comment]]]]\n"
+ "\n"
+ " where enc is -d for crypt hashing (default except on Win32, Netware)\n"
+ " -m for MD5 hashing (default on Win32, Netware)\n"
+ " -s for SHA1 hashing\n"
+ " -p for plaintext\n"
+ "\n"
+ " command is one of: " + cmds + "\n"
+ "\n"
+ " pw of . for update command retains the old password\n"
+ " pw of - (or blank) for update command prompts for the password\n"
+ "\n"
+ " groups or comment of . (or blank) for update command retains old values\n"
+ " groups or comment of - for update command clears the existing value\n"
+ " groups or comment of - for add and adduser commands is the empty value\n"
+ )
+ sys.exit(1)
+
+
+def die(msg):
+ sys.stderr.write(msg)
+ sys.exit(1)
+
+
+def randchar(n=1):
+ return "".join(random.choice(_SALT_CHARS) for _ in range(n))
+
+
+def saltpw_crypt():
+ # The Perl original optionally uses a "newstyle" salt on bsdos; that is
+ # an obscure edge case, so we always use the traditional 2-char salt.
+ return randchar(2)
+
+
+def hashpw_crypt(pw, salt=None):
+ if _crypt is None:
+ die("dbmmanage: crypt hashing is not available on this Python "
+ "interpreter.\nPlease use a different hashing option (-m, -s, -p).\n")
+ if not salt:
+ salt = saltpw_crypt()
+ return _crypt.crypt(pw, salt)
+
+
+def saltpw_md5():
+ return randchar(8)
+
+
+# Apache's apr1 / md5crypt algorithm. Python's stdlib has no apr1
+# implementation, so we implement it here. This is the classic md5crypt
+# routine with the magic string "$apr1$". Given the same (pw, salt) it
+# produces the identical $apr1$<salt>$<hash> string as htpasswd / Apache.
+def apache_md5_crypt(pw, salt):
+ magic = "$apr1$"
+ pw_b = pw.encode("utf-8", "surrogateescape")
+
+ # If a full $apr1$salt$ string was passed in (e.g. when verifying),
+ # extract just the salt portion.
+ if salt.startswith(magic):
+ salt = salt[len(magic):]
+ salt = salt.split("$", 1)[0]
+ salt = salt[:8]
+ salt_b = salt.encode("ascii")
+
+ # Primary digest: password + magic + salt.
+ ctx = hashlib.md5(pw_b + magic.encode("ascii") + salt_b)
+
+ # Alternate digest: password + salt + password.
+ alt = hashlib.md5(pw_b + salt_b + pw_b).digest()
+
+ # Add as many chars of the alternate digest as the password length.
+ pw_len = len(pw_b)
+ i = pw_len
+ while i > 0:
+ ctx.update(alt[:16] if i > 16 else alt[:i])
+ i -= 16
+
+ # For each bit of the password length, add either a NUL byte or the
+ # first byte of the password.
+ i = pw_len
+ while i:
+ if i & 1:
+ ctx.update(b"\x00")
+ else:
+ ctx.update(pw_b[:1])
+ i >>= 1
+
+ final = ctx.digest()
+
+ # 1000 iterations of strengthening.
+ for i in range(1000):
+ ctx = hashlib.md5()
+ if i & 1:
+ ctx.update(pw_b)
+ else:
+ ctx.update(final)
+ if i % 3:
+ ctx.update(salt_b)
+ if i % 7:
+ ctx.update(pw_b)
+ if i & 1:
+ ctx.update(final)
+ else:
+ ctx.update(pw_b)
+ final = ctx.digest()
+
+ # Custom base64 encoding (md5crypt order/alphabet: ./0-9A-Za-z).
+ itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+ def _to64(v, n):
+ out = []
+ for _ in range(n):
+ out.append(itoa64[v & 0x3f])
+ v >>= 6
+ return "".join(out)
+
+ out = ""
+ out += _to64((final[0] << 16) | (final[6] << 8) | final[12], 4)
+ out += _to64((final[1] << 16) | (final[7] << 8) | final[13], 4)
+ out += _to64((final[2] << 16) | (final[8] << 8) | final[14], 4)
+ out += _to64((final[3] << 16) | (final[9] << 8) | final[15], 4)
+ out += _to64((final[4] << 16) | (final[10] << 8) | final[5], 4)
+ out += _to64(final[11], 2)
+
+ return magic + salt + "$" + out
+
+
+def hashpw_md5(pw, salt=None):
+ if not salt:
+ salt = saltpw_md5()
+ return apache_md5_crypt(pw, salt)
+
+
+def hashpw_sha1(pw, salt=None):
+ # '{SHA}' + base64(sha1(pw)). The Perl original used unpadded base64
+ # from Digest::SHA1 then appended a literal "="; the de-facto correct
+ # Apache htpasswd {SHA} format is the standard *padded* base64 (28
+ # chars ending in "=" for a 20-byte digest), which base64.b64encode
+ # produces directly.
+ return "{SHA}" + base64.b64encode(
+ hashlib.sha1(pw.encode("utf-8", "surrogateescape")).digest()
+ ).decode("ascii")
+
+
+def hashpw(pw, salt=None):
+ if hash_method == "md5":
+ return hashpw_md5(pw, salt)
+ elif hash_method == "sha1":
+ return hashpw_sha1(pw, salt)
+ elif hash_method == "crypt":
+ return hashpw_crypt(pw, salt)
+ return pw # otherwise return plaintext
+
+
+def prompt_pass(prompt="Enter password:"):
+ pwd = getpass.getpass(prompt)
+ if not len(pwd):
+ die("Can't use empty password!\n")
+ return pwd
+
+
+# ---------------------------------------------------------------------------
+# DBM access helpers. dbm stores bytes; we use latin-1 consistently so that
+# arbitrary hashed-password bytes round-trip cleanly.
+# ---------------------------------------------------------------------------
+
+_ENC = "latin-1"
+
+
+def db_has(db, key):
+ return key.encode(_ENC) in db
+
+
+def db_get(db, key):
+ return db[key.encode(_ENC)].decode(_ENC)
+
+
+def db_set(db, key, value):
+ db[key.encode(_ENC)] = value.encode(_ENC)
+
+
+def db_del(db, key):
+ del db[key.encode(_ENC)]
+
+
+def db_keys(db):
+ return [k.decode(_ENC) for k in db.keys()]
+
+
+# ---------------------------------------------------------------------------
+# Commands (the Perl dbmc:: subs). These operate on the module-level state
+# (db, key, hashed_pwd, groups, comment, is_update).
+# ---------------------------------------------------------------------------
+
+class State:
+ db = None
+ key = None
+ hashed_pwd = None
+ groups = None
+ comment = None
+ is_update = False
+
+
+S = State()
+
+
+def cmd_add():
+ if not S.hashed_pwd:
+ die("Can't use empty password!\n")
+ if not S.is_update:
+ if db_has(S.db, S.key):
+ die("Sorry, user `%s' already exists!\n" % S.key)
+ groups = S.groups or ""
+ comment = S.comment or ""
+ if groups == "-":
+ groups = ""
+ if comment == "-":
+ comment = ""
+ if comment:
+ groups = groups + ":" + comment
+ value = S.hashed_pwd
+ if groups:
+ value = value + ":" + groups
+ db_set(S.db, S.key, value)
+ action = "updated" if S.is_update else "added"
+ print("User %s %s with password hashed to %s using %s"
+ % (S.key, action, value, hash_method))
+
+
+def cmd_adduser():
+ value = prompt_pass("New password:")
+ if prompt_pass("Re-type new password:") != value:
+ die("They don't match, sorry.\n")
+ S.hashed_pwd = hashpw(value)
+ cmd_add()
+
+
+def cmd_update():
+ if not db_has(S.db, S.key):
+ die("Sorry, user `%s' doesn't exist!\n" % S.key)
+ parts = (db_get(S.db, S.key).split(":", 2) + ["", "", ""])[:3]
+ if S.hashed_pwd == ".":
+ S.hashed_pwd = parts[0]
+ if not S.groups or S.groups == ".":
+ S.groups = parts[1]
+ if not S.comment or S.comment == ".":
+ S.comment = parts[2]
+ if not S.hashed_pwd or S.hashed_pwd == "-":
+ cmd_adduser()
+ else:
+ cmd_add()
+
+
+def cmd_delete():
+ if not db_has(S.db, S.key):
+ die("Sorry, user `%s' doesn't exist!\n" % S.key)
+ db_del(S.db, S.key)
+ print("`%s' deleted" % S.key)
+
+
+def cmd_view():
+ if S.key:
+ # Like Perl's $DB{$key}, a missing key yields the empty value.
+ val = db_get(S.db, S.key) if db_has(S.db, S.key) else ""
+ print("%s:%s" % (S.key, val))
+ else:
+ for k in db_keys(S.db):
+ v = db_get(S.db, k)
+ if v:
+ print("%s:%s" % (k, v))
+
+
+def cmd_check():
+ global hash_method
+ if not db_has(S.db, S.key):
+ die("Sorry, user `%s' doesn't exist!\n" % S.key)
+ chkpass = (db_get(S.db, S.key).split(":", 2) + ["", "", ""])[0]
+ testpass = prompt_pass()
+ if chkpass[:6] == "$apr1$":
+ hash_method = "md5"
+ elif chkpass[:5] == "{SHA}":
+ hash_method = "sha1"
+ elif len(chkpass) == 13 and chkpass != testpass:
+ hash_method = "crypt"
+ else:
+ hash_method = "plain"
+ ok = hashpw(testpass, chkpass) == chkpass
+ print(hash_method + (" password ok" if ok else " password mismatch"))
+
+
+def cmd_import():
+ for line in sys.stdin:
+ line = line.rstrip("\n").rstrip("\r")
+ if not line:
+ continue
+ fields = (line.split(":", 3) + ["", "", "", ""])[:4]
+ S.key, S.hashed_pwd, S.groups, S.comment = fields
+ cmd_add()
+
+
+COMMANDS = {
+ "add": cmd_add,
+ "adduser": cmd_adduser,
+ "check": cmd_check,
+ "delete": cmd_delete,
+ "import": cmd_import,
+ "update": cmd_update,
+ "view": cmd_view,
+}
+
+
+def main():
+ global hash_method
+ argv = sys.argv[1:]
+
+ # Consume the enc flag from the front of argv, if present.
+ if argv and argv[0] == "-d":
+ argv.pop(0)
+ hash_method = "crypt"
+ elif argv and argv[0] == "-m":
+ argv.pop(0)
+ hash_method = "md5"
+ elif argv and argv[0] == "-p":
+ argv.pop(0)
+ hash_method = "plain"
+ elif argv and argv[0] == "-s":
+ argv.pop(0)
+ hash_method = "sha1"
+
+ file = argv[0] if len(argv) > 0 else None
+ command = argv[1] if len(argv) > 1 else None
+ S.key = argv[2] if len(argv) > 2 else None
+ S.hashed_pwd = argv[3] if len(argv) > 3 else None
+ S.groups = argv[4] if len(argv) > 4 else None
+ S.comment = argv[5] if len(argv) > 5 else None
+
+ if not file or not command or command not in COMMANDS:
+ usage()
+
+ # remove extension if any: .db, .db?, .pag, .dir
+ file = re.sub(r"\.(db.?|pag|dir)$", "", file)
+
+ S.is_update = command == "update"
+
+ # view/check open read-only; everything else read-write (create).
+ if command in ("view", "check"):
+ flag = "r"
+ else:
+ flag = "c"
+
+ try:
+ S.db = dbm.open(file, flag)
+ except Exception as e:
+ die("Can't open %s: %s\n" % (file, e))
+
+ try:
+ COMMANDS[command]()
+ finally:
+ S.db.close()
+
+
+if __name__ == "__main__":
+ main()
Added: httpd/httpd/trunk/support/log_server_status-ng.in
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ httpd/httpd/trunk/support/log_server_status-ng.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -0,0 +1,93 @@
+#!@pythonbin@
+#
+# 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.
+#
+#
+# log_server_status-ng -- Python port of the historical Perl log_server_status.
+#
+# Log Server Status
+# Mark J Cox, UK Web Ltd 1996, mark ukweb.com
+#
+# This script is designed to be run at a frequent interval by something
+# like cron. It connects to the server and downloads the status
+# information. It reformats the information to a single line and logs
+# it to a file. Make sure the directory $wherelog is writable by the
+# user who runs this script.
+#
+
+import re
+import socket
+import sys
+import time
+
+wherelog = "@exp_logfiledir@/" # Logs will be like "@exp_logfiledir@/19960312"
+server = "localhost" # Name of server, could be "www.foo.com"
+port = "@PORT@" # Port on server
+request = "/server-status/?auto" # Request to send
+
+ltime = time.localtime(time.time())
+
+# day = year + zero-padded month + zero-padded day
+day = "%d%02d%02d" % (ltime.tm_year, ltime.tm_mon, ltime.tm_mday)
+
+# time = zero-padded hours + minutes + seconds, concatenated
+now = "%02d%02d%02d" % (ltime.tm_hour, ltime.tm_min, ltime.tm_sec)
+
+logfile = wherelog + day
+
+try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((server, int(port)))
+except OSError as err:
+ with open(logfile, "a") as out:
+ out.write("%s:-1:-1:-1:-1:%s\n" % (now, err))
+ sys.exit("Couldn't connect to %s:%s : %s\n" % (server, port, err))
+
+# Send the request and read the response.
+sock.sendall(
+ ("GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n\r\n"
+ % (request, server)).encode("latin-1")
+)
+
+# Read the entire response, then split into lines (mirrors Perl's <$socket>).
+response = b""
+while True:
+ chunk = sock.recv(4096)
+ if not chunk:
+ break
+ response += chunk
+sock.close()
+
+# Default to empty strings: Perl's undef interpolates to "" (the fields are
+# always present on a successful server-status response; this only matters
+# if the response is malformed).
+requests = idle = number = cpu = ""
+for line in response.decode("latin-1", "replace").splitlines():
+ m = re.match(r"^BusyWorkers: (\S+)", line)
+ if m:
+ requests = m.group(1)
+ m = re.match(r"^IdleWorkers: (\S+)", line)
+ if m:
+ idle = m.group(1)
+ m = re.search(r"sses: (\S+)", line)
+ if m:
+ number = m.group(1)
+ m = re.match(r"^CPULoad: (\S+)", line)
+ if m:
+ cpu = m.group(1)
+
+with open(logfile, "a") as out:
+ out.write("%s:%s:%s:%s:%s\n" % (now, requests, idle, number, cpu))
Added: httpd/httpd/trunk/support/logresolve.py.in
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ httpd/httpd/trunk/support/logresolve.py.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -0,0 +1,136 @@
+#!@pythonbin@
+#
+# 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.
+#
+# logresolve.py -- Python port of the historical Perl logresolve.pl.
+#
+# usage: logresolve.py <infile >outfile
+#
+# input = Apache/NCSA/.. logfile with IP numbers at start of lines
+# output = same logfile with IP addresses resolved to hostnames where
+# name lookups succeeded.
+#
+# this differs from the C based 'logresolve' in that this script
+# resolves a number (CHILDREN) of addresses concurrently and sets a
+# short timeout (TIMEOUT) for each lookup in order to keep things moving
+# quickly.
+#
+# The original Perl version forked CHILDREN subprocesses and exchanged
+# IPs/hostnames over Unix sockets, with a per-lookup alarm(TIMEOUT).
+# This Python port reproduces the *semantics* without the fork/socket
+# IPC machinery: a concurrent.futures.ThreadPoolExecutor with
+# max_workers=CHILDREN performs up to CHILDREN reverse lookups in
+# parallel, and the per-lookup timeout is enforced via
+# socket.setdefaulttimeout(TIMEOUT) (socket.gethostbyaddr() honours the
+# process default socket timeout; it has no timeout argument of its own).
+#
+# Results are cached in a dict so each unique IP is resolved only once,
+# and output lines are emitted in the exact same order as the input
+# (logfiles must stay in order).
+#
+# Concurrency / timeout / order-preservation:
+# * Concurrency: ThreadPoolExecutor(max_workers=CHILDREN). Each unique
+# IP is submitted exactly once; the cache dict guards against
+# resolving the same IP twice.
+# * Timeout: socket.setdefaulttimeout(TIMEOUT) bounds each DNS lookup.
+# As a belt-and-braces measure the worker also bounds itself via
+# future.result(timeout=...) when collecting results.
+# * Order: we buffer all input lines (remembering each line's leading
+# IP), resolve the unique IPs concurrently, then walk the buffered
+# lines in their original order substituting the cached hostname.
+
+import socket
+import sys
+from concurrent.futures import ThreadPoolExecutor
+
+CHILDREN = 40
+TIMEOUT = 5
+
+
+def nslookup(ip):
+ """Reverse-resolve an IP to a hostname.
+
+ Equivalent to the Perl gethostbyaddr(gethostbyname($ip), AF_INET).
+ Returns the resolved hostname, or the original IP on any failure or
+ timeout (matching the Perl behaviour of leaving the IP as-is).
+ """
+ try:
+ hostname = socket.gethostbyaddr(ip)[0]
+ except Exception:
+ return ip
+ return hostname if hostname else ip
+
+
+def main():
+ # Bound every DNS lookup to TIMEOUT seconds. socket.gethostbyaddr()
+ # has no timeout argument, so we rely on the process-wide default
+ # socket timeout, which it honours.
+ socket.setdefaulttimeout(TIMEOUT)
+
+ # Read the whole logfile, buffering each line and the IP that starts
+ # it. Order is preserved by replaying this buffer at the end.
+ lines = [] # list of (ip, rest, had_space); rest keeps its newline
+ unique_ips = [] # unique IPs in first-seen order (for stable submit)
+ seen = set()
+
+ for line in sys.stdin:
+ # split on the FIRST space only; a line with no space is all IP.
+ parts = line.split(' ', 1)
+ if len(parts) > 1:
+ ip = parts[0]
+ rest = parts[1]
+ had_space = True
+ else:
+ # No space: the whole line is the IP. Strip the trailing
+ # newline so it resolves cleanly, but remember the line
+ # ending so we can reproduce it verbatim on output.
+ stripped = line.rstrip('\n')
+ ip = stripped
+ rest = line[len(stripped):]
+ had_space = False
+ lines.append((ip, rest, had_space))
+ if ip not in seen:
+ seen.add(ip)
+ unique_ips.append(ip)
+
+ # Resolve all unique IPs concurrently, up to CHILDREN at a time.
+ cache = {}
+ if unique_ips:
+ with ThreadPoolExecutor(max_workers=CHILDREN) as pool:
+ futures = {ip: pool.submit(nslookup, ip) for ip in unique_ips}
+ for ip, fut in futures.items():
+ try:
+ cache[ip] = fut.result(timeout=TIMEOUT + 1)
+ except Exception:
+ # On timeout/failure leave the IP unchanged.
+ cache[ip] = ip
+
+ # Emit lines in the original input order, substituting hostnames.
+ out = sys.stdout
+ for ip, rest, had_space in lines:
+ host = cache.get(ip, ip)
+ if had_space:
+ out.write("%s %s" % (host, rest))
+ else:
+ # No space in the original line: emit host then the original
+ # line ending (rest is "\n", "" with EOF no-newline, etc.).
+ out.write(host)
+ if rest:
+ out.write(rest)
+
+
+if __name__ == "__main__":
+ main()
Added: httpd/httpd/trunk/support/phf_abuse_log-ng.cgi.in
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ httpd/httpd/trunk/support/phf_abuse_log-ng.cgi.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -0,0 +1,62 @@
+#!@pythonbin@
+#
+# 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.
+#
+#
+# phf_abuse_log-ng.cgi -- Python port of the historical Perl phf_abuse_log.cgi.
+#
+# This script is used to detect people trying to abuse the security hole which
+# existed in A CGI script direstributed with Apache 1.0.3 and earlier versions.
+# You can redirect them to here using the "<Location /cgi-bin/phf*>" suggestion
+# in httpd.conf.
+#
+# The format logged to is
+# "[date] remote_addr remote_host [date] referrer user_agent".
+
+import os
+import sys
+import time
+
+LOG = "/var/log/phf_log"
+
+# Perl's ctime() produces e.g. "Wed Jun 2 14:30:00 2026\n"; time.ctime()
+# matches that format and has no trailing newline to strip.
+when = time.ctime()
+
+remote_addr = os.environ.get("REMOTE_ADDR", "")
+remote_host = os.environ.get("REMOTE_HOST", "")
+# NOTE: the Perl original had a bug here -- it referenced $ENV{$HTTP_REFERER}
+# (with a stray leading $), so it looked up the env var *named by* the value of
+# the undefined Perl variable $HTTP_REFERER, which resolved to $ENV{""} and thus
+# always logged an empty string. We do the correct thing and read HTTP_REFERER.
+http_referer = os.environ.get("HTTP_REFERER", "")
+http_user_agent = os.environ.get("HTTP_USER_AGENT", "")
+
+http_via = os.environ.get("HTTP_VIA", "")
+if http_via:
+ http_user_agent += " via " + http_via
+
+try:
+ log = open(LOG, "a")
+except OSError as e:
+ sys.exit("boo hoo, phf_log " + str(e))
+
+with log:
+ log.write("[%s] %s %s %s %s\n" % (when, remote_addr, remote_host,
+ http_referer, http_user_agent))
+
+sys.stdout.write("Content-type: text/html\r\n\r\n"
+ "<BLINK>Smile, you're on Candid Camera.</BLINK>\n")
Added: httpd/httpd/trunk/support/split-logfile-ng.in
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ httpd/httpd/trunk/support/split-logfile-ng.in Tue Jun 2 20:29:00 2026 (r1934905)
@@ -0,0 +1,92 @@
+#!@pythonbin@
+#
+# 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.
+#
+# split-logfile-ng -- Python port of the historical Perl split-logfile.
+#
+# This script will take a combined Web server access
+# log file and break its contents into separate files.
+# It assumes that the first field of each line is the
+# virtual host identity (put there by "%v"), and that
+# the logfiles should be named that+".log" in the current
+# directory.
+#
+# The combined log file is read from stdin. Records read
+# will be appended to any existing log files.
+
+import re
+import sys
+
+
+def main():
+ #
+ # Operate on raw bytes, not decoded text, to match the Perl original
+ # exactly: Perl reads STDIN as bytes, so its \s matches only ASCII
+ # whitespace and lc() lowercases only ASCII A-Z. Python's bytes
+ # regexes are inherently ASCII-only and bytes.lower() lowercases only
+ # ASCII, so reading sys.stdin.buffer reproduces Perl's behavior on
+ # non-ASCII input (Unicode whitespace / case folding would otherwise
+ # diverge).
+ #
+ log_file = {}
+
+ for log_line in sys.stdin.buffer:
+ #
+ # Get the first token from the log record; it's the
+ # identity of the virtual host to which the record
+ # applies.
+ #
+ vhost = re.split(rb"\s", log_line, maxsplit=1)[0]
+ #
+ # Normalize the virtual host name to all lowercase.
+ # If it's blank, the request was handled by the default
+ # server, so supply a default name. This shouldn't
+ # happen, but caution rocks.
+ #
+ vhost = vhost.lower() or b"access"
+ #
+ # if the vhost contains a "/" or "\", it is illegal so just use
+ # the default log to avoid any security issues due if it is interprted
+ # as a directory separator.
+ #
+ if re.search(rb"[/\\]", vhost):
+ vhost = b"access"
+ #
+ # If the log file for this virtual host isn't opened
+ # yet, do it now.
+ #
+ if vhost not in log_file:
+ try:
+ log_file[vhost] = open(vhost + b".log", "ab")
+ except IOError:
+ sys.stderr.write("Can't open %s.log\n"
+ % vhost.decode("latin-1"))
+ sys.exit(1)
+ #
+ # Strip off the first token (which may be null in the
+ # case of the default server), and write the edited
+ # record to the current log file.
+ #
+ log_line = re.sub(rb"^\S*\s+", b"", log_line)
+ log_file[vhost].write(log_line)
+
+ for fh in log_file.values():
+ fh.close()
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.