Files
copyparty/scripts/sfx.py

503 lines
12 KiB
Python
Raw Normal View History

2021-09-16 00:26:52 +02:00
#!/usr/bin/env python3
2021-03-13 00:13:10 +01:00
# coding: latin-1
2020-05-03 22:48:05 +02:00
from __future__ import print_function, unicode_literals
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback
2021-04-29 22:41:57 +02:00
import subprocess as sp
2020-05-03 22:48:05 +02:00
2022-01-14 22:25:06 +01:00
2020-05-03 22:48:05 +02:00
"""
2021-07-15 00:26:33 +02:00
to edit this file, use HxD or "vim -b"
(there is compressed stuff at the end)
2020-05-03 22:48:05 +02:00
2021-10-09 22:29:23 +02:00
run me with python 2.7 or 3.3+ to unpack and run copyparty
2020-05-03 22:48:05 +02:00
2020-05-06 00:39:21 +02:00
there's zero binaries! just plaintext python scripts all the way down
2020-05-03 22:48:05 +02:00
so you can easily unpack the archive and inspect it for shady stuff
the archive data is attached after the b"\n# eof\n" archive marker,
2022-11-19 10:47:54 +00:00
b"?0" decodes to b"\x00"
b"?n" decodes to b"\n"
b"?r" decodes to b"\r"
b"??" decodes to b"?"
2020-05-03 22:48:05 +02:00
"""
2022-01-14 22:25:06 +01:00
2020-05-06 00:39:21 +02:00
# set by make-sfx.sh
2020-05-03 22:48:05 +02:00
VER = None
SIZE = None
CKSUM = None
STAMP = None
2022-09-25 21:31:47 +02:00
PY2 = sys.version_info < (3,)
2022-11-13 20:05:16 +00:00
PY37 = sys.version_info > (3, 7)
2021-04-21 18:28:44 +02:00
WINDOWS = sys.platform in ["win32", "msys"]
2020-05-03 22:48:05 +02:00
sys.dont_write_bytecode = True
me = os.path.abspath(os.path.realpath(__file__))
2021-04-29 22:41:57 +02:00
def eprint(*a, **ka):
ka["file"] = sys.stderr
print(*a, **ka)
2020-05-03 22:48:05 +02:00
2021-04-29 22:41:57 +02:00
def msg(*a, **ka):
if a:
a = ["[SFX]", a[0]] + list(a[1:])
2020-05-03 22:48:05 +02:00
2021-04-29 22:41:57 +02:00
eprint(*a, **ka)
2020-05-03 22:48:05 +02:00
# skip 1
def testptn1():
"""test: creates a test-pattern for encode()"""
import struct
buf = b""
for c in range(256):
buf += struct.pack("B", c)
yield buf
def testptn2():
import struct
for a in range(256):
if a % 16 == 0:
msg(a)
for b in range(256):
buf = b""
for c in range(256):
buf += struct.pack("BBBB", a, b, c, b)
yield buf
def testptn3():
with open("C:/Users/ed/Downloads/python-3.8.1-amd64.exe", "rb", 512 * 1024) as f:
while True:
buf = f.read(512 * 1024)
if not buf:
break
yield buf
testptn = testptn2
def testchk(cdata):
"""test: verifies that `data` yields testptn"""
import struct
cbuf = b""
mbuf = b""
checked = 0
t0 = time.time()
mdata = testptn()
while True:
if not mbuf:
try:
mbuf += next(mdata)
except:
break
if not cbuf:
try:
cbuf += next(cdata)
except:
expect = mbuf[:8]
expect = "".join(
" {:02x}".format(x)
for x in struct.unpack("B" * len(expect), expect)
)
raise Exception(
"truncated at {}, expected{}".format(checked + len(cbuf), expect)
)
ncmp = min(len(cbuf), len(mbuf))
# msg("checking {:x}H bytes, {:x}H ok so far".format(ncmp, checked))
for n in range(ncmp):
checked += 1
if cbuf[n] != mbuf[n]:
expect = mbuf[n : n + 8]
expect = "".join(
" {:02x}".format(x)
for x in struct.unpack("B" * len(expect), expect)
)
cc = struct.unpack(b"B", cbuf[n : n + 1])[0]
raise Exception(
"byte {:x}H bad, got {:02x}, expected{}".format(checked, cc, expect)
)
cbuf = cbuf[ncmp:]
mbuf = mbuf[ncmp:]
td = time.time() - t0
txt = "all {}d bytes OK in {:.3f} sec, {:.3f} MB/s".format(
checked, td, (checked / (1024 * 1024.0)) / td
)
msg(txt)
2020-05-06 00:39:21 +02:00
def encode(data, size, cksum, ver, ts):
2020-05-03 22:48:05 +02:00
"""creates a new sfx; `data` should yield bufs to attach"""
2022-10-09 22:56:27 +02:00
nb = 0
2020-05-03 22:48:05 +02:00
nin = 0
nout = 0
skip = False
with open(me, "rb") as fi:
unpk = ""
src = fi.read().replace(b"\r", b"").rstrip(b"\n").decode("utf-8")
for ln in src.split("\n"):
if ln.endswith("# skip 0"):
skip = False
2022-10-09 22:56:27 +02:00
nb = 9
2020-05-03 22:48:05 +02:00
continue
if ln.endswith("# skip 1") or skip:
skip = True
continue
2021-04-29 22:41:57 +02:00
if ln.strip().startswith("# fmt: "):
continue
2022-10-09 22:56:27 +02:00
if ln:
nb = 0
else:
nb += 1
if nb > 2:
continue
2020-05-03 22:48:05 +02:00
unpk += ln + "\n"
for k, v in [
["VER", '"' + ver + '"'],
["SIZE", size],
["CKSUM", '"' + cksum + '"'],
2020-05-06 00:39:21 +02:00
["STAMP", ts],
2020-05-03 22:48:05 +02:00
]:
v1 = "\n{} = None\n".format(k)
v2 = "\n{} = {}\n".format(k, v)
unpk = unpk.replace(v1, v2)
unpk = unpk.replace("\n ", "\n\t")
for _ in range(16):
unpk = unpk.replace("\t ", "\t\t")
with open("sfx.out", "wb") as f:
2022-11-19 10:47:54 +00:00
f.write(unpk.encode("utf-8").rstrip(b"\n") + b"\n\n\n# eof")
2020-05-03 22:48:05 +02:00
for buf in data:
2022-11-19 10:47:54 +00:00
ebuf = (
buf.replace(b"?", b"??")
.replace(b"\x00", b"?0")
.replace(b"\r", b"?r")
.replace(b"\n", b"?n")
)
2020-05-03 22:48:05 +02:00
nin += len(buf)
nout += len(ebuf)
2022-11-19 10:47:54 +00:00
while ebuf:
ep = 4090
while True:
a = ebuf.rfind(b"?", 0, ep)
if a < 0 or ep - a > 2:
break
ep = a
buf = ebuf[:ep]
ebuf = ebuf[ep:]
f.write(b"\n#" + buf)
f.write(b"\n\n")
2020-05-03 22:48:05 +02:00
msg("wrote {:x}H bytes ({:x}H after encode)".format(nin, nout))
2020-05-06 00:39:21 +02:00
def makesfx(tar_src, ver, ts):
2020-05-03 22:48:05 +02:00
sz = os.path.getsize(tar_src)
cksum = hashfile(tar_src)
2020-05-06 00:39:21 +02:00
encode(yieldfile(tar_src), sz, cksum, ver, ts)
2020-05-03 22:48:05 +02:00
# skip 0
def u8(gen):
try:
for s in gen:
yield s.decode("utf-8", "ignore")
except:
yield s
for s in gen:
yield s
2020-05-03 22:48:05 +02:00
def yieldfile(fn):
s = 64 * 1024
with open(fn, "rb", s * 4) as f:
for block in iter(lambda: f.read(s), b""):
2020-05-03 22:48:05 +02:00
yield block
def hashfile(fn):
h = hashlib.sha1()
2020-05-03 22:48:05 +02:00
for block in yieldfile(fn):
2021-04-24 20:08:07 +02:00
h.update(block)
2020-05-03 22:48:05 +02:00
return h.hexdigest()[:24]
2020-05-03 22:48:05 +02:00
def unpack():
"""unpacks the tar yielded by `data`"""
2022-10-24 18:48:12 +02:00
name = "pe-copyparty"
2022-09-16 22:19:59 +02:00
try:
2022-10-24 18:48:12 +02:00
name += "." + str(os.geteuid())
2022-09-16 22:19:59 +02:00
except:
2022-10-24 18:48:12 +02:00
pass
2022-09-16 22:19:59 +02:00
tag = "v" + str(STAMP)
2020-05-06 00:39:21 +02:00
top = tempfile.gettempdir()
2021-04-24 20:08:07 +02:00
opj = os.path.join
2022-10-21 18:49:25 +02:00
ofe = os.path.exists
2021-04-24 20:08:07 +02:00
final = opj(top, name)
2022-10-21 18:49:25 +02:00
san = opj(final, "copyparty/up2k.py")
for suf in range(0, 9001):
withpid = "%s.%d.%s" % (name, os.getpid(), suf)
mine = opj(top, withpid)
2022-10-21 18:49:25 +02:00
if not ofe(mine):
break
2021-04-24 20:08:07 +02:00
tar = opj(mine, "tar")
2020-05-06 00:39:21 +02:00
try:
2022-10-21 18:49:25 +02:00
if tag in os.listdir(final) and ofe(san):
msg("found early")
return final
except:
pass
2020-05-03 22:48:05 +02:00
2021-04-24 20:08:07 +02:00
sz = 0
2020-05-06 00:39:21 +02:00
os.mkdir(mine)
2020-05-03 22:48:05 +02:00
with open(tar, "wb") as f:
for buf in get_payload():
2021-04-24 20:08:07 +02:00
sz += len(buf)
2020-05-03 22:48:05 +02:00
f.write(buf)
2021-04-24 20:08:07 +02:00
ck = hashfile(tar)
if ck != CKSUM:
t = "\n\nexpected %s (%d byte)\nobtained %s (%d byte)\nsfx corrupt"
raise Exception(t % (CKSUM, SIZE, ck, sz))
2020-05-03 22:48:05 +02:00
with tarfile.open(tar, "r:bz2") as tf:
2022-10-11 17:44:38 +02:00
# this is safe against traversal
# skip 1
# since it will never process user-provided data;
# the only possible input is a single tar.bz2
# which gets hardcoded into this script at build stage
# skip 0
try:
tf.extractall(mine, filter="tar")
except TypeError:
tf.extractall(mine)
2020-05-03 22:48:05 +02:00
os.remove(tar)
2021-04-24 20:08:07 +02:00
with open(opj(mine, tag), "wb") as f:
2020-05-06 00:39:21 +02:00
f.write(b"h\n")
try:
2022-10-21 18:49:25 +02:00
if tag in os.listdir(final) and ofe(san):
msg("found late")
return final
except:
pass
2020-05-06 00:39:21 +02:00
try:
if os.path.islink(final):
os.remove(final)
else:
shutil.rmtree(final)
except:
2020-05-03 22:48:05 +02:00
pass
for fn in u8(os.listdir(top)):
if fn.startswith(name) and fn != withpid:
try:
old = opj(top, fn)
if time.time() - os.path.getmtime(old) > 86400:
shutil.rmtree(old)
except:
pass
2020-05-06 00:39:21 +02:00
try:
os.symlink(mine, final)
except:
try:
os.rename(mine, final)
return final
2020-05-06 00:39:21 +02:00
except:
msg("reloc fail,", mine)
return mine
2020-05-03 22:48:05 +02:00
def get_payload():
"""yields the binary data attached to script"""
with open(me, "rb") as f:
2022-11-19 10:47:54 +00:00
buf = f.read().rstrip(b"\r\n")
ptn = b"\n# eof\n#"
a = buf.find(ptn)
if a < 0:
raise Exception("could not find archive marker")
esc = {b"??": b"?", b"?r": b"\r", b"?n": b"\n", b"?0": b"\x00"}
buf = buf[a + len(ptn) :].replace(b"\n#", b"")
p = 0
while buf:
a = buf.find(b"?", p)
if a < 0:
yield buf[p:]
break
elif a == p:
yield esc[buf[p : p + 2]]
p += 2
else:
yield buf[p:a]
p = a
2020-05-03 22:48:05 +02:00
2021-03-13 00:13:10 +01:00
def confirm(rv):
2020-05-03 22:48:05 +02:00
msg()
2021-04-29 22:41:57 +02:00
msg("retcode", rv if rv else traceback.format_exc())
2022-07-15 02:04:00 +02:00
if WINDOWS:
msg("*** hit enter to exit ***")
try:
raw_input() if PY2 else input()
except:
pass
2020-05-03 22:48:05 +02:00
2021-08-09 22:13:00 +02:00
sys.exit(rv or 1)
2021-03-13 00:13:10 +01:00
2020-05-03 22:48:05 +02:00
def run(tmp, j2, ftp):
2021-04-24 20:08:07 +02:00
msg("jinja2:", j2 or "bundled")
msg("pyftpd:", ftp or "bundled")
2021-02-03 22:32:01 +01:00
msg("sfxdir:", tmp)
2021-03-13 00:13:10 +01:00
msg()
2020-05-03 22:48:05 +02:00
sys.argv.append("--sfx-tpoke=" + tmp)
2020-06-24 23:53:23 +00:00
2022-11-13 20:05:16 +00:00
ld = (("", ""), (j2, "j2"), (ftp, "ftp"), (not PY2, "py2"), (PY37, "py37"))
ld = [os.path.join(tmp, b) for a, b in ld if not a]
2020-05-03 22:48:05 +02:00
2022-06-16 01:07:15 +02:00
# skip 1
# enable this to dynamically remove type hints at startup,
# in case a future python version can use them for performance
if sys.version_info < (3, 10) and False:
sys.path.insert(0, ld[0])
from strip_hints.a import uh
uh(tmp + "/copyparty")
# skip 0
2021-04-29 22:41:57 +02:00
if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]):
run_s(ld)
else:
run_i(ld)
def run_i(ld):
2021-03-13 00:13:10 +01:00
for x in ld:
sys.path.insert(0, x)
2020-05-03 22:48:05 +02:00
e = os.environ
e["PRTY_NO_IMPRESO"] = "1"
2021-04-29 22:41:57 +02:00
from copyparty.__main__ import main as p
2021-04-29 22:41:57 +02:00
p()
2021-04-29 22:41:57 +02:00
def run_s(ld):
# fmt: off
2022-12-07 22:21:28 +00:00
c = "import sys,runpy;" + "".join(['sys.path.insert(0,r"' + x.replace("\\", "/") + '");' for x in ld]) + 'runpy.run_module("copyparty",run_name="__main__")'
2021-04-29 22:41:57 +02:00
c = [str(x) for x in [sys.executable, "-c", c] + list(sys.argv[1:])]
# fmt: on
msg("\n", c, "\n")
p = sp.Popen(c)
def bye(*a):
p.send_signal(signal.SIGINT)
signal.signal(signal.SIGTERM, bye)
p.wait()
raise SystemExit(p.returncode)
2020-05-03 22:48:05 +02:00
def main():
sysver = str(sys.version).replace("\n", "\n" + " " * 18)
2020-05-06 00:39:21 +02:00
pktime = time.strftime("%Y-%m-%d, %H:%M:%S", time.gmtime(STAMP))
2020-05-03 22:48:05 +02:00
msg()
msg(" this is: copyparty", VER)
2020-05-06 00:39:21 +02:00
msg(" packed at:", pktime, "UTC,", STAMP)
2020-05-03 22:48:05 +02:00
msg("archive is:", me)
msg("python bin:", sys.executable)
msg("python ver:", platform.python_implementation(), sysver)
msg()
arg = ""
try:
arg = sys.argv[1]
except:
pass
# skip 1
if arg == "--sfx-testgen":
2020-05-06 00:39:21 +02:00
return encode(testptn(), 1, "x", "x", 1)
2020-05-03 22:48:05 +02:00
if arg == "--sfx-testchk":
return testchk(get_payload())
if arg == "--sfx-make":
2020-05-06 00:39:21 +02:00
tar, ver, ts = sys.argv[2:]
return makesfx(tar, ver, ts)
2020-05-03 22:48:05 +02:00
# skip 0
tmp = os.path.realpath(unpack())
2020-05-06 00:39:21 +02:00
2021-02-03 22:32:01 +01:00
try:
2021-04-24 20:08:07 +02:00
from jinja2 import __version__ as j2
2021-02-03 22:32:01 +01:00
except:
2021-04-24 20:08:07 +02:00
j2 = None
2020-05-03 22:48:05 +02:00
2021-04-29 22:41:57 +02:00
try:
from pyftpdlib.__init__ import __ver__ as ftp
except:
ftp = None
try:
run(tmp, j2, ftp)
2021-04-29 22:41:57 +02:00
except SystemExit as ex:
c = ex.code
if c not in [0, -15]:
confirm(ex.code)
except KeyboardInterrupt:
pass
except:
confirm(0)
2020-05-03 22:48:05 +02:00
if __name__ == "__main__":
main()
# skip 1
# python sfx.py --sfx-testgen && python test.py --sfx-testchk
# c:\Python27\python.exe sfx.py --sfx-testgen && c:\Python27\python.exe test.py --sfx-testchk