In this minimal working example I'm trying to do the following:
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.
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
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