Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"""Fixed Linux editor protocol. Invoked in memory; never installed on the server.3
All path walks use directory descriptors and O_NOFOLLOW. The working copy is4
retained even after replacement: only a subsequent confirmed cleanup removes it.5
"""6
import base647
import errno8
import fcntl9
import hashlib10
import json11
import os12
import pwd13
import re14
import secrets15
import stat16
import sys18
LIMIT = 25 * 1024 * 102419
REPLACED = False22
def fail(code):23
raise ValueError(code)26
def directory(path, privileged=False):27
if not path.startswith('/') or '\x00' in path or any(p in ('.', '..') for p in path.split('/')):28
fail('FILE_UNSAFE_PATH')29
fd = os.open('/', os.O_RDONLY | os.O_DIRECTORY)30
try:31
def trusted(value):32
s = os.fstat(value)33
if privileged and (s.st_uid != 0 or s.st_mode & 0o022 or 'system.posix_acl_access' in os.listxattr(value)):34
fail('FILE_UNSAFE_PRIVILEGED_DIRECTORY')35
trusted(fd)36
for part in filter(None, path.split('/')):37
nxt = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)38
os.close(fd)39
fd = nxt40
trusted(fd)41
return fd42
except BaseException:43
os.close(fd)44
raise47
def regular(parent, name, owner=None, private=False):48
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)49
s = os.fstat(fd)50
if not stat.S_ISREG(s.st_mode) or s.st_nlink != 1 or (owner is not None and s.st_uid != owner) or (private and stat.S_IMODE(s.st_mode) != 0o600):51
os.close(fd)52
fail('FILE_UNSAFE_TYPE_OR_LINK')53
return fd56
def attributes(fd):57
# Linux ACLs, capabilities and SELinux labels are extended attributes too.58
# Fail closed if any attribute cannot be read or reproduced.59
return {k: base64.b64encode(os.getxattr(fd, k)).decode('ascii') for k in sorted(os.listxattr(fd))}62
def snapshot(fd):63
before = os.fstat(fd)64
if before.st_size > LIMIT:65
fail('FILE_TOO_LARGE')66
os.lseek(fd, 0, os.SEEK_SET)67
pieces = []68
length = 069
while True:70
piece = os.read(fd, min(65536, LIMIT + 1 - length))71
if not piece:72
break73
pieces.append(piece)74
length += len(piece)75
if length > LIMIT:76
fail('FILE_TOO_LARGE')77
data = b''.join(pieces)78
xattrs = attributes(fd)79
after = os.fstat(fd)80
if (before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns):81
fail('FILE_CONFLICT')82
meta = dict(dev=after.st_dev, ino=after.st_ino, uid=after.st_uid, gid=after.st_gid,83
mode=stat.S_IMODE(after.st_mode), size=after.st_size,84
mtimeNs=str(after.st_mtime_ns), ctimeNs=str(after.st_ctime_ns), xattrs=xattrs,85
hash=hashlib.sha256(data).hexdigest())86
return data, meta89
def target(path, privileged=False):90
parent, name = os.path.split(path)91
if not name or name in ('.', '..'):92
fail('FILE_UNSAFE_PATH')93
d = directory(parent, privileged)94
try:95
return d, name, regular(d, name)96
except BaseException:97
os.close(d)98
raise101
def workspace(uid=None):102
uid = os.getuid() if uid is None else uid103
home = pwd.getpwuid(uid).pw_dir104
h = directory(home)105
try:106
hs = os.fstat(h)107
if hs.st_uid != uid or hs.st_mode & 0o022 or 'system.posix_acl_access' in os.listxattr(h):108
fail('FILE_UNSAFE_WORKSPACE')109
try:110
os.mkdir('ch-j-sm', 0o700, dir_fd=h)111
except FileExistsError:112
pass113
d = os.open('ch-j-sm', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=h)114
s = os.fstat(d)115
if s.st_uid != uid:116
os.close(d)117
fail('FILE_UNSAFE_WORKSPACE')118
os.fchmod(d, 0o700)119
return d, os.path.join(home, 'ch-j-sm'), uid120
finally:121
os.close(h)124
def operation_id(value):125
if not re.fullmatch('[a-f0-9]{32}', str(value)):126
fail('FILE_INVALID_RECOVERY_ID')127
return value130
def names(value):131
value = operation_id(value)132
return '.' + value + '.tmp', '.' + value + '.json'135
def put_record(d, name, value):136
data = json.dumps(value).encode('ascii')137
if len(data) > 256 * 1024:138
fail('FILE_METADATA_UNSUPPORTED')139
fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=d)140
with os.fdopen(fd, 'wb') as out:141
os.fchmod(out.fileno(), 0o600)142
out.write(data)143
out.flush()144
os.fsync(out.fileno())145
os.fsync(d)148
def get_record(d, name, uid):149
fd = regular(d, name, uid, True)150
with os.fdopen(fd, 'rb') as inp:151
raw = inp.read(256 * 1024 + 1)152
if len(raw) > 256 * 1024:153
fail('FILE_INVALID_RECOVERY_RECORD')154
record = json.loads(raw)155
if not re.fullmatch('[a-f0-9]{64}', record['hash']):156
fail('FILE_INVALID_RECOVERY_RECORD')157
return record160
def preserved(actual, original):161
return all(actual[k] == original[k] for k in ('uid', 'gid', 'mode', 'xattrs'))164
def inspect_record(d, uid, value, include_text=False):165
tmp, journal = names(value)166
r = get_record(d, journal, uid)167
fd = regular(d, tmp, uid, True)168
try:169
data, staged = snapshot(fd)170
finally:171
os.close(fd)172
complete = staged['hash'] == r['hash']173
state = 'upload-incomplete'174
if complete:175
state = 'recovery-available'176
try:177
td, name, t = target(r['path'])178
try:179
_, current = snapshot(t)180
if current['hash'] == r['hash'] and preserved(current, r['baseline']):181
state = 'confirmed'182
elif current != r['baseline']:183
state = 'conflict'184
finally:185
os.close(t)186
os.close(td)187
except OSError:188
pass # Permission denied/disconnected from target: copy remains useful.189
result = dict(recoveryId=value, path=r['path'], status=state, hash=staged['hash'], complete=complete)190
if include_text:191
if not complete:192
fail('FILE_UPLOAD_INCOMPLETE')193
result['data'] = base64.b64encode(data).decode('ascii')194
return result197
def run(a):198
global REPLACED199
op = a['operation']200
if op == 'read':201
d, name, fd = target(a['path'])202
try:203
data, meta = snapshot(fd)204
return dict(data=base64.b64encode(data).decode('ascii'), baseline=meta)205
finally:206
os.close(fd)207
os.close(d)208
d, home, uid = workspace(a.get('uid') if op != 'prepare' else None)209
try:210
if op == 'identity':211
return dict(uid=uid)212
if op == 'prepare':213
tmp, journal = names(a['id'])214
put_record(d, journal, dict(path=a['path'], baseline=a['baseline'], hash=a['hash']))215
return dict(temporary=home + '/' + tmp, uid=uid, recoveryId=a['id'])216
if op == 'list':217
results = []218
for name in sorted(os.listdir(d))[:10000]:219
if re.fullmatch(r'\.[a-f0-9]{32}\.json', name):220
try:221
results.append(inspect_record(d, uid, name[1:-5]))222
except (OSError, ValueError, KeyError):223
results.append(dict(recoveryId=name[1:-5], status='inspection-unavailable'))224
return dict(items=results)225
tmp, journal = names(a['id'])226
if op in ('inspect', 'cleanup'):227
result = inspect_record(d, uid, a['id'], op == 'inspect')228
if op == 'cleanup':229
if result['status'] != 'confirmed':230
fail('FILE_RESULT_UNCONFIRMED')231
os.unlink(tmp, dir_fd=d)232
os.unlink(journal, dir_fd=d)233
os.fsync(d)234
return result235
if op != 'finalize':236
fail('FILE_INVALID_OPERATION')237
source = regular(d, tmp, uid, True)238
try:239
data, staged = snapshot(source)240
if staged['hash'] != a['hash']:241
fail('FILE_UPLOAD_INCOMPLETE')242
os.fsync(source)243
td, name, old = target(a['path'], os.geteuid() == 0)244
destination = '.chj-save-' + secrets.token_hex(16) + '.tmp'245
created = False246
try:247
fcntl.flock(old, fcntl.LOCK_EX | fcntl.LOCK_NB)248
_, baseline = snapshot(old)249
if baseline != a['baseline']:250
fail('FILE_CONFLICT')251
# Copying a content/inode-bound integrity signature would leave252
# invalid security metadata. Re-signing requires a separate flow.253
if set(baseline['xattrs']) & {'security.ima', 'security.evm'}:254
fail('FILE_METADATA_UNSUPPORTED')255
dest = os.open(destination, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=td)256
created = True257
try:258
# Write fully before applying security metadata (capabilities259
# and setuid bits can be cleared by writes or chown).260
with os.fdopen(os.dup(dest), 'wb') as out:261
out.write(data)262
out.flush()263
ds = os.fstat(dest)264
if (ds.st_uid, ds.st_gid) != (baseline['uid'], baseline['gid']):265
os.fchown(dest, baseline['uid'], baseline['gid'])266
os.fchmod(dest, baseline['mode'])267
for key in os.listxattr(dest):268
if key not in baseline['xattrs']:269
os.removexattr(dest, key)270
for key, value in baseline['xattrs'].items():271
os.setxattr(dest, key, base64.b64decode(value))272
_, ready = snapshot(dest)273
if ready['hash'] != a['hash'] or not preserved(ready, baseline):274
fail('FILE_METADATA_UNSUPPORTED')275
os.fsync(dest)276
# Detect writes, chmod/chown, link changes and path replacement277
# while preparing. flock serializes cooperating editor saves.278
_, current = snapshot(old)279
linked = os.stat(name, dir_fd=td, follow_symlinks=False)280
if current != baseline or linked.st_ino != baseline['ino'] or linked.st_dev != baseline['dev'] or linked.st_nlink != 1:281
fail('FILE_CONFLICT')282
os.replace(destination, name, src_dir_fd=td, dst_dir_fd=td)283
REPLACED = True284
created = False285
os.fsync(td)286
check_dir, check_name, verified = target(a['path'])287
try:288
_, final = snapshot(verified)289
finally:290
os.close(verified)291
os.close(check_dir)292
if final['ino'] != ready['ino'] or final['hash'] != a['hash'] or not preserved(final, baseline):293
fail('FILE_RESULT_UNCONFIRMED')294
return dict(status='saved', baseline=final, hash=final['hash'])295
finally:296
os.close(dest)297
finally:298
if created:299
try:300
os.unlink(destination, dir_fd=td)301
except OSError:302
pass303
os.close(old)304
os.close(td)305
finally:306
os.close(source)307
finally:308
os.close(d)311
try:312
if sys.platform != 'linux':313
fail('FILE_LINUX_REQUIRED')314
print(json.dumps(dict(ok=True, value=run(json.loads(sys.argv[1])))))315
except BlockingIOError:316
print(json.dumps(dict(ok=False, code='FILE_CONFLICT')))317
except PermissionError:318
print(json.dumps(dict(ok=False, code='FILE_RESULT_UNKNOWN' if REPLACED else 'FILE_PERMISSION_DENIED')))319
except FileNotFoundError:320
print(json.dumps(dict(ok=False, code='FILE_RESULT_UNKNOWN' if REPLACED else 'FILE_NOT_FOUND')))321
except OSError as e:322
code = 'FILE_UNSAFE_PATH' if e.errno in (errno.ELOOP, errno.ENOTDIR) else 'FILE_REMOTE_IO_FAILED'323
print(json.dumps(dict(ok=False, code='FILE_RESULT_UNKNOWN' if REPLACED else code)))324
except (ValueError, KeyError, TypeError):325
e = sys.exc_info()[1]326
code = str(e) if re.fullmatch('FILE_[A-Z_]+', str(e)) else 'FILE_PROTOCOL_ERROR'327
print(json.dumps(dict(ok=False, code='FILE_RESULT_UNKNOWN' if REPLACED else code)))SHA-256: 64e39a9361357921026ee1e88fec63c55d785cf112af0e560d6177bfdd09409b
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0