Skip to content

Commit

Permalink
Handle ssh known hosts addition prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
gmlexx committed Jul 8, 2024
1 parent b004b2e commit e8fdf03
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 21 deletions.
22 changes: 12 additions & 10 deletions pkg/commands/oscommands/cmd_obj_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,19 +277,20 @@ func (self *cmdObjRunner) runAndStreamAux(
return nil
}

type CredentialType int
type InputType int

const (
Password CredentialType = iota
Password InputType = iota
Username
Passphrase
PIN
Token
Ack
)

// Whenever we're asked for a password we just enter a newline, which will
// eventually cause the command to fail.
var failPromptFn = func(CredentialType) <-chan string {
var failPromptFn = func(InputType) <-chan string {
ch := make(chan string)
go func() {
ch <- "\n"
Expand All @@ -306,7 +307,7 @@ func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
}

func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(CredentialType) <-chan string, error) {
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(InputType) <-chan string, error) {
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
return self.guiIO.promptForCredentialFn, nil
Expand All @@ -323,7 +324,7 @@ func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(Credential
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (self *cmdObjRunner) runAndDetectCredentialRequest(
cmdObj ICmdObj,
promptUserForCredential func(CredentialType) <-chan string,
promptUserForCredential func(InputType) <-chan string,
) error {
// setting the output to english so we can parse it for a username/password request
cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
Expand All @@ -340,7 +341,7 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
func (self *cmdObjRunner) processOutput(
reader io.Reader,
writer io.Writer,
promptUserForCredential func(CredentialType) <-chan string,
promptUserForCredential func(InputType) <-chan string,
task gocui.Task,
) {
checkForCredentialRequest := self.getCheckForCredentialRequestFunc()
Expand Down Expand Up @@ -368,19 +369,20 @@ func (self *cmdObjRunner) processOutput(
}

// having a function that returns a function because we need to maintain some state inbetween calls hence the closure
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (CredentialType, bool) {
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (InputType, bool) {
var ttyText strings.Builder
prompts := map[string]CredentialType{
prompts := map[string]InputType{
`Password:`: Password,
`.+'s password:`: Password,
`Password\s*for\s*'.+':`: Password,
`Username\s*for\s*'.+':`: Username,
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
`.*2FA Token.*`: Token,
`Are you sure you want to continue.*\?`: Ack,
}

compiledPrompts := map[*regexp.Regexp]CredentialType{}
compiledPrompts := map[*regexp.Regexp]InputType{}
for pattern, askFor := range prompts {
compiledPattern := regexp.MustCompile(pattern)
compiledPrompts[compiledPattern] = askFor
Expand All @@ -389,7 +391,7 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
newlineRegex := regexp.MustCompile("\n")

// this function takes each word of output from the command and builds up a string to see if we're being asked for a password
return func(newBytes []byte) (CredentialType, bool) {
return func(newBytes []byte) (InputType, bool) {
_, err := ttyText.Write(newBytes)
if err != nil {
self.log.Error(err)
Expand Down
18 changes: 13 additions & 5 deletions pkg/commands/oscommands/cmd_obj_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ func getRunner() *cmdObjRunner {
}
}

func toChanFn(f func(ct CredentialType) string) func(CredentialType) <-chan string {
return func(ct CredentialType) <-chan string {
func toChanFn(f func(ct InputType) string) func(InputType) <-chan string {
return func(ct InputType) <-chan string {
ch := make(chan string)

go func() {
Expand All @@ -29,7 +29,7 @@ func toChanFn(f func(ct CredentialType) string) func(CredentialType) <-chan stri
}

func TestProcessOutput(t *testing.T) {
defaultPromptUserForCredential := func(ct CredentialType) string {
defaultPromptUserForCredential := func(ct InputType) string {
switch ct {
case Password:
return "password"
Expand All @@ -41,14 +41,16 @@ func TestProcessOutput(t *testing.T) {
return "pin"
case Token:
return "token"
case Ack:
return "acknowledgement"
default:
panic("unexpected credential type")
}
}

scenarios := []struct {
name string
promptUserForCredential func(CredentialType) string
promptUserForCredential func(InputType) string
output string
expectedToWrite string
}{
Expand Down Expand Up @@ -106,9 +108,15 @@ func TestProcessOutput(t *testing.T) {
output: "Password:\nUsername for 'Alice':\n",
expectedToWrite: "passwordusername",
},
{
name: "ssh known hosts fingerprint acknowledgement",
promptUserForCredential: defaultPromptUserForCredential,
output: "Are you sure you want to continue connecting (yes/no/[fingerprint])?:\n",
expectedToWrite: "acknowledgement",
},
{
name: "user submits empty credential",
promptUserForCredential: func(ct CredentialType) string { return "" },
promptUserForCredential: func(ct InputType) string { return "" },
output: "Password:\n",
expectedToWrite: "",
},
Expand Down
4 changes: 2 additions & 2 deletions pkg/commands/oscommands/gui_io.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ type guiIO struct {
// this allows us to request info from the user like username/password, in the event
// that a command requests it.
// the 'credential' arg is something like 'username' or 'password'
promptForCredentialFn func(credential CredentialType) <-chan string
promptForCredentialFn func(credential InputType) <-chan string
}

func NewGuiIO(
log *logrus.Entry,
logCommandFn func(string, bool),
newCmdWriterFn func() io.Writer,
promptForCredentialFn func(CredentialType) <-chan string,
promptForCredentialFn func(InputType) <-chan string,
) *guiIO {
return &guiIO{
log: log,
Expand Down
29 changes: 26 additions & 3 deletions pkg/gui/controllers/helpers/credentials_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,32 @@ func NewCredentialsHelper(
// We return a channel rather than returning the string directly so that the calling function knows
// when the prompt has been created (before the user has entered anything) so that it can
// note that we're now waiting on user input and lazygit isn't processing anything.
func (self *CredentialsHelper) PromptUserForCredential(passOrUname oscommands.CredentialType) <-chan string {
func (self *CredentialsHelper) PromptUserForInput(inputType oscommands.InputType) <-chan string {
ch := make(chan string)

self.c.OnUIThread(func() error {
title, mask := self.getTitleAndMask(passOrUname)
if inputType == oscommands.Ack {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.CommandLog + ": " + self.c.Tr.Actions.AckToContinue,
Items: []*types.MenuItem{
{
Label: self.c.Tr.Yes,
OnPress: func() error {
ch <- "yes" + "\n"
return nil
},
},
{
Label: self.c.Tr.No,
OnPress: func() error {
ch <- "no" + "\n"
return nil
},
},
},
})
}
title, mask := self.getTitleAndMask(inputType)

return self.c.Prompt(types.PromptOpts{
Title: title,
Expand All @@ -46,7 +67,7 @@ func (self *CredentialsHelper) PromptUserForCredential(passOrUname oscommands.Cr
return ch
}

func (self *CredentialsHelper) getTitleAndMask(passOrUname oscommands.CredentialType) (string, bool) {
func (self *CredentialsHelper) getTitleAndMask(passOrUname oscommands.InputType) (string, bool) {
switch passOrUname {
case oscommands.Username:
return self.c.Tr.CredentialsUsername, false
Expand All @@ -58,6 +79,8 @@ func (self *CredentialsHelper) getTitleAndMask(passOrUname oscommands.Credential
return self.c.Tr.CredentialsPIN, true
case oscommands.Token:
return self.c.Tr.CredentialsToken, true
case oscommands.Ack:
return self.c.Tr.Actions.AckToContinue, false
}

// should never land here
Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ func NewGui(
cmn.Log,
gui.LogCommand,
gui.getCmdWriter,
credentialsHelper.PromptUserForCredential,
credentialsHelper.PromptUserForInput,
)

osCommand := oscommands.NewOSCommand(cmn, config, oscommands.GetPlatform(), guiIO)
Expand Down
6 changes: 6 additions & 0 deletions pkg/i18n/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Todo list when making a new translation
package i18n

type TranslationSet struct {
Yes string
No string
NotEnoughSpace string
DiffTitle string
FilesTitle string
Expand Down Expand Up @@ -946,6 +948,7 @@ type Actions struct {
BisectMark string
RemoveWorktree string
AddWorktree string
AckToContinue string
}

const englishIntroPopupMessage = `
Expand Down Expand Up @@ -983,6 +986,8 @@ for up-to-date information how to configure your editor.
// exporting this so we can use it in tests
func EnglishTranslationSet() *TranslationSet {
return &TranslationSet{
Yes: "Yes",
No: "No",
NotEnoughSpace: "Not enough space to render panels",
DiffTitle: "Diff",
FilesTitle: "Files",
Expand Down Expand Up @@ -1884,6 +1889,7 @@ func EnglishTranslationSet() *TranslationSet {
BisectMark: "Bisect mark",
RemoveWorktree: "Remove worktree",
AddWorktree: "Add worktree",
AckToContinue: "Are you sure you want to continue?",
},
Bisect: Bisect{
Mark: "Mark current commit (%s) as %s",
Expand Down

0 comments on commit e8fdf03

Please sign in to comment.