public inbox for gentoo-commits@lists.gentoo.org
 help / color / mirror / Atom feed
From: "Zac Medico" <zmedico@gentoo.org>
To: gentoo-commits@lists.gentoo.org
Subject: [gentoo-commits] proj/portage:master commit in: lib/portage/util/, lib/portage/tests/util/
Date: Wed, 16 Jan 2019 08:33:12 +0000 (UTC)	[thread overview]
Message-ID: <1547624939.035582f0e31c071606635aac9cc4ba4b411612e7.zmedico@gentoo> (raw)

commit:     035582f0e31c071606635aac9cc4ba4b411612e7
Author:     Zac Medico <zmedico <AT> gentoo <DOT> org>
AuthorDate: Mon Jan 14 08:11:57 2019 +0000
Commit:     Zac Medico <zmedico <AT> gentoo <DOT> org>
CommitDate: Wed Jan 16 07:48:59 2019 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=035582f0

tests: add unit test for portage.util.socks5 (FEATURES=network-sandbox-proxy)

Bug: https://bugs.gentoo.org/604474
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>

 lib/portage/tests/util/test_socks5.py | 211 ++++++++++++++++++++++++++++++++++
 lib/portage/util/socks5.py            |  48 +++++++-
 2 files changed, 256 insertions(+), 3 deletions(-)

diff --git a/lib/portage/tests/util/test_socks5.py b/lib/portage/tests/util/test_socks5.py
new file mode 100644
index 000000000..5db85b0a6
--- /dev/null
+++ b/lib/portage/tests/util/test_socks5.py
@@ -0,0 +1,211 @@
+# Copyright 2019 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+import functools
+import platform
+import shutil
+import socket
+import struct
+import sys
+import tempfile
+import time
+
+import portage
+from portage.tests import TestCase
+from portage.util._eventloop.global_event_loop import global_event_loop
+from portage.util import socks5
+from portage.const import PORTAGE_BIN_PATH
+
+try:
+	from http.server import BaseHTTPRequestHandler, HTTPServer
+except ImportError:
+	from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+
+try:
+	from urllib.request import urlopen
+except ImportError:
+	from urllib import urlopen
+
+
+class _Handler(BaseHTTPRequestHandler):
+
+	def __init__(self, content, *args, **kwargs):
+		self.content = content
+		BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
+
+	def do_GET(self):
+		doc = self.send_head()
+		if doc is not None:
+			self.wfile.write(doc)
+
+	def do_HEAD(self):
+		self.send_head()
+
+	def send_head(self):
+		doc = self.content.get(self.path)
+		if doc is None:
+			self.send_error(404, "File not found")
+			return None
+
+		self.send_response(200)
+		self.send_header("Content-type", "text/plain")
+		self.send_header("Content-Length", len(doc))
+		self.send_header("Last-Modified", self.date_time_string(time.time()))
+		self.end_headers()
+		return doc
+
+	def log_message(self, fmt, *args):
+		pass
+
+
+class AsyncHTTPServer(object):
+	def __init__(self, host, content, loop):
+		self._host = host
+		self._content = content
+		self._loop = loop
+		self.server_port = None
+		self._httpd = None
+
+	def __enter__(self):
+		httpd = self._httpd = HTTPServer((self._host, 0), functools.partial(_Handler, self._content))
+		self.server_port = httpd.server_port
+		self._loop.add_reader(httpd.socket.fileno(), self._httpd._handle_request_noblock)
+		return self
+
+	def __exit__(self, exc_type, exc_value, exc_traceback):
+		if self._httpd is not None:
+			self._loop.remove_reader(self._httpd.socket.fileno())
+			self._httpd.socket.close()
+			self._httpd = None
+
+
+class AsyncHTTPServerTestCase(TestCase):
+
+	@staticmethod
+	def _fetch_directly(host, port, path):
+		# NOTE: python2.7 does not have context manager support here
+		try:
+			f = urlopen('http://{host}:{port}{path}'.format( # nosec
+				host=host, port=port, path=path))
+			return f.read()
+		finally:
+			if f is not None:
+				f.close()
+
+	def test_http_server(self):
+		host = '127.0.0.1'
+		content = b'Hello World!\n'
+		path = '/index.html'
+		loop = global_event_loop()
+		for i in range(2):
+			with AsyncHTTPServer(host, {path: content}, loop) as server:
+				for j in range(2):
+					result = loop.run_until_complete(loop.run_in_executor(None,
+						self._fetch_directly, host, server.server_port, path))
+					self.assertEqual(result, content)
+
+
+class _socket_file_wrapper(portage.proxy.objectproxy.ObjectProxy):
+	"""
+	A file-like object that wraps a socket and closes the socket when
+	closed. Since python2.7 does not support socket.detach(), this is a
+	convenient way to have a file attached to a socket that closes
+	automatically (without resource warnings about unclosed sockets).
+	"""
+
+	__slots__ = ('_file', '_socket')
+
+	def __init__(self, socket, f):
+		object.__setattr__(self, '_socket', socket)
+		object.__setattr__(self, '_file', f)
+
+	def _get_target(self):
+		return object.__getattribute__(self, '_file')
+
+	def __getattribute__(self, attr):
+		if attr == 'close':
+			return object.__getattribute__(self, 'close')
+		return super(_socket_file_wrapper, self).__getattribute__(attr)
+
+	def __enter__(self):
+		return self
+
+	def close(self):
+		object.__getattribute__(self, '_file').close()
+		object.__getattribute__(self, '_socket').close()
+
+	def __exit__(self, exc_type, exc_value, traceback):
+		self.close()
+
+
+def socks5_http_get_ipv4(proxy, host, port, path):
+	"""
+	Open http GET request via socks5 proxy listening on a unix socket,
+	and return a file to read the response body from.
+	"""
+	s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+	f = _socket_file_wrapper(s, s.makefile('rb', 1024))
+	try:
+		s.connect(proxy)
+		s.send(struct.pack('!BBB', 0x05, 0x01, 0x00))
+		vers, method = struct.unpack('!BB', s.recv(2))
+		s.send(struct.pack('!BBBB', 0x05, 0x01, 0x00, 0x01))
+		s.send(socket.inet_pton(socket.AF_INET, host))
+		s.send(struct.pack('!H', port))
+		reply = struct.unpack('!BBB', s.recv(3))
+		if reply != (0x05, 0x00, 0x00):
+			raise AssertionError(repr(reply))
+		struct.unpack('!B4sH', s.recv(7)) # contains proxied address info
+		s.send("GET {} HTTP/1.1\r\nHost: {}:{}\r\nAccept: */*\r\nConnection: close\r\n\r\n".format(
+			path, host, port).encode())
+		headers = []
+		while True:
+			headers.append(f.readline())
+			if headers[-1] == b'\r\n':
+				return f
+	except Exception:
+		f.close()
+		raise
+
+
+class Socks5ServerTestCase(TestCase):
+
+	@staticmethod
+	def _fetch_via_proxy(proxy, host, port, path):
+		with socks5_http_get_ipv4(proxy, host, port, path) as f:
+			return f.read()
+
+	def test_socks5_proxy(self):
+
+		loop = global_event_loop()
+
+		host = '127.0.0.1'
+		content = b'Hello World!'
+		path = '/index.html'
+		proxy = None
+		tempdir = tempfile.mkdtemp()
+
+		try:
+			with AsyncHTTPServer(host, {path: content}, loop) as server:
+
+				settings = {
+					'PORTAGE_TMPDIR': tempdir,
+					'PORTAGE_BIN_PATH': PORTAGE_BIN_PATH,
+				}
+
+				try:
+					proxy = socks5.get_socks5_proxy(settings)
+				except NotImplementedError:
+					# bug 658172 for python2.7
+					self.skipTest('get_socks5_proxy not implemented for {} {}.{}'.format(
+						platform.python_implementation(), *sys.version_info[:2]))
+				else:
+					loop.run_until_complete(socks5.proxy.ready())
+
+					result = loop.run_until_complete(loop.run_in_executor(None,
+						self._fetch_via_proxy, proxy, host, server.server_port, path))
+
+					self.assertEqual(result, content)
+		finally:
+			socks5.proxy.stop()
+			shutil.rmtree(tempdir)

diff --git a/lib/portage/util/socks5.py b/lib/portage/util/socks5.py
index 74b0714eb..59e6699ec 100644
--- a/lib/portage/util/socks5.py
+++ b/lib/portage/util/socks5.py
@@ -1,13 +1,18 @@
 # SOCKSv5 proxy manager for network-sandbox
-# Copyright 2015 Gentoo Foundation
+# Copyright 2015-2019 Gentoo Authors
 # Distributed under the terms of the GNU General Public License v2
 
+import errno
 import os
 import signal
+import socket
 
+import portage.data
 from portage import _python_interpreter
 from portage.data import portage_gid, portage_uid, userpriv_groups
 from portage.process import atexit_register, spawn
+from portage.util.futures.compat_coroutine import coroutine
+from portage.util.futures import asyncio
 
 
 class ProxyManager(object):
@@ -36,9 +41,16 @@ class ProxyManager(object):
 		self.socket_path = os.path.join(settings['PORTAGE_TMPDIR'],
 				'.portage.%d.net.sock' % os.getpid())
 		server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py')
+		spawn_kwargs = {}
+		# The portage_uid check solves EPERM failures in Travis CI.
+		if portage.data.secpass > 1 and os.geteuid() != portage_uid:
+			spawn_kwargs.update(
+				uid=portage_uid,
+				gid=portage_gid,
+				groups=userpriv_groups,
+				umask=0o077)
 		self._pids = spawn([_python_interpreter, server_bin, self.socket_path],
-				returnpid=True, uid=portage_uid, gid=portage_gid,
-				groups=userpriv_groups, umask=0o077)
+				returnpid=True, **spawn_kwargs)
 
 	def stop(self):
 		"""
@@ -60,6 +72,36 @@ class ProxyManager(object):
 		return self.socket_path is not None
 
 
+	@coroutine
+	def ready(self):
+		"""
+		Wait for the proxy socket to become ready. This method is a coroutine.
+		"""
+
+		while True:
+			try:
+				wait_retval = os.waitpid(self._pids[0], os.WNOHANG)
+			except OSError as e:
+				if e.errno == errno.EINTR:
+					continue
+				raise
+
+			if wait_retval is not None and wait_retval != (0, 0):
+				raise OSError(3, 'No such process')
+
+			try:
+				s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+				s.connect(self.socket_path)
+			except EnvironmentError as e:
+				if e.errno != errno.ENOENT:
+					raise
+				yield asyncio.sleep(0.2)
+			else:
+				break
+			finally:
+				s.close()
+
+
 proxy = ProxyManager()
 
 


             reply	other threads:[~2019-01-16  8:33 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-01-16  8:33 Zac Medico [this message]
  -- strict thread matches above, loose matches on Subject: below --
2022-06-07 23:48 [gentoo-commits] proj/portage:master commit in: lib/portage/util/, lib/portage/tests/util/ Mike Gilbert
2022-06-07 23:48 Mike Gilbert
2022-06-07 23:48 Mike Gilbert
2022-06-07 23:48 Mike Gilbert
2022-12-31 13:33 Sam James
2024-02-22 15:36 Zac Medico

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1547624939.035582f0e31c071606635aac9cc4ba4b411612e7.zmedico@gentoo \
    --to=zmedico@gentoo.org \
    --cc=gentoo-commits@lists.gentoo.org \
    --cc=gentoo-dev@lists.gentoo.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox