physician, update thyself

Photo by Marija Zaric on Unsplash:  https://unsplash.com/photos/XcFi9nPvm-o

In my continued pursuit of pretty much any excuse to sit down and knock around a bunch of Go for a few hours, I decided to finally try out go-update.

For some reason I’d been intimidated by the idea of using this library, but as soon as I dug in, I realized my anxiety was unwarranted. It took me more time to weave in all of the parameterization I wanted using package flag than it did to implement go-update itself.

I haven’t yet tried to implement binary patching, which seems like the way to go. I can’t recommend using this in production without checksum verification and code signature verification.

The client can easily update itself:


import (
    "net/http"

    "github.com/inconshreveable/go-update"
    "github.com/pkg/errors"
)

func UpdateFrom(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    err := update.Apply(resp.Body, update.Options{})
        if err != nil {
                if rerr := update.RollbackError(err); rerr != nil {
                        err = errors.Wrap(rerr, "rollback failed")
                }
        }
    return err
}

…and that’s it. Of course, I needed to write the server half of this conversation as well, which boils down to this:


import (
    "io"
    "net/http"
    "os"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/pkg/errors"
)

func UpdateHandler(binaryPath string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        f, err := os.Open(binaryPath)
        defer f.Close()

        if err != nil {
            code := http.StatusNoContent
            err = errors.Wrap(err, http.StatusText(code))

            // ...log the error...

            http.Error(w, err.Error(), code)
            return
        }

        header := make([]byte, 512)
        f.Read(header)
        contentType := http.DetectContentType(header)

        stat, _ := f.Stat()
        size := strconv.FormatInt(stat.Size(), 10)

        w.Header().Set("Content-Disposition", "attachment; filename="+binaryPath)
        w.Header().Set("Content-Type", contentType)
        w.Header().Set("Content-Length", size)

        f.Seek(0, 0) // rewind after having read header above
        io.Copy(w, f)

        // ...log the successful request...
    }
}

The UpdateHandler is invoked thus in main.go:


import (
    "flag"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    var (
        binaryPath = flag.String(
            "binary-path",
            "dist/new-binary",
            "the path of the binary to serve via the update endpoint",
        )
        addr = flag.String(
            "addr",
            ":8080",
            "port to run the HTTP update server",
        )
    )
    flag.Parse()

    m := mux.NewRouter()
    m.HandleFunc("/update", UpdateHandler(*binaryPath))

    srv := http.Server{Addr: addr, Handler: m}

    err := srv.ListenAndServe()
    if err != http.ErrServerClosed {
        // ...log the error...
    }
}

WARNING: Allowing a binary to update itself requires a lot of trust, and the network isn’t secure enough to allow that trust without additional verification mechanisms at the application layer:

If you use the naive implementation above in production, you may end up on the news.

comments powered by Disqus