def _do_login(self, req): """Log the remote user in. This function expects to be called when the remote user name is available. The user name is inserted into the `auth_cookie` table and a cookie identifying the user on subsequent requests is sent back to the client. If the Authenticator was created with `ignore_case` set to true, then the authentication name passed from the web server in req.remote_user will be converted to lower case before being used. This is to avoid problems on installations authenticating against Windows which is not case sensitive regarding user names and domain names """ if not req.remote_user: # TRANSLATOR: ... refer to the 'installation documentation'. (link) inst_doc = tag.a(_("installation documentation"), title=_("Configuring Authentication"), href=req.href.wiki('TracInstall') + "#ConfiguringAuthentication") raise TracError(tag_("Authentication information not available. " "Please refer to the %(inst_doc)s.", inst_doc=inst_doc)) remote_user = req.remote_user if self.ignore_case: remote_user = remote_user.lower() if req.authname not in ('anonymous', remote_user): raise TracError(_("Already logged in as %(user)s.", user=req.authname)) with self.env.db_transaction as db: # Delete cookies older than 10 days db("DELETE FROM auth_cookie WHERE time < %s", (int(time_now()) - 86400 * 10,)) # Insert a new cookie if we haven't already got one cookie = None trac_auth = req.incookie.get('trac_auth') if trac_auth is not None: name = self._cookie_to_name(req, trac_auth) cookie = trac_auth.value if name == remote_user else None if cookie is None: cookie = hex_entropy() db(""" INSERT INTO auth_cookie (cookie, name, ipnr, time) VALUES (%s, %s, %s, %s) """, (cookie, remote_user, req.remote_addr, int(time_now()))) req.authname = remote_user req.outcookie['trac_auth'] = cookie if self.auth_cookie_domain: req.outcookie['trac_auth']['domain'] = self.auth_cookie_domain req.outcookie['trac_auth']['path'] = self.auth_cookie_path \ or req.base_path or '/' if self.env.secure_cookies: req.outcookie['trac_auth']['secure'] = True req.outcookie['trac_auth']['httponly'] = True if self.auth_cookie_lifetime > 0: req.outcookie['trac_auth']['expires'] = self.auth_cookie_lifetime
def send(self, from_addr, recipients, message): global local_hostname # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port, local_hostname) local_hostname = server.local_hostname except smtplib.socket.error as e: raise ConfigurationError( tag_( "SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.code("[notification] smtp_server"), option2=tag.code("[notification] smtp_port"))) # server.set_debuglevel(True) if self.use_tls: server.ehlo() if 'starttls' not in server.esmtp_features: raise TracError( _("TLS enabled but server does not support" " TLS")) server.starttls() server.ehlo() if self.smtp_user: server.login(self.smtp_user.encode('utf-8'), self.smtp_password.encode('utf-8')) start = time_now() server.sendmail(from_addr, recipients, message) t = time_now() - start if t > 5: self.log.warning( "Slow mail submission (%.2f s), " "check your mail setup", t) if self.use_tls: # avoid false failure detection when the server closes # the SMTP connection with TLS enabled import socket try: server.quit() except socket.sslerror: pass else: server.quit()
def test_purge_anonymous_session(self): """ Verify that old sessions get purged. """ with self.env.db_transaction as db: db("INSERT INTO session VALUES ('123456', 0, %s)", (0, )) db("INSERT INTO session VALUES ('987654', 0, %s)", (int(time_now() - PURGE_AGE - 3600), )) db(""" INSERT INTO session_attribute VALUES ('987654', 0, 'foo', 'bar') """) # We need to modify a different session to trigger the purging req = MockRequest(self.env, authname='anonymous') req.incookie['trac_session'] = '123456' session = Session(self.env, req) session['foo'] = 'bar' session.save() self.assertEqual( 0, self.env.db_query(""" SELECT COUNT(*) FROM session WHERE sid='987654' AND authenticated=0 """)[0][0])
def test_delete_empty_session(self): """ Verify that a session gets deleted when it doesn't have any data except for the 'last_visit' timestamp. """ now = time_now() # Make sure the session has data so that it doesn't get dropped with self.env.db_transaction as db: db("INSERT INTO session VALUES ('123456', 0, %s)", (int(now - UPDATE_INTERVAL - 3600), )) db(""" INSERT INTO session_attribute VALUES ('123456', 0, 'foo', 'bar') """) req = MockRequest(self.env, authname='anonymous') req.incookie['trac_session'] = '123456' session = Session(self.env, req) del session['foo'] session.save() self.assertEqual( 0, self.env.db_query(""" SELECT COUNT(*) FROM session WHERE sid='123456' AND authenticated=0 """)[0][0])
def test_update_session(self): """ Verify that accessing a session after one day updates the sessions 'last_visit' variable so that the session doesn't get purged. """ now = time_now() # Make sure the session has data so that it doesn't get dropped with self.env.db_transaction as db: db("INSERT INTO session VALUES ('123456', 0, 1)") db(""" INSERT INTO session_attribute VALUES ('123456', 0, 'foo', 'bar') """) req = MockRequest(self.env, authname='anonymous') req.incookie['trac_session'] = '123456' session = Session(self.env, req) session['modified'] = True session.save() # updating does require modifications self.assertEqual(PURGE_AGE, req.outcookie['trac_session']['expires']) self.assertAlmostEqual( now, int( self.env.db_query(""" SELECT last_visit FROM session WHERE sid='123456' AND authenticated=0 """)[0][0]), -1)
def _purge_anonymous_session(self): now = int(time_now()) lifetime = 90 * 86400 # default lifetime with self.env.db_transaction as db: db.executemany("INSERT INTO session VALUES (%s, 0, %s)", [('123456', 0), ('987654', now - lifetime - 3600), ('876543', now - lifetime + 3600), ('765432', now - 3600)]) db.executemany( """ INSERT INTO session_attribute VALUES (%s, 0, 'foo', 'bar') """, [('987654', ), ('876543', ), ('765432', )]) with self.env.db_transaction as db: # We need to modify a different session to trigger the purging req = MockRequest(self.env, authname='anonymous') req.incookie['trac_session'] = '123456' session = Session(self.env, req) session['foo'] = 'bar' session.save() return [ row[0] for row in self.env.db_query(""" SELECT sid FROM session WHERE authenticated=0 ORDER BY sid """) ]
def get_users_with_permission(self, permission): """Return all users that have the specified permission. Users are returned as a list of user names. """ now = time_now() if now - self.last_reap > self.CACHE_REAP_TIME: self.permission_cache = {} self.last_reap = now timestamp, permissions = self.permission_cache.get( permission, (0, None)) if now - timestamp <= self.CACHE_EXPIRY: return permissions parent_map = {} for parent, children in self.get_actions_dict().iteritems(): for child in children: parent_map.setdefault(child, set()).add(parent) satisfying_perms = set() def append_with_parents(action): if action not in satisfying_perms: satisfying_perms.add(action) for action in parent_map.get(action, ()): append_with_parents(action) append_with_parents(permission) perms = self.store.get_users_with_permissions(satisfying_perms) or [] self.permission_cache[permission] = (now, perms) return perms
def urandom(n): result = [] hasher = hashlib.sha1(str(os.getpid()) + str(time_now())) while len(result) * hasher.digest_size < n: hasher.update(str(_entropy.random())) result.append(hasher.digest()) result = ''.join(result) return result[:n] if len(result) > n else result
def test_touch_changes_mtime(self): """Test that each touch command changes the file modification time.""" config = self._read() time.sleep(1.0 - time_now() % 1.0) config.touch() mtime = os.stat(self.filename).st_mtime config.touch() self.assertNotEqual(mtime, os.stat(self.filename).st_mtime)
def check_permission(self, action, username, resource, perm): now = time_now() if now - self.last_reap > self.CACHE_REAP_TIME: self.permission_cache = {} self.last_reap = time_now() timestamp, permissions = self.permission_cache.get(username, (0, None)) # Cache hit? if now - timestamp > self.CACHE_EXPIRY: # No, pull permissions from database. permissions = PermissionSystem(self.env). \ get_user_permissions(username) self.permission_cache[username] = (now, permissions) return action in permissions or None
def pre_process_request(self, req, handler): from trac.web.chrome import Chrome, add_warning if handler is not Chrome(self.env): for reponame in self.repository_sync_per_request: start = time_now() if is_default(reponame): reponame = '' try: repo = self.get_repository(reponame) if repo: repo.sync() else: self.log.warning( "Unable to find repository '%s' for " "synchronization", reponame or '(default)') continue except TracError, e: add_warning( req, _( "Can't synchronize with repository \"%(name)s\" " "(%(error)s). Look in the Trac log for more " "information.", name=reponame or '(default)', error=to_unicode(e))) except Exception, e: add_warning( req, _( "Failed to sync with repository \"%(name)s\": " "%(error)s; repository information may be out of " "date. Look in the Trac log for more information " "including mitigation strategies.", name=reponame or '(default)', error=to_unicode(e))) self.log.error( "Failed to sync with repository \"%s\"; You may be " "able to reduce the impact of this issue by " "configuring [trac] repository_sync_per_request; see " "http://trac.edgewall.org/wiki/TracRepositoryAdmin" "#ExplicitSync for more detail: %s", reponame or '(default)', exception_to_unicode(e, traceback=True)) self.log.info("Synchronized '%s' repository in %0.2f seconds", reponame or '(default)', time_now() - start)
def urandom(n): result = [] hasher = hashlib.sha1('{}{}'.format(os.getpid(), time_now()).encode('utf-8')) while len(result) * hasher.digest_size < n: hasher.update(str(_entropy.random()).encode('utf-8')) result.append(hasher.digest()) result = b''.join(result) return result[:n] if len(result) > n else result
def promote_session(self, sid): """Promotes an anonymous session to an authenticated session, if there is no preexisting session data for that user name. """ assert self.req.is_authenticated, \ "Cannot promote session of anonymous user" with self.env.db_transaction as db: authenticated_flags = [ authenticated for authenticated, in db( "SELECT authenticated FROM session WHERE sid=%s OR sid=%s", (sid, self.req.authname)) ] if len(authenticated_flags) == 2: # There's already an authenticated session for the user, # we simply delete the anonymous session db("DELETE FROM session WHERE sid=%s AND authenticated=0", (sid, )) db( """DELETE FROM session_attribute WHERE sid=%s AND authenticated=0 """, (sid, )) elif len(authenticated_flags) == 1: if not authenticated_flags[0]: # Update the anonymous session records so the session ID # becomes the user name, and set the authenticated flag. self.env.log.debug( "Promoting anonymous session %s to " "authenticated session for user %s", sid, self.req.authname) db( """UPDATE session SET sid=%s, authenticated=1 WHERE sid=%s AND authenticated=0 """, (self.req.authname, sid)) db( """UPDATE session_attribute SET sid=%s, authenticated=1 WHERE sid=%s """, (self.req.authname, sid)) else: # We didn't have an anonymous session for this sid. The # authenticated session might have been inserted between the # SELECT above and here, so we catch the error. try: db( """INSERT INTO session (sid, last_visit, authenticated) VALUES (%s, %s, 1) """, (self.req.authname, int(time_now()))) except self.env.db_exc.IntegrityError: self.env.log.warning( 'Authenticated session for %s ' 'already exists', self.req.authname) db.rollback() self._new = False self.sid = sid self.bake_cookie(0) # expire the cookie
def test_save_changes_mtime(self): """Test that each save operation changes the file modification time.""" class Foo(object): IntOption('section', 'option', 1) sconfig = self._read() sconfig.set_defaults() sconfig.save() rconfig = self._read() self.assertEqual(1, rconfig.getint('section', 'option')) sconfig.set('section', 'option', 2) time.sleep(1.0 - time_now() % 1.0) sconfig.save() rconfig.parse_if_needed() self.assertEqual(2, rconfig.getint('section', 'option'))
def get_session(self, sid, authenticated=False): refresh_cookie = False if not authenticated and not self._valid_sid_re.match(sid): raise TracValueError(_("Session ID must be alphanumeric.")) if self.sid and sid != self.sid: refresh_cookie = True super(Session, self).get_session(sid, authenticated) if self.last_visit and time_now() - self.last_visit > UPDATE_INTERVAL: refresh_cookie = True # Refresh the session cookie if this is the first visit after a day if not authenticated and refresh_cookie: self.bake_cookie()
def shutdown(self, tid=None): """Close pooled connections not used in a while""" delay = 120 if tid is None: delay = 0 when = time_now() - delay with self._available: if tid is None: # global shutdown, also close active connections for db, num in self._active.values(): db.close() self._active = {} while self._pool_time and self._pool_time[0] <= when: db = self._pool.pop(0) db.close() self._pool_key.pop(0) self._pool_time.pop(0)
def _do_add(self, sid, name=None, email=None): sid, authenticated = self._split_sid(sid) with self.env.db_transaction as db: try: db("INSERT INTO session VALUES (%s, %s, %s)", (sid, authenticated, int(time_now()))) except Exception: raise AdminCommandError( _("Session '%(sid)s' already exists", sid=sid)) if name: db("INSERT INTO session_attribute VALUES (%s,%s,'name',%s)", (sid, authenticated, name)) if email: db("INSERT INTO session_attribute VALUES (%s,%s,'email',%s)", (sid, authenticated, email)) self.env.invalidate_known_users_cache()
def _return_cnx(self, cnx, key, tid): # Decrement active refcount, clear slot if 1 with self._available: assert (tid, key) in self._active cnx, num = self._active[(tid, key)] if num == 1: del self._active[(tid, key)] else: self._active[(tid, key)] = (cnx, num - 1) if num == 1: # Reset connection outside of critical section try: cnx.rollback() # resets the connection except Exception: cnx.close() cnx = None # Connection available, from reuse or from creation of a new one with self._available: if cnx and cnx.poolable: self._pool.append(cnx) self._pool_key.append(key) self._pool_time.append(time_now()) self._available.notify()
def insert_users(self, users): """Insert a tuple representing a user session to the `session` and `session_attributes` tables. The tuple can be length 3 with entries username, name and email, in which case an authenticated user is assumed. The tuple can also be length 4, with the last entry specifying `1` for an authenticated user or `0` for an unauthenticated user. """ with self.db_transaction as db: for row in users: if len(row) == 3: username, name, email = row authenticated = 1 else: # len(row) == 4 username, name, email, authenticated = row db("INSERT INTO session VALUES (%s, %s, %s)", (username, authenticated, int(time_now()))) db("INSERT INTO session_attribute VALUES (%s,%s,'name',%s)", (username, authenticated, name)) db("INSERT INTO session_attribute VALUES (%s,%s,'email',%s)", (username, authenticated, email))
class SmtpEmailSender(Component): """E-mail sender connecting to an SMTP server.""" implements(IEmailSender) smtp_server = Option('notification', 'smtp_server', 'localhost', """SMTP server hostname to use for email notifications.""") smtp_port = IntOption('notification', 'smtp_port', 25, """SMTP server port to use for email notification.""") smtp_user = Option('notification', 'smtp_user', '', """Username for SMTP server. (''since 0.9'')""") smtp_password = Option('notification', 'smtp_password', '', """Password for SMTP server. (''since 0.9'')""") use_tls = BoolOption('notification', 'use_tls', 'false', """Use SSL/TLS to send notifications over SMTP. (''since 0.10'')""") def send(self, from_addr, recipients, message): global local_hostname # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port, local_hostname) local_hostname = server.local_hostname except smtplib.socket.error, e: raise ConfigurationError( tag_("SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.tt("[notification] smtp_server"), option2=tag.tt("[notification] smtp_port"))) # server.set_debuglevel(True) if self.use_tls: server.ehlo() if 'starttls' not in server.esmtp_features: raise TracError(_("TLS enabled but server does not support " "TLS")) server.starttls() server.ehlo() if self.smtp_user: server.login(self.smtp_user.encode('utf-8'), self.smtp_password.encode('utf-8')) start = time_now() server.sendmail(from_addr, recipients, message) t = time_now() - start if t > 5: self.log.warning('Slow mail submission (%.2f s), ' 'check your mail setup', t) if self.use_tls: # avoid false failure detection when the server closes # the SMTP connection with TLS enabled import socket try: server.quit() except socket.sslerror: pass else: server.quit()
def __init__(self): self.permission_cache = {} self.last_reap = time_now()
def _build_rev_cache(self, refs): self.logger.debug("triggered rebuild of commit tree db for '%s'", self.repo_path) ts0 = time_now() new_db = {} # db new_sdb = {} # short_rev db # helper for reusing strings revs_seen = {} def _rev_reuse(rev): return revs_seen.setdefault(rev, rev) refs = {refname: _rev_reuse(rev) for refname, rev in refs.iteritems()} head_revs = { rev for refname, rev in refs.iteritems() if refname.startswith('refs/heads/') } rev_list = [ map(_rev_reuse, line.split()) for line in self.repo.rev_list( '--parents', '--topo-order', '--all').splitlines() ] revs_seen = None if rev_list: # first rev seen is assumed to be the youngest one youngest = rev_list[0][0] # last rev seen is assumed to be the oldest one oldest = rev_list[-1][0] else: youngest = oldest = None rheads_seen = {} def _rheads_reuse(rheads): rheads = frozenset(rheads) return rheads_seen.setdefault(rheads, rheads) __rev_key = self.__rev_key for ord_rev, revs in enumerate(rev_list): rev = revs[0] parents = revs[1:] # shortrev "hash" map new_sdb.setdefault(__rev_key(rev), []).append(rev) # new_db[rev] = (children(rev), parents(rev), # ordinal_id(rev), rheads(rev)) if rev in new_db: # (incomplete) entry was already created by children _children, _parents, _ord_rev, _rheads = new_db[rev] assert _children assert not _parents assert _ord_rev == 0 else: # new entry _children = set() _rheads = set() if rev in head_revs: _rheads.add(rev) # create/update entry # transform into frozenset and tuple since entry will be final new_db[rev] = (frozenset(_children), tuple(parents), ord_rev + 1, _rheads_reuse(_rheads)) # update parents(rev)s for parent in parents: # by default, a dummy ordinal_id is used for the mean-time _children, _parents, _ord_rev, _rheads2 = \ new_db.setdefault(parent, (set(), [], 0, set())) # update parent(rev)'s children _children.add(rev) # update parent(rev)'s rheads _rheads2.update(_rheads) rheads_seen = None # convert sdb either to dict or array depending on size tmp = [()] * (max(new_sdb.keys()) + 1) if len(new_sdb) > 5000 else {} try: while True: k, v = new_sdb.popitem() tmp[k] = tuple(v) except KeyError: pass assert len(new_sdb) == 0 new_sdb = tmp rev_cache = self.RevCache(youngest, oldest, new_db, refs, new_sdb) self.logger.debug( "rebuilt commit tree db for '%s' with %d entries " "(took %.1f ms)", self.repo_path, len(new_db), 1000 * (time_now() - ts0)) return rev_cache
def get_cnx(self, connector, kwargs, timeout=None): cnx = None log = kwargs.get('log') key = unicode(kwargs) start = time_now() tid = get_thread_id() # Get a Connection, either directly or a deferred one with self._available: # First choice: Return the same cnx already used by the thread if (tid, key) in self._active: cnx, num = self._active[(tid, key)] num += 1 else: if self._waiters == 0: cnx = self._take_cnx(connector, kwargs, key, tid) if not cnx: self._waiters += 1 self._available.wait() self._waiters -= 1 cnx = self._take_cnx(connector, kwargs, key, tid) num = 1 if cnx: self._active[(tid, key)] = (cnx, num) deferred = num == 1 and isinstance(cnx, tuple) exc_info = (None, None, None) if deferred: # Potentially lengthy operations must be done without lock held op, cnx = cnx try: if op == 'ping': cnx.ping() elif op == 'close': cnx.close() if op in ('close', 'create'): cnx = connector.get_connection(**kwargs) except TracError: exc_info = sys.exc_info() cnx = None except Exception: exc_info = sys.exc_info() if log: log.error('Exception caught on %s', op, exc_info=True) cnx = None if cnx and not isinstance(cnx, tuple): if deferred: # replace placeholder with real Connection with self._available: self._active[(tid, key)] = (cnx, num) return PooledConnection(self, cnx, key, tid, log) if deferred: # cnx couldn't be reused, clear placeholder with self._available: del self._active[(tid, key)] if op == 'ping': # retry return self.get_cnx(connector, kwargs) # if we didn't get a cnx after wait(), something's fishy... if isinstance(exc_info[1], TracError): raise exc_info[0], exc_info[1], exc_info[2] timeout = time_now() - start errmsg = _( "Unable to get database connection within %(time)d seconds.", time=timeout) if exc_info[1]: errmsg += " (%s)" % exception_to_unicode(exc_info[1]) raise TimeoutError(errmsg)
def save(self): authenticated = int(self.authenticated) now = int(time_now()) items = self.items() if not authenticated and not self._old and not items: # The session for anonymous doesn't have associated data, # so there's no need to persist it return # We can't do the session management in one big transaction, # as the intertwined changes to both the session and # session_attribute tables are prone to deadlocks (#9705). # Therefore we first we save the current session, then we # eventually purge the tables. session_saved = False with self.env.db_transaction as db: # Try to save the session if it's a new one. A failure to # do so is not critical but we nevertheless skip the # following steps. new = self._new if new: self.last_visit = now self._new = False # The session might already exist even if _new is True since # it could have been created by a concurrent request (#3563). try: db( """INSERT INTO session (sid, last_visit, authenticated) VALUES (%s,%s,%s) """, (self.sid, self.last_visit, authenticated)) except self.env.db_exc.IntegrityError: self.env.log.warning('Session %s already exists', self.sid) db.rollback() return if authenticated and \ (new or self._old.get('name') != self.get('name') or \ self._old.get('email') != self.get('email')): self.env.invalidate_known_users_cache() # Remove former values for session_attribute and save the # new ones. The last concurrent request to do so "wins". if self._old != self: if not items and not authenticated: # No need to keep around empty unauthenticated sessions db("DELETE FROM session WHERE sid=%s AND authenticated=0", (self.sid, )) db( """DELETE FROM session_attribute WHERE sid=%s AND authenticated=%s """, (self.sid, authenticated)) self._old = dict(self.items()) # The session variables might already have been updated by a # concurrent request. try: db.executemany( """ INSERT INTO session_attribute (sid,authenticated,name,value) VALUES (%s,%s,%s,%s) """, [(self.sid, authenticated, k, v) for k, v in items]) except self.env.db_exc.IntegrityError: self.env.log.warning( 'Attributes for session %s already ' 'updated', self.sid) db.rollback() return session_saved = True # Purge expired sessions. We do this only when the session was # changed as to minimize the purging. if session_saved and now - self.last_visit > UPDATE_INTERVAL: self.last_visit = now lifetime = self.env.anonymous_session_lifetime mintime = now - lifetime * 86400 if lifetime > 0 else None with self.env.db_transaction as db: # Update the session last visit time if it is over an # hour old, so that session doesn't get purged self.env.log.info("Refreshing session %s", self.sid) db( """UPDATE session SET last_visit=%s WHERE sid=%s AND authenticated=%s """, (self.last_visit, self.sid, authenticated)) if mintime: self.env.log.debug('Purging old, expired, sessions.') db( """DELETE FROM session_attribute WHERE authenticated=0 AND sid IN ( SELECT sid FROM session WHERE authenticated=0 AND last_visit < %s ) """, (mintime, )) # Avoid holding locks on lot of rows on both session_attribute # and session tables if mintime: with self.env.db_transaction as db: db( """ DELETE FROM session WHERE authenticated=0 AND last_visit < %s """, (mintime, ))