aboutsummaryrefslogtreecommitdiff
blob: f8fcdf9fcaea9f101e011795dcb558595db75b6b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# SOCKSv5 proxy manager for network-sandbox
# Copyright 2015-2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import asyncio
import errno
import os
import socket
from typing import Union

import portage

portage.proxy.lazyimport.lazyimport(
    globals(),
    "portage.util._eventloop.global_event_loop:global_event_loop",
)

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


class ProxyManager:
    """
    A class to start and control a single running SOCKSv5 server process
    for Portage.
    """

    def __init__(self):
        self.socket_path = None
        self._proc = None
        self._proc_waiter = None

    def start(self, settings):
        """
        Start the SOCKSv5 server.

        @param settings: Portage settings instance (used to determine
        paths)
        @type settings: portage.config
        """

        tmpdir = os.path.join(settings["PORTAGE_TMPDIR"], "portage")
        ensure_dirs_kwargs = {}
        if portage.secpass >= 1:
            ensure_dirs_kwargs["gid"] = portage_gid
            ensure_dirs_kwargs["mode"] = 0o70
            ensure_dirs_kwargs["mask"] = 0
        portage.util.ensure_dirs(tmpdir, **ensure_dirs_kwargs)

        self.socket_path = os.path.join(
            tmpdir, ".portage.%d.net.sock" % portage.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._proc = spawn(
            [_python_interpreter, server_bin, self.socket_path],
            returnproc=True,
            **spawn_kwargs,
        )

    def stop(self) -> Union[None, asyncio.Future]:
        """
        Stop the SOCKSv5 server.

        If there is a running asyncio event loop then asyncio.Future is
        returned which should be used to wait for the server process
        to exit.
        """
        future = None
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = None
        if self._proc is not None:
            self._proc.terminate()
            if loop is None:
                # In this case spawn internals would have used
                # portage's global loop when attaching a waiter to
                # self._proc, so we are obligated to use that.
                global_event_loop().run_until_complete(self._proc.wait())
            else:
                if self._proc_waiter is None:
                    self._proc_waiter = asyncio.ensure_future(
                        self._proc.wait(), loop=loop
                    )
                future = asyncio.shield(self._proc_waiter)

        if loop is not None and future is None:
            future = loop.create_future()
            future.set_result(None)

        self.socket_path = None
        self._proc = None
        self._proc_waiter = None
        return future

    def is_running(self):
        """
        Check whether the SOCKSv5 server is running.

        @return: True if the server is running, False otherwise
        """
        return self.socket_path is not None

    async def ready(self):
        """
        Wait for the proxy socket to become ready. This method is a coroutine.
        """
        if self._proc_waiter is None:
            self._proc_waiter = asyncio.ensure_future(self._proc.wait())

        while True:
            if self._proc_waiter.done():
                raise OSError(3, "No such process")

            try:
                s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
                s.connect(self.socket_path)
            except OSError as e:
                if e.errno != errno.ENOENT:
                    raise
                await asyncio.sleep(0.2)
            else:
                break
            finally:
                s.close()


proxy = ProxyManager()


def get_socks5_proxy(settings):
    """
    Get UNIX socket path for a SOCKSv5 proxy. A new proxy is started if
    one isn't running yet, and an atexit event is added to stop the proxy
    on exit.

    @param settings: Portage settings instance (used to determine paths)
    @type settings: portage.config
    @return: (string) UNIX socket path
    """

    if not proxy.is_running():
        proxy.start(settings)
        atexit_register(proxy.stop)

    return proxy.socket_path