enforce single-instance for session/shares db
use file-locking to detect and prevent misconfigurations which could lead to subtle unexpected behavior
This commit is contained in:
		
							parent
							
								
									8e0364efad
								
							
						
					
					
						commit
						acfaacbd46
					
				| @ -2135,7 +2135,9 @@ buggy feature? rip it out  by setting any of the following environment variables | |||||||
| 
 | 
 | ||||||
| | env-var              | what it does | | | env-var              | what it does | | ||||||
| | -------------------- | ------------ | | | -------------------- | ------------ | | ||||||
|  | | `PRTY_NO_DB_LOCK`    | do not lock session/shares-databases for exclusive access | | ||||||
| | `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_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) | | ||||||
| | `PRTY_NO_LZMA`       | disable streaming xz compression of incoming uploads | | | `PRTY_NO_LZMA`       | disable streaming xz compression of incoming uploads | | ||||||
| | `PRTY_NO_MP`         | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) | | | `PRTY_NO_MP`         | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) | | ||||||
| @ -2676,7 +2678,6 @@ set any of the following environment variables to disable its associated optiona | |||||||
| | `PRTY_NO_CFSSL`      | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) | | | `PRTY_NO_CFSSL`      | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) | | ||||||
| | `PRTY_NO_FFMPEG`     | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips | | | `PRTY_NO_FFMPEG`     | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips | | ||||||
| | `PRTY_NO_FFPROBE`    | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen | | | `PRTY_NO_FFPROBE`    | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen | | ||||||
| | `PRTY_NO_IMPRESO`    | do not try to load js/css files using `importlib.resources` | |  | ||||||
| | `PRTY_NO_MUTAGEN`    | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe | | | `PRTY_NO_MUTAGEN`    | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe | | ||||||
| | `PRTY_NO_PIL`        | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg | | | `PRTY_NO_PIL`        | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg | | ||||||
| | `PRTY_NO_PILF`       | disable Pillow `ImageFont` text rendering, used for folder thumbnails | | | `PRTY_NO_PILF`       | disable Pillow `ImageFont` text rendering, used for folder thumbnails | | ||||||
|  | |||||||
| @ -64,6 +64,7 @@ from .util import ( | |||||||
|     expat_ver, |     expat_ver, | ||||||
|     gzip, |     gzip, | ||||||
|     load_ipu, |     load_ipu, | ||||||
|  |     lock_file, | ||||||
|     min_ex, |     min_ex, | ||||||
|     mp, |     mp, | ||||||
|     odfusion, |     odfusion, | ||||||
| @ -419,6 +420,7 @@ class SvcHub(object): | |||||||
|         # the sessions-db is whatever, if something looks broken then just nuke it |         # the sessions-db is whatever, if something looks broken then just nuke it | ||||||
| 
 | 
 | ||||||
|         db_path = self.args.ses_db |         db_path = self.args.ses_db | ||||||
|  |         db_lock = db_path + ".lock" | ||||||
|         try: |         try: | ||||||
|             create = not os.path.getsize(db_path) |             create = not os.path.getsize(db_path) | ||||||
|         except: |         except: | ||||||
| @ -444,7 +446,11 @@ class SvcHub(object): | |||||||
|                         raise |                         raise | ||||||
|                     sver = 1 |                     sver = 1 | ||||||
|                     self._create_session_db(cur) |                     self._create_session_db(cur) | ||||||
|                 self._verify_session_db(cur, sver) |                 err = self._verify_session_db(cur, sver, db_path) | ||||||
|  |                 if err: | ||||||
|  |                     tries = 99 | ||||||
|  |                     self.args.no_ses = True | ||||||
|  |                     self.log("root", err, 3) | ||||||
|                 break |                 break | ||||||
| 
 | 
 | ||||||
|             except Exception as ex: |             except Exception as ex: | ||||||
| @ -460,6 +466,10 @@ class SvcHub(object): | |||||||
|                     db.close()  # type: ignore |                     db.close()  # type: ignore | ||||||
|                 except: |                 except: | ||||||
|                     pass |                     pass | ||||||
|  |                 try: | ||||||
|  |                     os.unlink(db_lock) | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|                 os.unlink(db_path) |                 os.unlink(db_path) | ||||||
| 
 | 
 | ||||||
|     def _create_session_db(self, cur: "sqlite3.Cursor") -> None: |     def _create_session_db(self, cur: "sqlite3.Cursor") -> None: | ||||||
| @ -476,7 +486,7 @@ class SvcHub(object): | |||||||
|             cur.execute(cmd) |             cur.execute(cmd) | ||||||
|         self.log("root", "created new sessions-db") |         self.log("root", "created new sessions-db") | ||||||
| 
 | 
 | ||||||
|     def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int) -> None: |     def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int, db_path: str) -> str: | ||||||
|         # ensure writable (maybe owned by other user) |         # ensure writable (maybe owned by other user) | ||||||
|         db = cur.connection |         db = cur.connection | ||||||
| 
 | 
 | ||||||
| @ -488,6 +498,10 @@ class SvcHub(object): | |||||||
|         except: |         except: | ||||||
|             owner = 0 |             owner = 0 | ||||||
| 
 | 
 | ||||||
|  |         if not lock_file(db_path + ".lock"): | ||||||
|  |             t = "the sessions-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --ses-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now disable sessions and instead use plaintext passwords in cookies." | ||||||
|  |             return t % (db_path, owner) | ||||||
|  | 
 | ||||||
|         vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000))) |         vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000))) | ||||||
|         if owner: |         if owner: | ||||||
|             # wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720 |             # wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720 | ||||||
| @ -505,6 +519,7 @@ class SvcHub(object): | |||||||
|         db.commit() |         db.commit() | ||||||
|         cur.close() |         cur.close() | ||||||
|         db.close() |         db.close() | ||||||
|  |         return "" | ||||||
| 
 | 
 | ||||||
|     def setup_share_db(self) -> None: |     def setup_share_db(self) -> None: | ||||||
|         al = self.args |         al = self.args | ||||||
| @ -513,7 +528,7 @@ class SvcHub(object): | |||||||
|             al.shr = "" |             al.shr = "" | ||||||
|             return |             return | ||||||
| 
 | 
 | ||||||
|         import sqlite3 |         assert sqlite3  # type: ignore  # !rm | ||||||
| 
 | 
 | ||||||
|         al.shr = al.shr.strip("/") |         al.shr = al.shr.strip("/") | ||||||
|         if "/" in al.shr or not al.shr: |         if "/" in al.shr or not al.shr: | ||||||
| @ -528,6 +543,7 @@ class SvcHub(object): | |||||||
|         # the shares-db is important, so panic if something is wrong |         # the shares-db is important, so panic if something is wrong | ||||||
| 
 | 
 | ||||||
|         db_path = self.args.shr_db |         db_path = self.args.shr_db | ||||||
|  |         db_lock = db_path + ".lock" | ||||||
|         try: |         try: | ||||||
|             create = not os.path.getsize(db_path) |             create = not os.path.getsize(db_path) | ||||||
|         except: |         except: | ||||||
| @ -560,6 +576,12 @@ class SvcHub(object): | |||||||
|         except: |         except: | ||||||
|             owner = 0 |             owner = 0 | ||||||
| 
 | 
 | ||||||
|  |         if not lock_file(db_lock): | ||||||
|  |             t = "the shares-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --shr-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now panic." | ||||||
|  |             t = t % (db_path, owner) | ||||||
|  |             self.log("root", t, 1) | ||||||
|  |             raise Exception(t) | ||||||
|  | 
 | ||||||
|         sch1 = [ |         sch1 = [ | ||||||
|             r"create table kv (k text, v int)", |             r"create table kv (k text, v int)", | ||||||
|             r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)", |             r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)", | ||||||
|  | |||||||
| @ -114,8 +114,14 @@ IP6ALL = "0:0:0:0:0:0:0:0" | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| try: | try: | ||||||
|     import ctypes |  | ||||||
|     import fcntl |     import fcntl | ||||||
|  | 
 | ||||||
|  |     HAVE_FCNTL = True | ||||||
|  | except: | ||||||
|  |     HAVE_FCNTL = False | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     import ctypes | ||||||
|     import termios |     import termios | ||||||
| except: | except: | ||||||
|     pass |     pass | ||||||
| @ -3940,6 +3946,73 @@ def hidedir(dp) -> None: | |||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | _flocks = {} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _lock_file_noop(ap: str) -> bool: | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _lock_file_ioctl(ap: str) -> bool: | ||||||
|  |     assert fcntl  # type: ignore  # !rm | ||||||
|  |     try: | ||||||
|  |         fd = _flocks.pop(ap) | ||||||
|  |         os.close(fd) | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438) | ||||||
|  |     # NOTE: the fcntl.lockf identifier is (pid,node); | ||||||
|  |     #  the lock will be dropped if os.close(os.open(ap)) | ||||||
|  |     #  is performed anywhere else in this thread | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) | ||||||
|  |         _flocks[ap] = fd | ||||||
|  |         return True | ||||||
|  |     except Exception as ex: | ||||||
|  |         eno = getattr(ex, "errno", -1) | ||||||
|  |         try: | ||||||
|  |             os.close(fd) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |         if eno in (errno.EAGAIN, errno.EACCES): | ||||||
|  |             return False | ||||||
|  |         print("WARNING: unexpected errno %d from fcntl.lockf; %r" % (eno, ex)) | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _lock_file_windows(ap: str) -> bool: | ||||||
|  |     try: | ||||||
|  |         import msvcrt | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             fd = _flocks.pop(ap) | ||||||
|  |             os.close(fd) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438) | ||||||
|  |         msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) | ||||||
|  |         return True | ||||||
|  |     except Exception as ex: | ||||||
|  |         eno = getattr(ex, "errno", -1) | ||||||
|  |         if eno == errno.EACCES: | ||||||
|  |             return False | ||||||
|  |         print("WARNING: unexpected errno %d from msvcrt.locking; %r" % (eno, ex)) | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if os.environ.get("PRTY_NO_DB_LOCK"): | ||||||
|  |     lock_file = _lock_file_noop | ||||||
|  | elif ANYWIN: | ||||||
|  |     lock_file = _lock_file_windows | ||||||
|  | elif HAVE_FCNTL: | ||||||
|  |     lock_file = _lock_file_ioctl | ||||||
|  | else: | ||||||
|  |     lock_file = _lock_file_noop | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| try: | try: | ||||||
|     if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"): |     if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"): | ||||||
|         # py3.8 doesn't have .files |         # py3.8 doesn't have .files | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 ed
						ed