В конфигурации по умолчанию, когда требуется имя пользователя или пароль, 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 можно настроить для получения учетных данных из целого ряда местоположений, и они используются только в том случае, если удаленный репозиторий на самом деле проблемы для аутентификации.
Что еще хуже, для ssh://
удаленных URL-адресов git вообще не обрабатывает аутентификацию, а оставляет ее клиентскому процессу ssh
, который он открывает. Подробнее об этом ниже.
Как Git запрашивает учетные данные (для всего, кроме ssh
), однако настраивается; см. документацию gitcredentials . Вы можете использовать это, если ваш код должен перенаправлять запросы учетных данных конечному пользователю. Я не позволю командам git делать это через терминал, потому что как пользователь узнает, какая конкретная команда git получит какие учетные данные, не говоря уже о проблемах, которые у вас возникнут, чтобы убедиться, что приглашения приходят в логический порядок.
Вместо этого я бы направил все запросы на учетные данные через ваш скрипт. У вас есть два варианта сделать это с:
Установите переменную среды GIT_ASKPASS
, указывая на исполняемый файл, который git должен запускать для каждой подсказки.
Этот исполняемый файл вызывается с одним аргументом, подсказкой для отображения пользователю. Он вызывается отдельно для каждой части информации, необходимой для заданных учетных данных, например, для имени пользователя (если оно еще не известно) и пароля. Текст подсказки должен прояснить пользователю, о чем его просят (например, "Username for 'https://github.com': "
или "Password for 'https://someusername@github.com': "
.
Зарегистрировать помощник по учетным данным ; это выполняется как команда оболочки (так что может иметь свои предварительно сконфигурированные аргументы командной строки) и один дополнительный аргумент, сообщающий помощнику, какая операция ожидается от него. Если ему передано get
в качестве последнего аргумента, то его просят предоставить учетные данные для данного хоста и протокола, или можно сказать, что определенные учетные данные были успешными с store
или были отклонены с erase
. Во всех случаях он может читать информацию из stdin, чтобы узнать, на каком хосте git пытается пройти аутентификацию, в многострочном формате key=value
.
Таким образом, с помощью помощника по учетным данным вы получаете запрос на комбинацию имени пользователя и пароля вместе в качестве одного шага, и вы также получаете больше информации о процессе; обработка операций store
и erase
позволяет более эффективно кэшировать учетные данные.
Сначала заполняем Git, спрашивая каждого сконфигурированного помощника по учетным данным, в порядке конфигурации (см. Раздел FILES
, чтобы понять, как 4 расположения файла конфигурации обрабатываются по порядку). Вы можете добавить новую одноразовую вспомогательную конфигурацию в командной строке git
с помощью переключателя командной строки -c credential.helper=...
, который добавляется в конец. Если ни один из помощников по учетным данным не смог заполнить отсутствующее имя пользователя или пароль, пользователю будет предложено указать GIT_ASKPASS
или другие параметры запроса .
.
Для соединений SSH git создает новый ssh
дочерний процесс. Затем SSH будет обрабатывать аутентификацию и может запрашивать у пользователя учетные данные или ssh-ключи, запрашивать у пользователя кодовую фразу. Это снова будет сделано через /dev/tty
, и SSH более упрям в этом. В то время как вы можете установить переменную окружения SSH_ASKPASS
в двоичный файл, который будет использоваться для запроса, SSH будет использовать это , только если нет сеанса TTY и DISPLAY
также установлено .
SSH_ASKPASS
должен быть исполняемым файлом (поэтому не следует передавать аргументы), и вы не будете уведомлены об успехе или сбое запрашиваемых учетных данных.
Я также обязательно скопировал бы текущие переменные среды в дочерние процессы, потому что, если пользователь настроил агент ключей SSH для кэширования ключей ssh, вы бы хотели, чтобы процессы SSH, которые git начал использовать их; ключевой агент обнаружен через переменные окружения.
Итак, чтобы создать соединение для помощника по учетным данным, которое также работает для 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 как временный файл или включить предварительно созданный, и вы добавите путь к сокету домена 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
) с параметром командной строки 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://someuser@example.com/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
будет добавлено к передаваемой информации.
Для 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
Обязательно установите start_new_session=True
при запуске процессов git, чтобы 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
будет терпеливо ждать, пока помощник по учетным данным вернет запрошенную информацию), и вы можете ставить запросы в очередь для пользователя, чтобы заполнить, и вы можете кэшировать учетные данные по мере необходимости (если все команды ожидают учетные данные для одного хоста).