Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_auth.py349 linesDownload Raw Back to httpx
1from __future__ import annotations2 3import hashlib4import os5import re6import time7import typing8from base64 import b64encode9from urllib.request import parse_http_list10 11from ._exceptions import ProtocolError12from ._models import Cookies, Request, Response13from ._utils import to_bytes, to_str, unquote14 15if typing.TYPE_CHECKING:  # pragma: no cover16    from hashlib import _Hash17 18 19__all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"]20 21 22class Auth:23    """24    Base class for all authentication schemes.25 26    To implement a custom authentication scheme, subclass `Auth` and override27    the `.auth_flow()` method.28 29    If the authentication scheme does I/O such as disk access or network calls, or uses30    synchronization primitives such as locks, you should override `.sync_auth_flow()`31    and/or `.async_auth_flow()` instead of `.auth_flow()` to provide specialized32    implementations that will be used by `Client` and `AsyncClient` respectively.33    """34 35    requires_request_body = False36    requires_response_body = False37 38    def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:39        """40        Execute the authentication flow.41 42        To dispatch a request, `yield` it:43 44        ```45        yield request46        ```47 48        The client will `.send()` the response back into the flow generator. You can49        access it like so:50 51        ```52        response = yield request53        ```54 55        A `return` (or reaching the end of the generator) will result in the56        client returning the last response obtained from the server.57 58        You can dispatch as many requests as is necessary.59        """60        yield request61 62    def sync_auth_flow(63        self, request: Request64    ) -> typing.Generator[Request, Response, None]:65        """66        Execute the authentication flow synchronously.67 68        By default, this defers to `.auth_flow()`. You should override this method69        when the authentication scheme does I/O and/or uses concurrency primitives.70        """71        if self.requires_request_body:72            request.read()73 74        flow = self.auth_flow(request)75        request = next(flow)76 77        while True:78            response = yield request79            if self.requires_response_body:80                response.read()81 82            try:83                request = flow.send(response)84            except StopIteration:85                break86 87    async def async_auth_flow(88        self, request: Request89    ) -> typing.AsyncGenerator[Request, Response]:90        """91        Execute the authentication flow asynchronously.92 93        By default, this defers to `.auth_flow()`. You should override this method94        when the authentication scheme does I/O and/or uses concurrency primitives.95        """96        if self.requires_request_body:97            await request.aread()98 99        flow = self.auth_flow(request)100        request = next(flow)101 102        while True:103            response = yield request104            if self.requires_response_body:105                await response.aread()106 107            try:108                request = flow.send(response)109            except StopIteration:110                break111 112 113class FunctionAuth(Auth):114    """115    Allows the 'auth' argument to be passed as a simple callable function,116    that takes the request, and returns a new, modified request.117    """118 119    def __init__(self, func: typing.Callable[[Request], Request]) -> None:120        self._func = func121 122    def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:123        yield self._func(request)124 125 126class BasicAuth(Auth):127    """128    Allows the 'auth' argument to be passed as a (username, password) pair,129    and uses HTTP Basic authentication.130    """131 132    def __init__(self, username: str | bytes, password: str | bytes) -> None:133        self._auth_header = self._build_auth_header(username, password)134 135    def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:136        request.headers["Authorization"] = self._auth_header137        yield request138 139    def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:140        userpass = b":".join((to_bytes(username), to_bytes(password)))141        token = b64encode(userpass).decode()142        return f"Basic {token}"143 144 145class NetRCAuth(Auth):146    """147    Use a 'netrc' file to lookup basic auth credentials based on the url host.148    """149 150    def __init__(self, file: str | None = None) -> None:151        # Lazily import 'netrc'.152        # There's no need for us to load this module unless 'NetRCAuth' is being used.153        import netrc154 155        self._netrc_info = netrc.netrc(file)156 157    def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:158        auth_info = self._netrc_info.authenticators(request.url.host)159        if auth_info is None or not auth_info[2]:160            # The netrc file did not have authentication credentials for this host.161            yield request162        else:163            # Build a basic auth header with credentials from the netrc file.164            request.headers["Authorization"] = self._build_auth_header(165                username=auth_info[0], password=auth_info[2]166            )167            yield request168 169    def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:170        userpass = b":".join((to_bytes(username), to_bytes(password)))171        token = b64encode(userpass).decode()172        return f"Basic {token}"173 174 175class DigestAuth(Auth):176    _ALGORITHM_TO_HASH_FUNCTION: dict[str, typing.Callable[[bytes], _Hash]] = {177        "MD5": hashlib.md5,178        "MD5-SESS": hashlib.md5,179        "SHA": hashlib.sha1,180        "SHA-SESS": hashlib.sha1,181        "SHA-256": hashlib.sha256,182        "SHA-256-SESS": hashlib.sha256,183        "SHA-512": hashlib.sha512,184        "SHA-512-SESS": hashlib.sha512,185    }186 187    def __init__(self, username: str | bytes, password: str | bytes) -> None:188        self._username = to_bytes(username)189        self._password = to_bytes(password)190        self._last_challenge: _DigestAuthChallenge | None = None191        self._nonce_count = 1192 193    def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:194        if self._last_challenge:195            request.headers["Authorization"] = self._build_auth_header(196                request, self._last_challenge197            )198 199        response = yield request200 201        if response.status_code != 401 or "www-authenticate" not in response.headers:202            # If the response is not a 401 then we don't203            # need to build an authenticated request.204            return205 206        for auth_header in response.headers.get_list("www-authenticate"):207            if auth_header.lower().startswith("digest "):208                break209        else:210            # If the response does not include a 'WWW-Authenticate: Digest ...'211            # header, then we don't need to build an authenticated request.212            return213 214        self._last_challenge = self._parse_challenge(request, response, auth_header)215        self._nonce_count = 1216 217        request.headers["Authorization"] = self._build_auth_header(218            request, self._last_challenge219        )220        if response.cookies:221            Cookies(response.cookies).set_cookie_header(request=request)222        yield request223 224    def _parse_challenge(225        self, request: Request, response: Response, auth_header: str226    ) -> _DigestAuthChallenge:227        """228        Returns a challenge from a Digest WWW-Authenticate header.229        These take the form of:230        `Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"`231        """232        scheme, _, fields = auth_header.partition(" ")233 234        # This method should only ever have been called with a Digest auth header.235        assert scheme.lower() == "digest"236 237        header_dict: dict[str, str] = {}238        for field in parse_http_list(fields):239            key, value = field.strip().split("=", 1)240            header_dict[key] = unquote(value)241 242        try:243            realm = header_dict["realm"].encode()244            nonce = header_dict["nonce"].encode()245            algorithm = header_dict.get("algorithm", "MD5")246            opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None247            qop = header_dict["qop"].encode() if "qop" in header_dict else None248            return _DigestAuthChallenge(249                realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop250            )251        except KeyError as exc:252            message = "Malformed Digest WWW-Authenticate header"253            raise ProtocolError(message, request=request) from exc254 255    def _build_auth_header(256        self, request: Request, challenge: _DigestAuthChallenge257    ) -> str:258        hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()]259 260        def digest(data: bytes) -> bytes:261            return hash_func(data).hexdigest().encode()262 263        A1 = b":".join((self._username, challenge.realm, self._password))264 265        path = request.url.raw_path266        A2 = b":".join((request.method.encode(), path))267        # TODO: implement auth-int268        HA2 = digest(A2)269 270        nc_value = b"%08x" % self._nonce_count271        cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce)272        self._nonce_count += 1273 274        HA1 = digest(A1)275        if challenge.algorithm.lower().endswith("-sess"):276            HA1 = digest(b":".join((HA1, challenge.nonce, cnonce)))277 278        qop = self._resolve_qop(challenge.qop, request=request)279        if qop is None:280            # Following RFC 2069281            digest_data = [HA1, challenge.nonce, HA2]282        else:283            # Following RFC 2617/7616284            digest_data = [HA1, challenge.nonce, nc_value, cnonce, qop, HA2]285 286        format_args = {287            "username": self._username,288            "realm": challenge.realm,289            "nonce": challenge.nonce,290            "uri": path,291            "response": digest(b":".join(digest_data)),292            "algorithm": challenge.algorithm.encode(),293        }294        if challenge.opaque:295            format_args["opaque"] = challenge.opaque296        if qop:297            format_args["qop"] = b"auth"298            format_args["nc"] = nc_value299            format_args["cnonce"] = cnonce300 301        return "Digest " + self._get_header_value(format_args)302 303    def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes:304        s = str(nonce_count).encode()305        s += nonce306        s += time.ctime().encode()307        s += os.urandom(8)308 309        return hashlib.sha1(s).hexdigest()[:16].encode()310 311    def _get_header_value(self, header_fields: dict[str, bytes]) -> str:312        NON_QUOTED_FIELDS = ("algorithm", "qop", "nc")313        QUOTED_TEMPLATE = '{}="{}"'314        NON_QUOTED_TEMPLATE = "{}={}"315 316        header_value = ""317        for i, (field, value) in enumerate(header_fields.items()):318            if i > 0:319                header_value += ", "320            template = (321                QUOTED_TEMPLATE322                if field not in NON_QUOTED_FIELDS323                else NON_QUOTED_TEMPLATE324            )325            header_value += template.format(field, to_str(value))326 327        return header_value328 329    def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None:330        if qop is None:331            return None332        qops = re.split(b", ?", qop)333        if b"auth" in qops:334            return b"auth"335 336        if qops == [b"auth-int"]:337            raise NotImplementedError("Digest auth-int support is not yet implemented")338 339        message = f'Unexpected qop value "{qop!r}" in digest auth'340        raise ProtocolError(message, request=request)341 342 343class _DigestAuthChallenge(typing.NamedTuple):344    realm: bytes345    nonce: bytes346    algorithm: str347    opaque: bytes | None348    qop: bytes | None349 
codekingpro/portable-devtools · Team Ai