fluidattacks_core_git 12.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fluidattacks_core/git/__init__.py +382 -0
- fluidattacks_core/git/classes.py +29 -0
- fluidattacks_core/git/clone.py +243 -0
- fluidattacks_core/git/codecommit_utils.py +129 -0
- fluidattacks_core/git/constants.py +7 -0
- fluidattacks_core/git/delete_files.py +47 -0
- fluidattacks_core/git/download_file.py +47 -0
- fluidattacks_core/git/download_repo.py +148 -0
- fluidattacks_core/git/https_utils.py +230 -0
- fluidattacks_core/git/py.typed +0 -0
- fluidattacks_core/git/remote.py +45 -0
- fluidattacks_core/git/show_file.py +408 -0
- fluidattacks_core/git/ssh_utils.py +214 -0
- fluidattacks_core/git/upload_repo.py +338 -0
- fluidattacks_core/git/utils.py +143 -0
- fluidattacks_core/git/warp.py +184 -0
- fluidattacks_core_git-12.0.0.dist-info/METADATA +22 -0
- fluidattacks_core_git-12.0.0.dist-info/RECORD +19 -0
- fluidattacks_core_git-12.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
from botocore.exceptions import (
|
|
9
|
+
ClientError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from fluidattacks_core.git.utils import run_git
|
|
13
|
+
|
|
14
|
+
LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_region(url: str) -> str:
|
|
18
|
+
pattern = r"codecommit::([a-z0-9-]+)://"
|
|
19
|
+
match = re.search(pattern, url)
|
|
20
|
+
if match:
|
|
21
|
+
return match.group(1)
|
|
22
|
+
return "us-east-1"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def assume_role_and_execute_git_ls_remote(
|
|
26
|
+
arn: str,
|
|
27
|
+
repo_url: str,
|
|
28
|
+
branch: str,
|
|
29
|
+
org_external_id: str,
|
|
30
|
+
*,
|
|
31
|
+
follow_redirects: bool = False,
|
|
32
|
+
) -> tuple[str | None, str | None]:
|
|
33
|
+
try:
|
|
34
|
+
sts_client = boto3.client("sts")
|
|
35
|
+
assumed_role = sts_client.assume_role(
|
|
36
|
+
RoleArn=arn,
|
|
37
|
+
RoleSessionName=f"session-{uuid.uuid4()}",
|
|
38
|
+
ExternalId=org_external_id,
|
|
39
|
+
)
|
|
40
|
+
credentials = assumed_role["Credentials"]
|
|
41
|
+
|
|
42
|
+
return await codecommit_ls_remote(
|
|
43
|
+
env={
|
|
44
|
+
"AWS_ACCESS_KEY_ID": credentials["AccessKeyId"],
|
|
45
|
+
"AWS_SECRET_ACCESS_KEY": credentials["SecretAccessKey"],
|
|
46
|
+
"AWS_SESSION_TOKEN": credentials["SessionToken"],
|
|
47
|
+
"AWS_DEFAULT_REGION": extract_region(repo_url),
|
|
48
|
+
},
|
|
49
|
+
branch=branch,
|
|
50
|
+
repo_url=repo_url,
|
|
51
|
+
follow_redirects=follow_redirects,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
except ClientError as exc:
|
|
55
|
+
err_message = "Error executing ls-remote with codecommit"
|
|
56
|
+
LOGGER.exception(
|
|
57
|
+
err_message,
|
|
58
|
+
extra={
|
|
59
|
+
"extra": {
|
|
60
|
+
"repo_url": repo_url,
|
|
61
|
+
"arn": arn,
|
|
62
|
+
"org_external_id": org_external_id,
|
|
63
|
+
"exc": exc,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return None, err_message
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _execute_git_command(
|
|
72
|
+
*,
|
|
73
|
+
branch: str,
|
|
74
|
+
env: dict[str, str],
|
|
75
|
+
url: str,
|
|
76
|
+
follow_redirects: bool = False,
|
|
77
|
+
) -> tuple[bytes, bytes, int]:
|
|
78
|
+
return await run_git(
|
|
79
|
+
"-c",
|
|
80
|
+
"http.sslVerify=false",
|
|
81
|
+
"-c",
|
|
82
|
+
f"http.followRedirects={follow_redirects}",
|
|
83
|
+
"ls-remote",
|
|
84
|
+
"--",
|
|
85
|
+
url,
|
|
86
|
+
branch,
|
|
87
|
+
env={**os.environ.copy(), **env},
|
|
88
|
+
timeout=20,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def codecommit_ls_remote(
|
|
93
|
+
*,
|
|
94
|
+
branch: str,
|
|
95
|
+
env: dict[str, str],
|
|
96
|
+
repo_url: str,
|
|
97
|
+
follow_redirects: bool = False,
|
|
98
|
+
) -> tuple[str | None, str | None]:
|
|
99
|
+
try:
|
|
100
|
+
stdout, stderr, return_code = await _execute_git_command(
|
|
101
|
+
branch=branch,
|
|
102
|
+
env=env,
|
|
103
|
+
follow_redirects=follow_redirects,
|
|
104
|
+
url=repo_url,
|
|
105
|
+
)
|
|
106
|
+
except asyncio.exceptions.TimeoutError:
|
|
107
|
+
return None, "git ls-remote time out"
|
|
108
|
+
|
|
109
|
+
if return_code == 0:
|
|
110
|
+
return stdout.decode().split("\t")[0], None
|
|
111
|
+
|
|
112
|
+
return None, stderr.decode("utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def call_codecommit_ls_remote(
|
|
116
|
+
repo_url: str,
|
|
117
|
+
arn: str,
|
|
118
|
+
branch: str,
|
|
119
|
+
org_external_id: str,
|
|
120
|
+
*,
|
|
121
|
+
follow_redirects: bool = False,
|
|
122
|
+
) -> tuple[str | None, str | None]:
|
|
123
|
+
return await assume_role_and_execute_git_ls_remote(
|
|
124
|
+
repo_url=repo_url,
|
|
125
|
+
arn=arn,
|
|
126
|
+
branch=branch,
|
|
127
|
+
org_external_id=org_external_id,
|
|
128
|
+
follow_redirects=follow_redirects,
|
|
129
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pathspec import PathSpec
|
|
7
|
+
from pathspec import util as pathspec_util
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def remove_empty_directories(repo_path: str) -> None:
|
|
13
|
+
for root, dirs, _ in os.walk(repo_path, topdown=False):
|
|
14
|
+
for name in dirs:
|
|
15
|
+
path = os.path.join(root, name) # noqa: PTH118
|
|
16
|
+
try:
|
|
17
|
+
if not os.listdir(path): # noqa: PTH208
|
|
18
|
+
Path(path).rmdir()
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
LOGGER.exception(
|
|
21
|
+
"Error removing empty directory", extra={"extra": {"dir_path": path}}
|
|
22
|
+
)
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def delete_out_of_scope_files(git_ignore: list[str], repo_path: str) -> None:
|
|
27
|
+
# Compute what files should be deleted according to the scope rules
|
|
28
|
+
spec = PathSpec.from_lines("gitwildmatch", git_ignore)
|
|
29
|
+
try:
|
|
30
|
+
matches = list(spec.match_tree(repo_path, follow_links=False))
|
|
31
|
+
except pathspec_util.RecursionError:
|
|
32
|
+
LOGGER.exception(
|
|
33
|
+
"RecursionError while matching tree (symlink cycle detected)",
|
|
34
|
+
extra={"extra": {"repo_path": repo_path}},
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
for match in matches:
|
|
39
|
+
if match.startswith(".git/"):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
file_path = os.path.join(repo_path, match) # noqa: PTH118
|
|
43
|
+
if Path(file_path).is_file():
|
|
44
|
+
with suppress(FileNotFoundError):
|
|
45
|
+
Path(file_path).unlink()
|
|
46
|
+
|
|
47
|
+
remove_empty_directories(repo_path)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import aiofiles
|
|
4
|
+
import aiohttp
|
|
5
|
+
import anyio
|
|
6
|
+
|
|
7
|
+
from .constants import DEFAULT_DOWNLOAD_BUFFER_SIZE
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def _write_response_to_file(
|
|
13
|
+
response: aiohttp.ClientResponse,
|
|
14
|
+
destination_path: str,
|
|
15
|
+
download_buffer_size: int,
|
|
16
|
+
) -> bool:
|
|
17
|
+
async with aiofiles.open(destination_path, "wb") as file:
|
|
18
|
+
while True:
|
|
19
|
+
try:
|
|
20
|
+
chunk = await response.content.read(download_buffer_size)
|
|
21
|
+
except TimeoutError:
|
|
22
|
+
LOGGER.exception("Read timeout for path %s", destination_path)
|
|
23
|
+
return False
|
|
24
|
+
if not chunk:
|
|
25
|
+
break
|
|
26
|
+
await file.write(chunk)
|
|
27
|
+
return await anyio.Path(destination_path).exists()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def download_file(
|
|
31
|
+
*,
|
|
32
|
+
url: str,
|
|
33
|
+
destination_path: str,
|
|
34
|
+
download_buffer_size: int = DEFAULT_DOWNLOAD_BUFFER_SIZE,
|
|
35
|
+
) -> bool:
|
|
36
|
+
timeout = aiohttp.ClientTimeout(total=60 * 60, connect=30)
|
|
37
|
+
async with aiohttp.ClientSession(timeout=timeout) as session: # noqa: SIM117
|
|
38
|
+
async with session.get(url) as response:
|
|
39
|
+
if response.status != 200:
|
|
40
|
+
LOGGER.error(
|
|
41
|
+
"Failed to download file: HTTP %s, for path %s",
|
|
42
|
+
response.status,
|
|
43
|
+
destination_path,
|
|
44
|
+
)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
return await _write_response_to_file(response, destination_path, download_buffer_size)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tarfile
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import anyio
|
|
9
|
+
from git import GitError
|
|
10
|
+
from git.cmd import Git
|
|
11
|
+
from git.repo import Repo
|
|
12
|
+
|
|
13
|
+
from .constants import DEFAULT_DOWNLOAD_BUFFER_SIZE
|
|
14
|
+
from .delete_files import delete_out_of_scope_files
|
|
15
|
+
from .download_file import download_file
|
|
16
|
+
|
|
17
|
+
LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_member_safe(
|
|
21
|
+
member: tarfile.TarInfo,
|
|
22
|
+
) -> bool:
|
|
23
|
+
return not (
|
|
24
|
+
member.issym() or member.islnk() or Path(member.name).is_absolute() or "../" in member.name
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _safe_extract_tar(tar_handler: tarfile.TarFile, file_path: Path) -> bool:
|
|
29
|
+
for member in tar_handler.getmembers():
|
|
30
|
+
if not _is_member_safe(member):
|
|
31
|
+
LOGGER.error("Unsafe path detected: %s", member.name)
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
tar_handler.extract(member, path=file_path, numeric_owner=True)
|
|
35
|
+
except tarfile.ExtractError:
|
|
36
|
+
LOGGER.exception("Error extracting %s", member.name)
|
|
37
|
+
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def remove_symlinks_in_directory(directory: str) -> None:
|
|
42
|
+
for root, dirs, files in os.walk(directory):
|
|
43
|
+
for name in (*dirs, *files):
|
|
44
|
+
file_path = os.path.join(root, name) # noqa: PTH118
|
|
45
|
+
if Path(file_path).is_symlink():
|
|
46
|
+
Path(file_path).unlink(missing_ok=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _reset_repo_sync(repo_path: str) -> bool:
|
|
50
|
+
try:
|
|
51
|
+
Path.cwd()
|
|
52
|
+
except OSError:
|
|
53
|
+
LOGGER.exception("Failed to get the working directory: %s", repo_path)
|
|
54
|
+
os.chdir(repo_path)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
Git().execute(
|
|
58
|
+
[
|
|
59
|
+
"git",
|
|
60
|
+
"config",
|
|
61
|
+
"--global",
|
|
62
|
+
"--add",
|
|
63
|
+
"safe.directory",
|
|
64
|
+
"*",
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
except GitError:
|
|
68
|
+
LOGGER.exception("Failed to add safe directory %s", repo_path)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
repo = Repo(repo_path)
|
|
72
|
+
repo.git.reset("--hard", "HEAD")
|
|
73
|
+
except GitError:
|
|
74
|
+
LOGGER.exception("Expand repositories has failed for repository %s", repo_path)
|
|
75
|
+
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
if repo.working_dir:
|
|
79
|
+
remove_symlinks_in_directory(str(repo.working_dir))
|
|
80
|
+
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def reset_repo(repo_path: str) -> bool:
|
|
85
|
+
return await anyio.to_thread.run_sync(_reset_repo_sync, repo_path)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _extract_and_replace_sync(file_path: Path, tmp_path: Path, destination_path: Path) -> bool:
|
|
89
|
+
with tarfile.open(file_path, "r:gz") as tar_handler:
|
|
90
|
+
_safe_extract_tar(tar_handler, tmp_path)
|
|
91
|
+
|
|
92
|
+
extracted_dirs = [d for d in tmp_path.iterdir() if d.is_dir()]
|
|
93
|
+
if not extracted_dirs:
|
|
94
|
+
LOGGER.error("No directory found in the extracted archive: %s", destination_path)
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
if len(extracted_dirs) > 1:
|
|
98
|
+
LOGGER.warning(
|
|
99
|
+
"Multiple directories found in archive, using first one: %s",
|
|
100
|
+
destination_path,
|
|
101
|
+
)
|
|
102
|
+
extracted_dir = extracted_dirs[0]
|
|
103
|
+
|
|
104
|
+
if destination_path.exists():
|
|
105
|
+
shutil.rmtree(destination_path)
|
|
106
|
+
|
|
107
|
+
shutil.move(extracted_dir, destination_path)
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def download_repo_from_s3(
|
|
112
|
+
download_url: str,
|
|
113
|
+
destination_path: Path,
|
|
114
|
+
git_ignore: list[str] | None = None,
|
|
115
|
+
*,
|
|
116
|
+
download_buffer_size: int = DEFAULT_DOWNLOAD_BUFFER_SIZE,
|
|
117
|
+
) -> bool:
|
|
118
|
+
await anyio.Path(destination_path.parent).mkdir(parents=True, exist_ok=True)
|
|
119
|
+
with tempfile.TemporaryDirectory(prefix="fluidattacks_", ignore_cleanup_errors=True) as tmpdir:
|
|
120
|
+
tmp_path = Path(tmpdir)
|
|
121
|
+
file_path = tmp_path / "repo.tar.gz"
|
|
122
|
+
result = await download_file(
|
|
123
|
+
url=download_url,
|
|
124
|
+
destination_path=str(file_path.absolute()),
|
|
125
|
+
download_buffer_size=download_buffer_size,
|
|
126
|
+
)
|
|
127
|
+
if not result:
|
|
128
|
+
LOGGER.error("Failed to download repository from %s", download_url)
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
if not await anyio.to_thread.run_sync(
|
|
133
|
+
_extract_and_replace_sync, file_path, tmp_path, destination_path
|
|
134
|
+
):
|
|
135
|
+
return False
|
|
136
|
+
except OSError:
|
|
137
|
+
LOGGER.exception(
|
|
138
|
+
"Error downloading repository", extra={"extra": {"path": destination_path}}
|
|
139
|
+
)
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
if not await reset_repo(str(destination_path)):
|
|
143
|
+
shutil.rmtree(destination_path, ignore_errors=True)
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
delete_out_of_scope_files(git_ignore or [], str(destination_path))
|
|
147
|
+
|
|
148
|
+
return True
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
from fluidattacks_core.http.client import request
|
|
8
|
+
from fluidattacks_core.http.validations import HTTPValidationError
|
|
9
|
+
from urllib3.exceptions import LocationParseError
|
|
10
|
+
from urllib3.util import Url, parse_url
|
|
11
|
+
|
|
12
|
+
from fluidattacks_core.git.classes import InvalidParameter
|
|
13
|
+
from fluidattacks_core.git.utils import format_url, get_https_git_config_args, run_git
|
|
14
|
+
|
|
15
|
+
LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_redirected_url(
|
|
19
|
+
original_url: Url,
|
|
20
|
+
redirect_url: Url,
|
|
21
|
+
) -> str:
|
|
22
|
+
return (
|
|
23
|
+
redirect_url._replace(
|
|
24
|
+
query=None,
|
|
25
|
+
path=(redirect_url.path or "").removesuffix("info/refs"),
|
|
26
|
+
).url
|
|
27
|
+
if original_url.host == redirect_url.host
|
|
28
|
+
else original_url.url
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def get_redirected_url(
|
|
33
|
+
*,
|
|
34
|
+
url: str,
|
|
35
|
+
user: str | None,
|
|
36
|
+
password: str | None,
|
|
37
|
+
token: str | None,
|
|
38
|
+
is_pat: bool,
|
|
39
|
+
) -> str:
|
|
40
|
+
try:
|
|
41
|
+
uri = parse_url(url)._replace(auth=None).url
|
|
42
|
+
except LocationParseError:
|
|
43
|
+
uri = url
|
|
44
|
+
if user is not None and password is not None:
|
|
45
|
+
return await _get_redirected_url(
|
|
46
|
+
uri,
|
|
47
|
+
authorization="Basic " + base64.b64encode(f"{user}:{password}".encode()).decode(),
|
|
48
|
+
)
|
|
49
|
+
if token is not None and is_pat:
|
|
50
|
+
return await _get_redirected_url(
|
|
51
|
+
uri,
|
|
52
|
+
authorization=("Basic " + base64.b64encode(f":{token}".encode()).decode()),
|
|
53
|
+
)
|
|
54
|
+
if token is not None and not is_pat:
|
|
55
|
+
return await _get_redirected_url(uri, authorization=f"Bearer {token}")
|
|
56
|
+
if url.startswith("http"):
|
|
57
|
+
return await _get_redirected_url(url)
|
|
58
|
+
raise InvalidParameter
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def _get_redirected_url(
|
|
62
|
+
url: str,
|
|
63
|
+
authorization: str | None = None,
|
|
64
|
+
) -> str:
|
|
65
|
+
try:
|
|
66
|
+
return await _get_url(url, authorization=authorization)
|
|
67
|
+
except (aiohttp.ClientError, TimeoutError) as exc:
|
|
68
|
+
LOGGER.warning(
|
|
69
|
+
"Failed to get redirected-url",
|
|
70
|
+
extra={"extra": {"url": url, "exc": exc}},
|
|
71
|
+
)
|
|
72
|
+
raise
|
|
73
|
+
except (ValueError, HTTPValidationError) as exc:
|
|
74
|
+
LOGGER.warning(
|
|
75
|
+
"Failed validation to get redirected-url",
|
|
76
|
+
extra={"extra": {"url": url, "exc": exc}},
|
|
77
|
+
)
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _get_url(
|
|
82
|
+
original_url: str,
|
|
83
|
+
*,
|
|
84
|
+
redirect_url: str = "",
|
|
85
|
+
max_retries: int = 5,
|
|
86
|
+
authorization: str | None = None,
|
|
87
|
+
) -> str:
|
|
88
|
+
try:
|
|
89
|
+
original = parse_url(original_url.removesuffix("/"))
|
|
90
|
+
url = parse_url(redirect_url.removesuffix("/")) if redirect_url else original
|
|
91
|
+
except LocationParseError as exc:
|
|
92
|
+
msg = f"Invalid URL {redirect_url}"
|
|
93
|
+
raise HTTPValidationError(msg) from exc
|
|
94
|
+
|
|
95
|
+
if max_retries < 1:
|
|
96
|
+
return format_redirected_url(original, url)
|
|
97
|
+
|
|
98
|
+
# https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
|
|
99
|
+
url = (
|
|
100
|
+
url._replace(path=(url.path or "") + "/info/refs")
|
|
101
|
+
if not (url.path or "").endswith("info/refs")
|
|
102
|
+
else url
|
|
103
|
+
)
|
|
104
|
+
url = url._replace(query="service=git-upload-pack")
|
|
105
|
+
|
|
106
|
+
result = await request(
|
|
107
|
+
url.url,
|
|
108
|
+
method="GET",
|
|
109
|
+
headers={
|
|
110
|
+
"Host": url.host or "",
|
|
111
|
+
"Accept": "*/*",
|
|
112
|
+
"Accept-Encoding": "deflate, gzip",
|
|
113
|
+
"Pragma": "no-cache",
|
|
114
|
+
"Accept-Language": "*",
|
|
115
|
+
"User-Agent": "FluidAttacksAPIClient/1.0",
|
|
116
|
+
**({"Authorization": authorization} if authorization else {}),
|
|
117
|
+
},
|
|
118
|
+
timeout=20,
|
|
119
|
+
)
|
|
120
|
+
if result.status == 200:
|
|
121
|
+
return format_redirected_url(original, url)
|
|
122
|
+
if result.status > 300 and result.status < 400 and "Location" in result.headers:
|
|
123
|
+
with suppress(LocationParseError):
|
|
124
|
+
_url = parse_url(result.headers["Location"])
|
|
125
|
+
return await _get_url(
|
|
126
|
+
original_url,
|
|
127
|
+
redirect_url=result.headers["Location"],
|
|
128
|
+
max_retries=max_retries - 1,
|
|
129
|
+
authorization=authorization if _url.host == url.host else None,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return await _get_url(
|
|
133
|
+
original_url,
|
|
134
|
+
redirect_url=result.headers["Location"],
|
|
135
|
+
max_retries=max_retries - 1,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return format_redirected_url(original, url)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _execute_git_command(
|
|
142
|
+
*,
|
|
143
|
+
url: str,
|
|
144
|
+
branch: str,
|
|
145
|
+
is_pat: bool,
|
|
146
|
+
token: str | None = None,
|
|
147
|
+
follow_redirects: bool = False,
|
|
148
|
+
) -> tuple[bytes, bytes, int]:
|
|
149
|
+
git_config_args = get_https_git_config_args(
|
|
150
|
+
follow_redirects=follow_redirects,
|
|
151
|
+
is_pat=is_pat,
|
|
152
|
+
token=token,
|
|
153
|
+
)
|
|
154
|
+
return await run_git(*git_config_args, "ls-remote", "--", url, branch, timeout=20)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def https_ls_remote( # noqa: PLR0913
|
|
158
|
+
*,
|
|
159
|
+
repo_url: str,
|
|
160
|
+
branch: str,
|
|
161
|
+
user: str | None = None,
|
|
162
|
+
password: str | None = None,
|
|
163
|
+
token: str | None = None,
|
|
164
|
+
provider: str | None = None,
|
|
165
|
+
is_pat: bool = False,
|
|
166
|
+
follow_redirects: bool = False,
|
|
167
|
+
) -> tuple[str | None, str | None]:
|
|
168
|
+
url = format_url(
|
|
169
|
+
repo_url=repo_url,
|
|
170
|
+
user=user,
|
|
171
|
+
password=password,
|
|
172
|
+
token=token,
|
|
173
|
+
provider=provider,
|
|
174
|
+
is_pat=is_pat,
|
|
175
|
+
)
|
|
176
|
+
try:
|
|
177
|
+
stdout, stderr, return_code = await _execute_git_command(
|
|
178
|
+
url=url,
|
|
179
|
+
branch=branch,
|
|
180
|
+
is_pat=is_pat,
|
|
181
|
+
token=token,
|
|
182
|
+
follow_redirects=follow_redirects,
|
|
183
|
+
)
|
|
184
|
+
except asyncio.exceptions.TimeoutError:
|
|
185
|
+
return None, "git ls-remote time out"
|
|
186
|
+
|
|
187
|
+
if return_code == 0:
|
|
188
|
+
return stdout.decode().split("\t")[0], None
|
|
189
|
+
|
|
190
|
+
return None, stderr.decode("utf-8")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def call_https_ls_remote( # noqa: PLR0913
|
|
194
|
+
*,
|
|
195
|
+
repo_url: str,
|
|
196
|
+
user: str | None,
|
|
197
|
+
password: str | None,
|
|
198
|
+
token: str | None,
|
|
199
|
+
branch: str,
|
|
200
|
+
provider: str | None,
|
|
201
|
+
is_pat: bool,
|
|
202
|
+
follow_redirects: bool = False,
|
|
203
|
+
) -> tuple[str | None, str | None]:
|
|
204
|
+
if user is not None and password is not None:
|
|
205
|
+
return await https_ls_remote(
|
|
206
|
+
repo_url=repo_url,
|
|
207
|
+
user=user,
|
|
208
|
+
password=password,
|
|
209
|
+
branch=branch,
|
|
210
|
+
follow_redirects=follow_redirects,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if token is not None:
|
|
214
|
+
return await https_ls_remote(
|
|
215
|
+
repo_url=repo_url,
|
|
216
|
+
token=token,
|
|
217
|
+
branch=branch,
|
|
218
|
+
provider=provider or "",
|
|
219
|
+
is_pat=is_pat,
|
|
220
|
+
follow_redirects=follow_redirects,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if repo_url.startswith("http"):
|
|
224
|
+
return await https_ls_remote(
|
|
225
|
+
repo_url=repo_url,
|
|
226
|
+
branch=branch,
|
|
227
|
+
follow_redirects=follow_redirects,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
raise InvalidParameter
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .codecommit_utils import call_codecommit_ls_remote
|
|
2
|
+
from .https_utils import call_https_ls_remote
|
|
3
|
+
from .ssh_utils import call_ssh_ls_remote
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def ls_remote( # noqa: PLR0913
|
|
7
|
+
repo_url: str,
|
|
8
|
+
repo_branch: str,
|
|
9
|
+
*,
|
|
10
|
+
credential_key: str | None = None,
|
|
11
|
+
user: str | None = None,
|
|
12
|
+
password: str | None = None,
|
|
13
|
+
token: str | None = None,
|
|
14
|
+
provider: str | None = None,
|
|
15
|
+
is_pat: bool = False,
|
|
16
|
+
arn: str | None = None,
|
|
17
|
+
org_external_id: str | None = None,
|
|
18
|
+
follow_redirects: bool = False,
|
|
19
|
+
) -> tuple[str | None, str | None]:
|
|
20
|
+
if credential_key is not None:
|
|
21
|
+
return await call_ssh_ls_remote(
|
|
22
|
+
repo_url,
|
|
23
|
+
credential_key,
|
|
24
|
+
repo_branch,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if arn is not None and org_external_id is not None:
|
|
28
|
+
return await call_codecommit_ls_remote(
|
|
29
|
+
repo_url,
|
|
30
|
+
arn,
|
|
31
|
+
repo_branch,
|
|
32
|
+
org_external_id=org_external_id,
|
|
33
|
+
follow_redirects=follow_redirects,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return await call_https_ls_remote(
|
|
37
|
+
repo_url=repo_url,
|
|
38
|
+
user=user,
|
|
39
|
+
password=password,
|
|
40
|
+
token=token,
|
|
41
|
+
branch=repo_branch,
|
|
42
|
+
provider=provider,
|
|
43
|
+
is_pat=is_pat,
|
|
44
|
+
follow_redirects=follow_redirects,
|
|
45
|
+
)
|