diff --git a/cmd/sf/update.go b/cmd/sf/update.go index 17b693b9..748b13ef 100644 --- a/cmd/sf/update.go +++ b/cmd/sf/update.go @@ -22,8 +22,8 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/fs" - "io/ioutil" "net/http" "os" "path/filepath" @@ -34,6 +34,8 @@ import ( "github.com/richardlehane/siegfried/pkg/config" ) +type Updates []Update + type Update struct { Version [3]int `json:"sf"` Created string `json:"created"` @@ -74,7 +76,7 @@ func same(buf []byte, usize int, uhash string) bool { } func uptodate(utime, uhash string, usize int) bool { - fbuf, err := ioutil.ReadFile(config.Signature()) + fbuf, err := os.ReadFile(config.Signature()) if err != nil { return false } @@ -98,21 +100,31 @@ func location(base, sig string, args []string) string { } func updateSigs(sig string, args []string) (bool, string, error) { + return updateSigsDo(sig, args, getHttp) +} + +func updateSigsDo(sig string, args []string, gf getHttpFn) (bool, string, error) { url, _, _ := config.UpdateOptions() if url == "" { return false, "Update is not available for this distribution of siegfried", nil } - response, err := getHttp(location(url, sig, args)) + response, err := gf(location(url, sig, args)) if err != nil { return false, "", err } - var u Update - if err := json.Unmarshal(response, &u); err != nil { + var us Updates + if err := json.Unmarshal(response, &us); err != nil { return false, "", err } version := config.Version() - if version[0] < u.Version[0] || (version[0] == u.Version[0] && version[1] < u.Version[1]) || // if the version is out of date - u.Version == [3]int{0, 0, 0} || u.Created == "" || u.Size == 0 || u.Path == "" { // or if the unmarshalling hasn't worked and we have blank values + var u Update + for _, v := range us { + if version[0] == v.Version[0] && version[1] == v.Version[1] { + u = v + break + } + } + if u.Version == [3]int{0, 0, 0} { // we didn't find an eligible update return false, "Your version of siegfried is out of date; please install latest from http://www.itforarchivists.com/siegfried before continuing.", nil } if uptodate(u.Created, u.Hash, u.Size) { @@ -123,28 +135,30 @@ func updateSigs(sig string, args []string) (bool, string, error) { if errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(config.Home(), os.ModePerm) if err != nil { - return false, "", fmt.Errorf("Siegfried: cannot create home directory %s, %v", config.Home(), err) + return false, "", fmt.Errorf("siegfried: cannot create home directory %s, %v", config.Home(), err) } } else { - return false, "", fmt.Errorf("Siegfried: error opening directory %s, %v", config.Home(), err) + return false, "", fmt.Errorf("siegfried: error opening directory %s, %v", config.Home(), err) } } fmt.Println("... downloading latest signature file ...") - response, err = getHttp(u.Path) + response, err = gf(u.Path) if err != nil { - return false, "", fmt.Errorf("Siegfried: error retrieving %s.\nThis may be a network or firewall issue. See https://github.com/richardlehane/siegfried/wiki/Getting-started for manual instructions.\nSystem error: %v", config.SignatureBase(), err) + return false, "", fmt.Errorf("siegfried: retrieving %s.\nThis may be a network or firewall issue. See https://github.com/richardlehane/siegfried/wiki/Getting-started for manual instructions.\nSystem error: %v", config.SignatureBase(), err) } if !same(response, u.Size, u.Hash) { - return false, "", fmt.Errorf("Siegfried: error retrieving %s; SHA256 hash of response doesn't match %s", config.SignatureBase(), u.Hash) + return false, "", fmt.Errorf("siegfried: retrieving %s; SHA256 hash of response doesn't match %s", config.SignatureBase(), u.Hash) } - err = ioutil.WriteFile(config.Signature(), response, os.ModePerm) + err = os.WriteFile(config.Signature(), response, os.ModePerm) if err != nil { - return false, "", fmt.Errorf("Siegfried: error writing to directory, %v", err) + return false, "", fmt.Errorf("siegfried: error writing to directory, %v", err) } fmt.Printf("... writing %s ...\n", config.Signature()) return true, "Your signature file has been updated", nil } +type getHttpFn func(string) ([]byte, error) + func getHttp(url string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -153,17 +167,14 @@ func getHttp(url string) ([]byte, error) { _, timeout, transport := config.UpdateOptions() req.Header.Add("User-Agent", config.UserAgent()) req.Header.Add("Cache-Control", "no-cache") - timer := time.AfterFunc(timeout, func() { - transport.CancelRequest(req) - }) - defer timer.Stop() client := http.Client{ Transport: transport, + Timeout: timeout, } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() - return ioutil.ReadAll(resp.Body) + return io.ReadAll(resp.Body) } diff --git a/cmd/sf/update_test.go b/cmd/sf/update_test.go new file mode 100644 index 00000000..24c5dc35 --- /dev/null +++ b/cmd/sf/update_test.go @@ -0,0 +1,79 @@ +package main + +import ( + "bytes" + "compress/flate" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "testing" + "time" + + "github.com/richardlehane/siegfried/internal/persist" + "github.com/richardlehane/siegfried/pkg/config" +) + +var testResp = ` +[ + { + "sf":[1,11,1], + "created":"2023-05-12T09:10:13Z", + "hash":"2ad9b1cb28370add9320473676e3a1ba9e0311fc22a058c0862f4fb68f890582", + "size":204689, + "path":"https://www.itforarchivists.com/siegfried/latest" + }, + { + "sf":[1,10,1], + "created":"2023-05-12T09:10:13Z", + "hash":"2ad9b1cb28370add9320473676e3a1ba9e0311fc22a058c0862f4fb68f890582", + "size":204689, + "path":"https://www.itforarchivists.com/siegfried/latest" + } +] +` + +func TestUpdate(t *testing.T) { + config.SetHome("../roy/data") + var us Updates + if err := json.Unmarshal([]byte(testResp), &us); err != nil { + t.Fatal(err) + } + // Edit the version, size, hash, and created time of the first response so that + // it always matches and we are always up-to-date + us[0].Version = config.Version() + fbuf, err := os.ReadFile(config.Signature()) + if err != nil { + t.Fatal(err) + } + us[0].Size = len(fbuf) + h := sha256.New() + h.Write(fbuf) + us[0].Hash = hex.EncodeToString(h.Sum(nil)) + byt, err := json.Marshal(us) + if err != nil { + t.Fatal(err) + } + if len(fbuf) < len(config.Magic())+2+15 { + t.Fatal("signature file too short!") + } + rc := flate.NewReader(bytes.NewBuffer(fbuf[len(config.Magic())+2:])) + nbuf := make([]byte, 15) + if n, _ := rc.Read(nbuf); n < 15 { + t.Fatal("bad signature") + } + rc.Close() + ls := persist.NewLoadSaver(nbuf) + tt := ls.LoadTime() + if ls.Err != nil { + t.Fatal(ls.Err) + } + us[0].Created = tt.Format(time.RFC3339) + tgf := func(url string) ([]byte, error) { + return byt, nil + } + _, str, err := updateSigsDo("http://www.example.com", nil, tgf) + if str != "You are already up to date!" { + t.Fatalf("got: %s; error: %v", str, err) + } +} diff --git a/pkg/config/siegfried.go b/pkg/config/siegfried.go index 47e233d8..d03432a5 100644 --- a/pkg/config/siegfried.go +++ b/pkg/config/siegfried.go @@ -56,7 +56,7 @@ var siegfried = struct { choices: 128, cost: 25600000, repetition: 4, - updateURL: "https://www.itforarchivists.com/siegfried/update", // "http://localhost:8081/siegfried/update", + updateURL: "https://www.itforarchivists.com/siegfried/update/v2", // "http://localhost:8081/siegfried/update", updateTimeout: 30 * time.Second, updateTransport: &http.Transport{Proxy: http.ProxyFromEnvironment}, fpr: "/tmp/siegfried",