__pycache__/__init__.cpython-39.opt-1.pyc000064400000002217151116347500014110 0ustar00a /h\@sddlmZmZmZmZmZmZmZmZddl m Z ddl m Z m Z mZmZmZmZmZmZddlmZmZmZmZmZddlmZmZgdZdZdS) )BEIBOOT_GADGETSCOMMAND_TEMPLATEAskpassHandlerInteractionAgentInteractionErrorInteractionHandlertemporary_askpasswrite_askpass_to_tmpdir)Session) AskpassPromptSshAskpassResponderSshFIDOPINPromptSshFIDOUserPresencePromptSshHostKeyPromptSshPassphrasePromptSshPasswordPromptSshPKCS11PINPrompt)SshAuthenticationErrorSshChangedHostKeyErrorSshErrorSshHostKeyErrorSshUnknownHostKeyError)FernyTransportSubprocessError)rr ZAuthenticationErrorrrZChangedHostKeyErrorrZ HostKeyErrorrrrr r rrrr rrrrrrrrrr 0N)Zinteraction_agentrrrrrrrr Zsessionr Z ssh_askpassr r r rrrrrZ ssh_errorsrrrrrZ transportrr__all__ __version__rrB/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/__init__.pys ( ( __pycache__/__init__.cpython-39.pyc000064400000002217151116347500013151 0ustar00a /h\@sddlmZmZmZmZmZmZmZmZddl m Z ddl m Z m Z mZmZmZmZmZmZddlmZmZmZmZmZddlmZmZgdZdZdS) )BEIBOOT_GADGETSCOMMAND_TEMPLATEAskpassHandlerInteractionAgentInteractionErrorInteractionHandlertemporary_askpasswrite_askpass_to_tmpdir)Session) AskpassPromptSshAskpassResponderSshFIDOPINPromptSshFIDOUserPresencePromptSshHostKeyPromptSshPassphrasePromptSshPasswordPromptSshPKCS11PINPrompt)SshAuthenticationErrorSshChangedHostKeyErrorSshErrorSshHostKeyErrorSshUnknownHostKeyError)FernyTransportSubprocessError)rr ZAuthenticationErrorrrZChangedHostKeyErrorrZ HostKeyErrorrrrr r rrrr rrrrrrrrrr 0N)Zinteraction_agentrrrrrrrr Zsessionr Z ssh_askpassr r r rrrrrZ ssh_errorsrrrrrZ transportrr__all__ __version__rrB/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/__init__.pys ( ( __pycache__/askpass.cpython-39.opt-1.pyc000064400000000361151116347500014014 0ustar00a /hL@sddlmZedkredS))main__main__N)Zinteraction_clientr__name__rrA/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/askpass.pys __pycache__/askpass.cpython-39.pyc000064400000000361151116347500013055 0ustar00a /hL@sddlmZedkredS))main__main__N)Zinteraction_clientr__name__rrA/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/askpass.pys __pycache__/interaction_agent.cpython-39.opt-1.pyc000064400000032013151116347500016043 0ustar00a /h= @sRddlZddlZddlZddlZddlZddlZddlZddlZddlZddl m Z m Z m Z m Z mZddlmZeeZedZdZdedd d ZGd d d eZz ejZWn,eyd ejeeed dddZYn0ejdddZGdddZGdddeZ GdddZ!e"e"dddZ#ej$e e e"ddfdddZ%dS)!N)AnyCallableClassVar GeneratorSequence)interaction_clientsferny([^ ]*) zferny{(command, args)!r} zW import sys def command(command, *args): sys.stderr.write(fz%) sys.stderr.flush() z9 def end(): command('ferny.end') )commandendc@s eZdZdS)InteractionErrorN)__name__ __module__ __qualname__rrK/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/interaction_agent.pyr 3sr z"tuple[bytes, list[int], int, None])sockbufsizemaxfdsflagsreturnc Cstd}||t||j\}}}}|D]B\}} } |tjkr.| tjkr.|| dt| t| |jq.|t |||fS)Ni) arrayZrecvmsgsocketZCMSG_LENitemsizeZ SOL_SOCKETZ SCM_RIGHTSZ frombyteslenlist) rrrrfdsmsgZancdataaddrZ cmsg_levelZ cmsg_typeZ cmsg_datarrrrecv_fds<s   &rrcCs*z tWSty$tYS0dSN)asyncioget_running_loopAttributeErrorZget_event_looprrrrr#Gs  r#c@s4eZdZUeeeed<eddeddddZdS)InteractionHandlercommandstuple[object, ...] list[int]Nr argsrstderrrcstdSr!)NotImplementedErrorselfr r*rr+rrr run_commandRszInteractionHandler.run_command)r r rrrstr__annotations__r/rrrrr%Os r%c@seZdZUdZeeeed<eeeddddZeeeeee ddd Z ed d ed d ddZ d d ed dddZ ed d ed d ddZ d S)AskpassHandler) ferny.askpassr&z str | None)messagesprompthintrcsdS)a+Prompt the user for an authentication or confirmation interaction. 'messages' is data that was sent to stderr before the interaction was requested. 'prompt' is the interaction prompt. The expected response type depends on hint: - "confirm": ask for permission, returning "yes" if accepted - example: authorizing agent operation - "none": show a request without need for a response - example: please touch your authentication token - otherwise: return a password or other form of text token - examples: enter password, unlock private key In any case, the function should properly handle cancellation. For the "none" case, this will be the normal way to dismiss the dialog. Nr)r.r4r5r6rrr do_askpassYszAskpassHandler.do_askpass)reasonhost algorithmkey fingerprintrcsdS)aPrompt the user for a decision regarding acceptance of a host key. The "reason" will be either "HOSTNAME" or "ADDRESS" (if `CheckHostIP` is enabled). The host, algorithm, and key parameters are the values in the form that they would appear one a single line in the known hosts file. The fingerprint is the key fingerprint in the format that ssh would normally present it to the user. In case the host key should be accepted, this function needs to return True. Returning False means that ssh implements its default logic. To interrupt the connection, raise an exception. Fr)r.r8r9r:r;r<rrr do_hostkeyoszAskpassHandler.do_hostkeyr'r(Nr)csdS)zHandle a custom command. The command name, its arguments, the passed fds, and the stderr leading up to the command invocation are all provided. See doc/interaction-protocol.md Nrr-rrrdo_custom_commandsz AskpassHandler.do_custom_command)r*rr+rc s&td|||z |\}}Wn<tttfyX}ztd|||WYd}~dSd}~00t|dd}t|ddp}zPt} z t } Wnt yt j } Yn0| || jt|dkr:|d} |dd} td || | ||| | IdH} td | | durt| |d td|d nt|d kr|\}}}}}}|d vrtd|||||||||||IdHrt||||d n td|td|d n td|W| |n | |0Wdn1s0YWdn1s0YdS)Nz_askpass_command(%s, %s, %s)z4Invalid arguments to askpass interaction: %s, %s: %srwrZSSH_ASKPASS_PROMPTzdo_askpass(%r, %r, %r)zdo_askpass answer %r)file)ZADDRESSZHOSTNAMEzdo_hostkey(%r, %r, %r, %r, %r)z$ignoring KnownHostsCommand reason %rz?Incorrect number of command-line arguments to ferny-askpass: %s)loggerdebug ValueError TypeErrorAssertionErrorerroropenpopr#r"Z current_taskr$Task add_readercancelrgetr7printr= remove_reader)r.r*rr+argvenvexcstatusstdoutlooptaskr5r6ZanswerZargv0r8r9r:r;r<rrr_askpass_commandsD(        zAskpassHandler._askpass_commandcsJtd|||||dkr0||||IdHn|||||IdHdS)Nzrun_command(%s, %s, %s, %s)r3)rDrErYr>r-rrrr/szAskpassHandler.run_command)r r rr&rrr0r1r7boolr=r>rYr/rrrrr2Vs  3r2c@seZdZUded<ejed<ded<eed<ejed<ejed<d ed <d Zd ed <dZ e ed<d dddZ dd dddZ e e dd dddZe dd dddZd dddZd-eed d!d d"d#d$Zedd%d&Zd dd'd(Zd dd)d*Zd dd+d,Zd S).InteractionAgentzdict[str, InteractionHandler] _handlers_loopzset[asyncio.Task]_tasks_buffer_ours_theirszasyncio.Future[str]_completion_futureNzNone | str | Exception_pending_resultF_endr cCstd||jdus|jr(tdnX|jr>tdnBt|jtrhtd|j|j|jntd|j |jdS)Nz_consider_completion(%r)z but not ready yetz but already completez- submitting stderr (%r) to completion_futurez0 submitting exception (%r) to completion_future) rDrErcr^rbZdone isinstancer0Z set_resultZ set_exceptionr.rrr_consider_completions      z%InteractionAgent._consider_completionzstr | Exception)resultrcCstd|||jdur||_|jdkrLtd|j|j|j|jD]}td||qRtd|j |j | dS)Nz_result(%r, %r)z remove_reader(%r)z cancel(%r)z closing sockets) rDrErcr`filenor]rQr^rNracloserg)r.rhrXrrr_results       zInteractionAgent._resultr()r+ command_blobrrc s<td|||z2t|\}}t|tr8t|ts@tdWn<t t t tfy~}zt d||WYd}~dSd}~00|dkrd_ jjdddSzj|Wn tyt d|YdS0t|j|||tjdd fd d }|jg|dd<dS) Nz_invoke_command(%r, %r, %r)zInvalid argument typesz&Received invalid ferny command: %s: %sz ferny.endTreplaceerrorsz$Received unhandled ferny command: %s)completed_taskrc srtqjztdWnZtj yXtdYn<t y}z$td| |WYd}~n d}~00 dS)Nz%r completed cleanlyz%r was cancelledz %r raised %r) osrkrKr^removerhrDrEr"ZCancelledError Exceptionrlrg)rqrThandlerr.rXZtask_fdsrr bottom_halfs  z5InteractionAgent._invoke_command..bottom_half)rDrEastZ literal_evaldecoderer0tuplerGUnicodeDecodeError SyntaxErrorrFrIrdrlr_r\KeyErrorrr]Z create_taskr/r"rLadd_done_callbackr^add)r.r+rmrr r*rTrwrrur_invoke_commands.     z InteractionAgent._invoke_command)datarrcCstd|||dkr.||jjdddS|j|t|j}t| |_t |dkr| |d|dg|dd}qT|r|jdd}td|_t | dd }| }Wdn1s0Y| |||dS) Nz_got_data(%r, %r)rnrorrr@rirb)rDrErlr_ryextend COMMAND_REsplit bytearrayrKrrrJread)r.rrchunksr+Zcommand_channelr rrr _got_data)s     &zInteractionAgent._got_datac Cszz t|jddtjd\}}}}WnVtyLYW|rHt|q4dStyx}z| |WYd}~nd}~00| ||W|rt|qn|rt|q0dS)N )r) rr`r MSG_DONTWAITBlockingIOErrorrrrkrKOSErrorrlr)r.rrZ_flagsZ_addrrTrrr _read_readyBs   zInteractionAgent._read_readyz asyncio.AbstractEventLoop | Nonez,Callable[[asyncio.Future[str]], None] | None)handlersrW done_callbackrcCs~|pt|_|j|_t|_i|_|D]}|jD]}||j|<q4q*|durZ|j|t t j t j \|_ |_t|_dSr!)r#r]Z create_futurerbsetr^r\r&r~rZ socketpairZAF_UNIXZ SOCK_STREAMrar`rr_)r.rrWrrvr rrr__init__Os    zInteractionAgent.__init__cCs |jSr!)rarjrfrrrrjdszInteractionAgent.filenocCsftd||jdkr@td|j|j|j|jn tdtd|j|jdS)Nz start(%r)riz add_reader(%r)z# ...but agent is already finished.z close(%r)) rDrEr`rjr]rMrrarkrfrrrstartgs  zInteractionAgent.startc Cstd|zz|jdkrtdttD|jdtj }tdt ||sXqf|j |q2Wdn1sz0YWn.t y}z||WYd}~nd}~00||j jdddS)Nzforce_completion(%r)riz- draining pending stderr data (non-blocking)rz got %d bytesrnro)rDrEr`rj contextlibsuppressrZrecvrrrr_rrrlry)r.rrTrrrforce_completionrs   0 z!InteractionAgent.force_completionc std|z>|t|jIdH}td||Wtd|ntd|0|js|tdt| dS)Nz_communicate(%r)z$_communicate(%r) stderr result is %rz,_communicate finished. Ensuring completion.z<_communicate never saw ferny.end. raising InteractionError.) rDrErr"Zshieldrbrrdr strip)r.r+rrr communicates     zInteractionAgent.communicate)NN)r r rr1r"AbstractEventLooprrrcrdrZrgrlbytesrrrrr%rintrjrrrrrrrr[s4      0  r[)tmpdirrc Csjtj|d}t|tjtjBtjBtjBtjBd}z"t |t t j Wt|n t|0|S)Nz ferny-askpassi)rrpathjoinrJO_CREATO_WRONLY O_CLOEXECO_EXCL O_NOFOLLOWwrite __loader__get_datar__file__rk)rZ askpass_pathfdrrrwrite_askpass_to_tmpdirs (r)kwargsrcks>tjfi|}t|VWdn1s00YdSr!)tempfileZTemporaryDirectoryr)rZ directoryrrrtemporary_askpasssr)r)&rrxr"rZloggingrrrerrtypingrrrrrrArZ getLoggerr rDcompilerZCOMMAND_TEMPLATEZBEIBOOT_GADGETSrtr rr$rrr#r%r2r[r0rcontextmanagerrrrrrsF       oT __pycache__/interaction_agent.cpython-39.pyc000064400000032770151116347500015116 0ustar00a /h= @sRddlZddlZddlZddlZddlZddlZddlZddlZddlZddl m Z m Z m Z m Z mZddlmZeeZedZdZdedd d ZGd d d eZz ejZWn,eyd ejeeed dddZYn0ejdddZGdddZGdddeZ GdddZ!e"e"dddZ#ej$e e e"ddfdddZ%dS)!N)AnyCallableClassVar GeneratorSequence)interaction_clientsferny([^ ]*) zferny{(command, args)!r} zW import sys def command(command, *args): sys.stderr.write(fz%) sys.stderr.flush() z9 def end(): command('ferny.end') )commandendc@s eZdZdS)InteractionErrorN)__name__ __module__ __qualname__rrK/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/interaction_agent.pyr 3sr z"tuple[bytes, list[int], int, None])sockbufsizemaxfdsflagsreturnc Cstd}||t||j\}}}}|D]B\}} } |tjkr.| tjkr.|| dt| t| |jq.|t |||fS)Ni) arrayZrecvmsgsocketZCMSG_LENitemsizeZ SOL_SOCKETZ SCM_RIGHTSZ frombyteslenlist) rrrrfdsmsgZancdataaddrZ cmsg_levelZ cmsg_typeZ cmsg_datarrrrecv_fds<s   &rrcCs*z tWSty$tYS0dSN)asyncioget_running_loopAttributeErrorZget_event_looprrrrr#Gs  r#c@s4eZdZUeeeed<eddeddddZdS)InteractionHandlercommandstuple[object, ...] list[int]Nr argsrstderrrcstdSr!)NotImplementedErrorselfr r*rr+rrr run_commandRszInteractionHandler.run_command)r r rrrstr__annotations__r/rrrrr%Os r%c@seZdZUdZeeeed<eeeddddZeeeeee ddd Z ed d ed d ddZ d d ed dddZ ed d ed d ddZ d S)AskpassHandler) ferny.askpassr&z str | None)messagesprompthintrcsdS)a+Prompt the user for an authentication or confirmation interaction. 'messages' is data that was sent to stderr before the interaction was requested. 'prompt' is the interaction prompt. The expected response type depends on hint: - "confirm": ask for permission, returning "yes" if accepted - example: authorizing agent operation - "none": show a request without need for a response - example: please touch your authentication token - otherwise: return a password or other form of text token - examples: enter password, unlock private key In any case, the function should properly handle cancellation. For the "none" case, this will be the normal way to dismiss the dialog. Nr)r.r4r5r6rrr do_askpassYszAskpassHandler.do_askpass)reasonhost algorithmkey fingerprintrcsdS)aPrompt the user for a decision regarding acceptance of a host key. The "reason" will be either "HOSTNAME" or "ADDRESS" (if `CheckHostIP` is enabled). The host, algorithm, and key parameters are the values in the form that they would appear one a single line in the known hosts file. The fingerprint is the key fingerprint in the format that ssh would normally present it to the user. In case the host key should be accepted, this function needs to return True. Returning False means that ssh implements its default logic. To interrupt the connection, raise an exception. Fr)r.r8r9r:r;r<rrr do_hostkeyoszAskpassHandler.do_hostkeyr'r(Nr)csdS)zHandle a custom command. The command name, its arguments, the passed fds, and the stderr leading up to the command invocation are all provided. See doc/interaction-protocol.md Nrr-rrrdo_custom_commandsz AskpassHandler.do_custom_command)r*rr+rc std|||zh|\}}t|ts(Jtdd|Ds>Jt|tsLJtdd|DsfJt|dksvJWn<tt t fy}zt d|||WYd}~dSd}~00t | dd}t | dd}z`t} z t} Wntytj} Yn0| dus"J| || jt|dkr|d } |d d } td || | ||| | IdH} td | | dur4t| |dtd|dnt|dkr(|\}}}}}}|dvrtd|||||||||||IdHrt||||dn td|td|dn t d|W| |n | |0Wdn1sd0YWdn1s0YdS)Nz_askpass_command(%s, %s, %s)css|]}t|tVqdSr! isinstancer0).0argrrr z2AskpassHandler._askpass_command..css&|]\}}t|tot|tVqdSr!r?)rAr;valrrrrCrDz4Invalid arguments to askpass interaction: %s, %s: %srwrZSSH_ASKPASS_PROMPTzdo_askpass(%r, %r, %r)zdo_askpass answer %r)file)ZADDRESSZHOSTNAMEzdo_hostkey(%r, %r, %r, %r, %r)z$ignoring KnownHostsCommand reason %rz?Incorrect number of command-line arguments to ferny-askpass: %s)loggerdebugr@ralldictitemsr ValueError TypeErrorAssertionErrorerroropenpopr#r"Z current_taskr$Task add_readercancelgetr7printr= remove_reader)r.r*rr+argvenvexcstatusstdoutlooptaskr5r6ZanswerZargv0r8r9r:r;r<rrr_askpass_commandsN(       zAskpassHandler._askpass_commandcsJtd|||||dkr0||||IdHn|||||IdHdS)Nzrun_command(%s, %s, %s, %s)r3)rKrLrcr>r-rrrr/szAskpassHandler.run_command)r r rr&rrr0r1r7boolr=r>rcr/rrrrr2Vs  3r2c@seZdZUded<ejed<ded<eed<ejed<ejed<d ed <d Zd ed <dZ e ed<d dddZ dd dddZ e e dd dddZe dd dddZd dddZd-eed d!d d"d#d$Zedd%d&Zd dd'd(Zd dd)d*Zd dd+d,Zd S).InteractionAgentzdict[str, InteractionHandler] _handlers_loopzset[asyncio.Task]_tasks_buffer_ours_theirszasyncio.Future[str]_completion_futureNzNone | str | Exception_pending_resultF_endr cCstd||jdus|jr(tdnX|jr>tdnBt|jtrhtd|j|j|jntd|j |jdS)Nz_consider_completion(%r)z but not ready yetz but already completez- submitting stderr (%r) to completion_futurez0 submitting exception (%r) to completion_future) rKrLrmrhrlZdoner@r0Z set_resultZ set_exceptionr.rrr_consider_completions      z%InteractionAgent._consider_completionzstr | Exception)resultrcCstd|||jdur||_|jdkrLtd|j|j|j|jD]}td||qRtd|j |j | dS)Nz_result(%r, %r)z remove_reader(%r)z cancel(%r)z closing sockets) rKrLrmrjfilenorgr[rhrXrkcloserp)r.rqrbrrr_results       zInteractionAgent._resultr()r+ command_blobrrc s<td|||z2t|\}}t|tr8t|ts@tdWn<t t t tfy~}zt d||WYd}~dSd}~00|dkrd_ jjdddSzj|Wn tyt d|YdS0t|j|||tjdd fd d }|jg|dd<dS) Nz_invoke_command(%r, %r, %r)zInvalid argument typesz&Received invalid ferny command: %s: %sz ferny.endTreplaceerrorsz$Received unhandled ferny command: %s)completed_taskrc s|us Jr tq jztdWnZtj ydtdYn<t y}z$td| |WYd}~n d}~00 dS)Nz%r completed cleanlyz%r was cancelledz %r raised %r) osrtrUrhremoverqrKrLr"ZCancelledError Exceptionrurp)rzr^handlerr.rbZtask_fdsrr bottom_halfs   z5InteractionAgent._invoke_command..bottom_half)rKrLastZ literal_evaldecoder@r0tuplerQUnicodeDecodeError SyntaxErrorrPrSrnrurirfKeyErrorrrgZ create_taskr/r"rVadd_done_callbackrhadd)r.r+rvrr r*r^rrr~r_invoke_commands.     z InteractionAgent._invoke_command)datarrcCstd|||dkr.||jjdddS|j|t|j}t| |_t |dkr| |d|dg|dd}qT|r|j dsJ|j|jdd }td|_t | dd }|}Wdn1s0Y| |||dS) Nz_got_data(%r, %r)rDrwrxrrrFrrrb)rKrLrurirextend COMMAND_REsplit bytearrayrUrrendswithrTread)r.rrchunksr+Zcommand_channelr rrr _got_data)s"    &zInteractionAgent._got_datac Cszz t|jddtjd\}}}}WnVtyLYW|rHt|q4dStyx}z| |WYd}~nd}~00| ||W|rt|qn|rt|q0dS)N )r) rrjr MSG_DONTWAITBlockingIOErrorr{rtrUOSErrorrur)r.rrZ_flagsZ_addrr^rrr _read_readyBs   zInteractionAgent._read_readyz asyncio.AbstractEventLoop | Nonez,Callable[[asyncio.Future[str]], None] | None)handlersra done_callbackrcCs~|pt|_|j|_t|_i|_|D]}|jD]}||j|<q4q*|durZ|j|t t j t j \|_ |_t|_dSr!)r#rgZ create_futurerlsetrhrfr&rrZ socketpairZAF_UNIXZ SOCK_STREAMrkrjrri)r.rrarrr rrr__init__Os    zInteractionAgent.__init__cCs |jSr!)rkrsrorrrrsdszInteractionAgent.filenocCsftd||jdkr@td|j|j|j|jn tdtd|j|jdS)Nz start(%r)rrz add_reader(%r)z# ...but agent is already finished.z close(%r)) rKrLrjrsrgrWrrkrtrorrrstartgs  zInteractionAgent.startc Cstd|zz|jdkrtdttD|jdtj }tdt ||sXqf|j |q2Wdn1sz0YWn.t y}z||WYd}~nd}~00||j jdddS)Nzforce_completion(%r)rrz- draining pending stderr data (non-blocking)rz got %d bytesrwrx)rKrLrjrs contextlibsuppressrZrecvrrrrirrrur)r.rr^rrrforce_completionrs   0 z!InteractionAgent.force_completionc std|z>|t|jIdH}td||Wtd|ntd|0|js|tdt| dS)Nz_communicate(%r)z$_communicate(%r) stderr result is %rz,_communicate finished. Ensuring completion.z<_communicate never saw ferny.end. raising InteractionError.) rKrLrr"Zshieldrlrrnr strip)r.r+rrr communicates     zInteractionAgent.communicate)NN)r r rr1r"AbstractEventLooprrrmrnrdrprubytesrrrrr%rintrsrrrrrrrres4      0  re)tmpdirrc Csjtj|d}t|tjtjBtjBtjBtjBd}z"t |t t j Wt|n t|0|S)Nz ferny-askpassi)r{pathjoinrTO_CREATO_WRONLY O_CLOEXECO_EXCL O_NOFOLLOWwrite __loader__get_datar__file__rt)rZ askpass_pathfdrrrwrite_askpass_to_tmpdirs (r)kwargsrcks>tjfi|}t|VWdn1s00YdSr!)tempfileZTemporaryDirectoryr)rZ directoryrrrtemporary_askpasssr)r)&rrr"rZloggingr{rerrtypingrrrrrrHrZ getLoggerr rKcompilerZCOMMAND_TEMPLATEZBEIBOOT_GADGETSr}r rr$rrr#r%r2rer0rcontextmanagerrrrrrsF       oT __pycache__/interaction_client.cpython-39.opt-1.pyc000064400000003264151116347500016231 0ustar00a /hT@sddlZddlZddlZddlZddlZddlmZddeee eeddddZ eedd ed d d Z dd ddZ e dkre dS)N)Sequencefds) stderr_fdcommandargsrreturnc GsddttdD\}}||tt|tjtjB}td|g|R}| dgtj tj |fgWdn1s0YWdn1s0Y| t ||fWdn1s0YdS)NcSsg|]}tj|qSr)ioopen).0endrrL/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/interaction_client.py zcommand..rwi)zipospipesocketZfromfdZAF_UNIXZ SOCK_STREAMarrayfilenoZsendmsgZ SOL_SOCKETZ SCM_RIGHTSwriterepr)rrrrZcmd_readZ cmd_writeZsockZfd_arrayrrrr sVrz list[str]zdict[str, str])r stdout_fdrenvr cCst\}}|*t|d||||fdWdn1s@0Y|"t|dp^dWdS1sv0YdS)Nz ferny.askpassr1)rZ socketpairrrintZrecv)rrrrZoursZtheirsrrraskpasss  8r!)r cCs<ttjdkrtddgnttddtjttjdS)Nz ferny.end) lensysargvrexitr!dictrenvironrrrrmain!sr*__main__)rr rrr%typingrr strobjectrr!r*__name__rrrrs   __pycache__/interaction_client.cpython-39.pyc000064400000003264151116347500015272 0ustar00a /hT@sddlZddlZddlZddlZddlZddlmZddeee eeddddZ eedd ed d d Z dd ddZ e dkre dS)N)Sequencefds) stderr_fdcommandargsrreturnc GsddttdD\}}||tt|tjtjB}td|g|R}| dgtj tj |fgWdn1s0YWdn1s0Y| t ||fWdn1s0YdS)NcSsg|]}tj|qSr)ioopen).0endrrL/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/interaction_client.py zcommand..rwi)zipospipesocketZfromfdZAF_UNIXZ SOCK_STREAMarrayfilenoZsendmsgZ SOL_SOCKETZ SCM_RIGHTSwriterepr)rrrrZcmd_readZ cmd_writeZsockZfd_arrayrrrr sVrz list[str]zdict[str, str])r stdout_fdrenvr cCst\}}|*t|d||||fdWdn1s@0Y|"t|dp^dWdS1sv0YdS)Nz ferny.askpassr1)rZ socketpairrrintZrecv)rrrrZoursZtheirsrrraskpasss  8r!)r cCs<ttjdkrtddgnttddtjttjdS)Nz ferny.end) lensysargvrexitr!dictrenvironrrrrmain!sr*__main__)rr rrr%typingrr strobjectrr!r*__name__rrrrs   __pycache__/session.cpython-39.opt-1.pyc000064400000012516151116347500014037 0ustar00a /h@sddlZddlZddlZddlZddlZddlZddlZddlZddlZddl m Z m Z ddl m Z ddlmZmZmZmZedjZeeZdZedeeeddd ZGd d d ZGd d d eeZdS)N)MappingSequence) ssh_errors)InteractionAgentInteractionErrorInteractionHandlerwrite_askpass_to_tmpdirx)featureteststrreturncCsFz*tjdd|d|ddgtjdWdStjy@YdS0dS) Nssh-o z-GZ nonexisting)stderrTF) subprocessZ check_outputDEVNULLZCalledProcessError)r r rA/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/session.py has_feature%s $rc@sDeZdZeeeedddZeeefeeefdddZdS)SubprocessContextargsr cCs|S)aReturn the args required to launch a process in the given context. For example, this might return a vector with ["sudo"] or ["flatpak-spawn", "--host"] prepended. It is also possible that more substantial changes may be performed. This function is not permitted to modify its argument, although it may (optionally) return it unmodified, if no changes are required. rselfrrrrwrap_subprocess_args/sz&SubprocessContext.wrap_subprocess_args)envr cCs|S)ajReturn the envp required to launch a process in the given context. For example, this might set the "SUDO_ASKPASS" environment variable, if needed. As with wrap_subprocess_args(), this function is not permitted to modify its argument, although it may (optionally) return it unmodified if no changes are required. r)rrrrrwrap_subprocess_env?s z%SubprocessContext.wrap_subprocess_envN)__name__ __module__ __qualname__rstrrrrrrrrr.src @seZdZUdZded<dZded<dZded<deedddd dd d dd d dZ edddZ ddddZ ddddZ ddddZ eeeedddZdS)SessionNz"tempfile.TemporaryDirectory | None _controldirz str | None _controlsockz!asyncio.subprocess.Process | None_processFzMapping[str, str] | Nonez int | NonezInteractionHandler | None) destinationhandle_host_key configfile identity_file login_nameoptionspkcs11portinteraction_responderr c  sXtjtjddd} tj| ddtj| d|_|jj d|_ t |jj } t tj} | | d<d | d <d | d <d dd|j dddd| g} |dur| d||dur| d||dur|D]}| d|d||q|dur| d||dur| d||dur8| d||rbtdrb| dd| dddgt| durt| gng}tjdg| |R| dtjjtjj|ddd IdH}z|IdH||_Wnty}z*|IdHtt|dWYd}~nNd}~0tyRz |Wnty<Yn0|IdHYn0dS)!NZXDG_RUNTIME_DIRz/runZfernyT)exist_ok)dirz/socketZ SSH_ASKPASSforceZSSH_ASKPASS_REQUIRE-ZDISPLAYz-Mz-N-SrzPermitLocalCommand=yesz LocalCommand=z-Fz-irz-Iz-pz-lZKnownHostsCommandzKnownHostsCommand=z %I %H %t %K %fzStrictHostKeyChecking=yesz /usr/bin/sshcSs tttjSN)prctlPR_SET_PDEATHSIGsignalSIGKILLrrrrz!Session.connect..)rZstart_new_sessionstdinstdoutrZ preexec_fn)ospathjoinenvirongetmakedirstempfileZTemporaryDirectoryr$namer%r dictappendrextendrasyncioZcreate_subprocess_execrrZ communicater&rwaitrZget_exception_for_ssh_stderrr" BaseExceptionkillProcessLookupError)rr'r(r)r*r+r,r-r.r/ZrundirZ askpass_pathrrkeyZagentZprocessexcrrrconnectTsj         $ zSession.connect)r cCs |jduSr5)r&rrrr is_connectedszSession.is_connectedcs|jIdHdSr5)r&rJrQrrrrJsz Session.waitcCs|jdSr5)r&Z terminaterQrrrexitsz Session.exitcs||IdHdSr5)rSrJrQrrr disconnectszSession.disconnectrcCsdd|jdgttj|RS)Nrr4)r%mapshlexquoterrrrrszSession.wrap_subprocess_args)FNNNNNNN)rr r!r$__annotations__r%r&r"boolrPrRrJrSrTrrrrrrr#Ls6     Zr#)r )rIZctypes functoolsZloggingr>rWr8rrDtypingrrrUrZinteraction_agentrrrr ZCDLLr6Z getLoggerrloggerr7 lru_cacher"rZrrr#rrrrs$   __pycache__/session.cpython-39.pyc000064400000012634151116347500013101 0ustar00a /h@sddlZddlZddlZddlZddlZddlZddlZddlZddlZddl m Z m Z ddl m Z ddlmZmZmZmZedjZeeZdZedeeeddd ZGd d d ZGd d d eeZdS)N)MappingSequence) ssh_errors)InteractionAgentInteractionErrorInteractionHandlerwrite_askpass_to_tmpdirx)featureteststrreturncCsFz*tjdd|d|ddgtjdWdStjy@YdS0dS) Nssh-o z-GZ nonexisting)stderrTF) subprocessZ check_outputDEVNULLZCalledProcessError)r r rA/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/session.py has_feature%s $rc@sDeZdZeeeedddZeeefeeefdddZdS)SubprocessContextargsr cCs|S)aReturn the args required to launch a process in the given context. For example, this might return a vector with ["sudo"] or ["flatpak-spawn", "--host"] prepended. It is also possible that more substantial changes may be performed. This function is not permitted to modify its argument, although it may (optionally) return it unmodified, if no changes are required. rselfrrrrwrap_subprocess_args/sz&SubprocessContext.wrap_subprocess_args)envr cCs|S)ajReturn the envp required to launch a process in the given context. For example, this might set the "SUDO_ASKPASS" environment variable, if needed. As with wrap_subprocess_args(), this function is not permitted to modify its argument, although it may (optionally) return it unmodified if no changes are required. r)rrrrrwrap_subprocess_env?s z%SubprocessContext.wrap_subprocess_envN)__name__ __module__ __qualname__rstrrrrrrrrr.src @seZdZUdZded<dZded<dZded<deedddd dd d dd d dZ edddZ ddddZ ddddZ ddddZ eeeedddZdS)SessionNz"tempfile.TemporaryDirectory | None _controldirz str | None _controlsockz!asyncio.subprocess.Process | None_processFzMapping[str, str] | Nonez int | NonezInteractionHandler | None) destinationhandle_host_key configfile identity_file login_nameoptionspkcs11portinteraction_responderr c  sltjtjddd} tj| ddtj| d|_|jj d|_ t |jj } t tj} | | d<d | d <d | d <d dd|j dddd| g} |dur| d||dur| d||dur|D]}| d|d||q|dur| d||dur| d||dur8| d||rbtdrb| dd| dddgt| durt| gng}tjdg| |R| dtjjtjj|ddd IdH}z,|IdHtj|j sJ||_Wnty"}z*|IdHtt|dWYd}~nNd}~0tyfz |WntyPYn0|IdHYn0dS)!NZXDG_RUNTIME_DIRz/runZfernyT)exist_ok)dirz/socketZ SSH_ASKPASSforceZSSH_ASKPASS_REQUIRE-ZDISPLAYz-Mz-N-SrzPermitLocalCommand=yesz LocalCommand=z-Fz-irz-Iz-pz-lZKnownHostsCommandzKnownHostsCommand=z %I %H %t %K %fzStrictHostKeyChecking=yesz /usr/bin/sshcSs tttjSN)prctlPR_SET_PDEATHSIGsignalSIGKILLrrrrz!Session.connect..)rZstart_new_sessionstdinstdoutrZ preexec_fn) ospathjoinenvirongetmakedirstempfileZTemporaryDirectoryr$namer%r dictappendrextendrasyncioZcreate_subprocess_execrrZ communicateexistsr&rwaitrZget_exception_for_ssh_stderrr" BaseExceptionkillProcessLookupError)rr'r(r)r*r+r,r-r.r/ZrundirZ askpass_pathrrkeyZagentZprocessexcrrrconnectTsl         $ zSession.connect)r cCs |jduSr5)r&rrrr is_connectedszSession.is_connectedcs"|jdusJ|jIdHdSr5)r&rKrRrrrrKsz Session.waitcCs|jdusJ|jdSr5)r&Z terminaterRrrrexitsz Session.exitcs||IdHdSr5)rTrKrRrrr disconnectszSession.disconnectrcCs*|jdusJdd|jdgttj|RS)Nrr4)r%mapshlexquoterrrrrszSession.wrap_subprocess_args)FNNNNNNN)rr r!r$__annotations__r%r&r"boolrQrSrKrTrUrrrrrrr#Ls6     Zr#)r )rIZctypes functoolsZloggingr>rXr8rrDtypingrrrVrZinteraction_agentrrrr ZCDLLr6Z getLoggerrloggerr7 lru_cacher"r[rrr#rrrrs$   __pycache__/ssh_askpass.cpython-39.opt-1.pyc000064400000017203151116347500014674 0ustar00a /h@sddlZddlZddlmZmZmZddlmZee Z GdddZ Gddde Z d d d d d ddZ Gddde ZGddde ZGddde ZGddde ZGddde ZGddde ZeedddZeee dd d!ZGd"d#d#eZdS)$N)ClassVarMatchSequence)AskpassHandlerc@s|eZdZUdZeed<eed<eed<eeeddddZedd d d Zdd d dZdddddZ dddddZ dS) AskpassPromptaAn askpass prompt resulting from a call to ferny-askpass. stderr: the contents of stderr from before ferny-askpass was called. Likely related to previous failed operations. messages: all but the last line of the prompt as handed to ferny-askpass. Usually contains context about the question. prompt: the last line handed to ferny-askpass. The prompt itself. stderrmessagespromptN)r r rreturncCs||_||_||_dSN)rr r )selfr r rrE/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/ssh_askpass.py__init__szAskpassPrompt.__init__)responser cCsdSr r)r rrrrreplyszAskpassPrompt.reply)r cCsdSr r)r rrrcloseszAskpassPrompt.closeSshAskpassResponder responderr cs>z.||IdH}|dur$||W|n |0dSr )dispatchrr)r rrrrr handle_via"s  zAskpassPrompt.handle_via str | Nonecs||IdHSr  do_promptr rrrrr*szAskpassPrompt.dispatch) __name__ __module__ __qualname____doc__str__annotations__rrrrrrrrrr s rcsXeZdZUdZded<eeed<dZeeeed<eeee ddfdd Z Z S) SSHAskpassPromptNzClassVar[Sequence[str] | None]answers_patternr_extra_patterns)r r rmatchr cs\t||||j||jD]0}tt||tj }|dur&|j|q&dSr ) superr__dict__update groupdictr&research with_helpersM)r r r rr'patternZ extra_match __class__rrr:s  zSSHAskpassPrompt.__init__) rrrr$r"rr!r&rrr __classcell__rrr1rr#.s   r#z(?P\b[-\w]+\b)z(?P.+)z)(?PSHA256:[0-9A-Za-z+/]{43})z(?P[^ @']+)z(?P.+)z(?P[^ @']+))z %{algorithm}z %{filename}z%{fingerprint}z %{hostname}z %{pkcs11_id}z %{username}c@s:eZdZUdZdZded<dZded<ddddd ZdS) SshPasswordPromptz$%{username}@%{hostname}'s password: Nrusernamehostnamerrcs||IdHSr )do_password_promptrrrrrTszSshPasswordPrompt.dispatch)rrrr%r5r"r6rrrrrr4Os   r4c@s*eZdZUdZeed<dddddZdS) SshPassphrasePromptz(Enter passphrase for key '%{filename}': filenamerrrcs||IdHSr )do_passphrase_promptrrrrr\szSshPassphrasePrompt.dispatchNrrrr%r!r"rrrrrr8Xs r8c@s2eZdZUdZeed<eed<dddddZd S) SshFIDOPINPromptz,Enter PIN for %{algorithm} key %{filename}: algorithmr9rz str | Nonercs||IdHSr )do_fido_pin_promptrrrrreszSshFIDOPINPrompt.dispatchNr;rrrrr<`s r<c@s6eZdZUdZdZeed<eed<ddddd Zd S) SshFIDOUserPresencePromptz9Confirm user presence for key %{algorithm} %{fingerprint}rr= fingerprintrrrcs||IdHSr )do_fido_user_presence_promptrrrrrosz"SshFIDOUserPresencePrompt.dispatchN)rrrr%r$r!r"rrrrrr?is r?c@s*eZdZUdZeed<dddddZdS) SshPKCS11PINPromptzEnter PIN for '%{pkcs11_id}': Z pkcs11_idrrrcs||IdHSr )do_pkcs11_pin_promptrrrrrwszSshPKCS11PINPrompt.dispatchNr;rrrrrBss rBc@s>eZdZUdZgdZdZeed<eed<dddd d Zd S) SshHostKeyPromptzMAre you sure you want to continue connecting \(yes/no(/\[fingerprint\])?\)\? )z%{fingerprint}[.]$z ^%{algorithm} key fingerprint iszE^The fingerprint for the %{algorithm} key sent by the remote host is$)Zyesnor=r@rrrcs||IdHSr )do_host_key_promptrrrrrszSshHostKeyPrompt.dispatchN) rrrr%r&r$r!r"rrrrrrD{s rD)r0r cCs"tD]\}}|||}q|Sr )HELPERSitemsreplace)r0namehelperrrrr.sr.)stringrr c Csttttttg}|ddd}|dkrH||dd}|d|d}n|}d}|D]4}t|j}t ||}|durT|||||SqTt |||S)N rr) r<r?rDrBr8r4rfindr.r%r, fullmatchr) rLrclassesZsecond_last_newline last_lineextrasclsr0r'rrrcategorize_ssh_prompts&   rVc@seZdZeeeddddZeddddZedddd Ze ddd d Z e ddd d Z e ddddZeddddZeddddZdS)rr)rr hintr cst|||IdHSr )rVr)r rr rWrrr do_askpassszSshAskpassResponder.do_askpass)r r csdSr rr r rrrrszSshAskpassResponder.do_promptcs||IdHSr rrYrrrr>sz&SshAskpassResponder.do_fido_pin_promptcs||IdHSr rrYrrrrAsz0SshAskpassResponder.do_fido_user_presence_promptcs||IdHSr rrYrrrrFsz&SshAskpassResponder.do_host_key_promptcs||IdHSr rrYrrrrCsz(SshAskpassResponder.do_pkcs11_pin_promptcs||IdHSr rrYrrrr:sz(SshAskpassResponder.do_passphrase_promptcs||IdHSr rrYrrrr7sz&SshAskpassResponder.do_password_promptN)rrrr!rXrrr<r>r?rArDrFrBrCr8r:r4r7rrrrrsr)Zloggingr,typingrrrZinteraction_agentrZ getLoggerrloggerrr#rGr4r8r<r?rBrDr!r.rVrrrrrs,  $    __pycache__/ssh_askpass.cpython-39.pyc000064400000017223151116347500013737 0ustar00a /h@sddlZddlZddlmZmZmZddlmZee Z GdddZ Gddde Z d d d d d ddZ Gddde ZGddde ZGddde ZGddde ZGddde ZGddde ZeedddZeee dd d!ZGd"d#d#eZdS)$N)ClassVarMatchSequence)AskpassHandlerc@s|eZdZUdZeed<eed<eed<eeeddddZedd d d Zdd d dZdddddZ dddddZ dS) AskpassPromptaAn askpass prompt resulting from a call to ferny-askpass. stderr: the contents of stderr from before ferny-askpass was called. Likely related to previous failed operations. messages: all but the last line of the prompt as handed to ferny-askpass. Usually contains context about the question. prompt: the last line handed to ferny-askpass. The prompt itself. stderrmessagespromptN)r r rreturncCs||_||_||_dSN)rr r )selfr r rrE/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/ssh_askpass.py__init__szAskpassPrompt.__init__)responser cCsdSr r)r rrrrreplyszAskpassPrompt.reply)r cCsdSr r)r rrrcloseszAskpassPrompt.closeSshAskpassResponder responderr cs>z.||IdH}|dur$||W|n |0dSr )dispatchrr)r rrrrr handle_via"s  zAskpassPrompt.handle_via str | Nonecs||IdHSr  do_promptr rrrrr*szAskpassPrompt.dispatch) __name__ __module__ __qualname____doc__str__annotations__rrrrrrrrrr s rcsXeZdZUdZded<eeed<dZeeeed<eeee ddfdd Z Z S) SSHAskpassPromptNzClassVar[Sequence[str] | None]answers_patternr_extra_patterns)r r rmatchr cs\t||||j||jD]0}tt||tj }|dur&|j|q&dSr ) superr__dict__update groupdictr&research with_helpersM)r r r rr'patternZ extra_match __class__rrr:s  zSSHAskpassPrompt.__init__) rrrr$r"rr!r&rrr __classcell__rrr1rr#.s   r#z(?P\b[-\w]+\b)z(?P.+)z)(?PSHA256:[0-9A-Za-z+/]{43})z(?P[^ @']+)z(?P.+)z(?P[^ @']+))z %{algorithm}z %{filename}z%{fingerprint}z %{hostname}z %{pkcs11_id}z %{username}c@s:eZdZUdZdZded<dZded<ddddd ZdS) SshPasswordPromptz$%{username}@%{hostname}'s password: Nrusernamehostnamerrcs||IdHSr )do_password_promptrrrrrTszSshPasswordPrompt.dispatch)rrrr%r5r"r6rrrrrr4Os   r4c@s*eZdZUdZeed<dddddZdS) SshPassphrasePromptz(Enter passphrase for key '%{filename}': filenamerrrcs||IdHSr )do_passphrase_promptrrrrr\szSshPassphrasePrompt.dispatchNrrrr%r!r"rrrrrr8Xs r8c@s2eZdZUdZeed<eed<dddddZd S) SshFIDOPINPromptz,Enter PIN for %{algorithm} key %{filename}: algorithmr9rz str | Nonercs||IdHSr )do_fido_pin_promptrrrrreszSshFIDOPINPrompt.dispatchNr;rrrrr<`s r<c@s6eZdZUdZdZeed<eed<ddddd Zd S) SshFIDOUserPresencePromptz9Confirm user presence for key %{algorithm} %{fingerprint}rr= fingerprintrrrcs||IdHSr )do_fido_user_presence_promptrrrrrosz"SshFIDOUserPresencePrompt.dispatchN)rrrr%r$r!r"rrrrrr?is r?c@s*eZdZUdZeed<dddddZdS) SshPKCS11PINPromptzEnter PIN for '%{pkcs11_id}': Z pkcs11_idrrrcs||IdHSr )do_pkcs11_pin_promptrrrrrwszSshPKCS11PINPrompt.dispatchNr;rrrrrBss rBc@s>eZdZUdZgdZdZeed<eed<dddd d Zd S) SshHostKeyPromptzMAre you sure you want to continue connecting \(yes/no(/\[fingerprint\])?\)\? )z%{fingerprint}[.]$z ^%{algorithm} key fingerprint iszE^The fingerprint for the %{algorithm} key sent by the remote host is$)Zyesnor=r@rrrcs||IdHSr )do_host_key_promptrrrrrszSshHostKeyPrompt.dispatchN) rrrr%r&r$r!r"rrrrrrD{s rD)r0r cCs.tD]\}}|||}qd|vs*J|S)Nz%{)HELPERSitemsreplace)r0namehelperrrrr.s r.)stringrr c Csttttttg}|ddd}|dkrH||dd}|d|d}n|}d}|D]4}t|j}t ||}|durT|||||SqTt |||S)N rr) r<r?rDrBr8r4rfindr.r%r, fullmatchr) rLrclassesZsecond_last_newline last_lineextrasclsr0r'rrrcategorize_ssh_prompts&   rVc@seZdZeeeddddZeddddZedddd Ze ddd d Z e ddd d Z e ddddZeddddZeddddZdS)rr)rr hintr cst|||IdHSr )rVr)r rr rWrrr do_askpassszSshAskpassResponder.do_askpass)r r csdSr rr r rrrrszSshAskpassResponder.do_promptcs||IdHSr rrYrrrr>sz&SshAskpassResponder.do_fido_pin_promptcs||IdHSr rrYrrrrAsz0SshAskpassResponder.do_fido_user_presence_promptcs||IdHSr rrYrrrrFsz&SshAskpassResponder.do_host_key_promptcs||IdHSr rrYrrrrCsz(SshAskpassResponder.do_pkcs11_pin_promptcs||IdHSr rrYrrrr:sz(SshAskpassResponder.do_passphrase_promptcs||IdHSr rrYrrrr7sz&SshAskpassResponder.do_password_promptN)rrrr!rXrrr<r>r?rArDrFrBrCr8r:r4r7rrrrrsr)Zloggingr,typingrrrZinteraction_agentrZ getLoggerrloggerrr#rGr4r8r<r?rBrDr!r.rVrrrrrs,  $    __pycache__/ssh_errors.cpython-39.opt-1.pyc000064400000007646151116347500014555 0ustar00a /h@svddlZddlZddlZddlZddlZddlmZmZmZm Z Gddde Z Gddde Z Gddde Z Gd d d e ZGd d d eZGd ddeZddddZeeZeddeejfeejfeejfeejfeejfeejfeejfeejfe ej!fe"ej#fe$ej%fe&ej'fe(ej)fe*ej+fe,ej-fe.ej/fe.ej0fe1ej2fe3ej4ffDZ5e6e dddZ7dS)N)ClassVarIterableMatchPatterncs4eZdZUeeed<deddfdd ZZS)SshErrorPATTERNz Match | NoneNmatchstderrreturncs(t|dur|dn|||_dS)Nr)super__init__groupr selfr r  __class__D/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/ssh_errors.pyr szSshError.__init__) __name__ __module__ __qualname__rr__annotations__strr __classcell__rrrrrs  rcs4eZdZedejZeeddfdd Z Z S)SshAuthenticationErrorz+^([^:]+): Permission denied \(([^()]+)\)\.$Nrcs<t|||d|_|dd|_|d|_dS)N,r)r r rZ destinationsplitmethodsmessagerrrrr %s zSshAuthenticationError.__init__) rrrrecompileMrrrr rrrrrr"src@seZdZedejZdS)SshInvalidHostnameErrorz%^hostname contains invalid charactersNrrrr"r#Irrrrrr%,sr%c@seZdZedejZdS)SshHostKeyErrorz^Host key verification failed.$N)rrrr"r#r$rrrrrr(1sr(c@s eZdZedejejBZdS)SshUnknownHostKeyErrorz8^No .* host key is known.*Host key verification failed.$N)rrrr"r#Sr$rrrrrr)6sr)c@seZdZedejZdS)SshChangedHostKeyErrorz/warning.*remote host identification has changedNr&rrrrr+:sr+zIterable[tuple[str, int]])r ccsPtd}tj|j_ttD].}|drtt|}|| d|fVqdS)NZEAI_zutf-8) ctypesZCDLLZc_char_pZ gai_strerrorZrestypedirsocket startswithgetattrdecode)Zlibckeyerrnumrrrmake_gaierror_map@s      r4ccs|]\}}||fVqdS)Nr).0clsr3rrr Sr7)r r c Cs|dd}tttttfD]&}|j|}|dur|||Sq|d\}}}|r|r| }|t vr~t |}t ||St jD],}t||krt|t}|||Sqtd|S)Nz  :)replacer%rr+r)r(rsearch rpartitionstrip gaierror_mapr.Zgaierrorerrno errorcodeosstrerroroserror_subclass_mapgetOSErrorr) r Zssh_clsr beforecolonZafterZpotential_strerrorr3Zos_clsrrrget_exception_for_ssh_stderrjs$     rI)8r,r@rBr"r.typingrrrr Exceptionrrr%r(r)r+r4dictr?BlockingIOErrorZEAGAINZEALREADYZ EINPROGRESSZ EWOULDBLOCKBrokenPipeErrorZEPIPEZ ESHUTDOWNChildProcessErrorZECHILDConnectionAbortedErrorZ ECONNABORTEDConnectionRefusedErrorZ ECONNREFUSEDConnectionResetErrorZ ECONNRESETFileExistsErrorZEEXISTFileNotFoundErrorENOENTIsADirectoryErrorZEISDIRNotADirectoryErrorENOTDIRInterruptedErrorZEINTRPermissionErrorZEACCESEPERMProcessLookupErrorZESRCH TimeoutErrorZ ETIMEDOUTrDrrIrrrrsF   __pycache__/ssh_errors.cpython-39.pyc000064400000007646151116347500013616 0ustar00a /h@svddlZddlZddlZddlZddlZddlmZmZmZm Z Gddde Z Gddde Z Gddde Z Gd d d e ZGd d d eZGd ddeZddddZeeZeddeejfeejfeejfeejfeejfeejfeejfeejfe ej!fe"ej#fe$ej%fe&ej'fe(ej)fe*ej+fe,ej-fe.ej/fe.ej0fe1ej2fe3ej4ffDZ5e6e dddZ7dS)N)ClassVarIterableMatchPatterncs4eZdZUeeed<deddfdd ZZS)SshErrorPATTERNz Match | NoneNmatchstderrreturncs(t|dur|dn|||_dS)Nr)super__init__groupr selfr r  __class__D/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/ssh_errors.pyr szSshError.__init__) __name__ __module__ __qualname__rr__annotations__strr __classcell__rrrrrs  rcs4eZdZedejZeeddfdd Z Z S)SshAuthenticationErrorz+^([^:]+): Permission denied \(([^()]+)\)\.$Nrcs<t|||d|_|dd|_|d|_dS)N,r)r r rZ destinationsplitmethodsmessagerrrrr %s zSshAuthenticationError.__init__) rrrrecompileMrrrr rrrrrr"src@seZdZedejZdS)SshInvalidHostnameErrorz%^hostname contains invalid charactersNrrrr"r#Irrrrrr%,sr%c@seZdZedejZdS)SshHostKeyErrorz^Host key verification failed.$N)rrrr"r#r$rrrrrr(1sr(c@s eZdZedejejBZdS)SshUnknownHostKeyErrorz8^No .* host key is known.*Host key verification failed.$N)rrrr"r#Sr$rrrrrr)6sr)c@seZdZedejZdS)SshChangedHostKeyErrorz/warning.*remote host identification has changedNr&rrrrr+:sr+zIterable[tuple[str, int]])r ccsPtd}tj|j_ttD].}|drtt|}|| d|fVqdS)NZEAI_zutf-8) ctypesZCDLLZc_char_pZ gai_strerrorZrestypedirsocket startswithgetattrdecode)Zlibckeyerrnumrrrmake_gaierror_map@s      r4ccs|]\}}||fVqdS)Nr).0clsr3rrr Sr7)r r c Cs|dd}tttttfD]&}|j|}|dur|||Sq|d\}}}|r|r| }|t vr~t |}t ||St jD],}t||krt|t}|||Sqtd|S)Nz  :)replacer%rr+r)r(rsearch rpartitionstrip gaierror_mapr.Zgaierrorerrno errorcodeosstrerroroserror_subclass_mapgetOSErrorr) r Zssh_clsr beforecolonZafterZpotential_strerrorr3Zos_clsrrrget_exception_for_ssh_stderrjs$     rI)8r,r@rBr"r.typingrrrr Exceptionrrr%r(r)r+r4dictr?BlockingIOErrorZEAGAINZEALREADYZ EINPROGRESSZ EWOULDBLOCKBrokenPipeErrorZEPIPEZ ESHUTDOWNChildProcessErrorZECHILDConnectionAbortedErrorZ ECONNABORTEDConnectionRefusedErrorZ ECONNREFUSEDConnectionResetErrorZ ECONNRESETFileExistsErrorZEEXISTFileNotFoundErrorENOENTIsADirectoryErrorZEISDIRNotADirectoryErrorENOTDIRInterruptedErrorZEINTRPermissionErrorZEACCESEPERMProcessLookupErrorZESRCH TimeoutErrorZ ETIMEDOUTrDrrIrrrrsF   __pycache__/transport.cpython-39.opt-1.pyc000064400000031412151116347500014404 0ustar00a /hB@sddlZddlZddlZddlZddlmZmZmZmZmZddl m Z m Z m Z ddl mZeeZedejdZGdd d eZGd d d ejejZdS) N)AnyCallableIterableSequenceTypeVar)InteractionAgentInteractionHandlerget_running_loop)get_exception_for_ssh_stderrP)boundcs8eZdZUeed<eed<eeddfdd ZZS)SubprocessError returncodestderrN)rrreturncst||||_||_dSN)super__init__rr)selfrr __class__C/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/transport.pyr$szSubprocessError.__init__)__name__ __module__ __qualname__int__annotations__strr __classcell__rrrrr s rc @seZdZUeed<ded<eed<ejed<dZeed<dZ d ed <dZ d ed <dZ d ed<dZ ded<dZ ded<dZded<dZeed<dZeed<edkdegefeedeeeeddddZejddd d!Zdd"d#d$Zd%dd&d'd(Zejdd)d*d+Zddd,d-d.Zee dd/d0d1Z!eddd2d3d4Z"dd"d5d6Z#dd"d7d8Z$dd"d9d:Z%dlddd,d;d<Z&ed"d=d>Z'dmee(e(d?d@dAZ)ej*dddBdCZ+ejd"dDdEZ,ed"dFdGZ-dd"dHdIZ.dd"dJdKZ/dd"dLdMZ0ed"dNdOZ1ed"dPdQZ2dRd"dSdTZ3dnddddUdVdWZ4e ddXdYdZZ5e6e dd[d\d]Z7dd"d^d_Z8ed"d`daZ9dd"dbdcZ:dd"dddeZ;eddfdgdhZz&FernyTransport.spawn..)taskrc std|z|\}}tdWnPtjy>YdStyx}z$td||WYd}~dSd}~00jdS)Nzexec_completed(%r, %r)z success.z OSError %r) loggerdebugresultasyncioZCancelledErrorOSErrorcloser"start)r9 transportmeexcr6rrexec_completeds    z,FernyTransport.spawn..exec_completed)r:r;r$r r_interaction_completedr" setdefaultfilenoZ create_taskZsubprocess_execr#r=ZTaskZadd_done_callback) r/r0r1r2r3r4r5protocolrDrr6rspawn@s5( zFernyTransport.spawn)rHrcCs ||_dSrr%rrHrrrrszFernyTransport.__init__)rcCs*td||js(td|jdS|jdurJ|jsJtd|jdS|jdurbtddS|jrvtddSd|_|jdurtd|j|j |jn|j dks|j rtd |j dnZ|j r|j d krtd |j|j t|jn(td |j |j|j t|j |jdS) Nz_consider_disconnect(%r)z exec_task still running %rz transport still connected %rz agent still runningz already disconnectedTz disconnect with exception %rrz clean disconnectz disconnect with ssh error %rz) disconnect with exit code %r, stderr %r)r:r;r#doner'r-r+r&r*r%connection_lostr,r.r$r rr6rrr_consider_disconnects2       z#FernyTransport._consider_disconnectzasyncio.Future[str])futurerc Csxtd||z||_td|jWn@tyj}z(td|d|_||WYd}~n d}~00|dS)Nz_interaction_completed(%r, %r)z stderr: %rz exception: %r)r:r;r<r+ Exceptionr?rO)rrPrCrrrrEs   z%FernyTransport._interaction_completed)rArcCs^td||||_|d}||_|d}||_|d}td||j|j|dS)Nzconnection_made(%r, %r)rrzcalling connection_made(%r, %r))r:r;r'Zget_pipe_transportr(r)r%connection_made)rrAZstdin_transportZstdout_transportZstderr_transportrrrrTs   zFernyTransport.connection_made)rCrcCs0td|||jdur||_d|_|dS)Nzconnection_lost(%r, %r)T)r:r;r*r-rOrrCrrrrNs  zFernyTransport.connection_lost)fddatarcCs$td||t||j|dS)Nzpipe_data_received(%r, %r, %r))r:r;lenr%Z data_received)rrVrWrrrpipe_data_receivedsz!FernyTransport.pipe_data_received)rVrCrcCsVtd|||t|trd}|dur2||n |dkrR|jsR|jsR|dS)Nz pipe_connection_lost(%r, %r, %r)r)r:r; isinstanceBrokenPipeErrorr?r.r%Z eof_received)rrVrCrrrpipe_connection_losts   z#FernyTransport.pipe_connection_lostcCs4td||j|_td|j|jdS)Nzprocess_exited(%r)z ._returncode = %r)r:r;r'get_returncoder,r"force_completionr6rrrprocess_exiteds  zFernyTransport.process_exitedcCstd||jdS)Nzpause_writing(%r))r:r;r% pause_writingr6rrrr`s zFernyTransport.pause_writingcCstd||jdS)Nzresume_writing(%r))r:r;r%resume_writingr6rrrras zFernyTransport.resume_writingcCstd||d|_|jdur0td|||_|jsNtd|j|jdurtdt t |j Wdn1s0Y|j dS)Nz close(%r, %r)Tz setting exception %rz cancelling _exec_taskz closing _subprocess_transport)r:r;r.r*r#rMcancelr' contextlibsuppressPermissionErrorr?r"r^rUrrrr? s        (zFernyTransport.closecCs |jSr)r' is_closingr6rrrrf0szFernyTransport.is_closing)namedefaultrcCs|j||Sr)r'get_extra_info)rrgrhrrrri4szFernyTransport.get_extra_infocCs ||_dSrrJrKrrr set_protocol8szFernyTransport.set_protocolcCs|jSrrJr6rrr get_protocol<szFernyTransport.get_protocolc Csvz |jWSty(|jj YStypz |jjj}||jjWYdSt yjYYdS0Yn0dS)NTF) r) is_readingNotImplementedErrorZ_pausedAttributeErrorZ_loopZ _selectorZget_keyZ_filenoKeyError)rselectorrrrrl?s     zFernyTransport.is_readingcCs|jdSr)r) pause_readingr6rrrrqOszFernyTransport.pause_readingcCs|jdSr)r)resume_readingr6rrrrrSszFernyTransport.resume_readingcCs|j|jdSr)r(abortr'killr6rrrrsWs zFernyTransport.abortcCs |jSr)r( can_write_eofr6rrrru]szFernyTransport.can_write_eofcCs |jSr)r(get_write_buffer_sizer6rrrrvasz$FernyTransport.get_write_buffer_sizeztuple[int, int]cCs |jSr)r(get_write_buffer_limitsr6rrrrwesz&FernyTransport.get_write_buffer_limits)highlowrcCs|j||Sr)r(set_write_buffer_limits)rrxryrrrrzisz&FernyTransport.set_write_buffer_limits)rWrcCs |j|Sr)r(write)rrWrrrr{mszFernyTransport.write) list_of_datarcCs |j|Sr)r( writelines)rr|rrrr}qszFernyTransport.writelinescCs |jSr)r( write_eofr6rrrr~uszFernyTransport.write_eofcCs |jSr)r'get_pidr6rrrr{szFernyTransport.get_pidcCs|jSr)r,r6rrrr]szFernyTransport.get_returncodecCs|jdSr)r'rtr6rrrrtszFernyTransport.kill)numberrcCs|j|dSr)r' send_signal)rrrrrrszFernyTransport.send_signalcCs|jdSr)r' terminater6rrrrszFernyTransport.terminate)NrT)N)N)NN)>rrrrrboolr=Protocolr&r'r(r)r*r+r,r-r. classmethodrr rrr rrIrrOrEZ BaseTransportrTrNrbytesrYr\r_r`rar?rfobjectriZ BaseProtocolrjrkrlrqrrrsrurvrwrzr{rr}r~rr]rtrrrrrrr!*st            c, r!)r=rcZloggingtypingrrrrrZinteraction_agentrr r Z ssh_errorsr Z getLoggerrr:rr rRrZ TransportZSubprocessProtocolr!rrrrs   __pycache__/transport.cpython-39.pyc000064400000032600151116347500013445 0ustar00a /hB@sddlZddlZddlZddlZddlmZmZmZmZmZddl m Z m Z m Z ddl mZeeZedejdZGdd d eZGd d d ejejZdS) N)AnyCallableIterableSequenceTypeVar)InteractionAgentInteractionHandlerget_running_loop)get_exception_for_ssh_stderrP)boundcs8eZdZUeed<eed<eeddfdd ZZS)SubprocessError returncodestderrN)rrreturncst||||_||_dSN)super__init__rr)selfrr __class__C/usr/lib/python3.9/site-packages/cockpit/_vendor/ferny/transport.pyr$szSubprocessError.__init__)__name__ __module__ __qualname__int__annotations__strr __classcell__rrrrr s rc @seZdZUeed<ded<eed<ejed<dZeed<dZ d ed <dZ d ed <dZ d ed<dZ ded<dZ ded<dZded<dZeed<dZeed<edkdegefeedeeeeddddZejddd d!Zdd"d#d$Zd%dd&d'd(Zejdd)d*d+Zddd,d-d.Zee dd/d0d1Z!eddd2d3d4Z"dd"d5d6Z#dd"d7d8Z$dd"d9d:Z%dlddd,d;d<Z&ed"d=d>Z'dmee(e(d?d@dAZ)ej*dddBdCZ+ejd"dDdEZ,ed"dFdGZ-dd"dHdIZ.dd"dJdKZ/dd"dLdMZ0ed"dNdOZ1ed"dPdQZ2dRd"dSdTZ3dnddddUdVdWZ4e ddXdYdZZ5e6e dd[d\d]Z7dd"d^d_Z8ed"d`daZ9dd"dbdcZ:dd"dddeZ;eddfdgdhZz&FernyTransport.spawn..)taskrc std||jusJz&|\}}|us6JtdWnPtjyXYdSty}z$td||WYd}~dSd}~00j|usJj dusJj dusJj dS)Nzexec_completed(%r, %r)z success.z OSError %r) loggerdebugr#resultasyncioZCancelledErrorOSErrorcloser'r(r)r"start)r9 transportmeexcr6rrexec_completeds     z,FernyTransport.spawn..exec_completed)r:r;r$r r_interaction_completedr" setdefaultfilenoZ create_taskZsubprocess_execr#r=ZTaskZadd_done_callback) r/r0r1r2r3r4r5protocolrDrr6rspawn@s5( zFernyTransport.spawn)rHrcCs ||_dSrr%rrHrrrrszFernyTransport.__init__)rcCs:td||js(td|jdS|jdurJ|jsJtd|jdS|jdurbtddS|jrvtddSd|_|jdurtd|j|j |jn|j dks|j rtd |j dnj|j r|j d krtd |j|j t|jn8td |j |j|j dus J|j t|j |jdS) Nz_consider_disconnect(%r)z exec_task still running %rz transport still connected %rz agent still runningz already disconnectedTz disconnect with exception %rrz clean disconnectz disconnect with ssh error %rz) disconnect with exit code %r, stderr %r)r:r;r#doner'r-r+r&r*r%connection_lostr,r.r$r rr6rrr_consider_disconnects4       z#FernyTransport._consider_disconnectzasyncio.Future[str])futurerc Csxtd||z||_td|jWn@tyj}z(td|d|_||WYd}~n d}~00|dS)Nz_interaction_completed(%r, %r)z stderr: %rz exception: %r)r:r;r<r+ Exceptionr?rO)rrPrCrrrrEs   z%FernyTransport._interaction_completed)rArcCstd||t|tjsJ||_|d}t|tjs>J||_|d}t|tj s^J||_ |d}|duszJtd||j |j |dS)Nzconnection_made(%r, %r)rrzcalling connection_made(%r, %r)) r:r; isinstancer=ZSubprocessTransportr'Zget_pipe_transportZWriteTransportr(Z ReadTransportr)r%connection_made)rrAZstdin_transportZstdout_transportZstderr_transportrrrrUs    zFernyTransport.connection_made)rCrcCs0td|||jdur||_d|_|dS)Nzconnection_lost(%r, %r)T)r:r;r*r-rOrrCrrrrNs  zFernyTransport.connection_lost)fddatarcCs0td||t||dks J|j|dS)Nzpipe_data_received(%r, %r, %r)r)r:r;lenr%Z data_received)rrWrXrrrpipe_data_receiveds z!FernyTransport.pipe_data_received)rWrCrcCsbtd||||dvsJt|tr*d}|dur>||n |dkr^|js^|js^|dS)Nz pipe_connection_lost(%r, %r, %r))rrr)r:r;rTBrokenPipeErrorr?r.r%Z eof_received)rrWrCrrrpipe_connection_losts    z#FernyTransport.pipe_connection_lostcCsBtd||jdusJ|j|_td|j|jdS)Nzprocess_exited(%r)z ._returncode = %r)r:r;r'get_returncoder,r"force_completionr6rrrprocess_exiteds   zFernyTransport.process_exitedcCstd||jdS)Nzpause_writing(%r))r:r;r% pause_writingr6rrrr`s zFernyTransport.pause_writingcCstd||jdS)Nzresume_writing(%r))r:r;r%resume_writingr6rrrras zFernyTransport.resume_writingcCstd||d|_|jdur0td|||_|jsNtd|j|jdurtdt t |j Wdn1s0Y|j dS)Nz close(%r, %r)Tz setting exception %rz cancelling _exec_taskz closing _subprocess_transport)r:r;r.r*r#rMcancelr' contextlibsuppressPermissionErrorr?r"r^rVrrrr? s        (zFernyTransport.closecCs|jdusJ|jSr)r' is_closingr6rrrrf0szFernyTransport.is_closing)namedefaultrcCs|jdusJ|j||Sr)r'get_extra_info)rrgrhrrrri4szFernyTransport.get_extra_infocCst|tjsJ||_dSr)rTr=Protocolr%rKrrr set_protocol8szFernyTransport.set_protocolcCs|jSrrJr6rrr get_protocol<szFernyTransport.get_protocolc Cs|jdusJz |jWSty6|jj YSty~z |jjj}||jjWYdSt yxYYdS0Yn0dS)NTF) r) is_readingNotImplementedErrorZ_pausedAttributeErrorZ_loopZ _selectorZget_keyZ_filenoKeyError)rselectorrrrrm?s     zFernyTransport.is_readingcCs|jdusJ|jdSr)r) pause_readingr6rrrrrOszFernyTransport.pause_readingcCs|jdusJ|jdSr)r)resume_readingr6rrrrsSszFernyTransport.resume_readingcCs4|jdusJ|jdusJ|j|jdSr)r(r'abortkillr6rrrrtWs zFernyTransport.abortcCs|jdusJ|jSr)r( can_write_eofr6rrrrv]szFernyTransport.can_write_eofcCs|jdusJ|jSr)r(get_write_buffer_sizer6rrrrwasz$FernyTransport.get_write_buffer_sizeztuple[int, int]cCs|jdusJ|jSr)r(get_write_buffer_limitsr6rrrrxesz&FernyTransport.get_write_buffer_limits)highlowrcCs|jdusJ|j||Sr)r(set_write_buffer_limits)rryrzrrrr{isz&FernyTransport.set_write_buffer_limits)rXrcCs|jdusJ|j|Sr)r(write)rrXrrrr|mszFernyTransport.write) list_of_datarcCs|jdusJ|j|Sr)r( writelines)rr}rrrr~qszFernyTransport.writelinescCs|jdusJ|jSr)r( write_eofr6rrrruszFernyTransport.write_eofcCs|jdusJ|jSr)r'get_pidr6rrrr{szFernyTransport.get_pidcCs|jSr)r,r6rrrr]szFernyTransport.get_returncodecCs|jdusJ|jdSr)r'rur6rrrruszFernyTransport.kill)numberrcCs|jdusJ|j|dSr)r' send_signal)rrrrrrszFernyTransport.send_signalcCs|jdusJ|jdSr)r' terminater6rrrrszFernyTransport.terminate)NrT)N)N)NN)>rrrrrboolr=rjr&r'r(r)r*r+r,r-r. classmethodrr rrr rrIrrOrEZ BaseTransportrUrNrbytesrZr\r_r`rar?rfobjectriZ BaseProtocolrkrlrmrrrsrtrvrwrxr{r|rr~rrr]rurrrrrrr!*st            c, r!)r=rcZloggingtypingrrrrrZinteraction_agentrr r Z ssh_errorsr Z getLoggerrr:rjr rRrZ TransportZSubprocessProtocolr!rrrrs   __init__.py000064400000002534151116347500006664 0ustar00from .interaction_agent import ( BEIBOOT_GADGETS, COMMAND_TEMPLATE, AskpassHandler, InteractionAgent, InteractionError, InteractionHandler, temporary_askpass, write_askpass_to_tmpdir, ) from .session import Session from .ssh_askpass import ( AskpassPrompt, SshAskpassResponder, SshFIDOPINPrompt, SshFIDOUserPresencePrompt, SshHostKeyPrompt, SshPassphrasePrompt, SshPasswordPrompt, SshPKCS11PINPrompt, ) from .ssh_errors import ( SshAuthenticationError, SshChangedHostKeyError, SshError, SshHostKeyError, SshUnknownHostKeyError, ) from .transport import FernyTransport, SubprocessError __all__ = [ 'AskpassHandler', 'AskpassPrompt', 'AuthenticationError', 'BEIBOOT_GADGETS', 'COMMAND_TEMPLATE', 'ChangedHostKeyError', 'FernyTransport', 'HostKeyError', 'InteractionAgent', 'InteractionError', 'InteractionHandler', 'Session', 'SshAskpassResponder', 'SshAuthenticationError', 'SshChangedHostKeyError', 'SshError', 'SshFIDOPINPrompt', 'SshFIDOUserPresencePrompt', 'SshHostKeyError', 'SshHostKeyPrompt', 'SshPKCS11PINPrompt', 'SshPassphrasePrompt', 'SshPasswordPrompt', 'SshUnknownHostKeyError', 'SubprocessError', 'temporary_askpass', 'write_askpass_to_tmpdir', ] __version__ = '0' askpass.py000064400000000114151116347500006562 0ustar00from .interaction_client import main if __name__ == '__main__': main() interaction_agent.py000064400000036671151116347500010633 0ustar00# ferny - asyncio SSH client library, using ssh(1) # # Copyright (C) 2023 Allison Karlitskaya # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import array import ast import asyncio import contextlib import logging import os import re import socket import tempfile from typing import Any, Callable, ClassVar, Generator, Sequence from . import interaction_client logger = logging.getLogger(__name__) COMMAND_RE = re.compile(b'\0ferny\0([^\n]*)\0\0\n') COMMAND_TEMPLATE = '\0ferny\0{(command, args)!r}\0\0\n' BEIBOOT_GADGETS = { "command": fr""" import sys def command(command, *args): sys.stderr.write(f{COMMAND_TEMPLATE!r}) sys.stderr.flush() """, "end": r""" def end(): command('ferny.end') """, } class InteractionError(Exception): pass try: recv_fds = socket.recv_fds except AttributeError: # Python < 3.9 def recv_fds( sock: socket.socket, bufsize: int, maxfds: int, flags: int = 0 ) -> 'tuple[bytes, list[int], int, None]': fds = array.array("i") msg, ancdata, flags, addr = sock.recvmsg(bufsize, socket.CMSG_LEN(maxfds * fds.itemsize)) for cmsg_level, cmsg_type, cmsg_data in ancdata: if (cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS): fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) return msg, list(fds), flags, addr def get_running_loop() -> asyncio.AbstractEventLoop: try: return asyncio.get_running_loop() except AttributeError: # Python 3.6 return asyncio.get_event_loop() class InteractionHandler: commands: ClassVar[Sequence[str]] async def run_command(self, command: str, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str) -> None: raise NotImplementedError class AskpassHandler(InteractionHandler): commands: ClassVar[Sequence[str]] = ('ferny.askpass',) async def do_askpass(self, messages: str, prompt: str, hint: str) -> 'str | None': """Prompt the user for an authentication or confirmation interaction. 'messages' is data that was sent to stderr before the interaction was requested. 'prompt' is the interaction prompt. The expected response type depends on hint: - "confirm": ask for permission, returning "yes" if accepted - example: authorizing agent operation - "none": show a request without need for a response - example: please touch your authentication token - otherwise: return a password or other form of text token - examples: enter password, unlock private key In any case, the function should properly handle cancellation. For the "none" case, this will be the normal way to dismiss the dialog. """ return None async def do_hostkey(self, reason: str, host: str, algorithm: str, key: str, fingerprint: str) -> bool: """Prompt the user for a decision regarding acceptance of a host key. The "reason" will be either "HOSTNAME" or "ADDRESS" (if `CheckHostIP` is enabled). The host, algorithm, and key parameters are the values in the form that they would appear one a single line in the known hosts file. The fingerprint is the key fingerprint in the format that ssh would normally present it to the user. In case the host key should be accepted, this function needs to return True. Returning False means that ssh implements its default logic. To interrupt the connection, raise an exception. """ return False async def do_custom_command( self, command: str, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str ) -> None: """Handle a custom command. The command name, its arguments, the passed fds, and the stderr leading up to the command invocation are all provided. See doc/interaction-protocol.md """ async def _askpass_command(self, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str) -> None: logger.debug('_askpass_command(%s, %s, %s)', args, fds, stderr) try: argv, env = args assert isinstance(argv, list) assert all(isinstance(arg, str) for arg in argv) assert isinstance(env, dict) assert all(isinstance(key, str) and isinstance(val, str) for key, val in env.items()) assert len(fds) == 2 except (ValueError, TypeError, AssertionError) as exc: logger.error('Invalid arguments to askpass interaction: %s, %s: %s', args, fds, exc) return with open(fds.pop(0), 'w') as status, open(fds.pop(0), 'w') as stdout: try: loop = get_running_loop() try: task = asyncio.current_task() except AttributeError: task = asyncio.Task.current_task() # type:ignore[attr-defined] # (Python 3.6) assert task is not None loop.add_reader(status, task.cancel) if len(argv) == 2: # normal askpass prompt = argv[1] hint = env.get('SSH_ASKPASS_PROMPT', '') logger.debug('do_askpass(%r, %r, %r)', stderr, prompt, hint) answer = await self.do_askpass(stderr, prompt, hint) logger.debug('do_askpass answer %r', answer) if answer is not None: print(answer, file=stdout) print(0, file=status) elif len(argv) == 6: # KnownHostsCommand argv0, reason, host, algorithm, key, fingerprint = argv if reason in ['ADDRESS', 'HOSTNAME']: logger.debug('do_hostkey(%r, %r, %r, %r, %r)', reason, host, algorithm, key, fingerprint) if await self.do_hostkey(reason, host, algorithm, key, fingerprint): print(host, algorithm, key, file=stdout) else: logger.debug('ignoring KnownHostsCommand reason %r', reason) print(0, file=status) else: logger.error('Incorrect number of command-line arguments to ferny-askpass: %s', argv) finally: loop.remove_reader(status) async def run_command(self, command: str, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str) -> None: logger.debug('run_command(%s, %s, %s, %s)', command, args, fds, stderr) if command == 'ferny.askpass': await self._askpass_command(args, fds, stderr) else: await self.do_custom_command(command, args, fds, stderr) class InteractionAgent: _handlers: 'dict[str, InteractionHandler]' _loop: asyncio.AbstractEventLoop _tasks: 'set[asyncio.Task]' _buffer: bytearray _ours: socket.socket _theirs: socket.socket _completion_future: 'asyncio.Future[str]' _pending_result: 'None | str | Exception' = None _end: bool = False def _consider_completion(self) -> None: logger.debug('_consider_completion(%r)', self) if self._pending_result is None or self._tasks: logger.debug(' but not ready yet') elif self._completion_future.done(): logger.debug(' but already complete') elif isinstance(self._pending_result, str): logger.debug(' submitting stderr (%r) to completion_future', self._pending_result) self._completion_future.set_result(self._pending_result) else: logger.debug(' submitting exception (%r) to completion_future') self._completion_future.set_exception(self._pending_result) def _result(self, result: 'str | Exception') -> None: logger.debug('_result(%r, %r)', self, result) if self._pending_result is None: self._pending_result = result if self._ours.fileno() != -1: logger.debug(' remove_reader(%r)', self._ours) self._loop.remove_reader(self._ours.fileno()) for task in self._tasks: logger.debug(' cancel(%r)', task) task.cancel() logger.debug(' closing sockets') self._theirs.close() # idempotent self._ours.close() self._consider_completion() def _invoke_command(self, stderr: bytes, command_blob: bytes, fds: 'list[int]') -> None: logger.debug('_invoke_command(%r, %r, %r)', stderr, command_blob, fds) try: command, args = ast.literal_eval(command_blob.decode()) if not isinstance(command, str) or not isinstance(args, tuple): raise TypeError('Invalid argument types') except (UnicodeDecodeError, SyntaxError, ValueError, TypeError) as exc: logger.error('Received invalid ferny command: %s: %s', command_blob, exc) return if command == 'ferny.end': self._end = True self._result(self._buffer.decode(errors='replace')) return try: handler = self._handlers[command] except KeyError: logger.error('Received unhandled ferny command: %s', command) return # The task is responsible for the list of fds and removing itself # from the set. task_fds = list(fds) task = self._loop.create_task(handler.run_command(command, args, task_fds, stderr.decode())) def bottom_half(completed_task: asyncio.Task) -> None: assert completed_task is task while task_fds: os.close(task_fds.pop()) self._tasks.remove(task) try: task.result() logger.debug('%r completed cleanly', handler) except asyncio.CancelledError: # this is not an error — it just means ferny-askpass exited via signal logger.debug('%r was cancelled', handler) except Exception as exc: logger.debug('%r raised %r', handler, exc) self._result(exc) self._consider_completion() task.add_done_callback(bottom_half) self._tasks.add(task) fds[:] = [] def _got_data(self, data: bytes, fds: 'list[int]') -> None: logger.debug('_got_data(%r, %r)', data, fds) if data == b'': self._result(self._buffer.decode(errors='replace')) return self._buffer.extend(data) # Read zero or more "remote" messages chunks = COMMAND_RE.split(self._buffer) self._buffer = bytearray(chunks.pop()) while len(chunks) > 1: self._invoke_command(chunks[0], chunks[1], []) chunks = chunks[2:] # Maybe read one "local" message if fds: assert self._buffer.endswith(b'\0'), self._buffer stderr = self._buffer[:-1] self._buffer = bytearray(b'') with open(fds.pop(0), 'rb') as command_channel: command = command_channel.read() self._invoke_command(stderr, command, fds) def _read_ready(self) -> None: try: data, fds, _flags, _addr = recv_fds(self._ours, 4096, 10, flags=socket.MSG_DONTWAIT) except BlockingIOError: return except OSError as exc: self._result(exc) else: self._got_data(data, fds) finally: while fds: os.close(fds.pop()) def __init__( self, handlers: Sequence[InteractionHandler], loop: 'asyncio.AbstractEventLoop | None' = None, done_callback: 'Callable[[asyncio.Future[str]], None] | None' = None, ) -> None: self._loop = loop or get_running_loop() self._completion_future = self._loop.create_future() self._tasks = set() self._handlers = {} for handler in handlers: for command in handler.commands: self._handlers[command] = handler if done_callback is not None: self._completion_future.add_done_callback(done_callback) self._theirs, self._ours = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) self._buffer = bytearray() def fileno(self) -> int: return self._theirs.fileno() def start(self) -> None: logger.debug('start(%r)', self) if self._ours.fileno() != -1: logger.debug(' add_reader(%r)', self._ours) self._loop.add_reader(self._ours.fileno(), self._read_ready) else: logger.debug(' ...but agent is already finished.') logger.debug(' close(%r)', self._theirs) self._theirs.close() def force_completion(self) -> None: logger.debug('force_completion(%r)', self) # read any residual data on stderr, but don't process commands, and # don't block try: if self._ours.fileno() != -1: logger.debug(' draining pending stderr data (non-blocking)') with contextlib.suppress(BlockingIOError): while True: data = self._ours.recv(4096, socket.MSG_DONTWAIT) logger.debug(' got %d bytes', len(data)) if not data: break self._buffer.extend(data) except OSError as exc: self._result(exc) else: self._result(self._buffer.decode(errors='replace')) async def communicate(self) -> None: logger.debug('_communicate(%r)', self) try: self.start() # We assume that we are the only ones to write to # self._completion_future. If we directly await it, though, it can # also have a asyncio.CancelledError posted to it from outside. # Shield it to prevent that from happening. stderr = await asyncio.shield(self._completion_future) logger.debug('_communicate(%r) stderr result is %r', self, stderr) finally: logger.debug('_communicate finished. Ensuring completion.') self.force_completion() if not self._end: logger.debug('_communicate never saw ferny.end. raising InteractionError.') raise InteractionError(stderr.strip()) def write_askpass_to_tmpdir(tmpdir: str) -> str: askpass_path = os.path.join(tmpdir, 'ferny-askpass') fd = os.open(askpass_path, os.O_CREAT | os.O_WRONLY | os.O_CLOEXEC | os.O_EXCL | os.O_NOFOLLOW, 0o777) try: os.write(fd, __loader__.get_data(interaction_client.__file__)) # type: ignore finally: os.close(fd) return askpass_path @contextlib.contextmanager def temporary_askpass(**kwargs: Any) -> Generator[str, None, None]: with tempfile.TemporaryDirectory(**kwargs) as directory: yield write_askpass_to_tmpdir(directory) interaction_client.py000064400000002124151116347500010775 0ustar00#!/usr/bin/python3 import array import io import os import socket import sys from typing import Sequence def command(stderr_fd: int, command: str, *args: object, fds: Sequence[int] = ()) -> None: cmd_read, cmd_write = [io.open(*end) for end in zip(os.pipe(), 'rw')] with cmd_write: with cmd_read: with socket.fromfd(stderr_fd, socket.AF_UNIX, socket.SOCK_STREAM) as sock: fd_array = array.array('i', (cmd_read.fileno(), *fds)) sock.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fd_array)]) cmd_write.write(repr((command, args))) def askpass(stderr_fd: int, stdout_fd: int, args: 'list[str]', env: 'dict[str, str]') -> int: ours, theirs = socket.socketpair() with theirs: command(stderr_fd, 'ferny.askpass', args, env, fds=(theirs.fileno(), stdout_fd)) with ours: return int(ours.recv(16) or b'1') def main() -> None: if len(sys.argv) == 1: command(2, 'ferny.end', []) else: sys.exit(askpass(2, 1, sys.argv, dict(os.environ))) if __name__ == '__main__': main() py.typed000064400000000000151116347500006234 0ustar00session.py000064400000016743151116347500006617 0ustar00# ferny - asyncio SSH client library, using ssh(1) # # Copyright (C) 2022 Allison Karlitskaya # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import asyncio import ctypes import functools import logging import os import shlex import signal import subprocess import tempfile from typing import Mapping, Sequence from . import ssh_errors from .interaction_agent import InteractionAgent, InteractionError, InteractionHandler, write_askpass_to_tmpdir prctl = ctypes.CDLL(None).prctl logger = logging.getLogger(__name__) PR_SET_PDEATHSIG = 1 @functools.lru_cache() def has_feature(feature: str, teststr: str = 'x') -> bool: try: subprocess.check_output(['ssh', f'-o{feature} {teststr}', '-G', 'nonexisting'], stderr=subprocess.DEVNULL) return True except subprocess.CalledProcessError: return False class SubprocessContext: def wrap_subprocess_args(self, args: Sequence[str]) -> Sequence[str]: """Return the args required to launch a process in the given context. For example, this might return a vector with ["sudo"] or ["flatpak-spawn", "--host"] prepended. It is also possible that more substantial changes may be performed. This function is not permitted to modify its argument, although it may (optionally) return it unmodified, if no changes are required. """ return args def wrap_subprocess_env(self, env: Mapping[str, str]) -> Mapping[str, str]: """Return the envp required to launch a process in the given context. For example, this might set the "SUDO_ASKPASS" environment variable, if needed. As with wrap_subprocess_args(), this function is not permitted to modify its argument, although it may (optionally) return it unmodified if no changes are required. """ return env class Session(SubprocessContext, InteractionHandler): # Set after .connect() called, even if failed _controldir: 'tempfile.TemporaryDirectory | None' = None _controlsock: 'str | None' = None # Set if connected, else None _process: 'asyncio.subprocess.Process | None' = None async def connect(self, destination: str, handle_host_key: bool = False, configfile: 'str | None' = None, identity_file: 'str | None' = None, login_name: 'str | None' = None, options: 'Mapping[str, str] | None' = None, pkcs11: 'str | None' = None, port: 'int | None' = None, interaction_responder: 'InteractionHandler | None' = None) -> None: rundir = os.path.join(os.environ.get('XDG_RUNTIME_DIR', '/run'), 'ferny') os.makedirs(rundir, exist_ok=True) self._controldir = tempfile.TemporaryDirectory(dir=rundir) self._controlsock = f'{self._controldir.name}/socket' # In general, we can't guarantee an accessible and executable version # of this file, but since it's small and we're making a temporary # directory anyway, let's just copy it into place and use it from # there. askpass_path = write_askpass_to_tmpdir(self._controldir.name) env = dict(os.environ) env['SSH_ASKPASS'] = askpass_path env['SSH_ASKPASS_REQUIRE'] = 'force' # old SSH doesn't understand SSH_ASKPASS_REQUIRE and guesses based on DISPLAY instead env['DISPLAY'] = '-' args = [ '-M', '-N', '-S', self._controlsock, '-o', 'PermitLocalCommand=yes', '-o', f'LocalCommand={askpass_path}', ] if configfile is not None: args.append(f'-F{configfile}') if identity_file is not None: args.append(f'-i{identity_file}') if options is not None: for key in options: # Note: Mapping may not have .items() args.append(f'-o{key} {options[key]}') if pkcs11 is not None: args.append(f'-I{pkcs11}') if port is not None: args.append(f'-p{port}') if login_name is not None: args.append(f'-l{login_name}') if handle_host_key and has_feature('KnownHostsCommand'): args.extend([ '-o', f'KnownHostsCommand={askpass_path} %I %H %t %K %f', '-o', 'StrictHostKeyChecking=yes', ]) agent = InteractionAgent([interaction_responder] if interaction_responder is not None else []) # SSH_ASKPASS_REQUIRE is not generally available, so use setsid process = await asyncio.create_subprocess_exec( *('/usr/bin/ssh', *args, destination), env=env, start_new_session=True, stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.DEVNULL, stderr=agent, # type: ignore preexec_fn=lambda: prctl(PR_SET_PDEATHSIG, signal.SIGKILL)) # This is tricky: we need to clean up the subprocess, but only in case # if failure. Otherwise, we keep it around. try: await agent.communicate() assert os.path.exists(self._controlsock) self._process = process except InteractionError as exc: await process.wait() raise ssh_errors.get_exception_for_ssh_stderr(str(exc)) from None except BaseException: # If we get here because the InteractionHandler raised an # exception then SSH might still be running, and may even attempt # further interactions (ie: 2nd attempt for password). We already # have our exception and don't need any more info. Kill it. try: process.kill() except ProcessLookupError: pass # already exited? good. await process.wait() raise def is_connected(self) -> bool: return self._process is not None async def wait(self) -> None: assert self._process is not None await self._process.wait() def exit(self) -> None: assert self._process is not None self._process.terminate() async def disconnect(self) -> None: self.exit() await self.wait() # Launching of processes def wrap_subprocess_args(self, args: Sequence[str]) -> Sequence[str]: assert self._controlsock is not None # 1. We specify the hostname as the empty string: it will be ignored # when ssh is trying to use the control socket, but in case the # socket has stopped working, ssh will try to fall back to directly # connecting, in which case an empty hostname will prevent that. # 2. We need to quote the arguments — ssh will paste them together # using only spaces, executing the result using the user's shell. return ('ssh', '-S', self._controlsock, '', *map(shlex.quote, args)) ssh_askpass.py000064400000015315151116347500007450 0ustar00import logging import re from typing import ClassVar, Match, Sequence from .interaction_agent import AskpassHandler logger = logging.getLogger(__name__) class AskpassPrompt: """An askpass prompt resulting from a call to ferny-askpass. stderr: the contents of stderr from before ferny-askpass was called. Likely related to previous failed operations. messages: all but the last line of the prompt as handed to ferny-askpass. Usually contains context about the question. prompt: the last line handed to ferny-askpass. The prompt itself. """ stderr: str messages: str prompt: str def __init__(self, prompt: str, messages: str, stderr: str) -> None: self.stderr = stderr self.messages = messages self.prompt = prompt def reply(self, response: str) -> None: pass def close(self) -> None: pass async def handle_via(self, responder: 'SshAskpassResponder') -> None: try: response = await self.dispatch(responder) if response is not None: self.reply(response) finally: self.close() async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None': return await responder.do_prompt(self) class SSHAskpassPrompt(AskpassPrompt): # The valid answers to prompts of this type. If this is None then any # answer is permitted. If it's a sequence then only answers from the # sequence are permitted. If it's an empty sequence, then no answer is # permitted (ie: the askpass callback should never return). answers: 'ClassVar[Sequence[str] | None]' = None # Patterns to capture. `_pattern` *must* match. _pattern: ClassVar[str] # `_extra_patterns` can fill in extra class attributes if they match. _extra_patterns: ClassVar[Sequence[str]] = () def __init__(self, prompt: str, messages: str, stderr: str, match: Match) -> None: super().__init__(prompt, messages, stderr) self.__dict__.update(match.groupdict()) for pattern in self._extra_patterns: extra_match = re.search(with_helpers(pattern), messages, re.M) if extra_match is not None: self.__dict__.update(extra_match.groupdict()) # Specific prompts HELPERS = { "%{algorithm}": r"(?P\b[-\w]+\b)", "%{filename}": r"(?P.+)", "%{fingerprint}": r"(?PSHA256:[0-9A-Za-z+/]{43})", "%{hostname}": r"(?P[^ @']+)", "%{pkcs11_id}": r"(?P.+)", "%{username}": r"(?P[^ @']+)", } class SshPasswordPrompt(SSHAskpassPrompt): _pattern = r"%{username}@%{hostname}'s password: " username: 'str | None' = None hostname: 'str | None' = None async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None': return await responder.do_password_prompt(self) class SshPassphrasePrompt(SSHAskpassPrompt): _pattern = r"Enter passphrase for key '%{filename}': " filename: str async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None': return await responder.do_passphrase_prompt(self) class SshFIDOPINPrompt(SSHAskpassPrompt): _pattern = r"Enter PIN for %{algorithm} key %{filename}: " algorithm: str filename: str async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None': return await responder.do_fido_pin_prompt(self) class SshFIDOUserPresencePrompt(SSHAskpassPrompt): _pattern = r"Confirm user presence for key %{algorithm} %{fingerprint}" answers = () algorithm: str fingerprint: str async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None': return await responder.do_fido_user_presence_prompt(self) class SshPKCS11PINPrompt(SSHAskpassPrompt): _pattern = r"Enter PIN for '%{pkcs11_id}': " pkcs11_id: str async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None': return await responder.do_pkcs11_pin_prompt(self) class SshHostKeyPrompt(SSHAskpassPrompt): _pattern = r"Are you sure you want to continue connecting \(yes/no(/\[fingerprint\])?\)\? " _extra_patterns = [ r"%{fingerprint}[.]$", r"^%{algorithm} key fingerprint is", r"^The fingerprint for the %{algorithm} key sent by the remote host is$" ] answers = ('yes', 'no') algorithm: str fingerprint: str async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None': return await responder.do_host_key_prompt(self) def with_helpers(pattern: str) -> str: for name, helper in HELPERS.items(): pattern = pattern.replace(name, helper) assert '%{' not in pattern return pattern def categorize_ssh_prompt(string: str, stderr: str) -> AskpassPrompt: classes = [ SshFIDOPINPrompt, SshFIDOUserPresencePrompt, SshHostKeyPrompt, SshPKCS11PINPrompt, SshPassphrasePrompt, SshPasswordPrompt, ] # The last line is the line after the last newline character, excluding the # optional final newline character. eg: "x\ny\nLAST\n" or "x\ny\nLAST" second_last_newline = string.rfind('\n', 0, -1) if second_last_newline >= 0: last_line = string[second_last_newline + 1:] extras = string[:second_last_newline + 1] else: last_line = string extras = '' for cls in classes: pattern = with_helpers(cls._pattern) match = re.fullmatch(pattern, last_line) if match is not None: return cls(last_line, extras, stderr, match) return AskpassPrompt(last_line, extras, stderr) class SshAskpassResponder(AskpassHandler): async def do_askpass(self, stderr: str, prompt: str, hint: str) -> 'str | None': return await categorize_ssh_prompt(prompt, stderr).dispatch(self) async def do_prompt(self, prompt: AskpassPrompt) -> 'str | None': # Default fallback for unrecognised message types: unimplemented return None async def do_fido_pin_prompt(self, prompt: SshFIDOPINPrompt) -> 'str | None': return await self.do_prompt(prompt) async def do_fido_user_presence_prompt(self, prompt: SshFIDOUserPresencePrompt) -> 'str | None': return await self.do_prompt(prompt) async def do_host_key_prompt(self, prompt: SshHostKeyPrompt) -> 'str | None': return await self.do_prompt(prompt) async def do_pkcs11_pin_prompt(self, prompt: SshPKCS11PINPrompt) -> 'str | None': return await self.do_prompt(prompt) async def do_passphrase_prompt(self, prompt: SshPassphrasePrompt) -> 'str | None': return await self.do_prompt(prompt) async def do_password_prompt(self, prompt: SshPasswordPrompt) -> 'str | None': return await self.do_prompt(prompt) ssh_errors.py000064400000011357151116347500007321 0ustar00# ferny - asyncio SSH client library, using ssh(1) # # Copyright (C) 2023 Allison Karlitskaya # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import ctypes import errno import os import re import socket from typing import ClassVar, Iterable, Match, Pattern class SshError(Exception): PATTERN: ClassVar[Pattern] def __init__(self, match: 'Match | None', stderr: str) -> None: super().__init__(match.group(0) if match is not None else stderr) self.stderr = stderr class SshAuthenticationError(SshError): PATTERN = re.compile(r'^([^:]+): Permission denied \(([^()]+)\)\.$', re.M) def __init__(self, match: Match, stderr: str) -> None: super().__init__(match, stderr) self.destination = match.group(1) self.methods = match.group(2).split(',') self.message = match.group(0) class SshInvalidHostnameError(SshError): PATTERN = re.compile(r'^hostname contains invalid characters', re.I) # generic host key error for OSes without KnownHostsCommand support class SshHostKeyError(SshError): PATTERN = re.compile(r'^Host key verification failed.$', re.M) # specific errors for OSes with KnownHostsCommand class SshUnknownHostKeyError(SshHostKeyError): PATTERN = re.compile(r'^No .* host key is known.*Host key verification failed.$', re.S | re.M) class SshChangedHostKeyError(SshHostKeyError): PATTERN = re.compile(r'warning.*remote host identification has changed', re.I) # Functionality for mapping getaddrinfo()-family error messages to their # equivalent Python exceptions. def make_gaierror_map() -> 'Iterable[tuple[str, int]]': libc = ctypes.CDLL(None) libc.gai_strerror.restype = ctypes.c_char_p for key in dir(socket): if key.startswith('EAI_'): errnum = getattr(socket, key) yield libc.gai_strerror(errnum).decode('utf-8'), errnum gaierror_map = dict(make_gaierror_map()) # Functionality for passing strerror() error messages to their equivalent # Python exceptions. # There doesn't seem to be an official API for turning an errno into the # correct subtype of OSError, and the list that cpython uses is hidden fairly # deeply inside of the implementation. This is basically copied from the # ADD_ERRNO() lines in _PyExc_InitState in cpython/Objects/exceptions.c oserror_subclass_map = dict((errnum, cls) for cls, errnum in [ (BlockingIOError, errno.EAGAIN), (BlockingIOError, errno.EALREADY), (BlockingIOError, errno.EINPROGRESS), (BlockingIOError, errno.EWOULDBLOCK), (BrokenPipeError, errno.EPIPE), (BrokenPipeError, errno.ESHUTDOWN), (ChildProcessError, errno.ECHILD), (ConnectionAbortedError, errno.ECONNABORTED), (ConnectionRefusedError, errno.ECONNREFUSED), (ConnectionResetError, errno.ECONNRESET), (FileExistsError, errno.EEXIST), (FileNotFoundError, errno.ENOENT), (IsADirectoryError, errno.EISDIR), (NotADirectoryError, errno.ENOTDIR), (InterruptedError, errno.EINTR), (PermissionError, errno.EACCES), (PermissionError, errno.EPERM), (ProcessLookupError, errno.ESRCH), (TimeoutError, errno.ETIMEDOUT), ]) def get_exception_for_ssh_stderr(stderr: str) -> Exception: stderr = stderr.replace('\r\n', '\n') # fix line separators # check for the specific error messages first, then for generic SshHostKeyError for ssh_cls in [SshInvalidHostnameError, SshAuthenticationError, SshChangedHostKeyError, SshUnknownHostKeyError, SshHostKeyError]: match = ssh_cls.PATTERN.search(stderr) if match is not None: return ssh_cls(match, stderr) before, colon, after = stderr.rpartition(':') if colon and after: potential_strerror = after.strip() # DNS lookup errors if potential_strerror in gaierror_map: errnum = gaierror_map[potential_strerror] return socket.gaierror(errnum, stderr) # Network connect errors for errnum in errno.errorcode: if os.strerror(errnum) == potential_strerror: os_cls = oserror_subclass_map.get(errnum, OSError) return os_cls(errnum, stderr) # No match? Generic. return SshError(None, stderr) transport.py000064400000041017151116347500007160 0ustar00# ferny - asyncio SSH client library, using ssh(1) # # Copyright (C) 2023 Allison Karlitskaya # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import asyncio import contextlib import logging import typing from typing import Any, Callable, Iterable, Sequence, TypeVar from .interaction_agent import InteractionAgent, InteractionHandler, get_running_loop from .ssh_errors import get_exception_for_ssh_stderr logger = logging.getLogger(__name__) P = TypeVar('P', bound=asyncio.Protocol) class SubprocessError(Exception): returncode: int stderr: str def __init__(self, returncode: int, stderr: str) -> None: super().__init__(returncode, stderr) self.returncode = returncode self.stderr = stderr class FernyTransport(asyncio.Transport, asyncio.SubprocessProtocol): _agent: InteractionAgent _exec_task: 'asyncio.Task[tuple[asyncio.SubprocessTransport, FernyTransport]]' _is_ssh: bool _protocol: asyncio.Protocol _protocol_disconnected: bool = False # These get initialized in connection_made() and once set, never get unset. _subprocess_transport: 'asyncio.SubprocessTransport | None' = None _stdin_transport: 'asyncio.WriteTransport | None' = None _stdout_transport: 'asyncio.ReadTransport | None' = None # We record events that might build towards a connection termination here # and consider them from _consider_disconnect() in order to try to get the # best possible Exception for the protocol, rather than just taking the # first one (which is likely to be somewhat random). _exception: 'Exception | None' = None _stderr_output: 'str | None' = None _returncode: 'int | None' = None _transport_disconnected: bool = False _closed: bool = False @classmethod def spawn( cls: 'type[typing.Self]', protocol_factory: Callable[[], P], args: Sequence[str], loop: 'asyncio.AbstractEventLoop | None' = None, interaction_handlers: Sequence[InteractionHandler] = (), is_ssh: bool = True, **kwargs: Any ) -> 'tuple[typing.Self, P]': """Connects a FernyTransport to a protocol, using the given command. This spawns an external command and connects the stdin and stdout of the command to the protocol returned by the factory. An instance of ferny.InteractionAgent is created and attached to the stderr of the spawned process, using the provided handlers. It is the responsibility of the caller to ensure that: - a `ferny-askpass` client program is installed somewhere; and - any relevant command-line arguments or environment variables are passed correctly to the program to be spawned This function returns immediately and never raises exceptions, assuming all preconditions are met. If spawning the process fails then connection_lost() will be called with the relevant OSError, even before connection_made() is called. This is somewhat non-standard behaviour, but is the easiest way to report these errors without making this function async. Once the process is successfully executed, connection_made() will be called and the transport can be used as normal. connection_lost() will be called if the process exits or another error occurs. The return value of this function is the transport, but it exists in a semi-initialized state. You can call .close() on it, but nothing else. Once .connection_made() is called, you can call all the other functions. After you call this function, `.connection_lost()` will be called on your Protocol, exactly once, no matter what. Until that happens, you are responsible for holding a reference to the returned transport. :param args: the full argv of the command to spawn :param loop: the event loop to use. If none is provided, we use the one which is (read: must be) currently running. :param interaction_handlers: the handlers passed to the InteractionAgent :param is_ssh: whether we should attempt to interpret stderr as ssh error messages :param kwargs: anything else is passed through to `subprocess_exec()` :returns: the usual `(Transport, Protocol)` pair """ logger.debug('spawn(%r, %r, %r)', cls, protocol_factory, args) protocol = protocol_factory() self = cls(protocol) self._is_ssh = is_ssh if loop is None: loop = get_running_loop() self._agent = InteractionAgent(interaction_handlers, loop, self._interaction_completed) kwargs.setdefault('stderr', self._agent.fileno()) # As of Python 3.12 this isn't really asynchronous (since it uses the # subprocess module, which blocks while waiting for the exec() to # complete in the child), but we have to deal with the complication of # the async interface anyway. Since we, ourselves, want to export a # non-async interface, that means that we need a task here and a # bottom-half handler below. self._exec_task = loop.create_task(loop.subprocess_exec(lambda: self, *args, **kwargs)) def exec_completed(task: asyncio.Task) -> None: logger.debug('exec_completed(%r, %r)', self, task) assert task is self._exec_task try: transport, me = task.result() assert me is self logger.debug(' success.') except asyncio.CancelledError: return # in that case, do nothing except OSError as exc: logger.debug(' OSError %r', exc) self.close(exc) return # Our own .connection_made() handler should have gotten called by # now. Make sure everything got filled in properly. assert self._subprocess_transport is transport assert self._stdin_transport is not None assert self._stdout_transport is not None # Ask the InteractionAgent to start processing stderr. self._agent.start() self._exec_task.add_done_callback(exec_completed) return self, protocol def __init__(self, protocol: asyncio.Protocol) -> None: self._protocol = protocol def _consider_disconnect(self) -> None: logger.debug('_consider_disconnect(%r)', self) # We cannot disconnect as long as any of these three things are happening if not self._exec_task.done(): logger.debug(' exec_task still running %r', self._exec_task) return if self._subprocess_transport is not None and not self._transport_disconnected: logger.debug(' transport still connected %r', self._subprocess_transport) return if self._stderr_output is None: logger.debug(' agent still running') return # All conditions for disconnection are satisfied. if self._protocol_disconnected: logger.debug(' already disconnected') return self._protocol_disconnected = True # Now we just need to determine what we report to the protocol... if self._exception is not None: # If we got an exception reported, that's our reason for closing. logger.debug(' disconnect with exception %r', self._exception) self._protocol.connection_lost(self._exception) elif self._returncode == 0 or self._closed: # If we called close() or have a zero return status, that's a clean # exit, regardless of noise that might have landed in stderr. logger.debug(' clean disconnect') self._protocol.connection_lost(None) elif self._is_ssh and self._returncode == 255: # This is an error code due to an SSH failure. Try to interpret it. logger.debug(' disconnect with ssh error %r', self._stderr_output) self._protocol.connection_lost(get_exception_for_ssh_stderr(self._stderr_output)) else: # Otherwise, report the stderr text and return code. logger.debug(' disconnect with exit code %r, stderr %r', self._returncode, self._stderr_output) # We surely have _returncode set here, since otherwise: # - exec_task failed with an exception (which we handle above); or # - we're still connected... assert self._returncode is not None self._protocol.connection_lost(SubprocessError(self._returncode, self._stderr_output)) def _interaction_completed(self, future: 'asyncio.Future[str]') -> None: logger.debug('_interaction_completed(%r, %r)', self, future) try: self._stderr_output = future.result() logger.debug(' stderr: %r', self._stderr_output) except Exception as exc: logger.debug(' exception: %r', exc) self._stderr_output = '' # we need to set this in order to complete self.close(exc) self._consider_disconnect() # BaseProtocol implementation def connection_made(self, transport: asyncio.BaseTransport) -> None: logger.debug('connection_made(%r, %r)', self, transport) assert isinstance(transport, asyncio.SubprocessTransport) self._subprocess_transport = transport stdin_transport = transport.get_pipe_transport(0) assert isinstance(stdin_transport, asyncio.WriteTransport) self._stdin_transport = stdin_transport stdout_transport = transport.get_pipe_transport(1) assert isinstance(stdout_transport, asyncio.ReadTransport) self._stdout_transport = stdout_transport stderr_transport = transport.get_pipe_transport(2) assert stderr_transport is None logger.debug('calling connection_made(%r, %r)', self, self._protocol) self._protocol.connection_made(self) def connection_lost(self, exc: 'Exception | None') -> None: logger.debug('connection_lost(%r, %r)', self, exc) if self._exception is None: self._exception = exc self._transport_disconnected = True self._consider_disconnect() # SubprocessProtocol implementation def pipe_data_received(self, fd: int, data: bytes) -> None: logger.debug('pipe_data_received(%r, %r, %r)', self, fd, len(data)) assert fd == 1 # stderr is handled separately self._protocol.data_received(data) def pipe_connection_lost(self, fd: int, exc: 'Exception | None') -> None: logger.debug('pipe_connection_lost(%r, %r, %r)', self, fd, exc) assert fd in (0, 1) # stderr is handled separately # We treat this as a clean close if isinstance(exc, BrokenPipeError): exc = None # Record serious errors to propagate them to the protocol # If this is a clean exit on stdout, report an EOF if exc is not None: self.close(exc) elif fd == 1 and not self._closed: if not self._protocol.eof_received(): self.close() def process_exited(self) -> None: logger.debug('process_exited(%r)', self) assert self._subprocess_transport is not None self._returncode = self._subprocess_transport.get_returncode() logger.debug(' ._returncode = %r', self._returncode) self._agent.force_completion() def pause_writing(self) -> None: logger.debug('pause_writing(%r)', self) self._protocol.pause_writing() def resume_writing(self) -> None: logger.debug('resume_writing(%r)', self) self._protocol.resume_writing() # Transport implementation. Most of this is straight delegation. def close(self, exc: 'Exception | None' = None) -> None: logger.debug('close(%r, %r)', self, exc) self._closed = True if self._exception is None: logger.debug(' setting exception %r', exc) self._exception = exc if not self._exec_task.done(): logger.debug(' cancelling _exec_task') self._exec_task.cancel() if self._subprocess_transport is not None: logger.debug(' closing _subprocess_transport') # https://github.com/python/cpython/issues/112800 with contextlib.suppress(PermissionError): self._subprocess_transport.close() self._agent.force_completion() def is_closing(self) -> bool: assert self._subprocess_transport is not None return self._subprocess_transport.is_closing() def get_extra_info(self, name: str, default: object = None) -> object: assert self._subprocess_transport is not None return self._subprocess_transport.get_extra_info(name, default) def set_protocol(self, protocol: asyncio.BaseProtocol) -> None: assert isinstance(protocol, asyncio.Protocol) self._protocol = protocol def get_protocol(self) -> asyncio.Protocol: return self._protocol def is_reading(self) -> bool: assert self._stdout_transport is not None try: return self._stdout_transport.is_reading() except NotImplementedError: # This is (incorrectly) unimplemented before Python 3.11 return not self._stdout_transport._paused # type:ignore[attr-defined] except AttributeError: # ...and in Python 3.6 it's even worse try: selector = self._stdout_transport._loop._selector # type:ignore[attr-defined] selector.get_key(self._stdout_transport._fileno) # type:ignore[attr-defined] return True except KeyError: return False def pause_reading(self) -> None: assert self._stdout_transport is not None self._stdout_transport.pause_reading() def resume_reading(self) -> None: assert self._stdout_transport is not None self._stdout_transport.resume_reading() def abort(self) -> None: assert self._stdin_transport is not None assert self._subprocess_transport is not None self._stdin_transport.abort() self._subprocess_transport.kill() def can_write_eof(self) -> bool: assert self._stdin_transport is not None return self._stdin_transport.can_write_eof() # will always be True def get_write_buffer_size(self) -> int: assert self._stdin_transport is not None return self._stdin_transport.get_write_buffer_size() def get_write_buffer_limits(self) -> 'tuple[int, int]': assert self._stdin_transport is not None return self._stdin_transport.get_write_buffer_limits() def set_write_buffer_limits(self, high: 'int | None' = None, low: 'int | None' = None) -> None: assert self._stdin_transport is not None return self._stdin_transport.set_write_buffer_limits(high, low) def write(self, data: bytes) -> None: assert self._stdin_transport is not None return self._stdin_transport.write(data) def writelines(self, list_of_data: Iterable[bytes]) -> None: assert self._stdin_transport is not None return self._stdin_transport.writelines(list_of_data) def write_eof(self) -> None: assert self._stdin_transport is not None return self._stdin_transport.write_eof() # We don't really implement SubprocessTransport, but provide these as # "extras" to our user. def get_pid(self) -> int: assert self._subprocess_transport is not None return self._subprocess_transport.get_pid() def get_returncode(self) -> 'int | None': return self._returncode def kill(self) -> None: assert self._subprocess_transport is not None self._subprocess_transport.kill() def send_signal(self, number: int) -> None: assert self._subprocess_transport is not None self._subprocess_transport.send_signal(number) def terminate(self) -> None: assert self._subprocess_transport is not None self._subprocess_transport.terminate()