diff -r 000000000000 -r 676905a3b03c dejsem.1.5/python/dejsem.pycharm/server.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dejsem.1.5/python/dejsem.pycharm/server.py Wed Nov 27 09:50:16 2019 +0100 @@ -0,0 +1,600 @@ +#!/usr/bin/python3 +# coding=utf8 + +import os, sys, subprocess, random, string, traceback, urllib.parse +from os.path import join +from d import D +from parms import Parms +from node import Node + + +# operace v nekonečném cyklu serveru: +# - pro každý kanál v homedir je jedna instance Server +# - každá instance má k dispozici 10 portů pro LISTEN +# - číslování portů: +# - 17CCP +# - CC = číslo kanálu +# - P = číslo portu v kanálu +# - port 17xx0 je vyhrazen pro rychlé synchronní operace serveru - commands +# - porty 17xx1-9 jsou pro long running tasks + +""" + server pro určitý uživatelský kanál + instance vytváří a spouští main.py +""" +class Server(): + + def __init__(self, d, chan): + self._baseid = "{}.server[{:02d}]".format(d.debid, chan) + self.d = D("{}".format(self._baseid)) + + Parms.clientMode = False + Parms.sslCert = join(Parms.sslPath, "srv.pem") + self.d.log("ssl path: {}".format(Parms.sslPath), sev=3) + + self._chan = chan + self._homedir = join(Parms.srv_homedir, "{:02d}".format(self._chan)) + os.chdir(self._homedir) + self.d.log("working dir={}, ls: {}".format(os.getcwd(), os.listdir(), sev=4)) + os.umask(0o077) + + self._filedir = join(self._homedir, Parms.filedir) + + try: + self._node = Node(self.d, host='', chan=self._chan, conn=False, tryPort=False) + except Exception as e: + self.d.abend("creating network node", e) + sys.exit(1) + self.server() + + def server(self): + # v této metodě server poslouchá na základním portu v kanálu (port 0); + # po připojení klienta alokuje socket a synchronně cyklicky volá metodu service() k provedení příkazu klienta; + # cyklus volání service() trvá dokud service() nevrátí False; + # smyslem cyklu před novým acceptem je dát možnost klientu provést rychlou dávku příkazů proveditelných okamžitě; + if not os.path.exists(Parms.clipfile): + os.system("touch " + Parms.clipfile) + if not os.path.exists(Parms.filedir): + os.mkdir(Parms.filedir) + while True: + try: + if self._node.acc(acc_TO=None): + while self.service(): pass + except KeyboardInterrupt: + self.d.log("accept: KeyboardInterrupt") + break + if self.d.ll(4): self.d.log("closing ssc...") + self._node.close_ssc() + + def service(self): + # metoda synchronně ("blocking") dostává ze socketu příkaz klienta, provede ho + # a vrací true, když chce nechat socket otevřený a false, když se může socket zavřít; + # socket se nechává otevřený, když má smysl provádět danou operaci v dávce; + # je na klientu, aby dávka byla krátká a aby zavřel neprodleně + cmd = "" + try: + if self.d.ll(2): self.d.log("get command...") + random.getrandbits(16) + cmd = self._node.getcmd() # čtení osmi-znakového příkazu + if self.d.ll(5): self.d.log("cmd len={}".format(len(cmd))) + if not cmd: return False + if self.d.ll(3): self.d.log("cmd={}".format(cmd)) + # názvy operací pull/push odpovídají pohledu klienta; + # operace jsou synchronní, blokují další příkazy v daném kanále a musí být v "lidském" měřítku krátké; + # dlouhé operace se zahajují příkazem LONGTASK, po němž se alokuje paralelní nit k provedení dlouhé operace (typicky přenosu dat); + if cmd == "PULLCLIP": # přenos posledního uloženého clipboardu na klienta + return self.cmd_pullclip() + elif cmd == "PULLHIST": # přenos přehledu uložených clipboard entries na klienta + return self.cmd_pullhist() + elif cmd == "PULLLIST": # přenos seznamu souborů v adresáři na klienta + return self.cmd_pulllist() + elif cmd == "PUSHCLIP": # příjem nového clipboardu k uložení na serveru + return self.cmd_pushclip() + elif cmd == "GETPEER": # předání uložené peer-adresy klientu + return self.cmd_getpeer() + elif cmd == "SETPEER": # příjem peer-adresy k uložení na serveru + return self.cmd_setpeer() + elif cmd == "MOVE": # přesun uloženého souboru/adresáře v rámci serveru + return self.cmd_move() + elif cmd == "EXPOSE": # vystavení souboru/adresáře na http-serveru - vrací se URI path + return self.cmd_expose() + elif cmd == "HIDE": # zakrytí souboru/adresáře pro http-server + return self.cmd_hide() + elif cmd == "DELETE": # rekurzivní výmaz souboru/adresáře na serveru + return self.cmd_delete() + elif cmd == "CREATDIR": # založení adresáře na serveru + return self.cmd_createdir() + elif cmd == "FREE": # předání informace o volném prostoru na serveru + return self.cmd_freespace() + elif cmd == "RECKON": # předání informace o velikost souboru/adresáře + return self.cmd_reckon() + elif cmd == "LONGTASK": # zahájení operace v jiném procesu na jiném portu + return self.cmd_longtask() + elif cmd == "HACK": + self.d.log("hack") + return False + else: + if self.d.ll(4): self.d.log("unknown command") + self._node.close_sc() + return False + except KeyboardInterrupt: + raise + except Exception as e: + # if self.d.ll(4): self.d.log("service {} aborted: {}: {}".format(cmd, e.__class__.__name__, str(e))) + if self.d.ll(4): self.d.log("service {} aborted: {}".format(cmd, e)) + traceback.print_tb(sys.exc_info()[2]) + self._node.close_sc() + return False + + def cmd_longtask(self): + """ + najde volný port 1-9, pošle ho klientovi, forkne se do paralelního procesu v němž + čeká na připojení klienta, čte příkaz k operaci a provádí operaci + - operace se nedávkují, na konci procesu se socket a serversocket zavírají a port uvolňuje + - ssc, port = self._node.bindtrynext(Parms.srvhost) + """ + try: + longtaskNode = Node(self.d, host='', chan=self._chan, tryPort=True, conn=False) + except Node.AllPortsBusy: + self._node.sendport(0) + return False + self._node.sendport(longtaskNode.port) + self._node.close_sc() + if os.fork(): + return False + # subprocess -------------- + self._node = longtaskNode + self.d.debid = "{} long task[{:d}]".format(self._baseid, self._node.port) + random.seed() # je potřeba se odstřihnout od random-sekvence hlavního procesu + try: + if self._node.acc(acc_TO=Parms.long_run_accept_timeout): + while self.longserv(): pass # cykluj, dokud je longserv positivní + except Exception as e: + self.d.abendMsg("long task accept", e=e) + self._node.close_sc() + self._node.close_ssc() + os._exit(0) + + def longserv(self): + cmd = self._node.getcmd() + if not cmd: return False + try: + if self.d.ll(4): self.d.log("longtask cmd: " + cmd) + # názvy operací pull/push odpovídají pohledu klienta + if cmd == "PUSHFILE": # rekurzivní příjem souboru/adresáře k uložení na serveru + return self.cmd_pushfile() + elif cmd == "PUSHFIEX": # rekurzivní příjem souboru/adresáře k vystavení na http serveru + return self.cmd_pushFileExpose() + elif cmd == "PUSHSTEX": # příjem streamu k vystavení na http serveru + return self.cmd_pushStreamExpose() + elif cmd == "PULLFILE": # rekurzivní odeslání obsahu souboru/adresáře + return self.cmd_pullfile() + elif cmd == "PULLCLIP": # využije se pro dávkovou synchronizaci historie clipboardu + return self.cmd_pullclip() + elif cmd == "PULLHIST": # využije se pro dávkovou synchronizaci historie clipboardu + return self.cmd_pullhist() + elif cmd == "ECHO": + return self.ee(cmd) + elif cmd == "ECHOECHO": + return self.ee(cmd) + elif cmd == "HACK": + return self.rand(6) + else: + return False + except Exception as e: + self.d.abendMsg("long task action {}".format(cmd), e=e) + return False + + def rand(self, n): + for i in range(n): + self.d.log("{:04x}".format(random.getrandbits(16))) + return False + + def ee(self, cmd): + self.d.log("ECHO.004: cmd={}".format(cmd)) + while cmd == "ECHO____" or cmd == "ECHOECHO": + self.d.log("echoing...") + self._node.putstr("ECHO") + if cmd == "ECHOECHO": self.d.log("echo, got={}".format(self._node.getfn())) + self.d.log("waiting for cmd") + cmd = self._node.getcmd() + self.d.log("cmd got={}".format(cmd)) + self.d.log("waiting for close") + self._node.get(1) # wait for other side close + + def cmd_pullfile(self): + # operace download, v níž může klient dávkovat příkazy PULLFILE ke stažení objektů + cmd = "PULLFILE" + while cmd == "PULLFILE": + req = self.formpath(self._node.getfn()) + if req: + fp = os.path.normpath(join(Parms.filedir, req)) + if os.path.exists(fp): + prefix = os.path.dirname(fp) + if self.d.ll(3): self.d.log("pullfile request for '{}': starting...".format(fp)) + self.pullfilerecurse(fp, prefix) + else: + if self.d.ll(3): self.d.log("pullfile request for '{}': not found".format(fp)) + self._node.sendEOD() # end of recursive subtree of files + cmd = self._node.getcmd() # another PULLFILE is expected here + self._node.getcmd() # wait for other side close + return False # konec dávky + + def pullfilerecurse(self, fp, prefix): + if os.path.isdir(fp): + for cwd, void, files in os.walk(fp, topdown=False): + for entry in files: + self.pullfilerecurse(join(cwd, entry), prefix) + self._node.putfileinfo(cwd, os.path.relpath(cwd, start=prefix)) + else: + self._node.putfileinfo(fp, os.path.relpath(fp, start=prefix)) + with open(fp, mode='rb') as f: + g = self._node.genput() + g.send(None) + data = f.read(Parms.bufSize) + while data: + try: + g.send(data) + data = f.read(Parms.bufSize) + except Exception: + break + g.close() + if self.d.ll(3): self.d.log("PULLFILE: entry {} sent".format(fp)) + + def cmd_pushfile(self): + # předpokládá se, že subadresáře, do nichž se přijímá, jsou vždycky writable, vznikly uploadem + if self.d.ll(4): self.d.log("push file start") + cmd = "PUSHFILE" + while cmd == "PUSHFILE": + req = self._node.getfn() # relativní cesta + while len(req) > 0: # rekurzivní načtení stromu - končí prázdným req + req = self.formpath(req) + uploadPath = join(Parms.filedir, req) + size = self._node.getnum() + timestamp = self._node.getnum() + if self.d.ll(4): self.d.log("push: req={} to=[{}, dir={}]...".format(req, uploadPath, size == -1)) + if size < 0: + self._node.receive_dir(uploadPath, size, timestamp) + else: + self._node.receive_file(uploadPath, size, timestamp) + req = self._node.getfn() + cmd = self._node.getcmd() # PUSHFILE or BATCHEND is expected here + if self.d.ll(4): self.d.log("cmd_pushfile(), iterace v dávce, cmd={}".format(cmd)) + self._node.putnum(0) # client awaits end of transfer confirmation + if self.d.ll(4): self.d.log("push file finished") + return False # konec dávky + + def cmd_pushFileExpose(self): + """ + Upload and Expose to HTTP + ● klient posílá na pozadí objekt, který se vystaví na http-serveru + ● objekt se ukládá do zvláštního adresáře self._exposed pod randomizovaným jménem + ● klientovi se posílá segment "path" z výsledného URL, URL si zkomletuje klient + ● metoda vrací False, protože se expose nedávkuje + :return: False + """ + if self.d.ll(4): self.d.log("push and expose file start") + self.link_exposed() + req = self._node.getfn() # relativní cesta + if len(req) > 0: + stem, void, rest = self.formpath(req).partition('/') + exposedPath = self.randomize_path(stem, self._exposed) + while len(req) > 0: # rekurzivní načtení stromu - končí prázdným req + uploadPath = join(self._exposed, exposedPath) + void, void, rest = self.formpath(req).partition('/') + if rest: uploadPath = join(uploadPath, rest) + size = self._node.getnum() + timestamp = self._node.getnum() + if self.d.ll(4): self.d.log("push to '{}, dir={}'...".format(uploadPath, size == -1)) + if size < 0: + self._node.receive_dir(uploadPath, size, timestamp) + else: + self._node.receive_file(uploadPath, size, timestamp) + req = self._node.getfn() # relativní cesta + self.d.log("{} uploaded".format(exposedPath)) + self.expose_link(exposedPath) + if self.d.ll(4): self.d.log("push and expose file end") + else: + self.node.putnum(0) + return False # konec, expose se nedávkuje + + def cmd_pushStreamExpose(self): + """ + Upload and Expose to HTTP + ● klient posílá na pozadí stream, který se vystaví na http-serveru + ● stream se ukládá do zvláštního adresáře self._exposed pod randomizovaným jménem + ● klientovi se posílá segment "path" z výsledného URL, URL si zkomletuje klient + ● metoda vrací False, protože se expose nedávkuje + :return: False + """ + if self.d.ll(4): self.d.log("push and expose stream start") + expName, origName = None, Node + mimeType = self._node.getstr() + size = self._node.getnum() + if size > 0: + self.link_exposed() + (type, suffix) = mimeType.split('/') + if not type or type == '*': type = "content" + expName = self.randomize_path(type, self._exposed) + if suffix and suffix != '*': expName += '.' + suffix + expPath = join(self._exposed, expName) + if self.d.ll(4): self.d.log("push stream to '{}'...".format(expPath)) + self._node.receive_stream(expPath, size) + # subprocess.run(('touch', expPath)) + self.expose_link(expName) + if self.d.ll(4): self.d.log("push stream to '{}' finished".format(expPath)) + else: + """self.node.putnum(0)""" + return False # konec dávky + + def cmd_pulllist(self): + req = self.formpath(self._node.getfn()) + if req: + fp = os.path.normpath(join(Parms.filedir, req)) + if self.d.ll(4): self.d.log("dir=" + fp) + if os.path.exists(fp) and os.access(fp, os.R_OK | os.X_OK): + if os.path.isdir(fp): + for entry in os.listdir(path=fp): + self._node.putfileinfo(join(fp, entry), entry) + else: + self._node.putfileinfo(fp, req) + self._node.putnum(0) + self._node.close_sc() + return False # konec dávky + + def cmd_pullclip(self): + fn = self._node.getfn() + if fn == "": + fp = Parms.clipfile + else: + fp = Parms.histdir + "/" + fn + if self.d.ll(4): + self.d.log("clip fp={}".format(fp)) + if os.path.exists(fp): + self._node.putnum(os.path.getsize(fp)) + with open(fp, mode="rb") as f: + g = self._node.genput() + g.send(None) + data = f.read(Parms.bufSize) + while len(data) > 0: + try: + g.send(data) + data = f.read(Parms.bufSize) + except Exception: + break + g.close() + return True # případné dávkování + + def cmd_pushclip(self): + with open(Parms.clipfile, mode='wb') as f: + for data in self._node.genget(size = -1): + f.write(data) + self._node.close_sc() + subprocess.call(("cp", "-a", Parms.clipfile, join(Parms.histdir, self.hist_fn()))) + if self.d.ll(4): self.d.log("pushclip: {} bytes stored".format(os.path.getsize(Parms.clipfile))) + return False # konec dávky + + def hist_fn(self): # random string file name + if not os.path.exists(Parms.histdir): + os.mkdir(Parms.histdir) + a = (string.digits + string.ascii_letters) + fn = "" + for i in list(range(5)): + fn += a[random.randint(0, 61)] + while os.path.exists(join(Parms.histdir, fn)): + fn = "" + for i in list(range(5)): + fn += a[random.randint(0, 61)] + return fn + + def cmd_pullhist(self): + if os.path.exists(Parms.histdir) and os.path.isdir(Parms.histdir): + for entry in os.listdir(Parms.histdir): + p = join(Parms.histdir, entry) + self._node.putstr(entry) + sample = open(p, mode="rb").read(80) # pošli vzorek max.80 z každého entry + self._node.putnum(len(sample)) + self._node.put(sample) + self._node.putnum(os.path.getsize(p)) + self._node.putnum(int(os.path.getmtime(p))) + self._node.putnum(0) # konec streamu + return True + + def cmd_expose(self): + """ + Expose to HTTP + ● klient posílá jméno objektu na serveru, který se má vystavit na http-serveru + ● jméno je cesta relativní k self._filedir + ● objekt se symlinkuje ve zvláštním adresáři self._exposed randomizovaným jménem odvozeným ze jména objektu + ● klientovi se posílá segment "path" z výsledného URL, URL si zkompletuje klient + ● metoda vrací False, protože se expose nedávkuje + :return: False + """ + fpath = join(self._filedir, self.formpath(self._node.getfn())) # node.getfn() je cesta relativně k self._filedir + self.link_exposed() + expName = self.randomize_path(os.path.basename(fpath), self._exposed) + expPath = join(self._exposed, expName) + subprocess.run(('ln', '-sfnr', fpath, expPath), check=True) + if self.d.ll(3): self.d.log("fn={}".format(expPath)) + self.expose_link(expName) + return False # expose nemůže být v dávce, protože se klientovi posílá zpátky URI path + + def cmd_hide(self): + fpath = self.formpath(self._node.getfn()) + if self.d.ll(3): self.d.log("fn={}".format(fpath)) + channel = "{:02d}".format(self._chan) + fpath = join(Parms.srv_homedir, channel, Parms.filedir, fpath) + subprocess.run(('chmod', '-R', 'o-rwx', fpath)) + subprocess.run(('find', fpath, '-name', '.htaccess', '-exec', 'rm', '-f', '{}', '+')) + return True # případné dávkování + + def cmd_getpeer(self): # atavismus z doby před použitím UDP broadcast + host = "" + port = 0 + if len(self.peer) == 2: + host = "10.0.2.2" if self.peer[0] == "10.0.2.15" else str(self.peer[0]) + port = self.peer[1] + self.peer = [] + self._node.putstr(host) + self._node.putnum(port) + self._node.close_sc() + return False + + def cmd_setpeer(self): # atavismus z doby před použitím UDP broadcast + self.peer = (str(self._node.getfn()), int(self._node.getnum())) + self._node.close_sc() + return False + + def cmd_move(self): + orig = self.formpath(self._node.getfn()) + target = self.formpath(self._node.getfn()) + if orig and target: + if self.d.ll(4): self.d.log("performing mv -n {} {}".format(orig, target)) + subprocess.call(["mv", "-n", join(Parms.filedir, orig), join(Parms.filedir, target)]) + return True # případné dávkování + + def cmd_delete(self): + req = self.formpath(self._node.getfn()) + if req: + if self.d.ll(4): self.d.log("performing rm -rf {}".format(req)) + subprocess.call(["rm", "-rf", join(Parms.filedir, req)]) + return True # případné dávkování + + def cmd_createdir(self): + req = self.formpath(self._node.getfn()) + if req: + fp = join(Parms.filedir, req) + if self.d.ll(4): self.d.log("performing mkdir -p {}".format(fp)) + subprocess.call(["mkdir", "-p", fp]) + return True # případné dávkování + + def cmd_freespace(self): + # df v Debianu 7.6 nemá parametr --output :-( + p = subprocess.Popen(["df", "--block-size=1", "."], stdout=subprocess.PIPE) + p.wait() + if p.returncode == 0: + free = int(p.stdout.readlines()[1].decode().split()[3]) + else: + free = -1 + self._node.putnum(free) + self._node.close_sc() + return True # případné dávkování + + def cmd_reckon(self): + req = self.formpath(self._node.getfn()) + self.sendobjectsize(join(Parms.filedir, req) if req else None) + return True # případné dávkování + + def sendobjectsize(self, fp): + if not fp or not os.path.exists(fp): + size, fnum, dnum = (0, 0, 0) + elif os.path.isfile(fp): + size, fnum, dnum = (os.path.getsize(fp), 1, 0) + else: + size, fnum, dnum = self.dirsize(fp) + self._node.putnum(int(size)) + self._node.putnum(int(fnum)) + self._node.putnum(int(dnum)) + + def link_exposed(self): + """ + ● založení adresáře pro vystavené objekty self._exposed podle Parms.exposed + ● adresář je symlinkován z http-serveru číslem kanálu + ● symlink z http-serveru musí vytvořit instalace nebo super-user + ● na http-serveru se při instalaci aplikace zakládá adresář pro tuto aplikaci se symlinky na exposed dirs jednotlivých kanálů + ● // --> /// + ● / musí mít povoleny symlinky a povolen( instalovat .htaccess s Options Indexes + ● http-server musí mít x-access po cestě // --> + ● výšeuvedené atributy se nemůže zařídit aplikace, musí být nastaveny při instalaci nebo administrátorem + """ + self._exposed = join(self._filedir, Parms.exposed) + if not os.path.exists(self._exposed): + subprocess.run(('mkdir', '-pm771', self._exposed)) + # os.mkdir(self._exposed, mode=0o771) + channel = "{:02d}".format(self._chan) + self._wwwhome = join(Parms.srv_wwwhomedir, channel) + if not os.path.exists(self._wwwhome): + subprocess.run(('ln', '-sfnr', self._exposed, self._wwwhome), check=True) + + def expose_link(self, path): + """ + ● fpath je cesta relativní k self._exposed + ● úkolem je zařídit read-access k filům, x-access k dirs, případně .htacess v kořenu stromu + """ + orig = join(self._exposed, path) + self.d.log("orig={}".format(orig), sev=4) + try: + subprocess.run(('chmod', 'o+r', orig), check=True) + if os.path.isdir(orig): + self.expose_dir_tree(orig) + elif path.find('/') > 0: + self.expose_dir_path(path) + channel = "{:02d}".format(self._chan) + # uriPath = urllib.parse.quote(join(Parms.applName, channel, path)) + uriPath = join(Parms.applName, channel, path) + self._node.putstr(uriPath) # pošli URL-path klientovi + self.d.log("uri path {} sent".format(uriPath), sev=3) + except Exception as e: + self.d.abend("exposing file to web server", e) + + def expose_dir_path(self, rel_file_path): + """ + po cestě k vystavenému souboru je potřeba nastavit x-access pro http-server + :param rel_file_path: cesta relativní k self._exposed + """ + base = self._exposed + while rel_file_path: + subprocess.run(('chmod', 'o=x', base)) + (subdir, sep, rel_file_path) = rel_file_path.partition('/') + base = join(base, subdir) + + def expose_dir_tree(self, rel_dir_path): + """ + do vystaveného stromu je třeba umístit .htaccess, po cestě nastavit x-access, ve stromu nastavit rx-access + :param rel_dir_path: cesta relativní k self._exposed + """ + htaccess = join(rel_dir_path, ".htaccess") + with open(htaccess, mode="w") as f: + f.write("Options Indexes") + subprocess.run(('chmod', 'o+r', htaccess)) + subprocess.run(('chmod', 'o+x', rel_dir_path)) + subprocess.run(('chmod', '-R', 'o+r', rel_dir_path)) + for (rel_dir_path, dirs, files) in os.walk(rel_dir_path): + for dir in dirs: + dpath = join(rel_dir_path, dir) + subprocess.run(('chmod', 'o+x', dpath)) + + def randomize_path(self, fn, dirname): + name, void, suff = os.path.basename(fn).rpartition(".") + expName = "" + while os.path.exists(join(dirname, expName)) or expName == "": + uniq = "{:04x}".format(random.getrandbits(16)) + if name: + expName = name + "." + uniq + "." + suff + else: + expName = suff + "." + uniq + return expName + + def dirsize(self, fp): + if self.d.ll(4): self.d.log("performing du -sb '{}'".format(fp)) + try: + size = subprocess.check_output(["du", "-sb", fp])[:-1].decode().split("\t")[0] + fnum = len(subprocess.check_output(["find", fp, "-type", "f"]).split(b'\n'))-1 + dnum = len(subprocess.check_output(["find", fp, "-type", "d"]).split(b'\n'))-1 + except subprocess.CalledProcessError: + (size, fnum, dnum) = (-1, 0, 0) + if self.d.ll(5): self.d.log("size={}, type={}, fnum={}. type={}, dnum={}, type={}" + .format(size, type(size), fnum, type(fnum), dnum, type(dnum))) + return (size, fnum, dnum) + + def close_sc(self): + self._node.close_sc() + + def formpath(self, path): + """ + - v žádném případě absolutní cesta + - vrací None, když path jde up from current + """ + if path.startswith('/'): path = path[1:] + p = os.path.normpath(path) + return None if p == '..' or p.startswith('../') else p \ No newline at end of file