Beispiel #1
0
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});
Beispiel #2
0
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,
    });
Beispiel #3
0
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(),
    })
Beispiel #4
0
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(),
    });
Beispiel #5
0
 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
Beispiel #6
0
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
Beispiel #7
0
 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;
Beispiel #8
0
 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
Beispiel #9
0
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,
    })
Beispiel #10
0
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",
    })
Beispiel #11
0
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;
Beispiel #12
0
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;
Beispiel #13
0
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;
Beispiel #14
0
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),
    })
Beispiel #15
0
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()
Beispiel #16
0
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,
Beispiel #17
0
# 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)
Beispiel #18
0
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,
Beispiel #19
0
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.
    });
Beispiel #20
0
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;
Beispiel #21
0
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"
Beispiel #22
0
# 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,