def getSesh (strict=True, validateCsrf=None, req=None): "Get sesh (session-like) object with current 'user' property."; req = req or bu.request; # Defaults to (global) bu.request. if validateCsrf is None: validateCsrf = bool(req.method != "GET"); # Default behavior: false for GET, else true. # Check the 'userId' cookie: userId = bu.getCookie("userId", strict=strict, secret=K.AUTH_COOKIE_SECRET, req=req, ); if not userId: assert not strict; return dotsi.fy({"user": None}); # ==> Cookie found, signature valid. assert userId; if validateCsrf: # Ref: https://laravel.com/docs/5.8/csrf#csrf-x-csrf-token xCsrfToken = req.headers.get("X-Csrf-Token"); assert validateXCsrfToken(xCsrfToken, userId); # ==> CSRF TOKEN IS VALID. # ==> CSRF PREVENTED, if applicable. user = userMod.getUser(userId); assert user and user.isVerified; # User shouldn't be able to log-in if not .isVerified. Asserting here. if user.isDeactivated: # XXX:Note: Below 'log out' should force CLI logout. return bu.abort("ACCOUNT DEACTIVATED\n\n" + "Your account has been deactivated by your admin." + "You shall now proceed to log out." #+ ); # ==> User exists, is verified, non-deactivated. return dotsi.fy({"user": user});
def buildRoute(verb, path, fn, mode=None, name=None): verb = [verb] if type(verb) is str else verb; mode = detectRouteMode(path) if not mode else mode; assert mode in ["re", "wildcard", "exact"]; if mode == "wildcard": assert validateWildcardPath(path); return dotsi.fy({ "verb": verb, "path": path, "fn": fn, "mode": mode, "name": name, });
def buildDojo(title, creatorId): assert K.CURRENT_DOJO_V == 0 return dotsi.fy({ "_id": utils.objectId(), "_v": K.CURRENT_DOJO_V, # # Intro'd in _v0: "title": title, "scratchpad": "", "creatorId": creatorId, "createdAt": utils.now(), })
def buildCategory (creatorId, name="", rank=0, parentId=""): assert K.CURRENT_CATEGORY_V == 0; return dotsi.fy({ "_id": utils.objectId(), "_v": K.CURRENT_CATEGORY_V, # # Intro'd in _v0: # "name": name, "rank": rank, "parentId": parentId, "creatorId": creatorId, "createdAt": utils.now(), });
def fullStepAdapter(fooX): # Prelims: assert fooX._v == fromV fooY = dotsi.fy(utils.deepCopy(fooX)) # Non-alias copy, ensuring incoming `fooX` remains untouched. # Core: stepAdapterCore(fooY) # Make changes to fooY. It's already a non-alias copy, safe to work with. # Common: assert fooY._v == fromV # After core logic, ._v should still remain unchanged. We change it next. fooY._v += 1 assert fooY._v == toV return fooY
def test_wildcardMatch(): f = lambda w, a: vilo.checkWildcardMatch(w, a, dotsi.fy({})) assert f("/*", "/foo") is True assert f("/*", "/") is False assert f("/foo/*", "/foo/bar") is True assert f("/foo/*", "/foo/bar/baz") is False assert f("/s/**", "/s/foo") is True assert f("/s/**", "/s/foo/bar") is True assert f("/s/**", "/s/") is False assert f("/*/do", "/foo/do") is True assert f("/*/do", "/foo/bar/do") is False assert f("/*/do", "//do") is False assert f("/*/do/**", "/x/do/y") is True assert f("/*/do/**", "/x/do/y/z") is True assert f("/*/do/**", "/x/do/") is False assert f("/*/do/**", "//do/y/z") is False
def _signUnwrap (signWrappedDataStrQ, secret, maxExpiryInDays=30): # Note: maxExpiryInDays=30 can be changed w/ each call. "Unwraps and reads signWrappedDataStr, using secret."; signWrappedDataStr = utils.unquote(signWrappedDataStrQ); signWrappedData = json.loads(signWrappedDataStr); # Failure would raise json.decoder.JSONDecodeError # Unwrapping: data = signWrappedData["data"]; # Failure would raise KeyError msTs = signWrappedData["msTs"]; sig = signWrappedData["sig"]; # Validate signature: assert checkDetachedSign(sig, data, msTs, secret); # Failure would raise AssertionError # ==> SIGNATURE FORMAT OK. msSinceSigned = ms_now() - msTs; #print("msSinceSigned = ", msSinceSigned); daysSinceSigned = ms_delta_toDays(msSinceSigned); #print("daysSinceSigned = ", daysSinceSigned); assert daysSinceSigned <= maxExpiryInDays; # Failure would raise AssertionError # ==> SIGNATURE EXPIRY OK. return dotsi.fy(data) if type(data) is dict else data;
def adapt(foo, shouldUpdateDb=True): assert K[str_CURRENT_FOO_V] == int_CURRENT_FOO_V foo = dotsi.fy(foo) preV = foo._v # Previous (pre-adapting) version. itrV = preV # Incrementing loop variable. while (itrV < K[str_CURRENT_FOO_V]): stepAdapter = stepAdapterList[itrV] foo = stepAdapter(foo) assert foo._v == itrV + 1 itrV = foo._v # Update (increment) loop var. assert itrV == K[str_CURRENT_FOO_V] assert func_validateFoo(foo) if shouldUpdateDb and (preV < K[str_CURRENT_FOO_V]): # ==> The (previous) foo was adapted. So, update db: dbOut = db.fooBox.replace_one({"_id": foo._id}, foo) # Remember that db.fooBox --points-to--> mongo.db[str_fooBox]; assert dbOut.matched_count == 1 == dbOut.modified_count return foo
def buildUser( email, fname, lname="", pw="", isRootAdmin=False, isVerified=False, inviterId="", veriCode=None, accessLevel=K.USER_ACCESS_LEVEL_LIST[0], ): assert K.CURRENT_USER_V == 1 assert fname and email assert type(fname) == type(email) == str and "@" in email userId = utils.objectId() hpw = utils.hashPw(pw) if pw else "" veriCode = veriCode or genVeriCode() return dotsi.fy({ "_id": userId, "_v": K.CURRENT_USER_V, # # Intro'd in _v0: # "fname": fname, "lname": lname, "email": email, "hpw": hpw, "createdAt": utils.now(), "isVerified": isVerified, "hVeriCode": utils.hashPw(veriCode), "inviterId": inviterId, "isDeactivated": False, "hResetPw": "", "resetPwExpiresAt": 0, "isRootAdmin": isRootAdmin, # # Intro'd in _v1: # "accessLevel": accessLevel, })
def buildArticle(creatorId, title=""): assert K.CURRENT_ARTICLE_V == 2 return dotsi.fy({ "_id": utils.objectId(), "_v": K.CURRENT_ARTICLE_V, # # Intro'd in _v0: # "title": title, #"scratchpad": "", -- Renamed in _v1 to 'body' "creatorId": creatorId, "createdAt": utils.now(), # # Intro'd in _v1: # "body": "", "categoryId": "", # blank => Untitled, topmost category. #"sectionId": "", -- Removed in _v2 # # Intro'd in _v2: # "status": "draft", })
def buildApp (): "Builds an empty (i.e. routeless) app-container."; app = dotsi.fy({}); app.routeList = []; app.pluginList = []; # Route Adding: :::::::::::::::::::::::::::::::::::::::: def findNamedRoute (name): "Returns route named `name`, else None."; if name is None: return None; rtList = filterli(app.routeList, lambda rt: rt.name == name); assert len(rtList) <= 1; return rtList[0] if rtList else None; app.findNamedRoute = findNamedRoute; def addRoute (verb, path, fn, mode=None, name=None, top=False): "Add a route handler `fn` against `path`, for `verb`."; assert type(top) is bool; if findNamedRoute(name): raise ValueError("Route with name %r already exists." % name); index = 0 if top else len(app.routeList); route = buildRoute(verb, path, fn, mode, name); app.routeList.insert(index, route); app.addRoute = addRoute; def mkRouteDeco (verb, path, mode=None, name=None, top=False): "Makes a decorator for adding routes."; # TODO: Write documentation for param `top`. # TODO: Consider (DON'T!) making 'GET' the default verb. def identityDecorator (fn): addRoute(verb, path, fn, mode, name, top); return fn; return identityDecorator; app.route = mkRouteDeco; def popNamedRoute (name): "Removes route named `name`, else raises ValueError."; assert name and type(name) is str; rt = findNamedRoute(name); if not rt: raise ValueError("No such route with name %r." % name); # otherwise ... app.routeList.remove(rt); return rt; app.popNamedRoute = popNamedRoute; # Plugins: ::::::::::::::::::::::::::::::::::::::::::::: def install (plugin): "Installs `plugin`."; app.pluginList.append(plugin); app.install = install; def plugRoute (matchedRoute): pfn = matchedRoute.fn; # pfn: Plugged fn. for plugin in reversed(app.pluginList): # See note regarding `reversed(.)` below. pfn = plugin(pfn); # Apply each plugin. return pfn; # Why use `reversed(.)`? # If app.install(X), then Y, then Z. # Then, without reversed(): pfn = X(Y(Z(fn))); # With reversed(): pfn = Z(Y(X(fn))) # The latter feels more natural. # i.e., plugins installed 1st are applied 1st. # Errors: :::::::::::::::::::::::::::::::::::::::::::::: app.inDebugMode = False; def setDebug (boolean): "Enable/disable debug mode by passing `boolean`."; app.inDebugMode = bool(boolean); app.setDebug = setDebug; def mkDefault_frameworkError_handler (code, msg=None): "Makes a default error handler for framework errors." statusLine = getStatusLineFromCode(code); def defaultErrorHandler (xReq, xRes, xErr): if xReq.contentType == "application/json": return {"status": statusLine, "msg": msg}; # otherwise ... return escfmt("<h2>%s</h2><pre>%s</pre>", [statusLine, msg]); return defaultErrorHandler; def default_frameworkError_unexpected (xReq, xRes, xErr): "Default handler for unexpecte errors."; if not app.inDebugMode: return "<h2>500 Internal Server Error</h2>"; # otherwise ... return escfmt(""" <h2>500 Internal Server Error</h2> <p> <b>NB:</b> Disable debug mode to hide traceback below.<br> . . . . It should <b><i>ALWAYS</i></b> be disabled in production. </p> <hr> <pre style="font-size: 20px; font-weight: bold;">%s</pre> <pre style="font-size: 15px;">%s</pre> """, [ repr(xErr), traceback.format_exc(), ]); app.frameworkErrorHandlerMap = { "route_not_found": mkDefault_frameworkError_handler(404, "No such route."), "file_not_found": mkDefault_frameworkError_handler(404, "No such file."), "request_too_large": mkDefault_frameworkError_handler(413, "Request too large."), "unexpected_error": default_frameworkError_unexpected, }; def frameworkError (_fwCode): "Produces decorator for custom framework-error handling."; if _fwCode not in app.frameworkErrorHandlerMap: raise KeyError(_fwCode); def identityDecorator (oFunc): app.frameworkErrorHandlerMap[_fwCode] = oFunc; return oFunc; return identityDecorator; app.frameworkError = frameworkError; # Route matching, WSGI callable: ::::::::::::::::::::::: def getMatchingRoute (req): "Returns a matching route for a given request `req`."; reqVerb = req.getVerb(); reqPath = req.getPathInfo(); verbMatch = lambda rt: ( (type(rt.verb) is str and reqVerb == rt.verb) or (type(rt.verb) is list and reqVerb in rt.verb) #or ); for rt in app.routeList: if reqVerb in rt.verb and checkRouteMatch(rt, req): return rt; # otherwise .. raise HttpError("<h2>Route Not Found</h2>", 404, "route_not_found"); def wsgi (environ, start_response): "WSGI callable."; #pprint.pprint(environ); req = buildRequest(environ); res = buildResponse(start_response); req.bindApp(app, res); res.bindApp(app, req); #print(req.bodyBytes); try: mRoute = getMatchingRoute(req); pfn = plugRoute(mRoute); # p: Plugin, fn: FuNc handlerOut = pfn(req, res); except HttpError as e: res.statusLine = e.statusLine; if e._fwCode in app.frameworkErrorHandlerMap: efn = app.frameworkErrorHandlerMap[e._fwCode]; #TODO/Consider: Apply app plugins? Or NOT!? handlerOut = efn(req, res, e); else: handlerOut = e.body; except Exception as originalErr: print("\n" + traceback.format_exc() + "\n"); # ^ Use `+`, not `,` to avoid space. httpErr = HttpError( "<h2>Internal Server Error</h2>", 500, "unexpected_error", ); res.statusLine = httpErr.statusLine; efn = app.frameworkErrorHandlerMap[httpErr._fwCode]; # ^ i.e. app.frameworkErrorHandlerMap["unexpected_error"]; #TODO/Consider: Apply app plugins? Or NOT!? handlerOut = efn(req, res, originalErr); return res._finish(handlerOut); app.wsgi = wsgi; # Return built `app`: return app;
def buildResponse (start_response): res = dotsi.fy({}); res.statusLine = "200 OK"; res.contentType = "text/html; charset=UTF-8"; res._headerMap = {}; res.cookieJar = http.cookies.SimpleCookie(); #res._bOutput = b""; res.update({"app": None, "request": None}); def bindApp (appObject, reqObject): res.update({"app": appObject, "request": reqObject}); res.bindApp = bindApp; def setHeader (name, value): name = name.strip().upper(); if name == "CONTENT-TYPE": res.contentType = value; elif name == "CONTENT-LENGTH": raise Exception("The Content-Length header will be automatically set."); else: res._headerMap[name] = value; # TODO: str(value)? res.setHeader = setHeader; def getHeader (name): return res._headerMap.get(name.strip().upper()); res.getHeader = getHeader; def setHeaders (headerList): if type(headerList) is dict: headerList = list(headerList.items()); assert type(headerList) is list; mapli(headerList, lambda pair: setHeader(*pair)); res.setHeaders = setHeaders; def setUnsignedCookie (name, value, opt=None): assert type(value) is str; res.cookieJar[name] = value; morsel = res.cookieJar[name] assert type(morsel) is http.cookies.Morsel; opt = opt or {}; dictDefaults(opt, { "path": "/", "httponly": True, #"secure": True, }); for optKey, optVal in opt.items(): morsel[optKey] = optVal; return value; # `return` helps w/ testing. res.setUnsignedCookie = setUnsignedCookie; def setCookie (name, value, secret=None, opt=None): uVal = signWrap(value, secret) if secret else value; # Unsigned-ready val. setUnsignedCookie(name, uVal, opt); return uVal; # `return` helps w/ testing. res.setCookie = setCookie; #def getCookie (name, value): # pass; # ??? For getting just-res-set cookies. #res.getCookie = getCookie; def staticFile (filepath, mimeType=None): if not mimeType: mimeType, encoding = mimetypes.guess_type(filepath); mimeType = mimeType or "application/octet-stream"; try: with open(filepath, "rb") as f: res.contentType = mimeType; return f.read(); except IOError: raise HttpError("<h2>File Not Found<h2>", 404, "file_not_found"); res.staticFile = staticFile; def redirect (url): res.statusLine = "302 Found"; # Better to use '303 See Other' for HTTP/1.1 environ['SERVER_PROTOCOL'] res.setHeader("Location", url); # but 302 is backward compataible, and doesn't need access to req object. return b""; res.redirect = redirect; def _bytify (x): if type(x) is str: return x.encode("utf8"); if type(x) is bytes: return x; if isinstance(x, (dict, list)): res.contentType = "application/json"; return json.dumps(x).encode("utf8"); # ? latin1 ? # otherwise ... return str(x).encode("utf8"); def _finish (handlerOut): bBody = _bytify(handlerOut); headerList = ( list(res._headerMap.items()) + mapli( res.cookieJar.values(), lambda m: ("SET-COOKIE", m.OutputString()), ) + list({ "CONTENT-TYPE": res.contentType, "CONTENT-LENGTH": str(len(bBody)), }.items()) #+ ); #print("res.statusLine = ", res.statusLine); #pprint.pprint(headerList); latin1_headerList = []; for (name, value) in headerList: latin1_headerList.append((name, utf8_to_latin1(value))); #pprint.pprint(latin1_headerList); start_response( utf8_to_latin1(res.statusLine), latin1_headerList, ); return [bBody]; res._finish = _finish; # Return built `res`: return res;
def buildRequest (environ): req = dotsi.fy({}); req.getEnviron = lambda: environ; def ekey (key, default=None): # Utf8-friendly wrapper around environ. if key not in environ: return default; return latin1_to_utf8(environ[key]); ##Consider:: #value = environ[key]; #if value is str: return latin1_to_utf8(value); #return value; req._ekey = ekey; req.getPathInfo = lambda: ekey("PATH_INFO", "/"); req.getVerb = lambda: ekey("REQUEST_METHOD", "GET").upper(); req.wildcards = []; req.matched = None; req.cookieJar = http.cookies.SimpleCookie(ekey("HTTP_COOKIE", "")); req.app = None; req.response = None; def bindApp (app, response): req.app = app; req.response = response; req.bindApp = bindApp; req.bodyBytes = b""; def fillBody (): fileLike = environ["wsgi.input"]; # Not ekey(.) req.bodyBytes = fileLike.read(MAX_REQUEST_BODY_SIZE); assert type(req.bodyBytes) is bytes; if fileLike.read(1) != b"": raise HttpError("<h2>Request Too Large</h2>", 413, "request_too_large"); fillBody(); # Immediately called. req.url = ""; req.splitUrl = urllib.parse.urlsplit(""); def reconstructUrl (): # Scheme: scheme = (ekey("HTTP_X_FORWARDED_PROTO") or ekey("wsgi.url_scheme") or "http" #or ); # Netloc: netloc = ekey("HTTP_X_FORWARDED_HOST") or ekey("HTTP_HOST"); if not netloc: netloc = ekey("SERVER_NAME"); port = ekey("SERVER_PORT"); if port and port != ("80" if scheme == "http" else "443"): netloc += ":" + port; # Path: path = ( # ? urllib.parse.un/quot() ? ekey("SCRIPT_NAME", "") + ekey("PATH_INFO", "") ); # Query: query = ekey("QUERY_STRING", "") # Fragment: fragment = ""; # Full URL: req.splitUrl = urllib.parse.SplitResult( scheme, netloc, path, query, fragment, ); #print("type(req.splitUrl) = ", type(req.splitUrl)); #print("(req.splitUrl) = ", (req.splitUrl)); req.url = req.splitUrl.geturl(); reconstructUrl(); # Immediately called. # TODO: Handle HTTP_X_FORWARDED_FOR, HTTP_X_FORWARDED_PORT, # HTTP_X_FORWARDED_PREFIX, etc. def getHeader (name): cgikey = name.upper().replace("-", "_"); if cgikey not in ["CONTENT_TYPE", "CONTENT_LENGTH"]: cgikey = "HTTP_" + cgikey; return ekey(cgikey); req.getHeader = getHeader; req.contentType = getHeader("CONTENT_TYPE"); def parseQs (qs): "Parses query string into dict."; return dict(urllib.parse.parse_qsl(qs, keep_blank_values=True)); # parse_qsl(.) returns list of 2-tuples, then dict-ify req.qdata = parseQs(req.splitUrl.query); # IMMEDIATE. def helper_parseMultipartFormData (): assert req.contentType.startswith("multipart/form-data"); parsedData = {}; miniEnviron = { # Not ekey(.), use environ.get(.) directly: "QUERY_STRING": environ.get("QUERY_STRING"), "REQUEST_METHOD": environ.get("REQUEST_METHOD"), "CONTENT_TYPE": environ.get("CONTENT_TYPE"), "CONTENT_LENGTH": len(req.bodyBytes), }; fieldData = cgi.FieldStorage( fp = io.BytesIO(req.bodyBytes), environ = miniEnviron, encoding = "utf8", keep_blank_values = True, ); fieldList = fieldData.list or []; for field in fieldList: if field.filename: parsedData[field.name] = { "filename": field.filename, "bytes": field.file.read(), "mimeType": field.headers.get_content_type(), # TODO: Investigate if this includes charset. #?"charset": field.headers.get_charset(), #?"headers": field.headers, }; else: parsedData[field.name] = field.value; return parsedData; req.fdata = {}; def fill_fdata (): if not req.contentType: pass; # Falsy contentType, ignore. elif req.contentType == "application/json": req.fdata = json.loads(req.bodyBytes); elif req.contentType.startswith("multipart/form-data"): req.fdata = helper_parseMultipartFormData(); elif req.contentType.startswith("application/x-www-form-urlencoded"): req.fdata = parseQs(req.bodyBytes.decode("latin1")); # "utf8" wont't to work, WSGI uses "latin1" ^^ else: pass; # Other contentType, ignore. fill_fdata(); # Immediately called. def getUnsignedCookie (name): morsel = req.cookieJar.get(name); return morsel.value if morsel else None; req.getUnsignedCookie = getUnsignedCookie; def getCookie (name, secret=None): uVal = getUnsignedCookie(name); # Unsigned-ready val. if not uVal: return None; if not secret: return uVal; return signUnwrap(uVal, secret); req.getCookie = getCookie; # Return built `req`: return req;
def buildStdAdp( str_fooBox, str_CURRENT_FOO_V, int_CURRENT_FOO_V, func_validateFoo, ): assert K[str_CURRENT_FOO_V] == int_CURRENT_FOO_V # Ala:: assert K.CURRENT_USER_V == 1; db = dotsi.fy({"fooBox": mongo.db[str_fooBox]}) # Only fooBox accessible on db, pointing to mongo.db[str_fooBox] # Eg: if str_fooBox = 'userBox', db.fooBox -> mongo.db.userBox; # -- stepAdapterList = [] def addStepAdapter(stepAdapterCore): #print("adding step adapter: ", stepAdapterCore) fromV, toV = map(int, re.findall(r"\d+", stepAdapterCore.__name__)) assert toV == fromV + 1 assert len(stepAdapterList) == fromV def fullStepAdapter(fooX): # Prelims: assert fooX._v == fromV fooY = dotsi.fy(utils.deepCopy(fooX)) # Non-alias copy, ensuring incoming `fooX` remains untouched. # Core: stepAdapterCore(fooY) # Make changes to fooY. It's already a non-alias copy, safe to work with. # Common: assert fooY._v == fromV # After core logic, ._v should still remain unchanged. We change it next. fooY._v += 1 assert fooY._v == toV return fooY # Finally, return the adapted fooY. Again, it's stepAdapterList.append(fullStepAdapter) assert len(stepAdapterList) == toV #TODO: Consider: return stepAdapterCore; # Decorator-friendly, identity-func-style return statement. return None def adapt(foo, shouldUpdateDb=True): assert K[str_CURRENT_FOO_V] == int_CURRENT_FOO_V foo = dotsi.fy(foo) preV = foo._v # Previous (pre-adapting) version. itrV = preV # Incrementing loop variable. while (itrV < K[str_CURRENT_FOO_V]): stepAdapter = stepAdapterList[itrV] foo = stepAdapter(foo) assert foo._v == itrV + 1 itrV = foo._v # Update (increment) loop var. assert itrV == K[str_CURRENT_FOO_V] assert func_validateFoo(foo) if shouldUpdateDb and (preV < K[str_CURRENT_FOO_V]): # ==> The (previous) foo was adapted. So, update db: dbOut = db.fooBox.replace_one({"_id": foo._id}, foo) # Remember that db.fooBox --points-to--> mongo.db[str_fooBox]; assert dbOut.matched_count == 1 == dbOut.modified_count return foo # Export the built standard ADP bundle: # Exo: userAdp = stdAdpBuilder.buildStdAdp(..) return dotsi.fy({ # userAdp.addStepAdapter(..) "addStepAdapter": addStepAdapter, # user = userAdp.adapt(db.userBox.find_one({..})); "adapt": adapt, "getStepCount": lambda: len(stepAdapterList), })
import vf # loc: from constants import K import mongo import utils import bu import hashUp import stdAdpBuilder ############################################################ # Assertions & indexing: # ############################################################ assert K.CURRENT_USER_V == 1 db = dotsi.fy({"userBox": mongo.db.userBox}) # Isolate db.userBox.create_index([ ("email", pymongo.ASCENDING), ], unique=True, name=K.USER_EMAIL_INDEX_NAME) # Unique ############################################################ # User building and validation: # ############################################################ def genVeriCode(): return secrets.token_urlsafe()
import dotsi import vf # loc: from constants import K import mongo import utils import bu import stdAdpBuilder ############################################################ # Assertions & prelims: # ############################################################ assert K.CURRENT_ARTICLE_V == 2 db = dotsi.fy({"articleBox": mongo.db.articleBox}) # Isolate ############################################################ # Article building and validation: # ############################################################ validateArticle = vf.dictOf({ "_id": utils.isObjectId, "_v": lambda x: x == K.CURRENT_ARTICLE_V, # # Intro'd in _v0: # "title": vf.typeIs(str), #"scratchpad": vf.typeIs(str), -- Renamed in _v1 to 'body' "creatorId": utils.isObjectId,
# loc: import hashUp import utils request = bottle.request response = bottle.response redirect = bottle.redirect staticFile = bottle.static_file ############################################################ # Config related: # ############################################################ config = dotsi.fy( { # Exo-module: bu.config.cookieSecret = "new secret"; Or use .update({}); "cookieSecret": "__default_cookie_secret_123__" }) def setCookieSecret(newCookieSecret): config.cookieSecret = newCookieSecret ############################################################ # Static and shortcut related: # ############################################################ def addStaticFolder(app, folderPath, slug=None): "Helps serve static files in `folderPath`." folderPath = os.path.abspath(folderPath)
import dotsi; import vf; # loc: from constants import K; import mongo; import utils; import bu; import stdAdpBuilder; ############################################################ # Assertions & prelims: # ############################################################ assert K.CURRENT_CATEGORY_V == 0; db = dotsi.fy({"categoryBox": mongo.db.categoryBox}); # Isolate ############################################################ # Category building and validation: # ############################################################ validateCategory = vf.dictOf({ "_id": utils.isObjectId, "_v": lambda x: x == K.CURRENT_CATEGORY_V, # # Intro'd in _v0: # "name": vf.typeIs(str), "rank": utils.isNonNegativeNumber, # 0+, inty or float. "parentId": utils.isBlankOrObjectId, # "" => no parent => top "creatorId": utils.isObjectId,
def buildHasher (saltPrefix="", digestmod=hashlib.sha512): "Returns a dotsi.Dict() w/ .hash, .check etc. functions."; def hash (msg, salt): keyBytes = (saltPrefix + salt).encode("utf8"); msgBytes = msg.encode("utf8"); return hmac.HMAC(key=keyBytes, msg=msgBytes, digestmod=digestmod).hexdigest(); def check (expectedHash, msg, salt, digestmod=hashlib.sha512): return expectedHash == hash(msg, salt); def signDetached (data, msTs, secret): tData = {"data": data, "msTs": msTs}; tDataStr = json.dumps(tData); #print("tDataStr = ", tDataStr); return hash(tDataStr, secret); def checkDetachedSign (sig, data, msTs, secret): return sig == signDetached(data, msTs, secret); def signWrap (data, secret): "Signs json-stringifiable `data` using `secret`. Produces wrapped string."; msTs = ms_now(); sig = signDetached(data, msTs, secret); signWrappedData = {"data": data, "msTs": msTs, "sig": sig}; signWrappedDataStr = json.dumps(signWrappedData); signWrappedDataStrQ = utils.quote(signWrappedDataStr); #print("signWrappedDataStr = ", signWrappedDataStr); return signWrappedDataStrQ;# <--. # | # V-- def _signUnwrap (signWrappedDataStrQ, secret, maxExpiryInDays=30): # Note: maxExpiryInDays=30 can be changed w/ each call. "Unwraps and reads signWrappedDataStr, using secret."; signWrappedDataStr = utils.unquote(signWrappedDataStrQ); signWrappedData = json.loads(signWrappedDataStr); # Failure would raise json.decoder.JSONDecodeError # Unwrapping: data = signWrappedData["data"]; # Failure would raise KeyError msTs = signWrappedData["msTs"]; sig = signWrappedData["sig"]; # Validate signature: assert checkDetachedSign(sig, data, msTs, secret); # Failure would raise AssertionError # ==> SIGNATURE FORMAT OK. msSinceSigned = ms_now() - msTs; #print("msSinceSigned = ", msSinceSigned); daysSinceSigned = ms_delta_toDays(msSinceSigned); #print("daysSinceSigned = ", daysSinceSigned); assert daysSinceSigned <= maxExpiryInDays; # Failure would raise AssertionError # ==> SIGNATURE EXPIRY OK. return dotsi.fy(data) if type(data) is dict else data; def signUnwrap (signWrappedDataStr, secret, maxExpiryInDays=30): # Note: maxExpiryInDays=30 can be changed w/ each call. try: return _signUnwrap(signWrappedDataStr, secret, maxExpiryInDays); except (json.decoder.JSONDecodeError, KeyError, AssertionError) as e: raise SignatureInvalidError("Supplied signature is invalid."); # Export: return dotsi.fy({ "hash": hash, "check": check, "signDetached": "signDetached", "signWrap": signWrap, "_signUnwrap": _signUnwrap, "signUnwrap": signUnwrap, "SignatureInvalidError": SignatureInvalidError, # Exported the error class, for access via built e-dict. });
str_to_bytes = lambda s, enc="utf8": s.encode(enc); bytes_to_str = lambda b, enc="utf8": b.decode(enc); str_to_b64_bytes = lambda s, enc="utf8": base64.b64encode(s.encode(enc)); # 'foo' --s.encode--> b'foo' --b64encode--> b'Zm9v' b64_bytes_to_str = lambda b, enc="utf8": base64.b64decode(b).decode(enc); # b'Zm9v' --b64decode--> b'foo' --.decode--> 'foo' str_to_b64_str = lambda s, enc="utf8": str_to_b64_bytes(s, enc).decode(enc); # 'foo' --str_to_base64_bytes--> b'Zm9v' --> 'Zm9v' b64_str_to_str = lambda s, enc="utf8": b64_bytes_to_str(s.encode(enc)); # 'Zm9v' --.enode--> b'Zm9v' --b64_bytes_to_str--> 'foo' # URL Related: ::::::::::::::::::::::::::::::::::::::::::::: quote = lambda s: urllib.parse.quote(s, safe=''); unquote = urllib.parse.unquote; # Alias. qs = dotsi.fy({}); qs.loads = lambda s: dotsi.fy(dict(urllib.parse.parse_qsl(s))); # In qs.loads, a=a&b=b&a=A --> {"a":"A", "b":"b"}; i.e. last value is considered. qs.dumps = lambda d: urllib.parse.urlencode(d, safe='', quote_via=urllib.parse.quote); # In qs.dumps, we quote_via parse.quote() w/ safe='', not the default parse.quote_plus(). # Asserting: ::::::::::::::::::::::::::::::::::::::::::::::: isStr = lambda x: type(x) is str; # Although the var name starts w/ `is` and not `check`, they're functions. isStringy = lambda x: type(x) in [str, bytes]; isList = lambda x: type(x) is list; isListy = lambda x: type(x) in [list, tuple]; isDict = lambda x: type(x) is dict;
from collections import OrderedDict import dotsi # Basic dotsi.Dict: d = dotsi.fy({ "foo": "foo", "bar": "bar" }) assert d.foo == "foo" and d.bar == "bar" d.update({ "hello": "world", "bar": "baz" }) assert d.hello == "world" and d.bar == "baz" # Nested dotsi.Dict: d = dotsi.fy({ "data": { "users": [ { "id": 0, "name": "Alice" }, { "id": 1, "name": "Becci" }, ] } }) assert d.data.users[0].id == 0 and d.data.users[0].name == "Alice"
# pip-int: import dotsi import vf # loc: from constants import K import mongo import utils import bu ############################################################ # Assertions & prelims: # ############################################################ assert K.CURRENT_DOJO_V == 0 db = dotsi.fy({"dojoBox": mongo.db.dojoBox}) # Isolate ############################################################ # Dojo building and validation: # ############################################################ validateDojo = vf.dictOf({ "_id": utils.isObjectId, "_v": lambda x: x == K.CURRENT_DOJO_V, # # Intro'd in _v0: "title": vf.typeIs(str), "scratchpad": vf.typeIs(str), "creatorId": utils.isObjectId, "createdAt": utils.isInty,