Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Digest Authentication to client #788

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/websockets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

__all__ = [
"AbortHandshake",
"AuthenticationRequest",
"basic_auth_protocol_factory",
"BasicAuthWebSocketServerProtocol",
"connect",
Expand Down
28 changes: 27 additions & 1 deletion src/websockets/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import functools
import logging
import warnings
from python_digest import parse_digest_challenge, build_authorization_request
from types import TracebackType
from typing import Any, Generator, List, Optional, Sequence, Tuple, Type, cast

from .exceptions import (
AuthenticationRequest,
InvalidHandshake,
InvalidHeader,
InvalidMessage,
Expand Down Expand Up @@ -254,7 +256,7 @@ async def handshake(
else:
request_headers["Host"] = f"{wsuri.host}:{wsuri.port}"

if wsuri.user_info:
if wsuri.user_info and "Authorization" not in request_headers:
request_headers["Authorization"] = build_authorization_basic(
*wsuri.user_info
)
Expand Down Expand Up @@ -294,6 +296,10 @@ async def handshake(
if "Location" not in response_headers:
raise InvalidHeader("Location")
raise RedirectHandshake(response_headers["Location"])
elif status_code == 401:
if "WWW-Authenticate" not in response_headers:
raise InvalidHeader("WWW-Authenticate")
raise AuthenticationRequest(response_headers["WWW-Authenticate"])
elif status_code != 101:
raise InvalidStatusCode(status_code)

Expand Down Expand Up @@ -479,6 +485,18 @@ def __init__(
self._create_connection = create_connection
self._wsuri = wsuri

def handle_digest_auth(self, response: str) -> None:
wsuri = self._wsuri
challenge = parse_digest_challenge(response)
if challenge is None:
raise AuthenticationRequest(response)
kd = build_authorization_request(wsuri.user_info[0],
'GET', wsuri.resource_name, 1,
challenge,
password=wsuri.user_info[1])
return kd


def handle_redirect(self, uri: str) -> None:
# Update the state of this instance to connect to a new URI.
old_wsuri = self._wsuri
Expand Down Expand Up @@ -533,12 +551,18 @@ def __await__(self) -> Generator[Any, None, WebSocketClientProtocol]:
return self.__await_impl__().__await__()

async def __await_impl__(self) -> WebSocketClientProtocol:
auth_header = None
for redirects in range(self.MAX_REDIRECTS_ALLOWED):
transport, protocol = await self._create_connection()
# https://github.com/python/typeshed/pull/2756
transport = cast(asyncio.Transport, transport)
protocol = cast(WebSocketClientProtocol, protocol)

if auth_header is not None:
if protocol.extra_headers is None:
protocol.extra_headers = {}
protocol.extra_headers['Authorization'] = auth_header

try:
try:
await protocol.handshake(
Expand All @@ -557,6 +581,8 @@ async def __await_impl__(self) -> WebSocketClientProtocol:
return protocol
except RedirectHandshake as exc:
self.handle_redirect(exc.uri)
except AuthenticationRequest as exc:
auth_header = self.handle_digest_auth(exc.authresponse)
else:
raise SecurityError("too many redirects")

Expand Down
16 changes: 16 additions & 0 deletions src/websockets/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* :exc:`InvalidParameterValue`
* :exc:`AbortHandshake`
* :exc:`RedirectHandshake`
* :exc:`AuthenticationRequest`
* :exc:`InvalidState`
* :exc:`InvalidURI`
* :exc:`PayloadTooBig`
Expand Down Expand Up @@ -53,6 +54,7 @@
"InvalidParameterValue",
"AbortHandshake",
"RedirectHandshake",
"AuthenticationRequest",
"InvalidState",
"InvalidURI",
"PayloadTooBig",
Expand Down Expand Up @@ -326,6 +328,20 @@ def __str__(self) -> str:
return f"redirect to {self.uri}"


class AuthenticationRequest(InvalidHandshake):
"""
Raised when a 401 Unauthorized response is received.
Another implementation detail, to allow e.g., Digest authentication

"""
def __init__(self, details : str) -> None:
self.authresponse = details

def __str__(self) -> str:
return f"WWW-Authenticate: {self.authresponse}"



class InvalidState(WebSocketException, AssertionError):
"""
Raised when an operation is forbidden in the current state.
Expand Down