Skip to content

Commit ef36057

Browse files
committed
Move already_listening to plugins.util
1 parent a874654 commit ef36057

5 files changed

Lines changed: 158 additions & 146 deletions

File tree

docs/api/plugins/util.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:mod:`letsencrypt.plugins.util`
2+
-------------------------------
3+
4+
.. automodule:: letsencrypt.plugins.util
5+
:members:

letsencrypt/plugins/standalone/authenticator.py

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Standalone authenticator."""
22
import os
3-
import psutil
43
import signal
54
import socket
65
import sys
@@ -289,47 +288,6 @@ def start_listener(self, port):
289288
# should terminate via sys.exit().
290289
return self.do_child_process(port)
291290

292-
def already_listening(self, port): # pylint: disable=no-self-use
293-
"""Check if a process is already listening on the port.
294-
295-
If so, also tell the user via a display notification.
296-
297-
.. warning::
298-
On some operating systems, this function can only usefully be
299-
run as root.
300-
301-
:param int port: The TCP port in question.
302-
:returns: True or False."""
303-
304-
listeners = [conn.pid for conn in psutil.net_connections()
305-
if conn.status == 'LISTEN' and
306-
conn.type == socket.SOCK_STREAM and
307-
conn.laddr[1] == port]
308-
try:
309-
if listeners and listeners[0] is not None:
310-
# conn.pid may be None if the current process doesn't have
311-
# permission to identify the listening process! Additionally,
312-
# listeners may have more than one element if separate
313-
# sockets have bound the same port on separate interfaces.
314-
# We currently only have UI to notify the user about one
315-
# of them at a time.
316-
pid = listeners[0]
317-
name = psutil.Process(pid).name()
318-
display = zope.component.getUtility(interfaces.IDisplay)
319-
display.notification(
320-
"The program {0} (process ID {1}) is already listening "
321-
"on TCP port {2}. This will prevent us from binding to "
322-
"that port. Please stop the {0} program temporarily "
323-
"and then try again.".format(name, pid, port))
324-
return True
325-
except (psutil.NoSuchProcess, psutil.AccessDenied):
326-
# Perhaps the result of a race where the process could have
327-
# exited or relinquished the port (NoSuchProcess), or the result
328-
# of an OS policy where we're not allowed to look up the process
329-
# name (AccessDenied).
330-
pass
331-
return False
332-
333291
# IAuthenticator method implementations follow
334292

335293
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
@@ -383,7 +341,7 @@ def perform(self, achalls):
383341
if not self.tasks:
384342
raise ValueError("nothing for .perform() to do")
385343

386-
if self.already_listening(self.config.dvsni_port):
344+
if util.already_listening(self.config.dvsni_port):
387345
# If we know a process is already listening on this port,
388346
# tell the user, and don't even attempt to bind it. (This
389347
# test is Linux-specific and won't indicate that the port

letsencrypt/plugins/standalone/tests/authenticator_test.py

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -187,109 +187,6 @@ def test_subproc_signal_handler_trouble(self, mock_exit, mock_kill):
187187
mock_exit.assert_called_once_with(0)
188188

189189

190-
class AlreadyListeningTest(unittest.TestCase):
191-
"""Tests for already_listening() method."""
192-
def setUp(self):
193-
from letsencrypt.plugins.standalone.authenticator import \
194-
StandaloneAuthenticator
195-
self.authenticator = StandaloneAuthenticator(config=CONFIG, name=None)
196-
197-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil."
198-
"net_connections")
199-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil.Process")
200-
@mock.patch("letsencrypt.plugins.standalone.authenticator."
201-
"zope.component.getUtility")
202-
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
203-
# This tests a race condition, or permission problem, or OS
204-
# incompatibility in which, for some reason, no process name can be
205-
# found to match the identified listening PID.
206-
from psutil._common import sconn
207-
conns = [
208-
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
209-
raddr=(), status="LISTEN", pid=None),
210-
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
211-
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
212-
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
213-
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
214-
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
215-
raddr=(), status="LISTEN", pid=4416)]
216-
mock_net.return_value = conns
217-
mock_process.side_effect = psutil.NoSuchProcess("No such PID")
218-
# We simulate being unable to find the process name of PID 4416,
219-
# which results in returning False.
220-
self.assertFalse(self.authenticator.already_listening(17))
221-
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
222-
mock_process.assert_called_once_with(4416)
223-
224-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil."
225-
"net_connections")
226-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil.Process")
227-
@mock.patch("letsencrypt.plugins.standalone.authenticator."
228-
"zope.component.getUtility")
229-
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
230-
from psutil._common import sconn
231-
conns = [
232-
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
233-
raddr=(), status="LISTEN", pid=None),
234-
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
235-
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
236-
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
237-
raddr=("::1", 111), status="CLOSE_WAIT", pid=None)]
238-
mock_net.return_value = conns
239-
mock_process.name.return_value = "inetd"
240-
self.assertFalse(self.authenticator.already_listening(17))
241-
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
242-
self.assertEqual(mock_process.call_count, 0)
243-
244-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil."
245-
"net_connections")
246-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil.Process")
247-
@mock.patch("letsencrypt.plugins.standalone.authenticator."
248-
"zope.component.getUtility")
249-
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
250-
from psutil._common import sconn
251-
conns = [
252-
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
253-
raddr=(), status="LISTEN", pid=None),
254-
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
255-
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
256-
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
257-
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
258-
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
259-
raddr=(), status="LISTEN", pid=4416)]
260-
mock_net.return_value = conns
261-
mock_process.name.return_value = "inetd"
262-
result = self.authenticator.already_listening(17)
263-
self.assertTrue(result)
264-
self.assertEqual(mock_get_utility.call_count, 1)
265-
mock_process.assert_called_once_with(4416)
266-
267-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil."
268-
"net_connections")
269-
@mock.patch("letsencrypt.plugins.standalone.authenticator.psutil.Process")
270-
@mock.patch("letsencrypt.plugins.standalone.authenticator."
271-
"zope.component.getUtility")
272-
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
273-
from psutil._common import sconn
274-
conns = [
275-
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
276-
raddr=(), status="LISTEN", pid=None),
277-
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
278-
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
279-
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
280-
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
281-
sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(),
282-
status="LISTEN", pid=4420),
283-
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
284-
raddr=(), status="LISTEN", pid=4416)]
285-
mock_net.return_value = conns
286-
mock_process.name.return_value = "inetd"
287-
result = self.authenticator.already_listening(12345)
288-
self.assertTrue(result)
289-
self.assertEqual(mock_get_utility.call_count, 1)
290-
mock_process.assert_called_once_with(4420)
291-
292-
293190
class PerformTest(unittest.TestCase):
294191
"""Tests for perform() method."""
295192
def setUp(self):

letsencrypt/plugins/util.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Plugin utilities."""
2+
import socket
3+
4+
import psutil
5+
import zope.component
6+
7+
from letsencrypt import interfaces
8+
9+
10+
def already_listening(port):
11+
"""Check if a process is already listening on the port.
12+
13+
If so, also tell the user via a display notification.
14+
15+
.. warning::
16+
On some operating systems, this function can only usefully be
17+
run as root.
18+
19+
:param int port: The TCP port in question.
20+
:returns: True or False."""
21+
22+
listeners = [conn.pid for conn in psutil.net_connections()
23+
if conn.status == 'LISTEN' and
24+
conn.type == socket.SOCK_STREAM and
25+
conn.laddr[1] == port]
26+
try:
27+
if listeners and listeners[0] is not None:
28+
# conn.pid may be None if the current process doesn't have
29+
# permission to identify the listening process! Additionally,
30+
# listeners may have more than one element if separate
31+
# sockets have bound the same port on separate interfaces.
32+
# We currently only have UI to notify the user about one
33+
# of them at a time.
34+
pid = listeners[0]
35+
name = psutil.Process(pid).name()
36+
display = zope.component.getUtility(interfaces.IDisplay)
37+
display.notification(
38+
"The program {0} (process ID {1}) is already listening "
39+
"on TCP port {2}. This will prevent us from binding to "
40+
"that port. Please stop the {0} program temporarily "
41+
"and then try again.".format(name, pid, port))
42+
return True
43+
except (psutil.NoSuchProcess, psutil.AccessDenied):
44+
# Perhaps the result of a race where the process could have
45+
# exited or relinquished the port (NoSuchProcess), or the result
46+
# of an OS policy where we're not allowed to look up the process
47+
# name (AccessDenied).
48+
pass
49+
return False

letsencrypt/plugins/util_test.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Tests for letsencrypt.plugins.util."""
2+
import unittest
3+
4+
import mock
5+
import psutil
6+
7+
8+
class AlreadyListeningTest(unittest.TestCase):
9+
"""Tests for letsencrypt.plugins.already_listening."""
10+
def _call(self, *args, **kwargs):
11+
from letsencrypt.plugins.util import already_listening
12+
return already_listening(*args, **kwargs)
13+
14+
@mock.patch("letsencrypt.plugins.util.psutil.net_connections")
15+
@mock.patch("letsencrypt.plugins.util.psutil.Process")
16+
@mock.patch("letsencrypt.plugins.util.zope.component.getUtility")
17+
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
18+
# This tests a race condition, or permission problem, or OS
19+
# incompatibility in which, for some reason, no process name can be
20+
# found to match the identified listening PID.
21+
from psutil._common import sconn
22+
conns = [
23+
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
24+
raddr=(), status="LISTEN", pid=None),
25+
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
26+
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
27+
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
28+
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
29+
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
30+
raddr=(), status="LISTEN", pid=4416)]
31+
mock_net.return_value = conns
32+
mock_process.side_effect = psutil.NoSuchProcess("No such PID")
33+
# We simulate being unable to find the process name of PID 4416,
34+
# which results in returning False.
35+
self.assertFalse(self._call(17))
36+
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
37+
mock_process.assert_called_once_with(4416)
38+
39+
@mock.patch("letsencrypt.plugins.util.psutil.net_connections")
40+
@mock.patch("letsencrypt.plugins.util.psutil.Process")
41+
@mock.patch("letsencrypt.plugins.util.zope.component.getUtility")
42+
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
43+
from psutil._common import sconn
44+
conns = [
45+
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
46+
raddr=(), status="LISTEN", pid=None),
47+
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
48+
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
49+
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
50+
raddr=("::1", 111), status="CLOSE_WAIT", pid=None)]
51+
mock_net.return_value = conns
52+
mock_process.name.return_value = "inetd"
53+
self.assertFalse(self._call(17))
54+
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
55+
self.assertEqual(mock_process.call_count, 0)
56+
57+
@mock.patch("letsencrypt.plugins.util.psutil.net_connections")
58+
@mock.patch("letsencrypt.plugins.util.psutil.Process")
59+
@mock.patch("letsencrypt.plugins.util.zope.component.getUtility")
60+
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
61+
from psutil._common import sconn
62+
conns = [
63+
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
64+
raddr=(), status="LISTEN", pid=None),
65+
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
66+
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
67+
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
68+
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
69+
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
70+
raddr=(), status="LISTEN", pid=4416)]
71+
mock_net.return_value = conns
72+
mock_process.name.return_value = "inetd"
73+
result = self._call(17)
74+
self.assertTrue(result)
75+
self.assertEqual(mock_get_utility.call_count, 1)
76+
mock_process.assert_called_once_with(4416)
77+
78+
@mock.patch("letsencrypt.plugins.util.psutil.net_connections")
79+
@mock.patch("letsencrypt.plugins.util.psutil.Process")
80+
@mock.patch("letsencrypt.plugins.util.zope.component.getUtility")
81+
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
82+
from psutil._common import sconn
83+
conns = [
84+
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
85+
raddr=(), status="LISTEN", pid=None),
86+
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
87+
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
88+
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
89+
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
90+
sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(),
91+
status="LISTEN", pid=4420),
92+
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
93+
raddr=(), status="LISTEN", pid=4416)]
94+
mock_net.return_value = conns
95+
mock_process.name.return_value = "inetd"
96+
result = self._call(12345)
97+
self.assertTrue(result)
98+
self.assertEqual(mock_get_utility.call_count, 1)
99+
mock_process.assert_called_once_with(4420)
100+
101+
102+
if __name__ == "__main__":
103+
unittest.main() # pragma: no cover

0 commit comments

Comments
 (0)