From 36c9db6d8a8921c2e361e18980726ace2282394c Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Sat, 23 Jan 2016 15:49:39 -0700 Subject: [PATCH 01/13] accept a script from stdin --- main.go | 30 +++++++++++++++++++++++++----- script.go | 6 +++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index dd0dd46..7aebe6d 100644 --- a/main.go +++ b/main.go @@ -52,11 +52,6 @@ func main() { ) flag.Parse() - if len(flag.Args()) < 1 { - flag.Usage() - log.Panic("No script specified") - } - if _, err := os.Stat(*target); os.IsNotExist(err) { log.Panic("Script executable does not exist") } @@ -125,6 +120,10 @@ func parseToken(pattern string, reader io.Reader) string { // getFile tries to find location locally first, then tries remote func getFile(location string) (io.ReadCloser, error) { + if location == "" { + return getFromStdin() + } + body, err := getLocal(location) if err == nil { return body, nil @@ -133,6 +132,27 @@ func getFile(location string) (io.ReadCloser, error) { return getRemote(location) } +func getFromStdin() (io.ReadCloser, error) { + stat, err := os.Stdin.Stat() + if err != nil { + return nil, err + } + + if (stat.Mode() & os.ModeCharDevice) != 0 { + return nil, errors.New("No data in STDIN") + } + + return os.Stdin, nil + // return ioutil.NopCloser(bufio.NewReader(os.Stdin)), nil + + // body, err := ioutil.ReadAll(os.Stdin) + // if err != nil { + // return nil, err + // } + + // return ioutil.NopCloser(bytes.NewReader(body)), nil +} + func getRemote(location string) (io.ReadCloser, error) { parsed, err := url.Parse(location) if err != nil || parsed.Scheme == "" { diff --git a/script.go b/script.go index 3171ff1..669a705 100644 --- a/script.go +++ b/script.go @@ -143,7 +143,11 @@ func (s *Script) Author() (string, error) { // additional arguments from the command line. It returns the result of the // process. func (s Script) Run(target string, args ...string) error { - args[0] = s.Name() + if s.source == "" { + args = append([]string{s.Name()}, args...) + } else { + args[0] = s.Name() + } cmd := exec.Command(target, args...) cmd.Stdout = os.Stdout From 6c054f2f0fab333cbc48640ffd79cbf163159354 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Wed, 2 Mar 2016 21:38:47 -0700 Subject: [PATCH 02/13] ignore --target and --inspect for piped input If the script is coming from STDIN, ignore the --target executable and finish by echoing the contents instead. Also disable --inspect to avoid tangling the pipes. --- main.go | 23 ++++++++++++++++------- script.go | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 7aebe6d..927b8f7 100644 --- a/main.go +++ b/main.go @@ -52,11 +52,6 @@ func main() { ) flag.Parse() - if _, err := os.Stat(*target); os.IsNotExist(err) { - log.Panic("Script executable does not exist") - } - log.Println("Using script executable", *target) - // download the script, store it someplace temporary script, err := NewScript(flag.Arg(0)) if err != nil { @@ -65,6 +60,15 @@ func main() { defer os.Remove(script.Name()) log.Println("Script saved to", script.Name()) + // if we're not reading from a pipe we need a target executable + if !script.IsPiped() { + if _, err := os.Stat(*target); os.IsNotExist(err) { + log.Panic("Script executable does not exist") + } + + log.Println("Using script executable", *target) + } + // let the user look at it if they want if cont := script.Inspect(*inspect, *editor); !cont { log.Panic("Exiting without running", script.Name()) @@ -77,11 +81,13 @@ func main() { log.Panic(err) } + // todo force local keyring for piped input service, err := makeService(*serviceName) if err != nil { log.Panic(err) } + // todo this needs to only return one match for piped input key, err := lookup.Key(service, author) if err != nil { log.Panic(err) @@ -98,8 +104,11 @@ func main() { } // run the script - log.Println("Running", script.Name(), "with", *target) - script.Run(*target, flag.Args()...) + if script.IsPiped() { + script.Echo() + } else { + script.Run(*target, flag.Args()...) + } } func parseToken(pattern string, reader io.Reader) string { diff --git a/script.go b/script.go index 669a705..56c705a 100644 --- a/script.go +++ b/script.go @@ -102,6 +102,10 @@ func (s *Script) detachSignature(contents []byte) ([]byte, error) { return contents, nil } +func (s Script) IsPiped() bool { + return s.source == "" +} + // Name is the name of the temporary file holding the shell script. func (s Script) Name() string { return s.filename @@ -143,6 +147,8 @@ func (s *Script) Author() (string, error) { // additional arguments from the command line. It returns the result of the // process. func (s Script) Run(target string, args ...string) error { + log.Println("Running", s.Name(), "with", target) + if s.source == "" { args = append([]string{s.Name()}, args...) } else { @@ -155,11 +161,19 @@ func (s Script) Run(target string, args ...string) error { return cmd.Run() } +func (s Script) Echo() { + log.Println("Sending", s.Name(), "to STDOUT for more processing") + body, _ := s.Body() + defer body.Close() + + io.Copy(os.Stdout, body) +} + // Inspect checks whether an inspection was requested, and sends Script.Name() // to editor if so. When editor exits, Inspect prompts the user to continue // processing, and returns true to continue or false to stop. func (s Script) Inspect(inspect bool, editor string) bool { - if !inspect { + if !inspect || s.IsPiped() { return true } From eb57f2c5a77a55d417dfe87f047b5799290b5642 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Thu, 3 Mar 2016 20:20:07 -0700 Subject: [PATCH 03/13] when the script is piped in, force author lookup from the local keyring --- main.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 927b8f7..ceb17f3 100644 --- a/main.go +++ b/main.go @@ -81,8 +81,7 @@ func main() { log.Panic(err) } - // todo force local keyring for piped input - service, err := makeService(*serviceName) + service, err := makeService(*serviceName, script.IsPiped()) if err != nil { log.Panic(err) } @@ -184,7 +183,12 @@ func getLocal(location string) (io.ReadCloser, error) { return os.Open(location) } -func makeService(name string) (lookup.KeyService, error) { +func makeService(name string, fromPipe bool) (lookup.KeyService, error) { + // force the local keyring when reading the script from a pipe + if fromPipe { + name = "local" + } + switch name { case "keybase": return &lookup.KeybaseService{}, nil From 20c4a032a7c84a66165061232e9918b67b2788b5 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Thu, 3 Mar 2016 20:20:58 -0700 Subject: [PATCH 04/13] when the script is piped, automatically choose the author match If there's more than one match (or no matches), bail with an error. Piped input means we don't get to be interactive. --- lookup/lookup.go | 24 ++++++++++++++++++++---- main.go | 3 +-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lookup/lookup.go b/lookup/lookup.go index 8e0ab18..ed4b72f 100644 --- a/lookup/lookup.go +++ b/lookup/lookup.go @@ -101,10 +101,20 @@ func chooseMatch(matches []User) (User, error) { return matches[n], nil } +func chooseSingleMatch(matches []User) (User, error) { + if len(matches) != 1 { + return User{}, fmt.Errorf("Found %d author matches; need exactly 1 when reading from STDIN", len(matches)) + } + + return matches[0], nil +} + // Key looks up an author query in the provided KeyService, and prompts for a -// choice of matches. It returns an error if no matches were found, if no match -// was chosen, or if no PGP public was found. -func Key(service KeyService, query string) (openpgp.KeyRing, error) { +// choice of matches (if single is false) or automatically chooses the matched +// user when there is one and only one match (if single is true). It returns an +// error if no matches were found, if no match was chosen, or if no PGP public +// was found. +func Key(service KeyService, query string, single bool) (openpgp.KeyRing, error) { // get possible matches from the key service matches, err := service.Matches(query) if err != nil { @@ -113,7 +123,13 @@ func Key(service KeyService, query string) (openpgp.KeyRing, error) { // verify that the author is who the user was expecting by showing all the // details (twitter handle, github handle, websites, etc.) - match, err := chooseMatch(matches) + var match User + if single { + match, err = chooseSingleMatch(matches) + } else { + match, err = chooseMatch(matches) + } + if err != nil { return nil, err } diff --git a/main.go b/main.go index ceb17f3..78cea55 100644 --- a/main.go +++ b/main.go @@ -86,8 +86,7 @@ func main() { log.Panic(err) } - // todo this needs to only return one match for piped input - key, err := lookup.Key(service, author) + key, err := lookup.Key(service, author, script.IsPiped()) if err != nil { log.Panic(err) } From 1cc25608b1e2b603fcf247feb7760190834a9260 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Thu, 3 Mar 2016 20:22:34 -0700 Subject: [PATCH 05/13] clean up comments --- main.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/main.go b/main.go index 78cea55..0c21bd7 100644 --- a/main.go +++ b/main.go @@ -145,19 +145,12 @@ func getFromStdin() (io.ReadCloser, error) { return nil, err } + // could also use os.ModeNamedPipe here? not sure if the difference matters if (stat.Mode() & os.ModeCharDevice) != 0 { return nil, errors.New("No data in STDIN") } return os.Stdin, nil - // return ioutil.NopCloser(bufio.NewReader(os.Stdin)), nil - - // body, err := ioutil.ReadAll(os.Stdin) - // if err != nil { - // return nil, err - // } - - // return ioutil.NopCloser(bytes.NewReader(body)), nil } func getRemote(location string) (io.ReadCloser, error) { From 5d534bf36f28489e919800c0e934eff1e576a621 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Thu, 3 Mar 2016 20:25:24 -0700 Subject: [PATCH 06/13] don't need to check for the script source in Run Run only gets called when the script is specified on the command line (not piped in). --- script.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/script.go b/script.go index 56c705a..a381b77 100644 --- a/script.go +++ b/script.go @@ -149,11 +149,9 @@ func (s *Script) Author() (string, error) { func (s Script) Run(target string, args ...string) error { log.Println("Running", s.Name(), "with", target) - if s.source == "" { - args = append([]string{s.Name()}, args...) - } else { - args[0] = s.Name() - } + // the first argument is the script source location. replace it with the + // temporary filename. + args[0] = s.Name() cmd := exec.Command(target, args...) cmd.Stdout = os.Stdout From fb4330b3ab957546e537ff14588cecd8c3e10aa2 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Thu, 3 Mar 2016 21:06:50 -0700 Subject: [PATCH 07/13] move the KeyService factory to lookup, and add some tests --- lookup/lookup.go | 18 +++++++++++ lookup/lookup_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++ main.go | 18 +---------- 3 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 lookup/lookup_test.go diff --git a/lookup/lookup.go b/lookup/lookup.go index ed4b72f..8af7f49 100644 --- a/lookup/lookup.go +++ b/lookup/lookup.go @@ -69,6 +69,24 @@ func (u User) String() string { return s } +// NewKeyService creates the KeyService implementation requested by name. If +// fromPipe is true, it creates a LocalPGPService type. +func NewKeyService(name string, fromPipe bool) (KeyService, error) { + // force the local keyring when reading the script from a pipe + if fromPipe { + name = "local" + } + + switch name { + case "keybase": + return &KeybaseService{}, nil + case "local": + return NewLocalPGPService() + } + + return nil, errors.New("Unrecognized key service") +} + // chooseMatch prints all the matches provided, prompts for a choice, and // returns the chosen match. func chooseMatch(matches []User) (User, error) { diff --git a/lookup/lookup_test.go b/lookup/lookup_test.go new file mode 100644 index 0000000..a887222 --- /dev/null +++ b/lookup/lookup_test.go @@ -0,0 +1,72 @@ +/* +pipethis: Stop piping the internet into your shell +Copyright 2016 Ellotheth + +Use of this source code is governed by the GNU Public License version 2 +(GPLv2). You should have received a copy of the GPLv2 along with your copy of +the source. If not, see http://www.gnu.org/licenses/gpl-2.0.html. +*/ + +package lookup + +import ( + "os" + "testing" + + "github.com/stretchr/testify/suite" +) + +type LookupTest struct { + home string + suite.Suite +} + +func (s *LookupTest) SetupSuite() { + s.home = os.Getenv("HOME") + os.Setenv("HOME", os.TempDir()) +} + +func (s *LookupTest) TeardownSuite() { + os.Setenv("HOME", s.home) +} + +func (s *LookupTest) TestNewKeyServiceAcceptsKeybaseWithoutPipe() { + service, err := NewKeyService("keybase", false) + + s.NoError(err) + s.IsType(&KeybaseService{}, service) +} + +func (s *LookupTest) TestNewKeyServiceForcesLocalWithPipe() { + _, err := NewKeyService("keybase", true) + s.Error(err) + + perr, ok := err.(*os.PathError) + s.True(ok) + s.Equal("/tmp/.gnupg/pubring.gpg", perr.Path) +} + +func (s *LookupTest) TestChooseSingleMatchBailsWithoutMatches() { + user, err := chooseSingleMatch([]User{}) + + s.Error(err) + s.Equal(User{}, user) +} + +func (s *LookupTest) TestChooseSingleMatchBailsWithMoreThanOneMatch() { + user, err := chooseSingleMatch([]User{User{}, User{}}) + + s.Error(err) + s.Equal(User{}, user) +} + +func (s *LookupTest) TestChooseSingleMatchReturnsSingleMatch() { + user, err := chooseSingleMatch([]User{User{Username: "foo"}}) + + s.NoError(err) + s.Equal("foo", user.Username) +} + +func TestLookupTest(t *testing.T) { + suite.Run(t, new(LookupTest)) +} diff --git a/main.go b/main.go index 0c21bd7..99d2073 100644 --- a/main.go +++ b/main.go @@ -81,7 +81,7 @@ func main() { log.Panic(err) } - service, err := makeService(*serviceName, script.IsPiped()) + service, err := lookup.NewKeyService(*serviceName, script.IsPiped()) if err != nil { log.Panic(err) } @@ -174,19 +174,3 @@ func getLocal(location string) (io.ReadCloser, error) { return os.Open(location) } - -func makeService(name string, fromPipe bool) (lookup.KeyService, error) { - // force the local keyring when reading the script from a pipe - if fromPipe { - name = "local" - } - - switch name { - case "keybase": - return &lookup.KeybaseService{}, nil - case "local": - return lookup.NewLocalPGPService() - } - - return nil, errors.New("Unrecognized key service") -} From 29ea90c6da1c1509fb71dc5de8b7cd4c53e16950 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Thu, 3 Mar 2016 21:14:36 -0700 Subject: [PATCH 08/13] add missing comments --- script.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script.go b/script.go index a381b77..de5cd8e 100644 --- a/script.go +++ b/script.go @@ -102,6 +102,8 @@ func (s *Script) detachSignature(contents []byte) ([]byte, error) { return contents, nil } +// IsPiped is true when the script was read from STDIN (so the source location +// is empty) func (s Script) IsPiped() bool { return s.source == "" } @@ -159,6 +161,7 @@ func (s Script) Run(target string, args ...string) error { return cmd.Run() } +// Echo prints the contents of the script to STDOUT func (s Script) Echo() { log.Println("Sending", s.Name(), "to STDOUT for more processing") body, _ := s.Body() From 2e7792d301e62c9d79d69d9e82ae15e0df29839d Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Thu, 3 Mar 2016 21:19:54 -0700 Subject: [PATCH 09/13] handle leftover errors properly --- main.go | 7 +++++-- script.go | 21 +++++++++++++++++---- signature.go | 5 ++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 99d2073..6f873d5 100644 --- a/main.go +++ b/main.go @@ -103,9 +103,12 @@ func main() { // run the script if script.IsPiped() { - script.Echo() + err = script.Echo() } else { - script.Run(*target, flag.Args()...) + err = script.Run(*target, flag.Args()...) + } + if err != nil { + log.Panic(err) } } diff --git a/script.go b/script.go index de5cd8e..9c56cda 100644 --- a/script.go +++ b/script.go @@ -97,7 +97,11 @@ func (s *Script) detachSignature(contents []byte) ([]byte, error) { return nil, err } defer sigWriter.Close() - io.Copy(sigWriter, block.ArmoredSignature.Body) + + _, err = io.Copy(sigWriter, block.ArmoredSignature.Body) + if err != nil { + return nil, err + } return contents, nil } @@ -162,12 +166,21 @@ func (s Script) Run(target string, args ...string) error { } // Echo prints the contents of the script to STDOUT -func (s Script) Echo() { +func (s Script) Echo() error { log.Println("Sending", s.Name(), "to STDOUT for more processing") - body, _ := s.Body() + + body, err := s.Body() + if err != nil { + return err + } defer body.Close() - io.Copy(os.Stdout, body) + _, err = io.Copy(os.Stdout, body) + if err != nil { + return err + } + + return nil } // Inspect checks whether an inspection was requested, and sends Script.Name() diff --git a/signature.go b/signature.go index fabe142..235b01e 100644 --- a/signature.go +++ b/signature.go @@ -74,7 +74,10 @@ func (s *Signature) Download() error { } defer file.Close() - io.Copy(file, body) + _, err = io.Copy(file, body) + if err != nil { + return nil + } return nil } From d0f04ff179ab97e3ac4b329493232dc3ab98422b Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Sat, 19 Mar 2016 10:13:56 -0600 Subject: [PATCH 10/13] clean up some log messages Matches from the local keyring won't have usernames, and attached signatures won't have a Source(). --- lookup/lookup.go | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lookup/lookup.go b/lookup/lookup.go index 8af7f49..b04fa9a 100644 --- a/lookup/lookup.go +++ b/lookup/lookup.go @@ -157,7 +157,7 @@ func Key(service KeyService, query string, single bool) (openpgp.KeyRing, error) if err != nil { return nil, err } - log.Printf("Using %v (%v)", match.Username, ring[0].PrimaryKey.KeyIdShortString()) + log.Printf("Verifying your script against\n%v", match) return ring, nil } diff --git a/main.go b/main.go index 6f873d5..c89b34e 100644 --- a/main.go +++ b/main.go @@ -98,7 +98,7 @@ func main() { log.Panic(err) } - log.Println("Signature", signature.Source(), "verified!") + log.Println("Signature verified!") } // run the script From 539631dfd1ed67915dbcf79b824b9b9922570690 Mon Sep 17 00:00:00 2001 From: Gemma Lynn Date: Sat, 19 Mar 2016 11:25:32 -0600 Subject: [PATCH 11/13] use better error messages for missing signatures --- main.go | 3 ++- script.go | 6 ++++++ signature.go | 10 +++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index c89b34e..e1b9a00 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ func main() { // still happen. defer func() { if r := recover(); r != nil { + flag.Usage() os.Exit(1) } }() @@ -150,7 +151,7 @@ func getFromStdin() (io.ReadCloser, error) { // could also use os.ModeNamedPipe here? not sure if the difference matters if (stat.Mode() & os.ModeCharDevice) != 0 { - return nil, errors.New("No data in STDIN") + return nil, errors.New("Nothing to read from STDIN and no script given") } return os.Stdin, nil diff --git a/script.go b/script.go index 9c56cda..6b04fee 100644 --- a/script.go +++ b/script.go @@ -205,3 +205,9 @@ func (s Script) Inspect(inspect bool, editor string) bool { return strings.ToLower(runScript) == "y" } + +// IsClearsigned returns true if the script and signature are attached, +// and false otherwise. +func (s Script) IsClearsigned() bool { + return s.clearsigned +} diff --git a/signature.go b/signature.go index 235b01e..ed6caa7 100644 --- a/signature.go +++ b/signature.go @@ -42,7 +42,7 @@ func (s Signature) Name() string { // Source is the original location of the signature file. It defaults to //