babyweb 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
babyweb/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from .server import run_dez_webserver
2
+ from .util import succeed, succeed_sync, fail, redirect, local, cgi_get, cgi_dump, trysavedresponse, set_pre_close, send_file, send_text, send_xml, send_image, send_pdf
3
+ from .memcache import getmem, setmem, delmem, clearmem, getcache
4
+ from .requesters import fetch, post
5
+ from .response import read_file
6
+ from .controller import respond
7
+ from .version import __version__
babyweb/config.py ADDED
@@ -0,0 +1,54 @@
1
+ from fyg import Config, PCache
2
+
3
+ config = Config({
4
+ "cache": PCache(".tw"),
5
+ "encode": False,
6
+ "memcache": False,
7
+ "mempad": 0, # 0 = unset (uses dez's default)
8
+ "webs": {},
9
+ "log": {
10
+ "oflist": False,
11
+ "openfiles": False,
12
+ "tracemalloc": False,
13
+ "allow": ["info", "log", "warn", "error"] # access,info,log,warn,error,detail,db,query,kernel
14
+ },
15
+ "mail": {
16
+ "mailer": None,
17
+ "name": None,
18
+ "html": True,
19
+ "gmailer": False,
20
+ "verbose": False,
21
+ "scantick": 2
22
+ },
23
+ "web": {
24
+ "domain": "your.web.domain",
25
+ "host": "0.0.0.0",
26
+ "port": 8080,
27
+ "protocol": "http",
28
+ "xorigin": False,
29
+ "report": False,
30
+ "shield": False,
31
+ "debug": False,
32
+ "csp": None,
33
+ "log": None,
34
+ "errlog": None,
35
+ "rollz": {},
36
+ "eflags": [],
37
+ "blacklist": [],
38
+ "whitelist": []
39
+ },
40
+ "admin": {
41
+ "contacts": [],
42
+ "reportees": []
43
+ },
44
+ "ssl": {
45
+ "verify": True,
46
+ "certfile": None,
47
+ "keyfile": None,
48
+ "cacerts": None
49
+ },
50
+ "cron": {
51
+ "catchup": False
52
+ },
53
+ "scrambler": "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/=_"
54
+ })
babyweb/controller.py ADDED
@@ -0,0 +1,89 @@
1
+ import os, sys, platform
2
+ from dez.network import SocketController, daemon_wrapper
3
+ from .shield import setShield, writeBlacklist
4
+ from .logger import logger_getter
5
+ from .response import Response
6
+ from .daemons import webs
7
+ from .cron import Cron
8
+ from .version import __version__
9
+ from .util import do_respond, localvars
10
+ from .config import config
11
+
12
+ CTR = None
13
+
14
+ class Controller(SocketController):
15
+ def __init__(self, *args, **kwargs):
16
+ self.logger = logger_getter("Controller")
17
+ SocketController.__init__(self, *args, **kwargs)
18
+ self.handlers = {}
19
+ self.modules = {}
20
+ self.webs = {}
21
+ self.blcount = 0
22
+ self.blchunk = getattr(config.web.shield, "chunk", 5)
23
+ self.logger.info("babyweb: %s"%(__version__,))
24
+ self.logger.info("Python: %s"%(sys.version.split(' ')[0],))
25
+ self.logger.info("System: " + " > ".join([part for part in platform.uname() if part]))
26
+
27
+ def _respond(self, resp, *args, **kwargs):
28
+ if resp == "nothread": # "on start nothread" cron
29
+ kwargs["noLoad"] = True
30
+ elif resp: # regular request
31
+ kwargs["response"] = resp
32
+ localvars.response = resp
33
+ else: # regular cron
34
+ kwargs["noLoad"] = True
35
+ kwargs["threaded"] = True
36
+ do_respond(*args, **kwargs)
37
+
38
+ def register_handler(self, args, kwargs):
39
+ self.logger.info("register handler: %s"%(self.curpath,))
40
+ self.handlers[self.curpath] = lambda resp : self._respond(resp, *args, **kwargs)
41
+
42
+ def trigger_handler(self, rule, target, req=None, option=None):
43
+ self.curpath = rule
44
+ if rule not in self.handlers:
45
+ if target in self.modules:
46
+ self.logger.info("linking module: %s"%(target,))
47
+ self.handlers[rule] = self.modules[target]
48
+ else:
49
+ self.logger.info("importing module: %s"%(target,))
50
+ __import__(target)
51
+ self.modules[target] = self.handlers[rule]
52
+ self.handlers[rule](req and Response(req) or option)
53
+
54
+ def blup(self):
55
+ blen = writeBlacklist()
56
+ self.logger.warn("saved %s IPs in black.list"%(blen,))
57
+ if self.blcount != blen:
58
+ self.blcount = blen
59
+ if config.web.report and not blen % self.blchunk:
60
+ from .mail import email_admins
61
+ email_admins("sketch IPs blacklisted", "sketch count: %s"%(blen,))
62
+
63
+ def web(self, name, cfg, daemon, shield):
64
+ self.webs[name] = self.register_address(cfg.host,
65
+ cfg.port, dclass=daemon_wrapper(daemon, logger_getter, shield, config.mempad))
66
+ self.webs[name].controller = self
67
+
68
+ def getController():
69
+ global CTR
70
+ if not CTR:
71
+ # controller
72
+ CTR = Controller()
73
+ shield = setShield(CTR.blup)
74
+ mempad = config.mempad
75
+
76
+ for web in webs:
77
+ CTR.web(web, config.webs[web], webs[web], shield)
78
+
79
+ # cron
80
+ Cron(CTR, logger_getter)
81
+
82
+ return CTR
83
+
84
+ def dweb(wname="web"):
85
+ return getController().webs.get(wname)
86
+
87
+ def respond(*args, **kwargs):
88
+ getController().register_handler(args, kwargs)
89
+
babyweb/cron.py ADDED
@@ -0,0 +1,115 @@
1
+ import time, os
2
+ from datetime import datetime, timedelta
3
+ from fyg.util import read, write
4
+ from rel import timeout
5
+ from .config import config
6
+ from .util import do_respond
7
+
8
+ secsPerUnit = {
9
+ "days": 60 * 60 * 24,
10
+ "hours": 60 * 60,
11
+ "minutes": 60,
12
+ "mins": 60,
13
+ "seconds": 1
14
+ }
15
+
16
+ class Rule(object):
17
+ def __init__(self, controller, scheduler, url, rule, logger_getter):
18
+ self.logger = logger_getter("Rule(%s -> %s)"%(rule, url))
19
+ self.controller = controller
20
+ self.scheduler = scheduler
21
+ self.url = url
22
+ self.rule = rule
23
+ self.exact = len(rule) == 5 # 17:00
24
+ self.words = rule.split(" ")
25
+ self.timer = timeout(None, self.trigger)
26
+ self.parse()
27
+
28
+ def trigger(self):
29
+ self.logger.info("trigger: %s (%s)"%(self.url, getattr(self, "seconds", self.rule)))
30
+ self.controller.trigger_handler(self.url, self.url[1:],
31
+ option=self.rule.endswith("nothread") and "nothread")
32
+ if not self.exact and not self.rule.startswith("on start"):
33
+ self.scheduler.update(self)
34
+ return True
35
+
36
+ def start(self):
37
+ if self.rule.startswith("on start"):
38
+ return
39
+ self.logger.info("start (%s seconds)"%(self.seconds,))
40
+ self.timer.add(self.seconds)
41
+ if self.exact:
42
+ self.timer.delay = 60 * 60 * 24
43
+ else:
44
+ ff = self.scheduler.ff(self)
45
+ if ff:
46
+ self.logger.info("rescheduled for %s seconds"%(self.timer.delay - ff,))
47
+ self.timer.expiration -= ff
48
+
49
+ def parse(self):
50
+ self.logger.info("parse")
51
+ if self.exact:
52
+ hours, mins = self.rule.split(":")
53
+ n = datetime.now()
54
+ t = datetime(n.year, n.month, n.day, int(hours), int(mins))
55
+ self.seconds = (t - n).seconds
56
+ elif self.rule.startswith("on start"):
57
+ self.logger.info("triggering start script")
58
+ self.trigger()
59
+ elif len(self.words) == 3 and self.words[0] == "every": # every [NUMBER] [UNIT]
60
+ num = int(self.words[1])
61
+ unit = self.words[2]
62
+ self.seconds = num * secsPerUnit[unit]
63
+ else: # we can implement more later...
64
+ self.logger.error("can't parse: %s"%(self.rule,))
65
+
66
+ class Scheduler(object):
67
+ def __init__(self, logger_getter):
68
+ self.logger_getter = logger_getter
69
+ self.logger = logger_getter("Scheduler")
70
+ self.tsfile = os.path.join(os.path.abspath("."), "cron.ts")
71
+ self.lasts = read(self.tsfile, isjson=True, default={})
72
+ self.logger.info("initialized with %s timestamps"%(len(self.lasts.keys()),))
73
+ self.logger.info("saving state to timestamp file at %s"%(self.tsfile,))
74
+
75
+ def update(self, rule):
76
+ self.lasts[rule.url] = time.time()
77
+ self.logger.info("updating %s timestamp to %s"%(rule.url, self.lasts[rule.url]))
78
+ write(self.lasts, self.tsfile, isjson=True)
79
+
80
+ def ff(self, rule):
81
+ if rule.url in self.lasts:
82
+ ff = min(rule.seconds, time.time() - self.lasts[rule.url])
83
+ self.logger.info("fast-forwarding %s %s seconds"%(rule.url, ff))
84
+ return ff
85
+ elif config.cron.catchup:
86
+ self.logger.info("catching up %s"%(rule.url,))
87
+ return rule.seconds
88
+
89
+ class Cron(object):
90
+ def __init__(self, controller, logger_getter):
91
+ self.logger_getter = logger_getter
92
+ self.logger = logger_getter("Cron")
93
+ self.scheduler = Scheduler(logger_getter)
94
+ self.controller = controller
95
+ self.timers = {}
96
+ self.parse()
97
+ self.start()
98
+
99
+ def parse(self):
100
+ self.logger.info("parse")
101
+ url = None
102
+ for line in read("cron.yaml", True):
103
+ if line.startswith("- description: "):
104
+ self.logger.info("initializing %s"%(line[15:],))
105
+ elif line.startswith(" url: "):
106
+ url = line[7:].strip()
107
+ elif url:
108
+ self.timers[url] = Rule(self.controller, self.scheduler,
109
+ url, line[12:].strip(), self.logger_getter)
110
+ url = None
111
+
112
+ def start(self):
113
+ self.logger.info("start")
114
+ for rule in list(self.timers.values()):
115
+ rule.start()
babyweb/daemons.py ADDED
@@ -0,0 +1,45 @@
1
+ import sys
2
+ from dez.memcache import get_memcache
3
+ from dez.http.application import HTTPApplication
4
+ from .logger import logger_getter
5
+ from .routes import static, cb
6
+ from .config import config
7
+ sys.path.insert(0, ".") # for dynamically loading modules
8
+
9
+ class WebBase(HTTPApplication):
10
+ def __init__(self, bind_address, port, logger_getter, static=static, cb=cb, whitelist=[], blacklist=[], shield=False, mempad=0):
11
+ isprod = config.mode == "production"
12
+ HTTPApplication.__init__(self, bind_address, port, logger_getter, "dez/cantools",
13
+ config.ssl.certfile, config.ssl.keyfile, config.ssl.cacerts,
14
+ isprod, config.web.rollz, isprod, whitelist, blacklist, shield, mempad, config.web.xorigin)
15
+ self.memcache = get_memcache()
16
+ self.handlers = {}
17
+ for key, val in list(static.items()):
18
+ self.add_static_rule(key, val)
19
+ for key, val in list(cb.items()):
20
+ self.add_cb_rule(key, self._handler(key, val))
21
+
22
+ def _handler(self, rule, target):
23
+ self.logger.info("setting handler: %s %s"%(rule, target))
24
+ def h(req):
25
+ self.logger.info("triggering handler: %s %s"%(rule, target))
26
+ self.controller.trigger_handler(rule, target, req)
27
+ return h
28
+
29
+ class Web(WebBase):
30
+ def __init__(self, bind_address, port, logger_getter, shield, mempad):
31
+ self.logger = logger_getter("Web")
32
+ wcfg = config.web
33
+ WebBase.__init__(self, bind_address, port, logger_getter,
34
+ whitelist=wcfg.whitelist, blacklist=wcfg.blacklist, shield=shield, mempad=mempad)
35
+
36
+ webs = {}
37
+ def addWeb(name, daemon, cfg):
38
+ webs[name] = daemon
39
+ if config.webs[name]:
40
+ for key, val in cfg.items():
41
+ config.webs[name].update(key, val)
42
+ else:
43
+ config.webs.update(name, cfg)
44
+
45
+ addWeb("web", Web, config.web)
babyweb/logger.py ADDED
@@ -0,0 +1,48 @@
1
+ import tracemalloc, psutil, rel
2
+ from fyg.util import log as syslog
3
+ from dez.logging import get_logger_getter
4
+ from .config import config
5
+
6
+ logger_getter = get_logger_getter("httpd", syslog, config.log.allow)
7
+
8
+ def log(*args, **kwargs):
9
+ print(args, kwargs)
10
+
11
+ # setters (see above)
12
+ def setlog(f):
13
+ global log
14
+ log = f
15
+
16
+ TMSNAP = None
17
+
18
+ def log_tracemalloc():
19
+ global TMSNAP
20
+ snapshot = tracemalloc.take_snapshot()
21
+ log("[LINEMALLOC START]", important=True)
22
+ if TMSNAP:
23
+ lines = snapshot.compare_to(TMSNAP, 'lineno')
24
+ else:
25
+ lines = snapshot.statistics("lineno")
26
+ TMSNAP = snapshot
27
+ for line in lines[:10]:
28
+ log(line)
29
+ log("[LINEMALLOC END]", important=True)
30
+ return True
31
+
32
+ PROC = None
33
+
34
+ def log_openfiles():
35
+ global PROC
36
+ if not PROC:
37
+ PROC = psutil.Process(os.getpid())
38
+ ofz = PROC.open_files()
39
+ if config.log.oflist:
40
+ log("OPEN FILES: %s"%(ofz,), important=True)
41
+ else:
42
+ log("OPEN FILE COUNT: %s"%(len(ofz),), important=True)
43
+ return True
44
+
45
+ def log_kernel():
46
+ log(json.dumps(rel.report()), "kernel")
47
+ return True
48
+
babyweb/mail.py ADDED
@@ -0,0 +1,25 @@
1
+ from catmail import Mailer, Reader, Scanner
2
+ from catmail import config as catfyg
3
+ from .config import config
4
+
5
+ mfg = config.mail
6
+ catfyg.set({
7
+ "html": mfg.html,
8
+ "gmailer": mfg.gmailer,
9
+ "scantick": mfg.scantick,
10
+ "verbose": mfg.verbose,
11
+ "cache": config.cache
12
+ })
13
+ catfyg.admin.update("contacts", config.admin.contacts)
14
+ catfyg.admin.update("reportees", config.admin.reportees)
15
+
16
+ mailer = Mailer(mfg.mailer, mfg.name)
17
+ send_mail = mailer.mail
18
+ email_admins = mailer.admins
19
+ email_reportees = mailer.reportees
20
+
21
+ reader = Reader(config.mailer)
22
+ check_inbox = reader.inbox
23
+
24
+ scanner = Scanner(reader)
25
+ on_mail = scanner.on
babyweb/memcache.py ADDED
@@ -0,0 +1,25 @@
1
+ from .controller import dweb
2
+
3
+ def getmem(key, tojson=True):
4
+ return dweb().memcache.get(key, tojson)
5
+
6
+ def setmem(key, val, fromjson=True):
7
+ dweb().memcache.set(key, val, fromjson)
8
+
9
+ def delmem(key):
10
+ dweb().memcache.rm(key)
11
+
12
+ def clearmem():
13
+ dweb().memcache.clear()
14
+
15
+ def getcache():
16
+ c = {}
17
+ orig = dweb().memcache.cache
18
+ for k, v in list(orig.items()):
19
+ if v.__class__ == set:
20
+ v = list(v)
21
+ elif v.__class__ == dict and "ttl" in v: # countdown
22
+ v = str(v)
23
+ c[k] = v
24
+ return c
25
+
babyweb/requesters.py ADDED
@@ -0,0 +1,98 @@
1
+ import time, requests, json
2
+ from dez.http import fetch as dfetch, post as dpost
3
+ from fyg.util import log
4
+ from .util import rec_conv
5
+
6
+ def _ctjson(result):
7
+ if hasattr(result, "decode"):
8
+ result = result.decode()
9
+ code = result[0]
10
+ if code not in "0123":
11
+ log("response encoded:")
12
+ log(result)
13
+ log("attempting decode")
14
+ result = dec(result)
15
+ if code in "02":
16
+ log("request failed!! : %s"%(result,), important=True)
17
+ elif code == "3":
18
+ return rec_conv(json.loads(result[1:]), True)
19
+ else:
20
+ return json.loads(result[1:])
21
+
22
+ def parse_url_parts(host, path, port, protocol):
23
+ if "://" in host:
24
+ protocol, host = host.split("://", 1)
25
+ if "/" in host:
26
+ host, path = host.split("/", 1)
27
+ path = "/" + path
28
+ else:
29
+ path = "/"
30
+ if ":" in host:
31
+ host, port = host.split(":")
32
+ port = int(port)
33
+ elif not port:
34
+ port = protocol == "https" and 443 or 80
35
+ return host, path, port, protocol
36
+
37
+ def fetch(host, path="/", port=None, asjson=False, cb=None, timeout=1, asyn=False, protocol="http", ctjson=False, qsp=None, fakeua=False, retries=5):
38
+ host, path, port, protocol = parse_url_parts(host, path, port, protocol)
39
+ if qsp:
40
+ path += "?"
41
+ for k, v in list(qsp.items()):
42
+ path += "%s=%s&"%(k, v)
43
+ path = path[:-1]
44
+ gkwargs = {}
45
+ headers = {}
46
+ if fakeua:
47
+ if type(fakeua) is str:
48
+ headers['User-Agent'] = fakeua
49
+ else:
50
+ headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36'
51
+ gkwargs["headers"] = headers
52
+ if asyn or cb: # asyn w/o cb works, will just log
53
+ secure = protocol == "https"
54
+ if ctjson:
55
+ orig_cb = cb or log
56
+ cb = lambda v : orig_cb(_ctjson(v))
57
+ return dfetch(host, path, port, secure, headers, cb, timeout, asjson)
58
+ if timeout:
59
+ gkwargs["timeout"] = timeout
60
+ furl = "%s://%s:%s%s"%(protocol, host, port, path)
61
+ log("fetch %s"%(furl,))
62
+ return syncreq(furl, "get", asjson, ctjson, retries, gkwargs)
63
+
64
+ def post(host, path="/", port=80, data=None, protocol="http", asjson=False, ctjson=False, text=None, cb=None):
65
+ if ctjson:
66
+ data = rec_conv(data)
67
+ if cb:
68
+ if ctjson:
69
+ orig_cb = cb
70
+ cb = lambda v : orig_cb(_ctjson(v))
71
+ host, path, port, protocol = parse_url_parts(host, path, port, protocol)
72
+ return dpost(host, path, port, protocol == "https", data=data, text=text, cb=cb)
73
+ url = "://" in host and host or "%s://%s:%s%s"%(protocol, host, port, path)
74
+ log("post %s"%(url,))
75
+ kwargs = {}
76
+ if data:
77
+ kwargs["json"] = data
78
+ elif text:
79
+ kwargs["data"] = text
80
+ return syncreq(url, "post", asjson, ctjson, rekwargs=kwargs)
81
+
82
+ def _dosyncreq(requester, url, asjson, ctjson, rekwargs):
83
+ result = requester(url, **rekwargs).content
84
+ if ctjson:
85
+ return _ctjson(result)
86
+ return asjson and json.loads(result) or result
87
+
88
+ def syncreq(url, method="get", asjson=False, ctjson=False, retries=5, rekwargs={}):
89
+ attempt = 1
90
+ requester = getattr(requests, method)
91
+ while attempt < retries:
92
+ try:
93
+ return _dosyncreq(requester, url, asjson, ctjson, rekwargs)
94
+ except requests.exceptions.ConnectionError:
95
+ log("syncreq(%s %s) attempt #%s failed"%(method, url, attempt))
96
+ time.sleep(1)
97
+ attempt += 1
98
+ return _dosyncreq(requester, url, asjson, ctjson, rekwargs) # final try-less try
babyweb/response.py ADDED
@@ -0,0 +1,65 @@
1
+ import json
2
+ from rel import abort_branch, timeout
3
+ from dez.http.server import HTTPResponse
4
+ from .util import rec_conv, set_read, set_send, set_redir, set_header, set_close
5
+ from .config import config
6
+
7
+ files = {}
8
+ def read_file(data_field):
9
+ return files.pop(data_field)
10
+
11
+ class Response(object):
12
+ def __init__(self, request):
13
+ self.id = request.id
14
+ self.ip = request.real_ip
15
+ self.request = request
16
+ self.response = HTTPResponse(request)
17
+
18
+ def _read(self):
19
+ b = self.request.body or json.dumps(rec_conv(self.request.qs_params))
20
+ # print(b)
21
+ ctype = self.request.headers.get("content-type")
22
+ if ctype and ctype.startswith("multipart/form-data"):
23
+ if type(b) != bytes:
24
+ b = b.encode()
25
+ obj = {}
26
+ splitter, body = b.rsplit(b"\r\n", 2)[0].split(b"\r\n", 1)
27
+ for chunk in body.split(b"\r\n%s\r\n"%(splitter,)):
28
+ nameline, data = chunk.split(b"\r\n\r\n", 1)
29
+ nameline = nameline.decode()
30
+ name = nameline.split("; filename=")[0][:-1].split('name="')[1]
31
+ if "filename=" in nameline:
32
+ signature = "%s%s"%(self.id, name)
33
+ files[signature] = data
34
+ obj[name] = signature
35
+ else:
36
+ obj[name] = data
37
+ b = json.dumps(rec_conv(obj))
38
+ return b
39
+
40
+ def _send(self, *args, **kwargs):
41
+ wcfg = config.web
42
+ if wcfg.xorigin:
43
+ self.response.headers["Access-Control-Allow-Origin"] = "*"
44
+ if wcfg.csp:
45
+ if wcfg.csp.startswith("auto"):
46
+ d = wcfg.domain
47
+ if wcfg.csp != "autosub":
48
+ d = ".".join(d.split(".")[-2:])
49
+ wcfg.update("csp", "default-src 'self' %s *.%s"%(d, d))
50
+ self.response.headers["Content-Security-Policy"] = wcfg.csp
51
+ self.response.write(*args, **kwargs)
52
+
53
+ def _close(self):
54
+ timeout(.001, self.response.dispatch)
55
+ abort_branch()
56
+
57
+ def _header(self, *args, **kwargs):
58
+ self.response.__setitem__(*args, **kwargs)
59
+
60
+ def set_cbs(self):
61
+ set_read(self._read)
62
+ set_header(self._header)
63
+ set_send(self._send)
64
+ set_close(self._close)
65
+ set_redir(self.response.redirect)
babyweb/routes.py ADDED
@@ -0,0 +1,21 @@
1
+ from fyg.util import read
2
+
3
+ url = None
4
+ static = {}
5
+ cb = {}
6
+
7
+ for line in read("app.yaml", True):
8
+ if line.startswith("- url: "):
9
+ url = line[7:].strip()
10
+ elif url:
11
+ if line.startswith(" static_dir: "):
12
+ target = line[14:].strip()
13
+ if "*" in url: # bad regex detection...
14
+ static[url] = target
15
+ else:
16
+ url = url.rstrip("/")
17
+ static[url] = static[url + "/"] = target
18
+ url = None
19
+ elif line.startswith(" script: "):
20
+ cb[url] = line[10:].split(".")[0]
21
+ url = None
babyweb/server.py ADDED
@@ -0,0 +1,47 @@
1
+ import os, time, requests, rel, ssl, sys, json, resource, tracemalloc, psutil
2
+ from fyg.util import log, set_log, set_error
3
+ from .logger import setlog, logger_getter, log_tracemalloc, log_openfiles, log_kernel
4
+ from .controller import getController
5
+ from .util import fail
6
+ from .config import config
7
+
8
+ def fdup():
9
+ log("checking the number of file descriptors allowed on your system", important=True)
10
+ soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
11
+ half = int(hard / 2)
12
+ log("soft: %s. hard: %s. half: %s"%(soft, hard, half))
13
+ if soft < half:
14
+ log("increasing soft limit to half of hard limit")
15
+ resource.setrlimit(resource.RLIMIT_NOFILE, (half, hard))
16
+ log("new limits! soft: %s. hard: %s"%resource.getrlimit(resource.RLIMIT_NOFILE))
17
+
18
+ def quit():
19
+ if config.errlog:
20
+ log("closing error log", important=True)
21
+ sys.stderr.close()
22
+ log("quitting - goodbye!", important=True)
23
+
24
+ def run_dez_webserver():
25
+ if not config.ssl.verify and hasattr(ssl, "_https_verify_certificates"):
26
+ ssl._https_verify_certificates(False)
27
+ c = getController()
28
+ setlog(c.webs["web"].logger.simple)
29
+ if config.web.log:
30
+ set_log(os.path.join("logs", config.web.log))
31
+ clog = config.log
32
+ if clog.openfiles:
33
+ rel.timeout(clog.openfiles, log_openfiles)
34
+ if clog.tracemalloc:
35
+ tracemalloc.start()
36
+ rel.timeout(clog.tracemalloc, log_tracemalloc)
37
+ if "kernel" in clog.allow:
38
+ rel.timeout(1, log_kernel)
39
+ set_error(fail)
40
+ if config.fdup:
41
+ fdup()
42
+ if config.web.errlog:
43
+ sys.stderr = open(os.path.join("logs", config.web.errlog), "a")
44
+ c.start(quit)
45
+
46
+ if __name__ == "__main__":
47
+ run_dez_webserver()
babyweb/shield.py ADDED
@@ -0,0 +1,53 @@
1
+ import os
2
+ from dez.http.server.shield import Shield
3
+ from fyg.util import read, write
4
+ from .logger import logger_getter
5
+ from .config import config
6
+
7
+ class PaperShield(object):
8
+ def __init__(self):
9
+ self.logger = logger_getter("PaperShield")
10
+ self.default = { "reason": "I'm just a paper shield!" }
11
+ self.ips = {}
12
+
13
+ def __call__(self, path, ip, fspath=False, count=True):
14
+ self.logger.access('NOOP > paperShield("%s", "%s", fspath=%s, count=%s)'%(path, ip, fspath, count))
15
+
16
+ def suss(self, ip, reason):
17
+ self.logger.access("suss(%s) -> %s"%(ip, reason))
18
+ self.ips[ip] = { "reason": reason }
19
+
20
+ def ip(self, ip):
21
+ return self.ips.get(ip, self.default)
22
+
23
+ def setShield(blup=None):
24
+ shield = None
25
+ shfg = config.web.shield
26
+ if shfg: # web/admin share shield and blacklist
27
+ setBlacklist()
28
+ shield = Shield(config.web.blacklist, logger_getter, blup or writeBlacklist,
29
+ getattr(shfg, "limit", 400),
30
+ getattr(shfg, "interval", 2))
31
+ config.web.update("shield", shield or PaperShield())
32
+ return shield
33
+
34
+ def setBlacklist():
35
+ bl = {}
36
+ for preban in config.web.blacklist:
37
+ bl[preban] = "config ban"
38
+ if os.path.isfile("black.list"):
39
+ try:
40
+ bsaved = read("black.list", isjson=True)
41
+ except: # old style
42
+ bsaved = {}
43
+ for line in read("black.list", lines=True):
44
+ bsaved[line.strip()] = "legacy ban"
45
+ bsaved and bl.update(bsaved)
46
+ config.web.update("blacklist", bl)
47
+
48
+ def writeBlacklist():
49
+ wcfg = config.web
50
+ bl = wcfg.blacklist.obj()
51
+ write(bl, "black.list", isjson=True)
52
+ wcfg.blacklister and wcfg.blacklister.update(bl)
53
+ return len(bl.keys())
babyweb/sms.py ADDED
@@ -0,0 +1,11 @@
1
+ from .mail import send_mail
2
+
3
+ carriers = {
4
+ 'at&t': 'mms.att.net',
5
+ 'verizon': 'vtext.com',
6
+ 'tmobile': 'tmomail.net',
7
+ 'sprint': 'page.nextel.com'
8
+ }
9
+
10
+ def send_sms(number, subject="hello", body="goodbye", carrier="at&t"):
11
+ send_mail(to="%s@%s"%(number, carriers[carrier]), subject=subject, body=body)
@@ -0,0 +1,4 @@
1
+ from .setters import local, localvars, set_read, set_send, set_redir, set_header, set_close
2
+ from .converters import rec_conv, rdec, renc, setenc, setdec
3
+ from .responders import set_pre_close, do_respond, succeed, succeed_sync, fail, redirect, trysavedresponse, send_file, send_text, send_xml, send_image, send_pdf
4
+ from .loaders import cgi_get, cgi_dump
@@ -0,0 +1,69 @@
1
+ import json
2
+ from urllib.parse import quote, unquote
3
+ from base64 import b64encode, b64decode
4
+ from six import string_types
5
+ from ..config import config
6
+
7
+ _c = config.scrambler
8
+ _cl = len(_c)
9
+ _chl = int(_cl / 2)
10
+
11
+ def flip(c):
12
+ i = _c.find(c)
13
+ if i == -1:
14
+ return c
15
+ return _c[(i + _chl) % _cl]
16
+
17
+ def scramble(s):
18
+ return "".join([flip(c) for c in s])
19
+
20
+ def enc(data):
21
+ return scramble(b64encode(hasattr(data, "encode") and data.encode() or data).decode())
22
+
23
+ def dec(data):
24
+ return data.startswith("{") and data or b64decode(scramble(data)).decode()
25
+
26
+ def setenc(f):
27
+ global enc
28
+ enc = f
29
+
30
+ def setdec(f):
31
+ global dec
32
+ dec = f
33
+
34
+ def rdec(data):
35
+ return unquote(b64decode(data.encode()).decode())
36
+
37
+ def renc(data):
38
+ return b64encode(quote(data).encode()).decode()
39
+
40
+ def rec_conv(data, de=False):
41
+ if isinstance(data, bytes):
42
+ try:
43
+ data = data.decode()
44
+ except:
45
+ pass
46
+ if isinstance(data, string_types):
47
+ return (de and rdec or renc)(data)
48
+ elif isinstance(data, dict):
49
+ for k, v in list(data.items()):
50
+ data[k] = rec_conv(v, de)
51
+ elif isinstance(data, list):
52
+ return [rec_conv(d, de) for d in data]
53
+ return data
54
+
55
+ def processResponse(data, code):
56
+ if code == "1":
57
+ try:
58
+ data = json.dumps(data)
59
+ except:
60
+ data = json.dumps(rec_conv(data))
61
+ code = "3"
62
+ elif code == "0":
63
+ try:
64
+ json.dumps(data)
65
+ except:
66
+ data = rec_conv(data)
67
+ code = "2"
68
+ return "%s%s"%(code, data)
69
+
@@ -0,0 +1,56 @@
1
+ import json, cgi, ast, sys
2
+ from base64 import b64decode
3
+ from urllib.parse import unquote
4
+ from .setters import local, localvars
5
+ from .converters import dec, rec_conv
6
+ from ..config import config
7
+
8
+ def qs_get(x, y):
9
+ val = localvars.request.getvalue(x, y)
10
+ if val:
11
+ val = unquote(val)
12
+ return val
13
+
14
+ def cgi_read():
15
+ return local("read", sys.stdin.read)()
16
+
17
+ def cgi_dump():
18
+ return local("request_string")
19
+
20
+ def cgi_load():
21
+ localvars.request_string = cgi_read()
22
+ data = config.encode and dec(localvars.request_string) or localvars.request_string
23
+ try:
24
+ try:
25
+ jdata = json.loads(data)
26
+ except:
27
+ jdata = ast.literal_eval(data)
28
+ try:
29
+ localvars.request = rec_conv(jdata, True)
30
+ except:
31
+ localvars.request = jdata
32
+ except:
33
+ localvars.request = cgi.FieldStorage()
34
+ setattr(localvars.request, "get", qs_get)
35
+ if not localvars.request:
36
+ localvars.request = {}
37
+
38
+ def cgi_get(key, choices=None, required=True, default=None, shield=False, decode=False, base64=False):
39
+ from .responders import fail
40
+ request = local("request")
41
+ val = request.get(key, default)
42
+ if val is None:
43
+ required and fail('no value submitted for required field: "%s" [%s]'%(key, request))
44
+ elif shield:
45
+ ip = local("ip")
46
+ shield = config.web.shield
47
+ if shield(val, ip, fspath=True, count=False):
48
+ log('cgi_get() shield bounced "%s" for "%s"'%(ip, shield.ip(ip)["message"]))
49
+ fail()
50
+ if choices and val not in choices:
51
+ fail('invalid value for "%s": "%s"'%(key, val))
52
+ if base64 and val:
53
+ val = b64decode(unquote(val))
54
+ if decode and val:
55
+ val = unquote(val)
56
+ return val
@@ -0,0 +1,199 @@
1
+ import sys, rel, traceback
2
+ from urllib.parse import unquote
3
+ from fyg.util import basiclog as log
4
+ from .converters import enc, dec, rec_conv, processResponse
5
+ from .setters import local
6
+ from .loaders import cgi_load
7
+ from ..config import config
8
+
9
+ def _send(data):
10
+ send = local("send")
11
+ if send:
12
+ send(data)
13
+ else:
14
+ print(data)
15
+
16
+ def _close():
17
+ local("close", sys.exit)()
18
+
19
+ def _pre_close():
20
+ pass
21
+
22
+ def set_pre_close(f):
23
+ global _pre_close
24
+ _pre_close = f
25
+
26
+ def _env(html):
27
+ return "%s"
28
+
29
+ def set_env(f):
30
+ global _env
31
+ _env = f
32
+
33
+ def _header(hkey, hval):
34
+ header = local("header")
35
+ if header:
36
+ header(hkey, hval)
37
+ else:
38
+ _send("%s: %s"%(hkey, hval))
39
+
40
+ def _headers(headers):
41
+ for k, v in list(headers.items()):
42
+ _header(k, v)
43
+ if config.web.server == "gae":
44
+ _send("")
45
+
46
+ def _write(data, exit=True, savename=None):
47
+ if savename:
48
+ from ..memcache import setmem
49
+ setmem(savename, data, False)
50
+ _send(data)
51
+ if exit:
52
+ _pre_close()
53
+ _close()
54
+
55
+ FILETYPES = {"pdf": "application/pdf", "img": "image/png", "ico": "image/ico", "html": "text/html"}
56
+
57
+ def send_pdf(data, title=None):
58
+ if title:
59
+ _headers({
60
+ "Content-Type": 'application/pdf; name="%s.pdf"'%(title,),
61
+ "Content-Disposition": 'attachment; filename="%s.pdf"'%(title,)
62
+ })
63
+ else:
64
+ _headers({"Content-Type": "application/pdf"})
65
+ _send(data)
66
+ _close()
67
+
68
+ def send_image(data):
69
+ _headers({"Content-Type": "image/png"})
70
+ _send(data)
71
+ _close()
72
+
73
+ def send_file(data, file_type=None, detect=False, headers={}):
74
+ if detect:
75
+ import magic
76
+ file_type = data and magic.from_buffer(data, True)
77
+ if file_type:
78
+ headers["Content-Type"] = FILETYPES.get(file_type, file_type)
79
+ _headers(headers)
80
+ _send(data)
81
+ _close()
82
+
83
+ def send_text(data, dtype="html", fname=None, exit=True, headers={}):
84
+ headers["Content-Type"] = "text/%s"%(dtype,)
85
+ if fname:
86
+ headers['Content-Disposition'] = 'attachment; filename="%s.%s"'%(fname, dtype)
87
+ _headers(headers)
88
+ _write(data, exit)
89
+
90
+ def send_xml(data):
91
+ send_text(data, "xml")
92
+
93
+ def trysavedresponse(key=None):
94
+ from ..memcache import getmem
95
+ key = key or local("request_string")
96
+ response = getmem(key, False)
97
+ response and _write(response, exit=True)
98
+
99
+ def redirect(addr, msg="", noscript=False, exit=True, metas=None):
100
+ a = "<script>"
101
+ if msg:
102
+ a += 'alert("%s"); '%(msg,)
103
+ a += "document.location = '%s';</script>"%(addr,)
104
+ if noscript:
105
+ a += '<noscript>This site requires Javascript to function properly. To enable Javascript in your browser, please follow <a href="http://www.google.com/support/bin/answer.py?answer=23852">these instructions</a>. Thank you, and have a nice day.</noscript>'
106
+ if metas:
107
+ a = "<html><head>%s%s</head><body></body></html>"%(metas, a)
108
+ _header("Content-Type", "text/html")
109
+ _write(_env(True)%(a,), exit)
110
+
111
+ def resp_wrap(resp, failure):
112
+ def f():
113
+ try:
114
+ resp()
115
+ except rel.errors.AbortBranch as e:
116
+ _pre_close()
117
+ raise rel.errors.AbortBranch() # handled in rel
118
+ except SystemExit:
119
+ pass
120
+ except Exception as e:
121
+ failure(e)
122
+ return f
123
+
124
+ def succeed_sync(func, cb):
125
+ d = {}
126
+ def handle(*a, **k):
127
+ d["a"] = a
128
+ d["k"] = k
129
+ func(handle)
130
+ while True:
131
+ time.sleep(0.01)
132
+ if d["a"] or d["k"]:
133
+ succeed(cb(*d["a"], **d["k"]))
134
+
135
+ def succeed(data="", html=False, noenc=False, savename=None, cache=False):
136
+ if cache or config.memcache:
137
+ savename = local("request_string")
138
+ _header("Content-Type", "text/%s"%(html and "html" or "plain"))
139
+ draw = processResponse(data, "1")
140
+ dstring = (config.encode and not noenc) and enc(draw) or draw
141
+ _write(_env(html)%(dstring,), savename=savename)
142
+
143
+ def fail(data="failed", html=False, err=None, noenc=False, exit=True):
144
+ if err:
145
+ # log it
146
+ logdata = "%s --- %s --> %s"%(data, repr(err), traceback.format_exc())
147
+ log("error:", logdata)
148
+ if config.web.debug:
149
+ # write it
150
+ data = logdata
151
+ resp = local("response")
152
+ reqstring = local("request_string")
153
+ path = resp and resp.request.url or "can't find path!"
154
+ ip = local("ip") or (resp and resp.ip or "can't find ip!")
155
+ edump = "%s\n\n%s\n\n%s\n\n%s"%(path, ip, reqstring, logdata)
156
+ shield = config.web.shield
157
+ if reqstring and shield(reqstring, ip):
158
+ data = "nabra"
159
+ reason = shield.ip(ip)["message"]
160
+ logline = "%s - IP (%s) banned!"%(reason, ip)
161
+ edump = "%s\n\n%s"%(logline, edump)
162
+ log(logline)
163
+ elif config.web.eflags:
164
+ samples = {
165
+ "traceback": logdata
166
+ }
167
+ if reqstring:
168
+ samples["request"] = reqstring
169
+ for sample in samples:
170
+ for ef in config.web.eflags:
171
+ if ef in samples[sample]:
172
+ reason = '"%s" in %s'%(ef, sample)
173
+ logline = "%s - IP (%s) banned!"%(reason, ip)
174
+ edump = "%s\n\n%s"%(logline, edump)
175
+ shield.suss(ip, reason)
176
+ log(logline)
177
+ if config.web.report:
178
+ from .mail import email_admins
179
+ email_admins("error encountered", edump)
180
+ _header("Content-Type", "text/%s"%(html and "html" or "plain"))
181
+ draw = processResponse(data, "0")
182
+ dstring = (config.encode and not noenc) and enc(draw) or draw
183
+ _write(_env(html)%(dstring,), exit)
184
+
185
+ def do_respond(responseFunc, failMsg="failed", failHtml=False, failNoEnc=False, noLoad=False, threaded=False, response=None, autowin=True):
186
+ def resp():
187
+ response and response.set_cbs()
188
+ noLoad or cgi_load()
189
+ responseFunc()
190
+ autowin and succeed()
191
+
192
+ def failure(e):
193
+ fail(data=failMsg, html=failHtml, err=e, noenc=failNoEnc)
194
+
195
+ wrapped_response = resp_wrap(resp, failure)
196
+ if threaded:
197
+ rel.thread(wrapped_response)
198
+ else:
199
+ wrapped_response()
@@ -0,0 +1,20 @@
1
+ import threading
2
+
3
+ localvars = threading.local()
4
+ def local(key, fallback=None):
5
+ return getattr(localvars, key, fallback)
6
+
7
+ def set_read(f):
8
+ localvars.read = f
9
+
10
+ def set_send(f):
11
+ localvars.send = f
12
+
13
+ def set_redir(f):
14
+ localvars.redir = f
15
+
16
+ def set_header(f):
17
+ localvars.header = f
18
+
19
+ def set_close(f):
20
+ localvars.close = f
babyweb/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 bubbleboy14
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: babyweb
3
+ Version: 0.1.0
4
+ Summary: Basic Asynchronous weB librarY
5
+ Author: Mario Balibrera
6
+ Author-email: mario.balibrera@gmail.com
7
+ License: MIT License
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ License-File: LICENSE
16
+ Requires-Dist: dez >=0.10.10.42
17
+ Requires-Dist: fyg >=0.1.7.6
18
+
19
+ repackages async dez components like HTTPApplication and SocketController into a minimalist config-driven web (backend) framework
@@ -0,0 +1,25 @@
1
+ babyweb/__init__.py,sha256=ePOo3Lx3DdhdOrDMae2PHhOLr8SqNh81c2wN0h5JRNQ,404
2
+ babyweb/config.py,sha256=eCvjUDqgRxOg8c8puKdf1rSrJFL87GwZVX3dclSF_ic,1025
3
+ babyweb/controller.py,sha256=zuP_h0y9o2G98QVp_DqtgPDPOJIzGARizQzG5Aozv-g,2734
4
+ babyweb/cron.py,sha256=wQTtY3ZJBeTjvQFbMDmR83c-aQjfEmulzeBWKUob55E,4243
5
+ babyweb/daemons.py,sha256=4rPZyIdjBbYQ4pwSY_W2vkFY3KfvpvBEfEivkwTlE5Y,1840
6
+ babyweb/logger.py,sha256=R8X2gci31CzLx-eWSiCdm8P4RqAYGT0EU683Y3qZf34,1021
7
+ babyweb/mail.py,sha256=A7FL99zDU9Y_LAZPpw9-LWOsxuc1eoiGMN1EYVrwOIo,607
8
+ babyweb/memcache.py,sha256=W6JWcBjJPZx8RPYQXYRbCWMl-ziwy9OtzCvxV9RlCXs,484
9
+ babyweb/requesters.py,sha256=ZrnhYbYkjM7Z0JrwwRrxI5dt7pW9g2Y4EokDtD6A2NY,3197
10
+ babyweb/response.py,sha256=_2hY9ib39MG59nvLDKD37pNCFF9ENuydegBQsd0KV18,2328
11
+ babyweb/routes.py,sha256=V9EQKjWqbwWrR1t4Ren9IGAcmcS5awYCIHdseWoEqVU,483
12
+ babyweb/server.py,sha256=iK0bORQGrO2sZxBcSDl9_ACNGlBVpuKh9XR-TwigGWI,1579
13
+ babyweb/shield.py,sha256=uB4vDKf6jKwWIebAjitm_q9T86iKPRKPNNeKcPgLfrE,1551
14
+ babyweb/sms.py,sha256=lYQFgpFjAas1XIolE0QC4ro2wgXH7hruzV1Dxz72-sE,299
15
+ babyweb/version.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
16
+ babyweb/util/__init__.py,sha256=zia5MMGy8m3KR6HrZ5b3l9lE3kubg4p7r0sr9vAMoPA,352
17
+ babyweb/util/converters.py,sha256=tvAppsy79lrhkOi3E2LkdBpEJ__j0H22RSiKJHssMGs,1579
18
+ babyweb/util/loaders.py,sha256=kMNxQ-hN-oJVgrOU8TQU8LSqp6B3wMoUKOpgS1D5R6Y,1752
19
+ babyweb/util/responders.py,sha256=A8RF3AR32H9z3YAEFCTmIQQuQ_yUoSub7u0Txnv8YYk,6194
20
+ babyweb/util/setters.py,sha256=xIHtdLQgdlyCUC83q3ZW8ab6U5Rj3UU6ZgK70b-TOg4,337
21
+ babyweb-0.1.0.dist-info/LICENSE,sha256=TCGFq2RZnPZed3cKBhfq8EIxNzHcwJWJsBW8p9y1dDk,1068
22
+ babyweb-0.1.0.dist-info/METADATA,sha256=zItkGAOsx5Z4dfMaX8wvI-Y6w7KCtAVQBPXFFl4eqfI,727
23
+ babyweb-0.1.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
24
+ babyweb-0.1.0.dist-info/top_level.txt,sha256=-KcZVMe9-tSS5gwyDZGyLvU5bemV6aWTSofb0B37Mts,8
25
+ babyweb-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ babyweb