diff --git a/go.mod b/go.mod index ba216e7..a5fc715 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module golang.org/x/mod require golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 -go 1.13 +go 1.12 diff --git a/gosumcheck/main.go b/gosumcheck/main.go new file mode 100644 index 0000000..1e92f1f --- /dev/null +++ b/gosumcheck/main.go @@ -0,0 +1,213 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Gosumcheck checks a go.sum file against a go.sum database server. +// +// Usage: +// +// gosumcheck [-h H] [-k key] [-u url] [-v] go.sum +// +// The -h flag changes the tile height (default 8). +// +// The -k flag changes the go.sum database server key. +// +// The -u flag overrides the URL of the server (usually set from the key name). +// +// The -v flag enables verbose output. +// In particular, it causes gosumcheck to report +// the URL and elapsed time for each server request. +// +// WARNING! WARNING! WARNING! +// +// Gosumcheck is meant as a proof of concept demo and should not be +// used in production scripts or continuous integration testing. +// It does not cache any downloaded information from run to run, +// making it expensive and also keeping it from detecting server +// misbehavior or successful HTTPS man-in-the-middle timeline forks. +// +// To discourage misuse in automated settings, gosumcheck does not +// set any exit status to report whether any problems were found. +// +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "strings" + "sync" + "time" + + "golang.org/x/mod/sumdb" +) + +func usage() { + fmt.Fprintf(os.Stderr, "usage: gosumcheck [-h H] [-k key] [-u url] [-v] go.sum...\n") + os.Exit(2) +} + +var ( + height = flag.Int("h", 8, "tile height") + vkey = flag.String("k", "sum.golang.org+033de0ae+Ac4zctda0e5eza+HJyk9SxEdh+s3Ux18htTTAD8OuAn8", "key") + url = flag.String("u", "", "url to server (overriding name)") + vflag = flag.Bool("v", false, "enable verbose output") +) + +func main() { + log.SetPrefix("notecheck: ") + log.SetFlags(0) + + flag.Usage = usage + flag.Parse() + if flag.NArg() < 1 { + usage() + } + + client := sumdb.NewClient(new(clientOps)) + + // Look in environment explicitly, so that if 'go env' is old and + // doesn't know about GONOSUMDB, we at least get anything + // set in the environment. + env := os.Getenv("GONOSUMDB") + if env == "" { + out, err := exec.Command("go", "env", "GONOSUMDB").CombinedOutput() + if err != nil { + log.Fatalf("go env GONOSUMDB: %v\n%s", err, out) + } + env = strings.TrimSpace(string(out)) + } + client.SetGONOSUMDB(env) + + for _, arg := range flag.Args() { + data, err := ioutil.ReadFile(arg) + if err != nil { + log.Fatal(err) + } + checkGoSum(client, arg, data) + } +} + +func checkGoSum(client *sumdb.Client, name string, data []byte) { + lines := strings.Split(string(data), "\n") + if lines[len(lines)-1] != "" { + log.Printf("error: final line missing newline") + return + } + lines = lines[:len(lines)-1] + + errs := make([]string, len(lines)) + var wg sync.WaitGroup + for i, line := range lines { + wg.Add(1) + go func(i int, line string) { + defer wg.Done() + f := strings.Fields(line) + if len(f) != 3 { + errs[i] = "invalid number of fields" + return + } + + dbLines, err := client.Lookup(f[0], f[1]) + if err != nil { + if err == sumdb.ErrGONOSUMDB { + errs[i] = fmt.Sprintf("%s@%s: %v", f[0], f[1], err) + } else { + // Otherwise Lookup properly adds the prefix itself. + errs[i] = err.Error() + } + return + } + hashAlgPrefix := f[0] + " " + f[1] + " " + f[2][:strings.Index(f[2], ":")+1] + for _, dbLine := range dbLines { + if dbLine == line { + return + } + if strings.HasPrefix(dbLine, hashAlgPrefix) { + errs[i] = fmt.Sprintf("%s@%s hash mismatch: have %s, want %s", f[0], f[1], line, dbLine) + return + } + } + errs[i] = fmt.Sprintf("%s@%s hash algorithm mismatch: have %s, want one of:\n\t%s", f[0], f[1], line, strings.Join(dbLines, "\n\t")) + }(i, line) + } + wg.Wait() + + for i, err := range errs { + if err != "" { + fmt.Printf("%s:%d: %s\n", name, i+1, err) + } + } +} + +type clientOps struct{} + +func (*clientOps) ReadConfig(file string) ([]byte, error) { + if file == "key" { + return []byte(*vkey), nil + } + if strings.HasSuffix(file, "/latest") { + // Looking for cached latest tree head. + // Empty result means empty tree. + return []byte{}, nil + } + return nil, fmt.Errorf("unknown config %s", file) +} + +func (*clientOps) WriteConfig(file string, old, new []byte) error { + // Ignore writes. + return nil +} + +func (*clientOps) ReadCache(file string) ([]byte, error) { + return nil, fmt.Errorf("no cache") +} + +func (*clientOps) WriteCache(file string, data []byte) { + // Ignore writes. +} + +func (*clientOps) Log(msg string) { + log.Print(msg) +} + +func (*clientOps) SecurityError(msg string) { + log.Fatal(msg) +} + +func init() { + http.DefaultClient.Timeout = 1 * time.Minute +} + +func (*clientOps) ReadRemote(path string) ([]byte, error) { + name := *vkey + if i := strings.Index(name, "+"); i >= 0 { + name = name[:i] + } + start := time.Now() + target := "https://" + name + path + if *url != "" { + target = *url + path + } + resp, err := http.Get(target) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("GET %v: %v", target, resp.Status) + } + data, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, err + } + if *vflag { + fmt.Fprintf(os.Stderr, "%.3fs %s\n", time.Since(start).Seconds(), target) + } + return data, nil +} diff --git a/gosumcheck/test.bash b/gosumcheck/test.bash new file mode 100755 index 0000000..2a4b2af --- /dev/null +++ b/gosumcheck/test.bash @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e +go build -o gosumcheck.exe +export GONOSUMDB=*/text # rsc.io/text but not golang.org/x/text +./gosumcheck.exe "$@" -v test.sum +rm -f ./gosumcheck.exe +echo PASS diff --git a/gosumcheck/test.sum b/gosumcheck/test.sum new file mode 100644 index 0000000..1b252ff --- /dev/null +++ b/gosumcheck/test.sum @@ -0,0 +1,6 @@ +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= +rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= +rsc.io/text v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=