add equinox self update support

master 0.0.1
codeskyblue 8 years ago
parent 416a05367d
commit cc06b2e1a3

24
Godeps/Godeps.json generated

@ -7,6 +7,30 @@
"ImportPath": "github.com/codeskyblue/kexec",
"Rev": "098ccba5e5e7e676f3631983c5ea931a3592871f"
},
{
"ImportPath": "github.com/equinox-io/equinox",
"Rev": "6f97d0d3970881d3e53dd6f547a41109eb055e54"
},
{
"ImportPath": "github.com/equinox-io/equinox/internal/go-update",
"Rev": "6f97d0d3970881d3e53dd6f547a41109eb055e54"
},
{
"ImportPath": "github.com/equinox-io/equinox/internal/go-update/internal/binarydist",
"Rev": "6f97d0d3970881d3e53dd6f547a41109eb055e54"
},
{
"ImportPath": "github.com/equinox-io/equinox/internal/go-update/internal/osext",
"Rev": "6f97d0d3970881d3e53dd6f547a41109eb055e54"
},
{
"ImportPath": "github.com/equinox-io/equinox/internal/osext",
"Rev": "6f97d0d3970881d3e53dd6f547a41109eb055e54"
},
{
"ImportPath": "github.com/equinox-io/equinox/proto",
"Rev": "6f97d0d3970881d3e53dd6f547a41109eb055e54"
},
{
"ImportPath": "github.com/glycerine/rbuf",
"Rev": "b8b438717ac043d5224cc7f079b866e3b9ea0599"

@ -0,0 +1,5 @@
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEY8xsSkcFs8XXUicw3n7E77qN/vqKUQ/6
/X5aBiOVF1yTIRYRXrV3aEvJRzErvQxziT9cLxQq+BFUZqn9pISnPSf9dn0wf9kU
TxI79zIvne9UT/rDsM0BxSydwtjG00MT
-----END ECDSA PUBLIC KEY-----

@ -8,13 +8,50 @@ import (
"net/http"
"os"
"github.com/equinox-io/equinox"
"github.com/urfave/cli"
)
const appID = "app_8Gji4eEAdDx"
var (
Version string = "dev"
Version string = "dev"
publicKey = []byte(`
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEY8xsSkcFs8XXUicw3n7E77qN/vqKUQ/6
/X5aBiOVF1yTIRYRXrV3aEvJRzErvQxziT9cLxQq+BFUZqn9pISnPSf9dn0wf9kU
TxI79zIvne9UT/rDsM0BxSydwtjG00MT
-----END ECDSA PUBLIC KEY-----
`)
)
func equinoxUpdate() error {
var opts equinox.Options
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
return err
}
// check for the update
resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
fmt.Println("No update available, already at the latest version!")
return nil
case err != nil:
fmt.Println("Update failed:", err)
return err
}
// fetch the update and apply it
err = resp.Apply()
if err != nil {
return err
}
fmt.Printf("Updated to new version: %s!\n", resp.ReleaseVersion)
return nil
}
func actionStartServer(c *cli.Context) error {
if err := registerHTTPHandlers(); err != nil {
return err
@ -75,6 +112,10 @@ func actionConfigTest(c *cli.Context) error {
return nil
}
func actionUpdateSelf(c *cli.Context) error {
return equinoxUpdate()
}
func main() {
app := cli.NewApp()
app.Name = "gosuv"
@ -114,6 +155,11 @@ func main() {
Usage: "Test if config file is valid",
Action: actionConfigTest,
},
{
Name: "update-self",
Usage: "Update gosuv itself",
Action: actionUpdateSelf,
},
}
if err := app.Run(os.Args); err != nil {
os.Exit(1)

@ -22,9 +22,10 @@ body {
height: 50em;
}
.status-icon {
border-radius: 1.5em;
display: inline-block;
width: 1.5em;
font-size: 0.2em;
.status {
background-color: gray;
border-radius: 0.3em;
padding-left: 0.5em;
padding-right: 0.5em;
color: white;
}

@ -85,7 +85,9 @@
</tr>
<tr class="success">
<td>gohttpserver</td>
<td>Running(2h)</td>
<td>
<span class="status">Running(2h)</span>
</td>
<td>
<button class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-play"></span> Start
@ -94,7 +96,7 @@
<span class="glyphicon glyphicon-stop"></span> Stop
</button>
<button class="btn btn-default btn-xs" disabled="true">
<span class="glyphicon glyphicon-minus"></span> Tailf
<span class="glyphicon glyphicon-minus"></span> Tail
</button>
<button class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-cog"></span> Setting

@ -88,7 +88,6 @@ var vm = new Vue({
},
refresh: function() {
// ws.send("Hello")
console.log("RR");
$.ajax({
url: "/api/programs",
success: function(data) {
@ -172,7 +171,7 @@ Vue.filter('formatBytes', function(value) {
Vue.filter('colorStatus', function(value) {
var makeColorText = function(text, color) {
return "<span class='status-icon' style='background-color:" + color + "'>" + "&nbsp;" + "</span> " + text;
return "<span class='status' style='background-color:" + color + "'>" + text + "</span>";
}
switch (value) {
case "running":

@ -0,0 +1,5 @@
language: go
go:
- 1.4
- 1.5
script: go test -v github.com/equinox-io/equinox github.com/equinox-io/equinox/proto

@ -0,0 +1,99 @@
# equinox client SDK [![godoc reference](https://godoc.org/github.com/equinox-io/equinox?status.png)](https://godoc.org/github.com/equinox-io/equinox)
Package equinox allows applications to remotely update themselves with the [equinox.io](https://equinox.io) service.
## Minimal Working Example
```go
import "github.com/equinox-io/equinox"
const appID = "<YOUR EQUINOX APP ID>"
var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtrVmBxQvheRArXjg2vG1xIprWGuCyESx
MMY8pjmjepSy2kuz+nl9aFLqmr+rDNdYvEBqQaZrYMc6k29gjvoQnQ==
-----END PUBLIC KEY-----
`)
func update(channel string) error {
opts := equinox.Options{Channel: channel}
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
return err
}
// check for the update
resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
fmt.Println("No update available, already at the latest version!")
return nil
case err != nil:
return err
}
// fetch the update and apply it
err = resp.Apply()
if err != nil {
return err
}
fmt.Printf("Updated to new version: %s!\n", resp.ReleaseVersion)
return nil
}
```
## Update To Specific Version
When you specify a channel in the update options, equinox will try to update the application
to the latest release of your application published to that channel. Instead, you may wish to
update the application to a specific (possibly older) version. You can do this by explicitly setting
Version in the Options struct:
```go
opts := equinox.Options{Version: "0.1.2"}
```
## Prompt For Update
You may wish to ask the user for approval before updating to a new version. This is as simple
as calling the Check function and only calling Apply on the returned result if the user approves.
Example:
```go
// check for the update
resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
fmt.Println("No update available, already at the latest version!")
return nil
case err != nil:
return err
}
fmt.Println("New version available!")
fmt.Println("Version:", resp.ReleaseVersion)
fmt.Println("Name:", resp.ReleaseTitle)
fmt.Println("Details:", resp.ReleaseDescription)
ok := prompt("Would you like to update?")
if !ok {
return
}
err = resp.Apply()
// ...
```
## Generating Keys
All equinox releases must be signed with a private ECDSA key, and all updates verified with the
public key portion. To do that, you'll need to generate a key pair. The equinox release tool can
generate an ecdsa key pair for you easily:
```shell
equinox genkey
```

@ -0,0 +1,92 @@
/*
Package equinox allows applications to remotely update themselves with the equinox.io service.
Minimal Working Example
import "github.com/equinox-io/equinox"
const appID = "<YOUR EQUINOX APP ID>"
var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtrVmBxQvheRArXjg2vG1xIprWGuCyESx
MMY8pjmjepSy2kuz+nl9aFLqmr+rDNdYvEBqQaZrYMc6k29gjvoQnQ==
-----END PUBLIC KEY-----
`)
func update(channel string) error {
opts := equinox.Options{Channel: channel}
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
return err
}
// check for the update
resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
fmt.Println("No update available, already at the latest version!")
return nil
case err != nil:
return err
}
// fetch the update and apply it
err = resp.Apply()
if err != nil {
return err
}
fmt.Printf("Updated to new version: %s!\n", resp.ReleaseVersion)
return nil
}
Update To Specific Version
When you specify a channel in the update options, equinox will try to update the application
to the latest release of your application published to that channel. Instead, you may wish to
update the application to a specific (possibly older) version. You can do this by explicitly setting
Version in the Options struct:
opts := equinox.Options{Version: "0.1.2"}
Prompt For Update
You may wish to ask the user for approval before updating to a new version. This is as simple
as calling the Check function and only calling Apply on the returned result if the user approves.
Example:
// check for the update
resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
fmt.Println("No update available, already at the latest version!")
return nil
case err != nil:
return err
}
fmt.Println("New version available!")
fmt.Println("Version:", resp.ReleaseVersion)
fmt.Println("Name:", resp.ReleaseTitle)
fmt.Println("Details:", resp.ReleaseDescription)
ok := prompt("Would you like to update?")
if !ok {
return
}
err = resp.Apply()
// ...
Generating Keys
All equinox releases must be signed with a private ECDSA key, and all updates verified with the
public key portion. To do that, you'll need to generate a key pair. The equinox release tool can
generate an ecdsa key pair for you easily:
equinox genkey
*/
package equinox

@ -0,0 +1,13 @@
Copyright 2015 Alan Shreve
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,65 @@
# go-update: Build self-updating Go programs [![godoc reference](https://godoc.org/github.com/inconshreveable/go-update?status.png)](https://godoc.org/github.com/inconshreveable/go-update)
Package update provides functionality to implement secure, self-updating Go programs (or other single-file targets)
A program can update itself by replacing its executable file with a new version.
It provides the flexibility to implement different updating user experiences
like auto-updating, or manual user-initiated updates. It also boasts
advanced features like binary patching and code signing verification.
Example of updating from a URL:
```go
import (
"fmt"
"net/http"
"github.com/inconshreveable/go-update"
)
func doUpdate(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 {
// error handling
}
return err
}
```
## Features
- Cross platform support (Windows too!)
- Binary patch application
- Checksum verification
- Code signing verification
- Support for updating arbitrary files
## [equinox.io](https://equinox.io)
[equinox.io](https://equinox.io) is a complete ready-to-go updating solution built on top of go-update that provides:
- Hosted updates
- Update channels (stable, beta, nightly, ...)
- Dynamically computed binary diffs
- Automatic key generation and code
- Release tooling with proper code signing
- Update/download metrics
## API Compatibility Promises
The master branch of `go-update` is *not* guaranteed to have a stable API over time. For any production application, you should vendor
your dependency on `go-update` with a tool like git submodules, [gb](http://getgb.io/) or [govendor](https://github.com/kardianos/govendor).
The `go-update` package makes the following promises about API compatibility:
1. A list of all API-breaking changes will be documented in this README.
1. `go-update` will strive for as few API-breaking changes as possible.
## API Breaking Changes
- **Sept 3, 2015**: The `Options` struct passed to `Apply` was changed to be passed by value instead of passed by pointer. Old API at `28de026`.
- **Aug 9, 2015**: 2.0 API. Old API at `221d034` or `gopkg.in/inconshreveable/go-update.v0`.
## License
Apache

@ -0,0 +1,322 @@
package update
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/equinox-io/equinox/internal/go-update/internal/osext"
)
var (
openFile = os.OpenFile
)
// Apply performs an update of the current executable (or opts.TargetFile, if set) with the contents of the given io.Reader.
//
// Apply performs the following actions to ensure a safe cross-platform update:
//
// 1. If configured, applies the contents of the update io.Reader as a binary patch.
//
// 2. If configured, computes the checksum of the new executable and verifies it matches.
//
// 3. If configured, verifies the signature with a public key.
//
// 4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
//
// 5. Renames /path/to/target to /path/to/.target.old
//
// 6. Renames /path/to/.target.new to /path/to/target
//
// 7. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
// the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
//
// 8. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
// back to /path/to/target.
//
// If the roll back operation fails, the file system is left in an inconsistent state (betweet steps 5 and 6) where
// there is no new executable file and the old executable file could not be be moved to its original location. In this
// case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether
// the rollback failed by calling RollbackError, see the documentation on that function for additional detail.
func Apply(update io.Reader, opts Options) error {
// validate
verify := false
switch {
case opts.Signature != nil && opts.PublicKey != nil:
// okay
verify = true
case opts.Signature != nil:
return errors.New("no public key to verify signature with")
case opts.PublicKey != nil:
return errors.New("No signature to verify with")
}
// set defaults
if opts.Hash == 0 {
opts.Hash = crypto.SHA256
}
if opts.Verifier == nil {
opts.Verifier = NewECDSAVerifier()
}
if opts.TargetMode == 0 {
opts.TargetMode = 0755
}
// get target path
var err error
opts.TargetPath, err = opts.getPath()
if err != nil {
return err
}
var newBytes []byte
if opts.Patcher != nil {
if newBytes, err = opts.applyPatch(update); err != nil {
return err
}
} else {
// no patch to apply, go on through
if newBytes, err = ioutil.ReadAll(update); err != nil {
return err
}
}
// verify checksum if requested
if opts.Checksum != nil {
if err = opts.verifyChecksum(newBytes); err != nil {
return err
}
}
if verify {
if err = opts.verifySignature(newBytes); err != nil {
return err
}
}
// get the directory the executable exists in
updateDir := filepath.Dir(opts.TargetPath)
filename := filepath.Base(opts.TargetPath)
// Copy the contents of newbinary to a new executable file
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.TargetMode)
if err != nil {
return err
}
defer fp.Close()
_, err = io.Copy(fp, bytes.NewReader(newBytes))
if err != nil {
return err
}
// if we don't call fp.Close(), windows won't let us move the new executable
// because the file will still be "in use"
fp.Close()
// this is where we'll move the executable to so that we can swap in the updated replacement
oldPath := opts.OldSavePath
removeOld := opts.OldSavePath == ""
if removeOld {
oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
}
// delete any existing old exec file - this is necessary on Windows for two reasons:
// 1. after a successful update, Windows can't remove the .old file because the process is still running
// 2. windows rename operations fail if the destination file already exists
_ = os.Remove(oldPath)
// move the existing executable to a new file in the same directory
err = os.Rename(opts.TargetPath, oldPath)
if err != nil {
return err
}
// move the new exectuable in to become the new program
err = os.Rename(newPath, opts.TargetPath)
if err != nil {
// move unsuccessful
//
// The filesystem is now in a bad state. We have successfully
// moved the existing binary to a new location, but we couldn't move the new
// binary to take its place. That means there is no file where the current executable binary
// used to be!
// Try to rollback by restoring the old binary to its original path.
rerr := os.Rename(oldPath, opts.TargetPath)
if rerr != nil {
return &rollbackErr{err, rerr}
}
return err
}
// move successful, remove the old binary if needed
if removeOld {
errRemove := os.Remove(oldPath)
// windows has trouble with removing old binaries, so hide it instead
if errRemove != nil {
_ = hideFile(oldPath)
}
}
return nil
}
// RollbackError takes an error value returned by Apply and returns the error, if any,
// that occurred when attempting to roll back from a failed update. Applications should
// always call this function on any non-nil errors returned by Apply.
//
// If no rollback was needed or if the rollback was successful, RollbackError returns nil,
// otherwise it returns the error encountered when trying to roll back.
func RollbackError(err error) error {
if err == nil {
return nil
}
if rerr, ok := err.(*rollbackErr); ok {
return rerr.rollbackErr
}
return nil
}
type rollbackErr struct {
error // original error
rollbackErr error // error encountered while rolling back
}
type Options struct {
// TargetPath defines the path to the file to update.
// The emptry string means 'the executable file of the running program'.
TargetPath string
// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
TargetMode os.FileMode
// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
Checksum []byte
// Public key to use for signature verification. If nil, no signature verification is done.
PublicKey crypto.PublicKey
// Signature to verify the updated file. If nil, no signature verification is done.
Signature []byte
// Pluggable signature verification algorithm. If nil, ECDSA is used.
Verifier Verifier
// Use this hash function to generate the checksum. If not set, SHA256 is used.
Hash crypto.Hash
// If nil, treat the update as a complete replacement for the contents of the file at TargetPath.
// If non-nil, treat the update contents as a patch and use this object to apply the patch.
Patcher Patcher
// Store the old executable file at this path after a successful update.
// The empty string means the old executable file will be removed after the update.
OldSavePath string
}
// CheckPermissions determines whether the process has the correct permissions to
// perform the requested update. If the update can proceed, it returns nil, otherwise
// it returns the error that would occur if an update were attempted.
func (o *Options) CheckPermissions() error {
// get the directory the file exists in
path, err := o.getPath()
if err != nil {
return err
}
fileDir := filepath.Dir(path)
fileName := filepath.Base(path)
// attempt to open a file in the file's directory
newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName))
fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.TargetMode)
if err != nil {
return err
}
fp.Close()
_ = os.Remove(newPath)
return nil
}
// SetPublicKeyPEM is a convenience method to set the PublicKey property
// used for checking a completed update's signature by parsing a
// Public Key formatted as PEM data.
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
block, _ := pem.Decode(pembytes)
if block == nil {
return errors.New("couldn't parse PEM data")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
o.PublicKey = pub
return nil
}
func (o *Options) getPath() (string, error) {
if o.TargetPath == "" {
return osext.Executable()
} else {
return o.TargetPath, nil
}
}
func (o *Options) applyPatch(patch io.Reader) ([]byte, error) {
// open the file to patch
old, err := os.Open(o.TargetPath)
if err != nil {
return nil, err
}
defer old.Close()
// apply the patch
var applied bytes.Buffer
if err = o.Patcher.Patch(old, &applied, patch); err != nil {
return nil, err
}
return applied.Bytes(), nil
}
func (o *Options) verifyChecksum(updated []byte) error {
checksum, err := checksumFor(o.Hash, updated)
if err != nil {
return err
}
if !bytes.Equal(o.Checksum, checksum) {
return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
}
return nil
}
func (o *Options) verifySignature(updated []byte) error {
checksum, err := checksumFor(o.Hash, updated)
if err != nil {
return err
}
return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
}
func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
if !h.Available() {
return nil, errors.New("requested hash function not available")
}
hash := h.New()
hash.Write(payload) // guaranteed not to error
return hash.Sum([]byte{}), nil
}

@ -0,0 +1,172 @@
/*
Package update provides functionality to implement secure, self-updating Go programs (or other single-file targets).
For complete updating solutions please see Equinox (https://equinox.io) and go-tuf (https://github.com/flynn/go-tuf).
Basic Example
This example shows how to update a program remotely from a URL.
import (
"fmt"
"net/http"
"github.com/inconshreveable/go-update"
)
func doUpdate(url string) error {
// request the new file
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 {
fmt.Println("Failed to rollback from bad update: %v", rerr)
}
}
return err
}
Binary Patching
Go binaries can often be large. It can be advantageous to only ship a binary patch to a client
instead of the complete program text of a new version.
This example shows how to update a program with a bsdiff binary patch. Other patch formats
may be applied by implementing the Patcher interface.
import (
"encoding/hex"
"io"
"github.com/inconshreveable/go-update"
)
func updateWithPatch(patch io.Reader) error {
err := update.Apply(patch, update.Options{
Patcher: update.NewBSDiffPatcher()
})
if err != nil {
// error handling
}
return err
}
Checksum Verification
Updating executable code on a computer can be a dangerous operation unless you
take the appropriate steps to guarantee the authenticity of the new code. While
checksum verification is important, it should always be combined with signature
verification (next section) to guarantee that the code came from a trusted party.
go-update validates SHA256 checksums by default, but this is pluggable via the Hash
property on the Options struct.
This example shows how to guarantee that the newly-updated binary is verified to
have an appropriate checksum (that was otherwise retrived via a secure channel)
specified as a hex string.
import (
"crypto"
_ "crypto/sha256"
"encoding/hex"
"io"
"github.com/inconshreveable/go-update"
)
func updateWithChecksum(binary io.Reader, hexChecksum string) error {
checksum, err := hex.DecodeString(hexChecksum)
if err != nil {
return err
}
err = update.Apply(binary, update.Options{
Hash: crypto.SHA256, // this is the default, you don't need to specify it
Checksum: checksum,
})
if err != nil {
// error handling
}
return err
}
Cryptographic Signature Verification
Cryptographic verification of new code from an update is an extremely important way to guarantee the
security and integrity of your updates.
Verification is performed by validating the signature of a hash of the new file. This
means nothing changes if you apply your update with a patch.
This example shows how to add signature verification to your updates. To make all of this work
an application distributor must first create a public/private key pair and embed the public key
into their application. When they issue a new release, the issuer must sign the new executable file
with the private key and distribute the signature along with the update.
import (
"crypto"
_ "crypto/sha256"
"encoding/hex"
"io"
"github.com/inconshreveable/go-update"
)
var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtrVmBxQvheRArXjg2vG1xIprWGuCyESx
MMY8pjmjepSy2kuz+nl9aFLqmr+rDNdYvEBqQaZrYMc6k29gjvoQnQ==
-----END PUBLIC KEY-----
`)
func verifiedUpdate(binary io.Reader, hexChecksum, hexSignature string) {
checksum, err := hex.DecodeString(hexChecksum)
if err != nil {
return err
}
signature, err := hex.DecodeString(hexSignature)
if err != nil {
return err
}
opts := update.Options{
Checksum: checksum,
Signature: signature,
Hash: crypto.SHA256, // this is the default, you don't need to specify it
Verifier: update.NewECDSAVerifier(), // this is the default, you don't need to specify it
}
err = opts.SetPublicKeyPEM(publicKey)
if err != nil {
return err
}
err = update.Apply(binary, opts)
if err != nil {
// error handling
}
return err
}
Building Single-File Go Binaries
In order to update a Go application with go-update, you must distributed it as a single executable.
This is often easy, but some applications require static assets (like HTML and CSS asset files or TLS certificates).
In order to update applications like these, you'll want to make sure to embed those asset files into
the distributed binary with a tool like go-bindata (my favorite): https://github.com/jteeuwen/go-bindata
Non-Goals
Mechanisms and protocols for determining whether an update should be applied and, if so, which one are
out of scope for this package. Please consult go-tuf (https://github.com/flynn/go-tuf) or Equinox (https://equinox.io)
for more complete solutions.
go-update only works for self-updating applications that are distributed as a single binary, i.e.
applications that do not have additional assets or dependency files.
Updating application that are distributed as mutliple on-disk files is out of scope, although this
may change in future versions of this library.
*/
package update

@ -0,0 +1,7 @@
// +build !windows
package update
func hideFile(path string) error {
return nil
}

@ -0,0 +1,19 @@
package update
import (
"syscall"
"unsafe"
)
func hideFile(path string) error {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
setFileAttributes := kernel32.NewProc("SetFileAttributesW")
r1, _, err := setFileAttributes.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 2)
if r1 == 0 {
return err
} else {
return nil
}
}

@ -0,0 +1,22 @@
Copyright 2012 Keith Rarick
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,7 @@
# binarydist
Package binarydist implements binary diff and patch as described on
<http://www.daemonology.net/bsdiff/>. It reads and writes files
compatible with the tools there.
Documentation at <http://go.pkgdoc.org/github.com/kr/binarydist>.

@ -0,0 +1,40 @@
package binarydist
import (
"io"
"os/exec"
)
type bzip2Writer struct {
c *exec.Cmd
w io.WriteCloser
}
func (w bzip2Writer) Write(b []byte) (int, error) {
return w.w.Write(b)
}
func (w bzip2Writer) Close() error {
if err := w.w.Close(); err != nil {
return err
}
return w.c.Wait()
}
// Package compress/bzip2 implements only decompression,
// so we'll fake it by running bzip2 in another process.
func newBzip2Writer(w io.Writer) (wc io.WriteCloser, err error) {
var bw bzip2Writer
bw.c = exec.Command("bzip2", "-c")
bw.c.Stdout = w
if bw.w, err = bw.c.StdinPipe(); err != nil {
return nil, err
}
if err = bw.c.Start(); err != nil {
return nil, err
}
return bw, nil
}

@ -0,0 +1,408 @@
package binarydist
import (
"bytes"
"encoding/binary"
"io"
"io/ioutil"
)
func swap(a []int, i, j int) { a[i], a[j] = a[j], a[i] }
func split(I, V []int, start, length, h int) {
var i, j, k, x, jj, kk int
if length < 16 {
for k = start; k < start+length; k += j {
j = 1
x = V[I[k]+h]
for i = 1; k+i < start+length; i++ {
if V[I[k+i]+h] < x {
x = V[I[k+i]+h]
j = 0
}
if V[I[k+i]+h] == x {
swap(I, k+i, k+j)
j++
}
}
for i = 0; i < j; i++ {
V[I[k+i]] = k + j - 1
}
if j == 1 {
I[k] = -1
}
}
return
}
x = V[I[start+length/2]+h]
jj = 0
kk = 0
for i = start; i < start+length; i++ {
if V[I[i]+h] < x {
jj++
}
if V[I[i]+h] == x {
kk++
}
}
jj += start
kk += jj
i = start
j = 0
k = 0
for i < jj {
if V[I[i]+h] < x {
i++
} else if V[I[i]+h] == x {
swap(I, i, jj+j)
j++
} else {
swap(I, i, kk+k)
k++
}
}
for jj+j < kk {
if V[I[jj+j]+h] == x {
j++
} else {
swap(I, jj+j, kk+k)
k++
}
}
if jj > start {
split(I, V, start, jj-start, h)
}
for i = 0; i < kk-jj; i++ {
V[I[jj+i]] = kk - 1
}
if jj == kk-1 {
I[jj] = -1
}
if start+length > kk {
split(I, V, kk, start+length-kk, h)
}
}
func qsufsort(obuf []byte) []int {
var buckets [256]int
var i, h int
I := make([]int, len(obuf)+1)
V := make([]int, len(obuf)+1)
for _, c := range obuf {
buckets[c]++
}
for i = 1; i < 256; i++ {
buckets[i] += buckets[i-1]
}
copy(buckets[1:], buckets[:])
buckets[0] = 0
for i, c := range obuf {
buckets[c]++
I[buckets[c]] = i
}
I[0] = len(obuf)
for i, c := range obuf {
V[i] = buckets[c]
}
V[len(obuf)] = 0
for i = 1; i < 256; i++ {
if buckets[i] == buckets[i-1]+1 {
I[buckets[i]] = -1
}
}
I[0] = -1
for h = 1; I[0] != -(len(obuf) + 1); h += h {
var n int
for i = 0; i < len(obuf)+1; {
if I[i] < 0 {
n -= I[i]
i -= I[i]
} else {
if n != 0 {
I[i-n] = -n
}
n = V[I[i]] + 1 - i
split(I, V, i, n, h)
i += n
n = 0
}
}
if n != 0 {
I[i-n] = -n
}
}
for i = 0; i < len(obuf)+1; i++ {
I[V[i]] = i
}
return I
}
func matchlen(a, b []byte) (i int) {
for i < len(a) && i < len(b) && a[i] == b[i] {
i++
}
return i
}
func search(I []int, obuf, nbuf []byte, st, en int) (pos, n int) {
if en-st < 2 {
x := matchlen(obuf[I[st]:], nbuf)
y := matchlen(obuf[I[en]:], nbuf)
if x > y {
return I[st], x
} else {
return I[en], y
}
}
x := st + (en-st)/2
if bytes.Compare(obuf[I[x]:], nbuf) < 0 {
return search(I, obuf, nbuf, x, en)
} else {
return search(I, obuf, nbuf, st, x)
}
panic("unreached")
}
// Diff computes the difference between old and new, according to the bsdiff
// algorithm, and writes the result to patch.
func Diff(old, new io.Reader, patch io.Writer) error {
obuf, err := ioutil.ReadAll(old)
if err != nil {
return err
}
nbuf, err := ioutil.ReadAll(new)
if err != nil {
return err
}
pbuf, err := diffBytes(obuf, nbuf)
if err != nil {
return err
}
_, err = patch.Write(pbuf)
return err
}
func diffBytes(obuf, nbuf []byte) ([]byte, error) {
var patch seekBuffer
err := diff(obuf, nbuf, &patch)
if err != nil {
return nil, err
}
return patch.buf, nil
}
func diff(obuf, nbuf []byte, patch io.WriteSeeker) error {
var lenf int
I := qsufsort(obuf)
db := make([]byte, len(nbuf))
eb := make([]byte, len(nbuf))
var dblen, eblen int
var hdr header
hdr.Magic = magic
hdr.NewSize = int64(len(nbuf))
err := binary.Write(patch, signMagLittleEndian{}, &hdr)
if err != nil {
return err
}
// Compute the differences, writing ctrl as we go
pfbz2, err := newBzip2Writer(patch)
if err != nil {
return err
}
var scan, pos, length int
var lastscan, lastpos, lastoffset int
for scan < len(nbuf) {
var oldscore int
scan += length
for scsc := scan; scan < len(nbuf); scan++ {
pos, length = search(I, obuf, nbuf[scan:], 0, len(obuf))
for ; scsc < scan+length; scsc++ {
if scsc+lastoffset < len(obuf) &&
obuf[scsc+lastoffset] == nbuf[scsc] {
oldscore++
}
}
if (length == oldscore && length != 0) || length > oldscore+8 {
break
}
if scan+lastoffset < len(obuf) && obuf[scan+lastoffset] == nbuf[scan] {
oldscore--
}
}
if length != oldscore || scan == len(nbuf) {
var s, Sf int
lenf = 0
for i := 0; lastscan+i < scan && lastpos+i < len(obuf); {
if obuf[lastpos+i] == nbuf[lastscan+i] {
s++
}
i++
if s*2-i > Sf*2-lenf {
Sf = s
lenf = i
}
}
lenb := 0
if scan < len(nbuf) {
var s, Sb int
for i := 1; (scan >= lastscan+i) && (pos >= i); i++ {
if obuf[pos-i] == nbuf[scan-i] {
s++
}
if s*2-i > Sb*2-lenb {
Sb = s
lenb = i
}
}
}
if lastscan+lenf > scan-lenb {
overlap := (lastscan + lenf) - (scan - lenb)
s := 0
Ss := 0
lens := 0
for i := 0; i < overlap; i++ {
if nbuf[lastscan+lenf-overlap+i] == obuf[lastpos+lenf-overlap+i] {
s++
}
if nbuf[scan-lenb+i] == obuf[pos-lenb+i] {
s--
}
if s > Ss {
Ss = s
lens = i + 1
}
}
lenf += lens - overlap
lenb -= lens
}
for i := 0; i < lenf; i++ {
db[dblen+i] = nbuf[lastscan+i] - obuf[lastpos+i]
}
for i := 0; i < (scan-lenb)-(lastscan+lenf); i++ {
eb[eblen+i] = nbuf[lastscan+lenf+i]
}
dblen += lenf
eblen += (scan - lenb) - (lastscan + lenf)
err = binary.Write(pfbz2, signMagLittleEndian{}, int64(lenf))
if err != nil {
pfbz2.Close()
return err
}
val := (scan - lenb) - (lastscan + lenf)
err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val))
if err != nil {
pfbz2.Close()
return err
}
val = (pos - lenb) - (lastpos + lenf)
err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val))
if err != nil {
pfbz2.Close()
return err
}
lastscan = scan - lenb
lastpos = pos - lenb
lastoffset = pos - scan
}
}
err = pfbz2.Close()
if err != nil {
return err
}
// Compute size of compressed ctrl data
l64, err := patch.Seek(0, 1)
if err != nil {
return err
}
hdr.CtrlLen = int64(l64 - 32)
// Write compressed diff data
pfbz2, err = newBzip2Writer(patch)
if err != nil {
return err
}
n, err := pfbz2.Write(db[:dblen])
if err != nil {
pfbz2.Close()
return err
}
if n != dblen {
pfbz2.Close()
return io.ErrShortWrite
}
err = pfbz2.Close()
if err != nil {
return err
}
// Compute size of compressed diff data
n64, err := patch.Seek(0, 1)
if err != nil {
return err
}
hdr.DiffLen = n64 - l64
// Write compressed extra data
pfbz2, err = newBzip2Writer(patch)
if err != nil {
return err
}
n, err = pfbz2.Write(eb[:eblen])
if err != nil {
pfbz2.Close()
return err
}
if n != eblen {
pfbz2.Close()
return io.ErrShortWrite
}
err = pfbz2.Close()
if err != nil {
return err
}
// Seek to the beginning, write the header, and close the file
_, err = patch.Seek(0, 0)
if err != nil {
return err
}
err = binary.Write(patch, signMagLittleEndian{}, &hdr)
if err != nil {
return err
}
return nil
}

@ -0,0 +1,24 @@
// Package binarydist implements binary diff and patch as described on
// http://www.daemonology.net/bsdiff/. It reads and writes files
// compatible with the tools there.
package binarydist
var magic = [8]byte{'B', 'S', 'D', 'I', 'F', 'F', '4', '0'}
// File format:
// 0 8 "BSDIFF40"
// 8 8 X
// 16 8 Y
// 24 8 sizeof(newfile)
// 32 X bzip2(control block)
// 32+X Y bzip2(diff block)
// 32+X+Y ??? bzip2(extra block)
// with control block a set of triples (x,y,z) meaning "add x bytes
// from oldfile to x bytes from the diff block; copy y bytes from the
// extra block; seek forwards in oldfile by z bytes".
type header struct {
Magic [8]byte
CtrlLen int64
DiffLen int64
NewSize int64
}

@ -0,0 +1,53 @@
package binarydist
// SignMagLittleEndian is the numeric encoding used by the bsdiff tools.
// It implements binary.ByteOrder using a sign-magnitude format
// and little-endian byte order. Only methods Uint64 and String
// have been written; the rest panic.
type signMagLittleEndian struct{}
func (signMagLittleEndian) Uint16(b []byte) uint16 { panic("unimplemented") }
func (signMagLittleEndian) PutUint16(b []byte, v uint16) { panic("unimplemented") }
func (signMagLittleEndian) Uint32(b []byte) uint32 { panic("unimplemented") }
func (signMagLittleEndian) PutUint32(b []byte, v uint32) { panic("unimplemented") }
func (signMagLittleEndian) Uint64(b []byte) uint64 {
y := int64(b[0]) |
int64(b[1])<<8 |
int64(b[2])<<16 |
int64(b[3])<<24 |
int64(b[4])<<32 |
int64(b[5])<<40 |
int64(b[6])<<48 |
int64(b[7]&0x7f)<<56
if b[7]&0x80 != 0 {
y = -y
}
return uint64(y)
}
func (signMagLittleEndian) PutUint64(b []byte, v uint64) {
x := int64(v)
neg := x < 0
if neg {
x = -x
}
b[0] = byte(x)
b[1] = byte(x >> 8)
b[2] = byte(x >> 16)
b[3] = byte(x >> 24)
b[4] = byte(x >> 32)
b[5] = byte(x >> 40)
b[6] = byte(x >> 48)
b[7] = byte(x >> 56)
if neg {
b[7] |= 0x80
}
}
func (signMagLittleEndian) String() string { return "signMagLittleEndian" }

@ -0,0 +1,109 @@
package binarydist
import (
"bytes"
"compress/bzip2"
"encoding/binary"
"errors"
"io"
"io/ioutil"
)
var ErrCorrupt = errors.New("corrupt patch")
// Patch applies patch to old, according to the bspatch algorithm,
// and writes the result to new.
func Patch(old io.Reader, new io.Writer, patch io.Reader) error {
var hdr header
err := binary.Read(patch, signMagLittleEndian{}, &hdr)
if err != nil {
return err
}
if hdr.Magic != magic {
return ErrCorrupt
}
if hdr.CtrlLen < 0 || hdr.DiffLen < 0 || hdr.NewSize < 0 {
return ErrCorrupt
}
ctrlbuf := make([]byte, hdr.CtrlLen)
_, err = io.ReadFull(patch, ctrlbuf)
if err != nil {
return err
}
cpfbz2 := bzip2.NewReader(bytes.NewReader(ctrlbuf))
diffbuf := make([]byte, hdr.DiffLen)
_, err = io.ReadFull(patch, diffbuf)
if err != nil {
return err
}
dpfbz2 := bzip2.NewReader(bytes.NewReader(diffbuf))
// The entire rest of the file is the extra block.
epfbz2 := bzip2.NewReader(patch)
obuf, err := ioutil.ReadAll(old)
if err != nil {
return err
}
nbuf := make([]byte, hdr.NewSize)
var oldpos, newpos int64
for newpos < hdr.NewSize {
var ctrl struct{ Add, Copy, Seek int64 }
err = binary.Read(cpfbz2, signMagLittleEndian{}, &ctrl)
if err != nil {
return err
}
// Sanity-check
if newpos+ctrl.Add > hdr.NewSize {
return ErrCorrupt
}
// Read diff string
_, err = io.ReadFull(dpfbz2, nbuf[newpos:newpos+ctrl.Add])
if err != nil {
return ErrCorrupt
}
// Add old data to diff string
for i := int64(0); i < ctrl.Add; i++ {
if oldpos+i >= 0 && oldpos+i < int64(len(obuf)) {
nbuf[newpos+i] += obuf[oldpos+i]
}
}
// Adjust pointers
newpos += ctrl.Add
oldpos += ctrl.Add
// Sanity-check
if newpos+ctrl.Copy > hdr.NewSize {
return ErrCorrupt
}
// Read extra string
_, err = io.ReadFull(epfbz2, nbuf[newpos:newpos+ctrl.Copy])
if err != nil {
return ErrCorrupt
}
// Adjust pointers
newpos += ctrl.Copy
oldpos += ctrl.Seek
}
// Write the new file
for len(nbuf) > 0 {
n, err := new.Write(nbuf)
if err != nil {
return err
}
nbuf = nbuf[n:]
}
return nil
}

@ -0,0 +1,43 @@
package binarydist
import (
"errors"
)
type seekBuffer struct {
buf []byte
pos int
}
func (b *seekBuffer) Write(p []byte) (n int, err error) {
n = copy(b.buf[b.pos:], p)
if n == len(p) {
b.pos += n
return n, nil
}
b.buf = append(b.buf, p[n:]...)
b.pos += len(p)
return len(p), nil
}
func (b *seekBuffer) Seek(offset int64, whence int) (ret int64, err error) {
var abs int64
switch whence {
case 0:
abs = offset
case 1:
abs = int64(b.pos) + offset
case 2:
abs = int64(len(b.buf)) + offset
default:
return 0, errors.New("binarydist: invalid whence")
}
if abs < 0 {
return 0, errors.New("binarydist: negative position")
}
if abs >= 1<<31 {
return 0, errors.New("binarydist: position out of range")
}
b.pos = int(abs)
return abs, nil
}

@ -0,0 +1,27 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,16 @@
### Extensions to the "os" package.
## Find the current Executable and ExecutableFolder.
There is sometimes utility in finding the current executable file
that is running. This can be used for upgrading the current executable
or finding resources located relative to the executable file. Both
working directory and the os.Args[0] value are arbitrary and cannot
be relied on; os.Args[0] can be "faked".
Multi-platform and supports:
* Linux
* OS X
* Windows
* Plan 9
* BSDs.

@ -0,0 +1,27 @@
// Copyright 2012 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.
// Extensions to the standard "os" package.
package osext
import "path/filepath"
// Executable returns an absolute path that can be used to
// re-invoke the current program.
// It may not be valid after the current program exits.
func Executable() (string, error) {
p, err := executable()
return filepath.Clean(p), err
}
// Returns same path as Executable, returns just the folder
// path. Excludes the executable name and any trailing slash.
func ExecutableFolder() (string, error) {
p, err := Executable()
if err != nil {
return "", err
}
return filepath.Dir(p), nil
}

@ -0,0 +1,20 @@
// Copyright 2012 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.
package osext
import (
"os"
"strconv"
"syscall"
)
func executable() (string, error) {
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
if err != nil {
return "", err
}
defer f.Close()
return syscall.Fd2path(int(f.Fd()))
}

@ -0,0 +1,36 @@
// Copyright 2012 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.
// +build linux netbsd openbsd solaris dragonfly
package osext
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
)
func executable() (string, error) {
switch runtime.GOOS {
case "linux":
const deletedTag = " (deleted)"
execpath, err := os.Readlink("/proc/self/exe")
if err != nil {
return execpath, err
}
execpath = strings.TrimSuffix(execpath, deletedTag)
execpath = strings.TrimPrefix(execpath, deletedTag)
return execpath, nil
case "netbsd":
return os.Readlink("/proc/curproc/exe")
case "openbsd", "dragonfly":
return os.Readlink("/proc/curproc/file")
case "solaris":
return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid()))
}
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
}

@ -0,0 +1,79 @@
// Copyright 2012 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.
// +build darwin freebsd
package osext
import (
"os"
"path/filepath"
"runtime"
"syscall"
"unsafe"
)
var initCwd, initCwdErr = os.Getwd()
func executable() (string, error) {
var mib [4]int32
switch runtime.GOOS {
case "freebsd":
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
case "darwin":
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
}
n := uintptr(0)
// Get length.
_, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
if errNum != 0 {
return "", errNum
}
if n == 0 { // This shouldn't happen.
return "", nil
}
buf := make([]byte, n)
_, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
if errNum != 0 {
return "", errNum
}
if n == 0 { // This shouldn't happen.
return "", nil
}
for i, v := range buf {
if v == 0 {
buf = buf[:i]
break
}
}
var err error
execPath := string(buf)
// execPath will not be empty due to above checks.
// Try to get the absolute path if the execPath is not rooted.
if execPath[0] != '/' {
execPath, err = getAbs(execPath)
if err != nil {
return execPath, err
}
}
// For darwin KERN_PROCARGS may return the path to a symlink rather than the
// actual executable.
if runtime.GOOS == "darwin" {
if execPath, err = filepath.EvalSymlinks(execPath); err != nil {
return execPath, err
}
}
return execPath, nil
}
func getAbs(execPath string) (string, error) {
if initCwdErr != nil {
return execPath, initCwdErr
}
// The execPath may begin with a "../" or a "./" so clean it first.
// Join the two paths, trailing and starting slashes undetermined, so use
// the generic Join function.
return filepath.Join(initCwd, filepath.Clean(execPath)), nil
}

@ -0,0 +1,34 @@
// Copyright 2012 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.
package osext
import (
"syscall"
"unicode/utf16"
"unsafe"
)
var (
kernel = syscall.MustLoadDLL("kernel32.dll")
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
)
// GetModuleFileName() with hModule = NULL
func executable() (exePath string, err error) {
return getModuleFileName()
}
func getModuleFileName() (string, error) {
var n uint32
b := make([]uint16, syscall.MAX_PATH)
size := uint32(len(b))
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
n = uint32(r0)
if n == 0 {
return "", e1
}
return string(utf16.Decode(b[0:n])), nil
}

@ -0,0 +1,24 @@
package update
import (
"io"
"github.com/equinox-io/equinox/internal/go-update/internal/binarydist"
)
// Patcher defines an interface for applying binary patches to an old item to get an updated item.
type Patcher interface {
Patch(old io.Reader, new io.Writer, patch io.Reader) error
}
type patchFn func(io.Reader, io.Writer, io.Reader) error
func (fn patchFn) Patch(old io.Reader, new io.Writer, patch io.Reader) error {
return fn(old, new, patch)
}
// NewBSDifferPatcher returns a new Patcher that applies binary patches using
// the bsdiff algorithm. See http://www.daemonology.net/bsdiff/
func NewBSDiffPatcher() Patcher {
return patchFn(binarydist.Patch)
}

@ -0,0 +1,74 @@
package update
import (
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/rsa"
"encoding/asn1"
"errors"
"math/big"
)
// Verifier defines an interface for verfiying an update's signature with a public key.
type Verifier interface {
VerifySignature(checksum, signature []byte, h crypto.Hash, publicKey crypto.PublicKey) error
}
type verifyFn func([]byte, []byte, crypto.Hash, crypto.PublicKey) error
func (fn verifyFn) VerifySignature(checksum []byte, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
return fn(checksum, signature, hash, publicKey)
}
// NewRSAVerifier returns a Verifier that uses the RSA algorithm to verify updates.
func NewRSAVerifier() Verifier {
return verifyFn(func(checksum, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
key, ok := publicKey.(*rsa.PublicKey)
if !ok {
return errors.New("not a valid RSA public key")
}
return rsa.VerifyPKCS1v15(key, hash, checksum, signature)
})
}
type rsDER struct {
R *big.Int
S *big.Int
}
// NewECDSAVerifier returns a Verifier that uses the ECDSA algorithm to verify updates.
func NewECDSAVerifier() Verifier {
return verifyFn(func(checksum, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
key, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return errors.New("not a valid ECDSA public key")
}
var rs rsDER
if _, err := asn1.Unmarshal(signature, &rs); err != nil {
return err
}
if !ecdsa.Verify(key, checksum, rs.R, rs.S) {
return errors.New("failed to verify ecsda signature")
}
return nil
})
}
// NewDSAVerifier returns a Verifier that uses the DSA algorithm to verify updates.
func NewDSAVerifier() Verifier {
return verifyFn(func(checksum, signature []byte, hash crypto.Hash, publicKey crypto.PublicKey) error {
key, ok := publicKey.(*dsa.PublicKey)
if !ok {
return errors.New("not a valid DSA public key")
}
var rs rsDER
if _, err := asn1.Unmarshal(signature, &rs); err != nil {
return err
}
if !dsa.Verify(key, checksum, rs.R, rs.S) {
return errors.New("failed to verify ecsda signature")
}
return nil
})
}

@ -0,0 +1,27 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,16 @@
### Extensions to the "os" package.
## Find the current Executable and ExecutableFolder.
There is sometimes utility in finding the current executable file
that is running. This can be used for upgrading the current executable
or finding resources located relative to the executable file. Both
working directory and the os.Args[0] value are arbitrary and cannot
be relied on; os.Args[0] can be "faked".
Multi-platform and supports:
* Linux
* OS X
* Windows
* Plan 9
* BSDs.

@ -0,0 +1,27 @@
// Copyright 2012 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.
// Extensions to the standard "os" package.
package osext
import "path/filepath"
// Executable returns an absolute path that can be used to
// re-invoke the current program.
// It may not be valid after the current program exits.
func Executable() (string, error) {
p, err := executable()
return filepath.Clean(p), err
}
// Returns same path as Executable, returns just the folder
// path. Excludes the executable name and any trailing slash.
func ExecutableFolder() (string, error) {
p, err := Executable()
if err != nil {
return "", err
}
return filepath.Dir(p), nil
}

@ -0,0 +1,20 @@
// Copyright 2012 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.
package osext
import (
"os"
"strconv"
"syscall"
)
func executable() (string, error) {
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
if err != nil {
return "", err
}
defer f.Close()
return syscall.Fd2path(int(f.Fd()))
}

@ -0,0 +1,36 @@
// Copyright 2012 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.
// +build linux netbsd openbsd solaris dragonfly
package osext
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
)
func executable() (string, error) {
switch runtime.GOOS {
case "linux":
const deletedTag = " (deleted)"
execpath, err := os.Readlink("/proc/self/exe")
if err != nil {
return execpath, err
}
execpath = strings.TrimSuffix(execpath, deletedTag)
execpath = strings.TrimPrefix(execpath, deletedTag)
return execpath, nil
case "netbsd":
return os.Readlink("/proc/curproc/exe")
case "openbsd", "dragonfly":
return os.Readlink("/proc/curproc/file")
case "solaris":
return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid()))
}
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
}

@ -0,0 +1,79 @@
// Copyright 2012 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.
// +build darwin freebsd
package osext
import (
"os"
"path/filepath"
"runtime"
"syscall"
"unsafe"
)
var initCwd, initCwdErr = os.Getwd()
func executable() (string, error) {
var mib [4]int32
switch runtime.GOOS {
case "freebsd":
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
case "darwin":
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
}
n := uintptr(0)
// Get length.
_, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
if errNum != 0 {
return "", errNum
}
if n == 0 { // This shouldn't happen.
return "", nil
}
buf := make([]byte, n)
_, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
if errNum != 0 {
return "", errNum
}
if n == 0 { // This shouldn't happen.
return "", nil
}
for i, v := range buf {
if v == 0 {
buf = buf[:i]
break
}
}
var err error
execPath := string(buf)
// execPath will not be empty due to above checks.
// Try to get the absolute path if the execPath is not rooted.
if execPath[0] != '/' {
execPath, err = getAbs(execPath)
if err != nil {
return execPath, err
}
}
// For darwin KERN_PROCARGS may return the path to a symlink rather than the
// actual executable.
if runtime.GOOS == "darwin" {
if execPath, err = filepath.EvalSymlinks(execPath); err != nil {
return execPath, err
}
}
return execPath, nil
}
func getAbs(execPath string) (string, error) {
if initCwdErr != nil {
return execPath, initCwdErr
}
// The execPath may begin with a "../" or a "./" so clean it first.
// Join the two paths, trailing and starting slashes undetermined, so use
// the generic Join function.
return filepath.Join(initCwd, filepath.Clean(execPath)), nil
}

@ -0,0 +1,34 @@
// Copyright 2012 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.
package osext
import (
"syscall"
"unicode/utf16"
"unsafe"
)
var (
kernel = syscall.MustLoadDLL("kernel32.dll")
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
)
// GetModuleFileName() with hModule = NULL
func executable() (exePath string, err error) {
return getModuleFileName()
}
func getModuleFileName() (string, error) {
var n uint32
b := make([]uint16, syscall.MAX_PATH)
size := uint32(len(b))
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
n = uint32(r0)
if n == 0 {
return "", e1
}
return string(utf16.Decode(b[0:n])), nil
}

@ -0,0 +1,42 @@
/*
package proto defines a set of structures used to negotiate an update between an
an application (the client) and an Equinox update service.
*/
package proto
import "time"
type PatchKind string
const (
PatchNone PatchKind = "none"
PatchBSDiff PatchKind = "bsdiff"
)
type Request struct {
AppID string `json:"app_id"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
GoARM string `json:"goarm"`
TargetVersion string `json:"target_version"`
CurrentVersion string `json:"current_version"`
CurrentSHA256 string `json:"current_sha256"`
}
type Response struct {
Available bool `json:"available"`
DownloadURL string `json:"download_url"`
Checksum string `json:"checksum"`
Signature string `json:"signature"`
Patch PatchKind `json:"patch_type"`
Release Release `json:"release"`
}
type Release struct {
Title string `json:"title"`
Version string `json:"version"`
Description string `json:"description"`
CreateDate time.Time `json:"create_date"`
}

@ -0,0 +1,305 @@
package equinox
import (
"bytes"
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"runtime"
"time"
"github.com/equinox-io/equinox/internal/go-update"
"github.com/equinox-io/equinox/internal/osext"
"github.com/equinox-io/equinox/proto"
)
const protocolVersion = "1"
const defaultCheckURL = "https://update.equinox.io/check"
const userAgent = "EquinoxSDK/1.0"
var NotAvailableErr = errors.New("No update available")
type Options struct {
// Channel specifies the name of an Equinox release channel to check for
// a newer version of the application.
//
// If empty, defaults to 'stable'.
Channel string
// Version requests an update to a specific version of the application.
// If specified, `Channel` is ignored.
Version string
// TargetPath defines the path to the file to update.
// The emptry string means 'the executable file of the running program'.
TargetPath string
// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
TargetMode os.FileMode
// Public key to use for signature verification. If nil, no signature
// verification is done. Use `SetPublicKeyPEM` to set this field with PEM data.
PublicKey crypto.PublicKey
// Target operating system of the update. Uses the same standard OS names used
// by Go build tags (windows, darwin, linux, etc).
// If empty, it will be populated by consulting runtime.GOOS
OS string
// Target architecture of the update. Uses the same standard Arch names used
// by Go build tags (amd64, 386, arm, etc).
// If empty, it will be populated by consulting runtime.GOARCH
Arch string
// Target ARM architecture, if a specific one if required. Uses the same names
// as the GOARM environment variable (5, 6, 7).
//
// GoARM is ignored if Arch != 'arm'.
// GoARM is ignored if it is the empty string. Omit it if you do not need
// to distinguish between ARM versions.
GoARM string
// The current application version. This is used for statistics and reporting only,
// it is optional.
CurrentVersion string
// CheckURL is the URL to request an update check from. You should only set
// this if you are running an on-prem Equinox server.
// If empty the default Equinox update service endpoint is used.
CheckURL string
// HTTPClient is used to make all HTTP requests necessary for the update check protocol.
// You may configure it to use custom timeouts, proxy servers or other behaviors.
HTTPClient *http.Client
}
// Response is returned by Check when an update is available. It may be
// passed to Apply to perform the update.
type Response struct {
// Version of the release that will be updated to if applied.
ReleaseVersion string
// Title of the the release
ReleaseTitle string
// Additional details about the release
ReleaseDescription string
// Creation date of the release
ReleaseDate time.Time
downloadURL string
checksum []byte
signature []byte
patch proto.PatchKind
opts Options
}
// SetPublicKeyPEM is a convenience method to set the PublicKey property
// used for checking a completed update's signature by parsing a
// Public Key formatted as PEM data.
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
block, _ := pem.Decode(pembytes)
if block == nil {
return errors.New("couldn't parse PEM data")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
o.PublicKey = pub
return nil
}
// Check communicates with an Equinox update service to determine if
// an update for the given application matching the specified options is
// available. The returned error is nil only if an update is available.
//
// The appID is issued to you when creating an application at https://equinox.io
//
// You can compare the returned error to NotAvailableErr to differentiate between
// a successful check that found no update from other errors like a failed
// network connection.
func Check(appID string, opts Options) (Response, error) {
var r Response
if opts.Channel == "" {
opts.Channel = "stable"
}
if opts.TargetPath == "" {
var err error
opts.TargetPath, err = osext.Executable()
if err != nil {
return r, err
}
}
if opts.OS == "" {
opts.OS = runtime.GOOS
}
if opts.Arch == "" {
opts.Arch = runtime.GOARCH
}
if opts.CheckURL == "" {
opts.CheckURL = defaultCheckURL
}
if opts.HTTPClient == nil {
opts.HTTPClient = new(http.Client)
}
opts.HTTPClient.Transport = newUserAgentTransport(userAgent, opts.HTTPClient.Transport)
checksum := computeChecksum(opts.TargetPath)
payload, err := json.Marshal(proto.Request{
AppID: appID,
Channel: opts.Channel,
OS: opts.OS,
Arch: opts.Arch,
GoARM: opts.GoARM,
TargetVersion: opts.Version,
CurrentVersion: opts.CurrentVersion,
CurrentSHA256: checksum,
})
req, err := http.NewRequest("POST", opts.CheckURL, bytes.NewReader(payload))
if err != nil {
return r, err
}
req.Header.Set("Accept", fmt.Sprintf("application/json; q=1; version=%s; charset=utf-8", protocolVersion))
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err := opts.HTTPClient.Do(req)
if err != nil {
return r, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := ioutil.ReadAll(resp.Body)
return r, fmt.Errorf("Server responded with %s: %s", resp.Status, body)
}
var protoResp proto.Response
err = json.NewDecoder(resp.Body).Decode(&protoResp)
if err != nil {
return r, err
}
if !protoResp.Available {
return r, NotAvailableErr
}
r.ReleaseVersion = protoResp.Release.Version
r.ReleaseTitle = protoResp.Release.Title
r.ReleaseDescription = protoResp.Release.Description
r.ReleaseDate = protoResp.Release.CreateDate
r.downloadURL = protoResp.DownloadURL
r.patch = protoResp.Patch
r.opts = opts
r.checksum, err = hex.DecodeString(protoResp.Checksum)
if err != nil {
return r, err
}
r.signature, err = hex.DecodeString(protoResp.Signature)
if err != nil {
return r, err
}
return r, nil
}
func computeChecksum(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer f.Close()
h := sha256.New()
_, err = io.Copy(h, f)
if err != nil {
return ""
}
return hex.EncodeToString(h.Sum(nil))
}
// Apply performs an update of the current executable (or TargetFile, if it was
// set on the Options) with the update specified by Response.
//
// Error is nil if and only if the entire update completes successfully.
func (r Response) Apply() error {
opts := update.Options{
TargetPath: r.opts.TargetPath,
TargetMode: r.opts.TargetMode,
Checksum: r.checksum,
Signature: r.signature,
Verifier: update.NewECDSAVerifier(),
PublicKey: r.opts.PublicKey,
}
switch r.patch {
case proto.PatchBSDiff:
opts.Patcher = update.NewBSDiffPatcher()
}
if err := opts.CheckPermissions(); err != nil {
return err
}
req, err := http.NewRequest("GET", r.downloadURL, nil)
if err != nil {
return err
}
// fetch the update
resp, err := r.opts.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// check that we got a patch
if resp.StatusCode >= 400 {
msg := "error downloading patch"
id := resp.Header.Get("Request-Id")
if id != "" {
msg += ", request " + id
}
blob, err := ioutil.ReadAll(resp.Body)
if err == nil {
msg += ": " + string(bytes.TrimSpace(blob))
}
return fmt.Errorf(msg)
}
return update.Apply(resp.Body, opts)
}
type userAgentTransport struct {
userAgent string
http.RoundTripper
}
func newUserAgentTransport(userAgent string, rt http.RoundTripper) *userAgentTransport {
if rt == nil {
rt = http.DefaultTransport
}
return &userAgentTransport{userAgent, rt}
}
func (t *userAgentTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if r.Header.Get("User-Agent") == "" {
r.Header.Set("User-Agent", t.userAgent)
}
return t.RoundTripper.RoundTrip(r)
}
Loading…
Cancel
Save