Compare commits

..

No commits in common. "master" and "fixup-travis" have entirely different histories.

44 changed files with 377 additions and 3847 deletions

2
.gitignore vendored
View file

@ -1,3 +1 @@
pkg
bin

View file

@ -1,8 +1,5 @@
language: go
sudo: false
go:
- 1.7.5
- 1.8.1
go: 1.1
script:
- go test -v ./...

View file

@ -1,62 +0,0 @@
# How to Contribute
This project is [Apache 2.0 licensed](LICENSE) and accept contributions via
GitHub-like pull requests. This document outlines some of the conventions on
development workflow, commit message formatting, contact points and other
resources to make it easier to get your contribution accepted.
# Certificate of Origin
By contributing to this project you agree to the Developer Certificate of
Origin (DCO). This document was created by the Linux Kernel community and is a
simple statement that you, as a contributor, have the legal right to make the
contribution. See the [DCO](DCO) file for details.
## Getting Started
- Fork the repository
- Read the [README](README.md) for build and test instructions
- Play with the project, submit bugs, submit patches!
## Contribution Flow
This is a rough outline of what a contributor's workflow looks like:
- Create a topic branch from where you want to base your work (usually master).
- Make commits of logical units.
- Make sure your commit messages are in the proper format (see below).
- Push your changes to a topic branch in your fork of the repository.
- Make sure the tests pass, and add any new tests as appropriate.
- Submit a pull request to the original repository.
Thanks for your contributions!
### Format of the Commit Message
We follow a rough convention for commit messages that is designed to answer two
questions: what changed and why. The subject line should feature the what and
the body of the commit should describe the why.
```
scripts: add the test-cluster command
this uses tmux to setup a test cluster that you can easily kill and
start for debugging.
Fixes #38
```
The format can be described more formally as follows:
```
<subsystem>: <what changed>
<BLANK LINE>
<why this change was made>
<BLANK LINE>
<footer>
```
The first line is the subject and should be no longer than 70 characters, the
second line is always blank, and other lines should be wrapped at 80 characters.
This allows the message to be easier to read on GitHub as well as in various
git tools.

36
DCO
View file

@ -1,36 +0,0 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

View file

@ -1,4 +1,5 @@
Apache License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -178,7 +179,7 @@ Apache License
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@ -186,7 +187,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -199,4 +200,3 @@ Apache License
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.

View file

@ -1,24 +0,0 @@
# kernel-style V=1 build verbosity
ifeq ("$(origin V)", "command line")
BUILD_VERBOSE = $(V)
endif
ifeq ($(BUILD_VERBOSE),1)
Q =
else
Q = @
endif
.PHONY: all
all: bin/serve-package
bin/serve-package:
$(Q)go build -o $@ cmd/serve-package/main.go
.PHONY: clean
clean:
$(Q)rm -rf bin
.PHONY: vendor
vendor:
$(Q)go mod vendor

5
NOTICE
View file

@ -1,5 +0,0 @@
CoreOS Project
Copyright 2017 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

View file

@ -1,71 +1,5 @@
# Go Omaha
Implementation of the omaha protocol in Go.
[![GoDoc](https://godoc.org/git.thisco.de/vbatts/go-omaha/omaha?status.svg)](https://godoc.org/git.thisco.de/vbatts/go-omaha/omaha)
https://code.google.com/p/omaha/
Implementation of the [omaha update protocol](https://github.com/google/omaha) in Go.
## Status
This code is targeted for use with CoreOS's [CoreUpdate](https://coreos.com/products/coreupdate/) product and the Container Linux [update_engine](https://github.com/coreos/update_engine).
As a result this is not a complete implementation of the [protocol](https://github.com/google/omaha/blob/master/doc/ServerProtocolV3.md) and inherits a number of quirks from update_engine.
These differences include:
- No offline activity tracking.
The protocol's ping mechanism allows for tracking application usage, reporting the number of days since the last ping and how many of those days saw active usage.
CoreUpdate does not use this, instead assuming update clients are always online and checking in once every ~45-50 minutes.
Clients not actively updating should send only a ping, indicating CoreUpdate's "Instance-Hold" state.
Clients requesting an update should send a ping, update check, and an UpdateComplete:SuccessReboot event indicating CoreUpdate's "Complete" state.
- Various protocol extensions/abuses.
update_engine, likely due to earlier limitations of the protocol and Google's server implementation, uses a number of non-standard fields.
For example, packing a lot of extra attributes such as the package's SHA-256 hash into a "postinstall" action.
As much as possible the code includes comments about these extensions.
- Many data fields not used by CoreUpdate are omitted.
## `serve-package`
This project includes a very simple program designed to serve a single Container Linux package on the local host. It is intended to be used as a manual updater for a machine that is not able to use a full-fledged CoreUpdate instance. Binaries are available for each released version on the [releases page](https://git.thisco.de/vbatts/go-omaha/releases). `serve-package` can also be built from source using the provided Makefile:
```bash
make
```
The binary will be available in the `bin/` folder.
It is recommended that the server be run directly on the machine you intend to update. Go to the [Container Linux release notes](https://coreos.com/releases/) and find the version number for the release you would like to update to. The update payload can be retrieved from
```
https://update.release.core-os.net/amd64-usr/<version>/update.gz
```
where `<version>` is the version number you retrieved from the releases page. For example, `https://update.release.core-os.net/amd64-usr/1576.4.0/update.gz` is the payload required to update to Container Linux version 1576.4.0.
Copy the update payload and the `serve-package` binary to the server you would like to update. `serve-package` can be run as follows:
```bash
./serve-package --package-file update.gz --package-version <version>
```
By default, the server listens on `localhost:8000`. This can be modified using the `--listen-address` option.
Next, `update_engine` needs to be configured to use the local server that was just set up:
```bash
echo "SERVER=http://localhost:8000/v1/update" | sudo tee -a /etc/coreos/update.conf
```
Restart `update_engine` and tell it to check for an update:
```bash
sudo systemctl restart update-engine.service
update_engine_client -check_for_update
```
If `locksmithd.service` is running, the machine will restart once it has updated to the latest version. Otherwise, watch the logs from `update-engine.service` to determine when the update is complete and the machine is ready to restart:
```bash
journalctl -u update-engine.service -f
# wait for a line that says "Update successfully applied, waiting for reboot"
sudo systemctl reboot
```
[![Build Status](https://travis-ci.org/coreos/go-omaha.png)](https://travis-ci.org/coreos/go-omaha)

View file

@ -1,46 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
"git.thisco.de/vbatts/go-omaha/omaha"
)
func main() {
pkgfile := flag.String("package-file", "", "Path to the update payload")
version := flag.String("package-version", "", "Semantic version of the package provided")
listenAddress := flag.String("listen-address", ":8000", "Host and IP to listen on")
flag.Parse()
if *pkgfile == "" {
fmt.Println("package-file is a required flag")
os.Exit(1)
}
if *version == "" {
fmt.Println("package-version is a required flag")
os.Exit(1)
}
server, err := omaha.NewTrivialServer(*listenAddress)
if err != nil {
fmt.Printf("failed to make new server: %v\n", err)
os.Exit(1)
}
server.SetVersion(*version)
err = server.AddPackage(*pkgfile, "update.gz")
if err != nil {
fmt.Printf("failed to add package: %v\n", err)
os.Exit(1)
}
err = server.Serve()
if err != nil {
fmt.Printf("server exited with an error: %v\n", err)
os.Exit(1)
}
}

View file

@ -1,40 +0,0 @@
## Kinvolk Community Code of Conduct
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently applying these
principles to every aspect of managing this project. Project maintainers who do
not follow or enforce the Code of Conduct may be permanently removed from the
project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer, Vincent Batts <vbatts@thisco.de>
This Code of Conduct is adapted from the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" installsource="ondemandupdate" ismachine="1">
<os version="Indy" platform="Chrome OS" sp="ForcedUpdate_x86_64"></os>
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" bootid="{7D52A1CC-7066-40F0-91C7-7CB6A871BFDE}" machineid="{8BDE4C4D-9083-4D61-B41C-3253212C0C37}" oem="ec3000" version="ForcedUpdate" track="dev-channel" from_track="developer-build" lang="en-US" board="amd64-generic" hardware_class="" delta_okay="false" >
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" version="ForcedUpdate" track="" from_track="developer-build" lang="en-US" board="amd64-generic" hardware_class="" delta_okay="false" >
<ping active="1" a="-1" r="-1"></ping>
<updatecheck targetversionprefix=""></updatecheck>
<event eventtype="3" eventresult="2" previousversion=""></event>

9
go.mod
View file

@ -1,9 +0,0 @@
module git.thisco.de/vbatts/go-omaha
go 1.15
require (
github.com/blang/semver v3.5.1+incompatible
github.com/kylelemons/godebug v1.1.0
github.com/satori/go.uuid v1.1.0
)

8
go.sum
View file

@ -1,8 +0,0 @@
git.thisco.de/vbatts/go-omaha v0.0.0-20180327202221-e409d983eb60 h1:lqjtEFY5RgNgWJWOv0KUW0Llvh0+Ved7nWIqFEssY34=
git.thisco.de/vbatts/go-omaha v0.0.0-20180327202221-e409d983eb60/go.mod h1:w+Vl1DisYu3l2h88jvqZDV414oSIcTY8YrZLOa/vLME=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/satori/go.uuid v1.1.0 h1:B9KXyj+GzIpJbV7gmr873NsY6zpbxNy24CBtGrk7jHo=
github.com/satori/go.uuid v1.1.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=

View file

@ -1,368 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
// Package client provides a general purpose Omaha update client implementation.
package client
import (
"errors"
"fmt"
"net/url"
"time"
uuid "github.com/satori/go.uuid"
"git.thisco.de/vbatts/go-omaha/omaha"
)
const (
defaultClientVersion = "go-omaha"
// periodic update check and ping intervals
pingFuzz = 10 * time.Minute
pingDelay = 7 * time.Minute // first check after 2-12 minutes
pingInterval = 45 * time.Minute // check in every 40-50 minutes
)
// Client supports managing multiple apps using a single server.
type Client struct {
apiClient *httpClient
apiEndpoint string
clientVersion string
userID string
sessionID string
isMachine bool
sentPing bool
apps map[string]*AppClient
}
// AppClient supports managing a single application.
type AppClient struct {
*Client
appID string
track string
version string
oem string
}
// New creates an omaha client for updating one or more applications.
// userID must be a persistent unique identifier of this update client.
func New(serverURL, userID string) (*Client, error) {
if userID == "" {
return nil, errors.New("omaha: empty user identifier")
}
c := &Client{
apiClient: newHTTPClient(),
clientVersion: defaultClientVersion,
userID: userID,
sessionID: uuid.NewV4().String(),
apps: make(map[string]*AppClient),
}
if err := c.SetServerURL(serverURL); err != nil {
return nil, err
}
return c, nil
}
// SetServerURL changes the Omaha server this client talks to.
// If the URL does not include a path component /v1/update/ is assumed.
func (c *Client) SetServerURL(serverURL string) error {
u, err := url.Parse(serverURL)
if err != nil {
return fmt.Errorf("omaha: invalid server URL: %v", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("omaha: invalid server protocol: %s", u)
}
if u.Host == "" {
return fmt.Errorf("omaha: invalid server host: %s", u)
}
if u.Path == "" || u.Path == "/" {
u.Path = "/v1/update/"
}
c.apiEndpoint = u.String()
return nil
}
// SetClientVersion sets the identifier of this updater application.
// e.g. "update_engine-0.1.0". Default is "go-omaha".
func (c *Client) SetClientVersion(clientVersion string) {
c.clientVersion = clientVersion
}
// NextPing returns a timer channel that will fire when the next update
// check or ping should be sent.
func (c *Client) NextPing() <-chan time.Time {
d := pingDelay
if c.sentPing {
d = pingInterval
}
return FuzzyAfter(d, pingFuzz)
}
// AppClient gets the application client for the given application ID.
func (c *Client) AppClient(appID string) (*AppClient, error) {
if app, ok := c.apps[appID]; ok {
return app, nil
}
return nil, fmt.Errorf("omaha: missing app client %q", appID)
}
// NewAppClient creates a new application client.
func (c *Client) NewAppClient(appID, appVersion string) (*AppClient, error) {
if _, ok := c.apps[appID]; ok {
return nil, fmt.Errorf("omaha: duplicate app client %q", appID)
}
ac := &AppClient{
Client: c,
appID: appID,
}
c.apps[appID] = ac
return ac, nil
}
// NewAppClient creates a single application client.
// Shorthand for New(serverURL, userID).NewAppClient(appID, appVersion).
func NewAppClient(serverURL, userID, appID, appVersion string) (*AppClient, error) {
c, err := New(serverURL, userID)
if err != nil {
return nil, err
}
ac, err := c.NewAppClient(appID, appVersion)
if err := ac.SetVersion(appVersion); err != nil {
return nil, err
}
return ac, nil
}
func (ac *AppClient) SetAppID(appID string) error {
if appID == ac.appID {
return nil
}
if _, ok := ac.apps[appID]; ok {
return fmt.Errorf("omaha: duplicate app %q", appID)
}
delete(ac.apps, ac.appID)
ac.appID = appID
ac.apps[appID] = ac
return nil
}
// SetVersion changes the application version.
func (ac *AppClient) SetVersion(version string) error {
if version == "" {
return errors.New("omaha: empty application version")
}
ac.version = version
return nil
}
// SetTrack sets the application update track or group.
// This is a update_engine/Core Update protocol extension.
func (ac *AppClient) SetTrack(track string) error {
// Although track is an omaha extension and theoretically not required
// our Core Update server requires track to be set to a valid id/name.
// TODO: deprecate track and use the standard cohort protocol fields.
if track == "" {
return errors.New("omaha: empty application update track/group")
}
ac.track = track
return nil
}
// SetOEM sets the application OEM name.
// This is a update_engine/Core Update protocol extension.
func (ac *AppClient) SetOEM(oem string) {
ac.oem = oem
}
func (ac *AppClient) UpdateCheck() (*omaha.UpdateResponse, error) {
req := ac.NewAppRequest()
app := req.Apps[0]
app.AddPing()
app.AddUpdateCheck()
// Tell CoreUpdate to consider us in its "Complete" state,
// otherwise it interprets ping as "Instance-Hold" which is
// nonsense when we are sending an update check!
app.Events = append(app.Events, EventComplete)
ac.sentPing = true
appResp, err := ac.SendAppRequest(req)
if err != nil {
return nil, err
}
// BUG: CoreUpdate does not send ping status in response.
/*if appResp.Ping == nil {
ac.Event(NewErrorEvent(ExitCodeOmahaResponseInvalid))
return nil, fmt.Errorf("omaha: ping status missing from response")
}
if appResp.Ping.Status != "ok" {
return nil, fmt.Errorf("omaha: ping status %s", appResp.Ping.Status)
}*/
if appResp.UpdateCheck == nil {
ac.Event(NewErrorEvent(ExitCodeOmahaResponseInvalid))
return nil, fmt.Errorf("omaha: update check missing from response")
}
if appResp.UpdateCheck.Status != omaha.UpdateOK {
return nil, appResp.UpdateCheck.Status
}
return appResp.UpdateCheck, nil
}
func (ac *AppClient) Ping() error {
req := ac.NewAppRequest()
app := req.Apps[0]
app.AddPing()
ac.sentPing = true
appResp, err := ac.SendAppRequest(req)
if err != nil {
return err
}
// BUG: CoreUpdate does not send ping status in response.
_ = appResp
/*if appResp.Ping == nil {
ac.Event(NewErrorEvent(ExitCodeOmahaResponseInvalid))
return fmt.Errorf("omaha: ping status missing from response")
}
if appResp.Ping.Status != "ok" {
return fmt.Errorf("omaha: ping status %s", appResp.Ping.Status)
}*/
return nil
}
// Event asynchronously sends the given omaha event.
// Reading the error channel is optional.
func (ac *AppClient) Event(event *omaha.EventRequest) <-chan error {
errc := make(chan error, 1)
url := ac.apiEndpoint
req := ac.NewAppRequest()
app := req.Apps[0]
app.Events = append(app.Events, event)
go func() {
appResp, err := ac.doReq(url, req)
if err != nil {
errc <- err
return
}
// BUG: CoreUpdate does not send event status in response.
_ = appResp
/*if len(appResp.Events) == 0 {
errc <- fmt.Errorf("omaha: event status missing from response")
return
}
if appResp.Events[0].Status != "ok" {
errc <- fmt.Errorf("omaha: event status %s", appResp.Events[0].Status)
return
}*/
errc <- nil
return
}()
return errc
}
// NewAppRequest creates a Request object containing one application.
func (ac *AppClient) NewAppRequest() *omaha.Request {
req := omaha.NewRequest()
req.Version = ac.clientVersion
req.UserID = ac.userID
req.SessionID = ac.sessionID
if ac.isMachine {
req.IsMachine = 1
}
app := req.AddApp(ac.appID, ac.version)
app.Track = ac.track
app.OEM = ac.oem
// MachineID and BootID are non-standard fields used by CoreOS'
// update_engine and Core Update. Copy their values from the
// standard UserID and SessionID. Eventually the non-standard
// fields should be deprecated.
app.MachineID = req.UserID
app.BootID = req.SessionID
return req
}
// SendAppRequest sends a Request object and validates the response.
// On failure an error event is automatically sent to the server.
func (ac *AppClient) SendAppRequest(req *omaha.Request) (*omaha.AppResponse, error) {
resp, err := ac.doReq(ac.apiEndpoint, req)
if _, ok := err.(omaha.AppStatus); ok {
// No point to sending an error if we got a well-formed
// non-ok application status in the response.
} else if err, ok := err.(ErrorEvent); ok {
ac.Event(err.ErrorEvent())
} else if err != nil {
ac.Event(NewErrorEvent(ExitCodeOmahaRequestError))
}
return resp, err
}
// doReq posts an omaha request. It may be called in its own goroutine so
// it should not touch any mutable data in AppClient, but apiClient is ok.
func (ac *AppClient) doReq(url string, req *omaha.Request) (*omaha.AppResponse, error) {
if len(req.Apps) != 1 {
panic(fmt.Errorf("unexpected number of apps: %d", len(req.Apps)))
}
appID := req.Apps[0].ID
resp, err := ac.apiClient.Omaha(url, req)
if err != nil {
return nil, err
}
appResp := resp.GetApp(appID)
if appResp == nil {
return nil, &omahaError{
Err: fmt.Errorf("app %s missing from response", appID),
Code: ExitCodeOmahaResponseInvalid,
}
}
if appResp.Status != omaha.AppOK {
return nil, appResp.Status
}
return appResp, nil
}

View file

@ -1,199 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"reflect"
"testing"
"git.thisco.de/vbatts/go-omaha/omaha"
)
// implements omaha.Updater
type recorder struct {
t *testing.T
update *omaha.Update
checks []*omaha.UpdateRequest
events []*omaha.EventRequest
pings []*omaha.PingRequest
}
func newRecordingServer(t *testing.T, u *omaha.Update) (*recorder, *omaha.Server) {
r := &recorder{t: t, update: u}
s, err := omaha.NewServer("127.0.0.1:0", r)
if err != nil {
t.Fatal(err)
}
go s.Serve()
return r, s
}
func (r *recorder) CheckApp(req *omaha.Request, app *omaha.AppRequest) error {
// CheckApp is meant for checking if app.ID is valid but we don't
// care and accept any ID. Instead this is just a convenient place
// to check that all requests are well formed.
if len(req.SessionID) != 36 {
r.t.Errorf("SessionID %q is not a UUID", req.SessionID)
}
if app.BootID != req.SessionID {
r.t.Errorf("BootID %q != SessionID %q", app.BootID, req.SessionID)
}
if req.UserID == "" {
r.t.Error("UserID is blank")
}
if app.MachineID != req.UserID {
r.t.Errorf("MachineID %q != UserID %q", app.MachineID, req.UserID)
}
if app.Version == "" {
r.t.Error("App Version is blank")
}
return nil
}
func (r *recorder) CheckUpdate(req *omaha.Request, app *omaha.AppRequest) (*omaha.Update, error) {
r.checks = append(r.checks, app.UpdateCheck)
if r.update == nil {
return nil, omaha.NoUpdate
} else {
return r.update, nil
}
}
func (r *recorder) Event(req *omaha.Request, app *omaha.AppRequest, event *omaha.EventRequest) {
r.events = append(r.events, event)
}
func (r *recorder) Ping(req *omaha.Request, app *omaha.AppRequest) {
r.pings = append(r.pings, app.Ping)
}
func TestClientNoUpdate(t *testing.T) {
r, s := newRecordingServer(t, nil)
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
if _, err := ac.UpdateCheck(); err != omaha.NoUpdate {
t.Fatalf("UpdateCheck id not return NoUpdate: %v", err)
}
if len(r.pings) != 1 {
t.Fatalf("expected 1 ping, not %d", len(r.pings))
}
if len(r.checks) != 1 {
t.Fatalf("expected 1 update check, not %d", len(r.checks))
}
if len(r.events) != 1 {
t.Fatalf("expected 1 event, not %d", len(r.events))
}
if r.events[0].Type != omaha.EventTypeUpdateComplete ||
r.events[0].Result != omaha.EventResultSuccessReboot {
t.Fatalf("expected %#v, not %#v", EventComplete, r.events[0])
}
}
func TestClientWithUpdate(t *testing.T) {
r, s := newRecordingServer(t, &omaha.Update{
Manifest: omaha.Manifest{
Version: "1.1.1",
},
})
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
update, err := ac.UpdateCheck()
if err != nil {
t.Fatal(err)
}
if update.Manifest.Version != "1.1.1" {
t.Fatalf("expected version 1.1.1, not %s", update.Manifest.Version)
}
if len(r.pings) != 1 {
t.Fatalf("expected 1 ping, not %d", len(r.pings))
}
if len(r.checks) != 1 {
t.Fatalf("expected 1 update check, not %d", len(r.checks))
}
if len(r.events) != 1 {
t.Fatalf("expected 1 event, not %d", len(r.events))
}
if r.events[0].Type != omaha.EventTypeUpdateComplete ||
r.events[0].Result != omaha.EventResultSuccessReboot {
t.Fatalf("expected %#v, not %#v", EventComplete, r.events[0])
}
}
func TestClientPing(t *testing.T) {
r, s := newRecordingServer(t, nil)
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
if err := ac.Ping(); err != nil {
t.Fatal(err)
}
if len(r.pings) != 1 {
t.Fatalf("expected 1 ping, not %d", len(r.pings))
}
}
func TestClientEvent(t *testing.T) {
r, s := newRecordingServer(t, nil)
defer s.Destroy()
url := "http://" + s.Addr().String()
ac, err := NewAppClient(url, "client-id", "app-id", "0.0.0")
if err != nil {
t.Fatal(err)
}
event := &omaha.EventRequest{
Type: omaha.EventTypeDownloadComplete,
Result: omaha.EventResultSuccess,
}
if err := <-ac.Event(event); err != nil {
t.Fatal(err)
}
if len(r.events) != 1 {
t.Fatalf("expected 1 event, not %d", len(r.events))
}
if !reflect.DeepEqual(event, r.events[0]) {
t.Fatalf("sent != received:\n%#v\n%#v", event, r.events[0])
}
}

View file

@ -1,129 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"encoding/xml"
"errors"
"io"
"net"
"net/http"
"time"
"git.thisco.de/vbatts/go-omaha/omaha"
)
var (
bodySizeError = &omahaError{
Err: errors.New("http response exceeded 1MB"),
Code: ExitCodeOmahaResponseInvalid,
}
bodyEmptyError = &omahaError{
Err: errors.New("http response was empty"),
Code: ExitCodeOmahaRequestEmptyResponseError,
}
// default parameters for expNetBackoff
backoffStart = time.Second
backoffTries = 7
)
// retries and exponentially backs off for temporary network errors
func expNetBackoff(f func() error) error {
var (
backoff = backoffStart
tries = backoffTries
)
for {
err := f()
tries--
if tries <= 0 {
return err
}
if neterr, ok := err.(net.Error); !ok || !neterr.Temporary() {
return err
}
FuzzySleep(backoff, backoff)
backoff *= 2
}
}
// xml doesn't return the standard io.ErrUnexpectedEOF so check for both.
func isUnexpectedEOF(err error) bool {
if xerr, ok := err.(*xml.SyntaxError); ok {
return xerr.Msg == "unexpected EOF"
}
return err == io.ErrUnexpectedEOF
}
// omahaError implements error and ErrorEvent for omaha requests/responses.
type omahaError struct {
Err error
Code ExitCode
}
func (oe *omahaError) Error() string {
return "omaha: request failed: " + oe.Err.Error()
}
func (oe *omahaError) ErrorEvent() *omaha.EventRequest {
return NewErrorEvent(oe.Code)
}
// httpError implements error, net.Error, and ErrorEvent for http responses.
type httpError struct {
*http.Response
}
func (he *httpError) Error() string {
return "http error: " + he.Status
}
func (he *httpError) ErrorEvent() *omaha.EventRequest {
code := ExitCodeOmahaRequestError
if he.StatusCode > 0 && he.StatusCode < 1000 {
code = ExitCodeOmahaRequestHTTPResponseBase + ExitCode(he.StatusCode)
}
return NewErrorEvent(code)
}
func (he *httpError) Timeout() bool {
switch he.StatusCode {
case http.StatusRequestTimeout: // 408
return true
case http.StatusGatewayTimeout: // 504
return true
default:
return false
}
}
func (he *httpError) Temporary() bool {
if he.Timeout() {
return true
}
switch he.StatusCode {
case http.StatusTooManyRequests: // 429
return true
case http.StatusInternalServerError: // 500
return true
case http.StatusBadGateway: // 502
return true
case http.StatusServiceUnavailable: // 503
return true
default:
return false
}
}

View file

@ -1,49 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"testing"
"time"
)
func init() {
// use quicker backoff for testing
backoffStart = time.Millisecond
backoffTries = 3
}
type tmpErr struct{}
func (e tmpErr) Error() string { return "fake temporary error" }
func (e tmpErr) Temporary() bool { return true }
func (e tmpErr) Timeout() bool { return false }
func TestExpNetBackoff(t *testing.T) {
tries := 0
err := expNetBackoff(func() error {
tries++
if tries < 2 {
return tmpErr{}
}
return nil
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if tries != 2 {
t.Errorf("unexpected # of tries: %d", tries)
}
}

View file

@ -1,101 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"fmt"
"os"
//"os/signal"
"syscall"
"git.thisco.de/vbatts/go-omaha/omaha"
)
func Example() {
// Launch a dummy server for our client to talk to.
s, err := omaha.NewTrivialServer("127.0.0.1:0")
if err != nil {
fmt.Println(err)
return
}
defer s.Destroy()
go s.Serve()
// Configure our client. userID should be random but preserved
// across restarts. version is the current version of our app.
var (
serverURL = "http://" + s.Addr().String()
userID = "8b10fc6d-30ca-49b2-b1a2-8185f03d522b"
appID = "5ca607f8-61b5-4692-90ce-30380ba05a98"
version = "1.0.0"
)
c, err := NewAppClient(serverURL, userID, appID, version)
if err != nil {
fmt.Println(err)
return
}
// Client version is the name and version of this updater.
c.SetClientVersion("example-0.0.1")
// Use SIGUSR1 to trigger immediate update checks.
sigc := make(chan os.Signal, 1)
//signal.Notify(sigc, syscall.SIGUSR1)
sigc <- syscall.SIGUSR1 // Fake it
//for {
var source string
select {
case <-sigc:
source = "ondemandupdate"
case <-c.NextPing():
source = "scheduler"
}
// TODO: pass source to UpdateCheck
_ = source
// If updates are disabled call c.Ping() instead.
update, err := c.UpdateCheck()
if err != nil {
fmt.Println(err)
//continue
return
}
// Download new application version.
c.Event(&omaha.EventRequest{
Type: omaha.EventTypeUpdateDownloadFinished,
Result: omaha.EventResultSuccess,
})
// Install new application version here.
c.Event(&omaha.EventRequest{
Type: omaha.EventTypeUpdateComplete,
Result: omaha.EventResultSuccess,
})
// Restart, new application is now running.
c.SetVersion(update.Manifest.Version)
c.Event(&omaha.EventRequest{
Type: omaha.EventTypeUpdateComplete,
Result: omaha.EventResultSuccessReboot,
})
//}
// Output:
// omaha: update status noupdate
}

View file

@ -1,53 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"math/rand"
"time"
)
func init() {
// Ensure seeding the prng is never forgotten, that would defeat
// the whole point of using fuzzy timers to guard against a DoS.
rand.Seed(time.Now().UnixNano())
}
// FuzzyDuration randomizes the duration d within the range specified
// by fuzz. Specifically the value range is: [d-(fuzz/2), d+(fuzz/2)]
// The result will never be negative.
func FuzzyDuration(d, fuzz time.Duration) time.Duration {
if fuzz < 0 {
return d
}
// apply range [-fuzz/2, fuzz/2]
d += time.Duration(rand.Int63n(int64(fuzz)+1) - (int64(fuzz) / 2))
if d < 0 {
return 0
}
return d
}
// FuzzyAfter waits for the fuzzy duration to elapse and then sends the
// current time on the returned channel. See FuzzyDuration.
func FuzzyAfter(d, fuzz time.Duration) <-chan time.Time {
return time.After(FuzzyDuration(d, fuzz))
}
// FuzzySleep pauses the current goroutine for the fuzzy duration d.
// See FuzzyDuration.
func FuzzySleep(d, fuzz time.Duration) {
time.Sleep(FuzzyDuration(d, fuzz))
}

View file

@ -1,32 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"testing"
"time"
)
func TestFuzzyDuration(t *testing.T) {
const d = time.Minute
for i := 0; i < 1000; i++ {
f := FuzzyDuration(d, d)
if f < d/2 {
t.Errorf("%d < %d", f, d/2)
} else if f > d+d/2 {
t.Errorf("%d > %d", f, d+d/2)
}
}
}

View file

@ -1,88 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
"git.thisco.de/vbatts/go-omaha/omaha"
)
const (
defaultTimeout = 90 * time.Second
)
// httpClient extends the standard http.Client to support xml encoding
// and decoding as well as automatic retries on transient failures.
type httpClient struct {
http.Client
}
func newHTTPClient() *httpClient {
return &httpClient{http.Client{
Timeout: defaultTimeout,
}}
}
// doPost sends a single HTTP POST, returning a parsed omaha response.
func (hc *httpClient) doPost(url string, reqBody []byte) (*omaha.Response, error) {
resp, err := hc.Post(url, "text/xml; charset=utf-8", bytes.NewReader(reqBody))
if err != nil {
return nil, &omahaError{err, ExitCodeOmahaRequestError}
}
defer resp.Body.Close()
// A response over 1M in size is certainly bogus.
respBody := &io.LimitedReader{R: resp.Body, N: 1024 * 1024}
contentType := resp.Header.Get("Content-Type")
omahaResp, err := omaha.ParseResponse(contentType, respBody)
// Report a more sensible error if we truncated the body.
if isUnexpectedEOF(err) && respBody.N <= 0 {
err = bodySizeError
} else if err == io.EOF {
err = bodyEmptyError
} else if err != nil {
err = &omahaError{err, ExitCodeOmahaRequestXMLParseError}
}
// Prefer reporting HTTP errors over XML parsing errors.
if resp.StatusCode != http.StatusOK {
err = &httpError{resp}
}
return omahaResp, err
}
// Omaha encodes and sends an omaha request, retrying on any transient errors.
func (hc *httpClient) Omaha(url string, req *omaha.Request) (resp *omaha.Response, err error) {
buf := bytes.NewBufferString(xml.Header)
enc := xml.NewEncoder(buf)
if err := enc.Encode(req); err != nil {
return nil, fmt.Errorf("omaha: failed to encode request: %v", err)
}
expNetBackoff(func() error {
resp, err = hc.doPost(url, buf.Bytes())
return err
})
return resp, err
}

View file

@ -1,212 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package client
import (
"bytes"
"net"
"net/http"
"strings"
"testing"
"git.thisco.de/vbatts/go-omaha/omaha"
)
const (
sampleRequest = `<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" installsource="ondemandupdate" ismachine="1">
<os version="Indy" platform="Chrome OS" sp="ForcedUpdate_x86_64"></os>
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" bootid="{7D52A1CC-7066-40F0-91C7-7CB6A871BFDE}" machineid="{8BDE4C4D-9083-4D61-B41C-3253212C0C37}" oem="ec3000" version="ForcedUpdate" track="dev-channel" from_track="developer-build" lang="en-US" board="amd64-generic" hardware_class="" delta_okay="false" >
<ping active="1" a="-1" r="-1"></ping>
<updatecheck targetversionprefix=""></updatecheck>
<event eventtype="3" eventresult="2" previousversion=""></event>
</app>
</request>
`
)
func TestHTTPClientDoPost(t *testing.T) {
s, err := omaha.NewTrivialServer("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer s.Destroy()
go s.Serve()
c := newHTTPClient()
url := "http://" + s.Addr().String() + "/v1/update/"
resp, err := c.doPost(url, []byte(sampleRequest))
if err != nil {
t.Fatal(err)
}
if len(resp.Apps) != 1 {
t.Fatalf("Should be 1 app, not %d", len(resp.Apps))
}
if resp.Apps[0].Status != omaha.AppOK {
t.Fatalf("Bad apps status: %q", resp.Apps[0].Status)
}
}
type flakyHandler struct {
omaha.OmahaHandler
flakes int
reqs int
}
func (f *flakyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.reqs++
if f.flakes > 0 {
f.flakes--
http.Error(w, "Flake!", http.StatusInternalServerError)
return
}
f.OmahaHandler.ServeHTTP(w, r)
}
type flakyServer struct {
l net.Listener
s *http.Server
h *flakyHandler
}
func newFlakyServer() (*flakyServer, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
f := &flakyServer{
l: l,
s: &http.Server{},
h: &flakyHandler{
OmahaHandler: omaha.OmahaHandler{
Updater: omaha.UpdaterStub{},
},
flakes: 1,
},
}
f.s.Handler = f.h
go f.s.Serve(l)
return f, nil
}
func TestHTTPClientError(t *testing.T) {
f, err := newFlakyServer()
if err != nil {
t.Fatal(err)
}
defer f.l.Close()
c := newHTTPClient()
url := "http://" + f.l.Addr().String()
_, err = c.doPost(url, []byte(sampleRequest))
switch err := err.(type) {
case nil:
t.Fatal("doPost succeeded but should have failed")
case *httpError:
if err.StatusCode != http.StatusInternalServerError {
t.Fatalf("Unexpected http error: %v", err)
}
if err.Timeout() {
t.Fatal("http 500 error reported as timeout")
}
if !err.Temporary() {
t.Fatal("http 500 error not reported as temporary")
}
default:
t.Fatalf("Unexpected error: %v", err)
}
}
func TestHTTPClientRetry(t *testing.T) {
f, err := newFlakyServer()
if err != nil {
t.Fatal(err)
}
defer f.l.Close()
req, err := omaha.ParseRequest("", strings.NewReader(sampleRequest))
if err != nil {
t.Fatal(err)
}
c := newHTTPClient()
url := "http://" + f.l.Addr().String()
resp, err := c.Omaha(url, req)
if err != nil {
t.Fatal(err)
}
if len(resp.Apps) != 1 {
t.Fatalf("Should be 1 app, not %d", len(resp.Apps))
}
if resp.Apps[0].Status != omaha.AppOK {
t.Fatalf("Bad apps status: %q", resp.Apps[0].Status)
}
if f.h.reqs != 2 {
t.Fatalf("Server received %d requests, not 2", f.h.reqs)
}
}
// should result in an unexected EOF
func largeHandler1(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><response protocol="3.0">`))
w.Write(bytes.Repeat([]byte{' '}, 2*1024*1024))
w.Write([]byte(`</response>`))
}
// should result in an EOF
func largeHandler2(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
w.Write(bytes.Repeat([]byte{' '}, 2*1024*1024))
}
func TestHTTPClientLarge(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
s := &http.Server{
Handler: http.HandlerFunc(largeHandler1),
}
go s.Serve(l)
c := newHTTPClient()
url := "http://" + l.Addr().String()
_, err = c.doPost(url, []byte(sampleRequest))
if err != bodySizeError {
t.Errorf("Unexpected error: %v", err)
}
// switch to failing before XML is read instead of half-way
// through (which results in a different error internally)
s.Handler = http.HandlerFunc(largeHandler2)
_, err = c.doPost(url, []byte(sampleRequest))
if err != bodyEmptyError {
t.Errorf("Unexpected error: %v", err)
}
}

View file

@ -1,75 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
// +build linux
package client
import (
"bytes"
"fmt"
"io/ioutil"
)
const (
machineIDPath = "/etc/machine-id"
bootIDPath = "/proc/sys/kernel/random/boot_id"
)
// NewMachineClient creates a machine-wide client, updating applications
// that may be used by multiple users. On Linux the system's machine id
// is used as the user id, and boot id is used as the omaha session id.
func NewMachineClient(serverURL string) (*Client, error) {
machineID, err := ioutil.ReadFile(machineIDPath)
if err != nil {
fmt.Errorf("omaha: failed to read machine id: %v", err)
}
machineID = bytes.TrimSpace(machineID)
// Although machineID should be a UUID, it is formatted as a
// plain hex string, omitting the normal '-' separators, so it
// should be 32 bytes long. It would be nice to reformat it to
// add the '-' chars but update_engine doesn't so stick with its
// behavior for now.
if len(machineID) < 32 {
fmt.Errorf("omaha: incomplete machine id: %q",
machineID)
}
bootID, err := ioutil.ReadFile(bootIDPath)
if err != nil {
fmt.Errorf("omaha: failed to read boot id: %v", err)
}
bootID = bytes.TrimSpace(bootID)
// unlike machineID, bootID *does* include '-' chars.
if len(bootID) < 36 {
fmt.Errorf("omaha: incomplete boot id: %q", bootID)
}
c := &Client{
apiClient: newHTTPClient(),
clientVersion: defaultClientVersion,
userID: string(machineID),
sessionID: string(bootID),
isMachine: true,
apps: make(map[string]*AppClient),
}
if err := c.SetServerURL(serverURL); err != nil {
return nil, err
}
return c, nil
}

View file

@ -1,48 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
// +build linux
package client
import (
"bytes"
"io/ioutil"
"testing"
)
// skip test if external file isn't readable
func readOrSkip(t *testing.T, name string) string {
data, err := ioutil.ReadFile(name)
if err != nil {
t.Skip(err)
}
return string(bytes.TrimSpace(data))
}
func TestNewMachine(t *testing.T) {
userID := readOrSkip(t, machineIDPath)
sessionID := readOrSkip(t, bootIDPath)
c, err := NewMachineClient("https://example.com")
if err != nil {
t.Fatal(err)
}
if c.userID != userID {
t.Errorf("%q != %q", c.userID, userID)
}
if c.sessionID != sessionID {
t.Errorf("%q != %q", c.sessionID, sessionID)
}
}

View file

@ -1,226 +0,0 @@
// Copyright 2017 CoreOS, Inc.
// Copyright 2011 The Chromium OS Authors.
//
// 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.
package client
import (
"fmt"
"git.thisco.de/vbatts/go-omaha/omaha"
)
var (
// These events are what update_engine sends to CoreUpdate to
// mark different steps in the update process.
EventDownloading = &omaha.EventRequest{
Type: omaha.EventTypeUpdateDownloadStarted,
Result: omaha.EventResultSuccess,
}
EventDownloaded = &omaha.EventRequest{
Type: omaha.EventTypeUpdateDownloadFinished,
Result: omaha.EventResultSuccess,
}
EventInstalled = &omaha.EventRequest{
Type: omaha.EventTypeUpdateComplete,
Result: omaha.EventResultSuccess,
}
EventComplete = &omaha.EventRequest{
Type: omaha.EventTypeUpdateComplete,
Result: omaha.EventResultSuccessReboot,
}
)
// ExitCode is used for omaha event error codes derived from update_engine
type ExitCode int
// These error codes are from CoreOS Container Linux update_engine 0.4.x
// https://github.com/coreos/update_engine/blob/master/src/update_engine/action_processor.h
// The whole list is included for the sake of completeness but lots of these
// are not generally applicable and not even used by update_engine any more.
// Also there are clearly duplicate errors for the same condition.
const (
ExitCodeSuccess ExitCode = 0
ExitCodeError ExitCode = 1
ExitCodeOmahaRequestError ExitCode = 2
ExitCodeOmahaResponseHandlerError ExitCode = 3
ExitCodeFilesystemCopierError ExitCode = 4
ExitCodePostinstallRunnerError ExitCode = 5
ExitCodeSetBootableFlagError ExitCode = 6
ExitCodeInstallDeviceOpenError ExitCode = 7
ExitCodeKernelDeviceOpenError ExitCode = 8
ExitCodeDownloadTransferError ExitCode = 9
ExitCodePayloadHashMismatchError ExitCode = 10
ExitCodePayloadSizeMismatchError ExitCode = 11
ExitCodeDownloadPayloadVerificationError ExitCode = 12
ExitCodeDownloadNewPartitionInfoError ExitCode = 13
ExitCodeDownloadWriteError ExitCode = 14
ExitCodeNewRootfsVerificationError ExitCode = 15
ExitCodeNewKernelVerificationError ExitCode = 16
ExitCodeSignedDeltaPayloadExpectedError ExitCode = 17
ExitCodeDownloadPayloadPubKeyVerificationError ExitCode = 18
ExitCodePostinstallBootedFromFirmwareB ExitCode = 19
ExitCodeDownloadStateInitializationError ExitCode = 20
ExitCodeDownloadInvalidMetadataMagicString ExitCode = 21
ExitCodeDownloadSignatureMissingInManifest ExitCode = 22
ExitCodeDownloadManifestParseError ExitCode = 23
ExitCodeDownloadMetadataSignatureError ExitCode = 24
ExitCodeDownloadMetadataSignatureVerificationError ExitCode = 25
ExitCodeDownloadMetadataSignatureMismatch ExitCode = 26
ExitCodeDownloadOperationHashVerificationError ExitCode = 27
ExitCodeDownloadOperationExecutionError ExitCode = 28
ExitCodeDownloadOperationHashMismatch ExitCode = 29
ExitCodeOmahaRequestEmptyResponseError ExitCode = 30
ExitCodeOmahaRequestXMLParseError ExitCode = 31
ExitCodeDownloadInvalidMetadataSize ExitCode = 32
ExitCodeDownloadInvalidMetadataSignature ExitCode = 33
ExitCodeOmahaResponseInvalid ExitCode = 34
ExitCodeOmahaUpdateIgnoredPerPolicy ExitCode = 35
ExitCodeOmahaUpdateDeferredPerPolicy ExitCode = 36
ExitCodeOmahaErrorInHTTPResponse ExitCode = 37
ExitCodeDownloadOperationHashMissingError ExitCode = 38
ExitCodeDownloadMetadataSignatureMissingError ExitCode = 39
ExitCodeOmahaUpdateDeferredForBackoff ExitCode = 40
ExitCodePostinstallPowerwashError ExitCode = 41
ExitCodeNewPCRPolicyVerificationError ExitCode = 42
ExitCodeNewPCRPolicyHTTPError ExitCode = 43
// Use the 2xxx range to encode HTTP errors from the Omaha server.
// Sometimes aggregated into ExitCodeOmahaErrorInHTTPResponse
ExitCodeOmahaRequestHTTPResponseBase ExitCode = 2000 // + HTTP response code
)
func (e ExitCode) String() string {
switch e {
case ExitCodeSuccess:
return "success"
case ExitCodeError:
return "error"
case ExitCodeOmahaRequestError:
return "omaha request error"
case ExitCodeOmahaResponseHandlerError:
return "omaha response handler error"
case ExitCodeFilesystemCopierError:
return "filesystem copier error"
case ExitCodePostinstallRunnerError:
return "postinstall runner error"
case ExitCodeSetBootableFlagError:
return "set bootable flag error"
case ExitCodeInstallDeviceOpenError:
return "install device open error"
case ExitCodeKernelDeviceOpenError:
return "kernel device open error"
case ExitCodeDownloadTransferError:
return "download transfer error"
case ExitCodePayloadHashMismatchError:
return "payload hash mismatch error"
case ExitCodePayloadSizeMismatchError:
return "payload size mismatch error"
case ExitCodeDownloadPayloadVerificationError:
return "download payload verification error"
case ExitCodeDownloadNewPartitionInfoError:
return "download new partition info error"
case ExitCodeDownloadWriteError:
return "download write error"
case ExitCodeNewRootfsVerificationError:
return "new rootfs verification error"
case ExitCodeNewKernelVerificationError:
return "new kernel verification error"
case ExitCodeSignedDeltaPayloadExpectedError:
return "signed delta payload expected error"
case ExitCodeDownloadPayloadPubKeyVerificationError:
return "download payload pubkey verification error"
case ExitCodePostinstallBootedFromFirmwareB:
return "postinstall booted from firmware B"
case ExitCodeDownloadStateInitializationError:
return "download state initialization error"
case ExitCodeDownloadInvalidMetadataMagicString:
return "download invalid metadata magic string"
case ExitCodeDownloadSignatureMissingInManifest:
return "download signature missing in manifest"
case ExitCodeDownloadManifestParseError:
return "download manifest parse error"
case ExitCodeDownloadMetadataSignatureError:
return "download metadata signature error"
case ExitCodeDownloadMetadataSignatureVerificationError:
return "download metadata signature verification error"
case ExitCodeDownloadMetadataSignatureMismatch:
return "download metadata signature mismatch"
case ExitCodeDownloadOperationHashVerificationError:
return "download operation hash verification error"
case ExitCodeDownloadOperationExecutionError:
return "download operation execution error"
case ExitCodeDownloadOperationHashMismatch:
return "download operation hash mismatch"
case ExitCodeOmahaRequestEmptyResponseError:
return "omaha request empty response error"
case ExitCodeOmahaRequestXMLParseError:
return "omaha request XML parse error"
case ExitCodeDownloadInvalidMetadataSize:
return "download invalid metadata size"
case ExitCodeDownloadInvalidMetadataSignature:
return "download invalid metadata signature"
case ExitCodeOmahaResponseInvalid:
return "omaha response invalid"
case ExitCodeOmahaUpdateIgnoredPerPolicy:
return "omaha update ignored per policy"
case ExitCodeOmahaUpdateDeferredPerPolicy:
return "omaha update deferred per policy"
case ExitCodeOmahaErrorInHTTPResponse:
return "omaha error in HTTP response"
case ExitCodeDownloadOperationHashMissingError:
return "download operation hash missing error"
case ExitCodeDownloadMetadataSignatureMissingError:
return "download metadata signature missing error"
case ExitCodeOmahaUpdateDeferredForBackoff:
return "omaha update deferred for backoff"
case ExitCodePostinstallPowerwashError:
return "postinstall powerwash error"
case ExitCodeNewPCRPolicyVerificationError:
return "new PCR policy verification error"
case ExitCodeNewPCRPolicyHTTPError:
return "new PCR policy HTTP error"
default:
if e > ExitCodeOmahaRequestHTTPResponseBase {
return fmt.Sprintf("omaha response HTTP %d error",
e-ExitCodeOmahaRequestHTTPResponseBase)
}
return fmt.Sprintf("error code %d", e)
}
}
// NewErrorEvent creates an EventRequest for reporting errors.
func NewErrorEvent(e ExitCode) *omaha.EventRequest {
return &omaha.EventRequest{
Type: omaha.EventTypeUpdateComplete,
Result: omaha.EventResultError,
ErrorCode: int(e),
}
}
// EventString allows for easily logging events in a readable format.
func EventString(e *omaha.EventRequest) string {
s := fmt.Sprintf("omaha event: %s: %s", e.Type, e.Result)
if e.ErrorCode != 0 {
s = fmt.Sprintf("%s (%d - %s)", s,
e.ErrorCode, ExitCode(e.ErrorCode))
}
return s
}
// ErrorEvent is an error type that can generate EventRequests for reporting.
type ErrorEvent interface {
error
ErrorEvent() *omaha.EventRequest
}

View file

@ -1,181 +0,0 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"fmt"
)
type EventType int
const (
EventTypeUnknown EventType = 0
EventTypeDownloadComplete EventType = 1
EventTypeInstallComplete EventType = 2
EventTypeUpdateComplete EventType = 3
EventTypeUninstall EventType = 4
EventTypeDownloadStarted EventType = 5
EventTypeInstallStarted EventType = 6
EventTypeNewApplicationInstallStarted EventType = 9
EventTypeSetupStarted EventType = 10
EventTypeSetupFinished EventType = 11
EventTypeUpdateApplicationStarted EventType = 12
EventTypeUpdateDownloadStarted EventType = 13
EventTypeUpdateDownloadFinished EventType = 14
EventTypeUpdateInstallerStarted EventType = 15
EventTypeSetupUpdateBegin EventType = 16
EventTypeSetupUpdateComplete EventType = 17
EventTypeRegisterProductComplete EventType = 20
EventTypeOEMInstallFirstCheck EventType = 30
EventTypeAppSpecificCommandStarted EventType = 40
EventTypeAppSpecificCommandEnded EventType = 41
EventTypeSetupFailure EventType = 100
EventTypeComServerFailure EventType = 102
EventTypeSetupUpdateFailure EventType = 103
)
func (e EventType) String() string {
switch e {
case EventTypeUnknown:
return "unknown"
case EventTypeDownloadComplete:
return "download complete"
case EventTypeInstallComplete:
return "install complete"
case EventTypeUpdateComplete:
return "update complete"
case EventTypeUninstall:
return "uninstall"
case EventTypeDownloadStarted:
return "download started"
case EventTypeInstallStarted:
return "install started"
case EventTypeNewApplicationInstallStarted:
return "new application install started"
case EventTypeSetupStarted:
return "setup started"
case EventTypeSetupFinished:
return "setup finished"
case EventTypeUpdateApplicationStarted:
return "update application started"
case EventTypeUpdateDownloadStarted:
return "update download started"
case EventTypeUpdateDownloadFinished:
return "update download finished"
case EventTypeUpdateInstallerStarted:
return "update installer started"
case EventTypeSetupUpdateBegin:
return "setup update begin"
case EventTypeSetupUpdateComplete:
return "setup update complete"
case EventTypeRegisterProductComplete:
return "register product complete"
case EventTypeOEMInstallFirstCheck:
return "OEM install first check"
case EventTypeAppSpecificCommandStarted:
return "app-specific command started"
case EventTypeAppSpecificCommandEnded:
return "app-specific command ended"
case EventTypeSetupFailure:
return "setup failure"
case EventTypeComServerFailure:
return "COM server failure"
case EventTypeSetupUpdateFailure:
return "setup update failure "
default:
return fmt.Sprintf("event %d", e)
}
}
type EventResult int
const (
EventResultError EventResult = 0
EventResultSuccess EventResult = 1
EventResultSuccessReboot EventResult = 2
EventResultSuccessRestartBrowser EventResult = 3
EventResultCancelled EventResult = 4
EventResultErrorInstallerMSI EventResult = 5
EventResultErrorInstallerOther EventResult = 6
EventResultNoUpdate EventResult = 7
EventResultInstallerSystem EventResult = 8
EventResultUpdateDeferred EventResult = 9
EventResultHandoffError EventResult = 10
)
func (e EventResult) String() string {
switch e {
case EventResultError:
return "error"
case EventResultSuccess:
return "success"
case EventResultSuccessReboot:
return "success reboot"
case EventResultSuccessRestartBrowser:
return "success restart browser"
case EventResultCancelled:
return "cancelled"
case EventResultErrorInstallerMSI:
return "error installer MSI"
case EventResultErrorInstallerOther:
return "error installer other"
case EventResultNoUpdate:
return "noupdate"
case EventResultInstallerSystem:
return "error installer system"
case EventResultUpdateDeferred:
return "update deferred"
case EventResultHandoffError:
return "handoff error"
default:
return fmt.Sprintf("result %d", e)
}
}
type AppStatus string
const (
// Standard values
AppOK AppStatus = "ok"
AppRestricted AppStatus = "restricted"
AppUnknownID AppStatus = "error-unknownApplication"
AppInvalidID AppStatus = "error-invalidAppId"
// Extra error values
AppInvalidVersion AppStatus = "error-invalidVersion"
AppInternalError AppStatus = "error-internal"
)
// Make AppStatus easy to use as an error
func (a AppStatus) Error() string {
return "omaha: app status " + string(a)
}
type UpdateStatus string
const (
NoUpdate UpdateStatus = "noupdate"
UpdateOK UpdateStatus = "ok"
UpdateOSNotSupported UpdateStatus = "error-osnotsupported"
UpdateUnsupportedProtocol UpdateStatus = "error-unsupportedProtocol"
UpdatePluginRestrictedHost UpdateStatus = "error-pluginRestrictedHost"
UpdateHashError UpdateStatus = "error-hash"
UpdateInternalError UpdateStatus = "error-internal"
)
// Make UpdateStatus easy to use as an error
func (u UpdateStatus) Error() string {
return "omaha: update status " + string(u)
}

View file

@ -1,127 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"encoding/xml"
"log"
"net/http"
)
type OmahaHandler struct {
Updater
}
func (o *OmahaHandler) ServeHTTP(w http.ResponseWriter, httpReq *http.Request) {
if httpReq.Method != "POST" {
log.Printf("omaha: Unexpected HTTP method: %s", httpReq.Method)
http.Error(w, "Expected a POST", http.StatusBadRequest)
return
}
// A request over 1M in size is certainly bogus.
reader := http.MaxBytesReader(w, httpReq.Body, 1024*1024)
contentType := httpReq.Header.Get("Content-Type")
omahaReq, err := ParseRequest(contentType, reader)
if err != nil {
log.Printf("omaha: Failed parsing request: %v", err)
http.Error(w, "Bad Omaha Request", http.StatusBadRequest)
return
}
httpStatus := 0
omahaResp := NewResponse()
for _, appReq := range omahaReq.Apps {
appResp := o.serveApp(omahaResp, httpReq, omahaReq, appReq)
if appResp.Status == AppOK {
// HTTP is ok if any app is ok.
httpStatus = http.StatusOK
} else if httpStatus == 0 {
// If no app is ok HTTP will use the first error.
if appResp.Status == AppInternalError {
httpStatus = http.StatusInternalServerError
} else {
httpStatus = http.StatusBadRequest
}
}
}
if httpStatus == 0 {
httpStatus = http.StatusBadRequest
}
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
w.WriteHeader(httpStatus)
if _, err := w.Write([]byte(xml.Header)); err != nil {
log.Printf("omaha: Failed writing response: %v", err)
return
}
encoder := xml.NewEncoder(w)
encoder.Indent("", "\t")
if err := encoder.Encode(omahaResp); err != nil {
log.Printf("omaha: Failed encoding response: %v", err)
}
}
func (o *OmahaHandler) serveApp(omahaResp *Response, httpReq *http.Request, omahaReq *Request, appReq *AppRequest) *AppResponse {
if err := o.CheckApp(omahaReq, appReq); err != nil {
if appStatus, ok := err.(AppStatus); ok {
return omahaResp.AddApp(appReq.ID, appStatus)
}
log.Printf("omaha: CheckApp failed: %v", err)
return omahaResp.AddApp(appReq.ID, AppInternalError)
}
appResp := omahaResp.AddApp(appReq.ID, AppOK)
if appReq.UpdateCheck != nil {
o.checkUpdate(appResp, httpReq, omahaReq, appReq)
}
if appReq.Ping != nil {
o.Ping(omahaReq, appReq)
appResp.AddPing()
}
for _, event := range appReq.Events {
o.Event(omahaReq, appReq, event)
appResp.AddEvent()
}
return appResp
}
func (o *OmahaHandler) checkUpdate(appResp *AppResponse, httpReq *http.Request, omahaReq *Request, appReq *AppRequest) {
update, err := o.CheckUpdate(omahaReq, appReq)
if err != nil {
if updateStatus, ok := err.(UpdateStatus); ok {
appResp.AddUpdateCheck(updateStatus)
} else {
log.Printf("omaha: CheckUpdate failed: %v", err)
appResp.AddUpdateCheck(UpdateInternalError)
}
} else if update != nil {
u := appResp.AddUpdateCheck(UpdateOK)
fillUpdate(u, update, httpReq)
} else {
appResp.AddUpdateCheck(NoUpdate)
}
}
func fillUpdate(u *UpdateResponse, update *Update, httpReq *http.Request) {
u.URLs = update.URLs([]string{"http://" + httpReq.Host})
u.Manifest = &update.Manifest
}

View file

@ -1,68 +0,0 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"encoding/xml"
"fmt"
"testing"
"github.com/kylelemons/godebug/diff"
)
const (
testAppID = "{27BD862E-8AE8-4886-A055-F7F1A6460627}"
testAppVer = "1.0.0"
)
var (
nilRequest *Request
nilResponse *Response
)
func init() {
nilRequest = NewRequest()
nilRequest.AddApp(testAppID, testAppVer)
nilResponse = NewResponse()
nilResponse.AddApp(testAppID, AppOK)
}
func compareXML(a, b interface{}) error {
aXml, err := xml.MarshalIndent(a, "", "\t")
if err != nil {
return err
}
bXml, err := xml.MarshalIndent(b, "", "\t")
if err != nil {
return err
}
if d := diff.Diff(string(aXml), string(bXml)); d != "" {
err := fmt.Errorf("Unexpected XML:\n%s", d)
return err
}
return nil
}
func TestHandleNilRequest(t *testing.T) {
handler := OmahaHandler{UpdaterStub{}}
response := NewResponse()
handler.serveApp(response, nil, nilRequest, nilRequest.Apps[0])
if err := compareXML(nilResponse, response); err != nil {
t.Error(err)
}
}

View file

@ -1,32 +0,0 @@
// Copyright 2016 CoreOS, Inc.
//
// 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.
package omaha
import (
"net"
)
// isClosed detects if an error is due to a closed network connection,
// working around bug https://github.com/golang/go/issues/4373
func isClosed(err error) bool {
if err == nil {
return false
}
if operr, ok := err.(*net.OpError); ok {
err = operr.Err
}
// cry softly
return err.Error() == "use of closed network connection"
}

248
omaha/omaha.go Normal file
View file

@ -0,0 +1,248 @@
/*
Implements the Google omaha protocol.
Omaha is a request/response protocol using XML. Requests are made by
clients and responses are given by the Omaha server.
http://code.google.com/p/omaha/wiki/ServerProtocol
*/
package omaha
import (
"encoding/xml"
)
type Request struct {
XMLName xml.Name `xml:"request"`
Os Os `xml:"os"`
Apps []*App `xml:"app"`
Protocol string `xml:"protocol,attr"`
Version string `xml:"version,attr,omitempty"`
IsMachine string `xml:"ismachine,attr,omitempty"`
SessionId string `xml:"sessionid,attr,omitempty"`
UserId string `xml:"userid,attr,omitempty"`
InstallSource string `xml:"installsource,attr,omitempty"`
TestSource string `xml:"testsource,attr,omitempty"`
RequestId string `xml:"requestid,attr,omitempty"`
UpdaterVersion string `xml:"updaterversion,attr,omitempty"`
}
func NewRequest(version string, platform string, sp string, arch string) *Request {
r := new(Request)
r.Protocol = "3.0"
r.Os = Os{Version: version, Platform: platform, Sp: sp, Arch: arch}
return r
}
func (r *Request) AddApp(id string, version string) *App {
a := NewApp(id)
a.Version = version
r.Apps = append(r.Apps, a)
return a
}
/* Response
*/
type Response struct {
XMLName xml.Name `xml:"response"`
DayStart DayStart `xml:"daystart"`
Apps []*App `xml:"app"`
Protocol string `xml:"protocol,attr"`
Server string `xml:"server,attr"`
}
func NewResponse(server string) *Response {
r := &Response{Server: server, Protocol: "3.0"}
r.DayStart.ElapsedSeconds = "0"
return r
}
type DayStart struct {
ElapsedSeconds string `xml:"elapsed_seconds,attr"`
}
func (r *Response) AddApp(id string) *App {
a := NewApp(id)
r.Apps = append(r.Apps, a)
return a
}
type App struct {
XMLName xml.Name `xml:"app"`
Ping *Ping `xml:"ping"`
UpdateCheck *UpdateCheck `xml:"updatecheck"`
Events []*Event `xml:"event"`
Id string `xml:"appid,attr,omitempty"`
Version string `xml:"version,attr,omitempty"`
NextVersion string `xml:"nextversion,attr,omitempty"`
Lang string `xml:"lang,attr,omitempty"`
Client string `xml:"client,attr,omitempty"`
InstallAge string `xml:"installage,attr,omitempty"`
FromTrack string `xml:"from_track,attr,omitempty"`
Status string `xml:"status,attr,omitempty"`
}
func NewApp(id string) *App {
a := &App{Id: id}
return a
}
func (a *App) AddUpdateCheck() *UpdateCheck {
a.UpdateCheck = new(UpdateCheck)
return a.UpdateCheck
}
func (a *App) AddPing() *Ping {
a.Ping = new(Ping)
return a.Ping
}
func (a *App) AddEvent() *Event {
event := new(Event)
a.Events = append(a.Events, event)
return event
}
type UpdateCheck struct {
XMLName xml.Name `xml:"updatecheck"`
Urls *Urls `xml:"urls"`
Manifest *Manifest `xml:"manifest"`
TargetVersionPrefix string `xml:"targetversionprefix,attr,omitempty"`
Status string `xml:"status,attr,omitempty"`
}
func (u *UpdateCheck) AddUrl(codebase string) *Url {
if u.Urls == nil {
u.Urls = new(Urls)
}
url := new(Url)
url.CodeBase = codebase
u.Urls.Urls = append(u.Urls.Urls, *url)
return url
}
func (u *UpdateCheck) AddManifest(version string) *Manifest {
u.Manifest = &Manifest{Version: version}
return u.Manifest
}
type Ping struct {
XMLName xml.Name `xml:"ping"`
LastReportDays string `xml:"r,attr,omitempty"`
Status string `xml:"status,attr,omitempty"`
}
type Os struct {
XMLName xml.Name `xml:"os"`
Platform string `xml:"platform,attr,omitempty"`
Version string `xml:"version,attr,omitempty"`
Sp string `xml:"sp,attr,omitempty"`
Arch string `xml:"arch,attr,omitempty"`
}
func NewOs(platform string, version string, sp string, arch string) *Os {
o := &Os{Version: version, Platform: platform, Sp: sp, Arch: arch}
return o
}
type Event struct {
XMLName xml.Name `xml:"event"`
Type string `xml:"eventtype,attr,omitempty"`
Result string `xml:"eventresult,attr,omitempty"`
PreviousVersion string `xml:"previousversion,attr,omitempty"`
}
type Urls struct {
XMLName xml.Name `xml:"urls"`
Urls []Url `xml:"url"`
}
type Url struct {
XMLName xml.Name `xml:"url"`
CodeBase string `xml:"codebase,attr"`
}
type Manifest struct {
XMLName xml.Name `xml:"manifest"`
Packages Packages `xml:"packages"`
Actions Actions `xml:"actions"`
Version string `xml:"version,attr"`
}
type Packages struct {
XMLName xml.Name `xml:"packages"`
Packages []Package `xml:"package"`
}
type Package struct {
XMLName xml.Name `xml:"package"`
Hash string `xml:"hash,attr"`
Name string `xml:"name,attr"`
Size string `xml:"size,attr"`
Required bool `xml:"required,attr"`
}
func (m *Manifest) AddPackage(hash string, name string, size string, required bool) *Package {
p := &Package{Hash: hash, Name: name, Size: size, Required: required}
m.Packages.Packages = append(m.Packages.Packages, *p)
return p
}
type Actions struct {
XMLName xml.Name `xml:"actions"`
Actions []*Action `xml:"action"`
}
type Action struct {
XMLName xml.Name `xml:"action"`
Event string `xml:"event,attr"`
ChromeOSVersion string `xml:"ChromeOSVersion,attr"`
Sha256 string `xml:"sha256,attr"`
NeedsAdmin bool `xml:"needsadmin,attr"`
IsDelta bool `xml:"IsDelta,attr"`
}
func (m *Manifest) AddAction(event string) *Action {
a := &Action{Event: event}
m.Actions.Actions = append(m.Actions.Actions, a)
return a
}
var EventTypes = map[int]string{
0: "unknown",
1: "download complete",
2: "install complete",
3: "update complete",
4: "uninstall",
5: "download started",
6: "install started",
9: "new application install started",
10: "setup started",
11: "setup finished",
12: "update application started",
13: "update download started",
14: "update download finished",
15: "update installer started",
16: "setup update begin",
17: "setup update complete",
20: "register product complete",
30: "OEM install first check",
40: "app-specific command started",
41: "app-specific command ended",
100: "setup failure",
102: "COM server failure",
103: "setup update failure",
}
var EventResults = map[int]string{
0: "error",
1: "success",
2: "success reboot",
3: "success restart browser",
4: "cancelled",
5: "error installer MSI",
6: "error installer other",
7: "noupdate",
8: "error installer system",
9: "update deferred",
10: "handoff error",
}

120
omaha/omaha_test.go Normal file
View file

@ -0,0 +1,120 @@
package omaha
import (
"encoding/xml"
"fmt"
"io/ioutil"
"os"
"testing"
)
func TestOmahaRequestUpdateCheck(t *testing.T) {
file, err := os.Open("../fixtures/update-engine/update/request.xml")
if err != nil {
t.Error(err)
}
fix, err := ioutil.ReadAll(file)
if err != nil {
t.Error(err)
}
v := Request{}
xml.Unmarshal(fix, &v)
if v.Os.Version != "Indy" {
t.Error("Unexpected version", v.Os.Version)
}
if v.Apps[0].Id != "{87efface-864d-49a5-9bb3-4b050a7c227a}" {
t.Error("Expected an App Id")
}
if v.Apps[0].UpdateCheck == nil {
t.Error("Expected an UpdateCheck")
}
if v.Apps[0].Version != "ForcedUpdate" {
t.Error("Verison is ForcedUpdate")
}
if v.Apps[0].FromTrack != "developer-build" {
t.Error("developer-build")
}
if v.Apps[0].Events[0].Type != "3" {
t.Error("developer-build")
}
}
func ExampleOmaha_NewResponse() {
response := NewResponse("unit-test")
app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}")
app.Status = "ok"
p := app.AddPing()
p.Status = "ok"
u := app.AddUpdateCheck()
u.Status = "ok"
u.AddUrl("http://localhost/updates")
m := u.AddManifest("9999.0.0")
m.AddPackage("+LXvjiaPkeYDLHoNKlf9qbJwvnk=", "update.gz", "67546213", true)
a := m.AddAction("postinstall")
a.ChromeOSVersion = "9999.0.0"
a.Sha256 = "0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg="
a.NeedsAdmin = false
a.IsDelta = true
if raw, err := xml.MarshalIndent(response, "", " "); err != nil {
fmt.Println(err)
return
} else {
fmt.Printf("%s%s\n", xml.Header, raw)
}
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <response protocol="3.0" server="unit-test">
// <daystart elapsed_seconds="0"></daystart>
// <app appid="{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}" status="ok">
// <ping status="ok"></ping>
// <updatecheck status="ok">
// <urls>
// <url codebase="http://localhost/updates"></url>
// </urls>
// <manifest version="9999.0.0">
// <packages>
// <package hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" name="update.gz" size="67546213" required="true"></package>
// </packages>
// <actions>
// <action event="postinstall" ChromeOSVersion="9999.0.0" sha256="0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" needsadmin="false" IsDelta="true"></action>
// </actions>
// </manifest>
// </updatecheck>
// </app>
// </response>
}
func ExampleOmaha_NewRequest() {
request := NewRequest("Indy", "Chrome OS", "ForcedUpdate_x86_64", "")
app := request.AddApp("{27BD862E-8AE8-4886-A055-F7F1A6460627}", "1.0.0.0")
app.AddUpdateCheck()
event := app.AddEvent()
event.Type = "1"
event.Result = "0"
if raw, err := xml.MarshalIndent(request, "", " "); err != nil {
fmt.Println(err)
return
} else {
fmt.Printf("%s%s\n", xml.Header, raw)
}
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <request protocol="3.0">
// <os platform="Chrome OS" version="Indy" sp="ForcedUpdate_x86_64"></os>
// <app appid="{27BD862E-8AE8-4886-A055-F7F1A6460627}" version="1.0.0.0">
// <updatecheck></updatecheck>
// <event eventtype="1" eventresult="0"></event>
// </app>
// </request>
}

View file

@ -1,111 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
"os"
"path/filepath"
)
var (
PackageHashMismatchError = errors.New("package hash is invalid")
PackageSizeMismatchError = errors.New("package size is invalid")
)
// Package represents a single downloadable file.
type Package struct {
Name string `xml:"name,attr"`
SHA1 string `xml:"hash,attr"`
SHA256 string `xml:"hash_sha256,attr,omitempty"`
Size uint64 `xml:"size,attr"`
Required bool `xml:"required,attr"`
}
func (p *Package) FromPath(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
err = p.FromReader(f)
if err != nil {
return err
}
p.Name = filepath.Base(name)
return nil
}
func (p *Package) FromReader(r io.Reader) error {
sha1b64, sha256b64, n, err := multihash(r)
if err != nil {
return err
}
p.SHA1 = sha1b64
p.SHA256 = sha256b64
p.Size = uint64(n)
return nil
}
func (p *Package) Verify(dir string) error {
f, err := os.Open(filepath.Join(dir, p.Name))
if err != nil {
return err
}
defer f.Close()
return p.VerifyReader(f)
}
func (p *Package) VerifyReader(r io.Reader) error {
sha1b64, sha256b64, n, err := multihash(r)
if err != nil {
return err
}
if p.Size != uint64(n) {
return PackageSizeMismatchError
}
if p.SHA1 != sha1b64 {
return PackageHashMismatchError
}
// Allow SHA256 to be empty since it is a later protocol addition.
if p.SHA256 != "" && p.SHA256 != sha256b64 {
return PackageHashMismatchError
}
return nil
}
func multihash(r io.Reader) (sha1b64, sha256b64 string, n int64, err error) {
h1 := sha1.New()
h256 := sha256.New()
if n, err = io.Copy(io.MultiWriter(h1, h256), r); err != nil {
return
}
sha1b64 = base64.StdEncoding.EncodeToString(h1.Sum(nil))
sha256b64 = base64.StdEncoding.EncodeToString(h256.Sum(nil))
return
}

View file

@ -1,144 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"strings"
"testing"
"github.com/kylelemons/godebug/pretty"
)
func TestPackageFromPath(t *testing.T) {
expect := Package{
Name: "null",
SHA1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
SHA256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 0,
Required: false,
}
p := Package{}
if err := p.FromPath("/dev/null"); err != nil {
t.Fatal(err)
}
if diff := pretty.Compare(expect, p); diff != "" {
t.Errorf("Hashing /dev/null failed: %v", diff)
}
}
func TestProtocolFromReader(t *testing.T) {
data := strings.NewReader("testing\n")
expect := Package{
Name: "",
SHA1: "mAFznarkTsUpPU4fU9P00tQm2Rw=",
SHA256: "EqYfThc/s6EcBdZHH3Ryj3YjG0pfzZZnzvOvh6OuTcI=",
Size: 8,
Required: false,
}
p := Package{}
if err := p.FromReader(data); err != nil {
t.Fatal(err)
}
if diff := pretty.Compare(expect, p); diff != "" {
t.Errorf("Hashing failed: %v", diff)
}
}
func TestPackageVerify(t *testing.T) {
p := Package{
Name: "null",
SHA1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
SHA256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 0,
Required: false,
}
if err := p.Verify("/dev"); err != nil {
t.Fatal(err)
}
}
func TestPackageVerifyNoSHA256(t *testing.T) {
p := Package{
Name: "null",
SHA1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
SHA256: "",
Size: 0,
Required: false,
}
if err := p.Verify("/dev"); err != nil {
t.Fatal(err)
}
}
func TestPackageVerifyBadSize(t *testing.T) {
p := Package{
Name: "null",
SHA1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
SHA256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 1,
Required: false,
}
err := p.Verify("/dev")
if err == nil {
t.Error("verify passed")
}
if err != PackageSizeMismatchError {
t.Error(err)
}
}
func TestPackageVerifyBadSHA1(t *testing.T) {
p := Package{
Name: "null",
SHA1: "xxxxxxxxxxxxxxxxxxxxxxxxxxx=",
SHA256: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
Size: 0,
Required: false,
}
err := p.Verify("/dev")
if err == nil {
t.Error("verify passed")
}
if err != PackageHashMismatchError {
t.Error(err)
}
}
func TestPackageVerifyBadSHA256(t *testing.T) {
p := Package{
Name: "null",
SHA1: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
SHA256: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
Size: 0,
Required: false,
}
err := p.Verify("/dev")
if err == nil {
t.Error("verify passed")
}
if err != PackageHashMismatchError {
t.Error(err)
}
}

View file

@ -1,71 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package omaha
import (
"encoding/xml"
"fmt"
"io"
"mime"
"strings"
)
// checkContentType verifies the HTTP Content-Type header properly
// declares the document is XML and UTF-8. Blank is assumed OK.
func checkContentType(contentType string) error {
if contentType == "" {
return nil
}
mType, mParams, err := mime.ParseMediaType(contentType)
if err != nil {
return err
}
if mType != "text/xml" && mType != "application/xml" {
return fmt.Errorf("unsupported content type %q", mType)
}
charset, _ := mParams["charset"]
if charset != "" && strings.ToLower(charset) != "utf-8" {
return fmt.Errorf("unsupported content charset %q", charset)
}
return nil
}
// parseReqOrResp parses Request and Response objects.
func parseReqOrResp(r io.Reader, v interface{}) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(v); err != nil {
return err
}
var protocol string
switch v := v.(type) {
case *Request:
protocol = v.Protocol
case *Response:
protocol = v.Protocol
default:
panic(fmt.Errorf("unexpected type %T", v))
}
if protocol != "3.0" {
return fmt.Errorf("unsupported omaha protocol: %q", protocol)
}
return nil
}

View file

@ -1,55 +0,0 @@
// Copyright 2017 CoreOS, Inc.
//
// 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.
package omaha
import (
"strings"
"testing"
)
func TestCheckContentType(t *testing.T) {
for _, tt := range []struct {
ct string
ok bool
}{
{"", true},
{"text/xml", true},
{"text/XML", true},
{"application/xml", true},
{"text/plain", false},
{"xml", false},
{"text/xml; charset=utf-8", true},
{"text/xml; charset=UTF-8", true},
{"text/xml; charset=ascii", false},
} {
err := checkContentType(tt.ct)
if tt.ok && err != nil {
t.Errorf("%q failed: %v", tt.ct, err)
}
if !tt.ok && err == nil {
t.Errorf("%q was not rejected", tt.ct)
}
}
}
func TestParseBadVersion(t *testing.T) {
r := strings.NewReader(`<request protocol="2.0"></request>`)
err := parseReqOrResp(r, &Request{})
if err == nil {
t.Error("Bad protocol version was accepted")
} else if err.Error() != `unsupported omaha protocol: "2.0"` {
t.Errorf("Wrong error: %v", err)
}
}

View file

@ -1,304 +0,0 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// 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.
// Google's Omaha application update protocol, version 3.
//
// Omaha is a poll based protocol using XML. Requests are made by clients to
// check for updates or report events of an update process. Responses are given
// by the server to provide update information, if any, or to simply
// acknowledge the receipt of event status.
//
// https://github.com/google/omaha/blob/master/doc/ServerProtocolV3.md
package omaha
import (
"encoding/xml"
"io"
)
// Request sent by the Omaha client
type Request struct {
XMLName xml.Name `xml:"request" json:"-"`
OS *OS `xml:"os"`
Apps []*AppRequest `xml:"app"`
Protocol string `xml:"protocol,attr"`
InstallSource string `xml:"installsource,attr,omitempty"`
IsMachine int `xml:"ismachine,attr,omitempty"`
RequestID string `xml:"requestid,attr,omitempty"`
SessionID string `xml:"sessionid,attr,omitempty"`
TestSource string `xml:"testsource,attr,omitempty"`
UserID string `xml:"userid,attr,omitempty"`
Version string `xml:"version,attr,omitempty"`
// update engine extension, duplicates the version attribute.
UpdaterVersion string `xml:"updaterversion,attr,omitempty"`
}
func NewRequest() *Request {
return &Request{
Protocol: "3.0",
// TODO(marineam) set a default client Version
OS: &OS{
Platform: LocalPlatform(),
Arch: LocalArch(),
// TODO(marineam): Version and ServicePack
},
}
}
// ParseRequest verifies and returns the parsed Request document.
// The MIME Content-Type header may be provided to sanity check its
// value; if blank it is assumed to be XML in UTF-8.
func ParseRequest(contentType string, body io.Reader) (*Request, error) {
if err := checkContentType(contentType); err != nil {
return nil, err
}
r := &Request{}
if err := parseReqOrResp(body, r); err != nil {
return nil, err
}
return r, nil
}
func (r *Request) AddApp(id, version string) *AppRequest {
a := &AppRequest{ID: id, Version: version}
r.Apps = append(r.Apps, a)
return a
}
func (r *Request) GetApp(id string) *AppRequest {
for _, app := range r.Apps {
if app.ID == id {
return app
}
}
return nil
}
type AppRequest struct {
Ping *PingRequest `xml:"ping"`
UpdateCheck *UpdateRequest `xml:"updatecheck"`
Events []*EventRequest `xml:"event" json:",omitempty"`
ID string `xml:"appid,attr,omitempty"`
Client string `xml:"client,attr,omitempty"`
InstallAge string `xml:"installage,attr,omitempty"`
Lang string `xml:"lang,attr,omitempty"`
NextVersion string `xml:"nextversion,attr,omitempty"`
Version string `xml:"version,attr,omitempty"`
// update engine extensions
Board string `xml:"board,attr,omitempty"`
DeltaOK bool `xml:"delta_okay,attr,omitempty"`
FromTrack string `xml:"from_track,attr,omitempty"`
Track string `xml:"track,attr,omitempty"`
// coreos update engine extensions
AlephVersion string `xml:"alephversion,attr,omitempty"`
BootID string `xml:"bootid,attr,omitempty"`
MachineID string `xml:"machineid,attr,omitempty"`
OEM string `xml:"oem,attr,omitempty"`
OEMVersion string `xml:"oemversion,attr,omitempty"`
}
func (a *AppRequest) AddUpdateCheck() *UpdateRequest {
a.UpdateCheck = &UpdateRequest{}
return a.UpdateCheck
}
func (a *AppRequest) AddPing() *PingRequest {
a.Ping = &PingRequest{Active: 1}
return a.Ping
}
func (a *AppRequest) AddEvent() *EventRequest {
event := &EventRequest{}
a.Events = append(a.Events, event)
return event
}
type UpdateRequest struct {
TargetVersionPrefix string `xml:"targetversionprefix,attr,omitempty"`
}
type PingRequest struct {
Active int `xml:"active,attr,omitempty"`
LastActiveReportDays *int `xml:"a,attr,omitempty"`
LastReportDays int `xml:"r,attr,omitempty"`
}
type EventRequest struct {
Type EventType `xml:"eventtype,attr"`
Result EventResult `xml:"eventresult,attr"`
ErrorCode int `xml:"errorcode,attr,omitempty"`
NextVersion string `xml:"nextversion,attr,omitempty"`
PreviousVersion string `xml:"previousversion,attr,omitempty"`
}
// Response sent by the Omaha server
type Response struct {
XMLName xml.Name `xml:"response" json:"-"`
DayStart DayStart `xml:"daystart"`
Apps []*AppResponse `xml:"app"`
Protocol string `xml:"protocol,attr"`
Server string `xml:"server,attr"`
}
func NewResponse() *Response {
return &Response{
Protocol: "3.0",
Server: "go-omaha",
DayStart: DayStart{ElapsedSeconds: "0"},
}
}
// ParseResponse verifies and returns the parsed Response document.
// The MIME Content-Type header may be provided to sanity check its
// value; if blank it is assumed to be XML in UTF-8.
func ParseResponse(contentType string, body io.Reader) (*Response, error) {
if err := checkContentType(contentType); err != nil {
return nil, err
}
r := &Response{}
if err := parseReqOrResp(body, r); err != nil {
return nil, err
}
return r, nil
}
type DayStart struct {
ElapsedSeconds string `xml:"elapsed_seconds,attr"`
}
func (r *Response) AddApp(id string, status AppStatus) *AppResponse {
a := &AppResponse{ID: id, Status: status}
r.Apps = append(r.Apps, a)
return a
}
func (r *Response) GetApp(id string) *AppResponse {
for _, app := range r.Apps {
if app.ID == id {
return app
}
}
return nil
}
type AppResponse struct {
Ping *PingResponse `xml:"ping"`
UpdateCheck *UpdateResponse `xml:"updatecheck"`
Events []*EventResponse `xml:"event" json:",omitempty"`
ID string `xml:"appid,attr,omitempty"`
Status AppStatus `xml:"status,attr,omitempty"`
}
func (a *AppResponse) AddUpdateCheck(status UpdateStatus) *UpdateResponse {
a.UpdateCheck = &UpdateResponse{Status: status}
return a.UpdateCheck
}
func (a *AppResponse) AddPing() *PingResponse {
a.Ping = &PingResponse{"ok"}
return a.Ping
}
func (a *AppResponse) AddEvent() *EventResponse {
event := &EventResponse{"ok"}
a.Events = append(a.Events, event)
return event
}
type UpdateResponse struct {
URLs []*URL `xml:"urls>url" json:",omitempty"`
Manifest *Manifest `xml:"manifest"`
Status UpdateStatus `xml:"status,attr,omitempty"`
}
func (u *UpdateResponse) AddURL(codebase string) *URL {
url := &URL{CodeBase: codebase}
u.URLs = append(u.URLs, url)
return url
}
func (u *UpdateResponse) AddManifest(version string) *Manifest {
u.Manifest = &Manifest{Version: version}
return u.Manifest
}
type PingResponse struct {
Status string `xml:"status,attr"` // Always "ok".
}
type EventResponse struct {
Status string `xml:"status,attr"` // Always "ok".
}
type OS struct {
Platform string `xml:"platform,attr,omitempty"`
Version string `xml:"version,attr,omitempty"`
ServicePack string `xml:"sp,attr,omitempty"`
Arch string `xml:"arch,attr,omitempty"`
}
type URL struct {
CodeBase string `xml:"codebase,attr"`
}
type Manifest struct {
Packages []*Package `xml:"packages>package"`
Actions []*Action `xml:"actions>action"`
Version string `xml:"version,attr"`
}
func (m *Manifest) AddPackage() *Package {
p := &Package{}
m.Packages = append(m.Packages, p)
return p
}
func (m *Manifest) AddPackageFromPath(path string) (*Package, error) {
p := &Package{}
if err := p.FromPath(path); err != nil {
return nil, err
}
m.Packages = append(m.Packages, p)
return p, nil
}
func (m *Manifest) AddAction(event string) *Action {
a := &Action{Event: event}
m.Actions = append(m.Actions, a)
return a
}
type Action struct {
Event string `xml:"event,attr"`
// update engine extensions for event="postinstall"
DisplayVersion string `xml:"DisplayVersion,attr,omitempty"`
SHA256 string `xml:"sha256,attr,omitempty"`
NeedsAdmin bool `xml:"needsadmin,attr,omitempty"`
IsDeltaPayload bool `xml:"IsDeltaPayload,attr,omitempty"`
DisablePayloadBackoff bool `xml:"DisablePayloadBackoff,attr,omitempty"`
MaxFailureCountPerURL uint `xml:"MaxFailureCountPerUrl,attr,omitempty"`
MetadataSignatureRsa string `xml:"MetadataSignatureRsa,attr,omitempty"`
MetadataSize string `xml:"MetadataSize,attr,omitempty"`
Deadline string `xml:"deadline,attr,omitempty"`
MoreInfo string `xml:"MoreInfo,attr,omitempty"`
Prompt bool `xml:"Prompt,attr,omitempty"`
}

View file

@ -1,247 +0,0 @@
// Copyright 2013-2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"encoding/xml"
"fmt"
"reflect"
"strings"
"testing"
)
const (
sampleRequest = `<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" installsource="ondemandupdate" ismachine="1">
<os version="Indy" platform="Chrome OS" sp="ForcedUpdate_x86_64"></os>
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" bootid="{7D52A1CC-7066-40F0-91C7-7CB6A871BFDE}" machineid="{8BDE4C4D-9083-4D61-B41C-3253212C0C37}" oem="ec3000" version="ForcedUpdate" track="dev-channel" from_track="developer-build" lang="en-US" board="amd64-generic" hardware_class="" delta_okay="false" >
<ping active="1" a="-1" r="-1"></ping>
<updatecheck targetversionprefix=""></updatecheck>
<event eventtype="3" eventresult="2" previousversion=""></event>
</app>
</request>
`
sampleResponse = `<?xml version="1.0" encoding="UTF-8"?>
<response protocol="3.0">
<daystart elapsed_seconds="49008"/>
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" status="ok">
<ping status="ok"/>
<updatecheck status="ok">
<urls>
<url codebase="http://kam:8080/static/"/>
</urls>
<manifest version="9999.0.0">
<packages>
<package hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" name="update.gz" size="67546213" required="true"/>
</packages>
<actions>
<action event="postinstall" DisplayVersion="9999.0.0" sha256="0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" needsadmin="false" IsDeltaPayload="true" />
</actions>
</manifest>
</updatecheck>
</app>
</response>
`
)
func TestOmahaRequestUpdateCheck(t *testing.T) {
v, err := ParseRequest("", strings.NewReader(sampleRequest))
if err != nil {
t.Fatalf("ParseRequest failed: %v", err)
}
if v.OS.Version != "Indy" {
t.Error("Unexpected version", v.OS.Version)
}
if v.Apps[0].ID != "{87efface-864d-49a5-9bb3-4b050a7c227a}" {
t.Error("Expected an App ID")
}
if v.Apps[0].BootID != "{7D52A1CC-7066-40F0-91C7-7CB6A871BFDE}" {
t.Error("Expected a Boot ID")
}
if v.Apps[0].MachineID != "{8BDE4C4D-9083-4D61-B41C-3253212C0C37}" {
t.Error("Expected a Machine ID")
}
if v.Apps[0].OEM != "ec3000" {
t.Error("Expected an OEM")
}
if v.Apps[0].UpdateCheck == nil {
t.Error("Expected an UpdateCheck")
}
if v.Apps[0].Version != "ForcedUpdate" {
t.Error("Verison is ForcedUpdate")
}
if v.Apps[0].FromTrack != "developer-build" {
t.Error("developer-build")
}
if v.Apps[0].Track != "dev-channel" {
t.Error("dev-channel")
}
if v.Apps[0].Events[0].Type != EventTypeUpdateComplete {
t.Error("Expected EventTypeUpdateComplete")
}
if v.Apps[0].Events[0].Result != EventResultSuccessReboot {
t.Error("Expected EventResultSuccessReboot")
}
}
func TestOmahaResponseWithUpdate(t *testing.T) {
parsed, err := ParseResponse("", strings.NewReader(sampleResponse))
if err != nil {
t.Fatalf("ParseResponse failed: %v", err)
}
expected := &Response{
XMLName: xml.Name{Local: "response"},
Protocol: "3.0",
DayStart: DayStart{ElapsedSeconds: "49008"},
Apps: []*AppResponse{&AppResponse{
ID: "{87efface-864d-49a5-9bb3-4b050a7c227a}",
Status: AppOK,
Ping: &PingResponse{"ok"},
UpdateCheck: &UpdateResponse{
Status: UpdateOK,
URLs: []*URL{&URL{
CodeBase: "http://kam:8080/static/",
}},
Manifest: &Manifest{
Version: "9999.0.0",
Packages: []*Package{&Package{
SHA1: "+LXvjiaPkeYDLHoNKlf9qbJwvnk=",
Name: "update.gz",
Size: 67546213,
Required: true,
}},
Actions: []*Action{&Action{
Event: "postinstall",
DisplayVersion: "9999.0.0",
SHA256: "0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=",
IsDeltaPayload: true,
}},
},
},
}},
}
if !reflect.DeepEqual(parsed, expected) {
t.Errorf("parsed != expected\n%v\n%v", parsed, expected)
}
}
func TestOmahaResponsAsRequest(t *testing.T) {
_, err := ParseRequest("", strings.NewReader(sampleResponse))
if err == nil {
t.Fatal("ParseRequest successfully parsed a response")
}
}
func TestOmahaRequestAsResponse(t *testing.T) {
_, err := ParseResponse("", strings.NewReader(sampleRequest))
if err == nil {
t.Fatal("ParseResponse successfully parsed a request")
}
}
func ExampleNewResponse() {
response := NewResponse()
app := response.AddApp("{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}", "ok")
app.AddPing()
u := app.AddUpdateCheck(UpdateOK)
u.AddURL("http://localhost/updates")
m := u.AddManifest("9999.0.0")
k := m.AddPackage()
k.SHA1 = "+LXvjiaPkeYDLHoNKlf9qbJwvnk="
k.Name = "update.gz"
k.Size = 67546213
k.Required = true
a := m.AddAction("postinstall")
a.DisplayVersion = "9999.0.0"
a.SHA256 = "0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg="
a.NeedsAdmin = false
a.IsDeltaPayload = true
a.DisablePayloadBackoff = true
if raw, err := xml.MarshalIndent(response, "", " "); err != nil {
fmt.Println(err)
return
} else {
fmt.Printf("%s%s\n", xml.Header, raw)
}
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <response protocol="3.0" server="go-omaha">
// <daystart elapsed_seconds="0"></daystart>
// <app appid="{52F1B9BC-D31A-4D86-9276-CBC256AADF9A}" status="ok">
// <ping status="ok"></ping>
// <updatecheck status="ok">
// <urls>
// <url codebase="http://localhost/updates"></url>
// </urls>
// <manifest version="9999.0.0">
// <packages>
// <package name="update.gz" hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" size="67546213" required="true"></package>
// </packages>
// <actions>
// <action event="postinstall" DisplayVersion="9999.0.0" sha256="0VAlQW3RE99SGtSB5R4m08antAHO8XDoBMKDyxQT/Mg=" IsDeltaPayload="true" DisablePayloadBackoff="true"></action>
// </actions>
// </manifest>
// </updatecheck>
// </app>
// </response>
}
func ExampleNewRequest() {
request := NewRequest()
request.Version = ""
request.OS = &OS{
Platform: "Chrome OS",
Version: "Indy",
ServicePack: "ForcedUpdate_x86_64",
}
app := request.AddApp("{27BD862E-8AE8-4886-A055-F7F1A6460627}", "1.0.0.0")
app.AddUpdateCheck()
event := app.AddEvent()
event.Type = EventTypeDownloadComplete
event.Result = EventResultError
if raw, err := xml.MarshalIndent(request, "", " "); err != nil {
fmt.Println(err)
return
} else {
fmt.Printf("%s%s\n", xml.Header, raw)
}
// Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <request protocol="3.0">
// <os platform="Chrome OS" version="Indy" sp="ForcedUpdate_x86_64"></os>
// <app appid="{27BD862E-8AE8-4886-A055-F7F1A6460627}" version="1.0.0.0">
// <updatecheck></updatecheck>
// <event eventtype="1" eventresult="0"></event>
// </app>
// </request>
}

View file

@ -1,73 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"net"
"net/http"
)
func NewServer(addr string, updater Updater) (*Server, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
mux := http.NewServeMux()
srv := &http.Server{
Addr: addr,
Handler: mux,
}
s := &Server{
Updater: updater,
Mux: mux,
l: l,
srv: srv,
}
h := &OmahaHandler{s}
mux.Handle("/v1/update", h)
mux.Handle("/v1/update/", h)
return s, nil
}
type Server struct {
Updater
Mux *http.ServeMux
l net.Listener
srv *http.Server
}
func (s *Server) Serve() error {
err := s.srv.Serve(s.l)
if isClosed(err) {
// gracefully quit
err = nil
}
return nil
}
func (s *Server) Destroy() error {
return s.l.Close()
}
func (s *Server) Addr() net.Addr {
return s.l.Addr()
}

View file

@ -1,114 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"bytes"
"encoding/xml"
"fmt"
"net/http"
"sync"
"testing"
"time"
)
type mockServer struct {
UpdaterStub
reqChan chan *Request
}
func (m *mockServer) CheckApp(req *Request, app *AppRequest) error {
m.reqChan <- req
return nil
}
func TestServerRequestResponse(t *testing.T) {
var wg sync.WaitGroup
defer wg.Wait()
// make an omaha server
svc := &mockServer{
reqChan: make(chan *Request),
}
s, err := NewServer("127.0.0.1:0", svc)
if err != nil {
t.Fatalf("failed to create omaha server: %v", err)
}
defer func() {
err := s.Destroy()
if err != nil {
t.Error(err)
}
close(svc.reqChan)
}()
wg.Add(1)
go func() {
defer wg.Done()
if err := s.Serve(); err != nil {
t.Errorf("Serve failed: %v", err)
}
}()
buf := new(bytes.Buffer)
enc := xml.NewEncoder(buf)
enc.Indent("", "\t")
err = enc.Encode(nilRequest)
if err != nil {
t.Fatalf("failed to marshal request: %v", err)
}
// check that server gets the same thing we sent
wg.Add(1)
go func() {
defer wg.Done()
sreq, ok := <-svc.reqChan
if !ok {
t.Errorf("failed to get notification from server")
return
}
if err := compareXML(nilRequest, sreq); err != nil {
t.Error(err)
}
}()
// send omaha request
endpoint := fmt.Sprintf("http://%s/v1/update/", s.Addr())
httpClient := &http.Client{
Timeout: 2 * time.Second,
}
res, err := httpClient.Post(endpoint, "text/xml", buf)
if err != nil {
t.Fatalf("failed to post: %v", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
t.Fatalf("failed to post: %v", res.Status)
}
dec := xml.NewDecoder(res.Body)
sresp := &Response{}
if err := dec.Decode(sresp); err != nil {
t.Fatalf("failed to parse body: %v", err)
}
if err := compareXML(nilResponse, sresp); err != nil {
t.Error(err)
}
}

View file

@ -1,53 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"runtime"
)
// Translate GOARCH to Omaha's choice of names, because no two independent
// software projects *ever* use the same set of architecture names. ;-)
func LocalArch() string {
switch runtime.GOARCH {
case "386":
return "x86"
case "amd64":
return "x64"
case "amd64p32":
// Not actually specified by Omaha but it follows the above.
return "x32"
case "arm":
fallthrough
default:
// Nothing else is defined by Omaha so anything goes.
return runtime.GOARCH
}
}
// Translate GOOS to Omaha's platform names as best as we can.
func LocalPlatform() string {
switch runtime.GOOS {
case "darwin":
return "mac" // or "ios"
case "linux":
return "linux" // or "android"
case "windows":
return "win"
default:
// Nothing else is defined by Omaha so anything goes.
return runtime.GOOS
}
}

View file

@ -1,122 +0,0 @@
// Copyright 2016 CoreOS, Inc.
//
// 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.
package omaha
import (
"fmt"
"net/http"
"path"
"github.com/blang/semver"
)
const pkg_prefix = "/packages/"
// trivialUpdater always responds with the given Update.
type trivialUpdater struct {
UpdaterStub
Update
}
func (tu *trivialUpdater) CheckUpdate(req *Request, app *AppRequest) (*Update, error) {
if len(tu.Manifest.Packages) == 0 {
return nil, NoUpdate
}
v1, err := semver.Make(app.Version)
if err != nil {
return nil, err
}
v2, err := semver.Make(tu.Manifest.Version)
if err != nil {
return nil, err
}
if v1.LT(v2) {
return &tu.Update, nil
}
return nil, NoUpdate
}
// trivialHandler serves up a single file.
type trivialHandler struct {
Path string
}
func (th *trivialHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if th.Path == "" {
http.NotFound(w, r)
}
http.ServeFile(w, r, th.Path)
}
// TrivialServer is an extremely basic Omaha server that ignores all
// incoming metadata, always responding with the same update response.
// The update is constructed by calling AddPackage one or more times.
type TrivialServer struct {
*Server
tu trivialUpdater
}
func NewTrivialServer(addr string) (*TrivialServer, error) {
ts := TrivialServer{
tu: trivialUpdater{
Update: Update{
URL: URL{CodeBase: pkg_prefix},
},
},
}
s, err := NewServer(addr, &ts.tu)
if err != nil {
return nil, err
}
ts.Server = s
return &ts, nil
}
// AddPackage adds a new file to the update response.
// file is the local filesystem path, name is the final URL component.
func (ts *TrivialServer) AddPackage(file, name string) error {
// name may not include any path components
if path.Base(name) != name || name[0] == '.' {
return fmt.Errorf("invalid package name %q", name)
}
pkg, err := ts.tu.Manifest.AddPackageFromPath(file)
if err != nil {
return err
}
pkg.Name = name
// Insert the update_engine style postinstall action if
// this is the first (and probably only) package.
if len(ts.tu.Manifest.Actions) == 0 {
act := ts.tu.Manifest.AddAction("postinstall")
act.DisablePayloadBackoff = true
act.SHA256 = pkg.SHA256
}
ts.Mux.Handle(pkg_prefix+name, &trivialHandler{file})
return nil
}
// SetVersion sets the manifest's version with the provided one.
func (ts *TrivialServer) SetVersion(version string) {
ts.tu.Manifest.Version = version
}

View file

@ -1,140 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"os"
"testing"
)
func mkUpdateReq() (*bytes.Buffer, error) {
req := NewRequest()
app := req.AddApp(testAppID, testAppVer)
app.AddUpdateCheck()
buf := &bytes.Buffer{}
enc := xml.NewEncoder(buf)
enc.Indent("", "\t")
if err := enc.Encode(req); err != nil {
return nil, err
}
return buf, nil
}
func TestTrivialServer(t *testing.T) {
tmp, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer tmp.Close()
defer os.Remove(tmp.Name())
if _, err := tmp.WriteString("test"); err != nil {
t.Fatal(err)
}
s, err := NewTrivialServer(":0")
if err != nil {
t.Fatal(err)
}
defer s.Destroy()
if err := s.AddPackage(tmp.Name(), "update.gz"); err != nil {
t.Fatal(err)
}
s.SetVersion(testAppVer)
go s.Serve()
buf, err := mkUpdateReq()
if err != nil {
t.Fatal(err)
}
endpoint := fmt.Sprintf("http://%s/v1/update/", s.Addr())
res, err := http.Post(endpoint, "text/xml", buf)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
t.Fatalf("failed to post: %v", res.Status)
}
dec := xml.NewDecoder(res.Body)
resp := &Response{}
if err := dec.Decode(resp); err != nil {
t.Fatalf("failed to parse body: %v", err)
}
// Should get zero update because the version is already the latest.
if len(resp.Apps) != 1 ||
resp.Apps[0].UpdateCheck == nil ||
resp.Apps[0].UpdateCheck.Status != NoUpdate {
t.Fatalf("unexpected response: %#v", resp)
}
// Should get an update now.
s.SetVersion("999.999.999")
buf, err = mkUpdateReq()
if err != nil {
t.Fatal(err)
}
endpoint = fmt.Sprintf("http://%s/v1/update/", s.Addr())
res, err = http.Post(endpoint, "text/xml", buf)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
t.Fatalf("failed to post: %v", res.Status)
}
dec = xml.NewDecoder(res.Body)
resp = &Response{}
if err := dec.Decode(resp); err != nil {
t.Fatalf("failed to parse body: %v", err)
}
if len(resp.Apps) != 1 ||
resp.Apps[0].UpdateCheck == nil ||
resp.Apps[0].UpdateCheck.Status != UpdateOK ||
len(resp.Apps[0].UpdateCheck.URLs) != 1 ||
resp.Apps[0].UpdateCheck.Manifest == nil ||
len(resp.Apps[0].UpdateCheck.Manifest.Packages) != 1 {
t.Fatalf("unexpected response: %#v", resp)
}
pkgres, err := http.Get(resp.Apps[0].UpdateCheck.URLs[0].CodeBase +
resp.Apps[0].UpdateCheck.Manifest.Packages[0].Name)
if err != nil {
t.Fatal(err)
}
pkgdata, err := ioutil.ReadAll(pkgres.Body)
pkgres.Body.Close()
if err != nil {
t.Fatal(err)
}
if string(pkgdata) != "test" {
t.Fatalf("unexpected package data: %q", string(pkgdata))
}
}

View file

@ -1,73 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"encoding/xml"
)
// Update is a manifest for a single omaha update response. It extends
// the standard Manifest protocol element with the application id and
// previous version which are used to match against the update request.
// A blank previous version indicates this update can be applied to any
// existing install. The application id may not be blank.
type Update struct {
XMLName xml.Name `xml:"update" json:"-"`
ID string `xml:"appid,attr"`
PreviousVersion string `xml:"previousversion,attr,omitempty"`
URL URL `xml:"urls>url"`
Manifest
// The delta_okay request attribute is an update_engine extension.
RespectDeltaOK bool `xml:"respect_delta_okay,attr,omitempty"`
}
// The URL attribute in Update is currently assumed to be a relative
// path which may be found on multiple mirrors. A server using this is
// expected to know the mirror prefix(s) it can give the client.
func (u *Update) URLs(prefixes []string) []*URL {
urls := make([]*URL, len(prefixes))
for i, prefix := range prefixes {
urls[i] = &URL{CodeBase: prefix + u.URL.CodeBase}
}
return urls
}
// Updater provides a common interface for any backend that can respond to
// update requests made to an Omaha server.
type Updater interface {
CheckApp(req *Request, app *AppRequest) error
CheckUpdate(req *Request, app *AppRequest) (*Update, error)
Event(req *Request, app *AppRequest, event *EventRequest)
Ping(req *Request, app *AppRequest)
}
type UpdaterStub struct{}
func (u UpdaterStub) CheckApp(req *Request, app *AppRequest) error {
return nil
}
func (u UpdaterStub) CheckUpdate(req *Request, app *AppRequest) (*Update, error) {
return nil, NoUpdate
}
func (u UpdaterStub) Event(req *Request, app *AppRequest, event *EventRequest) {
return
}
func (u UpdaterStub) Ping(req *Request, app *AppRequest) {
return
}

View file

@ -1,42 +0,0 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
package omaha
import (
"encoding/xml"
"testing"
)
const SampleUpdate = `<?xml version="1.0" encoding="UTF-8"?>
<update appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" version="9999.0.0">
<urls>
<url codebase="packages/9999.0.0"></url>
</urls>
<manifest version="9999.0.0">
<packages>
<package name="update.gz" hash="+LXvjiaPkeYDLHoNKlf9qbJwvnk=" size="67546213" required="true"></package>
</packages>
</update>
`
func TestUpdateURLs(t *testing.T) {
u := Update{}
xml.Unmarshal([]byte(SampleUpdate), &u)
urls := u.URLs([]string{"http://localhost/updates/"})
if urls[0].CodeBase != "http://localhost/updates/packages/9999.0.0" {
t.Error("Unexpected URL", urls[0].CodeBase)
}
}