| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -652,42 +652,60 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[ | |||
| 652 | 652 | return None | |
| 653 | 653 | ||
| 654 | 654 | ||
| 655 | + def _redact_url_credentials(text: str) -> Optional[str]: | ||
| 656 | + """Return `text` with the userinfo of a URL redacted, or ``None`` if it has none.""" | ||
| 657 | + try: | ||
| 658 | + url = urlsplit(text) | ||
| 659 | + except ValueError: | ||
| 660 | + # This is not a valid URL. | ||
| 661 | + return None | ||
| 662 | + if url.password is None and url.username is None: | ||
| 663 | + return None | ||
| 664 | + | ||
| 665 | + # Match urllib.parse's userinfo boundary. Keeping the raw hostinfo | ||
| 666 | + # preserves hostname case, IPv6 brackets, and port formatting. | ||
| 667 | + _, _, hostinfo = url.netloc.rpartition("@") | ||
| 668 | + redacted = "*****:*****" if url.password is not None else "*****" | ||
| 669 | + return urlunsplit(url._replace(netloc=f"{redacted}@{hostinfo}")) | ||
| 670 | + | ||
| 671 | + | ||
| 655 | 672 | def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: | |
| 656 | 673 | """Redact credentials in URLs and HTTP Authorization extra headers in a command line. | |
| 657 | 674 | ||
| 675 | + URLs are also redacted in the key and value of a configuration assignment, such | ||
| 676 | + as ``http.proxy=<url>``. Assignments are recognized in the ``-c <key>=<value>``, | ||
| 677 | + ``-c<key>=<value>``, and ``--config=<key>=<value>`` forms. | ||
| 678 | + | ||
| 658 | 679 | If nothing is found, this just returns the command line as-is. | |
| 659 | 680 | ||
| 660 | 681 | This should be used for every log line that print a command line, as well as | |
| 661 | 682 | exception messages. | |
| 662 | 683 | """ | |
| 663 | 684 | new_cmdline = [] | |
| 664 | - for index, to_parse in enumerate(cmdline): | ||
| 665 | - new_cmdline.append(to_parse) | ||
| 666 | - config_key, separator, header = to_parse.partition("=") | ||
| 667 | - header_name, colon, _ = header.partition(":") | ||
| 685 | + for to_parse in cmdline: | ||
| 686 | + if to_parse.startswith("--config="): | ||
| 687 | + option = "--config=" | ||
| 688 | + elif to_parse.startswith("-c"): | ||
| 689 | + option = "-c" | ||
| 690 | + else: | ||
| 691 | + option = "" | ||
| 692 | + assignment = to_parse[len(option) :] | ||
| 693 | + # Redact the whole URL first, before splitting on a possible '=' in userinfo. | ||
| 694 | + assignment = _redact_url_credentials(assignment) or assignment | ||
| 695 | + config_key, separator, value = assignment.partition("=") | ||
| 696 | + header_name, colon, _ = value.partition(":") | ||
| 668 | 697 | if ( | |
| 669 | 698 | separator | |
| 670 | 699 | and colon | |
| 671 | 700 | and config_key.lower().endswith(".extraheader") | |
| 672 | 701 | and header_name.strip().lower() == "authorization" | |
| 673 | 702 | ): | |
| 674 | - new_cmdline[index] = "%s%s%s%s *****" % (config_key, separator, header_name, colon) | ||
| 675 | - continue | ||
| 676 | - try: | ||
| 677 | - url = urlsplit(to_parse) | ||
| 678 | - # Remove password from the URL if present. | ||
| 679 | - if url.password is None and url.username is None: | ||
| 680 | - continue | ||
| 681 | - | ||
| 682 | - # Match urllib.parse's userinfo boundary. Keeping the raw hostinfo | ||
| 683 | - # preserves hostname case, IPv6 brackets, and port formatting. | ||
| 684 | - _, _, hostinfo = url.netloc.rpartition("@") | ||
| 685 | - redacted = "*****:*****" if url.password is not None else "*****" | ||
| 686 | - url = url._replace(netloc=f"{redacted}@{hostinfo}") | ||
| 687 | - new_cmdline[index] = urlunsplit(url) | ||
| 688 | - except ValueError: | ||
| 689 | - # This is not a valid URL. | ||
| 703 | + new_cmdline.append("%s%s%s%s%s *****" % (option, config_key, separator, header_name, colon)) | ||
| 690 | 704 | continue | |
| 705 | + | ||
| 706 | + if separator: | ||
| 707 | + value = _redact_url_credentials(value) or value | ||
| 708 | + new_cmdline.append(option + config_key + separator + value) | ||
| 691 | 709 | return new_cmdline | |
| 692 | 710 | ||
| 693 | 711 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,7 +10,6 @@ | |||
| 10 | 10 | import logging | |
| 11 | 11 | import os | |
| 12 | 12 | import os.path as osp | |
| 13 | - from pathlib import Path | ||
| 14 | 13 | import pickle | |
| 15 | 14 | import re | |
| 16 | 15 | import shutil | |
@@ -20,14 +19,14 @@ | |||
| 20 | 19 | import tempfile | |
| 21 | 20 | import threading | |
| 22 | 21 | import time | |
| 22 | + from pathlib import Path | ||
| 23 | 23 | from unittest import mock, skipUnless | |
| 24 | 24 | ||
| 25 | 25 | import ddt | |
| 26 | 26 | ||
| 27 | 27 | from git import Git, GitCommandError, GitCommandNotFound, Repo, cmd, refresh | |
| 28 | 28 | from git.exc import UnsafeOptionError | |
| 29 | 29 | from git.util import cwd, finalize_process | |
| 30 | - | ||
| 31 | 30 | from test.lib import TestBase, fixture_path, with_rw_directory | |
| 32 | 31 | ||
| 33 | 32 | ||
@@ -547,6 +546,38 @@ def test_it_redacts_authorization_extra_header_from_error(self): | |||
| 547 | 546 | self.assertNotIn(token, message) | |
| 548 | 547 | self.assertIn("http.extraHeader=Authorization: *****", message) | |
| 549 | 548 | ||
| 549 | + @ddt.data( | ||
| 550 | + ( | ||
| 551 | + "--config=http.https://alice@host.proxy=http://bob:token@proxy", | ||
| 552 | + "--config=http.https://*****@host.proxy=http://*****:*****@proxy", | ||
| 553 | + ), | ||
| 554 | + ( | ||
| 555 | + "-curl.https://user:token@host/.insteadOf=https://old/", | ||
| 556 | + "-curl.https://*****:*****@host/.insteadOf=https://old/", | ||
| 557 | + ), | ||
| 558 | + ) | ||
| 559 | + @ddt.unpack | ||
| 560 | + def test_clone_redacts_config_urls_from_trace_and_error(self, option, expected): | ||
| 561 | + for trace in (True, "full"): | ||
| 562 | + with self.subTest(trace=trace), tempfile.TemporaryDirectory() as directory: | ||
| 563 | + with mock.patch.object(Git, "GIT_PYTHON_TRACE", trace): | ||
| 564 | + with self.assertLogs("git.cmd", level="INFO") as logs: | ||
| 565 | + with self.assertRaises(GitCommandError) as context: | ||
| 566 | + Repo.clone_from( | ||
| 567 | + osp.join(directory, "missing-source"), | ||
| 568 | + osp.join(directory, "destination"), | ||
| 569 | + multi_options=[option], | ||
| 570 | + allow_unsafe_options=True, | ||
| 571 | + ) | ||
| 572 | + self.assertIn(expected, context.exception.command) | ||
| 573 | + message = str(context.exception) | ||
| 574 | + assert logs is not None | ||
| 575 | + log_output = " ".join(logs.output) | ||
| 576 | + self.assertIn(expected, log_output) | ||
| 577 | + for credential in ("alice", "bob", "user", "token"): | ||
| 578 | + self.assertNotIn(credential, message) | ||
| 579 | + self.assertNotIn(credential, log_output) | ||
| 580 | + | ||
| 550 | 581 | def test_it_accepts_environment_variables(self): | |
| 551 | 582 | filename = fixture_path("ls_tree_empty") | |
| 552 | 583 | with open(filename, "r") as fh: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -849,13 +849,70 @@ def test_remove_empty_password_keeps_host_intact(self): | |||
| 849 | 849 | ("//user:pass@[2001:db8::1]:0080/repo", "//*****:*****@[2001:db8::1]:0080/repo"), | |
| 850 | 850 | ("https://user:p@ss@example.com/repo", "https://*****:*****@example.com/repo"), | |
| 851 | 851 | ("https://user:@example.com/repo", "https://*****:*****@example.com/repo"), | |
| 852 | + ("https://user:p=ass@example.com/repo", "https://*****:*****@example.com/repo"), | ||
| 852 | 853 | ("https://@example.com/repo", "https://*****@example.com/repo"), | |
| 853 | 854 | ("https://example.com/repo@name?q=a@b#c@d", "https://example.com/repo@name?q=a@b#c@d"), | |
| 854 | 855 | ) | |
| 855 | 856 | @ddt.unpack | |
| 856 | 857 | def test_remove_password_preserves_url_components(self, url, expected): | |
| 857 | 858 | assert remove_password_if_present([url]) == [expected] | |
| 858 | 859 | ||
| 860 | + @ddt.data( | ||
| 861 | + ( | ||
| 862 | + ["-c", "http.proxy=http://user:pass@proxy.example.com:3128"], | ||
| 863 | + ["-c", "http.proxy=http://*****:*****@proxy.example.com:3128"], | ||
| 864 | + ), | ||
| 865 | + ( | ||
| 866 | + ["-c", "remote.origin.url=https://user:pass@example.com/repo"], | ||
| 867 | + ["-c", "remote.origin.url=https://*****:*****@example.com/repo"], | ||
| 868 | + ), | ||
| 869 | + ( | ||
| 870 | + ["--config=http.proxy=http://user:pass@proxy.example.com:3128"], | ||
| 871 | + ["--config=http.proxy=http://*****:*****@proxy.example.com:3128"], | ||
| 872 | + ), | ||
| 873 | + ( | ||
| 874 | + ["--config=url.https://user:pass@example.com/.insteadOf=https://example.com/"], | ||
| 875 | + ["--config=url.https://*****:*****@example.com/.insteadOf=https://example.com/"], | ||
| 876 | + ), | ||
| 877 | + ( | ||
| 878 | + ["--config=http.extraHeader=Authorization: Bearer pass"], | ||
| 879 | + ["--config=http.extraHeader=Authorization: *****"], | ||
| 880 | + ), | ||
| 881 | + ( | ||
| 882 | + ["--config=http.https://alice@host.proxy=http://bob:token@proxy"], | ||
| 883 | + ["--config=http.https://*****@host.proxy=http://*****:*****@proxy"], | ||
| 884 | + ), | ||
| 885 | + ( | ||
| 886 | + ["-c", "http.https://alice@host.proxy=http://bob:token@proxy"], | ||
| 887 | + ["-c", "http.https://*****@host.proxy=http://*****:*****@proxy"], | ||
| 888 | + ), | ||
| 889 | + ( | ||
| 890 | + ["-chttp.https://alice@host.proxy=http://bob:token@proxy"], | ||
| 891 | + ["-chttp.https://*****@host.proxy=http://*****:*****@proxy"], | ||
| 892 | + ), | ||
| 893 | + ( | ||
| 894 | + ["-curl.https://user:token@host/.insteadOf=https://old/"], | ||
| 895 | + ["-curl.https://*****:*****@host/.insteadOf=https://old/"], | ||
| 896 | + ), | ||
| 897 | + ( | ||
| 898 | + ["-chttp.proxy=http://bob:token@proxy"], | ||
| 899 | + ["-chttp.proxy=http://*****:*****@proxy"], | ||
| 900 | + ), | ||
| 901 | + ( | ||
| 902 | + ["--config=http.https://alice:token@host/.extraHeader=Authorization: Bearer pass"], | ||
| 903 | + ["--config=http.https://*****:*****@host/.extraHeader=Authorization: *****"], | ||
| 904 | + ), | ||
| 905 | + ( | ||
| 906 | + ["-chttp.extraHeader=Authorization: Bearer pass"], | ||
| 907 | + ["-chttp.extraHeader=Authorization: *****"], | ||
| 908 | + ), | ||
| 909 | + (["-c", "user.name=pass"], ["-c", "user.name=pass"]), | ||
| 910 | + (["-cuser.name=pass"], ["-cuser.name=pass"]), | ||
| 911 | + ) | ||
| 912 | + @ddt.unpack | ||
| 913 | + def test_remove_password_from_config_values(self, cmdline, expected): | ||
| 914 | + assert remove_password_if_present(cmdline) == expected | ||
| 915 | + | ||
| 859 | 916 | ||
| 860 | 917 | def test_mode_str_to_int_accepts_bytes(): | |
| 861 | 918 | assert mode_str_to_int("100644") == 0o100644 | |
| Back | FazBrowse Home | New Git URL |
0 commit comments