Compare commits

...

127 Commits

Author SHA1 Message Date
Vincent Batts 3c546fa84f
README: there is not CI for this currently
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2020-09-24 14:58:01 -04:00
Vincent Batts f0ff244d87 CONTRIBUTING: update the docs
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2020-09-24 14:55:48 -04:00
Vincent Batts b0ceef0566 *: tidy and test fix
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2020-09-24 14:55:48 -04:00
Vincent Batts 9dca822092 *: build paths to my repo
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2020-09-24 14:55:48 -04:00
Vincent Batts 4b906a3877 CoC: update for a simple fork
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2020-09-24 14:55:48 -04:00
Vincent Batts 23a85a16c1
*: switch from glide to go mod
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2020-09-24 14:55:40 -04:00
Stephen Demos e409d983eb
Merge pull request #31 from ericchiang/coc
automated PR: update CoC
2018-03-27 13:22:21 -07:00
Eric Chiang 18971c0086 update CoC 2018-01-04 12:20:50 -08:00
Stephen Demos 6a34f44275
Merge pull request #30 from sdemos/cmd
cmd: add very simple cmdline omaha server
2017-12-19 11:24:16 -08:00
Stephen Demos 9c437f5a81
readme: add instructions for a manual update
The readme now includes instructions for using the provided
serve-package to apply a manual update.
2017-12-19 11:22:59 -08:00
Stephen Demos af61458371 cmd: add very simple cmdline omaha server
this implementation uses the TrivialServer that is already provided by
the go-omaha library. it is a very simple wrapper, simply asking for the
file, version, and listening address on the command line. it can only
handle one package at a time, naming the payload from the server
"update.gz", which is the standard filename for the update payload. the
additional metadata required for package creation is generated based on
the provided file. when the server is setup, update_engine can be
pointed at it by setting the SERVER variable in /etc/coreos/update.conf
2017-12-07 16:16:34 -08:00
Stephen Demos e95ad781c1 vendor: vendor glide dependencies
this lets us specify dependencies more specifically and also build
binaries as a part of the project.
2017-12-05 14:46:15 -08:00
Stephen Demos 15c12b686e glide: add glide yaml files
this allows us to build binaries in this library and generally control
our dependencies more concretely.
2017-12-05 14:45:21 -08:00
Yifan Gu c731a0389e Merge pull request #29 from coreos/revert-28-add_install_started_event
Revert "update_engine_events.go: Add a custom "InstallStarted" event."
2017-08-07 13:23:44 -07:00
Yifan Gu 6885a90813 Revert "update_engine_events.go: Add a custom "InstallStarted" event." 2017-08-07 13:19:09 -07:00
Yifan Gu 3648e19ebf Merge pull request #28 from yifan-gu/add_install_started_event
update_engine_events.go: Add a custom "InstallStarted" event.
2017-08-07 13:11:49 -07:00
Yifan Gu 13e4359083 update_engine_events.go: Add a custom "InstallStarted" event.
This can help us to know how many clusters have clicked "start upgrade",
plus those in automatic mode.
2017-08-07 13:00:40 -07:00
Yifan Gu 22285faf56 Merge pull request #27 from yifan-gu/smarter_trivial_server
omaha: Make the trivial server to only return newer packages.
2017-06-28 12:13:52 -07:00
Yifan Gu 1925112df9 omaha: Make the trivial server to only return newer packages.
This is for adding related test in TCO.
2017-06-28 12:13:30 -07:00
Michael Marineau 1833613ed6 Merge pull request #26 from marineam/oem
Add support for OEM and sending error events
2017-06-09 18:48:31 -07:00
Michael Marineau 0152a8b1b0 client: export NewAppRequest and SendAppRequest methods
Allow for more flexibility in constructing requests if needed.
2017-06-09 18:40:25 -07:00
Michael Marineau a2f653da34 client: send "Complete" event in update checks
Needed in order to match update_engine behavior.
2017-06-09 18:40:25 -07:00
Michael Marineau d946c1e7b2 client: disable checking for ping and event status in responses
Turns out CoreUpdate does not currently send <ping status="ok"> and
<event status="ok"> in responses like the protocol says it should.
2017-06-09 18:40:25 -07:00
Michael Marineau 2d48f3a029 client: send error events if update check or ping fails
Does not report errors when sending events since that would be cyclic.
2017-06-09 18:40:25 -07:00
Michael Marineau 41143a9d17 client: define error codes and events used by update_engine
Event error codes are application specific but for the sake of
simplicity lets re-use the update_engine ones where applicable.

update_engine and thus CoreUpdate only use a small subset of possible
event types so we can provide pre-defined event structs for them.
2017-06-09 18:40:25 -07:00
Michael Marineau 9ed95f2e49 protocol: fix data type for event errorcode, should be an int 2017-06-09 18:40:25 -07:00
Michael Marineau 03222d488c client: support sending events asynchronously
Makes blocking and waiting for success/error optional, also allows
concurrent use of the rest of the client while events are sent.
2017-06-09 18:40:25 -07:00
Michael Marineau c42bec118e client: support runtime changes of app id 2017-06-07 11:29:02 -07:00
Michael Marineau 3b5d143dc5 client: add application oem attribute 2017-06-07 11:29:02 -07:00
Yifan Gu b2ea5e0e78 Merge pull request #25 from marineam/timer
Fuzzy timer for exponential backoff and update polling
2017-06-05 13:45:48 -07:00
Michael Marineau bd1ae5648e client: add rough example for using the client 2017-06-05 13:23:32 -07:00
Michael Marineau 1b026dfef5 client: add fuzzy timer for update check and ping interval
Uses the same timing parameters as update_engine.
2017-06-02 18:44:29 -07:00
Michael Marineau c88c5916bb client: implement exponential backoff on temporary network errors
Uses a fuzzy timer to reduce chance of multiple clients synchronizing.
2017-06-02 18:44:29 -07:00
Michael Marineau c9e5a6a602 client: define default version as a constant 2017-06-02 18:44:29 -07:00
Michael Marineau d40844181a client: add fuzzy timer
For randomizing update check intervals and backoff delays to reduce
chance of DoSing the server if lots of clients start together.
2017-06-02 18:42:53 -07:00
Michael Marineau f8acb2d7b7 Merge pull request #24 from marineam/client
Begin generic omaha client implementation
2017-05-26 13:38:09 -07:00
Michael Marineau e67d0ff67a README: explain implementation status in more detail 2017-05-25 14:29:35 -07:00
Michael Marineau 73e12a0154 client: add support for machine-wide omaha clients
Uses machine id and boot id for user and session respectively, matching
the existing behavior of update_engine.
2017-05-25 13:57:28 -07:00
Michael Marineau ad277db627 client: implement event, ping, and updatecheck
For simplicity one request is sent for each event/ping/check for each
application. In the future it would be wise to batch together multiple
events and multiple applications to avoid excessive chatter.
2017-05-25 13:13:10 -07:00
Michael Marineau 5a03e1d183 codes: improve app and update status error messages 2017-05-25 13:13:10 -07:00
Michael Marineau afab572db3 protocol: change IsMachine flag to int
This flag is a boolean "0" or "1", with "0" as the default so keeping
the "omitempty" xml option is ok.
2017-05-25 13:13:10 -07:00
Michael Marineau d8ad567e7f protocol: add GetApp helper methods 2017-05-25 13:13:09 -07:00
Michael Marineau 45e1ea6221 client: report sensible errors if response is excessively large or empty 2017-05-25 13:13:09 -07:00
Michael Marineau e6f3abe15e client: support retrying requests after transient HTTP errors
Wraps failed http.Request objects in a net.Error interface.
2017-05-25 13:13:09 -07:00
Michael Marineau b8149cc683 client: add extended http client for making omaha api requests
Supports encoding/decoding omaha xml and retrying on transient failures.
2017-05-25 13:13:09 -07:00
Michael Marineau b2b975be5d client: begin omaha client implementation
This first commit just covers the basic data structures.
2017-05-25 13:13:09 -07:00
Yifan Gu a81e2c0792 Merge pull request #23 from marineam/trivial
protocol: remove stray Event type
2017-05-05 12:03:41 -07:00
Michael Marineau 036aedf2d7 protocol: remove stray Event type
The Event struct was split into EventRequest and EventResponse back in
75a1125f but the older now unused type was never deleted.
2017-05-05 11:53:52 -07:00
Michael Marineau d6e8a00feb Merge pull request #22 from marineam/parse
protocol: add ParseRequest and ParseResponse functions
2017-05-05 11:49:06 -07:00
Michael Marineau 3d5d24e0b8 Merge pull request #21 from marineam/trivial
protocol: style fix, use "ID" and "SHA" in identifiers.
2017-05-04 13:52:18 -07:00
Michael Marineau a6290c1b4f protocol: add ParseRequest and ParseResponse functions
For parsing and verification of of HTTP request and response bodies,
including optional checking the Content-Type field which the handler
previously didn't do.
2017-05-04 13:25:46 -07:00
Michael Marineau 6198ba9443 protocol: style fix, use "ID" and "SHA" in identifiers. 2017-05-04 13:22:27 -07:00
Yifan Gu 773016a0fd Merge pull request #20 from marineam/trivial
Trivial protocol struct updates
2017-05-02 13:38:33 -07:00
Michael Marineau 5f9a499a46 package: fix SHA256 attribute name, now defined by the spec.
The old name was my own extension and unused by anything so there aren't
any compatibility issues here.
2017-05-02 13:15:04 -07:00
Michael Marineau 79e582819b protocol: sort xml attributes
A cosmetic change, though this will change the order in the XML output.
The spec lists fields alphabetically so this makes comparison easier.
2017-05-02 13:07:53 -07:00
Michael Marineau 8e6feed630 Merge pull request #19 from marineam/template
Sync with CoreOS template project
2017-05-02 12:36:35 -07:00
Michael Marineau c694d6ed59 Merge pull request #18 from marineam/from-mantle
Import omaha updates from mantle
2017-05-02 12:34:24 -07:00
Michael Marineau 9a796427d5 omaha: distinguish zero and unset in ping active days field
Unlike the other fields unset here would mean unknown rather than "0" so
we must distinguish between the two. In the end it isn't very
significant since our update server and none of our clients use these
self-reported active times, exclusively using when pings were received.
2017-04-24 13:05:53 -07:00
Michael Marineau c7d81825c4 omaha: remove lingering references to mantle 2017-04-24 12:23:53 -07:00
Michael Marineau 4b95d8178b omaha: update upstream doc URL 2017-04-24 12:04:41 -07:00
Michael Marineau 7786cbf0d4 Merge branch 'master' of https://github.com/coreos/template-project 2017-04-21 17:16:37 -07:00
Michael Marineau acd5c75d52 travis: test against go 1.7 and 1.8 2017-04-21 14:12:44 -07:00
Michael Marineau 4d02220019 omaha: add basic but functional omaha server for testing
This server doesn't care about app id, versions, or really anything.
Once a payload has been set it will use it for all update requests.
2017-04-21 14:12:44 -07:00
Nick Owens e5eb9eb583 omaha: implement server based on OmahaHandler
As-is this server cannot do much and must be given an Updater
implementation to handle requests. Server.Mux is exposed to in case the
server needs to additional handlers for serving package payloads, etc.
2017-04-21 14:12:44 -07:00
Michael Marineau f33cb66abb omaha: add complete http handler implementation
The handler is driven by something implementing the 'Updater' interface.
2017-04-21 14:12:44 -07:00
Michael Marineau 2cf1d8f13e omaha: support using status codes as error values 2017-04-21 14:12:44 -07:00
Michael Marineau 5e54ada1e9 omaha: add structure for representing a single app update
The protocol structures are intended for representing a collection of
apps and their updates but for a server's internal API and data store we
need to represent a self-contained app update manifest.
2017-04-21 14:12:44 -07:00
Michael Marineau 5543f86194 omaha: add helper method for computing package metadata 2017-04-21 14:12:44 -07:00
Michael Marineau f208691b12 omaha: stop wrapping URL slice in a struct
Since splitting request and response structs it is no longer necessary
to work around Go's awkard handling of a `xml:"urls>url"` tag.
2017-04-21 14:12:44 -07:00
Michael Marineau a3bc668225 omaha: add/fix missing or outdated attributes 2017-04-21 14:12:44 -07:00
Michael Marineau 75a1125f53 omaha: split request and response structures
Despite having common names between the request and response XML
structures the actual values which may appear in them are completely
disjoint. Splitting the types up makes the protocol easier to understand
when reading the code. When applicable, required fields like status are
passed to Add* methods.
2017-04-21 14:12:44 -07:00
Michael Marineau ec70842bdd omaha: rework/cleanup protocol APIs
- Avoid long argument lists and only pass values that are strictly
   required such as status, fill in all other fields directly instead.
 - Fill OS struct in requests based on local system.
 - Define event and status codes as constants.
 - Misc style tweaks.
2017-04-21 14:12:44 -07:00
Michael Marineau fef283aeb6 omaha: remove XMLName from nested structures
The special XMLName field is only useful in the top level structs which
need something to attach the lower case tag to. On the rest the default
behavior without XMLName works just fine so it is clutter.

The datastore tags have been dropped too since they are not needed.
2017-04-21 14:12:44 -07:00
Nick Owens 6ab36bd0dc omaha: fix go vet complaints
omaha/protocol_test.go:83: ExampleOmaha_NewResponse refers to unknown identifier: Omaha
omaha/protocol_test.go:134: ExampleOmaha_NewRequest refers to unknown identifier: Omaha
2017-04-21 14:12:44 -07:00
Michael Marineau 8650026537 omaha: update file header style and file names 2017-04-21 14:12:44 -07:00
Michael Marineau 909299725c omaha: embed test data into the test code 2017-04-21 14:12:44 -07:00
Quentin Machu f628b91eb4 Merge pull request #9 from coreos/2017
NOTICE: Bump copyright year
2017-02-10 11:23:16 -08:00
Quentin Machu d2f2d56db9 NOTICE: Bump copyright year 2017-02-10 09:36:49 -08:00
Ben Spoon 49571ac5e8 Merge pull request #8 from spoonben/update-code-of-conduct
update code of conduct copy
2016-08-10 17:07:42 -07:00
Ben Spoon 064f420979 update code of conduct copy 2016-08-09 13:19:44 -07:00
Brandon Philips 4ab25f2ba0 Merge pull request #7 from philips/add-project-code-of-conduct
code-of-conduct: initial commit
2016-08-04 14:02:09 -07:00
Brandon Philips 30af5ac3e7 code-of-conduct: initial commit
And a code of conduct based on the well adopted Contributor Covenant
which is used in other large projects that CoreOS depends on such as
AngularJS, Kubernetes, Jekyll, curl, and many many more.

Also add the events code of conduct to cover any meetups, conferences,
or other social events based on the existing CoreOS Fest code of
conduct: https://coreos.com/fest/conduct/
2016-08-04 11:56:39 -07:00
Michael Marineau f26efba60f Merge pull request #17 from pwaller/patch-2
Update link to ServerProtocol.md
2015-11-01 14:24:06 -08:00
Michael Marineau 71d6a940b3 Merge pull request #16 from pwaller/patch-1
Update README.md link to github.com/google/omaha
2015-11-01 14:23:51 -08:00
Peter Waller 33e7ba03da Update link to ServerProtocol.md 2015-11-01 08:51:39 +00:00
Peter Waller 7ddae496e9 Update README.md link to github.com/google/omaha 2015-11-01 08:48:27 +00:00
Brandon Philips c0517c8e4f Merge pull request #4 from coreos/copyright
Bump copyright year
2015-10-20 12:36:59 -07:00
Jonathan Boulle 6dbd00837f CONTRIBUTING: fix line wrapping 2015-10-14 08:48:41 -07:00
Jonathan Boulle 5cfdc81c6f Merge pull request #5 from coreos/jonboulle-patch-1
CONTRIBUTING: add reference to golang style guide
2015-10-13 22:22:56 -07:00
Jonathan Boulle f1dd30f4ac CONTRIBUTING: add reference to golang style guide 2015-10-13 18:27:11 -07:00
Matt Jibson 952d034b50 Bump copyright year 2015-08-31 15:29:04 -04:00
Brandon Philips 774cf34827 CONTRIBUTING: add a section about not emailing maintainers
Setup some expectation for etiquette.
2014-06-18 18:38:28 -07:00
Brandon Philips 92df44276d CONTRIBUTING: remove the subject verbs
After talking to a few people on the team we have decided that the
feat/fix/etc tags aren't adding much value. Use a simpler format
instead.
2014-06-18 18:36:00 -07:00
Brandon Philips bd37826a4f Merge pull request #15 from sym3tri/errorcode-support
fix(event): add optional errorcode field to event
2014-05-30 11:30:03 -07:00
Ed Rooth 392deac926 fix(event): add optional errorcode field to event 2014-05-23 11:26:29 -07:00
Brandon Philips 6af1f79472 Merge pull request #14 from sym3tri/add-ping-type
feat(omaha): add "ping" event type.
2014-05-15 18:36:28 -07:00
Ed Rooth 1d08943ef3 feat(omaha): add "ping" event type. 2014-05-14 16:16:44 -07:00
Jonathan Boulle e01a1f70c3 Merge pull request #2 from jonboulle/master
Clean up CONTRIBUTING.md and other bits of template-project
2014-04-04 10:41:40 -07:00
Jonathan Boulle 2e4ea503b0 chore(contributing): clean up CONTRIBUTING.md and split out DCO 2014-04-04 10:40:37 -07:00
Brandon Philips 40e2aba4c3 Merge pull request #13 from philips/add-machineid-field
feat(omaha): add a machineid field
2014-03-17 14:32:13 -07:00
Brandon Philips fdce701e81 feat(omaha): add a machineid field 2014-03-17 14:29:04 -07:00
Brandon Philips b55bf960be Merge pull request #12 from philips/master
fix(omaha): fixup Oem test after change
2014-03-16 21:05:47 -07:00
Brandon Philips 963d7e4623 fix(omaha): fixup Oem test after change
I forgot to run the tests after making the change to OEM. Fix.
2014-03-16 21:04:57 -07:00
Brandon Philips 43f8a74b0f Merge pull request #11 from philips/master
fix(omaha): Oem to OEM
2014-03-16 21:01:15 -07:00
Brandon Philips 2c86b12c25 fix(omaha): Oem to OEM
Be consistent with the Go style guide and make the acronym fully
uppercase.
2014-03-16 21:00:20 -07:00
Brandon Philips c7aef5fdf2 Merge pull request #1 from bcwaldon/fix-case
fix(CONTRIBUTING.md): Fix title case
2014-02-05 15:52:24 -08:00
Brian Waldon c4605160c5 fix(CONTRIBUTING.md): Fix title case 2014-02-05 15:51:24 -08:00
Brandon Philips 054de85da2 feat(*): initial commit 2014-01-19 12:25:11 -08:00
Brandon Philips bb1265733a Merge pull request #9 from philips/add-new-fields
philips/add-new-fields
2013-09-03 16:41:27 -07:00
Brandon Philips ddc4c22205 feat(omaha): add oem and previousbootid
Add two new extensions for coreos
2013-09-03 16:39:37 -07:00
Brandon Philips 5ad4a72076 fix(omaha): go fmt 2013-09-03 16:20:46 -07:00
Brandon Philips 05776eb33d README: add a link to the go docs 2013-07-09 17:32:27 -07:00
Brandon Philips 528535f63e feat(omaha): add a bootid field
the bootid field will be used by CoreOS to uniquely identify a boot of
an instance.
2013-07-09 11:48:57 -07:00
Brandon Philips c2c4ee6b06 Merge pull request #7 from philips/add-track-extension
Add track extension
2013-07-02 09:53:22 -07:00
Brandon Philips ac9df07313 chore(omaha): group the update engine extensions together 2013-07-02 09:47:19 -07:00
Brandon Philips 5140f37b40 feat(omaha): add Track field
this is another update_engine extension. Add it.
2013-07-02 09:41:53 -07:00
Brandon Philips dfade567dd Merge pull request #6 from doodles526/working
added json omitempty tags
2013-07-01 13:28:22 -07:00
Joshua Deare 5d21f061f7 added json omitempty tags
bigquery was throwing errors without these due to null defined fields
2013-07-01 13:17:08 -07:00
Brandon Philips 17767637e0 Merge pull request #5 from doodles526/working
terminated XMLName from appearing in json
2013-06-28 23:20:44 -07:00
Joshua Deare 2315500129 terminated XMLName from appearing in json
when we need json encoding(putting AppRequestLogs into bigquery)
we don't want the XMLName tag adding on needless data
2013-06-28 22:48:55 -07:00
Brandon Philips 37c9cc50fb Merge pull request #4 from philips/update-engine-extensions
Update engine extensions
2013-06-26 15:59:42 -07:00
Brandon Philips 94018ed305 feat(omaha): add more update_engine extensions 2013-06-26 15:53:31 -07:00
Brandon Philips 60265ef0fc fix(omaha): create section for update_engine extensions
add this comment section just so it is a littlemore clear what is going
on.
2013-06-26 10:39:52 -07:00
Brandon Philips d68e528e59 feat(omaha): add disablebackoff field to action
another extension added by the the chromeos update engine
2013-06-26 10:39:52 -07:00
Brandon Philips c875cf3bb2 Merge pull request #3 from doodles526/working
added nil datastore tags to XMLName
2013-06-25 16:56:32 -07:00
Joshua Deare da9eaa53f9 added nil datastore tags to XMLName
now if a struct is put in datastore it won't be bloated
with needless XMLName field
2013-06-25 16:39:01 -07:00
Brandon Philips 3492dbe98c Merge pull request #2 from coreos/fixup-travis
travis.yml: try to build all of the packages
2013-06-25 09:21:45 -07:00
44 changed files with 3847 additions and 377 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
pkg
bin

View File

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

62
CONTRIBUTING.md Normal file
View 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
View 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.

View File

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

24
Makefile Normal file
View 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
View File

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

View File

@ -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
View 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
View 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/

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" installsource="ondemandupdate" ismachine="1">
<os version="Indy" platform="Chrome OS" sp="ForcedUpdate_x86_64"></os>
<app appid="{87efface-864d-49a5-9bb3-4b050a7c227a}" 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>
<updatecheck targetversionprefix=""></updatecheck>
<event eventtype="3" eventresult="2" previousversion=""></event>

9
go.mod Normal file
View 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
View 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
View 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
View 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
View 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
}
}

View 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)
}
}

View 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
View 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))
}

View 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
View 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
View 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)
}
}

View 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
}

View 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)
}
}

View 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
View 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
View 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
View 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
View 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"
}

View File

@ -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",
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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)
}
}