| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent b642373 commit e20796a
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -58,7 +58,9 @@ | |||
| 58 | 58 | except ImportError: # pragma: NO COVER | |
| 59 | 59 | from random import SystemRandom | |
| 60 | 60 | ||
| 61 | + import socket | ||
| 61 | 62 | from string import ascii_letters, digits | |
| 63 | + import sys | ||
| 62 | 64 | import webbrowser | |
| 63 | 65 | import wsgiref.simple_server | |
| 64 | 66 | import wsgiref.util | |
@@ -432,10 +434,14 @@ def run_local_server( | |||
| 432 | 434 | authorization server. | |
| 433 | 435 | """ | |
| 434 | 436 | wsgi_app = _RedirectWSGIApp(success_message) | |
| 435 | - # Fail fast if the address is occupied | ||
| 436 | - wsgiref.simple_server.WSGIServer.allow_reuse_address = False | ||
| 437 | + # Use _ExclusiveWSGIServer to fail fast if the address/port is occupied, | ||
| 438 | + # and to prevent other apps from binding to it on Windows. | ||
| 437 | 439 | local_server = wsgiref.simple_server.make_server( | |
| 438 | - bind_addr or host, port, wsgi_app, handler_class=_WSGIRequestHandler | ||
| 440 | + bind_addr or host, | ||
| 441 | + port, | ||
| 442 | + wsgi_app, | ||
| 443 | + server_class=_ExclusiveWSGIServer, | ||
| 444 | + handler_class=_WSGIRequestHandler, | ||
| 439 | 445 | ) | |
| 440 | 446 | ||
| 441 | 447 | try: | |
@@ -478,6 +484,23 @@ def run_local_server( | |||
| 478 | 484 | return self.credentials | |
| 479 | 485 | ||
| 480 | 486 | ||
| 487 | + class _ExclusiveWSGIServer(wsgiref.simple_server.WSGIServer): | ||
| 488 | + """Custom WSGIServer. | ||
| 489 | + | ||
| 490 | + Enforces exclusive address binding on Windows. | ||
| 491 | + Setting `WSGIServer.allow_reuse_address` is not enough, since it sets `SO_REUSEADDR` | ||
| 492 | + and not `SO_EXCLUSIVEADDRUSE`. `SO_REUSEADDR` alone allows other processes to bind | ||
| 493 | + to the same address and port on Windows. | ||
| 494 | + """ | ||
| 495 | + | ||
| 496 | + allow_reuse_address = False | ||
| 497 | + | ||
| 498 | + def server_bind(self): | ||
| 499 | + if sys.platform == "win32" and hasattr(socket, "SO_EXCLUSIVEADDRUSE"): | ||
| 500 | + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) | ||
| 501 | + super().server_bind() | ||
| 502 | + | ||
| 503 | + | ||
| 481 | 504 | class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler): | |
| 482 | 505 | """Custom WSGIRequestHandler. | |
| 483 | 506 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,9 +20,11 @@ | |||
| 20 | 20 | import os | |
| 21 | 21 | import re | |
| 22 | 22 | import socket | |
| 23 | + import time | ||
| 23 | 24 | from unittest import mock | |
| 24 | 25 | import urllib | |
| 25 | 26 | import webbrowser | |
| 27 | + import wsgiref.simple_server | ||
| 26 | 28 | ||
| 27 | 29 | import pytest | |
| 28 | 30 | import requests | |
@@ -444,10 +446,56 @@ def test_run_local_server_occupied_port( | |||
| 444 | 446 | self, webbrowser_mock, instance, mock_fetch_token, port, socket | |
| 445 | 447 | ): | |
| 446 | 448 | # socket fixture is already bound to http://localhost:port | |
| 447 | - instance.run_local_server | ||
| 448 | - with pytest.raises(OSError) as exc: | ||
| 449 | - instance.run_local_server(port=port) | ||
| 450 | - assert "address already in use" in exc.strerror.lower() | ||
| 449 | + with pytest.raises(OSError): | ||
| 450 | + instance.run_local_server(port=port, timeout_seconds=1) | ||
| 451 | + | ||
| 452 | + @pytest.mark.webtest | ||
| 453 | + @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) | ||
| 454 | + def test_run_local_server_exclusive_port( | ||
| 455 | + self, webbrowser_mock, instance, mock_fetch_token, port | ||
| 456 | + ): | ||
| 457 | + """Verify that while run_local_server is running, another socket cannot bind to its port.""" | ||
| 458 | + auth_redirect_url = urllib.parse.urljoin( | ||
| 459 | + f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH | ||
| 460 | + ) | ||
| 461 | + | ||
| 462 | + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: | ||
| 463 | + future = pool.submit(partial(instance.run_local_server, port=port)) | ||
| 464 | + time.sleep(0.2) | ||
| 465 | + | ||
| 466 | + hijack_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| 467 | + hijack_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
| 468 | + try: | ||
| 469 | + with pytest.raises(OSError): | ||
| 470 | + hijack_socket.bind(("localhost", port)) | ||
| 471 | + finally: | ||
| 472 | + hijack_socket.close() | ||
| 473 | + while not future.done(): | ||
| 474 | + try: | ||
| 475 | + requests.get(auth_redirect_url) | ||
| 476 | + except requests.ConnectionError: # pragma: NO COVER | ||
| 477 | + pass | ||
| 478 | + | ||
| 479 | + credentials = future.result() | ||
| 480 | + | ||
| 481 | + assert credentials.token == mock.sentinel.access_token | ||
| 482 | + | ||
| 483 | + @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) | ||
| 484 | + @mock.patch("wsgiref.simple_server.make_server", autospec=True) | ||
| 485 | + def test_run_local_server_uses_exclusive_server_class( | ||
| 486 | + self, make_server_mock, webbrowser_mock, instance | ||
| 487 | + ): | ||
| 488 | + server_mock = mock.MagicMock() | ||
| 489 | + make_server_mock.return_value = server_mock | ||
| 490 | + | ||
| 491 | + with pytest.raises(Exception): | ||
| 492 | + instance.run_local_server(port=0) | ||
| 493 | + | ||
| 494 | + make_server_mock.assert_called_once() | ||
| 495 | + assert ( | ||
| 496 | + make_server_mock.call_args.kwargs.get("server_class") | ||
| 497 | + is flow._ExclusiveWSGIServer | ||
| 498 | + ) | ||
| 451 | 499 | ||
| 452 | 500 | @mock.patch("google_auth_oauthlib.flow.webbrowser.get", autospec=True) | |
| 453 | 501 | @mock.patch("wsgiref.simple_server.make_server", autospec=True) | |
@@ -514,3 +562,34 @@ def test_run_local_server_timeout( | |||
| 514 | 562 | ||
| 515 | 563 | webbrowser_mock.get.assert_called_with(None) | |
| 516 | 564 | webbrowser_mock.get.return_value.open.assert_called_once() | |
| 565 | + | ||
| 566 | + | ||
| 567 | + class TestExclusiveWSGIServer(object): | ||
| 568 | + def test_exclusive_wsgi_server_bind_windows(self): | ||
| 569 | + with mock.patch("sys.platform", "win32"), mock.patch( | ||
| 570 | + "google_auth_oauthlib.flow.socket" | ||
| 571 | + ) as mock_socket: | ||
| 572 | + mock_socket.SOL_SOCKET = socket.SOL_SOCKET | ||
| 573 | + mock_socket.SO_EXCLUSIVEADDRUSE = getattr(socket, "SO_EXCLUSIVEADDRUSE", 1) | ||
| 574 | + | ||
| 575 | + server = flow._ExclusiveWSGIServer( | ||
| 576 | + ("localhost", 0), flow._WSGIRequestHandler, bind_and_activate=False | ||
| 577 | + ) | ||
| 578 | + server.socket = mock.Mock() | ||
| 579 | + | ||
| 580 | + with mock.patch.object(wsgiref.simple_server.WSGIServer, "server_bind"): | ||
| 581 | + server.server_bind() | ||
| 582 | + server.socket.setsockopt.assert_called_once_with( | ||
| 583 | + mock_socket.SOL_SOCKET, mock_socket.SO_EXCLUSIVEADDRUSE, 1 | ||
| 584 | + ) | ||
| 585 | + | ||
| 586 | + def test_exclusive_wsgi_server_bind_non_windows(self): | ||
| 587 | + with mock.patch("sys.platform", "linux"): | ||
| 588 | + server = flow._ExclusiveWSGIServer( | ||
| 589 | + ("localhost", 0), flow._WSGIRequestHandler, bind_and_activate=False | ||
| 590 | + ) | ||
| 591 | + server.socket = mock.Mock() | ||
| 592 | + | ||
| 593 | + with mock.patch.object(wsgiref.simple_server.WSGIServer, "server_bind"): | ||
| 594 | + server.server_bind() | ||
| 595 | + server.socket.setsockopt.assert_not_called() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments