credentials.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. # credentials.py -- support for git credential helpers
  2. # Copyright (C) 2022 Daniele Trifirò <daniele@iterative.ai>
  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. #
  21. """Support for git credential helpers.
  22. https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage
  23. """
  24. import sys
  25. from collections.abc import Iterator
  26. from typing import Optional
  27. from urllib.parse import ParseResult, urlparse
  28. from .config import ConfigDict, SectionLike
  29. def match_urls(url: ParseResult, url_prefix: ParseResult) -> bool:
  30. """Check if a URL matches a URL prefix.
  31. Args:
  32. url: Parsed URL to check
  33. url_prefix: Parsed URL prefix to match against
  34. Returns:
  35. True if url matches the prefix
  36. """
  37. base_match = (
  38. url.scheme == url_prefix.scheme
  39. and url.hostname == url_prefix.hostname
  40. and url.port == url_prefix.port
  41. )
  42. user_match = url.username == url_prefix.username if url_prefix.username else True
  43. path_match = url.path.rstrip("/").startswith(url_prefix.path.rstrip())
  44. return base_match and user_match and path_match
  45. def match_partial_url(valid_url: ParseResult, partial_url: str) -> bool:
  46. """Matches a parsed url with a partial url (no scheme/netloc)."""
  47. if "://" not in partial_url:
  48. parsed = urlparse("scheme://" + partial_url)
  49. else:
  50. parsed = urlparse(partial_url)
  51. if valid_url.scheme != parsed.scheme:
  52. return False
  53. if any(
  54. (
  55. (parsed.hostname and valid_url.hostname != parsed.hostname),
  56. (parsed.username and valid_url.username != parsed.username),
  57. (parsed.port and valid_url.port != parsed.port),
  58. (parsed.path and parsed.path.rstrip("/") != valid_url.path.rstrip("/")),
  59. ),
  60. ):
  61. return False
  62. return True
  63. def urlmatch_credential_sections(
  64. config: ConfigDict, url: Optional[str]
  65. ) -> Iterator[SectionLike]:
  66. """Returns credential sections from the config which match the given URL."""
  67. encoding = config.encoding or sys.getdefaultencoding()
  68. parsed_url = urlparse(url or "")
  69. for config_section in config.sections():
  70. if config_section[0] != b"credential":
  71. continue
  72. if len(config_section) < 2:
  73. yield config_section
  74. continue
  75. config_url = config_section[1].decode(encoding)
  76. parsed_config_url = urlparse(config_url)
  77. if parsed_config_url.scheme and parsed_config_url.netloc:
  78. is_match = match_urls(parsed_url, parsed_config_url)
  79. else:
  80. is_match = match_partial_url(parsed_url, config_url)
  81. if is_match:
  82. yield config_section