Compare commits
127 commits
fixup-trav
...
master
Author | SHA1 | Date | |
---|---|---|---|
3c546fa84f | |||
f0ff244d87 | |||
b0ceef0566 | |||
9dca822092 | |||
4b906a3877 | |||
23a85a16c1 | |||
|
e409d983eb | ||
|
18971c0086 | ||
|
6a34f44275 | ||
|
9c437f5a81 | ||
|
af61458371 | ||
|
e95ad781c1 | ||
|
15c12b686e | ||
|
c731a0389e | ||
|
6885a90813 | ||
|
3648e19ebf | ||
|
13e4359083 | ||
|
22285faf56 | ||
|
1925112df9 | ||
|
1833613ed6 | ||
|
0152a8b1b0 | ||
|
a2f653da34 | ||
|
d946c1e7b2 | ||
|
2d48f3a029 | ||
|
41143a9d17 | ||
|
9ed95f2e49 | ||
|
03222d488c | ||
|
c42bec118e | ||
|
3b5d143dc5 | ||
|
b2ea5e0e78 | ||
|
bd1ae5648e | ||
|
1b026dfef5 | ||
|
c88c5916bb | ||
|
c9e5a6a602 | ||
|
d40844181a | ||
|
f8acb2d7b7 | ||
|
e67d0ff67a | ||
|
73e12a0154 | ||
|
ad277db627 | ||
|
5a03e1d183 | ||
|
afab572db3 | ||
|
d8ad567e7f | ||
|
45e1ea6221 | ||
|
e6f3abe15e | ||
|
b8149cc683 | ||
|
b2b975be5d | ||
|
a81e2c0792 | ||
|
036aedf2d7 | ||
|
d6e8a00feb | ||
|
3d5d24e0b8 | ||
|
a6290c1b4f | ||
|
6198ba9443 | ||
|
773016a0fd | ||
|
5f9a499a46 | ||
|
79e582819b | ||
|
8e6feed630 | ||
|
c694d6ed59 | ||
|
9a796427d5 | ||
|
c7d81825c4 | ||
|
4b95d8178b | ||
|
7786cbf0d4 | ||
|
acd5c75d52 | ||
|
4d02220019 | ||
|
e5eb9eb583 | ||
|
f33cb66abb | ||
|
2cf1d8f13e | ||
|
5e54ada1e9 | ||
|
5543f86194 | ||
|
f208691b12 | ||
|
a3bc668225 | ||
|
75a1125f53 | ||
|
ec70842bdd | ||
|
fef283aeb6 | ||
|
6ab36bd0dc | ||
|
8650026537 | ||
|
909299725c | ||
|
f628b91eb4 | ||
|
d2f2d56db9 | ||
|
49571ac5e8 | ||
|
064f420979 | ||
|
4ab25f2ba0 | ||
|
30af5ac3e7 | ||
|
f26efba60f | ||
|
71d6a940b3 | ||
|
33e7ba03da | ||
|
7ddae496e9 | ||
|
c0517c8e4f | ||
|
6dbd00837f | ||
|
5cfdc81c6f | ||
|
f1dd30f4ac | ||
|
952d034b50 | ||
|
774cf34827 | ||
|
92df44276d | ||
|
bd37826a4f | ||
|
392deac926 | ||
|
6af1f79472 | ||
|
1d08943ef3 | ||
|
e01a1f70c3 | ||
|
2e4ea503b0 | ||
|
40e2aba4c3 | ||
|
fdce701e81 | ||
|
b55bf960be | ||
|
963d7e4623 | ||
|
43f8a74b0f | ||
|
2c86b12c25 | ||
|
c7aef5fdf2 | ||
|
c4605160c5 | ||
|
054de85da2 | ||
|
bb1265733a | ||
|
ddc4c22205 | ||
|
5ad4a72076 | ||
|
05776eb33d | ||
|
528535f63e | ||
|
c2c4ee6b06 | ||
|
ac9df07313 | ||
|
5140f37b40 | ||
|
dfade567dd | ||
|
5d21f061f7 | ||
|
17767637e0 | ||
|
2315500129 | ||
|
37c9cc50fb | ||
|
94018ed305 | ||
|
60265ef0fc | ||
|
d68e528e59 | ||
|
c875cf3bb2 | ||
|
da9eaa53f9 | ||
|
3492dbe98c |
44 changed files with 3847 additions and 377 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
pkg
|
pkg
|
||||||
|
bin
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
language: go
|
language: go
|
||||||
go: 1.1
|
sudo: false
|
||||||
|
go:
|
||||||
|
- 1.7.5
|
||||||
|
- 1.8.1
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- go test -v ./...
|
- go test -v ./...
|
||||||
|
|
62
CONTRIBUTING.md
Normal file
62
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# 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
Normal file
36
DCO
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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.
|
8
LICENSE
8
LICENSE
|
@ -1,5 +1,4 @@
|
||||||
|
Apache License
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
http://www.apache.org/licenses/
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
@ -179,7 +178,7 @@
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
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
|
replaced with your own identifying information. (Don't include
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
comment syntax for the file format. We also recommend that a
|
comment syntax for the file format. We also recommend that a
|
||||||
|
@ -187,7 +186,7 @@
|
||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -200,3 +199,4 @@
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
|
|
24
Makefile
Normal file
24
Makefile
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# 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
Normal file
5
NOTICE
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
CoreOS Project
|
||||||
|
Copyright 2017 CoreOS, Inc
|
||||||
|
|
||||||
|
This product includes software developed at CoreOS, Inc.
|
||||||
|
(http://www.coreos.com/).
|
72
README.md
72
README.md
|
@ -1,5 +1,71 @@
|
||||||
Implementation of the omaha protocol in Go.
|
# Go Omaha
|
||||||
|
|
||||||
https://code.google.com/p/omaha/
|
[![GoDoc](https://godoc.org/git.thisco.de/vbatts/go-omaha/omaha?status.svg)](https://godoc.org/git.thisco.de/vbatts/go-omaha/omaha)
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/coreos/go-omaha.png)](https://travis-ci.org/coreos/go-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
|
||||||
|
```
|
||||||
|
|
46
cmd/serve-package/main.go
Normal file
46
cmd/serve-package/main.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
40
code-of-conduct.md
Normal file
40
code-of-conduct.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
## 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/
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<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>
|
<os version="Indy" platform="Chrome OS" sp="ForcedUpdate_x86_64"></os>
|
||||||
<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" >
|
<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>
|
<ping active="1" a="-1" r="-1"></ping>
|
||||||
<updatecheck targetversionprefix=""></updatecheck>
|
<updatecheck targetversionprefix=""></updatecheck>
|
||||||
<event eventtype="3" eventresult="2" previousversion=""></event>
|
<event eventtype="3" eventresult="2" previousversion=""></event>
|
||||||
|
|
9
go.mod
Normal file
9
go.mod
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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
Normal file
8
go.sum
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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=
|
368
omaha/client/client.go
Normal file
368
omaha/client/client.go
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
// 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
|
||||||
|
}
|
199
omaha/client/client_test.go
Normal file
199
omaha/client/client_test.go
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
// 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])
|
||||||
|
}
|
||||||
|
}
|
129
omaha/client/error.go
Normal file
129
omaha/client/error.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
49
omaha/client/error_test.go
Normal file
49
omaha/client/error_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
101
omaha/client/example_test.go
Normal file
101
omaha/client/example_test.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
// 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
|
||||||
|
}
|
53
omaha/client/fuzzytime.go
Normal file
53
omaha/client/fuzzytime.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// 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))
|
||||||
|
}
|
32
omaha/client/fuzzytime_test.go
Normal file
32
omaha/client/fuzzytime_test.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
omaha/client/http.go
Normal file
88
omaha/client/http.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// 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
|
||||||
|
}
|
212
omaha/client/http_test.go
Normal file
212
omaha/client/http_test.go
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
75
omaha/client/machine_linux.go
Normal file
75
omaha/client/machine_linux.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// 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
|
||||||
|
}
|
48
omaha/client/machine_linux_test.go
Normal file
48
omaha/client/machine_linux_test.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
226
omaha/client/update_engine_events.go
Normal file
226
omaha/client/update_engine_events.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
// 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
|
||||||
|
}
|
181
omaha/codes.go
Normal file
181
omaha/codes.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
// 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)
|
||||||
|
}
|
127
omaha/handler.go
Normal file
127
omaha/handler.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
// 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
|
||||||
|
}
|
68
omaha/handler_test.go
Normal file
68
omaha/handler_test.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
32
omaha/isclosed.go
Normal file
32
omaha/isclosed.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// 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
248
omaha/omaha.go
|
@ -1,248 +0,0 @@
|
||||||
/*
|
|
||||||
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",
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
111
omaha/package.go
Normal file
111
omaha/package.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// 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
|
||||||
|
}
|
144
omaha/package_test.go
Normal file
144
omaha/package_test.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
71
omaha/parse.go
Normal file
71
omaha/parse.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// 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
|
||||||
|
}
|
55
omaha/parse_test.go
Normal file
55
omaha/parse_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
304
omaha/protocol.go
Normal file
304
omaha/protocol.go
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
// 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"`
|
||||||
|
}
|
247
omaha/protocol_test.go
Normal file
247
omaha/protocol_test.go
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
// 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>
|
||||||
|
}
|
73
omaha/server.go
Normal file
73
omaha/server.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// 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()
|
||||||
|
}
|
114
omaha/server_test.go
Normal file
114
omaha/server_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
53
omaha/system.go
Normal file
53
omaha/system.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
122
omaha/trivial_server.go
Normal file
122
omaha/trivial_server.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
// 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
|
||||||
|
}
|
140
omaha/trivial_server_test.go
Normal file
140
omaha/trivial_server_test.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
}
|
73
omaha/update.go
Normal file
73
omaha/update.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// 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
|
||||||
|
}
|
42
omaha/update_test.go
Normal file
42
omaha/update_test.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue