use ${ENV} syntax for env-vars;

only expand environment-variables of the form ${ENV}

by default, crash on startup if the old $ENV syntax is found,
explaning that the old syntax can be enabled with an option
This commit is contained in:
ed 2026-04-20 23:43:00 +02:00
parent b31f29024a
commit cbd82b654a
9 changed files with 101 additions and 23 deletions

View file

@ -2542,6 +2542,7 @@ buggy feature? rip it out by setting any of the following environment variables
| -------------------- | ------------ | | -------------------- | ------------ |
| `PRTY_NO_CTYPES` | do not use features from external libraries such as kernel32 | | `PRTY_NO_CTYPES` | do not use features from external libraries such as kernel32 |
| `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access | | `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access |
| `PRTY_NO_ENVEXPAND` | do not expand environment-variables in configs and args |
| `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes | | `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` | | `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) | | `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |

View file

@ -8,7 +8,7 @@
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the # and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt # full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end) # (note: enable compression by adding .xz at the end)
# q, lo: $LOGS_DIRECTORY/%Y-%m%d.log # q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE) # p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies) # i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)

View file

@ -16,7 +16,7 @@
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the # and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt # full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end) # (note: enable compression by adding .xz at the end)
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log
# enable version-checker by uncommenting one of the 'vc-url' lines below; this will # enable version-checker by uncommenting one of the 'vc-url' lines below; this will
# periodically check if your copyparty version has a known security vulnerability, # periodically check if your copyparty version has a known security vulnerability,

View file

@ -65,6 +65,10 @@ from .util import (
b64enc, b64enc,
ctypes, ctypes,
dedent, dedent,
expand_osenv_c,
expand_osenv_cs,
expand_osenv_noop,
expand_osenv_s,
has_resource, has_resource,
load_resource, load_resource,
min_ex, min_ex,
@ -427,9 +431,22 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None:
sys.exit(0) sys.exit(0)
def expand_cvars(argv) -> list[str]:
n = 0
for v in argv:
if "=" in v:
a, b = v.split("=", 1)
v = "%s=%s" % (a, os.path.expanduser(expand_osenv_c(b)))
else:
v = os.path.expanduser(expand_osenv_c(v))
argv[n] = v
n += 1
return argv
def args_from_cfg(cfg_path: str) -> list[str]: def args_from_cfg(cfg_path: str) -> list[str]:
lines: list[str] = [] lines: list[str] = []
expand_config_file(None, lines, cfg_path, "") expand_config_file(None, expand_osenv_c, lines, cfg_path, "")
lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "") lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "")
ret: list[str] = [] ret: list[str] = []
@ -453,10 +470,12 @@ def args_from_cfg(cfg_path: str) -> list[str]:
else: else:
ret.append(prefix + k + "=" + v) ret.append(prefix + k + "=" + v)
return ret return expand_cvars(ret)
def expand_cfg(argv) -> list[str]: def expand_cfg(argv) -> list[str]:
argv = expand_cvars(argv)
if CFG_DEF: if CFG_DEF:
supp = args_from_cfg(CFG_DEF[0]) supp = args_from_cfg(CFG_DEF[0])
argv = argv[:1] + supp + argv[1:] argv = argv[:1] + supp + argv[1:]
@ -1197,6 +1216,7 @@ def add_general(ap, nc, srvname):
ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)") ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)")
ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS) ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS)
ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]") ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]")
ap2.add_argument("--env-expand", metavar="N", type=int, default=-1, help="syntax to expect for environment-variables to expand in config-files; [\033[32m0\033[0m]=disable, [\033[32m1\033[0m]=$VAR (old syntax (scary)), [\033[32m2\033[0m]=${VAR} (new syntax (recommended))")
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
@ -2179,6 +2199,15 @@ def main(argv: Optional[list[str]] = None) -> None:
quotecheck(al) quotecheck(al)
if al.env_expand == 2:
al.shenvexp = expand_osenv_c
elif al.env_expand == 1:
al.shenvexp = expand_osenv_s
elif al.env_expand == 0:
al.shenvexp = expand_osenv_noop
else:
al.shenvexp = expand_osenv_cs
if al.chdir: if al.chdir:
os.chdir(al.chdir) os.chdir(al.chdir)

View file

@ -56,7 +56,7 @@ if HAVE_SQLITE3:
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from collections.abc import Iterable from collections.abc import Iterable
from typing import Any, Generator, Optional, Sequence, Union from typing import Any, Callable, Generator, Optional, Sequence, Union
from .util import NamedLogger, RootLogger from .util import NamedLogger, RootLogger
@ -541,13 +541,11 @@ class VFS(object):
hist = flags.get("hist") hist = flags.get("hist")
if hist and hist != "-": if hist and hist != "-":
zs = "{}/{}".format(hist.rstrip("/"), name) flags["hist"] = "%s/%s" % (hist.rstrip("/"), name)
flags["hist"] = os.path.expandvars(os.path.expanduser(zs))
dbp = flags.get("dbpath") dbp = flags.get("dbpath")
if dbp and dbp != "-": if dbp and dbp != "-":
zs = "{}/{}".format(dbp.rstrip("/"), name) flags["dbpath"] = "%s/%s" % (dbp.rstrip("/"), name)
flags["dbpath"] = os.path.expandvars(os.path.expanduser(zs))
return flags return flags
@ -1279,7 +1277,7 @@ class AuthSrv(object):
daxs: dict[str, AXS], daxs: dict[str, AXS],
mflags: dict[str, dict[str, Any]], mflags: dict[str, dict[str, Any]],
) -> tuple[str, str]: ) -> tuple[str, str]:
src = os.path.expandvars(os.path.expanduser(src)) src = os.path.expanduser(self.args.shenvexp(src))
src = absreal(src) src = absreal(src)
dst = dst.strip("/") dst = dst.strip("/")
@ -1372,7 +1370,7 @@ class AuthSrv(object):
) -> None: ) -> None:
self.line_ctr = 0 self.line_ctr = 0
expand_config_file(self.log, cfg_lines, fp, "") expand_config_file(self.log, self.args.shenvexp, cfg_lines, fp, "")
if self.args.vc: if self.args.vc:
lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)] lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)]
self.log("expanded config file (unprocessed):\n" + "\n".join(lns)) self.log("expanded config file (unprocessed):\n" + "\n".join(lns))
@ -2174,7 +2172,7 @@ class AuthSrv(object):
if vflag == "-": if vflag == "-":
pass pass
elif vflag: elif vflag:
vflag = os.path.expandvars(os.path.expanduser(vflag)) vflag = os.path.expanduser(self.args.shenvexp(vflag))
vol.histpath = vol.dbpath = uncyg(vflag) if WINDOWS else vflag vol.histpath = vol.dbpath = uncyg(vflag) if WINDOWS else vflag
elif self.args.hist: elif self.args.hist:
for nch in range(len(hid)): for nch in range(len(hid)):
@ -2209,7 +2207,7 @@ class AuthSrv(object):
if vflag == "-": if vflag == "-":
pass pass
elif vflag: elif vflag:
vflag = os.path.expandvars(os.path.expanduser(vflag)) vflag = os.path.expanduser(self.args.shenvexp(vflag))
vol.dbpath = uncyg(vflag) if WINDOWS else vflag vol.dbpath = uncyg(vflag) if WINDOWS else vflag
elif self.args.dbpath: elif self.args.dbpath:
for nch in range(len(hid)): for nch in range(len(hid)):
@ -3964,10 +3962,14 @@ def split_cfg_ln(ln: str) -> dict[str, Any]:
def expand_config_file( def expand_config_file(
log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str log: Optional["NamedLogger"],
shenvexp: "Callable[[str], str]",
ret: list[str],
fp: str,
ipath: str,
) -> None: ) -> None:
"""expand all % file includes""" """expand all % file includes"""
fp = absreal(fp) fp = absreal(os.path.expanduser(shenvexp(fp)))
if len(ipath.split(" -> ")) > 64: if len(ipath.split(" -> ")) > 64:
raise Exception("hit max depth of 64 includes") raise Exception("hit max depth of 64 includes")
@ -3998,7 +4000,7 @@ def expand_config_file(
if fp2 in ipath: if fp2 in ipath:
continue continue
expand_config_file(log, ret, fp2, ipath) expand_config_file(log, shenvexp, ret, fp2, ipath)
return return
@ -4023,7 +4025,7 @@ def expand_config_file(
fp2 = ln[1:].strip() fp2 = ln[1:].strip()
fp2 = os.path.join(os.path.dirname(fp), fp2) fp2 = os.path.join(os.path.dirname(fp), fp2)
ofs = len(ret) ofs = len(ret)
expand_config_file(log, ret, fp2, ipath) expand_config_file(log, shenvexp, ret, fp2, ipath)
for n in range(ofs, len(ret)): for n in range(ofs, len(ret)):
ret[n] = pad + ret[n] ret[n] = pad + ret[n]
continue continue

View file

@ -17,6 +17,7 @@ from .util import (
FFMPEG_URL, FFMPEG_URL,
REKOBO_LKEY, REKOBO_LKEY,
VF_CAREFUL, VF_CAREFUL,
expand_osenv_c,
fsenc, fsenc,
gzip, gzip,
min_ex, min_ex,
@ -86,7 +87,7 @@ class MParser(object):
while True: while True:
try: try:
bp = os.path.expanduser(args) bp = os.path.expanduser(expand_osenv_c(args))
if WINDOWS: if WINDOWS:
bp = uncyg(bp) bp = uncyg(bp)

View file

@ -1133,17 +1133,23 @@ class SvcHub(object):
al.th_coversd_set = set(al.th_coversd) al.th_coversd_set = set(al.th_coversd)
for k in "c".split(" "): for k in "c".split(" "):
if self.args.env_expand in (0, 2):
break
vl = getattr(al, k) vl = getattr(al, k)
if not vl: if not vl:
continue continue
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl] vl = [os.path.expanduser(self.args.shenvexp(x)) for x in vl]
setattr(al, k, vl) setattr(al, k, vl)
for k in "lo hist dbpath ssl_log".split(" "): for k in "lo hist dbpath ssl_log".split(" "):
if self.args.env_expand in (0, 2):
break
vs = getattr(al, k) vs = getattr(al, k)
if vs: if vs:
vs = os.path.expandvars(os.path.expanduser(vs)) vs = os.path.expanduser(self.args.shenvexp(vs))
setattr(al, k, vs) setattr(al, k, vs)
for k in "idp_adm stats_u".split(" "): for k in "idp_adm stats_u".split(" "):

View file

@ -1562,6 +1562,43 @@ def dedent(txt: str) -> str:
return "\n".join([ln[pad:] for ln in lns]) return "\n".join([ln[pad:] for ln in lns])
def expand_osenv_noop(txt) -> str:
return txt
def _expand_osenv_c(txt) -> str:
if "${" not in txt:
return txt
zsl = txt.split("${")
ret = zsl[0]
for v in zsl[1:]:
if "}" not in v:
raise Exception("missing '}' after %r in config-value %r" % (v, txt))
a, b = v.split("}", 1)
try:
ret += os.environ[a] + b
except:
raise Exception("env-var %r not defined; config-value %r" % (a, txt))
return ret
if os.environ.get("PRTY_NO_ENVEXPAND"):
expand_osenv_c = expand_osenv_noop
expand_osenv_s = expand_osenv_noop
else:
expand_osenv_c = _expand_osenv_c
expand_osenv_s = os.path.expandvars
def expand_osenv_cs(txt) -> str:
a = expand_osenv_c(txt)
b = expand_osenv_s(txt)
if a == b:
return a
t = "config-value %r is using the old syntax for environment-variables; choose one of the following options:\noption 1: update the config-value to the new syntax, ${VAR} instead of $VAR or %%VAR%%\noption 2: tell copyparty to allow the old syntax with global-option --env-expand 1 (risky)\noption 3: tell copyparty to only use the new syntax (and not expand this variable) with global-option --env-expand 2\noption 4: disable all environment-variable expansions with PRTY_NO_ENVEXPAND=1 or global-option --env-expand 0"
raise Exception(t % (txt,))
def rice_tid() -> str: def rice_tid() -> str:
tid = threading.current_thread().ident tid = threading.current_thread().ident
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:]) c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])
@ -3847,7 +3884,7 @@ def _parsehook(
argv = cmd.split(",") if "," in cmd else [cmd] argv = cmd.split(",") if "," in cmd else [cmd]
argv[0] = os.path.expandvars(os.path.expanduser(argv[0])) argv[0] = os.path.expanduser(expand_osenv_c(argv[0]))
return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv
@ -4190,7 +4227,7 @@ def loadpy(ap: str, hot: bool) -> Any:
depending on what other inconveniently named files happen depending on what other inconveniently named files happen
to be in the same folder to be in the same folder
""" """
ap = os.path.expandvars(os.path.expanduser(ap)) ap = os.path.expanduser(expand_osenv_c(ap))
mdir, mfile = os.path.split(absreal(ap)) mdir, mfile = os.path.split(absreal(ap))
mname = mfile.rsplit(".", 1)[0] mname = mfile.rsplit(".", 1)[0]
sys.path.insert(0, mdir) sys.path.insert(0, mdir)

View file

@ -38,7 +38,7 @@ from copyparty.broker_thr import BrokerThr
from copyparty.ico import Ico from copyparty.ico import Ico
from copyparty.u2idx import U2idx from copyparty.u2idx import U2idx
from copyparty.up2k import Up2k from copyparty.up2k import Up2k
from copyparty.util import FHC, CachedDict, Garda, Unrecv from copyparty.util import FHC, CachedDict, Garda, Unrecv, expand_osenv_c
init_E(E) init_E(E)
@ -195,6 +195,7 @@ class Cfg(Namespace):
du_who="all", du_who="all",
dk_salt="b" * 16, dk_salt="b" * 16,
fk_salt="a" * 16, fk_salt="a" * 16,
env_expand=2,
fsnt="lin", fsnt="lin",
grp_all="acct", grp_all="acct",
idp_gsep=re.compile("[|:;+,]"), idp_gsep=re.compile("[|:;+,]"),
@ -217,6 +218,7 @@ class Cfg(Namespace):
rw_edit="md", rw_edit="md",
s_rd_sz=256 * 1024, s_rd_sz=256 * 1024,
s_wr_sz=256 * 1024, s_wr_sz=256 * 1024,
shenvexp=expand_osenv_c,
shr_who="auth", shr_who="auth",
sort="href", sort="href",
srch_hits=99999, srch_hits=99999,