diff --git a/README.md b/README.md index b2899f80..c8f01048 100644 --- a/README.md +++ b/README.md @@ -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_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_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) | diff --git a/contrib/podman-systemd/copyparty.conf b/contrib/podman-systemd/copyparty.conf index aa3d4b2e..001835c6 100644 --- a/contrib/podman-systemd/copyparty.conf +++ b/contrib/podman-systemd/copyparty.conf @@ -8,7 +8,7 @@ # and copyparty replaces %Y-%m%d with Year-MonthDay, so the # full path will be something like /var/log/copyparty/2023-1130.txt # (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) # i: 127.0.0.1 # only allow connections from localhost (reverse-proxies) diff --git a/contrib/systemd/copyparty.example.conf b/contrib/systemd/copyparty.example.conf index de220e9f..b85ec770 100644 --- a/contrib/systemd/copyparty.example.conf +++ b/contrib/systemd/copyparty.example.conf @@ -16,7 +16,7 @@ # and copyparty replaces %Y-%m%d with Year-MonthDay, so the # full path will be something like /var/log/copyparty/2023-1130.txt # (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 # periodically check if your copyparty version has a known security vulnerability, diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 030adfeb..5c85a5b2 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -65,6 +65,10 @@ from .util import ( b64enc, ctypes, dedent, + expand_osenv_c, + expand_osenv_cs, + expand_osenv_noop, + expand_osenv_s, has_resource, load_resource, min_ex, @@ -427,9 +431,22 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None: 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]: 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, "") ret: list[str] = [] @@ -453,10 +470,12 @@ def args_from_cfg(cfg_path: str) -> list[str]: else: ret.append(prefix + k + "=" + v) - return ret + return expand_cvars(ret) def expand_cfg(argv) -> list[str]: + argv = expand_cvars(argv) + if CFG_DEF: supp = args_from_cfg(CFG_DEF[0]) 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-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("--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("--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)") @@ -2179,6 +2199,15 @@ def main(argv: Optional[list[str]] = None) -> None: 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: os.chdir(al.chdir) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 40d5944e..7dde17f4 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -56,7 +56,7 @@ if HAVE_SQLITE3: if True: # pylint: disable=using-constant-test 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 @@ -541,13 +541,11 @@ class VFS(object): hist = flags.get("hist") if hist and hist != "-": - zs = "{}/{}".format(hist.rstrip("/"), name) - flags["hist"] = os.path.expandvars(os.path.expanduser(zs)) + flags["hist"] = "%s/%s" % (hist.rstrip("/"), name) dbp = flags.get("dbpath") if dbp and dbp != "-": - zs = "{}/{}".format(dbp.rstrip("/"), name) - flags["dbpath"] = os.path.expandvars(os.path.expanduser(zs)) + flags["dbpath"] = "%s/%s" % (dbp.rstrip("/"), name) return flags @@ -1279,7 +1277,7 @@ class AuthSrv(object): daxs: dict[str, AXS], mflags: dict[str, dict[str, Any]], ) -> tuple[str, str]: - src = os.path.expandvars(os.path.expanduser(src)) + src = os.path.expanduser(self.args.shenvexp(src)) src = absreal(src) dst = dst.strip("/") @@ -1372,7 +1370,7 @@ class AuthSrv(object): ) -> None: 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: lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)] self.log("expanded config file (unprocessed):\n" + "\n".join(lns)) @@ -2174,7 +2172,7 @@ class AuthSrv(object): if vflag == "-": pass 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 elif self.args.hist: for nch in range(len(hid)): @@ -2209,7 +2207,7 @@ class AuthSrv(object): if vflag == "-": pass 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 elif self.args.dbpath: for nch in range(len(hid)): @@ -3964,10 +3962,14 @@ def split_cfg_ln(ln: str) -> dict[str, Any]: 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: """expand all % file includes""" - fp = absreal(fp) + fp = absreal(os.path.expanduser(shenvexp(fp))) if len(ipath.split(" -> ")) > 64: raise Exception("hit max depth of 64 includes") @@ -3998,7 +4000,7 @@ def expand_config_file( if fp2 in ipath: continue - expand_config_file(log, ret, fp2, ipath) + expand_config_file(log, shenvexp, ret, fp2, ipath) return @@ -4023,7 +4025,7 @@ def expand_config_file( fp2 = ln[1:].strip() fp2 = os.path.join(os.path.dirname(fp), fp2) 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)): ret[n] = pad + ret[n] continue diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 38caeb21..65dcd9b8 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -17,6 +17,7 @@ from .util import ( FFMPEG_URL, REKOBO_LKEY, VF_CAREFUL, + expand_osenv_c, fsenc, gzip, min_ex, @@ -86,7 +87,7 @@ class MParser(object): while True: try: - bp = os.path.expanduser(args) + bp = os.path.expanduser(expand_osenv_c(args)) if WINDOWS: bp = uncyg(bp) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index d5a59722..5f7441c3 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -1133,17 +1133,23 @@ class SvcHub(object): al.th_coversd_set = set(al.th_coversd) for k in "c".split(" "): + if self.args.env_expand in (0, 2): + break + vl = getattr(al, k) if not vl: 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) for k in "lo hist dbpath ssl_log".split(" "): + if self.args.env_expand in (0, 2): + break + vs = getattr(al, k) if vs: - vs = os.path.expandvars(os.path.expanduser(vs)) + vs = os.path.expanduser(self.args.shenvexp(vs)) setattr(al, k, vs) for k in "idp_adm stats_u".split(" "): diff --git a/copyparty/util.py b/copyparty/util.py index b0004e5a..c8a8dfc1 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -1562,6 +1562,43 @@ def dedent(txt: str) -> str: 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: tid = threading.current_thread().ident 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[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 @@ -4190,7 +4227,7 @@ def loadpy(ap: str, hot: bool) -> Any: depending on what other inconveniently named files happen 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)) mname = mfile.rsplit(".", 1)[0] sys.path.insert(0, mdir) diff --git a/tests/util.py b/tests/util.py index dd10201d..6cdb7ed5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -38,7 +38,7 @@ from copyparty.broker_thr import BrokerThr from copyparty.ico import Ico from copyparty.u2idx import U2idx 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) @@ -195,6 +195,7 @@ class Cfg(Namespace): du_who="all", dk_salt="b" * 16, fk_salt="a" * 16, + env_expand=2, fsnt="lin", grp_all="acct", idp_gsep=re.compile("[|:;+,]"), @@ -217,6 +218,7 @@ class Cfg(Namespace): rw_edit="md", s_rd_sz=256 * 1024, s_wr_sz=256 * 1024, + shenvexp=expand_osenv_c, shr_who="auth", sort="href", srch_hits=99999,