[tor-commits] [stem/master] Allow stem.util.proc.connection() to query by user

atagar at torproject.org atagar at torproject.org
Mon Feb 1 04:21:04 UTC 2016


commit 4082270d068338fa01a433a9006a1073fc4d087b
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Jan 30 12:56:27 2016 -0800

    Allow stem.util.proc.connection() to query by user
    
    Our proc contents provide connection metadata with only two things we can
    filter by: inode and uid. Thus far we've used inodes to allow lookups by pid
    (thing we usually want to resolve connections by), but soon I'm gonna have
    a need for resolution by uid.
    
    This also allows the funciton to provide all system connections if no pid or
    user is provided.
---
 stem/util/connection.py |   2 +-
 stem/util/proc.py       | 133 ++++++++++++++++++++++++++----------------------
 test/unit/util/proc.py  |  47 +++++++++++++----
 3 files changed, 111 insertions(+), 71 deletions(-)

diff --git a/stem/util/connection.py b/stem/util/connection.py
index e72bd12..342afde 100644
--- a/stem/util/connection.py
+++ b/stem/util/connection.py
@@ -202,7 +202,7 @@ def get_connections(resolver, process_pid = None, process_name = None):
         raise IOError("There's multiple processes named '%s'. %s requires a single pid to provide the connections." % (process_name, resolver))
 
   if resolver == Resolver.PROC:
-    return [Connection(*conn) for conn in stem.util.proc.connections(process_pid)]
+    return stem.util.proc.connections(pid = process_pid)
 
   resolver_command = RESOLVER_COMMAND[resolver].format(pid = process_pid)
 
diff --git a/stem/util/proc.py b/stem/util/proc.py
index 7fdd70f..40f9ba8 100644
--- a/stem/util/proc.py
+++ b/stem/util/proc.py
@@ -50,10 +50,12 @@ future, use them at your own risk.**
 import base64
 import os
 import platform
+import pwd
 import socket
 import sys
 import time
 
+import stem.util.connection
 import stem.util.enum
 import stem.util.str_tools
 
@@ -325,35 +327,87 @@ def file_descriptors_used(pid):
     raise IOError('Unable to check number of file descriptors used: %s' % exc)
 
 
-def connections(pid):
+def connections(pid = None, user = None):
   """
-  Queries connection related information from the proc contents. This provides
-  similar results to netstat, lsof, sockstat, and other connection resolution
-  utilities (though the lookup is far quicker).
+  Queries connections from the proc contents. This matches netstat, lsof, and
+  friends but is much faster. If no **pid** or **user** are provided this
+  provides all present connections.
 
-  :param int pid: process id of the process to be queried
+  :param int pid: pid to provide connections for
+  :param str user: username to look up connections for
 
-  :returns: A listing of connection tuples of the form **[(local_ipAddr1,
-    local_port1, foreign_ipAddr1, foreign_port1, protocol, is_ipv6), ...]**
-    (addresses and protocols are strings and ports are ints)
+  :returns: **list** of :class:`~stem.util.connection.Connection` instances
 
   :raises: **IOError** if it can't be determined
   """
 
+  start_time, conn = time.time(), []
+
+  if pid:
+    parameter = 'connections for pid %s' % pid
+
+    try:
+      pid = int(pid)
+
+      if pid < 0:
+        raise IOError("Process pids can't be negative: %s" % pid)
+    except (ValueError, TypeError):
+      raise IOError('Process pid was non-numeric: %s' % pid)
+  elif user:
+    parameter = 'connections for user %s' % user
+  else:
+    parameter = 'all connections'
+
   try:
-    pid = int(pid)
+    inodes = _inodes_for_sockets(pid) if pid else []
+    process_uid = pwd.getpwnam(user).pw_uid if user else None
 
-    if pid < 0:
-      raise IOError("Process pids can't be negative: %s" % pid)
-  except (ValueError, TypeError):
-    raise IOError('Process pid was non-numeric: %s' % pid)
+    for proc_file_path in ('/proc/net/tcp', '/proc/net/tcp6', '/proc/net/udp', '/proc/net/udp6'):
+      if proc_file_path.endswith('6') and not os.path.exists(proc_file_path):
+        continue  # ipv6 proc contents are optional
 
-  if pid == 0:
-    return []
+      try:
+        with open(proc_file_path, 'rb') as proc_file:
+          proc_file.readline()  # skip the first line
+
+          for line in proc_file:
+            _, l_addr, f_addr, status, _, _, _, uid, _, inode = line.split()[:10]
+            protocol = proc_file_path[10:].rstrip('6')  # 'tcp' or 'udp'
+            is_ipv6 = proc_file_path.endswith('6')
+
+            if inodes and inode not in inodes:
+              continue
+            elif process_uid and int(uid) != process_uid:
+              continue
+            elif protocol == 'tcp' and status != b'01':
+              continue  # skip tcp connections that aren't yet established
+
+            local_ip, local_port = _decode_proc_address_encoding(l_addr, is_ipv6)
+            foreign_ip, foreign_port = _decode_proc_address_encoding(f_addr, is_ipv6)
+            conn.append(stem.util.connection.Connection(local_ip, local_port, foreign_ip, foreign_port, protocol, is_ipv6))
+      except IOError as exc:
+        raise IOError("unable to read '%s': %s" % (proc_file_path, exc))
+      except Exception as exc:
+        raise IOError("unable to parse '%s': %s" % (proc_file_path, exc))
+
+    _log_runtime(parameter, '/proc/net/[tcp|udp]', start_time)
+    return conn
+  except IOError as exc:
+    _log_failure(parameter, exc)
+    raise
+
+
+def _inodes_for_sockets(pid):
+  """
+  Provides inodes in use by a process for its sockets.
 
-  # fetches the inode numbers for socket file descriptors
+  :param int pid: process id of the process to be queried
+
+  :returns: **list** with inodes for its sockets
+
+  :raises: **IOError** if it can't be determined
+  """
 
-  start_time, parameter = time.time(), 'process connections'
   inodes = []
 
   try:
@@ -376,50 +430,9 @@ def connections(pid):
         continue  # descriptors may shift while we're in the middle of iterating over them
 
       # most likely couldn't be read due to permissions
-      exc = IOError('unable to determine file descriptor destination (%s): %s' % (exc, fd_path))
-      _log_failure(parameter, exc)
-      raise exc
-
-  if not inodes:
-    # unable to fetch any connections for this process
-    return []
-
-  # check for the connection information from the /proc/net contents
-
-  conn = []
-
-  for proc_file_path in ('/proc/net/tcp', '/proc/net/tcp6', '/proc/net/udp', '/proc/net/udp6'):
-    if not os.path.exists(proc_file_path):
-      continue
-
-    try:
-      with open(proc_file_path, 'rb') as proc_file:
-        proc_file.readline()  # skip the first line
-
-        for line in proc_file:
-          _, l_addr, f_addr, status, _, _, _, _, _, inode = line.split()[:10]
-
-          if inode in inodes:
-            protocol = proc_file_path[10:].rstrip('6')  # 'tcp' or 'udp'
-            is_ipv6 = proc_file_path.endswith('6')
-
-            if protocol == 'tcp' and status != b'01':
-              continue  # skip tcp connections that aren't yet established
-
-            local_ip, local_port = _decode_proc_address_encoding(l_addr, is_ipv6)
-            foreign_ip, foreign_port = _decode_proc_address_encoding(f_addr, is_ipv6)
-            conn.append((local_ip, local_port, foreign_ip, foreign_port, protocol, is_ipv6))
-    except IOError as exc:
-      exc = IOError("unable to read '%s': %s" % (proc_file_path, exc))
-      _log_failure(parameter, exc)
-      raise exc
-    except Exception as exc:
-      exc = IOError("unable to parse '%s': %s" % (proc_file_path, exc))
-      _log_failure(parameter, exc)
-      raise exc
+      raise IOError('unable to determine file descriptor destination (%s): %s' % (exc, fd_path))
 
-  _log_runtime(parameter, '/proc/net/[tcp|udp]', start_time)
-  return conn
+  return inodes
 
 
 def _decode_proc_address_encoding(addr, is_ipv6):
diff --git a/test/unit/util/proc.py b/test/unit/util/proc.py
index ee52d21..a828981 100644
--- a/test/unit/util/proc.py
+++ b/test/unit/util/proc.py
@@ -6,6 +6,7 @@ import io
 import unittest
 
 from stem.util import proc
+from stem.util.connection import Connection
 from test import mocking
 
 try:
@@ -225,12 +226,9 @@ class TestProc(unittest.TestCase):
       '/proc/net/udp': io.BytesIO(udp)
     }[param]
 
-    # tests the edge case of pid = 0
-    self.assertEqual([], proc.connections(0))
-
     expected_results = [
-      ('17.17.17.17', 4369, '34.34.34.34', 8738, 'tcp', False),
-      ('187.187.187.187', 48059, '204.204.204.204', 52428, 'udp', False),
+      Connection('17.17.17.17', 4369, '34.34.34.34', 8738, 'tcp', False),
+      Connection('187.187.187.187', 48059, '204.204.204.204', 52428, 'udp', False),
     ]
 
     self.assertEqual(expected_results, proc.connections(pid))
@@ -256,19 +254,48 @@ class TestProc(unittest.TestCase):
     }[param]
 
     path_exists_mock.side_effect = lambda param: {
-      '/proc/net/tcp': False,
       '/proc/net/tcp6': True,
-      '/proc/net/udp': False,
       '/proc/net/udp6': False
     }[param]
 
     open_mock.side_effect = lambda param, mode: {
+      '/proc/net/tcp': io.BytesIO(''),
       '/proc/net/tcp6': io.BytesIO(TCP6_CONTENT),
+      '/proc/net/udp': io.BytesIO(''),
     }[param]
 
     expected_results = [
-      ('2a01:4f8:190:514a::2', 443, '2001:638:a000:4140::ffff:189', 40435, 'tcp', True),
-      ('2a01:4f8:190:514a::2', 443, '2001:858:2:2:aabb:0:563b:1526', 44469, 'tcp', True),
+      Connection('2a01:4f8:190:514a::2', 443, '2001:638:a000:4140::ffff:189', 40435, 'tcp', True),
+      Connection('2a01:4f8:190:514a::2', 443, '2001:858:2:2:aabb:0:563b:1526', 44469, 'tcp', True),
     ]
 
-    self.assertEqual(expected_results, proc.connections(pid))
+    self.assertEqual(expected_results, proc.connections(pid = pid))
+
+  @patch('os.path.exists')
+  @patch('pwd.getpwnam')
+  @patch('stem.util.proc.open', create = True)
+  def test_connections_ipv6_by_user(self, open_mock, getpwnam_mock, path_exists_mock):
+    """
+    Tests the connections function with ipv6 addresses.
+    """
+
+    getpwnam_mock('me').pw_uid = 106
+
+    path_exists_mock.side_effect = lambda param: {
+      '/proc/net/tcp6': True,
+      '/proc/net/udp6': False
+    }[param]
+
+    open_mock.side_effect = lambda param, mode: {
+      '/proc/net/tcp': io.BytesIO(''),
+      '/proc/net/tcp6': io.BytesIO(TCP6_CONTENT),
+      '/proc/net/udp': io.BytesIO(''),
+    }[param]
+
+    expected_results = [
+      Connection('::ffff:5.9.158.75', 5222, '::ffff:78.54.134.33', 38330, 'tcp', True),
+      Connection('2a01:4f8:190:514a::2', 5269, '2001:6f8:126f:11::26', 50594, 'tcp', True),
+      Connection('::ffff:5.9.158.75', 5222, '::ffff:78.54.134.33', 38174, 'tcp', True),
+    ]
+
+    self.assertEqual(expected_results, proc.connections(user = 'me'))





More information about the tor-commits mailing list