requests_vendor.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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 public 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 io import BytesIO
  29. from typing import TYPE_CHECKING, Any, Callable, Optional
  30. if TYPE_CHECKING:
  31. from ..config import ConfigFile
  32. from requests import Session
  33. from ..client import (
  34. AbstractHttpGitClient,
  35. HTTPProxyUnauthorized,
  36. HTTPUnauthorized,
  37. default_user_agent_string,
  38. )
  39. from ..errors import GitProtocolError, NotGitRepository
  40. class RequestsHttpGitClient(AbstractHttpGitClient):
  41. def __init__(
  42. self,
  43. base_url: str,
  44. dumb: Optional[bool] = None,
  45. config: Optional["ConfigFile"] = None,
  46. username: Optional[str] = None,
  47. password: Optional[str] = None,
  48. **kwargs: object,
  49. ) -> None:
  50. self._username = username
  51. self._password = password
  52. self.session = get_session(config)
  53. if username is not None:
  54. self.session.auth = (username, password) # type: ignore[assignment]
  55. super().__init__(
  56. base_url=base_url, dumb=bool(dumb) if dumb is not None else False, **kwargs
  57. )
  58. def _http_request(
  59. self,
  60. url: str,
  61. headers: Optional[dict[str, str]] = None,
  62. data: Optional[bytes] = None,
  63. allow_compression: bool = False,
  64. ) -> tuple[Any, Callable[[int], bytes]]:
  65. req_headers = self.session.headers.copy() # type: ignore[attr-defined]
  66. if headers is not None:
  67. req_headers.update(headers)
  68. if allow_compression:
  69. req_headers["Accept-Encoding"] = "gzip"
  70. else:
  71. req_headers["Accept-Encoding"] = "identity"
  72. if data:
  73. resp = self.session.post(url, headers=req_headers, data=data)
  74. else:
  75. resp = self.session.get(url, headers=req_headers)
  76. if resp.status_code == 404:
  77. raise NotGitRepository
  78. if resp.status_code == 401:
  79. raise HTTPUnauthorized(resp.headers.get("WWW-Authenticate"), url)
  80. if resp.status_code == 407:
  81. raise HTTPProxyUnauthorized(resp.headers.get("Proxy-Authenticate"), url)
  82. if resp.status_code != 200:
  83. raise GitProtocolError(f"unexpected http resp {resp.status_code} for {url}")
  84. # Add required fields as stated in AbstractHttpGitClient._http_request
  85. resp.content_type = resp.headers.get("Content-Type") # type: ignore[attr-defined]
  86. resp.redirect_location = "" # type: ignore[attr-defined]
  87. if resp.history:
  88. resp.redirect_location = resp.url # type: ignore[attr-defined]
  89. read = BytesIO(resp.content).read
  90. return resp, read
  91. def get_session(config: Optional["ConfigFile"]) -> Session:
  92. session = Session()
  93. session.headers.update({"Pragma": "no-cache"})
  94. proxy_server: Optional[str] = None
  95. user_agent: Optional[str] = None
  96. ca_certs: Optional[str] = None
  97. ssl_verify: Optional[bool] = None
  98. if config is not None:
  99. try:
  100. proxy_bytes = config.get(b"http", b"proxy")
  101. if isinstance(proxy_bytes, bytes):
  102. proxy_server = proxy_bytes.decode()
  103. except KeyError:
  104. pass
  105. try:
  106. agent_bytes = config.get(b"http", b"useragent")
  107. if isinstance(agent_bytes, bytes):
  108. user_agent = agent_bytes.decode()
  109. except KeyError:
  110. pass
  111. try:
  112. ssl_verify = config.get_boolean(b"http", b"sslVerify")
  113. except KeyError:
  114. ssl_verify = True
  115. try:
  116. certs_bytes = config.get(b"http", b"sslCAInfo")
  117. if isinstance(certs_bytes, bytes):
  118. ca_certs = certs_bytes.decode()
  119. except KeyError:
  120. ca_certs = None
  121. if user_agent is None:
  122. user_agent = default_user_agent_string()
  123. if user_agent is not None:
  124. session.headers.update({"User-agent": user_agent})
  125. if ca_certs:
  126. session.verify = ca_certs
  127. elif ssl_verify is False:
  128. session.verify = ssl_verify
  129. if proxy_server is not None:
  130. session.proxies.update({"http": proxy_server, "https": proxy_server})
  131. return session