diff --git a/copyparty/broker_mp.py b/copyparty/broker_mp.py index 52c9afed..b1b2f9d9 100644 --- a/copyparty/broker_mp.py +++ b/copyparty/broker_mp.py @@ -157,6 +157,10 @@ class BrokerMp(object): elif dest == "cb_httpsrv_up": self.hub.cb_httpsrv_up() + elif dest == "httpsrv.set_bad_ver": + for p in self.procs: + p.q_pend.put((0, dest, list(args))) + else: raise Exception("what is " + str(dest)) diff --git a/copyparty/broker_thr.py b/copyparty/broker_thr.py index 43bea239..b433a300 100644 --- a/copyparty/broker_thr.py +++ b/copyparty/broker_thr.py @@ -61,6 +61,10 @@ class BrokerThr(BrokerCli): self.httpsrv.set_netdevs(args[0]) return + if dest == "httpsrv.set_bad_ver": + self.httpsrv.set_bad_ver(args[0]) + return + # new ipc invoking managed service in hub obj = self.hub for node in dest.split("."): diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 472c270c..0be15e56 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -155,7 +155,8 @@ ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n" BADXFP = ', or change the copyparty global-option "xf-proto" to another header-name to read this value from. Alternatively, if your reverseproxy is not able to provide a header similar to "X-Forwarded-Proto", then you must tell copyparty which protocol to assume; either "--xf-proto-fb=http" or "--xf-proto-fb=https"' -BADXFFB = "NOTE: serverlog has a message regarding your reverse-proxy config" +BADXFFB = "
NOTE: serverlog has a message regarding your reverse-proxy config
" +BADVER = "
The version of copyparty currently active has a known vulnerability (more info) that has been fixed; please update to the latest version. This message is only visible to users with admin (a/A) permissions.
" H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" @@ -5624,7 +5625,8 @@ class HttpCli(object): no304=self.no304(), k304vis=self.args.k304 > 0, no304vis=self.args.no304 > 0, - msg=BADXFFB if hasattr(self, "bad_xff") else "", + msg=(BADXFFB if not hasattr(self, "bad_xff") else "") + + (BADVER if self.conn.hsrv.bad_ver and self.can_admin else ""), ver=S_VERSION if show_ver else "", chpw=self.args.chpw and self.uname != "*", ahttps="" if self.is_https else "https://" + self.host + self.req, diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index ea89a0aa..728a3743 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -119,6 +119,7 @@ class HttpSrv(object): socket.setdefaulttimeout(120) self.t0 = time.time() + self.bad_ver = False nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else "" self.magician = Magician() self.nm = NetMap([], []) @@ -232,6 +233,9 @@ class HttpSrv(object): self.th_cfg: dict[str, set[str]] = {} Daemon(self.post_init, "hsrv-init2") + def set_bad_ver(self, val: bool) -> None: + self.bad_ver = val + def post_init(self) -> None: try: x = self.broker.ask("thumbsrv.getcfg") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 929392f7..f17c502d 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals import argparse import atexit import errno +import json import logging import os import re @@ -27,6 +28,7 @@ if True: # pylint: disable=using-constant-test from typing import Any, Optional, Union from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode +from .__version__ import S_VERSION from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who from .bos import bos from .cert import ensure_cert @@ -75,6 +77,7 @@ from .util import ( mp, odfusion, pybin, + read_utf8, start_log_thrs, start_stackmon, termsize, @@ -95,11 +98,19 @@ if TYPE_CHECKING: if PY2: range = xrange # type: ignore +if PY2: + from urllib2 import Request, urlopen +else: + from urllib.request import Request, urlopen + VER_IDP_DB = 1 VER_SESSION_DB = 1 VER_SHARES_DB = 2 +VULN_CHECK_URL = "https://api.github.com/repos/9001/copyparty/security-advisories" +VULN_CHECK_INTERVAL = 86400 + class SvcHub(object): """ @@ -1382,6 +1393,7 @@ class SvcHub(object): Daemon(self.tcpsrv.netmon, "netmon") Daemon(self.thr_httpsrv_up, "sig-hsrv-up2") + Daemon(self.check_ver, "ver-chk") sigs = [signal.SIGINT, signal.SIGTERM] if not ANYWIN: @@ -1778,3 +1790,61 @@ class SvcHub(object): zb = gzip.compress(zb) zs = ub64enc(zb).decode("ascii") self.log("stacks", zs) + + def parse_version(self, ver: str) -> tuple: + match = re.search(r'[\d.]+', ver) + if not match: + return () + clean = match.group(0).strip('.') + return tuple(int(x) for x in clean.split(".")) + + def get_vuln_cache_path(self) -> str: + return os.path.join(self.E.cfg, "vuln_advisory.json") + + def check_ver(self) -> None: + ver_cpp = self.parse_version(S_VERSION) + + while not self.stopping: + fpath = self.get_vuln_cache_path() + data = None + + try: + mtime = os.path.getmtime(fpath) + if time.time() - mtime < VULN_CHECK_INTERVAL: + data = read_utf8(None, fpath, True) + except Exception as e: + self.log("ver-chk", "no vulnerability advisory cache found; {}".format(e)) + + if not data: + try: + req = Request(VULN_CHECK_URL) + with urlopen(req, timeout=30) as f: + data = f.read().decode("utf-8") + + with open(fpath, "wb") as f: + f.write(data.encode("utf-8")) + + except Exception as e: + self.log("ver-chk", "failed to fetch vulnerability advisory; {}".format(e)) + + if data: + try: + advisories = json.loads(data) + + fixes = ( + self.parse_version(vuln.get("patched_versions")) + for adv in advisories + for vuln in adv.get("vulnerabilities", []) + if vuln.get("patched_versions") + ) + newest_fix = max(fixes, default=None) + + if newest_fix and ver_cpp < newest_fix: + self.broker.say("httpsrv.set_bad_ver", True) + except Exception as e: + self.log("ver-chk", "failed to process vulnerability advisory; {}".format(e)) + + for _ in range(VULN_CHECK_INTERVAL): + if self.stopping: + return + time.sleep(1) diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css index 832ef566..5ddcd18f 100644 --- a/copyparty/web/splash.css +++ b/copyparty/web/splash.css @@ -257,3 +257,19 @@ html.bz { html.bz .vols img { filter: sepia(0.8) hue-rotate(180deg); } +.box-warning { + background: #804; + border-bottom: 1px solid #c28; + padding: .75em; + text-align: center; + border-radius: .2em; + margin-top: 0em; +} +.box-warning + .box-warning { + margin-top: .5em; +} +.unbox { + border: unset !important; + padding: unset !important; + margin: unset !important; +} \ No newline at end of file