def validate(key: str, useSessionKey: bool) -> Union[bool, db.Entity]: """ Validates a onetime securitykey :type key: str :param key: The key to validate :type useSessionKey: Bool :param useSessionKey: If True, we validate against the session's skey, otherwise we'll lookup an unbound key :returns: False if the key was not valid for whatever reasons, the data (given during createSecurityKey) as dictionary or True if the dict is empty. """ if useSessionKey: if key == "staticSessionKey": skeyHeaderValue = currentRequest.get().request.headers.get( "Sec-X-ViUR-StaticSKey") if skeyHeaderValue and currentSession.get( ).validateStaticSecurityKey(skeyHeaderValue): return True elif currentSession.get().validateSecurityKey(key): return True return False if not key: return False dbKey = db.Key(securityKeyKindName, key) dbObj = db.Get(dbKey) if dbObj: db.Delete(dbKey) if dbObj["until"] < datetime.now(): # This key has expired return False del dbObj["until"] if not dbObj: return True return dbObj return False
def validate(key: str, useSessionKey: bool) -> Union[bool, db.Entity]: """ Validates a security key. If useSessionKey is true, the key is expected to be the sessions current security key (or it's static security key). Otherwise it must be a key created with a duration (so it's not session dependent) :param key: The key to validate :param useSessionKey: If True, we validate against the session's skey, otherwise we'll lookup an unbound key :returns: False if the key was not valid for whatever reasons, the data (given during createSecurityKey) as dictionary or True if the dict is empty (or :param:useSessionKey was true). """ if useSessionKey: if key == "staticSessionKey": skeyHeaderValue = currentRequest.get().request.headers.get( "Sec-X-ViUR-StaticSKey") if skeyHeaderValue and currentSession.get( ).validateStaticSecurityKey(skeyHeaderValue): return True elif currentSession.get().validateSecurityKey(key): return True return False if not key: return False dbKey = db.Key(securityKeyKindName, key) dbObj = db.Get(dbKey) if dbObj: db.Delete(dbKey) until = dbObj["until"] if until < utcNow(): # This key has expired return False del dbObj["until"] if not dbObj: return True return dbObj return False
def startProcessing(self, userKey): user = db.Get(userKey) if all([(x in user and user[x]) for x in ["otpid", "otpkey"]]): logging.info("OTP wanted for user") currentSession.get()["_otp_user"] = {"uid": str(userKey), "otpid": user["otpid"], "otpkey": user["otpkey"], "otptimedrift": user["otptimedrift"], "timestamp": time(), "failures": 0} currentSession.get().markChanged() return self.userModule.render.loginSucceeded(msg="X-VIUR-2FACTOR-TimeBasedOTP") return None
def authenticateUser(self, userKey, **kwargs): """ Performs Log-In for the current session and the given userKey. This resets the current session: All fields not explicitly marked as persistent by conf["viur.session.persistentFieldsOnLogin"] are gone afterwards. :param authProvider: Which authentication-provider issued the authenticateUser request :type authProvider: object :param userKey: The (DB-)Key of the user we shall authenticate :type userKey: db.Key """ currSess = currentSession.get() res = db.Get(userKey) assert res, "Unable to authenticate unknown user %s" % userKey oldSession = {k: v for k, v in currSess.items() } # Store all items in the current session currSess.reset() # Copy the persistent fields over for k in conf["viur.session.persistentFieldsOnLogin"]: if k in oldSession: currSess[k] = oldSession[k] del oldSession currSess["user"] = res currSess.markChanged() currentRequest.get().response.headers[ "Sec-X-ViUR-StaticSKey"] = currSess.staticSecurityKey self.onLogin() return self.render.loginSucceeded(**kwargs)
def secondFactorSucceeded(self, secondFactor, userKey): currSess = currentSession.get() logging.debug("Got SecondFactorSucceeded call from %s." % secondFactor) if str(currSess["_mayBeUserKey"]) != str(userKey): raise errors.Forbidden() # Assert that the second factor verification finished in time if datetime.datetime.now() - currSess["_secondFactorStart"] > self.secondFactorTimeWindow: raise errors.RequestTimeout() return self.authenticateUser(userKey)
def getCurrentUser(self, *args, **kwargs): session = currentSession.get() if not session: # May be a deferred task return None userData = session.get("user") if userData: skel = self.viewSkel() skel.setEntity(userData) return skel return None
def selectLanguage(self, path: str): """ Tries to select the best language for the current request. Depending on the value of conf["viur.languageMethod"], we'll either try to load it from the session, determine it by the domain or extract it from the URL. """ sessionReference = currentSession.get() if not conf["viur.availableLanguages"]: # This project doesn't use the multi-language feature, nothing to do here return path if conf["viur.languageMethod"] == "session": # We store the language inside the session, try to load it from there if "lang" not in sessionReference: if "X-Appengine-Country" in self.request.headers: lng = self.request.headers["X-Appengine-Country"].lower() if lng in conf["viur.availableLanguages"] + list( conf["viur.languageAliasMap"].keys()): sessionReference["lang"] = lng currentLanguage.set(lng) else: sessionReference["lang"] = conf["viur.defaultLanguage"] else: currentLanguage.set(sessionReference["lang"]) elif conf["viur.languageMethod"] == "domain": host = self.request.host_url.lower() host = host[host.find("://") + 3:].strip(" /") # strip http(s):// if host.startswith("www."): host = host[4:] if host in conf["viur.domainLanguageMapping"]: currentLanguage.set(conf["viur.domainLanguageMapping"][host]) else: # We have no language configured for this domain, try to read it from session if "lang" in sessionReference: currentLanguage.set(sessionReference["lang"]) elif conf["viur.languageMethod"] == "url": tmppath = urlparse(path).path tmppath = [ unquote(x) for x in tmppath.lower().strip("/").split("/") ] if len(tmppath) > 0 and tmppath[ 0] in conf["viur.availableLanguages"] + list( conf["viur.languageAliasMap"].keys()): currentLanguage.set(tmppath[0]) return (path[len(tmppath[0]) + 1:] ) # Return the path stripped by its language segment else: # This URL doesnt contain an language prefix, try to read it from session if "lang" in sessionReference: currentLanguage.set(sessionReference["lang"]) elif "X-Appengine-Country" in self.request.headers.keys(): lng = self.request.headers["X-Appengine-Country"].lower() if lng in conf["viur.availableLanguages"] or lng in conf[ "viur.languageAliasMap"]: currentLanguage.set(lng) return path
def getSession(render): """ Jinja2 global: Allows templates to store variables server-side inside the session. Note: This is done in a separated part of the session for security reasons. :returns: A dictionary of session variables. :rtype: dict """ currSess = currentSession.get() if not currSess.get("JinjaSpace"): currSess["JinjaSpace"] = {} return currSess.get("JinjaSpace")
def setSession(render, name, value): """ Jinja2 global: Allows templates to store variables on server-side inside the session. Note: This is done in a separated part of the session for security reasons. :param name: Name of the key :type name: str :param value: Value to store with name. :type value: any """ sessionData = getSession(render) sessionData[name] = value currSess = currentSession.get() currSess["JinjaSpace"] = sessionData currSess.markChanged()
def continueAuthenticationFlow(self, caller, userKey): currSess = currentSession.get() currSess["_mayBeUserKey"] = str(userKey) currSess["_secondFactorStart"] = datetime.datetime.now() currSess.markChanged() for authProvider, secondFactor in self.validAuthenticationMethods: if isinstance(caller, authProvider): if secondFactor is None: # We allow sign-in without a second factor return self.authenticateUser(userKey) # This Auth-Request was issued from this authenticationProvider secondFactorProvider = self.secondFactorProviderByClass(secondFactor) if secondFactorProvider.canHandle(userKey): # We choose the first second factor provider which claims it can verify that user return secondFactorProvider.startProcessing(userKey) # Whoops.. This user logged in successfully - but we have no second factor provider willing to confirm it raise errors.NotAcceptable("There are no more authentication methods to try") # Sorry...
def otp(self, otptoken=None, skey=None, *args, **kwargs): currSess = currentSession.get() token = currSess.get("_otp_user") if not token: raise errors.Forbidden() if otptoken is None: self.userModule.render.edit(self.otpSkel()) if not securitykey.validate(skey, useSessionKey=True): raise errors.PreconditionFailed() if token["failures"] > 3: raise errors.Forbidden( "Maximum amount of authentication retries exceeded") if len(token["otpkey"]) % 2 == 1: raise errors.PreconditionFailed( "The otp secret stored for this user is invalid (uneven length)" ) validTokens = self.generateOtps(token["otpkey"], token["otptimedrift"]) try: otptoken = int(otptoken) except: # We got a non-numeric token - this cant be correct self.userModule.render.edit(self.otpSkel(), tpl=self.otpTemplate) if otptoken in validTokens: userKey = currSess["_otp_user"]["uid"] del currSess["_otp_user"] currSess.markChanged() idx = validTokens.index(int(otptoken)) if abs(idx - self.windowSize) > 2: # The time-drift accumulates to more than 2 minutes, update our # clock-drift value accordingly self.updateTimeDrift(userKey, idx - self.windowSize) return self.userModule.secondFactorSucceeded(self, userKey) else: token["failures"] += 1 currSess["_otp_user"] = token currSess.markChanged() return self.userModule.render.edit(self.otpSkel(), loginFailed=True, tpl=self.otpTemplate)
def create(duration: Union[None, int] = None, **kwargs) -> str: """ Creates a new onetime Securitykey or returns the current sessions csrf-token. The custom data (given as keyword arguments) that can be stored with the key if :param:duration is set must be serializable by the datastore. :param duration: Make this key valid for a fixed timeframe (and independent of the current session) :returns: The new onetime key """ if not duration: return currentSession.get().getSecurityKey() key = generateRandomString() duration = int(duration) dbObj = db.Entity(db.Key(securityKeyKindName, key)) for k, v in kwargs.items(): dbObj[k] = v dbObj["until"] = utcNow() + timedelta(seconds=duration) db.Put(dbObj) return key
def logout(self, skey="", *args, **kwargs): """ Implements the logout action. It also terminates the current session (all keys not listed in viur.session.persistentFieldsOnLogout will be lost). """ currSess = currentSession.get() user = currSess.get("user") if not user: raise errors.Unauthorized() if not securitykey.validate(skey, useSessionKey=True): raise errors.PreconditionFailed() self.onLogout(user) oldSession = {k: v for k, v in currSess.items()} # Store all items in the current session currSess.reset() # Copy the persistent fields over for k in conf["viur.session.persistentFieldsOnLogout"]: if k in oldSession: currSess[k] = oldSession[k] del oldSession return self.render.logoutSuccess()
def create(duration: Union[None, int] = None, **kwargs): """ Creates a new onetime Securitykey for the current session If duration is not set, this key is valid only for the current session. Otherwise, the key and its data is serialized and saved inside the datastore for up to duration-seconds :param duration: Make this key valid for a fixed timeframe (and independend of the current session) :type duration: int or None :returns: The new onetime key """ if not duration: return currentSession.get().getSecurityKey() key = generateRandomString() duration = int(duration) dbObj = db.Entity(securityKeyKindName, name=key) for k, v in kwargs.items(): dbObj[k] = v dbObj["until"] = datetime.now() + timedelta(seconds=duration) db.Put(dbObj) return key
def edit(self, *args, **kwargs): currSess = currentSession.get() if len(args) == 0 and not "key" in kwargs and currSess.get("user"): kwargs["key"] = currSess.get("user")["key"] return super(User, self).edit(*args, **kwargs)
def pwrecover(self, *args, **kwargs): """ This implements the password recovery process which let them set a new password for their account after validating a code send to them by email. The process is as following: - The user enters his email adress - We'll generate a random code, store it in his session and call sendUserPasswordRecoveryCode - sendUserPasswordRecoveryCode will run in the background, check if we have a user with that name and send the code. It runs as a deferredTask so we don't leak the information if a user account exists. - If the user received his code, he can paste the code and set a new password for his account. To prevent automated attacks, the fist step is guarded by a captcha and we limited calls to this function to 10 actions per 15 minutes. (One complete recovery process consists of two calls). """ if not self.passwordRecoveryRateLimit.isQuotaAvailable(): raise errors.Forbidden() # Quota exhausted, bail out session = currentSession.get() request = currentRequest.get() recoverStep = session.get("user.auth_userpassword.pwrecover") if not recoverStep: # This is the first step, where we ask for the username of the account we'll going to reset the password on skel = self.lostPasswordStep1Skel() if not request.isPostRequest or not skel.fromClient(kwargs): return self.userModule.render.edit( skel, self.passwordRecoveryStep1Template) if not securitykey.validate(kwargs.get("skey"), useSessionKey=True): raise errors.PreconditionFailed() self.passwordRecoveryRateLimit.decrementQuota() recoveryKey = utils.generateRandomString( 13) # This is the key the user will have to Copy&Paste self.sendUserPasswordRecoveryCode( skel["name"].lower(), recoveryKey) # Send the code in the background session["user.auth_userpassword.pwrecover"] = { "name": skel["name"].lower(), "recoveryKey": recoveryKey, "creationdate": utcNow(), "errorCount": 0 } del recoveryKey return self.pwrecover( ) # Fall through to the second step as that key in the session is now set else: if request.isPostRequest and kwargs.get("abort") == "1" \ and securitykey.validate(kwargs.get("skey"), useSessionKey=True): # Allow a user to abort the process if a wrong email has been used session["user.auth_userpassword.pwrecover"] = None return self.pwrecover() # We're in the second step - the code has been send and is waiting for confirmation from the user if utcNow() - session["user.auth_userpassword.pwrecover"][ "creationdate"] > datetime.timedelta(minutes=15): # This recovery-process is expired; reset the session and start over session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryKeyExpired) skel = self.lostPasswordStep2Skel() if not skel.fromClient(kwargs) or not request.isPostRequest: return self.userModule.render.edit( skel, self.passwordRecoveryStep2Template) if not securitykey.validate(kwargs.get("skey"), useSessionKey=True): raise errors.PreconditionFailed() self.passwordRecoveryRateLimit.decrementQuota() if not hmac.compare_digest( session["user.auth_userpassword.pwrecover"]["recoveryKey"], skel["recoveryKey"]): # The key was invalid, increase error-count or abort this recovery process altogether session["user.auth_userpassword.pwrecover"]["errorCount"] += 1 if session["user.auth_userpassword.pwrecover"][ "errorCount"] > 3: session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryKeyInvalid) return self.userModule.render.edit( skel, self.passwordRecoveryStep2Template) # Let's try again # If we made it here, the key was correct, so we'd hopefully have a valid user for this uSkel = userSkel().all().filter( "name.idx =", session["user.auth_userpassword.pwrecover"]["name"]).getSkel() if not uSkel: # This *should* never happen - if we don't have a matching account we'll not send the key. session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryUserNotFound) if uSkel["status"] != 10: # The account is locked or not yet validated. Abort the process session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( skel=None, tpl=self.passwordRecoveryFailedTemplate, reason=self.passwordRecoveryAccountLocked) # Update the password, save the user, reset his session and show the success-template uSkel["password"] = skel["password"] uSkel.toDB() session["user.auth_userpassword.pwrecover"] = None return self.userModule.render.view( None, self.passwordRecoverySuccessTemplate)
def mkDefered(func, self=__undefinedFlag_, *args, **kwargs): if not queueRegion: # Run tasks inline logging.info("Running inline: %s" % func) if self is __undefinedFlag_: task = lambda: func(*args, **kwargs) else: task = lambda: func(self, *args, **kwargs) req = currentRequest.get() if req: req.pendingTasks.append( task ) # < This property will be only exist on development server! else: # Warmup request or something - we have to call it now as we can't defer it :/ task() return # Ensure no result gets passed back from viur.core.utils import getCurrentUser try: req = currentRequest.get() except: # This will fail for warmup requests req = None if req is not None and req.request.headers.get("X-Appengine-Taskretrycount") \ and "DEFERED_TASK_CALLED" not in dir(req): # This is the deferred call req.DEFERED_TASK_CALLED = True # Defer recursive calls to an deferred function again. if self is __undefinedFlag_: return func(*args, **kwargs) else: return func(self, *args, **kwargs) else: try: if self.__class__.__name__ == "index": funcPath = func.__name__ else: funcPath = "%s/%s" % (self.modulePath, func.__name__) command = "rel" except: funcPath = "%s.%s" % (func.__name__, func.__module__) if self != __undefinedFlag_: args = ( self, ) + args # Reappend self to args, as this function is (hopefully) unbound command = "unb" taskargs = dict((x, kwargs.pop(("_%s" % x), None)) for x in ("countdown", "eta", "name", "target", "retry_options")) taskargs["url"] = "/_tasks/deferred" transactional = kwargs.pop("_transactional", False) taskargs["headers"] = {"Content-Type": "application/octet-stream"} queue = kwargs.pop("_queue", "default") # Try to preserve the important data from the current environment try: # We might get called inside a warmup request without session usr = currentSession.get().get("user") if "password" in usr: del usr["password"] except: usr = None env = {"user": usr} logging.error(env) try: env["lang"] = currentRequest.get().language except AttributeError: # This isn't originating from a normal request pass if db.IsInTransaction(): # We have to ensure transaction guarantees for that task also env["transactionMarker"] = db.acquireTransactionSuccessMarker() # We move that task at least 90 seconds into the future so the transaction has time to settle taskargs["countdown"] = max( 90, (taskargs.get("countdown") or 0)) # Countdown can be set to None if conf["viur.tasks.customEnvironmentHandler"]: # Check if this project relies on additional environmental variables and serialize them too assert isinstance(conf["viur.tasks.customEnvironmentHandler"], tuple) \ and len(conf["viur.tasks.customEnvironmentHandler"]) == 2 \ and callable(conf["viur.tasks.customEnvironmentHandler"][0]), \ "Your customEnvironmentHandler must be a tuple of two callable if set!" env["custom"] = conf["viur.tasks.customEnvironmentHandler"][0]( ) pickled = json.dumps((command, (funcPath, args, kwargs, env)), cls=JsonKeyEncoder).encode("UTF-8") project = utils.projectID location = queueRegion parent = taskClient.queue_path(project, location, queue) task = { 'app_engine_http_request': { # Specify the type of request. 'http_method': tasks_v2.HttpMethod.POST, 'relative_uri': '/_tasks/deferred' } } if taskargs.get("countdown"): # We must send a Timestamp Protobuf instead of a date-string timestamp = timestamp_pb2.Timestamp() timestamp.FromDatetime(utils.utcNow() + timedelta( seconds=taskargs["countdown"])) task['schedule_time'] = timestamp task['app_engine_http_request']['body'] = pickled # Use the client to build and send the task. response = taskClient.create_task(parent=parent, task=task) print('Created task {}'.format(response.name))
def processRequest(self): """ Bring up the enviroment for this request, start processing and handle errors """ # Check if it's a HTTP-Method we support reqestMethod = self.request.method.lower() if reqestMethod not in ["get", "post", "head"]: logging.error("Not supported") return self.isPostRequest = reqestMethod == "post" # Configure some basic parameters for this request self.internalRequest = False self.isDevServer = os.environ[ 'GAE_ENV'] == "localdev" # Were running on development Server self.isSSLConnection = self.request.host_url.lower().startswith( "https://") # We have an encrypted channel currentLanguage.set(conf["viur.defaultLanguage"]) self.disableCache = False # Shall this request bypass the caches? self.args = [] self.kwargs = {} # Check if we should process or abort the request for validator, reqValidatorResult in [(x, x.validate(self)) for x in self.requestValidators]: if reqValidatorResult is not None: logging.warning("Request rejected by validator %s" % validator.name) statusCode, statusStr, statusDescr = reqValidatorResult self.response.status = '%d %s' % (statusCode, statusStr) self.response.write(statusDescr) return path = self.request.path if self.isDevServer: # We'll have to emulate the task-queue locally as far as possible until supported by dev_appserver again self.pendingTasks = [] # Add CSP headers early (if any) if conf["viur.security.contentSecurityPolicy"] and conf[ "viur.security.contentSecurityPolicy"]["_headerCache"]: for k, v in conf["viur.security.contentSecurityPolicy"][ "_headerCache"].items(): self.response.headers[k] = v if self.isSSLConnection: # Check for HTST and PKP headers only if we have a secure channel. if conf["viur.security.strictTransportSecurity"]: self.response.headers["Strict-Transport-Security"] = conf[ "viur.security.strictTransportSecurity"] # Check for X-Security-Headers we shall emit if conf["viur.security.xContentTypeOptions"]: self.response.headers["X-Content-Type-Options"] = "nosniff" if conf["viur.security.xXssProtection"] is not None: if conf["viur.security.xXssProtection"]: self.response.headers["X-XSS-Protection"] = "1; mode=block" elif conf["viur.security.xXssProtection"] is False: self.response.headers["X-XSS-Protection"] = "0" if conf["viur.security.xFrameOptions"] is not None and isinstance( conf["viur.security.xFrameOptions"], tuple): mode, uri = conf["viur.security.xFrameOptions"] if mode in ["deny", "sameorigin"]: self.response.headers["X-Frame-Options"] = mode elif mode == "allow-from": self.response.headers[ "X-Frame-Options"] = "allow-from %s" % uri if conf["viur.security.xPermittedCrossDomainPolicies"] is not None: self.response.headers["X-Permitted-Cross-Domain-Policies"] = conf[ "viur.security.xPermittedCrossDomainPolicies"] if conf["viur.security.referrerPolicy"]: self.response.headers["Referrer-Policy"] = conf[ "viur.security.referrerPolicy"] if conf["viur.security.permissionsPolicy"].get("_headerCache"): self.response.headers["Permissions-Policy"] = conf[ "viur.security.permissionsPolicy"]["_headerCache"] if conf["viur.security.enableCOEP"]: self.response.headers[ "Cross-Origin-Embedder-Policy"] = "require-corp" if conf["viur.security.enableCOOP"]: self.response.headers["Cross-Origin-Opener-Policy"] = conf[ "viur.security.enableCOOP"] if conf["viur.security.enableCORP"]: self.response.headers["Cross-Origin-Resource-Policy"] = conf[ "viur.security.enableCORP"] # Ensure that TLS is used if required if conf["viur.forceSSL"] and not self.isSSLConnection and not self.isDevServer: isWhitelisted = False reqPath = self.request.path for testUrl in conf["viur.noSSLCheckUrls"]: if testUrl.endswith("*"): if reqPath.startswith(testUrl[:-1]): isWhitelisted = True break else: if testUrl == reqPath: isWhitelisted = True break if not isWhitelisted: # Some URLs need to be whitelisted (as f.e. the Tasks-Queue doesn't call using https) # Redirect the user to the startpage (using ssl this time) host = self.request.host_url.lower() host = host[host.find("://") + 3:].strip( " /") # strip http(s):// self.response.status = "302 Found" self.response.headers['Location'] = "https://%s/" % host return if path.startswith("/_ah/warmup"): self.response.write("okay") return try: currentSession.get().load(self) # self.request.cookies ) path = self.selectLanguage(path)[1:] if conf["viur.requestPreprocessor"]: path = conf["viur.requestPreprocessor"](path) self.findAndCall(path) except errors.Redirect as e: if conf["viur.debug.traceExceptions"]: raise self.response.status = '%d %s' % (e.status, e.name) url = e.url if url.startswith(('.', '/')): url = str(urljoin(self.request.url, url)) self.response.headers['Location'] = url except errors.HTTPException as e: if conf["viur.debug.traceExceptions"]: raise self.response.body = b"" self.response.status = '%d %s' % (e.status, e.name) # Set machine-readable x-viur-error response header in case there is an exception description. if e.descr: self.response.headers["x-viur-error"] = e.descr.replace( "\n", "") res = None if conf["viur.errorHandler"]: try: res = conf["viur.errorHandler"](e) except Exception as newE: logging.error("viur.errorHandler failed!") logging.exception(newE) res = None if not res: tpl = Template( open( os.path.join(utils.coreBasePath, conf["viur.errorTemplate"]), "r").read()) res = tpl.safe_substitute({ "error_code": e.status, "error_name": e.name, "error_descr": e.descr }) extendCsp({ "style-src": ['sha256-Lwf7c88gJwuw6L6p6ILPSs/+Ui7zCk8VaIvp8wLhQ4A='] }) self.response.write(res.encode("UTF-8")) except Exception as e: # Something got really wrong logging.error("Viur caught an unhandled exception!") logging.exception(e) self.response.body = b"" self.response.status = 500 res = None if conf["viur.errorHandler"]: try: res = conf["viur.errorHandler"](e) except Exception as newE: logging.error("viur.errorHandler failed!") logging.exception(newE) res = None if not res: tpl = Template( open( os.path.join(utils.coreBasePath, conf["viur.errorTemplate"]), "r").read()) descr = "The server encountered an unexpected error and is unable to process your request." if self.isDevServer: # Were running on development Server strIO = StringIO() traceback.print_exc(file=strIO) descr = strIO.getvalue() descr = descr.replace("<", "<").replace( ">", ">").replace(" ", " ").replace("\n", "<br />") res = tpl.safe_substitute({ "error_code": "500", "error_name": "Internal Server Error", "error_descr": descr }) extendCsp({ "style-src": ['sha256-Lwf7c88gJwuw6L6p6ILPSs/+Ui7zCk8VaIvp8wLhQ4A='] }) self.response.write(res.encode("UTF-8")) finally: self.saveSession() SEVERITY = "DEBUG" if self.maxLogLevel >= 50: SEVERITY = "CRITICAL" elif self.maxLogLevel >= 40: SEVERITY = "ERROR" elif self.maxLogLevel >= 30: SEVERITY = "WARNING" elif self.maxLogLevel >= 20: SEVERITY = "INFO" TRACE = "projects/{}/traces/{}".format(loggingClient.project, self._traceID) REQUEST = { 'requestMethod': self.request.method, 'requestUrl': self.request.url, 'status': self.response.status_code, 'userAgent': self.request.headers.get('USER-AGENT'), 'responseSize': self.response.content_length, 'latency': "%0.3fs" % (time() - self.startTime), 'remoteIp': self.request.environ.get("HTTP_X_APPENGINE_USER_IP") } requestLogger.log_text("", client=loggingClient, severity=SEVERITY, http_request=REQUEST, trace=TRACE, resource=requestLoggingRessource) if self.isDevServer: while self.pendingTasks: task = self.pendingTasks.pop() logging.info("Running task directly after request: %s" % str(task)) task()
def saveSession(self): currentSession.get().save(self)
def deferred(self, *args, **kwargs): """ This catches one deferred call and routes it to its destination """ global _deferedTasks, _appengineServiceIPs req = currentRequest.get().request if 'X-AppEngine-TaskName' not in req.headers: logging.critical( 'Detected an attempted XSRF attack. The header "X-AppEngine-Taskname" was not set.' ) raise errors.Forbidden() if req.environ.get( "HTTP_X_APPENGINE_USER_IP") not in _appengineServiceIPs: logging.critical( 'Detected an attempted XSRF attack. This request did not originate from Task Queue.' ) raise errors.Forbidden() # Check if the retry count exceeds our warning threshold retryCount = req.headers.get("X-Appengine-Taskretrycount", None) if retryCount: if int(retryCount) == self.retryCountWarningThreshold: utils.sendEMailToAdmins( "Deferred task retry count exceeded warning threshold", "Task %s will now be retried for the %sth time." % (req.headers.get("X-Appengine-Taskname", ""), retryCount)) cmd, data = json.loads(req.body, object_hook=jsonDecodeObjectHook) funcPath, args, kwargs, env = data if env: if "user" in env and env["user"]: currentSession.get()["user"] = env["user"] if "lang" in env and env["lang"]: currentRequest.get().language = env["lang"] if "transactionMarker" in env: marker = db.Get( db.Key("viur-transactionmarker", env["transactionMarker"])) if not marker: logging.info( "Dropping task, transaction %s did not apply" % env["transactionMarker"]) return else: logging.info("Executing task, transaction %s did succeed" % env["transactionMarker"]) if "custom" in env and conf["viur.tasks.customEnvironmentHandler"]: # Check if we need to restore additional environmental data assert isinstance(conf["viur.tasks.customEnvironmentHandler"], tuple) \ and len(conf["viur.tasks.customEnvironmentHandler"]) == 2 \ and callable(conf["viur.tasks.customEnvironmentHandler"][1]), \ "Your customEnvironmentHandler must be a tuple of two callable if set!" conf["viur.tasks.customEnvironmentHandler"][1](env["custom"]) if cmd == "rel": caller = conf["viur.mainApp"] pathlist = [x for x in funcPath.split("/") if x] for currpath in pathlist: if currpath not in dir(caller): logging.error( "ViUR missed a deferred task! Could not resolve the path %s. " "Failed segment was %s", funcPath, currpath) return caller = getattr(caller, currpath) try: caller(*args, **kwargs) except PermanentTaskFailure: pass except Exception as e: logging.exception(e) raise errors.RequestTimeout() # Task-API should retry elif cmd == "unb": if not funcPath in _deferedTasks: logging.error("ViUR missed a deferred task! %s(%s,%s)", funcPath, args, kwargs) try: _deferedTasks[funcPath](*args, **kwargs) except PermanentTaskFailure: pass except Exception as e: logging.exception(e) raise errors.RequestTimeout() # Task-API should retry