123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- import cherrypy # <3
- from cherrypy.process import plugins
- import chevron # its fate is the same as cherrypy
- import os
- import hashlib
- import mimetypes
- import urllib
- import re
- import json
- import random
- import threading
- from storage import Storage, InvalidPath
- @cherrypy.tools.register("before_handler")
- def auth():
- if cherrypy.session.get("login"):
- return
- else:
- raise cherrypy.HTTPRedirect("/login")
- class IconHandler:
- def __init__(self):
- self.mimeDic = mimetypes.types_map
- self.custom = {}
- with open(os.path.abspath('./icons-config.json')) as ic:
- conf = json.load(ic)
- self.custom = conf['custom']
- self.mime_filter = conf['mime-filter']
- self.default = conf['default']
- def get_icon_and_class(self, ext):
- for exts, p in self.custom.items():
- if ext in exts.split(', '):
- return p['icon'], p['class']
- if '.' + ext in self.mimeDic:
- for pat, p in self.mime_filter.items():
- type_ = self.mimeDic['.' + ext]
- if re.match(pat, type_):
- return p['icon'], p['class']
- else:
- return self.default['icon'], self.default['class']
- class TimerKiller(plugins.SimplePlugin):
- def __init__(self, bus, app):
- plugins.SimplePlugin.__init__(self, bus)
- self.app = app
- def stop(self):
- self.app._timer.cancel()
- class FarCloud:
- def __init__(self, admin, templates, storage):
- # from 23 Oct 2020 admin MUST be
- # {"username": ..., "setpass": func with 1 arg, "getpass": func}
- self.admin = admin
- self.iconhandler = IconHandler()
- self.storage = storage
- self.templates = templates
- mimetypes.init()
- self.ip_request_count = {} # ip: requests number
- # ^ this will be cleared every self.COUNTER_RESET_INTERVAL
- # seconds with threading.Timer() and the limit below is used
- # to block IPs which reach this limit. If an IP is not in
- # this dictionary, it will be initialized to 0.
- #
- # Request count of an IP >= self.REQUEST_LIMIT
- # => the counter won't be increased anymore
- #
- # TODO: Should we decrease the requests number each time
- # instead of resetting it?
- # Advantage: Some IP may not send 2 * REQUEST_LIMIT at once
- # let's say in just one second.
- # Disadvantage: We have to store numbers higher than
- # REQUEST_LIMIT and some IP may force high
- # memory usage for our App since integers
- # in Python are limited by memory size
- # not number of bits like in C/C++
- #
- # >>>> This limit is used ONLY for login page <<<<
- self.REQUEST_LIMIT = 6
- self.COUNTER_RESET_INTERVAL = 30 # in seconds
- # ^ meaning 6 login validation request every 30 seconds
- self.reset_req_count()
-
- def reset_req_count(self):
- self.ip_request_count.clear()
- self._timer = threading.Timer(self.COUNTER_RESET_INTERVAL, self.reset_req_count)
- self._timer.start()
- def getmime(self, ext):
- if not ext.startswith("."):
- ext = "." + ext
- unknown = "application/octet-stream"
- return mimetypes.types_map.get(ext) or unknown
-
- def get_pass_hash(self, pass_):
- the_hash = hashlib.sha512(pass_.encode()).hexdigest()
- return the_hash
- def get_file_pass_hash(self, pass_):
- the_hash = hashlib.sha256(pass_.encode()).hexdigest()
- return the_hash
- def humanreadablesize(self, size):
- units = ("B", "KiB", "MiB", "GiB", "TiB")
- unit = 0
- while size >= 1024 and unit < 5:
- size /= 1024
- unit += 1
- return f"{size:.2f} " + units[unit]
- @cherrypy.tools.auth()
- @cherrypy.expose
- def modify(self, command, name, new):
- commands = dict()
- commands["name"] = self.storage.move
- commands["desc"] = lambda a,b: self.storage.set_property(a,"description",b)
- commands["pass"] = lambda a,b: self.storage.set_property(a,"password",b)
-
- if command not in commands:
- return "Invalid command"
- try:
- commands[command](name, new)
- except InvalidPath:
- raise cherrypy.HTTPError(400, "Invalid path")
- return "Well done :)"
- @cherrypy.tools.auth()
- @cherrypy.expose
- def change_password(self, old, new):
- old_hash = self.get_pass_hash(old)
- if old_hash == self.admin["getpass"]():
- new_hash = self.get_pass_hash(new)
- self.admin["setpass"](new_hash)
- cherrypy.session["login"] = False
- return "Successfully changed the password! Now logging out..."
- else:
- return "Sorry! Incorrect old password"
- @cherrypy.tools.auth()
- @cherrypy.expose
- def upload_url(self, url, name=""):
- try:
- _ = not name or self.storage._safe(name)
- req = urllib.request.Request(url)
- resp = urllib.request.urlopen(req)
- except ValueError as e:
- return cherrypy.HTTPError(400, str(e))
- except urllib.error.HTTPError as e:
- code = e.code
- return f"Cannot fetch: {code}"
- except InvalidPath:
- raise cherrypy.HTTPError(400, "Invalid path")
- except urllib.error.URLError:
- return "Cannot open URL"
- if not name:
- pat = "filename(.+)"
- hdr = resp.headers.get("Content-Disposition") or ' '
- fname = re.findall(pat, hdr)
- aname = url.split("/")[-1]
- if fname:
- name = fname[0]
- elif aname:
- name = aname
- else:
- name = str(random.randint(1_000, 10_000_000))
-
- self.storage.add(name, resp)
- return f"Downloaded the file and saved it with name {name}"
- @cherrypy.expose
- def hashsum(self, name):
- try:
- if not self.storage.exists(name):
- return "File does not exist"
- if self.storage.isdir(name):
- return "It is a directory"
- return self.storage.hashsum(name)
- except InvalidPath:
- return "The path you have sent is not valid(or in my scope)"
- except:
- return "Cannot open file."
- @cherrypy.tools.auth()
- @cherrypy.expose
- def logout(self):
- cherrypy.session["login"] = False
- raise cherrypy.HTTPRedirect("/")
- @cherrypy.expose
- def login(self, username="", password=""):
- ip = cherrypy.request.remote.ip
-
- if self.ip_request_count.setdefault(ip,0) >= self.REQUEST_LIMIT:
- return "Sorry! Your machine is sending too many validation requests. Please wait a few minutes and retry"
- if len(username) > 16:
- return "Too long username"
- if len(password) > 256:
- return "Too long password"
- if not username or not password:
- template_data = dict() # Let it be as is, for future needs
- return chevron.render(get_template("login"), template_data)
- if self.ip_request_count[ip] < self.REQUEST_LIMIT:
- self.ip_request_count[ip] += 1
- pwd_h = self.get_pass_hash(password)
- if username == self.admin["username"] and pwd_h == self.admin["getpass"]():
- cherrypy.session["login"] = True
- raise cherrypy.HTTPRedirect("/")
- return "Invalid username and/or password"
- @cherrypy.tools.auth()
- @cherrypy.expose
- def delete(self, name):
- try:
- if not self.storage.exists(name):
- return "File does not exist"
- self.storage.delete(name)
- return f"Deleted {name}"
- except InvalidPath:
- raise cherrypy.HTTPError(400, "Invalid path")
- except:
- raise cherrypy.HTTPError(500, "Cannot delete")
- @cherrypy.expose
- def download(self, name, password=""):
- hidden = os.path.basename(name).startswith(".")
- is_user = not cherrypy.session.get("login")
- if hidden and is_user:
- raise cherrypy.HTTPError(404, "No such file")
-
- if not name:
- raise cherrypy.HTTPError(418, "Hello! I am Teapotware :)")
- try:
- if not self.storage.exists(name):
- raise cherrypy.HTTPError(404, f"No file named {name}")
- except InvalidPath:
- raise cherrypy.HTTPError(400, "Invalid path was sent")
-
- file_password = storage.get_property(name, "password")
- if file_password and file_password != password:
- raise cherrypy.HTTPError(403, "Password mismatch")
- else:
- count = self.storage.get_property(name, "dcount") or 0
- self.storage.set_property(name, "dcount", count + 1)
- bname = os.path.basename(name)
- mime = self.getmime(bname.split(".")[-1])
- cherrypy.response.headers["Content-type"] = mime
- name_ = f"attachment; filename=\"{bname}\""
- cherrypy.response.headers["content-disposition"] = name_
- return cherrypy.lib.file_generator(self.storage.get(name))
- @cherrypy.tools.auth()
- @cherrypy.expose
- def upload(self, pwd="", desc="", name=""):
- if not name:
- name = str(random.randint(10000, 100000))
- try:
- p = self.storage._safe(name)
- except InvalidPath:
- raise cherrypy.HTTPError(400, "Invalid path")
- props = dict()
- props["password"] = pwd
- props["description"] = desc
- self.storage.add(name, cherrypy.request.body.fp, props)
- return f"Uploaded {name}"
-
- @cherrypy.tools.auth()
- @cherrypy.tools.json_out()
- @cherrypy.expose
- def counter(self):
- result = {}
- for file in self.storage.ls():
- print(file)
- count = self.storage.get_property(file, "dcount")
- if count:
- result[file] = count
- return result
-
- @cherrypy.tools.json_out()
- @cherrypy.expose
- def ls(self):
- files = dict()
- admin = cherrypy.session.get("login")
- for file in self.storage.ls():
- if not admin and file.startswith("."):
- continue
- props = dict()
- if not self.storage.isdir(file):
- props["size"] = self.storage.size(file)
- props["desc"] = self.storage.get_property(file, "description")
- if admin:
- props["pass"] = self.storage.get_property(file, "password")
- files[file] = props
- return files
- @cherrypy.expose
- def index(self):
- admin = cherrypy.session.get("login")
- basename = os.path.basename
- predicate = lambda f: admin or not basename(f).startswith(".")
- files = filter(predicate, self.storage.ls())
- table = []
- for file in files:
- if self.storage.isdir(file):
- continue
-
- pwd = self.storage.get_property(file, "password") or ""
- desc = self.storage.get_property(file, "description") or ""
- size = self.storage.size(file)
- file_ = file.split(".")
- ext = "" if len(file_) == 1 else file_[-1]
- icon, class_ = self.iconhandler.get_icon_and_class(ext)
- table.append({"name": file,
- "type_icon": icon,
- "type_class": class_,
- "desc": desc,
- "size": self.humanreadablesize(size),
- "admin": admin,
- "pass": pwd,
- "has_pass": bool(pwd)
- })
- totalsize = self.humanreadablesize(self.storage.total_size)
- return chevron.render(get_template("index"),
- {
- "files": table,
- "show_table": bool(table),
- "admin": admin,
- "totalsize": totalsize
- })
- if __name__ == "__main__":
- import configparser
- import sys
- def conv_SI_bytes(s):
- if s.isdigit():
- return int(s)
- s = s.upper()
- units = ("B", "K", "M", "G", "T")
- unit = s[-1]
- if unit not in units:
- raise ValueError("Unit MUST be either B,K,M,G,T")
- result = int(s[:-1])
- i = units.index(unit)
- return result * 1024 ** i
- admin = dict()
- config = configparser.ConfigParser()
- config_file = "server_config.ini"
- argv = sys.argv
- if "-h" in argv or (len(argv) > 1 and "-c" not in argv):
- print("Usage:", argv[0], "[-c <config_file>]", file=sys.stderr)
- sys.exit(0)
- if "-c" in argv:
- config_file = argv[argv.index("-c") + 1]
- config_file = os.path.abspath(config_file)
- if not os.path.exists(config_file):
- print("File does not exist:", config_file, file=sys.stderr)
- sys.exit(0)
- config.read(config_file)
- def_config = config["DEFAULT"]
- port = def_config.getint("port", 8085)
- host = def_config.get("host", "0.0.0.0")
- max_req_size = def_config.get("upload_max", "64G")
- unix_socket = def_config.get("unix_socket_file", None)
- posixtmp = "/tmp/"
- nttmp = os.path.expandvars("%systemdrive%/Windows/Temp/")
- def_base_dir = nttmp if os.name == "nt" else posixtmp
- base_dir = def_config.get("base_dir", def_base_dir)
- admin["username"] = def_config.get("admin_username", "admin")
- def_admin_pass_hash = hashlib.sha512(b"admin").hexdigest()
- def getpass_func():
- return def_config.get("admin_pass", def_admin_pass_hash)
- admin["getpass"] = getpass_func
- def setpass_func(new):
- def_config.update({"admin_pass": new})
- print(f"let's write to {config_file}")
- with open(config_file, "w") as fp:
- config.write(fp)
- admin["setpass"] = setpass_func
- debug = def_config.getboolean("debug", False)
- secure = def_config.getboolean("secure", False)
- db_dir = def_config.get("db_dir", os.path.join(base_dir, ".db"))
- max_req_size = conv_SI_bytes(max_req_size)
- conf = {"global":
- {
- "server.max_request_body_size": max_req_size,
- "server.socket_host": host,
- "server.socket_port": port,
- "server.socket_file": unix_socket,
- "tools.sessions.on": True,
- "tools.sessions.secure": secure,
- "tools.sessions.httponly": secure,
- "tools.staticdir.on": True,
- "tools.staticdir.dir": os.path.abspath("./webInterface/files/"),
- "tools.staticdir.root": "/",
- "environment": None if debug else "production"
- }
- }
- templates = dict()
- templates["index"] = os.path.realpath("./webInterface/index.mustache")
- templates["login"] = os.path.realpath("./webInterface/login.mustache")
- if not debug:
- for template, dir_ in templates.items():
- with open(dir_) as f:
- templates[template] = f.read()
- def get_template(template):
- if not debug:
- return templates[template]
- else:
- with open(templates[template]) as f:
- return f.read()
- valid_props = ("password", "description", "dcount")
- storage = Storage(base_dir, db_dir, valid_props)
- farcloud = FarCloud(admin, templates, storage)
- TimerKiller(cherrypy.engine, farcloud).subscribe()
- cherrypy.quickstart(farcloud, "/", conf)
|