FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(google-auth-oauthlib): prevent port re-use on windows (#18166) · googleapis/google-cloud-python@e20796a · GitHub

Commit e20796a

Browse files
fix(google-auth-oauthlib): prevent port re-use on windows (#18166)
Building off of #18137 The commit history has test results running against windows, using a temporary GitHub Action workflow. It was removed from the latest commit
1 parent b642373 commit e20796a

2 files changed

Lines changed: 109 additions & 7 deletions

File tree

‎packages/google-auth-oauthlib/google_auth_oauthlib/flow.py‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@
5858
except ImportError: # pragma: NO COVER
5959
from random import SystemRandom
6060

61+
import socket
6162
from string import ascii_letters, digits
63+
import sys
6264
import webbrowser
6365
import wsgiref.simple_server
6466
import wsgiref.util
@@ -432,10 +434,14 @@ def run_local_server(
432434
authorization server.
433435
"""
434436
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.
437439
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,
439445
)
440446

441447
try:
@@ -478,6 +484,23 @@ def run_local_server(
478484
return self.credentials
479485

480486

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+
481504
class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
482505
"""Custom WSGIRequestHandler.
483506

‎packages/google-auth-oauthlib/tests/unit/test_flow.py‎

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import os
2121
import re
2222
import socket
23+
import time
2324
from unittest import mock
2425
import urllib
2526
import webbrowser
27+
import wsgiref.simple_server
2628

2729
import pytest
2830
import requests
@@ -444,10 +446,56 @@ def test_run_local_server_occupied_port(
444446
self, webbrowser_mock, instance, mock_fetch_token, port, socket
445447
):
446448
# 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+
)
451499

452500
@mock.patch("google_auth_oauthlib.flow.webbrowser.get", autospec=True)
453501
@mock.patch("wsgiref.simple_server.make_server", autospec=True)
@@ -514,3 +562,34 @@ def test_run_local_server_timeout(
514562

515563
webbrowser_mock.get.assert_called_with(None)
516564
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()

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL