requests_vendor.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. # requests_vendor.py -- requests implementation of the AbstractHttpGitClient interface
  2. # Copyright (C) 2022 Eden Shalit <epopcop@gmail.com>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. """Requests HTTP client support for Dulwich.
  21. To use this implementation as the HTTP implementation in Dulwich, override
  22. the dulwich.client.HttpGitClient attribute:
  23. >>> from dulwich import client as _mod_client
  24. >>> from dulwich.contrib.requests_vendor import RequestsHttpGitClient
  25. >>> _mod_client.HttpGitClient = RequestsHttpGitClient
  26. This implementation is experimental and does not have any tests.
  27. """
  28. from collections.abc import Iterator
  29. from io import BytesIO
  30. from typing import TYPE_CHECKING, Any, Callable, Optional, Union
  31. if TYPE_CHECKING:
  32. from ..config import ConfigFile
  33. from requests import Session
  34. from ..client import (
  35. AbstractHttpGitClient,
  36. HTTPProxyUnauthorized,
  37. HTTPUnauthorized,
  38. default_user_agent_string,
  39. )
  40. from ..errors import GitProtocolError, NotGitRepository
  41. class RequestsHttpGitClient(AbstractHttpGitClient):
  42. """HTTP Git client using the requests library."""
  43. def __init__(
  44. self,
  45. base_url: str,
  46. dumb: Optional[bool] = None,
  47. config: Optional["ConfigFile"] = None,
  48. username: Optional[str] = None,
  49. password: Optional[str] = None,
  50. thin_packs: bool = True,
  51. report_activity: Optional[Callable[[int, str], None]] = None,
  52. quiet: bool = False,
  53. include_tags: bool = False,
  54. ) -> None:
  55. """Initialize RequestsHttpGitClient.
  56. Args:
  57. base_url: Base URL of the Git repository
  58. dumb: Whether to use dumb HTTP transport
  59. config: Git configuration file
  60. username: Username for authentication
  61. password: Password for authentication
  62. thin_packs: Whether to use thin packs
  63. report_activity: Function to report activity
  64. quiet: Whether to suppress output
  65. include_tags: Whether to include tags
  66. """
  67. self._username = username
  68. self._password = password
  69. self.session = get_session(config)
  70. if username is not None:
  71. self.session.auth = (username, password) # type: ignore[assignment]
  72. super().__init__(
  73. base_url=base_url,
  74. dumb=bool(dumb) if dumb is not None else False,
  75. thin_packs=thin_packs,
  76. report_activity=report_activity,
  77. quiet=quiet,
  78. include_tags=include_tags,
  79. )
  80. def _http_request(
  81. self,
  82. url: str,
  83. headers: Optional[dict[str, str]] = None,
  84. data: Optional[Union[bytes, Iterator[bytes]]] = None,
  85. raise_for_status: bool = True,
  86. ) -> tuple[Any, Callable[[int], bytes]]:
  87. req_headers = self.session.headers.copy() # type: ignore[attr-defined]
  88. if headers is not None:
  89. req_headers.update(headers)
  90. # Accept compression by default
  91. req_headers.setdefault("Accept-Encoding", "gzip")
  92. if data:
  93. resp = self.session.post(url, headers=req_headers, data=data)
  94. else:
  95. resp = self.session.get(url, headers=req_headers)
  96. if resp.status_code == 404:
  97. raise NotGitRepository
  98. if resp.status_code == 401:
  99. raise HTTPUnauthorized(resp.headers.get("WWW-Authenticate"), url)
  100. if resp.status_code == 407:
  101. raise HTTPProxyUnauthorized(resp.headers.get("Proxy-Authenticate"), url)
  102. if resp.status_code != 200:
  103. raise GitProtocolError(f"unexpected http resp {resp.status_code} for {url}")
  104. # Add required fields as stated in AbstractHttpGitClient._http_request
  105. resp.content_type = resp.headers.get("Content-Type") # type: ignore[attr-defined]
  106. resp.redirect_location = "" # type: ignore[attr-defined]
  107. if resp.history:
  108. resp.redirect_location = resp.url # type: ignore[attr-defined]
  109. read = BytesIO(resp.content).read
  110. return resp, read
  111. def get_session(config: Optional["ConfigFile"]) -> Session:
  112. """Create a requests session with Git configuration.
  113. Args:
  114. config: Git configuration file
  115. Returns:
  116. Configured requests Session
  117. """
  118. session = Session()
  119. session.headers.update({"Pragma": "no-cache"})
  120. proxy_server: Optional[str] = None
  121. user_agent: Optional[str] = None
  122. ca_certs: Optional[str] = None
  123. ssl_verify: Optional[bool] = None
  124. if config is not None:
  125. try:
  126. proxy_bytes = config.get(b"http", b"proxy")
  127. if isinstance(proxy_bytes, bytes):
  128. proxy_server = proxy_bytes.decode()
  129. except KeyError:
  130. pass
  131. try:
  132. agent_bytes = config.get(b"http", b"useragent")
  133. if isinstance(agent_bytes, bytes):
  134. user_agent = agent_bytes.decode()
  135. except KeyError:
  136. pass
  137. try:
  138. ssl_verify = config.get_boolean(b"http", b"sslVerify")
  139. except KeyError:
  140. ssl_verify = True
  141. try:
  142. certs_bytes = config.get(b"http", b"sslCAInfo")
  143. if isinstance(certs_bytes, bytes):
  144. ca_certs = certs_bytes.decode()
  145. except KeyError:
  146. ca_certs = None
  147. if user_agent is None:
  148. user_agent = default_user_agent_string()
  149. if user_agent is not None:
  150. session.headers.update({"User-agent": user_agent})
  151. if ca_certs:
  152. session.verify = ca_certs
  153. elif ssl_verify is False:
  154. session.verify = ssl_verify
  155. if proxy_server is not None:
  156. session.proxies.update({"http": proxy_server, "https": proxy_server})
  157. return session