Ответ 1
В конфигурации по умолчанию, когда требуется имя пользователя или пароль, git
будет напрямую обращаться к /dev/tty
для лучшего контроля над "управляющим" терминальным устройством, например устройством, которое позволяет вам взаимодействовать с пользователем. Поскольку подпроцессы по умолчанию наследуют управляющий терминал от своего родителя, все запущенные вами процессы git будут обращаться к одному и тому же устройству TTY. Так что да, они будут зависать при попытке чтения и записи в один и тот же TTY с процессами, забивающими друг друга ожидаемым вводом.
Упрощенный метод предотвращения этого состоит в том, чтобы дать каждому подпроцессу свой собственный сеанс; каждый сеанс имеет различный управляющий TTY. Сделайте это, установив start_new_session=True
:
process = await asyncio.create_subprocess_exec(
*cmds, stdout=asyncio.subprocess.PIPE, cwd=path, start_new_session=True)
Вы не можете заранее определить, какие команды git могут требовать учетные данные пользователя, потому что git может быть настроен на получение учетных данных из целого ряда мест, и они используются только в том случае, если удаленный репозиторий действительно требует проверки подлинности.
Что еще хуже, для удаленных URL-адресов ssh://
git вообще не обрабатывает аутентификацию, а оставляет ее клиентскому процессу ssh
который он открывает. Подробнее об этом ниже.
Как Git запрашивает учетные данные (для чего угодно, кроме ssh
), однако настраивается; см. документацию gitcredentials. Вы можете использовать это, если ваш код должен перенаправлять запросы учетных данных конечному пользователю. Я не позволю командам git делать это через терминал, потому что как пользователь узнает, какая конкретная команда git получит какие учетные данные, не говоря уже о проблемах, которые у вас возникнут, чтобы убедиться, что приглашения приходят в логический порядок.
Вместо этого я бы направил все запросы на учетные данные через ваш скрипт. У вас есть два варианта сделать это с:
-
Установите
GIT_ASKPASS
средыGIT_ASKPASS
, указав на исполняемый файл, который git должен запускать для каждого приглашения.Этот исполняемый файл вызывается с одним аргументом, подсказкой для отображения пользователю. Он вызывается отдельно для каждой части информации, необходимой для заданных учетных данных, например, для имени пользователя (если оно еще не известно) и пароля. Текст подсказки должен прояснить пользователю, о чем его просят (например,
"Username for 'https://github.com': "
или"Password for 'https://[email protected]': "
. -
Зарегистрировать учетную запись помощника; это выполняется как команда оболочки (так что может иметь свои собственные предварительно настроенные аргументы командной строки -c) и один дополнительный аргумент, сообщающий помощнику, какая операция ожидается от него. Если он передается
get
в качестве последнего аргумента, то просят предоставить учетные данные для данного хоста и протокола, или можно сказать, что определенные полномочия были успешными сstore
, или были отвергнуты сerase
. Во всех случаях он может читать информацию из stdin, чтобы узнать, на каком хосте git пытается пройти аутентификацию, в формате многострочныйkey=value
.Таким образом, с помощником по учетным данным, вы получаете запрос на комбинацию имени пользователя и пароля вместе, как один шаг, и вы также получаете больше информации о процессе; обработка операций
store
иerase
позволяет более эффективно кэшировать учетные данные.
Git fill сначала запрашивает каждого сконфигурированного помощника по FILES
данным в порядке конфигурации (см. Раздел " FILES
", чтобы понять, как 4 расположения файла конфигурации обрабатываются по порядку). Вы можете добавить новую одноразовую вспомогательную конфигурацию в командной строке git
с помощью -c credential.helper=...
командной строки -c credential.helper=...
, который добавляется в конец. Если никакой помощник по GIT_ASKPASS
данным не смог заполнить отсутствующее имя пользователя или пароль, то пользователю предлагается GIT_ASKPASS
или другие параметры запроса.
Для соединений SSH git создает новый дочерний процесс ssh
. Затем SSH будет обрабатывать аутентификацию и может запрашивать у пользователя учетные данные или ssh-ключи, запрашивать у пользователя кодовую фразу. Это снова будет сделано через /dev/tty
, и SSH более упрям в этом. Хотя вы можете установить SSH_ASKPASS
среды SSH_ASKPASS
в двоичный файл, который будет использоваться для запроса, SSH будет использовать его только в том случае, если нет сеанса TTY и также установлен DISPLAY
.
SSH_ASKPASS
должен быть исполняемым файлом (поэтому не следует передавать аргументы), и вы не будете уведомлены об успехе или сбое запрашиваемых учетных данных.
Я также обязательно скопировал бы текущие переменные окружения в дочерние процессы, потому что, если пользователь настроил агент ключей SSH для кэширования ключей ssh, вы бы хотели, чтобы процессы SSH, которые git начал использовать их; ключевой агент обнаруживается через переменные среды.
Итак, чтобы создать соединение для помощника по SSH_ASKPASS
, который также работает для SSH_ASKPASS
, вы можете использовать простой синхронный скрипт, который берет сокет из переменной среды:
#!/path/to/python3
import os, socket, sys
path = os.environ['PROMPTING_SOCKET_PATH']
operation = sys.argv[1]
if operation not in {'get', 'store', 'erase'}:
operation, params = 'prompt', f'prompt={operation}\n'
else:
params = sys.stdin.read()
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect(path)
s.sendall(f'''operation={operation}\n{params}'''.encode())
print(s.recv(2048).decode())
Для этого должен быть установлен исполняемый бит.
Затем он может быть передан команде git как временный файл или включен в PROMPTING_SOCKET_PATH
, и вы добавите путь к PROMPTING_SOCKET_PATH
домена Unix в переменную среды PROMPTING_SOCKET_PATH
. Он может использоваться как SSH_ASKPASS
, что позволяет настроить операцию на prompt
.
Затем этот сценарий заставляет и SSH, и git запрашивать учетные данные пользователя на сервере сокетов домена UNIX в отдельном соединении для каждого пользователя. Я использовал щедрый размер приемного буфера, я не думаю, что вы когда-нибудь столкнетесь с обменом с этим протоколом, который будет превышать его, и при этом я не вижу причин для его недостаточного заполнения. Это делает сценарий красивым и простым.
Вместо этого вы можете использовать ее в качестве команды GIT_ASKPASS
, но тогда вы не получите ценной информации об успешном использовании учетных данных для соединений не-ssh.
Вот демонстрационная реализация сервера сокетов домена UNIX, который обрабатывает git и запросы учетных данных от вышеупомянутого помощника учетных данных, тот, который просто генерирует случайные шестнадцатеричные значения, а не спрашивает пользователя:
import asyncio
import os
import secrets
import tempfile
async def handle_git_prompt(reader, writer):
data = await reader.read(2048)
info = dict(line.split('=', 1) for line in data.decode().splitlines())
print(f"Received credentials request: {info!r}")
response = []
operation = info.pop('operation', 'get')
if operation == 'prompt':
# new prompt for a username or password or pass phrase for SSH
password = secrets.token_hex(10)
print(f"Sending prompt response: {password!r}")
response.append(password)
elif operation == 'get':
# new request for credentials, for a username (optional) and password
if 'username' not in info:
username = secrets.token_hex(10)
print(f"Sending username: {username!r}")
response.append(f'username={username}\n')
password = secrets.token_hex(10)
print(f"Sending password: {password!r}")
response.append(f'password={password}\n')
elif operation == 'store':
# credentials were used successfully, perhaps store these for re-use
print(f"Credentials for {info['username']} were approved")
elif operation == 'erase':
# credentials were rejected, if we cached anything, clear this now.
print(f"Credentials for {info['username']} were rejected")
writer.write(''.join(response).encode())
await writer.drain()
print("Closing the connection")
writer.close()
await writer.wait_closed()
async def main():
with tempfile.TemporaryDirectory() as dirname:
socket_path = os.path.join(dirname, 'credential.helper.sock')
server = await asyncio.start_unix_server(handle_git_prompt, socket_path)
print(f'Starting a domain socket at {server.sockets[0].getsockname()}')
async with server:
await server.serve_forever()
asyncio.run(main())
Обратите внимание, что помощник по учетным данным может также добавить quit=true
или quit=1
к выводу, чтобы сказать git, что не нужно искать никаких других помощников по учетным данным и никаких дальнейших запросов.
Вы можете использовать команду git credential <operation>
чтобы проверить, работает ли помощник по /full/path/to/credhelper.py
данным, передав сценарий помощника (/full/path/to/credhelper.py
) с помощью git -c credential.helper=...
опция командной строки. git credential
могут принимать строку url=...
на стандартном вводе, это будет проанализировано так же, как git связался бы с помощниками учетных данных; см. документацию для полной спецификации формата обмена.
Сначала запустите приведенный выше демонстрационный скрипт в отдельном терминале:
$ /usr/local/bin/python3.7 git-credentials-demo.py
Starting a domain socket at /tmp/credhelper.py /var/folders/vh/80414gbd6p1cs28cfjtql3l80000gn/T/tmprxgyvecj/credential.helper.sock
и затем попытайтесь получить учетные данные от этого; Я включил демонстрацию операций store
и erase
тоже:
$ export PROMPTING_SOCKET_PATH="/var/folders/vh/80414gbd6p1cs28cfjtql3l80000gn/T/tmprxgyvecj/credential.helper.sock"
$ CREDHELPER="/tmp/credhelper.py"
$ echo "url=https://example.com:4242/some/path.git" | git -c "credential.helper=$CREDHELPER" credential fill
protocol=https
host=example.com:4242
username=5b5b0b9609c1a4f94119
password=e259f5be2c96fed718e6
$ echo "url=https://[email protected]/some/path.git" | git -c "credential.helper=$CREDHELPER" credential fill
protocol=https
host=example.com
username=someuser
password=766df0fba1de153c3e99
$ printf "protocol=https\nhost=example.com:4242\nusername=5b5b0b9609c1a4f94119\npassword=e259f5be2c96fed718e6" | git -c "credential.helper=$CREDHELPER" credential approve
$ printf "protocol=https\nhost=example.com\nusername=someuser\npassword=e259f5be2c96fed718e6" | git -c "credential.helper=$CREDHELPER" credential reject
и когда вы посмотрите на вывод примера сценария, вы увидите:
Received credentials request: {'operation': 'get', 'protocol': 'https', 'host': 'example.com:4242'}
Sending username: '5b5b0b9609c1a4f94119'
Sending password: 'e259f5be2c96fed718e6'
Closing the connection
Received credentials request: {'operation': 'get', 'protocol': 'https', 'host': 'example.com', 'username': 'someuser'}
Sending password: '766df0fba1de153c3e99'
Closing the connection
Received credentials request: {'operation': 'store', 'protocol': 'https', 'host': 'example.com:4242', 'username': '5b5b0b9609c1a4f94119', 'password': 'e259f5be2c96fed718e6'}
Credentials for 5b5b0b9609c1a4f94119 were approved
Closing the connection
Received credentials request: {'operation': 'erase', 'protocol': 'https', 'host': 'example.com', 'username': 'someuser', 'password': 'e259f5be2c96fed718e6'}
Credentials for someuser were rejected
Closing the connection
Обратите внимание, как помощнику предоставляется разобранный набор полей для protocol
и host
, а путь опущен; если вы установите опцию git config credential.useHttpPath=true
(или она уже была установлена для вас), то к path=some/path.git
будет добавлен path=some/path.git
.
Для SSH исполняемый файл просто вызывается с приглашением для отображения:
$ $CREDHELPER "Please enter a super-secret passphrase: "
30b5978210f46bb968b2
и демонстрационный сервер напечатал:
Received credentials request: {'operation': 'prompt', 'prompt': 'Please enter a super-secret passphrase: '}
Sending prompt response: '30b5978210f46bb968b2'
Closing the connection
Просто убедитесь, что при запуске процессов git все еще установлено start_new_session=True
чтобы SSH принудительно использовал SSH_ASKPASS
.
env = {
os.environ,
SSH_ASKPASS='../path/to/credhelper.py',
DISPLAY='dummy value',
PROMPTING_SOCKET_PATH='../path/to/domain/socket',
}
process = await asyncio.create_subprocess_exec(
*cmds, stdout=asyncio.subprocess.PIPE, cwd=path,
start_new_session=True, env=env)
Конечно, то, как вы затем обрабатываете запросы пользователей, является отдельной проблемой, но ваш скрипт теперь имеет полный контроль (каждая команда git
будет терпеливо ждать, пока помощник по учетным данным вернет запрошенную информацию), и вы можете ставить в очередь запросы, чтобы пользователь заполнил в, и вы можете кэшировать учетные данные по мере необходимости (в случае, если все команды ждут учетные данные для одного и того же хоста).