-1

In this minimal working example I'm trying to do the following:

  1. Prompt user for password
  2. Unmarshal JSON either from files specified as arguments or from STDIN

Here's the source code:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "os"
    "syscall"

    "golang.org/x/crypto/ssh/terminal"
)

const correctPassword = "secret"

func main() {
    args := os.Args[1:]

    var passwd string

    for {
        passwd = promptPassword()
        if passwd == correctPassword {
            log.Println("Correct password! Begin processing...")
            break
        }
        log.Println("Incorrect password!")
    }

    if len(args) == 0 { // Read from stdin
        log.Println("Reading from stdin")
        dec := json.NewDecoder(os.Stdin)
        for {
            var v interface{}
            if err := dec.Decode(&v); err == io.EOF {
                break
            } else if err != nil {
                log.Fatal(err)
            }
            log.Printf("%#v", v)
        }
    }

    for _, fileName := range args {
        log.Println("Reading from", fileName)
        f, err := os.Open(fileName)
        if err != nil {
            log.Println(err)
            continue
        }
        defer f.Close()
        dec := json.NewDecoder(f)
        for {
            var v interface{}
            if err := dec.Decode(&v); err == io.EOF {
                break
            } else if err != nil {
                log.Fatal(err)
            }
            log.Printf("%#v", v)
        }
    }
}

func promptPassword() (passwd string) {
    for {
        fmt.Fprintln(os.Stderr, "Enter password:")
        b, _ := terminal.ReadPassword(int(syscall.Stdin))
        passwd = string(b)
        if passwd != "" {
            break
        }
    }
    return passwd
}

Everything works all right except when already prepared data is piped or redirected (e.g. go run main.go < mydata.json, or echo 42 | go run main.go, etc).

When I pipe or redirect some data to the program, the data gets processed by the password prompt, not the JSON decoder part. Is there any way to at first prompt for the password, and only after process the incoming data?

I was trying to detect if there's any data in STDIN to read it and store in some temporary bytes slice, but I can't find how to close/truncate the STDIN, so it won't read data twice.

Petr Razumov
  • 1,952
  • 2
  • 17
  • 32
  • 6
    You can pass a file path in as an argument, but there's no way to have both interactive and piped stdin. Either stdin is attached to the terminal, or it's coming from a file. – Adrian Sep 24 '18 at 13:38
  • But wait, if data gets processed by the password prompt, why don't you save it as password variable, ask user the password then process it. Just change the location of your code blocks – atakanyenel Sep 26 '18 at 19:36

2 Answers2

1

Without any changes to your program, you can include password in stdin before json, eg (bash): {echo pass; cat data.json; } | goprog, or cat pass.txt data.json | goprog

For better method for password passing (eg environment or file descriptor) look at sshpass: https://linux.die.net/man/1/sshpass


You can also buffer all stdin, and reuse its content later (via io.Reader)


Redesign your application logic to function which accept io.Reader as source of data to unmarshall.

In main() pass os.Stdin to mentioned function if there is no file argument on command line, otherwise (try to) open file and pass it to unmarshalling function.


Note: for deciding whether to print prompt or not you may use isatty like function, which tells if stdin is interactive: https://github.com/mattn/go-isatty

Kokos
  • 442
  • 4
  • 12
0

You wont be able because the shell will close the stdin file descriptor once it has finished to write the content.

Once stdin is closed you cant re open it, this is controlled by the shell.

Check this program, to test this behavior

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    io.Copy(os.Stdout, os.Stdin)
    fmt.Println("done")
    some := make([]byte, 100)
    _, err := os.Stdin.Read(some)
    fmt.Println(err)
    fmt.Println(string(some))
}

The output will be

$ echo "some" | go run main.go 
some
done
EOF